4、训练后量化(PTQ):无需重新训练,快速压缩模型的方法与工具
训练后量化,圈里人常叫它 PTQ。说白了,就是模型训练完了,我们直接拿它做手脚,把权重和激活值从 FP32 砍到 INT8 甚至 INT4。好处很明显——不用动训练代码,不用重新跑几十个 epoch,几小时甚至几分钟就能拿到一个更小、更快的模型。
我最早接触 PTQ 是在一个边缘端的检测项目上。模型在 GPU 上跑得飞起,一上 Jetson 就卡成 PPT。当时时间紧,重新训练肯定来不及。硬着头皮试了试 TensorRT 的 INT8 量化,结果速度翻了 3 倍,精度只掉了 0.5%。嗯,从那以后我就成了 PTQ 的忠实用户。
4.1 量化到底在做什么?
量化本质上就是个映射过程。把 FP32 的数值范围,映射到 INT8 的 256 个离散值上。公式很简单:
scale = (max_val - min_val) / 255
zero_point = round(-min_val / scale)
quantized_val = round(float_val / scale) + zero_point
你想想看,原本一个 float 占 4 字节,现在一个 int8 只占 1 字节。模型体积直接缩到 1/4,内存带宽压力也小了很多。代价就是精度损失——但大部分场景下,这点损失完全能接受。
核心要点:PTQ 的关键在于校准。你需要准备一小批有代表性的数据,让工具统计出每层激活值的 min/max 或者直方图分布。校准集的质量,直接决定量化后的精度。
4.2 两种量化策略:对称 vs 非对称
我个人习惯把量化策略分成两类。理解它们的区别,能帮你少踩很多坑。
| 策略 | 映射方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 对称量化 | zero_point = 0,正负对称 | 权重(权重通常关于 0 对称) | 计算简单,硬件友好 | 对非对称分布精度略低 |
| 非对称量化 | zero_point 可偏移 | 激活值(ReLU 后全为正) | 能更好利用 INT8 的表示范围 | 计算稍复杂 |
我在项目中遇到过一个问题:用对称量化去量化 ReLU 后的激活值,结果精度掉得厉害。后来换成非对称量化,精度一下就回来了。原因很简单——ReLU 输出全是正数,对称量化会浪费一半的表示范围。
4.3 校准方法:选哪种?
校准说白了就是找每层的 min 和 max。但直接取 min/max 容易被离群点带偏。常用的方法有几种:
- MinMax:直接取统计到的全局最小值和最大值。简单粗暴,但对离群点敏感。
- Percentile:去掉头尾一定百分比的极端值。比如取 99.9% 分位点。我一般用这个,效果比较稳。
- KL 散度:让量化后的分布尽量接近原始分布。TensorRT 的默认方法,精度通常最好。
- MSE:最小化量化前后的均方误差。适合对数值精度要求高的场景。
我的经验:如果校准集足够有代表性,直接用 KL 散度就好。如果校准集质量一般,试试 Percentile,把阈值设在 99.9% 左右,能有效抵抗离群点。
4.4 TensorRT 量化实战
TensorRT 做 INT8 量化,流程很清晰。我习惯用 Python 接口,配合 PyTorch 模型来做。
基本步骤:
- 导出 ONNX 模型
- 准备校准数据集(通常 500-1000 张图就够了)
- 实现校准器类
- 构建 INT8 引擎
下面是一个简化的校准器实现:
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
class Calibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, dataloader, cache_file):
trt.IInt8EntropyCalibrator2.__init__(self)
self.dataloader = dataloader
self.cache_file = cache_file
self.batch_size = dataloader.batch_size
self.batch_idx = 0
# 分配 GPU 内存
self.device_input = cuda.mem_alloc(
self.batch_size * 3 * 224 * 224 * 4
)
def get_batch(self, names):
if self.batch_idx < len(self.dataloader):
data = next(self.dataloader)
cuda.memcpy_htod(self.device_input, data)
self.batch_idx += 1
return [int(self.device_input)]
else:
return None
def get_batch_size(self):
return self.batch_size
def read_calibration_cache(self):
if os.path.exists(self.cache_file):
with open(self.cache_file, "rb") as f:
return f.read()
return None
def write_calibration_cache(self, cache):
with open(self.cache_file, "wb") as f:
f.write(cache)
构建引擎时,关键参数就两个:
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = calibrator
注意:校准缓存文件(.cache)可以复用。同一个模型、同一批校准数据,第二次构建引擎时直接读缓存,省掉校准过程。我在 CI 流水线里就用这个技巧,每次构建省了 5 分钟。
4.5 ONNX Runtime 量化
ONNX Runtime 的量化更轻量。它提供了两种方式:动态量化和静态量化。
动态量化:只在推理时动态计算量化参数。简单,但速度提升有限。
import onnxruntime as ort
from onnxruntime.quantization import quantize_dynamic
quantized_model = quantize_dynamic(
model_input="model.onnx",
model_output="model_quant.onnx",
weight_type=ort.quantization.QuantType.QInt8
)
静态量化:需要校准数据,提前算好每层的量化参数。效果更好,速度提升也更明显。
from onnxruntime.quantization import quantize_static, QuantType
from onnxruntime.quantization import CalibrationMethod
quantize_static(
model_input="model.onnx",
model_output="model_quant.onnx",
calibration_data_reader=calibration_reader,
quant_format=QuantFormat.QDQ,
per_channel=True,
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8,
calibrate_method=CalibrationMethod.MinMax
)
我个人更推荐静态量化。虽然多了一步校准,但精度和速度都更可控。动态量化我一般只在快速验证时用。
4.6 量化后的精度验证
量化完别急着部署。先跑一遍验证集,看看精度变化。我一般关注两个指标:
- 精度损失:跟 FP32 模型比,Top-1/Top-5 或者 mAP 掉了多少。通常 1% 以内算正常。
- 异常层:如果某层量化后输出跟 FP32 差异特别大,那层就是瓶颈。可以用 ONNX Runtime 的调试工具逐层对比。
避坑指南:我曾经遇到一个模型,量化后精度直接崩了。排查了半天,发现是某一层的权重分布特别不均匀,大部分值集中在 0 附近,但有几个离群点特别大。MinMax 校准被离群点带偏了。换成 Percentile 校准,问题解决。
4.7 量化效果对比
拿我最近做的一个分类模型举例,效果很直观:
| 指标 | FP32 | INT8 (TensorRT) | INT8 (ORT) |
|---|---|---|---|
| 模型大小 | 120 MB | 32 MB | 32 MB |
| 推理延迟 | 15 ms | 4 ms | 6 ms |
| Top-1 精度 | 78.5% | 78.1% | 78.0% |
| 内存占用 | 1.2 GB | 0.4 GB | 0.5 GB |
你看,模型大小缩到 1/4,速度翻了 3-4 倍,精度只掉了 0.4-0.5%。这笔买卖,怎么看都划算。
4.8 知识体系总览
下面这张图,把 PTQ 的核心流程和关键决策点串起来了。我每次做量化前都会过一遍,确保没漏掉什么。
PTQ 这条路,说白了就是用一点点精度换一大截速度和内存。只要校准集选得好,大部分模型都能做到无损或接近无损量化。我建议你从 TensorRT 的 INT8 量化入手,它生态成熟,踩坑的人多,网上资料也全。等玩熟了,再试试 ONNX Runtime 的静态量化,灵活性更高。