4. 量化技术实战

量化,说白了就是把模型里那些高精度的浮点数(FP32),硬塞进低精度的整数(INT8、INT4)里。为什么要这么干?因为边缘设备上的算力和带宽都金贵得很。我刚开始接触量化时,总觉得这是“有损压缩”,心里有点抵触。后来在Jetson Orin上跑一个YOLOv8模型,FP32版本只能跑30帧,量化到INT8后直接飙到90帧,而且精度只掉了0.3%。嗯,从那以后我就真香了。

4.1 量化原理:从FP32到INT8的映射

量化的核心公式其实很简单。就是把一个浮点数的范围,映射到整数的范围里。

// 量化公式:FP32 = INT * scale + zero_point
// 反量化公式:INT = round((FP32 - zero_point) / scale)

// 以对称量化为例(zero_point = 0)
// INT8 范围:[-128, 127]
// 假设FP32的激活值范围是 [-6.0, 6.0]
scale = (6.0 - (-6.0)) / (127 - (-128)) = 12.0 / 255 ≈ 0.04706
// 那么 FP32 的 3.0 量化后:
INT = round(3.0 / 0.04706) ≈ 64

这里有两个关键参数:scale(缩放因子)和 zero_point(零点偏移)。

量化类型 zero_point 适用场景 我踩过的坑
对称量化 固定为0 权重(分布对称) ReLU后的激活值用对称量化,浪费了一半的表示范围
非对称量化 根据数据分布计算 激活值(分布偏斜) 计算时多一个减法,硬件不支持就白搭
逐张量量化 每个张量一个scale 通用场景 精度够用,但粒度粗
逐通道量化 每个通道一个scale 卷积权重 精度更好,但硬件支持有限
我的习惯:权重用对称逐通道量化,激活值用非对称逐张量量化。这个组合在大多数边缘芯片上都能兼顾精度和性能。

4.2 PTQ vs QAT:两种量化路线

量化不是直接把模型扔进去就完事了。主要有两条路:训练后量化(PTQ)和量化感知训练(QAT)。

PTQ(Post-Training Quantization):模型训练完后,拿一小部分校准数据跑一遍,统计出每层的scale和zero_point。然后直接转换。

QAT(Quantization-Aware Training):在训练过程中模拟量化误差,让模型自己学会适应低精度。

怎么选?我一般这么判断:

  • 选PTQ的情况:模型已经训好了,不想动训练流程。或者数据量太大,重新训练成本高。我有个项目,用1000张图片做校准,PTQ后精度只掉了0.5%,完全够用。
  • 选QAT的情况:模型对精度极其敏感(比如人脸识别、关键点检测)。或者要量化到INT4这种极低精度。我曾经在一个人脸识别模型上,PTQ后精度掉了2%,换成QAT后只掉了0.3%。

你想想看,QAT为什么比PTQ好?因为PTQ是“事后诸葛亮”,它不知道量化后的误差会怎么影响后续层。而QAT在训练时就模拟了量化的“噪声”,模型学会了抵抗这种噪声。

注意:QAT不是简单的“训练时加量化”。它需要在前向传播时插入伪量化节点(FakeQuant),反向传播时用直通估计器(STE)来近似梯度。我刚开始实现时,忘了加STE,结果梯度全变成0,模型根本训不动。

4.3 INT4量化:更极致的压缩

INT4比INT8更进一步。表示范围只有16个值(-8到7),精度损失更大。但好处也明显:模型体积再减半,推理速度还能再提。

我一般在以下场景才用INT4:

  • 模型本身很大(比如ViT),边缘设备内存吃紧
  • 对精度要求不那么苛刻(比如简单的分类任务)
  • 硬件原生支持INT4(比如某些NPU)

INT4量化有个常见问题:离群值。如果激活值里有个别特别大的值,整个量化范围会被拉宽,导致大部分值都挤在几个整数上。我遇到过这种情况:一个层的激活值99%都在[-1, 1]之间,但有个别值到了10。量化后,大部分值都变成了0,信息全丢了。

