☰
大模型训练优化器从SGD到AdamW:方向、步长与权重衰减详解
2026/10/14 22:23:33 网站建设 项目流程

一聊到大模型训练,大家最先想到的总是网络结构、数据规模、算力卡数这些“大件”,但真正决定训练顺不顺的,往往是那个被当成“默认项”的优化器。优化器就是每一轮参数更新的方向盘和油门,在动辄几千亿次参数更新的训练里,它哪怕偏一点点,累积起来的差别也会大得吓人。这篇是“大模型训练”系列的第二篇,围绕 SGD、EMA、Momentum、RMSProp、Adam 与 AdamW 这条主线,把优化器的基础逻辑一次讲透。

这篇文章适合三类人:一是刚从图像分类、目标检测这类任务转到生成式大模型训练的同学,想搞明白为什么现在一上来就选 AdamW,而不是教科书里那个经典的 SGD;二是已经在跑训练但只会“用”不会“调”的工程师,想彻底理解 lr、beta1、beta2、weight decay 这些参数到底在干吗,改哪个最见效;三是准备自己写训练框架、做分布式底层优化的朋友,需要知道优化器状态量对显存和通信的影响。

读完你至少能回答几个实际问题:Momentum 到底解决了 SGD 的什么毛病?RMSProp 的自适应步长是怎么来的?Adam 和 AdamW 差的那个 weight decay 为什么重要?出现 loss 震荡、NaN、收敛缓慢时,应该先动哪个旋钮?

1. 没你想的那么玄:优化器在大模型训练里的真实地位

1.1 为什么大模型比小模型更吃优化器配置

以前训一个几十层的分类模型,SGD 跑得动,Adam 也能跑,差别只是收敛快慢和最终精度差一两个点。但大模型一上来,情况完全变了。参数量动辄几十亿、上百亿,训练预算以“万卡时”计算,loss 要稳定下降成千上万步,任何一个小环节出问题,浪费的算力成本都是真金白银。

优化器这时候不再只是“加速收敛”的助推器,它要同时扛下三件事。

第一,控制参数更新的方向和步长。梯度只是告诉我们哪个方向下降最快,但步子迈多大、要不要参考历史梯度,都由优化器决定。大模型 loss 曲面比小模型复杂得多,局部鞍点、陡峭峡谷到处都是,单靠原始梯度很容易被困住或者震荡。

第二,缓和梯度噪声。大模型训练几乎都是 mini-batch,batch 里的样本分布天然带来噪声,优化器需要把这种噪声尽量平滑掉,让更新方向更稳定。

第三,保持数值稳定。混合精度训练下,梯度可能上下溢出、更新量可能被下溢出吞掉,优化器内部的 EMA 状态如果设计不当,很容易变成 NaN 的源头。

我自己踩过的一个小教训:早期调小模型,lr 从 1e-3 调到 1e-2,loss 照样能降;换到十亿参数模型,lr 从 3e-4 调到 6e-4,loss 直接原地起飞,梯度 norm 和 update norm 爆到离谱。这说明大模型对优化器配置的敏感度,比想象中高得多。

1.2 优化器选型背后的三层核心矛盾

理解优化器,别只背公式,要看它解决了哪一层矛盾。我习惯把优化器要解决的难题拆成三层。

  • 更新方向怎么定:用当前梯度,还是结合历史梯度的“动量”?
  • 更新步长怎么定:所有参数用一个固定学习率,还是每个参数按历史梯度大小自动调整?
  • 参数约束怎么加:如何防止参数在训练中无限增大,让泛化更稳?

SGD 只解决最基础的方向问题;Momentum 在方向上加入历史信息;RMSProp 把步长从“全局固定”变成“逐参数自适应”;Adam 把方向自适应的 Momentum 和步长自适应的 RMSProp 合在一起;AdamW 又补上了第三层,把权重衰减从梯度里“解耦”出来。这个大框架,就是整篇文章的骨架。

很多新手一开始就跳到 AdamW,反而看不懂每个超参数的语义。我建议先按这个顺序把五个优化器过一遍,再回过头看 AdamW,会发现它其实就是几个成熟组件的拼装。

2. 从 SGD 到 AdamW 的五个关键阶梯

2.1 SGD:最朴素的基准线,也是理解一切的起点

随机梯度下降的更新规则只有一行:

θ_{t+1} = θ_t - lr * g_t

其中 g_t 是当前 batch 的梯度,lr 是学习率。它的特点非常鲜明:每一步都沿着当前梯度的反方向走,不参考历史信息。

