清晨,你坐进车里,打开导航。目的地是公司,导航用一秒就规划好了一条完整路线——全程多少公里、哪个路口转弯、哪里上高架都清清楚楚。这就像 TorchDynamo 拿到你的 Python 模型后,把整段代码“看”一遍,编译成一张完整的计算图。可车开到第三个路口,前方忽然竖起施工牌:“前方道路封闭,请绕行。”导航事先根本不知道这里会施工——这条路能不能走,要等你开到跟前才知道。Python 代码里的动态控制流正是这样的“施工牌”:if x > 0: 里的 x 是运行时才知道的张量值,编译器没法提前预测该走哪条支路。 导航的应对方式,就是本文的主角——图重建(Graph Rebuild):它记录你的当前位置,在施工点果断切断原有路线,立刻重新规划一段绕行路线,再把你接回主路。每经过一个路口,导航还会默默核对:“你还在我预期的路线上吗?”——这就是 Guard 守卫机制。整个故事里,藏着 TorchDynamo 在“动态 Python 世界”与“静态编译优化”之间搭桥的全部智慧:图断点、子图重建、守卫校验、缓存失效与重编译,环环相扣。读懂这套机制,你就能理解编译引擎为什么“时而聪明、时而绕路”,以及怎样写出让编译器少绕路的模型代码。

岗位关联

掌握图重建原理,你就能在模型编译变慢、动态 shape 拖垮推理延迟时,快速定位“断点藏在哪”并给出精准优化方案——这是 AI 编译器工程师与推理调优工程师的看家本领。

目录
  1. 可视化演示
  2. 图断点机制
  3. 子图重建流程
  4. 守卫机制(Guard)
  5. 重编译执行
  6. 性能代价分析
  7. 减少重建策略
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示TorchDynamo的guard守卫机制触发图重建、重追踪编译流水线及缓存特化复用策略,支持步进切换与参数调节。

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

Guard守卫与图重建触发 Dynamo首次执行时AST追踪生成FX图 Guard条件不匹配时触发图重建 guard(input.shape==specialized) Guard机制确保已编译图的正确性。

图断点机制

导航规划路线时会遇到两种路:确定能走的,和必须看实时路况才能定的。聪明的导航会把“实时路况部分”单独标记出来,先规划确定的部分。TorchDynamo 也一样——它逐条分析 Python 字节码,凡是不依赖运行时数据的算子全部收进当前子图;一旦遇到“要等运行时才能知道结果”的控制流,它就停住脚步说:这一段我只能先编到这里了。

什么代码会触发“停下”?最典型的是依赖张量内部值的条件分支,比如 if loss.item() > 0.5: 或 if x.sum() > 0:,以及动态循环 while t < x.shape[0]。这些条件的真假取决于具体输入,编译期无法静态展开。Dynamo 的字节码分析器就在这里发出图断点(Graph Break)信号,把捕获过程硬生生切成两段。

技术层面,Dynamo 内部维护着一张不断增长的 FX 图。遇到断点时,它停止接收新算子,把当前图封口成一个 GraphModule,并记录“接下来从哪条指令继续”。这个断点既是分界线,也是两个子图之间的接驳口——第一段的输出张量将在这里交给第二段作为输入。

回到导航的故事:施工牌就是图断点,导航不会因为一段路施工就放弃整个行程,它只是把路线分成“施工前”和“施工后”两段分别处理。图断点同理——它不是错误,而是编译器的主动妥协,是为了在动态代码面前保住“能优化的部分尽量优化”的能力。

