拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

神经网络量化与反量化:INT8部署的精度排查实战

神经网络量化与反量化:INT8部署的精度排查实战

先给结论:神经网络量化这事儿,这些年被包装得有点玄乎。市面上讲量化的文章,要么堆公式把人劝退,要么直接甩工具链截图让人照抄,真正把“量化是什么、反量化为什么存在、量化后精度掉了该怎么查”这串问题讲明白的,不多。我前后在端侧推理、边缘设备部署上折腾了快三年,从ONNX Runtime的INT8量化,到RKNN的NPU部署,踩过的坑能排成两列。这篇文章想做的,就是把这些经验摊开来:从量化最底层的数学关系,到反量化的真实作用,再到精度掉点时的完整排查思路,一步一步讲清楚。适合正在做模型部署、刚接触量化的工程师,也适合那些已经被“量化后数值不动”这类问题折磨过的朋友——看完你就知道,绝大多数坑都有迹可循。

1. 量化到底在干什么:从“用整数近似浮点”说起

1.1 先理清量化解决的核心矛盾

神经网络训练的时候,权重和激活值基本都用FP32(32位浮点数)存。FP32的精度高,动态范围大,训练时反向传播的梯度更新需要这个精度,这没问题。但模型要落地部署,尤其是跑到手机、摄像头、开发板这类边缘设备上时,FP32就成了负担。

举个例子:一个ResNet50模型,FP32权重文件大概98MB。这个大小放在服务器上不算什么,但放到一个内存1GB的嵌入式设备上,光模型权重就占了近十分之一,再跑推理时的中间激活值,内存直接吃紧。更麻烦的是带宽——边缘设备的内存带宽本来就有限,每从内存读一个FP32数,就是4个字节,这些数据要喂给计算单元做矩阵乘,带宽往往比算力更先成为瓶颈。

所以量化的核心动机就一个字:省。省内存、省带宽、省计算量。把FP32的权重和激活值压缩成INT8(8位整数),权重体积直接缩到四分之一,矩阵乘法从浮点运算变成整数运算,这在很多硬件上都是数量级的加速。

而且神经网络有个特点:它对输入里的噪声和小扰动有很强的鲁棒性。你给一张图片稍微加点噪点,模型输出的分类结果大概率不变。量化本质上就是给网络的权重和激活引入一层“可控的噪声”,只要这层噪声不超过网络自身的容错范围,精度损失就能控制在可接受区间。这也是量化能成立的底层前提。

1.2 从FP32到INT8,省下来的到底是什么

很多人以为省下来的只是“文件变小”,其实远不止。我把推理过程拆开给你看:

  • 存储省了四倍:权重从4字节变成1字节,模型体积大幅缩小,能塞进更小内存的设备。
  • 带宽省了四倍:内存读取的数据量少了,计算单元“等数据”的时间变少,吞吐量提升。
  • 计算更快:INT8的矩阵乘,在支持SIMD指令的CPU或NPU上,能比FP32快好几倍;很多NPU(比如瑞芯微RK3588上的NPU)甚至只支持INT8推理,FP32模型根本跑不起来。

这三条里,最容易忽略的是带宽。实际做嵌入式优化时你会发现,很多模型的推理瓶颈不在算力,而在数据搬运。把数据从DDR搬到片上缓存的速度,往往决定了整个推理的耗时。量化的收益,在带宽受限的场景中体现得比算力更明显。

1.3 反量化不是“还原”,而是另一面镜子

聊量化必然绕不开反量化(Dequantization)。很多资料把反量化解释成“把量化后的整数还原回浮点数”——这个说法我一开始也这么理解,直到在调精度问题时才发现,这种理解太容易误导人了。

量化是浮点到整数的映射,反量化是整数到浮点的映射,但它们不是互逆的。浮点数经过量化变成整数,信息已经丢失了,反量化只能把这个整数映射回一个浮点值区间内的代表值,不可能精确还原成原来的那个浮点数。这就好比把一张高清照片压缩成JPEG再解压回来,解压后的图看起来像原图,但像素值已经对不上了。

明白这一点,再看“量化后精度下降”这个问题,视角就完全不一样了——精度下降是必然的,只是程度问题。我们做的所有优化,都是在最小化反量化后的数值与原始浮点值的偏差,而不是消灭偏差。

2. 量化参数背后的数学关系:scale、zero point与对称性

2.1 核心公式拆解:从浮点到整数

