厨房里,大厨写下拿手菜的菜谱,交给后厨团队照做。可菜谱是给人看的,上面写着「轻轻翻炒三十秒」这种模糊描述,谁能保证每位大厨执行得一模一样?于是后厨会先把它翻译成一份标准化工序单:「第 3 步,开火;第 4 步,下油 10 毫升」,每个动作都精确对应。Python 的世界也是如此:我们写的源码是那份写意菜谱,而字节码就是翻译后的标准化工序单。 TorchDynamo,就是站在出菜口的一位总监督员。它不修改大厨的菜谱,只是在标准化工序单被一张张递给厨师时,站在旁边仔细阅读:哪些步骤是「炒菜」(张量计算)?哪些只是「摆盘」(普通 Python 操作)?它把炒菜的步骤抄进自己随身带的优化菜谱里,下次这道菜再来,直接按优化版工序单执行,速度快了好几倍。而这一切,大厨本人毫无察觉——他一个字都不用改。 本文将带你以「字节码」为线索,看这位监督员如何做到透明、精准、零侵入的加速。读完之后你会在心里建立起一条完整的链路:源码变字节码,字节码驱动帧,帧触发钩子,钩子接管图构建,图加速整个训练。

岗位关联

对 AI 从业者而言,理解 TorchDynamo 的字节码机制等于拿到了 torch.compile 的钥匙——今后排查性能瓶颈、设计自定义编译后端,都能直击要害。

目录
  1. 可视化演示
  2. 字节码是什么
  3. 帧与代码对象
  4. eval帧钩子
  5. 指令解析
  6. 图捕获构建
  7. 守卫与重编译
  8. 透明加速
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

三步展示基于字节码的图捕获:Python字节码栈式执行(LOAD/CONST/BOP/STORE)、TorchDynamo帧评估钩子构建计算图、以及guard条件(shape/dtype/device)命中后复用缓存图。

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

字节码栈式执行 LOAD: 加载变量 CONST: 常量入栈 BOP: 二元运算 LOAD→CONST→BOP→STORE CPython虚拟机栈式执行字节码。

字节码是什么

大厨手写的菜谱叫《源码》,它方便人类阅读,却不方便机器直接照做。为了让执行更稳定,后厨会把菜谱翻译成一张《标准化工序单》,每一步都精确到编号和参数:这份工序单,就是 Python 的字节码。它是源码与底层执行之间的中间形态,由 Python 编译器在导入模块时自动生成,通常保存在 .pyc 缓存文件里。

字节码是一种「栈式指令集」,意思是每条指令都在一个小栈上操作:把食材放上案台(压栈),取出两份合并翻炒(弹出并计算),再把成品放回案台(压栈)。常见指令如 LOAD_FAST(把局部变量放到案台)、BINARY_ADD(把案台上两个值相加)、STORE_FAST(把结果存回变量)。全程不依赖寄存器分配,CPython 解释器只要机械地执行即可。

想偷看这份工序单,可以借助 Python 自带的 dis 模块。它是字节码的「翻译官」,能把二进制指令转成可读的文字清单,例如:

输出中每一行对应一条指令:地址、操作名、操作参数。TorchDynamo 的整个职业生涯,就是站在这里逐条阅读这份清单,并判断「这条是否值得抄进优化菜谱」。

回到厨房:监督员要做的第一件事,就是学会读这份工序单。他知道哪个编号代表「炒」,哪个编号代表「装盘」,才能在接下来的扫描中迅速锁定关键步骤。字节码,就是他与后厨沟通的共同语言。

instri=(opcodei,opargi)\text{instr}_i = (\text{opcode}_i, \text{oparg}_i)
这条公式说明:一条字节码指令由两半组成——「做什么」(操作码)和「对什么做」(操作参数)。理解它,就理解了字节码的最小单元。
  1. 定位单条指令字节码是一长串字节,但我们可以按固定步长切出一个个小单元,每个单元处理一件事。
    instri\text{instr}_i
  2. 前半段是操作码opcode 告诉解释器该执行什么动作,比如「加法」还是「加载变量」,它是动作的编码代号。
    opcodei\text{opcode}_i
  3. 后半段是操作数oparg 提供动作所需的细节,比如「加载几号变量」「加上常量几」,它是动作的材料参数。
    opargi\text{oparg}_i
