想象你楼下有一家手艺很好的裁缝铺。老板第一次给你量体,记下肩宽、腰围、袖长,然后裁剪出一套合身的西服。可如果第二次见面你瘦了十斤,他还按旧尺寸把衣服递给你,那就尴尬了。TorchDynamo 就是 PyTorch 2.0 请来的“AI 裁缝”:它先把 Python 代码观察一遍,再编译成一套优化过的计算图。Guard 机制则是裁缝手里的量体卡——每次执行编译产物前,先重新核对输入是否还是当初的形状、类型和值。没变,直接用;变了,就回炉重做。这套机制让动态的 Python 也能吃到静态编译的性能红利。

岗位关联

理解 Guard 机制,等于掌握 TorchDynamo 的“正确性保险丝”。你能解释重编译原因、定位性能瓶颈,让推理服务与训练任务真正享受到编译加速。

目录
  1. 可视化演示
  2. 动态语言困境
  3. Guard 基本概念
  4. 常见 Guard 类型
  5. 缓存与失效管理
  6. 性能优化策略
  7. 调试与监控
  8. 工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Guard机制:trace阶段生成Guard、check阶段校验输入、命中复用缓存图或触发重建,以及Guard检查开销分析。

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

生命周期:trace→check trace记录输入属性生成Guard hit⇒复用图,miss⇒重建 check失败触发受控回退重建。

动态语言困境

想象裁缝铺老板面对一位特别善变的客人:今天说肩宽 42,明天说改成 44,后天又说颜色换掉。老板每改一次都要重新裁布,时间全耗在反复调整上。Python 就是这位善变的客人——函数里的参数类型可以是任意的,全局变量可以随时被修改,同一个张量今天是 3×4,明天是 5×6。TorchDynamo 想帮 PyTorch 把一段 Python 代码“编译”成高效模板,就必须先处理这种动态性。

在 C++ 这类静态语言里,变量类型在编译期已经确定,编译器可以放心生成机器码。Python 则要逐条解释执行,类型、属性、全局值都可能在下一条语句改变,直接生成机器码风险极大。TorchDynamo 的办法不是硬编译所有 Python,而是先用跟踪(tracing)捕获一段 FX 计算图,并记录“这个图成立的前提”。Guard 就是这些前提的“量体卡”:只有条件仍然满足,编译产物才能继续使用。

编译一次有固定成本,而普通解释执行单次成本高但无需前置准备。因此,TorchDynamo 的性能收益来自“同一段代码被反复执行”。如果同一个函数只调用一两次,编译成本会超过收益;如果在一个训练循环中被调用成千上万次,编译成本就会被摊薄,加速效果才明显。

所以 TorchDynamo 的 Guard 机制,本质上就是裁缝铺的量体记录:它把“客人暂时没变”变成可验证的条件,而把验证之外的变化交给下一次量体。

Ccompile+N⋅Ccompiled<N⋅Cinterpret,Nbreak-even=CcompileCinterpret−Ccompiled\displaystyle C_{\text{compile}} + N \cdot C_{\text{compiled}} < N \cdot C_{\text{interpret}},\qquad N_{\text{break-even}} = \frac{C_{\text{compile}}}{C_{\text{interpret}} - C_{\text{compiled}}}
公式说明:只要同一个 Python 函数被调用的次数足够多,编译总成本就能低于逐次解释执行的成本。
  1. 固定编译成本C_compile 是无论调用多少次都只付一次的追踪与编译开销。
    CcompileC_{\text{compile}}
  2. 单次执行成本C_interpret 是普通解释执行一次的成本,C_compiled 是编译后执行一次的成本。
    Cinterpret,  CcompiledC_{\text{interpret}},\; C_{\text{compiled}}
  3. 盈亏平衡点当调用次数 N 大于平衡点后,编译总成本更低。
    N>Nbreak-evenN > N_{\text{break-even}}

Guard 基本概念

