惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
Hacker News: Ask HN
Hacker News: Ask HN
V2EX - 技术
V2EX - 技术
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Stack Overflow Blog
Stack Overflow Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Proofpoint News Feed
A
Arctic Wolf
Forbes - Security
Forbes - Security
Spread Privacy
Spread Privacy
Security Latest
Security Latest
C
CXSECURITY Database RSS Feed - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
AWS News Blog
AWS News Blog
M
MIT News - Artificial intelligence
GbyAI
GbyAI
V
Visual Studio Blog
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
Help Net Security
Help Net Security
博客园 - Franky
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
Schneier on Security
S
SegmentFault 最新的问题
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
PCI Perspectives
PCI Perspectives
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Palo Alto Networks Blog
SecWiki News
SecWiki News
TaoSecurity Blog
TaoSecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
K
Kaspersky official blog
G
Google Developers Blog
H
Hacker News: Front Page
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
博客园 - 叶小钗
T
Troy Hunt's Blog
F
Full Disclosure
T
Threat Research - Cisco Blogs

Пусть этот камень будет более крепким, чем человек

【琐记】烟火与尘埃 【Triton】Triton实现矩阵乘 【LLM推理加速】FlashAttention 【LLM推理加速】PagedAttention 【LLM推理加速】Online Softmax LLM基础知识【1】 Transformer模型 【AI编译】LayerGroup Tiling Tile的疑惑和思考 【AI编译】深度优先的Tile调度,万事大吉? 【AI编译】多级流水线Tile调度策略 【CUDA C++】GPU内存使用【3】 【AI编译】Cache缓存地址映射 【CUDA C++】GPU存储【2】 【CUDA C++】GPU基本介绍【1】 【00】0序章-不受欢迎的来客 【转载】我来了——持续低熵 【Halide】调度优化【2】 【感想】写作进度报告5 【Halide】调度优化【1】 【转载】北大中文男足战报2 【BYOC】TVM切分子图 【转载】北大中文男足战报1 【AI编译】张量生命周期管理 SystemC 用寄存器同步建模方法 【脉动阵列】脉动阵列类型 【im2col】AScend conv accelerate 【感想】写作进度报告4 【BYOC】TVM添加自定义编译器 ccompiler 【感想】写作进度报告3 【Tengine】推理流程脑图【2】 【Tengine】推理流程脑图【1】 【NCNN】学习ncnn模型转换 【编译器】使用llvm编译自定义语言【3】编译 object 【编译器】使用llvm编译自定义语言【2】转llvm IR 【编译器】使用llvm编译自定义语言【1】构建AST 【AI编译】如何进行内存分配 【感想】写作进度报告2 【AI编译】layer-group之后如何tiling 【AI编译】如何进行layer-group 【量化】连续卷积层首尾量化的可行性 【Gemm】内存对齐 【gemm】Gemm计算加速 【TVM】通过代码学习编译流程【6】CodeGen 【TVM】通过代码学习类【3.5】Pass 【TVM】通过代码学习编译流程【4】BuildRelay 【AI编译】Tiling操作能优化什么时间 【TVM】通过代码学习编译流程【3】模型编译 【TVM】通过代码学习编译流程【2】模型转换 【TVM】通过代码学习编译流程【1】必要知识 【感想】写作进度报告1 【Winograd】卷积加速算法原理及实现 SystemC 等待异步事件解决方案 【TVM】Python脚本实现模型编译和保存 【推理引擎】常见AI推理框架 【3D建模】T110E3卡迪夫蓝调皮肤模型 【TVM】C++部署运行TVM 【推理引擎】NCNN和Tengine量化推理逻辑对比 【3D建模】IS-7攻城锤流纹岩皮肤展示 【TVM】根据例子走通代码库 博客汇总目录 【Im2Col】卷积加速算法【2】NHWC 【Im2Col】卷积加速算法【1】 NCHW openBlas库的安装与简单使用 C语言工程调用Cpp库解决方案 foo Hello World
【TVM】通过代码学习编译流程【5】FuseOps
Post author: XianMu@Пусть этот камень будет более крепким, чем ч · 2024-10-31 · via Пусть этот камень будет более крепким, чем человек