python2 行
import disdis.dis("a + b")

帧与代码对象

当一道菜被正式点单,后厨并不会直接拿着工序单操作,而是先准备一份「开工包」:食材清单、锅具位置、当前做到哪一步。在 Python 里,这个开工包就叫「帧」(frame)。而那份静态的工序单本身,叫「代码对象」(code object)。

代码对象是编译期产物,包含字节码、常量表、变量名表等数据,它是「设计图」。帧则是运行期的临时状态,每次函数被调用都会新建一个,记录这份设计图当前被用到哪一行、变量现在值是多少、案台上堆着什么。两者关系类似于「菜谱」与「正在照着菜谱做菜的那位厨师手中的便签」。

帧的内部结构很有规律:它有局部变量字典(locals)、全局变量字典(globals)、内建名称集合(builtins)、一个值栈(stack),以及一个指针 f_lasti 指向当前执行到第几条字节码。TorchDynamo 的钩子拿到的正是这样一个完整的帧对象,相当于监督员接过开工包,一眼就能看清所有食材与动作。

正因帧把一切都摆在明面上,监督员才能在不打扰厨师的情况下,快速判断:这道菜的工序是否是「可优化」的。如果局部变量、全局变量、甚至案台上的值都是张量,那这道菜很适合全程抄进优化菜谱;如果混入了文件读写这样的「杂活」,则要考虑分段处理。

frame=(code,locals,globals,builtins,stack,f_lasti)\text{frame} = (\text{code}, \text{locals}, \text{globals}, \text{builtins}, \text{stack}, f\_lasti)
帧是一次函数调用的完整现场,它把「静态的设计图」和「动态的运行状态」缝在了一起。
  1. 静态设计图code 字段引用代码对象,里面有字节码和常量表,定义了这道菜的标准做法。
    code\text{code}
  2. 两份变量清单locals 记录函数内部变量,globals 记录模块级全局变量,它们是食材的来源。
    locals,globals\text{locals}, \text{globals}
  3. 案台与进度stack 是运算时临时堆放值的案台,f_lasti 则像一根书签,标记当前读到工序单的哪一行。
    stack,f_lasti\text{stack}, f\_lasti

eval帧钩子

工序单准备好了,开工包也递上去了。可监督员怎么知道「现在开始做菜了」?答案是一声铃响——在 Python 中,每当一个帧要开始评估(即函数调用准备执行一条条字节码)时,CPython 会给注册过的回调发通知。这个回调机制,就是「eval-frame 钩子」。

TorchDynamo 的核心入口是一个 eval-frame 钩子函数:它接收当前帧对象和自定义上下文(context),一被调用就拿到开工包。钩子的签名大致形如 callback(frame, ctx),其中 ctx 由用户在注册时指定。调用方(如 torch.compile)注册好钩子后,之后每发生一次函数调用、每新建一个帧,钩子都会被触发。

钩子被触发的瞬间,Dynamo 面临一个决策:这个帧对应的代码对象,我之前是否已经抄过优化菜谱?如果抄过,再快速核对「守卫」(稍后详述)是否仍然成立,成立就直接复用;没抄过,则进入追踪模式,开始逐条扫描字节码并构建计算图。整个过程发生在解释器真正逐条执行字节码之前,因此对普通用户完全透明。

铃响、接包、查账、决策——这四步构成了 TorchDynamo 对每一次函数调用的「半秒导演」。它不需要改动源码,也不需要在代码里插入任何标记,纯粹靠钩子机制站在了执行链路的关键节点上。

cache_hit(f)⇒use_graph;¬cache_hit(f)⇒trace(f)\text{cache\_hit}(f) \Rightarrow \text{use\_graph}; \quad \neg \text{cache\_hit}(f) \Rightarrow \text{trace}(f)
钩子像出菜口的电铃:铃声一响,监督员先查手账——这道菜我熟不熟?熟就直接按优化流程走,不熟才开始动笔抄。
  1. 查手账cache_hit(f) 判断当前帧对应的函数是否已被追踪过,手账里有记录为真。
    cache_hit(f)\text{cache\_hit}(f)
  2. 熟菜直接出如果命中缓存,说明该函数的优化菜谱已存在,直接调用编译后的图执行。
    use_graph\text{use\_graph}
  3. 生菜现抄如果没记录,则进入 trace 追踪流程:逐条读字节码、捕获张量操作、生成优化图。
    trace(f)\text{trace}(f)

