昇思MindSpore大模型优化器参数详解:从Adam到分布式训练
2026/9/17 5:01:31 网站建设 项目流程

昇思 MindSpore 大模型优化器参数详解

打开一份典型的大模型训练脚本,大部分人的注意力都放在模型结构、数据集、分布式并行配置上,优化器那一块往往是从某个开源项目里复制过来的几行参数。我过去也这样干过,直到有次用 MindSpore 训练一个十亿级参数的生成模型,loss 在 1.8 附近死活降不下去,改学习率、换调度策略、调整批大小都没用,最后逐条排查发现是 Adam 的 beta2 用了默认的 0.999,导致二阶动量估计对近期梯度变化太迟钝,整个优化器陷入了一种“看起来在更新、实际上步子已经迈不开”的状态。从那时起我就明白,在大模型训练里,优化器参数不是填个表格交差的事,它直接决定你的训练曲线是平缓收敛、剧烈抖动还是直接飞掉。这篇内容我就以 MindSpore 为背景,把大模型训练中优化器选型、核心参数、动态调度、分布式状态这几个关键环节掰开揉碎讲一遍,里面也会带上我在实际训练里踩过的坑和一些调参经验,希望对正在折腾大模型的朋友有帮助。

1. 先弄清优化器在大模型中扮演的角色:为什么它经常被低估

1.1 大模型场景下,优化器比很多人想象的更“重”

模型规模大了之后,优化器不再只是“把梯度方向算出来然后更新权重”这么简单。首先,优化器参数的数量会直接决定训练状态占据的显存规模。以 Adam 为例,每个参与训练的参数都要额外保存一阶动量(m)和二阶动量(v),如果优化器状态用 FP32 存储,仅这两项就是模型参数量字节数的 8 倍。一个 7B 参数的模型,光优化器状态就要占 224GB 的显存,这还没算模型本身和梯度。所以在大模型训练里,优化器的选型和参数配置,首先就要回答“我能不能放得下”的问题。

其次,优化器的数值行为会影响整个训练的稳定性。大模型的 loss landscape 往往非常陡峭且存在大量“坏区域”,学习率稍微大一点就可能撞进 NaN,beta 参数不合适又可能导致收敛极慢。换句话说,大模型训练对优化器超参数的要求比中小模型严格得多,一个小参数的偏差会被放大到整个训练过程中。

1.2 MindSpore 优化器选型图谱:SGD、Adam、AdamWeightDecay 与 LAMB 的适用边界

MindSpore 的mindspore.nn里提供了一系列优化器,我在实际项目里主要用过四类:SGDMomentumAdam(以及它的变体AdamWeightDecay)、还有面向超大批量训练的LAMB

  • SGD / Momentum:在小规模任务或者迁移学习微调浅层网络时还能用,但在大模型预训练场景下基本不推荐。SGD 对学习率的敏感度极高,且没有逐参数的自适应调整能力,在稀疏梯度占比较高的 Transformer 结构里会出现明显的收敛迟缓。
  • Adam:经典的逐参数自适应方法,利用一阶动量估计梯度方向、二阶动量估计梯度尺度的缩放,在 NLP、CV 的各种任务上表现都相当稳健。MindSpore 的nn.Adam已经做了针对昇腾硬件的算子融合优化,实测训练吞吐比逐个算子组合高出不少。
  • AdamWeightDecay(AdamW):这是大模型预训练最常用的默认优化器。它与 Adam 的核心区别在于把权重衰减从 L2 正则中解耦出来,不参与动量计算,而是直接在参数更新时乘一个衰减系数。近几年的工作流里,基于 Transformer 的大模型,尤其 GPT 风格模型,基本都会选择 AdamW。
  • LAMB:在超大批量(batch size 从几千到几万)的条件下,LAMB 能以较大的学习率稳定训练,因为它会对每一层参数单独归一化更新幅度。做超大规模预训练时可以考虑,但它的敏感参数比较多,需要更细致的调参,不能无脑套用。

