chapter14.md — 常见问题与诊断

开篇段落

欢迎来到大型语言模型训练的“急诊室”。无论准备多么周全,在消耗数百万 GPU 小时进行训练时,几乎总会遇到各种棘手的问题:损失突然变成 NaN、模型拒绝收敛、GPU 利用率低下、长文本评估一塌糊涂。本章的目标不是提供一个万能的修复手册,而是建立一套系统性的诊断框架。我们将从最常见的“症状”入手,层层剖析背后可能的“病因”,并给出在 Lightning + DeepSpeed 环境下行之有效的“治疗方”与“处方”。学完本章,你将能够像一位经验丰富的医生一样,面对复杂的训练故障,沉着冷静地运用工具、分析日志、定位问题并采取果断行动,将宝贵的计算资源从失败的边缘拉回正轨。

文字论述

大规模训练的复杂性源于数据、模型、算法和硬件系统的深度耦合。一个地方的微小问题,可能在分布式环境中被放大,最终导致灾难性的失败。我们的诊断流程遵循“由简到繁,由表及里”的原则,从最表层的症状开始,逐步深入到问题的根源。

1. 训练不稳:损失爆炸(Loss Spike)与 NaN

这是最常见也最致命的问题。训练可能在几小时甚至几天后,在一个迭代步骤内突然“死亡”。

  • 症状

    • 训练损失(loss)在一个 step 内从一个正常值(如 2.5)飙升到一个巨大的数值(如 15.0)、inf 或直接变成 NaN
    • 监控工具(如 wandb 或 TensorBoard)示梯度范数(gradient norm)曲线同样出现尖峰,其值可能超过 float16 (65504) 或 bfloat16 的表示上限。
    • 训练进程中断,日志中通常伴随着 loss is NaNgradient is NaN/inf 的错误信息。
  • 诊断流程与解决方案

    这是一个多层次的排查过程,应从最可能的原因开始。

+-----------------+
| Loss is NaN/inf |
+-----------------+
         |
         V
+-------------------------------------------------+
| Level 1: "驾驶技术" - 优化器与学习率            |
| 1a. 学习率 (η) 过高?                            |
| 1b. Warmup 步数不足?                            |
| 1c. 梯度裁剪 (grad clip) 未开启或值过大?        |
+-------------------------------------------------+
         | (若排除)
         V
+-------------------------------------------------+
| Level 2: "燃料质量" - 数据问题                  |
| 2a. 脏数据 (空文本, 乱码, 超长无意义序列)?       |
| 2b. 极端长度分布 (一个batch内长度差异过大)?      |
+-------------------------------------------------+
         | (若排除)
         V
+-------------------------------------------------+
| Level 3: "发动机工艺" - 数值精度与模型实现      |
| 3a. bf16/fp16 溢出 (中间激活值)?                |
| 3b. 数值不稳定的操作 (RMSNorm ε 过小)?          |
| 3c. 模型初始化方差过大?                         |
+-------------------------------------------------+
**详细排查步骤**:

1.  **检查学习率与梯度裁剪**
    *   **学习率 (η)**:这是首要怀疑对象。过高的学习率会让优化器步子迈得太大,直接“跨过”损失函数的山谷,导致参数更新到数值不稳定的区域。对于 Large Batch 训练,即使遵循了 scaling law,初始值也可能偏高。**解决方案**:将峰值学习率降低 30-50%,或将 warmup 步数延长一倍,给模型更充分的“热身”时间。
    *   **梯度裁剪 (Gradient Clipping)**:这是防止单次梯度爆炸毁掉整个训练的“保险丝”。**解决方案**:务必开启。在 DeepSpeed 配置或 Lightning Trainer 中设置 `gradient_clip_val=1.0` 是一个非常稳健的起点。如果裁剪频繁被触发(可通过日志监控),这本身就是一个强烈的信号,说明学习率可能过高或存在其他不稳定因素。

