首页 / PyTorch 入门教程 / 调试技巧与常见报错

PyTorch 入门教程

调试技巧与常见报错

本教程共 60 篇 · 第 33 篇 · 更新于 2026-08-17 · 约 4 分钟阅读

PyTorch调试报错DebugModeTORCH_LOGSNaN梯度

本节目标:学会读报错信息、系统排查训练异常,掌握三大高频报错的处理套路,会用 TORCH_LOGS 和 DebugMode 定位更深层的问题。

前面章节零零散散提过一些坑:忘记搬设备、忘记 zero_grad。这章把它们串起来,讲一套调试思路。先记住一句话:报错不可怕,报错信息里九成有答案,剩下那一成靠排查套路。

排查三步走

遇到问题先别急着改代码,按这三步来:

  1. 完整读一遍报错信息,在 Traceback(回溯)里找到你自己的那行代码。
  2. 看它提示的原因:是形状不对?设备不对?还是类型不对?
  3. 用最小例子复现:把出错的那行单独拎出来跑,加 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,参数根本更新不动。常见原因有三个:

  1. 参数没参与前向计算,比如定义了却没用。
  2. 整个前向写在 torch.no_grad() 里。
  3. 输入张量的 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、学习率太大导致梯度爆炸。排查分两步:

  1. 找到 NaN 首次出现的位置:在前向各层输出上检查 torch.isnan(x).any()
  2. 对症下药:给 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 下 DataLoadernum_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。