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在训练时就模拟了量化的“噪声”,模型学会了抵抗这种噪声。
4.3 INT4量化:更极致的压缩
INT4比INT8更进一步。表示范围只有16个值(-8到7),精度损失更大。但好处也明显:模型体积再减半,推理速度还能再提。
我一般在以下场景才用INT4:
- 模型本身很大(比如ViT),边缘设备内存吃紧
- 对精度要求不那么苛刻(比如简单的分类任务)
- 硬件原生支持INT4(比如某些NPU)
INT4量化有个常见问题:离群值。如果激活值里有个别特别大的值,整个量化范围会被拉宽,导致大部分值都挤在几个整数上。我遇到过这种情况:一个层的激活值99%都在[-1, 1]之间,但有个别值到了10。量化后,大部分值都变成了0,信息全丢了。
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 | 旧版算法 | 兼容旧模型,不推荐新项目用 |
对于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
最后说一句:量化不是银弹。如果你的模型本身精度就不高(比如只有85%),量化后掉到83%可能就不可用了。我一般会先确保FP32模型的精度足够好,再考虑量化。毕竟,量化只是锦上添花,不是雪中送炭。