量化 ONNX 模型
目录
量化概述
ONNX Runtime 中的量化是指 ONNX 模型的 8 位线性量化。
在量化过程中,浮点值被映射到 8 位量化空间,形式为:val_fp32 = scale * (val_quantized - zero_point)
scale
是一个正实数,用于将浮点数映射到量化空间。其计算方法如下:
对于非对称量化
scale = (data_range_max - data_range_min) / (quantization_range_max - quantization_range_min)
对于对称量化
scale = max(abs(data_range_max), abs(data_range_min)) * 2 / (quantization_range_max - quantization_range_min)
zero_point
表示量化空间中的零点。重要的是,浮点零值必须在量化空间中精确表示。这是因为许多 CNN 中都使用零填充。如果量化后无法唯一表示 0,则会导致精度误差。
ONNX 量化表示格式
有两种方式表示量化的 ONNX 模型
- 面向运算符(QOperator)
所有量化后的运算符都有其自己的 ONNX 定义,例如 QLinearConv、MatMulInteger 等。 - 面向张量(QDQ;Quantize and DeQuantize)
这种格式在原始运算符之间插入 DeQuantizeLinear(QuantizeLinear(tensor)) 以模拟量化和反量化过程。
在静态量化中,QuantizeLinear 和 DeQuantizeLinear 运算符也带有量化参数。
在动态量化中,会插入一个 ComputeQuantizationParameters 函数原型来动态计算量化参数。 - 通过以下方式生成的模型采用 QDQ 格式
- 通过 quantize_static(下文解释)且
quant_format=QuantFormat.QDQ
进行量化的模型。 - 从 Tensorflow 转换或从 PyTorch 导出的量化感知训练 (QAT) 模型。
- 从 TFLite 和其他框架转换的量化模型。
- 通过 quantize_static(下文解释)且
对于后两种情况,您无需使用量化工具对模型进行量化。ONNX Runtime 可以直接将它们作为量化模型运行。
下图显示了量化 Conv 的 QOperator 和 QDQ 格式的等效表示。这个端到端示例演示了这两种格式。
量化 ONNX 模型
ONNX Runtime 提供 Python API,用于将 32 位浮点模型转换为 8 位整数模型,即量化。这些 API 包括预处理、动态/静态量化和调试。
预处理
预处理是将 float32 模型转换为准备好进行量化的过程。它包括以下三个可选步骤:
- 符号形状推断。这最适合 Transformer 模型。
- 模型优化:此步骤使用 ONNX Runtime 本机库重写计算图,包括合并计算节点、消除冗余以提高运行时效率。
- ONNX 形状推断。
这些步骤的目标是提高量化质量。当张量形状已知时,我们的量化工具效果最佳。符号形状推断和 ONNX 形状推断都有助于确定张量形状。符号形状推断最适用于基于 Transformer 的模型,而 ONNX 形状推断适用于其他模型。
模型优化执行某些运算符融合,使量化工具的工作更轻松。例如,一个卷积运算符后跟批归一化可以在优化过程中融合为一个,从而可以非常高效地进行量化。
不幸的是,ONNX Runtime 的一个已知问题是模型优化无法输出大于 2GB 的模型。因此,对于大型模型,必须跳过优化。
预处理 API 位于 Python 模块 onnxruntime.quantization.shape_inference
中的函数 quant_pre_process()
。请参阅 shape_inference.py
。要了解有关预处理的更多选项和更精细的控制,请运行以下命令:
python -m onnxruntime.quantization.preprocess --help
模型优化也可以在量化期间进行。然而,即使由于历史原因这是默认行为,但不推荐这样做。量化期间的模型优化会给调试由量化引起的精度损失带来困难,这将在后续章节中讨论。因此,最好在预处理期间而不是在量化期间执行模型优化。
动态量化
模型量化有两种方式:动态量化和静态量化。动态量化动态计算激活的量化参数(缩放因子和零点)。这些计算会增加推理成本,但通常比静态量化能获得更高的精度。
动态量化的 Python API 位于模块 onnxruntime.quantization.quantize
中的函数 quantize_dynamic()
。
静态量化
静态量化方法首先使用一组称为校准数据(calibration data)的输入运行模型。在这些运行期间,我们计算每个激活的量化参数。这些量化参数作为常量写入到量化模型中,并用于所有输入。我们的量化工具支持三种校准方法:MinMax、Entropy 和 Percentile。请参阅 calibrate.py
了解详细信息。
静态量化的 Python API 位于模块 onnxruntime.quantization.quantize
中的函数 quantize_static()
。请参阅 quantize.py 了解详细信息。
量化调试
量化并非无损转换。它可能会对模型的精度产生负面影响。解决此问题的方法是比较原始计算图与量化后计算图的权重和激活张量,找出它们差异最大的地方,并避免量化这些张量,或者选择其他量化/校准方法。这被称为量化调试。为了方便此过程,我们提供了 Python API,用于匹配 float32 模型及其量化对应模型的权重和激活张量。
调试 API 位于模块 onnxruntime.quantization.qdq_loss_debug
,它具有以下功能:
- 函数
create_weight_matching()
。它接受一个 float32 模型及其量化模型,并输出一个字典,用于匹配这两个模型之间对应的权重。 - 函数
modify_model_output_intermediate_tensors()
。它接受一个 float32 或量化模型,并对其进行增强以保存所有激活。 - 函数
collect_activations()
。它接受一个由modify_model_output_intermediate_tensors()
增强的模型和一个输入数据读取器,运行增强模型以收集所有激活。 - 函数
create_activation_matching()
。您可以想象您在 float32 模型及其量化模型上都运行collect_activations(modify_model_output_intermediate_tensors())
,以收集两组激活。此函数接受这两组激活,并匹配相应的激活,以便用户可以轻松比较它们。
总而言之,ONNX Runtimes 提供了 Python API,用于匹配 float32 模型及其量化对应模型之间的相应权重和激活张量。这允许用户轻松比较它们,以找到最大的差异所在。
然而,量化期间的模型优化会给这个调试过程带来困难,因为它可能会显著改变计算图,导致量化模型与原始模型截然不同。这使得匹配来自两个模型的相应张量变得困难。因此,我们建议在预处理期间而不是量化过程中执行模型优化。
示例
- 动态量化
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
model_fp32 = 'path/to/the/model.onnx'
model_quant = 'path/to/the/model.quant.onnx'
quantized_model = quantize_dynamic(model_fp32, model_quant)
- 静态量化:请参阅端到端示例。
方法选择
动态量化和静态量化之间的主要区别在于激活的缩放因子和零点如何计算。对于静态量化,它们是预先(离线)使用校准数据集计算的。因此,激活在每个前向传播中都具有相同的缩放因子和零点。对于动态量化,它们是即时(在线)计算的,并且特定于每个前向传播。因此,它们更准确,但会引入额外的计算开销。
一般来说,对于 RNN 和基于 Transformer 的模型,建议使用动态量化;对于 CNN 模型,建议使用静态量化。
如果两种训练后量化方法都无法达到您的精度目标,您可以尝试使用量化感知训练 (QAT) 来重新训练模型。ONNX Runtime 目前不提供再训练功能,但您可以使用原始框架重新训练模型并将其转换回 ONNX。
数据类型选择
量化值是 8 位宽,可以是带符号(int8)或无符号(uint8)的。我们可以分别选择激活和权重的符号性,因此数据格式可以是(激活:uint8,权重:uint8),(激活:uint8,权重:int8)等。我们用 U8U8 作为(激活:uint8,权重:uint8)的简称,U8S8 作为(激活:uint8,权重:int8)的简称,S8U8 和 S8S8 以此类推用于剩余两种格式。
ONNX Runtime 在 CPU 上的量化可以运行 U8U8、U8S8 和 S8S8。S8S8 和 QDQ 是默认设置,并在性能和精度之间取得平衡。它应该是首选。只有在精度下降很多的情况下,您可以尝试 U8U8。请注意,QOperator 的 S8S8 在 x86-64 CPU 上会很慢,通常应避免使用。ONNX Runtime 在 GPU 上的量化仅支持 S8S8。
我何时以及为何需要尝试 U8U8?
在具有 AVX2 和 AVX512 扩展的 x86-64 机器上,ONNX Runtime 使用 VPMADDUBSW 指令进行 U8S8 以提高性能。此指令可能会出现饱和问题:输出可能无法容纳 16 位整数,并且必须被钳位(饱和)以适应。通常,这对最终结果影响不大。但是,如果您确实遇到较大的精度下降,则可能是由饱和引起的。在这种情况下,您可以尝试 reduce_range 或 U8U8 格式,后者没有饱和问题。
在其他 CPU 架构(带 VNNI 的 x64 和 Arm®)上没有此类问题。
支持的量化操作列表
请参阅 注册表 以获取支持的操作列表。
量化和模型 opset 版本
模型必须是 opset10 或更高版本才能进行量化。opset < 10 的模型必须使用更新的 opset 从其原始框架重新转换为 ONNX。
基于 Transformer 的模型
针对基于 Transformer 的模型有一些特定的优化,例如用于量化注意力层的 QAttention。为了利用这些优化,您需要在量化模型之前,使用 Transformer 模型优化工具优化您的模型。
此笔记本演示了整个过程。
GPU 上的量化
在 GPU 上实现量化更好的性能需要硬件支持。您需要一个支持 Tensor Core int8 计算的设备,例如 T4 或 A100。旧硬件无法从量化中受益。
ONNX Runtime 现在利用 TensorRT 执行提供程序在 GPU 上进行量化。与 CPU 执行提供程序不同,TensorRT 接受全精度模型和输入的校准结果。它根据自己的逻辑决定如何量化。利用 TensorRT EP 量化的总体流程是:
- 实现 CalibrationDataReader。
- 使用校准数据集计算量化参数。注意:为了包含模型中所有张量以实现更好的校准,请先运行
symbolic_shape_infer.py
。请参阅此处了解详细信息。 - 将量化参数保存到 flatbuffer 文件中
- 加载模型和量化参数文件,并使用 TensorRT EP 运行。
我们提供了两个端到端示例:Yolo V3 和 resnet50。
量化到 Int4/UInt4
ONNX Runtime 可以将模型中的某些运算符量化为 4 位整数类型。对这些运算符应用块级仅权重(weight-only)量化。支持的运算符类型有:
- MatMul:
- 仅当输入
B
为常量时,节点才会被量化 - 支持 QOperator 或 QDQ 格式。
- 如果选择了 QOperator,节点将转换为 MatMulNBits 节点。权重
B
将进行块级量化并保存到新节点中。支持 HQQ、GPTQ 和 RTN(默认)算法。 - 如果选择了 QDQ,MatMul 节点将替换为 DequantizeLinear -> MatMul 对。权重
B
将进行块级量化并作为初始化器保存在 DequantizeLinear 节点中。
- 仅当输入
- Gather:
- 仅当输入
data
为常量时,节点才会被量化。 - 支持 QOperator
- Gather 量化为 GatherBlockQuantized 节点。输入
data
将进行块级量化并保存到新节点中。仅支持 RTN 算法。
- 仅当输入
由于 Int4/UInt4 类型在 onnx opset 21 中引入,如果模型的 onnx 域版本 < 21,它将被强制升级到 opset 21。请确保模型中的运算符与 onnx opset 21 兼容。
要运行包含 GatherBlockQuantized 节点的模型,需要 ONNX Runtime 1.20。
代码示例
from onnxruntime.quantization import (
matmul_4bits_quantizer,
quant_utils,
quantize
)
from pathlib import Path
model_fp32_path="path/to/orignal/model.onnx"
model_int4_path="path/to/save/quantized/model.onnx"
quant_config = matmul_4bits_quantizer.DefaultWeightOnlyQuantConfig(
block_size=128, # 2's exponential and >= 16
is_symmetric=True, # if true, quantize to Int4. otherwise, quantize to uint4.
accuracy_level=4, # used by MatMulNbits, see https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#attributes-35
quant_format=quant_utils.QuantFormat.QOperator,
op_types_to_quantize=("MatMul","Gather"), # specify which op types to quantize
quant_axes=(("MatMul", 0), ("Gather", 1),) # specify which axis to quantize for an op type.
model = quant_utils.load_model_with_shape_infer(Path(model_fp32_path))
quant = matmul_4bits_quantizer.MatMul4BitsQuantizer(
model,
nodes_to_exclude=None, # specify a list of nodes to exclude from quantization
nodes_to_include=None, # specify a list of nodes to force include from quantization
algo_config=quant_config,)
quant.process()
quant.model.save_model_to_file(
model_int4_path,
True) # save data to external file
有关 AWQ 和 GTPQ 量化用法,请参阅 Gen-AI 模型构建器。
常见问题
为什么我没有看到性能提升?
性能提升取决于您的模型和硬件。量化带来的性能提升有两个方面:计算和内存。旧硬件没有或很少有执行 int8 高效推理所需的指令。而且量化有开销(来自量化和反量化),因此在旧设备上获得更差的性能并非罕见。
支持 VNNI 的 x86-64、支持 Tensor Core int8 的 GPU 以及支持点积指令的 Arm® 处理器通常可以获得更好的性能。
我应该选择哪种量化方法,动态量化还是静态量化?
请参阅方法选择一节。
何时使用 reduce-range 和逐通道量化?
Reduce-range 会将权重量化为 7 位。它专为 AVX2 和 AVX512(非 VNNI)机器上的 U8S8 格式设计,以缓解饱和问题。在支持 VNNI 的机器上则不需要。
对于权重范围较大的模型,逐通道量化可以提高精度。如果精度损失较大,请尝试使用它。在 AVX2 和 AVX512 机器上,如果启用逐通道量化,通常还需要启用 reduce-range。
为什么像 MaxPool 这样的运算符没有被量化?
ONNX opset 12 中增加了对 MaxPool 等某些运算符的 8 位类型支持。请检查您的模型版本并将其升级到 opset 12 及以上。