2.  **数据溯源与清洗**
    *   一个格式错误或内容异常的样本就足以产生 `inf`  loss,进而污染整个梯度。
    *   **如何定位坏数据?**
        1.  在训练脚本中加入逻辑,当检测到 loss  `NaN` 时,保存当前的 `global_batch` 到本地。
        2.  编写一个独立的脚本,加载这个坏批次,逐一样本输入模型(在 `eval` 模式和 `torch.no_grad()`下),观察哪个样本在 forward pass 中就产了 `NaN`  `inf`  logits。
        3.  回溯该样本的来源(例如,它来自哪个 `.tar` 文件的第几条记录),检查其原始文本。
    *   **常见数据问题**:空文档、只包含特殊控制符的文本、超长的无间隔字符串(如 base64 编码)、或是由于编码错误产生的乱码。
    *   **解决方案**:在数据预处理阶段(`chapter02`)加入更严格的过滤规则,例如:文档最小/最大长度限制、字母/数字占比检查、平均词长检查等。

3.  **数值精度与模型实现**
    *   **混合精度**`bfloat16` 的动态范围远大于 `fp16`,能更好地容忍大的激活值,是 H100 及以上 GPU 的首选,能极大减少溢出问题。如果仍在使用 `fp16`,中间激活值(尤其是在 SwiGLU FFN 层)很容易超出 `65504` 的上限。
    *   **操作稳定性**
        *   `RMSNorm` 中的 `epsilon` (ε) 是为了防止除以零。默认的 `1e-6` 通常是安的,但在极端情况下,如果某个 token 的隐状态向量几乎为零,这里仍可能出现问题。可以尝试增大到 `1e-5`
        *   `RoPE` 的实现是否在特定位置上产生极端值?检查是否有除法操作可能导致不稳定。
    *   **模型初始化**:LLaMA 风格的模型初始化策略经过精心设计以保证初始激活值和梯度的方差稳定。如果你修改了模型结构或初始化方法(例如,使用了标准差过大的 `torch.nn.init.normal_`),可能会在训练开始时就遇到数值问题。**解决方案**:坚持使用 LLaMA 论文中描述的初始化方法。
  • Rule-of-thumb
    • 诊断流程:先降 LR,再查数据,最后审视模型代码。
    • 日志先行:在训练日志中,除了 loss,务必记录 learning_rategradient_norm。一个健康的训练,gradient_norm 会在 warmup 后稳定在一个较小的范围内(例如 0.5 到 5.0 之间)。

2. 收敛难题:停滞、缓慢或震荡

模型在训练,但 loss 就是不下降,或者下降得极其缓慢。

  • 症状

    • 停滞 (Stagnation):Loss 在最初几百步略有下降后,迅速进入一个平台期,几乎不再变化,像一条水平线。
    • 缓慢 (Slow Convergence):Loss 在下降,但斜率远小于预期,与公开的同规模模型训练曲线相比,可能要多花数倍的 tokens 才能达到相同的 loss 水平。
    • 震荡 (Oscillation):Loss 曲线在 warmup 结束后剧烈上下波动,没有稳定的下降趋势。
  • 诊断流程与解决方案

    1. 检查学习率与批次大小的匹配度

      • 这是最核心的原因。回顾 chapter05GB_tokη 之间存在紧密的 scaling law 关系。
      • 场景A:LR 过低 -> 停滞或缓慢。模型更新步长太小,无法有效走出初始点。
      • 场景B:LR 过高 -> 震荡或停滞。模型在 loss "山谷"的两壁来回反,无法落到谷底。
      • 解决方案:进行一次小规模的 LR Range Test。用一小部分数据(例如 10B tokens)跑几个短的实验,GB_tok 固定,尝试几组峰值学习率,例如 {base_lr * 0.5, base_lr, base_lr * 2.0}。观察哪个 LR 能在最初的 1000-2000 步带来最快且最稳定的 loss 下降。
    2. 审视 LR Schedule

      • Warmup 不足:对于大 batch 训练,Adam 优化器的二阶动量(variance estimate)需要时间来“预热”并稳定下来。过短的 warmup 会导致初始更新方向不稳定。经验法则:对于 1T token 规模的训练,warmup 至少需要 3-5 亿 tokens(例如,GB_tok=4M,需要 75-125 步,但通常设为 2000 步左右更稳健)。
      • Decay 不当:总训练步数(total_steps = T_tokens / GB_tok)是否计算正确?错误的 total_steps 会导致 cosine decay 过快或过慢。如果 loss 在训练中途就早早平台期,检查你的 decay 点是否设置得太早。
    3. 检查 Weight Decay

      • weight_decay 是一个重要的正则化项。如果设置过大(例如 0.5),它会过度惩罚大权重,可能导致模型“学不动”。典型值是 0.1
      • 确保你使用的是 decoupled weight decay (AdamW),而不是 L2 正则化,这在高学习率下表现更稳定。
    4. 数据混比与质量

      • 如果你的数据质量极差,充满了噪声和不相关内容,模型可能无法从中学习到有意义的模式,导致 loss 停滞。
      • 在 CPT 阶段,如果新旧数据的领域差异过大,且混比不当(chapter06),可能导致模型在两个分布上“左右为难”,收敛困难。
  • Rule-of-thumb

    • wandb 上将 losslearning_rate 曲线绘制在同一个 Y 轴上。一个健康的训练,loss 曲线应该在 LR 曲线达到峰值并开始下降时,呈现出最陡峭的下降斜率。

