调试技巧与常见报错
本教程共 60 篇 · 第 33 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:学会读报错信息、系统排查训练异常,掌握三大高频报错的处理套路,会用 TORCH_LOGS 和 DebugMode 定位更深层的问题。
前面章节零零散散提过一些坑:忘记搬设备、忘记 zero_grad。这章把它们串起来,讲一套调试思路。先记住一句话:报错不可怕,报错信息里九成有答案,剩下那一成靠排查套路。
排查三步走
遇到问题先别急着改代码,按这三步来:
- 完整读一遍报错信息,在 Traceback(回溯)里找到你自己的那行代码。
- 看它提示的原因:是形状不对?设备不对?还是类型不对?
- 用最小例子复现:把出错的那行单独拎出来跑,加 print 观察变量。
九成问题在第三步就现形了。下面按「报错三兄弟」逐一过。
报错一:形状不匹配
形状不匹配(size mismatch)是新手第一大坑。典型报错长这样:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x400 and 300x120)
翻译过来:两个矩阵要相乘,32×400 对上了 300×120,400 和 300 对不上。问题多半出在卷积之后展平算错了,或者全连接层的 in_features 写错了。
排查方法:把数据流一层层打出来,不用手写 print 大法,写个循环更省事:
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2),
)
x = torch.randn(4, 3, 32, 32)
for name, layer in model.named_children():
x = layer(x)
print(f"{name}: {tuple(x.shape)}")
每层输出的形状一目了然,哪一层和预期不符,问题就锁定在哪一层。展平后的大小 = 通道数 × 高 × 宽,三个数乘出来,就是你该写进 nn.Linear 的输入维度。
Tip报错里常有
size mismatch, m1: [4 x 400], m2: [300 x 120]。m1 是输入,m2 是权重。看 m1 的最后一维和 m2 的第一维是否相等即可,其余数字不用管。
报错二:设备不一致
第二个高频报错:
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!
模型在 GPU 上,数据还在 CPU 里,两者无法运算。排查方法:print(next(model.parameters()).device) 看模型在哪,print(x.device) 看数据在哪。修复原则就是「谁没搬就搬谁」:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# 训练循环里
inputs, labels = inputs.to(device), labels.to(device)
容易漏的地方:算出来的 loss、后面手动造的张量(比如手写的 one-hot 标签)、验证循环里的数据,统统要确认设备。新代码建议用 torch.accelerator.current_accelerator() 判断设备,一套代码自动适配 CUDA、MPS、XPU,别把 cuda 写死。
报错三:梯度为 None
loss.backward() 之后一检查,param.grad 是 None,参数根本更新不动。常见原因有三个:
- 参数没参与前向计算,比如定义了却没用。
- 整个前向写在
torch.no_grad()里。 - 输入张量的
requires_grad=False,梯度链断了。
排查代码:
loss = criterion(model(x), y)
loss.backward()
for name, p in model.named_parameters():
if p.grad is None:
print(f"{name}: grad is None!")
还有一个姊妹报错:element 0 of tensors does not require grad and does not have a grad_fn。意思是你在给一个不需要梯度的张量求 backward,检查 loss 的源头张量是否 requires_grad=True。
Loss 曲线会说话
训练不一定报错,但 Loss 曲线的形态能暴露问题。第 17 章已经给过四种典型形态(两条都降、训练降验证升、都不降、来回震荡)的判断表,这里不重复,只补充排查顺序:先看训练 loss 自身动不动,再把验证 loss 叠上看差距,最后才动学习率和正则化。画图用本章的 TensorBoard 或 matplotlib,横轴 epoch、纵轴 loss,肉眼一看比盯着单个数字猜靠谱。
NaN 排查与梯度裁剪
Loss 突然变成 NaN(Not a Number,非数字),训练当场爆炸。常见来源:log(0)、除以 0、学习率太大导致梯度爆炸。排查分两步:
- 找到 NaN 首次出现的位置:在前向各层输出上检查
torch.isnan(x).any()。 - 对症下药:给
log加极小值防止取到 0;换小学习率;加梯度裁剪。
NaN 要是出现在反向传播里,torch.isnan 就盯不住了。这时用 detect_anomaly 自动定位:
with torch.autograd.detect_anomaly():
loss.backward()
反向传播一旦产生 NaN 或 Inf,它会直接抛异常并指出出错的位置。平时别开着它(很慢),排查时再启用。
梯度裁剪(Gradient Clipping)是防爆炸的通用手段,把梯度的总长度限制在阈值内:
import torch.nn.utils as utils
utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
放在 loss.backward() 之后、optimizer.step() 之前。训练 RNN 和 Transformer 时它几乎是标配。
更多常见报错速查
下面几个报错出现频率也不低,一并列出来:
IndexError: index out of range:标签越界。检查类别数,特别是把预训练模型的输出层换成自定义类别数时,标签从 0 开始编号。RuntimeError: CUDA out of memory:显存不够。先减 batch size,再考虑换小模型,梯度累积也可以救急。TypeError相关报错:数据类型不匹配。检查dtype,比如模型要 float32,你喂了 float64 的张量。- Windows 下
DataLoader带num_workers报多进程错误:把主流程包进if __name__ == "__main__":,这是 Python 多进程的经典要求。
遇到没见过的报错,把报错原文整段复制去搜,大概率有人踩过同一个坑。搜之前先确认你用的 PyTorch 版本,旧答案的 API 可能已经变了。
TORCH_LOGS:看编译过程
第 32 章的 Profiler 是「事后」工具。想「事前」看 torch.compile 到底把代码变成了什么,用 TORCH_LOGS。它分环境变量和 Python API 两种用法,Python 里这样开:
import torch
@torch.compile()
def fn(x, y):
return x + y + 2
torch._logging.set_logs(dynamo=True) # 看图追踪过程
fn(torch.ones(2, 2), torch.zeros(2, 2))
dynamo=True 打印追踪过程,graph=True 打印捕获的图,output_code=True 打印 Inductor 生成的底层代码。也可以设环境变量 TORCH_LOGS="+dynamo,graph",效果一样;想看全部选项,把 TORCH_LOGS 设成 help。日志很长,按需开,别贪多。
DebugMode:定位数值偏差
训练好好的,一开 torch.compile 结果就变了,怎么定位是哪一步差的?进阶玩法:用 DebugMode。
DebugMode(torch.utils._debug_mode.DebugMode,PyTorch 2.10 起提供,目前是原型功能)会拦截运行时调度,给每个算子的输入输出算一个「哈希值」。同一段代码,eager 跑一遍、编译跑一遍,两份日志对比,哈希第一个对不上的地方就是数值偏差的源头:
import torch
from torch.utils._debug_mode import DebugMode
def run_once():
x = torch.randn(8, 8)
y = torch.randn(8, 8)
return torch.mm(torch.relu(x), y)
with DebugMode(record_output=True) as dm, \
DebugMode.log_tensor_hashes(hash_inputs=True):
out = run_once()
print(dm.debug_string())
日志里每行是「算子(输入) -> 输出 # {‘hash’: …}」的格式。哈希相同说明数值一致,不同说明从这里开始分叉。开 record_stack_trace=True 还能把每步对回你的源代码行号。新手知道有这么个工具就行,真遇到编译数值不一致再回来查文档。
让问题可复现
调试的前提是问题能稳定复现。随机性会让 bug 时有时无,排查前先把种子固定:
import random
import numpy as np
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
注意:GPU 上的部分算子即使固定种子,结果也可能有微小差异。追求严格一致可以再开 torch.backends.cudnn.deterministic = True,代价是速度略降。
小结
调试的套路就一句话:先读报错,再最小化复现,然后逐层缩小范围。这章讲的工具和姿势记个大概就行,真正用的时候再回来查——调试能力是练出来的,不是背出来的。三大高频报错(形状、设备、梯度)各有固定排查姿势;Loss 曲线、NaN 检查、梯度裁剪是训练异常的常规武器;TORCH_LOGS 和 DebugMode 留给编译场景。工具在手,报错不慌。下一章进入计算机视觉的地盘,先认识 PyTorch 的视觉工具箱 TorchVision。