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

推荐订阅源

G
Google Developers Blog
博客园 - 聂微东
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
Jina AI
Jina AI
D
Docker
B
Blog
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
D
DataBreaches.Net
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
月光博客
月光博客
F
Fortinet All Blogs
爱范儿
爱范儿
H
Help Net Security
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
The Cloudflare Blog
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
U
Unit 42

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

【MLIR】Transform Dialect【0】保持计算结构 【MLIR】跟着Toy语言学习MLIR【6】支持复合结构类型 【MLIR】跟着Toy语言学习MLIR【5】降级到LLVM及CodeGen 【MLIR】跟着Toy语言学习MLIR【4】部分降级到低层方言 【MLIR】跟着Toy语言学习MLIR【3】通过接口实现通用转换 【MLIR】跟着Toy语言学习MLIR【2】pattern匹配和重写 【MLIR】跟着Toy语言学习MLIR【1】Toy语言和 Toy Dialect 【琐记】你的爱好能坚持多少年 【琐记】烟火与尘埃 【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
【TVM】通过代码学习编译流程【6】CodeGen
Post author: XianMu@Пусть этот камень будет более крепким, чем ч · 2024-10-25 · via Пусть этот камень будет более крепким, чем человек

# 前言

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

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

本篇文章介绍 TVM CodeGen 函数。文章 《【TVM】通过代码学习编译流程【4】BuildRelay》 已经介绍了 BuildRelay 总体流程和其子函数 OptimizeImpl 。本篇文章将介绍后续的 CodeGen 流程的部分内容。 Codegen(func_module, func, mod_name) —— 将 Relay IRModule 降级为 TIR Module。

因为代码量巨大,模型编译会分成若干篇文章进行解析。接下来的若干篇都会介绍 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)

再回顾一下 BuildRelay 函数,文章 《【TVM】通过代码学习编译流程【4】BuildRelay》 已经介绍了 BuildRelay 总体流程和其子函数 OptimizeImpl 。下面将介绍其中的: MakeExecutorCodegenInitCodegen

void BuildRelay(IRModule relay_module, const String& mod_name) {
    
    IRModule module = WithAttrs(  
        relay_module, <!--swig0-->);
    relay_module = OptimizeImpl(std::move(module));  
    
    Function func = Downcast<Function>(relay_module->Lookup("main"));  
    IRModule func_module = WithAttrs(IRModule::FromExpr(func),  
                                     <!--swig1-->);
    
    executor_codegen_ = MakeExecutorCodegen(executor_->name);   
    executor_codegen_->Init(nullptr, config_->primitive_targets);  
    executor_codegen_->Codegen(func_module, func, mod_name);   
    executor_codegen_->UpdateOutput(&ret_);  
    ret_.params = executor_codegen_->GetParams();   
    auto lowered_funcs = executor_codegen_->GetIRModule(); 
    
    Target ext_dev("ext_dev");
    if (lowered_funcs.find(ext_dev) != lowered_funcs.end()) {
      lowered_funcs.Set(ext_dev, IRModule());
    }
    const Target& host_target = config_->host_virtual_device->target;
    const runtime::PackedFunc* pf = runtime::Registry::Get("codegen.LLVMModuleCreate");
    
    if (lowered_funcs.size() == 0) {
      if (host_target->kind->name == "llvm") {
        CHECK(pf != nullptr) << "Unable to create empty module for llvm without llvm codegen.";
        
        ret_.mod = (*pf)(host_target->str(), "empty_module");
      } else {
        
        
        
        ret_.mod = tvm::codegen::CSourceModuleCreate(";", "", Array<String>{});
      }
    } else {
      ret_.mod = tvm::TIRToRuntime(lowered_funcs, host_target);  
    }
    auto ext_mods = executor_codegen_->GetExternalModules();
    ret_.mod = tvm::codegen::CreateMetadataModule(ret_.params, ret_.mod, ext_mods, host_target,
                                                  runtime_, executor_,
                                                  executor_codegen_->GetExecutorCodegenMetadata());
    
    for (tvm::runtime::Module mod : ext_mods) {
      auto pf_var = mod.GetFunction("get_const_vars");
      if (pf_var != nullptr) {
        Array<String> variables = pf_var();
        for (size_t i = 0; i < variables.size(); i++) {
          auto it = ret_.params.find(variables[i].operator std::string());
          if (it != ret_.params.end()) {
            VLOG(1) << "constant '" << variables[i] << "' has been captured in external module";
            ret_.params.erase(it);
          }
        }
      }
    }
  }

# MakeExecutorCodegen

BuildRelay 函数的总体过程如下图:

buildRelay流程图

函数 MakeExecutorCodegen() 用于创建 GraphCodegen 对象 executor_codegen_GraphCodegen 继承于 ExecutorCodegen .

