前阵子帮某团队把一个图像分类模型往嵌入式设备上搬,模型在服务器上跑得飞快,一上板子就露馅:内存直接顶满,跑一帧要两秒多。问题根本不是精度不够,而是模型太大、计算太重。这时候能想到的第一个手段就是模型量化——把 FP32 的权重和激活从 32 位浮点压到 8 位整数,模型体积直接缩到四分之一,推理速度能提好几倍。这篇内容就把模型量化这块最基础的知识一次性讲清楚,包括它解决什么问题、有哪些关键概念、实际落地怎么做、又会踩到哪些坑。不管你是刚接触部署优化的同学,还是已经在做端侧推理但一直没系统梳理过量化原理的工程师,这篇都适合当一份入门地图来用。
1. 为什么模型越跑越慢:量化要解决的问题
1.1 部署现场的三个老大难
模型训练的时候,大家关心的是精度、收敛速度、显存占用;模型一旦要部署,问题就完全变了。我见过很多项目在服务器上表现挺好,一换到手机、摄像头、机器人控制器这些端侧设备,立刻遇到三个拦路虎:内存不够、算力吃紧、带宽卡脖子。
先说内存。一个常见的分类模型,如果权重是 FP32,那就是每个参数占 4 个字节。模型不大还好,一旦涉及到检测、分割这类大模型,动辄几百兆字节,很多嵌入式设备根本装不下,更别说还要给激活值、中间缓存留空间。再说算力,大部分端侧芯片的浮点计算单元和整数计算单元数量差距悬殊,同一个芯片上 INT8 的运算速度往往是 FP32 的好几倍,这也是为什么量化之后“跑得快”。最后是带宽,模型推理时要把权重从内存搬到计算单元,内存带宽越窄,搬运时间越长。如果你把 4 字节的参数压成 1 字节,搬运量直接变成原来的四分之一,很多存储带宽瓶颈一下子就解开了。
这三个问题,量化都能从根上缓解。所以它不只是“省内存的小技巧”,而是端侧部署绕不开的核心手段。
1.2 量化的本质:一次带精度的“单位换算”
量化听起来高大上,其实本质就是一次单位换算。我用一个生活化的类比:FP32 相当于一台高精度电子秤,能精确到 0.1 克;INT8 相当于一把带刻度的量杯,只有 256 个刻度。你不可能用量杯称出电子秤那么精细的值,但只要量程设计得合理,绝大多数东西都能量个八九不离十。
数学上,量化就是把浮点数值映射到整数。假设浮点数的范围是 [r_min, r_max],目标整数范围是 [q_min, q_max],比如 INT8 就是 [-128, 127]。量化公式长这样:
q = clamp(round(r / S) + Z, q_min, q_max)其中 S 是缩放因子,Z 是零点。反量化则是:
r ≈ S * (q - Z)S 和 Z 怎么算?也很好理解:
S = (r_max - r_min) / (q_max - q_min) Z = round(q_min - r_min / S)举一个具体例子。假设某层权重范围是 [-1.28, 1.27],要量化到 INT8,那么 S = (1.27 - (-1.28)) / (127 - (-128)) = 2.55 / 255 ≈ 0.01,Z = round(-128 - (-1.28) / 0.01) = 0。 这时候权重里的 0.14 就会变成 q = round(0.14 / 0.01) + 0 = 14,反量化回来就是 0.14。整个过程相当于确定了“1 个整数刻度代表多大的浮点数”,然后做一次四舍五入。
这里的核心矛盾在于:量化一定会引入精度损失,因为 round 这一步把信息丢了。好的量化方案,就是要让这个损失尽量不影响最终任务指标。
1.3 换来的收益和付出的代价
量化带来的收益非常直接。模型体积通常能变成原来的四分之一,因为 32 位变 8 位;推理延迟在很多整数加速单元上能降低一半甚至更多;内存占用和能耗也会显著下降。这些收益在端侧设备上比在服务器上更突出,因为服务器本身算力富裕,省下来的时间未必明显;但芯片资源紧缺时,差距就是“能跑”和“跑不动”的区别。
代价也没法回避。首先是精度可能掉点,尤其对于小模型、检测任务或者分布特别集中的层,掉点会更明显。其次是量化得做额外的工作,至少你得准备校准数据、做精度对比实验。最后是兼容性,不是每一层、每个算子都能被量化工具无缝处理,有些层的计算在整数域里很麻烦,可能需要回退到浮点。
所以我的建议是:不要一上来就把量化当银弹。先搞清楚瓶颈在内存、算力还是带宽,再决定要不要量化、量化到多少位。
2. 先别急着动手:量化方案和关键概念怎么选
2.1 PTQ 与 QAT:训练后量化还是量化感知训练
量化方案按“什么时候做”分成两大类:训练后量化 PTQ 和量化感知训练 QAT。理解这两者,是迈过量化的第一道门槛。
PTQ 的做法是:模型已经训练好了,我不动训练过程,只拿一部分数据做“校准”,统计出每一层权重的数值分布,算出 S 和 Z,然后把模型转成整数版本。优点是快,通常几十分钟就能跑完;缺点是精度损失相对不可控,如果模型原本就很敏感,可能掉点严重。这里说的校准,意指用一小批真实数据在模型里跑一遍前向,记录每层激活值的分布范围——激活范围是从数据里统计出来的,不能凭空猜。
QAT 则在训练阶段就模拟量化误差。做法是在模型里插入“伪量化节点”,前向传播时把权重和激活先量化再反量化,让模型在训练过程中逐渐适应这种精度损失,权重会被优化得更加“抗量化”。精度通常比 PTQ 好不少,但需要你有训练流程、GPU 资源和时间成本。
我自己的选型经验是:模型比较大、任务指标容忍度高的场景,先试 PTQ,如果精度损失在可接受范围就直接用;如果是小模型或者部署到安全敏感的场景,直接做好 QAT 的心理准备。先量化一个副本跑对比实验,再决定投不投入训练成本,这是最稳妥的顺序。
2.2 对称量化与非对称量化:零点“归不归零”的差别
另一组绕不开的概念是对称量化和非对称量化。区别就在零点上。
对称量化把浮点范围映射成对称的整数范围,零点 Z 强制等于 0,公式变成:
q = clamp(round(r / S), q_min, q_max)它的好处是实现简单,很多硬件原生支持;坏处是如果数据分布不对称,比如激活值经过 ReLU 之后全是非负数,范围 [0, 6.2],对称量化却要把 [-6.2, 6.2] 的一段范围白白浪费掉,整数刻度的利用率就低了。
非对称量化则允许零点不为 0,可以让量化范围贴着真实数据的分布走,比如激活范围 [0, 6.2] 会映射到 [-128, 127] 的完整区间,量化分辨率更高。代价是实现复杂一点,推理时多一步零点偏移的计算。
实际落地时有个很常见的配置组合:权重用对称量化,激活用非对称量化。为什么?因为权重分布通常围绕 0 上下展开,对称量化浪费很少;而激活经过各种激活函数之后往往带偏移,用非对称量化更合适。当然,这只是经验配比,具体要对齐目标硬件支持情况。
2.3 校准粒度:per-tensor、per-channel 与 per-group
很多人第一次接触量化时不理解,为什么还要区分 per-tensor 和 per-channel。说白了就是一个网络层里,是一整套参数共用一个 S,还是每个输出通道单独用一个 S。
per-tensor 最简单,整层权重算一个统计范围,计算量小,硬件实现也方便。问题是卷积层的不同输出通道,权重数值分布可能差异很大。想象一下:一个通道权重范围是 [-1.28, 1.27],另一个通道范围是 [-0.01, 0.01],如果共用同一个 S,第二通道的小数值会几乎全部被“压”成同一个整数,信息几乎全丢。
per-channel 则是每个输出通道计算自己的 S 和 Z,相当于给每个通道单独配了一把刻度合适的“量尺”,精度会好很多,这是目前 CNN 量化里非常实用的做法。代价是部分硬件不支持,或者在某些算子实现里会引入额外的计算开销。
后来还有个折中方案 per-group,即把参数分成几个组,每组一个量化刻度。它比 per-tensor 精细,比 per-channel 灵活,在部分低比特量化场景里很流行。选粒度的时候,先看硬件支持列表,再看精度对比结果,别只看理论精度。
2.4 校准方法:min-max、百分位与 KL 散度
刚才一直提到校准,其实就是确定浮点范围 [r_min, r_max] 的过程。范围选得越准,量化精度越好。这个环节常用三种办法。
min-max 最简单,直接取校准数据里观测到的最大值和最小值。它的弱点是对离群点非常敏感,如果某个样本的激活值突然冒出一个超大值,整个量化范围被拉宽,大部分数据的分辨率就会被稀释。
百分位法比 min-max 稳一些,取观测分布的第 0.01 或 0.001 百分位作为边界,主动忽略极端异常值,减少离群点的影响。代价是需要缓存足够的统计量,计算分布。
KL 散度法更讲究,它的思路是:在给定整数精度下,找一个最优截断阈值,让量化前后的分布差异最小。很多主流工具的校准方式就是基于 KL 散度。使用它的时候,校准数据集和分布选择对结果影响更大,样本越有代表性效果越好。
我个人的理解是:min-max 适合快速验证,百分位法适合直觉调整,KL 散度适合作为默认选择。在实操里可以在同一份校准集上分别跑一遍,比较验证集精度来定。
3. 实操记录:从 FP32 到 INT8 的完整流程
3.1 先搭最小验证闭环
讲完概念,接下来是动手环节。我强烈建议不要一上来就接项目里的完整生产链路,而是先搭一个最小验证闭环:模型、校准集、量化工具、评估脚本,四样东西齐了就开跑。
我这里以某开源深度学习框架配合常见推理工具链为例,主要用示意图代码表达流程,因为不同版本 API 名称会有差异,但步骤高度一致。整体流程就是:加载 FP32 模型 → 准备校准数据 → 配置量化参数 → 跑校准 → 导出量化模型 → 对比评估。
model = load_model('model.pth') # 加载原始 FP32 模型 calib_loader = build_calib_loader(data_dir, count=500) # 准备校准数据 quant_model = prepare_ptq(model, calib_loader) # 进入量化模式 quant_model.calibrate() # 执行校准,统计范围 quant_model.export(format='int8') # 导出量化模型 fp32_metric = evaluate(model, val_loader) int8_metric = evaluate(quant_model, val_loader) print('FP32 top-1:', fp32_metric['top1']) print('INT8 top-1:', int8_metric['top1'])这段流程短,但信息量不少。prepare_ptq 不只是把模型“变成”量化模式,它还会把一些算子融合,比如把卷积和后面的 ReLU 合并成一个算子,减少计算和内存访问;calibrate 阶段会跑几次前向,统计每层的激活分布,并据此确定缩放因子。
3.2 校准集准备:别用训练集硬凑
校准集就是用来统计激活值分布的那一批数据,很多人在这里翻车。第一,不要直接拿训练集来校准。训练集和真实推理场景的数据分布不一定一致,而且训练集通常做了大量数据增强,统计出来的分布会“更宽”,导致量化分辨率被稀释。我一般从验证集里均匀抽 500 到 1000 张图片或样本,尽量覆盖真实场景中的各种典型情况。
第二,样本数量不用太多,但要足够“杂”。有人觉得越多越好,其实校准数据和训练不一样,它只统计分布范围。500 张代表性样本通常就够了,再多意义不大。关键是覆盖面要广,比如明暗、大小、远近、不同类别都要有。如果校准集只有“晴天”的图片,部署时来了“雨夜”的数据,分布一偏,量化精度就会出问题。
第三,校准前要把数据预处理流程对齐推理时的流程。归一化参数、尺寸缩放这些不一致的话,校准出来的是另一个分布。
3.3 量化与导出:一天内跑通第一个量化模型
跑通第一个量化模型的目标应该是一天内完成,不要拖。对 PTQ 来说,大多数配置都有默认参数,我的建议是先用“比较稳”的一组默认参数跑通:per-channel 权重量化、非对称激活量化、KL 散度校准。先不管极致精度,拿到量化前后精度对比再说。
导出模型时注意两个事情。一是要确认工具链导出的是不是“真量化”。有些时候导出的模型只是把权重从浮点转成了整数,但计算时还是会“反量化回浮点来算”,这种叫伪量化,速度提升非常有限。真正的整数推理,是整个计算图尽量都保持在整数域里运算。你可以通过工具链的 profile 功能看每一层实际执行的数据类型,一眼就能识别。
二是保留 FP32 的原始模型,不要覆盖。量化模型只是部署产物,后续调参、定位问题都需要原模型。我见过有人把原模型直接覆盖掉,最后只能从 git 历史里翻,非常被动。
3.4 精度评估:量化前后到底差多少
评估是量化流程里最容易糊弄的环节。这里的核心是:必须用和上线一致的数据和指标来对比。
分类任务看 top-1、top-5;检测任务看 mAP;分割任务看 mIoU;如果是特征提取模型,可以看特征向量的余弦相似度。不要只用 loss 值判断,loss 的绝对值下降看不出任务层面的影响。
评估时还要注意,FP32 和 INT8 模型要跑同一份验证集、同样的预处理、同样的 batch size。我曾见过同事对比时,一个用了原始尺寸,一个用了缩放后的尺寸,结果精度差异根本不是量化造成的。
如果量化前后指标差距在 0.5% 以内,通常直接上线;超过 1%,就要去做敏感层定位了。可以把量化误差大的层单独回退到 FP32,看指标能否恢复,借此找到需要混合精度的层。
4. 摸爬滚打后总结的常见坑位
4.1 精度掉点,到底是谁的锅
精度掉点是最常见的现象,但背后的原因五花八门。我排查时有个固定顺序:先看校准集,再看敏感层,最后看量化配置。
校准集不代表性是最容易出问题的地方。如果校准数据和真实数据分布存在差异,统计出的范围就不准。简单验证方法是:换一批校准数据重新跑一遍,如果精度波动很大,说明校准集没选好。
敏感层问题也常见。有些网络层对量化特别不友好,比如检测任务里的回归头、分割任务的上采样层、注意力机制里的 Softmax 与矩阵乘,这些层稍微量化一下就会放大误差。定位方法很简单:逐层或者逐块做量化回退实验,把某一层保留 FP16,其他层量化,看谁对精度影响最大。找到敏感层之后,配合混合精度方案解决。
还有一种情况是量化配置本身不合理。比如激活用对称量化、权重用非对称量化,又比如 per-tensor 在通道差异大的层上用了,都会造成额外损失。把配置切换成 per-channel 权重、非对称激活通常就能改善。
4.2 校准集和真实场景分布不一致
这个问题在部署后才会暴露——量化时验证集精度挺正常,一上真实环境就掉点。典型的例子是图像分类模型在校准时只用了室内光线数据,上线后遇到户外强光、夜间低光,分布直接漂移。
解决思路有两个方向。一是让校准集更贴近真实分布,尽量从线上日志或代表性场景里采集样本;如果做不到,考虑做一定程度的数据增强再校准。二是给模型保留少量“灵活性”,比如把输入输出层或对分布敏感的层留在高精度,让整体模型对分布漂移不那么敏感。
另外提醒一句,量化模型的分布漂移比 FP32 模型更剧烈。因为浮点模型对输入分布的容忍度高,量化后每个值都经过了刻度映射,分布一偏就可能导致大量值落在一个较差的量化区间里。
4.3 量化后速度反而更慢
很多人第一次量化后会困惑:模型变小了,为什么推理时间没有变短,甚至更长了?这种情况通常是“量化没有真正落地”。
第一种可能是硬件不支持整数加速算子。有些平台对 INT8 卷积有优化,但对 INT8 的某些激活函数、上采样、拼接操作没有原生支持,于是推理引擎只能把数据“反量化回浮点”来计算,算完再“量化回去”。来回转换的开销可能比省下来的还大。方案是检查 target 平台的算子支持表,尽量把不支持算子回退到 FP16,而不是跟着整个网络一起转 INT8。
第二种可能是模型太小,带宽收益不明显。对于已经很小或计算量很低的模型,量化的主要收益是体积和内存,推理速度未必有质的提升。量化一个非常轻量的分类模型,经常出现 INT8 和 FP32 差不多的情况,因为耗时大头可能在预处理或者数据搬运。
第三种可能是高层 API 默认配置不适合当前芯片。有些推理引擎需要手动指定“推理设备”和“精度策略”,如果默认策略是 FP32,你量化了个寂寞。性能调优时必须看 profiler 输出,确认实际跑起来的每一层是什么精度。
4.4 算子不支持导致的“伪量化”
伪量化是一个隐蔽的坑。从模型文件看,权重是 INT8 了,但推理引擎拿到之后先把权重反量化成浮点,再用 FP32 计算,最后输出。表面上是量化模型,实际上计算没有省。
出现这种情况,往往是不支持某些自定义算子、某些动态形状的分支、或者注意力类算子。排查办法就是前面提过的 profiler:看每一层的实际执行精度。如果发现某几层全部是 FP32,那它们就是干掉性能的元凶。
处理方式分为几类:一是用算子融合把不支持的算子合并到支持的操作里;二是把不支持的层调度到浮点通道混合执行;三是修改模型结构,用更友好的算子替换。多数情况下,混合精度方案足够解决问题,不必为了让整个模型“全 INT8”而大改结构。
5. 进阶玩法:混合精度与量化边界
5.1 混合精度:让敏感层留在高精度
当全模型 INT8 精度不达标时,下一步通常就是混合精度,也就是让某些层保持 FP16 甚至 FP32,其余层走 INT8。相比全量化,混合量化通常在精度和速度之间取得比较好的平衡。
怎么决定哪些层保留高精度?我的经验是:先对关键层做精度实验,再结合 profiler 看哪些层延迟占比高。延迟占比高、量化误差小的层优先量化;延迟占比低、但对精度影响大的层,就保留高精度。理论上这样性价比最高。
输入输出层也值得单独考虑。输入层的通道数少、计算量小,量化与否对速度影响不大,但保留 FP32 往往能让模型对输入分布变化更稳健。输出层同理,直接决定最终数值质量。
另外,混合精度方案的“整数/浮点切换”会产生一定的转换开销,所以不要把模型切得太碎。一般以模块为单位切,比如某个残差块整体走 INT8,某个检测头整体走 FP16,减少切换次数。
5.2 量化和剪枝、蒸馏怎么配合
在部署优化这个领域,量化经常不是单独使用的。模型压缩有三大件:剪枝、蒸馏、量化,它们可以叠加使用。
剪枝是把不重要的连接或通道删掉,减少计算量;量化是在剩余结构上进一步降低精度。这两者叠加时有个顺序问题:一般建议先剪枝再量化。剪完的模型更小,量化后的精度损失也可能更小,但剪枝比例过大会让模型变得过于脆弱,量化掉点反而更严重。
蒸馏则是用一个大模型“教”一个小模型。如果蒸馏和量化一起用,可以在训练阶段就让小模型适应量化误差,通常做法是:teacher 是 FP32 大模型,student 是量化感知训练的小模型,这样训练出来的模型精度往往比“先普通训练再量化”好很多。
如果你做 QAT,可以顺便把蒸馏损失加上,几乎不增加太多训练成本,但精度提升明显。
5.3 低比特量化的现实约束
INT8 是目前最主流的量化位宽,因为绝大多数硬件对其支持最成熟。但最近 INT4、甚至混合的 INT4/INT8 方案也逐渐变多,主要是为了进一步压内存和带宽。
INT4 带来的体积收益更诱人,但现实约束不小。一是硬件支持参差不齐,很多平台对 INT4 乘法的加速能力有限,甚至根本没有;二是精度损失显著,一般需要更强的 QAT 手段和更复杂的混合精度策略来兜底。有人会在权重上试 INT4,在激活上保留 INT8,也算一种折中方案。
我个人建议,除非目标硬件明确支持且你的任务精度宽容度很高,否则先老老实实把 INT8 做扎实。低比特是优化方向,不是默认选项。先把 INT8 的校准、混合精度、算子融合这些都调明白了,再去试低比特才有意义。
最后分享一点个人体会。量化这件事,入门门槛并不高,几行配置就能跑通一个 PTQ,但真正把它调好,需要对模型结构、数据分布和底层硬件都有理解。我踩过最深的坑从来不是公式看不懂,而是校准集随便准备、量化配置照抄默认、评估不做对齐——这三个问题不解决,量化结果就是碰运气。如果你正在做部署项目,建议花点时间把“校准集怎么选”“敏感层怎么找”“profile 怎么看”这三件事挨个实践一遍,之后遇到量化问题都会顺很多。