4、训练后量化(PTQ)技巧—— 不用重新训练,如何用校准数据集把模型压到INT8?
说实话,很多同学一听到「量化」两个字,第一反应就是:「完了,又要重新训模型了?」
其实不是的。训练后量化(Post-Training Quantization,简称 PTQ)就是专门解决这个问题的——你手里已经有一个训练好的 FP32 模型,不需要动它的权重,只需要做一点「校准」工作,就能把它压到 INT8,推理速度翻倍,显存减半。
我在项目中遇到过好几次这种情况:模型已经上线了,但延迟太高,客户催着优化。重新训练?时间不够。换模型?成本太高。最后就是用 PTQ 搞定的。嗯,今天我就把这里面的门道给你讲透。
4.1 PTQ 的核心思想:用「校准」代替「训练」
PTQ 的原理其实很简单。你想想看,FP32 转 INT8,本质上就是给每个权重和激活值找一个「缩放因子」(scale)和一个「零点偏移」(zero point)。
这个缩放因子怎么找?最笨的办法就是直接看权重的最大值和最小值,然后线性映射过去。但这样做有个问题——权重分布往往不是均匀的,直接硬映射会丢失大量信息。
所以 PTQ 引入了「校准数据集」的概念。你拿一小批有代表性的数据(通常几百到几千张图片或几十条文本),让模型跑一遍前向推理,同时统计每一层的激活值分布。然后根据这个分布,去计算最优的缩放因子。
关键点:校准数据集不需要标签,只需要输入数据。因为 PTQ 只关心激活值的分布,不关心预测结果对不对。
4.2 校准数据集怎么选?我踩过的坑
校准数据集的选择,直接决定了量化后的精度。我刚开始做 PTQ 时,图省事,随便拿了 100 张训练集图片做校准。结果量化后模型精度掉了 5 个点,查了半天才发现问题。
后来我总结了几条经验:
- 数据分布要对齐:校准数据集必须和实际部署场景的数据分布一致。比如你做的是人脸检测,校准集里就不能全是风景图。
- 数量不用太多:我个人习惯用 200-500 条数据就够了。再多,边际收益递减;再少,统计不稳定。
- 多样性要够:不要只挑「好识别」的样本。极端情况、边缘案例也要包含进去,否则量化后遇到这些情况会崩。
- 避免重复:校准集里不要有大量重复或高度相似的样本,否则统计出来的分布会偏。
小技巧:如果你实在不知道怎么选校准集,可以试试「随机采样 + 人工筛选」的组合。先随机抽 500 条,然后快速扫一遍,去掉明显异常的样本。
4.3 量化参数的计算方法:对称 vs 非对称
量化参数的计算,说白了就是找 scale 和 zero point。常见的方法有两种:
| 方法 | 原理 | 适用场景 | 精度表现 |
|---|---|---|---|
| 对称量化 | scale = max(|min|, |max|) / 127,zero point = 0 | 权重量化(权重分布通常对称) | 一般 |
| 非对称量化 | scale = (max - min) / 255,zero point = round(-min / scale) | 激活值量化(分布可能偏斜) | 较好 |
我在实际项目中,权重一般用对称量化,激活值用非对称量化。为什么?因为权重的分布通常以 0 为中心,对称量化不会浪费表示范围;而激活值经过 ReLU 后全是非负的,非对称量化能更好地利用 INT8 的 256 个刻度。
4.4 校准算法:不止是 min-max
校准算法决定了你怎么从激活值分布中算出 scale 和 zero point。常见的算法有几种:
- Min-Max 校准:直接取激活值的最小值和最大值。简单粗暴,但对 outlier 非常敏感。我遇到过激活值里有一个 100 的 outlier,其他值都在 0-10 之间,结果量化后大部分值都被压缩到了 0-10 的范围内,精度直接崩了。
- Percentile 校准:取某个百分位(比如 99.9%)的值作为最大值,忽略极端 outlier。这个方法比 min-max 稳定很多,我个人比较推荐。
- KL 散度校准:通过最小化 FP32 和 INT8 分布的 KL 散度,找到最优的截断阈值。这是 NVIDIA TensorRT 默认使用的方法,精度通常最好,但计算量也最大。
- MSE 校准:最小化量化前后的均方误差。效果和 KL 散度接近,但实现更简单。
注意:校准算法没有绝对的好坏,取决于你的模型和数据。我建议你在项目中至少试两种方法(比如 Percentile 和 KL 散度),对比一下精度再决定。
4.5 实战:用 PyTorch 做 PTQ
说了这么多理论,咱们来点实际的。下面是一个用 PyTorch 做 PTQ 的完整流程:
import torch
import torch.quantization as quant
# 1. 加载预训练模型(FP32)
model = torch.load('model_fp32.pth')
model.eval()
# 2. 准备校准数据集(假设是 DataLoader)
calib_loader = get_calib_dataloader(batch_size=32, num_samples=200)
# 3. 配置量化参数
model.qconfig = quant.get_default_qconfig('fbgemm') # 用于 x86 CPU
# 或者用:quant.get_default_qconfig('qnnpack') # 用于 ARM CPU
# 4. 插入量化/反量化节点
model_prepared = quant.prepare(model, inplace=False)
# 5. 用校准数据集跑一遍前向
with torch.no_grad():
for data, _ in calib_loader:
model_prepared(data)
# 6. 转换为 INT8 模型
model_int8 = quant.convert(model_prepared, inplace=False)
# 7. 保存量化后的模型
torch.save(model_int8.state_dict(), 'model_int8.pth')
这段代码看起来简单,但有几个细节要注意:
- 校准阶段不要 backward:只需要 forward,不需要计算梯度。所以记得用
torch.no_grad()。 - 校准数据要 shuffle:虽然不需要标签,但数据顺序会影响统计结果。我习惯把校准集 shuffle 一下。
- 校准完成后要 convert:prepare 只是插入了量化节点,真正把权重转成 INT8 是在 convert 这一步。
4.6 精度回退:当量化后精度不够怎么办?
有时候,PTQ 做完后精度下降得比较厉害。别慌,有几个补救措施:
- 检查校准数据集:是不是数据分布不对?样本数量够不够?这是最常见的原因。
- 尝试不同的校准算法:从 min-max 换成 KL 散度,或者试试 percentile。
- 部分层回退到 FP16:有些层对量化特别敏感(比如 attention 的 softmax 层),可以单独把这些层保留为 FP16 或 FP32。
- 使用量化感知训练(QAT):如果 PTQ 实在搞不定,那就只能上 QAT 了。不过那是下一章的内容。
我的经验:80% 的 PTQ 精度问题,都是校准数据集没选好。先花时间把校准集搞对,比调算法参数有效得多。
4.7 本章知识体系
下面这张图总结了 PTQ 的核心流程和关键决策点:
这张图把 PTQ 的五个核心步骤串起来了。你从 FP32 模型出发,准备好校准集,选好算法,跑一遍前向,最后得到 INT8 模型。每一步都有坑,但每一步也都有对应的解决方案。
4.8 总结
PTQ 是大模型部署中最实用的技术之一。它不需要重新训练,只需要一个校准数据集和几百行代码,就能把模型压到 INT8,推理速度提升 2-4 倍,显存占用减半。
我个人觉得,PTQ 是每个做模型部署的工程师必须掌握的技能。它不复杂,但细节很多。校准数据集怎么选、校准算法怎么配、精度不够怎么回退——这些经验都是在项目中一点点积累出来的。
嗯,今天的内容就到这里。记住一句话:PTQ 不是万能的,但 80% 的场景下它都够用。剩下的 20%,咱们下一章用 QAT 来解决。
核心要点回顾:
- PTQ 不需要重新训练,只需要校准数据集
- 校准数据集要分布对齐、数量适中、多样性够
- 权重用对称量化,激活用非对称量化
- 校准算法推荐 KL 散度或 Percentile
- 精度不够时,先检查校准集,再尝试部分层回退