选优化器不是“哪个新选哪个”,核心要看三件事:你的模型结构是否有稀疏/长尾参数、你的批量大小是否已经大到影响梯度噪声水平、你的显存是否能装下对应优化器的状态。

2. 五个核心参数逐一拆解:learning_rate、beta、weight_decay、eps 的作用边界

2.1 learning_rate:一个看似简单但最容易被误用的参数

学习率是整个优化器里最显式的一个旋钮,但它在大模型训练里并不是“越大越快、越小越稳”。我习惯把大模型训练的学习率分成几个档位看:

  • 预训练从头开始:常见在1e-43e-4之间,具体取决于批量大小和数据质量。批量越大,梯度估计越稳定,学习率可以适当上调。
  • 领域微调或指令微调:通常在1e-55e-5之间,因为预训练权重已经进入了较好的局部区域,学习率过大会直接破坏已有参数结构。
  • LoRA 这类参数高效微调:基础学习率可以稍大,约1e-42e-4,但需要配合适当的层分配策略。

有一个实际经验,当你在小规模试跑时发现 loss 在某个值附近反复震荡、不下降也不发散,很多人第一反应是加大学习率,实际上大模型场景里更可能是学习率偏大导致在最优解附近来回“弹跳”。把学习率降到原来的 1/5 到 1/10,往往比换模型结构更有效。

MindSpore 中可以这样设置固定学习率:

import mindspore as ms from mindspore import nn optimizer = nn.AdamWeightDecay( params=net.trainable_params(), learning_rate=2e-5, weight_decay=0.01 )

但要注意,实际大模型训练几乎不会用固定学习率,会配合动态调度,这部分在下一节详述。

2.2 beta1 与 beta2:一阶、二阶动量的衰减策略决定了优化器的“记忆长度”

Adam 的更新公式里,beta1控制一阶动量(梯度均值)的指数衰减速率,beta2控制二阶动量(梯度平方均值)的指数衰减速率。用直白的话解释:beta1=0.9意味着优化器大约会参考最近 10 步的梯度方向;beta2=0.999则意味着二阶动量会考虑近 1000 步的梯度幅度信息。

大模型场景里,beta1=0.9基本是共识,很少动它。而beta2是一个非常容易被忽略但影响极大的参数。

  • beta2=0.999时,二阶动量估计非常平滑,适合数据分布相对稳定的场景。
  • 当数据分布发生变化较快(比如训练新领域数据、长文本、对话类数据混合),或者出现较大的梯度波动时,过大的beta2会让二阶动量“反应迟钝”,导致优化器无法快速调整步长,loss 看起来就卡住了。

我实际踩过的例子是:用 MindSpore 训练一个多任务混合数据的大模型,loss 在过了某个阶段后持续横盘,后来把beta20.999调成0.95,同样的总步数下收敛速度明显提升。原因是混合数据带来的梯度方差更大,过快平滑的二阶动量会把有用的梯度信号“抹平”。

MindSpore 中写法:

optimizer = nn.AdamWeightDecay( params=net.trainable_params(), learning_rate=2e-5, beta1=0.9, beta2=0.95, eps=1e-6, weight_decay=0.01 )

2.3 weight_decay:Adam 和 AdamW 的关键区别,以及哪些层不该衰减

很多人把weight_decay等同于 L2 正则,这在 SGD 里差不多,但在 Adam 里有一个经典问题:L2 正则的梯度会先被加到参数梯度上,再参与一阶、二阶动量的计算,这使得权重衰减的实际效果会被自适应学习率“稀释”,尤其是参数本身尺度较小的时候。

AdamWeightDecay(AdamW)的做法是解耦权重衰减:更新参数时,直接从当前参数中减去一个与梯度无关的weight_decay * param项。这样权重衰减就能真正发挥抑制过拟合的作用。

在大模型实践中,weight_decay的常用值在0.010.1之间。但有一个细节容易被忽略:并不是所有参数都需要做权重衰减。归一化层的 gamma/beta、偏置项这些参数,本身不参与权重规模的累积,如果对它们也做 decay,反而可能引入不必要的限制,甚至导致训练不稳定。

