PyTorch torch.compile 的 fullgraph=False 编程模型:图断点续迹语义、处理策略与调试实战
2026/9/10 0:03:05 网站建设 项目流程

PyTorch torch.compile 的 fullgraph=False 编程模型:图断点续迹语义、处理策略与调试实战

【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch

fullgraph=Falsetorch.compile的默认设置,也是大多数模型接入编译时的起点。它的核心语义是:当 Dynamo 追踪过程中遇到无法编译的代码(graph break,图断点)时,先编译并运行已累积的图,再用普通 Python 执行不支持的代码,然后从断点处恢复追踪——这套"断点续迹"机制比fullgraph=True复杂得多,却也是工程实践中最灵活的接入模式。读完本文,你将掌握:如何决定torch.compile的施加位置、如何用torch.compiler.disable隔离问题代码、如何用TORCH_LOGS/tlparse定位残留图断点,以及嵌套图断点、error_on_graph_break、skipped functions 等边界行为的准确语义。

以下内容基于仓库文档 Working with fullgraph=False 展开,并结合仓库中的关联章节与源码实现加以佐证。

1. fullgraph=False 下的推荐处理策略

原文档给出了使用torch.compile(fullgraph=False)的三步策略,这三步构成了处理图断点的完整工作流:

  1. 确定理想的torch.compile施加位置:通常是不引起过度图断点的最高层函数。做大量预处理或 I/O 的函数会产生大量图断点,从编译中获益甚微。
    • 可以先编译单个函数/模块以隔离问题,再扩展到整个模型。
  2. 对编译区域内产生大量图断点且无编译收益的函数使用torch.compiler.disable:在这种情况下,一个图断点胜过潜在的上百个。
  3. TORCH_LOGS="graph_breaks"或 tlparse 调查残留图断点,并用与fullgraph=True编程模型相同的手法绕过它们。并非所有图断点都必须消除——有些对性能影响远大于其他;一般原则是聚焦发生在模型计算期间的图断点
    • 调试图断点时,文档推荐torch.compile(backend='eager'),以获得更快的调试迭代速度。

下面逐一展开每一步的实战细节。

2. 第一步:决定 torch.compile 施加在哪里

文档建议将torch.compile施加在"不会造成过多问题"的最高层函数上,通常是:

  • 你的traineval步(含优化器,但不含循环);
  • 你的顶层nn.Module
  • 或某些子nn.Module

与分布式封装模块配合torch.compile对 DDP/FSDP 这类分布式包装模块支持不佳,建议将torch.compile施加在传给封装器的内层模块上。三种典型写法:

# 推理 model = ... model.compile() for _ in range(N_ITERS): inp = ... out = model(inp)
# 训练 model = ... opt = torch.optim.Adam(model.parameters()) @torch.compile def train(mod, data): opt.zero_grad(True) pred = mod(data[0]) loss = torch.nn.CrossEntropyLoss()(pred, data[1]) loss.backward() opt.step() for _ in range(N_ITERS): inp = ... train(model, inp)
# DistributedDataParallel:先 compile 内层模块,再包 DDP model = ... model.compile() model_ddp = DistributedDataParallel(model, ...) for _ in range(N_ITERS): inp = ... out = model_ddp(inp)

compile(model)vsmodel.compile()

由于torch.compilenn.Module实例交互存在一些细微差别,当希望把模块作为顶层函数编译时,应使用nn.Module.compile()方法。嵌套的模块调用会被正确追踪——不需要对它们再调用.compile()

# 不要这样写 model = MyModel() model = torch.compile(model) model(inp) # 应该这样写 model = MyModel() model.compile() model(inp) # 这也是一种可接受写法 @torch.compile def fn(model, inp): return model(inp) model = MyModel() fn(model, inp)

此外,将torch.compile施加到较小的重复区域(例如单个 transformer block)而不是整个模型,也能显著缩短编译时间,相关做法见 Reducing Compile Time 中的区域化/分层编译章节。更多细节见 Where to apply torch.compile?。