Gtotal=G1∪G2∪{break},G1∩G2=∅\displaystyle\mathcal{G}_{\text{total}} = \mathcal{G}_1 \cup \mathcal{G}_2 \cup \{\text{break}\} ,\qquad\mathcal{G}_1 \cap \mathcal{G}_2 = \varnothing
这张图说明:一次理想的完整编译被断点拆成了两段,两段各自独立、互不共享内部结构。
  1. 整图 \mathcal{G}_{\text{total}}理想状态下 Dynamo 想一次性捕获的完整计算图,相当于导航出发前规划的“全程一条线”路线。
    Gtotal\mathcal{G}_{\text{total}}
  2. 前段子图 \mathcal{G}_1图断点之前已经被成功捕获的那部分算子,相当于你已经走完且完全符合预期的路段。
    G1\mathcal{G}_1
  3. 后段子图 \mathcal{G}_2断点之后需要重建的那部分算子,相当于施工点之后尚未确定怎么走的道路。
    G2\mathcal{G}_2
  4. \{\text{break}\}图断点本身,一个特殊的边界标记,它既不属于 \mathcal{G}_1 也不属于 \mathcal{G}_2。
    break\text{break}
  5. 并集与交集两段子图并起来恰好覆盖完整逻辑,但交集为空——它们各自独立编译、独立优化,这正是后续性能代价的根源。
    G1∪G2=Gtotal∖{break},G1∩G2=∅\mathcal{G}_1 \cup \mathcal{G}_2 = \mathcal{G}_{\text{total}} \setminus \{\text{break}\}, \quad \mathcal{G}_1 \cap \mathcal{G}_2 = \varnothing

子图重建流程

施工点前,导航迅速记录当前位置,立刻调用路径规划算法,以当前位置为起点重新计算绕行路线。这个“重新计算”的动作,就是图重建的本体。步骤并不复杂:旧路线作废,新路线从脚下生成。

Dynamo 的图重建分三步走:先把已捕获的第一段子图保存好;然后在断点处新建一张空白 FX 图,把第一段的输出张量注册成新图的输入;最后从断点后的第一条字节码继续捕获,新算子源源不断加入这张新图。

细节上,每个子图都会被封装成 torch.fx.GraphModule,再交给后端编译成可执行的算子序列。运行时,主执行流像接力一样调用它们:先跑图1,拿到输出张量,再喂给图2。图与图之间没有共享中间表示——这正是“图重建”与“一次编译整图”的本质区别。

导航重新规划的绕行路线,虽然绕了一段,但依然是从“当前位置”通往目的地的一条完整路径。Dynamo 重建的第二段子图也一样:它从断点出发,走到函数结尾或下一个断点,形成一条新的、可编译的路线。一整条“动态代码路线”就这样被拆成若干段静态子图,一段段编译、一段段执行。

G2=Build( inputs=Outputs(G1),  start=break+ )\displaystyle\mathcal{G}_2 = \text{Build}\Big(\,\text{inputs} = \text{Outputs}(\mathcal{G}_1),\;\text{start} = \text{break}^{+}\,\Big)
新子图不是从零开始的孤儿,而是“继承”了上一段子图的输出,从断点之后的指令起步。
  1. Build(\cdot)Dynamo 在断点之后重新启动的图捕获与编译流程,是图重建的本体,对应导航的路径重规划算法。
    Build\text{Build}
  2. Outputs(\mathcal{G}_1)第一段子图的输出张量集合,即接力赛中第一棒运动员手里的交接棒。
    Outputs(G1)\text{Outputs}(\mathcal{G}_1)
  3. inputs = Outputs(\mathcal{G}_1)新子图的输入必须精确对齐上一段子图的输出,错一个张量都会导致执行断裂或结果错误。
    inputs=Outputs(G1)\text{inputs} = \text{Outputs}(\mathcal{G}_1)
  4. break^{+}紧挨着图断点的下一条字节码指令,是图2捕获的起点。
    break+\text{break}^{+}
  5. start = break^{+}新子图从断点后的第一条指令开始,保证不漏算子、也不重复捕获。
    start=break+\text{start} = \text{break}^{+}

守卫机制(Guard)

导航最厉害的一点,是它会持续核对“你还在预期路线上吗”。出发时它记住了路线上的每个路口特征;每到一个路口,它把 GPS 实际位置与预期轨迹对比,一旦偏差过大立刻重新规划。TorchDynamo 的 guard,就是编译版本的“GPS 位置核对”。

编译某段子图时,Dynamo 会捕捉输入张量的一些静态属性——形状(shape)、数据类型(dtype)、设备(device)——把它们包装成一组可执行的 guard 条件,绑定在缓存图上。比如一个子图在 (8, 3, 224, 224) 的输入下编译完成,guard 里就会有一条 shape == (8, 3, 224, 224) 的记录。

