量化类型:对称量化与非对称量化、动态量化与静态量化、量化粒度
核心原理:量化维度的三重划分
在ONNX模型量化中,我们通常从三个维度来定义一次量化操作:数值映射方式(对称/非对称)、量化执行时机(动态/静态)、参数共享范围(量化粒度)。这三个维度相互正交,组合起来便构成了实际部署中的量化方案。理解它们之间的区别,是选择正确量化策略的前提。
对称量化与非对称量化解决的是“浮点数值如何映射到整数”的问题。对称量化将浮点范围 [-max_abs, max_abs] 映射到整数范围(如INT8的 [-128, 127]),零点(zero point)固定为0,因此不需要额外存储零点偏移。非对称量化则允许浮点范围 [min, max] 映射到整数范围,零点可以不为0,从而更精细地利用整数表示范围,尤其适合权重分布偏向一侧(如ReLU后的激活值均为非负)的场景。
动态量化与静态量化关注的是“量化参数(scale和zero point)何时计算”。动态量化在推理时实时统计输入数据的范围并计算量化参数,权重则提前量化好;静态量化则使用校准数据集预先统计出每一层的输入输出范围,推理时直接使用固定参数,不再动态计算。
量化粒度决定了量化参数(scale/zero point)的共享范围。最粗的是逐张量(per-tensor),整层共用一个scale;较细的是逐通道(per-channel),卷积核的每个输出通道独立拥有scale;更细的还有逐组(per-group),将通道进一步分组。粒度越细,量化误差越小,但计算和存储开销也越大。
方法流程:在ONNX Runtime中配置量化参数
ONNX Runtime提供了两种量化API:quantize_dynamic(动态量化)和quantize_static(静态量化)。动态量化API只需一行代码即可完成,而静态量化则需要额外准备校准数据。量化粒度的选择则通过per_channel参数控制。
# 动态量化(默认per-tensor,非对称)
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic(
model_input='model_fp32.onnx',
model_output='model_dynamic_int8.onnx',
weight_type=QuantType.QInt8
)
# 静态量化(需校准数据)
from onnxruntime.quantization import quantize_static, CalibrationDataReader
from onnxruntime.quantization.shape_inference import quant_pre_process
# 第一步:先做形状推断,消除模型中的动态维度
quant_pre_process('model_fp32.onnx', 'model_preprocessed.onnx')
# 第二步:定义校准数据读取器
class MyDataReader(CalibrationDataReader):
def __init__(self, calib_data):
self.data = calib_data
self.iter = iter(calib_data)
def get_next(self):
return next(self.iter, None)
# 第三步:执行静态量化(per-channel需显式开启)
quantize_static(
model_input='model_preprocessed.onnx',
model_output='model_static_int8.onnx',
calibration_data_reader=MyDataReader(calib_samples),
quant_format=QuantFormat.QDQ, # QDQ格式便于混合精度
per_channel=True, # 开启per-channel量化
activation_type=QuantType.QUInt8, # 激活用无符号8位
weight_type=QuantType.QInt8 # 权重用有符号8位
)
per_channel=True时,ONNX Runtime会对卷积/矩阵乘的每个输出通道独立计算scale和zero point,显著降低权重量化误差。activation_type=QUInt8配合非对称量化,能更好覆盖ReLU后的非负激活分布。- 静态量化前务必执行
quant_pre_process,否则遇到动态shape会导致量化失败。
案例与场景:不同组合的实战选择
我们以三个典型ONNX模型为例,说明如何根据模型特性选择量化方案。
| 模型类型 | 代表模型 | 推荐方案 | 原因分析 |
|---|---|---|---|
| NLP(BERT类) | BERT-Squad | 动态量化 + per-tensor | 输入长度动态变化,激活范围波动大,静态校准困难;动态量化无需校准即可获得2~3倍加速 |
| 图像分类 | ResNet-50 | 静态量化 + per-tensor | 激活分布稳定,校准容易;per-tensor已足够,推理速度最快 |
| 目标检测 | YOLOv5s | 静态量化 + per-channel | 检测框回归对权重精度敏感,per-channel可保留更多信息,mAP下降通常<1% |
实际测试中,BERT模型使用动态量化后,在Intel Xeon CPU上推理延迟从12ms降至5ms,F1分数仅下降0.3%;而YOLOv5s若使用per-tensor静态量化,mAP会从0.502降至0.471(下降3.1%),改用per-channel后mAP回升至0.497(仅下降0.5%)。这说明量化粒度的选择对精度影响往往比动态/静态的选择更大。
误区与复盘:常见量化陷阱
若模型输入长度不固定(如NLP中的变长序列),静态量化校准数据难以覆盖所有长度分布,会导致推理时实际输入范围超出校准范围,产生严重精度损失。此时应优先考虑动态量化。
per-channel虽然精度更高,但并非所有推理引擎都支持。例如某些移动端NPU只支持per-tensor的INT8计算,强行使用per-channel会导致算子回退到FP32,反而失去加速效果。部署前务必确认目标硬件的支持情况。
- 先动态后静态:先用动态量化快速验证精度损失,若损失<1%则直接采用;若损失较大,再尝试静态量化。
- 校准集要有代表性:静态量化的校准数据应覆盖真实部署场景的分布,包括光照变化、噪声、不同类别比例等,建议从训练集中随机抽取500~1000张。
- 逐层分析敏感度:使用ONNX Runtime的
quantization.evaluate工具逐层比较量化前后输出差异,定位敏感层后可在QDQ格式中对该层跳过量化(设置nodes_to_exclude)。 - 验证硬件兼容性:在目标设备(CPU/GPU/NPU)上实际运行量化模型,对比延迟和精度,不要仅依赖PC端测试结果。
对称量化在权重分布接近零对称时(如经过LayerNorm的权重)效率更高,但大多数CNN权重并非严格对称,因此实际部署中非对称量化是更通用的默认选择。理解这三个维度的正交组合关系,能帮助你在不同硬件约束和精度要求下快速找到最优量化配置。