3. 第二步:用 torch.compiler.disable 隔离问题代码

对于某些模型架构,存在特别难以编译的部分——要么图断点密集,要么会崩溃。此时可以用@torch.compiler.disable装饰器显式禁用这些部分,让torch.compile只作用于能正常工作的部分。语义是:当torch.compile试图调用被禁用的函数时,会打断图并跳过该函数的追踪,在调用结束后恢复追踪。默认情况下,从被禁用函数发出的所有递归调用也都被禁用;用recursive=False可允许递归调用继续编译。完整说明见 Disabling and Suppressing Errors。

默认行为(递归调用也被禁用):

def inner1(x): torch._dynamo.graph_break() # 不会被追踪 return x + 1 # 不会被追踪 @torch.compiler.disable def outer1(x): x = x + 2 # 不会被追踪 torch._dynamo.graph_break() # 不会被追踪 return inner1(x) @torch.compile def f(x): x = outer1(x) return x + 4 # 会被追踪 print(f(torch.ones(3)))

使用recursive=False后,被禁用函数内部调用的inner2仍会正常追踪(其中显式的graph_break()会生效,因为追踪是活跃的):

def inner2(x): torch._dynamo.graph_break() # 会被追踪 return x + 1 # 会被追踪 @torch.compiler.disable(recursive=False) def outer2(x): x = x + 2 # 不会被追踪 torch._dynamo.graph_break() # 不会被追踪 return inner2(x) @torch.compile def g(x): x = outer2(x) return x + 4 # 会被追踪 print(g(torch.ones(3)))

典型适用场景:

  • 推荐模型中的稀疏架构——稀疏部分难以编译,适合整体禁用;
  • 预处理与日志函数——天然产生大量图断点且编译收益低。

另外,如果遭遇编译器崩溃但仍希望继续运行,可设置torch._dynamo.config.suppress_errors = True:编译器崩溃时会跳过该函数的追踪并在之后重试。文档明确强调这不是最佳实践——更好的做法是最终按需手动添加disable注解。

4. 第三步:用 TORCH_LOGS 与 tlparse 调查残留图断点

调查残留图断点有两条互补路径,完整文档见 tlparse / TORCH_TRACE。

4.1 tlparse:大模型编译的高层全景

收集一条编译 trace 的方法很简单:

TORCH_TRACE="/tmp/tracedir" python foo.py pip install tlparse tlparse /tmp/tracedir --latest
  • --latest处理目录中最新的日志;也可以用tlparse <log_file>处理指定文件;
  • 输出默认存到tl_out文件夹,可用-o my_folder指定输出目录;
  • 分布式任务同样适用,每个 rank 都会产生一份 trace,并在浏览器中打开 HTML 报告。

非 PyTorch 开发者也能从中提取关键信息:

  • 哪些模型代码被编译了(查看 stack trie,对陌生代码库尤其有用);
  • 有多少图断点/编译区域——每次独立编译是一个颜色编码块(如[0/0]),可能被打断的帧呈浅绿色(如[2/4]);帧数量过多说明存在灾难性图断点或代码与torch.compile不匹配;
  • 某个帧重编译了多少次——频繁重编译的帧形如[10/0][10/1][10/2],非常可疑,值得排查;
  • 是否发生了编译错误——出错的帧形如红色的[0/1]
  • 某个帧生成了哪些中间编译器产物——例如高层 FX 图或生成的 Triton 代码;
  • 特定帧的元信息——在compilation_metrics中查找。

报告中关键中间产物文件(并非所有程序都会出现全部文件):

