假设你经营着两家截然不同的餐饮店:一家是中央厨房工厂,另一家是精致私房菜餐厅。工厂在开工前就把所有工序焊死在一条流水线上——洗菜、切配、翻炒、装盒,每一步的工位、顺序、分量都提前定好,原料进去、成品出来,效率惊人。餐厅则完全相反,客人落座才点菜,厨师看着菜单现场决定先备料还是先起锅,遇到“今天海鲜售罄”还能立刻换菜——灵活至极,但每一桌都要重新动一轮脑筋。 RELAY Executor 就是 TVM 这台“算法中央调度机”里的两位后厨总管。当你的模型被翻译成 RELAY IR(一种中间表示,相当于菜谱)后,到底该把它交给工厂流水线(图执行器 Graph Executor)还是私房餐厅(VM 执行器 Virtual Machine Executor),直接决定了推理速度、内存占用和部署灵活性。本文用这两个后厨的故事,带你搞懂两种执行器的差异、选型思路和实际配置方法。
掌握 RELAY Executor 的两种模式及其选型,是 AI 工程师把模型高效部署到真实硬件的必备技能——小到手机 App 的实时推理,大到服务器集群的批量服务,都离不开这个“最后一百米”的决策。
目录
可视化演示
在损失曲面上展示SGD、Momentum、AdaGrad三种梯度下降优化器的收敛轨迹,支持切换优化器类型与调节学习率,直观对比不同策略的收敛速度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
SGD:随机梯度下降 沿负梯度方向逐步更新参数,简单但收敛路径可能振荡 \u03B8 = \u03B8 - \u03B7 \u00B7 \u2207L(\u03B8) 学习率过大导致振荡,过小则收敛缓慢
执行器总览:两张后厨的开门生意
中央厨房工厂和私房菜餐厅共用同一个“菜品研发部”。研发部把菜谱标准化为 RELAY IR——一种与硬件无关的中间表示,既保留计算逻辑,又方便做算术简化、算子融合等优化。菜谱研发完成后,真正把它做成菜的后厨有两个候选:图执行器和 VM 执行器。
从技术上看,RELAY Executor 是 RELAY IR 的执行后端,负责把编译优化后的 IR 跑在 CPU、GPU 等目标硬件上。整个流程可以概括为:relay.build() 把 IR 模块编译成可执行对象,随后 Executor 在目标设备上加载并运行。两者产出的“中间物”形态不同——图执行器产出一张展开的静态计算图,VM 执行器产出一段字节码。
这里有一个贯穿全文的视角:任何推理服务的总耗时都可以拆成三段。先把这个“账单结构”刻在脑子里,后面所有选型逻辑都从它出发。
理解了这三段费用结构,选型本质就清晰了:如果每天要出十万份标准化快餐,流水线工厂(图执行器)自然划算;如果每桌客人需求都不同,私房餐厅(VM 执行器)才是出路。接下来我们分别走进这两间后厨。
- 编译时间把 RELAY IR 翻译成目标硬件可执行代码的耗时,只在首次构建时发生一次。
- 启动时间创建运行时、分配内存、加载参数等初始化开销,发生在每次进程启动时。
- 推理总时长服务处理 N 次请求的累计推理时间,N 越大,单次推理性能越重要。
图执行器:一条焊死的流水线
走进中央厨房工厂,你会看到一条壮观的生产线:工位一字排开,传送带把原料从第一个工位送到最后一个工位,每个工位只做一件事,顺序永不改变。图执行器(Graph Executor)就是这条流水线。它在编译期拿到完整的 RELAY IR 计算图后,一口气完成三件事:把图展开成有序的算子序列、为每个中间张量规划好内存地址、确定算子的执行顺序。此后每次推理,数据就像传送带上的原料,按预排路线一路走到底。
流水线的核心竞争力是“确定性”。因为执行顺序和内存布局在编译期就固定了,图执行器可以精确复用内存——两个生命周期不重叠的中间张量可以共享同一块内存,这在工程上称为“静态内存规划”。
流水线的代价同样明显:只要有一个环节被要求“临时改动”,整条线就得停下来重新铺设。对应到模型上,如果输入尺寸不固定,或者模型里有条件分支、循环,图执行器就难以应对。所以它最擅长伺候那些“结构稳定、按部就班”的静态模型。
- 下译lower 把高层 IR 节点逐个翻译成目标硬件上的底层算子调用,这是编译期的核心动作。
- 算子序列翻译结果是有严格先后顺序的算子集合,顺序由拓扑排序确定,运行时不改变。
VM执行器:一间现点现做的私房厨房
离工厂三条街,私房菜餐厅的厨房完全是另一番景象。没有传送带,只有一位拿着菜单的厨师:菜单上写着一条条指令(字节码),厨师手里捏着一个“当前步骤”指针(程序计数器),读到“备料”就备料,读到“起锅”就起锅,读到“如果客人不吃辣,跳到第 7 步”就果断跳转。VM 执行器(VM Executor)正是这样一位厨师。
VM 执行器把 RELAY IR 编译成一段紧凑的字节码,运行时通过程序计数器逐条取指令、解释、执行。因为每个指令都可以自由跳转,VM 天然支持条件分支、循环、动态 shape 这类需要运行时决策的行为。这也是它逼近图执行器“灵活性”的底气。
当然,现点现做意味着每一步都要“看一眼菜单再动手”,比流水线上的机械动作多了一层解释开销。厨师再灵活,也快不过一台不会思考的传送带——这正是两种执行器在性能上的根本张力。
- 当前指令位置程序计数器 P_t 记录正在执行第几条字节码指令。
- 跳转映射pc 函数根据当前指令内容和运行条件,决定下一条要执行的指令编号。
- 指令推进执行完当前指令后,计数器更新为跳转目标,这就是控制流的底层机制。
静态与动态图:固定套餐和私人订制
去工厂点餐,你只能从固定套餐里选;去私房餐厅,你可以跟厨师说“少放盐、多加辣”。模型世界里的这道分界线,就是静态图与动态图。静态图模型的输入尺寸、计算路径在编译时全都已知——比如 ResNet 处理 224×224 的图片,路径永远是卷积、池化、全连接那一套。动态图模型则不同:输入序列可能忽长忽短,模型内部可能有循环、有提前退出的分支。
判断一个模型属于哪一类,有一条非常简单的判据:看它的计算图里是否存在“未知维度”或“运行时分叉”。只要存在,就是动态图。
这道判据直接决定了该进哪个后厨:静态图上流水线,动态图进私房厨房。值得注意的是,很多真实模型是“灰姑娘”——比如带 beam search 的翻译模型,主体是静态的,但搜索过程是动态的。这类模型在选择执行器时要格外小心,不能只看主干结构就盲目下单。
- 存在性判断只要图中至少有一个节点满足条件,整张图就被判定为动态图。
- 未定形状shape 为 None 表示该张量的某个维度(如序列长度、batch 大小)要等运行时才知道。
性能对比:效率与灵活的跷跷板
工厂和餐厅谁赚得更多?答案是:看你的客流长什么样。如果只卖一款爆品、每天十万单,工厂完胜;如果客人众口难调、每桌菜都不一样,餐厅虽然慢,却是唯一能接单的店。两种执行器的性能差异,正是这架跷跷板。
先把开销公式摆出来。图执行器和 VM 执行器跑同一份模型,总耗时可以分别写成:
但性能不只是延迟。内存方面,图执行器的静态内存规划让中间张量复用率达到极致;VM 虽然内存占用稍高,却能秒级切换不同模型——因为字节码加载比重编译快得多。所以说,“快”要分场景:延迟敏感、固定结构的服务选图执行器;需要热切换、动态行为的服务,VM 的“灵活”本身就是一种性能。
\displaystyle T_{\text{graph}} = T_{\text{init}} + N \times t_g,\qquadT_{\text{vm}} = (T_{\text{init}} + T_{\text{load}}) + N \times (t_v + t_d)- 图执行器总耗时初始化一次后,N 次推理每步都是纯算子执行,单次耗时最低。
- VM 总耗时除初始化外,还要加载字节码,且每次推理都叠加解释执行的开销。
- 单步开销对比同一算子在图中是直接调用,在 VM 中要先解码指令再调用,因此 t_g 更小。
部署选型:把合适的菜送进合适的厨房
假设你是两家店的“菜品调度总监”,手里有一批模型要上菜,怎么分派?原则只有一个:让结构性强的模型走流水线,让需求多变的模型进餐厅。具体到工程场景——
服务端批量推理,典型的做法是把固定 batch 的图像分类模型编译成图执行器,它能在 GPU 上把流水线排到最满。边缘端的智能摄像头,模型结构固定、内存紧张,图执行器的内存复用优势极其宝贵。反过来,自然语言处理里的动态序列模型、含 while 循环的控制流模型,以及需要频繁在多个模型间切换的推荐系统,VM 执行器才是正解。
可以用一个加权决策式来概括这个选择过程:
现实中,λ 的取值往往不难:模型结构固定、延迟敏感,就把 λ 压到接近 1,选图执行器;模型动态、需要频繁切换,就把 λ 放低,选 VM 执行器。记住这个跷跷板,你就抓住了选型的灵魂。
- 候选集合可选方案只有图执行器和 VM 执行器两种,不存在第三种万能选项。
- 加权代价延迟和灵活性按权重合并成一个可比较的标量,λ 由业务场景决定。
- 最优选择argmin 表示在所有候选中挑出总代价最小的那个执行器。
配置实践:给后厨下对订单
选定了后厨,接下来要做的就是给后厨“下准确订单”——在 TVM 代码里把执行器类型告诉编译器。这一步很机械,但细节决定成败。核心是 relay.build 的参数配置:
工程上有一个经典坑:使用 VM 执行器时,编译成功后不能直接跑,必须额外调用 vm.initialize() 完成内存池和依赖库的初始化;而图执行器则用 graph_executor.create() 直接绑定模块。跳过 initialize 往往报出难以排查的 shape 错误。调试建议是先以 opt_level=0 跑通正确性,再逐步提升优化等级。最后记得用 time_evaluator 或 profiler 实测推理耗时——纸上谈兵的选型,永远不如一次真实压测来得可靠。
- 执行器取值executor 是 relay.build 的一个字符串参数,决定产出图模块还是可执行字节码。
- 优化等级opt_level 控制算子融合、常量折叠等优化措施的激进程度,0 最保守,3 最激进。
AI 应用场景
场景一:边缘端智能相机的固定结构推理
阶段:模型部署与推理优化
解决的问题:边缘设备内存小、算力弱,传统 PyTorch 运行时动态分配内存、解释执行开销大,跑不动实时检测模型。图执行器在编译期完成内存规划和算子融合,把模型压成一条“固定流水线”,推理延迟可降低 30%-50%。
具体案例:某智慧零售项目在 Jetson Nano 上部署 YOLOv5s 检测模型。模型结构固定、batch=1,工程师用 relay.build(..., executor="graph") 编译出静态图模块,再用 graph_executor.create 加载,实测单帧推理从 120ms 降到 60ms,内存占用减少约 40%。
场景二:NLP 服务端的动态序列推理
阶段:动态模型部署
解决的问题:机器翻译、文本生成模型输入长度不固定,且 beam search 需要循环控制流。图执行器面对这种“客人随时点菜”的场景会直接哑火;VM 执行器的字节码跳转机制能优雅处理动态 shape 和循环。
具体案例:某智能客服系统部署 BART 摘要模型,输入序列长度从 20 到 512 不等,且解码阶段有 while 循环。使用 executor="vm" 编译后,程序计数器在字节码间自由跳转,无需为每种长度重新编译,首 Token 延迟降低 200ms 以上。
场景三:多模型推荐系统的热切换
阶段:多模型服务编排
解决的问题:推荐系统经常要按用户画像在多个模型间切换,传统方式每次切换都要重新编译图,耗时数秒。VM 执行器加载字节码仅需毫秒级,适合“后厨随时换菜单”。
具体案例:某广告推荐平台有 5 个候选模型,按流量特征动态切换。使用 VM 执行器后,模型热切换时间从 3 秒降到 30ms,SLA 达标率显著提升。
Python 演示
"""演示图执行器与VM执行器的执行方式差异."""import time class GraphExecutor: """模拟图执行器 固定算子序列 顺序执行""" def __init__(self, ops): self.ops = list(ops) print("[Graph] 编译期: 图结构固定 算子序列已确定") def run(self, x): for op in self.ops: x = op(x) return x class VMExecutor: """模拟VM执行器 字节码解释执行 支持动态分支""" def __init__(self, bytecode): self.code = bytecode print("[VM] 编译期: 只生成字节码 执行期逐条解释") def run(self, x, cond): pc = 0 while pc < len(self.code): instr = self.code[pc] if instr == "push": x = x + 1 pc += 1 elif instr == "jif_true": pc = pc + 1 if cond else pc + 2 elif instr == "mul": x = x * 2 pc += 1 elif instr == "sub": x = x - 1 pc += 1 elif instr == "out": return x return x def main(): ops = [lambda v: v + 1, lambda v: v * 2, lambda v: v - 1] graph = GraphExecutor(ops) t0 = time.perf_counter() out = graph.run(10) print("[Graph] 输出:", out, "耗时 %.6f 秒" % (time.perf_counter() - t0)) code = ["push", "jif_true", "mul", "sub", "out"] vm = VMExecutor(code) for cond in (True, False): t0 = time.perf_counter() out = vm.run(10, cond) print("[VM] cond=%s 输出:" % cond, out, "耗时 %.6f 秒" % (time.perf_counter() - t0)) if __name__ == "__main__": main()
self.ops = list(ops)把预编译好的算子序列存下来,模拟图执行器在编译期“焊死”流水线。for op in self.ops:图执行器的核心循环:按固定顺序逐个调用算子,没有运行时决策。self.code = bytecodeVM 保存的是字节码指令列表,而非展开的算子序列。pc = 0程序计数器从第一条指令开始,这是 VM 执行器的“当前步骤指针”。instr = self.code[pc]每次循环先取当前指令,对应“看一眼菜单再动手”。pc = pc + 1 if cond else pc + 2条件跳转指令的精髓:条件为真走乘 2 分支,为假跳过它走减 1 分支。
常见误区
误区我听说图执行器做了编译期优化,所以不管什么模型,选图执行器一定比 VM 快,选 VM 就是牺牲性能。
正解在结构固定的静态模型上,图执行器确实更快;但如果模型带动态 shape 或控制流,图执行器要么不支持,要么只能退化成“每来一个新 shape 就重新编译一次”,这时反而比 VM 慢几十倍。就像用固定模具做菜,客人要的菜超出模具范围,你还得重新开模,不如厨师现场炒。
为何常见:入门例程大多是静态 CNN,图执行器的优势被放大,动态场景下的劣势很少被提及。
误区VM 执行器是为了动态图设计的,静态模型用 VM 纯属浪费,肯定不划算。
正解VM 执行器同样能跑静态模型,而且在模型热切换、需要运行时调试、跨版本部署等场景下,VM 的字节码加载快、行为可控,反而更合适。选用哪个执行器不仅要看模型是否动态,还要看整个部署流程的需求。好比餐厅也能做固定套餐,只是价格贵一点,但你获得了随时换菜单的自由。
为何常见:把“动态模型需要 VM”错误理解为“VM 只能用于动态模型”,逻辑上倒置了。
误区我的模型没有 if/else 分支,结构从来不变,那它肯定是静态图,可以放心用图执行器。
正解静态图的定义不仅看分支结构,更关键的是所有张量 shape 在编译期是否完全确定。很多 NLP 模型结构固定,但输入序列长度可变,导致中间张量 shape 带 None,仍然是动态图。图执行器遇到这种情况会要求你填一个固定 shape,填了浪费计算,不填直接报错。就像工厂接单时,你不告诉它盒子尺寸,它没法决定开哪条线。
为何常见:初学时期把“动态”简单等同于“有控制流”,忽略了 shape 动态性这一更常见的触发条件。
误区relay.build 里 executor 参数填 graph 和 vm 效果一样,反正最后出来的模型都能推理。
正解两者产物完全不同:graph 产出“图模块 + 参数”,vm 产出“字节码 + 参数 + 虚拟机运行时”。用错了会导致调用接口对不上——比如拿 graph 的用法去调 vm 模块,会报“找不到 initialize”。选型必须在 build 之前定好,就像下订单时就得选工厂还是餐厅,菜都出锅了再换后厨就晚了。
为何常见:教程里常常只用一种执行器演示,让人误以为 executor 参数只是一个无关紧要的开关。
面试题
Q1. RELAY Executor 的两种执行模式是什么?
- A.CPU 执行器和 GPU 执行器
- B.图执行器和 VM 执行器← 正确答案
- C.同步执行器和异步执行器
- D.推理执行器和训练执行器
答案:B。RELAY Executor 只有图执行器(Graph Executor)和 VM 执行器(Virtual Machine Executor)两种模式。A、C、D 描述的是硬件或运行机制的划分,不是 RELAY Executor 的执行后端模式。
Q2. 以下哪类模型最适合选择图执行器?
- A.结构固定的静态模型← 正确答案
- B.含大量控制分支的模型
- C.输入长度不定的模型
- D.需要热切换的模型
答案:A。图执行器在编译期固定算子顺序和内存布局,最适合“结构稳定、按部就班”的静态模型。控制分支、动态输入长度和热切换都是动态特征,更适合 VM 执行器。
Q3. VM 执行器相比图执行器的核心优势是什么?
- A.内存占用方面总是更小
- B.支持动态shape分支← 正确答案
- C.启动速度方面必然更快
- D.算子融合上比图更彻底
答案:B。VM 执行器以字节码加程序计数器驱动执行,能自由跳转,天然支持动态 shape 和控制流。内存、启动速度、算子融合都不是 VM 的必然优势,可能因场景而异。
Q4. 图执行器在编译期主要完成哪项工作?
- A.运行时动态创建算子
- B.解释执行字节码指令
- C.预分配内存和固定算子顺序← 正确答案
- D.每次推理重新规划路径
答案:C。图执行器编译期的两大核心动作是把图展开成有序算子序列、规划内存复用方案。解释字节码是 VM 的工作,动态创建算子和重新规划路径则违背了图执行器“编译期确定一切”的设计哲学。
Q5. 以下哪些属于 VM 执行器的特点?
- A.基于字节码逐条解释执行← 正确答案
- B.支持程序计数器自由跳转← 正确答案
- C.适合动态控制流模型← 正确答案
- D.一定比图执行器执行慢
答案:ABC。VM 执行器把 IR 编译成字节码,用程序计数器逐条解释,跳转指令使其适合动态控制流。但“一定更慢”是绝对化表述——在动态 shape 场景下 VM 反而可能比被迫反复重编译的图执行器更快。
Q6. 针对推理服务总耗时公式 T_total = T_compile + T_startup + N × T_infer,以下哪些策略可以降低总时间?
- A.压缩编译流水时间开销← 正确答案
- B.优化算子缩短每次推理← 正确答案
- C.延长启动预热时间窗口
- D.合并小批量以减小次数← 正确答案
答案:ABD。T_compile、T_infer 和 N 都出现在分子上,压缩编译时间、优化单次推理、合并小批量都能降低总耗时。延长启动预热只会增加 T_startup,方向相反。
Q7. 在 TVM 中编译时指定 VM 执行器的正确做法是?
- A.relay.compile 指定 mode
- B.tvm.build 指定 backend
- C.vm.build 指定 executor
- D.relay.build 指定 executor← 正确答案
答案:D。TVM 中统一通过 relay.build 的 executor 参数指定执行器,取值为 “graph“ 或 ”vm”。relay.compile 和 tvm.build 不是标准入口,vm.build 也不存在——VM 只是 relay.build 选中后产出的模块类型。
Q8. 以下哪些部署场景更适合选择 VM 执行器?
- A.模型需要频繁热切换服务← 正确答案
- B.输入序列长度动态变化← 正确答案
- C.批量大小完全恒定不动
- D.模型含条件循环控制流← 正确答案
答案:ABD。热切换靠字节码快速加载,动态序列和控制流靠程序计数器跳转——这三个都是 VM 的舒适区。批量完全恒定的场景通常适合图执行器躺平吃满流水线。
Q9. 图执行器静态内存规划的直接好处是什么?
- A.中间张量复用同一内存← 正确答案
- B.运行时按需零散申请
- C.内存占用一定为零
- D.无需分配任何张量内存
答案:A。静态内存规划在编译期分析张量生命周期,让生命周期不重叠的中间张量共享内存,从而降低峰值占用。B 是动态分配的特征,C 和 D 是绝对化表述,实际推理不可能零内存。
Q10. 静态图与动态图最关键的区分点是什么?
- A.结构与尺寸是否运行可变← 正确答案
- B.模型参数的规模大小
- C.训练数据量的多少
- D.推理精度的要求高低
答案:A。静态图的 shape 和控制流在编译期全部确定,动态图则存在运行时可变的维度或分支。参数量、训练数据量、精度要求都与此无关。