你是否见过高铁站里那套“智能闸机”?旅客刷脸、验票、查行李,几秒钟完成,随后被引导走向不同的通道:老熟人走绿色快通道,新面孔走进安检厅做详细检查,特殊情况则被带去人工窗口。Python 程序运行时,也有一个类似的“进站口”——每一次函数调用都会生成一个“帧”。TorchDynamo 这个 AI 编译器,就站在这个“进站口”上,对每一帧代码做评估:这趟车熟不熟、货单稳不稳定、要不要重新编组。这套机制,就是 Frame 评估(帧评估)。 TorchDynamo 利用 PEP 523 提供的帧评估钩子,在 Python 解释器执行每一帧之前拦截下来。它先查缓存,再验守卫,最后决定是直接放行、走编译快通道,还是拆开重查。正是这“一眼识破”的评估能力,让 PyTorch 2.x 能把训练提速 30% 以上,又不需要你改写模型代码。本文就用“火车站智能闸机”这个比喻,带你把帧评估的来龙去脉拆开揉碎。

岗位关联

理解帧评估,你就能真正看懂 torch.compile 何时命中缓存、何时失效重编、何时悄悄变慢。这是 AI 编译器工程师、模型训练性能调优者必须掌握的底层地图。

目录
  1. 可视化演示
  2. 帧评估概念
  3. 钩子机制
  4. 守卫审查
  5. 追踪编译
  6. 缓存复用
  7. 工程实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示TorchDynamo运行时Frame评估捕获Python计算图、字节码分析生成FX图及Guard校验的过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

图捕获:运行时钩子安装 PEP523注册帧评估钩子 forward执行时捕获计算图 dynamo.optimize(f) 零侵入式动态图捕获

帧评估概念

想象高铁站调度室:每列火车进站,车站都会生成一张“到站记录单”,写着车次、编组、货物、线路。Python 程序也一样——每次函数调用,解释器就生成一个“帧对象”。帧对象是 Python 虚拟机的最小执行单位,它记录着该函数要执行的字节码(code)、局部变量(locals)、全局变量(globals)和调用来源(back)。整个 Python 程序就是由无数个帧组成的调用栈,像一列列进站的火车排成车队。

在深度学习训练里,一个模型的一次 forward 可能要执行几万次 PyTorch 算子调用。每一次算子调用都是一个 Python 函数,都会产生一个帧。如果每个帧都老老实实走一遍解释器,大量时间就耗在了“翻记录单”上。TorchDynamo 盯上的正是这个环节:如果能在帧生成的瞬间判断“这列火车很眼熟”,就能跳过繁琐流程,直接走编译好的快车道。

回到火车站:帧评估,就是闸机口系统对“到站记录单”做快速判读的过程。谁该放行、谁要检查、谁得改签,全在这一张记录单里写得清清楚楚。理解了帧评估,你就拿到了进入 TorchDynamo 世界的第一张车票。

frame=⟨code,locals,globals,back⟩\displaystyle \text{frame} = \langle \text{code}, \text{locals}, \text{globals}, \text{back} \rangle
帧就像一张“到站记录单”,code 是车次时刻表,locals 是本车厢货物清单,globals 是全站共享物资表,back 指向前一辆车。
  1. 帧(frame)帧是 Python 每次函数调用生成的“到站记录单”,它把一次调用的所有上下文装进一个对象。
    frame=⟨code,locals,globals,back⟩\displaystyle \text{frame} = \langle \text{code}, \text{locals}, \text{globals}, \text{back} \rangle
  2. code 字段code 保存该函数的 Python 字节码对象,也就是“车次与时刻表”,告诉解释器具体执行什么指令序列。
    code=CodeType 对象\displaystyle \text{code} = \text{CodeType 对象}
  3. locals 字段locals 是局部变量表,映射变量名到值,相当于“本车厢货物清单”,只在本次调用内有效。
    locals={name→value}\displaystyle \text{locals} = \{ \text{name} \to \text{value} \}
  4. globals 字段globals 是模块级全局变量表,映射全局名到值,相当于“全站共享物资表”,所有帧都能看到。
    globals={name→value}\displaystyle \text{globals} = \{ \text{name} \to \text{value} \}