# 前言

本系列文章将从代码和流程图入手,详细介绍 TVM AI 编译器的编译流程。本文章为第五篇,对应的 TVM 版本为当前最新版本 1.7。

网络上有不少 TVM 工程的教程资源,如果本博客也是其他教程的简单重复的话,则网络的角落里又多了一份纯粹的空间占用者。所以我在想,本文章有什么特点才值得一看呢?我觉得有两个优点: 1、本文从代码出发,不会泛泛而谈,能够从细节了解 TVM;2、自认为结构流程图画的不错,能够从整体上把握 TVM 的脉络。所以,也许值得一看呢。

本篇文章介绍 TVM FuseOps 算子融合 Pass。文章 《【TVM】通过代码学习编译流程【4】BuildRelay》 已经介绍了函数 OptimizeImplOptimizeImpl 收集并执行大量针对高级中间表示 Relay IRModule 的优化 Pass。本篇文章将选择其中非常重要的一个 Pass:FuseOps 算子融合 Pass 进行介绍。

因为代码量巨大,模型编译会分成若干篇文章进行解析。接下来的若干篇都会介绍 BuildRelay 函数 及其调用的子函数。

作为初学者,错误在所难免,还望不吝赐教。

# Python 脚本

这里提供一个简单的 Python 脚本,调用 TVM Python 前端,实现 onnx 模型的编译过程。tvm 通过代码学习编程流程系列文章将基本采用这个脚本帮助追踪代码。

import onnx
from PIL import Image
import numpy as np
import tvm.relay as relay
import tvm
from tvm.contrib import graph_executor
model_path = "/home/xianmu/module/resnet18.onnx"
save_path = "/home/xianmu/module/pythonSave/"
onnx_model = onnx.load(model_path)
img_path = "/home/xianmu/.tvm_test_data/data/imagenet_cat.png"
resized_image = Image.open(img_path).resize((224, 224))
img_data = np.asarray(resized_image).astype("float32")
img_data = np.transpose(img_data, (2, 0, 1))
imagenet_mean = np.array([0.485, 0.456, 0.406]).reshape((3, 1, 1))
imagenet_stddev = np.array([0.229, 0.224, 0.225]).reshape((3, 1, 1))
norm_img_data = (img_data / 255 - imagenet_mean) / imagenet_stddev
img_data = np.expand_dims(norm_img_data, axis=0)
input_name = "data"
target = tvm.target.Target(target="llvm", host="llvm")
shape_dict = {input_name: img_data.shape}
mod, params = relay.frontend.from_onnx(onnx_model, shape_dict, export_node_renamed_model_path=save_path)  
with tvm.transform.PassContext(opt_level=3):
    lib = relay.build(mod, target=target, params=params)   
lib_fname = save_path + "mod.so"
lib.get_lib().export_library(lib_fname)
params_fname = save_path + "mod.params"
with open(params_fname, "wb") as param_file:
    param_file.write(relay.save_param_dict(lib.get_params()))
json_fname = save_path + "mod.json"
with open(json_fname, "w") as json_file:
    json_file.write(lib.get_executor_config())
dev = tvm.device(str(target), 0)
module = graph_executor.GraphModule(lib["default"](dev))    
module.set_input(input_name, img_data)
module.run()
output_shape = (1, 1000)
tvm_output = module.get_output(0, tvm.nd.empty(output_shape)).numpy()
print(tvm_output)

文章 《【TVM】通过代码学习编译流程【4】BuildRelay》 已经介绍了函数 OptimizeImplOptimizeImpl 收集并执行大量针对高级中间表示 Relay IRModule 的优化 Pass。本篇文章将选择其中非常重要的一个 Pass:FuseOps 算子融合 Pass 进行介绍。

