模型优化实战:从量化剪枝到算子融合的推理加速指南

发布时间:2026/9/30 8:24:25
模型优化实战:从量化剪枝到算子融合的推理加速指南
1. Model-Optimizer 到底解决什么问题从一次 300ms 到 85ms 的调优复盘1.1 复盘那三天我们经历了什么去年有个客户项目要把一个检测模型部署到一台只有 4 核 CPU 的工控机上。模型本身不大ResNet 风格的骨干加一个轻量检测头FP32 序列化后大约 120MB。客户给的目标很直接端到端延迟 100ms 以内。第一次压测单帧推理 300ms。这个数字离目标差得不是一点半点。我刚开始还想着调 batch size、换线程数、开多进程折腾了半天最好成绩也才 260ms。后来静下心用 profiler 看热点算子问题一下就清楚了模型里 3x3 卷积占了 65% 左右的耗时而 BN 和 ReLU 这两类“轻量算子”单独算下来也要占 10% 以上的算力再加上每次算完一个算子就要把中间结果写回内存、下一个算子再读出来光是这种张量搬运就浪费了大量带宽。我们把 ConvBNReLU 融合成一个算子省掉中间 Feature Map 的写回和读取延迟立刻降到 210ms。接着做了 INT8 量化校准集选的是客户现场拍的回流图量化后模型从 120MB 缩到 32MB单帧延迟降到 85ms。最后交付时精度掉了 0.3 个点的 mAP客户完全能接受。这套从“逐算子分析”到“算子融合”再到“量化压缩”的链路就是我们内部叫的 Model-Optimizer。它不是一个单一的脚本也不是某个框架自带的一键开关而是一套围绕模型精度、体积、延迟做取舍的工程方法。1.2 容易被混淆的概念训练优化器和模型优化器我在查资料时发现Model-Optimizer 这个词很容易被理解为深度学习训练里的优化器比如 SGD、Adam、AdamW 这些。它们确实是 optimizer但作用对象完全不同。训练优化器解决的是“梯度往哪走、走多远”的问题它只存在于训练阶段。我们这篇讨论的是部署阶段的模型优化解决的是“计算图怎么重写、权重怎么压缩、算子怎么融合”的问题。两者有关联——做量化感知训练时训练优化器的超参需要调做知识蒸馏时也要用合适的优化器去训小模型——但职责边界很清楚。判断一个任务属于哪一类可以看目标如果你的目标是降低部署延迟、减小模型体积、降低功耗那就是模型优化如果你的目标是让 loss 降得更快、收敛更稳那就是训练优化器。本文后面讲的所有内容都属于前者。1.3 优化前必须先回答的三个问题很多同学一上来就问“怎么把模型变小”我一般会反问三个问题精度指标是什么mAP、准确率、BLEU还是端到端成功率允许掉几个点性能指标是什么看 P50 还是 P99单帧延迟还是吞吐是否要求低功耗体积限制是多少芯片 Flash 只有 64MB还是服务器上完全不在乎这三个问题构成一个约束立方体。你只能在一个三角形里挑两点做优化不可能既要模型最小、又要速度最快、还要精度完全不掉。我们那次能做到 300ms 到 85ms是因为客户对精度掉 0.3 个点无所谓如果客户要求 mAP 必须保持在 0.710 以上方案就要换成混合精度量化加结构化剪枝工作量和最终延迟都会不一样。2. 量化、剪枝、蒸馏、算子融合四类优化手段的原理与组合方式2.1 量化为什么换一种数值表示能快这么多量化最简单粗暴的理解就是把模型里的 FP32 权重和激活从 32 位浮点数变成 8 位整数。纸面上看乘加计算的复杂度似乎降了 4 倍但真实加速的主要来源不是“算得快”而是“搬得少”。以 CPU 推理为例数据要从内存搬到寄存器才能计算。FP32 模型跑一遍权重和中间激活都要按 4 字节存取改成 INT8 后同一块内存能装 4 倍的数据Cache 命中率大幅提升内存带宽压力直接降到原来的 1/4。算子如果还规避了中间张量的写读收益就更明显。做 INT8 量化时有个概念要分清楚对称量化还是非对称量化per-tensor 还是 per-channel。对称量化把权重的正负范围统一映射到 [-127, 127]不需要零点实现简单但小数范围浪费多非对称量化为每个张量额外记录一个零点偏移精度通常更好。per-channel 是每个输出通道单独算一个缩放因子比 per-tensor 的全局缩放更贴合卷积权重的分布INT8 下的精度下降通常能控制在更小范围。校准也是关键一步。直接把 FP32 的 min/max 拿来用最省事但容易受离群点影响激活范围会被拉得很大。一般会用校准集跑一遍前向然后选择 percentile比如 99.99%而不是绝对的 min/max。像 TensorRT 还有基于熵的校准方法本质是在“信息损失最小”的前提下找最佳截断点。我做量化时有一条经验先看敏感层不要指望全模型一键 INT8。检测模型的回归头、分割模型的上采样层对数值误差都比普通卷积敏感得多。把敏感层保留 FP32其他层压到 INT8精度损失和压缩率往往能达到一个更理想的平衡点。2.2 剪枝别只盯着稀疏度数字看剪枝的本质是去掉不重要的权重。但很多人第一次做剪枝都会踩同一个坑用非结构化剪枝把 50% 的权重置零模型文件确实小了部署之后延迟却几乎没变化。原因在于普通推理引擎依然按稠密矩阵乘法执行。权重矩阵里有一半是 0但存储布局没有变运算也没减少。非结构化稀疏必须依赖支持稀疏张量运算的特殊算子库才能生效比如 CPU 上对稀疏矩阵按行压缩存储并按稀疏行计算。这类库在有些框架里支持度不完整直接上手很容易无功而返。更适合大多数部署场景的是结构化剪枝也就是把整个 Channel、整个 Head、整个 Block 剪掉。剪掉之后权重矩阵的 shape 真正变小了任何推理引擎都能直接受益。缺点是它会改变网络结构几乎必然需要重新训练或至少微调一段时间不然精度会掉得很难看。我做结构化剪枝时一般先看每个通道对输出的影响常用方式是在验证集上把某一通道置零观察损失变化。麻烦但比只看权重范数靠谱。剪枝比例也不是越高越好我曾经把一个检测模型的卷积通道从 64 剪到 40FLOPs 降了 40%实际延迟只降了 15%。原因很简单CPU 上的卷积实现有分块计算通道数变成 40 以后数据排布和指令流水线反而变差了。所以剪枝一定要在目标硬件上实测不能只看 FLOPs 报表。2.3 蒸馏把大模型的“暗知识”教给小模型知识蒸馏严格来说不是压缩手段而是训练手段。它解决的是小模型在结构变小之后“学不动”的问题。大模型在训练时学到的不只是 one-hot 标签里的正确类别还有类别之间的相似关系。比如一张图里有猫模型可能以 0.7 的概率预测猫0.2 预测狗0.1 预测狐狸。这些被 softmax 压得很小的概率分布其实包含大量“暗知识”。蒸馏时用温度参数 T 把分布“烫软”让小模型去拟合这个软分布比直接学真实标签更容易收敛。典型的蒸馏 loss 是三部分叠加小模型自己的交叉熵损失、小模型与大模型在温度 T 下的 KL 散度、以及两者特征层的距离约束。温度 T 一般取 2 到 5我们试过 T3 效果最稳KL 损失的权重 alpha 取 0.7 左右说明大模型的知识信号要占主导。蒸馏在优化流水线里的位置也很讲究。最稳的做法是先做结构化剪枝再用蒸馏把被剪坏的精度补回来最后做量化感知训练。如果先量化再蒸馏量化误差会随着蒸馏过程一起被放大后面再想调回来就麻烦了。2.4 算子融合与图优化编译层送的白菜收益算子融合是收益最高、风险最低的一类优化因为它不改变数值精度只是改写计算图。最常见的融合模式是 ConvBNReLU。推理时 BN 的均值和方差已经固定卷积和 BN 可以合并成一个带偏置的卷积再接 ReLU。原来需要三个算子和两次中间张量落盘现在一次卷积就完成了中间 Feature Map 直接从寄存器或 L1 Cache 传给激活函数不再经过内存。Transformer 模型里也有大量可融合的模式比如 LayerNorm 和后面的 QKV 线性层、GELU 和后面的 Add、Attention 里的 Softmax(QK^T)V。GNMT、BERT 这类结构融合收益通常比纯 CNN 更明显。图优化还包括常量折叠、公共子表达式消除、死代码消除。一句话总结让计算图更短、中间张量更少、每个节点的计算更密实。这一步在任何硬件上都安全所以无论走哪条优化路线我都建议先做算子融合。2.5 组合策略先剪枝、后蒸馏、再量化很多刚接触优化的同学会想量化、剪枝、蒸馏能上全上是不是效果最好我劝你冷静。这些手段的收益不是简单相加。剪枝改变网络结构需要蒸馏和重训练去恢复精度量化引入数值误差某些层会放大剪枝造成的损失蒸馏又依赖一个质量够好的大模型作为教师。三者叠加时只要一个环节没对齐精度掉起来会非常快甚至不如只用一个手段。我目前的常规组合顺序是先算子融合和图优化然后结构化剪枝再知识蒸馏恢复精度最后做 INT8 量化。如果量化后精度不达标再考虑混合精度或者量化感知训练微调。每一步之间都要停一下用同一套验证集跑精度和延迟回归确认没有恶化再走下一步。这一节可以总结成一句话优化手段是工具组合策略才是工程。工具选错了可以换组合策略错了后面所有验证都白做。3. 可复用的优化工作流从基线测量到回归验证3.1 基线测量先把“优化成功”定义清楚我见过太多项目败在“没有可比较的基线”上。优化做了一半想证明有效结果发现前任改过输入分辨率测试时开了 8 线程机器还在跑别的任务数据根本没法对比。基线测量必须固定几样东西硬件环境锁 CPU 频率、固定核心数、关掉自动降频和睿频。模型输入分辨率、batch size、输入 dtype 必须写进文档。精度指标同一份测试集同一个评测脚本不能随机抽样。性能指标至少统计 P50、P90、P99 三种延迟别只记平均值。我们做基线时会顺手生成一张表。这张表在后面每一步优化后都要更新。比如项目数值模型格式ONNX (opset 17)精度类型FP32输入分辨率640x640单帧延迟 (P50)300ms模型体积120MB验证集 mAP0.712后续每做一次优化就把表格里对应项改掉保存一条记录。谁再说“这次优化有效”拿表说话。3.2 剖析热点没有 profiler 的优化都是盲改基线建好之后不要急着量化。先用性能分析工具回答一个问题这 300ms 到底花在哪了CPU 上我一般先用框架自带的 profiler。ONNX Runtime 有专门的 profiling toolPyTorch 有 torch.profiler都能输出每个算子的耗时占比和调用次数。GPU 上会额外用硬件级工具看 kernel 占用率和显存搬运。拿到算子级数据后把耗时从高到低排列。注意看一个关键点这个算子是计算密集型的还是访存密集型的如果算子的浮点运算时间占比很高属于 compute-bound可以考虑算子融合、低精度运算。如果算子的内存读取时间占比很高属于 memory-bound优化的重心要放在减少中间张量、提高 Cache 命中率上。我们那次检测模型profiler 显示 3x3 卷积本身占 50%BN 和 ReLU 加起来 15%。BNReLU 不是计算量大而是每个都要把上一层的输出写回内存再读一遍。这就是典型的 memory-bound 现象所以 ConvBNReLU 融合收益特别大。3.3 单点优化一次只改一个变量优化过程最容易失控的做法是同时上多个方案量化了剪枝了还换了推理引擎最后延迟是降了 30%但你根本说不清楚哪一步贡献了 20%哪一步其实在拖后腿。我执行优化时坚持单点改动也就是一次实验只动一个变量。想证明量化有效就保持网络结构、线程数、输入分辨率不变只把 FP32 换成 INT8想证明融合有效就保持精度和模型结构不变只开图优化开关。每次实验得出结果后和基线表对比一轮。如果收益明显保留这个改动更新基线表如果收益微弱或者有副作用回滚。这个循环做好优化过程会非常干净后面出了问题排查范围也被大幅缩小。3.4 回归验证端到端才是终点很多优化在模型推理这一层看着很快整条链路一跑又打回原形。原因往往出在前后处理、数据拷贝、动态申请内存和线程切换上。所以回归测试必须做端到端。前端读图的耗时、预处理 resize 和归一化的耗时、后处理 NMS 的耗时、还有推理前后的张量拷贝全部算进去。客户要求 100ms 是整帧处理不是单纯的模型推理时间。校准集和测试集也要分开。校准集只用于确定量化参数测试集只用于精度评估两者不能混用否则精度数字虚高上线后被真实数据一冲就原形毕露。4. 落地过程中的拦路虎精度回退、算子不兼容与测量误差4.1 量化后精度掉点的排查链路INT8 量化之后精度掉得厉害大多数人第一反应是“换更大的校准集”或者“全模型保留 FP32”。这两种都属于撞运气科学的做法是沿着链路一步步查。第一步先确认校准集和训练集/测试集是否同分布。我们有一次量化后 mAP 掉了 2 个点后来发现校准集用的是白天场景的图而验证集有一半是夜间图量化参数估算的激活范围完全跑偏。换成同分布的校准集后掉点缩到 0.5 个点。第二步做逐层误差分析。把量化模型和 FP32 模型的每一层输出拉出来对比计算余弦相似度或均方误差。误差异常大的层就是敏感层。检测模型里常见的是回归头里的某些卷积层分割模型里是上采样前的最后几层。第三步把敏感层单独设成混合精度也就是这些层跑 FP16 或 FP32其余层继续 INT8。我们那次把回归头两层设成 FP32 后掉点从 0.8 缩到 0.3模型体积只多了 4MB。第四步如果混合精度还是救不回来再上量化感知训练。把伪量化节点插入模型在训练阶段模拟 INT8 的舍入误差让网络自己去适应。这个过程比较费训练资源但通常是量化精度的最终兜底方案。4.2 算子不兼容模型导出的坑比优化本身还多很多时候优化还没开始模型转换导出就把人卡住了。常见的问题有三个。第一个是 opset 版本过低。ONNX 的 opset 直接决定算子表达能力的上限旧版本里很多新算子没有对应的导出实现。我们的做法是统一固定到 opset 17遇到不支持的算子时再针对性处理。第二个是自定义算子。PyTorch 里随便写一个自定义 autograd Function导出时如果没有注册 ONNX symbolic就会直接报错或者导出成一个无法被推理引擎执行的节点。处理方式通常是把自定义算子替换成若干标准算子的组合或者单独写一个自定义 kernel 注册到推理引擎里。第三个是动态 shape。很多模型导出时输入尺寸是动态的转换到 TensorRT 或 OpenVINO 时某些算子对不同 shape 的优化策略不同会触发 fallback。能固定输入分辨率就不要用动态 shape实在要动态提前在目标引擎里配置 shape range。遇到算子不兼容我一般不会急着换引擎。先看这个算子负责什么功能能不能被拆成多个标准算子替代子图 fallback 是在前几种方案都无效时才考虑的。4.3 测量误差为什么同样一个优化有人测出 10 倍加速有人只测出 2 倍我不止一次看到同一个模型同一套优化手段不同人报告的性能提升天差地别。排除吹牛的成分绝大多数是测量方法的问题。CPU 推理最坑的是频率漂移。现代 CPU 有睿频有功耗墙还有散热降频。跑第一个 batch 时主频从 2.2GHz 飙到 4.0GHz测出来的延迟肯定好看连续跑 10 分钟后温度上来主频掉到 2.5GHz延迟直接翻倍。所以测 CPU 延迟必须先绑核、锁频再做 warmup最后取多轮中位数。具体到我常用的做法先跑 50 轮 warmup让缓存热起来、内存分配稳定下来再正式跑 200 轮去掉最高和最低的 10%取中位数作为 P50取 99% 位置作为 P99。还有一个小细节容易被忽略多线程推理时THREAD 数设得越高不一定越快。有些模型在 4 线程时缓存友好8 线程反而因为内存带宽竞争而变慢。所以每次优化前后都要把线程数这一项列为固定变量而不是“让系统自己决定”。4.4 一次剪枝蒸馏同时上马导致返工的复盘讲一个反面案例是我自己踩过的。当时为了赶上线把 channel pruning 和知识蒸馏放同一次实验里跑结果模型体积小了 30%延迟降了 20%但 mAP 掉了 3 个点。我们当时的第一反应是调蒸馏温度连续试了好几天效果都不理想。后来回头一查日志发现剪枝改变了骨干网络最后几层通道数蒸馏时教师模型的中间层特征和学生的特征空间已经对不上了特征对齐损失一直震荡学生模型学到的是扭曲的分布。把特征对齐层改到新的通道维度上再训精度才算恢复。这个案例让我形成了前面的单点优化习惯。优化手段越快看到效果时越要警惕是不是埋了看不见的隐患。5. 按部署场景选型同一个模型的优化路径可能完全不同5.1 CPU 场景内存带宽是第一约束CPU 推理的核心瓶颈通常不在算力而在内存带宽和指令集支持。所以 CPU 优化的优先级是先算子融合减少内存搬运再量化降低数据体积最后看 SIMD 指令集是否支持 INT8 加速。工具上ONNX Runtime 自带图优化和量化支持Intel 平台还可以接 OpenVINO 做后端。但注意一个细节如果 CPU 不支持 VNNI 或 AVX512 这类新指令INT8 量化后的收益可能非常有限甚至因为反量化操作额外开销导致变慢。所以做 CPU 量化前先查目标机器的指令集。线程数也要按目标机器核数去试。4 核机器用 2 到 4 线程通常有收益8 核以上超过一定线程数后收益趋于平缓过高的线程数反而引入上下文切换开销。5.2 GPU 场景TensorRT 是绕不开的选项NVIDIA GPU 上优化路径相对清晰ONNX 模型导出后转 TensorRT开 FP16再考虑 INT8。TensorRT 的图优化能力很强很多我们手动做的算子融合它转换时已经自动做了。GPU 优化的关键参数是显存占用和吞吐。FP16 能把模型体积和显存占用直接减半收益最明显INT8 则依赖校准集质量掉点风险略高。GPU 上还有一个容易被忽略的点TensorRT engine 的构建时间。同一个模型用动态 shape 构建 engine 可能要十几分钟甚至更久每次部署到新机器都要重新构建。优化时要把这部分流程自动化而不是手动跑。5.3 移动端与 NPU 场景量化和算子支持清单同样重要如果是手机、摄像头、边缘盒子这类设备NPU 或 DSP 通常负责跑模型而 NPU 对算子支持范围比 CPU 和 GPU 窄得多。最常见的坑是模型里有一个 Softmax 变体或者上采样方式不被 NPU 支持推理引擎自动把这个算子切到 CPU 上执行结果数据要在 NPU 和 CPU 之间来回拷贝性能反而比全 CPU 跑还差。这类场景的第一步就是在目标硬件上跑一遍算子支持清单把不支持或 fallback 的算子全部列出来逐个替换成硬件支持的标准算子。第二步再做 INT8 量化因为 NPU 的定点运算优势明显FP32 模型在 NPU 上往往没有速度优势。工具链一般看平台Android 生态用 TFLite 或 NNAPIiOS 用 Core ML某些厂商芯片还有自己的 SDK。这类 SDK 版本更新快建议锁定版本并做全量回归。5.4 一张优化策略决策表部署场景应该优先采用的手段常用工具链最容易踩的坑普通 CPU 服务器算子融合 INT8 量化ONNX Runtime, OpenVINOCPU 不支持新指令集量化反而不快NVIDIA GPUFP16 TensorRTTensorRT, Triton动态 shape 构建时间过长手机/边缘 NPU算子替换 INT8 量化TFLite, NNAPI, Core ML算子 fallback 导致 NPU 效率丧失内存受限设备结构化剪枝 量化自研剪枝脚本 ONNX剪枝比例脱离硬件实测只看 FLOPs这张表不是绝对标准但它能帮你在面对一个新项目时快速确定第一步往哪个方向走。方向对了后面所有努力才有意义。6. 最后说一点个人体会做模型优化三年我最深的感受是这个领域从来不缺工具缺的是对测量和回归的敬畏。量化、剪枝、蒸馏、算子融合每一个手段都值得尊敬但真正决定项目成败的往往是你能不能把基线建好、能不能一次只改一个变量、能不能在每一步之后都诚实地记录精度和性能数据。我现在接手任何优化需求第一件事永远是问三个指标精度允许掉多少、延迟要求多少、体积限制多少。这三个数字没有对齐之前我不会动任何代码。这个过程看起来很慢但恰恰是它让我避开了大多数返工。如果你也想搭一套自己的 Model-Optimizer 流程从今天开始先把基线表和 profiler 跑通这比任何花哨的压缩技巧都重要。