优点是简单、省显存、泛化性出奇得好。尤其在小 batch 场景下,SGD 的解常常落在比较“平缓”的极小值点,测试误差并不差。很多经典论文里都有“SGD 虽然慢,但最终效果并不比自适应优化器差”的结论。

缺点也很致命:在大模型场景下,梯度噪声大、地形复杂,SGD 很容易在峡谷两侧来回震荡,收敛很慢。还有一个现实问题:它对学习率极其敏感,lr 稍微大一点就发散,稍微小一点就半天不动。

在大模型训练里,SGD 几乎不会作为首选,但它是最重要的参照系。后面所有优化器都可以看成“在 SGD 上加了什么东西”。我也建议大家在调试自定义优化器时,先把 SGD 的 loss 曲线跑出来当基线,否则你没法判断新优化器到底是变好还是变差。

2.2 EMA:被当成“优化器”的平滑技术,其实有两副面孔

EMA 全称是 Exponential Moving Average,指数移动平均。它不是一个完整的优化器,但经常出现在优化器相关的讨论里,原因是它有两个完全不同的使用位置。

第一个位置:作为优化器内部的状态量。Adam 里的一阶矩 m_t 和二阶矩 v_t,本质都是 EMA。只是在 Adam 的语境下,大家叫它“动量估计”或者“梯度平方的滑动平均”,不直接叫 EMA。

第二个位置:作为参数本身的后处理。训练过程中,除了维护正常训出来的参数 θ,我们还同步维护一份参数的 EMA 副本,比如 λ_t = β * λ_{t-1} + (1-β) * θ_t。这个副本在训练结束时往往比 θ_t 本身表现更好,因为它在时间维上做了平滑,减轻了后期梯度噪声带来的抖动。现在很多开源大模型和图像生成模型,最终发布或评测用的都是这种 EMA 权重,而不是最后一步的原始权重。

EMA 的 β 值通常取 0.99 或 0.999,越接近 1,平均窗口越长,对近期变化越不敏感。实际使用时要注意初始的冷启动问题,前几步 λ 几乎等于 0,所以一般建议做偏差修正,把 λ_t 除以 1 - β^t,这在 Adam 里也一样。

我自己在训练早期经常忘了区分这两个 EMA,导致看代码时很困惑:为什么优化器里有 EMA,checkpoint 里又有 EMA 权重?搞清楚之后,再去看 Adam 的公式就轻松了大半。

2.3 Momentum:让梯度更新带上“惯性”

Momentum 解决的是 SGD 震荡问题。想象一个小球从山顶滚下来,如果每一步都只跟着当前坡面方向走,遇到小坑就会来回弹;但如果小球有速度,它就能冲过小坑,沿大方向继续滚。

数学上,Momentum 引入一个速度项 m_t:

m_t = β1 * m_{t-1} + (1-β1) * g_t θ_{t+1} = θ_t - lr * m_t

m_t 就是前几步梯度的加权平均。当连续几步梯度方向一致时,m_t 较大,更新加速;当梯度方向来回变幻时,m_t 相互抵消,更新放缓,相当于自动过滤高频噪声。

β1 一般取 0.9,意思是新梯度只占 1/10 权重,历史梯度占 9/10 的惯性。这里有个细节:有的教材写成 m_t = β1 * m_{t-1} + g_t,这两种写法系数不同,但本质等价,只是最终有效学习率差一个 (1-β1) 的因子。看代码时不要被系数差异吓到,先确认它有没有做重缩放。

Momentum 还有一个变体叫 Nesterov Momentum。它比标准 Momentum 多做一步:先沿着当前的动量方向“预瞄”一下,再计算那个位置的梯度并更新。在小模型和部分计算机视觉任务上表现更好,但在大模型里,大家几乎都直接用 Adam 或 AdamW,Nesterov 反而少见。

2.4 RMSProp:给每个参数单独配一把“尺子”

SGD 全家桶都是一个全局学习率管所有参数,但大模型不同位置的参数,梯度尺度可以差好几个数量级。比如 embedding 层和最后的分类头,梯度可能很大;有些深层模块梯度又很小。如果只有一个全局 lr,就会顾此失彼。

RMSProp 的核心思路是:用每个参数的历史梯度平方的 EMA,来衡量这个参数的梯度大小,然后按比例缩放学习率。

v_t = β2 * v_{t-1} + (1-β2) * g_t^2 θ_{t+1} = θ_t - lr * g_t / (sqrt(v_t) + ε)

这里的 v_t 是梯度平方的滑动平均,开根号之后近似等于梯度的 RMS 值。当某个参数历史梯度很大时,v_t 很大,分母大,更新变小;当历史梯度很小时,更新变大。这样一来,每个参数都拥有了自己的自适应步长,近乎天然处理稀疏特征和不同尺度的梯度。

