资讯动态

大模型训练三大炸点:Loss升天、显存OOM与数据污染的排查清单

发布时间:2026/10/19 11:36:11 来源:尧图企业网站定制
1. 开篇那次loss从3.2跳到NaN的晚班让我把三处风险写成了清单先交代一下背景。去年我在跟一个开源的大模型微调项目跑一个7B规模的模型数据清洗好了学习率按常规设了1e-5一切看起来都正常。然后某个周五晚上loss从3.2一路平稳下降到第1870步时突然跳出一个NaN接着整个训练进程白屏报错日志滚了二十几屏最后只剩一行“Floating point exception”。我当时盯着屏幕看了半分钟唯一的想法是这周白干了。如果你也碰过大模型训练你一定知道我在说什么。大模型训练不像普通深度学习项目它的问题往往不发生在代码写错的瞬间而是发生在你以为一切正常之后的第几百步甚至第几千步。损失函数炸了、显存爆了、数据取出来不对这三个问题几乎占了训练事故的80%以上。而这篇文章要聊的就是这三处最让人头疼的“会炸”的地方我为什么说它们会炸、它们炸之前有什么征兆、以及排查时应该按什么顺序来。标题里说的是“三个会炸的地方”但你别误会这三处不是并列的三种报错而是三个层面的系统性风险数值稳定性层面的loss升天资源管理层面的显存耗尽数据管线层面的隐性污染。它们之间有连带关系比如数据里有坏样本会触发loss爆炸显存不够会把中间状态裁掉又加剧数值波动。所以这篇文章不是给你三个孤立补丁而是给你一套训练前的预检思路把它当成体检清单来用比等到出事再修要省太多时间。适合看这篇的人我默认是正准备跑第一个大模型训练任务、或者已经在跑但还没踩过完整一轮坑的工程师。你不需要是算法研究员但需要对PyTorch、数据加载和分布式训练有基本的概念。接下来我按我的实际排查经验把我踩过的和帮别人排过的坑一个一个拆开讲。2. 第一处会“炸”loss升天——梯度爆炸、学习率与数值不稳定的排查链路2.1 症状与根因不是所有NaN都是梯度爆炸引起的先说最常见的表象训练到某一steploss变成NaN或者先飙到1e8再变成NaN。大多数人第一反应是“梯度爆炸了”然后去调梯度裁剪。但根据我的经验“梯度爆炸”这四个字其实是冗余的病名真正的病根通常在三个地方第一学习率跟模型规模不匹配。大模型的loss曲面比小模型复杂得多同样一个学习率在小模型上能稳定收敛放到大模型上就可能让优化器一步跨过“悬崖”。尤其是7B以上的模型即便用AdamW初始学习率设置过大照样会在一两个step内把权重推到NaN。这个跟优化器无关纯属数值波动。第二数据里有“脏样本”。这在你用爬取语料、未过筛的OCR文本、或者混合了多语言内容时特别明显。某些样本可能包含异常的重复片段、超长的数字序列、或者几乎全是特殊符号模型在这种样本上会产生极端输出对应的梯度也极端化直接污染整个batch的更新方向。第三损失函数本身或模型输出层的数值设计问题。比如你用了fp16但没开loss scaling或者输出层没有归一化却在损失里做了大数乘法又或者标签出现越界值这类问题平时不暴露一旦某个batch碰上了就是致命一击。所以我建议的排查原则是不要一看到NaN就去调参数先定位NaN第一次出现的位置。2.2 排查顺序从第一轮迭代开始逐层定位具体做法分四步。第一步看日志。如果NaN不是从第1步就出现而是中途出现的去看NaN出现前大约50步的loss曲线。如果loss是缓慢上升后突然变NaN大概率是学习率问题或优化器状态异常如果loss原本正常、某一step直接跳变优先怀疑数据。第二步小规模复现。把batch size缩到1把训练步数限制在100步以内在同一份数据切片上反复跑三次。如果小数据量下仍在某个样本附近触发NaN那基本能锁定是数据问题。如果小规模下完全正常则是并发采样或数据顺序带来的“批次组成”问题去查shuffle逻辑和dataloader的worker设置。第三步开启anomaly detection。PyTorch里有个torch.autograd.set_detect_anomaly(True)它会帮你定位到第一个产生inf/NaN梯度的前向或反向操作。代价是训练变慢所以只用于排查不要长期开。第四步检查优化器参数。如果你的模型用了fp16混合精度确认GradScaler没有因为连续的inf梯度把scale值降到极小然后再检查优化器状态里是否已经有NaN。很多情况下权重还没炸但Adam的二阶动量里已经混进NaN了后续所有步都白算。排查完之后我一般会按这四道防线做修复顺序很重要不要跳过前面的直接上重药。2.3 防线设计loss缩放、梯度裁剪与warmup的取舍先说梯度裁剪。很多人把max_grad_norm设为1.0就放心了但在大模型上这个值不一定合适。我看过一份比较可靠的实践经验裁剪阈值应该根据模型有效梯度范数分布来定而不是拍脑袋。你可以先用几轮调试跑记录下正常step的梯度范数取P90值作为裁剪阈值参考比固定1.0要靠谱得多。再说warmup。大模型训练基本必开warmup但warmup步数不是越长越好。对7B模型假设总步数5000步warmup设200步左右就够过长会让模型在前期更新过慢后期又因为lr衰减过快出现新的不稳定。如果你的优化器是AdamW还需要注意beta2的设置某些大模型任务里把beta2从0.999调到0.95可以让训练更稳代价是收敛精度略降。最后说loss缩放。如果你用fp16训练loss scaling的初始值建议别设太低65536起步是常见做法如果频繁触发scale下降先解决数据问题而不是反复调scale策略。这里我要多说一句很多人一看到NaN就想到混合精度但fp16的NaN很大概率只是“背锅”的真正原因还是前面说的三类不要被表面现象带偏。这些防线都设置了之后训练稳定性会明显改善但仍然不能说万无一失因为数值问题只是第一个“炸点”。我接下来要说的显存问题才是让人最无力、最容易“一顿操作全白跑”的坑。3. 第二处会“炸”显存静默OOM——一顿操作全白跑的“隐形杀手”3.1 为什么只有你的规模上不去显存估算的正确逻辑大模型的OOM和传统深度学习不一样。传统项目里OOM通常发生在forward过程中某一层报错干脆利落而大模型训练的OOM往往发生在你改了某个看似无关的参数之后比如把序列长度从2048调到2049或者把梯度累计步数调大结果直接cuda out of memory。它炸得毫无预兆更让人崩溃的是你调参前的那个版本明明还能正常跑50个step。根子在于显存占用不是一个线性问题。模型参数、优化器状态、梯度、激活值、中间buffer这几块加起来每一块的增长规律都不同。模型参数是线性的优化器状态是参数量的若干倍激活值则跟batch size、序列长度、层数、头数共同相关而且某些情况下是超线性增长。我见过最典型的错误估算只算了模型权重和优化器状态的显存忘记算激活值。以7B模型为例fp16权重约14GBAdamW优化器状态通常需要权重的12倍字节数fp32主权重、fp32一阶动量、fp32二阶动量合计16字节每参数光这两块就接近112GB。你看到这个数字可能会说“我这张卡才80GB根本跑不了”但实际上很多人确实用80GB卡在跑7B模型因为激活值并没有按32字节计算而且通过gradient checkpointing把激活显存大幅压缩了。所以我建议你在规划任何大模型训练前做一个分项估算表至少要有这几行模型权重fp16/bf16参数量乘以2字节梯度fp16/bf16参数量乘以2字节通常与权重同尺寸优化器状态AdamW全精度参数量乘以12字节如果启用fused优化器或使用bitsandbytes可以降激活值取决于层数、隐藏维度、序列长度、batch size和checkpoint策略Rough estimation要看具体公式很多框架像transformers的trainer会有模型参数量和分配显存的报告但说实话那只是基座真正决定你卡能不能跑通还是激活值这一项。3.2 从activation checkpoint到序列长度做减法的顺序遇到OOM时大多数人的第一反应是减小batch size。这没有错但“做减法”的顺序大有讲究顺序错了会让你白白损失训练效率。我的建议顺序是优先调整序列长度再考虑gradient checkpointing最后才是batch size。为什么先调序列长度因为序列长度对显存的影响是非线性的尤其在attention层显存占用跟序列长度的平方相关。你把序列长度从4096降到2048attention部分的显存直接降到原来的四分之一这种收益是batch size减半给不了的。第二步是开gradient checkpointing。这个选项在transformers里叫gradient_checkpointing_enable()PyTorch里是torch.utils.checkpoint。它的原理是反向传播时不保存中间激活值等到计算梯度时再重新前向算一遍典型的效果是用多30%左右的计算时间换取约60%-70%的激活显存节省。7B模型上光这一项就能让你在80GB卡上把序列长度拉高一倍性价比极高。第三步才是减小batch size。batch size减半通常会让吞吐量掉一半但显存只减激活值中的一部分线性收益没那么大。如果需要保持等效batch size可以用梯度累积来弥补不过梯度累积会导致有效batch变大涉及后面说的Batchnorm和loss scaling问题这里先不展开。按这个顺序做完还是OOM的话再考虑更激进的方案比如把模型改为8bit加载、使用offload optimizer state到CPU或者把输入序列进行分段训练。但这些方案的副作用都比较复杂能不用就不用。3.3 单卡/多卡切换时的显存坑很多人在单卡上调完了脚本高高兴兴放到多卡上跑结果一发就OOM。这里有个容易被忽略的原因你的配置里可能写了batch_size4在单卡上每个step吃4条样本但切到数据并行DDP/FSDP后每张卡还是吃4条样本整体batch变成了卡数乘以4显存自然不够。反过来还有一个坑有些人为了在多卡上跑通把batch_size调小到单卡都喂不饱GPU的程度导致多卡并行时的通信开销占比变大训练速度不升反降。正确做法是保持单卡batch size不变让总batch size通过卡数和梯度累积组合尽量接近目标值。我印象很深的一次事故某同学把模型从单卡切到8卡其他配置完全没动只改了分布式启动参数结果第二张卡报OOM第一张卡报hang。最后定位下来是数据采样器的问题因为每张卡都创建了一个完整的dataset但dataset内部做了全局shuffle导致每张卡的数据分布完全相同不仅浪费还在某些框架下触发了内存分配的竞争。解决方案是给每个rank设置独立的shuffle seed或者使用框架自带的DistributedSampler。显存相关的坑大概就是这样。但显存再炸报错也是明确的你能看到OOM字样。真正磨人心智的是那种训练不报错、日志也正常、但loss曲线越走越偏或者结果完全不可复现的“慢性炸弹”这就得说到数据管线了。4. 第三处会“炸”数据管线里的“假正常”——乱序、截断与越界4.1 样本长度处理不一致训练到一半shape炸掉大模型训练里最常见的“假正常”数据问题就是样本长度被截断的方式不一致。假设你的语料里有长文本你定义了max_seq_len2048把超过长度的直接截断。听起来没问题但如果你同时混合了不同来源的数据某些来源的tokenizer做了截断某些来源没截断而是返回了超长的input_ids模型前向时就会报shape mismatch或者不报错但attention mask失配导致训练出来的模型注意力分布完全不对。更隐蔽的情况是padding方向的错误。大模型训练通常要求左侧padding或者右侧padding保持一致如果你在预处理时用了右侧padding而推理时改了左侧padding微调出来的模型在生成时会有明显的性能衰退但训练过程完全正常日志也都好看。我处理这类问题的办法是在训练启动前写一个数据完整性检查脚本至少统计三件事样本长度的分布、padding方向的统计、label与input_ids对齐的检查。统计结果用图表打印出来肉眼扫一遍再进训练。这个脚本看起来笨但它能在5分钟内拦住大量无效训练。4.2 Embedding越界与词表不一致另一个典型炸点是词表不一致。常见的复现路径是这样的你在某个开源模型权重上做continue pretraining自己准备了一份语料用该模型的tokenizer处理数据一切都好。但后来你为了提升编解码效率把tokenizer换成另一个或者给模型加了新词重新初始化了embedding矩阵的某些行后面训练时没有同步更新数据预处理导致数据里出现了词表之外的token id。模型不一定报错因为embedding层是查表操作越界id可能直接被随机初始化了或者在某个框架下查到了脏数据于是loss曲线出现周期性的尖刺你又找不到原因。我的建议是在数据预处理pipeline的最后加一个token id范围校验任何超出词表大小的id直接当作异常样本输出而不是静默丢弃。这句话听起来像废话但我见过至少三个项目在“运行正常”的情况下丢失了某类token因为它们都依赖框架的ignore_index行为来兜底结果ignore_index设成了-100有些框架默认不检查范围就这样乱着跑完了整个训练。4.3 对“确定性复现”的执念seed设了为什么还漂最后聊一个很容易引发纠纷的问题为什么设置了seed两次训练出来的结果还是不完全一致很多人的第一反应是“代码有bug”但其实在大模型训练场景下完全确定性的复现本来就是奢侈品。它的干扰源至少有四个CUDA的atomic操作顺序在不同kernel launch下并不稳定基于num_workers0的DataLoader在多进程下不同进程的数据读取顺序会有微小差异使用了某些非确定性算子比如某些attention kernel的flash attention变体以及分布式训练下AllReduce的求和顺序在多卡间不一致。如果你追求的是“可复现的实验结果”我的做法是不追求bit-level一致性但保证结果级别的稳定性。比如同一个超参数跑三次三次的最终损失差异不超过0.05我视为可复现。为了达到这个级别我在确定性上做了三件事设置torch.use_deterministic_algorithms(True)并同时处理它可能报的算子不支持问题固定DataLoader的worker seed并关闭shuffle的全局随机每次实验记录到代码仓库的commit版本。这样即使三次不完全一致排错时也有据可查。数据管线的坑说完了但还有一块很多人会忽略就是多卡分布式训练中那些跟数据和显存都不直接相关的“半颗雷”。标题说三个炸点但我诚意推荐你把它当作第四个预检点因为它一旦发生恢复成本极高。5. 附赠多卡分布式里另半颗雷——通信死锁与checkpoint保存失败5.1 卡住不动的真正原因allreduce等待与动态shape你可能会遇到这种情况训练日志停在某一个step显存没爆进程还活着但就是不往下走。等半小时都没反应CtrlC杀掉重启又卡在同一个step。这就是典型的分布式死锁。最普遍的原因是数据并行组的某个rank因为数据长度不一致提前结束了当前迭代而其他rank还在等它的通信结果。换句话说你的DataLoader在每张卡上生成了不完全相同长度的迭代序列某个rank没有参与到本轮通信就提前推进了导致其他rank一直在等一个不会来的包。要排查这个问题第一件事是确认分布式下所有rank的dataloader是否严格等长。如果你用了自定义采样器必须在切分数据时确保每个rank分到的样本数量一致。尤其是当你用了drop_lastTrue但只在一个rank上生效时死锁几乎是必然的。第二件事是启用NCCL的调试环境变量比如NCCL_DEBUGINFO、NCCL_DEBUG_SUBSYSALL在卡住的那几分钟里能明显看到是哪两个rank之间的通信超时。但说实话大多数情况下日志量巨大不如直接检查数据长度来得快。还有一类死锁原因是模型forward过程中出现了动态shape。例如你在forward里对序列做了按长度的mask然后某些分支导致tensor的shape发生改变且这个改变只发生在部分rank上。这种情况下AllReduce会因为tensor shape不一致报错或者因为shape匹配但数据不对齐产生静默的内存踩踏。处理办法是在模型forward入口加shape断言训练早期宁可慢一点也要跑完前200步别急着开性能优化。5.2 Checkpoint保存失败的三种典型场景第二个多卡下的隐藏炸点是checkpoint保存失败。不是磁盘满那种“低级失败”而是让你丢一整天进度的“高级失败”。场景一多进程同时写同一个checkpoint目录。如果每个rank都执行save_pretrained你会遇到文件被覆盖和目录锁竞争有些进程写了一半被另一个进程重命名。解决方法是只在rank 0上保存保存完成后再通过一个同步原语让其他rank确认。场景二checkpoint保存期间发生通信超时。分布式训练通常有超时设置比如NCCL timeout如果一个rank在保存checkpoint时把计算停了而其他rank还在训练流程里继续推进几轮通信之后就会触发超时。解决方法是让保存操作在单独的进程中完成或者使用异步保存工具不阻塞训练主循环。场景三混合精度状态与checkpoint不兼容。你用fp16训练时保存的optimizer state换到bf16训练时直接load某些框架会报shape mismatch或者直接load出垃圾数据。这个问题在升级框架版本后尤其常见。我建议你在每个checkpoint目录里额外记录一个manifest文件写明模型版本、训练框架版本、精度类型、优化器类型、词表大小加载前先校验manifest而不是直接load。多卡分布式的问题说完最后我把我现在每次训练前必跑的检查清单整理出来算是对这篇文章的落地收尾。6. 收尾我现在的训练前必查清单我自己被上面这些坑挨个伤害过一轮之后现在每次开启大模型训练前都会按固定顺序过一遍这份清单。它不花多少时间但能拦住绝大部分“训练到一半再翻车”的情况。第一项数值稳定性预检。先用小规模数据跑100步打开anomaly detection确认没有NaN或inf同时打印梯度范数分布把梯度裁剪阈值定下来。确认loss在100步内没有异常跳变后再关闭anomaly detection进入正式训练。第二项显存预检。按前面说的分项估算表算出理论显存占用再跑50步实测一次性分配峰值两者做对照。实测峰值建议至少比显存总量低4GB给临时buffer留余量。另外检查gradient checkpointing是否开启序列长度是否为2的幂附近的最优值。第三项数据管线预检。跑一遍数据完整性检查脚本确认token id不越界、样本长度分布合理、padding方向一致、label与input对齐。再验证两件事设相同seed后连续跑两次dataloader确认取出的第一个batch一致用两个不同的seed各跑一遍确认整体分布不会有明显偏差。第四项分布式预检。如果计划用多卡用2卡先跑通100步确认没有死锁、NCCL报错或checkpoint保存问题。再切成目标卡数跑200步观察吞吐量变化确保通信开销没有吃掉单卡的性能优势。第五项可复现性预检。记录commit版本、依赖版本、seed、超参数和数据集版本跑一次完整训练后保存日志。全流程做完这些记录就是你未来排查各种问题的最强索引。最后说一下我的体会训练大模型本身是一件工程复杂性很高的事你不能指望靠“报错了再修”来驾驭它。我见过太多人花两周时间试错最后发现自己第一周的实验全废了只因为一开始没做预检。上面这份清单是我和团队现在每次训练前的固定动作也是我能给同行最实在的建议。如果这篇文章能帮你少熬一个修NaN或OOM的晚上我就没白写。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