PyTorch 2.0 故障排查 (旧版)#
创建于:2025 年 6 月 6 日 | 最后更新于:2025 年 6 月 6 日
作者: Michael Lazos
注意
本文档已过时,现在主要作为如何运行 torch.compile
最小化器的主要资源。请参阅更新的故障排查文档。此外,还有一个更全面的 torch.compile 手册可供查阅。
我们正在积极开发调试工具、分析器,并改进我们的错误和警告信息。下表列出了可用的工具及其典型用法。如需更多帮助,请参阅诊断运行时错误。
工具 |
目的 |
用法 |
---|---|---|
信息日志记录 |
查看编译的摘要步骤 |
|
调试日志记录 |
查看详细的编译步骤(打印每条追踪的指令) |
|
适用于任何后端的最小化器 |
为任何后端查找复现错误的最小子图 |
设置环境变量 |
|
如果已知错误发生在 |
设置环境变量 |
Dynamo 精度最小化器 |
当您怀疑问题出在 |
|
Inductor 精度最小化器 |
当您怀疑问题出在后端(例如 inductor)时,查找在 eager 模式模型和优化模型之间复现精度问题的最小子图。如果此方法无效,请尝试 Dynamo 精度最小化器。 |
|
|
查找图中断并显示其原因 |
|
记录/回放 |
记录和回放帧,以复现在图捕获期间的错误 |
|
TorchDynamo 函数名过滤 |
仅编译具有给定名称的函数,以在调试问题时减少干扰 |
设置环境变量 |
TorchInductor 调试日志记录 |
打印通用的 TorchInductor 调试信息和生成的 Triton/C++ 代码 |
|
TorchInductor 追踪 |
显示每个 TorchInductor 阶段所花费的时间 + 输出代码和图可视化 |
设置环境变量 TORCH_COMPILE_DEBUG=1 或 |
除了信息和调试日志记录外,您还可以使用 torch._logging 进行更细粒度的日志记录。
诊断运行时错误#
从高层次来看,TorchDynamo 堆栈由一个从 Python 代码进行的图捕获(TorchDynamo)和一个后端编译器组成。例如,一个后端编译器可能包括反向图追踪(AOTAutograd)和图降级(TorchInductor)*。错误可能发生在堆栈的任何组件中,并会提供完整的堆栈追踪。
要确定错误发生在哪个组件,您可以使用信息级日志记录 torch._logging.set_logs(dynamo = logging.INFO)
或 TORCH_LOGS="dynamo"
并查找 Step #: ...
输出。日志在每个步骤的开始和结束时生成,因此错误对应的步骤是最近记录的、其结束尚未被记录的步骤。这些步骤对应于堆栈的以下部分
步骤 |
组件 |
---|---|
1 |
TorchDynamo |
2 |
编译器后端 |
3 |
TorchInductor |
如果信息日志记录不足,您可以使用可用的后端选项。这些选项包括
"eager"
:仅运行 TorchDynamo 前向图捕获,然后用 PyTorch 运行捕获的图。这可以表明 TorchDynamo 是否引发了错误。"aot_eager"
:运行 TorchDynamo 捕获前向图,然后 AOTAutograd 追踪反向图,没有任何额外的后端编译器步骤。然后 PyTorch eager 将用于运行前向和反向图。这对于将问题缩小到 AOTAutograd 很有用。
缩小问题范围的一般步骤如下
使用
"eager"
后端运行您的程序。如果错误不再出现,则问题出在正在使用的后端编译器中(如果使用 TorchInductor,请继续执行第 2 步。否则,请参阅最小化后端编译器错误)。如果使用"eager"
后端时错误仍然出现,则是由Torchdynamo 错误引起的。仅当使用
TorchInductor
作为后端编译器时,此步骤才必要。使用"aot_eager"
后端运行模型。如果此后端引发错误,则错误发生在 AOTAutograd 追踪期间。如果此后端不再出现错误,则最小化 TorchInductor 错误。
以下各节将分析这些情况。
注意
TorchInductor 后端既包含 AOTAutograd 追踪,也包含 TorchInductor 编译器本身。我们将通过将 TorchInductor
称为后端,并将 TorchInductor 降级称为降低由 AOTAutograd 追踪的图的阶段来进行区分。
Torchdynamo 错误#
如果生成的错误在使用 "eager"
后端时出现,那么 TorchDynamo 很可能是错误的来源。下面是一个会产生错误的代码示例。
import torch
import torch._dynamo as dynamo
def test_assertion_error():
y = torch.ones(200, 200)
z = {y: 5}
return z
compiled_test_assertion_error = torch.compile(test_assertion_error, backend="eager")
compiled_test_assertion_error()
上述代码会生成以下错误
torch._dynamo.convert_frame: [ERROR] WON'T CONVERT test_assertion_error /scratch/mlazos/torchdynamo/../test/errors.py line 26
due to:
Traceback (most recent call last):
File "/scratch/mlazos/torchdynamo/torchdynamo/symbolic_convert.py", line 837, in BUILD_MAP
assert isinstance(k, ConstantVariable) or (
AssertionError
from user code:
File "/scratch/mlazos/torchdynamo/../test/errors.py", line 34, in test_assertion_error
z = {y: 5}
Set torch._dynamo.config.verbose=True for more information
==========
正如消息所建议的,您可以设置 torch._dynamo.config.verbose=True
以获取指向 TorchDynamo 中错误和用户代码的完整堆栈跟踪。除了这个标志,您还可以通过 torch._logging.set_logs(dynamo = logging.INFO)
或 TORCH_LOGS="dynamo"
设置 TorchDynamo 的 log_level
。这些级别包括
logging.DEBUG
或TORCH_LOGS="+dynamo"
:除了下面列出的所有日志级别外,还打印遇到的每条指令。logging.INFO
:打印每个被编译的函数(原始和修改后的字节码)以及捕获的图,此外还包括下面列出的所有日志级别。logging.WARNING
(默认): 除了下面列出的所有日志级别之外,还打印图中断。logging.ERROR
:仅打印错误。
如果模型非常大,日志可能会变得难以处理。如果错误发生在模型 Python 代码的深层,为了便于调试,只执行发生错误的帧会很有用。有两个工具可以实现这一点
将环境变量
TORCHDYNAMO_DEBUG_FUNCTION
设置为所需的函数名将只对具有该名称的函数运行 torchdynamo。启用记录/回放工具(设置
torch._dynamo.config.replay_record_enabled = True
),它会在遇到错误时转储执行记录。然后可以回放此记录,以仅运行发生错误的帧。
诊断 TorchInductor 错误#
如果错误在使用 "eager"
后端时没有出现,那么后端编译器就是错误的来源(错误示例)。TorchDynamo 有不同的后端编译器选择,其中 TorchInductor 满足大多数用户的需求。本节重点介绍 TorchInductor 作为激励示例,但某些工具也可以与其他后端编译器一起使用。
下面是我们关注的堆栈部分
选择 TorchInductor 作为后端后,AOTAutograd 用于从 torchdynamo 捕获的前向图中生成反向图。需要注意的是,错误可能发生在此追踪过程中,也可能发生在 TorchInductor 将前向和反向图降级为 GPU 代码或 C++ 的过程中。一个模型通常由数百或数千个 FX 节点组成,因此要缩小问题发生的具体节点可能非常困难。幸运的是,有工具可以自动将这些输入图最小化到导致问题的节点。第一步是确定错误是发生在用 AOTAutograd 追踪反向图的过程中,还是发生在 TorchInductor 降级过程中。如上文第 2 步所述,"aot_eager"
后端可用于仅隔离运行 AOTAutograd 而不进行降级。如果使用此后端时错误仍然发生,则表明错误发生在 AOTAutograd 追踪期间。
这里有一个例子
import torch
import torch._dynamo as dynamo
model = torch.nn.Sequential(*[torch.nn.Linear(200, 200) for _ in range(5)])
def test_backend_error():
y = torch.ones(200, 200)
x = torch.ones(200, 200)
z = x + y
a = torch.ops.aten._foobar(z) # dummy function which errors
return model(a)
compiled_test_backend_error = torch.compile(test_backend_error, backend="inductor")
compiled_test_backend_error()
运行此代码应该会给您这个错误,并在其下方显示更长的堆栈跟踪
Traceback (most recent call last):
File "/scratch/mlazos/torchdynamo/torchinductor/graph.py", line 246, in call_function
return lowerings[target](*args, **kwargs)
File "/scratch/mlazos/torchdynamo/torchinductor/lowering.py", line 185, in wrapped
return decomp_fn(*args, **kwargs)
File "/scratch/mlazos/torchdynamo/torchinductor/lowering.py", line 810, in _foobar
assert False
AssertionError
...
如果你接着将 torch.compile(backend="inductor")
改为 torch.compile(backend="aot_eager")
,它将无错误运行,因为问题出在 TorchInductor 的降级过程中,而不是在 AOTAutograd 中。
最小化 TorchInductor 错误#
从这里开始,我们运行最小化器来获得一个最小的复现。设置环境变量 TORCHDYNAMO_REPRO_AFTER="aot"
(或直接设置 torch._dynamo.config.repro_after="aot"
)将生成一个 Python 程序,该程序将 AOTAutograd 生成的图简化为能复现错误的最小子图。(请参见下面的示例,其中我们最小化了 TorchDynamo 生成的图)使用此环境变量运行程序应该会显示几乎相同的输出,并附加一行指示 minifier_launcher.py
已被写入的位置。输出目录可通过将 torch._dynamo.config.base_dir
设置为有效的目录名来配置。最后一步是运行最小化器并检查其是否成功运行。一次成功的运行看起来像这样。如果最小化器成功运行,它会生成可运行的 Python 代码,精确复现错误。对于我们的示例,这是以下代码
import torch
from torch import tensor, device
import torch.fx as fx
from torch._dynamo.testing import rand_strided
from math import inf
from torch.fx.experimental.proxy_tensor import make_fx
# torch version: 1.13.0a0+gitfddfc44
# torch cuda version: 11.6
# torch git version: fddfc4488afb207971c54ad4bf58130fdc8a4dc5
# CUDA Info:
# nvcc: NVIDIA (R) Cuda compiler driver
# Copyright (c) 2005-2022 NVIDIA Corporation
# Built on Thu_Feb_10_18:23:41_PST_2022
# Cuda compilation tools, release 11.6, V11.6.112
# Build cuda_11.6.r11.6/compiler.30978841_0
# GPU Hardware Info:
# NVIDIA A100-SXM4-40GB : 8
from torch.nn import *
class Repro(torch.nn.Module):
def __init__(self):
super().__init__()
def forward(self, add):
_foobar = torch.ops.aten._foobar.default(add); add = None
return (_foobar,)
args = [((200, 200), (200, 1), torch.float32, 'cpu')]
args = [rand_strided(shape, stride, dtype, device) for shape, stride, dtype, device in args]
mod = make_fx(Repro())(*args)
from torch._inductor.compile_fx import compile_fx_inner
compiled = compile_fx_inner(mod, args)
compiled(*args)
Repro
模块的 forward
方法包含了导致问题的确切操作。在提交问题时,请包含任何最小化的复现代码以帮助调试。
最小化后端编译器错误#
对于除 TorchInductor 之外的后端编译器,查找导致错误的子图的过程与最小化 TorchInductor 错误中的过程几乎相同,但有一个重要的注意事项。即,最小化器现在将在 TorchDynamo 追踪的图上运行,而不是在 AOTAutograd 的输出图上运行。让我们来看一个例子。
import torch
import torch._dynamo as dynamo
model = torch.nn.Sequential(*[torch.nn.Linear(200, 200) for _ in range(5)])
# toy compiler which fails if graph contains relu
def toy_compiler(gm: torch.fx.GraphModule, _):
for node in gm.graph.nodes:
if node.target == torch.relu:
assert False
return gm
def test_backend_error():
y = torch.ones(200, 200)
x = torch.ones(200, 200)
z = x + y
a = torch.relu(z)
return model(a)
compiled_test_backend_error = torch.compile(test_backend_error, backend=toy_compiler)
compiled_test_backend_error()
为了在 TorchDynamo 追踪完前向图后运行代码,你可以使用 TORCHDYNAMO_REPRO_AFTER
环境变量。使用 TORCHDYNAMO_REPRO_AFTER="dynamo"
(或 torch._dynamo.config.repro_after="dynamo"
)运行此程序应该会产生此输出以及在 {torch._dynamo.config.base_dir}/repro.py
中的以下代码。
注意
TORCHDYNAMO_REPRO_AFTER 的另一个选项是 "aot"
,它将在生成反向图后运行最小化器。
import torch
import torch._dynamo as dynamo
from torch import tensor, device
import torch.fx as fx
from torch._dynamo.testing import rand_strided
from math import inf
from torch._dynamo.debug_utils import run_fwd_maybe_bwd
from torch.nn import *
class Repro(torch.nn.Module):
def __init__(self):
super().__init__()
def forward(self, add):
relu = torch.relu(add); add = None
return (relu,)
mod = Repro().cuda()
opt_mod = torch.compile(mod, backend="None")
args = [((200, 200), (200, 1), torch.float32, 'cpu', False)]
args = [rand_strided(sh, st, dt, dev).requires_grad_(rg) for (sh, st, dt, dev, rg) in args]
with torch.cuda.amp.autocast(enabled=False):
ref = run_fwd_maybe_bwd(mod, args)
res = run_fwd_maybe_bwd(opt_mod, args)
最小化器成功地将图简化为在 toy_compiler
中引发错误的操作。与最小化 TorchInductor 错误中的过程的另一个区别是,在遇到后端编译器错误后,最小化器会自动运行。成功运行后,最小化器会将 repro.py
写入 torch._dynamo.config.base_dir
。
性能分析#
访问 TorchDynamo 分析器#
TorchDynamo 有一个内置的统计函数,用于收集和显示在每个编译阶段花费的时间。在执行 Torch._Dynamo 后,可以通过调用 torch._dynamo.utils.compile_times()
来访问这些统计信息。默认情况下,这会返回一个字符串表示,按名称显示在每个 TorchDynamo 函数中花费的编译时间。
使用 TORCH_COMPILE_DEBUG 调试 TorchInductor#
TorchInductor 有一个内置的统计和追踪功能,用于显示在每个编译阶段花费的时间、输出代码、输出图可视化和 IR 转储。这是一个调试工具,旨在使理解和解决 TorchInductor 内部问题变得更容易。
让我们用以下测试程序(repro.py
)运行一个示例
import torch
@torch.compile()
def test_model(x):
model = torch.nn.Sequential(
torch.nn.Linear(10, 10),
torch.nn.LayerNorm(10),
torch.nn.ReLU(),
)
return model(x)
y = test_model(torch.ones(10, 10))
设置环境变量 TORCH_COMPILE_DEBUG=1
将创建一个调试跟踪目录,默认情况下该目录将在当前目录下,名为 torch_compile_debug(这可以在 torchdynamo 配置字段 debug_dir_root
以及环境变量 TORCH_COMPILE_DEBUG_DIR
中覆盖)。在此目录内,每次运行都会有一个单独的文件夹,以运行的时间戳和进程 ID 命名。
$ env TORCH_COMPILE_DEBUG=1 python repro.py
$ cd torch_compile_debug
$ ls
run_2023_03_01_08_20_52_143510-pid_180167
在运行文件夹中,会有一个 torchdynamo
目录,其中包含调试日志,以及一个 torchinductor
文件夹,其中包含每个已编译内核的子文件夹,内有 inductor 调试工件。
$ cd
run_2023_03_01_08_20_52_143510-pid_180167
$ ls
torchinductor torchdynamo
进一步进入 torchinductor
目录,\*.log
文件是来自编译的 AOT Autograd 阶段的日志,model__0_forward_1.0
包含了 inductor 调试工件。
$ cd torchinductor
$ ls
aot_model___0_debug.log model__0_forward_1.0
$ cd model__0_forward_1.0
$ ls
debug.log fx_graph_readable.py fx_graph_runnable.py fx_graph_transformed.py ir_post_fusion.txt ir_pre_fusion.txt output_code.py
以下是内容的摘要
fx_graph_readable.py
和fx_graph_runnable.py
是 inductor 接收到的fx_graph
的可读和可运行版本。fx_graph_transformed.py
是在 inductor 运行所有 fx 遍后的 fx 图。ir\*.txt
是融合前后的 inductor IR。output_code.py
是为子图编译的 triton 内核。
这是测试程序的示例调试目录内容
import torch
@torch.compile()
def test_model(x):
model = torch.nn.Sequential(
torch.nn.Linear(10, 10),
torch.nn.LayerNorm(10),
torch.nn.ReLU(),
)
return model(x)
y = test_model(torch.ones(10, 10))
该调试跟踪中的每个文件都可以通过 torch._inductor.config.trace.*
来启用和禁用。配置文件和图表默认都是禁用的,因为生成它们开销很大。
在这种新的调试格式中,单个节点看起来像这样
buf1: SchedulerNode(ComputedBuffer)
buf1.writes =
{ MemoryDep(name='buf1', index=0, size=()),
MemoryDep(name='buf1', index=0, size=(s0,))}
buf1.unmet_dependencies = {MemoryDep(name='buf0', index=c0, size=(s0,))}
buf1.met_dependencies = {MemoryDep(name='primals_2', index=c0, size=(s0,))}
buf1.group.device = cuda:0
buf1.group.iteration = (1, s0)
buf1.sizes = ([], [s0])
class buf1_loop_body:
var_ranges = {z0: s0}
index0 = z0
index1 = 0
def body(self, ops):
get_index = self.get_index('index0')
load = ops.load('buf0', get_index, False)
get_index_1 = self.get_index('index0')
load_1 = ops.load('primals_2', get_index_1, False)
add = ops.add(load, load_1)
get_index_2 = self.get_index('index1')
reduction = ops.reduction('buf1', torch.float32, torch.float32, 'sum', get_index_2, add)
return reduction
请参阅示例调试目录输出以获取更多示例。
图中断#
给定一个像这样的程序
def some_fun(x):
...
compiled_fun = torch.compile(some_fun, ...)
...
TorchDynamo 会尝试将 some_fun 中所有的 torch/tensor 操作编译成一个单一的 FX 图,但它可能无法将所有内容捕获到一个图中。
一些图中断的原因是 TorchDynamo 无法克服的,并且不容易修复。- 调用除 torch 之外的 C 扩展对 torchdynamo 是不可见的,并且可能在 TorchDynamo 无法引入必要的守卫(请参阅使 Dynamo 健全:守卫)以确保编译后的程序可以安全重用的情况下执行任意操作。如果产生的片段很小,图中断会影响性能。为了最大化性能,尽可能少地出现图中断是很重要的。
识别图中断的原因#
为了识别程序中所有的图中断以及中断的相关原因,可以使用 torch._dynamo.explain
。该工具在提供的函数上运行 TorchDynamo,并汇总遇到的图中断。以下是一个使用示例
import torch
import torch._dynamo as dynamo
def toy_example(a, b):
x = a / (torch.abs(a) + 1)
print("woo")
if b.sum() < 0:
b = b * -1
return x * b
explanation = dynamo.explain(toy_example)(torch.randn(10), torch.randn(10))
print(explanation_verbose)
"""
Graph Count: 3
Graph Break Count: 2
Op Count: 5
Break Reasons:
Break Reason 1:
Reason: builtin: print [<class 'torch._dynamo.variables.constant.ConstantVariable'>] False
User Stack:
<FrameSummary file foo.py, line 5 in toy_example>
Break Reason 2:
Reason: generic_jump TensorVariable()
User Stack:
<FrameSummary file foo.py, line 6 in torch_dynamo_resume_in_toy_example_at_5>
Ops per Graph:
...
Out Guards:
...
"""
输出包括
out_guards
- 一个列表的列表,其中每个子列表包含必须通过以确保追踪图有效的守卫。graphs
- 成功追踪的图模块列表。ops_per_graph
- 列表的列表,其中每个子列表包含在图中运行的操作。
要在遇到的第一个图中断处抛出错误,请使用 fullgraph
模式。此模式禁用 TorchDynamo 的 Python 回退,并且只有当整个程序可转换为单个图时才会成功。使用示例
def toy_example(a, b):
...
compiled_toy = torch.compile(toy_example, fullgraph=True, backend=<compiler>)(a, b)
过度重编译#
当 TorchDynamo 编译一个函数(或其一部分)时,它会对局部变量和全局变量做出某些假设,以允许编译器优化,并将这些假设表示为在运行时检查特定值的守卫。如果这些守卫中的任何一个失败,Dynamo 将重新编译该函数(或部分),最多可达 torch._dynamo.config.recompile_limit
次。如果您的程序达到了缓存限制,您首先需要确定哪个守卫失败了,以及程序的哪个部分触发了它。
如果您的程序表现出有界的动态性,您也许可以调整 TorchDynamo 缓存限制,以允许每个变体被编译和缓存,但如果缓存限制太高,您可能会发现重编译的成本超过了任何优化带来的好处。
torch._dynamo.config.recompile_limit = <your desired cache limit>
TorchDynamo 计划支持许多常见的动态张量形状情况,例如变化的批量大小或序列长度。它不计划支持秩动态。在此期间,设置特定的缓存限制可以与分桶技术配合使用,为某些动态模型实现可接受的重编译次数。
精度调试#
如果您设置环境变量 TORCHDYNAMO_REPRO_LEVEL=4
,精度问题也可以被最小化,它采用类似的 git bisect 模型运行,一个完整的复现可能类似于 TORCHDYNAMO_REPRO_AFTER="aot" TORCHDYNAMO_REPRO_LEVEL=4
。我们需要这个的原因是,下游编译器会生成代码,无论是 Triton 代码还是 C++ 后端,这些下游编译器的数值可能会有细微的差别,但会对您的训练稳定性产生巨大影响。因此,精度调试器对于我们检测代码生成或后端编译器中的错误非常有用。
如果您想确保随机数生成在 torch 和 triton 之间是相同的,那么您可以启用 torch._inductor.config.fallback_random = True
扩展调试#
可以通过使用以下实验性标志来启用扩展调试。
TORCHDYNAMO_EXTENDED_DEBUG_GUARD_ADDED
- 如果守卫的字符串表示与此标志值匹配,则提供扩展调试信息。例如,将其设置为 “Ne(s0, 10)” 以在每次发出该守卫时生成完整的 Python 和 C++ 回溯。TORCHDYNAMO_EXTENDED_DEBUG_CREATE_SYMBOL
- 在分配特定符号时提供扩展调试信息。例如,将其设置为 “u2” 以在每次创建此符号时生成完整的 Python 和 C++ 回溯。TORCHDYNAMO_EXTENDED_DEBUG_CPP
- 为所有扩展调试设置以及错误提供扩展调试信息(C++ 回溯)。例如,将其设置为 “1”。C++ 回溯速度慢且信息量非常大,因此默认情况下不包含在扩展调试中。
冷启动计时和缓存损坏调试#
为了测量冷启动编译时间或调试缓存损坏,可以传递 TORCHINDUCTOR_FORCE_DISABLE_CACHES=1
或设置 torch._inductor.config.force_disable_caches = True
,这将覆盖任何其他缓存配置选项并禁用所有编译时缓存。