文件说明
dynamo_output_graphDynamo 前端捕获的输出图
before_pre_grad_graph/after_pre_grad_graph运行 pre-autograd 图 pass 之前/之后的 FX 图
aot_autograd_cache_miss/aot_autograd_cache_hitaot_autograd_cache 的缓存键及命中情况
aot_inference_graph无需自动求导时的分解后 FX 图
aot_joint_graph自动求导与分解后的联合前向-反向图
aot_forward_graph/aot_backward_graphaot_joint_graph切分出的前向图/反向图
before_joint_graph/after_joint_graph联合图 pass 运行之前/之后的 FX 图
before_post_grad_graph/inductor_post_grad_graphpost-autograd 图 pass 运行之前/之后的 FX 图
fx_graph_runnablebefore_post_grad_graph基本相同,但是可运行的 Python 脚本,含 torch 配置与包装代码,可用 dummy 输入运行
inductor_output_codeInductor 生成的代码
fx_graph_cache_miss/fx_graph_cache_hitFX 图缓存的键与命中情况
dynamo_cpp_guards_strDynamo 的 guard 信息

安全提示:trace 日志包含你的全部模型代码,但不包含权重;若模型敏感请勿外传。提交复杂问题的 bug 报告时,建议附上/tmp/tracedir的 trace 日志,或打包全部 tlparse 输出tl_out中所有文件)——不要只附index.html,它只是输出文件的目录而非真实产物。

4.2 TORCH_LOGS:细粒度调试

TORCH_LOGS环境变量可选择性打开torch.compile各组件的日志,它也是 tlparse 的日志来源:

TORCH_LOGS="<option1>,<option2>,..." python foo.py

也可以编程式设置:

import logging torch._logging.set_logs(graph_breaks=True, dynamic=logging.DEBUG)

最常用的选项:

  • graph_breaks:记录用户代码中图断点的位置及原因;
  • guards:记录生成的 guards;
  • recompiles:记录哪个函数发生了重编译、哪个 guard 检查失败;
  • dynamic:动态形状相关日志;
  • output_code:Inductor 生成的代码。

更多有用选项:

选项说明
+all输出所有torch.compile组件的调试日志
+dynamo输出 TorchDynamo 的调试日志
+aot输出 AOTAutograd 的调试日志
+inductor输出 TorchInductor 的调试日志
graph_code输出 Dynamo 生成的 FX 图 Python 代码
graph_sizes输出 FX 图的张量尺寸
trace_bytecode输出 Dynamo 正在追踪的字节码指令及符号解释器栈
trace_source输出 Dynamo 当前追踪的原始源码行
bytecode输出 Dynamo 生成的字节码
guards输出生成的 guards
recompiles/recompiles_verbose输出重编译原因(仅首个失败 guard / 全部失败 guard)
aot_graphs/aot_joint_graphs输出 AOTAutograd 生成的(联合)图
output_code/kernel_code输出 Inductor 生成的(按 kernel 的)代码
schedule/perf_hints/fusionInductor 调度/性能提示/融合日志

两者如何取舍:遇到大问题先用tlparse——它适合调试大模型、获得模型如何被编译的高层全景;TORCH_LOGS更适合小示例与细粒度调试,在你已大致知道是哪个组件出问题时使用。调试图断点阶段可配合torch.compile(backend='eager')加快迭代。

5. 边界行为一:嵌套图断点(Nested Graph Breaks)

理解fullgraph=False的续迹语义,绕不开嵌套图断点。完整文档见 Nested Graph Breaks。

前提:torch.compile施加到某函数后,嵌套函数调用也会被追踪;嵌套图断点指发生在嵌套函数调用中的图断点。回顾fullgraph=False下图断点的处理方式是:编译已确定的 FX 图、用普通 Python 运行不支持的代码、然后以新 FX 图恢复追踪。而恢复追踪只支持在顶层函数上进行——这是一个关键限制,它决定了嵌套图断点的处理链条。

以文档示例为例,torch.compilef开始追踪,一直追到inner1中的图断点:

def inner1(x): x = x + 1 torch._dynamo.graph_break() # 因图断点停止追踪 return x + 2 def inner2(x): x = x + 4 x = inner1(x) x = x + 8 @torch.compile def f(x): x = x + 16 x = inner2(x) x = x + 32 f(torch.randn(3))

由于只能从顶层函数恢复,实际语义等价于:先在f中对inner2调用处断图,再让inner2inner1依次被自动当作顶层函数编译

