数值异常是深度学习训练里最容易被忽视、也最容易让人抓狂的一类问题。你跑了一个模型loss 曲线前几百步看着挺正常突然某一轮直接飙到inf或者干脆变成nan然后整个网络参数全废后面再怎么训练都是nan。这种情况在 PyTorch 里尤其常见因为张量运算默认不做异常拦截inf和nan会像病毒一样在计算图里扩散。这篇内容就是围绕PyTorch 中 inf 和 nan 数值的处理展开把我这些年踩过的坑、用过的排查手段、以及可以直接抄的代码方案整理出来。不管你是刚入门还在调第一个分类模型还是已经在做多卡训练的老手只要你的 loss 曾经莫名其妙变成nan这篇都值得看完。核心会讲清楚三件事inf和nan到底从哪来、怎么快速定位到出问题的那一行、以及怎么在训练流程里做防御性处理让训练不中断。1. 先搞清楚 inf 和 nan 到底是什么很多人一看到nan就慌其实先把这两个概念分清楚排查方向能少走一半弯路。它们在 IEEE 754 浮点标准里有明确定义不是 PyTorch 特有的东西理解它们的数学性质是后面所有排查手段的基础。1.1 inf 的两种形态与产生条件inf表示无穷大分正无穷inf和负无穷-inf。在 PyTorch 里用torch.isinf()检测返回一个同形状的布尔张量。它产生的典型场景就两类一是数值溢出比如float32能表示的最大值约3.4e38你做一个指数运算torch.exp(torch.tensor(100.0))直接就是inf二是除以零torch.tensor(1.0) / torch.tensor(0.0)得到inf而0.0 / 0.0得到的是nan。这里有个容易被忽略的点inf本身在计算里是可以传染的。inf 1 infinf * 0 naninf - inf nan。所以一旦某个中间张量出现inf后面很可能演变成nan这也是为什么很多人的报错信息里直接看到的是nan而不是inf——源头其实是溢出。import torch a torch.tensor([1.0, 2.0, 0.0]) b torch.tensor([0.0, 1.0, 0.0]) print(a / b) # tensor([inf, 2., nan]) print(torch.isinf(a / b)) # tensor([True, False, False]) print(torch.isnan(a / b)) # tensor([False, False, True])上面这段代码很直观1/0是inf0/0是nan。实际训练里更隐蔽的是log(0)得到-inf、sqrt(负数)得到nan、softmax在极端大值下溢出等这些都不会报错只会静默污染。1.2 nan 的传播性与沉默杀手属性nan全称 Not a Number它最恶心的特性是任何与 nan 的运算结果都是 nan。nan 1 nannan * 0 nan甚至nan nan都是False。这意味着只要计算图里混进一个nan反向传播时梯度全是nan优化器一步更新就把所有参数写成nan模型彻底报废。我见过太多人用if loss float(nan)去判断结果永远进不去分支因为nan ! nan。正确做法只有torch.isnan()或者math.isnan()。这个细节看似小但排查时能省你半小时。注意判断 nan 绝对不能用等号比较nan nan恒为 False这是 IEEE 754 的规定不是 bug。1.3 为什么 PyTorch 默认不拦截PyTorch 的设计哲学是性能优先张量运算走的是底层 C/CUDA 内核逐元素检查inf/nan会带来显著开销所以默认不拦截。这就导致异常数值会静默传播直到你在 loss 上看到nan才发现但那时候已经很难回溯是哪一步出的问题。理解这一点你就明白为什么需要主动加检测——不是 PyTorch 做不到是它为了速度选择不做。2. 训练中 inf/nan 的常见来源拆解知道定义还不够真正难的是定位来源。我把实际项目里遇到过的成因归了几类基本覆盖 90% 的场景你可以对照自己的模型逐条排查。2.1 学习率过大导致的梯度爆炸这是最经典的原因。学习率设成0.1去训一个深层网络前几步 loss 可能还降某一步梯度范数突然爆炸参数更新后变成极大值下一轮前向传播直接溢出成inf再一轮就nan。判断方法很简单在optimizer.step()之前打印梯度范数如果看到某个 batch 的 grad norm 从个位数跳到几千甚至上万基本就是它。total_norm 0.0 for p in model.parameters(): if p.grad is not None: total_norm p.grad.data.norm(2).item() ** 2 total_norm total_norm ** 0.5 print(fgrad norm: {total_norm})梯度裁剪是最直接的解法torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)一行搞定。但裁剪只是止血根因还是学习率或网络结构问题得配合调整。2.2 除零与 log(0) 类运算自定义 loss 或数据预处理里特别容易出现。比如做归一化时x / x.std()某个 batch 的 std 恰好为 0或者算torch.log(pred)时 pred 里有 0。这类问题的特点是偶发大部分 batch 正常偶尔一个 batch 触发排查起来很烦。防御手段是加一个极小值epstorch.log(pred 1e-8)归一化写成x / (x.std() 1e-8)。1e-8是float32下的常用经验值太小会被浮点精度吃掉太大又影响数值准确性。如果是float16混合精度训练建议用1e-5甚至1e-4因为半精度能表示的最小正规数大得多。2.3 混合精度训练中的溢出用torch.cuda.amp做混合精度时float16的动态范围只有约6e-5到65504比float32窄得多。梯度稍微大一点就溢出成inf。好在 AMP 自带 GradScaler 会动态调整 scale 因子溢出时自动跳过该步并降低 scale但如果你的 loss 本身就算出了infScaler 也救不了。实际经验是混合精度下要格外注意 loss 里的指数、除法运算尽量在float32下算 loss前向用float16。autocast上下文里做 loss 计算时可以用with torch.autocast(device_typecuda, enabledFalse):临时关掉。2.4 数据本身带脏值这个最容易被忽略。输入数据里如果有nan比如 CSV 里缺失值没处理、传感器采集的异常点前向传播第一步就污染了。我建议在Dataset.__getitem__里加一道检查或者在collate_fn里统一过滤。数据侧的检查成本低、收益高比在模型里到处埋检测点划算得多。成因类别典型表现首选排查手段学习率过大grad norm 突增后 loss 变 nan打印梯度范数、梯度裁剪除零/log(0)偶发、特定 batch 触发加 eps、检查自定义算子混合精度溢出训练若干步后突然 nan检查 GradScaler、loss 用 fp32数据脏值第一个 batch 就 nanDataset 层加 isnan 检查3. 定位问题让异常数值无处可藏光知道成因不够实战里最耗时的就是到底是哪一行出的问题。PyTorch 提供了几个工具配合起来能把范围缩到很小。3.1 用 torch.autograd.set_detect_anomaly 抓反向传播元凶这是官方给的慢速调试模式。开启后反向传播会逐算子检查一旦发现nan就抛出异常并打印出问题的前向算子。代价是速度慢好几倍所以只在调试时开。with torch.autograd.set_detect_anomaly(True): loss model(x) loss.backward()它会告诉你类似 Function DivBackward0 returned nan values in its 0th output 的信息直接指向除法反向。我第一次用它定位到一个自定义 normalization 里的除零五分钟解决比盲猜快太多。缺点是它只查反向前向的nan还得靠别的手段。3.2 前向传播埋点逐层检查输出前向的nan用 hook 最方便。给每个子模块注册forward_hook检查输出张量是否含inf/nan一旦发现就打印模块名。def check_hook(module, input, output): if isinstance(output, torch.Tensor): if torch.isnan(output).any() or torch.isinf(output).any(): print(f[异常] {module.__class__.__name__} 输出含 inf/nan) for name, m in model.named_modules(): m.register_forward_hook(check_hook)跑一个 batch控制台直接告诉你哪一层开始出问题。注意 hook 会拖慢速度定位完记得移除。这个方法对定位第几层开始崩特别有效尤其是深层网络里某一层 BN 的 running stats 出问题时。3.3 二分法缩小范围如果 hook 输出太多不好看用二分法先检查模型中间某层的输出正常就往后半段查异常就往前半段查。配合model.named_modules()拿到层列表几轮就能锁定。这个方法土但极其可靠我在一个上百层的模型上用过四轮定位到具体是某个 attention 分支的 scale 除了零。提示调试时把 batch size 调成 1、关掉 shuffle能让问题稳定复现比随机 batch 好排查得多。4. 防御性编程让训练流程扛住异常数值定位是事后手段真正省心的是事前防御。核心思路是在关键节点做检查发现异常就跳过该 batch 或降级处理而不是让整个训练崩掉。下面这套方案我在多个项目里用过稳定性提升明显。4.1 loss 检查与跳过策略最外层加一道 loss 检查nan或inf就跳过这个 batch 的更新。注意要跳过整个backward step不能只跳过 step否则梯度里已经污染了。loss criterion(output, target) if torch.isnan(loss) or torch.isinf(loss): print(检测到异常 loss跳过该 batch) optimizer.zero_grad() continue loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() optimizer.zero_grad()这里有个细节跳过前一定要zero_grad()把可能残留的脏梯度清掉。另外建议加个计数器连续跳过超过 N 个 batch 就报警或停止因为连续异常往往意味着模型已经坏了硬撑没意义。4.2 梯度层面的检查loss 正常不代表梯度正常。有时候 loss 是有限值但反向传播中途溢出梯度变nan。所以在step()之前检查梯度def check_grads(model): for name, p in model.named_parameters(): if p.grad is not None: if torch.isnan(p.grad).any() or torch.isinf(p.grad).any(): print(f[梯度异常] {name}) return False return True loss.backward() if not check_grads(model): optimizer.zero_grad() continue这个检查有开销全量检查每个参数在大模型上会明显拖慢。折中方案是只检查最后几层或者每隔 N 步检查一次。我一般调试期全查稳定后改成抽样。4.3 参数更新后的兜底极端情况下即使 loss 和梯度都正常optimizer.step()后参数也可能因为更新量过大变成inf。加一道参数检查做最后兜底optimizer.step() bad False for p in model.parameters(): if torch.isnan(p).any() or torch.isinf(p).any(): bad True break if bad: print(参数更新后出现异常回滚) # 需要提前保存 state_dict 才能回滚回滚需要提前存一份参数快照内存开销大一般只在关键实验里用。更轻量的做法是发现异常直接终止训练保存现场供分析别让坏参数继续跑。4.4 用 torch.nan_to_num 做数值清洗有些场景不适合跳过比如推理阶段或数据预处理这时候用torch.nan_to_num()把异常值替换成指定值更合适。x torch.tensor([1.0, float(nan), float(inf), float(-inf)]) y torch.nan_to_num(x, nan0.0, posinf1e4, neginf-1e4) print(y) # tensor([1.0000e00, 0.0000e00, 1.0000e04, -1.0000e04])nan、posinf、neginf三个参数分别指定替换值。注意这是清洗不是修复替换值的选择要结合业务含义比如特征归一化后inf替换成该特征的最大合理值而不是随便填 0。5. 实操心得与常见问题速查这一节是我这些年攒下来的零碎经验都是文档里不会写、但实际能救命的东西。5.1 几个反直觉的坑第一个坑float16下1e-8直接下溢成 0。很多人抄了float32的eps1e-8到混合精度代码里结果log(x 1e-8)里1e-8变成 0等于没加。半精度下最小正规数约6e-5eps至少用1e-5。第二个坑torch.isnan()对整型张量会报错。如果你不确定张量类型先.float()再检查或者用torch.is_floating_point(x)判断。第三个坑BN 层的running_mean和running_var也可能变nan而且它们不在model.parameters()里梯度检查查不到。排查时要单独看model.buffers()。5.2 常见问题速查表现象可能原因解决方向第一个 batch 就 nan数据含脏值Dataset 层加 isnan 过滤训练几百步后 nan学习率过大/梯度爆炸降 lr、加梯度裁剪混合精度下偶发 nanfp16 溢出loss 用 fp32、调 GradScaler只有某些层 nan该层自定义算子除零加 eps、检查算子实现推理时输出 nan 但训练正常BN 的 running stats 异常检查 buffers、重算统计量5.3 一个实用的调试习惯我现在写任何带自定义 loss 或复杂算子的代码都会在开发阶段加一个debugTrue开关打开时每个 batch 检查 loss、梯度、参数三层一旦异常立刻pdb.set_trace()断下来看现场。上线前关掉开关零性能损耗。这个习惯帮我省了无数个通宵排查的夜晚——异常数值最怕的就是事后才发现能在发生的那一刻断下来问题基本当场就能看清。另外保存 checkpoint 时顺手存一份当前 loss 和 grad norm 的日志等训练崩了回头看日志能快速判断是渐变式恶化还是突变式爆炸这对定位根因帮助很大。渐变式通常是学习率或数据分布问题突变式多半是某个 batch 的极端值触发两者排查方向完全不同。
阅读完成 · 觉得有帮助?