Ascend for PyTorch 中虚拟机 run 的执行过程
背景概述
TorchDynamo 是 PyTorch 2.x 中负责将 Python 字节码转换为计算图的核心组件。其设计目标是在不修改用户代码的前提下,通过符号执行的方式捕获模型计算图,进而交由后端编译器进行优化。理解 TorchDynamo 的虚拟机(VM)执行机制,对于深入掌握 PyTorch 编译优化原理、排查图捕获问题以及进行二次开发都至关重要。
本文将从虚拟机核心执行循环出发,详细剖析典型操作码(opcode)的处理逻辑,并深入探讨控制流、异常处理等复杂场景的实现机制,帮助读者建立对 TorchDynamo 符号执行引擎的系统认知。在昇腾 NPU 的 PyTorch 训练场景中,torch.compile 的图捕获阶段同样由 TorchDynamo 完成符号执行,掌握其虚拟机工作机制,有助于昇腾环境下图捕获问题的定位与编译优化工作的开展。
虚拟机核心执行机制
TorchDynamo 虚拟机的运行核心是逐指令执行字节码,通过分派表(dispatch table)将每条指令映射到对应的处理函数:
self.dispatch_table[inst.opcode](self, inst)
这种设计模式将指令解码与指令执行解耦,使得新增指令支持或修改指令语义都变得非常灵活。整个执行过程本质上是一个"符号版"的 Python 虚拟机——它不执行真实的计算,而是通过符号值(VariableTracker)来追踪数据流和控制流。
栈与局部变量:典型操作码处理逻辑
操作码处理器的通用模式
所有操作码处理器都遵循"符号版 VM 指令语义"的设计原则:
- push/pop/popn:维护操作数栈
self.stack - LOAD_FAST/STORE_FAST/DELETE_FAST:读写符号局部变量
symbolic_locals - LOAD_DEREF/STORE_DEREF:对 cell/freevar 的读写通过
output.side_effects建模,确保可重放性和 guard/codegen 的正确性 - LOAD_GLOBAL/STORE_GLOBAL:读全局变量时进行符号化;写全局变量时通过
side_effects.track_global_existing/store_global记录 - CALL* 系列:最终都会汇聚到
call_function(fn, args, kwargs),由VariableTracker.call_function()根据变量类型决定后续行为:- 可追踪的调用:向
OutputGraph添加计算图节点 - 不可追踪的调用:抛出
Unsupported异常,触发断图(graph break)或回退
- 可追踪的调用:向
LOAD_FAST 详解
LOAD_FAST 对应 CPython 字节码中的"读取局部变量"操作,将指定局部变量的值压入操作数栈顶。在 TorchDynamo 中,压栈的不是原始 Python 值,而是 VariableTracker(符号值)。
执行流程:
-
获取局部变量名:
inst.argval提供局部变量名(如"a"、"b"等) -
可选:记录真实值(用于失败重放/诊断)
如果启用了ExecutionRecorder,会将真实 frame 中的f_locals[name]记录下来。这样在后续 tracing 失败时能更好地复现和打印上下文。此步骤不影响符号执行,仅作为"记账"用途。 -
核心:从 symbolic_locals 取符号变量并压栈
self.symbolic_locals[name]是该局部变量的VariableTracker。调用.unwrap()通常表示"取出可用的值表示",常见场景包括:LazyVariableTracker需要在此"解包"为更直接的 VT(或至少得到一个可放栈使用的 VT)- 拆掉某些 wrapper 变量,得到真正用于后续 opcode 的对象
随后
self.push(...)将其压入 VM 栈顶,供后续指令(如BINARY_OP、CALL)使用。这就是符号 VM 的LOAD_FAST语义:读 local → push 到 stack。 -
变量不存在时的处理
如果symbolic_locals中没有该 key,会进入except KeyError:分支,触发 Dynamo 的unimplemented/graph break 提示(而非 Python 的UnboundLocalError)。 -
continuation(恢复函数)专用处理
断图后会生成 resume 函数,将当时 VM 栈上的值通过参数/locals 的形式传入,常用临时 local 命名如___stack0、___stack1。这些___stack*locals 的特点:- 仅用于"把栈值喂给 resume 函数"这一瞬间
- 一旦
LOAD_FAST ___stackX将其压回栈,就应立即从symbolic_locals中移除,否则会导致:- livevars 分析/后续恢复误认为它是长期局部变量
- 状态泄漏、变量名污染、错误依赖
总结:LOAD_FAST 在 Dynamo VM 中的语义是:从 symbolic_locals 取出对应局部变量的 VariableTracker(必要时 unwrap),压入 stack;如果是 resume/continuation 用的 ___stack* 临时变量,读完立刻删除;如果找不到变量名则触发 Dynamo 的 unimplemented/graph break 提示。
BINARY_ADD 详解
BINARY_ADD 通过工厂函数 stack_op(operator.add) 生成,其实现机制如下:
-
工厂模式构建
stack_op返回一个函数,实现"从 VM 栈里弹出两个操作数 → 做加法 → 把结果压回栈"的通用逻辑。 -
stack_op 工厂实现细节
nargs = len(inspect.signature(fn).parameters):通过反射获取 fn 需要的参数个数fn_var = BuiltinVariable(fn):将 fn 包装为"内建变量/内建函数对象",提供统一的call_function(...)调用协议@functools.wraps(fn):让返回的impl继承 fn 的名字、docstring 等元信息,便于调试def impl(self, inst):真正被解释器调用的实现,通过闭包捕获外层的nargs和fn_varself.popn(nargs):从解释器栈弹出 nargs 个参数fn_var.call_function(self, self.popn(nargs), {}):通过统一调用机制执行操作self.push(...):将结果压回栈顶
STORE_FAST 详解
STORE_FAST 的执行流程相对简洁:
- 取出要写入的局部变量名(如
"x") - 从 VM 操作数栈弹出一个符号值(
VariableTracker) - 为该符号值设置"名字提示"(name hint)
- 存入
symbolic_locals,使后续的LOAD_FAST name能读到该值
控制流与异常处理
控制流与异常处理是 TorchDynamo 虚拟机中最为复杂的部分,需要模拟 CPython 解释器的完整语义。
跳转指令
- jump 系列:
jump()方法将instruction_pointer改为目标指令索引,同时更新 metrics(ir_count)
循环处理
- FOR_ITER:调用 iterator 的
next_variable(),遇到StopIteration时跳转到循环结束位置(Python 3.12+ 还需模拟END_FOR的栈行为)
异常处理机制
异常处理是版本差异最大的部分:
-
Python 3.11+:使用
inst.exn_tab_entry(exception table)定位 handler,需要:- 调整栈深度
- push lasti
- push exception
- 跳转到 handler
-
旧版本:通过
block_stack模拟 CPython ceval 的异常路径 -
RAISE_VARARGS/RERAISE/CLEANUP_THROW:将"raise 的值/类型"转换为符号异常对象,记录到
exn_vt_stack,以 Dynamo 的 observed exception 形式抛出/处理
with 语句处理
- BEFORE_WITH/setup_or_before_with:将 context manager 包装为
WithExitFunctionVariable,压入必要的 block entry - 某些 generic context manager 不支持 graph break,会被登记进
active_generic_context_managers,影响should_compile_partial_graph()判断
版本差异的兼容策略
TorchDynamo 需要兼容 CPython 3.8~3.13 的字节码变化,这是代码中大量分支存在的根本原因:
- Python 3.11 引入
RESUME、PUSH_NULL、新的CALL约定、exception table - Python 3.12/3.13 继续修改指令组合、NULL 与 callable 顺序、
END_FOR行为等
Dynamo 必须在符号层面精确模拟这些细节,否则会导致 resume、异常、with、调用约定不一致,进而产生错误恢复或错误代码生成。
OutputGraph:计算图的核心容器
OutputGraph 是 TorchDynamo 在分析/翻译 Python frame(字节码)时,承载"本次捕获到的计算图及其上下文"的核心容器。可以将其理解为一次 torch.compile/Dynamo tracing 过程中,负责收集 fx.Graph、维护 FakeTensor/形状环境、记录副作用与 guards,并最终将图交给后端 compiler_fn 编译的"总控对象"。
关键设计:一个 OutputGraph 对应一个正在处理的 frame;当用户代码调用其他函数并发生 inlining 时,会创建新的 translator(如 InliningInstructionTranslator),但它们仍然写入同一个 root translator 的 OutputGraph。因此,OutputGraph 是跨 inlining 的共享图状态。
OutputGraph 的使用位置
-
convert_frame.py 中的
_compile方法:- 定义
output: Optional[OutputGraph] = None _compile_inner方法使用assert output.guards is not None和output.is_empty_graph()transform方法使用output = tracer.output、instructions[:] = output.output_instructions、code_options.update(output.code_options)
- 定义
-
symbolic_convert 模块:
- 在
InstructionTranslator类的初始化中构建并传递给InstructionTranslatorBase类 - 在
InstructionTranslatorBase的run方法中调用self.output.push_tx(self)、self.output.pop_tx()、self.output.cleanup()
- 在
执行流程关系
tracer 是 InstructionTranslator 类实例,接收旧的 instructions、code 等信息,通过 tracer.run() 执行转换。转换后,tracer.output 就是生成的 fx_graph,tracer.output.output_instructions 构成新的字节码。
InstructionTranslator 类在初始化过程中会给父类 InstructionTranslatorBase 构造 OutputGraph() 对象。InstructionTranslatorBase 维护一个 VM,其 run 过程逐指令虚拟执行,执行中间接使用 OutputGraph 从而产生 FX graph 等内容。
总结
TorchDynamo 的 InstructionTranslatorBase 是 Dynamo tracing 的"执行引擎/字节码解释器框架",提供:
- 执行循环
- 投机与断图机制
- 异常与控制流模拟
- 大量 opcode handler
子类负责将真实 frame 的输入状态符号化,并提供"何时断图/如何恢复"的策略实现。理解这一核心机制,是深入掌握 PyTorch 2.x 编译优化技术栈的关键一步。在昇腾 NPU 的 PyTorch 训练场景中,torch.compile 的图捕获阶段同样依赖该虚拟机机制完成符号执行,深入理解其执行流程,是昇腾侧开展图捕获问题定位与编译优化工作的基础。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)