裁缝把尺寸写在卡片上,客人再次上门先量一遍,确认尺寸没变,才从衣柜取出那套西装。TorchDynamo 也有一张卡片:在追踪计算图时,它会记录 x 是什么类型、形状多大、是否需要梯度;这些记录最终会被打包成一个 Guard 函数,每次调用编译产物前先执行一遍。

一个 Guard 是返回布尔值的运行时代码片段,例如 x.shape == (64, 3, 224, 224) 或 type(x) is torch.Tensor。多个 Guard 之间以 and 连接,全部为真,说明这次输入仍然符合编译时的假设,可以直接使用缓存中的编译图;只要有一个为假,说明环境变了,必须重新追踪、重新编译。

TorchDynamo 会为每个被编译的 frame 生成一个 guard_fn,并把它与编译结果一起放入缓存。缓存命中不是“我见过这个函数”,而是“这个函数在当前条件下可以复用之前的编译结果”。如果没有 Guard,编译器为特定形状优化出的 kernel 会被错误地用在另一个形状上,结果就不可靠了。

这张量体卡片不会替客人做决定,它只负责反复确认:你还没变,所以上次的裁剪依然合身。

G(x)=⋀i=1ngi(x),G(x)=True⇒Use(cache)\displaystyle G(x)=\bigwedge_{i=1}^{n} g_i(x),\quad G(x)=\text{True}\Rightarrow \text{Use}(\text{cache})
公式说明:Guard 是多个条件的合取,只有全部成立才允许复用缓存。
  1. 单个守卫条件g_i 表示第 i 个守卫,例如“类型是 Tensor 且形状是 (3,4)”。
    gi(x)g_i(x)
  2. 合取运算∧ 表示所有守卫必须同时为真,任何一个为假整个表达式就为假。
    ⋀i=1ngi(x)\bigwedge_{i=1}^{n} g_i(x)
  3. 缓存复用⇒ 表示当守卫全部为真时,才允许直接使用缓存中的编译产物。
    Use(cache)\text{Use}(\text{cache})

常见 Guard 类型

一张量体卡上不仅有腰围,还有肩宽、袖长、领口。TorchDynamo 的 Guard 也可以按关注对象分成几类:类型守卫、形状守卫、值守卫。它们分别回答不同的问题:你是什么类型的对象?你的形状还是原来那样吗?你携带的某个常量有没有被改动?

类型守卫检查 type(x) is torch.Tensor 或 isinstance(m, nn.Module),因为同一个运算符在不同类型上的行为可能完全不同。形状守卫检查张量的 .shape 或 .size(),因为很多算子优化依赖固定维度,例如把矩阵乘法折叠成专用 kernel。值守卫检查 Python 整数、浮点或字符串是否还是编译时看到的常量,例如把 steps = 128 固化在图中后,运行时必须确认它仍然是 128。

此外,dtype、device、requires_grad 也可以看作属性守卫,因为它们会改变算子选择和结果语义。Guard 不需要覆盖所有属性,只覆盖编译后端真正依赖的属性,否则检查本身会拖慢速度。TorchDynamo 会自动裁剪冗余守卫,这也是后续性能优化的一部分。

这些条目合起来就是裁缝对客人身体的完整记忆:你不仅是“那个人”,还是“那个肩宽、腰围、袖长都没变的人”。

1[type(x)=Tensor] ∧ 1[shape(x)=s] ∧ 1[value(p)=v]\displaystyle \mathbb{1}\left[\text{type}(x)=\text{Tensor}\right]\ \wedge\ \mathbb{1}\left[\text{shape}(x)=\mathbf{s}\right]\ \wedge\ \mathbb{1}\left[\text{value}(p)=v\right]
公式说明:类型、形状、值三类条件共同构成一个典型守卫组合。
  1. 艾弗森括号1[P] 表示当命题 P 为真时取 1,为假时取 0。
    1[P]\mathbb{1}[P]
  2. 类型守卫type(x)=Tensor 确认输入仍然是张量类型。
    1[type(x)=Tensor]\mathbb{1}[\text{type}(x)=\text{Tensor}]
  3. 形状守卫shape(x)=s 确认张量形状仍等于编译时的形状向量。
    1[shape(x)=s]\mathbb{1}[\text{shape}(x)=\mathbf{s}]
  4. 值守卫value(p)=v 确认 Python 标量值仍等于编译时看到的值。
    1[value(p)=v]\mathbb{1}[\text{value}(p)=v]