# torch.compile(f)(x) 的语义大致等价于: def compiled_f_semantics(x): y = x + 16 z = inner2(y) # 此处断图 return torch.compile(resume_f_semantics)(z) def resume_f_semantics(x): return x + 32 # inner2 被自动编译为顶层函数,再次追到 inner1 的图断点,再对 inner1 调用断图; # inner1 被自动编译,其内部的显式 graph_break() 按常规方式处理: def compiled_inner1_semantics(x): y = x + 1 torch._dynamo.graph_break() return torch.compile(resume_inner1_semantics)(y) def resume_inner1_semantics(x): return x + 2

由此引出两个重要结论:

  1. 这就是你在torch.compile中可能看到"重复图断点"的原因——上面示例共追踪了 3 个顶层函数,同一个图断点被追踪了 3 次;
  2. 处理该图断点的运行时间是 O(NK):N 为嵌套深度,K 为从顶层函数到图断点的指令数;会追踪 O(N²) 个帧,同一个图断点被追踪 O(N) 次。

处理流程可概括为:从顶层函数追踪至嵌套图断点 → 在顶层函数对第二层函数调用处断图 → 编译并运行已追踪的 PyTorch ops → 调用第二层函数(它被自动编译为顶层函数)→ 在该调用后恢复追踪。

6. 边界行为二:error_on_graph_break 的精细开关

fullgraph=True/False是两个极端,error_on_graph_break提供了中间的精细控制。完整文档见 Toggling error_on_graph_break。

  • error_on_graph_break=False(初始值):遇到图断点或编译器错误时,torch.compile尝试在断点/错误后继续编译;
  • error_on_graph_break=True:终止编译并把错误传播到用户代码。

fullgraph=True的三个关键区别:

  1. error_on_graph_break=True不保证只捕获一个图
  2. 可以在编译期间随时切换(通过torch._dynamo.error_on_graph_break()上下文管理器/装饰器),而fullgraph=True一旦设定就不能改回False
  3. error_on_graph_break优先级低于fullgraph,仅在fullgraph=False时生效。

在"总体严格(error_on_graph_break=True)+ 局部宽松"的场景下,把难缠的图断点隔离进error_on_graph_break(False)函数即可放行:

@torch._dynamo.error_on_graph_break(False) def code_with_a_difficult_graph_break(x): x = x + 1 torch._dynamo.graph_break() return x + 2 def inner(x): return code_with_a_difficult_graph_break(x) # 注意:fullgraph=False @torch._dynamo.error_on_graph_break(True) @torch.compile def fn(x): return inner(x) # 不报错,但存在图断点 fn(torch.randn(3))

它也可以作为上下文管理器使用;对于无法编辑源码的第三方/框架代码,还可以用猴子补丁切换:

class ThirdPartyModule(torch.nn.Module): def forward(self, x): x = x + 1 torch._dynamo.graph_break() return x + 2 tp_mod = ThirdPartyModule() tp_mod.forward = torch._dynamo.error_on_graph_break(False)(tp_mod.forward) @torch._dynamo.error_on_graph_break(True) @torch.compile def fn(x): return tp_mod.forward(x) # 不报错,但存在图断点 fn(torch.randn(3))

反向场景(总体宽松 + 性能关键路径严格)同样成立:外层error_on_graph_break=False下,把关键计算包进error_on_graph_break(True)区域,该区域内的图断点会直接报错。error_on_graph_break的设置会影响嵌套调用,且可以在另一个error_on_graph_break区域内再嵌套一层。

fullgrapherror_on_graph_break的完整组合语义汇总(摘自原文档表格):

error_on_graph_break=Trueerror_on_graph_break=False(默认)
fullgraph=True图断点导致错误,只报告第一个断点,保证单图fullgraph无法切回Falseerror_on_graph_break不生效。用户代码必须与torch.compile完全兼容,保证无图断点性能损失。适合对图断点敏感的框架/库代码或追求极致性能的场景fullgraph=True+error_on_graph_break=True相同(error_on_graph_breakfullgraph=True时无效)
fullgraph=False(默认)图断点导致错误,只报告第一个断点,无单图保证。可切换为False。用户代码必须与torch.compile完全兼容。适合用户代码中对图断点敏感、又存在难以绕过的非关键断点的场景遇到图断点继续编译,报告所有图断点。可切换为True。几乎不需要改动用户代码即可工作,但性能可能受损。适合开箱即用、"常规"代码或不追求极致性能的场景

