模型量化这个话题,但凡做端侧AI部署的人迟早都要碰到。我在某移动芯片平台上折腾AI推理的时候,第一次用其官方SDK跑量化模型,最直观的感受就是:模型文件小了四倍,推理速度提了两倍多,板子不烫了,电池也扛得住了。但前提是,你得先搞懂量化到底在干什么。所以这篇把原理讲透,安装和实战我放到下一篇单独展开。
1. 模型量化到底在做什么
量化这个词听起来高大上,其实本质特别朴素:把精度高的数字换成精度低的数字。打个比方,一张高清照片你看着颜色细腻、细节丰富,但你要是把它打印成报纸上的黑白图片,色彩信息肯定丢了,但轮廓形状还是能看清。模型量化就是类似的逻辑——把神经网络里的浮点数参数,换成整数,让模型变轻、变快,代价是牺牲一点精度。
神经网络训练和推理的默认数据类型是FP32,也就是32位浮点数。这个精度很高,能表达的数字范围极大,小数部分也很精细。但问题来了,参数动辄几百万个,每个都要占4字节内存,推理时还要做大量浮点运算,这在云端GPU上不算什么,但在手机、摄像头、耳机这些端侧设备上,就是灾难。
量化最主流的做法是INT8,也就是8位整数。每个参数只占1字节,模型体积直接缩到原来的四分之一。更重要的是,整数运算在专用硬件上可以用乘累加阵列并行执行,速度比浮点快一大截,功耗还低。这原理其实每个人都能懂,真正常被卡住的是后面这些细节:怎么把浮点映射成整数,映射完精度损失多少,为什么有的模型量化完还是准的,有的直接就废了。
这里需要建立两个核心公式。正向量化,把浮点x变成整数q:
q = round(x / scale) + zero_point反向反量化,把整数还原成浮点,虽然是近似值:
x_approx = (q - zero_point) * scale其中scale是缩放因子,zero_point是零点偏移。这两个参数怎么算,用什么方式算,基本就是所有量化工具链的核心逻辑。理解了这两个公式,你再看任何一家平台的量化文档,都会觉得顺眼很多。
2. 量化方案选型背后的尺寸考量
实际工程里,量化方案不是拍脑袋定的,而是根据目标硬件特性、算子类型、精度要求综合选出来的。这里重点拆解三个最关键的维度。
2.1 对称量化与非对称量化
对称量化最简单,zero_point固定为0,公式直接变成q = round(x / scale)。假设你有一个浮点范围[-a, a],那么scale就是:
scale = a / 127为什么要除以127而不是128?因为INT8的取值范围是[-128, 127],对称量化要保证浮点0精确对应整数0,所以负数侧能多用一个-128,但正数侧只有127。有些工具会强制用[-127, 127]来保证绝对对称,但代价是丢掉一个量化级别,损失很小但没必要。
非对称量化则允许浮点范围不从0开始,比如某个激活值分布在[0.1, 3.2],那就可以用scale = (3.2 - 0.1) / 255,再配一个zero_point来对齐。这样整个动态范围都被充分利用了,误差更小。
我个人的经验是:权重一般用对称量化就够了,因为训练好的权重分布通常接近关于0对称的钟形分布;激活值就不好说了,尤其是经过ReLU之类的函数之后,分布完全偏到正半边,这时候还用对称量化就是在浪费一半的编码空间,精度损失会非常明显。所以好多推理框架的默认配置就是权重对称、激活非对称,这不是拍脑袋定的,是大量实验测出来的选择。
2.2 张量粒度
量化粒度的意思是一个scale管多大范围的参数。最粗的是per-tensor,整个张量共享一个scale。最细的可以细化到per-axis或者per-channel,每个输出通道都有自己独立的scale。
卷积层的权重形状一般是[输出通道, 输入通道, 高, 宽]。不同输出通道的数值分布可以差很远,比如某一层第一个通道的值都在0.1附近,第32个通道的却集中在10左右。如果整个张量共用一个scale,那就相当于拿一个尺子去量所有东西,小数值通道的误差会大得离谱。per-channel量化可以有效解决这个问题,但代价是硬件要能支持这种粒度,否则反量化逻辑会变得很复杂,推理速度不升反降。
实践中的建议很简单:模型里的卷积权重优先用per-channel,激活值用per-tensor就够。大部分主流推理引擎也是这么做的,比如常见的tflite格式,其量化规则就是权重per-channel、激活per-tensor。真没必要你对每个tensor都上per-channel,硬件不支持,收益也边际递减。
2.3 校准方法的选择逻辑
校准的意思,是统计模型运行时每一层激活值和权重的真实数值范围,然后确定scale和zero_point。这不是随便拿训练数据跑一遍就完事,统计方式直接决定了量化误差的分布。
最常见的几个方法:
- MinMax:直接取统计到的最大值和最小值作为范围。实现最简单,但容易被离群值带偏。所谓离群值,就是极少数特别大或特别小的点,往往来自数据里的异常样本或者过渡阶段的激活跳跃。一旦把它们算进范围,绝大多数正常数值的量化步长就变大了,相当于白白牺牲了精度。
- Percentile:取某个百分位,比如99.99%,主动把极端的0.01%截断掉。这个方法实际用下来比MinMax稳很多,因为深度网络里离群值比想象中常见得多。但百分位选多少,不同层不一样,得试。截断太狠,信息损失反而更大,截断不够,离群值照样祸害全局。
- MSE和KL散度:这类方法的思路是找最优截断点,让量化前后的分布尽可能接近。具体做法是:枚举不同的scale,计算量化前后概率分布的KL散度或平方误差,取最小值的那个scale。这个是很多工具(比如炼丹框架内置的PTQ工具)默认采用的方式,因为效果好,计算量可控,几百个样本就够用。
校准数据集的选择同样关键。不需要用全部训练集,几百张有代表性的图就足够了,但一定要覆盖真实部署场景中可能出现的分布。一个坑我踩过:之前用一个全是清晰正面照的数据集去校准一个监控场景的人脸检测模型,校准出来的量化模型在阴天、逆光、远距离这些真实场景下精度直接崩盘。后来换成混合场景数据重新校准,精度才恢复正常。校准数据在精不在多,但要贴近真实应用。
3. 量化误差从哪来,怎么控制
模型量化之后精度下降,是个必然事件,只是幅度问题。误差来源可以拆成两种,数学上很好理解。
第一种是舍入误差。浮点数映射到整数,最后一步round必然有舍入。比如scale=0.5,浮点值1.3会被量化成round(2.6)=3,反量化回来是1.5,误差0.2。这是所有量化方案都无法完全消除的,只能通过选对scale来缩小步长。舍入误差像你记账时把所有金额都四舍五入到元,单笔错不了太多,但笔数一多,累积起来就不好看了。
第二种是截断误差。量化能表达的整数范围是有限的,超出范围的浮点值直接一刀切到边界。比如INT8范围是[-128, 127],某层激活值是128,但quantize范围上限设成了100,那这个值就被截断成100。反向量化回来是100,误差达到28。这个误差严重的时候,信息几乎是扔掉的,所以校准方法的核心任务就是如何最优地选择截断点,让超出范围的概率尽量低,但范围又不要太宽导致步长变大。
权重误差和激活误差还有一个区别。权重误差是静态的——模型加载后权重就固定了,误差是确定性的;激活误差是动态的,取决于每个输入样本,在不同的推理请求之间会波动。所以激活量化通常对校准数据更敏感,更容易出问题。
还有离群值问题。模型训练到后期,某些层的激活分布会出现明显的长尾,个别值达到了正常值几十倍的量级。如果你用MinMax统计范围,那正常的数值区间就被压缩得极窄,量化步长变小,所有正常值都被舍入得七零八落。应对手段就是前面说的截断式校准,不管是百分位法还是熵方法,本质上都是在跟你讲一个道理:别被极端少数绑架。
误差控制方面,最有效的一招是混合精度。不是所有层都适合INT8,有些层天生对量化极其敏感,量化掉一点精度就崩。敏感层的特征一般是:参数量不大,但输出对后续整体结果影响极大,比如检测框回归分支、关键的注意力层。处理办法是这些层保持FP16甚至FP32,其余层用INT8。代价是模型体积和速度优势被削掉一点,但换来的是精度基本无损。
再补充一个实操里很有用的技巧:先逐层分析误差。把模型冻结下来,逐层把某一层替换成量化版本,其余保持浮点,观测整体精度变化。这样跑一轮,哪些层是“量化敏感层”一目了然。我见过的很多量化失败案例,最后都是靠这种逐层排查定位到一两层害群之马,然后单独处理才挽救回来的。
4. 端侧硬件与AI Engine Direct SDK的匹配逻辑
理解了量化原理,再来看端侧部署就顺理成章了。移动芯片上的CPU、GPU、NPU、DSP这几种计算单元里,最吃量化的是NPU和DSP这类专用加速器。它们为了追求能效,内部的乘累加单元只做了整数运算,浮点要么不支持,要么效率极低。而CPU和GPU虽然也支持浮点,但跑浮点的能效比远不如整数。
所以说白了,在端侧芯片上部署AI模型,量化不是可选项,而是必选项。你想调用NPU的算力,就得把模型搞成它认识的样子,也就是量化或者半精度。
这里要重点提一下某主流移动芯片平台提供的AI Engine Direct SDK。这套推理SDK的思路是上层提供统一API,下层把计算调度到合适的计算单元上:简单任务走CPU,图形相关的走GPU,重型推理走NPU。而NPU那部分,能效最高,但约束也最多。约束主要就体现在数据格式上:NPU只能高效处理特定格式的数据,低比特量化就是其中之一。
从实际操作看,用这套SDK做量化部署有几个关键点:
- 模型格式转换。训练好的模型通常要转成该SDK支持的模型格式(比如某种轻量级格式或ONNX等再转),转换过程本身就可能引入计算图变化和精度损耗,要先确认转换这一步的误差几乎为零,再开始量化,避免两个误差叠加难以排查。
- 算子支持范围。不是模型里所有算子都能被加速器接受。比如某些自定义算子、较特殊的归一化算子,可能不被支持而回退到CPU执行。一旦发生这种情况,模型就会被拆成多段,整体推理性能会大打折扣,所以要么改模型结构去适配,要么接受CPU兜底的开销。
- 量化参数的对齐。SDK里有些模型优化选项会做常量折叠、算子融合等,量化的统计过程最好在最终优化后的图上重新做一遍,否则统计的是原始结构,优化之后数值分布变了,精度对不上。
这些坑说实话文档里都写了,但没实际踩过一遍,很难真正体会“为什么我说要先理解原理再去碰工具”。你懂原理之后,看到一个量化后精度下降的模型,至少知道往哪几个方向去查、去调。不懂原理的话,就只是对着文档改参数瞎试,效率极低。
模型体积、推理延迟、功耗这三者的权衡,基本就是端侧AI所有工作的核心矛盾。量化把三个方向都往好的带:体积变小,带宽占用降低;整数运算更快,延迟下降;能效比提升,发热减少。这也是为什么现在头部芯片厂商、系统平台、AI框架全都花了大力气把量化工具链做成标配。而搞明白原理,是使好这套工具链的前提。
5. 实操心得与避坑记录
最后分享几个我在实际项目中积累的经验。
- 校准数据千万别只挑“好看”的样本。很多人随便挑几张清晰端正的图就算校准完,结果真实场景一跑,精度大跌。正确做法是覆盖模糊、低光、遮挡、旋转等现实情况的样本,宁可数量少,分布必须全。
- 确认反量化是在哪里做的。有的硬件是把INT8送进计算单元,出来再反量化为浮点,有的是内部把weight反量化成FP16再算。不同实现策略,同样的量化参数,效果可能完全不同。一定要查清楚你的目标后端实际执行的数据通道。
- 别贪心一次全量量化。先把全模型量化跑通,确认部署链路没问题,再逐层做精度分析和混合精度优化。一步到位往往会让问题混合在一起,排查时根本不知道是量化错误还是转换错误还是运行时错误。
- 模型源码里做过的一些奇奇怪怪的操作,比如数值截断、自定义clip、奇怪的reshape顺序,量化前最好先看清楚,很可能会让校准统计值和实际运行值不一致。
- 版本之间要注意差异。同一套SDK不同版本之间,量化结果可能不一样,因为底层算子实现、融合优化规则在变。换版本之后,即使代码没动,也建议重新做一轮精度测试。
我最早接触量化的时候,也犯过低级错误,想都不想直接套默认参数,然后对着评测结果挠头好久。后来老老实实从原理开始看,反而干活顺了。有一说一,原理这东西,你今天学可能觉得抽象,但到了排查问题的时候,它就是你的地图。地图在手,路就好走多了。
这篇先到原理为止,你能把对称非对称、per-channel粒度、校准方法、误差来源这四件事想明白,量化就算入门了。下一步我会写具体的SDK安装、模型转换、量化参数设置和上板实测,到时候咱们实操见。