ExecutorCodegen 有个成员 tvm::runtime::Module mod; ,因此 GraphCodegen 也包含这个成员 mod

如下代码所示, GraphCodegen 在初始化的时候,将成员 mod 赋值为 GetPackedFunc("relay.build_module._GraphExecutorCodegen") 获得的构建函数。

 * \brief GraphCodegen module wrapper
 
 */
struct GraphCodegen : ExecutorCodegen {  
  GraphCodegen() {
    auto pf = GetPackedFunc("relay.build_module._GraphExecutorCodegen");  
    mod = (*pf)();  
  }
  void UpdateOutput(BuildOutput* ret) override { ret->graph_json = GetGraphJSON(); }
  std::string GetGraphJSON() { return CallFunc<std::string>("get_graph_json", nullptr); }
  ~GraphCodegen() {}
};

GetPackedFunc("relay.build_module._GraphExecutorCodegen") 函数通过名字获取 TVM_REGISTER_GLOBAL 注册的全局函数 CreateGraphCodegenMod()

TVM_REGISTER_GLOBAL("relay.build_module._GraphExecutorCodegen")
    .set_body([](TVMArgs args, TVMRetValue* rv) { *rv = CreateGraphCodegenMod(); });
     }

CreateGraphCodegenMod() 函数创建对象 GraphExecutorCodegenModule 并返回。因此 GraphCodegen 对象的成员 mod 赋值为对象 GraphExecutorCodegenModule

而后续 GraphCodegen 的很多功能都会调用 mod 的功能,也就是 GraphExecutorCodegenModule 对象的功能。

runtime::Module CreateGraphCodegenMod() {
  auto ptr = make_object<GraphExecutorCodegenModule>();  
  return runtime::Module(ptr);  
}

# Init

buildRelay流程图

Init() 函数完成 CodeGen 的初始化。 GraphCodegen 对象 executor_codegen_Init() 函数,首先调用父类 ExecutorCodegenInit() 函数,该函数又调用成员 mod 的初始化函数,即 GraphExecutorCodegenModule 的初始化函数,如下所示:

该函数将 GraphExecutorCodegenModule 的成员 std::shared_ptr<GraphExecutorCodegen> codegen_; 赋值为 GraphExecutorCodegen 对象。

