免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

模型剪枝与量化:从原理到落地,解决边缘端推理加速难题

模型剪枝与量化:从原理到落地,解决边缘端推理加速难题 1. 一张显卡装不下一次推理等不起为什么要做模型优化去年帮客户调一个边缘端识别模型发现模型优化这件事比想象中复杂得多。模型在训练集上精度98.7%一跑到RK3588的NPU上延迟直接300多毫秒内存也快到极限。老板开会第一句话是“要不要换块更贵的开发板”但最后真正解决问题的不是加钱买硬件而是把模型剪枝和量化老老实实做了一遍先剪枝再量化最后盯着标定精度重调一轮。这篇就当一次系统性的模型优化概述重点讲清楚两条最常见的路线——模型剪枝和模型量化各自解决什么问题、适合什么场景、实操时有哪些坑。正在做推理加速、端侧部署、服务端降本的同学应该能从这里找到一些能直接拿去用的东西。1.1 从训练到部署瓶颈到底卡在哪训练阶段我们只看一个指标能不能收敛泛化误差低不低。部署阶段完全不是一回事业务方只会问你三个问题一次推理要多久显存或者内存占多少上线之后精度掉不掉拿一个经典的ResNet-50来说ImageNet上精度约76%权重大约2500万参数FP32权重文件就有100MB。在V100上跑一次前向大约4ms看起来很轻松但放到移动端或边缘盒子上内存带宽和算力都缩水一个数量级推理延迟可能就变成几百毫秒。对视频流处理这种场景一秒钟要跑25帧每帧预算只有40ms直接上原始模型必然超时。更麻烦的是内存很多边缘设备的内存总共只有2GB到4GB操作系统和业务逻辑已经吃掉一半能分给模型推理的往往不到1GB。模型权重100MB勉强能忍激活值、中间缓冲、多路并发一起算内存很快就爆了。所以模型优化解决的不是“训练精度不够”的问题而是“模型在目标硬件上跑不起来”的问题。约束条件一般有三类延迟、内存/显存、功耗。不同业务的优先级不一样但你做优化时所有技术选型都得围绕这三个约束来定。提示动手做任何优化之前先花半天时间跑一下Profiling确认瓶颈到底是计算量、内存带宽还是显存占用。我看到过太多项目一上来就剪枝结果瓶颈是数据读入或者后处理代码白忙活。1.2 剪枝和量化解决的是两个不同维度的问题模型剪枝和模型量化经常被放在一起提但它们的原理完全不同。剪枝解决的是“参数冗余”问题。深度神经网络训练完之后大量权重其实非常接近零对最终输出的影响很小。把这些接近零的参数去掉网络结构变小计算量也随之减少。可以理解成公司里有些岗位其实长期不产生价值优化就是把他们裁掉组织变瘦但主营业务照常。量化解决的是“参数精度过剩”问题。训练时用FP32是因为反向传播对精度敏感需要精确的梯度。但推理时其实不需要每个权重都精确到小数点后好几位很多任务用INT8甚至INT4就能达到相同的结果。量化就是把浮点运算替换成整数运算每个参数从32bit缩到8bit体积直接变四分之一而且整数运算在CPU、GPU、NPU上通常都比浮点快。这两个手段并不互斥而且是乘法叠加的剪枝把计算量减半量化再把每个参数占的比特数降到四分之一最终的理论收益接近八倍。但收益大不代表容易做两者组合之后精度评估的复杂度也会翻倍这个放到后面专门讲。1.3 评估优化效果的三维框架精度、体积、速度一个都不能少很多团队做优化只盯着精度掉了多少这是不够的。我习惯用一个三维框架来判断一次优化是否成功精度、模型体积、推理速度。精度用验证集上与原模型对比看下降多少。业务上要定义“可接受阈值”比如Top-1掉点不能超过0.5%。体积模型文件大小、参数量、内存/显存占用。速度单次推理延迟、吞吐量要区分是在哪个硬件上测的。用表格整理常见优化手段在不同维度上的大致收益优化手段参数体积计算量内存占用部署难度非结构化剪枝理论降低理论降低视框架支持而定中结构化剪枝明显降低明显降低明显降低低PTQ量化INT8降到约1/4视算子而定明显降低低QAT量化INT8降到约1/4视算子而定明显降低中动态量化权重降到1/4不一定降降低低看到没不同方法在不同维度上的收益差异很大。非结构化剪枝虽然理论上FLOPs降了但如果推理框架不支持稀疏算子实际延迟可能一点没降。所以后面的章节我会把这些差异展开讲让你少走弯路。2. 模型剪枝把“不重要的连接”从网络里抽掉2.1 剪枝的本质是给每个参数排重要性剪枝不是随便删参数核心是回答两个问题哪些参数可以删删多少最关键的一步就是“重要性排序”。最简单的做法是看权重的绝对值大小——绝对值为0.001和绝对值为0.8相比前者对输出的影响通常小得多删掉它引起的结果扰动也更小。这种启发式方法很朴素但在实际项目里已经能带来不错的压缩率。在动手剪之前建议先把模型的权重分布拉出来看一眼。如果绝大多数权重集中在0附近只有少数尖峰剪枝的成功率就高反之如果权重分布比较均匀盲目剪可能大量删掉有用特征。更高级一点的做法是看权重对损失函数的影响比如近似Hessian方法或者看剪掉某个结构后损失上升多少。但这些方法计算成本高工程上用得少。我自己的经验是普通业务模型先从L1/L2范数排序开始能解决80%的问题剩下20%再上更复杂的算法。2.2 结构化剪枝与非结构化剪枝硬件加速的差异很大剪枝可以分成非结构化剪枝和结构化剪枝两者的部署收益差距非常大。非结构化剪枝直接对权重矩阵里的任意元素置零用mask来实现。优点是可以做到很高的稀疏度90%以上精度下降小缺点是在GPU/CPU上很难加速因为稀疏矩阵的存储格式和计算路径不规整需要专门的稀疏算子。除非部署框架已经高度优化了稀疏卷积否则模型文件变小了实际推理速度几乎没变。结构化剪枝则是按整行、整列、甚至整个channel/kernel来剪。比如把某个卷积层输出的32个channel剪掉一半保留16个。这样剪完后网络的形状是规整的可以真正减少计算量在标准卷积算子下就能获得速度提升。缺点是对结构的限制比较大精度下降通常比非结构化剪枝更明显需要微调来恢复。比较维度非结构化剪枝结构化剪枝剪枝粒度单个权重元素行、列、Channel、Kernel可实现稀疏度高90%中30%-50%较稳妥推理加速依赖稀疏算子支持天然适配标准算子精度恢复难度相对容易相对困难工具链支持PyTorch等训练框架支持部署端需额外支持多数部署框架天然支持选择哪个完全取决于你的部署目标。如果模型要上NPU那基本只能走结构化剪枝因为NPU的算子大多要求通道数对齐如果在GPU上用TensorRT可以考虑稀疏化方案但支持范围也有限。2.3 一次剪枝实操PyTorch的prune模块怎么用才不出错PyTorch在torch.nn.utils.prune里内置了常用的剪枝方法代码很简单但有几个坑。import torch import torch.nn.utils.prune as prune model torch.load(my_model.pth) # 假设model.features[0]是第一个Conv2d # 对weight做L1Norm非结构化剪枝剪掉30%的连接 prune.l1_unstructured( modulemodel.features[0], nameweight, amount0.3 ) # 剪枝后该module的weight变成了pruned tensor真正的权重在weight_orig # 很多推理框架不认识mask所以要调用remove把剪枝结果固化 prune.remove(model.features[0], weight)第一个坑是prune.remove这一步。如果不调用remove模型里保存的是weight_orig和weight_mask两个参数推理框架加载的时候通常不知道mask怎么处理很容易当成普通参数又完整恢复了等于白剪。调用remove之后会把mask后的权重复制到weight替换掉原来的参数这样保存下来的模型才是真正瘦身后的。第二个坑是剪枝比例不能一次设太高。l1_unstructured直接把30%权重置零不经过微调就上线掉点可能非常严重。正确的流程是剪枝、微调、再评估。微调时建议把学习率降到原来的1/10甚至1/100用较小的batch size跑几个epoch让剩余参数逐渐适应被删掉的连接。第三个坑是剪枝最好迭代进行。一次小比例多轮次剪比一次大比例剪完效果好。比如每轮剪10%微调后再剪10%目标50%就分5轮。这种“剪-训-再剪”的迭代式剪枝我实测在多个分类模型上比一次性剪50%能少掉1到2个点的精度。2.4 为什么“训练时剪枝”比“训练后再剪枝”更稳如果你读过一些剪枝方向的经典工作会发现核心观点是剪枝应该嵌入训练过程而不是训练完再一刀切。原因很简单网络在训练中会不断调整各通道之间的协作关系你提前告诉它“某些通道以后会被删掉”它会主动把重要信息分布到保留下来的通道上等训练完成再看权重绝对值来剪某些通道可能早就承担了关键特征但权重绝对值并不大直接剪掉损失就大了。工程上我常用三阶段流程先正常预训练一个模型然后在保留原模型参数的前提下用带稀疏约束的训练方式微调比如给权重加L1正则让尽量多的权重接近零最后再修剪并微调恢复精度。这样比“预训练→剪枝→微调”多了一步“稀疏化预训练”但最终精度通常能高不少。代价是训练时间变长了所以要不要上这个流程取决于你对精度恢复的焦虑程度。3. 模型量化把浮点计算换成整数计算赚回的不只是体积3.1 量化到底改变了什么从FP32到INT8的信息损失量化简单说就是把连续取值的浮点数映射到离散的整数。训练好的模型里权重和激活值基本都在某个范围比如-6.0到6.0。我们找到这个范围用一个scale值和一个zero_point把浮点数映射成INT8的0到255。计算公式是[ q \text{round}\left(\frac{r}{\text{scale}}\right) \text{zero_point} ]反量化就是[ r (q - \text{zero_point}) \times \text{scale} ]这里的scale决定了一个整数步长代表多大的浮点数变化zero_point负责对齐浮点范围里的零点和整数范围里的零点。范围选得好不好直接决定量化误差。FP32有约7位有效十进制数字INT8只有约2到3位。所以量化本质上就是用精度换体积和速度。通常来说权重和激活值量化成INT8后分类模型精度掉0.5%到1%以内是正常的但如果掉得更多就要检查量化范围和校准数据了。3.2 PTQ、QAT、动态量化三种量化方式的选型逻辑模型量化主要有三条路线我按实际选型的优先级来说。动态量化Dynamic Quantization只把权重离线量化为INT8激活值在推理时动态量化。实现最简单适合LSTM、Transformer这类输入长度动态变化的模型。但这种模式下激活的量化开销发生在运行时速度提升有限。静态量化Post-Training Quantization, PTQ把权重和激活都离线量化好。这需要你准备一批有代表性的校准数据前向跑一遍模型统计每一层激活值的范围然后算出scale和zero_point。PTQ不需要重训模型周期短是目前工程落地最常用的方式。ONNX Runtime的INT8量化、TensorRT的INT8校准都属于这类。量化感知训练Quantization-Aware Training, QAT在训练过程中模拟量化的舍入误差让模型逐步适应低比特表示。QAT精度最好但需要重新训练成本最高。只有当PTQ掉点超过业务容忍范围时才值得上。三种方式的关键差异整理成表格方式精度保持实现成本部署额外要求适用场景动态量化较好低低权重敏感、输入长度动态变化的模型PTQ静态量化中等较低需要校准集绝大多数CNN/Transformer部署QAT最好高训练流程改造精度敏感、PTQ掉点严重的模型3.3 校准数据集与量化范围为什么“数值不动”、精度崩掉PTQ最容易踩的坑是校准数据集没选好。校准集的作用是统计每一层激活值的真实分布如果校准数据和生产数据的分布差得远算出来的scale就是错的。举个实际例子。某个模型某一层激活值绝大多数在-1到1之间但校准集里恰好有几张极端图片让这层激活值跑到了50以上。如果直接按min-50、max50去设量化范围那-1到1之间的数值全部被压缩到INT8的一小段区间里整数刻度一映射输出就跟“数值不动”一样——不是真的没动是被量化范围压平了。这类问题的排查思路是先打印敏感层的激活值分布看min/max和直方图再用百分位截断代替纯min/max。TensorRT、ONNX Runtime的校准器里都有percentile选项设置为99.99%或99.999%把极端离群值丢掉量化范围会合理很多。注意如果你的模型量化后输出几乎变成常数优先怀疑量化范围设置不要急着改网络结构。把校准集换成500到1000张覆盖不同光照、角度、类别的样本八成问题能解决。3.4 LLM场景下的4bit量化和传统INT8不一样前面说的主要是CNN和Transformer的INT8量化。现在大语言模型LLM部署越来越普遍4bit量化是另一个重要方向。LLM推理的主要瓶颈在于内存带宽因为Decoder要反复读取权重权重占多大直接决定单卡能不能装下模型也决定每秒能生成多少个token。常见的LLM量化方法是GPTQ、AWQ以及GGUF格式里的4bit方案。它们和传统INT8量化的关键区别是为了应对LLM权重里明显的“离群值”它们按block计算scale而不是整层共享一个scale。比如把权重矩阵切成每128个一组的小块每个块单独算scale离群值被单独保护精度损失就小很多。实际部署时很多项目会用“W4A16”这样的表示权重4bit量化激活保持FP16。这样既把模型体积降到了原来的八分之一左右又避免激活量化带来的精度风险是“性价比”很高的配置。如果连激活都想压到8bit对精度模型来说训练成本会显著提高。4. 剪枝和量化如何组合先剪后量化的工程路线与反例4.1 先剪枝再量化还是先量化再剪枝我的结论是常规项目先剪枝、微调再量化、校准。原因是剪枝和量化都会引入误差分步做可以定位每一步的损失来源出了问题也更容易回滚。如果先把模型量化成INT8再做结构化剪枝会出现一个麻烦量化后的整数权重做重要性排序受scale和zero_point影响排序出来的方向和浮点权重可能不一样而且剪枝之后原来算好的量化范围又变了需要重新校准。等于做了两遍无用功。推荐的流水线是大模型预训练/微调 → 迭代式剪枝剪→微调 → 剪枝后的模型做PTQ量化校准集合理选择 → 用验证集对比精度与速度 → 如果精度损失大再考虑QAT或混合精度。这个顺序可以让每一步的效果都可视化出了问题也能回退到上一个阶段。4.2 每一步的收益怎么量化建立一张跟踪表模型优化最怕你凭感觉判断“好像快了一点”。一定要在每一步之后跑同一套评测脚本记录四个维度模型大小、精度、延迟、吞吐。阶段模型大小验证集Top-1平均延迟吞吐原始FP32100MB92.3%45ms22 fps剪枝后FP3250%稀疏60MB91.8%32ms31 fps剪枝INT8量化20MB91.5%18ms55 fps延迟测试要注意固定batch size多次运行取中位数或者p95不要取第一次因为第一次包含初始化开销。另外如果模型输入尺寸不固定要按生产环境最常见的shape来测不然结果没有参考意义。4.3 一个真实案例不量化正常量化后精度暴跌有次朋友公司做RKNN平台部署现象非常典型原始模型在PC上FP32推理正常转成RKNN格式不量化保留FP16也能跑但一开INT8量化精度从93%直接掉到72%甚至某个类别输出全部变成了同一个值。排查过程大概是这样的。第一步确认校准集。他们原本为了省事只拿了50张测试图做校准类别覆盖也不均匀。换成1000张覆盖全部类别的训练集子集后掉点从21%缩小到5%。第二步检查敏感层。我把量化后的中间层激活值和FP32版对比发现最后几个全连接层的激活值分布极宽有大量离群点。这种情况下可以对这些层做“跳过量化”保持FP16或者改用per-channel量化。第三步确认算子是否走INT8。RKNN有专门的量化分析工具可以看到每个算子的量化类型和性能表现结果发现有几个自定义算子没被INT8化实际还在跑FP32速度自然也没有达到预期。如果你也遇到“不量化正常量化后精度突然崩了”的情况按这个顺序排查校准集够不够、层敏感度有没有分析、算子是否全被INT8化、量化范围有没有用百分位截断。大多数问题都能定位到这四个环节之一。4.4 精度回不来的兜底方案QAT、蒸馏、混合精度如果上面的常规手段都试了精度还是回不来有三个兜底方案我按优先顺序说一下。第一换成QAT量化感知训练。PTQ不动训练过程QAT则是在训练里加入伪量化算子。这个改动没有想象中那么大PyTorch里有一套流程在模型中插入QuantStub和DeQuantStub用torch.quantization.quantize_qat训练然后转换。代价是要重新训模型但对大多数分类、检测模型来说一次训练可接受。第二知识蒸馏。用原始FP32模型做教师模型量化模型做学生模型让学生去学教师的软标签而不是硬标签。这个方法在目标检测里效果尤其好能在保持量化收益的前提下把精度拉回来1到3个点。第三混合精度量化。不是所有层都对量化敏感。分析每一层的敏感性后把敏感的层保留为FP16或INT16其余层继续用INT8。这种部分量化策略在工程上很实用虽然收益没有全INT8那么极致但“精度不掉”这个前提比什么都重要。5. 落地避坑清单从离线指标好看到线上真正能跑5.1 校准集和验证集的卫生问题量化评估里的数据泄露陷阱量化评估时最容易出现“假阳性”——离线指标看起来很好线上却崩了。最常见的根因是数据泄露校准集和验证集共用了一部分数据或者预处理时用了全量统计量。举一个时间序列分类的例子。模型要预测未来一段时间的趋势如果量化校准的时候拿整段数据包括未来部分去算均值和方差归一化参数里就包含了“未来信息”。量化模型在离线测试时表现很好但上线后新数据进来归一化参数对不上精度立刻跳水。这个问题的本质是校准环节污染了评估环节。正确的做法是校准集、验证集、测试集三者严格独立且都来自生产环境的数据分布。校准样本量建议在几百到一千之间太少统计不准太多又浪费时间。校准集和验证集的重合度为0这是底线。5.2 算子不支持INT8的隐蔽坑模型变小了速度却更慢了这是一个特别容易骗过初学者的现象量化后模型文件确实从100MB变成了25MB但推理延迟不降反升。为什么因为很多框架在算子不被量化支持时会回退到FP32执行而为了在INT8和FP32之间做数据转换反而增加了额外的格式转换开销。我建议你在部署前后都跑一遍框架自带的Profiler或者性能报告。以ONNX Runtime为例量化可以用onnxruntime.quantization.quantize_static导出INT8模型然后在推理会话里打开Profiling查看每个Node的实际执行时间与数据类型。如果某个算子附近没有QDQQuantizeDequantize节点而前面又强行加了Cast那这个算子大概率在偷偷跑FP32。把这些算子单独拎出来找替代实现或者改写模型结构速度才能回来。提示不要只看模型文件大小判断优化是否成功一定要看推理会话里实际执行的数据类型。文件小了但算子在跑FP32是部署环境里最常见的“假优化”。5.3 工具链版本差异同样的模型换个驱动结果不同我在多个项目里踩过同一个坑开发机上用某个版本的推理框架量化出来精度96%到了生产服务器上换了个小版本精度变成90%。不是模型变了是不同版本对量化算子的实现和融合逻辑做了调整。这个问题在NPU平台更严重。RKNN或者Android NNAPI的驱动版本会直接影响INT8算子的执行方式甚至同一份模型在不同固件版本上精度都不一样。应对方式是锁定工具链版本包括训练框架、导出工具、推理框架、驱动固件全部固定到一个组合然后做完整的回归测试。优化结果只有基于同一套基线对比才有意义。5.4 建立“一次只改一个变量”的回归习惯最后聊一个管理层面的心得。模型优化过程变量很多剪枝比例、微调学习率、量化方法、校准集大小每一个都会影响最终效果。如果同时改了三处精度掉了你根本不知道该回退哪一步。我的习惯是建立一张优化实验表每行只改一个变量记录实验编号、改动内容、精度、速度、体积以及实验时间。实验编号改动内容精度延迟备注v1FP32基线92.3%45ms无v2剪枝50%91.8%32ms结构化剪枝v3微调3epoch92.0%32mslr1e-5v4PTQ INT891.5%18ms校准集800张另外每个实验的模型权重都要备份最好连同导出脚本、量化参数配置一起打成一个压缩包。模型优化不是一个一次性的动作而是一套需要反复迭代、随时回滚的过程。没有好的实验管理所谓优化就是纯粹靠运气。我见过太多团队把“量化效果不好”归因于模型不行其实只是校准集选错了。如果你准备在自己项目里试这两条路线我的建议是先花半天做Profiling确认瓶颈再用PTQ量化做一次快速试水如果精度能接受就直接走量化如果掉点厉害再上迭代式剪枝和QAT。我个人更信奉一句话优化的最高原则不是把指标拉到极限而是用最小的成本和风险让模型在规定约束下稳定上线。指标再好看不能上线都是零。
返回列表