手把手教你:loss 变 NaN 用 hook 逐层定位|AI工程师踩坑实录·第4篇


本文是《AI工程师踩坑实录》系列第 4 篇。训练跑得好好的,几百步之后 loss 突然变 NaN,重启之后换个位置再崩一次。这一篇用真机复现两个经典 NaN 现场,再教你用 forward hook 把「哪一步、哪一层、哪个值先溢出」钉出来。

本篇配套的 nan_doctor 定位脚本(hook 逐层监控 + 两个崩溃案例)关注公Z号

loss 变 NaN 这个坑的特点是,它不给任何解释。

训练曲线一路正常,某个瞬间直接跳成 NaN。重启再跑,崩的位置还不一样。网上搜一圈,答案清一色是「调小学习率」,至于多小、为什么、除了学习率还有什么可能,没人说。

这一篇把这三件事一次说清。全部过程在我这台 GTX 1660 Ti 笔记本上真实复现,一个 5829 参数的小模型就够用,不需要 GPU。

一、先看一个完整崩溃现场

我用 SGD 配了个离谱的学习率 100,跑一个三层小网络。真实输出,一步没剪。

五个步骤,一条完整的死亡时间线。0.2027,正常。2.7 万,不妙。2.55 万亿,已经很吓人。第四步 inf。第五步,NaN,彻底阵亡。

这张截图里最有价值的是最后三行。

第一步,NaN 出现在第 5 步。第二步,前一步(第 4 步)loss 先溢出成 inf,这就是预警信号。第三步,hook 定位,第 5 步,layer0 的输出首次出现 inf。

二、inf 是烟感报警器,NaN 是房子烧没了

很多人把 inf 和 NaN 当一回事,其实它们是事故链上前后两个阶段。

loss 变 inf,说明某次计算结果超出了 float32 的上限(约 3.4×10^38)。此时网络还没死透,值还「存在」,只是大到存不下。

到了下一步,权重里已经有了 inf。前向传播拿 inf 去做运算,inf 减 inf、零乘 inf,浮点规则规定这些运算结果一律是 NaN。从这一刻起,值变成了「不是数」,污染顺着网络一层层扩散,再也回不来了。

所以排查 NaN 的第一原则,盯住 inf 出现的那一步。它是烟感报警器,报警器响的时候火才刚起,等 NaN 出现,房子已经烧完了。

三、hook 逐层定位,给每一层装行车记录仪

知道了哪一步,还想知道哪一层。方法是用 PyTorch 的 forward hook,给每个线性层挂一个记录器,每一步前向传播时记下这一层输出的最小值、最大值、有没有 inf 或 NaN。

回到上面那张截图的定位输出,第 5 步,layer0 的输出首次出现 inf。信息量很大。溢出不是从深层开始的,是第一层就爆了,权重被学习率推飞了。要修的是学习率,不是模型结构。

这套监控的完整实现在资源包第 4 章,给每一层注册 hook 只需要三行代码,定位完删掉就行。

四、第二个经典 NaN,手写损失里的 log(0)

学习率过大只是 NaN 的来源之一。另一类高发地是自定义损失函数,看第二个真实案例。

手写交叉熵,softmax 之后直接取 log。logits 稍微一大,softmax 输出里就会出现精确的 0,log(0) 等于负无穷,浮点规则里 0 乘负无穷直接 NaN。第一步,一步都没训练就阵亡。

这个案例的迷惑性在于,它跟学习率无关,调参调到天荒地老也没用。两个正确写法,用数值稳定的 log_softmax(推荐),或者给 log 里的值加个下限保护。

五、修复对照,只差一个数量级

把学习率从 100 降到 0.05,其他一个字不改。真实结果。

200 步,loss 从 0.2 平稳降到 0.0100,训练准确率 1.000。两条曲线放一起看更直观。

同一个模型,同一份数据,一条曲线五步升天,一条曲线两百步收敛,中间只隔一个学习率的数量级(实际差了 2000 倍)。

六、常见问题 3 连

1. 网上都说调小学习率,到底调到多小?