每次调用编译后的函数时,Dynamo 先执行这组 guard。全部通过 → 直接使用缓存图;任一失败 → 说明当前输入偏离了编译时的假设,缓存图不再安全,进入重编译流程。guard 是“正确性与性能之间的安全气囊”:没有它,缓存图会在 shape 变化的瞬间默默产出错误结果。

路上没施工时,导航不也一路念叨“请沿当前道路继续行驶”?这正是 guard 的价值——它不只为“本次成功”设计,更为“下次万一变化”兜底。编译器无法预知未来输入,只能靠守卫在每时每刻确认:这张图还适用于现在的输入吗?

\displaystyle\text{Guard}(\mathcal{G}) = \bigwedge_{i=1}^{k} c_i(x) ,\qquadc_i(x) : \big( \text{shape}(x_i) = s_i \big) \wedge \big( \text{dtype}(x_i) = d_i \big)
守卫条件是一串“并且”关系:所有条件都成立,缓存图才可复用。
  1. Guard(\mathcal{G})与该编译子图绑定的守卫条件组,相当于导航为这条路线记录的“所有路口特征清单”。
    Guard(G)\text{Guard}(\mathcal{G})
  2. \bigwedge_{i=1}^{k}对所有 k 个条件做逻辑与,任何一个不满足整体就失败。
    ⋀i=1k\bigwedge_{i=1}^{k}
  3. c_i(x)第 i 个守卫条件,描述第 i 个输入 x_i 在编译时观察到的某个静态属性。
    ci(x)c_i(x)
  4. shape(x_i) = s_i形状守卫,要求新输入的 shape 与编译时完全一致。
    shape(xi)=si\text{shape}(x_i) = s_i
  5. dtype(x_i) = d_i数据类型守卫,要求新输入的类型与编译时一致(如 float32 不能变 float64)。
    dtype(xi)=di\text{dtype}(x_i) = d_i

重编译执行

假如导航核对后发现你真的偏离了路线,比如错过了一个转弯,它会立刻放弃旧路线、重新规划一条新路线。这个过程费几十秒时间,但总比一路错下去强。编译世界里,guard 失败就是“错过转弯”。

当 guard 失败,Dynamo 会完成四件事:第一,把失效的缓存图标记为不可用;第二,在断点处重新启动一次“捕获 → 重建 → 编译”的完整流程;第三,用新编译结果替换旧缓存;第四,从新图中继续执行。整个过程中,用户代码没有任何感知,就像导航默默重算路线一样。

值得强调的是,重编译不是每次调用都发生。只要 guard 通过,执行走的就是“检查 + 缓存命中”的快路径;重编译是间发事件,频率等于 guard 失败概率。一次调用的平均耗时可以精确建模:固定检查开销 + 图执行时间 + 重建概率 × 重建耗时。

导航重新规划后的新路线也会被记住——同一个路口再走错,它会直接给出上次修正后的方案。Dynamo 的编译缓存正是这样:每次重建都让缓存更新,让“见过的场景”越来越快地通过 guard。这就是为什么动态模型在预热(warm-up)后性能会明显改善。

\displaystyleT_{\text{avg}} = T_{\text{guard}} + T_{\text{run}} + f_{\text{fail}} \cdot T_{\text{rebuild}}
平均耗时由三部分组成:守卫检查、缓存图执行、以及偶尔发生的重建开销。
  1. T_{\text{avg}}调用编译后函数的平均耗时,衡量图重建方案的整体性价比。
    TavgT_{\text{avg}}
  2. T_{\text{guard}}每次进入缓存图前执行 guard 检查的固定开销,类似导航每秒钟的位置核对。
    TguardT_{\text{guard}}
  3. T_{\text{run}}缓存图真正执行一次的耗时,即沿已规划路线行驶的时间。
    TrunT_{\text{run}}
  4. f_{\text{fail}}guard 失败的概率,与输入动态性成正比——输入越“跳脱”,失败越频繁。
    ffailf_{\text{fail}}
  5. T_{\text{rebuild}}一次完整图重建的耗时,包括重新捕获、重新编译,是公式里最贵的一项。
    TrebuildT_{\text{rebuild}}

