第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_eagercudagraphs 等做实验
  • 理解 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_frameconvert_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 干几件事:

  1. frame.f_code 拿到字节码 + locals + globals
  2. 检查编译缓存(同 code object + 相同 guards 命中 → 复用)
  3. 创建 InstructionTranslator 开始 trace
  4. trace 完拿到 OutputGraph + Guards
  5. 把 OutputGraph 喂给 backend(默认 Inductor)拿到编译产物
  6. 返回新的 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:

BytecodeInstructionTranslator 行为
LOAD_FAST从 locals 取出对应 VariableTracker(包装 Python 对象的符号)
CALL_FUNCTION如果是 PyTorch 算子,往 FX Graph 加 node;如果是普通函数,inline trace 进去
BINARY_OP同上 —— Tensor + Tensor 加节点,int + int 直接算
RETURN_VALUEtrace 结束,返回 OutputGraph

每个 Python 对象在 trace 时被包装成 VariableTrackerTensorVariable(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 Graph
  • range(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_MATCHSHAPE_ENVOBJECT_MISMATCHCONSTANT_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:583OutputGraph 类持有 trace 阶段构建的 FX Graph。它的核心字段:

  • graph: torch.fx.Graph —— FX 节点列表
  • guards: set[Guard] —— 收集到的 guards
  • side_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])时:

  1. Dynamo 拦截 frame,看到有 Tensor → 进入 _compile
  2. cache miss,开始 trace
  3. InstructionTranslator 解析字节码:LOAD_FAST aLOAD_FAST bBINARY_ADDSTORE_FAST cLOAD_FAST cLOAD_METHOD reluCALL_METHODRETURN_VALUE
  4. trace 时往 FX Graph 加 2 个节点:add(a, b)relu(_)
  5. 收集 guards:a 是 fp32+cuda+[4,4]、b 是 fp32+cuda+[4,4]
  6. 把 FX Graph + example_inputs 送给 Inductor
  7. Inductor 编译成 Triton kernel,返回 callable
  8. Dynamo 把 callable 缓存进 _GUARDED_CODE,下次直接跳过 trace

第二次调用 add_relu(x2, y2)(同样 dtype/device/shape):

  1. Dynamo 拦截 frame
  2. 检查 guards:x2 / y2 也是 fp32+cuda+[4,4] → 通过
  3. 直接跑缓存的 Triton kernel
  4. 完全跳过 dispatcher / autograd / Python 解释器

第二次起的开销几乎是 0 —— 这就是 torch.compile 的核心收益。

12.8.5 VariableBuilder:Python 对象 → VariableTracker

进入 trace 前要把 frame 的 locals / globals 里每个 Python 对象包装成 VariableTrackertorch/_dynamo/variables/builder.py:464VariableBuilder 是这个包装器。

它按对象类型分流:

Python 对象包装后类型处理
torch.TensorTensorVariable记录 shape/dtype/device 加 guard、加 graph input
int / floatConstantVariabletrace 时直接当常量参与
nn.ModuleNNModuleVariable把 module 整个”内联”进 trace,递归 trace 它的 forward
list / tuple / dictListVariable / DictVariable容器递归包装每个元素
torch.dtypeTorchInGraphFunctionVariableConstantVariabledtype 是 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:1013GuardBuilder 把 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:1605compile_subgraph

  1. 把 trace 出来的 FX Graph 包装成 torch.fx.GraphModule
  2. compiler_fn(gm, example_inputs)(默认是 Inductor)拿到 callable
  3. install_global 把 callable 注册成 frame 的全局变量(如 __compiled_fn_0
  4. 生成”调用 callable 的字节码序列”,存到 self.output_instructions

bytecode_transformation.py:1593transform_code_objectoutput_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 来自系统库(如 printjson.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:1236InstructionTranslatorBase 是字节码解释器。核心是 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 例子:

OpcodeHandler 行为
LOAD_FAST从 local symtable 取出 VariableTracker
STORE_FAST把 VariableTracker 存到 local symtable
BINARY_ADDBinaryAdd.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 的是 GuardManagerWrapperguards.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 > 0s0 % 8 == 0)都被记录。fx graph 生成时所有 shape 表达式是 SymInt,Inductor 拿到后能用它做 dynamic codegen(第 14 章 §14.8.7)。

如果 trace 中有”shape 决定控制流”的代码(如 if x.shape[0] > 100),Dynamo 会创建一个 shape guards0 > 100 必须为真才能复用此编译。下次调用 s0 < 100 → guard 失败 → 重 trace 走 else 分支、新 graph 缓存。

12.8.13 SideEffects:副作用的精确跟踪

side_effects.py:89SideEffects 类管理 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 时怎么处理?

机制:

  1. trace 看到 LOAD_ATTR linear
  2. handler 调 model.__getattr__('linear') → 返回 Linear 子模块
  3. Dynamo 把它包装成 NNModuleVariable(第 12 章 §12.8.5)
  4. 继续 trace LOAD_ATTR weight
  5. 第 9 章 §9.4 的 __getattr___parameters['weight'] 取出 → 返回 Tensor
  6. 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 值的 ifif x.sum() > 0: ...重构成 torch.where
Python list 操作lst.append(x)用预分配 tensor
dict 动态 keyd[x.item()] = ...避免 .item() / 用 fixed key
numpy 操作np.array(x)用 torch op 替代
print / logprint(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 实现。

机制:

  1. trace 跑到 break 处停下,已 trace 的部分编成 graph_A
  2. Dynamo 生成新字节码:调 graph_A → 用 eager 跑 break 那一行 → 创建一个 continuation function 接管剩余字节码
  3. continuation function 是个新的函数,包含 break 之后的所有字节码
  4. 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= 支持多个标签同时开(逗号分隔):

标签输出内容
dynamoDynamo 整体流程(trace 开始 / 结束、cache 命中 / 失败)
recompiles每次重 trace 的原因(哪个 guard 失败、shape 变了什么)
graph_breaks每次 graph break 的位置 + 原因(哪条字节码不支持)
bytecode详细字节码 trace 过程(每条 opcode 的处理)
output_codeInductor 生成的 Triton 代码(与 §14.9.8 联动)
aot_graphsAOTAutograd 输出的 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

关键步骤(精简版):

  1. 入口校验:跳过库代码、被 @disable 装饰的函数、recursion 太深的 frame
  2. InstructionTranslator 实例化:把 frame 的 co_codef_localsf_globals 包成 trace 上下文
  3. 跑 trace 主循环step_until_not_supported 直到 RETURN_VALUE 或 graph break
  4. OutputGraph.compile_subgraph:把累积的 fx node 整理成可调用的 fx Graph
  5. 调 backend:默认 aot_autograd_simplifiedinductor.compile_fx
  6. 重写 frame 的 bytecode:原始字节码替换为”check guard → call CompiledFn → return”
  7. 包成 GuardedCode:guards + 新 bytecode + 编译产物绑在一起,存进 cache

这个 6 步流程是 Dynamo 全部价值的实现。卡在哪一步可以从 TORCH_LOGS=dynamo 的输出看出来:每一步打印一条耗时记录。debug compile 慢时这是第一手信息。

12.8.27 OutputGraph:fx graph + 副作用打包器

torch/_dynamo/output_graph.pyOutputGraph 类负责”把 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.compiletorch.export
目的runtime 加速导出可序列化 graph(部署到 mobile / 推理引擎)
graph break允许(退回 eager)不允许(直接报错)
guards失败时重 trace保存为 ExportedProgram 的输入约束
输出callable functionExportedProgram(含 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.pyVariableBuilder 负责”把 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_limit8单 frame 最多缓存 graph 数,超了就 fallback eager
accumulated_cache_size_limit256跨进程的全局缓存上限
recompile_limit8单 frame 最多 recompile 次数
suppress_errorsFalse是否吞掉 Dynamo 错误转 fallback eager
verboseFalse打印详细 trace 流程
dynamic_shapesTrue (v2.1+)全局开 dynamic shape 推断
assume_static_by_defaultTrue第一次假定 static shape,第二次不同 shape 再转 dynamic
inline_inbuilt_nn_modulesTrue (v2.4+)inline trace 进 nn.Module 的 forward
capture_scalar_outputsFalsetensor.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 自身异常(如 UnsupportedInternalError):表明 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.pySource 类层次表达这个:

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_dynamictorch._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