钩子机制

如果车站想加装一套智能闸机,硬拆铁轨肯定不行,最优雅的做法是在进站口加一个“接站专员”。Python 世界里的这个专员就是 PEP 523 提供的帧评估钩子。它允许外部工具把自己的回调函数插入 CPython 的帧评估流程中。原本 Python 解释器执行一个帧时,会直接调用内部函数 PyEval_EvalFrameEx;有了钩子,TorchDynamo 就能在这个函数外侧套一层自己的判断逻辑,相当于在进站口先由自家安保系统“筛一遍”。

TorchDynamo 在调用 torch.compile 时,会把钩子安装到解释器上。从这一刻起,每一个 Python 帧执行前都会先经过 evaluate_frame 回调。回调拿到帧对象后,开始走一套固定流程:先看这个函数代码长什么样,再查一下之前有没有编译过,如果有编译结果就检查守卫,最后决定是直接返回编译好的代码对象,还是进入追踪编译流程。这套“拦截—判断—放行/编译”的组合拳,就是帧评估的核心机制。

接站专员不是来取代调度员的,而是在进站口先做一次分级。它让“熟客快速通过、新客引导检查”,整个车站的通行效率因此大幅提升。TorchDynamo 的钩子机制,就是 PyTorch 2.x 提速奇迹的起点。