指令解析

监督员翻开工序单,开始逐行阅读。工序单上的每一行就是之前见过的 instr = (opcode, oparg)。解析时,Dynamo 关注三件事:这条指令在做什么?它会让案台上的值发生什么变化?这些值里有没有张量?

Python 官方维护了一份「操作码名 → 数值」的映射表,Dynamo 拿到字节码后,先取出操作码数值,查到对应的操作名(如 BINARY_ADD);再取出紧跟在后面的参数,有时候参数是变量索引,有时候是常量值。之后,它会模拟指令对栈的影响:比如 LOAD_FAST 让栈增加一个元素,STORE_FAST 让栈减少一个元素,BINARY_ADD 弹出两个、压入一个。

更关键的是「张量操作识别」。如果一条指令(如 BINARY_ADD)弹出的是两个 torch.Tensor,Dynamo 就意识到:这是一步可以写进优化菜谱的炒菜动作。它会把这个操作记录为图中的一个节点。反之,如果遇到完全无法追踪的指令——比如调用了一个外部大函数,或者执行了某种非常规操作——Dynamo 不会硬着头皮整段追踪,而是当场「切一刀」(graph break),把这段代码与之前追踪的子图切开,剩余部分退回原生解释器执行。

读到这里你会发现,监督员读工序单的方式和厨师完全不同:厨师是「做一步看一步」,监督员是「先翻完整本,标出所有炒菜步骤,再决定怎么重组」。正是这种全局视野,让它可以跨过多个中间变量,直接把一系列矩阵运算连成一座可融合的计算图。

opname=opcode_name[co_code[i]],oparg=co_code[i+1]\text{opname} = \text{opcode\_name}[\text{co\_code}[i]], \quad \text{oparg} = \text{co\_code}[i+1]
解析字节码的本质是「反查字典 + 取邻居字节」:查清动作名,再读出紧随其后的参数,指令含义就浮出水面了。
  1. 取出动作代号从 co_code(字节码序列)的第 i 个字节取出操作码数值。
    co_code[i]\text{co\_code}[i]
  2. 查表得到动作名用操作码数值去 opcode_name 映射表里查,得到人类可读的名字,例如加法、乘法。
    opcode_name[⋅]\text{opcode\_name}[\cdot]
  3. 读出参数第 i+1 个字节就是操作数,它可能是变量下标或常量索引,配合动作名构成完整语义。
    oparg=co_code[i+1]\text{oparg} = \text{co\_code}[i+1]

图捕获构建

把值得抄的步骤都标出来之后,监督员开始在自己的本子上写一份新菜谱——计算图。它不再是一行行琐碎的「加载变量、压栈、相加、存回变量」,而是抽象的算子与数据流:Add(x, y) 的结果直接流向 Multiply(·, 2),中间那些临时变量全部消失。

这一过程在 TorchDynamo 中称为「图捕获」(graph capture)。它在自建的中间表示(IR)上推进:Dynamo 用自己的「占位对象」模拟真实张量,逐条执行追踪到的字节码指令,每当遇到张量操作,就把这个操作作为节点挂到图上,并把占位对象的输出传给下一个操作。由于是模拟执行而非真实计算,整个过程几乎零开销。

遇到普通 Python 对象时,Dynamo 也有一套对象捕获策略:它不会试图把字典、列表也编译成图节点,而是把它们当作「常量」或「守卫依据」记录下来。比如循环范围 range(N) 中的 N,一旦被捕获为常量,后续如果 N 变了,守卫就会失败并触发重编译——这也是它能在保证正确性的前提下大胆优化的底气所在。

新菜谱抄好了,但它不是一劳永逸的。监督员很清醒:下次再来这道菜,如果食材变了(比如张量形状变了)、锅换了(比如设备从 CPU 换成 GPU),老菜谱还适用吗?于是他在菜谱封面上钉了一张「检查清单」——那就是守卫。