性能代价分析

绕路当然有代价:多烧的油、多花的时间。图重建同样有三笔不得不付的账单,而且第一笔就让人肉疼。

第一笔,编译开销。每次 guard 失败都要重新捕获加编译,耗时可能高达数百毫秒甚至几秒。第二笔,guard 检查开销。每次调用都多了一层条件判断,虽然单次微秒级,但高频调用时不容忽视。第三笔,也是初学者最容易忽略的——优化机会损失:整图编译时,编译器可以跨算子做算子融合、内存规划、布局优化;一旦切成两张子图,跨断点的优化全部失效。

用数学语言说:单独优化图1、图2 的收益之和,通常小于优化整张图的收益。这中间的差值,就是切图交的“税”。

所以图断点不是免费的“功能”,而是编译器在动态代码面前的“最优妥协”:要么为了完全静态化而拒绝动态代码,要么接受动态代码并承担切图代价。理解这笔账,你就能解释很多“为什么 torch.compile 之后反而更慢了”的现象——大概率就是图断点太多、图切得太碎。

Δopt=Opt(G1)+Opt(G2)−Opt(Gwhole)\displaystyle\Delta_{\text{opt}} = \text{Opt}\big(\mathcal{G}_1\big) + \text{Opt}\big(\mathcal{G}_2\big) - \text{Opt}\big(\mathcal{G}_{\text{whole}}\big)
这个差值通常是负数:图切得越碎,跨图融合越难发生,优化损失越大。
  1. Opt(\mathcal{G}_{\text{whole}})不切图时全局优化的理想收益,相当于一条高架直达的最高通行效率。
    Opt(Gwhole)\text{Opt}(\mathcal{G}_{\text{whole}})
  2. Opt(\mathcal{G}_1)第一段子图单独编译能获得的局部优化,比如段内算子融合。
    Opt(G1)\text{Opt}(\mathcal{G}_1)
  3. Opt(\mathcal{G}_2)第二段子图单独编译能获得的局部优化。
    Opt(G2)\text{Opt}(\mathcal{G}_2)
  4. \Delta_{\text{opt}}两者之差,即因图断点牺牲掉的优化空间,是切图交的“过路费”。
    Δopt\Delta_{\text{opt}}
  5. 符号的意义一般 \Delta_{\text{opt}} < 0,说明局部优化之和拼不过全局优化——这正是编译器最不愿看到、又不得不接受的结果。
    Δopt<0\Delta_{\text{opt}} < 0

减少重建策略

老司机跑熟路时,会主动避开常年施工的路段,宁可多等一个红绿灯,也不愿被堵在高架上。写模型代码也一样,眼光放远一点,就能让编译器少“开沟”。

实践中减少图重建,核心思路是“让 guard 条件更稳定、让断点更少”。四个常用策略缺一不可:静态化改写——把依赖张量值的条件改成 Python 布尔量,让决策发生在子图外;守卫条件简化——固定输入 shape(padding 或 mark_dynamic),避免 shape 频繁抖动;循环展开补偿——把固定次数的 for 循环显式展开,消除动态循环这一断点源;断点合并归一——把函数划分为“动态部分”和“静态部分”,让静态大块合成单图,减少断点总数。

这些策略的优化目标,就是最小化一段运行周期内的图重建次数。写代码时多为编译期考虑一点,运行时的 guard 就少失败一次。

好的司机不是反应快,而是预判准;好的模型代码也不是“跑得快的单段”,而是“编译器友好”的结构:断点少、guard 稳、子图大。掌握了减少重建的策略,你就能真正驾驭 TorchDynamo,而不是被它偶尔的“重新规划”吓一跳。

