3. 模型量化原理

量化,说白了就是把模型里那些 32 位浮点数(FP32)的权重和激活值,用更少的 bit 来表示。比如 8 位整数(INT8)。

为什么要这么干?因为 NPU 算 INT8 比算 FP32 快得多,而且内存占用直接砍掉 75%。我最早接触量化是在一个智能音箱项目上,模型死活塞不进 2MB 的 SRAM,量化到 INT8 之后,不仅塞进去了,推理速度还快了 3 倍。

但量化不是简单的四舍五入。它有一套数学原理在背后支撑。搞懂了这些,你才能用好它。

3.1 量化映射:从浮点到定点

量化的核心,就是找一个映射关系,把 FP32 的值域映射到 INT8 的值域上。INT8 能表示 256 个不同的值(-128 到 127,或者 0 到 255)。

这个映射关系由两个参数决定:缩放因子(Scale) 和 零点(Zero Point)。

数学公式很简单:

q = round(r / S) + Z

其中:

  • r:原始的 FP32 值
  • q:量化后的 INT8 值
  • S:缩放因子(Scale),一个 FP32 的步长
  • Z:零点(Zero Point),INT8 中代表 0 的那个值

反量化就是逆过程:

r ≈ (q - Z) × S

这里有个关键点:量化一定有精度损失。因为 round 操作会丢掉信息。我们的目标,就是让这个损失对最终任务的影响最小。

3.2 对称量化 vs 非对称量化

这两种方式的区别,就在于零点 Z 怎么处理。

3.2.1 对称量化

对称量化把零点 Z 固定为 0。公式简化成:

q = round(r / S)

它的映射范围是对称的。比如 FP32 的 [-A, A] 映射到 INT8 的 [-127, 127](通常不用 -128,为了对称)。

优点:实现简单,计算量小。很多 NPU 硬件原生支持对称量化,跑起来最快。

缺点:如果数据分布不对称(比如 ReLU 后的激活值全是正数),那有一半的 INT8 表示范围就浪费了。精度损失会比较大。

我的经验:对于权重,我习惯用对称量化。因为训练好的权重分布通常接近 0 对称。但对于激活值,尤其是 ReLU 之后的,非对称量化往往效果更好。

3.2.2 非对称量化

非对称量化不要求零点为 0。Z 可以取任意 INT8 值。它能充分利用 INT8 的 256 个刻度。

公式就是最前面那个:q = round(r / S) + Z。

优点:能更好地匹配数据分布,精度损失更小。

缺点:计算时多了一个 Z 的偏移量,硬件实现稍微复杂一点。不过现在大部分 NPU 都支持得很好。

特性 对称量化 非对称量化
零点 Z 固定为 0 根据数据分布计算
表示范围 [-127, 127] [0, 255] 或 [-128, 127]
适用场景 权重、分布对称的激活值 分布不对称的激活值(如 ReLU 输出)
硬件实现 简单、高效 稍复杂
精度 数据不对称时损失大 通常更高

3.3 训练后量化(PTQ)

PTQ 是最省事的方法。模型训练完,直接拿过来量化。不需要重新训练。

做法很简单:

  1. 准备一小部分校准数据(几百张图片或几十个文本样本)。
  2. 跑一遍推理,收集每一层的激活值分布。
  3. 根据分布,计算出每层的 S 和 Z。
  4. 把权重和激活值量化到 INT8。

PTQ 的优点是快,几分钟就能搞定。但精度损失可能比较大,尤其是小模型或对精度敏感的任务。

避坑指南:我曾经在一个人脸识别项目上直接用 PTQ,结果准确率从 98% 掉到了 85%。后来发现是某一层的激活值分布有长尾,导致量化步长 S 选得太大。解决办法是改用非对称量化,并且把那层的 S 单独用 KL 散度优化了一下。

3.4 量化感知训练(QAT)

QAT 的思路是:让模型在训练阶段就「感知」到量化误差,然后自己去适应它。

具体做法是在训练图中插入伪量化节点(Fake Quantize)。这些节点在前向传播时模拟量化-反量化的过程,产生精度损失。反向传播时,梯度绕过这些节点(直通估计器,STE),正常更新权重。

伪量化操作:

# 伪代码:模拟量化
def fake_quantize(r, S, Z):
    q = torch.round(r / S + Z)
    r_hat = (q - Z) * S  # 反量化回 FP32
    return r_hat

这样训练出来的权重,对量化误差有天然的鲁棒性。推理时,直接去掉伪量化节点,换成真正的 INT8 计算。

QAT 的代价:训练时间变长(大约 1.5 到 2 倍),而且需要修改训练代码。但精度通常比 PTQ 高 1-3 个百分点。

什么时候用 QAT?
  • 模型精度要求极高(比如医疗影像、自动驾驶)。
  • 模型很小(MobileNet、SqueezeNet 这类),PTQ 掉点严重。
  • 你手头有充足的计算资源和时间。

3.5 知识结构图:量化方法选择流程

下面这张图,是我自己总结的量化方法选择决策流程。你可以参考一下。

量化方法选择决策流程 开始:训练好的 FP32 模型 精度要求高?模型小? 否 是 PTQ:准备校准数据 收集激活值分布 计算 S 和 Z 输出:INT8 模型 QAT:插入伪量化节点 微调训练(带 STE) 移除伪量化节点 输出:INT8 模型 PTQ 快但精度可能掉;QAT 慢但精度高

3.6 量化粒度:逐层 vs 逐通道

量化粒度指的是:S 和 Z 是每层共享一套,还是每个通道各有一套。

  • 逐层量化(Per-Layer):整层共用一个 S 和 Z。计算简单,但精度差。因为不同通道的数值范围可能差很多。
  • 逐通道量化(Per-Channel):每个输出通道有自己的 S 和 Z。精度好很多,但计算量稍大。现在主流 NPU 都支持。

我个人的习惯是:权重用逐通道量化,激活值用逐层量化。因为权重的分布在不同通道差异大,而激活值经过 BatchNorm 后,通道间分布相对均匀。这样能在精度和计算量之间取得平衡。

3.7 量化后的计算:INT8 矩阵乘法

量化后的矩阵乘法,不再是 FP32 的乘加,而是 INT8 的乘加,最后再反量化回 FP32。

假设我们有两个量化后的矩阵 Q_A 和 Q_B,以及对应的缩放因子 S_A、S_B 和零点 Z_A、Z_B。

那么:

R_C = S_A × S_B × (Q_A - Z_A) × (Q_B - Z_B)

其中 (Q_A - Z_A) × (Q_B - Z_B) 是 INT8 的矩阵乘法,结果是一个 INT32 的累加器(防止溢出)。最后乘以 S_A × S_B 得到 FP32 的结果。

嗯,这里要注意:累加器要用 INT32。两个 INT8 相乘最大是 127×127=16129,如果累加很多次,INT16 会溢出。我见过有人踩过这个坑,结果推理结果全是乱码。

3.8 总结:量化方法对比

方法 需要训练 精度 部署速度 适用场景
PTQ(对称) 否 较低 快 大模型、对精度不敏感
PTQ(非对称) 否 中等 快 激活值分布不对称的场景
QAT 是 高 慢(训练阶段) 小模型、精度要求极高

量化不是银弹。它是在精度、速度和内存之间做 trade-off。理解了这些原理,你就能针对自己的项目,选出最合适的量化方案。