想象一家参加美食大赛的餐厅,主厨就是 PyTorch——他的母语是 Python,刀工行云流水,随手就能颠勺、淋油、改配方。为了出菜更快,后厨提出了两种提速方案。 方案一由「誊写员」负责:他要求主厨把每道招牌菜都用标准工艺卡重写一遍,不能临场发挥,只能使用有限的固定动作。誊写完成后,帮厨照着卡片就能做出几乎一样的菜,还能打包到任何厨房。代价是:主厨的即兴手法、动态创意统统写不进卡片。这套方案就是 TorchScript。 方案二由「跟拍观察员」负责:他完全不动主厨的菜谱,只是站在旁边看主厨做菜,默默记录哪道菜被反复点单(热点子图),并画成一张小抄(计算图)。下次再点同一道菜,观察员先快速核对主料、锅具、火候是否与上次一致(guard 校验),一致就照着加速小抄直接出菜;稍有变化,立刻把活儿交还主厨。这套方案就是 TorchDynamo。 今天这篇文章,我们把这两套方案放在同一张案板上细细对比:它们各自擅长什么、限制在哪、工程上如何选型,以及未来会走向何方。

岗位关联

理解 TorchDynamo 与 TorchScript 的分工,能帮你在训练加速与部署优化的实际工作中做出正确选型,避免「优化反而变慢」的尴尬。

目录
  1. 可视化演示
  2. 双轨编译理念
  3. TorchScript 特性
  4. 关键差异对比
  5. 性能优化策略
  6. 工程实践选型
  7. 未来演进趋势
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

对比TorchScript的tracing与scripting捕获与TorchDynamo的帧评估捕获机制、在Python支持控制流guard序列化性能等维度的特性差异、以及两者在训练加速场景下的性能对比。

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

三种捕获机制对比 tracing记录执行路径无法捕获控制流 Dynamo帧评估支持完整Python语法 Trc<Scr<Dyn 捕获能力逐级增强

双轨编译理念

回到那家正在装修的后厨。方案一的誊写员抱着标准工艺卡,方案二的跟拍观察员揣着空白小抄本,两人同时看向主厨。核心分歧一句话就能说完:要不要改变主厨的做菜方式?

TorchScript 的回答是「要」——把代码限制在可静态分析的子集里,编译器才能安心优化;TorchDynamo 的回答是「不要」——保持 Python 原生语义,编译器自己去观察执行过程。一个要求厨师迁就流程,一个让流程迁就厨师。

TorchDynamo 的观察能力来自 PEP 523 提供的解释器钩子 PyEval_SetProfile。它在 Python 执行每个字节码时都能插入回调,相当于观察员的眼睛始终盯着主厨的手。TorchDynamo 逐条分析字节码:发现 torch.nn.Linear、relu 这类可图化算子,就收进一张 FX Graph;遇到 list.append、自定义 Python 对象等无法图化的操作,就触发 graph break,把当前子图封口,之后继续捕获下一段。首次执行像第一次跟拍,开销较大;之后 guard 快速校验输入 shape、dtype,命中就走编译图,未命中则回退 eager。

观察员的小抄越攒越多,主厨亲自动手的次数就越来越少——这,就是即时捕获的本质:不改变主厨,只改变等待。

Ttotal=Tcapture+Tguard+Tgraph+TrestT_{\text{total}} = T_{\text{capture}} + T_{\text{guard}} + T_{\text{graph}} + T_{\text{rest}}
  1. $T_{\text{capture}}$ 捕获开销首次执行时分析字节码并构建 FX 图的开销,只在第一次调用出现。
    Tcapture=cost(bytecode analysis)T_{\text{capture}} = \text{cost}(\text{bytecode analysis})
  2. $T_{\text{guard}}$ 校验开销之后每次调用时校验输入 shape、dtype 等属性是否与已编译图一致的开销。
    Tguard=cost(shape/dtype check)T_{\text{guard}} = \text{cost}(\text{shape/dtype check})
  3. $T_{\text{graph}}$ 图中执行命中缓存后,在优化计算图上执行的时间,通常远小于逐算子 eager 执行。
    Tgraph≪TeagerT_{\text{graph}} \ll T_{\text{eager}}
  4. $T_{\text{rest}}$ 回退时间guard 未命中时回退到 eager 模式逐算子执行的时间,动态模型中常大于零。
    Trest=1guard miss⋅TeagerT_{\text{rest}} = \mathbb{1}_{\text{guard miss}} \cdot T_{\text{eager}}