缓存与失效管理

裁缝铺不可能把每件衣服永远挂在店里,衣柜满了,旧款就要清理;尺寸变了,旧衣服就不能再拿出去。TorchDynamo 也给每个函数维护一个编译缓存,每个缓存条目由守卫函数和编译产物组成。

当 Guard 全部通过,本次直接执行编译后的 FX Graph,称为命中。当某个 Guard 失败,Dynamo 会重新追踪并生成新图,然后把新守卫和新产物写入缓存;下一次若新守卫通过,又可命中。Guard 失败并不是错误,而是编译器自我修正的流程。

缓存条目不能无限增长。cache_size_limit 控制同一个函数最多保留多少版本;若一直命中,没有额外编译开销;若 miss 频繁,可能每次调用都在编译,速度反而不如 eager 模式。因此工程上需要同时观察命中率和重编译率。

客人不变,衣服从衣柜里拿出来就穿;客人变了,裁缝重新量体再做一件。Guard 失效并不等于裁缝手艺不行,而是变化被及时发现了。

H=NhitNhit+Nmiss,Tavg=Hthit+(1−H)(tguard+trecompile+thit)\displaystyle H = \frac{N_{\text{hit}}}{N_{\text{hit}}+N_{\text{miss}}},\qquad T_{\text{avg}} = H t_{\text{hit}} + (1-H)(t_{\text{guard}}+t_{\text{recompile}}+t_{\text{hit}})
公式说明:命中率越高,平均单次调用的总时间越接近纯执行时间。
  1. 命中率H 是命中次数占总调用次数的比例。
    H=NhitNhit+NmissH = \frac{N_{\text{hit}}}{N_{\text{hit}}+N_{\text{miss}}}
  2. 命中耗时t_hit 表示守卫通过并执行编译图的平均时间。
    thitt_{\text{hit}}
  3. 失效耗时1-H 表示未命中比例,此时还要额外付出守卫时间与重编译时间。
    (1−H)(tguard+trecompile+thit)(1-H)(t_{\text{guard}}+t_{\text{recompile}}+t_{\text{hit}})
  4. 平均总时间T_avg 是命中与失效两种情况的加权平均。

性能优化策略

老裁缝会建议:量体时少穿厚毛衣,避免尺寸时大时小;也会把最常变化的项目和最便宜的项目放在前面先核对。TorchDynamo 的优化遵循同样的思路:减少不必要的量体项目,优化检查顺序。

弱引用守卫(weakref guard)避免缓存持有模块引用从而形成循环引用;宽松守卫允许某些维度在一定范围内变化,而不是每次都严格相等;守卫排序则把检查代价低、失败概率高的条件放在前面,尽早失败,从而跳过后面的检查。

TorchDynamo 在生成 Guard 时会做可达性分析,只保留后端真正会用到的假设;如果某个值在编译图中没有被固化,就不会为它生成守卫。实践中也可以显式声明动态维度,例如使用 torch._dynamo.mark_dynamic(x, 0),或让 torch.compile 使用 dynamic=True,从而生成更宽松的守卫,减少微小 shape 变化带来的重复编译。

钱要花在刀刃上,量体也要先量最可能变化的部位。宽松守卫就像版型稍松的西装,客人稍微胖一点也穿得下;weakref 则像裁缝记得客人名字,但不能一直拽着客人衣角不放。