MindSpore 的AdamWeightDecay支持通过decay_filter来精细化控制哪些参数执行衰减:

def decay_filter(param): if 'layernorm' in param.name.lower() or 'bias' in param.name.lower(): return False return True optimizer = nn.AdamWeightDecay( params=net.trainable_params(), learning_rate=2e-5, beta1=0.9, beta2=0.95, eps=1e-6, weight_decay=0.01, decay_filter=decay_filter )

这个细节在中小模型上可能感觉不明显,但在大模型上,千亿参数规模的 LayerNorm 和 bias 参数量并不少,正确的衰减过滤会把最终效果和稳定性都提升一个档次。

2.4 eps:数值稳定性与低精度训练的平衡点

eps是加法到二阶动量上的极小常数,作用就是防止除零。默认值1e-6在标准 FP32 训练下够用,但在混合精度训练(FP16 或 BF16)成为大模型标配的今天,eps的选择就值得重新审视。

FP16 能表示的数值范围有限,如果eps设得太小(比如1e-8),二阶动量在某些梯度很小的参数上会等于 0,导致更新量变为极不稳定的除零结果。反过来,如果eps设得太大(比如1e-4),又会压缩自适应学习率的动态范围,让优化器偏向于“伪 SGD”的行为,影响收敛精细度。

我的经验范围是:FP32 训练用1e-6;FP16 混合精度训练,如果梯度裁剪和 loss scaling 已经正常工作,1e-6也可以,但训练初期如果出现loss = NaN,优先检查eps是否被调低到了1e-8以下;BF16 因为指数位更宽,eps的敏感度会低一些,但仍然不建议设到1e-8以下。

3. 学习率调度与批量大小的配合:不是所有“5e-5”都长一个样

3.1 预热(warmup)为什么是必修课

刚开始训练时,模型权重还是随机的,梯度方向包含大量噪声。如果一开始就用较大的学习率,优化器会被带进一个比较差的参数区域,后面再想拉回来非常困难。预热阶段用很小的学习率“让模型先站稳”,再逐步增大到目标学习率,这相当于给一个刚睡醒的人慢慢睁开眼看路,而不是直接拉到强光下。

MindSpore 生成动态学习率的方式比较直接,可以用nn.dynamic_lr下的系列函数构造学习率列表,再传给优化器。一个典型设置:

import mindspore as ms import mindspore.nn as nn total_steps = 20000 warmup_steps = 1000 base_lr = 2e-5 lr_schedule = nn.dynamic_lr.cosine_decay_lr( min_lr=2e-6, max_lr=base_lr, total_step=total_steps, step_per_epoch=1, decay_epoch=total_steps, warmup_step=warmup_steps ) optimizer = nn.AdamWeightDecay( params=net.trainable_params(), learning_rate=lr_schedule, beta1=0.9, beta2=0.95, eps=1e-6, weight_decay=0.01 )

这里有几个点需要特别留意:

  • warmup_step必须和实际训练步数对齐,不要估算。如果训练提前终止或中途改变数据 epoch 数,学习率序列会失配。
  • min_lr一般取base_lr的 1/10 左右,不要设成 0。完全衰减到 0 在后半程会让优化器丧失微调能力,最后几轮训练基本在“原地踏步”。

3.2 余弦衰减与线性衰减的真实差异

大模型训练后期到底应该用余弦衰减还是线性衰减,业界没有绝对定论,但经验上有一些倾向:

  • 余弦衰减会让学习率在后半段平滑降低,模型有机会在局部区域精细搜索,适合训练步数比较充足、数据质量比较高的场景。
  • 线性衰减逻辑更“硬核”,降到某一步直接归零,适合训练预算紧张、希望在后段快速收敛的情况。

