大模型训练避坑手记:数据、Loss与分布式工程排雷
搞大模型训练这几年我最深的体会就是训练任务挂掉从来不是一声炸雷而是层层埋雷。你以为万事俱备结果数据 pipeline 里一个重复采样分支tokenizer 里一个没对齐的特殊 token分布式集合通信里某个节点慢了两秒都会让前面几千张卡跑了一星期的实验变成一场昂贵的无声事故。这篇手记我重点整理三个我反复踩坑、也反复帮人排雷的高危区域数据配比与 tokenizer、loss 动态与梯度健康度、分布式训练的基础设施韧性。无论你是在单机八卡上调 7B 模型还是准备上大规模集群做预训练这三个地方都值得在开工前逐项过一遍。1. 数据这一关往往是第一个爆雷的地方大模型和小模型一个最大的不同就是数据错了不再只是“精度下降”这么简单。在传统分类任务里坏样本顶多让某个类别召回率变低你还能从测试集上发现问题但大模型预训练是一个“数据驱动”的过程数据分布稍有偏差损失曲线可能仍然降得很好看可模型学会的东西已经悄悄偏离了你的预期。这种问题最难的地方在于它不会让训练崩掉而是让训练“成功地产出垃圾”。我见过不止一次团队花了大量算力把模型跑到收敛结果下游任务评测一出来通用能力全面拉垮查到最后才发现是数据配比阶段就埋下了雷。所以这一节先说数据相关的三个常见爆点。1.1 数据配比失衡loss 曲线看着正常实际已经跑偏数据配比指的是预训练时不同来源、不同领域、不同质量的语料在整体样本里占的比例。很多人下意识觉得“语料越多越好、各领域平均放就行”实际操作下来根本不是这样。如果代码数据和百科数据比例失当模型最后的数学推理能力会明显偏弱如果低质量的网页文本占比过高模型又会出现严重的重复生成倾向。最坑的是这时候只看整体 loss 是发现不了问题的因为 loss 是海量 token 的平均某个领域的退化会被其他领域的正常表现掩盖。我建议开工前做三件事。第一建立一张显式的配比表按 token 数而不是按文件数分配比例把每个领域的目标占比写死并在训练日志里输出每个领域实际消耗的 token 数。第二训练时按数据源维度拆开记录 loss至少按大类拆成代码、百科、论文、对话、网页等几个桶每 500 步输出一次分桶 loss。第三跑一个 1B 左右的小模型做配比验证实验用下游任务榜单快速对比不同配比的效果差异而不是直接在大模型上反复试错。这里还要提一个容易被忽略的点数据配比是在“去重之后”才有意义的。如果先做了全局去重再统计各个领域的 token 量比例才是真实的如果拿原始未经处理的数据做配比重复样本会被数成两份配比表的参考价值就打了折扣。所以配比计算一定要挂在去重之后而不是去重之前。1.2 tokenizer 里的隐蔽炸弹vocab 对不上、截断与静默丢数据tokenizer 看起来是数据处理里最不起眼的一环实际是“炸”起来最安静的一个。我遇到过一次特别典型的故障用旧版词表跑完了几百亿 token 的数据清洗中途换了新版词表结果下游任务里只要遇到某些特殊字符生成结果直接乱码。表面原因是 vocab 对不上本质上是在数据清洗阶段没有把 tokenizer 版本和训练阶段锁死。这个问题在大规模数据流水线里特别容易发生因为清洗任务和训练任务经常由不同的人、不同的时间执行词表一更新前面清洗出来的数据就全部作废了。除了版本对齐截断问题也非常常见。很多数据清洗脚本为了图省事把超过长度限制的文本直接截断但不会告诉你哪些文本被截了、截了多少。这意味着模型实际上看到的文本是不完整的句子甚至可能把一个句子的前半部分和另一个完全无关的文本的后半部分拼接在一起。这种“静默截断”比显式报错更难排查因为它不报错只是让训练数据的质量持续缓慢下降。我的操作习惯是在 tokenizer 处理之后做一次全量统计包括总 token 数、序列长度分布、被截断样本数量、特殊 token 出现的频次。然后在正式训练前随机抽样 100 条样本把原始文本和 token 解码后的文本对照着看一遍确认编码-解码闭环没有损失信息。这一步看起来笨但确实能拦住绝大部分 tokenizer 层面的隐性故障。另外还要注意分布式训练时所有数据并行节点必须使用完全相同的 tokenizer 配置词表文件本身也应该做一次校验值比对防止某个节点的词表文件在传输过程中损坏。1.3 数据污染与去重重复样本比坏样本更可怕数据污染和去重是大模型预训练里最容易低估的一环。很多人以为去重就是“去掉完全相同的文本”但实际操作中真正破坏训练稳定性的是大量“高度相似”的样本——同一条新闻在不同网站转载了几十次同一个代码片段被不同仓库复制来复制去。这些高度相似样本如果不处理模型会把大量容量花在背这些重复内容上表现出来就是 loss 降得特别快、但下游任务泛化能力差甚至某些 token 序列被模型“背”得过多导致生成时出现顽固的复读机现象。我一般会做三层去重。第一层是精确去重去掉完全相同的文本这一步成本最低。第二层是模糊去重用基于哈希的近似算法比如 minhash 思路去掉高相似样本这一步需要比较多计算资源但对配比表的真实性和训练稳定性都有明显帮助。第三层是“高质量数据优先”的筛选策略这一步不完全是去重而是给数据做质量分级低质量且低相似度的数据可以降采样保留少量作为多样性补充。值得提醒的是去重对 loss 曲线的影响并不是立竿见影的很多人做完去重之后发现第一轮 loss 反而比不去重时高以为去重去坏了。其实这是正常现象去掉重复样本之后模型没有捷径可走必须真正学习文本中的模式所以初期 loss 会更高、收敛会稍微慢一点但后续的泛化效果会明显更好。这个预期一定要提前跟团队对齐不然很容易在去重这一步半途而废。2. 训练动态里的“定时炸弹”loss、梯度与学习率数据关过了第二个炸点来自训练过程本身。大模型训练周期长、成本高一旦 loss 或梯度出现异常你不可能像小模型那样快速重跑一遍而是要在尽量少损失算力的前提下快速定位、止血、恢复。这一节我会聊三个训练动态中最常见的爆点loss spike、梯度爆炸或消失、学习率与 warmup 设置不当。2.1 loss spike不是玄学是故障信号loss spike 指的是训练过程中 loss 突然出现一个或几个异常高的尖峰然后又恢复正常。很多人第一次遇到时以为是自己学习率调大了或者偶然的数值波动但真正搞过大模型训练的人都知道loss spike 往往是一个更底层故障的信号。我遇到过一次最典型的案例训练跑到 1200 步左右loss 突然涨到原来的三倍然后又在几十步内恢复正常。第一次没在意结果每隔一段时间就涨一次最后整个训练的最终效果比预期差了一大截。后来排查发现问题出在数据 pipeline 的某个 epoch 边界。数据采样器在每个 epoch 结束时重新打乱样本顺序但打乱逻辑里有一个分支会引入重复采样导致某些样本在同一个 batch 里出现多次模型在那一批数据上产生了剧烈的梯度更新。这个问题的隐蔽性在于它不报错不崩溃只表现为周期性 spike如果不是记录了每个 step 的 loss 并且做了回溯很难定位到数据采样器上。我的建议是从一开始就在训练脚本里把每一步的 loss 和 timestamp 落盘不要只打印到控制台。然后写一个简单的“spike 检测器”当 loss 相比滑动平均值偏高 20% 以上时自动打一条告警日志并且把当前 batch 的采样索引一并记录下来。这样下次 spike 出现时你可以直接跳回那个 batch检查里面出现了什么数据、是不是重复样本、是不是特殊 token 过多。没有任何监控手段比“精确回溯到触发样本”更有效。另一个常见的 loss spike 来源是混合精度训练下的溢出。用半精度计算时如果某个中间结果超过了可表示范围loss 会在那一刻变成一个大数或 nan。这种情况通常表现为 loss 突然炸掉之后再也没有恢复和周期性 spike 的表现不一样。处理思路是开启动态损失缩放并对异常梯度做保护性处理同时在下一次 checkpoint 恢复时验证 loss 是否回到正常水平。2.2 梯度爆炸与消失监控 grad norm 是基本功梯度爆炸和梯度消失在大模型训练里比小模型更常见也更致命。小模型炸一次梯度最多训练失败大模型炸一次梯度可能意味着海量算力白费。判断梯度的核心指标是 grad norm也就是所有参数梯度向量化之后的模长。这个指标直接反映了模型在反向传播过程中梯度信号的强弱。我通常会设定两条警戒线。一条是上限grad norm 超过某个值具体要看模型 scale一般可以按正常分布的 3 倍标准差来设就应该触发告警并且自动开启更激进的梯度裁剪。另一条是下限grad norm 长期低于一个很小的值说明某些层已经“死”掉了梯度无法传导到浅层模型深层可能还在变化但浅层参数几乎不动。这种情况最常见的原因是初始化方式不合理或残差连接比例设置不当单纯靠加大学习率往往效果不好更值得做的是检查网络结构里每一层的梯度贡献。关于梯度裁剪我想多说一句。很多人把梯度裁剪当成一个随便设个 max_norm 就完事的参数实际上它的阈值直接影响训练动态。裁剪阈值设得太小会把有效梯度信息也切掉模型收敛变慢设得太大又起不到防爆炸的作用。我的做法是从模型规模的 1% 量级开始试观察 grad norm 的分布让裁剪阈值落在“正常梯度分布的 3 倍标准差”附近这样既能保护异常步又不干扰正常更新。这个数字不需要很精确但它比随手填一个 1.0 要科学得多。另外梯度监控不能只看全局 norm还要分模块看。因为某个模块的梯度爆炸可能被其他模块的正常梯度掩盖全局 norm 依旧在合理范围内但那个模块的参数已经崩了。我习惯在训练脚本里对 embedding、attention、ffn 三个部分分别求梯度 norm并输出到日志。这多花的计算量很小但定位问题时能省下一整天的排查时间。2.3 学习率与 warmup不是玄学是拆弹手册学习率和 warmup 是训练动态里最“玄学”的部分实际却是有规律可循的。大模型初始化时参数处于一个相对不稳定的状态如果一开始就用很高的学习率冲很容易在起步阶段就踩中 loss spike 甚至发散。warmup 的作用就是让优化器在最开始几轮里小步试探等 loss 稳定之后再把学习率提到预定峰值。warmup 的步数没有一个放之四海皆准的值但可以按总步数比例来设。比如总训练步数在几万步以内warmup 设在 500 到 2000 步之间比较稳妥如果是大规模预训练总步数几十万步warmup 可以按总步数的 1% 到 3% 来设。这里有一个经验口诀宁可 warmup 边长一点也不要让它太短。初期多走几百步稳定步代价远小于中期一次 loss spike 导致的回滚。学习率峰值的选择我习惯从两个方向来锚定。一是参考同规模模型的公开经验值。以 7B 级别模型为例常用配置下学习率峰值通常在 2e-4 到 5e-4 的区间具体值要结合 batch size 做微调。二是做一个学习率扫描实验用 200M 左右的小模型在不超过 2000 步内从小学习率线性爬升到大学习率观察 loss 曲线最低点的位置和拐点帮助确定峰值学习率。这个方法成本低但能给出非常有效的先验信息。说到学习率还有一个很容易被忽略的点学习率调度器的终点和最小值。预训练后期如果 min-lr 设得太高loss 容易在底部震荡模型无法收敛到精细的局部最优设得太低则可能浪费训练步数。一般做法是把最小学习率设为峰值的 1/10 到 1/30并在训练结束时用几个下游任务验证模型是否处于稳定状态而不是只看训练集 loss。3. 工程底座显存、通信与 checkpoint 的三重爆点算法问题再复杂至少还能从日志和曲线里找线索工程问题有时候直接让你连日志都来不及看训练进程就没了。这一节聊工程侧最常爆的三个点显存 OOM、分布式通信故障、checkpoint 损坏或无法恢复。这三个问题每一个都足以抹掉数小时的算力而它们往往还会共同出现显存 OOM 导致进程崩溃进程崩溃之后分布式通信超时超时重连之后 checkpoint 没存下来……所以在工程侧我的核心原则是“把每一层都加上保险”。3.1 显存 OOM不是显存不够而是峰值与碎片显存 OOM 是大模型训练里最常见、也最容易被误判的问题。很多人一看到显存不够就想换更大的卡但我遇到过很多次真正的原因不是显存容量不够而是显存使用存在一个很高的峰值或者出现了严重的显存碎片化。要理解这个峰值得先明白大模型训练过程中显存占用并不是一条平线。前向传播阶段要保存每层的激活值用于反向传播时计算梯度直到反向传播结束之前这些激活值都活在显存里。所以显存峰值通常出现在一次完整的前向加反向过程的某个时间点而不是你在显卡监控工具上看到的“此刻数值”。如果你只是随机看一眼显存占用很容易低估训练的实际需求。解决思路通常有三个方向。第一减小 batch size 或者减小序列长度直接压低激活值峰值但要注意梯度累积带来的步数变化。第二开启激活重计算用“重新计算激活值”换显存代价是额外的计算时间适合在显存紧张但算力有余时使用。第三使用显存分片或混合精度等优化手段把模型状态和优化器状态分散到更多设备上。具体选哪种要看你的瓶颈是显存还是算力。我还有一个实用经验在正式大任务之前先用一个小批量把显存峰值测出来。具体做法是写一段脚本从 batch size 1 开始逐步加倍观察每个 batch size 下的显存峰值占用曲线。如果曲线在某个 batch size 之后急剧上翘说明激活值开始成为主导这时优先用激活重计算如果曲线是平缓上升说明权重和优化器状态占了大头这时优先用分片策略。这个“分而治之”的思路能让你在还没开始正式训练之前就知道优化方向而不是等 OOM 之后再去试各种方案。3.2 分布式通信卡死、超时、梯度不同步分布式训练最恼人的问题是通信层故障。尤其在大规模集群上任何一个节点因为故障退出整个集合通信组就可能一直卡在那里等到超时。小规模实验时很少遇到这种问题因为设备少、网络简单但一旦规模上来通信故障几乎必然会出现。我见过一次最典型的例子整个训练突然从每小时 80 个 step 掉到 30 个查了半天发现是某个节点的网络接口降速了数据包重传率暴涨但这个节点没有报错、没有崩溃只是默默拖慢了整个训练。避免这种“慢性死亡”我的做法是给训练脚本加一层通信健康检查。第一在启动训练前先用小规模数据跑一个通信自检统计每个节点之间的梯度同步耗时找出异动节点。第二训练过程中把每个 step 的通信耗时记录到日志里如果某个 step 的通信耗时比滑动平均值高出 50%触发告警。第三给集合通信操作设置合理的超时时间避免某个节点失联时整个训练无限制等待。这里还要提醒一个细节超时时间不是越大越好。有些团队为了“防止误报”把超时设成几十分钟结果节点真的故障时训练就傻等几十分钟。我一般把集合通信超时设在 5 到 10 分钟配合自动重启和断点续训机制。这样即使超时发生也能快速把训练拉回来而不是让整个集群空转等一个不可能回来的节点。另外网络拓扑对通信性能的影响经常被忽略。在大规模集群里如果某个节点的位置离主交换节点太远或者跨机柜通信不走最优路径这个节点就会成为慢节点拖慢所有与之通信的节点。与其在故障发生后花大量时间排查拓扑不如在集群交付阶段就做好性能基线测试记录每对节点之间的实际带宽和延迟后续训练中一旦出现通信耗时异常可以对照基线快速判断是网络问题还是负载问题。3.3 checkpoint 救不回来比训练失败更绝望训练中断不可怕可怕的是中断之后你发现最近的 checkpoint 是坏的。我吃过一次大亏一次预训练任务在运行到 60% 时因为机房断电中断恢复时加载 checkpoint发现优化器状态文件和模型权重文件在写入时没有对齐模型能加载但优化器状态错位整个训练从理论上来说已经“悄悄地”不健康了。那次事故之后我给自己定了一条铁律checkpoint 不仅要存还要“能验”能证明它真的可以恢复。具体做法有四点。第一至少保留最近 3 个连续版本的 checkpoint不要只存一个“最新的”防止最新版本写入到一半时进程崩溃。第二每次保存时除了写权重和优化器状态还要附带一个配置信息文件里面记录模型结构参数、词表版本、数据配比版本等关键信息恢复时先比对当前配置和 checkpoint 里的配置是否一致不一致就报错而不是静默加载。第三对 checkpoint 文件计算校验值保存时记录恢复时校验防止文件在存储或传输过程中损坏。第四尽量使用“先写临时文件、再原子替换”的保存方式避免直接覆盖正在使用的文件。保存方式上很多人只关注“保存频率”但异步保存和同步保存的差异也很大。同步保存可能会阻塞训练几十秒甚至几分钟对长周期训练来说是一个不小的性能损失异步保存则可以在后台写盘不阻塞前向和后向计算但要注意在进程退出前把异步任务 flush 完。如果训练脚本因为崩溃退出异步保存任务可能还没落盘最近的 checkpoint 其实是不完整的。我通常的做法是每 N 步做一个同步保存作为“稳定锚点”每 N 步之间用异步保存作为“高频快照”。这样既保证了恢复点可靠又尽量少地牺牲训练吞吐。关于 checkpoint 的位置我不建议只存在训练节点本地。节点本地磁盘容易随节点故障丢失正确做法是定期把 checkpoint 镜像到独立存储或另一个节点上。这个“异地备份”操作不需要太频繁但至少保证每几个稳定锚点之后有一次备份防止整个节点宕机导致 checkpoint 全灭。分布式训练本来就充满不确定性checkpoint 是你最后的救命稻草这根稻草不能太细。4. 动手前的“防炸”检查清单工地先验收再施工讲完三个主要爆点最后把这些经验浓缩成一份可以在开工前直接照着做的检查清单。这份清单我在每个新训练任务启动前都会跑一遍整个过程多花半天时间但能避免后面几天甚至几周的手忙脚乱。4.1 小规模烟测1% 数据跑 100 步正式预训练开始前先做一个小规模烟测。具体做法是从完整训练数据里抽 1% 的样本保持各领域配比不变在最小可行的设备规模上跑 100 步左右。烟测不是看模型最终效果而是验证整条链路是否通畅。重点检查四件事第一loss 是否能在前几十步内平稳下降如果前 20 步就出现异常波动说明数据或学习率配置有问题第二grad norm 是否在合理范围内既没有爆炸也没有归零第三日志和监控系统是否正常工作指标是否都能正确输出第四在烟测中途保存一次 checkpoint 并尝试恢复确认断点续训链路是通的。烟测还有一个容易被忽略的作用确定每一步的实际耗时。从这个耗时可以估算出正式训练的总时长并据此倒推 checkpoint 保存频率、告警阈值、超时时间等训练参数。比如烟测时发现每一步稳定耗时 2 秒那正式任务一小时大概能跑 1800 步你可以把 checkpoint 保存间隔设为 2000 步左右把告警滑动窗口设为 100 步。这些参数不经过烟测就直接拍脑袋定往往会在正式训练中暴露出各种不适配。烟测数据量少不代表可以放松要求。我见过有人在烟测时把数据配比改了、把随机种子换了结果烟测一切正常正式训练却爆雷。烟测必须使用正式训练完全一致的代码路径、数据路径和配置只是数据量缩小、设备规模缩小。否则烟测的意义就大打折扣相当于拿一套逻辑去验证另一套逻辑。4.2 日志与监控指标速查表很多训练事故之所以排查慢是因为日志里没有关键指标或者有指标但没人在意。我把自己常用的监控指标整理成一张速查表开工前按这张表把日志输出和告警配置都配好。监控指标正常范围参考异常表现触发动作整体 loss平滑下降无异常尖峰单步或连续几步跳升 20% 以上告警并回溯到触发 batch分领域 loss各领域均平稳下降某个领域长期平台或反升检查数据配比与采样顺序grad norm在固定区间平稳波动超过正常 3 倍或长期接近 0调整梯度裁剪或检查初始化学习率按调度曲线变化与实际配置不符校验调度器代码与恢复逻辑显存峰值低于设备容量 85%逼近 100% 或频繁 OOM降低 batch size 或开启重计算通信耗时各 step 差异不大出现明显长尾或持续上升检查网络与慢节点吞吐步/秒稳定或缓慢变化突然腰斩检查 I/O、CPU 与通信瓶颈这张表里的正常范围只是一个参考基线不同模型、不同规模差异很大但监控项本身是通用的。另一个关键点是日志里必须带上 step 序号和 timestamp。没有 step 序号的数值是没法回溯的没有 timestamp 的数值是没法对齐其他系统事件的。这两列信息看似简单却是排查事故时的第一线索。告警不是越多越好太多告警会让人麻木。我通常只给四种情况配置自动告警loss 异常跳升、grad norm 超限、通信超时、显存不足。其他指标进入日志留存供事后分析。这样既能保证关键故障第一时间被发现又不至于让值班的人被噪声淹没。告警通知渠道也要挑一个大家真的会看的否则告警发了没人理等于没发。4.3 可恢复训练流程设计任何一步都能“重新来过”最后在训练任务设计层面我的建议是把它设计成“任何一步都能重新来过”的形态。这句话的意思是无论什么时候断掉都能在尽可能少损失算力的情况下继续推进。要做到这一点光有 checkpoint 还不够训练任务本身要具备可重入性。可重入的第一个要求是数据加载是幂等的。也就是说从某个 step 恢复时模型拿到的下一批数据必须和中间没断过时完全一致。这要求在数据采样器里显式记录 step 和 epoch 的对应关系恢复时从 checkpoint 里读取当前 step而不是从头重建数据顺序。如果采样器是随机打乱的必须把随机种子和打乱状态一起存入 checkpoint否则恢复后数据顺序会错位导致模型在部分数据上重复训练、在部分数据上缺失训练。可重入的第二个要求是恢复逻辑必须完整恢复“模型 优化器 调度器 数据采样器”四个组件的状态。很多人只恢复模型权重优化器和调度器从头开始结果学习率调度错乱、优化器状态和模型状态不匹配训练质量悄悄劣化。恢复后应该先跑 50 步校验损失值是否和中断前一致如果相差太大说明恢复链路有问题要立即排查。第三个要求是进程崩溃后能自动拉起。人工介入的延迟在大规模训练中是非常致命的尤其夜间故障如果没人发现可能白白损失一晚上算力。我习惯用统一进程管理器来维护训练进程设定重启策略并在重启后自动校验 checkpoint 是否有效。这套机制配合前面说的监控告警可以做到“故障自动发现、自动重启、自动校验”的闭环。当然自动重启不意味着可以放松告警因为如果反复崩溃说明不是暂时性故障而是需要人工介入的硬件或代码问题。这份清单我在每个训练任务前都会走一遍说起来麻烦其实半天到一天就能跑完。但正因为这半天跑了后面几周才不用天天提心吊胆。搞大模型训练这两年我最大的感受是模型参数越多我们对“确定性”的依赖就越强。小模型可以靠随机性碰运气大模型必须把每一步都钉死在可预期、可恢复、可排查的轨道上。如果让我只给一条最终建议那就是不要等到训练炸了才回头找原因把“防炸”动作前置到开工前的那张清单里你会发现它比任何应急方案都便宜得多。