3. 长上下文性能退化与 RoPE Scaling 伪影

模型在训练长度(如 4k)上表现良好,但在更长的评估文本(如 6k/8k)上困惑度飙升或生成内容质量断崖式下跌。

  • 症状

    • 外推 PPL 飙升:在超过训练长度的文本上,滑动窗口困惑度急剧恶化。
    • 生成内容崩溃:生成长文本时,在接近或超过训练长度的位置,开始出现重复、逻辑中断、或者“lost in the middle”(忽略了文本中间的信息)。
    • RoPE Scaling 未生效:应用了 PI、NTK-aware 或 YaRN 等扩展技术,但长文本性能甚至不如基线模型。
  • 诊断流程与解决方案

    1. 确认 RoPE Scaling 配置
      • 这是算法层面的核心。每种 scaling 方法都有关键超参,配置错误会导致其完全失效。
      • PI (Positional Interpolation):核心是修改 rope_theta (在 transformers 实现中) 或 base 频率。你是否在模型加载和训练脚本中都正确地传递了这个新值?CPT 时,是从一个没有 PI 的模型开始,还是加载了已经调整过 theta 的模型?
      • YaRN (Yet another RoPE scaling)scale 因子和 attention_factor 等参数是否根据目标长度和原始长度正确计算和设置?YaRN 的论文提供了明确的公式,需要逐一核对。
    2. Attention 可视化

      • 这是一个强大的诊断工具。编写一个钩子(hook)来捕获模型前向传播时某一层的 attention 权重矩阵。
      • 输入一个长度超过 L_ctx 的序列,然后用 matplotlibseabornimshow 绘制 attention 矩阵的热力图。
      • 健康的 Attention:应该呈现出局部性(对角线附近)和一些有意义的全局模式。
      • 失败的 Attention:可能会看到灾难性的模式,比如所有 token 只关注第一个 token,或者 attention 权重完全散乱成噪声,或者在 L_ctx 的边界处出现明显的直或水平条纹。
    3. 数据长度分布

      • 即使你的 L_ctx 设置为 8k,但如果训练数据中 99% 的样本都短于 2k,模型也学不到长距离依赖。
      • 解决方案:使用 chapter02 中提到的方法绘制训练 token 数据的长度直方图。确保你的数据打包策略(packing)能够有效地创建出足够多的长序列。在 CPT 阶段,有意识地混入一批原生就是长文档的数据集。
A bad distribution for L_ctx=8k:
(count)
^
|
| *
| * *
| * * *
| * * * * *
+----------------------------> length
  1k  2k  3k ...           8k
  • Rule-of-thumb
    • 先微调再相信:任何 RoPE Scaling 技术,在应用于大规模 CPT 之前,都应该先在一个小的下游任务上进行短时间的微调,验证其在目标长度上是否确实有效。
    • YaRN 是 2024 年的稳健默认选项,但其超参需要根据你的具体模型和长度目标进行调整。

