2. 模型量化基础

模型量化,说白了就是给模型“减肥”。一个 FP32 的模型,参数动辄几百 MB,放到边缘设备上根本跑不动。我刚开始接触部署时,总觉得量化会掉精度,后来发现——只要方法对,掉的那点精度完全能接受,换来的却是数倍的加速。

这一章,我们就把量化的底裤扒干净。从 INT8/FP16 的原理,到对称与非对称量化的选择,再到校准数据集怎么挑,一步到位。

2.1 量化本质:用更少的比特表示数值

量化就是把连续的浮点数映射到离散的整数空间。举个例子,一个 FP32 的权重是 0.123456,量化成 INT8 后可能变成 12。这个映射过程,就是量化。

数学上,量化可以表示为:

Q = round(r / S) + Z

其中:

  • r:原始浮点值
  • S:缩放因子(Scale)
  • Z:零点偏移(Zero Point)
  • Q:量化后的整数值

反量化就是逆过程:

r ≈ (Q - Z) × S

嗯,这里要注意,round 操作会引入误差,这就是量化误差的来源。

核心结论:量化不是无损的,但误差可控。关键在于 S 和 Z 的选择。

2.2 FP16 vs INT8:精度与速度的博弈

FP16 和 INT8 是目前最主流的两种量化格式。我个人的经验是:能上 INT8 就别用 FP16,除非精度实在扛不住。

格式 位宽 表示范围 精度损失 推理加速 典型场景
FP32 32-bit ±3.4×10³⁸ 无 1× 训练、高精度推理
FP16 16-bit ±6.5×10⁴ 较小 ~2× GPU 推理、部分边缘设备
INT8 8-bit -128 ~ 127 中等 ~4× CPU/边缘设备推理

FP16 的好处是几乎不需要校准,直接转换就行。但 INT8 需要做校准,选好 S 和 Z。我曾经在一个视觉检测项目里,FP16 只掉了 0.3% mAP,但 INT8 掉了 1.2%。后来换了校准集,INT8 的损失降到了 0.5%。

我的建议:如果硬件支持 INT8 加速(比如 Jetson 系列、高通 DSP),优先尝试 INT8。如果精度不达标,再退回到 FP16。

2.3 对称量化 vs 非对称量化

这是量化里最容易搞混的概念。我画个图帮你理清楚。

对称量化 vs 非对称量化 对称量化 Z = 0 范围对称:[-max, max] 适合权重(分布对称) 实现简单,硬件友好 非对称量化 Z ≠ 0 范围非对称:[min, max] 适合激活值(分布偏斜) 精度更高,计算稍复杂 权重通常用对称量化(分布对称,Z=0 减少计算) 激活值通常用非对称量化(分布偏斜,精度更高) 实际部署中:权重用对称 INT8 + 激活值用非对称 INT8 这是 TensorRT、ONNX Runtime 等框架的默认策略

对称量化:Z=0,范围关于原点对称。适合权重,因为权重分布通常接近高斯分布,对称性很好。

非对称量化:Z≠0,范围从 min 到 max。适合激活值,因为 ReLU 后的激活值全是非负的,用对称量化会浪费一半的表示范围。

避坑指南:我曾经在一个项目中,对 ReLU 后的激活值用了对称量化,结果精度掉了 3%。换成非对称量化后,损失降到了 0.8%。记住:激活值用非对称,权重用对称。

2.4 量化误差分析

量化误差主要来自三个方面:

  1. 舍入误差:round 操作导致的精度损失
  2. 截断误差:超出量化范围的数值被截断
  3. 缩放误差:S 和 Z 的选择不理想

其中,截断误差是最致命的。我见过一个案例,某个层的激活值范围是 [-0.1, 100],如果用对称量化,-0.1 和 100 的缩放因子会非常不平衡,导致大量数值被截断。

量化误差的数学表达:

量化误差 = |r - r_hat| = |r - (Q - Z) × S|

其中 r_hat 是反量化后的值。误差大小取决于 S 和 Z 的选择。

关键指标:量化误差通常用 SNR(信噪比)或 MSE(均方误差)来衡量。SNR 越高,量化质量越好。

2.5 校准数据集的选择

校准数据集,就是用来确定 S 和 Z 的。选得好,量化精度高;选得不好,模型直接崩。

我个人的经验是:校准集必须覆盖模型在真实场景中可能遇到的所有数据分布。比如一个视觉检测模型,校准集里不能只有晴天图片,还得有阴天、雨天、夜晚的图片。

校准方法 原理 优点 缺点 推荐场景
MinMax 取校准集的最小/最大值 简单、快速 对异常值敏感 数据分布稳定的场景
Percentile 取指定百分位(如 99.9%) 抗异常值 需要调参 有少量异常值的场景
KL 散度 最小化量化前后的分布差异 精度最高 计算量大 精度要求高的场景
MSE 最小化量化前后的均方误差 理论最优 需要遍历搜索 学术研究、调优

校准集的大小也有讲究。太小了,统计不准确;太大了,校准时间太长。我一般用 500~2000 张图片,覆盖所有类别和光照条件。

我的经验:校准集不要用训练集,用验证集。训练集的数据分布可能过拟合,验证集更接近真实场景。另外,校准集里不要有重复图片,否则统计会偏。

2.6 实战:用 PyTorch 做 INT8 量化

说了这么多理论,来点实际的。下面是一个用 PyTorch 做 INT8 量化的完整流程:

import torch
import torch.quantization as quant

# 1. 准备模型
model = MyModel().eval()

# 2. 准备校准数据集
calibration_loader = get_calibration_loader(batch_size=32, num_samples=500)

# 3. 配置量化参数
model.qconfig = quant.get_default_qconfig('fbgemm')
# 或者自定义:
# model.qconfig = quant.QConfig(
#     activation=quant.MinMaxObserver.with_args(dtype=torch.quint8),
#     weight=quant.MinMaxObserver.with_args(dtype=torch.qint8)
# )

# 4. 准备量化(插入观察器)
model_prepared = quant.prepare(model, inplace=False)

# 5. 校准(跑一遍校准集)
with torch.no_grad():
    for images, _ in calibration_loader:
        model_prepared(images)

# 6. 转换为量化模型
model_quantized = quant.convert(model_prepared, inplace=False)

# 7. 保存量化模型
torch.save(model_quantized.state_dict(), 'model_int8.pth')

这里要注意几点:

  • get_default_qconfig('fbgemm') 是给 x86 CPU 用的,ARM 用 'qnnpack'
  • 校准阶段一定要用 torch.no_grad(),否则会更新模型参数
  • 校准集的大小直接影响量化质量,别偷懒

我曾经踩过的坑:校准集里混入了标签噪声(比如一张猫的图片被标成了狗),结果量化后的模型对猫的检测精度暴跌。后来我加了数据清洗步骤,问题才解决。

2.7 量化后的模型验证

量化完不是终点,得验证。我一般做三件事:

  1. 精度验证:在验证集上跑一遍,对比 FP32 和 INT8 的精度差异
  2. 速度验证:在目标硬件上测推理延迟,看加速比是否达标
  3. 稳定性验证:跑 1000 次推理,看结果是否一致(防止数值溢出)

如果精度下降超过 1%,我会检查:

  • 校准集是否覆盖了所有数据分布?
  • 是否有层用了对称量化但实际分布不对称?
  • 是否有异常值导致缩放因子过大?

总结:量化不是玄学,是工程。选对格式、选对方法、选对校准集,你就能在精度和速度之间找到最佳平衡点。