从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 30 分钟 页内实验 ×2
C5 优化:参数怎么更新得又快又稳 第 6 课

一个超大的梯度会把模型甩飞吗?

梯度裁剪:只缩长度,不改方向
在浏览器里真训练的小模型第 150 步注入一次异常 batch:动量 SGD 的梯度范数是平时的 40 倍,验证 loss 从 3.70 跳到 4.52,150 步后还没回到原水平。全局范数裁剪只在 ‖g‖ 超过阈值时把整个梯度按比例缩短,方向不变:阈值取 2(略高于平时的 1.4)时全程只裁 8 步,曲线与不注入重合。Adam 自带一层保护,但代价是之后很多步都变小。
❓

本课一问:学习率的时间表定好了。可训练几万亿 token,不可能每一批都正常;只要有一批让梯度一下子大了几十倍,这一步会把模型甩飞吗?

这一课先不碰:开着裁剪为什么大模型还会出 loss spike、以及怎么按住注意力 logit(C8 兑现);FP16 下梯度溢出成 inf(C9)。

从已有出发

正常的一批——比如一批「小明把苹果给了小王」这样的句子——给出的梯度,长度 ‖g‖ 通常在一个稳定的范围里。异常的一批会让 ‖g‖ 突然大几十倍。按前几课的规则,这一步会照单全收。这一课第一次碰训练循环里「裁剪」那一格:

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层
从已有出发 · 训练的一步(点任一环节看它在 Qwen/Qwen3.8-27B 上有多大) 本课本章后面的课

大模型里的异常批次没法在页面上复现,所以用浏览器里的小模型做一个受控实验:第 150 步那一批的输入 embedding 整体放大 k 倍(模拟一次数值异常),其余一切照常,三个 run 共用同一个初始化和取批顺序。这个「放大」是我们人为注入的,不是小模型自己出的事——注入之后发生的一切则都是真算出来的。

先自己设计:不管它

最省事的是不做任何处理:梯度多大,就按多大去更新。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案不管它,梯度多大就按多大更新
注入一次异常 batch:裁剪与不裁剪 字级 MLP · 24,896 参数 · 《三國志演義》24,000 字 · 浏览器内真训练
等待开始(滚动到这里会自动开始)

数字来源:浏览器实时训练(纯 JS,Float64)。模型、语料、种子写在上方;同一组对照里所有 run 共用初始化和取批顺序,只改被比较的那一项。换种子重跑可以看到噪声有多大——差异小于种子间波动时不下结论。

🎯

试三件事:① 默认设置读第一行:‖g‖ = 49.9、裁剪系数 c/‖g‖ = 0.0401;第二行:全局裁剪的方向余弦是 1,而「逐元素截断到 ±c/√P」的方向余弦只有 0.584——那种做法把方向也改了。② 把注入倍数切到 ×100:不裁剪的验证 loss 冲到约 5.26,第 300 步还有 3.94;裁剪的那条线纹丝不动。③ 把阈值切到 1:第三行显示 300 步里每一步都被裁了——这个小模型平时的 ‖g‖ 约 1.2–1.4,阈值 1 让裁剪从「拦异常」变成「每步把梯度长度固定成 1」,等于换了一种优化器。阈值要看自己模型的典型范数。

为什么会甩飞:它不止影响一步

动量 SGD 的位移是 η × v,而 v 会把这次的超大梯度存下来:β = 0.9 时,它在之后每一步里还剩 0.9、0.81、0.73……倍。一次异常,推着参数连走好几步,验证 loss 在后面几十步里一直偏高。

裁剪把这一步的 g 缩到长度 c,方向不变 ;v 里存进去的也就是一个正常大小的梯度。阈值取在典型范数之上,平时几乎不触发,只拦异常——这是 Google 调参手册给的思路:阈值略高于「典型」梯度范数 。

Adam 自带一层保护,但有代价

把优化器切到 AdamW 再跑一遍(组件里「优化器」选 AdamW):同样注入 ×30,不裁剪的验证 loss 只到约 3.78。原因在分母:

所以对 Adam 来说,裁剪的作用是让尖峰根本进不了 m 和 v,而不是救这一步。

真实配置:GPT-3 把全局梯度范数裁剪在 1.0;DeepSeek-V3 的 gradient clipping norm 是 1.0 。但 PaLM 开着裁剪,训练中仍出现了约 20 次 loss spike(R4 从原文摘录)——裁剪只拦得住「单步梯度太长」,拦不住「模型内部某个量在慢慢失控」。