G=⋃opi∈TENSOR_OPSnode(opi)\mathcal{G} = \bigcup_{\text{op}_i \in \text{TENSOR\_OPS}} \text{node}(\text{op}_i)
计算图是「抄下来的新菜谱」:把原工序单里所有张量操作抽出来重新连线,琐碎的搬运动作全部省略。
  1. 遍历全部指令扫描上一节解析出的每条字节码指令,逐个判断是否为张量操作。
    ∀ opi\forall \ \text{op}_i
  2. 筛出张量操作只有属于 TENSOR_OPS 集合的指令才会被捕获,普通赋值、跳转等指令被跳过。
    opi∈TENSOR_OPS\text{op}_i \in \text{TENSOR\_OPS}
  3. 连成一张图把每个被捕获的算子提升为图节点,把它们的输入输出相连,形成一张完整的有向无环图。
    ⋃node(opi)\bigcup \text{node}(\text{op}_i)

守卫与重编译

监督员给每份优化菜谱都附了一张检查清单,上面写着当初抄菜谱时的前提条件:张量的形状是 (4, 4)、元素的类型是 float32、运算发生在 GPU 上、某个 Python 变量的值还是那个整数。这张清单,就是「守卫」(guards)。

每次函数再次被调用时,Dynamo 先不急着用优化菜谱,而是先跑一遍守卫检查。如果所有条件依然成立,说明新情况和上次一模一样,直接跑图中即可,这是最快的路径。但哪怕只有一个条件不满足,比如输入的 batch size 从 4 变到了 8,整份老菜谱就作废了,Dynamo 必须重新走一遍追踪流程,生成一份适配新条件的新菜谱并重新编译——这就是「重编译」(recompilation)。

守卫机制是 Dynamo 正确性与性能之间的压舱石。它记录了「图是在什么环境下抄出来的」,确保编译优化不会在条件变化后偷偷给错结果。工业实践中,重编译并非洪水猛兽:常见 shape 的数量通常不多,缓存命中率很高;只有当每次输入形状都不同时,才会出现「重编译风暴」,这时需要用 torch.compile(dynamic=True) 等策略来缓解。

回到厨房视角:守卫就像监督员在优化菜谱扉页写下的「适用条件」。老主顾点同样的菜,看一眼食材火候没变,直接上优化菜谱;哪天顾客说「少放盐」,条件变了,监督员二话不说重抄一份。这份死磕正确性的态度,正是它敢于大胆做「零侵入」加速的底气。

recompile  ⟺  ¬⋀g∈guardsg(inputs)\text{recompile} \iff \neg \bigwedge_{g \in \text{guards}} g(\text{inputs})
所有守卫都通过,菜谱才能继续用;任何一个守卫失败,整份菜谱报废重炒。这是「宁缺毋滥」的正确性哲学。
  1. 逐个核对守卫遍历检查清单上的每一条条件,例如形状、类型、设备。
    ∀g∈guards\forall g \in \text{guards}
  2. 求「全部通过」所有检查项的逻辑与(AND)结果为真,才代表环境完全匹配。
    ⋀g(inputs)\bigwedge g(\text{inputs})
  3. 失败即重编译对上面这个整体结果取反,只要有一项不成立,就触发重新追踪与编译。
    ¬(⋯ )⇒recompile\neg (\cdots) \Rightarrow \text{recompile}

透明加速

终于,我们可以聊聊那位大厨的视角了。在他眼里,一切如常:菜谱还是原来的菜谱,手法还是原来的手法,他甚至不知道厨房里多了一位监督员。可上菜速度就是快了一截——因为监督员已经在后台把大量重复的「炒菜动作」合成了几道大菜,一次炒完。这就是 TorchDynamo 最迷人的特质:透明。

在用户层面,透明意味着零代码修改。你不需要把 model(x) 换成 compiled_model(x) 之外的东西,更不需要手工定义计算图或用 torch.jit.script 改写模型。只需在训练或推理代码前加一句:

这句背后,torch.compile 完成了「三步走」:先用 Dynamo 在字节码层面捕获计算图,再把图交给后端(如 Inductor)生成高效的 GPU 内核,最后用守卫守护缓存。对使用者而言,这只是「换了个更好的执行引擎」。

端到端的收益则体现在两层:一是图层面的算子融合,省去了多次内核启动与显存读写的开销;二是后端代码生成,把 Python 这种动态语言的调度成本彻底压低。在典型的 Transformer 训练中,这种透明加速常能带来 20%–30% 以上的性能提升,而模型行为几乎不变。

