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 模型来做。

基本步骤:

  1. 导出 ONNX 模型
  2. 准备校准数据集(通常 500-1000 张图就够了)
  3. 实现校准器类
  4. 构建 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)核心流程 FP32 训练模型 准备校准数据集 选择校准方法:MinMax / Percentile / KL / MSE 选择量化策略:对称 / 非对称 选择工具:TensorRT / ONNX Runtime INT8 量化模型 关键:校准集质量 > 校准方法 > 量化策略,校准集没选好,后面再折腾也白搭

PTQ 这条路,说白了就是用一点点精度换一大截速度和内存。只要校准集选得好,大部分模型都能做到无损或接近无损量化。我建议你从 TensorRT 的 INT8 量化入手,它生态成熟,踩坑的人多,网上资料也全。等玩熟了,再试试 ONNX Runtime 的静态量化,灵活性更高。