量化技术基础:INT8/INT4量化原理,对称量化与非对称量化,量化误差分析

聊到推理优化,量化是绕不开的一关。

说白了,就是把模型里那些 FP32 的权重和激活值,硬生生塞进 INT8 甚至 INT4 的「小盒子」里。你想想看,一个 FP32 占 4 字节,INT8 只占 1 字节,INT4 更狠,半个字节。这带宽和存储一下子就省下来了。我在项目里见过不少团队,模型精度明明够用,就是推理速度上不去,一上量化,延迟直接砍半。

但量化不是无脑压缩。搞不好,模型就「崩」了。今天咱们就把这背后的门道捋清楚。

1. 量化到底在干什么?

量化本质上是一个映射过程。把 FP32 的浮点数范围,映射到 INT8(-128 到 127)或者 INT4(-8 到 7)的整数范围。

数学上很简单:

Q = round( (R - zero_point) / scale )

反量化回去就是:

R = Q * scale + zero_point

这里的 scale 是缩放因子,zero_point 是零点偏移。这两个参数,就是量化的「灵魂」。我习惯把 scale 理解成「步长」,它决定了你能用多少个整数格子去覆盖原来的浮点范围。

核心要点:量化不是简单的截断,而是带参数的线性映射。参数选得好,精度损失就小。

2. 对称量化 vs 非对称量化

这里有个经典选择题:对称还是非对称?

我个人习惯先看数据的分布。如果数据正负对称,比如权重经过 BN 后大致是零均值的,那对称量化就很香。如果数据全是正的,比如 ReLU 后的激活值,那非对称量化更合适。

对称量化

对称量化的特点是 zero_point = 0。映射公式简化为:

Q = round( R / scale )

它的好处是计算简单,硬件实现起来特别快。INT8 的乘法器直接怼上去就行,不用额外处理零点偏移。

但有个坑:如果数据分布不对称,比如全是正数,那负半轴的整数格子就浪费了。你想想看,INT8 总共 256 个格子,有一半空着,精度自然就差了。

我的经验:我曾经在量化一个 NLP 模型时,发现权重分布很对称,但激活值分布偏正。我果断对权重用对称量化,对激活值用非对称量化。混合精度量化,效果出奇的好。

非对称量化

非对称量化允许 zero_point 不为零。它能把浮点范围完整地映射到整数范围,不浪费格子。

Q = round( (R - zero_point) / scale )

举个例子,假设激活值范围是 [0.0, 6.0],用 INT8 非对称量化:

  • scale = (6.0 - 0.0) / 255 ≈ 0.0235
  • zero_point = 0(因为最小值映射到 0)

这样 256 个格子全用上了。但代价是计算时多了一步减法,硬件上稍微复杂一点。

类型 zero_point 适用场景 硬件开销
对称量化 0 权重(分布对称)
非对称量化 非零 激活值(分布偏正)

注意:非对称量化虽然精度好,但零点偏移会导致卷积或全连接层计算时多出一些「交叉项」。我在优化算子时,经常需要手动展开这些项,否则性能会打折扣。

3. INT4 量化:更极致的压缩

INT4 量化,说白了就是把格子数从 256 个砍到 16 个。精度损失肯定更大,但存储和带宽能省 4 倍。

我见过一些场景,比如大模型的 KV Cache,用 INT4 量化后显存占用直接降为原来的四分之一。推理速度也上去了。

但 INT4 有个问题:硬件支持不普遍。很多 GPU 和 CPU 没有原生的 INT4 指令。这时候就得靠「打包」技巧——把两个 INT4 数据塞进一个 INT8 寄存器里,然后通过位运算拆开。

// 伪代码:INT4 打包与解包
// 打包:将两个 INT4 值 a 和 b 打包成一个 INT8
uint8_t packed = (a & 0x0F) | ((b & 0x0F) << 4);

// 解包:从 INT8 中提取两个 INT4
uint8_t a = packed & 0x0F;
uint8_t b = (packed >> 4) & 0x0F;

嗯,这里要注意,INT4 的量化参数 scalezero_point 计算方式跟 INT8 一样,只是整数范围变成了 [-8, 7] 或 [0, 15](取决于有符号还是无符号)。

4. 量化误差分析:到底损失在哪?

量化不是无损的。误差主要来自三个方面:

4.1 截断误差

当浮点数的范围超出整数能表示的范围时,超出部分会被截断。比如 INT8 只能表示 [-128, 127],如果某个权重是 200.0,那量化后就是 127,直接截断了。

我习惯的做法是:先统计数据的最大值和最小值,然后适当留一点「余量」。比如最大值是 100.0,我可能会把量化范围设到 110.0,避免极端值被截断。

4.2 舍入误差

量化公式里有 round() 操作。这个四舍五入会引入误差。单个值的舍入误差很小,但成千上万个值累积起来,就可能影响最终结果。

举个例子:

FP32: 0.1234
scale: 0.01
Q = round(0.1234 / 0.01) = round(12.34) = 12
反量化: 12 * 0.01 = 0.12
误差: 0.1234 - 0.12 = 0.0034

这个误差看起来不大,但如果是 1000 层网络,每层都来这么一下,误差就会累积。

4.3 精度退化与校准

量化后模型精度下降,通常是因为激活值的分布被破坏了。我遇到过最头疼的情况是:量化后某个层的输出分布完全变了,导致后续层全部「跑偏」。

解决办法是「校准」。用一小部分真实数据跑一遍 FP32 模型,统计每层激活值的分布,然后根据这个分布来选 scalezero_point。这叫 Calibration

避坑指南:我曾经直接用训练集的统计量做校准,结果推理时精度崩了。后来发现,训练集和推理集的分布有差异。改用验证集做校准后,问题就解决了。所以,校准数据集一定要能代表真实推理场景。

5. 量化误差的缓解策略

既然误差不可避免,那怎么让它「可控」?

  • 逐通道量化:每个卷积核单独算 scale,而不是整个层共享一个。精度能提升不少,但计算量也上去了。
  • 量化感知训练(QAT):在训练过程中模拟量化操作,让模型自己适应量化误差。我一般只在精度要求极高的场景用 QAT,因为它训练时间太长。
  • 混合精度量化:敏感层用 FP16 或 INT8,不敏感层用 INT4。怎么判断敏感层?跑一遍逐层误差分析,误差大的层就保留高精度。

一个小技巧:如果你发现量化后某个层输出全是 0,大概率是 scale 设得太大了,导致所有值都被量化到 0。把 scale 调小一点,或者改用非对称量化,通常能解决。

好了,量化技术的基础就聊到这儿。说白了,量化就是在精度和效率之间找平衡。没有银弹,只有根据你的模型和数据,选最合适的策略。下一章咱们聊聊更高级的量化技术——比如动态量化和权重共享量化,到时候再细说。