PyTorch torch.compile 的 fullgraph=False 编程模型:图断点续迹语义、处理策略与调试实战
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
fullgraph=False是torch.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)的三步策略,这三步构成了处理图断点的完整工作流:
- 确定理想的
torch.compile施加位置:通常是不引起过度图断点的最高层函数。做大量预处理或 I/O 的函数会产生大量图断点,从编译中获益甚微。- 可以先编译单个函数/模块以隔离问题,再扩展到整个模型。
- 对编译区域内产生大量图断点且无编译收益的函数使用
torch.compiler.disable:在这种情况下,一个图断点胜过潜在的上百个。 - 用
TORCH_LOGS="graph_breaks"或 tlparse 调查残留图断点,并用与fullgraph=True编程模型相同的手法绕过它们。并非所有图断点都必须消除——有些对性能影响远大于其他;一般原则是聚焦发生在模型计算期间的图断点。- 调试图断点时,文档推荐
torch.compile(backend='eager'),以获得更快的调试迭代速度。
- 调试图断点时,文档推荐
下面逐一展开每一步的实战细节。
2. 第一步:决定 torch.compile 施加在哪里
文档建议将torch.compile施加在"不会造成过多问题"的最高层函数上,通常是:
- 你的
train或eval步(含优化器,但不含循环); - 你的顶层
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.compile与nn.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_graph | Dynamo 前端捕获的输出图 |
before_pre_grad_graph/after_pre_grad_graph | 运行 pre-autograd 图 pass 之前/之后的 FX 图 |
aot_autograd_cache_miss/aot_autograd_cache_hit | aot_autograd_cache 的缓存键及命中情况 |
aot_inference_graph | 无需自动求导时的分解后 FX 图 |
aot_joint_graph | 自动求导与分解后的联合前向-反向图 |
aot_forward_graph/aot_backward_graph | 从aot_joint_graph切分出的前向图/反向图 |
before_joint_graph/after_joint_graph | 联合图 pass 运行之前/之后的 FX 图 |
before_post_grad_graph/inductor_post_grad_graph | post-autograd 图 pass 运行之前/之后的 FX 图 |
fx_graph_runnable | 与before_post_grad_graph基本相同,但是可运行的 Python 脚本,含 torch 配置与包装代码,可用 dummy 输入运行 |
inductor_output_code | Inductor 生成的代码 |
fx_graph_cache_miss/fx_graph_cache_hit | FX 图缓存的键与命中情况 |
dynamo_cpp_guards_str | Dynamo 的 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/fusion | Inductor 调度/性能提示/融合日志 |
两者如何取舍:遇到大问题先用tlparse——它适合调试大模型、获得模型如何被编译的高层全景;TORCH_LOGS更适合小示例与细粒度调试,在你已大致知道是哪个组件出问题时使用。调试图断点阶段可配合torch.compile(backend='eager')加快迭代。
5. 边界行为一:嵌套图断点(Nested Graph Breaks)
理解fullgraph=False的续迹语义,绕不开嵌套图断点。完整文档见 Nested Graph Breaks。
前提:torch.compile施加到某函数后,嵌套函数调用也会被追踪;嵌套图断点指发生在嵌套函数调用中的图断点。回顾fullgraph=False下图断点的处理方式是:编译已确定的 FX 图、用普通 Python 运行不支持的代码、然后以新 FX 图恢复追踪。而恢复追踪只支持在顶层函数上进行——这是一个关键限制,它决定了嵌套图断点的处理链条。
以文档示例为例,torch.compile从f开始追踪,一直追到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调用处断图,再让inner2、inner1依次被自动当作顶层函数编译:
# 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由此引出两个重要结论:
- 这就是你在
torch.compile中可能看到"重复图断点"的原因——上面示例共追踪了 3 个顶层函数,同一个图断点被追踪了 3 次; - 处理该图断点的运行时间是 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的三个关键区别:
error_on_graph_break=True不保证只捕获一个图;- 它可以在编译期间随时切换(通过
torch._dynamo.error_on_graph_break()上下文管理器/装饰器),而fullgraph=True一旦设定就不能改回False; 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区域内再嵌套一层。
fullgraph与error_on_graph_break的完整组合语义汇总(摘自原文档表格):
error_on_graph_break=True | error_on_graph_break=False(默认) | |
|---|---|---|
fullgraph=True | 图断点导致错误,只报告第一个断点,保证单图。fullgraph无法切回False,error_on_graph_break不生效。用户代码必须与torch.compile完全兼容,保证无图断点性能损失。适合对图断点敏感的框架/库代码或追求极致性能的场景 | 与fullgraph=True+error_on_graph_break=True相同(error_on_graph_break在fullgraph=True时无效) |
fullgraph=False(默认) | 图断点导致错误,只报告第一个断点,无单图保证。可切换为False。用户代码必须与torch.compile完全兼容。适合用户代码中对图断点敏感、又存在难以绕过的非关键断点的场景 | 遇到图断点继续编译,报告所有图断点。可切换为True。几乎不需要改动用户代码即可工作,但性能可能受损。适合开箱即用、"常规"代码或不追求极致性能的场景 |
7. 边界行为三:Skipped Functions(被整体跳过的函数)
完整文档见 Skipped Functions。
有时torch.compile在fullgraph=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 的决策清单
把上述机制串起来,一个可执行的检查清单是:
- 定位:
torch.compile加在不含大量预处理/I/O 的最高层函数(推理用model.compile(),训练可包住"forward + loss + backward + step"的训练步);DDP/FSDP 场景编译内层模块; - 隔离:图断点密集或会崩溃的函数(稀疏架构、日志/预处理)用
@torch.compiler.disable(默认连递归调用一起禁用,必要时recursive=False); - 收紧/放宽:性能关键路径用
torch._dynamo.error_on_graph_break(True)强制无断点,难缠的非关键断点用error_on_graph_break(False)放行或 monkey patch 处理第三方代码; - 诊断:
TORCH_LOGS="graph_breaks"看断点位置与原因,大模型用TORCH_TRACE+tlparse --latest看编译全景与重编译热点;调试期用backend='eager'提速; - 核对:确认关键函数没有被整体 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),仅供参考