min⁡  Nrebuild=min⁡∑t=1T1[ ¬Guardt ]\displaystyle\min \; N_{\text{rebuild}} = \min \sum_{t=1}^{T} \mathbb{1}\big[\, \neg \text{Guard}_t \,\big]
优化目标一句话:让“guard 失败”这件事发生的次数尽可能少。
  1. N_{\text{rebuild}}T 次调用内触发图重建的总次数,是衡量动态代码质量的核心指标。
    NrebuildN_{\text{rebuild}}
  2. \sum_{t=1}^{T}对每一次调用累加,从第 1 次数到第 T 次。
    ∑t=1T\sum_{t=1}^{T}
  3. \mathbb{1}[\cdot]指示函数,括号里的命题为真记 1、为假记 0,相当于“这次是否走了弯路”的计数器。
    1[⋅]\mathbb{1}[\cdot]
  4. \neg \text{Guard}_t第 t 次调用时 guard 不成立,即守卫条件被违反。
    ¬Guardt\neg \text{Guard}_t
  5. \min最优化的目标——通过重构让重建总次数最小,而不是消除所有动态性。
    min⁡\min

AI 应用场景

场景一:LLM 推理服务中的动态序列长度

阶段:推理部署与性能优化

解决的问题:生成式模型每步输出的 token 数量变化,导致序列长度持续动态变化。直接把模型丢给 torch.compile 后,每次长度变化都会让 shape guard 失效,触发整图重编译,首 token 延迟瞬间飙升。这就像导航规划好的路线,因为每个路口车流量都不同而反复重算。

具体案例:某文本生成服务把输入 padding 到 8 的倍数,并用 torch._dynamo.mark_dynamic 声明“长度 ≤ 2048”这一更宽松的 guard 条件,使缓存图在长度变化时仍然命中。图重建从每周上千次降到接近零,端到端吞吐提升约 1.8 倍。

场景二:强化学习仿真训练中的决策分支

阶段:训练循环优化

解决的问题:强化学习 policy 常依据 action 标量走不同分支,主干网络和各分支本可静态化,但“选哪条分支”编译期未知,导致图断点把网络切得七零八落,训练吞吐下降。这相当于导航不知道你出门是左转还是右转,只能把每条路都单独规划。

具体案例:工程师把分支决策上提到 Python 层,写成 if action < threshold: subgraph_A(x) else: subgraph_B(x),并用 torch._dynamo.disable 标注不参与编译的辅助函数。图断点由 6 处降为 1 处,训练 step 吞吐提升约 40%。

场景三:编译器诊断与性能归因

阶段:性能分析与调优

解决的问题:模型编译后反而变慢,传统 profiling 工具看不到“图重建次数”这类编译器内部事件,问题难以定位。

具体案例:开发者用 torch._dynamo.explain 对模型做诊断,发现某函数产生了 27 个图断点,元凶是一个张量标量比较 loss > 0.5。改成 Python 侧判断后断点降到 2 个,端到端推理加速比从 0.9x 提升到 2.4x。

Python 演示

python35 行
import torchimport torch._dynamo  def dynamic_fn(x, flag):    # 这段张量运算可被静态捕获    a = x * 2    # flag 来自运行时,Dynamo 无法静态展开 -> 图断点    if flag:        a = a + 1    else:        a = a - 1    # 断点之后重新形成可捕获的子图    return a.relu()  def main():    # 清空编译缓存,便于观察完整流程    torch._dynamo.reset()    x = torch.randn(3, 3)     # 编译入口:遇到动态分支时自动完成子图重建    compiled = torch.compile(dynamic_fn, backend="eager")     for i in range(5):        flag = (i % 2 == 0)        out = compiled(x, flag)        # eager 参考结果,用于验证重建后的图没有破坏正确性        ref = dynamic_fn(x, flag)        ok = torch.equal(out, ref)        print("iter", i, "flag =", flag, "result ok:", ok)  if __name__ == "__main__":    main()
目的:演示 TorchDynamo 遇到动态控制流时自动完成图断点检测与子图重建,并用 eager 参考结果验证重建后的语义正确性。
  1. if flag:运行时标量条件,Dynamo 无法静态展开,触发图断点
  2. torch._dynamo.reset()清空编译缓存,确保演示从第一次编译开始
  3. compiled = torch.compile(dynamic_fn, backend="eager")官方编译入口,eager 后端便于快速演示
  4. out = compiled(x, flag)每次 flag 切换,guard 重新校验,必要时触发子图重建
  5. ok = torch.equal(out, ref)用 eager 结果校验编译语义一致,确认图重建正确

