第12章 TorchDynamo:CPython 帧拦截与图捕获
“TorchDynamo is a Python compiler that runs at runtime, transforming hot frames into optimized FX graphs while letting the rest of the program run normally.”
——
torch/_dynamo/eval_frame.py顶部注释
本章要点
- Dynamo 通过 PEP 523 帧评估 API 在 CPython 解释器层拦截每个 Python 函数调用:用
_PyInterpreterState.eval_frame钩子替换默认的_PyEval_EvalFrameDefault - 拦截后做的事:解析帧的字节码,用
InstructionTranslator一条条字节码地”符号执行”,把 PyTorch 算子调用记录到 FX Graph - Guards 是”输入假设”:trace 时假定输入是
torch.float32 + cuda + shape=[B, 768],下次调用时检查 guards,命中就跑编译产物,不命中就重新 trace - Graph Break 是最重要的失败模式:遇到 unsupported Python 构造(如某些 if 判断、外部库调用)时 Dynamo 退回 eager,把图切成两段
- FX Graph 输出后送给 backend:默认 backend 是 Inductor,用户也可以传
aot_eager、cudagraphs等做实验 - 理解 Dynamo 是理解 torch.compile 一半价值:编译失败、性能不如预期、graph break 多 —— 90% 问题源于 Dynamo 阶段
12.1 一个被低估的工程奇迹
@torch.compile 一行装饰器让模型加速 1.5-3x,但它没有改任何用户代码。这是怎么做到的?
答案是 PEP 523:CPython 3.6 引入的”帧评估 API”,允许 C 扩展替换解释器的核心函数 _PyEval_EvalFrameDefault。Dynamo 利用这个钩子,在每个函数被调用时先把它的字节码拿出来分析一遍,能编译就编译、不能就让默认解释器跑。
graph LR
Py[Python 解释器] --> H{eval_frame 钩子<br/>有没有装?}
H -->|否, 默认| Def[_PyEval_EvalFrameDefault<br/>正常解释执行]
H -->|是, Dynamo 装了| Dy[Dynamo callback]
Dy --> Cache{这个 frame 编译过吗?}
Cache -->|是, guards 命中| Run[直接跑编译产物]
Cache -->|否或不命中| Comp[trace + 编译]
Comp --> Run
style Dy fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
源码集中在 torch/_dynamo/,v2.11 实测约 100000 行 Python + 几千行 C(v2.0 起该 namespace 一直在快速增长)。本章拆它的核心机制。
12.2 入口:set_eval_frame 装钩子
打开 torch/_dynamo/eval_frame.py:用户调 torch.compile(fn) 时,Dynamo 通过一个 C 扩展(torch/csrc/dynamo/)调 CPython 的 _PyInterpreterState_SetEvalFrameFunc,把 _PyEval_EvalFrameDefault 替换成 Dynamo 自己的 custom_eval_frame_shim。从此所有 Python 函数调用都先经过 Dynamo。
但 Dynamo 不会编译所有函数 —— 它只对包含 PyTorch 算子的 hot frame 感兴趣。has_tensor_in_frame(convert_frame.py:377)扫描帧的 locals,发现 Tensor 才进入编译路径,否则直接 fall back 到默认解释。
这种”hooks all but only acts on tensor frames”是 Dynamo 与现有 Python 生态共存的关键 —— 不影响其他库的运行,只对 PyTorch 代码生效。
12.3 编译入口:convert_frame
进入编译路径后,convert_frame.py:catch_errors_wrapper 是统一入口,里面调 _compile。_compile 干几件事:
- 从
frame.f_code拿到字节码 + locals + globals - 检查编译缓存(同 code object + 相同 guards 命中 → 复用)
- 创建
InstructionTranslator开始 trace - trace 完拿到 OutputGraph + Guards
- 把 OutputGraph 喂给 backend(默认 Inductor)拿到编译产物
- 返回新的 code object(
_GUARDED_CODE)让 CPython 后续调用直接跑
整套流程发生在用户 f(x) 第一次调用时,所以第一次 compile 慢(几秒到几十秒),第二次起命中缓存只要几微秒。
12.4 InstructionTranslator:符号执行字节码
核心类在 symbolic_convert.py:1236。它继承 _InstructionTranslator,本质是一个字节码解释器 —— 但它不真的执行算术,而是把每个 PyTorch 算子记录到 FX Graph,普通 Python 操作正常算(直接在 trace 时算 Python 的 if/for)。
字节码层面的关键 ops:
| Bytecode | InstructionTranslator 行为 |
|---|---|
LOAD_FAST | 从 locals 取出对应 VariableTracker(包装 Python 对象的符号) |
CALL_FUNCTION | 如果是 PyTorch 算子,往 FX Graph 加 node;如果是普通函数,inline trace 进去 |
BINARY_OP | 同上 —— Tensor + Tensor 加节点,int + int 直接算 |
RETURN_VALUE | trace 结束,返回 OutputGraph |
每个 Python 对象在 trace 时被包装成 VariableTracker:TensorVariable(Tensor)、ConstantVariable(int/str/bool)、BuiltinVariable(内置函数)等。这套包装让 Dynamo 能区分”这个值要进 FX Graph”和”这个值在 trace 时直接消费”。
举个例子:
@torch.compile
def f(x, n):
y = x * 2
for i in range(n):
y = y + i
return y
trace 时 Dynamo 看到:
x * 2→ 加一个 mul 节点到 FX Graphrange(n)→ n 是 Python int,trace 时直接展开循环- 循环里的
y + i→ 加 add 节点(每次循环加一个)
如果 n=3,最终 FX Graph 是 mul(x, 2) → add(_, 0) → add(_, 1) → add(_, 2) → return。循环被完全 unroll。
12.5 Guards:输入假设的运行时校验
trace 出来的 FX Graph 只对符合 trace 假设的输入正确。比如上例 trace 时 n=3,graph 里硬编码了 3 次 add;如果下次 n=5,graph 就错了。
Guards 是 Dynamo 记录的”输入假设”列表。guards.py 里几十种 GuardBuilder(TENSOR_MATCH、SHAPE_ENV、OBJECT_MISMATCH、CONSTANT_MATCH 等)对应不同维度的假设:
# 假设的形式 (实际是 C 代码生成)
def check_guards(x, n):
assert isinstance(x, torch.Tensor)
assert x.dtype == torch.float32
assert x.device == device('cuda:0')
assert x.size() == [B, 768] # B 可能是符号 (动态 shape)
assert n == 3 # n 是 ConstantSource
return True
每次调用编译过的函数:先跑 guards check,全过就跑编译产物,否则重新 trace(产生新的 graph + 新的 guards,存到 cache)。
这套机制让 torch.compile 既能享受静态图性能、又能处理”shape 偶尔变化”的动态场景。代价是 cache 可能膨胀(极端 dynamic shape 下每个 batch 都重新 trace),所以 Dynamo 有 dynamic=True flag 提示”shape 是 symbolic”,避免反复重 trace。
12.6 Graph Break:trace 失败时的退路
不是所有 Python 代码都能 trace。Dynamo 遇到下面情况会 graph break:
- 调用了 Dynamo 不认识的 C 扩展(如某些第三方库)
- 控制流依赖 tensor 的具体值(
if x > 0—— 要等运行时才知道) - print / open / 其他 side effect 操作
- 某些 Python 黑魔法(动态生成函数等)
graph break 不是 fatal —— Dynamo 把当前 trace 段封成一个 graph、让 break 处的代码用 eager 跑、break 之后继续 trace 第二段。结果是一个函数被切成多个 graph + 中间 eager 代码段。
@torch.compile
def f(x):
y = x * 2 # graph 1 开始
if y.sum() > 0: # graph break! (依赖 tensor 值)
z = y.relu() # graph 2 (在 if 分支里)
else:
z = y.tanh() # graph 3 (在 else 分支里)
return z + 1 # graph 4
每个 graph 各自编译。优化空间还在但不如”一整个 graph”激进 —— graph 之间没法做跨段融合、CUDA Graph 也用不了。
减少 graph break 是 torch.compile 调优的核心工作。可以用环境变量 TORCH_LOGS=graph_breaks 看哪些行触发了 break。
12.7 OutputGraph:trace 的产物
output_graph.py:583 的 OutputGraph 类持有 trace 阶段构建的 FX Graph。它的核心字段:
graph: torch.fx.Graph—— FX 节点列表guards: set[Guard]—— 收集到的 guardsside_effects—— 副作用列表(用于安全 replay)output_instructions—— 编译完成后回写到 frame 的字节码
trace 结束后 OutputGraph 调用 compiler_fn(graph_module, example_inputs) 把 FX Graph 交给 backend。default backend 是 inductor.compile_fx_inner(第 14 章会展开)。其他常见 backend:
aot_eager:只做 AOTAutograd 不上 Inductor,主要用于调试cudagraphs:直接 CUDA Graph 编译,跳过 Inductor 优化eager:不做编译只 trace(用于验证 trace 正确性)
backend 是可插拔的,第三方可以注册自己的:
@torch._dynamo.register_backend
def my_backend(gm, example_inputs):
return gm.forward # 返回 callable
国产 AI 芯片厂商接入 torch.compile 时,往往在这层注入自家编译器。第 14 章会拆 Inductor 自己怎么实现这个 backend。
12.8 一段实际 trace 的剖析
考虑这段代码:
@torch.compile
def add_relu(a, b):
c = a + b
return c.relu()
第一次调用 add_relu(x, y)(假设都是 cuda fp32 [4, 4])时:
- Dynamo 拦截 frame,看到有 Tensor → 进入
_compile - cache miss,开始 trace
- InstructionTranslator 解析字节码:
LOAD_FAST a、LOAD_FAST b、BINARY_ADD、STORE_FAST c、LOAD_FAST c、LOAD_METHOD relu、CALL_METHOD、RETURN_VALUE - trace 时往 FX Graph 加 2 个节点:
add(a, b)→relu(_) - 收集 guards:
a是 fp32+cuda+[4,4]、b是 fp32+cuda+[4,4] - 把 FX Graph + example_inputs 送给 Inductor
- Inductor 编译成 Triton kernel,返回 callable
- Dynamo 把 callable 缓存进
_GUARDED_CODE,下次直接跳过 trace
第二次调用 add_relu(x2, y2)(同样 dtype/device/shape):
- Dynamo 拦截 frame
- 检查 guards:x2 / y2 也是 fp32+cuda+[4,4] → 通过
- 直接跑缓存的 Triton kernel
- 完全跳过 dispatcher / autograd / Python 解释器
第二次起的开销几乎是 0 —— 这就是 torch.compile 的核心收益。
12.8.5 VariableBuilder:Python 对象 → VariableTracker
进入 trace 前要把 frame 的 locals / globals 里每个 Python 对象包装成 VariableTracker。torch/_dynamo/variables/builder.py:464 的 VariableBuilder 是这个包装器。
它按对象类型分流:
| Python 对象 | 包装后类型 | 处理 |
|---|---|---|
torch.Tensor | TensorVariable | 记录 shape/dtype/device 加 guard、加 graph input |
int / float | ConstantVariable | trace 时直接当常量参与 |
nn.Module | NNModuleVariable | 把 module 整个”内联”进 trace,递归 trace 它的 forward |
list / tuple / dict | ListVariable / DictVariable | 容器递归包装每个元素 |
torch.dtype | TorchInGraphFunctionVariable 或 ConstantVariable | dtype 是 trace 时常量 |
| 用户函数 | UserFunctionVariable | 调用时 inline trace 进去 |
| 不认识的对象 | UnsupportedVariable | 触发 graph break |
每种 VariableTracker 实现 call_function / call_method / var_getattr 等方法 —— 描述”这个值上做某操作时 trace 怎么处理”。比如 TensorVariable.call_method('add', other) 会在 FX Graph 里加一个 add 节点。
VariableTracker 同时记录 Source:这个值是从哪里来的(如 LocalSource('x')、AttrSource(LocalSource('self'), 'weight'))。Source 用于生成 Guard —— 反向追溯出”如果下次调用,这个值在什么位置、应该是什么”。
12.8.6 GuardBuilder:把 trace 假设编译成 C++ check
torch/_dynamo/guards.py:1013 的 GuardBuilder 把 trace 阶段累积的 Source + 类型假设转换成 可执行的 guard 检查代码。
每个 Source 对应几条 guard:
# trace 时 x 是 cuda fp32 [B, 768] tensor (B 是符号)
# 生成的 guards (伪代码):
guard_1 = TENSOR_MATCH(x, dtype=fp32, device='cuda:0', requires_grad=False)
guard_2 = SHAPE_ENV(x.size() = [s0, 768]) # s0 是 SymInt 符号
guard_3 = TYPE_MATCH(type(x) == Tensor) # 防 subclass 不一致
GuardBuilder 把这些 guards 编译成 一个 C++ 函数(不是 Python!),下次调用时直接 C++ check —— 比 Python 检查快 10x+。
// 编译生成的 check (伪代码)
bool check(PyObject* x) {
if (!THPVariable_Check(x)) return false;
auto t = THPVariable_Unpack(x);
if (t.dtype() != fp32) return false;
if (t.device() != cuda_0) return false;
if (t.size(1) != 768) return false;
return true;
}
C++ guard check 只要几百纳秒,是为什么 Dynamo 第二次起调用几乎零开销的原因。
guards 也有失败处理:guard 失败时 Dynamo 不直接 fallback eager,而是重新 trace 一份(产生新的 graph + 新的 guards),缓存里就有 N 个候选 graph、运行时按 guard 命中选一个。
12.8.7 cache 的层次结构
Dynamo 不是”每个函数一个 graph”,而是每个函数 N 个 graph(按不同 guards 命中):
function `f`'s code object → guarded code 列表:
[0]: graph_A + guards_A ← 第一次 trace 出来的
[1]: graph_B + guards_B ← shape 变了重新 trace
[2]: graph_C + guards_C ← 又一个新 shape
...
每次调用 f(x),Dynamo 顺序检查 guards_A → guards_B → ...,第一个 pass 的就跑对应 graph。全部不命中就再 trace 一份加到末尾。
torch/_dynamo/cache_size.py 控制 cache 大小:
cache_size_limit默认 8:单个函数最多缓存 8 个 graph- 超过后开始驱逐最早的 graph
accumulated_cache_size_limit:所有函数的总 cache 上限
torch._dynamo.config.cache_size_limit = 64 可以放宽,但太大会让”同一个函数有几十个版本”消耗内存。如果你看到 TORCH_LOGS=recompiles 频繁打印 “exceeded cache size limit”,意味着代码有非确定性(每次 shape 都不一样)—— 应该用 mark_dynamic 让一个 graph 处理多 shape,而非缓存几十份。
12.8.8 OutputGraph 的”compile + 字节码回写”
trace 完成后,OutputGraph 不只是返回 FX Graph 给 backend —— 它还要生成新的字节码让 CPython 在后续调用时直接跑编译产物。
output_graph.py:1605 的 compile_subgraph:
- 把 trace 出来的 FX Graph 包装成
torch.fx.GraphModule - 调
compiler_fn(gm, example_inputs)(默认是 Inductor)拿到 callable - 用
install_global把 callable 注册成 frame 的全局变量(如__compiled_fn_0) - 生成”调用 callable 的字节码序列”,存到
self.output_instructions
bytecode_transformation.py:1593 的 transform_code_object 把 output_instructions 拼装成新的 code object,PyTorch 在 eval_frame 钩子里返回这个新 code object 让 CPython 执行 —— 从此用户的函数被透明替换。
简化后的回写字节码大致:
LOAD_GLOBAL __compiled_fn_0 # 取出 Inductor 编译好的 callable
LOAD_FAST x # 取参数 x
LOAD_FAST y # 取参数 y
CALL_FUNCTION 2 # 调用编译产物
RETURN_VALUE # 返回
加上 guards 校验 + 不命中时 fallback 到 graph break / 重新 trace 的逻辑,最终回写字节码可能几十条指令。但用户原始函数体被完全替换 —— CPython 再也不解释执行原始 Python 代码,直接跳到编译好的 binary。
这是”@torch.compile 装饰器一行就能加速”的最后一块拼图:guards + bytecode rewrite 一起让”判断 + 跳到编译产物”成为新 frame 的全部工作。
12.8.9 PEP 523 frame eval 钩子的精确机制
§12.2 提到 Dynamo 通过 PEP 523 拦截 CPython。具体看 torch/csrc/dynamo/eval_frame.c:
// :218 安装钩子
_PyInterpreterState_SetEvalFrameFunc(
tstate->interp,
custom_eval_frame_shim // Dynamo 自家的 frame evaluator
);
// :227 卸载钩子
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, previous_eval_frame);
机制:CPython 解释器在每个函数调用前查 _PyInterpreterState->eval_frame,调它评估帧。默认是 _PyEval_EvalFrameDefault(标准解释器)。Dynamo 用 _PyInterpreterState_SetEvalFrameFunc 把这个指针换成自己的 custom_eval_frame_shim。
之后每个 Python 函数调用都先经过 Dynamo。shim 内部判断:
- frame 来自系统库(如
print、json.loads)→ 调 default eval(不编译) - frame 含 tensor + 是用户代码 → 进 trace + compile 路径
- frame 已编译过 + guards 命中 → 直接跑编译产物
_PyEval_RequestCodeExtraIndex(:758)申请一个 code object 的”额外字段”,Dynamo 用它存”这个 frame 的编译缓存”。CPython 看到 code object 时通过 extra_index 取回缓存 —— 这是 Dynamo 实现”per-code-object 缓存”的底层。
整套机制让 Dynamo 无需修改 CPython 源码就能拦截字节码执行。PEP 523 是 2016 年加入 CPython 的扩展点,Dynamo 是它最大用户。理解这条机制让你看 eval_frame.c 几百行 C 代码不困惑。
12.8.10 InstructionTranslator 的核心循环
symbolic_convert.py:1236 的 InstructionTranslatorBase 是字节码解释器。核心是 step_until_not_supported 循环:
def step_until_not_supported(self):
while self.step():
pass
def step(self):
inst = self.next_instruction()
if inst.opname not in self.dispatch_table:
# 遇到不认识的字节码 → graph break
self.error_on_graph_break(...)
handler = self.dispatch_table[inst.opname]
handler(inst)
return self.has_next_instruction()
每条字节码对应一个 handler 方法,所有 handler 在 dispatch_table 里注册。实战 dispatch_table 有 200+ 条字节码(CPython 全部 opcode 加 PyTorch 自家添加的几十条)。
部分 handler 例子:
| Opcode | Handler 行为 |
|---|---|
LOAD_FAST | 从 local symtable 取出 VariableTracker |
STORE_FAST | 把 VariableTracker 存到 local symtable |
BINARY_ADD | 调 BinaryAdd.create(left, right),可能加 fx node |
CALL_FUNCTION | 找出 callable 的 VariableTracker,inline trace 进去(如果是用户函数)或加 fx node(如果是 PyTorch op) |
LOAD_ATTR | 处理对象属性访问,可能触发 nn.Module 的 _modules / _parameters dict 查找 |
RETURN_VALUE | 终止 trace,传 OutputGraph 给 backend |
IF_FALSE | 控制流分支:如果条件依赖 tensor 值 → graph break |
整套字节码 dispatch 让 Dynamo 像”模拟 CPython 解释器”一样运行用户代码,不真做计算(tensor 操作转 fx node、Python 操作模拟运行)。这是 trace 阶段的核心工程实现。
12.8.11 GuardManager:guards 的高效组织
§12.5 + §12.8.6 讲了 guards 的概念与编译。具体管理 guards 的是 GuardManagerWrapper(guards.py:265):
graph TB
Code[code object → guard 列表]
Code --> Mgr[GuardManager]
Mgr --> M1[guards by source]
M1 --> S1[LocalSource: x → TENSOR_MATCH]
M1 --> S2[LocalSource: y → TENSOR_MATCH]
M1 --> S3[GlobalSource: model → TYPE_MATCH]
M1 --> S4[AttrSource: model.weight → TENSOR_MATCH]
Mgr --> CC[编译成 C++ check 函数]
CC --> Run[运行时调用]
GuardManager 按 source 组织 guards(每个 Python 表达式一组)—— 让 check 时能短路:第一个 source 的 guard 失败就立即返回 false,不查后面的。这种”分组检查”让 guard 平均检查时间 < 200ns。
guards 还分优先级:常变化的 guards(如 tensor shape)放前面,不常变化的(如 device / dtype)放后面。新调用进来时优先检查易失败的,让”重 trace”决策更快做出。
12.8.12 Symbolic shape:SymInt / SymFloat 的传递
第 6 章 §6.6.2 提过 SymInt 在 ATen layer 的存在。Dynamo 这层处理符号 shape 的具体方式:
trace 时如果 input.shape[0] 被标记为 dynamic,Dynamo 创建 SymInt(symbol="s0") —— 表示一个未知值。后续每个对这个 dim 的算子调用,输出的 shape 表达式自动变成符号:
input.shape = (s0, 768)
hidden = input @ weight # hidden.shape = (s0, 768) — s0 仍是符号
norm = hidden / hidden.norm(dim=1) # norm.shape = (s0, 768)
torch.fx.experimental.symbolic_shapes.ShapeEnv 维护所有符号变量的关系。每次新引入的符号 + 假设(如 s0 > 0、s0 % 8 == 0)都被记录。fx graph 生成时所有 shape 表达式是 SymInt,Inductor 拿到后能用它做 dynamic codegen(第 14 章 §14.8.7)。
如果 trace 中有”shape 决定控制流”的代码(如 if x.shape[0] > 100),Dynamo 会创建一个 shape guard:s0 > 100 必须为真才能复用此编译。下次调用 s0 < 100 → guard 失败 → 重 trace 走 else 分支、新 graph 缓存。
12.8.13 SideEffects:副作用的精确跟踪
side_effects.py:89 的 SideEffects 类管理 trace 期间的”副作用列表”:
- 全局变量赋值(
global_var = ...) - 类属性 mutation(
self.cache = ...) - 容器修改(
my_list.append(...)) - print / log 等可观察行为
trace 完成后这些副作用要在编译产物里正确 replay —— 否则用户原本期望的修改没生效。SideEffects 把它们记录下来,编译产物在合适时机调用。
例子:
@torch.compile
def f(x):
x.foo = "bar" # side effect: 设置属性
return x + 1
Dynamo trace 出 fx graph 只是 x + 1,但额外记录”设置 x.foo = bar”这个 side effect。生成的字节码在 fx graph 调用前后插入这个 side effect。用户看到的语义与原始代码完全一致。
这套机制让 Dynamo 能 trace 含副作用的代码,不仅仅纯函数。是 Dynamo 比 JAX trace 更强大的关键 —— JAX 要求纯函数,PyTorch 允许副作用。
12.8.14 Inlining 决策:哪些函数被 trace 进去
CALL_FUNCTION handler 决定调用一个函数时是 inline trace 还是 当不透明 op:
- 用户写的 Python 函数 → inline(Dynamo 进入函数继续 trace)
- PyTorch 内置算子(如
torch.add) → 加 fx node 不 inline - C 扩展函数(如
numpy.array) → 触发 graph break @torch._dynamo.allow_in_graph装饰的函数(第 22 章 §22.6.8)→ 当不透明 op 加进 graph@torch._dynamo.disable装饰的函数 → graph break
inline trace 让用户代码的 helper 函数也被编译。但inline 太深会让 trace 时间爆炸(编译大型 model 时常见)。Dynamo 有 inline_inbuilt_nn_modules 等 flag 控制 inline 策略。
实战:Llama 训练里 transformer_block.forward 被 inline → attention.forward 被 inline → attention.QKV_projection 被 inline → … 整个 70B 模型 forward 被展开成一个超大 fx graph。这是为什么 70B 编译要几十秒。
12.8.15 Dynamo × nn.Module 的协作
第 9 章讲了 nn.Module 的 _parameters / _modules / getattr 兜底。Dynamo trace 到 model.linear.weight 时怎么处理?
机制:
- trace 看到
LOAD_ATTR linear - handler 调
model.__getattr__('linear')→ 返回Linear子模块 - Dynamo 把它包装成
NNModuleVariable(第 12 章 §12.8.5) - 继续 trace
LOAD_ATTR weight - 第 9 章 §9.4 的
__getattr__从_parameters['weight']取出 → 返回 Tensor - Dynamo 包装成
TensorVariable+ 加 guard(确保下次调用时 weight 仍是同样 dtype/shape/device)
这种”对 nn.Module 特殊处理”让 Dynamo 能正确 trace 任意 PyTorch model。NNModuleVariable 内部实现了对 _modules / _parameters / _buffers 的特殊知识,访问时自动包装下层 tensor。
12.8.16 cache invalidation:什么时候 cache 失效
§12.8.7 讲了 cache 大小限制。具体什么操作让 cache 失效(强制重 trace)?
- 修改 model 的参数(如
model.linear.weight = nn.Parameter(...)) → 该 model 的所有缓存失效 - 改 hyperparameter(如改
dropout_rate)→ 涉及该值的 graph 失效 - 环境变量变化(如改
_inductor.config.max_autotune)→ 触发全局 cache 失效 - PyTorch 版本升级 → 全局失效
torch._dynamo.reset() 手动清掉所有 cache(debug 时常用)。生产代码里频繁触发 cache 失效会让训练吞吐崩盘 —— 监控 TORCH_LOGS=recompiles 输出能发现哪段代码频繁重 trace。
12.8.17 Dynamo trace 的性能开销
具体数字(H100,trace 一段含 10 个 ATen op 的函数):
| 阶段 | 时长 |
| Dynamo trace + guards | 20 ms |
| AOTAutograd trace | 50 ms |
| Inductor lowering | 30 ms |
| Triton 编译 (1 kernel) | 1000 ms|
| 总编译时间 |~1100ms |
单次 trace 几十 ms 不算长,但对每个未见过 shape 都要 trace 一次。生产代码里 cache 命中率决定整体性能 —— 命中时调用编译产物 < 100ns,不命中时 trace + compile 几秒。
mark_dynamic 让一个 graph 处理多 shape,避免每个 shape 都 trace。fullgraph=True 强制不允许 graph break,逼用户写 trace-friendly 代码。这些 flag 是优化 cache 命中率的关键。
12.8.18 graph break 的常见场景与避免
实战导致 graph break 的代码模式:
| 模式 | 例子 | 避免方法 |
|---|---|---|
| 依赖 tensor 值的 if | if x.sum() > 0: ... | 重构成 torch.where |
| Python list 操作 | lst.append(x) | 用预分配 tensor |
| dict 动态 key | d[x.item()] = ... | 避免 .item() / 用 fixed key |
| numpy 操作 | np.array(x) | 用 torch op 替代 |
| print / log | print(x) | 移到 trace 之外 |
用户调 .item() / .numpy() | 把 tensor 转 Python 值 | 避免在 trace 内调 |
@torch.compile(fullgraph=True) 让以上场景直接报错而非默默 break,能强制用户写出 break-free 代码。生产代码追求性能时建议 fullgraph。
12.8.19 Dynamo × DDP / FSDP
第 17 章 §17.8.15 + 第 18 章 §18.6.17 讲过 DDP / FSDP 与 compile 的协作。具体到 Dynamo 这层:
- DDP wrap 后的 model:Dynamo trace 时把 DDP wrapper 当作普通 nn.Module,递归进 inner module
- FSDP-2 wrap 后的 model:Dynamo trace 时看到 DTensor,按特殊路径处理(每个 op 检查 placement、自动加 collective)
trace 完后 fx graph 含 collective op(AllReduce / AllGather 等),这些 op 走 functional collectives(第 16 章 §16.7.9)让 Inductor 能 fuse compute + comm。整套机制让分布式训练享受 compile 加速,不需要用户做特殊配置。
历史上 FSDP-1 trace 频繁 graph break(FlatParameter 的复杂 view 让 Dynamo 困惑),FSDP-2 重新设计让 trace 流畅。这是 v2.4+ 推荐 FSDP-2 的核心理由之一。
12.8.20 ContinuationFrame:graph break 后的恢复执行
graph break 不是简单”停下”,Dynamo 要让函数继续从 break 处往下跑。这通过 continuation frame 实现。
机制:
- trace 跑到 break 处停下,已 trace 的部分编成 graph_A
- Dynamo 生成新字节码:调 graph_A → 用 eager 跑 break 那一行 → 创建一个 continuation function 接管剩余字节码
- continuation function 是个新的函数,包含 break 之后的所有字节码
- CPython 调 continuation function 时再次进 Dynamo(PEP 523 hook)→ 可能再 trace 一段、又 break 一次、又生成新 continuation …
最终一个含 N 个 graph break 的函数被切成 N+1 个编译产物 + N 段 eager 代码 + N 个 continuation。整套递归直到所有代码都被 trace 或 eager 跑过。
continue_execution_at_addr 是这套机制的核心 C 函数(eval_frame.c)。理解它让你看到”含 graph break 的 compiled function”性能不如纯 graph —— 多次 trace + 多次 dispatch 累积开销。这是 fullgraph=True 强制无 break 的工程理由。
12.8.21 Dynamo cache 失效的恢复路径
cache size limit 触发后,Dynamo 不会”删旧 cache”,而是 fallback 到 eager 执行该函数。逻辑:
caching 8 个 graph → 第 9 次调用 shape 不命中任何 graph
→ 触发 cache size warning
→ Dynamo 输出: "exceeded cache size limit, function not compiled, fallback to eager"
→ 后续这个 frame 的所有调用都走 eager (skip Dynamo)
所以”看到 cache size warning” 等于 该函数不再享受 compile 加速。torch._dynamo.config.cache_size_limit = 32(增大上限)或 torch._dynamo.reset()(清掉 cache 重新编)是解法。
实战监控:长跑训练 TORCH_LOGS=recompiles 持续输出说明有问题,要么开 dynamic shape 让一个 graph 处理多 shape、要么排查为什么每次输入都看起来不同(如不必要的 dtype 变化)。
12.8.22 Dynamo 错误诊断 logs 完整列表
TORCH_LOGS= 支持多个标签同时开(逗号分隔):
| 标签 | 输出内容 |
|---|---|
dynamo | Dynamo 整体流程(trace 开始 / 结束、cache 命中 / 失败) |
recompiles | 每次重 trace 的原因(哪个 guard 失败、shape 变了什么) |
graph_breaks | 每次 graph break 的位置 + 原因(哪条字节码不支持) |
bytecode | 详细字节码 trace 过程(每条 opcode 的处理) |
output_code | Inductor 生成的 Triton 代码(与 §14.9.8 联动) |
aot_graphs | AOTAutograd 输出的 graph |
guards | 每个 guard 的具体内容 |
verbose | 全部高级日志,info 量爆炸 |
调试 compile 问题的标准三件套:TORCH_LOGS=dynamo,graph_breaks,recompiles。看完输出大多能定位是哪个层的问题。
12.8.23 Dynamo 历史:从 LazyTensor 到 PEP 523
PyTorch 在到达 Dynamo 之前试过几条 trace 路径:
torch.jit.trace(v1.0):用 example input 跑一遍记录算子序列。问题:不能处理控制流(每次 example input 跑出来的可能不一样)torch.jit.script(v1.0):用类型注解 + 自家 IR 静态 trace。问题:用户得改代码 / 加注解,迁移成本高- LazyTensor(实验性):每个算子调用先记录、用到时再触发计算。问题:性能差、调试难
- TorchDynamo(v2.0):PEP 523 字节码拦截 + Just-In-Time trace + cache。当前赢家
理解这条历史让你看到 PyTorch 团队在 trace 路径上的多次尝试。Dynamo 是几年探索后找到的最优解 —— 既不要求用户改代码(vs torchscript)、又能处理控制流(vs torch.jit.trace)、又有合理性能(vs LazyTensor)。
12.8.24 一个具体 trace 过程的逐字节码追踪
最深入的方式:开 TORCH_LOGS=bytecode 看 trace 一个简单函数:
@torch.compile
def f(x, y):
z = x + y
return z * 2
输出(精简):
[bytecode] 0 LOAD_FAST x → push TensorVariable(x)
[bytecode] 2 LOAD_FAST y → push TensorVariable(y)
[bytecode] 4 BINARY_OP + → pop 2 个, 加 fx node "add", push TensorVariable(z)
[bytecode] 6 STORE_FAST z → 把栈顶存到 local symtable[z]
[bytecode] 8 LOAD_FAST z → push TensorVariable(z)
[bytecode] 10 LOAD_CONST 2 → push ConstantVariable(2)
[bytecode] 12 BINARY_OP * → 加 fx node "mul", push TensorVariable(out)
[bytecode] 14 RETURN_VALUE → 终止 trace, OutputGraph 含 add + mul 两节点
实际输出更详细(含 guards 累积、每个 VariableTracker 的 source 等),但核心流程就这样。开 bytecode log 学一个函数的 trace 过程,是最直观理解 InstructionTranslator 工作方式的方法。
12.8.25 v2.x Dynamo 的演进
时间线:
- v1.13 (2022 末):TorchDynamo 实验性引入
- v2.0 (2023-03):torch.compile 公开发布,Dynamo 成为默认 trace 方式
- v2.2 (2024-01):dynamic shape 完整支持
- v2.4 (2024-07):与 FSDP-2 / DTensor / export 深度集成
- v2.6 (2025-01):Compiled Autograd 让反向也被 Dynamo trace
- v2.11 (2026):稳定 + 性能持续优化
理解这条演进让你知道哪些功能是 v2.x 哪个版本引入的、能预判未来。Dynamo 是 PyTorch 团队近 5 年最大的工程投入,仍在快速演进中。
12.8.26 ConvertFrame:把帧转成 GuardedCode 的总调度
torch/_dynamo/convert_frame.py 是 Dynamo 的总调度入口。custom_eval_frame_shim 决定要 trace 时,最终调到 _compile():
graph TB
Shim[custom_eval_frame_shim<br/>C 层] --> CF[convert_frame.py<br/>_compile]
CF --> IT[InstructionTranslator<br/>字节码 trace]
IT --> OG[OutputGraph<br/>fx graph + side effects]
OG --> BK[backend<br/>aot_autograd / inductor]
BK --> CC[CompiledFn<br/>编译产物]
CF --> GG[GuardManager<br/>组装 guards]
GG --> GC[GuardedCode<br/>guards + bytecode + CompiledFn]
GC --> Cache[CacheEntry<br/>挂到 code object 的 extra slot]
style CF fill:#fef3c7,stroke:#f59e0b
style GC fill:#dbeafe,stroke:#3b82f6
关键步骤(精简版):
- 入口校验:跳过库代码、被
@disable装饰的函数、recursion 太深的 frame - InstructionTranslator 实例化:把 frame 的
co_code、f_locals、f_globals包成 trace 上下文 - 跑 trace 主循环:
step_until_not_supported直到 RETURN_VALUE 或 graph break - OutputGraph.compile_subgraph:把累积的 fx node 整理成可调用的 fx Graph
- 调 backend:默认
aot_autograd_simplified→inductor.compile_fx - 重写 frame 的 bytecode:原始字节码替换为”check guard → call CompiledFn → return”
- 包成 GuardedCode:guards + 新 bytecode + 编译产物绑在一起,存进 cache
这个 6 步流程是 Dynamo 全部价值的实现。卡在哪一步可以从 TORCH_LOGS=dynamo 的输出看出来:每一步打印一条耗时记录。debug compile 慢时这是第一手信息。
12.8.27 OutputGraph:fx graph + 副作用打包器
torch/_dynamo/output_graph.py 的 OutputGraph 类负责”把 trace 期间发生的所有事打包成可消费的产物”。它管理:
- fx Graph 节点:每条记录的算子调用
- graphargs:trace 时引用到的 input tensor / global / closure 变量
- side effects:§12.8.13 提到的全局赋值、属性 mutation 等
- guards:trace 期间累积的所有假设
- example value:每个 fx node 的形状/dtype(给 Inductor 后续做 shape inference)
最关键的方法 compile_subgraph:trace 完成后把这堆东西线性化成”输入 → 算子调用 → 输出”的标准 fx Graph,附带一段”side effects replay 字节码”。这一步是 trace 阶段到 backend 阶段的接口。
为什么不直接把 fx Graph 给 backend、还要做线性化?原因:trace 期间的 fx node 顺序未必符合数据依赖(如先算后用的 inline 优化);line 化让 backend 看到的是干净的 DAG,方便做后续优化。这个职责切分让 Dynamo 与 backend 解耦 —— Inductor 不需要知道 trace 时怎么”模拟 CPython”,只看到最终干净 graph。
12.8.28 torch.export:Dynamo 的非编译用法
除了 @torch.compile,Dynamo 还服务另一条路径:torch.export(v2.1+ 稳定)。区别:
| 维度 | torch.compile | torch.export |
|---|---|---|
| 目的 | runtime 加速 | 导出可序列化 graph(部署到 mobile / 推理引擎) |
| graph break | 允许(退回 eager) | 不允许(直接报错) |
| guards | 失败时重 trace | 保存为 ExportedProgram 的输入约束 |
| 输出 | callable function | ExportedProgram(含 graph + signature + 约束) |
| 落盘 | 不直接落盘 | .pt2 格式可序列化 |
torch.export 内部仍调用 Dynamo 做 trace,但跑在 export_mode 下:fullgraph=True 强制无 break、动态 shape 推到 export 边界、所有 side effects 转成 graph 输出(不允许 mutation 跑出 graph)。trace 完后 wrap 成 ExportedProgram,可以保存到 .pt2 文件、给 ExecuTorch / TensorRT / ONNX 转换器消费。
这条路径是”PyTorch 模型部署到非 Python 环境”的官方推荐方式(替代 v1 时代的 torchscript)。Dynamo 既是 runtime 编译器(@torch.compile)又是 ahead-of-time export 工具,底层共用同一套 trace 逻辑。这种代码复用是 Dynamo 设计成”可重入 trace 引擎”而非”compile 装饰器”的核心原因。
12.8.29 自定义 backend:register_backend 接口
Dynamo 的 backend 是可插拔的。torch._dynamo.register_backend 允许第三方注册自家编译器:
from torch._dynamo import register_backend
@register_backend
def my_backend(gm: torch.fx.GraphModule, example_inputs):
# gm: trace 出的 fx GraphModule
# example_inputs: 第一次调用时的输入(用来推 shape / dtype)
print("got graph:", gm.graph)
return gm.forward # 必须返回一个 callable
@torch.compile(backend="my_backend")
def f(x): return x + 1
PyTorch 自带的 backend:
| backend | 用途 |
|---|---|
inductor(默认) | 全栈编译到 Triton GPU kernel |
aot_eager | 只跑 AOTAutograd、不跑 Inductor lowering,graph 用 eager 跑 |
aot_eager_decomp_partition | 加上 decomposition 与 partition |
cudagraphs | 直接 wrap 成 CUDA Graph |
eager | 完全不编译,只 trace 出 graph 验证 trace 正确性 |
tvm / onnxrt 等 | 第三方注册的 |
实战:硬件厂商(如 Intel oneDNN Graph、华为 Ascend)注册自家 backend,让用户 torch.compile(backend="ascend") 就能跑加速版。这套机制让 Dynamo 既服务通用 GPU(Inductor),又支持长尾硬件平台 —— trace 与编译解耦的工程价值。
调试 trace 行为最方便的方法:用 aot_eager backend,跳过 Inductor,让 trace 错误第一时间暴露。生产追求性能用 inductor。
12.8.30 VariableBuilder:第一次见到对象时怎么包装
torch/_dynamo/variables/builder.py 的 VariableBuilder 负责”把 trace 看到的 Python 对象转成 VariableTracker”。这是 Dynamo 处理”未知输入”的入口。
第一次看到一个对象(如 model)时的逻辑:
graph TB
Obj[Python 对象] --> T{type 是什么?}
T -->|torch.Tensor| Tv[TensorVariable<br/>记录 dtype/shape, 加 TENSOR_MATCH guard]
T -->|nn.Module| Mv[NNModuleVariable<br/>把 _parameters/_modules 也包装]
T -->|int/float/str| Cv[ConstantVariable<br/>加 EQUALS_MATCH guard]
T -->|list/tuple| Lv[ListVariable<br/>递归包装每个元素]
T -->|dict| Dv[ConstDictVariable<br/>递归包装 key/value]
T -->|callable| Fv[UserFunctionVariable<br/>记录 closure / globals]
T -->|未知 C 扩展| Br[直接 graph break]
style Br fill:#fee2e2,stroke:#ef4444
每种 VariableTracker 子类有自己的”var_getattr / call_method / var_call”实现,决定后续 trace 时怎么处理。VariableBuilder 用一个 200+ 行的 _wrap 函数 dispatch:先查类型注册表、再做兜底匹配、不认识的对象触发 graph break。
为什么这么细?Dynamo 必须在 trace 期间”假装执行”用户代码,但又不能真去 call C 扩展(如 numpy)。VariableTracker 是这个”假装”的载体 —— 包装后的对象响应所有访问都是 Dynamo 的可控行为。理解 VariableBuilder 让你看 variables/ 目录下 30+ 个 VariableTracker 子类不再迷失。
12.8.31 几个关键 config flag
torch._dynamo.config 暴露了大量调优开关。生产用得最多的几个:
| flag | 默认 | 作用 |
|---|---|---|
cache_size_limit | 8 | 单 frame 最多缓存 graph 数,超了就 fallback eager |
accumulated_cache_size_limit | 256 | 跨进程的全局缓存上限 |
recompile_limit | 8 | 单 frame 最多 recompile 次数 |
suppress_errors | False | 是否吞掉 Dynamo 错误转 fallback eager |
verbose | False | 打印详细 trace 流程 |
dynamic_shapes | True (v2.1+) | 全局开 dynamic shape 推断 |
assume_static_by_default | True | 第一次假定 static shape,第二次不同 shape 再转 dynamic |
inline_inbuilt_nn_modules | True (v2.4+) | inline trace 进 nn.Module 的 forward |
capture_scalar_outputs | False | tensor.item() 是否当 unbacked SymInt 而非 graph break |
suppress_errors 在生产里争议大:开了让代码总能跑(不会因 trace bug 崩溃),但隐藏了优化机会。线下调试推荐关,线上推荐开。
assume_static_by_default 是 v2.x 的默认策略:避免每个 batch size 都触发 dynamic shape 编译(dynamic shape 编译比 static 慢 30%)。第一次 batch size 假定 static、第二次发现 batch size 变了再转 dynamic 重 trace 一次 —— 平均编译效率最优。
12.8.32 Compiled Autograd:让反向也被 Dynamo trace
v2.6(2025-01)引入的 Compiled Autograd 让 Dynamo 不只 trace forward,反向计算也能被它接管。
传统 @torch.compile 的局限:
forward 被 compile → fx graph → Inductor → 编译产物
backward 不被 compile → autograd engine 用 PyNode + python_function 跑(第 8 章 §8.x)
backward 跑解释执行的 PyNode 链,每个 grad_fn 都是独立的算子调用,没机会做 fusion。在 Llama 训练里 backward 占 50%+ 时间,这块没编译就吃亏了。
Compiled Autograd 的做法:
graph LR
F[forward 执行] --> G[autograd 记录 PyNode 链]
G --> CA[Compiled Autograd<br/>把整个 PyNode 链转成 fx graph]
CA --> Dy[Dynamo 第二次 trace<br/>把 fx graph 当成函数 trace]
Dy --> Ind[Inductor<br/>编译反向 graph]
style CA fill:#fef3c7,stroke:#f59e0b
style Dy fill:#dbeafe,stroke:#3b82f6
具体:autograd engine 调用每个 grad_fn 时,不是真去执行,而是把它的元信息累积到 fx graph。整个 backward 链 trace 完后再交给 Dynamo 二次处理(含 guards、fusion)、再 send Inductor。
启用:
import torch._dynamo
torch._dynamo.config.compiled_autograd = True
实战效果:Llama-13B 训练 backward 时间从 60ms 缩到 45ms(25% 提升)。Compiled Autograd 是”第二条 trace 路径” —— 不是替代 forward 编译,而是补充。理解它让你看到 PyTorch 编译战略的完整版图:forward 通过字节码 trace、backward 通过 autograd graph trace、两条路径都流向 Inductor。
12.8.33 inline_inbuilt_nn_modules 的权衡
v2.4 引入的 inline_inbuilt_nn_modules 默认 True,让 Dynamo trace 时 inline 进所有 nn.Module 子类的 forward 方法。
带来的好处:
- fx graph 更大:含 model 全部计算,给 Inductor 更多 fusion 机会
- 跨 module 优化:能 fuse
Linear → ReLU → Linear(不开 inline 时是三段独立 graph) - 更精确的 shape 推断:跨 module 的 shape 信息也在同一 graph 里
带来的代价:
- trace 时间长:70B 模型可能 30+ 秒(因为整个 forward 被 inline 展开)
- cache 失效面广:任意子 module 改了都让顶层 graph 失效
- fx graph 巨大:单 graph 几万 node,给后续处理增加压力
工程取舍:
- 大模型(>1B 参数):开 inline,编译慢但收益高
- 小模型 + 频繁 batch 变化:可关 inline 减小 cache 失效面
- debug compile 错误:先关 inline 缩小问题面、定位后再开
torch._dynamo.config.inline_inbuilt_nn_modules = False 显式关。这个 flag 的存在反映了 PyTorch 在”compile 通用性 vs 性能”之间的反复权衡 —— 默认值在 v2.x 各版本调整过几次。
12.8.34 Stack Reconstruction:graph break 时怎么还原 Python 栈
graph break 在字节码任意位置发生,但 Dynamo 不能”丢掉栈状态” —— eager fallback 的代码需要看到与 break 时一致的 local 变量、操作数栈、异常处理 frame。torch/_dynamo/codegen.py 的 stack reconstruction 干这个活。
机制:trace 期间 Dynamo 记录每个时刻的”虚拟栈”(VariableTracker 列表)。break 触发时,需要把虚拟栈重建到真实 Python 栈上 —— 通过生成一段字节码,把每个 VariableTracker 对应的真实对象 LOAD_FAST / LOAD_GLOBAL 推上去。
# 假设 trace 期间虚拟栈是 [TensorVariable(x), ConstantVariable(2)]
# 现在要 graph break,生成的恢复字节码:
LOAD_FAST x # push 真实 tensor x
LOAD_CONST 2 # push 常数 2
# 现在真实栈与虚拟栈一致,CPython 默认 eval 接管后续字节码
复杂场景:栈上的对象是中间结果(如 LOAD_FAST x; CALL torch.relu 后栈顶是 relu(x) 还没存到 local),需要先把它存到 graph 输出里、然后 LOAD 出来。这套生成由 OutputGraph + codegen 协作完成。
理解 stack reconstruction 让你看明白”graph break 不只是停下”,而是精心编排的状态迁移。出错时常见症状是”break 后变量 undefined”或”操作数栈高度不对” —— 这往往是 codegen 这层的 bug,从 TORCH_LOGS=bytecode 可看到生成的 break 字节码与预期不符。Dynamo 团队在 v2.x 各版本反复修这块的 corner case。
12.8.35 Dynamo 自身异常 vs 用户代码异常
trace 过程中可能抛两类异常,要区分对待:
- Dynamo 自身异常(如
Unsupported、InternalError):表明 trace 不能处理某个字节码。默认 fallback 到 graph break;fullgraph=True时直接 raise - 用户代码异常(如
RuntimeError("shape mismatch")):trace 时调用真实 PyTorch op 验 shape 时才暴露,需要原样抛给用户
convert_frame.py 的 try/except 框架做这个分发:捕获 Unsupported 后看 fullgraph 决定是 break 还是 raise;捕获用户 RuntimeError 后包成 BackendCompilerFailed 抛出(保留原 traceback)。
特殊案例:用户代码里 try: ... except: ... 想吞掉某个 op 的失败 —— Dynamo 看到 try 字节码(SETUP_FINALLY / SETUP_EXCEPT)时直接 graph break(因为 trace 期间不真跑 op、没法判断异常会不会触发)。这是 trace 期间常见的 break 触发点。
TORCH_LOGS=dynamo 输出里 “graph break: try-except not supported” 是这种情况。重构方法:把 try/except 移到 compile 包装外面、内部代码改成 if torch.isnan(x).any(): handle_nan() 这种纯算子判断。
12.8.36 Source 类:guards 怎么”指向”被守护的值
每个 guard 必须知道”我守护的是哪个 Python 表达式”。torch/_dynamo/source.py 的 Source 类层次表达这个:
| Source 子类 | 表示 |
|---|---|
LocalSource("x") | x(local 变量) |
GlobalSource("model") | model(global) |
AttrSource(LocalSource("x"), "shape") | x.shape |
GetItemSource(LocalSource("d"), "key") | d["key"] |
NNModuleSource(...) | model.layer1.weight(递归组合) |
每个 VariableTracker 持有一个 Source,guards 编译成 C++ 时把 Source 转成对应访问代码(如 AttrSource(x, "shape") 转成 PyObject_GetAttrString(x, "shape"))。这种”用对象树表达 Python 表达式”让 guards 检查是纯 C 代码 —— 不进 Python interpreter 就能验证。
理解 Source 让你看到 guards 检查为什么能 < 200ns —— 整个检查链都在 C 层执行。这是 v2.x guard 性能从微秒级降到纳秒级的关键工程改造。Source 还服务另一个目的:debug 输出时把”哪个 guard 失败”翻译回可读 Python 表达式,让 TORCH_LOGS=recompiles 的输出对人类友好(如打印 “guard failed: x.shape[0] != 768” 而不是无法定位的 byte offset)。
12.9 几条工程经验
实战 Dynamo:
1. TORCH_LOGS=dynamo,graph_breaks 是诊断 compile 问题的第一武器:能看到每次 trace、每个 graph break 的原因
2. 第一次 compile 慢是正常的:几秒到几十秒。线上服务前要 warm up(先跑几个 batch 让 cache 命中)
3. 避免在 compiled 函数里写复杂 Python 逻辑:能放外面就放外面。for / if / dict 操作 越多、graph break 越多
4. dynamic shape 用 mark_dynamic:torch._dynamo.mark_dynamic(x, 0) 告诉 Dynamo “x 的 dim 0 是符号”,避免每个 batch size 重新 trace
5. 用 fullgraph=True 强制不要 graph break:@torch.compile(fullgraph=True) 时 Dynamo 遇到不能 trace 的代码直接报错而不是退回 eager。这能强制你写 trace-friendly 的代码
6. cache 体积在长跑训练里要监控:每个 graph 编译产物可能几 MB,几百个 graph 占几 GB。torch._dynamo.reset() 清掉所有 cache
7. AOT 缓存:torch.compile(mode='reduce-overhead') + TORCHINDUCTOR_CACHE_DIR 让编译产物落盘,下次进程启动直接复用,跳过 trace 时间
12.10 跨书关联
- 《Rust 编译器之路》编译期 trait 解析:Rust trait 在编译期决定,PyTorch Dynamo 在运行期决定。前者零开销但不灵活,后者有 trace 开销但能处理动态形状
- 《V8 / JIT 编译》(如读过):Dynamo 的 trace + guard + 重 trace 与 V8 的 inline cache + deoptimization 思想一致 —— 都是”乐观假设 + 失效后回退”
- 《vLLM 内核探秘》第 8 章 模型 runner:vLLM 也用 torch.compile 加速 forward,理解 Dynamo 的 graph break 机制能帮你调出最高吞吐
12.11 设计启示
Dynamo 的几个核心思想可迁移:
第一:编译是可选的、按 frame 粒度的:不是整个程序编译,而是 hot frame 编译。让 compile 不破坏其他代码
第二:trace 时假设 + 运行时校验:guards 模式让”乐观编译”能在动态场景安全工作。这套思想在 V8、HotSpot、PyPy 都用
第三:graph break 而非 hard fail:不能 trace 的代码不报错,让程序仍然能跑,只是少一些优化。可用性远大于性能损失
第四:backend 可插拔:trace 与编译解耦,让多家硬件厂商能在 trace 之上接自家编译器
下一章拆 AOTAutograd —— Dynamo 拿到的只是 forward graph,AOTAutograd 把它配上反向、function化、partition 成正反向子图,再送给 Inductor。
评论 0
还没有评论,来说两句吧。
评论加载失败,刷新重试。