1. 模型优化器到底在解决什么问题
第一次接触 Model-Optimizer 这个概念,是在一个推荐系统的项目里。当时线上推理服务用的是 8 张 A10,单次请求 P99 延迟卡在 180ms 下不来,业务方要求压到 80ms 以内。我一开始以为是模型结构的问题,折腾了两周剪枝和蒸馏,效果都不理想。后来一位做推理框架的同事点了我一句:你先把优化器换掉试试。结果只改了一行配置,延迟直接掉到 95ms。那次经历让我彻底意识到,模型优化器在整个训练和推理链路里的分量,远比大多数人想象的要重。
Model-Optimizer 这个词,字面理解就是“模型优化器”,但它其实是一个复合概念。狭义上,它指的是深度学习训练过程中用于更新参数的优化算法,比如 SGD、Adam、AdamW、LAMB 这些;广义上,它涵盖了从训练阶段的参数更新策略,到推理阶段的图优化、算子融合、量化压缩、内存复用等一整套让模型跑得更快、更省、更稳的技术手段。你在实际项目里听到别人说“优化器调一下”,可能指的是换一个优化算法,也可能指的是换一套推理引擎的优化配置,具体要看上下文。
这篇文章适合三类人看。第一类是刚入行的算法工程师,训练模型时 loss 不收敛、梯度爆炸,不知道怎么排查,其实很多时候问题就出在优化器选型和参数配置上。第二类是做推理部署的工程同学,模型训完了但上线跑不动,延迟高、显存爆、吞吐上不去,需要从优化器层面找突破口。第三类是做 MLOps 和平台建设的同学,需要把优化器相关的配置标准化、自动化,让整个团队的训练和推理效率提上去。
我写这篇东西的出发点很简单:网上讲优化器的文章要么太学术,满屏公式推导,看完还是不知道怎么用;要么太浅,只告诉你 Adam 比 SGD 好,但不说为什么、什么时候不适用。我想用自己在实际项目里踩过的坑、调过的参数、验证过的方案,把 Model-Optimizer 这件事讲清楚,让你看完能直接上手改配置、跑实验、看效果。
2. 训练阶段优化器的选型逻辑与实操
2.1 为什么优化器选型不能拍脑袋
很多人选优化器的方式是“别人用啥我用啥”,或者“默认 Adam 就完事了”。这在 demo 阶段没问题,但到了真实项目里,优化器选型直接决定了三件事:收敛速度、最终精度、训练稳定性。我见过太多项目因为优化器没选对,白白多烧了几十张卡的算力。
先说收敛速度。SGD 和 Adam 的收敛行为完全不同。SGD 在凸优化问题上有理论保证,但在深度网络这种高度非凸的损失曲面上,它的收敛路径往往很曲折,需要精细的学习率调度才能跑得快。Adam 通过一阶矩和二阶矩的估计,自适应地调整每个参数的学习率,在大多数 NLP 和 Transformer 类模型上收敛速度明显快于 SGD。但 Adam 也有代价:它需要额外存储一阶矩和二阶矩,显存占用是 SGD 的三倍左右。如果你在训一个百亿参数的大模型,显存本来就是瓶颈,这时候优化器的选择就不是“哪个效果好”的问题,而是“哪个跑得起来”的问题。
再说最终精度。有一个反直觉的现象:在很多视觉任务上,精心调参的 SGD 最终精度反而比 Adam 高。这是因为 Adam 的自适应学习率在某些情况下会导致泛化性能下降,尤其是在训练后期,模型容易陷入尖锐的局部极小值。SGD 的噪声更大,反而有助于跳出尖锐极小值,找到更平坦的解。所以如果你做的是图像分类、目标检测这类任务,并且有足够的算力做学习率调优,SGD + Momentum 仍然是值得认真考虑的选项。
最后说训练稳定性。优化器对梯度异常的敏感度差异很大。Adam 在遇到梯度爆炸时,因为二阶矩的平滑作用,参数更新幅度不会瞬间失控,相对更稳。但 Adam 也有自己的问题:当二阶矩估计过小时,学习率会被放大,导致参数更新过大,这就是所谓的“Adam 发散”现象。后来 AdamW 通过解耦权重衰减,部分缓解了这个问题。再后来 LAMB 优化器通过层自适应学习率,让 BERT 这种大模型的训练可以用更大的 batch size,进一步提升了训练效率。
2.2 主流优化器对比与选型决策表
我把实际项目里常用的几种优化器整理成了一张对比表,方便你快速做决策。
| 优化器 | 显存开销 | 收敛速度 | 最终精度 | 适用场景 | 主要坑点 |
|---|---|---|---|---|---|
| SGD + Momentum | 低 | 慢 | 高(调参后) | CV 任务、小模型 | 学习率敏感,需要精细调度 |
| Adam | 高 | 快 | 中 | NLP、Transformer | 泛化可能不如 SGD,显存占用大 |
| AdamW | 高 | 快 | 中高 | 大模型训练首选 | 权重衰减系数需要调 |
| LAMB | 很高 | 很快 | 高 | 超大 batch 训练 | 实现复杂,小模型上优势不明显 |
| Adagrad | 中 | 慢 | 中 | 稀疏特征场景 | 学习率单调递减,后期几乎不更新 |
| RMSprop | 中 | 中 | 中 | RNN、非平稳目标 | 需要手动调衰减率 |
这张表不是让你死记硬背,而是帮你建立选型直觉。我的经验是:先看任务类型,再看模型规模,最后看算力预算。CV 任务优先试 SGD,NLP 任务优先试 AdamW,超大模型优先试 LAMB。如果显存吃紧,考虑用 Adafactor 或者 8-bit Adam 这类省显存的变体。
2.3 学习率调度与优化器的配合
优化器选好了,学习率调度没配好,照样白搭。我见过一个项目,用的是 AdamW,但学习率从头到尾固定 1e-3,结果训练到中期 loss 就开始震荡,怎么都下不去。后来改成余弦退火加 warmup,loss 曲线立刻变得平滑,最终精度涨了两个点。
学习率调度的核心逻辑是:训练初期用 warmup 让模型稳定进入训练状态,训练中期用较大的学习率快速下降,训练后期用较小的学习率精细收敛。常见的调度策略有这几种:
- Step Decay:每隔固定 epoch 把学习率乘以一个衰减系数。简单粗暴,适合传统 CV 任务。
- Cosine Annealing:学习率按余弦曲线从最大值衰减到最小值。平滑,适合大多数场景。
- Linear Warmup + Linear Decay:线性上升再线性下降。Transformer 类模型的标准配置。
- One Cycle:先上升再下降,形成一个完整的周期。适合训练轮数固定的场景。
具体参数怎么定?warmup 步数一般是总步数的 5% 到 10%。峰值学习率取决于优化器和 batch size。有一个经验公式:lr = base_lr * sqrt(batch_size / base_batch_size)。比如你用的 base_lr 是 1e-4,base_batch_size 是 256,实际 batch size 是 1024,那峰值学习率可以设成 2e-4。这个公式不是绝对的,但作为起点很实用。
注意:warmup 阶段的学习率不能太大,否则模型一开始就会跑偏。我一般把 warmup 的起始学习率设成峰值学习率的 1/100 到 1/10。
2.4 梯度裁剪与优化器的协同
梯度裁剪是训练稳定性的最后一道防线。尤其是 RNN、Transformer 这类容易出现梯度爆炸的结构,不加梯度裁剪,训练随时可能崩掉。但梯度裁剪和优化器是有交互的,配不好会互相拖后腿。
常见的梯度裁剪方式有两种:按值裁剪和按范数裁剪。按值裁剪是把每个梯度元素限制在[-clip_value, clip_value]之间,简单但可能改变梯度方向。按范数裁剪是当梯度范数超过阈值时,整体缩放梯度,保持方向不变。我一般推荐按范数裁剪,阈值设在 1.0 到 5.0 之间。
这里有个细节:如果你用的是 Adam 类优化器,梯度裁剪的阈值可以设大一点,因为 Adam 本身对梯度幅度有一定的归一化作用。如果你用的是 SGD,梯度裁剪阈值要设小一点,否则裁剪起不到保护作用。我在一个 Transformer 项目里实测过,AdamW + 梯度裁剪阈值 1.0 的组合,比 AdamW 不加裁剪的最终精度高了 0.8 个点,训练崩溃次数从每周两三次降到零。
3. 推理阶段优化器的核心手段与落地
3.1 图优化与算子融合到底在做什么
训练阶段的优化器管的是参数更新,推理阶段的优化器管的是计算图。很多人以为模型训完直接拿去推理就行了,实际上原始计算图里有很多冗余操作,比如连续的 Conv + BN + ReLU,如果不融合,每个算子都要单独读写显存,延迟和带宽都浪费了。
算子融合的原理很简单:把多个连续的小算子合并成一个大的算子,减少 kernel launch 次数和显存读写。比如 Conv + BN + ReLU 融合之后,BN 的参数可以被吸收到 Conv 的权重里,ReLU 直接接在 Conv 输出后面,整个计算只需要一次 kernel launch。实测下来,这种融合在 ResNet 类模型上能带来 20% 到 30% 的推理加速。
图优化还包括常量折叠、死代码消除、内存复用等。常量折叠是把编译期就能算出来的表达式提前算好,比如x * 1直接简化成x。死代码消除是删掉对输出没有贡献的节点。内存复用是让不同层的中间张量共享同一块显存,降低峰值显存占用。这些优化在 TensorRT、ONNX Runtime、TVM 这些推理引擎里都是默认开启的,但你需要确认你的模型导出格式是否正确,否则优化器可能识别不出可融合的模式。
3.2 量化压缩的实操细节
量化是推理优化里收益最直接的手段之一。FP32 转 FP16,显存占用减半,推理速度提升 1.5 到 2 倍,精度损失通常小于 0.1%。FP32 转 INT8,显存占用降到四分之一,推理速度提升 2 到 4 倍,但精度损失需要仔细评估。
量化的方式主要有两种:训练后量化(PTQ)和量化感知训练(QAT)。PTQ 是模型训完之后直接量化,不需要重新训练,适合快速验证。QAT 是在训练过程中模拟量化误差,让模型提前适应,精度损失更小,但需要额外的训练成本。
我一般建议先用 PTQ 跑一遍,看精度损失能不能接受。如果精度掉得太多,再考虑 QAT。PTQ 的关键是校准数据的选取,校准数据要能代表真实推理时的数据分布。我见过一个项目,校准数据用的是训练集的一个子集,但训练集和线上数据分布差异很大,导致量化后精度掉了 5 个点。后来换成线上采样的一千条真实请求做校准,精度损失降到 0.3%。
注意:INT8 量化对激活值的动态范围很敏感。如果某一层的激活值分布很宽,量化误差会很大。这时候可以考虑混合精度量化,对敏感层保留 FP16,其他层用 INT8。
3.3 内存复用与批处理策略
推理阶段的显存瓶颈往往不在模型权重,而在中间激活值。尤其是大 batch size 推理时,中间激活值占的显存可能是权重的好几倍。内存复用的思路是:分析计算图的生命周期,让不同时刻的中间张量共享同一块显存。比如第 1 层的输出在第 3 层用完之后就可以释放,第 4 层的输出可以复用这块显存。
批处理策略也很关键。很多人以为 batch size 越大吞吐越高,实际上存在一个拐点。batch size 增大到一定程度后,显存带宽成为瓶颈,吞吐不再提升,延迟反而增加。我一般会做一个 batch size 扫描实验,从 1 到 64 逐个测吞吐和延迟,找到吞吐最高且延迟可接受的那个点。
还有一个技巧是动态批处理。线上请求是流式到来的,如果每个请求单独推理,GPU 利用率很低。动态批处理是把短时间内到达的多个请求攒成一个 batch 一起推理,提升 GPU 利用率。但攒批会引入额外延迟,需要根据业务对延迟的容忍度来设置攒批窗口。我做过一个实验,攒批窗口设成 10ms,吞吐提升了 3 倍,P99 延迟只增加了 8ms,性价比很高。
4. 常见问题排查与避坑经验
4.1 训练不收敛的排查路径
训练不收敛是优化器相关问题里最常见的一类。排查思路要按顺序来,不要跳步。
第一步,检查学习率。学习率太大,loss 会震荡甚至发散;学习率太小,loss 下降极慢。你可以做一个学习率扫描实验,从 1e-5 到 1e-1 取几个值,每个跑几百步,看 loss 曲线。如果所有学习率都不收敛,那问题可能不在学习率。
第二步,检查梯度。打印每一层的梯度范数,看有没有梯度爆炸或梯度消失。如果某一层的梯度范数突然变得很大,说明这一层可能是问题源头。常见原因是初始化不当或者学习率过大。
第三步,检查优化器状态。Adam 类优化器会维护一阶矩和二阶矩,如果这些状态被意外重置,训练会突然变差。检查你的代码里有没有在训练中途重新初始化优化器。
第四步,检查数据。数据里有没有 NaN 或 Inf,标签有没有越界,这些都会导致 loss 变成 NaN。我遇到过一次,数据预处理时除零产生了 Inf,训练到第 300 步 loss 直接爆掉,排查了一天才找到。
4.2 推理精度下降的定位方法
量化或图优化之后精度下降,定位方法要分层进行。
先看整体精度掉了多少。如果掉了一个点以内,可能是正常的量化误差,可以通过调整校准数据或混合精度来改善。如果掉了好几个点,那说明某些层对量化特别敏感。
然后逐层对比。把量化模型和原始模型的中间层输出拿出来对比,看哪一层的输出差异最大。差异大的层就是敏感层,需要特殊处理。
最后看算子融合有没有引入错误。有些融合规则在特定模型结构下会出错,比如 Conv + BN 融合时如果 BN 的 momentum 设置不对,融合后的权重会有偏差。这时候可以关闭融合,逐个开启,定位到具体是哪个融合规则出了问题。
4.3 显存溢出的应急处理
显存溢出在训练和推理阶段都可能发生。应急处理的手段有这几个:
- 减小 batch size。这是最直接的方法,但会影响训练稳定性或推理吞吐。
- 开启梯度累积。用多个小 batch 累积梯度,模拟大 batch 的效果。但 BN 层的统计量会受影响,需要额外处理。
- 使用混合精度训练。FP16 的显存占用是 FP32 的一半,能省不少显存。
- 使用梯度检查点。把中间激活值不保存,反向传播时重新计算。时间换空间,显存能省 50% 以上,但训练速度会慢 20% 左右。
- 换用省显存的优化器。比如 Adafactor、8-bit Adam、Lion 等。
我一般按这个顺序试:先开混合精度,再开梯度检查点,最后才考虑减小 batch size。因为减小 batch size 对训练效果的影响最大,能不动就不动。
4.4 优化器相关常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss 震荡不下降 | 学习率过大 | 学习率扫描 | 降低学习率或加 warmup |
| loss 变成 NaN | 梯度爆炸或数据异常 | 打印梯度范数和数据统计 | 加梯度裁剪,清洗数据 |
| 训练后期精度停滞 | 学习率没有衰减 | 检查调度器配置 | 加余弦退火或 step decay |
| 显存溢出 | batch size 过大或优化器状态占用高 | 打印显存占用 | 减小 batch,换省显存优化器 |
| 量化后精度掉点多 | 校准数据分布不对 | 对比逐层输出 | 换校准数据,混合精度量化 |
| 推理延迟高 | 算子未融合或 batch 策略不当 | profiling 各算子耗时 | 开启图优化,调 batch size |
| 多卡训练速度不涨 | 通信瓶颈或优化器不同步 | 检查 NCCL 配置 | 调通信策略,检查优化器同步 |
这张表是我从实际项目里总结出来的,覆盖了八成以上的常见问题。遇到问题先查表,能省不少排查时间。
5. 工具链与自动化实践
5.1 主流优化工具的选择
训练阶段的优化器工具,PyTorch 生态里最常用的是torch.optim,里面集成了 SGD、Adam、AdamW、RMSprop 等。如果你需要更省显存的优化器,可以看看bitsandbytes的 8-bit Adam,或者transformers里集成的 Adafactor。Hugging Face 的Trainer已经把这些优化器封装好了,改个配置就能切换。
推理阶段的优化工具,TensorRT 是 NVIDIA 生态里最成熟的,支持算子融合、量化、动态 shape。ONNX Runtime 跨平台支持好,适合非 NVIDIA 硬件。TVM 更灵活,适合需要深度定制的场景。OpenVINO 在 Intel 硬件上表现最好。选择哪个工具,取决于你的部署硬件和团队技术栈。
我个人的经验是:如果团队已经在用 PyTorch,推理部署优先考虑 TensorRT 或 ONNX Runtime,学习成本低,社区资源多。如果硬件是异构的,ONNX Runtime 的跨平台优势更明显。
5.2 自动化调优的落地思路
手动调优化器参数很费时间,自动化调优能省不少事。常见的自动化调优方法有网格搜索、随机搜索、贝叶斯优化。网格搜索适合参数少、范围小的场景。随机搜索在参数多的时候比网格搜索更高效。贝叶斯优化适合评估成本高的场景,比如大模型训练。
我一般用 Optuna 做自动化调优,它支持剪枝策略,能提前终止表现不好的试验。比如你调学习率和权重衰减,可以设 50 组试验,Optuna 会自动帮你找到最优组合。实测下来,自动化调优比手动调参能省 60% 以上的时间,而且找到的参数组合往往比手动调的好。
注意:自动化调优的评估指标要选对。训练阶段看验证集 loss 或精度,推理阶段看延迟和吞吐。不要只看训练 loss,那个容易过拟合。
5.3 优化器配置的版本管理
优化器配置是模型训练的重要超参数,必须做版本管理。我见过太多项目,训练脚本改来改去,最后没人记得哪个配置对应哪个模型。建议把优化器配置单独抽成一个 YAML 文件,和模型代码一起纳入 Git 管理。每次训练实验记录配置文件的 commit hash,方便复现。
配置文件的字段包括:优化器类型、学习率、权重衰减、梯度裁剪阈值、学习率调度器类型、warmup 步数、batch size 等。这些字段一旦确定,不要轻易改动。如果要做对比实验,新建一个配置文件,不要在原文件上改。
6. 个人实操体会与建议
优化器这件事,说到底是经验和实验的结合。理论能告诉你 Adam 和 SGD 的区别,但具体到你的模型和数据,哪个更好、参数怎么设,只有跑实验才知道。我的建议是:不要怕试错,但要控制试错成本。每次实验只改一个变量,记录清楚配置和结果,积累自己的经验库。
另外,优化器的选择不是一锤定音的。训练初期和训练后期可能需要不同的优化器策略。比如前期用 Adam 快速下降,后期切到 SGD 精细收敛。这种切换策略在一些竞赛方案里被验证有效,但实现起来要注意优化器状态的迁移,不能直接换,否则 loss 会跳变。
最后分享一个小技巧:如果你不确定优化器参数怎么设,可以先在一个小规模数据集上做快速实验,找到大致范围,再放大到全量数据。小规模实验的结论不一定完全适用,但能帮你排除掉明显不合理的参数区间,节省大量时间。我在一个百亿参数模型的项目里,就是先用 1% 的数据做了 20 组实验,把学习率范围从 1e-5 到 1e-2 缩小到 5e-5 到 2e-4,然后全量训练只跑了 3 组就找到了最优配置。