pi=Pr⁡(gi=False),p1t1≤p2t2≤⋯≤pntn\displaystyle p_i = \Pr(g_i = \text{False}), \qquad p_1 t_1 \leq p_2 t_2 \leq \cdots \leq p_n t_n
公式说明:按“失败概率 × 检查代价”的升序排列守卫,让整体检查期望开销更小。
  1. 失败概率p_i 表示第 i 个守卫失败的可能性。
    pi=Pr⁡(gi=False)p_i = \Pr(g_i = \text{False})
  2. 检查代价t_i 表示执行第 i 个守卫检查需要的时间。
    tit_i
  3. 排序规则让 p_i t_i 小的守卫先执行,期望总检查时间更短。
    p1t1≤p2t2≤⋯≤pntnp_1 t_1 \leq p_2 t_2 \leq \cdots \leq p_n t_n

调试与监控

裁缝铺会记录“这个月有多少客人因为尺寸没变却来改衣服”,以此判断量体是否准确。TorchDynamo 也需要一套监控手段,回答两个问题:Guard 失败了多少次?为什么失败?

最直接的是重编译计数与 Guard 失败计数。官方推荐使用 torch._dynamo.explain(fn)(*args) 查看 guard 数和重编译原因,也可以设置环境变量 TORCH_LOGS="+dynamo" 打开详细日志。torch._dynamo.config 中还有一些调试开关,例如 verbose 和 suppress_errors,排查问题时可以临时打开。

Dynamo 日志会输出类似 Recompiling function ... because guard ... failed 的信息。常见失败原因包括张量 shape 变化、Python int 改变、全局变量被修改。把失败次数和失败原因结合起来,才能判断是该改代码,还是该调编译器配置。

监控不是目的,而是裁缝反思的开始:如果每个人量体后都改尺寸,那可能是量体工具本身有问题,需要回头检查训练代码或数据 pipeline。

rrecompile=NfailNcall(Ncall≈Nhit+Nmiss)\displaystyle r_{\text{recompile}} = \frac{N_{\text{fail}}}{N_{\text{call}}} \quad (N_{\text{call}} \approx N_{\text{hit}} + N_{\text{miss}})
公式说明:重编译率是 Guard 失败次数占总调用次数的比例,用来衡量输入假设的稳定性。
  1. 失败次数N_fail 表示某段时间内 Guard 失败的次数。
    NfailN_{\text{fail}}
  2. 总调用次数N_call 约等于命中与未命中次数之和。
    Ncall≈Nhit+NmissN_{\text{call}} \approx N_{\text{hit}} + N_{\text{miss}}
  3. 重编译率r_recompile 越高,说明输入假设越不稳定,越需要优化。

工程实践

一个成熟裁缝铺不会同时接待所有客人:VIP 客人提供稳定体型,标准客单用统一尺码。TorchDynamo 工程实践的核心也一样:让 Guard 尽量稳定,减少重编译。

常见做法有三类:固定 batch size 或将序列 padding 到固定长度;把动态分支拆成独立函数,让每个分支拥有稳定的 Guard;使用 torch._dynamo.mark_dynamic 或 torch.compile(dynamic=True) 显式声明动态维度,避免编译器为微小变化反复重编译。还要注意,Guard 检查本身有开销;如果函数很小又被频繁调用,编译收益可能被 Guard 和编译成本抵消。一般适合训练步数多、GPU batch size 大的场景。

最后,用性能分析确认收益。观察 torch._dynamo.explain 给出的 guard 数、graph break 与重编译原因,再结合命中率公式判断瓶颈。优化的目标不是“完全不重编译”,而是让总平均耗时最小。

真正落地的工程经验,不是让编译器学得更聪明,而是给编译器一个稳定可预期的环境。Shape 固定、分支收敛、条件显式,Guard 自然从“经常失败”变成“几乎不失败”。

