模型优化这件事我的理解里从来不是单指某个优化器算法也不单是量化剪枝那一堆部署技巧。Model-Optimizer 这个名字放在项目里往往意味着从训练阶段的优化策略到推理阶段的压缩提速整条链路都要纳入考虑。这篇文章就从一个偏实操的角度把我这些年经手过的模型优化工程做一个系统梳理讲讲优化器怎么选、学习率怎么配、量化剪枝蒸馏怎么落地以及那些反复踩过的坑。适合正在调模型训练效果、准备做推理加速或者只想搞懂优化器之间到底差在哪的读者。1. 模型优化到底在优化什么1.1 训练阶段优化器让模型学得动训练优化器是模型学会做事的核心驱动器。它的职责是基于损失函数的梯度更新网络权重使其逐步逼近最优解。可以这样类比损失函数给出了“你现在做得有多差”的反馈优化器则用这个反馈去修正模型参数让打分越来越好看。不同的优化器区别主要在更新步长和方向的估计方式上。一个工程项目里模型不收敛、收敛太慢、过拟合、损失震荡很多时候问题不在网络结构而在优化器选择和超参配置上。这也解释了为什么 Model-Optimizer 这类项目第一优先级往往是把训练阶段调稳。训练阶段优化器通常涉及 SGD、SGDMomentum、Adam、AdamW、RAdam、LAMB、Lion、Adafactor 等它们各有适用的场景和数据特点没有所谓的“万能优化器”。选择优化器本质上是在泛化能力、收敛速度和内存开销之间做权衡。1.2 推理阶段优化器让模型跑得快推理阶段优化是另一个维度的“优化器”它的目标是在尽量不损失精度的前提下降低模型推理时的显存占用、计算量或内存带宽压力。这个阶段的常用手段包括模型量化、剪枝、知识蒸馏、算子融合、在线编译等。与训练阶段的优化器不同这里不处理梯度改动的是模型的结构或数值表示方式让它在目标硬件上运行得更高效。我见过不少团队把训练阶段的损失压得很漂亮模型却因为推理太慢上不了线最终还得靠量化剪枝把模型压缩回可部署的规模。所以在 Model-Optimizer 这类项目里训练的“精度优先”和推理的“效率优先”不能被割裂看待最好从一开始就把部署目标纳入设计。1.3 两个阶段的关系别把账算混训练优化和推理优化不是先后两件事那么简单它们是同一目标下的前后端配合。比如训练时使用合适的数据增广、权重衰减、随机深度等往往能产出更平滑的损失曲面和更强的冗余度间接提高后续量化的表现。反过来量化感知训练QAT也会影响训练优化器的选择因为微调阶段的优化器和初始训练阶段的优化器学习率调度策略可能需要调整。更重要的是评估指标要分开。训练阶段看 loss、accuracy、收敛曲线部署阶段看时延、吞吐、显存和精度回退。如果项目一开始没有定义清楚这两个阶段的指标后面优化就会变成无头苍蝇。很多优化失败的真实原因是把“训练不收敛”和“部署精度掉点”混在一起排查结果两头都顾不好。2. 训练阶段优化器选型与参数调优2.1 常见优化器的路数与适用场景从业者嘴里经常念叨的 SGD、Adam、AdamW其实各自代表了不同的更新公式和设计哲学。SGDMomentum 是经典选择。它的更新步长只由当前梯度和动量方向决定不带逐参数的自适应缩放所以它对学习率非常敏感但泛化能力通常更好。实践经验里很多视觉分类任务用 SGDMomentum 配一个合理的初始学习率和 weight decay效果就是比 Adam 稳。SGD 的另一个特点是需要手工调长时间的学习率 schedule才能把精度磨出来。Adam 系列则对梯度进行一阶矩和二阶矩的平滑估计相当于给每个参数分配独立的学习率习惯了之后确实省心。SGD 要求你精细调学习率Adam 则要求你控制好 beta、epsilon 和 weight decay否则容易出现训练初期震荡或后期损失不降的情况。大模型和 Transformer 结构的项目里AdamW 是主流选择因为它在做权重衰减时更符合 Adam 的更新逻辑。RAdam 在 Adam 基础上修掉了训练初期因二阶矩方差过大带来的不稳定问题。LAMB 在 Adam 之上给每个 layer 的学习率做归一化适合大批量分布式训练。Lion 相比 AdamW 省一半显存因为只维护动量而不用二阶矩但调节参数的方式不太一样。Adafactor 进一步节省优化器状态内存适合超长文本或大模型微调。选型逻辑可以简单归纳为数据规模较小、模型比较敏感时优先 SGD 或 SGDMomentum模型结构复杂、梯度噪声大或者不想花太多时间调参用 Adam 系做大规模并行训练或长序列大模型优先 LAMB、Adafactor显存就是不够Lion 或 Adafactor 会更友好。优化器内存开销典型场景主要风险SGDMomentum低CNN、中小模型对学习率敏感难调Adam中通用场景、RNN/Transformer泛化可能偏差AdamW中Transformer、预训练微调参数多需谨慎RAdam中训练初期需求稳定的场景和 Adam 差异有限LAMB中大 batch 预训练大规模超参复杂Lion低大规模模型训练符号函数风格特殊Adafactor低长文本训练收敛偏慢2.2 关键参数怎么调学习率、weight decay、momentum学习率是模型能否收敛的最直接影响因素。用 SGD 做视觉任务时我习惯把初始学习率和 batch size 挂钩比如 batch size 翻倍学习率也按比例放大这个规律在卷积网络上通常成立。而用 Adam 类优化器时初始学习率一般从 1e-4 到 3e-4 之间起跳太大容易震荡太小收敛慢。batch size 的调整对 Adam 类的学习率放大不像 SGD 那么直接盲目照搬会导致 loss 曲线失控。weight decay 是另一个容易踩坑的地方。很多文章把 Adam 里的 weight decay 和 L2 正则化当成同一个东西实际上在 Adam 中直接加 L2 正则其效果会被自适应学习率扭曲导致权重衰减量级不稳定。AdamW 的价值正在于把 weight decay 从梯度里拆出来让衰减不参与自适应学习率的计算。momentum 一般取 0.9 就有不错效果但对某些任务0.95 或 0.99 能带来更平滑的收敛。momentum 太大历史梯度占比过重模型对方向变化的响应会变慢容易在最优解附近来回振荡。除了这些基础参数warmup 和 lr schedule 也对结果影响巨大。如果是 Transformer 类模型从头训练或微调都需要一个相对温和的 warmup通常在 总步数的 5% 到 10% 之间。CNN 任务可能只需要先跑几个 epoch 的低学习率再进入主训练阶段。这个步骤做不好模型很容易在早期大步长下直接飞到损失悬崖上后面再怎么降学习率都救不回来。2.3 实操中的选型逻辑与坑点我经手过一个文本分类项目起初用的是 Adam训练到一半发现验证集一直上不去损失在后期也降不动。换成 AdamW 并把 weight decay 从 0.01 调到 0.1 后收敛曲线明显平稳最终精度也提升了不少。这件事让我意识到很多人只关心学习率而不重视 weight decay实际上 weight decay 对防止过拟合和稳定训练都很关键。还有一次做视觉任务同事直接在 ResNet 上用了带默认参的 Adam结果精度始终比不过 SGDMomentum 的基线。这里的差距不在代码而在优化器和数据结构的匹配度。卷积网络往往有大量重复结构和较小的可学习参数量SGD 的平滑更新能更好地探索泛化解而 Adam 的自适应策略容易过早进入锐利极小值对泛化不利。训练 Transformer 大模型时我一般优先选 AdamW配合梯度裁剪和混合精度效果比较稳定。使用混合精度时还需要留意梯度裁减和 loss scaling 操作的顺序不能搞反否则容易触发 NaN。另一个坑是优化器状态本身就占显存。Adam 需要对每个参数保存一阶矩和二阶矩两个状态显存占用比 SGD 高不少。如果显存实在吃紧可以换 Adafactor、Lion或者用 8-bit 优化器这些方案在实践中都能显著降低显存压力代价可能是训练速度或收敛曲线的微小变化。3. 推理阶段优化实践量化、剪枝与蒸馏3.1 量化用精度换速度是否值得量化是把模型权重和激活值从 FP32 压缩到更低比特表示的过程。最常见的做法是量化到 INT8理论上能把模型体积和推理时的计算开销降到原来的四分之一左右但对精度的影响需要仔细评估。先明确一个常见误区动态量化和静态量化不是同一个东西。动态量化在推理时逐层计算激活的缩放因子不需要校准数据但对速度提升有限。静态量化则需要在量化前用一批有代表性的数据做校准预先统计激活值的分布推理时直接使用固定缩放因子性能通常更好只是流程复杂一些。具体到项目落地我建议按这个顺序尝试先做 FP16 或 BF16如果精度没问题就上线再尝试动态量化适合 CPU 部署场景最后才考虑静态量化和 QAT。FP16 对大多数任务影响极小是性价比最高的方案。BF16 的动态范围大适合大模型推理。在 PyTorch 中动态量化通常只有几行代码import torch model load_fp32_model() model.eval() quantized_model torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.LSTM}, dtypetorch.qint8 ) # 保存量化模型 torch.save(quantized_model.state_dict(), model_dynamic_int8.pt)这段代码把 Linear 和 LSTM 层量化到 INT8适合 NLP 模型在 CPU 端做推理优化。如果是卷积网络我一般用静态量化并配合 BN 融合和代表性校准数据集。静态量化的掉点情况通常和校准数据的覆盖度高度相关校准集不能太小也不能和线上数据分布差异太大。使用静态量化时的关键步骤是设置好观测器。PyTorch 中可以用torch.quantization.prepare插入观测器校准完成后再用torch.quantization.convert转换成量化模型中间的数据批次数量也需要经验判断。500 到 1000 个样本通常够用但若模型输出特别敏感还需要增加样本量或改用 per-channel 量化。3.2 剪枝与稀疏化删掉冗余参数剪枝的思路是所有权重并非同等重要把接近零的权重直接删掉或者把不重要的通道从结构上移除可以有效压缩模型而不至于让性能断崖下跌。剪枝分结构化剪枝和非结构化剪枝。非结构化剪枝是细粒度地把某些权重置零比如用阈值检测出绝对值很小的权重直接去掉。这种做法的好处是保留原有模型精度容易但稀疏矩阵在普通硬件上不一定能获得加速需要专门的稀疏计算库支持。结构化剪枝则按通道、行或整层剪掉权重保持了规则的稠密矩阵运算部署时可获得明显加速但剪枝力度过大容易掉点。实操上更常用的做法是先训练好大模型再使用迭代式剪枝。PyTorch 提供的torch.nn.utils.prune模块可以方便地做局部或全局剪枝import torch.nn.utils.prune as prune # 对某个需要剪枝的层进行 L1 标准剪枝 prune.l1_unstructured(model.fc, nameweight, amount0.3) # 查看剪枝后的权重 prune.remove(model.fc, weight)剪枝率过大会导致模型能力受损所以一般要配合微调步骤把剪枝后的模型放到训练集上做恢复训练。不是一次性剪到目标比例而是小步剪枝、反复微调的效果更好。每次剪一定比例比如 10% 或 20%然后微调收敛再继续剪。这样操作下来项目里的模型在保留了绝大部分精度的同时推理速度提升明显。3.3 知识蒸馏小模型的弯道超车知识蒸馏是用一个已经训练好的大模型去引导一个小模型的学习过程。大模型在输出预测时会给出类别间的置信度分布这里往往包含了比硬标签更丰富的信息比如哪些类别容易混淆哪些特征更重要。实现蒸馏最常用的方式是计算大模型和小模型的预测分布之间的 KL 散度损失再加上小模型和真实标签之间的交叉熵损失。为了让大模型输出的分布更有信息量可以在 logits 上除以一个温度系数 T。T 越大输出的概率分布越平滑但学习难度也更高。实践中 T 一般取 4 到 6 之间这是一个非常常用的经验值。一次完整的蒸馏流程可以简单拆解成先训练或拿到一个精度较高的教师模型再准备一份由教师模型产出的软标签数据集接着用小模型去学这个软标签学习时同时保留硬标签监督。硬标签和软标签的权重分配也很关键一般硬标签的损失占比略高或者两者各占一半需要根据数据情况做微调。在我的一个图像分类项目里教师模型 ResNet-50 达到了 96% 的准确率学生模型只用了 ResNet-18 的结构通过蒸馏训练后准确率仍然达到了 94.5%模型体积缩小了近一半。这样的收益在纯压缩手段里很难保持。在实际项目中蒸馏往往和量化、剪枝组合使用。比如先对模型做蒸馏再对剪刀后的小模型做量化每一步的精度损失相互叠加最后只要控制在几个点以内就都是可以接受的范围。4. 工具链与配置参考4.1 训练阶段PyTorch 侧的常规配置现在的深度学习训练几乎离不开 PyTorch 这类框架。写训练脚本时优化器配置往往长这样from torch.optim import AdamW from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR optimizer AdamW(model.parameters(), lr3e-5, betas(0.9, 0.999), eps1e-8, weight_decay0.01) scheduler CosineAnnealingLR(optimizer, T_maxtotal_steps)这里有个容易忽略的细节weight_decay0.01只是起点。在部分任务上把它提高到 0.1 反而更好。如果你想快速找到合适的区间可以按 0.01、0.05、0.1 三档做小规模消融实验。使用学习率调度时需要记住它是在每个 step 更新不是每个 epoch。如果你的训练脚本里step()调用位置不对学习率曲线会完全偏离预期。这一点在长训练任务里表现尤其明显损失在后期降不下来时我第一个检查的就是 scheduler 有没有按正确频率更新。混合精度训练是显存不够时的常规解法。torch.cuda.amp使用自动混合精度把部分算子转成 FP16 执行同时用 GradScaler 动态调节缩放因子避免梯度下溢。需要在反向传播前后配对使用scaler.scale(loss)和scaler.step(optimizer)并且调用scaler.update()更新缩放因子。4.2 部署阶段ONNX Runtime 与 TensorRT模型训练收敛后下一站通常是导出 ONNX再注入 ONNX Runtime 或 TensorRT 做推理优化。ONNX 是一个中间表示格式能把 PyTorch 模型转换成跨框架的图结构然后用各家的运行时引擎做图优化和算子融合。ONNX Runtime 的量化接口比较友好支持动态量化和静态量化。如果目标设备是 NVIDIA GPUTensorRT 能发挥更强的算力优势特别是在 INT8 推理场景TensorRT 的校准过程和算子选择都会影响最终性能。在使用 TensorRT 时一个比较重要的经验是尽量减少动态 shape 的开关固定 batch size 会让 TensorRT 做更激进的优化。如果必须动态 batch需要仔细设置优化 profile 的边界范围否则会明显损失推理性能。ONNX 导出时有一个常见的坑模型里如果有静态的 Python 控制流或某些 tensor 操作在推理时就可能导致导出失败或产生错误的计算图。最好的做法是导出前先确认整个模型推理路径都只涉及常量张量形状和标准算子必要时换用torch.jit.trace做导出。4.3 优化效果的评估与验证优化效果不能只靠精度曲线判断。推理阶段的优化必须关注时延、吞吐量、显存占用、模型体积这几个维度。我一般会做一个对照表把优化前后各阶段的关键指标列出来才能看清楚收益到底来自哪里。精度回退的评估也很讲究。不能只看总体准确率还要分门别类地看。某些尾类别的召回率可能在量化后暴跌而这在总准确率上几乎看不出异常。部署前我会专门留出一批困难样本做回归测试确保它们在优化后的输出依然可靠。更严格的做法是引入可接受的精度阈值。比如要求优化后模型相比原始模型在验证集上 Top-1 准确率下降不超过 1 个百分点。有了明确阈值优化工作才不会陷入“为了压缩而压缩”的误区。5. 常见问题与排查技巧实录5.1 训练不收敛、loss 为 NaN训练时遇到 NaN不用怀疑第一反应该是对优化过程做系统性排查。首先检查输入数据。数据里有 NaN 或者异常的大数值会让梯度瞬间爆炸。这种情况最常见的来源是文本 padding 后的非常规数值或图像像素值没有归一化。其次检查学习率。学习率过大是梯度爆炸的直接原因可以尝试把初始学习率降到当前数值的十分之一看损失曲线是否恢复稳定。再次是梯度裁剪。Transformer 类模型几乎总是需要梯度裁剪通常设定为 max_norm1.0 到 5.0 之间。在 PyTorch 里用torch.nn.utils.clip_grad_norm_即可。最后检查混合精度的 loss scaling 是否配置正确。torch.cuda.amp.GradScaler如果长期不更新或者在不该打开的地方强行使用会导致梯度下溢成 NaN。关闭混合精度后如果训练恢复正常问题就出在缩放策略上。5.2 量化后精度掉点严重量化后掉点是最常见的问题。很多人以为 INT8 只是简单地把大权重改成小权重其实量化带来的误差来源非常多。第一个检查点是校准数据集。校准集不能随便用训练集的一小部分必须和真实线上数据分布一致。如果原来模型在部署时遇到的数据与校准集差异过大量化后掉点会非常明显。第二个检查点是是否做了 BN 融合。卷积层后面通常跟着 BN 层如果量化的实现没有把 BN 的均值方差折算进卷积权重量化误差会被放大。在动手量化前先torch.quantization.fuse_modules融合这些层能解决很大一部分精度损失。第三个检查点是 per-channel 量化。对卷积权重使用 per-channel 量化而非 per-tensor通常能减少分布不均带来的误差。虽然个别硬件对 per-channel 支持不友好但精度收益往往更值得。如果都试过了还是掉点那就到了 QAT 阶段。用带伪量化的模型在训练集上做若干 epoch 微调让模型适应量化噪声。这时候优化器一般用 SGD 或 AdamW学习率要比正常训练小一个量级。5.3 显存不足与梯度累积显存不足是工程中经常被卡住的地方。优先考虑三个方向减小 batch size、开启混合精度、使用梯度累积。梯度累积的思路是把多个小 batch 的梯度先累计起来再执行一次优化器更新等效于一个更大的 batch size。这里的一个教训是如果数据集的 BN 统计需要真实的大 batch梯度累积不一定能完全替代大 batch 的效果。另外做梯度累积时必须正确缩放 loss通常把 loss 除以累积步数否则梯度会整体偏大导致训练不稳定。把torch.cuda.amp和梯度累积配合使用时要注意 GradScaler 的调用方式。如果每一个小 batch 都执行scaler.step()但优化器不更新缩放因子可能不会按预期工作。合理的做法是先累积梯度在所有累积步骤完成后统一 scale、step、update。当显存还是不够还可以用梯度检查点技术。它用一部分额外的计算量换取显存占用的大量节约。对 Transformer 这样层数较深的模型梯度检查点带来的收益非常可观。5.4 优化器状态过量导致的显存开销很多人忽略优化器状态在显存中的占比。训练一个大模型时AdamW 的优化器状态可能比模型权重本身占的显存还多。这在显存不够时是一个非常隐蔽的坑。减少优化器状态占用的方案不只有换优化器这一条路。比如使用 Adafactor本来是 Transformer 预训练常用的选择但在中小规模任务上也能带来明显减负。另一个方向是使用 8-bit 优化器这类库能把优化器的状态用低精度存储显存降幅明显精度影响较小。如果既不想换优化器又不想装额外依赖可以考虑 ZeRO 等分布式优化策略。通过把优化器状态拆到多个设备上每个设备只保存一部分状态也能在设备间均摊显存压力。一点收尾的体会做模型优化的过程中我被问次数最多的问题是“哪个优化器最好”但其实每个优化器都只是一把工具。真正有价值的是对自己任务的数据规模、模型结构、部署硬件的理解。模型优化从来不是一步到位的开关而是一遍遍对比实验、定位瓶颈、调整策略后的累积结果。最后一句话优化器、量化、剪枝这些手段都很好但在动手之前先把评价指标定义清楚把全流程的数据分布摸透你会发现优化这件事远比自己想象中更可控。