- 编译器
- 深度学习
- 模型优化
【免费下载链接】tvm
Open deep learning compiler stack for cpu, gpu and specialized accelerators
导读
tvm.relay.backend是 Apache TVM 中承接「Relay 前端 IR」与「后端运行时」的关键桥梁模块,它负责把经过算子融合与优化后的 Relay 函数,降级(lower)为可直接在 CPU/GPU/专用加速器上执行的底层实现。本篇指南以 TVM 仓库中的官方 API 文档 backend.rst 为骨架,深入其引用的四个核心子模块——interpreter、te_compiler、graph_executor_codegen与vm,并结合 python/tvm/relay/backend/ 与 src/relay/backend/ 下的真实源码,逐一讲解 TE 编译器缓存机制、算子实现选择策略、参考解释器、图执行器代码生成和 Relay 虚拟机的完整调用链。读完本文,你将能理解relay.build从 Relay 模块到可运行运行时模块的完整降级路径,并掌握relay.create_executor、TECompiler、VMCompiler等关键 API 的实际用法。
一、模块总览:backend 包导出什么
在 python/tvm/relay/backend/init.py 中,tvm.relay.backend包只做了三件事:
"""Backend codegen modules for relay.""" from . import te_compiler from .executor import Executor from .runtime import Runtime- 导入
te_compiler子模块(TE 编译器引擎); - 导出
Executor(执行器配置类,见 executor.py); - 导出
Runtime(运行时配置类,见 runtime.py)。
这两个配置类是后续relay.build阶段控制"用哪种执行器(executor)、跑在哪个运行时(runtime)"的核心句柄:
| 类 | 构造方式 | 注册表名 | 关键方法 |
|---|---|---|---|
Executor(name, options) | 如tvm.relay.backend.Executor("graph", {"link-params": 1}) | "executor" | list_registered()、list_registered_options() |
Runtime(name, options) | 如tvm.relay.backend.Runtime("cpp") | "runtime" | list_registered()、list_registered_options() |
Executor.__getitem__/Runtime.__getitem__可用于读取对应配置项;底层通过_backend.CreateExecutor/_backend.CreateRuntime(C++ FFI)注册对象,真正的可选项由 C++ 侧src/relay/backend/executor.cc、src/relay/backend/runtime.cc维护。
二、tvm.relay.backend.interpreter:Relay 参考解释器
文档的automodule:: tvm.relay.backend.interpreter对应 interpreter.py,其模块 docstring 明确写它是 "The Python interface to the Relay reference interpreter"。
2.1 抽象基类Executor
Executor是执行 Relay 程序的抽象接口,定义了三个方法:
_convert_args(expr, args, kwargs):把「位置参数 + 关键字参数」合并为与 Relay 函数形参顺序一致的参数序列。规则是:先依次放置位置参数,再用关键字参数补齐剩余形参;若某个形参既被位置参数占用又出现在 kwargs 中,会抛出 "duplicate argument supplied" 异常;参数不足则抛 "insufficient arguments"。_make_executor(expr=None):构造一个实现了expr行为的 Python 可调用对象,子类必须实现。evaluate(expr=None, binds=None):评估 Relay 表达式。binds允许为自由变量提供额外绑定(内部通过ScopeBuilder转成let绑定);当expr是Function或GlobalVar时直接构造执行器,普通表达式则会包装成无参函数执行。若expr为 None,则默认取模块中的main函数。
executor = relay.create_executor(kind="debug", mod=module) func = executor.evaluate(expr) # 复用同一个可调用对象 a = func(args1) b = func(args2)2.2 解释器实现Interpreter
Interpreter(Executor)的构造参数为mod(tvm.IRModule)、device(运行时设备)、target(构建目标,仅支持同构执行)。其_make_executor通过 FFI 调用_backend.EvalFunction(self.mod, expr, self.device, self.target)把表达式编译为一个可重复应用的 packed function,随后把 Python 侧的 numpy 数组 / NDArray / ADT / 元组等参数通过_arg_to_ast反射回 Relay AST(Constant、Tuple、RefCreate、Call、const),再调用 packed function 求值。
需要特别注意的是 docstring 中的 CAUTION:解释器在每次evaluate调用时才准备模块,而非在create_executor时一次性准备,因此若用同一 executor 多次求值不同参数,模块绑定会被重复处理。最佳实践是像上文示例那样把evaluate的结果保存为函数复用,而不是每轮都调用evaluate。
2.3 工厂函数relay.create_executor
解释器通常不是直接实例化的,而是通过 build_module.py 中的relay.create_executor工厂创建:
def create_executor(kind="debug", mod=None, device=None, target="llvm", params=None):kind:执行器类型,可选debug(解释器)、graph(图执行器)、aot(AOT 执行器)、vm(虚拟机);mod:tvm.IRModule,缺省为空模块;device:执行设备,缺省时从第一个 target 推导默认设备;target:支持多目标(heterogeneous),但文档明确提示该 API 不允许多设备,因此异构编译尚未支持;params:推理期不变的常量参数,会通过bind_params_by_name绑定进main。
官方 docstring 自带一个最小可运行示例:构造x + 1的 Relay 表达式后用kind="vm"求值,输入[2]返回[3.]。
三、tvm.relay.backend.te_compiler:TE 编译引擎
文档的automodule:: tvm.relay.backend.te_compiler对应 te_compiler.py,模块 docstring 即 "TE compiler engine (replacing legacy compile_engine)"——它是取代旧compile_engine的新一代编译引擎。
3.1 核心数据结构
LoweredOutput:承载outputs(te.Tensor 列表)与implement(OpImplementation)两个字段,是一次算子降级的输出结果。CCacheKey:编译缓存的键,由source_func(relay.Function)与target(tvm.Target)组成。CCacheValue:编译缓存的值,"including usage statistics"(含使用统计),用于后续缓存淘汰策略。
3.2 算子实现选择:get_valid_implementations与select_implementation
这是 TE 编译引擎最核心的决策逻辑:
get_valid_implementations(op, attrs, inputs, out_type, target)读取算子注册的FTVMStrategy(可用tvm.relay.op.register_strategy注册),在 target 上下文中调用 strategy 得到OpStrategy,遍历其specializations,用tvm.arith.Analyzer的canonical_simplify化简各 specialization 的条件子句,筛选出全部有效实现。select_implementation(op, attrs, inputs, out_type, target, use_autotvm=True)在上面的基础上挑选最优实现:- 若
use_autotvm=False(或 AutoTVM / AutoScheduler / MetaSchedule 已启用),直接选择plevel最高的实现; - 若
use_autotvm=True,先对每个可调优实现计算 AutoTVM workload,查询DispatchContext得到配置与代价,选代价最低的实现;找不到调优记录时退回最高plevel实现,并打印提示 "One or more operators have not been tuned. Please tune your model for better performance."(可通过 DEBUG 日志查看细节); - 找不到任何有效实现时抛出
RuntimeError(f"No valid {op} implementations for {target}")。
- 若
从源码结构看,
plevel(优先级)机制是 TVM 为同一算子注册多种后端实现(如 CUDA 的 cublas、cutlass 与 generic 实现)时做默认排序的依据。
3.3 动态形状处理
lower_call(注册为relay.backend.lower_call)在把 Call 表达式降级前,会检查输入输出类型是否动态(_ty.is_dynamic);对动态形状的调用,会选择use_autotvm=False直接取最高优先级实现,因为当前 TVM 尚未允许为动态形状自动生成多个 kernel(源码中保留 TODO 注释)。同时get_shape负责把IntImm维度按INDEX_DEFAULT_I64编译开关转换为 int64 或 int32,把tvm.tir.Any(动态维度)转为te.size_var("any_dim", "int32")。
3.4TECompiler类与全局单例
class TECompiler(Object): def lower(self, source_func, target=None, mod_name="default"): ... def jit(self, source_func, target=None): ... def clear(self): ... def items(self): ...lower:把source_func(relay.Function 或 CCacheKey)降级为CachedFunc;mod_name会先经过mangle_module_name修正;出错时会把函数 AST(astext(show_meta_data=False))拼进异常信息方便定位。jit:即时编译为tvm.runtime.PackedFunc。clear:清空缓存。items:列出缓存中的所有(CCacheKey, CCacheValue)对。get():返回全局 TE 编译器单例(_backend._TECompilerGlobal())。
此外模块还提供lower_to_primfunc(relay_func, target),调用全局函数relay.backend.LowerToPrimFunc把融合后的 Relay 原始函数直接转为tir.PrimFunc——这一步正是"Relay → TIR"的关键跳转。
C++ 侧实现位于 src/relay/backend/te_compiler.cc、src/relay/backend/te_compiler_cache.cc(缓存)与 src/relay/backend/task_extraction.cc(AutoTVM 任务抽取)。
四、tvm.relay.backend.graph_executor_codegen:图执行器代码生成
文档的automodule:: tvm.relay.backend.graph_executor_codegen对应 graph_executor_codegen.py。模块 docstring 把编译流程概括为三步:
- 把单个 Relay 表达式(要求是函数)编译为图语言程序:函数形参对应计算图中的 placeholder/输入与模型参数,函数体即计算图本身;
- 图语言由
Node、NodeRef、InputNode、OpNode构成,表示 TVM 图格式的程序; - 通过一个 printer 把图格式转成JSON 字符串,该字符串可被
contrib.graph_executor或其他兼容 TVM runtime 的系统加载。
GraphExecutorCodegen(mod, target)的codegen(ir_module, func)方法返回三元组:
graph_json : str—— 供运行时消费的图 JSON;mod : IRModule—— 降级后的函数集合;params : Dict[str, tvm.nd.NDArray]—— 额外的常量参数(通过_list_params_name与_get_param_by_name从 C++ 侧取回并 copy 到新数组)。
底层 C++ 实现在 src/relay/backend/graph_executor_codegen.cc,内存规划依赖 src/relay/backend/graph_plan_memory.cc(静态内存池分配);_GraphExecutorCodegenFFI 对象则由 python/tvm/relay/_build_module.py 暴露。这一路径是传统relay.build+tvm.contrib.graph_executor部署方案(含 Android/iOS 等移动端)的基石。
五、tvm.relay.backend.vm:Relay 虚拟机
文档的automodule:: tvm.relay.backend.vm对应 vm.py,为 Relay VM 提供 Python 接口,包含编译与执行两部分。
5.1 便捷函数compile
def compile(mod, target=None, target_host=None, params=None): compiler = VMCompiler() if params: compiler.set_params(params) compiler.lower(mod, target, target_host) compiler.codegen() return compiler.get_exec()一次编译分四步:设置参数 →lower降级为 VM 字节码 →codegen生成 kernel 库 → 取回tvm.runtime.vm.Executable(同时包含库代码与字节码)。
5.2VMCompiler类
set_params(params)/get_params():设置/取回常量参数。set_params会把 numpy 数组转为NDArray再包成relay.const;get_params返回{name: ndarray}字典。lower(mod, target, target_host):通过Target.canon_multi_target_and_host规范化目标后调用 C++_VMCompiler["lower"]。注意它会先根据当前DispatchContext判断是否处于 fallback(根)上下文——若是则进入autotvm.tophub.context(raw_targets),即自动从TopHub拉取预调优参数;否则使用EmptyContext。这就是 VM 编译默认能吃到社区调优结果的原因。codegen():生成 kernel 库。optimize(mod, target, target_host, params):仅优化(返回优化后的IRModule与参数),不产出可执行文件,适合调试或查看优化结果。get_exec():返回tvm.runtime.vm.Executable。
target_host参数的语义:编译 CUDA 等设备程序时,还需要 CPU 侧的宿主代码与驱动交互(设置维度与参数),target_host即宿主代码生成目标;默认优先用llvm,未启用时退回 stackvm 解释器。
5.3VMExecutor
VMExecutor(Executor)把 VM 适配进统一的执行器接口:_make_executor中用self.mod["main"] = expr替换 main 后调用compile得到可执行文件,实例化tvm.runtime.vm.VirtualMachine,最终返回的包装函数把 Python 参数经过_convert_args转换后直接self.vm.run(*args)。它特别适合做实验与调试(docstring 也说明 VM 可直接通过tvm.runtime.vm使用)。
C++ 侧编译实现在 src/relay/backend/vm/compiler.cc,配套的 VM 专用 pass 包括lambda_lift.cc(闭包转换)、manifest_lifetimes.cc(生命周期分析)与removed_unused_funcs.cc(无用函数移除)。
六、后端降级全景与验证路径
综合以上四个子模块,一次典型的relay.build后端流程可以归纳为:
Relay IRModule │ pass 优化(算子融合 FuseOps 等) ▼ 融合后的原始函数(primitive function) │ te_compiler.lower_call / select_implementation(strategy + plevel + AutoTVM) ▼ te.Tensor 计算 + schedule → LowerToPrimFunc │ TECompilerCache(CCacheKey → CachedFunc) ▼ TIR PrimFunc → 各目标代码生成(LLVM/CUDA/OpenCL/...) │ graph_executor_codegen 或 vm/compiler 或 aot_executor_codegen ▼ graph JSON + lowered func + params → 运行时模块在仓库中验证这条链路最直接的方式是运行测试:tests/python/relay/下存在大量覆盖 interpreter、VM、graph executor 与 TECompiler 的用例,例如通过python -m pytest tests/python/relay/test_vm.py -k "compile"可验证 VM 编译路径。对希望深入 C++ 实现的读者,推荐按以下顺序阅读:
- src/relay/backend/te_compiler.cc + src/relay/backend/te_compiler_cache.cc:降级主流程与缓存;
- src/relay/backend/graph_executor_codegen.cc + src/relay/backend/graph_plan_memory.cc:图 JSON 生成与静态内存规划;
- src/relay/backend/interpreter.cc:参考解释器求值;
- src/relay/backend/vm/compiler.cc:VM 字节码编译。
七、小结:如何选择后端执行路径
| 执行路径 | 适用场景 | 核心 API |
|---|---|---|
debug(Interpreter) | 原型验证、逐表达式调试,不追求性能 | relay.create_executor(kind="debug") |
graph(GraphExecutor) | 静态形状、传统部署(含移动端) | relay.build+tvm.contrib.graph_executor |
vm(VirtualMachine) | 动态形状、函数调用/控制流丰富的模型 | relay.vm.compile/VMCompiler |
aot(AOT Executor) | 无操作系统嵌入式/MCU 场景,编译期规划内存 | relay.build(..., executor=Executor("aot")) |
tvm.relay.backend的设计核心在于解耦:前端策略(FTVMStrategy/ plevel)决定"有哪些实现"、AutoTVM 决定"选哪个实现"、TECompiler决定"如何缓存与复用降级结果",最终由 interpreter / graph / vm / aot 四种执行器把降级产物映射到对应运行时。理解这条链路,就等于掌握了 TVM 从模型 IR 到实际推理的全部关键环节。
- 编译器
- 深度学习
- 模型优化
【免费下载链接】tvm
Open deep learning compiler stack for cpu, gpu and specialized accelerators
相关推荐
TVM Relay 虚拟机(Virtual Machine)架构深度解析:设计、指令集、编译器与序列化
TVM Relay 虚拟机(Virtual Machine)架构深度解析:设计、指令集、编译器与序列化 导读 本文以 docs/arch/virtual_mac
编译器深度学习模型优化Electrobun 与 Svelte 实战:轻量级前端框架的桌面应用开发
Electrobun 与 Svelte 实战:轻量级前端框架的桌面应用开发 Electrobun 是一个基于 TypeScript 的超快速、轻量级跨平台桌面应
桌面应用跨平台KDBush:2D点的终极静态空间索引库,让搜索速度提升10倍!
KDBush:2D点的终极静态空间索引库,让搜索速度提升10倍! KDBush 是一款基于扁平 KD 树的超快速 2D 点静态空间索引库,专为高效处理海量二维点
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考