RMSProp 相比更早的 AdaGrad,进步在于没有把历史梯度平方无限累积下去,而是用滑动平均,所以不会出现学习率过早衰减到零的问题。这个机制的代价也很明显:额外增加一份与参数等大的 v 状态,显存开销翻倍。

在大模型时代,RMSProp 单独出现的频率不高,但它的“逐参数缩放”思想是整个 Adam 家族的核心。你去看任何一篇大模型训练相关的优化器改进论文,十有八九都在改这个 v_t 的计算方式。

2.5 Adam/AdamW:把两个成熟思路组装成默认选择

Adam 就是 Momentum 和 RMSProp 的组合体。它同时维护 m_t 和 v_t,并且都做偏差修正,避免训练初期估计偏离。

Adam 的更新公式:

m_t = β1 * m_{t-1} + (1-β1) * g_t v_t = β2 * v_{t-1} + (1-β2) * g_t^2 m_hat = m_t / (1 - β1^t) v_hat = v_t / (1 - β2^t) θ_{t+1} = θ_t - lr * m_hat / (sqrt(v_hat) + ε)

它一方面有了动量带来的方向平滑,另一方面有了自适应步长带来的参数级缩放,所以在很多任务上都是“开箱即用”的默认优化器。早期的 GPT、BERT 类模型,基本都是 Adam。

但 Adam 有一个隐藏问题:它把 L2 正则(权重衰减)直接加到了梯度里。在普通 SGD 里,L2 正则是“梯度减去一个和参数成比例的项”,效果是把权重朝零拉。但在 Adam 里,梯度会被 v_t 归一化,结果 L2 这一项也被归一化了,导致实际正则效果被“稀释”,和期望的 weight decay 不一致。

AdamW 的修改看起来很小:把 weight decay 从梯度中拆出来,不参与 Adam 的自适应缩放,而是在 Adam 更新完之后,直接对参数做一步线性衰减:

θ_{t+1} = θ_t - lr * (m_hat / (sqrt(v_hat) + ε) + wd * θ_t)

这里 wd 就是 weight decay 系数。它和 L2 正则的差别在大模型场景下非常明显。你如果从 Adam 切到 AdamW,通常会发现 loss 下降更稳定,最终泛化更好。

这也是为什么现在几乎所有开源大模型训练代码,第一行写的就是 AdamW。它不是一个多么高深的改动,但把“方向自适应、步长自适应、参数约束”这三层问题完整地解耦出来,非常适合大模型这种参数规模巨大、训练极其漫长的场景。

3. 大模型训练里怎么选、怎么配:参数与策略细节

3.1 为什么大模型训练几乎默认 AdamW,而不是原生 Adam

这个问题被问过很多次。除了上面说的 weight decay 解耦之外,还有一个很实际的原因:AdamW 和现代 learning rate schedule、混合精度训练的配合更稳定。

在原生 Adam 里,如果你既想加正则又想用大 lr,weight decay 项会随着 lr 一起被放大,造成过强的隐式正则,模型可能出现欠拟合。AdamW 把两者解耦后,你可以在比较大的 lr 下训练,同时独立控制权重衰减强度,这在预训练阶段尤其关键,因为预训练往往要跑几十万步,正则不均衡的累计效应非常大。

我也见过一些团队在微调阶段用原生 Adam,结果也还行,毕竟微调步数短、lr 小。但只要是上百亿参数、全量预训练,我的建议是无脑 AdamW,别在优化器选型上给自己加戏。

3.2 必须搞懂的六个关键超参数

我把最常用的超参数整理成一张速查表,基于我实际训练经验,大模型场景下的典型取值如下:

参数常见范围说明
lr1e-4 ~ 3e-4大模型常用 1e-4 到 3e-4,过高会发散,过低收敛慢
beta10.9Adam 的一阶矩衰减,几乎不用改
beta20.95 ~ 0.999控制二阶矩滑动窗口,大 batch 场景常调低到 0.95 或 0.98
eps1e-6 ~ 1e-8防止除零,也影响数值稳定;混合精度下建议 1e-6 以上
weight_decay0.01 ~ 0.1控制参数衰减强度,常见 0.01 或 0.1
warmup_steps总步数的 1%~2%前一段线性把 lr 从 0 升到目标值,避免早期梯度爆炸

beta2 是我在调试时最喜欢动的参数。理论上 beta2 越大,对梯度平方的估计越长程,更新越稳定;但大模型 batch 通常很大,梯度本来就比较平稳,再用 0.999 反而让自适应步长反应迟钝。很多团队直接把 beta2 设成 0.95,效果反而更好。