我在 MindSpore 里尝试过两种方式,结合实验对比,预训练阶段我更倾向余弦衰减,它的平滑特性在长时间训练里更容易保持稳定性;而在领域微调或指令微调阶段,因为总步数本身不多,线性衰减在实践中也够用。关键原则是:学习率衰减的速率不能过快,否则优化器还剩大量“动量惯性”时学习率已经归零,会导致最终效果明显变差。

3.3 梯度累积改变的是什么

当单卡显存放不下足够大的 batch 时,通常会使用梯度累积:把多个小 batch 的梯度累加后再更新一次参数。在优化器眼里,这等价于把有效 batch size 变大了若干倍。

有效 batch size 变大后,梯度估计更平滑,学习率的上限理论上是可以提高的。但需要注意,Batch size 增大时,学习率并非必须线性增长。在超大批量场景下,优化器的自适应机制会弱化这种线性关系的适用性,盲目随 batch 翻倍加大学习率很容易导致训练震荡。我的做法是:batch size 翻一倍时,学习率先加 30%-50%,观察 loss 曲线的抖动情况,如果训练不稳,退回原学习率,而不是强行追高。

4. 分布式训练下优化器状态:显存占用、混合精度与并行策略

4.1 大模型训练中,优化器状态究竟占了多少显存

很多人一开始以为大模型显存占用“大头是模型参数”,实际训练几次就会明白,优化器状态才是那个最占地方的隐形大户。以 FP32 的 Adam 为例,每个参数需要保存一阶动量(4 字节)、二阶动量(4 字节),加上参数本身(4 字节)和梯度(4 字节),一个参数就要吃掉 16 字节。如果模型 7B 参数,单卡训练需要的裸数据量就是 112GB,这在单卡环境下根本不可能。

好在大模型训练几乎都会走分布式并行。MindSpore 的数据并行、模型并行都支持将优化器状态切片,将不同分片放在不同设备上,这样每个设备只要维护自己负责的那部分参数对应的优化器状态,显存压力大幅下降。专业一点说,这就是 ZeRO 优化器状态分片思路。配置这类并行时,建议先统计出模型参数总量,再反推每个设备的可用显存,最后决定并行切分的维度。

4.2 混合精度下的优化器参数:FP16 梯度与 FP32 状态的配合

混合精度训练里通常模型参数和梯度用 FP16/BF16,但优化器状态会保留 FP32 副本。为什么要保留 FP32?因为更新步骤中如果直接在 FP16 上做动量累积,精度损失会随着迭代不断累积,最终参数更新方向都会被误差淹没。

MindSpore 中使用amp模块可以方便地把模型和优化器组装起来:

from mindspore import amp train_network = amp.build_train_network( network=net, optimizer=optimizer, loss_fn=loss_fn, level="O2", loss_scaler=amp.DynamicLossScaler(scale_value=1024, scale_factor=2, scale_window=2000) )

在这种配置下,eps和 loss scaling 是有联动关系的。梯度经过 loss scaling 放大后再反传,相当于优化器看到的梯度幅度整体被放大,此时如果eps相对梯度尺度太小,二阶动量的数值稳定性指标会完全被 loss scaling 支配,导致极端情况下出现异常。我的经验是,开启动态 loss scaler 时,优先让eps保持1e-6级别,不要在低精度训练里把eps随意改小。

4.3 优化器算子融合与状态缓存:吞吐量优化的一个易忽略点

MindSpore 在昇腾硬件上做了大量优化器级算子融合,理论上你只需要调用nn.AdamWeightDecay这类高层 API,底层会自动选择融合策略。但有一个经验仍值得分享:当网络里存在大量分组参数时,把参数切得过于零碎会降低优化器算子融合的效率,因为每个参数组都要走一次单独的更新内核。

如果你发现自己训练吞吐上不去,且模型结构里有大量重复模块,可以考虑对参数分组做合并处理,或者在定义网络时考虑共享参数/统一命名空间,让优化器可以更高效地处理参数批次。这是很多文档不会提的“隐形优化点”,实测在相同硬件上能把吞吐提升 10%-15%。