virtual PackedFunc GetFunction(const String& name, const ObjectPtr<Object>& sptr_to_self) {
    if (name == "init") {
      return PackedFunc([sptr_to_self, this](TVMArgs args, TVMRetValue* rv) {
        ICHECK_EQ(args.num_args, 2) << "The expected of arguments are: "
                                    << "runtime::Module mod and Array<Target> targets";
        void* mod = args[0];
        Array<Target> targets = args[1];
        codegen_ = std::make_shared<GraphExecutorCodegen>(reinterpret_cast<runtime::Module*>(mod),
                                                          std::move(targets));
      });
    }

buildRelay流程图

Codegen(func_module, func, mod_name) —— 将 Relay IRModule 降级为 TIR Module。

这是一个很复杂的过程,本篇只讲解部分。

Codegen() 函数调用的也是成员 mod ,也就是 GraphExecutorCodegenModuleCodegen() 函数,这里不在赘述。下面是 GraphExecutorCodegenModuleCodegen() 函数。

显然, codegen 又调用了 GraphExecutorCodegenModule 成员 codegen_Codegen 函数,即 GraphExecutorCodegen 类的 Codegen 函数。

else if (name == "codegen") {
      return PackedFunc([sptr_to_self, this](TVMArgs args, TVMRetValue* rv) {
        IRModule mod = args[0];
        Function func = args[1];
        String mod_name = args[2];
        this->output_ = this->codegen_->Codegen(mod, func, mod_name); 
      });

GraphExecutorCodegen 类的 Codegen 函数如下所示:

该函数实现 Relay IRModule 降级为 TIR Module,并返回包含所有信息的结构体 LoweredOutput

其中关键函数 tec::LowerTE() 完成降级过程。下文代码中做了简单的注释。

LoweredOutput Codegen(IRModule mod, relay::Function func, String mod_name) {
    mod_name_ = mod_name;
    VLOG_CONTEXT << "GraphExecutorCodegen";
    VLOG(1) << "compiling:" << std::endl << PrettyPrint(func);
    
    memory_plan_ = GraphPlanMemory(func);  
    backend::FunctionInfo func_info;
    if (memory_plan_.defined()) {
      
      func_info =
          relay::tec::UpdateMainWorkspaceSize(mod, config_, memory_plan_->expr_to_storage_info);
      mod = WithAttr(mod, "main_func_info", func_info);
    }
    IRModule lowered_mod = tec::LowerTE(mod_name_, config_, [this](BaseFunc func) {  
      
      
      
      if (func->GetAttr<String>(attr::kCompiler).defined()) {
        UpdateConstants(func, &params_);
      }
      tec::UpdateFunctionMetadata(func, this->function_metadata_);
    })(mod);  
    Optional<backend::FunctionInfo> main_func_info =
        lowered_mod->GetAttr<backend::FunctionInfo>("main_func_info");  
    function_metadata_.Set(runtime::symbol::tvm_module_main, main_func_info.value());
    Function lowered_main_func = Downcast<Function>(lowered_mod->Lookup("main"));
    
    
    
    
    memory_plan_ = GraphPlanMemory(lowered_main_func);  
    
    
    for (auto param : lowered_main_func->params) {
      auto node_ptr = GraphInputNode::make_node_ptr(param->name_hint(), GraphAttrs());
      var_map_[param.get()] = AddNode(node_ptr, param);
    }
    heads_ = VisitExpr(lowered_main_func->body);
    std::ostringstream os;
    dmlc::JSONWriter writer(&os);
    GetJSON(&writer);    
    LoweredOutput ret;  
    ret.graph_json = os.str();  
    
    ret.external_mods =  
        lowered_mod->GetAttr<Array<runtime::Module>>(tvm::attr::kExternalMods).value_or({});
    
    ret.params = std::unordered_map<std::string, tvm::runtime::NDArray>();
    Map<String, runtime::NDArray> const_name_to_constant =
        lowered_mod->GetAttr<Map<String, runtime::NDArray>>(tvm::attr::kConstNameToConstant)
            .value_or({});
    for (const auto& kv : const_name_to_constant) {
      VLOG(1) << "constant '" << kv.first << "' contributed by external codegen";
      ICHECK(ret.params.emplace(kv.first, kv.second).second);
    }
    
    for (const auto& kv : params_) {
      VLOG(1) << "constant '" << kv.first << "' contributed by TECompiler";
      ICHECK(ret.params.emplace(kv.first, kv.second).second);
    }
    ret.function_metadata = std::move(function_metadata_);  
    
    ret.lowered_funcs = tec::GetPerTargetModules(lowered_mod);
    ret.metadata =  
        ExecutorCodegenMetadata({} , {} , {} ,
                                {} , {} , {} ,
                                runtime::kTvmExecutorGraph , mod_name_ ,
                                "packed" , Bool(false) );
    return ret;  
  }

再看一下 tec::LowerTE() 函数。其第三个参数 [this](BaseFunc func) {...} 是一个 lambda 表达式,功能和 “外部函数” 相关,外部函数指的是 “标明使用外部编译器编译的 function”,如 “dnnl,ccompilmer” 等,现在暂不关注。

IRModule lowered_mod = tec::LowerTE(mod_name_, config_, [this](BaseFunc func) {  
      
      
      
      if (func->GetAttr<String>(attr::kCompiler).defined()) {
        UpdateConstants(func, &params_);
      }
      tec::UpdateFunctionMetadata(func, this->function_metadata_);
    })(mod);  

tec::LowerTE() 函数返回的是一个 Sequential 类,其包含多个按照顺序执行的 Pass。如果对 Pass 还不了解或者遗忘了,可以再回顾一下《【TVM】通过代码学习类【3.5】Pass》

返回的 Sequential 类不仅包含 RelayToTIRTargetHookExtractPrimFuncConstantsInferType() 三个函数获得的 Pass,还包含 CreateModulePass(pass_func, 0, "LowerTE", {"InferType"}) 封装成的 Pass。

CreateModulePass(pass_func, 0, "LowerTE", {"InferType"})pass_func 封装成 Pass。 pass_func 是 lambda 表达式,其调用了含有四个参数的函数 LowerTE(module, module_name, process_fn, complilation_config); ,完成了降级的主要内容。注意该 LowerTE() 含有四个参数,非前述提到的含有三个参数的 tec::LowerTE()

tec::LowerTE() 函数返回 Sequential 类之后立即执行,完成对 Relay Module 的降级。

Pass LowerTE(String module_name, CompilationConfig complilation_config, ProcessFn process_fn) {
  runtime::TypedPackedFunc<IRModule(IRModule, PassContext)> pass_func = [=](IRModule module,  
                                                                            PassContext ctx) {
    return LowerTE(module, module_name, process_fn, complilation_config);
  };
  return tvm::transform::Sequential(
      {tvm::relay::transform::RelayToTIRTargetHook(complilation_config),
       tvm::transform::CreateModulePass(pass_func, 0, "LowerTE", {"InferType"}), InferType(),
       tvm::tir::transform::ExtractPrimFuncConstants()});
}

后续文章将按照顺序介绍 RelayToTIRTargetHookLowerTE()InferType()ExtractPrimFuncConstants

# 后记

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