Tavg≈tguard+(1−H)trecompile+texec,min⁡Tavg 为目标\displaystyle T_{\text{avg}} \approx t_{\text{guard}} + (1-H)t_{\text{recompile}} + t_{\text{exec}},\quad \min T_{\text{avg}} \ \text{为目标}
公式说明:平均耗时由守卫检查、失效重编译和最终执行三部分组成,工程上围绕这三项做权衡。
  1. 守卫检查时间t_guard 是每次调用都存在的固定检查开销。
    tguardt_{\text{guard}}
  2. 重编译期望成本(1-H) 是未命中概率,乘上单次重编译时间得到期望成本。
    (1−H)trecompile(1-H)t_{\text{recompile}}
  3. 执行时间t_exec 是真正运行编译图的时间。
    texect_{\text{exec}}
  4. 优化目标在保证正确性的前提下,让 T_avg 尽可能小。

AI 应用场景

动态形状推理服务优化

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

解决的问题:在线推理请求的 batch size 或序列长度动态变化,TorchDynamo 的 shape guard 频繁失败,每次请求都可能触发重编译,服务吞吐明显下降。传统做法只能关闭编译,退回到 eager 模式。

具体案例:一个 LLM 离线批量推理服务把输入文本 padding 到固定长度 512,并固定 batch size 为 8,shape guard 不再频繁失效,编译图稳定命中,吞吐提升约 30%。

训练循环防抖动

阶段:训练框架优化

解决的问题:强化学习训练中,Python 全局配置如 update_interval、batch_size 在每次实验时被修改,导致值守卫失效并反复重编译。

具体案例:训练框架把“实验配置”与“热循环”分离,训练循环内不再修改 Python 常量,所有动态变化通过 tensor 传入,重编译次数大幅下降,GPU 利用率提升。

动态分支模型治理

阶段:算法开发与模型改造

解决的问题:模型里出现 if x.numel() > 100 这类依赖数据形状的分支,每次输入长度变化都会切换分支,TorchDynamo 需要为每个分支重新编译。

具体案例:将模型拆成 short_path 和 long_path 两个独立函数,各自参与 torch.compile,Guard 稳定在各自分支内,整体编译开销显著降低。

Python 演示

python94 行
"""模拟 TorchDynamo 的 Guard 机制:每次调用前验证类型/形状/值,全部通过才复用“编译结果”。"""import functools  class Tensor:    def __init__(self, data):        self.data = list(data)        self.shape = (len(data),)     def add(self, k):        return Tensor([x + k for x in self.data])  class SpecializingCompiler:    def __init__(self):        self.cache_map = {}        self.compile_count = 0        self.guard_fail_count = 0     def _make_guards(self, args, kwargs):        guards = []        for arg in args:            if isinstance(arg, Tensor):                guards.append(("type", type(arg)))                guards.append(("shape", arg.shape))            else:                guards.append(("value", arg))        for key in sorted(kwargs):            v = kwargs[key]            if isinstance(v, Tensor):                guards.append(("kw_" + key, (type(v), v.shape)))            else:                guards.append(("kw_" + key, v))        return tuple(guards)     def _check(self, guards, args, kwargs):        i = 0        for arg in args:            if isinstance(arg, Tensor):                if guards[i][1] is not type(arg):                    return False                if guards[i + 1][1] != arg.shape:                    return False                i += 2            else:                if guards[i][1] != arg:                    return False                i += 1        for key in sorted(kwargs):            v = kwargs[key]            if isinstance(v, Tensor):                if guards[i][1] != (type(v), v.shape):                    return False            else:                if guards[i][1] != v:                    return False            i += 1        return True     def compile(self, fn):        @functools.wraps(fn)        def compiled(*args, **kwargs):            return fn(*args, **kwargs)        return compiled     def __call__(self, fn, *args, **kwargs):        if fn in self.cache_map:            guards, compiled = self.cache_map[fn]            if self._check(guards, args, kwargs):                return compiled(*args, **kwargs)            self.guard_fail_count += 1        guards = self._make_guards(args, kwargs)        compiled = self.compile(fn)        self.cache_map[fn] = (guards, compiled)        self.compile_count += 1        return compiled(*args, **kwargs)  def model(x, steps):    return x.add(steps)  if __name__ == "__main__":    compiler = SpecializingCompiler()     x = Tensor([1.0, 2.0, 3.0])    print(compiler(model, x, 1).data)          # 首次编译    print(compiler(model, x, 1).data)          # 守卫命中    print(compiler(model, Tensor([1.0, 2.0]), 1).data)  # 形状变化,重编译    print("compile_count =", compiler.compile_count)    print("guard_fail_count =", compiler.guard_fail_count)
目的:用纯 Python 模拟 TorchDynamo 的守卫生成、缓存命中和重编译流程,帮助理解 Guard 机制的核心思想。
  1. def _make_guards(self, args, kwargs):把每个输入的当前类型、形状或值记录下来,相当于裁缝“量体”。
  2. guards.append(("shape", arg.shape))保存形状假设,之后只要形状一样就大概率命中缓存。
  3. if self._check(guards, args, kwargs):每次调用先过守卫,全部通过才复用旧“编译结果”。
  4. self.guard_fail_count += 1任何一个守卫不通过,就记录一次失败,并触发重编译。
  5. self.cache_map[fn] = (guards, compiled)新守卫与新编译产物一起放入缓存。