别猜,用二分法。当前值除以 10 跑一次,还炸再除以 10。两次之内基本能找到稳定区间的上沿。原理说穿了很简单,单步更新量超过了当前地形允许的最大步长,权重就会被推飞,学习率控制的就是这个步长。

2. 除了学习率和 log(0),还有哪些常见 NaN 源?

按我实际遇到的频率排个序。除法或归一化的分母可能是 0。开方或 log 的输入可能是负数或 0。混合精度训练里 fp16 上限只有 65504,比 fp32 容易溢出得多。梯度一直累积从不裁剪。数据本身混进了 inf 或超大异常值。逐个排,从损失函数开始查,它是最常见的事发地。

3. hook 监控会拖慢训练吗?

前向 hook 只在每步多算几个统计量,开销可以忽略。我的习惯是定位完就移除。但给生产训练脚本常驻一个轻量检查器很值,每步只判一次 loss 是不是 NaN,是就地保存现场并停止,比跑完两小时才发现白跑要强。

七、本篇小结

  1. NaN 出现前大概率有 inf 预警,盯住 inf 那一步,别等房子烧完。
  2. forward hook 逐层监控,哪层先溢出一行定位出来。
  3. NaN 的两大高发地是过大的学习率和自定义损失里的 log、除法、开方。

本篇坑卡片

字段 内容
坑名 学习率过大导致 MSE 损失溢出,loss 五步内从 0.2 变 NaN
现象原文 step 4 loss = inf,step 5 loss = nan
环境 Win11,Python 3.12,torch 2.9.1+cu126(CPU 复现),5829 参数三层 MLP
复现步骤 SGD lr=100 momentum=0.9 → 第 2 步 2.7 万 → 第 3 步 2.55 万亿 → 第 4 步 inf → 第 5 步 NaN
根因 MSE 梯度随输出无界增长,步长超过曲率允许上限,权重溢出后经 inf 传导为 NaN
解法 学习率降一个数量级起步做二分排查;用 forward hook 记录每层输出范围定位首溢出层
避坑建议 自定义损失优先用 log_softmax;训练脚本常驻轻量 NaN 检查器
可信度 本机验证
版本时间戳 2026-11-04,torch 2.9.1+cu126
关联坑 第 3 篇标签错位(同为数据与训练稳定性赛道)

专栏导航

篇次 主题 状态
1 nvidia-smi 显示的 CUDA 版本到底是不是你装的那个 ✅
2 torch.cuda.is_available() 一直是 False,一条命令定位断点 ✅
3 标签和图像没对齐,模型一直在学噪声 ✅
4 loss 变 NaN,用 hook 逐层定位 ✅ (本篇)
5 DataLoader 卡死在第一个 batch,Windows 多进程的坑 排期中
6 装了两套 CUDA 还在串,PATH 与优先级的真相 排期中
7 数据泄漏:预处理做在了划分之前 排期中
8 RAG 答一半开始编,检索阈值与拒答机制实操 排期中
9 随机种子都设了,结果还是不可复现 排期中
10 相似度全是 0.9:向量没归一化 排期中
11 开了混合精度就 NaN,GradScaler 到底在做什么 排期中
12 loss 不降反升,先别怀疑模型结构 排期中
13 训练降验证不降:先查数据再谈正则 排期中

说明:no kernel image(需新架构显卡)、WSL2 装驱动、docker --gpus 三题本机暂不支持真实复现,按「跑不出来就换题」原则暂缓,后续具备条件再补番外篇。第二季余下选题(top-k 拐点、Tokenizer 截断、ONNX 对不上等)陆续更新。
说明:no kernel image(需新架构显卡)、WSL2 装驱动、docker --gpus 三题本机暂不支持真实复现,按「跑不出来就换题」原则暂缓,后续具备条件再补番外篇。


下一篇预告,《no kernel image is available,显卡算力与编译目标错配》。换了张新显卡,代码一行没改,训练直接报这个错。重装驱动、重装 CUDA 全没用,因为框架在编译的时候就没带你的显卡。下一篇讲怎么对照算力表一次选对版本。


版权说明,本文崩溃现场与曲线均来自本人真机复现,专栏读者可学习使用,禁止转售或整篇搬运。