eps 在混合精度训练里尤其重要。你如果在 fp16 下把 eps 设成 1e-8,sqrt(v_hat) 可能被舍入成 0,导致更新量异常。我自己习惯在 fp16/bf16 训练里用 eps=1e-6,stability 明显改善。

3.3 和 learning rate schedule 的配合方式

优化器和 lr schedule 是绑在一起看的,单谈“AdamW 用多少 lr”没有意义。

常见做法是 warmup 加 cosine decay。原因很简单:训练初期模型参数完全随机,梯度很大,如果一开始就用大 lr,参数会被冲到奇怪的位置。warmup 阶段让 lr 从 0 逐步升到目标值,相当于给模型一个“热身”时间。之后用 cosine 把 lr 平滑降到接近 0,帮助模型在 loss 曲面底部稳定落地。

warmup 步数一般取总步数的 1% 到 2%,比如 10 万步训练,warmup 设 1000 到 2000 步。有的做法是固定 warmup,比如 2000 步,在总步数较短时也够用。

还有一个经验法则:如果你的总 batch size 变大了,lr 可以按比例往上调,但不要超过一个阈值。比如原来 batch 512、lr 1e-4,batch 加到 4096,通常 lr 可以升到 2e-4 到 3e-4,而不是直接乘 8,因为梯度估计变好之后,最优 lr 的上升幅度是亚线性的。

3.4 weight decay 在 AdamW 里到底怎么解耦的

很多人看公式看到 wd 就以为自己懂了,但实际写代码时容易搞错顺序。重点在于:weight decay 不参与 v_t 的归一化,否则就会被“除”掉。

我这里放一个简化的伪代码顺序:

# 1. 计算梯度 g_t # 2. 更新一阶矩和二阶矩 m_t = beta1 * m_t + (1 - beta1) * g_t v_t = beta2 * v_t + (1 - beta2) * g_t^2 # 3. 偏差修正 m_hat = m_t / (1 - beta1^t) v_hat = v_t / (1 - beta2^t) # 4. 先按优化器方向更新 theta_t = theta_t - lr * m_hat / (sqrt(v_hat) + eps) # 5. 再独立做 weight decay theta_t = theta_t - lr * wd * theta_t

第 5 步的顺序,有些实现是放在第 4 步之前,差别不大;核心是它没被包进那个除法里面。如果你把 wd 加到梯度里再一起归一化,那就是原生 Adam 的 L2 正则,效果完全不同。

实操中还有一个坑:不同框架对 weight_decay 的系数定义可能不一样。PyTorch 的 AdamW 里 weight_decay 会乘到 lr 上;有些框架直接对参数乘 (1 - wd),不乘 lr。你在看 loss 曲线时,如果发现模型发散或者参数范数异常,先检查框架里 weight decay 的语义,而不是盲目调数值。

4. 实操复盘:从 Adam 切到 AdamW,以及混合精度排障

4.1 一个典型的十亿级参数训练配置

我在这里给出一份“基于常见实践补充”的训练配置,不是唯一答案,但可以作为一个起点。

假设你在训练一个 1B~3B 参数的 decoder-only 模型,使用 bf16 混合精度,总 batch size 1024,序列长度 2048,训练步数 20 万步。

配置项取值说明
optimizerAdamW首选解耦权重衰减
lr2e-4常见范围内偏中上,配合 warmup 使用
beta10.9默认
beta20.95大 batch 下更灵活
eps1e-6bf16 下保证稳定
weight_decay0.1看正则需求,常用 0.01~0.1
warmup2000 步总步数 1%
schedulecosine降到接近 0
grad clip1.0按全局梯度 norm 裁剪

这个配置能跑通大多数中小型大模型实验。如果你发现 loss 很快下降但后期不稳,优先把 weight_decay 调低;如果 loss 一直不降,先检查 lr 是不是太大,再检查 warmup 是不是太短。

4.2 从 Adam 切到 AdamW 时最容易踩的两个问题

第一个问题:切换后 loss 曲线明显变差。原因多半是 wd 被错误地当成 L2 来理解,或者 wd 设得过大。AdamW 的 weight_decay 效果比原生 Adam 更强,同样是 0.1,原生 Adam 被归一化掉一部分,AdamW 是实打实的参数衰减。我建议从 0.01 起步,再慢慢往上试。

第二个问题:切换后参数范数快速缩小,导致层输出分布漂移。这种情况在 attention 层和 MLP 层最容易出现。解决办法是把 wd 降到 0.01,或者检查是否把 weight_decay 应用到了 bias、LayerNorm 等不打 L2 正则的参数上。很多时候,不应该对 bias 和 norm 类的参数做 weight decay。