4. 数据污染与泄漏

  • 症状

    • 验证集(validation set)的 loss 异常地低,甚至持续低于训练集 loss。
    • 模型在标准 benchmark(如 MMLU, GSM8K)上的得分远超预期,甚至超过了规模大得多的 SOTA 模型。
    • 模型能够逐字逐句地“复述”出验证集或测试集中的长段落。
  • 诊断流程与解决方案

    1. Tokenizer 训练数据污染

      • 根本原因:BPE Tokenizer 的训练过程本身是一种学习。如果用来训练 tokenizer 的语料包含了验证/测试集,那么 tokenizer 的词表和合并规则就会“记住”这些数据的统计特性,这是一种 subtle 的信息泄漏。
      • 解决方案:建立严格的数据防火墙。在项目开始时就一次性划分好 train/val/test,并只用 train 部分的数据来训练 tokenizer。
    2. 训练/验证集重叠

      • 这是更严重的数据泄漏。
      • 诊断方法
        • 精确去重:计算所有文档的哈希值(如 SHA256),检查训练集和验证集之间是否存在哈希碰撞。
        • 模糊去重:使用 MinHash LSH 或 n-gram 重叠率等方法,来检测高度相似但非完全相同的文档。例如,如果一个训练样本和一个验证样本的 3-gram 重叠率超过 80%,就应视为污染。
      • 解决方案:执行严格的数据去重流程。网络爬取的数据(如 Common Crawl)中天然存在大量重复和近重复内容,必须处理。
    3. Benchmark 污染

      • 预训练语料可能无意中包含了常见的学术 benchmark 的题目和答案。
      • 解决方案:在数据清洗阶段,主动搜索并移除与已知 benchmark 高度相似的内容。社区有一些开源的污染检测工具可以利用。
  • Rule-of-thumb:数据治理是严肃的工程。一个看似完美的 loss 曲线背后,可能是数据泄漏导致的虚假繁荣。始终对出乎意料的好结果保持警惕。

5. 训练瓶颈:吞吐(tokens/s)不达标

  • 症状

    • tokens/s 吞吐远低于硬件理论值或社区报告的基准。在 64x H100 80GB 集群上,一个 7B 模型的 MFU(Model FLOPs Utilization)应该能达到 50% 以上,如果你的值只有 20-30%,说明存在严重瓶颈。
    • GPU 利用率监控(如 nvidia-smidcgm)显示周期性的低谷,或者 GPU SM a_active 百分比不高。
    • PyTorch Profiler 或 nsys 显示大量时间消耗在 dataloadercudaMemcpyNCCL 通信操作上,而不是计算 Kernel。
  • 诊断流程与解决方案 (瀑布模型)

    1. 数据加载 (IO/CPU) 是瓶颈吗?

      • 这是最常见的瓶颈。
      • 诊断:Profiler 显示 __next__ 或 dataloader 相关的函数耗时占比较高。训练时 htop 显示 CPU 核心跑满。
      • 解决方案
        • 数据格式:使用 WebDataset (.tar)Parquet 替代海量小文件。
        • 离线处理:将所有 tokenization 和 packing 操作都在离线完成,数据加载器只做最简单的二进制读取和张量转换。
        • Dataloader 参数:增大 num_workers,设置 pin_memory=True,调整 prefetch_factor
        • 避免 Hot-Spot Shard:确保每个 rank 在每个 epoch 开始时读取不同的数据分片。
    2. 数据传输 (CPU-GPU) 是瓶颈吗?

      • 诊断:Profiler 显示 cudaMemcpy 占比较高。
      • 解决方案:确认 pin_memory=True 已开启。检查 batch size 是否过小,导致频繁的小数据块传输。
    3. 并行通信 (GPU-GPU) 是瓶颈吗?

      • 诊断:Profiler 显示 AllReduce, AllGather 等 NCCL 操作耗时占比高。
      • 解决方案
        • 这是算法和系统 infra 结合的问题。于 AI Scientist,可以检查:ZeRO-3 的 bucket size 是否合理?张量并行(TP)是否引入了过多的通信开销?
        • 与 infra 团队确认网络配置是否最优,是否存在落单的慢节点。
    4. 计算 Kernel 是瓶颈吗? (这是理想情况)

      • 诊断:Profiler 显示大部分时间都在 gemm (矩阵乘法) 或 FlashAttention 等计算密集型 kernel 上。
      • 解决方案:这就是需要算法优化的地方了。确认 FlashAttention-v2fused RMSNorm/SwiGLU 等高性能算子都已启用。在 PyTorch 2.x 中,torch.compile() 也能带来显著的性能提升。
  • Rule-of-thumb:使用 PyTorch Profiler 是定位性能瓶颈的黄金标准。先跑一个短的 profile(例如 10 步),分析时间消耗的分布,然后针对性地优化。