量化公式的核心是两组参数:scale(缩放因子)和zero point(零点)。公式长这样:

r = s × (q - z)

其中r是原始的浮点值,q是量化后的整数值,s是缩放因子,z是零点。反量化(整数转浮点)用的就是上面这个公式,而量化(浮点转整数)则是它的逆过程:

q = round(r / s + z)

s的计算方式也很直接。假设原始浮点数据的范围是[min, max],量化目标范围是[q_min, q_max](INT8 就是[-128, 127]或[0, 255]),那么:

s = (max - min) / (q_max - q_min) z = round(q_min - min / s)

这里的“范围”是最关键的变量。它取多少,直接决定量化的精度。如果范围取得太大,浮点值在映射到整数时就会“挤”在一起,量化误差大;如果范围取得太小,超出范围的值会被截断,产生大量饱和。

这个取范围的环节,在专业术语里叫calibration(校准),是量化里最讲究的步骤,后面我会单独展开。

2.2 对称量化和非对称量化:为什么权重和激活不一样

对称量化要求零点z = 0,公式简化为r = s × q。这意味着浮点数据的正负范围必须对称,比如[-6, 6]而不是[-4, 8]。非对称量化不要求这个,可以完整覆盖[min, max],精度上限更高。

我在实操里有一个实际体感:权重适合对称量化,激活适合非对称量化。原因也很简单:

  • 权重训练完后通常围绕0附近近似对称分布,正负范围差不多,用对称量化不会浪费太多量化范围。
  • 激活值经过ReLU这类激活函数后,通常是单侧分布(比如全为正值),如果强行对称量化,会导致一半的量化范围被浪费掉。非对称量化能更充分利用INT8的256个级别。

这里有个常见误解:不少人觉得对称量化精度一定比非对称差。其实不一定,如果数据本身就是对称的,对称量化反而更省事(不用存zero point,计算也少一次减法)。很多推理框架默认对权重用对称量化,对激活用非对称量化,就是这个道理。

2.3 per-tensor和per-channel:精度与计算量的取舍

量化粒度是另一个容易被忽略的维度。per-tensor量化是整个张量共用一个scale,per-channel量化是张量的每个通道各用一个scale。

以卷积层为例,假设输入是[N, C, H, W],权重是[M, C, K, K](M为输出通道数)。per-tensor量化对整张权重算一个scale和zero point,简单但精度差——因为不同的输出通道,权重数值分布差异可能很大,共用一个scale会放大误差。per-channel量化则对每个输出通道单独算scale,精度明显好得多,代价是推理时需要多做一些处理,对硬件有一定要求。

我自己的经验是:纯CPU部署用per-channel量化,问题不大;但如果是NPU部署,一定要查清楚工具链是否支持per-channel,否则很可能被隐式降级为per-tensor,导致精度掉得莫名其妙。

此外还有分组量化(group quantization),在LLM量化场景很常见(比如GPTQ的group size 128),属于在per-tensor和per-channel之间取折中。这个在边缘CNN部署里用得少,但如果你的模型有全连接层对大范围矩阵乘敏感,可以留意下。

3. 量化的两种落地路线:PTQ与QAT

3.1 PTQ:事后转换的轻量路线

PTQ(Post-Training Quantization,训练后量化)的思路很简单:模型训练完,我不动训练过程,直接对模型做转换。转换时准备一小部分校准数据(比如500到1000张验证集图片),喂给FP32模型做前向推理,统计每层激活值的范围,然后计算scale和zero point,最后把浮点权重映射成整数。

PTQ的最大优势是快,几乎不需要额外的工作量,一个脚本跑完就出结果。而且很多工具链把PTQ做成了傻瓜式接口:

  • ONNX Runtime的onnxruntime.quantization.quantize_static
  • OpenVINO的ov.quantize_model
  • RKNN Toolkit直接支持PTQ配置

适合PTQ的场景:模型本身就是从大模型蒸馏出来的、精度余量大;或者部署设备对精度要求相对宽松(比如分类任务Top-1掉个0.5%以内都不影响)。在大多数CNN分类模型上,PTQ + INT8通常能维持在FP32精度的99%以上,只有遇到一些小模型、轻量模型(比如MobileNet系列部分变体)才会明显拉胯。

3.2 QAT:训练感知量化