监督员的智慧不在于发明了新菜,而在于「在不打扰任何人做饭的前提下,让整条出菜流水线变得更快」。这正是字节码方案的独特价值:它站在语言运行机制的高度,从上而下地理解程序,再悄无声息地把性能还给你。理解 Python 字节码,就是拿到了这把钥匙。

speedup=TeagerTcompiled,code_changes=0\text{speedup} = \frac{T_{\text{eager}}}{T_{\text{compiled}}}, \quad \text{code\_changes} = 0
加速比等于「原本用时 ÷ 编译后用时」,而用户代码的改动量恒等于零。这是速度与便捷兼得的双重胜利。
  1. 测量原执行时间用原生 PyTorch 的 eager 模式(逐算子即时执行)跑一遍任务,记下耗时。
    TeagerT_{\text{eager}}
  2. 测量编译执行时间用 torch.compile 加持后的模式再跑一遍,记录新的耗时。
    TcompiledT_{\text{compiled}}
  3. 两者相除得加速比比值大于 1 说明编译更快;同时用户源码的改动行数保持为零。
    TeagerTcompiled,code_changes=0\frac{T_{\text{eager}}}{T_{\text{compiled}}}, \quad \text{code\_changes} = 0
python1 行
model = torch.compile(model)

AI 应用场景

大模型训练加速

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

解决的问题:传统训练中 PyTorch 采用 eager 模式逐算子启动 GPU 内核,启动开销大、算子间无法融合。Dynamo 通过在字节码层捕获计算图,把整个 Transformer 块编译为融合内核,减少内核启动次数。

具体案例:某团队训练 7B 规模的大语言模型,原本每个 transformer 层有 200 多次内核调用;使用 torch.compile(model) 后,Dynamo 捕获到完整子图,Inductor 后端将其融合为几十个 Triton 内核,单卡吞吐提升约 25%。

动态形状推理服务

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

解决的问题:推理请求的 batch size 常动态变化,静态图工具往往在形状变化时失效。Dynamo 的守卫机制会记录 shape 条件,命中则直接走缓存图,未命中自动重编译,兼顾正确性与吞吐。

具体案例:某推荐系统线上服务每轮请求 batch 在 1~64 之间波动。接入 torch.compile(mode="reduce-overhead") 后,Dynamo 在常见形状下命中缓存图,p99 延迟降低 18%;当请求形状落入新范围时,自动触发重编译,无需人工干预。

自定义编译后端开发

阶段:AI 编译器与框架研发

解决的问题:硬件厂商希望把自家算子的优势发挥出来,但 PyTorch 原生后端无法覆盖。Dynamo 的字节码捕获层与框架解耦,开发者可以用 torch._dynamo.optimize(my_backend) 接入自定义编译器。

具体案例:某国产加速芯片厂商实现了一个自定义后端,将 Dynamo 捕获到的图节点映射到自家算子库;脚本仅需将模型外层包一层 torch.compile(model, backend="my_chip"),用户模型零改动即可运行在新硬件上,并获得 1.5 倍于默认后端的性能。

Python 演示

python47 行
import dis TENSOR_OPS = {"BINARY_ADD", "BINARY_SUBTRACT", "BINARY_MULTIPLY",              "BINARY_TRUE_DIVIDE", "BINARY_MATRIX_MULTIPLY",              "CALL_FUNCTION"} class BytecodeScanner:    def __init__(self, fn, tensor_ops=None):        self.fn = fn        self.code = fn.__code__        self.tensor_ops = tensor_ops if tensor_ops is not None else TENSOR_OPS        self.captured_ops = []        self.gc_count = 0     def scan(self):        for instr in dis.get_instructions(self.code):            self.gc_count += 1            if instr.opname in self.tensor_ops:                self.captured_ops.append(                    (instr.offset, instr.opname, instr.argval)                )        return self.captured_ops     def report(self):        print("函数:", self.fn.__name__)        print("字节码指令总数:", self.gc_count)        print("捕获到的张量算子:")        for offset, opname, argval in self.captured_ops:            print(f"  offset={offset:4d}  {opname}  arg={argval}")        print(f"捕获率: {len(self.captured_ops) / self.gc_count:.1%}")  def matmul_plus(a, b):    y = a * b    z = y + b    return z  def trace(fn):    scanner = BytecodeScanner(fn)    scanner.scan()    scanner.report()    return scanner  if __name__ == "__main__":    trace(matmul_plus)
目的:这段代码模拟了 TorchDynamo 的字节码扫描核心:用 dis 模块把函数展开成指令流,再根据自定义的算子白名单挑选出张量计算指令,输出「捕获率」报告。
  1. instructions = dis.get_instructions(self.code)把函数代码对象展开成一条条带属性(操作名、参数、偏移量)的指令迭代器
  2. self.captured_ops = []存储捕获到的张量算子,等价于 Dynamo 计算图的原始节点清单
  3. self.gc_count += 1计数器:记录总共扫描了多少条字节码,用于计算捕获率
  4. if instr.opname in self.tensor_ops:用白名单集合判断当前指令是否为「值得抄进菜谱」的张量操作
  5. trace(matmul_plus)程序入口,对示例函数 matmul_plus 执行一次完整的「扫描-报告」流程