先来看看算子融合的效果。对前述 Python 脚本做一下小修改,在 35 行 from_onnx() 函数后面增加下面几行代码:

from tvm.relay import transform
print("===========================================")
print(mod)  
mod = transform.FuseOps()(mod)
print("===========================================")
print(mod)  

算子融合之前:

%+数字 表示临时变量; @+字母 表示全局变量。

Relay IRModule 只有一个 main 函数,main 函数中包含许多单个的算子表达式,这些表达式组合成完整的图结构。

def @main(%data: Tensor[(1, 3, 224, 224), float32]) -> Tensor[(1, 10), float32] {
  %0 = nn.conv2d(%data, meta[relay.Constant][0], padding=[0, 0, 0, 0], channels=4, kernel_size=[3, 3]) ;
  %1 = nn.bias_add(%0, meta[relay.Constant][1] );
  %2 = nn.relu(%1) ;
  %3 = nn.max_pool2d(%2, pool_size=[3, 3], strides=[2, 2], padding=[1, 1, 1, 1]) ;
  %4 = nn.conv2d(%3, meta[relay.Constant][2], padding=[0, 0, 0, 0], channels=5, kernel_size=[3, 3]) ;
  %5 = nn.bias_add(%4, meta[relay.Constant][3] ) ;
  ...
  }

算子融合之后:

TVM 将原本独立的 nn.conv2dnn.bias_add%2 = nn.relu 封装成了复合表达式 %5 = fn (...) ,复合表达式里面包含 nn.conv2dnn.bias_add%2 = nn.relu 三个算子。 之后通过 %6 = %5(...) 来调用执行该复合表达式。

def @main(%data: Tensor[(1, 3, 224, 224), float32]) -> Tensor[(1, 10), float32] {
  %5 = fn (%p05: Tensor[(1, 3, 224, 224), float32] , %p12: Tensor[(4, 3, 3, 3), float32] , %p22: Tensor[(4), float32], Primitive=1) -> Tensor[(1, 4, 222, 222), float32] { 
    %3 = nn.conv2d(%p05, %p12, padding=[0, 0, 0, 0], channels=4, kernel_size=[3, 3]);
    %4 = nn.bias_add(%3, %p22) ;
    nn.relu(%4) 
  } ;
  %6 = %5(%data, meta[relay.Constant][0], meta[relay.Constant][1] ) ;
  ...
  }

总之,该例子展示了 FuseOps() 匹配到了连续的 nn.conv2dnn.bias_add%2 = nn.relu 算子,并在 Relay IRModule 中通过复合表达式表示。

# FuseOps

算子融合流程图

FuseOps 函数如下所示。

FuseOps 函数目的是创建一个 FunctionPass 完成 IRModule 的优化。可以通过文章《【TVM】通过代码学习类【3.5】Pass》再回顾一下 FunctionPass

FunctionPass 的关键执行函数是传入的 pass_func

pass_func 里面有很多参数的配置,我们直接看含有六个参数的重载函数: FuseOps()

Pass FuseOps(int fuse_opt_level) {
 runtime::TypedPackedFunc<Function(Function, IRModule, PassContext)> pass_func = 
     [=](Function f, IRModule m, PassContext pc) {
       bool link_params = false;
       Executor executor =
           m->GetAttr<Executor>(tvm::attr::kExecutor).value_or(NullValue<Executor>());
       link_params = executor.defined()
                         ? executor->attrs.GetAttr<Bool>("link-params").value_or(Bool(link_params))
                         : link_params;
       link_params = pc->GetConfig("relay.FuseOps.link_params", Bool(link_params)).value();
       int opt_level = fuse_opt_level == -1 ? pc->opt_level : fuse_opt_level;
       auto max_fuse_depth = pc->GetConfig("relay.FuseOps.max_depth", Integer(kMaxFusedOps));
       auto target = Target::Current();
       size_t max_function_args =
           (target.defined())
               ? target->GetAttr<Integer>("max_function_args", Integer(0)).value().IntValue()
               : 0;
       return Downcast<Function>(FuseOps(f, opt_level, max_fuse_depth.value().IntValue(),
                                         max_function_args, link_params, m));
     };
 return CreateFunctionPass(pass_func, 0, "FuseOps", {"InferType"});  
}