QAT(Quantization-Aware Training,量化感知训练)走的是另一条路:在训练过程中就“模拟”量化的效果。它会在模型里插入伪量化节点(fake quant节点),前向传播时先对权重和激活做量化再反量化,让模型在训练阶段就“见识”到量化带来的误差,从而自适应地调整权重,让模型对量化更鲁棒。

QAT最核心的技术点是直通估计器(STE,Straight Through Estimator)——量化函数在数学上不可导(round函数的梯度几乎处处为0),反向传播如果按真实梯度传,权重根本更新不了。STE的做法是:前向传播用真实的量化,反向传播跳过round操作,把梯度直接传回去,相当于“假装”量化函数是恒等的。

QAT的精度通常比PTQ好,但代价不小:

  • 需要重新训练模型,少则几个epoch,多则整个训练流程再来一遍;
  • 需要训练脚本支持伪量化,改造现有训练代码;
  • 需要维护训练和部署两套流程,工程复杂度上了一个台阶。

适合QAT的场景:模型本身精度就比较紧张(逐层误差会积累到不可接受);部署的目标硬件对量化要求极为严苛(比如某些NPU强制per-tensor且只支持特定激活函数);或者模型结构里有一些对量化极其敏感的操作(比如检测头的边界框回归分支)。

3.3 到底怎么选:我的一般判断逻辑

我一般按这个顺序做判断,省时间也省心:

判断条件选择
时间紧,精度余量足直接PTQ,先跑通全流程
PTQ后精度掉得不多(<1%)用PTQ,优化校准集
PTQ掉点明显(>1%)先做敏感层分析和混合精度,仍不理想再转QAT
目标硬件特别吃量化精度直接上QAT,别折腾PTQ

很多团队一上来就上QAT,这是浪费。实际上大部分CNN模型PTQ就能搞定,先通过工具链把它跑通,结合校准集调优,精度大多能回到可接受范围。真到了PTQ救不回来的那一步,再考虑QAT不迟。

4. 反量化不是“还原”,而是带误差的近似重建

4.1 推理过程中量化/反量化发生在哪里

很多人以为量化部署就是“整个模型都变成整数计算”,实际不是。更准确的图景是:模型在计算图层面被插入了一组Q(Quantize)和DQ(Dequantize)节点,在各个算子之间传递的数据,交替出现在整数域和浮点域之间。

以ONNX Runtime的INT8静态量化为例,它会在卷积层的输入边插入Q节点(把浮点输入转INT8),在输出边插入DQ节点(把INT8结果转回浮点)。这样从计算图看,模型大部分算子的核心计算(比如Conv的矩阵乘)跑的是INT8整数运算,但节点之间传的数据在某些阶段仍然是浮点格式。

更深处还有个细节:很多推理框架会做算子融合,把Q -> Conv -> DQ融合成一个量化算子,减少不必要的“量化-反量化”切换。这个融合做得好不好,直接影响推理性能。如果一个框架对某个算子的融合支持不完整,它可能就在那个算子处来回切整数浮点,性能掉得厉害。

4.2 为什么“数值不动”不等于没有误差

这个点我要专门拿出来讲,因为几乎所有做量化部署的人都遇到过这个诡异现象:量化后一跑,输出数值跟FP32完全一样,心里还窃喜“我的模型量化无损”,但换个模型、调个参数,精度就崩了。数值不动,到底说明什么?

先要排除几种“假性不动”:

  1. 模型根本没有走量化路径。这是最常见的原因。工具链校准完,你以为模型量化了,实际上它跑的还是浮点算子——比如算子不支持INT8被回退到CPU的浮点实现。此时输出当然和FP32一模一样,因为推理根本没发生量化。

  2. 最后一层没量化。对于分类模型,Softmax前的Logits如果没被量化,最终输出自然不变。但这不代表中间层没有量化误差——中间层的误差被全连接层和Softmax“吞”掉了,不体现出来而已。

  3. 校准范围刚好覆盖了所有激活值。如果校准数据集代表性强,某些层的范围统计得比较准,量化误差小到在输出层体现不出来。这个情况是真的“量化成功”。

所以“数值不动”本身不是坏事,关键是要确认是哪种“不动”。如果是第1、2种,你其实没有量化;如果是第3种,恭喜你,量化很成功。

4.3 误差是怎么逐层累积和扩散的

理解了反量化的本质,再看精度问题就顺了。量化误差从输入到输出是逐层累积的:第一层有量化误差,输出喂给第二层,第二层在它自己的量化误差之上继续叠加,逐层放大。

