4. 校准与训练后量化原理

TensorRT 的 INT8 量化在推理阶段使用 8 位整数完成矩阵乘与卷积运算,但网络权重和激活值本身是浮点数。要让 INT8 计算尽可能接近 FP32 结果,核心问题是确定每个张量的量化尺度(scale)。TensorRT 采用对称线性量化:

q = round(x / scale)      // 量化:FP32 → INT8
x ≈ q * scale             // 反量化:INT8 → FP32
scale = max_abs / 127     // 对称量化,INT8 范围 [-127, 127]

权重在构建期即可离线统计,而激活值依赖真实输入,无法预先知道。因此 TensorRT 在构建引擎时运行校准(Calibration):用一批有代表性的输入样本前向推理,统计每个激活张量的动态范围,据此计算 scale,并写入校准缓存。这一过程即训练后量化(PTQ)的核心环节。

校准数据集的选择

校准集的质量直接决定量化精度。它不需要标注,但必须覆盖推理时可能出现的输入分布。工程上通常遵循以下原则:样本数量在 100~1000 张之间,过少会导致统计不稳,过多则构建时间线性增长;样本应覆盖不同场景、光照、尺度、类别,避免只取单一场景;预处理必须与真实推理完全一致,包括 resize、归一化、通道顺序(RGB/BGR)、均值方差,任何不一致都会让统计范围偏移。

一个常见误区是直接用训练集或验证集。训练集可能包含大量增强样本,分布与线上输入不同;验证集若类别不均衡,某些激活通道的极值会被低估。更稳妥的做法是从真实业务数据中分层抽样,并保留少量边界样本(如过曝、遮挡、小目标)。

三种校准算法

TensorRT 提供多种校准器,本质区别在于如何从统计直方图中选取阈值 T,再令 scale = T / 127。

最小最大校准(MinMax):直接取激活张量的全局最小值和最大值,令 T = max(|min|, |max|)。实现最简单,但极易受离群点影响。若某个激活值因噪声出现极端峰值,整个张量的量化分辨率会被拉低,大量正常值挤在少数几个量化级上,误差显著增大。

熵校准(Entropy):TensorRT 默认算法。它先统计激活值的直方图(默认 2048 个 bin),然后遍历候选阈值,把超出阈值的部分裁剪并累加到边界 bin,计算裁剪后分布与原始分布的 KL 散度。选择 KL 散度最小的阈值作为 T。其思想是:宁可牺牲少量极端值,也要让主体分布的量化误差最小。熵校准对离群点鲁棒,是大多数视觉模型的推荐选择。

百分位校准(Percentile):取激活值分布的某个百分位(如 99.9%、99.99%)作为阈值,直接丢弃尾部。相比熵校准,它计算更快、更易理解,适合分布已知且尾部确为噪声的场景。百分位过低会裁剪真实信号,过高则退化为 MinMax,需要结合验证集精度调参。

三种校准算法对同一激活分布的阈值选择 激活值(FP32) 频次 主体分布 长尾 / 离群点 MinMax T = max|·| 熵校准 KL 散度最小 百分位 99.9% 分位 被裁剪的尾部区域(不同算法裁剪程度不同) MinMax:保留全部极值,分辨率最低 熵校准:平衡主体与尾部 百分位:按分位裁剪

校准缓存生成与复用

校准过程耗时较长,尤其当校准集较大或模型较深时。TensorRT 允许把统计结果序列化为校准缓存(Calibration Cache),下次构建引擎时直接读取,跳过前向统计。缓存文件本质是每个张量名到其直方图或 scale 的映射。

使用 Python API 时,可继承 IInt8EntropyCalibrator2 并实现 get_batch、read_calibration_cache、write_calibration_cache 三个方法。典型实现如下:

class EntropyCalibrator(trt.IInt8EntropyCalibrator2):
    def __init__(self, data_loader, cache_file):
        super().__init__()
        self.data_loader = data_loader
        self.cache_file = cache_file
        self.batch = next(data_loader)
        self.device_input = cuda.mem_alloc(self.batch.nbytes)

    def get_batch_size(self):
        return self.batch.shape[0]

    def get_batch(self, names):
        try:
            batch = next(self.data_loader)
            cuda.memcpy_htod(self.device_input, batch)
            return [int(self.device_input)]
        except StopIteration:
            return None

    def read_calibration_cache(self):
        if os.path.exists(self.cache_file):
            with open(self.cache_file, "rb") as f:
                return f.read()

    def write_calibration_cache(self, cache):
        with open(self.cache_file, "wb") as f:
            f.write(cache)

配置构建器时设置 config.int8_calibrator = calibrator,并启用 BuilderFlag.INT8。首次构建会生成缓存文件,后续构建若模型结构、校准集、TensorRT 版本均未变化,可直接复用。需要注意:缓存与网络层名强绑定,一旦修改网络结构或更换 TensorRT 大版本,缓存可能失效,应重新校准。

校准缓存复用的前提是输入预处理、校准集分布、网络结构三者保持一致。若线上数据分布发生漂移,建议定期用新数据重新校准并更新缓存,而不是长期复用旧缓存。

校准结果如何判断

校准完成后,可通过 trtexec --int8 --calib=<cache> 构建引擎,并在验证集上对比 FP32 与 INT8 的输出。常用指标包括:Top-1/Top-5 精度下降幅度、逐层输出余弦相似度、以及检测任务中的 mAP。若精度下降超过可接受阈值(例如分类任务 Top-1 下降大于 1%),应优先检查校准集是否覆盖不足,再尝试切换熵校准与百分位校准,或对敏感层保留 FP16/FP32。

理解校准的本质,是把“如何用 256 个离散级表示连续激活分布”这一信息论问题,转化为可工程化的统计与阈值选择问题。校准集决定统计是否可信,算法决定阈值是否合理,缓存决定迭代是否高效,三者共同构成 PTQ 精度控制的基础。