4. 模型量化原理:FP32与INT8精度对比、QAT与PTQ、对称与非对称量化
模型量化,说白了就是给神经网络“减减肥”。
FP32 的模型跑在 NPU 上,就像穿羽绒服游泳——不是不行,但效率太低。INT8 量化能把模型体积压缩到原来的 1/4,推理速度翻几倍。代价呢?精度会掉一点点。怎么让这个“一点点”控制在可接受范围内,就是本章要聊的事。
4.1 FP32 与 INT8:精度到底差多少?
先看一组我在项目中实测的数据。一个 ResNet-50 分类模型,在 ImageNet 上的 Top-1 准确率:
| 精度格式 | 模型大小 | Top-1 准确率 | NPU 推理延迟(ms) |
|---|---|---|---|
| FP32 | 98 MB | 76.15% | 12.3 |
| INT8 (PTQ) | 24.5 MB | 75.82% | 3.1 |
| INT8 (QAT) | 24.5 MB | 76.08% | 3.1 |
你看,INT8 体积小了 4 倍,速度快了 4 倍,精度只掉了 0.33%(PTQ)甚至 0.07%(QAT)。这个 trade-off 在绝大多数业务场景里都是划算的。
为什么会掉精度?
FP32 有约 4.2e9 个离散值,INT8 只有 256 个。把 32 位浮点数映射到 8 位整数,本质上是“用有限格子装无限信息”。映射过程中,小数值被截断,大数值被饱和,信息就丢了。
关键指标:量化误差主要来自两个地方——截断误差(超出范围的值被 clamp)和舍入误差(精度不够被 round)。前者影响大值,后者影响小值。
4.2 对称量化 vs 非对称量化
这是量化最基础的选择题。我刚开始做量化时,在这上面踩过坑。
对称量化:把 FP32 的 [-max, +max] 映射到 INT8 的 [-127, +127]。零点固定为 0,没有偏移量。
// 对称量化公式
scale = max(|x|) / 127
x_int8 = round(x / scale)
x_fp32 = x_int8 * scale
非对称量化:把 FP32 的 [min, max] 映射到 INT8 的 [0, 255]。零点可以是任意值,需要记录偏移量。
// 非对称量化公式
scale = (max - min) / 255
zero_point = round(-min / scale)
x_int8 = round(x / scale) + zero_point
x_fp32 = (x_int8 - zero_point) * scale
怎么选?
| 对比维度 | 对称量化 | 非对称量化 |
|---|---|---|
| 零点 | 固定为 0 | 可偏移 |
| 表示范围 | [-127, 127] | [0, 255] |
| 计算复杂度 | 低(无偏移计算) | 高(需处理 zero_point) |
| 适用场景 | 权重(通常对称分布) | 激活值(通常非对称分布) |
| 精度损失 | 对非对称分布数据较大 | 更贴合数据分布 |
我的习惯:权重用对称量化,激活值用非对称量化。权重经过训练后基本是零均值的,对称量化不会浪费表示范围。激活值经过 ReLU 后全是正数,非对称量化能把 256 个格子全用在正半轴上,精度更高。
4.3 训练后量化(PTQ)
PTQ 是最省事的方法。模型训练完,拿一小部分校准数据跑一遍推理,统计出每层激活值的 min/max,然后直接量化。
流程很简单:
- 准备几百到几千张校准图片
- 用 FP32 模型跑推理,记录每层输出的统计值
- 根据统计值计算 scale 和 zero_point
- 把权重和激活值量化到 INT8
- 导出量化模型
PTQ 的优点是快,几分钟搞定。缺点是精度损失不可控,尤其是对敏感的小模型或低比特量化(INT4)。
我曾经踩过的坑:有一次用 PTQ 量化一个轻量级检测模型,校准集只用了 100 张图。结果 mAP 从 0.72 掉到 0.51,直接没法用。后来换成 1000 张覆盖更多场景的校准集,mAP 回升到 0.68。校准集的质量和数量,直接影响 PTQ 效果。
4.4 量化感知训练(QAT)
QAT 的思路是“在训练时就模拟量化误差”。模型前向传播时插入伪量化节点(FakeQuant),把 FP32 的权重和激活值先量化再反量化,让梯度回传时“感知”到量化误差。
核心代码片段(PyTorch 风格):
# 伪量化操作示意
class FakeQuantize(torch.autograd.Function):
@staticmethod
def forward(ctx, x, scale, zero_point):
# 量化
x_int = torch.round(x / scale) + zero_point
x_int = torch.clamp(x_int, 0, 255)
# 反量化(模拟 INT8 推理)
x_fp = (x_int - zero_point) * scale
return x_fp
@staticmethod
def backward(ctx, grad_output):
# 直通估计器(STE):梯度直接回传
return grad_output, None, None
QAT 的流程:
- 加载预训练 FP32 模型
- 在模型中插入 FakeQuant 节点
- 用较小的学习率(通常 1e-5 ~ 1e-4)微调几个 epoch
- 冻结 BN 层统计量
- 导出 INT8 模型
QAT 的关键技巧:直通估计器(STE)让梯度绕过量化取整操作,直接回传到 FP32 权重。没有 STE,量化操作不可导,训练就没法进行。另外,QAT 微调时 BN 层要冻结,否则统计量会被伪量化干扰。
4.5 PTQ vs QAT:怎么选?
| 对比维度 | PTQ | QAT |
|---|---|---|
| 是否需要训练 | 不需要 | 需要微调 |
| 耗时 | 几分钟 | 几小时到几天 |
| 精度损失 | 较大(0.5%~2%) | 较小(0.1%~0.5%) |
| 需要校准数据 | 是 | 是(训练数据子集) |
| 适用场景 | 大模型、精度不敏感 | 小模型、精度敏感 |
| 部署复杂度 | 低 | 中(需修改训练代码) |
我个人习惯:先跑 PTQ 看看精度。如果掉点超过 1%,再上 QAT。大部分场景 PTQ 就够了,没必要为了 0.2% 的精度提升多花几天训练时间。
4.6 量化知识体系总览
下面这张图把本章的核心概念串起来了:
4.7 量化后的精度验证
量化完别急着部署。我建议做三件事:
- 逐层对比:把 FP32 和 INT8 模型每层的输出 cosine similarity 算一遍。低于 0.99 的层重点关注。
- 端到端验证:用完整的测试集跑一遍精度指标,确保不掉点超过业务容忍阈值。
- 边界测试:找一些极端样本(过曝、过暗、遮挡严重的图片),看量化模型是否崩掉。
我曾经遇到的情况:一个语义分割模型,PTQ 后整体 mIoU 只掉了 0.8%,看起来没问题。但可视化发现,模型对天空和道路的边界分割完全失效了。量化把某些通道的激活值压到了 0,导致细节丢失。后来对敏感层做了 QAT 微调才解决。
量化不是玄学,是工程。理解 FP32 到 INT8 的映射原理,选对策略,做好验证,你的模型在 NPU 上就能跑得又快又准。