7. 边界行为三:Skipped Functions(被整体跳过的函数)

完整文档见 Skipped Functions。

有时torch.compilefullgraph=False下遇到图断点或其他编译器错误时无法恢复追踪,此时它会干脆放弃编译该函数、整体以 eager 方式运行,从而可能丢失优化机会。注意:跳过只作用于当前函数,不影响其嵌套函数调用——嵌套调用仍会被尝试编译。

典型触发场景与规避手法:

(1)循环中的图断点——无法恢复:

@torch.compile def fn(x): for i in range(5): x = x + 1 if i == 3: torch._dynamo.graph_break() return x fn(torch.randn(3))

规避方法:手动展开循环,使图断点落在可恢复的位置:

@torch.compile def fn(x): def inner(i): nonlocal x x = x + 1 if i == 3: torch._dynamo.graph_break() inner(0) inner(1) inner(2) inner(3) inner(4) return x fn(torch.randn(3))

(2)上下文管理器中的图断点——多数上下文管理器中无法恢复。规避方法是把图断点移出with块:

@torch.compile def fn(x): with CustomCtxManager(): x = x + 1 torch._dynamo.graph_break() with CustomCtxManager(): return x + 1 fn(torch.randn(3))

但有例外:Dynamo 对部分上下文管理器支持断点后恢复。从源码结构看,支持列表位于 torch/_dynamo/variables/torch.py 的supported_ctx_manager_classes,凡是在 torch/_dynamo/variables/ctx_manager.py 中由ContextWrappingVariable子类表示的上下文管理器都支持恢复。例如contextlib.nullcontext()torch.no_grad()组合内即可断点续迹:

import contextlib @torch.compile def fn(x): with contextlib.nullcontext(): with torch.no_grad(): x = x + 1 torch._dynamo.graph_break() return x + 1 fn(torch.randn(3))

(3)try 块中的图断点——无法恢复,规避方法同样是把图断点移出 try 块(把 try 拆成两段)。

(4)触达重编译上限——见 Changing the Cache Size Limit;(5)编译器错误——部分导致函数被跳过,部分则直接报硬错误。

处理 skipped functions 的一般原则:优先修复导致跳过的底层图断点/错误;若难以修复,就把图断点/错误隔离到独立的小函数中,把被跳过的范围降到最低(原文档示例即用嵌套的problematic_code()包住torch._dynamo.skip_frame(),使其余部分继续参与编译)。

8. 端到端小结:fullgraph=False 的决策清单

把上述机制串起来,一个可执行的检查清单是:

  1. 定位torch.compile加在不含大量预处理/I/O 的最高层函数(推理用model.compile(),训练可包住"forward + loss + backward + step"的训练步);DDP/FSDP 场景编译内层模块;
  2. 隔离:图断点密集或会崩溃的函数(稀疏架构、日志/预处理)用@torch.compiler.disable(默认连递归调用一起禁用,必要时recursive=False);
  3. 收紧/放宽:性能关键路径用torch._dynamo.error_on_graph_break(True)强制无断点,难缠的非关键断点用error_on_graph_break(False)放行或 monkey patch 处理第三方代码;
  4. 诊断TORCH_LOGS="graph_breaks"看断点位置与原因,大模型用TORCH_TRACE+tlparse --latest看编译全景与重编译热点;调试期用backend='eager'提速;
  5. 核对:确认关键函数没有被整体 skip(循环/上下文管理器/try 块内的断点是最常见的 skip 诱因,按第 7 节手法改写);留意嵌套图断点导致的 O(N) 次重复断点与重复编译。

配套文档索引:Dynamo 核心概念、常见图断点、fullgraph=True 编程模型、重编译机制。

【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询