模型量化这事儿,最折磨人的不是把模型压小,也不是让它跑得更快,而是每次一量化就觉得精度要崩了,但指标没掉多少,心里那块石头又放不下。这几年我做AI模型部署,量化从一开始的“敢不敢上”变成了“必备流程”——显存不够、时延超标、带宽打满,几乎每个要上线的模型最终都要走一遍量化这条路。而做得多了就会发现,量化真正难的不是“会量化”,而是精度和延迟之间那杆秤怎么摆。本文把我踩过的坑、调过的手段、以及衡量精度与延迟这套方法论做个系统整理,适合正在做模型上线、推理加速、或者被“压缩率翻倍但效果不掉”这种KPI逼过的朋友参考。
1. 量化方案的选型逻辑:先搞清楚为什么精度会掉
1.1 量化为什么能同时影响精度和延迟
模型量化说白了就是把 FP32 的浮点权重和激活值,用更低比特位宽的整数(比如 INT8、INT4)来表示。内存占用能降到原来的 1/4 甚至 1/8,计算单元在处理低精度数据时吞吐更高,所以延迟和显存压力一起降下来。但代价是数值从连续分布变成了有限几个离散的台阶,信息量丢了一截,精度掉一点是必然的。
但这里有一个最容易被人忽略的事实:延迟提升的真正来源,未必是计算变快,而是内存搬运变少。很多大模型(特别是 LLM 类)的推理瓶颈在显存带宽和缓存命中率,权重变小之后,加载权重的时间大大缩短,延迟才有质的改善。这也是为什么有些模型量化后计算速度没怎么变,但整体时延反而明显下降——它省的是 IO 的时间,不是算术单元的时间。理解这一点,才会明白为什么在有些场景下 INT8 比 INT4 还划算,因为 INT4 虽然更省内存,但反量化开销和精度损失可能把收益吃掉了大半。
1.2 量化对象怎么选:只压权重还是连激活一起压
先说说只做权重量化的场景。如果激活值每条数据差异都很大,或者硬件对激活的整数计算支持有限,只量化权重是稳妥的第一步。这种方式最简单,精度损失通常很小,内存减少一半以上,但延迟收益相对有限,因为激活值还在用 FP16/FP32 计算。
要追求更低延迟,就得做权重和激活都量化的“全量化”。激活量化最难的地方在于校准——激活值的分布不是静态的,它随输入变化,尤其是有 BatchNorm 或者 attention 结构的模型,激活值分布带有明显的“尖峰”特征,直接按均匀分布去量化,误差会集中在峰值附近,经常一掉就是十几个点。
还有一类是 KV Cache 量化。在做长序列生成类任务时,KV Cache 占的显存是个大头,把它从 FP16 压到 INT8,显存占用立刻降低,而且不会增加计算延迟,反而因为 cache 数据量小、读写更快,整体吞吐能提升不少。KV Cache 量化在模型部署中经常被当成“白嫖”项目,因为它不直接改权重和计算的数值路径,精度损失通常比全量化小得多。
1.3 数据格式与量化粒度:对称还是非对称、按层还是按通道
量化格式的选择直接影响精度表现。对称量化简单、硬件友好,但它的零点固定在 0,如果数据分布整体偏移,比如激活值全为正,对称量化会浪费大量档位。非对称量化用 zero-point 来修正偏移,精度更好,但计算时候要多一次移位操作,速度稍微吃亏。
量化粒度的选择同样关键。Per-tensor 量化整层用一个 scale,计算最简单,但如果层里不同通道的数据范围差异大(卷积层的权重很常见),小数值的通道容易直接被压成 0,信息全丢。Per-channel 量化每个输出通道单独算 scale,精度恢复明显,代价是存储 scale 的额外开销和计算时的稍微复杂化。
我的经验是:权重量化直接上 per-channel 基本无脑收益,激活量化先用 per-tensor 看结果,掉点严重再细化到 per-group。别一上来就堆组量化,参数量大了排查起来会很头痛。
1.4 校准数据集:精度的大半条命都在这里
很多人量化效果好与不好,第一个怀疑模型,第二个怀疑算法,却忘了校准数据集可能早就埋了雷。校准集的作用是用一小批真实输入数据,统计激活值的分布,以此确定 scale 和 zero-point。它根本不需要带标签,但一定要能代表真实线上输入的分布。
我见过最典型的错误是用训练集做校准。训练集样本来自批量增强后的分布,和真实部署时遇到的输入往往有偏差。更合理的做法是:从一条线上采样日志里抽几百到一两千条样本,覆盖各种极端情况,比如空输入、超长文本、超亮图片、高噪声音频。校准集长度与批量大小也有讲究,一般建议在校准时固定 batch size,比如 64 或 128,让统计到的分布更稳定。批量太大会抹平峰值,batch size 太小则噪声太大,scale 跳动很厉害。
2. 精度评估与延迟测量的正确姿势:没有测量就没有平衡
2.1 精度评估的三个层次:不只是看平均指标
量化效果好不好,不能只看最终任务指标(比如准确率、BLEU 分数)有没有掉。任务指标太宏观,它会掩盖很多局部问题。我建议分成三个层次去评估。
第一层是任务级指标,这个必须有,但只作为过门槛标准。比如分类任务 Top-1 掉不超过 0.5%,生成任务指标掉不超过 2%,这只是“能不能上线”的下限,不是“好不好”的标准。
第二层是中间层特征对比。把量化模型的中间激活和原始模型做余弦相似度对比,哪里相似度掉得多,哪里就是敏感层。这个穿层对比相当好用,它能帮你把掉点归因到具体的 block 或 head 上,而不是笼统地“量化掉点了”。
第三层是分布可视化。把量化前后的输出分布画出来,重点看有没有太多的“离群值”被截断在量化范围之外。很多掉点其实只发生在长尾分布的少数极端输入上,但那些极端输入往往是短视频、高分辨率图这类对体验权重高的场景,不能光看平均指标糊弄过去。
2.2 延迟测量里的坑:warm-up、batch size 与统计口径
延迟听起来简单,实际上报数时稍有不慎,数据就变得不可信。首先必须做 warm-up,前几十次推理是在加载权重、建立缓存、初始化 CUDA context,不算数。等运行稳定后再开始采样。我自己习惯用前 100 次作为 warm-up 不参与统计,之后至少测 1000 次。
batch size 的影响也很大。batch size = 1 时测到的主要是算子和 IO 的串行延迟,适合衡量单请求的响应时间。调大 batch 后测到的是吞吐和饱和计算能力。部署时候如果你的服务是单请求模式,却拿大 batch 的吞吐报给业务方,那是找骂。反过来,你想压榨硬件最高利用率,却拿 batch=1 的延迟数据说话,同样没意义。
统计口径建议同时报 P50、P95 和 P99。P50 反映大多数请求的水平,P99 反映最差的尾部延迟。量化对 P50 的影响通常小于对 P99 的影响,因为 P99 往往来自输入长度过长的请求,激活尺度溢出在长序列上更容易发生。
2.3 精度和延迟之外:显存、功耗、吞吐的联动
仅盯精度和延迟两个指标,容易漏掉一些“后知后觉”的问题。显存是其中一个。量化后模型体积变小,但 KV Cache 可能还是大头,尤其长上下文场景。量化方案发布后,建议把显存峰值也固化进测试报告,因为线上显存不足会导致 OOM 重启,延迟直接飙到秒级,精度再好也没意义。
功耗在移动端和边缘设备上同样关键。INT8 和 INT4 的推理计算,功耗可能相差 30% 以上。手机端电池续航天差地别,模型量化后 CPU/GPU 占用下降,发热减少,用户的体感差距比几个毫秒更明显。如果做嵌入式设备,功耗甚至比延迟还要优先考虑。
3. 实操过程与核心环节实现:从基线到收益落地
3.1 第一步:建立一套可信的基线
我在做量化之前,一定先建立一个完整的基线评估流程。这里有几个固定的动作:选定评估数据集、确定评估指标、固定硬件环境和推理配置、测出 FP32/FP16 的延迟、吞吐、显存、功耗和精度指标。这些数据是后面所有比较的锚点,一旦锚点不牢靠,后面说什么都是空中楼阁。
实际操作时,我通常用一个脚本统一完成这五件事,保证可重复。硬件环境要固定到具体型号、具体驱动版本和特定推理后端,因为换一个后端或者换一个编译优化选项,延迟和量化行为都完全不一样。基线建立后记得归档,后面所有实验都拿新量化模型和这组基线比对,别每次重测一套新基线,不然不同批次的测试数据混在一起根本没法归因。
3.2 第二步:从最保守的方案开始跑通整条链路
第一次上手量化,我强烈建议不要直接挑战最低 bit 的极限。先用权重 INT8 + 激活 INT8 这种最成熟的方案,跑通高质量校准、评估和部署的全链路。这一步不是为了拿到极限性能,而是为了确认工具链没有大坑、数据通路没有丢精度、推理引擎能正常加载与执行量化模型。
我曾经做过一个模拟项目,一开始就直接上 INT4 权重量化,结果模型跑起来精度掉得很厉害,我花了一整天排查,最后发现是推理引擎对 INT4 的 kernel 支持不够充分,某些算子回退到 FP16,精度和延迟都没达到预期。后来老实从 INT8 开始,链路通了之后再看哪些算子是真正“值得”压成 INT4 的。从保守起步可以帮你把“工具链问题”和“量化方案问题”分开。
3.3 第三步:逐层分析精度损失,定位敏感算子
全量化跑通后,精度可能掉了 1% 到 3%,这时候就需要做敏感度分析。我用过两种高效的方法。
一种是逐层替换法:把量化模型某一层的权重换成 FP16 原始权重,看最终指标能恢复多少。恢复得越多,说明这一层对量化越敏感。用这个方法遍历所有层,原则上就能得出敏感层排序。缺点是遍历成本高、耗时久,适合模型层数不多的场景。
另一种是代理指标法,也就是前面提到的中间激活余弦相似度。跑一遍原始模型和量化模型,逐层输出中间特征,算相似度,相似度低于 0.9 的层优先处理。这个速度快,一次跑完能覆盖所有层,缺点是不能完全反映最终任务指标的变化。实际中我会把两种方法结合:先用代理指标全量扫描,按敏感度排序取 Top 10 层,再对 Top 10 做逐层替换验证,这样速度和准确度都能兼顾。
定位到敏感层后,优先检查它是不是 attention 的 Q/K/V 投影层或者归一化层。这几个位置在多数模型里是量化最容易掉点的地方。我的经验是,如果 Q/K/V 的权重量化掉点严重,可以先试 per-channel 粒度,不行就把该层改成混合精度(保留 FP16),通常能拿回大部分精度损失。
3.4 第四步:延迟数据的系统化采集与分析
延迟测试建议单独做一个脚本,包含 warm-up + 多次采样 + 多 batch 测试。我常用的配置是 batch size 从 1、8、32 三个档位分别测量,输出帧:P50、P95、P99。
有一个经验值是:如果 INT8 的延迟比 FP16 只提升不到 5%,先别怀疑量化没用,去看是不是“内存墙”被顶到了。小模型或 IO 密集型算子在 INT8 下计算很快,但数据加载反而可能变成瓶颈。这时候要从算子层面看每个 OP 在做什么,找到真正耗时的那几个 kernel,看量化前后的耗时对比,重点看矩阵乘、归一化这类计算密集型的算子有没有吃到低精度的红利。
另外延迟测试必须和精度测试是“同一份数据分布”下的结果。有的优化手段会在某些输入分布上表现好,换个输入分布就拉胯。所以在延迟报告里,同时标注测试输入的长度分布和复杂度属性,这个细节能帮后面排查线上问题省很多功夫。
3.5 第五步:迭代调优——混合精度、敏感层保护与重校准
当敏感层定位完成之后,就进入了迭代调优阶段。核心思路是:用最小的精度代价换取最大延迟收益。
混合精度是这一步的核心策略。拿整个模型做 INT4 量化,精度可能掉 5%;但只把 20% 的敏感层留在 INT8,其余 80% 全部压成 INT4,精度可能只掉 1.5%,而延迟改善几乎和全 INT4 持平。所以别迷信“全模型统一 bit 位”这种省事的做法,混合精度才是量化工程里的真正价值点。
重校准也是常用技巧。在量化过程中尝试不同的校准方式:有的训练框架支持每个算子单独做校准,有的可以选不同校准集。有时候换一个校准集就能把精度拉回 1 到 2 个点。我印象很深刻的是模拟项目 X,我之前用 uniform 校准集,激活量化掉点 2%,改成 power law 采样校准集后,掉点直接降到 0.8%。
另外,如果模型里有 BatchNorm 层,可以先 bn 融合再量化。融合后的卷积层数值分布更稳定,量化误差会明显减少。这是一个性价比非常高的步骤,强烈建议先做再说。
3.6 验收与回归:量化方案上线前必须过这四关
- 精度关:在保留的评估集上,确认任务指标掉点在预设阈值内。
- 延迟关:在真实部署硬件上测出的 P50/P95/P99 均达标。
- 显存关:峰值显存不超过分配份额,长序列场景下也不爆显存。
- 稳定性关:连续跑 24 小时或者跑满 10 万次,看有没有累积误差和内存泄漏。
这四关缺一不可。我之前遇到过一个案例,精度、延迟、显存都过了,但上线不到一周就频繁报错,查了半天是量化模型中某个算子对长尾输入触发处理分支时异常,属于边界情况稳定性问题。所以稳定性测试还是别省,量化模型的边界行为比稠密模型更难预判。
4. 常见问题与排查技巧实录
4.1 精度掉点大幅度:从三个方向入手
遇到掉点超过预期,先别急着换更宽的位宽,先按以下顺序排查:
第一,校准集是不是有问题。置信度最高的嫌疑对象通常是校准数据与真实数据分布偏差太大,比如线上是短视频封面图,校准集却用了大量高清风景图。先把校准集换成贴近线上分布的数据,说不定问题直接消失。
第二,激活值有没有越界。打印量化后各层的 scale,看激活值 max 是否集中在几个离群点上。偶尔出现的超大值会把 scale 拉大,后面的正常数值全被压缩到低精度档位,精度损失不只是几个点的事。这时候用截断策略,把百分位在 99.99% 以上的离群点直接截掉,效果往往立竿见影。
第三,敏感层有没有被“一刀切”。如果统计结果显示某几个 block 的精度贡献极高,那就别把全部层压到同一 bit 位。混出混合精度方案,把敏感层保留在更高精度,这一条前面已经详细说过,是实战里最有效的保护手段。
4.2 延迟不降反升:为什么量化后反而更慢了
量化后延迟增加,主要有两个原因:一是某些算子不支持低精度计算,运行时回退到 FP16/FP32,还要额外做一遍反量化,相当于“多走一步”;二是小算子在低精度下的 kernel 实现本身效率不高,尤其是一些形状不规整、channel 数很小的层,在大 batch 下收益为正,小 batch 下反而更慢。
排查办法是分阶段测耗时。先测整个模型的端到端延迟,再用 profiling 工具按算子逐层看耗时。如果发现某个算子在量化后耗时反而变长了,直接把它保护成高精度算子,或者换一种推理后端,看有没有更高效的 kernel 实现。有时候换一个 tensor 布局(比如 NHWC 与 NCHW)也能明显改观。
还要提一句:如果是为了低延迟而上 INT4,但业务场景里输入长度变化剧烈,INT4 的加速在长序列下有优势,在短序列下可能被预处理和反量化开销抵消。这时候可以考虑只在长序列分支里启用 INT4,短序列留在 INT8,做成动态路由方案。
4.3 校准集过拟合与静态校准的局限性
静态校准的原理是用一小批统计分布代表线上所有情况,局限性很明显:遇到分布之外的输入,量化误差就会变大。实践中常出现校准集里表现很好、真实场景精度却拉胯的情况。
要降低过拟合风险,可以动态校准。动态校准思路是运行时按输入 batch 的实际范围动态计算 scale。开销比静态校准大一些,但精度有保障,尤其适合输入分布特别分散的场景。如果你的硬件和推理引擎支持动态量化,值得在敏感场景下测一轮。
也有人用“多组校准量取平均”的办法,比如准备三组不同来源的校准集,分别生成 scale,最后取合适的统计值融合。这比单组要稳,但复杂度也上来了。我倾向于先看单组校准下的表现,不够再加强制性更强的多组方案。
4.4 输入形状变化与动态 shape 兼容
模型部署时经常遇到动态 shape 请求,尤其是 NLP 类模型,输入长度不定。动态 shape 对量化影响很大:激活值的 scale 如果是针对固定长度校准的,遇到超长输入时,超出部分的数值分布没被统计到,量化误差就会显著增大。
一个方案是在校准阶段引入不同长度的样本,生成更泛化的 scale。另一个方案是设置分档:短序列用一组 scale,长序列用另一组。如果推理引擎支持 shape 变长时的 scale 动态切换,优先用这种机制。最省事的方案反而是“限制线上请求长度”:服务端对过长输入做截断或切片,既守护量化精度,也守住延迟天花板,虽然不是最优雅的解法,但最可靠。
4.5 硬件适配差异:同样的量化模型在不同平台上效果不同
同样的 INT8 模型,在某家 GPU 上跑得很好,换到另一家 GPU 或者 CPU、NPU、手机端上,结果可能天差地别。原因在于不同硬件 IP 对低精度运算的处理方式不同,有的原生支持 INT4 乘加,有的要先转成 FP16 做计算再转回 INT4,有的在反量化上有专用电路单元,有的全靠软实现。
所以量化方案必须与目标硬件绑定来选型。做手机端,优先考虑对端侧 NPU 支持最好的量化格式和算子;做服务端,以 GPU 上的 tensor core 支持能力为准。同一个模型如果要部署到多个平台,建议为每个平台各做一套专用量化配置,而不是强行用一套统一配置打天下。
我在实际项目中吃过这个亏:模型在测试服务器上量化后延迟降了 40%,信心满满部署到生产环境后,延迟反而比 FP16 还高。查下来的原因是生产机器的驱动版本较旧,新的低精度 kernel 没有及时支持。最后把新驱动升级完才恢复正常。所以在做延迟对比时,驱动版本和推理引擎版本必须列在测试报告里,作为重要的复现参数。
写在最后:量化调优的几个个人习惯
做了这么久量化,几个习惯一直保留着:所有实验前先写死随机种子,保证校准过程可复现;每次修改都留下完整配置记录,方便定位是哪一步导致精度波动;任何一次优化都必须同时附上精度和延迟两种数据,不然只报一个指标等于白做。
另外,我越来越觉得量化调优不是一次性工程。模型上线后,线上的输入分布会随业务变化而漂移。有条件的话,定期用最新线上数据重新校准一次,比上了线就放着不管靠谱得多。最后再分享一个我常用的“压箱底”小技巧:将几个关键敏感层量化后如果精度还是不达标,可以试试把它们单独做成一个小算子插到模型外面做后处理,或者用量化模型的输出去微调一个小的精调头。这种土办法看起来不够“正经”,但在紧急上线场景中救过我好几次。量化的方法论重要,但解决问题的灵活性,才是每个做落地部署的人最该积累的资产。