ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

模型优化全流程:从训练侧优化器选型到推理侧量化剪枝实战

模型优化全流程:从训练侧优化器选型到推理侧量化剪枝实战 Model-Optimizer这个名字最早是我在公司内部群里起的工程代号。当时我们团队面临一个特别典型的困境训练那边觉得模型效果不好就调优化器、调学习率部署那边觉得模型跑得慢就提量化、提剪枝两边都在做模型优化但互相听不懂对方在说什么。Model-Optimizer这个内部项目就是想把这套流程收拢成一套可复用的实践方法。这篇文章会把它沉淀下来的原理拆解、工具使用思路和踩坑记录完整整理一遍适合那些既要管训练收敛又要管推理性能的算法工程师也适合刚接触模型压缩、还在纠结该从哪个环节入手的读者。1. 先厘清Model-Optimizer到底在优化什么1.1 一个名字两个战场很多第一次看到Model-Optimizer的人都以为它只是个优化器工具包类似timm里那一堆SGD、Adam、AdamW的封装。实际上在我这里Model-Optimizer承担的任务要宽得多——它同时覆盖了训练侧的优化器配置和推理侧的模型轻量化。这两个战场经常被混为一谈但本质完全不同。训练侧优化器解决的是模型能不能更好地收敛的问题它的作用发生在反向传播阶段通过改变参数更新的方向和步长来寻找损失函数的最优解推理侧优化解决的是模型部署之后能不能跑得更快、更省资源的问题它的对象是已经训练好的模型通过压缩参数量、降低数值精度、简化计算图等手段来加速推理。从工程角度看缺了任何一边都不完整。我见过太多团队在推理侧花了两周做量化结果精度掉得没法上线最后发现是训练侧用了不合适的优化器导致模型本身分布就很差也见过训练侧把模型精度调得漂漂亮亮部署时延迟超标只能回头重新设计网络结构。Model-Optimizer的初衷就是把这两条线统一到一个工作流里让优化这件事从训练一路管到上线。1.2 业务中最常被提到的四类优化诉求结合我们团队服务过的十几个业务场景Model-Optimizer解决的诉求基本可以划分为四类业务诉求典型症状首选优化手段训练不收敛或收敛太慢loss震荡、val精度上不去优化器选型、学习率调度、warmup显存爆掉或算力吃紧OOM、batch size只能设很小混合精度、梯度累积、激活重计算推理延迟高、吞吐低线上QPS不达标、用户等待感明显量化、剪枝、算子融合、蒸馏模型精度差但又想换轻量结构小模型欠拟合、大模型太重知识蒸馏、结构重参数化这四类诉求不是孤立的。比如显存受限的模型训练时batch size被迫缩小导致BN统计量不稳定最终精度也受影响推理延迟高的模型如果直接上INT8量化又可能因为精度损失触发回滚。Model-Optimizer在工作流设计上坚持一个原则先诊断、再对症、后回归验证任何优化操作都必须有可量化的收益证明不允许先改了再说。2. 训练侧核心优化器的工作原理与选型逻辑2.1 优化器本质上是怎么下山的策略想搞懂优化器先忘掉复杂的公式想象一下你站在一片山谷里要摸黑走到最低点。最朴素的策略是每走一步都顺着当前脚下最陡的方向迈一步步子大小固定这就是最原始的梯度下降对应公式是theta theta - lr * grad这个策略的问题是如果lr设得太大你会在山谷两侧反复横跳永远到不了底部如果设得太小你得走一万步才能下山。更麻烦的是山谷的地形不是均匀的——有的地方陡峭有的地方平缓有的地方还是布满碎石的上坡。固定步长根本应付不了这种复杂地形。动量Momentum思路就像给下山的人装了一个惯性如果之前一直在往同一个方向走那就让这个方向的力量累积起来遇到小坑时靠着惯性直接冲过去。数学上是维护一个速度变量v每一步把当前梯度累加进去v mu * v grad theta theta - lr * v自适应学习率思路则更进一步——它给每一段路单独配步长陡峭的地方步子小一点防止横跳平缓的地方步子大一点提高效率。这对应的是Adam这类优化器的核心贡献它分别为每个参数维护了梯度的一阶矩动量和二阶矩梯度平方的滑动平均再对二者做归一化所以即使某个维度的梯度量级很大更新步长也不会失控。2.2 主流优化器的性格差异与适用场景不同优化器的性格差别很大我在Model-Optimizer里维护了一张选型对照表靠着它基本不会选错方向优化器更新思路典型学习率擅长场景主要缺点SGD Momentum全局统一步长 动量冲量0.01-0.1CV卷积网络、训练周期长的任务对lr敏感、收敛速度慢Adam每个参数自适应步长1e-4-3e-4NLP Transformer、复杂结构泛化性有时偏弱AdamWAdam 解耦权重衰减1e-4-3e-4大模型、预训练微调相比SGD仍需更谨慎调参LAMB分层自适应学习率1e-3-3e-3超大batch分布式训练实现复杂、调试成本高Lion符号化更新sign3e-4-1e-3大模型训练、内存受限场景特性研究还不够充分我通常会给新人一个非常朴素的建议如果做的是图像分类、检测这类任务且训练数据量够大SGD Momentum 依然是稳定性最高的选择就是收敛慢一点要有点耐心如果是NLP、Transformer结构、生成模型这一类直接AdamW起步不要浪费时间在普通Adam上——AdamL2正则和权重衰减耦合在一起在大模型场景下解耦后的AdamW明显更可控。2.3 为什么AdamW成了大模型时代的事实标准AdamW和Adam在实现上的区别很小普通Adam在做L2正则化时权重衰减项会混入梯度的滑动平均计算导致正则效果被自适应学习率稀释AdamW把权重衰减单独拿出来在参数更新时直接减去weight_decay * theta不再经过一阶矩和二阶矩的归一化。这个解耦带来的实际收益是正则强度变得可预测。在预训练大模型时我们往往需要精细控制参数范数防止某些层过拟合如果用普通Adam同一个weight_decay在不同的梯度量级下实际正则力度完全不同调起来特别痛苦。AdamW把这一项独立之后weight_decay0.01基本就是0.01的效果不会因为梯度爆炸或衰减而隐形失效。从工程角度看AdamW的另一个隐性优势是它对学习率不那么敏感。我实测在GPT风格的模型上lr从1e-4调到5e-5训练曲线依然稳定换SGD早就发散好几轮了。这也解释了为什么HuggingFace的Trainer默认配置几乎是清一色的AdamW——它把选择成本降到了最低。2.4 Model-Optimizer里的优化器配置模板训练侧的沉淀不能只停留在用AdamW这句话上我在Model-Optimizer里把它固化成了配置模板下面是微调BERT类模型的经验配置optimizer: name: adamw lr: 2e-5 betas: [0.9, 0.999] eps: 1e-8 weight_decay: 0.01 schedule: warmup_ratio: 0.1 decay_type: cosine training: grad_clip: 1.0 fp16: true这里有几个关键点值得展开。一是warmup前10%的step让学习率从零线性爬升到目标值避免模型在训练初期用大梯度冲击预训练权重这在NLP微调里几乎是必需的二是grad_clip把梯度范数裁到1.0可以防止个别异常batch把参数更新带飞三是weight_decay通常只作用于权重矩阵和偏置之外的参数LayerNorm的gamma、beta一般要在配置里排除掉因为它们本身是归一化参数不该被正则约束。提示当你的loss长时间稳定但val指标纹丝不动时先别急着换模型把lr下降一个数量级、warmup比例提高到0.2往往就能见效。这是我在Model-Optimizer里第一个被验证有效的保守调参法。3. 推理侧优化剪枝、量化、蒸馏的组合战术3.1 剪枝到底剪掉什么怎么剪才划算剪枝的目标是去掉模型里不重要的参数让它变轻。但不重要的参数怎么定义直接决定了剪枝的收益和代价。按粒度分剪枝主要有两类。非结构化剪枝会把权重矩阵里绝对值接近零的单个元素直接置零然后以稀疏矩阵的方式存储理论上压缩比很高但问题在于大多数硬件和推理引擎对稀疏矩阵的支持非常有限CPU上要走CSR等稀疏存储格式GPU上要依赖专门优化过的稀疏算子否则模型文件变小了实际推理速度几乎没变化。结构化为Channel/Filter剪枝则是整行整列地删掉某些通道模型变成更窄的网络FLOPs确实降下来了推理引擎也能直接受益但大幅剪枝之后精度通常明显下降需要重新微调整个周期可能比训练一个新模型还长。我在Model-Optimizer里对剪枝这件事的态度是先确认你的硬件和推理库到底支持什么。如果你的目标是减小模型文件体积非结构化剪枝配熵编码可能够用如果你的目标是降低线上延迟结构化剪枝在CNN上才值得做Transformer结构里Attention和FFN的剪枝收益则要复杂得多——FFN的中间维度往往存在大量冗余但Attention层对剪枝极其敏感剪多了精度的代价立刻显现。3.2 量化从FP32到INT8收益和代价都在哪量化是目前工业界性价比最高的推理加速手段。它在模型运行之前把权重和激活值从FP32变成低精度表示比如INT8推理时用专门的整数算子计算显存占用和内存带宽消耗同时下降延迟也跟着下降。FP16/FP32混合精度训练已经是标配大家应该不陌生。真正需要做决策的是推理阶段的INT8、INT4量化方案方案精度损失额外的工程成本适用场景PTQ动态量化小低不需要校准集NLP模型、CPU部署PTQ静态量化中需要校准集CNN、固定输入尺寸模型QAT量化感知训练很小高需重新训练对精度极其敏感的小模型PTQ静态量化的关键在校准Calibration选一批有代表性的输入数据前向推理时统计每一层激活值的动态范围然后确定INT8的量化scale。这里最常见的错误是用训练集的随机样本校准——线下分布和线上真实输入差异一大激活范围统计就失真量化后精度掉得莫名其妙。我在Model-Optimizer里的做法是校准集至少取1000张来自线上日志的真实输入宁可从流式日志里现采也不偷懒用现成的train loader。注意LayerNorm、BatchNorm这一类有归一化语义的层以及最后的分类头通常不适合做INT8量化实践中需要单独配置为保留FP32。很多团队量化掉点其实是把这几个敏感层一刀切量掉了。3.3 蒸馏让轻量模型接住大模型的软知识知识蒸馏的核心思想是纯粹用硬标签(0/1)训练一个轻量模型它只能学到苹果是苹果、梨是梨这种离散结论如果用大模型的soft label——比如对某个输入大模型认为P(苹果)0.7、P(梨)0.28、P(橘子)0.02——轻量模型就能额外学到苹果和梨比苹果和橘子更相近这种类间关系。实现上通常是把大模型的logits除以温度T之后做softmax得到软化的概率分布温度越高分布越平滑隐藏的类间关系越能被暴露出来。蒸馏loss由两部分组成硬标签的交叉熵让模型分类正确加上软标签的KL散度让模型逼近大模型的判断。我在蒸馏实践中最常用的配置是# 假设 student_logits 和 teacher_logits 都是 [batch, num_classes] temperature 4.0 alpha 0.7 soft_teacher torch.softmax(teacher_logits / temperature, dim-1) soft_student torch.log_softmax(student_logits / temperature, dim-1) kd_loss torch.nn.functional.kl_div(soft_student, soft_teacher, reductionbatchmean) * (temperature ** 2) ce_loss torch.nn.functional.cross_entropy(student_logits, hard_label) total_loss alpha * kd_loss (1.0 - alpha) * ce_loss温度T的经验值在4左右alpha在0.7附近。T太大soft label过于平滑小模型什么都学不到T太小soft label退化成hard label蒸馏失去意义。这个度需要用验证集实测去卡拍脑袋容易翻车下面第5章会详细讲。3.4 剪枝、量化、蒸馏的正确组合顺序Model-Optimizer推理侧流水线的推荐顺序是先蒸馏、再剪枝、最后量化。蒸馏放在最前面是因为它能够把大模型的泛化能力迁移给一个小模型小模型在精度上有了底子后面剪枝和量化才有缓冲空间。剪枝放在量化之前是因为剪枝改变的是网络结构结构变了之后量化时需要的校准统计也会变——如果先量化再剪枝剪枝后的权重分布会偏离校准时的分布就得重新校准相当于做了两遍无用功。量化放在最后是因为它默认保留网络结构不变只是在数值精度上做压缩风险最可控。一旦前面蒸馏和剪枝的收益都拿到手了量化就是在收益之上再加收益而且量化引入的精度损失可以通过微调部分缓解微调过程不受剪枝等结构变化的干扰。4. 一套可落地的完整流程从基线评估到上线验收4.1 先做基线评估搞清楚钱花在哪Model-Optimizer的流程强制要求第一步就记录基线指标否则后面所有优化动作都说不清楚收益。基线阶段一般要拿到这些数据指标采集方式基线示例模型参数量torchsummary / model size86M计算量FLOPsthop.profile8.6G单次推理延迟取P95排除异常点24.6ms显存峰值CUDA runtime抓取1560MB模型精度验证集/线上shadowF188.9%采集指标时有个细节延迟一定要区分用户感知延迟和吞吐延迟。线上服务通常有batch推理用户感知的是单条延迟吞吐能力决定的是QPS。这两个方向不同优化手段也可能相反——为了提升吞吐可以适当增大batch但单条延迟会变高。Model-Optimizer里要求两个指标分开记录不能混在一个数字里评估。4.2 瓶颈定位计算密集还是访存密集基线数据拿到后不要急着上量化或剪枝先定位瓶颈在哪类。最简单的办法把相同模型分别用FP32和FP16跑一遍推理记录延迟变化和显存带宽占用。如果FP16相对FP32几乎没有加速说明模型已经是计算密集度偏低的访存密集结构瓶颈在内存拷贝和参数搬运这时单纯往低精度走可能收益有限反而要做算子融合或减小特征图尺寸。如果FP16延迟明显下降说明计算密集量化、剪枝都会比较见效。C部署时还可以用perf看cache miss率用nsight compute看CUDA kernel的张量核心利用率。这些profiler输出对新手确实不太友好但至少抓两个数值GPU利用率和平均内存带宽利用率。前者低说明算子调度有问题后者高说明访存是瓶颈方向一下子就清楚了。4.3 分阶段优化的执行清单瓶颈定位完成后Model-Optimizer的执行清单大致是训练侧先调优化器把optimizer切换到AdamWlr按任务类型取值warmup设为10%-20%。运行一个短脉冲训练5%-10%的step观察loss曲线是否稳定下降。混合精度训练/推理先开AMP在不改变模型结构的前提下拿FP16推理的基线看延迟和显存收益。蒸馏准备如果有现成的大模型用它作为teacher蒸馏出一个同结构的student没有就跳过这一步直接做压缩。结构化剪枝用BN的gamma值或者通道幅度作为重要性指标做通道剪枝剪枝率从10%起步每次递增5%每次都重新微调几个epoch观察精度恢复程度。INT8量化收集有代表性的校准集跑PTQ静态量化针对层间精度敏感度分析决定哪些层保持FP32。回归验证完整评估精度、P95延迟、显存峰值三项指标和基线逐项对比。4.4 一个实测案例的收益表这里放一个我们在某个图像分类项目里的实测数据方便大家对各阶段收益有个直观感受阶段P95延迟显存峰值模型体积精度(F1)基线ResNet5024.6ms1560MB98MB0.889混合精度19.8ms830MB49MB0.889结构化剪枝30%16.4ms690MB42MB0.881微调恢复16.4ms690MB42MB0.887INT8量化9.1ms435MB11MB0.884可以看到剪枝和量化叠加之后延迟从24.6ms降到9.1ms降低了约63%体积从98MB压缩到11MB精度从0.889微降到0.884整体在线上完全可接受。关键在于每一步都做了微调和回归验证避免精度雪崩。提示如果优化之后精度掉得超过0.5个点且微调多个epoch都无法恢复最可能的两个原因是校准集分布和线上不一致或者剪枝率超出模型冗余度。前者换校准集后者降低剪枝率不要盲目堆训练轮数。5. 踩坑实录五类高频翻车现场与完整排查链路5.1 学习率一步到位训练曲线直接放飞某次我在微调一个生成类模型时图省事把lr直接设成1e-3理由是之前Adam 1e-3效果挺好。结果训练才跑了几百步loss曲线直接冲出合理范围在几个大数之间反复横跳board上看起来像心电图。一开始我还怀疑是数据管道有问题排查了整整半天才意识到是优化器参数被覆盖成了默认值实际生效的lr远大于预期。这个坑的教训是不同任务lr经验值差一个数量级是常态NLP微调2e-5起步CV从3e-4起步生成模型从1e-4起步然后据此往下调。更规范的排查顺序是先打印优化器参数确认生效值再查数据batch顺序是否打乱最后才动模型结构。顺序反了会浪费大量时间。5.2 AdamW替换SGD后收敛速度和泛化性能一起消失我们团队之前有个老模型训练时用的是SGDMomentumlr0.01epoch跑到第80轮左右收敛val精度0.912。后来有同事想着统一优化器配置把SGD改成了AdamW其他什么都没动。结果是收敛速度确实快了第20轮val精度就接近0.90但一直训练到100轮val精度始终卡在0.905左右上不去。排查后确认原因在于SGD配合退火式lr调度在训练后期能形成一种精细打磨效果平滑地收敛到更优的局部极小AdamW的自适应更新即使lr很小依然保持相对较大的有效步长在二维参数空间里步态偏激进无法进行同样精细的调节。这个案例告诉我们优化器不是随便能平移替换的换优化器等于换了另一种训练行为lr调度、weight decay都要跟着重调。Model-Optimizer里的规范做法是如果换了优化器必须重新跑一轮超参网格搜索不允许只改类名就上线。5.3 INT8量化后精度掉得莫名其妙背锅的其实是校准集有一个CTR预估模型PTQ量化之后AUC从0.803掉到0.789跌幅远超预期。团队第一反应是量化算法的问题换了各种校准算法percentile、mse、entropy怎么调都回不来。后来我在Model-Optimizer的日志系统里对比训练集和线上特征的分布发现特征embedding向量的模长在线上和训练集差异非常大校准集的激活范围统计完全偏了。换了5000条线上近7天的真实流量样本作为校准集后量化后AUC恢复到了0.798几乎无损。这个坑的根本原因是PTQ校准本质上是用少量样本来估计权值和激活的整体分布样本如果和真实业务分布不一致估计必然出错。从那之后我们团队把校准集必须来自线上真实分布立成了硬约束。5.4 从FLOPs看剪枝很划算GPU上实测延迟只降了10%这是一个特别打击信心的案例。我们把一个Transformer结构的模型做了FFN层的通道剪枝FLOPs从12G降到6G理论计算量减半。结果在A100上实测推理P95只从18.5ms降到16.8ms优化了个寂寞。原因在于FLOPs衡量的是计算量不直接等于延迟。GPU推理延迟还取决于内存访问带宽、算子是否融合、kernel启动开销、以及核心利用率。Transformer的FFN层在剪枝后虽然计算量小了但激活值的访存模式没变带宽占用依然很高GPU核心处于等数据的状态计算量减半自然体现不出来。之后我把优化KPI从FLOPs降低调整成了延迟和带宽实测降低所有剪枝决策都跑真机benchmark再定。5.5 蒸馏温度拍脑袋小模型学到的东西全部变形一次用6B大模型蒸馏一个小模型做意图识别参考论文把温度设成了20。结果小模型在验证集上越训越差分类结果软塌塌的什么都分不清。排查时我用temperature20计算了一组soft label发现所有类别的概率都被拉得非常均匀KL散度几乎没提供任何有效梯度信号小模型等于在拟合一个大致均匀分布当然学不到判别能力。后来把温度从20一路往下降做敏感性测试T8的时候依然偏平滑T4效果最佳T2和1.5则接近硬标签蒸馏收益不明显。从此蒸馏配置里加了一条默认规则先做温度的粗扫描[2, 4, 8, 12]用验证集精度收敛最快的那个值不要直接照搬论文的超参。5.6 排查链路总结五类翻车现场背后有一个共同点每一步优化都在局部看指标没有在全局链路里做串行验证。Model-Optimizer沉淀下来的排查顺序是先确认优化器参数实际生效值打印优化器state再谈其他再确认数据分布是否正常尤其是校准集、蒸馏teacher的输入分布然后才是模型结构层面的判断最后才动训练配置。这个顺序不能说覆盖所有问题但至少能避免模型崩了先怀疑模型这种低效路径。我自己在做了这么多轮模型优化项目之后最深的一个体会是那些看起来特别神秘的精度问题九成最后都落在数据分布和优化器配置这两个最朴素的地方上——所以排查的时候永远先从最土的地方查起。
返回列表