常见误区

误区网上有人说“加了 torch.compile 之后模型反而更慢了”,于是认为图重建意味着每次调用都要重新编译,编译优化形同虚设。

正解图重建只在 guard 失败时发生。绝大多数调用中,输入 shape、dtype 与编译时一致,guard 通过,走的是缓存命中快路径。真正需要重建的只是“形状变了、条件变了”的少量调用。就像导航只在走错路或路线变化时才重新规划,正常行驶时它只是安静地播报路况。

为何常见:初学者看到重编译日志里的"Recompiling"字样,误以为是常态;再加上网上案例常常只展示最坏情况,容易以偏概全。

误区既然 Dynamo 会把每段子图都单独优化,那断点越多、子图越碎,岂不是每段都能被精细处理,优化效果更好?

正解恰恰相反。每个断点都阻断跨子图的算子融合、内存规划和后端代码生成,相当于在两段路之间立了一堵墙,墙两侧的优化互不相通。断点越多,Δ_opt 的负值越大,性能损失越严重。编译器真正喜欢的是“大而完整”的图,断点只是面对动态代码的不得已妥协。

为何常见:把“程序模块化”的直觉迁移到了编译图上,却忘了编译器优化最需要的是全局视野,而不是局部精致。

误区觉得每次调用都要跑一遍 guard 检查太浪费,干脆把 guard 全部关掉,性能不就上来了?

正解guard 是正确性保障,本质上是一份“缓存图适用条件说明书”。去掉 guard,意味着输入 shape 或 dtype 变化时,缓存图仍然被直接复用,计算结果将会是错的——比慢更可怕。Dynamo 的哲学是“宁可多查一次,不可错算一次”;guard 的开销与它防止的错误相比,微乎其微。

为何常见:人们习惯把运行时检查理解为“防御性代码”,在追求极致性能时优先砍掉,却忽略了它守护的是缓存复用这一核心加速机制。

误区以为 `torch.compile` 是万能加速器,包上之后无论代码多动态都能自动优化,不可能比 eager 慢。

正解torch.compile 对静态代码几乎总能带来收益,但对高度动态的代码,图断点会把模型切成碎片,重建编译成本甚至可能超过优化收益,最终出现“编译后比 eager 还慢”的反转。正确做法是配合本章的策略改写代码——静态化分支、固定 shape、合并断点——让编译器发挥真正实力。

为何常见:框架宣传中强调“一行代码加速”,让人忽略了 torch.compile 是一个需要“编译器友好代码”配合的优化器,而不是魔法。

面试题

Q1. TorchDynamo 遇到图断点后,系统会怎样处理?

单选题 · easy

  • A.直接报错终止整个编译
  • B.中断当前图,重建新子图← 正确答案
  • C.忽略动态代码继续执行
  • D.把整个模型转为解释模式

答案:B。Dynamo 遇到图断点不会终止,而是保存已捕获的第一段子图,在断点处重建新子图并继续编译,类似导航遇施工后重新规划路线。选项 A 是“放弃”、C 是“硬闯”、D 是“整体降级”,都不符合 Dynamo 的“分段编译、整体衔接”策略。

Q2. 关于 Guard 守卫机制的作用,下列说法正确的是?

多选题 · mid

  • A.检查输入是否满足已缓存图的捕获条件← 正确答案
  • B.保证编译后的图不比解释执行慢
  • C.条件不满足时会触发重新编译← 正确答案
  • D.Guard 能消除 Python 解释开销

答案:AC。guard 记录编译时的输入属性,运行时先校验再决定是否复用缓存图,条件不满足则触发重编译。B 错在编译不一定比解释快,guard 检查与重建开销可能反超;D 错在 guard 是校验机制而非解释器,它消除不了 Python 层面的解释开销。

Q3. 当 Guard 检查失败时,TorchDynamo 最合理的动作是什么?