含有六个参数的重载函数: FuseOps() ,函数内部初始化了类 FuseMutator ,并直接调用该类的 Transform() 成员函数。

Expr FuseOps(const Expr& expr, int fuse_opt_level, size_t max_fuse_depth, size_t max_function_args,
            bool link_params, const IRModule& module) {
 return FuseMutator(fuse_opt_level, max_fuse_depth, max_function_args, link_params)
     .Transform(expr);
}

算子融合流程图

Transform() 函数有三个部分:

IndexedForwardGraphCreator::Create : 创建前序图结构(只有当前节点指向后续节点的单向边)。顺带构建一个由 “逆深度优先搜索” 序列 post_dfs_order ,实际上是个节点顺序执行序列。

GraphPartitioner().Partition() : 创建后序支配树,匹配算子,通过并查集算法融合 group。

this->Mutate(body) : 构建新的算子融合后的 IRModule 抽象语法树结构。

 Expr Transform(const Expr& body, int fuse_opt_level, size_t max_fuse_depth, bool link_params) {
   
   auto graph = IndexedForwardGraphCreator::Create(&arena_, body);
   auto groups = GraphPartitioner(&arena_, fuse_opt_level, max_fuse_depth, max_function_args_)
                     .Partition(graph);
   for (size_t nid = 0; nid < graph.post_dfs_order.size(); ++nid) {
     ICHECK(graph.post_dfs_order[nid]->ref != nullptr);
     gmap_[graph.post_dfs_order[nid]->ref] = groups[nid];
   }
   
   
   return this->Mutate(body);
 }

# IndexedForwardGraphCreator

IndexedForwardGraphCreator::Create 函数如下所示。

该函数创建前序图结构(只有当前节点指向后续节点的单向边),完成主要功能的是 Prepare(const Expr& body)

this->Update() 功能是创建当前节点 node ,如果有父节点,则创建一条指向父节点的边 edge