4.3 混合精度训练下,优化器怎么和数值稳定共存

bf16 训练越来越普及,它的好处是动态范围大,不容易溢出,但精度低。优化器状态在这种环境下可能有“精度截断”问题。

我常用的几个检查点:

  • 二次矩 v_t 用 fp32 保存,不要用 bf16,否则长时间累积误差会偏大。
  • 参数的 master weight 通常要保留 fp32 副本,优化器更新也放到 fp32 里做,不能直接在 bf16 参数上做加减小量更新,误差容易被吃掉。
  • eps 不要小于 1e-6,梯度很小时,sqrt(v_hat) 可能下溢出。
  • 如果你的框架允许,给 m_t 和 v_t 开启 per-tensor 压缩,能省不少显存,但要小心长时间训练里的误差累积。

我在一次训练里遇到过诡异现象:前 5 万步 loss 正常,第 6 万步突然出现一串 NaN,排查后发现是 v_t 里的一个极小数在 fp16 下被截断成 0,更新量直接爆炸。后来把优化器状态都改成 fp32,问题消失。这个经验后来一直被我写进新框架检查清单里。

4.4 优化器状态到底吃了多少显存

很多人低估了优化器状态占的显存。以 AdamW 为例,每个参数至少需要保存 fp32 的 m_t 和 v_t,这就相当于每个参数 8 字节。一个 20B 参数的模型,光优化器状态就是 160GB。这还不算 fp32 的 master weight 和梯度本身。

这也是为什么大模型训练里经常看到量化优化器、Adafactor、8-bit Adam 这些方案出现。它们的基本思路都一样:在不显著影响收敛的前提下,砍掉一部分优化器状态精度。比如 v_t 可以用动态指数编码压缩,m_t 可以保留 fp16,显存能省一半以上。

如果你在搭分布式训练,要提前把这些开销算进显存规划里,否则很容易出现“模型能放得下,优化器一初始化就 OOM”的尴尬情况。

5. 常见问题与排查速查表

症状可能原因检查方式调整方向
loss 发散或冲高lr 太大、warmup 太短看 loss 前 100 步的变化趋势降低 lr、延长 warmup
loss 不降lr 太小、model 初始化有问题看梯度 norm 是否接近 0适当增大 lr,检查初始化
训练中后期 loss 震荡grad clip 阈值过大、batch 噪声大对比训练与 eval loss降低 lr,增大 beta2
出现 NaN/Inf除零、eps 太小、混合精度下梯度溢出开启 NaN 检测,定位第一个 NaN 层提升 eps 到 1e-6、梯度裁剪、fp32 优化器状态
参数范数增长过快weight_decay 过小定期记录参数 norm提高 wd,或对 norm、bias 排除 wd
评测效果差、训练 loss 正常EMA 权重没用上 / 过拟合对比 EMA 权重和原始权重的评测结果使用 EMA 权重评估,检查 EMA decay

还有一个经常被忽视的检查项:记录 update_norm 和 param_norm 的比值。通常这个比值在 1e-3 到 1e-4 量级比较健康。如果比值过大,说明更新量相对于参数变化太猛,容易导致 loss 震荡;如果比值长时间过小,说明模型在“原地磨”,几乎没有有效更新。

我在实际项目里,会把这几个量做成训练日志的一部分,每隔几百步打一次:loss、grad_norm、update_norm、param_norm、lr 当前值。这样一旦出问题,能快速回看是哪个环节异常,而不是靠猜。

另外,如果在检查点恢复训练时发现 loss 出现跳变,先检查优化器状态有没有完整加载。AdamW 的 m_t 和 v_t 不同步恢复会让训练曲线瞬间崩掉,这属于“看起来是优化器问题,其实是 checkpoint 管理问题”的高频坑。

我个人实际操作的体会是:优化器选型不要追求花哨,先固定一个 AdamW,把 lr、beta2、weight_decay 这三个旋钮调明白,比试遍所有优化器都管用。遇到复杂问题,再回到这个基础框架里找原因,绝大多数训练不稳定都能追溯到某个超参数上一两个数量级的偏差。

最后再分享一个常用小技巧:在大模型训练中,尽量让优化器状态和参数状态分开保存,方便做对比实验。我习惯在检查点里同时存“原始权重”和“EMA权重”,评测时优先用 EMA 权重,如果发现它明显比原始权重好,说明后段训练噪声确实比较重,可以考虑提前降低 lr 或者延长训练。这个习惯,在很多开源项目里也成了默认操作,值得你直接抄走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询