在一个50层的ResNet里,如果每层的相对误差只有0.1%,理论上累积50层后,最坏情况会很吓人。但神经网络结构的冗余性和批归一化(BatchNorm)的调制作用会让误差被部分吸收,所以实际表现没那么差。可一旦遇到结构比较“脆”的模型——比如深度可分离卷积占主导的MobileNet,或者对数值极其敏感的检测回归头——误差累积效应就会立刻显现。

我实测过一个例子:同一个YOLOv5s检测模型,PTQ量化后,分类分支精度几乎不掉,但边界框回归分支的坐标输出偏移明显增大。原因就是回归任务对数值微小扰动比分类任务敏感得多,坐标值差个0.01,映射到原图就是几个像素的偏差,目标框位置就跑了。

5. int8量化后精度掉点与数值异常的排查链路

5.1 先判断是“假掉点”还是“真掉点”

进行任何优化之前,先做一个最简单的实验:把一个测试样本分别喂给FP32模型和量化模型,对比每一层(或者中间特征图)的输出。

具体做法是:在ONNX Runtime里分别加载FP32和INT8模型,开启IOBinding或把中间层指定为输出,拿到同一输入下某一中间层的tensor,然后逐一对比。第一次做这个工作的人,建议直接用工具脚本化跑,保存成JSON或NPY逐层对比即可。

这一步的作用是把“哪一层开始出现偏差”定位出来。如果第一层就偏差很大,问题多半在输入预处理或校准集的分布上;如果前面好好的、某层之后开始漂,问题大概率在那层对应的算子上。

5.2 数值完全不动的最常见原因(工具链视角)

前面提过“数值不动”有三种情况,这里给出更具体的排查路径。

先检查工具链的日志。以RKNN Toolkit为例,转换模型时会打印每个算子是否被NPU成功解析、是否有算子回退到CPU运行。如果你在日志里看到NOT SUPPORT、FALLBACK、UNKNOWN这样的字样,那基本就印证了:模型部分算子没有被量化支持,走了浮点路径,整图根本没有真正量化起来。

再检查输入端。有些框架对RGB/CHW/BGR的布局有硬性要求,输入数据格式不对,模型会走预处理的前处理算子,而这些算子往往是浮点实现,导致从最前端开始就没有量化。这种问题看起来是“量化后精度正常”,实际上模型压根没被你量化到。

还有一种情况:框架在校准和推理时对是否开启量化做了隐式控制。比如某些Runtime,只有显式设置enable_quant=True才会真正调用量化核函数,否则即使模型文件是INT8,运行时也静默回退到浮点。这种坑最隐蔽,排查时先去看推理时的配置和日志,别急着调模型。

5.3 精度“真掉”的时候怎么处理

排除了假掉点,剩下的就是真精度损失。我的排查顺序是:

  • 先看敏感层。工具链通常会输出每层的量化误差(比如SNR或MSE),误差大的层优先处理。如果你用的是ONNX Runtime量化,可以自定义Calibration方法,或者把误差最大的几层单独设成更高的位宽(部分工具链支持混合精度)。
  • 次看校准集。校准集数量太少、和真实部署时的输入分布差太远,会导致范围统计失准。校准集最好从真实业务数据里采样,别只用训练集里挑几张好看的图。500到1000张是一个比较稳的起步量级。
  • 再看特殊算子。有些算子在INT8下极其容易出错,比如Sigmoid、Softmax、Exp这些非线性函数,计算图里常常需要特殊处理(查表量化或保留浮点)。如果这类算子出现在关键路径上(比如检测头的输出分支),精度受影响会很大。
  • 最后考虑结构问题。如果以上都调了,精度仍然不行,那就是模型本身对量化太敏感。这时候要么切QAT,要么考虑对关键分支参数做敏感性分析和部分层回退。

我遇到的一个典型case:某回归模型在RK3588上,FP32精度正常,INT8量化后输出数值几乎不变(注意,是“几乎不变”,不是完全不变),但损失函数的评估掉得离谱。后来逐层对比发现,误差集中在最后一个全连接层之前的特征图上,特征值范围极小(比如0.001到0.01),而校准工具默认的范围统计对极小值分布不敏感,量化时把这些值直接压到0附近,信息全丢了。处理方法是把该层的scale手动调小,或者改用自适应范围校准,精度立刻恢得差不多。

