3、训练后量化(PTQ):校准数据集、动态量化与静态量化、逐层与逐通道量化
训练后量化,英文叫 Post-Training Quantization,简称 PTQ。说白了,就是模型训练完了,我们直接拿它做手脚,把它从 FP32 变成 INT8。不需要重新训练,成本低,见效快。我个人习惯把 PTQ 当作模型部署的第一道工序——先试试 PTQ,不行再考虑量化感知训练。
但 PTQ 不是随便跑个脚本就能搞定的。这里面有几个关键环节:校准数据集怎么选、动态和静态量化怎么取舍、粒度量到层还是量到通道。今天咱们一个一个掰开聊。
3.1 校准数据集:选不对,量化就废了
校准数据集是 PTQ 的第一步,也是很多人容易忽视的一步。它的作用是让量化工具“看到”真实的数据分布,从而算出合理的缩放因子和零点。
校准数据集长什么样?
- 一般从训练集或验证集中抽取几百到几千条样本
- 必须覆盖模型推理时可能遇到的各种情况
- 不能只用一类数据,比如全是猫的图片去校准一个分类猫狗车的模型
校准数据量怎么定?
我一般从 500 张开始试。太少,分布估计不准;太多,校准时间变长,收益却不大。你想想看,校准的本质是统计激活值的 min/max 或直方图,样本量够了就行,不是越多越好。
| 校准集大小 | 特点 | 适用场景 |
|---|---|---|
| 100-500 条 | 速度快,但分布可能偏差 | 快速验证、模型迭代 |
| 500-2000 条 | 平衡速度与精度 | 大多数生产场景 |
| 2000+ 条 | 分布更准,但耗时 | 高精度要求、敏感场景 |
3.2 动态量化 vs 静态量化
这两个概念,我刚开始也搞混过。其实一句话就能说清楚:量化因子什么时候算?
动态量化
量化因子在推理时实时计算。每次推理,都会根据当前输入动态算出缩放因子。好处是精度损失小,坏处是计算开销大——因为每次都要算一遍 min/max。
适用场景: CPU 推理、对延迟不敏感的任务、模型权重占大头的情况。
我个人的经验: 动态量化在 NLP 模型上效果特别好。比如 BERT 这种,权重量化到 INT8 后体积缩小 4 倍,精度几乎不掉。但如果你跑的是实时视频流,动态量化可能扛不住。
静态量化
量化因子在校准阶段就计算好,推理时直接使用。说白了,就是提前把“尺子”做好,推理时只做查表和计算。
优点: 推理速度快,没有额外计算开销。
缺点: 对校准数据集质量敏感,选不好精度会掉。
| 对比维度 | 动态量化 | 静态量化 |
|---|---|---|
| 量化时机 | 推理时实时计算 | 校准阶段提前计算 |
| 推理速度 | 较慢(有额外计算) | 快(直接查表) |
| 精度损失 | 较小 | 取决于校准集质量 |
| 推荐场景 | CPU、NLP 模型 | GPU、边缘设备、批量推理 |
3.3 逐层量化 vs 逐通道量化
量化粒度,说白了就是“一把尺子量多大范围”。
逐层量化(Per-Tensor)
整个层的所有通道共用一套缩放因子和零点。实现简单,计算量小,但精度损失大——因为不同通道的数值范围可能差很多。
举个例子: 卷积层有 64 个输出通道,有的通道数值在 [-1, 1] 之间,有的在 [-100, 100] 之间。用同一把尺子去量,小数值通道的精度就全丢了。
逐通道量化(Per-Channel)
每个通道独立计算自己的缩放因子和零点。精度更高,但计算量和存储量也更大。
我遇到过的情况: 有一次做 MobileNet 的量化,逐层量化精度掉了 3%,换成逐通道量化后只掉了 0.5%。代价是模型体积大了 5%,但推理速度几乎没变——因为计算还是 INT8 的,只是多存了几个缩放因子。
| 对比维度 | 逐层量化 | 逐通道量化 |
|---|---|---|
| 精度 | 较低 | 较高 |
| 计算复杂度 | 低 | 中 |
| 存储开销 | 小 | 略大 |
| 推荐场景 | 大模型、对精度不敏感 | 小模型、精度敏感场景 |
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 避坑指南
- 校准集不能太小: 少于 100 张基本没用,分布估计不准。
- 校准集不能太偏: 全是理想数据,真实场景就崩了。
- 逐通道量化不是万能药: 精度提升有限,但兼容性问题多。
- 动态量化别用在 GPU 上: GPU 对动态量化的支持很差,性能反而下降。
- 量化后一定要测延迟: 模型体积小了,不代表推理快了。有些算子 INT8 实现没优化好,反而更慢。