项目里的同一个实验(第 200 步注入异常 batch,开 / 关裁剪,AdamW 与动量 SGD 各一对;项目代码已有 inject_step、inject_kind、inject_loss_scale 三个配置项):

项目真实日志 · 注入异常 batch 时有无裁剪

读这张表要分两对看(数字来自 c05-optimization.json,2026-09-26 导出):

⚠ 常见误解

⚠

「裁剪会改变梯度的方向。」全局范数裁剪不会:所有分量乘同一个正数。会改方向的是「逐元素截断」(每个数单独夹到 ±τ),组件里它的方向余弦只剩 0.584。

⚠

「开了梯度裁剪,就不会有 loss spike。」PaLM 开着 c = 1.0 的裁剪,仍然出现了约 20 次 spike。裁剪管的是单步的梯度长度;spike 的其他来源(注意力 logit 失控等)要靠别的手段,C8 专门讲。

⚠

「阈值 1.0 是标准答案。」1.0 是 GPT-3、DeepSeek-V3 的选择,不是物理常数。我们的小模型平时 ‖g‖ 就在 1.2–1.4,阈值 1 会让每一步都被裁。先看自己模型的典型范数(项目日志里的 grad_norm),再定阈值。

在演进地图上的位置

优化的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 全量梯度下降
  2. 小批量随机梯度下降 SGD
  3. 动量(重球法)
  4. Adam:每个参数一把尺子
  5. AdamW:解耦权重衰减
  6. 学习率 warmup
  7. 学习率衰减:cosine → WSD
  8. batch 大小:渐增,还是一开始就大
  9. 全局梯度范数裁剪
  10. Muon:按矩阵的奇异方向更新
稳定全局梯度范数裁剪Pascanu et al. 2012你在这里
解决了‖g‖ 超过 c 就整体乘 c/‖g‖,方向不变;GPT-3、DeepSeek-V3 都取 c = 1.0。
卡在Adam 把一个权重矩阵当成一堆独立标量:微型模型隐层矩阵的一阶矩,最大的一个奇异方向占57%的能量(第 7 课浏览器实训)。
数字口径:峡谷步数、Newton-Schulz 范围、DeepSeek-V3 与 WSD 的步数账在构建时现算;标「浏览器实训」的来自本章 MicroTrainer 的默认设置(种子 1,Chrome 实测);DeepSeek-V3 数字出自其报告 §4.2(本地 PDF);Moonlight「约 2 倍」出自 arXiv 2502.16982 摘要;Kimi K2「max logit > 1000」出自 arXiv 2507.20534;Qwen3.8-Flash-Next 为官方技术报告自报。

证据三问

问本课的回答
① 它解决什么问题?偶发的超大梯度一步把参数推远,还通过动量(或 Adam 的 v)影响之后很多步
② 数学上为什么可能有效?裁剪后长度 ≤ c、方向不变 ;动量 SGD 与 Adam 对单次尖峰的反应可以直接代入更新式推出
③ 实验上为什么最后选了它?RNN 上的梯度爆炸(Pascanu 2012);此后几乎所有 LLM 预训练都开着裁剪(GPT-3、PaLM、DeepSeek-V3 都是 1.0)

一句话

📌

裁剪只缩长度、不改方向:把一次离谱的梯度变成一次普通的步子。

🚦

先停在这里:记住「阈值略高于典型范数,平时不触发」,进下一课。
再深一层:在组件里把优化器切到 AdamW,对照上面 v 被放大的推导,看注入之后的几十步里 loss 曲线是不是下降得更慢。

下一个缺口

到这里,每一个参数都被照顾到了:有自己的尺子(Adam)、有干净的衰减(AdamW)、开局有 warmup、后期会降、异常会被裁。可第 2 课的旋转峡谷留了一个问题:Adam 只认坐标轴。一个权重矩阵,被 Adam 当成一堆互不相干的数——浪费了什么? → 第 7 课:Muon

回扣先记下:PaLM「开着裁剪仍有约 20 次 spike」会在 C8(稳定性)兑现。

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C5-L6.md。

🔍 除了裁剪,真实训练里遇到 spike 还怎么办?

常见的事后处理是「回滚到 spike 之前的 checkpoint,跳过出事的那几百个 batch 再继续」——PaLM 报告里就是这么做的:从 spike 前约 100 步的 checkpoint 重启,跳过 200–500 个 batch。报告还发现,把同一批数据喂给更早的 checkpoint 并不会 spike——spike 是「特定数据 × 特定参数状态」一起触发的,不是单纯的坏数据(R4 从 PaLM §5.1 摘录)。这属于稳定性工程,C8 展开;本课只讲「每一步」层面的裁剪。