常见误区

误区我以为 TorchDynamo 会先拿到我的 `.py` 源码,用 AST 重新解析一遍,然后再做优化。既然要读懂代码,肯定要从源码入手呀。

正解实际上 Dynamo 根本不碰源码文本。它消费的是源码编译后的字节码——这是「工序单」而非「菜谱手稿」。代码在导入时已被 Python 编译器编译为字节码并加载为帧,Dynamo 通过帧钩子在字节码层直接操作,比解析源码更贴近真实执行语义,也天然支持动态语言特性。

为何常见:大部分讲解会从「源码如何变成字节码」讲起,容易让人误以为 Dynamo 的输入也是源码;而 AST 语法分析在各类编译教材中过于常见,产生了思维惯性。

误区我听说字节码是一段线性指令,那遇到 `if`、`for`、`while` 这样的动态控制流,Dynamo 应该就束手无策、直接报错了吧?

正解Dynamo 确实无法把整段动态控制流编译成一张静态图,但它会在无法追踪的指令处执行 graph break(图断裂):前半段可追踪的算子照常捕获并编译,断裂点之后的代码回退到原生解释器执行。训练照样进行,只是加速幅度打了折扣,远谈不上「罢工」。

为何常见:人们常把「编译优化」与「静态图」画等号,忽略了 Dynamo 的设计目标不是「全图编译」,而是「能编多少编多少」的渐进式优化。

误区我觉得只要第一次追踪生成了计算图,后面就能一直用这份图跑,毕竟代码又没变,图怎么会失效呢?

正解缓存图的有效性不仅取决于代码,还取决于运行条件。守卫会检查张量形状、类型、设备等输入属性;只要有一个条件变化,缓存图就会失效并触发重编译。这就像同一道菜,顾客说「少盐」,之前抄的工序单就不能照搬了。

为何常见:静态图工具(如 torch.jit.trace)往往忽略输入元数据的变化,让人误以为「代码不变图就不变」,忽略了 Dynamo 对正确性的严格守护。

误区我觉得这种底层的字节码扫描听起来很高深,用户为了配合优化,至少得把模型写成特定风格,或者加上某些标记吧?

正解恰恰相反,字节码方案最核心的优势就是「零侵入」。Dynamo 通过 eval-frame 钩子在运行时接管帧,一切发生在执行链路的夹缝里,用户代码无需任何改动。你只要在训练脚本里加一句 torch.compile(model),其余照旧。

为何常见:人们习惯了「要优化就得配合框架约束」的旧模式,比如 TorchScript 需要源码改写或 trace 变形,于是想当然地认为 Dynamo 也有同样的前提条件。

面试题

Q1. Python 字节码位于源码与执行的哪一层?

单选题 · easy

  • A.源码在解释执行前先编译成字节码← 正确答案
  • B.源码首先被显卡驱动编译成 GPU 内核
  • C.字节码只存在于 JVM 中,Python 没有字节码
  • D.源码在每次运行前都需要人工翻译成汇编

答案:A。Python 源码会先被编译成与平台无关的字节码,再由虚拟机逐条执行。这与显卡驱动和 JVM 无关,也不需要人工翻译。

Q2. TorchDynamo 在什么时机接管帧?

单选题 · easy

  • A.函数被定义时立即介入并修改源码
  • B.帧开始评估时通过钩子介入追踪← 正确答案
  • C.张量在 GPU 上完成运算之后触发
  • D.每次显存分配时触发一次介入