单选题 · mid

  • A.直接复用旧图继续执行
  • B.丢弃失效图并重新编译新子图← 正确答案
  • C.回退到 eager 模式且不再优化
  • D.抛出一个运行时异常

答案:B。guard 失败说明缓存图的前提条件已不成立,Dynamo 会丢弃失效图并重新编译该段子图。A 会产生错误结果,C 是杀鸡取卵,D 会让动态代码直接崩溃。只有重编译既保证正确性,又保留后续优化潜力。

Q4. 以下哪种代码结构最容易触发图重建?

单选题 · mid

  • A.依赖张量内部数据的 if 分支← 正确答案
  • B.固定次数的 for 循环
  • C.全部张量的乘加运算
  • D.只看张量形状的断言

答案:A。依赖张量内部值(如 x.sum() > 0)的分支要运行时才知道结果,编译器无法静态展开,必然产生图断点。固定循环、纯张量乘加都能被静态捕获,形状断言通常在编译期即可判定。

Q5. 图重建会带来哪些性能影响?

多选题 · hard

  • A.增加一次子图编译的时间← 正确答案
  • B.阻断跨子图的算子融合优化← 正确答案
  • C.每次执行都必然触发重建
  • D.多次重建加大编译开销← 正确答案

答案:ABD。每次重建都要重新捕获与编译,消耗编译时间;断点两侧的算子融合与内存规划失效;多次重建会累积出可观的编译开销。C 错在只要 guard 通过就复用缓存图,根本不会进入重建流程。

Q6. 图断点通常发生在什么位置?

单选题 · easy

  • A.两段静态代码的图分界处← 正确答案
  • B.整个模型输入的起点
  • C.反向传播的最后阶段
  • D.CUDA 内核启动的位置

答案:A。图断点发生在两段可静态捕获代码的边界:断点之前的部分已被完整捕获,断点之后等待重建。输入起点、反向传播末尾和 CUDA 内核位置都不是“动态控制流”造成的边界。

Q7. 想减少图重建次数,哪种做法最有效?

单选题 · mid

  • A.用 Python 标量替换张量条件← 正确答案
  • B.把多个小模型合并成一个
  • C.增大 batch size 提升吞吐
  • D.使用更大的学习率数值

答案:A。把张量条件改成 Python 布尔量,让决策发生在子图外,这是消除断点的最直接方法;合并模型、调 batch size、调学习率与图断点数量没有直接关系。

Q8. 关于图重建中的 Guard 条件,以下说法正确的是?

多选题 · mid

  • A.记录编译时输入张量的形状等属性← 正确答案
  • B.运行时用 guard 条件校验新输入← 正确答案
  • C.条件满足时可直接复用缓存图← 正确答案
  • D.Guard 只在训练时启用

答案:ABC。guard 记录编译时观察到的输入属性,运行时逐条校验,全部满足即可复用缓存图,不满足才进入重建。训练和推理都会使用 guard,D 的说法没有依据。

Q9. TorchDynamo 图重建后,新旧子图是如何衔接的?

单选题 · hard

  • A.从图断点处通过输出输入张量桥接← 正确答案
  • B.用随机数初始化新子图
  • C.丢弃第一段图的所有输出
  • D.每一段图都重头捕获完整模型

答案:A。第一段子图的输出张量作为第二段子图的输入,在断点处无缝桥接,如同接力跑中交棒。B 和 C 都会丢失关键数据,D 则意味着完全不需要断点机制,与图重建的设计初衷矛盾。

Q10. 图重建机制最终保证了什么?

单选题 · extreme

  • A.让动态代码也能获得正确加速← 正确答案
  • B.编译缓存占用内存最小化
  • C.所有 Python 代码都被转换为 CUDA
  • D.模型精度高于 eager 模式

答案:A。图重建的根本目标是让动态控制流代码在 guard 正确性保障下,尽可能获得编译加速——把“动态的路”拆成若干段“静态的路”分别优化。B 是优化方向而非目标,C 不现实(并非所有 Python 语义都能落成 CUDA),D 错在编译不改变数值精度,它与 eager 等价而非更高。