搞大模型训练这几年,我最深的体会就是:训练任务挂掉从来不是一声炸雷,而是层层埋雷。你以为万事俱备,结果数据 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 是否有效。这套机制配合前面说的监控告警,可以做到“故障自动发现、自动重启、自动校验”的闭环。当然,自动重启不意味着可以放松告警,因为如果反复崩溃,说明不是暂时性故障,而是需要人工介入的硬件或代码问题。
这份清单我在每个训练任务前都会走一遍,说起来麻烦,其实半天到一天就能跑完。但正因为这半天跑了,后面几周才不用天天提心吊胆。搞大模型训练这两年,我最大的感受是:模型参数越多,我们对“确定性”的依赖就越强。小模型可以靠随机性碰运气,大模型必须把每一步都钉死在可预期、可恢复、可排查的轨道上。如果让我只给一条最终建议,那就是:不要等到训练炸了才回头找原因,把“防炸”动作前置到开工前的那张清单里,你会发现它比任何应急方案都便宜得多。