资讯动态

模型压缩路线怎么选:剪枝、量化、蒸馏的实操与组合顺序

发布时间:2026/10/26 14:38:49 来源:尧图企业网站定制
1. 先想清楚压缩到底在压什么模型压缩这件事我在不同阶段的理解完全不一样。刚接触时以为就是把模型变小后来才发现变小只是结果真正要解决的是显存占用、推理时延、功耗预算这三笔账。剪枝、量化、蒸馏这三板斧之所以被反复提起是因为它们分别从参数冗余、数值精度、知识迁移三个完全不同的角度切入解决的问题层次完全不同。如果你只记住压缩两个字很容易拿错工具——比如明明瓶颈在内存带宽却去做了剪枝参数数量掉了一半推理速度纹丝不动。这篇文章我想讲的是这三条路线的判断依据、实操链路和踩坑记录从该不该压、先压哪个到具体的打分策略、校准集构造、温度选择再到部署侧那些文档里不会写的坑。适合已经把模型训起来、正在琢磨怎么把它塞进实际运行环境的同学也适合刚入门想搞清楚这三者边界的人。全程按我一贯的做法讲先量基线再动手最后拿数据说话。1.1 部署端的三道硬约束动手之前必须搞清楚瓶颈在哪否则压完发现没用。第一道是显存/内存容量模型参数、中间激活、KV 缓存、框架自身的开销都要算进去。举例来说一个 1 亿参数量的模型FP32 存储约 400MB看着不大但推理时激活值可能再翻几倍加上批处理之后线性增长很容易把预算吃穿。第二道是时延端侧场景对首帧响应敏感服务端场景看的是吞吐和尾延迟。第三道是功耗与带宽移动设备上 DRAM 访问的能耗远高于计算本身这也是量化在端侧收益特别明显的原因——它减少的是搬运的数据量。对应的三种手段的收益点并不重合。剪枝主要削减参数量和计算量但对内存带宽密集型的算子帮助有限量化主要削减数据位宽直接降低访存开销在支持 INT8 指令的硬件上还能提升算力利用率蒸馏不改变模型结构它换的是模型本身——用一个更小的网络去逼近大网络的输出分布。搞清楚这一层后面的顺序选择才有依据。还有一点常被忽略压缩的收益要放在端到端链路里衡量。我见过预处理占了 40% 耗时的流水线把主干网络优化了一倍整体只快了不到两成。所以第一步永远是做 profile把耗时按算子类型、按模块拆开找出真正的热点再去决定压哪里。1.2 三条技术路线的分工我把这三者当成三个不同的问题域来看。剪枝回答的是这个模型是不是训练过头了——过参数化是深度学习里的常态大量通道和连接的贡献接近于零去掉它们几乎不掉点。量化回答的是这些数字需要这么高的精度吗——权重和激活的动态范围通常远比 FP32 能表达的窄8 位甚至 4 位就够用代价是引入舍入误差。蒸馏回答的是能不能用更少的容量装下同样的知识——它不关心原始网络的冗余而是重新设计一个更紧凑的结构然后让大模型当老师把类别之间的相对关系这种暗知识传过去。它们之间还有个重要区别剪枝和量化是对已有模型的改造通常需要少量数据做校准或微调蒸馏是重新训练需要完整训练流程和数据。这就决定了成本差异——蒸馏最贵量化最便宜剪枝居中。做工程选型时成本是硬约束能用量化解决的绝不上蒸馏。放一张对比表是我自己在做方案评审时会用的那张手段主要收益数据需求是否改变结构典型压缩比主要风险非结构化剪枝参数稀疏度少量校准否加掩码50%~90% 稀疏通用硬件不加速结构化剪枝参数量计算量少量微调是30%~50% 通道连续剪裁掉点训练后量化位宽、访存数百张校准图否4x32→8离群值敏感层量化感知训练位宽、访存完整训练否4x 甚至 8x训练成本高知识蒸馏直接换小模型完整训练是学生结构依学生而定容量差距过大失效看这张表能得出一个朴素结论先量化再考虑剪枝蒸馏是最后的杀手锏。后面第 5 节我会详细讲为什么这个顺序以及什么情况下要反过来。1.3 压缩前必须做的基线测量我踩过最亏的一次坑是花了两天做完通道剪枝回头测速发现只快了 8%。原因是那个模型里大头是几个大矩阵乘通道剪掉一部分但 kernel 的调用次数没变GPU 利用率反而更低了。从那以后我养成了一个习惯动手之前先建一张基线表把参数量、FLOPs、峰值内存、单batch时延、批处理吞吐、端到端时延占比这六项测出来压缩之后逐项对比。只有参数量和 FLOPs 下降、其他指标不动说明压了个寂寞。基线测量有两个细节要注意。一是测速必须预热第一次跑总会有算子编译、内存分配、缓存冷启动的额外开销我一般预热 20 次再取 100 次的中位数同时记录 P99 尾延迟因为线上体验是由尾延迟决定的不是平均值。二是固定输入形状动态 shape 会让某些推理引擎反复做内存规划测出来的数字没参考价值。另外建议同时记录逐层耗时。主流的推理引擎和性能分析工具都能导出算子级的耗时分布按耗时排序你会很直观地看到到底哪些层值得压。如果前三个算子占了 70% 的时间那你压缩后面那些小层意义不大不如考虑换实现或者做算子融合。注意基线要在目标硬件上测别在训练机上测完就下结论。同一份模型在不同芯片上的瓶颈可能完全不同一个在服务器 GPU 上受限于算力的模型搬到端侧 SoC 上可能变成纯带宽受限。2. 剪枝先把不干活的参数找出来剪枝的直觉非常朴素神经网络是过参数化的训完之后大量权重接近于零把它们删掉理论上不影响输出。这句话是对的但魔鬼在理论上三个字里。删掉权重之后得到的是一个稀疏矩阵而绝大多数通用硬件的矩阵乘法单元对稀疏矩阵并没有加速能力——你只是把存储省了计算还是按稠密算的。这就是非结构化剪枝最大的尴尬。所以讨论剪枝必须先分两类。非结构化剪枝粒度到单个权重稀疏率可以做到很高精度损失小但只有配套的稀疏算子或专用硬件才能拿到加速。结构化剪枝以通道、滤波器、注意力头为单位整块删除剪完之后还是稠密矩阵所有硬件都能直接吃到收益代价是粒度粗、精度掉得更多需要微调恢复。2.1 非结构化剪枝与稀疏的落地陷阱非结构化剪枝的做法很直接按权重的绝对值排序把最小的那一批置零。幅度剪枝magnitude pruning之所以有效是因为权重绝对值小意味着它对输出的贡献小这个假设在大多数训练充分的模型上成立。实操上一般不会一次剪到目标稀疏度而是迭代剪枝剪 20%微调几轮恢复精度再剪 20%循环到目标值。一次剪太狠模型没有恢复的机会精度会直接崩掉。我试过一个分类模型一次性剪到 70% 稀疏度top-1 掉了 6 个点怎么微调都拉不回来改成每轮剪 10%、每轮微调 2 个 epoch最后 80% 稀疏度只掉了 0.4 个点。这个差距不是玄学是因为逐步剪枝给了剩余权重重新分配职责的机会。但真正的坑在部署侧。稀疏矩阵要在通用硬件上跑出加速需要满足两个条件一是推理引擎支持稀疏算子二是稀疏模式符合硬件的结构化要求比如 2:4 这种固定比例的块稀疏。如果只是任意位置的零很多引擎会默默退回稠密计算你省了存储、没省时间还多了一次解压开销。我的建议如果你的目标平台没有明确的稀疏加速支持非结构化剪枝只当作存储压缩手段来用别指望它提速度。想要速度收益直接走结构化路线。2.2 结构化剪枝的重要性打分怎么做结构化剪枝的核心问题是凭什么判断一个通道不重要。这里有几种主流思路我按实际效果排一下。最常用的是基于卷积核范数的打分把每个输出通道对应的卷积核求 L1 或 L2 范数范数小的认为贡献低。实现极简效果稳定是我默认的首选。它的隐含假设是权重大小约等于重要性在 ReLU 之后的网络里基本成立。第二种是基于批归一化缩放因子的打分也就是常说的稀疏化训练。做法是在 BN 的 gamma 参数上加 L1 正则训练时让不重要的通道 gamma 自然趋近于零然后按 gamma 排序剪枝。好处是有训练过程参与打分更贴近真实贡献坏处是要重新训一遍成本高。我在数据量充足、对精度要求苛刻的场景下会用这条路线。第三种是基于激活统计的打分统计每个通道在校准集上的平均激活值或激活的稀疏率。如果某个通道在绝大多数输入上都是零那它确实可以删。这个方法对 ReLU 类激活特别有效但要注意它对输入分布敏感校准集选偏了打分就失真。还有一类是基于一阶泰勒展开的贡献估计把损失函数对通道做一阶近似估算删掉它之后损失会涨多少。理论上更严谨实现上要算梯度成本高一些收益在中小模型上不太明显。我的实际经验是先用范数打分跑一版看效果不够再上更复杂的打分别一上来就搞最重的方案。2.3 一次完整的通道剪枝实操下面这段是我常用的流程骨架以主流深度学习框架为例逻辑换到别的框架也一样。第一步是给每个待剪的卷积层建立通道重要性索引。核心是把 BN 层的参数和它前面的卷积层绑定起来——因为推理时 BN 是可以折叠进卷积的剪通道必须同时剪掉卷积的输出通道、BN 的对应参数、以及下一层卷积的对应输入通道。这三处不同步模型直接报维度错误。import torch import torch.nn as nn def collect_importance(model, calib_loader, device): 用校准集统计每个 BN 层的通道重要性激活均值绝对值 stats {} hooks [] def make_hook(name): def hook(module, inp, out): # out: [N, C, H, W] score out.detach().abs().mean(dim(0, 2, 3)) if name not in stats: stats[name] score else: stats[name] score return hook for name, m in model.named_modules(): if isinstance(m, nn.BatchNorm2d): hooks.append(m.register_forward_hook(make_hook(name))) model.eval() with torch.no_grad(): for i, (x, _) in enumerate(calib_loader): if i 20: # 20 个 batch 足够别贪多 break model(x.to(device)) for h in hooks: h.remove() return stats拿到重要性之后按比例确定每层要保留的通道数。这里有个关键决策逐层独立剪还是全局统一剪。逐层独立剪每层都剪掉相同比例实现简单但会让本来就窄的层雪上加霜。全局剪是把所有层的通道重要性放到一起排序按全局分位数决定阈值能自动让宽层多剪、窄层少剪。我一般用全局方案效果通常好 1~2 个点。第二步是真正的结构改造。要注意卷积的 groups 参数——如果是分组卷积或深度可分离卷积通道剪裁的约束会复杂很多剪完必须保证每组通道数还是整除关系。这块我踩过坑对深度可分离卷积直接按重要性剪结果 groups 和通道数对不上模型加载时才报错。第三步是微调恢复。剪完的模型精度通常会掉微调的学习率要比原始训练小一个量级epoch 数不用多5~10 个足够。如果掉点超过 2 个点还拉不回来说明剪狠了退回上一档比例重来。2.4 剪枝过程中最容易忽略的三件事第一件是首尾层不要动。第一层卷积直接接触输入通道数本来就少三通道剪它对计算量帮助微乎其微对精度影响却不小。最后一层分类头直接决定输出剪了容易让类别间混淆加剧。我的默认策略是这两层保持原样。第二件是残差连接和拼接处的通道必须对齐。残差结构里主分支剪了通道短接分支必须同步剪同样的通道否则加法维度对不上。拼接concat结构更麻烦拼接后的通道是几路之和剪的时候要按来源分别规划。这块没有通用解法只能手写映射表我一个一个模块对着改过很枯燥但必须做对。第三件是剪枝后的模型要重新评估不只是看准确率。我遇到过剪完之后总体指标持平、但某些类别掉得很厉害的情况原因是这些类别恰好依赖被剪掉的通道。所以评估时要看分类别指标和混淆矩阵不能只看一个总数。另外推理时延一定要重测别信理论 FLOPs 的降幅。提示剪枝比例和精度不是线性关系。通常存在一个悬崖点超过它精度会断崖式下跌。找悬崖点的方法是从保守比例开始比如 20%每次加 10%画一条精度-稀疏度曲线取曲线拐点前的那一档。3. 量化用整数换掉浮点量化是我认为性价比最高的一招。它不改网络结构、不需要重新训练大多数情况、工具链成熟压缩比直接是 4 倍FP32 到 INT8在支持定点指令的硬件上还有额外的算力收益。原理也不复杂把浮点数的连续取值区间映射到整数区间上用一组缩放因子和零点偏移来描述这个映射。但量化的坑集中在细节里。哪些层量化、用对称还是非对称、校准集怎么选、激活的离群值怎么处理每一个选择都会影响最终精度。而且量化的误差是逐层累积的单层看着只掉 0.1 个点几十层叠起来可能就是 3 个点。3.1 量化的数学本质与参数计算先看最基本的线性量化公式。给定浮点张量 x要把它映射到 b 位整数需要两个参数缩放因子 scale 和零点 zero_point。对非对称量化适合激活值因为激活经 ReLU 后分布严重偏斜全在正半轴scale (x_max - x_min) / (2^b - 1) zero_point round(-x_min / scale) q clamp(round(x / scale) zero_point, 0, 2^b - 1) x_dequant (q - zero_point) * scale对对称量化适合权重因为权重分布通常以零为中心scale max(|x_max|, |x_min|) / (2^(b-1) - 1) q clamp(round(x / scale), -2^(b-1), 2^(b-1) - 1) x_dequant q * scale举个具体数字。某层激活值的范围是 [-0.2, 6.0]用 8 位非对称量化scale (6.0 - (-0.2)) / 255 0.02431 zero_point round(0.2 / 0.02431) 8也就是说整数 8 对应浮点 0整数 0 对应 -0.1945整数 255 对应 6.0。这个映射之下量化误差最大是 scale/2约 0.0122。对大部分层来说这个误差可以接受。问题出在离群值上。假设同一层里有个别激活值达到了 60那 scale 会被拉到 0.236其他正常范围内的值分辨率直接掉了十倍。这是量化掉点最常见的元凶尤其是在 Transformer 类模型里某些通道的激活值天生比其他通道大几十倍业内叫激活离群。解决思路有几种一是逐通道量化权重按输出通道各自算 scale能显著缓解二是截断校准不取绝对最大值而是取 99.99 分位数牺牲极少数离群点的精度换整体分辨率三是把离群通道单独拎出来用高精度算混合精度方案工程上最有效但实现最麻烦。3.2 PTQ 与 QAT 的取舍标准**训练后量化PTQ**只需要一批校准数据跑一遍前向统计每层激活的动态范围然后把 scale 和 zero_point 定下来。成本极低几十分钟能搞定一个模型。**量化感知训练QAT**则在训练时插入伪量化节点前向用模拟量化的方式计算反向传播时用直通估计器绕过不可导的取整操作让模型学会适应量化误差。成本高但精度通常更好。我的选择标准很粗暴PTQ 掉点在 1 个点以内就用 PTQ超过 2 个点才上 QAT中间地带看时间预算。实践中卷积类视觉模型用 PTQ 基本无损检测、分割这类密集预测任务掉点会多一些。Transformer 类模型对量化明显更敏感尤其是激活量化经常需要 QAT 或者更精细的方案。有一个容易被忽略的点QAT 训完之后导出的模型精度取决于伪量化节点和真实量化算子的对齐程度。我遇到过 QAT 训得好好的导出到推理引擎后掉点严重排查发现是伪量化用的是四舍五入而推理引擎用的是另一种舍入策略。这种不一致只能靠逐层对齐输出数值来排查比较折磨人。3.3 静态量化的实操链路下面是我惯用的四步流程。第一步确定量化方案。权重用逐通道对称量化8 位激活用逐张量非对称量化8 位。这个组合在绝大多数场景下是稳妥的起点别一上来就追求 4 位。第二步构造校准集。这是最影响成败的一步。校准集要能覆盖真实输入的分布一般从训练集或验证集里抽 200~1000 个样本。我的经验是样本多样性比数量重要不同类别、不同光照、不同尺度的样本都要有而不是随便抽一千张同一场景的图。校准集选偏了统计出来的动态范围就不对量化参数直接错。第三步执行量化并逐层比对。这一步必须做误差归因不能只看最终指标。# 伪代码示意逐层输出比对定位敏感层 def layerwise_compare(fp_model, q_model, calib_batches, topk5): errors {} hooks_fp, hooks_q [], [] def make_hook(name, store): def hook(module, inp, out): store[name] out.detach() return hook for name, m in fp_model.named_modules(): if should_watch(m): hooks_fp.append(m.register_forward_hook(make_hook(name, fp_out))) # q_model 同理注册 run_both(fp_model, q_model, calib_batches) # 计算每层的相对误差排序取 top-k for name in fp_out: err ((fp_out[name] - q_out[name]).norm() / fp_out[name].norm()).item() errors[name] err return sorted(errors.items(), keylambda kv: -kv[1])[:topk]这个流程能直接告诉你哪几层量化误差最大。如果集中在某几层就用混合精度——把这些层留在浮点其余量化。我做过一个检测模型全靠这个手段把掉点从 2.3 压到了 0.4代价是整体速度只慢了 6%非常划算。第四步端到端验收。精度、时延、内存都要重测。特别注意内存峰值有些引擎在量化过程中会有临时的反量化缓冲区峰值内存反而涨了。3.4 量化掉点的排查顺序掉点之后不要瞎调按这个顺序查排查项现象处理方式校准集分布偏移某几类样本掉点严重补充该类别样本重校准激活离群值逐层误差集中在个别层截断校准 / 混合精度首尾层量化输入输出层误差大首层和输出层保持浮点BN 折叠顺序错误量化后指标异常崩坏先折叠 BN 再量化舍入策略不一致QAT 导出后掉点对齐伪量化与推理算子逐张量量化通道间动态范围差异大改为逐通道量化4. 蒸馏把大模型的手感搬过来蒸馏和前两者有本质区别——它不是修修补补而是换一个模型。你设计一个参数量更少、层数更浅的学生网络然后让已经训好的大模型当老师用老师的输出去指导学生训练。表面上看这像是用软标签训练但里面有个很关键的点老师的输出里包含了类别之间的相对关系这个信息在硬标签里是完全丢失的。举个例子一个图像分类任务里一张模糊的猫图硬标签只说这是猫1.0而老师的输出可能是猫 0.7狗 0.2狐狸 0.08。后者的信息量明显更大——它告诉学生这张图有点像狗学生在学习时就知道了猫和狗在特征空间里的接近程度。这种被称为暗知识的东西是蒸馏真正的价值所在。4.1 软标签为什么比硬标签信息量大从信息论角度看硬标签是一个 one-hot 向量熵为零除了正确类别之外不提供任何信息。软标签是一个概率分布熵大于零携带了类间相似度信息。训练时学生网络在拟合软标签的过程中实际上是在学习老师的特征空间几何结构而不只是学分类边界。这个额外的约束相当于一种正则化能让学生模型在参数量远小于老师的情况下仍然保持不错的泛化能力。温度参数 T 在这里起关键作用。直接把老师的原始 softmax 输出拿来蒸馏概率分布往往非常尖锐正确类别 0.99其他加起来 0.01暗知识被压在很小的数值里学生学不到。所以要把 logits 除以温度 T 再算 softmax分布被软化类间关系被放大。T 越大分布越平滑通常取 2 到 10 之间。数学上蒸馏的损失由两部分组成L α * CE(y_hard, p_student) (1 - α) * T^2 * KL(p_teacher^T || p_student^T)T^2这个系数不是拍脑袋来的。因为软目标的梯度幅度会随温度变化大约按1/T^2缩放乘上T^2是为了让软损失和硬损失的梯度量级保持可比避免调温度时还得同步调学习率。4.2 损失权重与温度参数的确定α和T这两个超参怎么定我的经验是这样α控制硬标签和软标签的配比取值在 0.1 到 0.5 之间比较常见。数据量充足、标签质量高时可以给硬标签更大权重数据少、标签有噪声时软标签的平滑作用更有价值可以把α调小。我一般从 0.3 开始试。T的选择要看老师输出的尖锐程度。如果老师的 top-1 置信度普遍在 0.99 以上训练过拟合的典型表现T 要取大一点5 到 10如果老师本身有正则化、输出比较平缓T 取 2 到 4 就够。有个简单判断方法把老师的 logits 除以不同 T 后算一下熵选熵值在ln(类别数)的 30%~60% 之间的那个 T通常效果不错。这里有个反直觉的点老师的准确率不是越高越好老师输出的平滑度更重要。一个准确率 95% 但极度自信的老师蒸馏效果可能不如一个准确率 93% 但输出更柔和的老师。所以有些做法会用多个模型的集成输出当软标签或者用带温度平均的检查点目的都是让软标签携带更多信息。4.3 蒸馏实操与容量匹配学生结构的设计是整个流程里最需要经验的部分。核心矛盾是学生太小装不下老师的知识怎么训都追不上学生太大蒸馏的意义就没了。业内叫容量差距问题。我的做法是先定学生的参数预算由部署约束倒推然后保证几个结构上的匹配感受野不能差太多学生层数更浅时可以用更大的卷积核或空洞卷积补偿特征图分辨率衔接要合理别在很浅的层就把分辨率降到底通道数按比例缩放而不是简单地砍层下面是一个简化的蒸馏训练骨架def distill_step(student, teacher, x, y, T4.0, alpha0.3): with torch.no_grad(): t_logits teacher(x) s_logits student(x) # 软化后的分布 p_t F.softmax(t_logits / T, dim-1) log_p_s F.log_softmax(s_logits / T, dim-1) loss_soft F.kl_div(log_p_s, p_t, reductionbatchmean) * (T ** 2) loss_hard F.cross_entropy(s_logits, y) return alpha * loss_hard (1 - alpha) * loss_soft训练策略上我一般用两阶段先纯蒸馏α 接近 0跑一段让学生充分吸收老师的输出分布再加大硬标签权重做精调。也有做法是让 α 从 0 线性升到目标值效果类似。学习率用学生独立训练时的 1.5 倍左右因为软标签的梯度更平滑能承受更大步长。4.4 特征蒸馏的补充手段只用输出层做蒸馏信息传递只发生在最后一层中间的表示没有被约束。特征蒸馏把约束加到中间层——让学生某个中间层的特征去逼近老师对应层的特征。实现上通常在两边各加一个投影层把维度对齐然后算 L2 距离或者注意力图的差异。这条路线的坑在于层与层的对应关系不是天然的。学生层数少哪一层对应老师的哪一层常见做法是按相对深度匹配学生第 k 层对应老师的第 2k 层或者干脆只对最后几个 stage 做特征蒸馏。我试过对全部层做效果反而变差——中间层特征的语义层级不同强行对齐是在引入噪声。我现在的默认配置是输出蒸馏为主最后两三个 stage 加特征蒸馏做辅助权重控制在主损失的 0.1 到 0.3 之间。这个配比既能利用中间表示的信息又不至于干扰主任务的学习。5. 组合使用顺序错了等于白干单独用某一种手段已经能拿到不错的收益但实际项目里往往需要叠加使用——既要小又要快只靠一种不够。这时候顺序就成了关键问题顺序错了不仅收益打折还可能互相破坏。我在不同项目里试过三种顺序结论比较明确先蒸馏定结构再剪枝降冗余最后量化压位宽。这个顺序的逻辑是每一步都在为下一步创造更好的条件。5.1 三种手段的兼容性分析蒸馏在前的理由蒸馏需要一个完整的训练流程它改变的是模型结构本身。如果你先剪枝再蒸馏剪完之后的结构已经是残缺的学生网络的设计空间被限制住了蒸馏的效果会打折。反过来先蒸馏得到一个紧凑的学生模型这个模型本身就是浓缩过的冗余比原始模型少后续剪枝的空间虽然小一些但剪起来更安全。量化在最后的理由量化是所有手段里对精度最敏感的它对模型权重的分布有要求。如果你先量化再剪枝剪枝的微调过程会在量化后的离散空间里进行梯度信息损失严重恢复能力差。而且剪枝会改变每层的动态范围前面统计好的量化参数全废了还得重新校准。所以量化必须是最后一步。剪枝和量化的顺序还有个细节剪枝后的模型各层通道数变了动态范围也变了量化参数必须重新统计。如果反过来先量化、再剪枝你得先反量化回浮点才能剪中间多了一轮精度损失。所以顺序不可逆。有一个例外情况如果你的目标硬件对稀疏有原生支持比如某些加速器支持 2:4 块稀疏那剪枝可以和量化同时进行甚至剪枝的收益会更大因为稀疏矩阵的存储格式本身就能省带宽。这种场景下顺序可以灵活一些。5.2 不同场景的配置组合我按常见场景整理了一张配置表都是实际项目里验证过的组合场景首选方案备选关键约束端侧视觉分类蒸馏 INT8 量化加 30% 通道剪枝首帧时延 50ms服务器批量推理INT8 量化 算子融合结构化剪枝 20%吞吐优先精度容差 1 点边缘检测/分割PTQ 敏感层混合精度QAT 兜底小目标召回不能掉大语言模型推理权重量化8 位或 4 位 KV 缓存优化蒸馏小模型显存是硬瓶颈资源极度受限蒸馏小模型 量化结构化剪枝参数量级压缩注意表里大模型那一行——它的瓶颈通常在显存和带宽而不是算力所以权重量化的收益远大于剪枝。而且大模型的激活离群问题特别严重激活量化要非常小心很多时候只做权重量化weight-only就够激活保持浮点。5.3 压缩后的验收标准压缩完不等于完事验收环节我固定跑这几项精度维度——总体指标、分类别指标、混淆矩阵还要看置信度分布。量化之后模型的输出置信度往往会整体偏低或偏高虽然不影响准确率但会影响下游的阈值策略。我遇到过一次量化后 mAP 只掉 0.3但置信度整体偏移导致后处理的阈值全部失效实际业务指标掉了 8 个点。性能维度——单 batch 时延、批处理吞吐、P99 尾延迟、峰值内存。四个指标缺一不可我见过吞吐涨了但尾延迟恶化的案例线上体验反而变差。数值维度——逐层输出和浮点基线比对记录最大相对误差和误差分布。这个数据在后续排查线上问题时非常有用能快速定位是哪一层出的问题。稳定性维度——用同一批输入连跑 1000 次检查输出是否完全一致。量化引入了确定性理论上应该完全可复现如果出现抖动说明有非确定性算子或者内存复用的问题。6. 常见问题排查实录这一节是我这些年攒下来的问题清单都是实际遇到过、文档里查不到的。按出现频率排序前面几条几乎每个项目都会撞上。6.1 精度掉点速查表症状最可能原因验证方法处理剪枝后某几类掉点集中这些类依赖被剪通道分类别指标 通道激活统计回退这几层的剪枝比例量化后整体均匀掉点校准集不具代表性换校准集重测补充多样本重校准量化后个别层误差爆炸激活离群值逐层误差排序该层保持浮点或截断校准蒸馏后学生追不上老师容量差距过大对比学生独立训练加大学生或加特征蒸馏蒸馏前期 loss 不降温度过高或 α 过小扫 T 和 α从 T4、α0.3 起步剪枝后时延没降非结构化稀疏无加速检查稀疏格式改结构化剪枝量化后内存反涨反量化缓冲区峰值内存 profile调整算子融合策略说两个印象深的案例。第一个是量化后整体掉 2 个点逐层比对发现误差集中在一个残差块的输出上原因是这个块的输入有一个通道的动态范围是其他通道的 200 倍。处理方式是把这一层改成逐通道量化掉点立刻回到 0.3。这种问题靠调超参是解决不了的必须做归因。第二个是剪枝后精度恢复不了微调了 20 个 epoch 还是差 3 个点。后来发现是拼接层的通道映射写错了——剪的时候按照卷积的输出通道顺序删的但拼接之后通道顺序变了导致后面的卷积读到了错位的数据。模型能跑、不报错但语义完全乱了。这类 bug 最难查因为没有任何报错信息。后来我养成了习惯剪枝后逐层比对浮点基线的输出误差超过阈值的层立刻标记出来。6.2 部署侧的性能陷阱算子不支持导致的回退。量化模型导出后如果某个算子在目标引擎里没有对应的定点实现引擎会悄悄把它退回浮点执行中间插入反量化-量化转换。结果是模型能跑、精度也对但速度提升远低于预期。排查方法是看引擎的日志或者算子分配报告找出被回退的算子。常见的回退点是激活函数、特殊归一化层、动态 shape 相关的操作。内存峰值反超。有些引擎在流水线里会同时保留浮点和定点两套权重或者为反量化分配临时缓冲区导致峰值内存反而比压缩前高。这个在端侧是致命的。处理方式是调整算子融合策略让反量化尽可能融合进前面的算子减少中间张量的存活时间。吞吐不升反降。在小 batch 场景下量化带来的收益可能被额外的量化/反量化开销抵消。尤其是 batch size 为 1 时计算量本来就不大瓶颈在访存和调度这时候量化的收益主要来自位宽下降而不是算力提升。如果模型的瓶颈本来就不在计算上量化可能帮不上忙。有个容易被忽略的点线程数和 batch size 要一起调。量化的收益和并行度强相关默认线程配置往往不是最优的值得花时间扫一遍。6.3 几条花钱买来的经验不要一次改多个变量。压缩调优最常见的错误是同时改剪枝比例、量化方案、蒸馏温度然后看最终指标。出了问题根本不知道是哪个导致的。正确做法是串行优化每一步单独验证、单独记录形成一条可回溯的实验链。保留完整的浮点基线。所有对比都要以浮点模型为基准包括逐层输出、精度指标、性能数据。我在项目里会把这套基线数据存档后面任何一版压缩模型出问题都能快速定位偏差来源。关注分布而不是均值。精度指标是均值掩盖了很多问题。误差分布的尾部才是关键——如果 99% 的样本误差很小但 1% 的样本误差极大均值看起来没问题线上就是灾难。所以我会额外统计误差的 P95 和 P99。压缩比不要一次拉满。留 10%~20% 的余量。因为模型上线后还会遇到分布漂移、新场景接入压缩到极限的模型没有任何缓冲空间稍微偏一点就崩。我一般会做两版——激进版和保守版线上先跑保守版观察一段时间。团队协作时把压缩配置写进版本管理。剪枝的通道索引、量化的校准参数、蒸馏的超参这些都要跟着模型版本一起存。我吃过亏——模型更新了一版但剪枝索引还是旧的加载时维度对不上排查了半天才发现是配置没同步。最后分享一个我现在的习惯每做完一次压缩都会把这一版为什么这么压、试过哪些没work、下次可以怎么改记一段笔记。压缩这件事高度依赖具体模型和具体硬件通用结论很有限真正有用的是自己积累的那份对应关系。前面那些表格和阈值本质上都是从这类笔记里提炼出来的你可以直接拿去用但最终还是要按自己的模型再校准一遍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