TorchScript 特性

方案一的誊写员正式上岗。他要求主厨把菜谱写成标准工艺卡,限定动作集合:只能洗、切、焯、炒、炸,不能即兴抛锅、不能随意换料。主厨虽有怨言,但誊写员承诺:只要照他的格式写,任何厨房都能复刻这道菜。

TorchScript 给主厨提供了两种誊写方式。torch.jit.trace 是「拍成品照」:你现场做一盘菜,他根据最后的成品倒推出流程图;torch.jit.script 是「读菜谱」:他直接看你写的源码,翻译成一份结构化的中间表示(IR)。前者省事但只认结果,后者难写但能懂逻辑。

trace 的问题在于「只认结果不认逻辑」——示例输入里 if 走过分支 A,图里就永远只有 A;循环跑了 3 次,图里就固化 3 次。script 更聪明,支持 if/for 等控制流,但前提是代码必须落在 TorchScript 语言子集里:类型需可推断,大量 Python 容器操作不被支持。转换成功后得到 TorchScript IR,可以序列化成 .pt 文件,交由 C++ 运行时加载,完全不依赖 Python 解释器。

工艺卡的短板也显而易见:主厨的松露刨花、分子料理这些「动态创意」一个都写不进去。TorchScript 的稳定来自「可预测」,代价也是「可预测」。