Run(f)={Optimized(f)守卫通过Trace(f)守卫失败或未命中\displaystyle \text{Run}(f) = \begin{cases} \text{Optimized}(f) & \text{守卫通过} \\ \text{Trace}(f) & \text{守卫失败或未命中} \end{cases}
Run(f) 表示“处理当前帧 f”。如果守卫检查全部通过,就走 Optimized 快车道;否则走 Trace 追踪编译通道。
  1. Run(f)Run 是帧执行入口函数,f 代表当前被拦截的 Python 帧,TorchDynamo 将在这里决定去哪条路。
    Run(f)\displaystyle \text{Run}(f)
  2. Optimized(f)当缓存中已有编译产物且守卫通过时,执行优化后的代码对象,相当于绿通道直接放行。
    Optimized(f)=执行编译后代码对象\displaystyle \text{Optimized}(f) = \text{执行编译后代码对象}
  3. Trace(f)守卫失败或缓存未命中时,进入追踪编译流程,把帧内的 Python 逻辑逐条记录成计算图。
    Trace(f)=符号执行并生成计算图\displaystyle \text{Trace}(f) = \text{符号执行并生成计算图}

守卫审查

火车站闸机验票时,会核对车次号、车厢数、乘客名字和目的地。TorchDynamo 的“守卫”就是这套验票规则:它把编译结果所依赖的所有条件记录下来,下次遇到相同代码帧时,逐项核对当前环境是否还满足。最常见的守卫检查对象包括张量形状(shape)、数据类型(dtype)、设备类型(device)以及 Python 全局变量。只要有一项不满足,编译结果就不能再用。

为什么需要守卫?因为编译后的代码是在特定条件下生成的。比如你的模型输入一直是 [32, 128] 的矩阵,编译器就能针对 32×128 这个形状做极致优化;但如果某一次突然变成 [64, 128],原来的优化假设就失效了,必须重新追踪编译。守卫本质上是一组“条件断言”,它们把编译产物和运行时环境绑定在一起,确保复用安全。

守卫过严,会让很多本该复用的代码白白重编;守卫过松,又可能导致编译结果在错误条件下运行。就像火车站安检,查得太狠排长队,查得太松出事。TorchDynamo 在两者之间寻找平衡,这也是帧评估中最微妙的设计艺术。

Guard(x)=⋀i=1nchecki(x)\displaystyle \text{Guard}(x) = \bigwedge_{i=1}^{n} \text{check}_i(x)
守卫是所有检查项的“逻辑与”:只要有一个 check 失败,整个守卫就失败,编译结果作废。
  1. check_i(x)第 i 项检查条件,比如“形状是否等于 (32,128)”“dtype 是否为 float32”“设备是否为 cuda”。
    checki(x)∈{True,False}\displaystyle \text{check}_i(x) \in \{\text{True}, \text{False}\}
  2. 逻辑与 ⋀符号 ⋀ 表示“同时成立”,一票否决。只要一项检查不通过,Guard(x) 就是 False。
    ⋀i=1nchecki(x)\displaystyle \bigwedge_{i=1}^{n} \text{check}_i(x)
  3. Guard(x) 为真所有检查项都为真时,编译缓存可以安全复用,帧走快车道;否则进入重编译流程。
    Guard(x)=True⇒复用缓存\displaystyle \text{Guard}(x) = \text{True} \Rightarrow \text{复用缓存}

追踪编译

当守卫失败,一列车新到,调度员就要做一次“全面编组演练”。这对应 TorchDynamo 的追踪编译流程。它会对帧内的 Python 代码进行符号执行:不真正计算张量数值,而是像导演走戏一样,把每一行代码变成计算图上的一个节点。torch.add(a, b) 变成一个加法节点,matmul(x, w) 变成一个矩阵乘法节点,张量之间的数据流动成为节点间的边。

追踪过程中,如果遇到无法纳入图的操作,比如 Python 原生的条件分支、外部库调用、动态字典操作,Dynamo 会在那里“断图”。这个断点就是 Graph Break。断图之后,程序被拆成多个子图:可编译的部分交给后端编译成高效内核,不可编译的部分退回 Python 解释器执行。子图之间通过新的字节码串起来,就像一列长火车被拆成几段,可优化的车厢走快轨,难处理的行李走人工通道。

追踪编译结束后,Dynamo 会生成一段新的 Python 字节码,这段字节码在运行时调用编译好的子图。原来的帧就像换了一张“新时刻表”,照常发车,但内部已经悄然变成了更高效的运行图。这正是帧评估的最终成果:用一次精心编组,换一整个训练历程的畅通无阻。

G=(V,E),V={v1,v2,…,vn}\displaystyle G = (V, E), \quad V = \{v_1, v_2, \ldots, v_n\}
追踪生成的计算图 G 由节点集 V 和边集 E 组成:节点是一次张量运算,边是数据依赖关系。
  1. 节点集 VV 中的每个节点代表一次算子调用,如 add、matmul、relu,对应车站里的一节车厢。
    V={v1,v2,…,vn}\displaystyle V = \{v_1, v_2, \ldots, v_n\}
  2. 边集 EE 中的每条边代表张量从一个算子流向另一个算子,也就是车厢之间的挂钩连接。
    E={(vi,vj)∣vj 需要 vi 的输出}\displaystyle E = \{(v_i, v_j) \mid v_j \text{ 需要 } v_i \text{ 的输出}\}
  3. 子图划分Graph Break 将 G 切成若干子图,每个子图独立编译,子图之间靠字节码桥接。
    G=G1∪G2∪⋯∪Gk\displaystyle G = G_1 \cup G_2 \cup \cdots \cup G_k

缓存复用

智能闸机最值钱的资产,不是检查能力,而是记忆能力。一个熟客第二次来,刷个脸就能过;只有陌生面孔才需要全面检查。TorchDynamo 的缓存正是这段“记忆”。每当一次追踪编译完成,生成的代码对象和守卫信息就会被放进缓存,以原函数代码对象为 key。下一次同一段代码生成帧时,Dynamo 先查缓存:找到了,再验守卫;守卫通过,直接复用编译结果,完全跳过追踪流程。

缓存的查找过程很像车站的“熟客库”:先按身份证号(函数代码对象)找到档案,再核对今天的车次信息(守卫)。如果守卫对不上,说明档案过期,需要重新建档并覆盖旧缓存。值得注意的是,缓存不是无限的。它根据代码对象和缓存槽位进行管理,防止内存无限膨胀。因此,缓存的命中率直接决定了帧评估的收益:命中率越高,编译开销被分摊得越薄,整体速度越快。

缓存复用是帧评估中最直接的性能杠杆。优秀的使用者会让线上训练全程保持 95% 以上的命中率;而糟糕的动态输入则会让命中率跌到 50% 以下,导致“每次都在重新编译”。记住:闸机的记忆越好,车站的通行才越顺畅。

hit_rate=hh+m\displaystyle \text{hit\_rate} = \frac{h}{h + m}
命中率是命中次数 h 除以总尝试次数 h+m。命中率越高,说明越多的帧避免了重复编译。
  1. 命中数 hh 是守卫通过且复用缓存成功的帧次数,相当于熟客直接走绿通道。
    h=缓存命中次数\displaystyle h = \text{缓存命中次数}
  2. 未命中数 mm 是缓存没找到或守卫失败的帧次数,这些帧必须重新追踪编译。
    m=缓存未命中次数\displaystyle m = \text{缓存未命中次数}
  3. 命中率计算命中率反映缓存系统的效率,目标是把埋单集中在少数陌生帧上。
    hit_rate=hh+m≈1 时,编译开销可忽略\displaystyle \text{hit\_rate} = \frac{h}{h + m} \approx 1 \text{ 时,编译开销可忽略}

工程实战

理解了帧评估的底层原理,实战中就能像老调度员一样精准调优。核心目标只有一个:提高缓存命中率、减少重编译。最容易踩的坑是动态 shape。如果你的训练代码中 batch size、序列长度频繁变化,守卫会反复失效,每次迭代都在重新追踪编译。解决办法通常是固定 batch size、用 torch.compile(dynamic=False) 明确关闭动态形状支持,或在数据加载阶段做 padding 对齐。

第二个实战要点是学会观察。TorchDynamo 提供了丰富的诊断工具,例如设置 TORCH_LOGS=graph_breaks,recompiles 可以打印断图位置和重编译次数;torch._dynamo.config.recompile_limit 可以控制重编译上限;torch._dynamo.explain() 能汇总帧评估的拦截情况。通过这些手段,你能精准定位是哪一行代码引发了 Graph Break,哪一次 shape 变化导致了守卫失效。

回到车站比喻:老调度员的智慧不在于蛮力提速,而在于让绝大多数列车走熟客通道,只把极少量的陌生车辆交给编组站处理。帧评估的工程实践同样如此——先看观测日志,再稳定输入形状,最后针对断图点逐个击破。当你把命中率拉满,就能真正感受到 TorchDynamo 那句“一次编译、终身受益”的含金量。

speedup=TeagerTtrace+Trun\displaystyle \text{speedup} = \frac{T_{\text{eager}}}{T_{\text{trace}} + T_{\text{run}}}
加速比等于 eager 模式耗时除以“追踪编译时间 + 编译后运行时间”。当 T_trace 很小时,加速比趋于理想状态。
  1. T_eager不使用 TorchDynamo 的原始 PyTorch 执行耗时,作为性能基准线。
    Teager=eager 模式总耗时\displaystyle T_{\text{eager}} = \text{eager 模式总耗时}
  2. T_trace所有帧进行一次或多次追踪编译的总开销,包括符号执行和后端编译。
    Ttrace=重编译次数×单次追踪耗时\displaystyle T_{\text{trace}} = \text{重编译次数} \times \text{单次追踪耗时}
  3. T_run编译后代码实际运行的总耗时,通常远小于 eager 模式。
    Trun=编译后运行总耗时\displaystyle T_{\text{run}} = \text{编译后运行总耗时}

AI 应用场景

大模型训练加速

阶段:模型训练与编译优化

解决的问题:在 eager 模式下,GPT 等大模型的每次迭代都要执行数百万次 Python 算子调用,Python 解释器的调度开销非常可观。TorchDynamo 的帧评估在每次函数调用时快速判定能否复用已编译内核,从而把 Python 层的重复劳动大幅压缩。

具体案例:某团队训练 7B 参数的 LLM,训练循环中 batch size 固定为 32、序列长度固定为 2048。通过 torch.compile(model) 后,帧评估的守卫稳定命中同一套编译产物,算子自动融合,整体训练吞吐提升了约 32%。

推理服务部署优化

阶段:推理部署与延迟优化

解决的问题:线上推理请求的 batch 大小往往频繁波动,导致 TorchDynamo 反复重编译,P99 延迟剧烈抖动。通过帧评估机制理解缓存失效的原因后,工程师会给请求做动态批处理,固定 batch 为 64 并统一 dtype,守卫就能长期保持有效。

具体案例:某推荐系统推理服务将 batch 对齐为 64,并开启静态 shape 编译。帧评估的缓存命中率从 68% 提升到 99%,推理 P99 延迟降低了 41%。

基于日志的断点排查

阶段:性能分析与故障诊断

解决的问题:很多开发者遇到 torch.compile 提速不明显时不知从何下手。借助帧评估的日志机制,可以逐帧检查哪些代码引发了 Graph Break,哪些 frame 触发了重编译,从而做出针对性修改。

具体案例:工程师在 transformer 训练中开启 TORCH_LOGS=graph_breaks,发现模型中的 for 循环依赖 Python 字典推导式导致断图。改为张量操作实现后,Graph Break 数量从 9 个降到 1 个,训练速度提升 18%。

Python 演示

python42 行
class FrameEvaluator:    def __init__(self):        self.code_cache = {}        self.hit = 0        self.miss = 0     def _trace_and_compile(self, frame):        # 模拟 TorchDynamo 中 convert_frame 的追踪开销        self.miss += 1        guard = (frame["shape"], frame["dtype"], frame["device"])        code_obj = "compiled:" + frame["func"]        return code_obj, guard     def evaluate(self, frame):        key = frame["func"]        if key in self.code_cache:            code_obj, guard = self.code_cache[key]            now = (frame["shape"], frame["dtype"], frame["device"])            if now == guard:                self.hit += 1                return code_obj            del self.code_cache[key]        code_obj, guard = self._trace_and_compile(frame)        self.code_cache[key] = (code_obj, guard)        return code_obj def run():    ev = FrameEvaluator()    frames = [        {"func": "matmul", "shape": (32, 128), "dtype": "float32", "device": "cuda"},        {"func": "matmul", "shape": (32, 128), "dtype": "float32", "device": "cuda"},        {"func": "matmul", "shape": (64, 128), "dtype": "float32", "device": "cuda"},        {"func": "add", "shape": (32, 128), "dtype": "float32", "device": "cuda"},        {"func": "add", "shape": (32, 128), "dtype": "float32", "device": "cuda"},    ]    for f in frames:        print(ev.evaluate(f))    print("hit:", ev.hit)    print("miss:", ev.miss) if __name__ == "__main__":    run()
目的:用 50 行代码模拟 TorchDynamo 帧评估中的“查缓存—验守卫—追踪编译”核心流程,输出每次帧的处理结果以及命中/未命中统计。
  1. guard = (frame["shape"], frame["dtype"], frame["device"])生成守卫三元组,就像闸机记录“车厢号、燃料类型、运行轨道”。
  2. if key in self.code_cache:先按函数名查缓存,对应 Dynamo 先看这个代码对象有没有编译记录。
  3. if now == guard:当前帧环境与守卫记录完全一致,说明还是那趟熟车,可以直接复用。
  4. del self.code_cache[key]守卫失效时删除旧缓存,避免下次继续误判。
  5. self.code_cache[key] = (code_obj, guard)把新编译结果放进缓存,供后续同类帧复用。
  6. print("hit:", ev.hit)打印命中次数,帮助判断帧评估的整体健康度。

常见误区

误区TorchDynamo 就是像 Numba 那样的 JIT 编译器,运行中自动把 Python 翻译成 C++,所以任何 Python 代码都能无脑加速。

正解TorchDynamo 的真实身份是“帧评估拦截器”。它并没有在底层重写 Python 解释器,而是通过 PEP 523 钩子在帧执行前介入,把可编译部分截取出来生成图,再交给 Inductor 等后端编译。它不是自动无死角的翻译机,而是有选择、有守卫、有缓存的调度系统。

为何常见:torch.compile 这个 API 名称让很多人联想起传统 JIT,加上官方宣传强调“无需改动模型”,就更容易误以为它是全自动魔法。

误区只要代码识别到缓存命中,就可以放心大胆地执行编译产物,守卫检查是多余的。

正解命中的只是“编译产物”,而不是“执行许可”。每次帧评估仍然要重新核对守卫条件,因为张量形状、dtype、设备都可能悄悄变化。守卫是编译代码与环境之间的安全契约,不是一劳永逸的通行证。

为何常见:人们容易把“缓存”类比成 Web 浏览器缓存,觉得存过就能直接用,忽略了 TorchDynamo 的守卫是一个运行时动态验证机制。

误区只要在训练代码外层包一个 `torch.compile(model)`,所有 Python 帧都会走编译快车道。

正解帧评估会把帧分成三类:可编译帧、部分可编译帧、不可编译帧。遇到 Python 原生动态特性时会发生 Graph Break,这部分代码仍回退到解释器执行。如果模型里到处都是动态控制流,编译收益会被大量断点抵消。

为何常见:演示教程常用静态 transformer 模型,展示效果很好,让人误以为所有模型都能获得同等加速。

误区反正守卫是保命符,把所有能检查的变量统统加入守卫列表,越细越好。

正解守卫数量越多,检查成本越高,且任何一项微小变化都会引发重编译。过度守卫会让缓存命中率断崖式下跌,最终编译开销反噬性能。真正优秀的设计是只守卫影响编译结果的必要条件。

为何常见:程序员天然倾向“防御式编程”,但在编译器场景中,安全检查的代价不是抽象的,而是真金白银的重编译时间。

面试题

Q1. TorchDynamo 最早是在哪个环节介入 Python 执行的?

单选题 · easy

  • A.GPU 驱动调用层
  • B.帧评估钩子回调← 正确答案
  • C.操作系统进程调度
  • D.显存分配器内部

答案:B。TorchDynamo 通过 PEP 523 的帧评估钩子,在 Python 帧执行前安装回调,从而拦截每一次函数调用。GPU 驱动、进程调度和显存分配都发生在更底层,不属于 TorchDynamo 的介入点。

Q2. Python 帧对象中最核心的“时刻表”字段是什么?

单选题 · easy

  • A.GPU 显存地址
  • B.函数字节码对象← 正确答案
  • C.模型权重数值
  • D.训练数据路径

答案:B。帧对象里负责“具体执行什么”的是 code 字段,它保存函数字节码对象,相当于列车的车次时刻表。显存地址、权重数值和数据路径都不是帧对象必须直接保存的内容。

Q3. Dynamo 追踪一个帧时主要采用什么执行方式?

单选题 · easy

  • A.真实跑一遍 CUDA 内核
  • B.符号执行记录张量流转← 正确答案
  • C.直接读取显存中的张量
  • D.把 Python 翻译成汇编

答案:B。追踪过程中 Dynamo 并不真正计算数值,而是做符号执行,把每个算子调用记录为计算图上的节点。CUDA 真实执行、显存读取和汇编翻译都不是追踪阶段的动作。

Q4. 当帧的守卫条件全部通过时,TorchDynamo 会执行什么?

单选题 · mid

  • A.原始 Python 字节码
  • B.缓存中的编译后代码← 正确答案
  • C.强制回退到 eager 模式
  • D.重新追踪并再次编译

答案:B。守卫通过意味着当前环境与编译产物的假设一致,可以直接复用缓存中的编译后代码。原始字节码和 eager 回退是守卫失败或未命中时的行为,重新编译更是没有必要。

Q5. “Graph Break”在帧评估中最准确的含义是什么?

单选题 · mid

  • A.追踪时遇到无法捕获的代码点← 正确答案
  • B.缓存空间用尽导致程序中断
  • C.编译器崩溃后自动重启
  • D.模型训练损失函数发散

答案:A。Graph Break 指追踪过程中遇到无法纳入计算图的 Python 操作,于是程序被拆分为多个子图,可编译部分继续优化,断点以后的部分回退到解释器执行。它不是缓存崩溃,更不是训练发散。

Q6. 帧对象中 globals 字段的核心作用是什么?

单选题 · mid

  • A.记录当前函数的局部变量
  • B.提供模块级全局变量的访问← 正确答案
  • C.保存 GPU 上的张量数据
  • D.指向下一个待执行帧

答案:B。globals 字段保存模块级全局变量表,函数体内对全局变量的读取都要通过它完成。locals 才对应局部变量,f_back 指向前一个帧,GPU 张量数据则属于运行时值对象。

Q7. 以下哪些条件变化会导致守卫失效?

多选题 · mid

  • A.张量形状发生改变← 正确答案
  • B.张量数据类型改变← 正确答案
  • C.张量所在设备改变← 正确答案
  • D.训练轮数数字增大

答案:ABC。守卫本质上约束的是“编译所依赖的环境”,shape、dtype、device 都会改变算子选择和内存布局,因此必须失效重编。训练轮数这样的普通 Python 整数如果没有被追踪进代码逻辑,就不会触发守卫变化。

Q8. 为什么高 miss 率会让 torch.compile 的性能劣于 eager 模式?

单选题 · hard

  • A.编译产物会被系统自动删除
  • B.追踪编译开销大于省下的收益← 正确答案
  • C.GPU 会在编译时降低频率
  • D.缓存在未命中后彻底失效

答案:B。加速比 = eager 耗时 /(追踪耗时 + 运行耗时)。当 miss 频繁时,追踪耗时被反复叠加到每次迭代上,最终导致整体时间超过 eager。编译产物不会自动删除,GPU 也不会因此降频。

Q9. TorchDynamo 生成新字节码回填的核心目的是什么?

单选题 · hard

  • A.让 Python 帧能够调用编译后的子图← 正确答案
  • B.让解释器绕过守卫直接执行
  • C.让原始 Python 源码被永久覆盖
  • D.让模型权重从显存搬运到内存

答案:A。追踪完后,原始帧的执行路径需要被替换为“调用编译子图”的新逻辑,因此 Dynamo 会生成新的字节码对象交给帧执行。守卫不会绕过,源码文件不会被改写,权重搬运与字节码回填无关。

Q10. Tuning 帧评估性能时,哪些做法符合最优实践?

多选题 · extreme

  • A.固定 batch size 与序列长度← 正确答案
  • B.保持张量 dtype 全程稳定← 正确答案
  • C.使用 TORCH_LOGS 定位断点← 正确答案
  • D.每轮随机改变中间层维度

答案:ABC。固定输入形状和 dtype 能提高守卫命中率,日志工具能定位 Graph Break 源头,这些都是工程实战的推荐做法。随机改变中间层维度会让守卫频繁失效,导致反复重编译,是典型的反面操作。