5. 优化器引起的训练事故排查:从 loss 不降到训练崩坏的实战记录

5.1 训练初期 loss 直接发散成 NaN 的排查链路

如果模型结构本身没有问题(可以先用几层全连接网络跑同数据验证),那 loss 发散大概率出在优化器参数组合上。我的排查顺序通常是:

  1. 看学习率是不是过大。尤其从开源项目复制来的参数,不见得适配你的模型规模和批量大小。先把学习率降到原来的 1/10 试跑 100 步。
  2. 看 warmup 是否缺位。随机初始化权重加上大学习率,往往第一波更新就把参数推到极端区域。
  3. 看 eps 是否过小。在混合精度训练中,eps=1e-8很容易成为 NaN 的温床。
  4. 看梯度裁剪配置。大模型里梯度范数裁剪建议设置为1.0左右,在 MindSpore 中可以用nn.GradNormClip这类封装来限制极端梯度。

下表是我实际遇到过的几种问题与对应解决方向:

现象常见原因优先调整项
第 1-2 步 loss 直接变成 NaN学习率过大 / 缺少预热降低学习率,增加 warmup 步数
前几步正常,几百步后突然发散二阶动量估计受极端梯度污染增大 beta2(如 0.999),调整梯度裁剪
loss 在某个平台期横盘不动beta2 过大导致动量反应迟钝将 beta2 从 0.999 下调到 0.95 左右
混合精度下持续小幅度抖动eps 与 loss scaling 不匹配保持 eps 在 1e-6 级别,检查动态 loss scaler

5.2 训练后期收敛过慢:优化器侧的修正经验

训练到后期,loss 下降趋缓是正常现象,但如果“缓”得太离谱,就要从优化器参数里找原因。最常见的是学习率衰减到了极小值后,优化器仍然在按固定动量方向更新,造成参数在最优解附近“绕圈子”。这时可以检查学习率是否真的按 schedule 降到位,以及weight_decay是否导致参数被过渡压缩。

另一个容易忽略的点是:不要频繁尝试在训练中途修改优化器超参数并保存 checkpoint。优化器的动量状态是从零开始累积的,你改了 beta1、beta2,相当于让一个已经养成运动习惯的物体突然换了一双新鞋,前期动量记忆全部作废,短期会有一段剧烈调整期。如果非要改,建议带着优化器状态一起加载,而不是只加载模型权重后重新初始化优化器。

5.3 长期训练的“体检”:优化器状态也要进 checkpoint

训练大模型动不动就是几周甚至几个月,中间机器故障、断点是家常便饭。很多人的 checkpoint 只保存模型权重,结果恢复训练后发现 loss 曲线比之前差了一截。原因很简单:优化器的动量与二阶动量没有被保存,重新初始化的优化器在恢复训练后需要一个很长的“热身期”才能重建动量。

所以在 MindSpore 里保存 checkpoint 时,我会同时把优化器状态写进去:

import mindspore as ms ckpt = [ {"name": "model", "data": net.parameters_dict()}, {"name": "optimizer", "data": optimizer.parameters_dict()}, {"name": "step", "data": current_step} ] ms.save_checkpoint(ckpt, "train_ckpt.ckpt")

恢复训练时,先把模型参数和优化器参数一起加载,再继续跑,训练曲线就能平滑衔接。这个细节在长周期训练中非常关键,我建议从一开始就把优化器状态纳入存档机制,而不是事后补。

优化器参数看起来是训练脚本里的几行代码,但它贯穿了显存规划、训练稳定性、收敛速度和故障恢复全过程。我在 MindSpore 上训练大模型的经验是:先明确优化器和核心参数的范围,再把学习率调度、混合精度、分布式分片这些环节联动起来看,最后把所有超参和 checkpoint 策略固化到训练框架里,而不是每次重启后靠感觉重新调。这样即使训练过程中出现异常,你也能第一时间定位到问题在优化器,还是在模型,还是在数据上。

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

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

立即咨询