3、训练后量化(PTQ):校准数据集、动态量化与静态量化、逐层与逐通道量化

训练后量化,英文叫 Post-Training Quantization,简称 PTQ。说白了,就是模型训练完了,我们直接拿它做手脚,把它从 FP32 变成 INT8。不需要重新训练,成本低,见效快。我个人习惯把 PTQ 当作模型部署的第一道工序——先试试 PTQ,不行再考虑量化感知训练。

但 PTQ 不是随便跑个脚本就能搞定的。这里面有几个关键环节:校准数据集怎么选、动态和静态量化怎么取舍、粒度量到层还是量到通道。今天咱们一个一个掰开聊。

训练后量化(PTQ)核心流程 预训练 FP32 模型 校准数据集(Calibration) 选择量化方式:动态 vs 静态 量化粒度:逐层 vs 逐通道

3.1 校准数据集:选不对,量化就废了

校准数据集是 PTQ 的第一步,也是很多人容易忽视的一步。它的作用是让量化工具“看到”真实的数据分布,从而算出合理的缩放因子和零点。

校准数据集长什么样?

  • 一般从训练集或验证集中抽取几百到几千条样本
  • 必须覆盖模型推理时可能遇到的各种情况
  • 不能只用一类数据,比如全是猫的图片去校准一个分类猫狗车的模型
我曾经踩过的一个坑: 用验证集里全是高分辨率、光照良好的图片做校准,结果模型部署到低光照、有噪声的摄像头画面时,精度直接掉了 5 个点。后来换成混合场景的校准集,问题才解决。

校准数据量怎么定?

我一般从 500 张开始试。太少,分布估计不准;太多,校准时间变长,收益却不大。你想想看,校准的本质是统计激活值的 min/max 或直方图,样本量够了就行,不是越多越好。

校准集大小 特点 适用场景
100-500 条 速度快,但分布可能偏差 快速验证、模型迭代
500-2000 条 平衡速度与精度 大多数生产场景
2000+ 条 分布更准,但耗时 高精度要求、敏感场景

3.2 动态量化 vs 静态量化

这两个概念,我刚开始也搞混过。其实一句话就能说清楚:量化因子什么时候算?

动态量化

量化因子在推理时实时计算。每次推理,都会根据当前输入动态算出缩放因子。好处是精度损失小,坏处是计算开销大——因为每次都要算一遍 min/max。

适用场景: CPU 推理、对延迟不敏感的任务、模型权重占大头的情况。

我个人的经验: 动态量化在 NLP 模型上效果特别好。比如 BERT 这种,权重量化到 INT8 后体积缩小 4 倍,精度几乎不掉。但如果你跑的是实时视频流,动态量化可能扛不住。

静态量化

量化因子在校准阶段就计算好,推理时直接使用。说白了,就是提前把“尺子”做好,推理时只做查表和计算。

优点: 推理速度快,没有额外计算开销。
缺点: 对校准数据集质量敏感,选不好精度会掉。

对比维度 动态量化 静态量化
量化时机 推理时实时计算 校准阶段提前计算
推理速度 较慢(有额外计算) 快(直接查表)
精度损失 较小 取决于校准集质量
推荐场景 CPU、NLP 模型 GPU、边缘设备、批量推理
我的建议: 如果你不确定选哪个,先试静态量化。因为大部分推理框架(TensorRT、ONNX Runtime)对静态量化优化得更好。如果精度掉得厉害,再切回动态量化。

3.3 逐层量化 vs 逐通道量化

量化粒度,说白了就是“一把尺子量多大范围”。

逐层量化(Per-Tensor)

整个层的所有通道共用一套缩放因子和零点。实现简单,计算量小,但精度损失大——因为不同通道的数值范围可能差很多。

举个例子: 卷积层有 64 个输出通道,有的通道数值在 [-1, 1] 之间,有的在 [-100, 100] 之间。用同一把尺子去量,小数值通道的精度就全丢了。

逐通道量化(Per-Channel)

每个通道独立计算自己的缩放因子和零点。精度更高,但计算量和存储量也更大。

我遇到过的情况: 有一次做 MobileNet 的量化,逐层量化精度掉了 3%,换成逐通道量化后只掉了 0.5%。代价是模型体积大了 5%,但推理速度几乎没变——因为计算还是 INT8 的,只是多存了几个缩放因子。

对比维度 逐层量化 逐通道量化
精度 较低 较高
计算复杂度 低 中
存储开销 小 略大
推荐场景 大模型、对精度不敏感 小模型、精度敏感场景
注意: 不是所有硬件都支持逐通道量化。部署前一定要查一下目标硬件的算子支持列表。我曾经在某个边缘设备上踩过坑——逐通道量化在 PC 上跑得好好的,部署到设备上直接报错,因为那个设备的 NPU 不支持。

3.4 实战:用 PyTorch 做 PTQ

说了这么多理论,咱们来点实际的。下面是一个用 PyTorch 做静态量化的简单示例:

import torch
import torch.quantization as quant

# 加载预训练模型
model = torch.load('resnet18_fp32.pth')
model.eval()

# 配置量化
model.qconfig = quant.get_default_qconfig('fbgemm')
quant.prepare(model, inplace=True)

# 校准:跑几百张图片
with torch.no_grad():
    for images, _ in calibration_loader:
        model(images)

# 转换为量化模型
quant.convert(model, inplace=True)

# 保存
torch.jit.save(torch.jit.script(model), 'resnet18_int8.pt')

这段代码看着简单,但有几个细节要注意:

  • calibration_loader 的数据分布要和真实场景一致
  • fbgemm 是给 x86 CPU 用的,如果是 ARM 设备要换成 qnnpack
  • 校准过程中模型不能 train(),否则 BN 层会更新

嗯,这里要特别提醒: 校准完成后,一定要用验证集跑一遍精度对比。我见过有人校准完直接部署,结果精度掉了 10% 才发现是校准集选错了。

3.5 避坑指南

  1. 校准集不能太小: 少于 100 张基本没用,分布估计不准。
  2. 校准集不能太偏: 全是理想数据,真实场景就崩了。
  3. 逐通道量化不是万能药: 精度提升有限,但兼容性问题多。
  4. 动态量化别用在 GPU 上: GPU 对动态量化的支持很差,性能反而下降。
  5. 量化后一定要测延迟: 模型体积小了,不代表推理快了。有些算子 INT8 实现没优化好,反而更慢。