解决办法:用per-group量化。把通道分成小组(比如每组32个通道),每组独立计算scale。或者用平滑量化,先对权重做平滑处理,再量化。我在MobileNetV3上试过,INT4量化后精度从78%提到了81%。

4.4 TensorRT量化部署实战

TensorRT是NVIDIA的推理优化库。它支持PTQ和QAT两种方式。我一般用它的PTQ流程,因为快。

下面是一个完整的TensorRT INT8量化部署流程:

import tensorrt as trt
from cuda import cudart

# 1. 创建builder和config
builder = trt.Builder(trt.Logger(trt.Logger.WARNING))
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()

# 2. 设置INT8模式
config.set_flag(trt.BuilderFlag.INT8)

# 3. 设置校准器(PTQ的关键)
class MyCalibrator(trt.IInt8EntropyCalibrator2):
    def __init__(self, calibration_data, batch_size):
        trt.IInt8EntropyCalibrator2.__init__(self)
        self.calibration_data = calibration_data
        self.batch_size = batch_size
        self.current_index = 0
        self.device_input = cudart.cudaMalloc(batch_size * 3 * 224 * 224 * 4)[1]

    def get_batch_size(self):
        return self.batch_size

    def get_batch(self, names):
        if self.current_index + self.batch_size > len(self.calibration_data):
            return None
        batch = self.calibration_data[self.current_index:self.current_index + self.batch_size]
        self.current_index += self.batch_size
        cudart.cudaMemcpy(self.device_input, batch.ctypes.data, batch.nbytes, cudart.cudaMemcpyHostToDevice)
        return [int(self.device_input)]

    def read_calibration_cache(self):
        return None

    def write_calibration_cache(self, cache):
        with open('calibration.cache', 'wb') as f:
            f.write(cache)

calibrator = MyCalibrator(calibration_data, batch_size=32)
config.int8_calibrator = calibrator

# 4. 构建引擎
serialized_engine = builder.build_serialized_network(network, config)

# 5. 保存引擎
with open('model_int8.engine', 'wb') as f:
    f.write(serialized_engine)

这段代码里,最核心的是校准器。它决定了scale和zero_point怎么算。TensorRT提供了几种校准算法:

校准器 原理 我推荐的使用场景
IInt8EntropyCalibrator2 最小化KL散度 通用场景,精度最好
IInt8MinMaxCalibrator 直接取min/max 对离群值敏感的场景
IInt8LegacyCalibrator 旧版算法 兼容旧模型,不推荐新项目用
我曾经踩过的坑:校准数据一定要和实际部署场景的数据分布一致。有一次我用ImageNet的图片做校准,但实际部署的是工业检测场景(全是金属表面缺陷)。结果量化后精度掉了5%。后来换成实际场景的1000张图片做校准,精度只掉了0.8%。

对于INT4量化,TensorRT需要额外设置:

# 在config中启用INT4
config.set_flag(trt.BuilderFlag.INT4)
# 注意:INT4需要硬件支持(如Ada Lovelace架构的GPU)
# 如果不支持,builder会报错

嗯,这里要注意:不是所有层都适合量化。有些层(比如Softmax、LayerNorm)对精度极其敏感,强行量化反而会拖慢速度(因为需要频繁反量化)。我一般会在网络定义时,手动指定哪些层跳过量化:

# 在构建网络时,为特定层设置精度
layer = network.add_convolution(...)
layer.precision = trt.float32  # 强制该层用FP32
TensorRT INT8量化部署流程 FP32模型 校准数据准备 校准器计算scale/zero_point 构建INT8推理引擎 精度验证 部署上线 精度不达标,回退

最后说一句:量化不是银弹。如果你的模型本身精度就不高(比如只有85%),量化后掉到83%可能就不可用了。我一般会先确保FP32模型的精度足够好,再考虑量化。毕竟,量化只是锦上添花,不是雪中送炭。