P∈LTS  ⇒  ∃ IR(P)P \in \mathcal{L}_{\text{TS}} \;\Rightarrow\; \exists\, \text{IR}(P)
  1. $P$ 用户代码用户编写的模型代码,即主厨的原始菜谱。
    P=user’s Python moduleP = \text{user's Python module}
  2. $\mathcal{L}_{\text{TS}}$ 语言子集TorchScript 支持的静态语言子集,包含张量运算与受控的 if/for。
    LTS={static tensor ops, typed control flow}\mathcal{L}_{\text{TS}} = \{\text{static tensor ops, typed control flow}\}
  3. $\text{IR}(P)$ 中间表示编译器生成的 TorchScript 图,可由 C++ 运行时直接执行。
    IR(P)=TorchScript graph\text{IR}(P) = \text{TorchScript graph}
  4. 整体含义代码必须先在受限子集中可表达,静态图编译才有意义;否则转换失败或语义丢失。
    P∉LTS⇒conversion fails or loses semanticsP \notin \mathcal{L}_{\text{TS}} \Rightarrow \text{conversion fails or loses semantics}

关键差异对比

观察员和誊写员虽然同处一厨,但方法论针锋相对。我们把这盘菜端到灯光下,从四个角度对比:捕获方式、控制流支持、调试体验、生态覆盖。

  • 捕获方式:TorchScript 离线转换(读源码或跑示例),TorchDynamo 在线监听(执行字节码时捕获)。
  • 控制流支持:TorchScript 的 trace 固化控制流、script 要求静态类型;TorchDynamo 让 Python 控制流保持原生语义,遇动态循环只做子图划分,不改变 Python 执行逻辑。
  • 调试体验:TorchDynamo 出错能退回 Python 堆栈,每个算子都能单步;TorchScript 报错在 IR 层,需要再映射回源码才能定位。
  • 生态覆盖:TorchDynamo 对 HuggingFace 这类 Python 味极浓的模型几乎是开箱即用;TorchScript 对 C++ 部署场景更亲善。

差异的根源是「何时建模」。TorchScript 在编译时把模型固化成一张全局静态图;TorchDynamo 在运行时动态拼接子图,并依赖 guard 维持图的有效性。所以同样一个带循环的模型,TorchScript 要么拒绝、要么固化,TorchDynamo 则可以把循环之外的张量运算都捕获成图,循环本身交还 Python 管理。

一个在厨房现场随机应变,一个在后台伏案整理。TorchDynamo 赢在「活得」,TorchScript 赢在「稳、可搬」。

Speedup=TeagerTguard+Tgraph\text{Speedup} = \frac{T_{\text{eager}}}{T_{\text{guard}} + T_{\text{graph}}}
  1. $T_{\text{eager}}$ 基线耗时完全不加编译,逐算子启动 Python 层计算的总耗时。
    Teager=∑iTopi+n⋅TlaunchT_{\text{eager}} = \textstyle\sum_{i} T_{op_i} + n \cdot T_{\text{launch}}
  2. $T_{\text{guard}}$ 守卫开销每轮调用前 guard 校验输入属性的额外开销,应保持轻量。
    Tguard=∑jcj,cj∈{shape,dtype,device}T_{\text{guard}} = \sum_{j} c_j, \quad c_j \in \{shape, dtype, device\}
  3. $T_{\text{graph}}$ 图执行时间优化图在 GPU 上的执行时间,已含算子融合收益。
    Tgraph=time(fused kernel graph)T_{\text{graph}} = \text{time}(\text{fused kernel graph})
  4. 阈值条件比值大于 1 时编译才有正向收益,否则不如直接 eager。
    Speedup>1  ⟺  Tguard+Tgraph<Teager\text{Speedup} > 1 \iff T_{\text{guard}} + T_{\text{graph}} < T_{\text{eager}}

性能优化策略

观察员发现,主厨做「宫保鸡丁」时总要先切丁、再滑油、再爆炒,三个动作之间盘子端来端去,浪费时间。如果能把它们合并成一个大动作,效率会高得多。这个念头,正是算子融合的雏形。

TorchDynamo 捕获子图后,会把它交给默认后端 Inductor。Inductor 负责把图上的多个算子(比如 conv、bn、relu)融合成少数几个 Triton 内核,再编译成 GPU 代码。观察员的小抄不再记录每一个细碎动作,而是记成「颠勺」这样的组合技。

每次 GPU 内核启动都有微秒级开销,且内核之间要经过显存读写中间张量;融合后中间结果直接留在寄存器或共享内存里,既省启动又省带宽。此外 guard 机制也会刻意保持轻量:只检查 shape、dtype、device、requires_grad 等廉价属性,不去深比较张量数值,因为它要的是「快问快答」。如果输入结构频繁变化,guard 命中率下降,编译收益就会被回退成本吃掉。

小抄不再是一步步的流水账,而是「颠勺」这样的组合技。观察员把功夫越练越精,主厨的活自然越来越少。

Saving=∑i=1nTi−Tfused−(n−1) Tlaunch\text{Saving} = \sum_{i=1}^{n} T_{i} - T_{\text{fused}} - (n-1)\, T_{\text{launch}}
  1. $T_i$ 单算子耗时第 i 个算子的 GPU 内核执行时间。
    Ti=kernel time of operator iT_i = \text{kernel time of operator } i
  2. $T_{\text{fused}}$ 融合内核融合后单个内核的耗时,通常远小于各 $T_i$ 之和。
    Tfused≪∑i=1nTiT_{\text{fused}} \ll \textstyle\sum_{i=1}^{n} T_i
  3. $T_{\text{launch}}$ 启动开销每次内核启动的固定开销,GPU 上通常为微秒量级。
    Tlaunch≈10−6 sT_{\text{launch}} \approx 10^{-6}\,\text{s}
  4. Saving 意义把启动开销与中间读写省下来,才是融合收益的真实来源。
    Saving>0  ⟺  fusion reduces launches and memory traffic\text{Saving} > 0 \iff \text{fusion reduces launches and memory traffic}

工程实践选型

比赛进入白热化,点菜场景千变万化。有时客人是「固定套餐」——输入 shape 恒定;有时是「神秘盲盒」——输入结构天马行空。到底该请哪位师傅?

原则一句话:训练加速优先 TorchDynamo,静态部署优先 TorchScript。训练阶段 Python 语义很重要,torch.compile(model) 一行接入,几乎不改变代码结构;服务端推理若想摆脱 Python 解释器、在 C++ 里稳定运行,TorchScript 的 .pt 序列化仍是成熟选项。

选型时要关注 graph break 数量——一个模型如果到处 break,说明大部分算子无法进入编译图,加速收益趋近于零。同时要算编译开销的摊薄账:模型很小、调用次数很少,编译开销可能远超收益。实践中常采用混合策略:训练用 TorchDynamo 加速,部署用 torch.export 导出静态图,再交给 TorchScript 或 AOTInductor 运行。

选型就像点菜:固定的商务套餐让誊写员打包最稳妥,变化多端的创意料理还得靠观察员现场跟进。

Benefit=Speedup−TcompileN>0\text{Benefit} = \text{Speedup} - \frac{T_{\text{compile}}}{N} > 0
  1. Speedup 加速倍数单个样本的加速倍数,来自编译图与算子融合。
    Speedup=TeagerTcompiled\text{Speedup} = \frac{T_{\text{eager}}}{T_{\text{compiled}}}
  2. $T_{\text{compile}}$ 一次性开销包含捕获、后端代码生成与内核编译的总成本。
    Tcompile=Tcapture+TinductorT_{\text{compile}} = T_{\text{capture}} + T_{\text{inductor}}
  3. $N$ 调用次数模型总调用次数,编译开销被 N 次调用分摊。
    TcompileN→0(N→∞)\frac{T_{\text{compile}}}{N} \to 0 \quad (N \to \infty)
  4. 决策标准只有摊薄后的开销小于速度提升,采用编译才有净收益。
    Benefit>0  ⇒  adopt compilation\text{Benefit} > 0 \;\Rightarrow\; \text{adopt compilation}

未来演进趋势

观察员跟拍了半年,小抄攒了一抽屉,大家发现这些记录越来越像一套「标准动作库」。誊写员也慢慢意识到:与其逼主厨改写,不如直接采用观察员的成果。

PyTorch 2.x 已将 torch.compile 设为默认优化入口,TorchDynamo 是唯一的捕获前端。TorchScript 渐渐退出「转换主力」的舞台,转向部署执行侧。二者由 torch.export 桥接:先把 Dynamo 捕获的 FX 图导出为语义严格、边界清晰的静态图,再交付给 TorchScript 或 AOTInductor 做无 Python 运行。

工具链也在加速成熟:torch.profiler 可以直观看到每个子图是否被捕获、guard 命中率如何;AOTInductor 把编译产物直接接到 C++ 推理栈;更多后端(TensorRT、ONNX Runtime)通过同样的捕获接口接入。训练侧动态捕获与部署侧静态执行之间的沟壑,正在被一层层填平,动态 Python 开销占比持续下降。

当年互看不顺眼的观察员和誊写员,最终在「动态捕获 + 静态导出 + 部署执行」的新厨房里各就各位,合作把出菜速度推向了新高度。

Coverage=TgraphTtotal→1\text{Coverage} = \frac{T_{\text{graph}}}{T_{\text{total}}} \to 1
  1. $T_{\text{graph}}$ 图内时间落入捕获子图并经过优化的张量计算时间。
    Tgraph=∑t(optimized subgraphs)T_{\text{graph}} = \textstyle\sum t(\text{optimized subgraphs})
  2. $T_{\text{total}}$ 总时间模型端到端总执行时间,含回退与额外开销。
    Ttotal=Tgraph+Tfallback+ToverheadT_{\text{total}} = T_{\text{graph}} + T_{\text{fallback}} + T_{\text{overhead}}
  3. Coverage 覆盖率图执行时间占总时间的比例,衡量编译的有效覆盖程度。
    Coverage=TgraphTtotal\text{Coverage} = \frac{T_{\text{graph}}}{T_{\text{total}}}
  4. 趋近 1 的意义随着编译器成熟,回退部分趋近于零,几乎整个模型可在图中执行。
    lim⁡maturingCoverage=1\lim_{\text{maturing}} \text{Coverage} = 1

AI 应用场景

大语言模型训练加速

阶段:训练优化

解决的问题:大模型训练时算子密集、内核启动频繁,显存与算力浪费严重。传统逐算子 eager 执行需要为每个算子单独启动 GPU 内核,性能瓶颈明显。TorchDynamo 捕获热点子图后交给 Inductor 融合算子,能同时降低训练耗时与峰值显存。

具体案例:某团队微调 7B 语言模型,训练循环中调用 torch.compile(model, backend="inductor"),配合 guard 对固定 shape 的命中,吞吐提升约 15%-20%,显存占用减少约 10%,且模型代码无需任何改动。

无 Python 依赖的服务端部署

阶段:模型部署

解决的问题:生产环境部署模型通常不希望依赖完整 Python 解释器,否则容器体积大、启动慢、安全暴露面广。TorchScript 把模型序列化为 .pt,可在 C++ 运行时直接加载执行。

具体案例:某推荐系统将召回模型用 torch.jit.script 转换后部署到 C++ 服务,模型加载时间从秒级降到百毫秒级,推理延迟下降 30%,服务进程不再携带 Python 运行时。

NLP 动态变长输入的推理

阶段:推理服务

解决的问题:线上 NLP 请求的序列长度波动大,用固定 shape 部署会导致大量 padding 浪费;若完全不用编译又损失加速。TorchDynamo 的 guard 能感知输入 shape 变化,相近长度复用同一编译图。

具体案例:某对话系统使用 torch.compile 处理变长输入,guard 将长度相近的请求归入同一 bucket 的编译图,极端异形长度触发回退并重新编译。相比固定 padding 到最大长度的方案,平均延迟降低 25%。

Python 演示

python28 行
import torch # 带动态控制流的模型: 循环次数由输入张量决定class LoopModel(torch.nn.Module):    def __init__(self):        super().__init__()        self.fc = torch.nn.Linear(8, 8)     def forward(self, x, steps):        n = int(steps.item())              # Python 原生控制流        for _ in range(n):            x = torch.relu(self.fc(x))        return x model = LoopModel()x = torch.randn(4, 8)s4, s6 = torch.tensor(4), torch.tensor(6) # 1) TorchScript trace: 循环次数被当作常量固化traced = torch.jit.trace(model, (x, s4))print('trace steps=6 ->', traced(x, s6).sum().item(), '(仍按 4 次展开)') # 2) TorchDynamo: 保留 Python 语义, 按需捕获子图compiled = torch.compile(model, backend='eager')print('dynamo steps=6 ->', compiled(x, s6).sum().item(), '(正确 6 次)') # 3) 对照 eager 基线print('eager  steps=6 ->', model(x, s6).sum().item())
目的:用同一个带动态循环的模型,直观展示 TorchScript trace 固化循环次数与 TorchDynamo 保留 Python 语义的差异。
  1. n = int(steps.item())把输入张量转成 Python int,把循环留在 Python 层,这正是 TorchDynamo 要保护的「动态语义」。
  2. traced = torch.jit.trace(model, (x, s4))用 steps=4 作为示例输入追踪模型,循环次数被固化为 4。
  3. traced(x, s6).sum().item()传入 steps=6,但图已固化,输出仍是 4 次循环的结果,暴露 trace 的局限。
  4. compiled = torch.compile(model, backend='eager')一行接入 TorchDynamo,backend 用 eager 便于验证正确性。
  5. compiled(x, s6).sum().item()TorchDynamo 让 Python 控制流保持原生,steps=6 时正确执行 6 次循环。
  6. model(x, s6).sum().item()对照 eager 基线,确认 dynamo 结果与直接执行一致。

常见误区

误区用 torch.jit.trace 时多传几个不同 shape 的示例输入,就以为模型支持动态 shape 和动态循环了。

正解trace 是「照示例做菜」,不是「听懂菜谱」。动态分支与循环在 trace 时被当成常量固化:示例走了 if 的 True 分支,图里就永远只有该分支。需要动态控制流时,要么改用 jit.script(须满足语言子集),要么直接上 TorchDynamo,后者保留 Python 原生语义。

为何常见:trace 用起来太简单——给两个输入就行,容易让人误以为它「智能地」理解了模型逻辑。

误区以为 torch.compile 和 TorchScript 一样,要求把模型改写成受限语法,否则不能编译。

正解TorchDynamo 不要求任何源码改写。它从字节码层捕获计算图,遇到无法捕获的操作只是 graph break 回退,模型代码完全保持 Python 风格。你可以把 torch.compile 想象成观察员,他不需要主厨按他的格式做菜,他只是在一旁记笔记。

为何常见:TorchScript 的「转换 + 约束」心智太深入人心,导致初学者把这种负担投射到了 TorchDynamo 身上。

误区只要给模型套上 torch.compile,训练和推理就会自动加速。

正解编译有一次性开销(捕获 + Inductor 代码生成 + 内核编译),还有每次调用的 guard 开销。小模型、短任务、输入结构每次都变,编译成本摊不薄,收益可能为负。工程上要先做 profile,用公式 Benefit = Speedup - T_compile / N 判断是否值得。

为何常见:PyTorch 官方博客的加速数字多来自大规模训练场景,小模型的「平均收益」被掩盖了。

误区以为 Dynamo 成熟后,TorchScript 就该被彻底淘汰。

正解两者分属编译链不同位置:Dynamo 是训练/动态侧的捕获前端,TorchScript 是部署/静态侧的执行表示。torch.export 可将 Dynamo 捕获的图导出,再由 TorchScript 或 AOTInductor 执行,形成「捕获—导出—部署」链路。

为何常见:「新框架取代旧框架」是 AI 圈常见叙事,但编译器领域更常见的是职责拆分而非简单替代。

误区看到训练日志里出现 Graph break 就以为模型编译失败、代码写错了。

正解graph break 是回退机制而非错误。它表示某些 Python 操作无法图化,Dynamo 在此划分子图并交还 Python 执行,保证结果正确。真正需要担心的是 break 太多,导致捕获覆盖率低、加速不明显。可以用 torch.profiler 观察子图覆盖率。

为何常见:名字里带 break,容易让人联想到异常中断;实际上它是正常的设计边界。

面试题

Q1. TorchDynamo 捕获计算图的核心机制是什么?

单选题 · easy

  • A.解析 Python 源码并重写语言
  • B.监听字节码并捕获子图← 正确答案
  • C.替换 PyTorch 全部算子
  • D.要求用户提供示例输入

答案:B。TorchDynamo 利用 PEP 523 的解释器钩子在字节码执行时实时捕获计算图,不需要重写源码。A 是传统静态编译的思路,C 不现实,D 是 torch.jit.trace 的做法。

Q2. torch.jit.trace 在遇到动态控制流时会发生什么?

单选题 · easy

  • A.自动改写控制流结构
  • B.把分支条件固化常量← 正确答案
  • C.回退到 Python 逐算子执行
  • D.抛出运行时类型异常

答案:B。trace 是根据示例输入「拍成品照」,if/for 等控制流会被当作常量固化。它不会改写控制流(A),通常也不抛异常(D),更不会回退到 eager(C),而是默默丢失动态语义。

Q3. TorchDynamo 中 guard 机制的核心作用是什么?

单选题 · mid

  • A.防止模型过拟合训练数据
  • B.校验输入属性与图是否匹配← 正确答案
  • C.对模型权重进行加密防护
  • D.管理 GPU 显存分配策略

答案:B。guard 像观察员核对「主料、锅具、火候」,只检查输入属性(shape、dtype、device 等)是否与已编译图匹配,匹配则命中加速,不匹配则回退 eager。A、C、D 与 guard 无直接关系。

Q4. 关于 TorchDynamo 的 graph break,以下描述正确的是?

单选题 · mid

  • A.表示编译彻底失败需重装
  • B.在无法图化处回退并划分子图← 正确答案
  • C.只在推理阶段才会出现
  • D.会导致模型输出错误结果

答案:B。graph break 是正常的回退机制:遇到无法图化的 Python 操作时,Dynamo 在此划分子图,把控制权交还 Python 执行,保证结果正确。它不是错误,训练和推理都可能出现。

Q5. TorchScript 的 script 模式相比 trace 模式的优势是什么?

单选题 · mid

  • A.无需任何类型标注即可转换
  • B.支持动态控制流的源码级编译← 正确答案
  • C.运行速度必然比 trace 更快
  • D.完全兼容全部 Python 语法

答案:B。script 直接读源码翻译成 IR,能真正理解 if/for 等动态控制流;trace 只是照着示例固化路径。script 仍有语言子集限制(D 错),速度和类型标注也不是它的优势(A、C 错)。

Q6. 算子融合降低耗时的根本原因是什么?

单选题 · hard

  • A.减少算子自身的数学计算量
  • B.减少内核启动与显存读写开销← 正确答案
  • C.使用更高精度的浮点运算
  • D.降低模型的参数量与存储

答案:B。融合并不改变算子的数学计算量(A 错),而是把多个内核合并成一个,省去每次内核启动的开销和中间张量的显存往返。浮点精度(C)和模型体积(D)与融合无关。

Q7. 一个小模型共调用 10 次,编译一次性开销 0.3s,单次 eager 耗时 0.05s,编译后单次 0.02s。总收益是多少?

单选题 · mid

  • A.提速 0.2 秒收益显著
  • B.总耗时持平无收益← 正确答案
  • C.变慢 0.3 秒得不偿失
  • D.提速 0.5 秒表现优异

答案:B。eager 总耗时 = 10 × 0.05 = 0.5s;编译后总耗时 = 0.3 + 10 × 0.02 = 0.5s,恰好打平。用公式 Benefit = Speedup - T_compile / N 判断,编译开销被 10 次调用摊薄后刚好抵消收益。

Q8. 下列哪些情况下 TorchDynamo 容易出现 graph break 并回退 eager?

多选题 · mid

  • A.使用 Python 列表容器操作← 正确答案
  • B.控制流依赖张量取值← 正确答案
  • C.简单线性层堆叠模型
  • D.调用第三方自定义函数← 正确答案

答案:ABD。list.append、依赖 tensor.item() 的控制流、自定义 Python 函数都难以直接图化,会触发 graph break;而简单线性层堆叠(C)正是 TorchDynamo 最擅长捕获的场景,通常不会 break。

Q9. 下列属于 TorchScript 序列化部署优势的有哪些?

多选题 · hard

  • A.可脱离 Python 解释器运行← 正确答案
  • B.模型格式紧凑便于分发← 正确答案
  • C.支持任意 Python 第三方库
  • D.拥有稳定可复现的执行图← 正确答案

答案:ABD。TorchScript 编译为 IR 后可被 C++ 运行时加载,不依赖 Python,格式紧凑,且静态图执行行为稳定。但它受语言子集限制,不可能支持任意 Python 第三方库(C 错)。

Q10. 关于 TorchDynamo 与 TorchScript 的关系,下列说法正确的有哪些?

多选题 · hard

  • A.Dynamo 负责训练期动态捕获← 正确答案
  • B.TorchScript 可做部署期执行← 正确答案
  • C.两者互相取代不可共存
  • D.torch.export 是两者融合桥梁← 正确答案

答案:ABD。TorchDynamo 是 PyTorch 2.x 默认的训练期捕获前端,TorchScript 仍是部署侧的成熟执行后端,torch.export 将前者的 FX 图导出为静态图再交给后者执行,三者形成完整链路而非替代关系(C 错)。