六:训练稳定性与调试
训练大模型时,Loss 突然变成 NaN,可能有哪些原因?¶
训练中损失突然变为NaN是模型崩溃的标志,意味着模型参数或梯度中出现了非法数值,后续所有计算都会连锁污染。原因通常来自数值不稳定、数据问题或超参数配置错误。
数值溢出是最常见的原因。FP16混合精度训练中,FP16能表示的最大值约65504,最小值约6×10⁻⁸。当激活值或梯度超过这个范围,就会变成Inf,Inf参与运算产生NaN。注意力机制中Q和K的点积在序列长度大时容易产生极大值,如果缩放因子√d_k不够或未正确应用,softmax前的值可能溢出。此外,损失函数的log操作若输入为0,也会产生NaN。
学习率过大导致参数更新步幅过猛,权重值急剧膨胀。前向传播时激活值指数级放大,经过几层后超出浮点数范围,反向传播时梯度爆炸。这是新手最常犯的错误。
数据问题也不容忽视。训练数据中可能存在缺失值、异常值、未正确归一化的特征,或者文本中包含模型词表无法处理的特殊字符。某条数据可能触发极端计算,导致该batch的梯度异常。在多模态任务中,图像损坏或文本乱码也会引起NaN。
优化器状态损坏。Adam等自适应优化器内部维护动量m和方差v。如果训练中断后从checkpoint恢复,但优化器状态加载不正确,或者混合精度下优化器状态精度损失,可能导致学习率异常缩放,产生NaN。
模型设计缺陷。某些自定义层缺少归一化,或者残差连接配置错误,导致深层网络信号逐层放大。在Post-Norm Transformer中,如果残差累积未得到控制,深层激活值可能膨胀。此外,初始权重设置不当也会在训练初期就引发NaN。
排查时,建议按顺序检查:首先确认数据是否正常;然后观察梯度范数是否在NaN出现前骤升;降低学习率测试;切换FP16为BF16(动态范围大)或FP32全精度测试;逐步增加模型复杂度定位问题层。
梯度爆炸的直观表现是什么?如何通过监控梯度范数发现?¶
梯度爆炸是指反向传播过程中,某些层的梯度值异常巨大,导致参数更新量远超正常范围。其直观表现包括:
-
Loss曲线突然飙升:在某个训练步,损失值突然跳涨到正常值的数倍甚至数十倍,随后可能恢复或继续恶化。
-
Loss变为NaN或Inf:梯度爆炸严重时直接产生非法数值,训练崩溃。
-
参数L2范数急剧增大:模型权重的范数突然增加,说明更新步幅过大。
-
训练速度突然变慢:梯度爆炸引发数值异常后,混合精度训练的Loss Scaling会自动降低缩放因子以尝试恢复,但可能导致训练停滞。
监控梯度范数是预防和诊断梯度爆炸的核心手段。具体做法是:
-
在每次优化器更新前,计算所有参数梯度的全局L2范数(
total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)的返回值)。 -
记录梯度范数随时间步的变化曲线。正常训练中,梯度范数通常在0.1到10之间波动,有逐步下降的趋势。
-
如果梯度范数突然从正常值(如2.0)跳变到几百甚至几千,说明发生了梯度爆炸。
-
设置预警阈值:当梯度范数超过历史均值的5~10倍时,自动触发告警或跳过该批次更新。
-
还可以分层监控,定位爆炸发生在哪一层。通常深层网络的浅层(靠近输入)更容易出现梯度消失,而深层(靠近输出)或RNN类型网络容易出现爆炸。
通过持续监控梯度范数,结合自动化工具(如WandB、TensorBoard),可以在训练崩溃前及时调整超参数或回滚检查点。
梯度裁剪的阈值如何设置?按值裁剪和按范数裁剪的选择。¶
梯度裁剪是防止梯度爆炸的直接手段。核心是设定一个阈值,如果梯度超过阈值,就将其缩回到合理范围。
阈值设置方法:
-
经验值:对于Transformer大模型,常见的梯度裁剪阈值设为1.0。这是一个经过大量实验验证的安全值,能有效防止大多数梯度爆炸。
-
基于历史统计:记录前N步的梯度范数,取第90或95百分位数作为阈值。这样能自适应数据分布,避免固定阈值过松或过紧。
-
逐步调优:在训练初期,用较小阈值(如0.5),观察梯度范数的分布。如果经常触发裁剪,可适当放宽;如果偶尔出现极大值(尖峰),阈值就设在这个尖峰之下。
-
模型规模调整:大模型由于参数量大,整体梯度范数可能偏高,阈值通常设为1.0~5.0。小模型可设0.5~1.0。
按值裁剪 vs 按范数裁剪:
-
按值裁剪:对每个梯度元素独立截断,将范围限制在[-v, v]内。优点是简单,能精确控制每个元素的上下限。缺点是可能破坏梯度的整体方向,因为不同参数的梯度可能被不同程度地截断,导致更新方向偏离真实梯度方向。适用于需要精细控制单个参数更新量的场景,如某些特定的正则化或防御对抗攻击,但在大模型训练中较少使用。
-
按范数裁剪:计算全局梯度的L2范数,如果超过阈值,则对整个梯度向量等比例缩放,使得缩放后的范数等于阈值。这保留了梯度的方向,只限制了“步长”。这是目前训练Transformer和大模型的标准方法,因为它不会扭曲梯度的方向信息,仅防止单步更新过大。
总结:大模型训练中几乎总是使用按范数裁剪,阈值设为1.0作为起点。按值裁剪只在特殊需要时使用。
什么情况下会出现 Loss Spike(损失尖峰)?如何处理?¶
Loss Spike指损失值在某个训练步瞬间剧烈升高,形成一个尖锐的波峰,随后可能回落。它与梯度爆炸密切相关,但梯度爆炸不一定每次都导致Loss Spike(可能直接NaN)。
出现原因:
-
异常数据样本:某条数据包含极端噪声、错误标注或过长文本,导致该batch的损失远高于正常值,梯度被异常放大,引发连锁反应。这是预训练中Loss Spike最常见的原因。
-
优化器状态突变:Adam等自适应优化器维护的动量m和方差v可能因某个异常梯度而剧烈变化,导致有效学习率瞬间放大,产生巨大的参数更新。
-
混合精度训练的Loss Scaling动态调整滞后:当遇到大梯度时,Loss Scaling可能未及时降低,导致梯度上溢,然后又突然降低缩放因子,造成损失值剧烈波动。
-
学习率与模型当前状态不匹配:训练后期,模型接近收敛,此时若学习率衰减不当或突然增大(如从预训练切换到微调时的错误配置),可能产生尖峰。
-
硬件故障:GPU内存错误(ECC错误)或网络传输丢包,导致计算的梯度在传输中损坏,产生异常值。
处理措施:
-
立即反应:如果在训练循环中检测到Loss Spike,最安全的做法是跳过本次参数更新,不将该batch的梯度应用到模型上。同时可丢弃该batch,重新采样下一个batch。
-
回滚检查点:如果Loss Spike后损失持续升高或不稳定,应从最近的健康检查点恢复训练,并跳过引发尖峰的数据(如果可定位)。
-
降低学习率:尖峰频繁出现时,将当前学习率减半,并适当延长Warmup。
-
增强梯度裁剪:适当降低裁剪阈值,如从1.0降至0.5,确保异常梯度被及时抑制。
-
检查数据质量:分析引发尖峰的batch,排查是否存在异常样本,必要时加强数据清洗。
-
调整优化器:适当增大Adam的β₂(如从0.999到0.9999),使二阶矩估计更平滑,避免有效学习率骤然放大。
-
切换精度:如果使用FP16,可切换到BF16(动态范围大,不易溢出)或采用更高精度的梯度累积。
如果遇到 Loss Spike,你首先会做什么?(跳过更新、回滚检查点、调参)¶
遇到Loss Spike,我的第一反应是保护模型状态不被污染。具体动作优先级如下:
-
立即跳过当前更新:在训练循环中增加判断逻辑,如果当前batch的损失超过前N步移动平均的若干倍(如5倍),则
continue不执行optimizer.step(),并丢弃该batch。这是最快速、代价最小的止损措施,可以避免一次异常数据毁掉数小时的训练成果。 -
检查梯度范数:在跳过更新后,打印当前梯度范数,与历史值对比。如果梯度范数异常高,确认是梯度爆炸;如果梯度正常但损失尖峰,可能是数据问题。
-
评估损失恢复情况:继续训练几个batch,观察损失是否回到正常范围。如果迅速恢复,则可能只是偶发异常数据,无需进一步干预。
-
若损失持续异常:如果损失在尖峰后无法回到正常水平,或者发生多次尖峰,则需要回滚到最近的健康checkpoint。从断点恢复后,尝试用较小的学习率(如当前学习率的0.5倍)继续训练。回滚是防止模型退化到不可恢复状态的关键保险。
-
数据排查:记录引发尖峰的batch的索引或特征,分析数据分布。若发现规律(如特定来源、特定长度的文本),则针对性清洗或剔除。
-
调整超参数:如果尖峰频繁发生,考虑降低学习率、增大Warmup步数、加强梯度裁剪或切换到更稳定的优化器配置。
-
监控与自动化:建立自动化监控系统,当损失尖峰或梯度范数超限时自动跳过更新、发送告警,并可配置自动回滚策略。
解释“学习率过大”导致训练崩溃的机制。¶
学习率控制着参数更新的步幅。过大的学习率会导致训练崩溃,其机制可以从多个层面理解:
参数更新层面:设梯度为g,学习率为η,参数更新为Δθ = -ηg。如果η过大,一次更新就可能将参数推向远离当前最优点的位置。在非凸的损失曲面上,这意味着参数可能从一个相对稳定的区域“跳”到一个高损失区域,甚至跳出合理的数值范围。
激活值爆炸:参数骤变后,下一轮前向传播时,线性层的输出会急剧放大。经过多层堆叠,激活值呈指数级增长(尤其在无归一化的深层网络中)。当激活值超出浮点数表示范围(FP16最大65504),就变成Inf。Inf经过后续激活函数(如softmax、log)计算,产生NaN,整个网络输出崩溃。
梯度爆炸:爆炸的激活值导致损失函数值极大,反向传播计算的梯度相应极大(因为梯度与激活值成正比)。这些巨大梯度在深层网络中连乘,导致浅层参数的梯度也大到不可思议。即使使用梯度裁剪,如果学习率本身过大,裁剪后的更新量仍然可能太大,无法完全阻止参数被推出合理区域。
优化器状态失衡:对于Adam等自适应优化器,过大的梯度会迅速改变动量m和二阶矩v的估计。由于v的更新依赖梯度平方,大梯度会让v变得极大,导致后续步骤的有效学习率(η / (√v + ε))变得极小,模型可能陷入停滞;或者因为初始v很小,第一次大梯度使得更新量超乎想象,直接破坏模型。
损失曲面“悬崖”:大模型的损失曲面存在大量陡峭区域(“悬崖”)。在这些区域,梯度值巨大。如果学习率过大,参数可能被推下悬崖,损失瞬间飙升,再也无法通过常规梯度下降找回。Warmup的初衷就是避免训练初期参数在随机初始状态下直接遭遇悬崖。
因此,学习率过大本质上是破坏了梯度下降的稳定性假设,使得局部线性近似失效,参数更新失去控制。合适的初始学习率和Warmup是确保训练稳定的基石。
如何选择合适的初始学习率?LR Finder 是如何工作的?¶
选择初始学习率是深度学习中最关键的调参之一。传统方法是基于经验或网格搜索,而LR Finder提供了一种更高效、更科学的方法。
经验规则:
-
对于Transformer和大模型,常用的初始学习率在1e-5到1e-3之间,通常取3e-4(如GPT-3)或1e-4。
-
使用AdamW优化器时,学习率通常比SGD小一个数量级。
-
批量越大,学习率可适当增大(线性缩放法则)。
LR Finder原理与步骤:
LR Finder由Leslie Smith提出,核心思想是在一个小批量数据上,让学习率从极小值指数增长到极大值,同时记录每个学习率对应的loss,然后绘制loss-学习率曲线,从中选择最优点。
工作流程:
-
准备一个小的代表性数据集(如几百个batch)。
-
将模型初始化为干净状态(或从训练中某点开始)。
-
设置一个极低的起始学习率(如1e-8)。
-
在每个batch训练后,按指数增长学习率(例如乘以1.01),直到学习率变得很大(如10)。
-
记录每个batch的loss值和学习率,绘制平滑后的曲线。
-
分析曲线:loss会先下降(因为学习率增大加速收敛),然后趋于平缓,最后剧烈上升(学习率过大导致发散)。
-
选择初始学习率:不是选loss最低点,因为那是学习率已经过大、开始发散的临界点。通常选择loss下降最快、且还在稳定下降的区域对应的学习率。一个经验规则是:选择loss最低点对应学习率的1/10,或者曲线从快速下降转为平稳的那个拐点之前的位置。
LR Finder的变体:对于大模型,可以只在一个子集上运行,或使用动量平滑loss曲线。它在实践中非常有效,能快速给出一个合理的学习率范围,避免盲目试错。
为什么 Warmup 对 Transformer 训练至关重要?多少步合适?¶
Warmup(学习率预热)是指在训练开始阶段,将学习率从零或极小值线性增加到预设的初始学习率。它对Transformer训练至关重要,原因有三:
-
防止初期梯度爆炸/消失:Transformer在随机初始化状态下,各层的输出方差可能不均衡,注意力矩阵可能极端稀疏或集中。如果一开始就用大学习率,参数剧烈更新,容易导致梯度爆炸。小学习率在初期提供“软启动”,让模型先调整到比较稳定的状态。
-
建立稳定的优化器状态:Adam等自适应优化器内部的状态(m和v)在初期是零初始化的,对梯度的估计极不准确。如果立刻用大学习率,优化器会基于不准确的统计量做出过大的更新。Warmup给优化器一段时间来累积准确的梯度统计,使后续的自适应学习率更可靠。
-
降低对初始化的敏感度:深层Transformer的训练对权重初始化非常敏感。Warmup通过逐步增加学习率,让模型在训练初期对初始化不够好的层进行缓慢修正,而不是直接面临大学习率的冲击,从而降低对初始化的依赖。
步数选择:
-
对于大模型预训练(如7B-175B),常用Warmup步数在1000到4000步之间,或占总训练步数的1%~5%。例如,训练100k步,Warmup 1k-4k步。
-
对于微调任务,通常在100-500步左右。
-
步数过短:预热不充分,早期大学习率仍可能导致训练不稳定。
-
步数过长:前期训练过慢,浪费计算资源,且学习率长时间偏低可能使模型陷入次优局部解。
-
实践建议:可以使用动态Warmup,监控初始几个batch的梯度范数,如果梯度范数显著大于后续稳定阶段,则适当延长Warmup。此外,对于超大模型(如175B),Warmup步数需要更保守(如4000步),甚至可以采用多阶段预热。
总之,Warmup是保障Transformer训练稳定性的关键技巧,合理的步数选择能有效避免训练初期的崩溃。
如何通过观察训练日志判断是否需要调整超参数?¶
训练日志是模型训练的体检报告,其中包含的每一个指标变化都可能暗示着超参数配置的优劣。学会解读这些信号,是训练工程师的核心能力。
Loss曲线的形态是最直观的诊断窗口:
-
Loss持续不下降或下降极其缓慢:往往意味着学习率过低。模型被困在一个平坦区域,每次更新的步幅太小,无法有效逃离。此时应将学习率放大2-3倍尝试。
-
Loss出现剧烈震荡或尖峰:这是学习率过大的典型表现。模型参数在损失曲面的“峡谷”两侧来回跳跃,甚至被抛出合理区域。需要降低学习率或启用更长Warmup。
-
Loss一开始就NaN:极可能是学习率过大或权重初始化不当,导致前向传播数值溢出。应优先检查初始化和数据范围,然后大幅降低学习率。
-
Loss收敛到一个较高平台后不再下降:可能是模型容量不足(欠拟合),或者数据中存在大量噪声。可以尝试增加模型深度/宽度,或进行更严格的数据清洗。
-
训练损失继续下降,但验证损失开始上升:这是过拟合的经典信号,但并不绝对(详见下题)。如果确定是过拟合,应增大Weight Decay、增加Dropout率、使用标签平滑,或减少模型容量。
梯度范数是判断训练稳定性的关键指标。每一步的梯度L2范数应被记录。正常训练中,梯度范数通常在0.1到10之间,且呈缓慢下降趋势。如果梯度范数突然跳变至几百甚至上千,说明发生了梯度爆炸,应立即检查数据样本、降低学习率或加强梯度裁剪。如果梯度范数持续极小(如<1e-4),则是梯度消失,可能是激活函数选择不当或初始化问题。
参数更新量与参数范数的比值也值得关注。比值过大(如>0.1),说明单步更新可能过于激进;比值过小(<1e-5),则可能学习率太低。这个比值应在训练过程中保持相对稳定。
学习率的变化曲线需要与实际训练策略一致。Warmup是否按预期结束?衰减是否在正确的时间点执行?有时因为配置错误,学习率可能没有衰减,导致后期训练震荡。
激活值的统计量(均值和方差)是深层网络健康的晴雨表。如果某一层的激活值均值严重偏离0,或方差逐层持续放大,说明初始化或归一化设置有问题。使用TensorBoard等工具可视化各层激活值的直方图,可以快速定位问题层。
数据加载和GPU利用率同样不容忽视。如果GPU利用率长期低于80%,可能数据加载成为瓶颈,需增加num_workers或优化预处理。如果多机训练中某些节点的利用率明显低于其他节点,可能是网络通信瓶颈或负载不均。
综合诊断流程:每次启动训练前,先在小规模模型和数据上做快速实验,观察上述指标是否健康。若发现问题,按“学习率 → 初始化 → 模型结构 → 数据”的顺序排查,通常能快速定位根因。
训练过程中,验证损失不降反升,一定是过拟合吗?可能还有哪些原因?¶
验证损失上升是训练中常见的“警报”,但它不总是过拟合。许多其他因素可以导致同样的现象,盲目采用抗过拟合措施可能适得其反。
确实是过拟合:训练损失继续下降,验证损失开始上升,且两者差距不断扩大。这是因为模型开始记忆训练数据中的噪声和细节,丧失泛化能力。此时应增大Weight Decay、增加Dropout、使用标签平滑或减小模型容量。
训练集和验证集分布不一致(数据漂移):如果验证集来自不同的领域、时间段或数据源,其分布与训练集存在系统性差异。即使模型泛化良好,验证损失也可能上升。解决办法是确保数据划分随机且同分布,或使用领域适应技术。
验证集太小或抽样偏差:如果验证集只有几百条样本,其损失波动可能纯粹由抽样噪声引起,并不反映真实泛化能力。应适当增大验证集规模,并多次随机划分观察波动幅度。
学习率过大导致震荡:学习率过高时,模型参数在最优解附近大幅摆动,无法精细收敛,验证损失因此居高不下甚至上升。此时训练损失也可能不稳定。应降低学习率或采用更平滑的衰减策略。
数据预处理不一致:训练和验证的预处理逻辑如果存在细微差异(如归一化参数不一致、分词器版本不同),会导致模型在验证集上看到“陌生”的输入分布。这需要仔细检查数据处理管道的一致性。
标签错误或泄漏:验证集中如果存在大量错误标签,模型当然无法正确预测。另外,如果训练集中的某些信息意外“泄漏”到验证集(如随机划分时未按样本去重),会导致验证损失虚假下降后又上升,因为模型过拟合到了泄漏的模式。
模型容量不足(欠拟合):当模型容量不足以捕捉数据中的复杂模式时,训练损失和验证损失都较高,且差距不大。这种情况下继续训练损失也不会下降,增大模型容量是正确方向。
诊断方法:首先,比较训练损失和验证损失的绝对数值和变化趋势。如果两者都在下降但验证损失更低,可能是验证集太小。如果训练损失仍在快速下降但验证损失停止下降,观察是否接近收敛或学习率是否需要衰减。还可以计算训练集和验证集上的准确率或其他指标,交叉验证判断。
解释“Double Descent”现象,它对超参数调优有何启示?¶
“Double Descent”(双下降)是现代深度学习对经典统计学习理论的一大修正。经典理论认为,模型复杂度(参数数量)与测试误差呈U型关系:复杂度太低欠拟合,太高过拟合。但实际中,当模型参数远超训练样本数,进入“过度参数化”区域后,测试误差竟会再次下降,形成“先降-后升-再降”的双谷曲线。
现象解释:在参数数量增加到能够完全拟合训练数据(插值点)的临界处,模型只有一个解可以精确拟合所有数据,这个解可能振荡剧烈、泛化极差(即第一次上升的峰值)。但当模型参数继续增加,存在无穷多个解都能完美拟合训练数据,优化算法(如SGD)会隐式地偏向于寻找范数较小、“更简单”的解,这些解的泛化能力反而更好,所以测试误差再次下降。
这一现象不仅与模型大小有关,也与训练时长有关。训练初期模型欠拟合,测试误差下降;随着训练进行,模型过拟合训练数据,测试误差上升;但如果继续训练更久(“过训练”),测试误差可能再次下降。这就是所谓的“Epoch-wise Double Descent”。
对超参数调优的启示:
-
不要害怕大模型:经典U型曲线给出的“最佳模型大小”可能远非最佳,更大的模型可能获得更好泛化。调参时应将模型容量作为可以持续增大的维度。
-
训练时间的影响需重新评估:早停(Early Stopping)虽然能防止第一阶段过拟合,但可能错过“过训练”后泛化再次提升的机会。这需要更细致的验证监控。
-
正则化强度需动态调整:在插值临界点附近,正则化可能尤为重要;但在过度参数化区域,模型自带隐式正则化,显式正则化(如Weight Decay)的强度可能需要降低。
-
学习率策略也有影响:大学习率在初期加速收敛,但在插值点附近可能加剧振荡。Warmup和衰减策略需要配合模型规模设置。
-
验证集设计要合理:由于测试误差变化是非单调的,验证集必须足够大且分布一致,才能准确判断模型真实泛化能力。
因此,Double Descent告诉我们,传统的“偏差-方差权衡”在大模型时代需要重新审视。调参时不应过早限制模型大小或训练时间,而应利用这一特性,通过扩大模型和延长训练来探索更优解。
什么是“权重衰减” (Weight Decay)?如何与学习率配合?¶
权重衰减是一种正则化技术,通过对模型参数施加“收缩”惩罚,防止权重值过大,从而控制模型复杂度,缓解过拟合。
在标准SGD中,权重衰减等价于在损失函数中添加L2正则项:Loss_total = Loss_original + (λ/2) * ||θ||²。对参数θ求导后,梯度增加了λθ项,参数更新变为:θ = θ - η*(g + λθ) = (1 - ηλ)θ - ηg。可见,每个参数在每次更新后都会被乘上一个小于1的因子,即“衰减”。
权重衰减与学习率的配合:从更新公式可以看出,实际的衰减强度由乘积ηλ决定。因此,学习率η和衰减系数λ需要协同调整。
-
如果增大学习率,而保持λ不变,参数收缩效应会增强(因为衰减因子1-ηλ变小),可能导致模型欠拟合。
-
如果减小学习率,而λ不变,衰减效应减弱,模型可能过拟合。
-
实践中,通常先确定一个合适的学习率,然后独立调优λ。常用λ值在1e-4到1e-2之间,大模型(如GPT-3)甚至使用0.1的大权重衰减。
关键区别:在Adam等自适应优化器中,如果直接在损失函数中添加L2正则,梯度中的λθ项会被自适应学习率缩放(除以二阶矩的平方根),导致实际衰减量不均匀,效果不佳。因此,AdamW将权重衰减从自适应机制中解耦,直接作用于参数更新之后:θ = θ - η*g - ηλθ。这使得衰减强度独立于梯度大小,更易控制和调优。这也是现代大模型训练普遍使用AdamW的原因。
在 AdamW 中,如何调整 weight decay?过大或过小有什么影响?¶
AdamW的权重衰减独立于自适应学习率,使得调整更加直接。参数更新分为两步:先进行自适应梯度更新 θ = θ - η * m_hat / (sqrt(v_hat) + ε),然后再进行权重衰减 θ = (1 - ηλ) θ。
调整方法:
-
初始值:对于Transformer模型,常用的λ范围是0.01到0.1。LLaMA系列使用0.1,GPT-3使用0.1。对于较小模型或微调,通常用1e-4到1e-2。
-
与学习率配合:虽然AdamW解耦了衰减,但乘积ηλ仍决定总衰减量。实践中,通常固定λ,然后调优η。λ设置后,尝试不同η,观察训练和验证损失。
-
根据模型大小调整:大模型参数多,冗余度高,可以承受更强的权重衰减(如0.1),有助于控制过拟合。小模型则需要更谨慎,过大的λ会导致欠拟合。
-
根据数据量调整:数据量少时,模型容易过拟合,需要较大的λ;数据量充足时,λ可适当减小。
λ过大的影响:
-
参数被过度压缩趋向于零,模型容量受限,训练损失和验证损失都较高(欠拟合)。
-
模型无法学习到数据中的复杂模式,最终性能低下。
-
某些层的输出可能变得极小,深层网络信号衰减,训练困难。
λ过小的影响:
-
正则化不足,模型容易过拟合训练数据中的噪声。
-
训练损失可以降得很低,但验证损失上升,泛化性能差。
-
模型参数可能变得很大,对输入扰动敏感,鲁棒性下降。
监控与调优:训练时同时观察训练损失和验证损失。如果两者差距很大(过拟合),增大λ;如果两者都很高且差距小(欠拟合),减小λ。还可以观察参数L2范数的变化,λ应使参数范数稳定在一个合理水平。
训练中 Batch Size 突然改变,学习率应该如何调整?(线性缩放原则)¶
当改变训练的全局批量大小(Global Batch Size, GBS)时,学习率需要相应调整,以保持训练动态的一致性。最经典的指导原则是线性缩放法则:当批量大小乘以k倍时,学习率也应乘以k倍。
原理:在SGD中,参数更新为 Δθ = -η * (1/B) Σᵢ gᵢ,其中B是批量大小。梯度的方差与1/B成正比。当B增大时,梯度估计更准确,噪声更小,因此可以采用更大的学习率,而不会导致更新方向过于随机。线性缩放保持了Δθ的期望值不变,从而保持了训练稳定性。
应用:如果原配置GBS=256, η=1e-4,现改为GBS=1024(扩大4倍),则学习率应调整为η=4e-4。
注意事项:
-
上限约束:学习率不能无限增大。过大的学习率会导致单步更新量过大,即使梯度方向准确,也可能因为步幅太大而错过最优点或陷入不稳定。通常需要配合Warmup来平滑过渡到大的学习率。
-
大批量退化:当批量非常大(如几万)时,线性缩放可能不再适用,因为梯度噪声已经极小,继续增大学习率会导致收敛到尖锐极小值,泛化性能下降。实践中,超大批量训练时,学习率缩放因子通常小于线性比例,需要实验调优。
-
优化器差异:对于Adam等自适应优化器,其更新量已经通过二阶矩进行了归一化,对学习率的敏感度低于SGD。但线性缩放原则仍然适用,只是可能需要微调。
-
Warmup配合:如果突然将批量增大数倍并同步提高学习率,模型可能不适应,应在增大批量后先进行短暂的Warmup(例如几千步),让模型适应新的更新动态。
因此,批量大小改变时,优先按线性缩放调整学习率,同时密切监控训练初期的损失和梯度范数,必要时进行微调。
为什么推荐使用 Cosine Decay 而不是 Step Decay?¶
学习率衰减策略影响模型在训练后期的精细收敛。Cosine Decay(余弦退火)相比传统的Step Decay(阶梯衰减)具有显著优势。
Step Decay:在预设的训练步数(如每30%的总步数)时,将学习率乘以一个衰减因子(如0.1)。这种策略存在突变,每次衰减后模型需要重新适应新的学习率,可能导致损失震荡。而且预设衰减节点需要经验,不同任务最优节点不同。
Cosine Decay:学习率按照余弦曲线从初始值平滑降低到接近零。公式为:η_t = η_min + 0.5(η_max - η_min)(1 + cos(π * t / T))。曲线初期变化平缓,让模型在较高学习率下充分探索;后期快速衰减,使模型在收敛阶段以极小步长精调。
推荐理由:
-
平滑无突变:学习率连续变化,避免了因学习率突然下降导致的损失尖峰或停滞。
-
更好收敛性:在接近训练结束时,学习率变得极低,模型可以收敛到更深的极小值,通常获得更好的最终性能。
-
鲁棒性强:不需要手动设置衰减步长和衰减因子,只需设置初始学习率和训练总步数,大大减少了调参负担。
-
实验证明:在图像分类、语言模型等大量任务上,Cosine Decay一致地优于Step Decay,尤其在训练大模型时,这一差距更为明显。
当然,Step Decay在某些特定场景(如迁移学习、特定任务微调)仍有用武之地,因为它可以强制模型在特定阶段“重启”适应,但作为通用的默认衰减策略,Cosine Decay已是业界标准。
训练 LLM 时,如何检测数据中的异常样本导致的不稳定?¶
预训练语料规模庞大,难免混入异常样本。这些“脏数据”可能引发Loss Spike、梯度爆炸等问题。因此,需要建立一套从预处理到训练中的全流程异常检测体系。
训练前-数据预处理阶段的检测:
-
长度过滤:统计文本长度分布,剔除过长(如超过100k字符)或过短(如少于10个字符)的样本。异常长度往往是噪声。
-
字符集和语言检测:使用fastText或langdetect等工具检测文本语言,剔除与目标语言不符或包含大量乱码、特殊字符的文本。
-
困惑度过滤:使用一个小的预训练语言模型(如GPT-2)计算文本的困惑度。困惑度过高说明文本流畅性差,很可能是随机字符或格式错乱。
-
重复文本检测:使用MinHash或SimHash算法识别近乎重复的文档或段落,去重以避免模型记忆。
-
规则清洗:移除HTML标签、多余空行、非文本符号等。
训练中-动态异常检测:
-
记录每个batch的损失和梯度范数:如果某个batch的损失显著高于近N步的平均值(如超过3倍标准差),标记该batch为异常。可以当即丢弃该batch的梯度,不更新模型。
-
梯度裁剪触发分析:记录哪些batch触发了梯度裁剪(即梯度范数超过阈值)。如果同一类数据频繁触发裁剪,说明这类数据可能含有大量异常样本。
-
保留异常样本索引:在发生Loss Spike或梯度异常时,将该batch的样本索引或内容摘要保存到日志,供事后人工审查。
-
嵌入异常检测:利用模型中间层的输出(如[CLS]向量)进行聚类,检测离群点。异常样本的嵌入往往远离正常数据簇。
持续迭代:将检测到的异常样本收集起来,分析其共性(如特定来源、特定模式),更新预处理规则。这是一个“检测-分析-改进”的闭环,能够逐步提升数据质量,保障训练稳定。
如果训练不收敛,你从数据、模型、优化器三方面如何排查?¶
训练不收敛(loss居高不下,或震荡,或NaN)是大模型训练中常见且令人头疼的问题。系统性地从数据、模型、优化器三个维度排查,可以快速定位根因。
数据方面:
-
数据质量:检查输入文本是否包含大量乱码、特殊字符或空文本。异常数据可能导致极端梯度,破坏训练稳定性。可通过采样一批数据人工检查,或统计文本长度分布、字符频率等发现异常。
-
数据预处理一致性:确认训练和验证的 tokenization、截断、padding 策略完全一致。如果训练用了动态 padding 而验证没有,或 tokenizer 版本不同,会导致验证时模型看到完全不同的分布。
-
标签正确性:如果是监督任务,检查标签是否正确编码,是否存在标签偏移或全部为同一类别。对于语言模型,确保 targets 是输入错位一位,没有偏移错误。
-
数据分布:验证数据是否充分打乱。如果连续多个batch都是同类或同领域的样本,优化器可能发生剧烈震荡。检查数据加载器的 shuffle 是否开启,以及是否有数据重复或缺失。
-
数据归一化:对于数值特征,确保已经做了标准化或归一化。对于图像,检查像素值是否在合理范围(如0-1或归一化后)。对于文本,检查 input_ids 是否在词表范围内。
模型方面:
-
模型结构:检查模型定义是否正确,尤其是层数、隐藏维度、注意力头数等超参数是否符合预期。确认残差连接、LayerNorm 的位置是否正确。使用
print(model)或torchinfo可视化。 -
初始化:不当的初始化是常见原因。如果所有权重初始化为0或过大,网络无法学习。对于 Transformer,通常使用 Xavier/Glorot 初始化(或 He 初始化),且残差分支常用0初始化或极小初始化。检查初始化代码,确保使用了正确的初始化和种子。
-
激活函数:确认激活函数选择合理,且没有在需要概率输出的地方误用 ReLU 等。对于分类,输出层是否正确使用了 Softmax 或 Sigmoid。
-
梯度流:检查是否存在梯度消失或爆炸。通过打印各层梯度的 L2 范数,看哪些层梯度极小或极大。梯度消失可能源于激活函数饱和(如 Sigmoid)或初始化不当;梯度爆炸可能源于学习率过大或模型结构问题。
-
BatchNorm/LayerNorm:对于 Transformer,LayerNorm 通常放在子层前面(Pre-Norm)有助于训练稳定。如果是 Post-Norm,深层网络可能更难训练。
优化器方面:
-
学习率:学习率过高会导致 loss 震荡或 NaN,过低会导致收敛极慢。使用 LR Finder 确定合适的学习率范围。检查学习率预热(warmup)是否开启,预热步数是否足够。
-
优化器类型:对于大模型,AdamW 是标配。SGD 可能需要更精细的调参。检查优化器参数:betas、epsilon、weight_decay 是否合理。
-
梯度裁剪:是否开启了梯度裁剪?对于 Transformer,通常将梯度范数裁剪到1.0左右。如果没有裁剪,梯度爆炸风险高。
-
混合精度:如果使用 FP16,是否开启了 loss scaling?动态 loss scaling 是否正常工作?scale 是否一直下降?可以尝试切换 BF16 或 FP32 排查是否是精度问题。
-
批量大小:批量过小会导致梯度噪声大、训练不稳定。可尝试增大 batch size 或增加梯度累积步数。
-
优化器状态:如果从 checkpoint 恢复训练,确认优化器状态加载正确。不同框架的优化器状态可能不兼容。
排查顺序建议:先过一遍数据,确保没有明显错误;然后用一个极小的数据子集测试模型能否过拟合(应该能),若不能则是模型或优化器问题;接着检查初始化、学习率和梯度。通常能较快锁定问题。
模型参数初始化不当会导致什么问题?如何选择合适的初始化?¶
初始化不当的常见问题:
-
所有权重初始化为0:导致所有神经元在每层学习到相同的特征,网络退化为线性模型,无法学习复杂函数。
-
初始化过大:前向传播时激活值迅速放大,经过深层后超出浮点数范围,产生 NaN 或 Inf;反向传播时梯度爆炸,参数更新量巨大,导致 loss 震荡或不收敛。
-
初始化过小:前向传播时信号逐层衰减,到深层已经接近0,反向传播时梯度消失,浅层参数几乎无法更新,训练停滞。
-
初始化不匹配激活函数:例如,对 ReLU 使用 Xavier 初始化(假设线性激活),会导致深层输出方差逐渐减小;使用 He 初始化才能保持方差稳定。
如何选择合适的初始化:
-
Xavier/Glorot 初始化:适用于 Tanh、Sigmoid 等关于原点对称的激活函数。它将权重初始化为均值为0、方差为
2 / (fan_in + fan_out)的分布(正态或均匀)。目的是保持前向和反向传播的方差大致相等。 -
He/Kaiming 初始化:专为 ReLU 及其变体设计。由于 ReLU 将负半轴置零,输出方差减半,因此 He 初始化将方差设为
2 / fan_in(前向考虑)或2 / fan_out(反向考虑),以补偿方差损失。通常使用fan_in模式。 -
Transformer 的特殊处理:
- 注意力层的 QKV 投影和 FFN 的第一层通常使用 Xavier 或 He 初始化。
- 残差分支的最后一层(如注意力输出投影、FFN 的第二层)常采用0初始化或极小初始化,以保证训练初期模型接近恒等映射,增强训练稳定性。
-
一些大模型(如 GPT-3、LLaMA)会进一步调整初始化的标准差(如 0.02),这是经验值。
-
偏置初始化:通常初始化为0。但对于某些门控机制(如 LSTM 的 forget gate),会初始化为1以促进初期记忆。
实践:在 PyTorch 中,通过 nn.init.xavier_uniform_ 或 nn.init.kaiming_uniform_ 应用初始化。对于自定义模型,务必在 init 中显式调用。如果使用 HuggingFace 等框架,通常已有合理默认值,微调时无需改动。
为什么 Transformer 的残差分支常用 0 初始化或小初始化?¶
在 Transformer 中,残差分支(即每个子层内部的通路,如注意力输出投影、FFN 的第二层)常采用 0 初始化或极小初始化,目的是在训练初期让模型表现为恒等映射,从而稳定训练。
原因:
-
训练刚开始时,注意力层和 FFN 的权重是随机初始化的,输出带有大量噪声。如果这些噪声不加约束地通过残差连接与主干信号相加,会迅速污染有用的嵌入信息,导致深层网络早期训练极不稳定甚至发散。
-
通过将残差分支的最后一层初始化为0(或接近0),则整个子层的初始输出近似为0,那么
x + F(x) ≈ x,模型退化为恒等映射。这样,信号和梯度都能沿着残差连接顺畅传播,不受噪声干扰。 -
随着训练进行,这些被抑制的权重逐渐学习到有意义的变换,模型平稳地从“复述”输入过渡到“改造”输入。
具体实践:
-
对于每个 Transformer 层的注意力输出投影矩阵,常使用 Xavier 初始化后再乘以一个很小的缩放因子(如
1/sqrt(2*num_layers)),或者直接初始化为0。 -
对于 FFN 的第二层(降维层),也常采用类似的极小初始化。
-
Megatron-LM 和 DeepSpeed 等框架通常对残差分支的权重进行特殊的初始化缩放。
这种技巧是深层 Transformer 能够成功训练的关键创新之一。
什么是“梯度消失”?在深层 Transformer 中还存在吗?Pre-Norm 如何缓解?¶
梯度消失是指反向传播过程中,梯度从输出层向输入层传递时,由于链式法则中的连乘效应,梯度值指数级衰减,最终趋近于0,导致浅层参数几乎无法更新。它通常发生在使用饱和激活函数(如 Sigmoid、Tanh)或初始化不当的网络中。
在深层 Transformer 中,梯度消失仍然可能存在,但已通过多种设计被大幅缓解:
-
ReLU 及其变体:正半区导数恒为1,避免了 Sigmoid 类函数的梯度衰减。
-
残差连接:梯度可以直接通过恒等映射路径回传,不被权重矩阵多次缩放,从而保持梯度幅度。
-
LayerNorm:稳定每层的输入分布,防止激活值漂移导致的梯度异常。
然而,在极深的 Transformer(数百层)中,即使有残差连接,如果使用 Post-Norm(原始 Transformer 的结构),梯度消失仍可能发生。因为残差分支的梯度必须经过 LayerNorm 的缩放,多层累积可能导致梯度消失或爆炸。
Pre-Norm 如何缓解:
-
Pre-Norm 将 LayerNorm 放在子层(注意力和 FFN)的前面。这样,残差路径完全绕过了 LayerNorm,梯度可以沿着残差连接畅通无阻地回传。
-
同时,Pre-Norm 对子层的输入进行归一化,使其保持稳定的分布,有利于子层的学习,但不影响梯度的跨层流动。
-
因此,Pre-Norm 架构极大改善了深层 Transformer 的训练稳定性,使千层以上的网络也能顺利训练。这也是当前所有大模型(GPT、LLaMA 等)均采用 Pre-Norm 的原因。
分析 Pre-Norm 和 Post-Norm 对训练稳定性的影响。¶
Pre-Norm 和 Post-Norm 是 Transformer 中 LayerNorm 的两种放置方式,对训练稳定性影响深远。
Post-Norm(原始 Transformer):
-
结构:
x + Sublayer(LN(x))?不,标准 Post-Norm 是LN(x + Sublayer(x))。即残差相加之后再进行 LayerNorm。 -
梯度流:残差分支的梯度需要经过 LayerNorm。LayerNorm 的梯度包含输入方差的缩放因子,在深层网络中,多层缩放因子连乘可能导致梯度指数级放大或缩小,极易引发梯度爆炸或消失。
-
对学习率和初始化极其敏感,训练深层网络需要非常精细的超参数调优,甚至需要特殊的初始化策略(如 DeepNorm)才能稳定。
Pre-Norm(当前主流):
-
结构:
x + Sublayer(LN(x))。即先对输入进行 LayerNorm,再送入子层,最后与原始输入相加。 -
梯度流:残差路径完全绕过了 LayerNorm,梯度可以无障碍地反向传播,不受 LayerNorm 缩放的影响。LayerNorm 只用于调控子层的输入分布。
-
训练更稳定,对学习率和初始化更鲁棒,允许更大的学习率,收敛速度更快。
实验对比:
-
Pre-Norm 的训练损失下降更平滑,且能支持更深层的网络。
-
在相同配置下,Pre-Norm 的最终性能通常略低于 Post-Norm(因为 Post-Norm 的 LayerNorm 起到了更强的正则化效果),但 Pre-Norm 的稳定性优势使得它成为大模型训练的默认选择。后续工作(如 DeepNorm)在 Post-Norm 中结合特殊初始化,也能达到稳定训练千层 Transformer 的效果。
DeepNorm 是如何让 Post-Norm 稳定训练千层 Transformer 的?¶
DeepNorm 是微软提出的改进 Post-Norm 的方法,成功训练了 1000 层的 Transformer。它的核心思想是通过特殊的权重初始化和残差缩放,抑制 Post-Norm 中梯度爆炸/消失的风险。
具体做法:
-
在初始化时,将某些层的权重(如注意力投影和 FFN 第二层)缩小。缩放因子与网络深度有关,通常为
α = (2L)^{-1/4},其中 L 是层数。 -
在残差连接分支上引入一个可学习的缩放因子,初始化为一个较小的值(例如
β = (2L)^{-1/2})。 -
这种设计使得训练初期,每个子层的输出几乎为 0,整个模型退化为恒等映射。这样,Post-Norm 中的 LayerNorm 对梯度的缩放效应被初始化为恒等,不会指数放大/缩小。
-
随着训练进行,缩放因子逐渐被更新,模型缓慢地偏离恒等映射,但始终被 DeepNorm 的结构约束在稳定范围内。
为什么有效:
-
通过初始化缩放,DeepNorm 保证了训练初期信号的平稳传播,解决了 Post-Norm 在深层时的梯度消失/爆炸问题。
-
同时保留了 Post-Norm 的正则化优势(因为 LayerNorm 仍然在残差相加之后),通常能取得比 Pre-Norm 更好的最终性能。
DeepNorm 证明了 Post-Norm 并非天生不适合深层网络,只需配合合适的初始化策略,即可兼得稳定性与高性能。
训练过程中,激活值的分布 (Activation Distribution) 异常说明了什么?¶
监控各层激活值的分布是诊断网络训练状态的有力手段。异常分布可能提示多种问题:
-
激活值均值严重偏离0:可能说明激活函数输出不是零中心的(如 ReLU 输出均为非负),或者存在偏置项过大等问题。这会导致后续层输入的分布偏移,影响梯度方向。
-
激活值方差过大或过小:方差过大意味着信号膨胀,可能引发梯度爆炸;方差过小意味着信号衰减,导致梯度消失。这通常是初始化不当或学习率过大/过小引起的。理想情况下,各层的激活值方差应保持在1左右。
-
激活值大量为0(死亡神经元):对于 ReLU,如果某个神经元对所有输入都输出0,其梯度也为0,永久无法更新。这通常由过大的学习率或不当的初始化导致。监控死亡神经元比例有助于决定是否切换到 Leaky ReLU 等变体。
-
激活值分布出现双峰或多峰:可能暗示数据中存在多种模式,或者网络不同部分学习了完全不同的特征。这本身不一定有害,但值得关注。
-
激活值出现 NaN 或 Inf:说明数值溢出,训练已经崩溃。需要立即检查数据、学习率、梯度等。
可视化工具:使用 TensorBoard 或 wandb 记录各层激活值的直方图、均值和标准差,在训练过程中观察其变化趋势。如果发现某一层的分布逐渐恶化,可以定位问题层并采取相应措施。
如何使用 TensorBoard 或 wandb 监控梯度直方图?¶
TensorBoard:
-
使用 PyTorch 的
torch.utils.tensorboard.SummaryWriter。 -
在训练循环中,每隔一定步数,遍历模型所有命名参数,通过
writer.add_histogram(f'gradients/{name}', param.grad, global_step)记录梯度的直方图。 -
同时可以记录梯度的 L2 范数:
total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)并使用writer.add_scalar('gradients/total_norm', total_norm, step)。
wandb:
-
使用
wandb.init()和wandb.log()。 -
类似地,可以构建一个梯度的字典:
grads = {f'gradients/{name}': wandb.Histogram(param.grad.cpu().numpy()) for name, param in model.named_parameters() if param.grad is not None}。 -
然后
wandb.log(grads, step=step)。wandb 会自动将 Histogram 渲染为直方图。 -
也可记录梯度范数标量。
分析梯度直方图:
-
观察梯度的分布形状(是否正态、是否有长尾)、范围(是否过大或过小)。
-
大部分参数的梯度应该集中在0附近,呈正态分布。如果分布极其分散,或出现大量 NaN,则说明有问题。
-
对比不同层的梯度:浅层梯度应较大,深层梯度应较小?实际上 Pre-Norm 下各层梯度范数大致均衡。如果某层梯度异常小,可能存在梯度消失。
通过监控梯度直方图,可以直观了解训练动态,及早发现并定位梯度爆炸、消失或优化器状态异常等问题。
在混合精度训练中,梯度下溢 (Gradient Underflow) 如何处理?¶
梯度下溢是指在 FP16 训练中,某些梯度值小于 FP16 能表示的最小正规数(约 6×10⁻⁸),导致在 FP16 下被截断为 0。这会使对应参数无法获得有效更新,训练停滞或偏向次优解。
处理方法:
-
Loss Scaling(损失缩放):这是最核心的解决方案。将损失值乘以一个较大的缩放因子(如 2¹⁶),反向传播计算出的梯度也会相应放大,从而被推入 FP16 的可表示范围。在更新参数前,再将梯度除以相同的缩放因子还原。动态损失缩放会自动调整缩放因子:如果连续多个 step 没有溢出,则增大缩放因子;如果检测到溢出(梯度中有 Inf/NaN),则减小缩放因子并跳过本次更新。
-
切换到 BF16 训练:BF16 拥有与 FP32 相同的 8 位指数,动态范围极大(最小正规数约 1.2×10⁻³⁸),几乎不会发生梯度下溢,因此不需要损失缩放。这是当前大模型训练的首选。
-
关键操作保持 FP32:对于容易产生小梯度的操作(如 softmax 的梯度、LayerNorm 的梯度),在计算时提升到 FP32 精度。
-
适当增大学习率:一定程度上能增大梯度幅度,但风险较大,不建议作为主要手段。
-
使用梯度累积时注意缩放:如果使用梯度累积,应在每次微批次反向传播前应用缩放,或者在累积完成后再统一缩放,需保持一致。
通常,结合动态损失缩放的 FP16 训练可以很好地解决梯度下溢。但 BF16 的出现使问题大大简化。
BF16 训练是否就没有梯度下溢问题?¶
基本上没有。BF16 的指数位宽与 FP32 相同(8 位),动态范围覆盖了 FP32 的绝大部分,能表示的最小正规数约为 1.2×10⁻³⁸,远小于训练中通常的梯度值(即使是极小的梯度也在 1e-10 以上)。因此,梯度下溢在 BF16 下几乎不可能发生。
但这并不意味着可以完全高枕无忧:
-
虽然下溢解决了,但 BF16 的尾数只有 7 位,精度较低,理论上某些微小梯度可能被舍入为 0。然而,实际训练中,随机梯度下降的噪声通常远大于这种舍入误差,不会影响收敛。
-
极少数情况下,如果确实存在非常小的梯度,BF16 也可能出现精度损失,但概率远低于 FP16。
-
因此,BF16 训练通常不需要损失缩放,这也是它比 FP16 更受欢迎的原因之一。
什么是“Loss Scaling”的动态策略?如何选择初始 scale?¶
动态损失缩放是在训练过程中自动调整缩放因子,以在防止梯度下溢和避免数值溢出之间取得平衡。其流程如下:
-
初始化一个缩放因子(如 2¹⁶)。
-
每个训练步:将损失乘以缩放因子,反向传播得到缩放后的梯度。
-
检查缩放后的梯度是否包含 Inf 或 NaN(即是否溢出)。如果没有溢出,则:
- 将梯度除以缩放因子还原。
- 执行优化器更新。
-
根据策略决定是否增大缩放因子(例如,如果连续 N 步没有溢出,则将缩放因子乘以一个增长因子,如 1.05)。
-
如果检测到溢出(梯度中存在 Inf/NaN),则:
- 跳过本次参数更新(不执行 optimizer.step())。
- 将缩放因子减小(例如乘以 0.5)。
- 可选地,重新计算该 batch 或直接丢弃,进行下一步。
选择初始 scale:
-
通常从一个保守的值开始,如
2^16(65536)。这是一个经验上对大多数网络都安全的起点。 -
也可以在训练开始前进行一个简短的“预热”阶段:用一个小批量数据,从较低的 scale(如 1)开始,逐步增大 scale,直到遇到第一次溢出,然后选择一个略低于此值的作为初始 scale。
-
如果训练初期频繁出现溢出并降低 scale,说明初始 scale 设得过大;如果长时间 scale 不增长且没有溢出,可能初始 scale 设得太小,可以适当调大。
PyTorch 中的 torch.cuda.amp.GradScaler 封装了动态缩放策略,自动处理上述逻辑。
如果混合精度训练的 scale 一直在下降,意味着什么?¶
如果动态损失缩放的缩放因子在训练过程中持续下降(例如从 65536 不断降至 2 甚至 1),这是一个重要的警告信号,表明梯度中频繁出现溢出(Inf/NaN)。可能的原因包括:
-
学习率过大:导致参数更新步幅过大,梯度值爆炸。应降低学习率或检查学习率预热。
-
数据中存在异常值:某些 batch 包含极端数据,产生巨大梯度。检查数据质量,剔除异常样本。
-
模型结构或初始化问题:某些层的初始化不当,或使用了不稳定的操作(如某些激活函数),容易产生数值溢出。可以尝试切换到 BF16 或调整模型结构。
-
Loss Scaling 的初始 scale 过高:如果初始 scale 设得过大,频繁溢出导致连续下降。可以适当降低初始 scale。
-
训练过程中的正常波动:有时 scale 下降后又能恢复,属于正常。但如果持续下降且无法回升,说明训练处于数值不稳定状态,需要干预。
如果 scale 降到 1 仍然出现溢出,说明 FP16 已经无法承载当前的数值范围,必须切换到 FP32 或 BF16,或者大幅降低学习率。持续下降的 scale 是训练即将崩溃的前兆,应及时排查并采取措施。
“数值不稳定”在注意力计算中的表现(如 softmax 上溢)¶
自注意力机制的核心是计算 softmax(QK^T / √d_k)。当序列长度L很大,或者隐藏维度d_k很大时,Q和K的点积值会非常分散,极可能出现绝对值很大的数。在FP16下,数值范围上限只有65504,一旦点积超过这个值,指数函数exp(x)就会直接爆掉变成Inf,然后softmax的分母也会变成Inf,最终整个输出变成NaN。
即使没有超过上限,如果点积值普遍很大,比如全部在10到20之间,softmax的输出会极度尖锐——某个位置的权重接近1,其他位置接近0。这意味着注意力几乎变成了“硬选择”,梯度几乎为零,这就是典型的softmax“饱和”问题。饱和不仅导致梯度消失,还会让模型丧失从多个位置聚合信息的能力。
更隐蔽的问题在于FP16混合精度训练。即使使用了stable softmax(先减去每行最大值),如果点积值太大,减去最大值后仍然可能有部分值非常小(比如-50以下)。在FP16下,exp(-50)约等于1.9e-22,已经远小于FP16能表示的最小正规数(约6e-8),会直接下溢变成0。当softmax分母中大量值变成0,只剩下最大值那项是1,分母就变成1,但梯度计算时涉及除以分母,如果分母恰好也是0(所有值都极小),就会产生除零NaN。
解决方法:
-
Stable Softmax:计算前先减去每行最大值,这是数学上等价的变换,保证指数输入最大为0,不会上溢。PyTorch的
F.softmax默认就实现了这个trick。 -
缩放因子√d_k:将点积方差控制在1左右,避免点积值过大。这是Transformer原始论文中的关键设计。
-
使用BF16替代FP16:BF16的指数位与FP32相同,动态范围极大,几乎不会上溢下溢。
-
FlashAttention:通过分块计算和在线softmax,在GPU SRAM中完成所有计算,避免中间结果写入HBM,同时内部处理了数值稳定性。
如何通过在模型中添加“LayerNorm”来稳定训练?放在哪里最好?¶
LayerNorm的作用是对每个token的表示向量进行归一化(减均值除方差),使得每层输入保持稳定的分布(均值0方差1)。这能防止深层网络的激活值逐层放大或缩小,从而抑制梯度消失和爆炸,加速收敛。
LayerNorm的放置有两种经典方式:
Post-Norm(原始Transformer论文):
结构为 LayerNorm(x + Sublayer(x))。即先计算残差连接,再做归一化。在这种设计下,残差分支的梯度必须经过LayerNorm才能回传。LayerNorm的梯度包含对输入方差的缩放因子,当层数很深时,多层缩放因子连乘可能指数级放大或缩小梯度,导致梯度爆炸或消失。因此Post-Norm对学习率和初始化极其敏感,训练深层网络非常困难。早期很多工作使用Post-Norm,需要精细的warmup和学习率调优才能稳定。
Pre-Norm(现代主流):
结构为 x + Sublayer(LayerNorm(x))。即先对输入归一化,再送入子层,最后与原始输入相加。这种设计下,残差路径完全绕过了LayerNorm,梯度可以沿着残差连接直接回传,不受任何归一化缩放的影响。LayerNorm只负责调控进入子层的输入分布,使其稳定。因此Pre-Norm训练极其稳定,可以堆叠数百甚至上千层,对学习率和初始化也不敏感。几乎所有现代大模型(GPT系列、LLaMA、BERT等)都使用Pre-Norm。
经验总结:如果你要训练超过24层的Transformer,务必使用Pre-Norm。Post-Norm虽然在同等配置下有时能获得略好的最终性能(因为其LayerNorm起到了更强的正则化效果),但调参成本太高。Pre-Norm开箱即用,是大规模训练的标配。
为什么现代大模型都用 RMSNorm 而不是 LayerNorm?对稳定性有影响吗?¶
RMSNorm是LayerNorm的简化版,去掉了“减去均值”的步骤,只做缩放:y = x / RMS(x) * γ,其中RMS(x) = sqrt(mean(x²) + ε)。
为什么大模型青睐RMSNorm?
计算效率:LayerNorm需要计算均值和方差两个统计量,RMSNorm只需计算均方根。在千亿参数、百万步训练中,省去均值计算能节省约5%-10%的归一化开销。虽然单步节省微乎其微,但累积起来相当可观。
效果相当:大量实验(包括LLaMA、PaLM等)表明,RMSNorm在收敛速度和最终效果上与LayerNorm几乎无差异。原因是减去均值这个操作,在深层网络中后续的线性变换可以通过学习偏置来自适应补偿。即模型自己“学会”了如何处理未中心化的数据。有理论分析甚至认为,强制中心化可能在某些情况下破坏有用的特征偏移信息。
数值稳定性:RMSNorm在混合精度训练中可能略微更稳定,因为少了一次减法操作,减少了精度损失的机会。但这不是主要考量。
因此,RMSNorm凭借更低的计算开销和同等的效果,成为了大模型的标配。对训练稳定性没有负面影响,反而因为更少的计算可能带来微小的吞吐提升。
训练时出现“NAN in parameter”如何定位具体层?¶
参数里出现NaN,通常是在optimizer.step()之后发生的,说明梯度更新把参数推到了无效值。定位具体层需要系统性排查:
步骤一:检查梯度是否NaN。在optimizer.step()之前,遍历所有参数的.grad属性,检查是否存在NaN或Inf。如果有,说明问题出在反向传播;如果没有,可能是优化器更新本身的计算有问题(比如Adam的二阶矩v为0导致除零)。
步骤二:使用torch.autograd.detect_anomaly()。这是最快速的定位工具。在训练代码最前面加上:
python
torch.autograd.set_detect_anomaly(True)
这会让autograd在反向传播时对每个操作的梯度进行数值检查,一旦发现NaN就抛出RuntimeError,并精确指出是哪个算子、哪个层的梯度出问题。缺点是训练速度会显著下降(2-3倍),仅用于调试。
步骤三:手动插入检查点。如果不想用detect_anomaly,可以在模型各层的forward方法中插入检查:
python
if torch.isnan(x).any(): print(f"Layer {layer_name}: activation NaN detected!")
以及在反向传播后检查梯度:
python
for name, param in model.named_parameters(): if param.grad is not None and torch.isnan(param.grad).any(): print(f"Parameter {name}: gradient NaN detected!")
步骤四:利用梯度裁剪监控。正常训练时,记录每个参数的梯度范数。当NaN出现时,查看最近几步的梯度范数趋势,通常爆炸发生前会有异常尖峰。定位到梯度范数最大的那个层,往往就是问题源头。
步骤五:二分法逐步逼近。将模型按层拆分,逐步开启/冻结某些层的训练,观察NaN是否复现,缩小可疑范围。
实战中,我通常先用detect_anomaly快速锁定问题层,然后检查该层的输入数据、权重初始化、激活函数等。常见原因包括:该层权重初始化不当导致激活值爆炸;该层处理了异常数据(如全零向量、超长序列);该层使用了不稳定的激活函数。
使用 torch.autograd.detect_anomaly() 的代价是什么?何时启用?¶
detect_anomaly()会开启autograd的异常检测模式。它的工作原理是:在前向传播时记录每个操作的函数信息,并在反向传播时对每个操作的输出梯度进行数值检查(是否为NaN/Inf)。一旦发现异常,抛出RuntimeError并显示完整的调用栈。
代价非常明显:
-
训练速度大幅下降:通常慢2-3倍,因为每个操作的梯度都要额外检查,且autograd需要保留更多调试信息。
-
显存占用增加:为了能够回溯定位,autograd需要保留额外的中间状态。
-
破坏异步计算:检测是同步的,会打断CUDA流的并行执行,降低GPU利用率。
何时启用:
-
调试阶段:当训练出现NaN,且其他方法无法定位时。通常只在几百步内使用,找到问题后立即关闭。
-
模型修改后的验证:对模型结构做了较大改动后,可以在初期几个epoch启用,确保新结构没有引入数值问题。
-
不建议在生产训练中开启:大规模训练时,detect_anomaly的开销不可接受。
替代方案:PyTorch也提供了torch.autograd.gradcheck用于单元测试级别的梯度检查,以及torch.autograd.profiler用于性能分析。日常训练中,更推荐用梯度范数监控和手动插入检查点来替代detect_anomaly。
梯度检查点 (Gradient Checkpointing) 会影响训练稳定性吗?¶
一般不会影响训练稳定性。Gradient Checkpointing只是改变了激活值的存储策略:前向传播时不保存所有中间激活,反向传播时重新计算被丢弃的激活值。它不改变前向传播和反向传播的计算逻辑,也不改变梯度值本身(在数值精度范围内)。
需要注意的细微影响:
-
数值精度:重计算时由于浮点运算的非结合性,结果可能与原始计算有微小差异(通常在1e-7量级)。对于混合精度训练,这个差异可能略微放大,但通常不足以影响收敛。
-
Dropout等随机操作:如果模型中有Dropout,重计算时因为随机种子可能不同(取决于框架实现),会导致激活值与原始前向传播不一致。PyTorch等框架对此有专门处理,确保重计算时使用相同的mask。
-
BatchNorm:如果模型中有BatchNorm,重计算时统计量可能不同。但Transformer使用LayerNorm,不受此影响。
因此,Gradient Checkpointing是一个“安全”的显存优化技术,可以放心使用。它与混合精度、ZeRO等结合使用,是训练大模型的标准配置。
随机深度 (Stochastic Depth) 作为一种正则化方法,会带来不稳定吗?¶
随机深度在训练时随机丢弃整个层(将该层输出替换为恒等映射),相当于训练了大量不同深度的子网络。推理时使用完整网络。
潜在的不稳定因素:
-
初期训练震荡:如果随机深度的丢弃概率设置过高,训练初期可能因为频繁跳过关键层而导致梯度传播路径不稳定,损失震荡加剧。
-
与Pre-Norm的交互:Pre-Norm下,层输出被放大后再跳过,可能引入额外的方差。需要适当调整丢弃概率。
-
与Dropout的叠加:如果同时使用Dropout和随机深度,正则化强度可能过大,导致欠拟合或训练缓慢。
如何稳定使用:
-
线性递增的丢弃概率:浅层使用较低的丢弃概率,深层使用较高的丢弃概率。这样浅层特征得以保留,深层的高级语义被正则化。
-
适中的最大丢弃概率:通常最大概率设为0.1-0.5,不宜过大。
-
预热:在训练初期不开启随机深度,等模型稳定后再逐步增加概率。
随机深度在ViT等视觉Transformer中广泛应用,能有效防止过拟合。只要概率设置合理,不会带来训练不稳定。
如何通过调整 Dropout 概率来改善过拟合和训练稳定性?¶
Dropout通过在训练时随机丢弃一部分神经元(输出置零),迫使网络学习冗余表示,防止过拟合。同时,Dropout引入的噪声也有正则化效果。
调整策略:
-
过拟合时增大Dropout:如果训练损失远低于验证损失,可以逐步增大Dropout概率(如从0.1增到0.3)。但注意不要过大(一般不超过0.5),否则模型可能欠拟合。
-
欠拟合时减小或关闭Dropout:如果训练损失本身就很高,Dropout可能阻碍了模型学习。可以先关闭Dropout,让模型充分拟合,再逐步开启。
-
分层设置Dropout:浅层(靠近输入)的Dropout概率通常较低(0.1),深层可以稍高(0.2-0.3)。嵌入层通常也使用较低的Dropout。
-
训练初期和后期:初期可以使用标准Dropout;后期如果切换到微调阶段,可能需要降低Dropout。
-
与Batch Size的配合:大批量训练时,梯度噪声小,可以适当增大Dropout;小批量训练时,Dropout可能引入过多噪声,应适当减小。
稳定性考量:
-
Dropout不会导致数值不稳定(不会产生NaN/Inf),但过度Dropout会使训练损失居高不下,看起来像“不收敛”。这实际上是欠拟合,不是数值问题。
-
在混合精度训练中,Dropout的实现需要注意mask的精度,但主流框架已处理妥当。
什么是“Label Smoothing”?它有助于稳定训练吗?¶
Label Smoothing是一种正则化技术:将one-hot硬标签替换为软标签,例如正确类别概率为1-ε,其他类别均分ε/(K-1)。它让模型不要对预测过于自信,输出更平滑的概率分布。
对训练稳定性的帮助:
-
防止过拟合:硬标签鼓励模型对正确类别输出极高概率(如0.999),这可能导致权重被推向极端值,在深层网络中引发梯度不稳定。Label Smoothing抑制了这种倾向。
-
改善模型校准:训练出的模型预测置信度更贴近真实准确率,泛化性能更好。
-
对噪声标签更鲁棒:当数据中存在少量标注错误时,Label Smoothing能减轻模型对错误标签的“执着”。
潜在问题:
-
蒸馏场景下,Label Smoothing可能会削弱教师模型暗知识的传递,需要谨慎使用。
-
过大的ε(如>0.2)会导致模型欠拟合。
实践建议:在大模型预训练中,通常使用ε=0.1。微调时可以适当降低或关闭。
训练 MoE 模型时,负载均衡损失 (Load Balance Loss) 的权重如何影响训练?¶
MoE(混合专家)模型中,门控网络将token路由到不同专家。如果不加约束,门控可能坍缩到少数几个专家,导致大部分专家闲置,模型容量浪费。Load Balance Loss鼓励token均匀分配到所有专家。
权重影响:
-
权重过大:门控受到强约束,token分配接近均匀,但可能违背了“不同专家擅长不同模式”的初衷,模型性能受限。同时,额外的损失项可能干扰主任务损失的优化,导致收敛变慢。
-
权重过小:门控不受约束,迅速坍缩到极少数专家,大部分专家没有收到token,无法被训练,造成容量浪费。这是MoE训练中最常见的失败模式。
-
适中权重:能在均匀分配和专家专业化之间取得平衡。通常权重设为0.01-0.1,具体值需根据模型大小、专家数量、token分布等实验确定。
诊断:监控每个专家接收到的token数量分布。如果某些专家长期接收不到token,应增大负载均衡损失权重;如果所有专家token数量几乎完全均匀,且下游任务性能不佳,可适当减小权重。理想情况是各专家负载大致均衡,但有一定差异。
稳定性:负载均衡损失本身不会导致数值不稳定,但它改变了优化目标。如果权重调整后训练loss突然上升,可能是权重过大干扰了主任务。应逐步调整,观察训练loss和下游指标的变化趋势。
MoE 训练中经常出现“专家坍塌”,为什么?如何缓解?¶
专家坍塌(Expert Collapse)是MoE模型训练中最常见、也最致命的故障模式。它表现为门控网络(router)逐渐或突然地将几乎所有token都分配给极少数几个专家(甚至仅1个),而其他专家接收不到任何token,梯度为零,参数停止更新,彻底沦为“死专家”。这导致模型退化为一个普通稠密模型,完全浪费了MoE的参数量和计算优势。
坍塌的根源在于门控的正反馈循环:初始时,门控权重随机,某个专家碰巧对大部分token给出略高的分数,于是它被选中更多;更多的token意味着更多的训练数据,该专家学得更快,分数变得更高,下一轮又会被选中更多……如此循环,富者愈富。同时,其他专家由于缺乏训练样本,无法提升,被永久边缘化。此外,门控通常使用softmax产生概率分布,一旦某个专家的logit占据主导,梯度就会变得极小而无法改变路由。
缓解手段:
-
负载均衡损失(Load Balance Loss):这是最基础的防线。在训练损失中加入一个辅助项,惩罚专家之间的token分配不均。例如,计算每个专家处理的token比例,与理想的均匀分布之间的KL散度或平方差。权重要调得恰到好处:太小不起作用,太大会削弱模型性能(强制均匀分配违背了专家特化的初衷)。通常从0.01开始尝试。
-
容量因子(Capacity Factor):为每个专家设置一个token处理上限。当某个专家收到的token超过容量时,多余的token被“溢出”到下一个专家,或者直接丢弃。这能防止单个专家过载,强制token分配到其他专家。容量因子通常设为1.0~1.5,即允许专家处理略高于平均数的token。
-
Top-k路由中的随机性:在选取Top-k专家时,不是直接选分数最高的k个,而是根据分数进行多项式采样,让低分专家也有机会被选中。这种“探索”机制能打破早期正反馈循环。
-
辅助损失中的z-loss:有些实现中会加入z-loss,惩罚门控logits的绝对值,防止logits变得过大,从而避免softmax过度尖锐。
-
梯度裁剪:限制门控参数的梯度范数,防止某次更新将门控推向极端。
在训练初期,我通常会先运行几百步观察专家负载分布(用wandb或TensorBoard绘制每个专家接收token数量的直方图)。如果发现分布极度不均衡,立刻增大负载均衡损失权重或开启容量限制。这是一个需要持续监控的指标。
在训练中,如果某张 GPU 的 loss 显著高于其他卡,可能是什么问题?¶
在数据并行训练中,每张GPU独立计算自己那个micro-batch的loss,理论上loss值应当相近(因为数据是随机分配的)。如果某张GPU的loss持续显著偏高,这就是一个危险信号,需要立即排查。
可能原因:
-
数据分片问题:该GPU分配到的数据可能存在异常。比如数据预处理时某类样本未被正确打乱,导致该卡一直处理较难或噪声较大的数据;或者分布式采样器未正确同步epoch,导致重复或缺失。
-
权重初始化不一致:在初始化阶段,如果该GPU的模型权重没有与其他卡同步(例如,仅在rank0初始化然后广播,但广播出错),那么该卡从一开始就在不同的参数上训练,loss自然会偏离。
-
随机种子不一致:如果各卡的随机种子没有统一,数据增强、dropout等随机操作产生不同结果,可能造成loss差异。但通常影响不大。
-
梯度累积或优化器状态不一致:如果在某个step中,该卡因为通信故障或软件bug导致梯度未正确同步,优化器状态与其他卡不同,后续的loss就会产生持续性偏差。
-
硬件故障:GPU的显存存在位错误(ECC可纠正,但不可纠正的错误会导致静默数据损坏),计算单元故障,导致计算出的logits出现随机偏差,loss异常。
-
网络通信问题:该卡与其他卡的通信带宽不足,导致梯度allreduce延迟或丢包,影响参数更新,但通常表现为整体性能下降而非单卡loss偏高。
排查步骤:
-
首先确认loss是偶然波动还是持续性偏高。如果只是偶尔一个batch偏高,可能是数据噪声。持续偏高才需要警惕。
-
将该卡的训练数据采样出来,用其他卡或CPU重新计算loss,对比是否一致,判断是数据问题还是模型问题。
-
将该卡的模型参数保存下来,与其他卡对比(逐层比较L2距离),如果差异较大,说明参数不同步。
-
检查分布式通信日志(如NCCL_DEBUG=INFO),看是否有该卡的通信超时或错误。
-
重启该卡的训练进程,从最新的checkpoint恢复,看是否解决。
实践中,这种问题出现得不多,但一旦出现,通常很隐蔽。我会在训练框架里加一个“loss健康检查”:如果某个rank的loss超过所有rank平均值的3倍标准差,就自动告警并dump现场数据。
如何利用分布式训练的“同步”特性来定位问题卡?¶
分布式训练的同步点(如AllReduce梯度、AllGather参数)可以成为诊断的利器。因为在这些集合操作中,所有参与卡必须达成一致,如果某张卡迟迟未完成,或者计算结果与其他卡不一致(导致归约结果异常),就很容易暴露问题。
方法:
-
插入“心跳”或“校验和”AllReduce:在训练循环的特定位置(比如前向传播完成后,计算loss之后),让每张卡计算当前loss或某个中间激活值的校验和(如简单的求和),然后通过AllReduce收集所有卡的校验和。如果某张卡的值与其他卡显著不同,说明其计算有误。
-
同步屏障(barrier)超时检测:在每次优化器更新前添加一个
dist.barrier(),如果某张卡在规定时间内(如10分钟)未到达屏障,说明该卡速度严重落后或已经卡死。记录该卡的rank并报警。但注意频繁barrier会影响性能,仅用于调试模式。 -
梯度归约结果验证:在AllReduce完成后,每张卡得到的是全局梯度。可以在本地也计算一个预期值(如果知道其他卡的梯度),但这不太现实。更简单的做法:在参数更新后,让所有卡对某个参数计算其L2范数,再AllReduce比较,差异应该极小(浮点误差范围内)。如果某张卡的范数异常,说明参数更新有问题。
-
利用DeepSpeed的debug模式:DeepSpeed提供了
dump_state等工具,可以在指定step收集所有rank的状态(参数、梯度、优化器),用于离线对比。
定位问题卡后,就可以有针对性地重启该卡、检查硬件或排查数据。
在多机训练中,时钟不同步会导致什么?需要关注吗?¶
多机训练通常不要求节点间的系统时钟严格同步(毫秒级差异通常无影响),但时钟偏差过大(秒级甚至分钟级)会导致某些微妙的问题。
影响:
-
日志时间戳错乱:难以根据时间戳对齐多机日志来追踪某个全局事件(比如loss spike),增加了调试难度。这是最直接的痛点。
-
分布式文件系统一致性:如果checkpoint保存依赖修改时间,时钟偏差可能导致错误地认为某个分片陈旧。
-
某些认证或令牌过期:如果训练框架依赖时间戳进行通信认证(如NCCL的某些内部校验),偏差可能引发握手失败。
-
数据加载:如果数据依赖于时间窗口(如实时数据流),时钟不同步可能导致不同节点加载的数据属于不同时段,破坏一致性。
需要关注吗?
在大模型训练中,我通常会确保所有节点的时钟通过NTP同步,误差控制在1秒以内。这主要不是为了训练本身,而是为了运维和调试。一个简单的做法是在训练脚本启动前执行ntpdate或使用chrony服务。很多云平台(如AWS)默认已配置NTP。如果实在无法同步,至少要在训练开始时记录各节点的系统时间偏移,方便后续日志分析。
为什么推荐在训练过程中定期保存 Checkpoint,并保留最佳模型?¶
这看似基础,却是我见过最多人因为忽视而付出惨痛代价的地方。
定期保存Checkpoint的核心目的:
-
故障恢复:硬件故障(GPU、网卡、电源)在大规模集群中是家常便饭。如果你的训练已经跑了三周,因为没有checkpoint而一次性丢失,那损失的不只是算力,更是团队的时间和士气。定期保存(如每1000步)能将损失控制在可接受范围内。
-
回溯最优状态:随着训练进行,模型可能会过拟合,或者在某个阶段出现loss spike后性能退化。保留历史checkpoint可以让你回到之前的“最佳点”。最佳模型不一定是最后一个。
-
复现与调试:当你需要分析某个训练阶段的行为时(比如为何中间loss突然上升),你可以加载那个时期的checkpoint进行回放和诊断。
-
迁移和微调:中间checkpoint可能比最终模型更适合作为下游任务的起点(因为最终模型可能过拟合)。
最佳实践:
-
保存频率取决于训练总步数和任务价值。总步数10万步以上,每1000-2000步保存一次;总步数较少,每epoch保存一次。
-
除了定期保存,还要保留最佳模型:在每个验证评估后,如果当前模型的验证指标(如困惑度、准确率)超越了历史最佳,额外保存一个“best”模型,并标记其步数。
-
保留最近N个checkpoint(如5个),同时将最佳模型和关键里程碑(如学习率衰减点)的checkpoint单独归档,防止误删。
如何设计一个自动化的训练重启和恢复流程?¶
在云上使用可抢占实例(Spot/Preemptible)或面对不可靠硬件时,自动化恢复是降本增效的关键。
流程设计:
-
训练脚本自身支持断点续训:启动时检查指定路径是否存在最新checkpoint,如果有,加载模型参数、优化器状态、学习率调度器、数据迭代器状态(如
torch.utils.data.DataLoader需要配合torch.utils.data.DistributedSampler记录epoch和seed)。这一步是最基础的。 -
外层监控和调度器:在训练任务外部,有一个守护进程或Kubernetes Operator(如Training Operator)监控训练进程状态。当进程退出(无论正常还是异常),分析退出码。如果是硬件故障或抢占,自动重新提交Job。
-
健康检查和优雅退出:训练进程中内嵌一个健康检查线程,定期检查GPU状态、通信状态、磁盘空间。如果检测到异常(如显存ECC不可纠正错误),主动保存checkpoint并优雅退出(
sys.exit(0)),让外部调度器触发恢复。 -
动态节点替换:如果使用弹性训练(如PyTorch Elastic),当某个节点故障时,可以直接用备用节点替换,无需全量重启。DeepSpeed也支持弹性训练。
-
状态持久化与同步:checkpoint保存在共享存储(如NFS、HDFS、S3)上,且在所有rank上同步保存(分片checkpoint)。恢复时,需确保所有rank从同一个全局step开始。
一个典型例子:在Kubernetes上提交PyTorchJob,配置backoffLimit和restartPolicy。当Pod因Spot interruption被终止时,K8s会自动重建Pod,训练代码加载最新checkpoint继续。这一切无需人工干预。
训练大模型时,如何设置“早停” (Early Stopping) 条件?¶
大模型预训练通常不设置传统意义上的早停(因为几乎总是在欠拟合,数据量极大),但在微调和特定场景下,早停仍然必要。
早停条件的设定:
-
基于验证集指标:每隔N步在验证集上评估一次(如困惑度PPL)。如果验证PPL在连续K次评估中没有下降(或上升),且训练PPL仍在下降(过拟合),则触发早停。K通常设为3-5次。需要设定一个最小改善阈值(如PPL下降小于0.01),避免随机波动误触发。
-
基于训练损失平台期:如果训练损失在一个很长的窗口内(如数万步)不再下降,且学习率已经衰减到极小值,说明模型已经收敛,可以停止。
-
基于时间预算:在资源受限时,设定一个最大训练时间或最大训练步数,到了就直接停。
-
基于下游任务指标:如果在训练过程中定期评估下游任务(如GLUE、MMLU),当下游指标不再提升时停止。这是最能反映实际价值的早停条件。
注意事项:
-
大模型预训练常常出现“双下降”现象(Double Descent),验证损失先降后升再降。如果设置过早的早停,可能错过后续的再次下降。因此需要足够长的耐心窗口。
-
预训练中,验证损失上升不一定全是过拟合,可能是验证集太小或分布不一致,需结合下游任务判断。
-
早停条件应当配合checkpoint保存策略,始终保留验证指标最好的模型。
什么是“蒸馏”在训练稳定中的作用?(比如从大模型蒸馏小模型时)¶
知识蒸馏是将一个大型教师模型的知识迁移到小型学生模型的过程。它在稳定性方面的作用常常被低估。
稳定性的贡献:
-
提供平滑的目标分布:教师模型的softmax输出(软标签)携带了类别之间的相似性信息(例如,一个猫的图片,教师可能预测80%是猫,15%是老虎,5%是豹)。相比硬标签(one-hot),软标签提供了更丰富的监督信号,目标分布更平滑,避免学生模型过于极端地拟合某个类别,从而减少过拟合和训练震荡。
-
正则化效果:软标签可以看作是一种标签平滑,能提升模型的泛化能力和校准度。这使得学生模型在数据量少或噪声大的场景下更稳定。
-
加速收敛:学生模型不必从零开始探索,而是直接学习教师已经提炼好的特征表示,训练曲线更平滑,收敛更快。
-
减弱噪声标签影响:如果训练数据中存在错误标签,硬标签会强迫模型学习错误映射,而软标签由于概率分散,对错误标签有一定的容错能力。
在蒸馏过程中,温度系数T控制软标签的平滑度:较高的T使分布更软,提供更多暗知识,但可能模糊类别边界;较低的T更接近硬标签。实践中常将T设在2~10之间,配合适当的损失权重(蒸馏损失与硬标签损失),能使训练更加稳定和高效。
训练时发现 GPU 温度过高降频,如何影响训练稳定性?¶
GPU温度过高导致降频(throttling),核心时钟频率下降,计算性能降低。这直接破坏训练的同步性。
影响:
-
慢节点效应:在数据并行中,所有GPU必须等待同步(AllReduce)。如果某张GPU因为降频而计算变慢,整个训练集群都会被拖慢,GPU利用率骤降。这虽然不直接导致数值错误,但严重影响训练吞吐。
-
时间依赖的操作:某些通信集合操作有内部超时设置。如果降频卡迟迟未完成计算,可能导致通信超时,触发NCCL超时错误,训练崩溃。
-
显存温度过高:显存(HBM)也有工作温度范围。过热可能导致数据传输错误(ECC错误),产生静默数据损坏(见下题),这是最致命的稳定性威胁。
-
硬件保护关机:极端情况下,GPU会为了保护自己而触发硬件断电,导致训练硬中断。
缓解和监测:
-
使用
nvidia-smi或DCGM持续监控GPU温度(核心温度<85°C,显存温度<95°C为宜)。 -
确保服务器散热良好,机架风扇全速,数据中心环境温度在规定范围内。
-
在训练脚本中检测到降频时,输出警告;如果持续降频,可主动暂停训练,等待温度恢复或迁移任务。
-
对大功率GPU(如A100/H100),采用液冷方案是最彻底的解决办法。
ECC 内存错误会导致训练静默数据损坏,如何应对?¶
ECC(Error Correction Code)内存是GPU显存的保护机制,能纠正单比特错误,检测双比特错误。但在大模型训练的巨大计算量下,ECC错误仍会发生,而且可能导致静默数据损坏(Silent Data Corruption, SDC)——计算出的数值是错的,但程序不报任何错误。这非常可怕,因为你会基于错误的结果继续训练,浪费无数资源。
应对策略:
-
监控ECC错误计数:
nvidia-smi可以查询每个GPU的Volatile ECC计数(可纠正和不可纠正)。设置自动化监控,一旦发现不可纠正错误(Uncorrectable ECC error),立即告警并停止该卡上的训练。 -
主动验证:定期(如每个epoch)对模型参数做一次checksum(如逐层求和),然后通过AllReduce与其他卡比对。如果某张卡的值与其他卡显著不一致,说明该卡计算出现了SDC。这虽然会引入额外通信,但在关键训练中是值得的。
-
冗余计算:对同一批数据,让两张卡独立计算,然后比较结果(代价太高,极少使用)。或者依赖框架的浮点异常捕获。
-
硬件替换:一旦确认某卡存在不可纠正ECC错误,立即下线该卡并联系硬件团队更换。
-
使用确定性算法(见下题):虽然不能防止ECC,但能避免因非确定性操作导致的微小差异,从而更容易识别真正的错误。
目前主流大模型训练中,对静默错误的防护仍是一个开放挑战。大多数团队依赖定期checkpoint和密集监控,出现问题后回滚。
大模型训练中,如何保证数值计算的确定性?为什么有时要牺牲它?¶
确定性意味着用相同的输入、相同的初始权重和相同的随机种子,每次运行得到的输出完全一致(bit-exact)。在深度学习中,完全确定性难以实现,因为GPU的浮点运算非结合性(例如并行reduction顺序不可控)会导致微小差异。但我们可以通过设置来消除大部分随机性。
保证方法(PyTorch):
-
设置
torch.use_deterministic_algorithms(True),强制使用确定性算法(如将atomicAdd替换为确定性实现)。 -
设置
torch.backends.cudnn.deterministic = True,让cuDNN使用确定性算法。 -
设置
torch.backends.cudnn.benchmark = False,关闭自动算法搜索(否则每次运行可能选不同算法)。 -
设置所有的随机种子:Python、numpy、PyTorch、CUDA。
-
禁用数据加载的多线程随机性(worker_init_fn固定seed)。
-
控制cuDNN的卷积操作等。
为什么要牺牲确定性?
-
性能:确定性算法通常比非确定性算法慢。例如,cuDNN的某些卷积算法在非确定性模式下可以利用硬件并行性更快地计算,但结果有微小的浮点误差。开启确定性后,框架必须选择速度较慢的算法,训练吞吐可能下降10%~30%。
-
内存:某些确定性实现需要额外的缓冲区。
在大模型训练中,我们通常不追求bit-exact确定性,因为这点浮点误差不会影响收敛和最终性能。牺牲一点确定性换取显著的训练速度提升是划算的。只有在需要严格复现实验结果、定位bug或者进行敏感的科学计算时,才会开启确定性模式。
PyTorch 中的 deterministic 模式是什么?对性能影响。¶
torch.use_deterministic_algorithms(True)是PyTorch提供的全局开关,要求底层库(如cuBLAS、cuDNN)使用确定性的算法实现。这意味着:给定相同的输入,每次运行得到的结果在bit级别完全一致,消除了因并行计算顺序不同而引入的浮点误差。
对性能的影响:
-
显著降低训练速度:许多高度优化的CUDA kernel依赖于非确定性的并行归约(如warp-level reduction)来达到最大吞吐。确定性算法通常需要更慢的串行化步骤或者多次内存访问,导致速度下降。在Transformer中,注意力计算和矩阵乘法的性能损失可能在10%~30%之间,甚至更高(取决于具体操作和GPU架构)。
-
可能报错:如果某个操作没有可用的确定性实现,PyTorch会直接抛出
RuntimeError,这意味着某些模型结构可能根本无法在确定性模式下运行。此时需要修改模型或关闭该模式。 -
仅在调试或特定需求时开启:日常训练绝不应该开启确定性模式,除非你正在:
- 调试一个难以复现的数值bug。
- 需要严格比对两个不同版本代码的输出是否一致。
- 在特定领域(如部分科学计算、金融)对确定性有强制性要求。
实际生产中,我们几乎不会开启它,而是依赖随机种子固定和常规的统计复现性。对性能的追求压倒一切。