答案:B。Dynamo 注册 eval-frame 钩子,每当一个 Python 帧开始评估时被回调,它借此拿到帧对象并决定追踪还是复用缓存图。

Q3. 「零侵入加速」指的是什么?

单选题 · easy

  • A.无需修改用户模型源码即可获得加速← 正确答案
  • B.只在模型权重全部为零时才会自动加速
  • C.要求所有算子提前用 CUDA 重写一次
  • D.必须在编译期删除所有 Python 代码

答案:A。Dynamo 在字节码层透明工作,用户只需调用 torch.compile(model),模型本身零改动;其余选项都不符合它的设计初衷。

Q4. 关于字节码中的 oparg,下列说法正确的是?

单选题 · mid

  • A.描述指令操作的具体参数或数据索引← 正确答案
  • B.代表指令执行时消耗的显存容量上限
  • C.只能表示 Python 解释器的版本号信息
  • D.指示函数装饰器的执行顺序和优先级

答案:A。oparg(操作数)紧跟在 opcode 之后,提供参数下标或常量索引等细节。它与显存、解释器版本和装饰器无关。

Q5. Dynamo 识别张量算子主要依靠什么?

单选题 · mid

  • A.分析源码中的全部字符串与注释内容
  • B.扫描字节码并结合栈上值的类型判断← 正确答案
  • C.直接读取模型权重文件的二进制内容
  • D.依赖用户主动声明每一处计算图边界

答案:B。Dynamo 用字节码扫描识别操作名,同时追踪模拟栈上值的类型,若弹出的是 torch.Tensor,才判定为张量算子。这是编译期静态与动态信息的结合。

Q6. 若 Dynamo 遇到无法追踪的指令,会发生什么?

单选题 · mid

  • A.抛出异常并终止整个训练过程
  • B.在断点处切分,只优化可跟踪的子图← 正确答案
  • C.忽略该指令继续捕获剩余全部运算
  • D.返回该指令并重写整个 Python 源码

答案:B。graph break 是 Dynamo 的优雅降级策略:无法追踪处切一刀,前面的子图照常编译,后续部分回退原生执行,训练不会中断。

Q7. 关于 frame(帧),以下哪些信息它是包含的?

多选题 · mid

  • A.引用的代码对象与局部变量字典← 正确答案
  • B.全局变量表与内建名称集合引用← 正确答案
  • C.值栈以及当前执行位置 f_lasti← 正确答案
  • D.GPU 内核的完整二进制机器码内容

答案:ABC。帧是 Python 执行现场的快照:code 对象、locals/globals/builtins、值栈和指令指针都有。它不包含任何编译后的 GPU 机器码。

Q8. 关于 Guards(守卫)与重编译,正确的是?

多选题 · hard

  • A.守卫检查张量形状、设备、数据类型等属性← 正确答案
  • B.守卫全部通过时直接复用已缓存的计算图← 正确答案
  • C.守卫任一失败时会触发重新捕获与重编译← 正确答案
  • D.守卫一旦通过一次就会永久跳过后续检查

答案:ABC。守卫是每次调用都执行的运行时检查,记录计算图成立的前提条件。任何条件不满足就重编译,绝不存在「一次通过永久有效」的捷径。

Q9. 关于 eval-frame 钩子的触发时机,正确的是?

单选题 · mid

  • A.每解释一条字节码指令都会触发一次
  • B.每个 Python 帧开始评估时触发一次← 正确答案
  • C.只在 GPU 上运行算子时才会触发一次
  • D.仅当脚本由命令行主入口启动时触发

答案:B。CPython 在每次帧评估前调用已注册的回调,因此粒度是「每帧一次」,而非逐指令、逐算子或仅限主入口。

Q10. 与 AST 源码分析相比,字节码方案的优势有哪些?

多选题 · hard

  • A.无需重写源码即可支持动态语言特性← 正确答案
  • B.能捕获编译后真实执行的指令序列← 正确答案
  • C.对嵌套函数与闭包也能统一透明处理← 正确答案
  • D.因此完全不需要维护任何图缓存机制

答案:ABC。字节码直接反映运行时实际行为,天然适配动态特性,且钩子作用在帧层,嵌套函数同样被覆盖。D 错误:字节码方案依然需要守卫与缓存来保证正确性和性能。