常见误区

误区我以为 Guard 就是 `shape == ...` 一种检查,平时只要 batch size 不变就不会重编译。

正解实际上 Guard 会检查类型、dtype、device、requires_grad、Python 标量值、容器长度、全局对象属性等。任何被 tracing 固化的假设都可能成为守卫,shape 只是最常见的一种。

为何常见:因为 shape 变化是最容易观察到的重编译原因,资料里也常以 shape guard 举例,容易以偏概全。

误区看到日志里出现 “Recompiling” 就以为代码写错了,于是到处加 try/except 或彻底关闭编译。

正解Guard 失败是编译假设被破坏的正常信号,TorchDynamo 正是通过它保证正确性。它不是报错,而是触发重新追踪和重新编译。真正要关注的是失败频率过高带来的性能问题。

为何常见:日志中的 “Recompiling” 看起来像异常,尤其与某些错误堆栈同时出现时,容易被误判为代码缺陷。

误区把 `cache_size_limit` 调到很大,以为所有 shape 都缓存下来就不会慢了。

正解缓存条目越多,内存占用越高,Guard 匹配也可能更复杂;而且每个新 shape 仍需要一次编译。应该通过 padding 或动态声明减少 shape 变化,而不是靠无限缓存兜底。

为何常见:直觉上缓存命中率越高越好,忽略了缓存本身也有维护成本和内存上限。

误区给所有变量都加上严格守卫,总能保证编译结果绝对正确。

正解TorchDynamo 会自动生成必要守卫;人为添加过多守卫会增加每次调用的检查开销,还可能阻碍编译优化。正确做法是在“覆盖关键假设”和“检查开销”之间保持平衡。

为何常见:人们容易把 Guard 等同于单元测试里的断言,忘了它是在每次真实调用中都会执行的代码。

面试题

Q1. TorchDynamo 中 Guard 的主要职责是什么?

单选题 · easy

  • A.加快 Python 语法词法分析
  • B.验证输入仍符合编译假设← 正确答案
  • C.替代核心 CUDA 算子调度
  • D.减少模型参数显存占用

答案:B。TorchDynamo 编译图的前提是运行时类型、形状、值都未变,Guard 就是验证这些前提的“量体卡”。A 是解释器职责,C 是算子层职责,D 与显存管理无关。

Q2. Guard 检查失败后,TorchDynamo 的下一步是?

单选题 · easy

  • A.直接忽略错误继续运行
  • B.抛出异常停止训练
  • C.重新追踪并编译新图← 正确答案
  • D.切换成普通 Python 执行