Prepare(const Expr& body) 函数中, this->Update(); 创建首个根节点,之后调用 this->VisitExpr(body) ,遍历抽象语法树,逐个创建节点和边,构建前序图结构。( IndexedForwardGraphCreator 类继承了 ExprVisitor 类, ExprVisitor 类是 TVM 提供的帮助遍历抽象语法树的工具,只需要重写 VisitExpr_() 函数,就读取或修改语法树相应的节点,如 void VisitExpr_(const CallNode* call) final {}

class IndexedForwardGraphCreator : private ExprVisitor {
 public:
  static IndexedForwardGraph Create(support::Arena* arena, const Expr& body) {
    IndexedForwardGraphCreator creator(arena);
    return creator.Prepare(body);
  }
 private:
  explicit IndexedForwardGraphCreator(support::Arena* arena) : arena_(arena) {}
  IndexedForwardGraph Prepare(const Expr& body) { 
    this->Update(body, nullptr, kOpaque);  
    this->VisitExpr(body);  
    return std::move(graph_);
  }
  
   private:
  
  support::Arena* arena_;
  
  IndexedForwardGraph graph_;  

以下是 IndexedForwardGraph 类,包含节点 node 、边 edge ,通过 LinkedList<Edge> outputs; 构建链接关系。

class IndexedForwardGraph {
 public:
  struct Edge {
    
    Node* node{nullptr};
    
    OpPatternKind pattern{kOpaque};
  };
  
  struct Node {  
    
    const tvm::Object* ref{nullptr};
    
    size_t index{0};
    
    bool extern_ref{false};
    
    OpPatternKind pattern{kOpaque};
    
    LinkedList<Edge> outputs;
  };
  
  std::unordered_map<const tvm::Object*, Node*> node_map;
  
  std::vector<Node*> post_dfs_order;
}

# GraphPartitioner

算子融合流程图

GraphPartitioner().Partition() 函数如下所示。

this->InitGroups(graph) : 初始化,即将图中所有节点都初始化为一个 group。

DominatorTree::PostDom() : 构建后支配树

this->RunFuse : 执行融合

std::vector<GraphPartitioner::Group*> GraphPartitioner::Partition(
    const IndexedForwardGraph& graph) {
  this->InitGroups(graph);  
  if (opt_level_ == 0) return std::move(groups_);
  
  auto post_dom_tree = DominatorTree::PostDom(arena_, graph);  
  
  for (int phase = 0; phase < 3; ++phase) {  
    this->RunFuse(graph, post_dom_tree, phase);
  }
  return std::move(groups_);
}

Post dominator tree(后支配树)是程序分析中的一种数据结构,主要用于编译器优化和静态分析。在控制流图(CFG, Control Flow Graph)中,一个节点 B 后支配另一个节点 A,当且仅当从 A 到程序结束点的所有路径都必须经过 B。后支配树以程序的退出点为根,每个节点指向它的直接后支配者。后支配树可以帮助识别哪些操作可以被安全地融合在一起。如果两个或多个操作属于同一个后支配节点,这意味着它们之间没有其他控制流分支,因此可以被看作是顺序执行的,并且有可能被融合。

this->RunFuse : 根据不同匹配模式 pattern 融合 group。复合匹配模式的 group 通过 MergeFromTo 函数进行合并。合并过程是找到两个 group 的根,将其中一个 root 作为另一个的后续节点 child->parent = parent;

void GraphPartitioner::MergeFromTo(Group* child, Group* parent) {
  child = child->FindRoot();
  parent = parent->FindRoot();
  if (child == parent) return;
  
  parent->num_nodes += child->num_nodes;
  parent->args_num += child->args_num;
  child->parent = parent;    
  
  if (child->anchor_ref != nullptr) {
    ICHECK(parent->anchor_ref == nullptr);
    parent->anchor_ref = child->anchor_ref;
    parent->pattern = CombinePattern(child->pattern, parent->pattern);
  }
}

# Mutate(body)

算子融合流程图

Mutate(body) 构建算子融合后的 IRModule 抽象语法树结构。

Mutate(body) 函数所在的类 FuseMutator 派生自祖先类 ExprMutator (抽象语法树修改工具类),只需要重写 Rewrite_() 函数就能完成对 IRModule 的修改。

Rewrite_(const CallNode* call, const Expr& post) 为例,在遍历到 Call 节点时正常创建 Call 节点 。若如果该节点是 group 的根节点,则将该 Call 节点子图分支封装成 function。

  Expr Rewrite_(const CallNode* call, const Expr& post) {
    if (call->op.as<OpNode>()) {
      static auto fnoncomputational = Op::GetAttrMap<TNonComputational>("TNonComputational");
      static auto fqnncanonicalize = Op::GetAttrMap<FTVMLegalize>("FTVMQnnCanonicalize");
      Op op = Downcast<Op>(call->op);
      if (fnoncomputational.get(op, false) && !fqnncanonicalize.count(op)) {
        return ExprMutator::VisitExpr_(call);
      }
      
      
      ICHECK(gmap_.count(call));
      if (call->op == stop_fusion_op) {
        return ExprMutator::VisitExpr(call->args[0]);
      }
      auto* ret_group = gmap_.at(call)->FindRoot();
      Array<Expr> new_args = GetNewArguments(call->args, ret_group);
      
      auto new_call = Call(call->op, new_args, call->attrs, call->type_args, call->span);
      if (ret_group->root_ref == call) {  
        
        
        return MakeNewFunction(ret_group, call->checked_type(), new_call); 
      } else {
        
        
        return std::move(new_call);
      }
    } else {
      return ExprMutator::VisitExpr_(call);
    }
  }

# 后记

本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,我看到一定及时回复。