本章小结

训练 LLM 的过程充满了挑战,但绝大多数问题都有迹可循。本章建立的诊断框架旨在将混乱的“炼丹”过程,变为结构化的工程问题排查。

| 症状 (Symptom) | 主要嫌疑 (Primary Suspects) | 关键诊断工具 (Key Diagnostic Tool) | 首选解决方案 (First-line Solution) |

症状 (Symptom) 主要嫌疑 (Primary Suspects) 关键诊断工具 (Key Diagnostic Tool) 首选解决方案 (First-line Solution)
Loss 爆炸 / NaN 学习率过高, 数据异常, 数值精度, 初始化问题 梯度范数曲线, 失败批次数据回溯, fp32 复现 降低学习率, 启用梯度裁剪, 过滤数据, 使用bfloat16
不收敛 / 收敛慢 / 震荡 学习率与批次不匹配, warmup/decay 不当, weight decay 过大 Loss/LR 叠加曲线, LR 范围测试 遵循 scaling law 调整 LR, 延长 warmup, 检查 total_steps
长上下文性能差 RoPE Scaling 配置错误, 数据长度分布不均 滑动窗口 PPL 评估, Attention 可视化, 数据长度直方图 使用 YaRN/PI, 核对超参, 混入长文本数据
验证 Loss 过低 (数据泄漏) Tokenizer/训练集污染, 验证集重叠 n-gram 重合度检查, 哈希去重, 检查 tokenizer 训练脚本 严格划分数据集, 仅用训练集训练 tokenizer, 执行去重
吞吐低 / GPU 利用率低 IO/CPU 瓶颈, 通信开销, 未使用 fused kernel PyTorch Profiler, htop, dcgm (MFU) 采用 WebDataset/Parquet, 离线预处理, 启用 FlashAttention

常见陷阱与错误 (Gotchas)

  1. “我的 Loss 是 NaN,一定是 DeepSpeed/Lightning 的 Bug!”

    • 陷阱:在没有充分证据的情况下,将问题归咎于底层框架。
    • 真相:99% 的情况下,NaN 是由你的模型、数据或超参数的数学不稳定性引起的。分布式框架只是忠实地执行了你的计算,并放大了问题。诊断的第一步永远是尝试在单 GPU 上用一个小 batch 复现问题 如果单卡无法复现,再考虑是否是分布式通信或状态同步的特定问题。
  2. “梯度裁剪一直被触发,但训练没挂,应该没问题。”

    • 陷阱:将梯度裁剪(Gradient Clipping)视为解决方案,而非一个重要的诊断信号。
    • 真相:梯度裁剪是安全气囊,不是刹车。频繁的碰撞(裁剪被触发)说明你的“驾驶”(学习率、数据质量)存在严重问题。你应该将裁剪事件记录下来,如果发生频率过高(例如,超过 1% 的 steps),就必须停下来分析原因,通常是降低学习率。
  3. “我的 Loss 曲线有抖动,训练要失败了!”

    • 陷阱:对 Loss 曲线的正常随机波动反应过度,进行不必要的干预。
    • 真相:基于随机梯度下降(SGD)的训练,其 Loss 曲线本身就是带噪的。应该关注经过平滑处理(例如,滑动平均 100 步)后的长期趋势。健康的抖动是正常的只有持续的平台期、持续上升或剧烈的尖峰才值得警惕。过早地降低学习率会扼杀模型的学习潜力。
  4. 混淆 micro_batch_size (MBS) 和 global_batch_size (GBS)

    • 陷阱:在计算学习率 scaling 或总步数时,使用了 MBS 而非 GBS。在 DeepSpeed 配置中,train_micro_batch_size_per_gpu 只是 GBS 的一个因子。
    • 真相GB_tok = (micro_batch_size) × (gradient_accumulation_steps) × (data_parallel_world_size) × L_ctx。所有与 scaling law 和优化器动态相关的讨论,都应基于 Global Batch (tokens)。错误的配置是导致收敛问题的头号杀手。在启动任何大规模训练前,务必手动计算并打印出你预期的 GB_toktotal_steps,与配置生成的值进行交叉验证。