答案:C。Guard 失败说明旧编译图不再适用,TorchDynamo 会重新追踪并生成新图,再把新守卫与新图写入缓存。它不会直接抛错,也不会忽略检查,D 的普通执行也不准确。

Q3. 下列哪一项通常不会出现在 Guard 条件中?

单选题 · easy

  • A.张量的形状和类型
  • B.Python 标量的取值
  • C.数据文件所在的磁盘路径← 正确答案
  • D.设备与 requires_grad

答案:C。Guard 关注编译时被固化的计算假设,例如类型、形状、值、dtype、device、requires_grad。数据文件的磁盘路径通常不参与计算图,不需要被守卫。

Q4. 为了减少 Guard 检查开销,检查顺序应优先安排?

单选题 · mid

  • A.预期代价低的守卫在前← 正确答案
  • B.输出结果大的守卫在前
  • C.和重编译无关的守卫
  • D.只在最后执行一次守卫

答案:A。性能优化策略中,按“失败概率 × 检查代价”升序排列,让期望代价低的守卫先执行,尽早失败,减少后续无用检查。

Q5. weakref 守卫主要解决什么问题?

单选题 · mid

  • A.张量形状频繁变化
  • B.缓存与对象循环引用← 正确答案
  • C.梯度计算无法回传
  • D.多卡通信拓扑失效

答案:B。weakref 守卫使用弱引用持有对象,避免编译缓存与 Python 对象之间形成循环引用,从而让对象能被正常回收。

Q6. 常见的 Guard 类型包括哪些?

多选题 · mid

  • A.类型守卫和形状守卫← 正确答案
  • B.常量值守卫← 正确答案
  • C.设备与数据类型守卫← 正确答案
  • D.网络传输带宽守卫

答案:ABC。Guard 会记录类型、形状、常量值以及设备、dtype、requires_grad 等影响计算语义的属性。网络传输带宽不属于 Python 运行时假设。

Q7. 减少 Guard 失效导致的重复编译,可采用的策略有?

多选题 · mid

  • A.固定 batch 或 padding← 正确答案
  • B.显式声明动态维度← 正确答案
  • C.每次随机改变全局常量
  • D.将动态分支拆开编译← 正确答案

答案:ABD。固定 batch 和 padding 可以稳定 shape guard;显式声明动态维度可以生成更宽松的守卫;拆分动态分支能降低单个函数的复杂度。随机改变全局常量只会加剧值守卫失效。

Q8. 关于 Dynamo 缓存与 Guard 的关系,正确的有?

多选题 · hard

  • A.缓存条目绑定守卫条件← 正确答案
  • B.守卫失败会触发重编译← 正确答案
  • C.守卫越多缓存命中率越高
  • D.缓存无限增长没有开销

答案:AB。每个缓存条目都包含 guard_fn 和编译结果,守卫失败后重新编译并更新缓存。但守卫数量与命中率无关,缓存无限增长也会带来内存与匹配开销。

Q9. 日志中出现频繁 Recompiling 后,正确做法是?

单选题 · hard

  • A.立刻关闭 TorchDynamo 避免报错
  • B.分析失败守卫并优化输入稳定性← 正确答案
  • C.把所有 Guard 都删除
  • D.增大 batch 但不改 shape

答案:B。频繁重编译说明输入假设不稳定,应分析失败的是类型、形状还是值守卫,再通过 padding、固定配置、拆分分支等手段稳定输入。关闭编译或删除 Guard 都会损失性能或正确性。

Q10. 下列关于 cache_size_limit 的说法,正确的是?

单选题 · extreme

  • A.限制单个守卫占用的内存
  • B.限制同一函数缓存条目数← 正确答案
  • C.限制 Python 函数最大字节码
  • D.限制张量 shape 的组合数

答案:B。cache_size_limit 控制同一个函数最多保留多少个编译缓存条目,是缓存与内存之间的权衡开关。它不限制单个守卫大小,也不限制字节码或 shape 组合。