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 表示范围就浪费了。精度损失会比较大。
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 是最省事的方法。模型训练完,直接拿过来量化。不需要重新训练。
做法很简单:
- 准备一小部分校准数据(几百张图片或几十个文本样本)。
- 跑一遍推理,收集每一层的激活值分布。
- 根据分布,计算出每层的 S 和 Z。
- 把权重和激活值量化到 INT8。
PTQ 的优点是快,几分钟就能搞定。但精度损失可能比较大,尤其是小模型或对精度敏感的任务。
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 个百分点。
- 模型精度要求极高(比如医疗影像、自动驾驶)。
- 模型很小(MobileNet、SqueezeNet 这类),PTQ 掉点严重。
- 你手头有充足的计算资源和时间。
3.5 知识结构图:量化方法选择流程
下面这张图,是我自己总结的量化方法选择决策流程。你可以参考一下。
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。理解了这些原理,你就能针对自己的项目,选出最合适的量化方案。