量化 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 中使用了零填充(zero padding)。如果量化後無法唯一表示 0,則會導致精度誤差。
ONNX 量化表示格式
表示量化 ONNX 模型有兩種方式
- 面向運算元 (QOperator)
所有量化運算元都有其自己的 ONNX 定義,如 QLinearConv、MatMulInteger 等。 - 面向張量 (QDQ; 量化與反量化)
此格式在原始運算元之間插入 DeQuantizeLinear(QuantizeLinear(tensor)),以模擬量化和反量化過程。
在靜態量化中,QuantizeLinear 和 DeQuantizeLinear 運算元也攜帶量化引數。
在動態量化中,插入一個 ComputeQuantizationParameters 函式原型,用於即時計算量化引數。 - 透過以下方式生成的模型採用 QDQ 格式
- 透過下文所述的 quantize_static 進行量化,並設定
quant_format=QuantFormat.QDQ的模型。 - 從 Tensorflow 轉換或從 PyTorch 匯出的量化感知訓練 (QAT) 模型。
- 從 TFLite 和其他框架轉換的量化模型。
- 透過下文所述的 quantize_static 進行量化,並設定
對於後兩種情況,無需使用量化工具對模型進行量化。ONNX Runtime 可以直接將其作為量化模型執行。
下圖展示了量化卷積的 QOperator 和 QDQ 格式的等效表示。此端到端示例演示了這兩種格式。

量化 ONNX 模型
ONNX Runtime 提供 Python API,用於將 32 位浮點模型轉換為 8 位整數模型,即“量化”。這些 API 包括預處理、動態/靜態量化和除錯。
預處理
預處理旨在轉換 float32 模型,為量化做好準備。它包含以下三個可選步驟
- 符號形狀推斷(Symbolic shape inference)。這最適合 Transformer 模型。
- 模型最佳化:此步驟使用 ONNX Runtime 原生庫重寫計算圖,包括合併計算節點、消除冗餘以提高執行時效率。
- ONNX 形狀推斷。
這些步驟的目標是提高量化質量。當張量形狀已知時,我們的量化工具工作效果最好。符號形狀推斷和 ONNX 形狀推斷都有助於確定張量形狀。符號形狀推斷最適合基於 Transformer 的模型,而 ONNX 形狀推斷適用於其他模型。
模型最佳化執行特定的運算元融合,使量化工具的工作更容易。例如,卷積運算元後跟 BatchNormalization 可以在最佳化期間融合為一個,從而可以非常高效地進行量化。
遺憾的是,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 Runtime 提供了 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 代表其餘兩種格式。
CPU 上的 ONNX Runtime 量化可以執行 U8U8, U8S8 和 S8S8。帶有 QDQ 的 S8S8 是預設設定,平衡了效能和精度。它應該是首選。只有在精度下降嚴重的情況下,您才可以嘗試 U8U8。注意,帶有 QOperator 的 S8S8 在 x86-64 CPU 上會很慢,通常應避免使用。GPU 上的 ONNX Runtime 量化僅支援 S8S8。
何時以及為何需要嘗試 U8U8?
在具有 AVX2 和 AVX512 擴充套件的 x86-64 機器上,ONNX Runtime 為了效能會使用 VPMADDUBSW 指令進行 U8S8 計算。此指令可能會遇到飽和問題:輸出可能不適合 16 位整數,必須進行鉗位(飽和)以使其適合。通常,這對於最終結果不是大問題。但是,如果您確實遇到了嚴重的精度下降,則可能是由飽和引起的。在這種情況下,您可以嘗試 reduce_range 或沒有飽和問題的 U8U8 格式。
在其他 CPU 架構(帶有 VNNI 和 Arm® 的 x64)上不存在此類問題。
支援的量化運算元列表
請參閱登錄檔獲取支援的運算元列表。
量化和模型 opset 版本
模型必須為 opset10 或更高版本才能進行量化。opset < 10 的模型必須使用更新的 opset 從其原始框架重新轉換為 ONNX。
基於 Transformer 的模型
針對基於 Transformer 的模型有特定的最佳化,例如用於量化注意力層的 QAttention。為了利用這些最佳化,您需要在量化模型之前使用 Transformer 模型最佳化工具最佳化您的模型。
此筆記本演示了該過程。
GPU 上的量化
在 GPU 上透過量化獲得更好的效能需要硬體支援。您需要一個支援 Tensor Core int8 計算的裝置,例如 T4 或 A100。較舊的硬體將無法從量化中受益。
ONNX Runtime 目前利用 TensorRT 執行提供程式 (Execution Provider) 在 GPU 上進行量化。與 CPU 執行提供程式不同,TensorRT 接收一個全精度模型和一個輸入校準結果。它使用自己的邏輯決定如何量化。利用 TensorRT EP 量化的總體流程是
- 實現一個 CalibrationDataReader。
- 使用校準資料集計算量化引數。注意:為了包含模型中的所有張量以獲得更好的校準效果,請先執行
symbolic_shape_infer.py。請參考此處瞭解詳情。 - 將量化引數儲存到 flatbuffer 檔案中
- 載入模型和量化引數檔案,並使用 TensorRT EP 執行。
我們提供了兩個端到端示例:Yolo V3 和 resnet50。
量化為 Int4/UInt4
ONNX Runtime 可以將模型中的某些運算元量化為 4 位整數型別。塊級權重量化應用於這些運算元。支援的運算元型別為
- 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 的機器上不需要此操作。
逐通道量化(Per-channel quantization)可以提高權重範圍較大的模型的精度。如果精度損失很大,請嘗試此方法。在 AVX2 和 AVX512 機器上,如果啟用了逐通道量化,通常還需要啟用 reduce-range。
為什麼 MaxPool 等運算元沒有被量化?
ONNX opset 12 中增加了對 MaxPool 等某些運算元的 8 位型別支援。請檢查您的模型版本並將其升級到 opset 12 或更高版本。