这个case特别典型,说明“数值不动”和“精度掉”是可以同时出现的:某一层的中间输出因为量化被压没了,但最终输出经过后续算子的缩放,看起来没怎么变,实际上模型的行为已经变了。

6. 部署实操中容易忽略的几个关键细节

6.1 校准集覆盖不了真实分布,再好的量化也白搭

校准集是PTQ里最容易出问题的环节。很多人直接拿训练集的验证集前100张图做校准,这通常没问题,但如果训练集和真实业务数据分布差距很大,校准统计出来的范围就会失真。

举个实际例子:在智能安防场景里,训练数据大多是白天拍摄的图片,而部署环境大量出现在夜间或低光照场景。用白天图片做校准,统计出的激活值范围会偏向高亮度分布;真正跑夜间图时,激活值大量落在量化范围的边缘,截断误差巨大。

所以校准集的选取原则很简单:必须需要贴近真实部署输入分布。如果拿不到真实数据,至少要做充分的增广,把光照、噪声、角度这些因素都覆盖进去。

另外校准集的数量也不是越多越好。我见过有人拿1万张图做校准,效果反而不如500张。原因是范围统计容易受离群点影响,更多数据会引入更多离群值,把范围撑大,导致量化分辨率降低。一般500到1000张是经验上的甜区。

6.2 数据预处理的一致性:一个容易被忽略的变量

有些工具链在训练时代码里做了归一化(除以255、减均值、除方差),但在量化部署时,转换脚本或预处理代码没有保持一致,输入分布直接变了,校准统计的范围全部作废。

更隐蔽的是通道顺序。RGB还是BGR、CHW还是HWC,训练和部署一旦不一致,模型看到的数据就完全不同。这种错误的表现通常是:FP32模型测试时精度正常(因为它在训练时见过这个格式),量化模型突然崩了(量化对输入分布更敏感,格式一错,范围全部失准)。

我建议在量化之前先做一次“干跑”:用同一个输入,分别跑FP32模型和量化模型,确认两边的输入张量数值完全一致,再开始评估精度。

6.3 工具链版本和算子支持矩阵

量化部署依赖具体的工具链版本。同一个ONNX模型,用ONNX Runtime 1.13和1.15,支持的算子范围、量化实现都可能不一样。这也意味着:升级工具链版本后,你的量化效果可能会变,有可能会突然变好,也有可能会突然崩掉。

部署前做两件事:一是把工具链版本锁死,记录在项目的环境说明里;二是确认目标算子(尤其是你模型里独有的算子)在该版本的支持矩阵里。很多人在模型转换时卡住,就是因为新版本工具链不再支持某个旧算子的INT8实现,回退到CPU后性能大幅下降,而日志又不明显。

6.4 端到端验证:别只盯着精度指标

最后一条经验:量化部署完成后,不要只盯着准确率指标,还要做端到端的业务验证。量化改变了模型的行为细节,虽然在准确率指标上大概率接近,但在具体业务场景里,可能表现为某些特定类别/特定形状的误判率变化。

我的习惯是部署后除了跑测试集,还会再抽一些真实业务数据,对比FP32模型和量化模型输出在哪些样本上分叉了,逐个看原因。如果分叉集中在某一种场景,那说明那一类输入分布在校准阶段覆盖不足,需要针对性补充校准数据。

7. 写在最后:量化是工程,不是魔法

做了三年的量化部署,我最大的感受是:量化本身不复杂,公式和参数就那么几个;复杂的是一整套工程链路——数据分布怎么对齐、工具链哪里会回退、算子哪层会累积误差、硬件哪里有限制。真正解决问题靠的不是背公式,而是系统性的排查方法和经验判断。

我遇到过太多人一上来就上QAT,一遇到精度掉就怀疑量化方案不可行,其实大多数问题在PTQ阶段、校准集层面就能解决。先把日志看仔细,把每一层输出对比清楚,把工具链的行为摸透,90%的坑都能在半小时内定位出来。

这里再分享一个实际使用的小技巧:在做量化前,先给模型做一次“敏感层扫描”——用脚本对每一层的激活值加一个模拟量化噪声,观察哪几层对最终输出影响最大。提前知道哪些层是“易碎品”,后面做混合精度或者人工调scale时,思路会清晰很多。

如果每次量化都要从零开始排一次,那是流程没沉淀好,建议把自己踩过的坑整理成一份排查清单,下次遇到直接按清单走。磨刀不误砍柴工,这份清单的价值,比再调一个模型的精度高得多。

返回列表