Open1

ONNXモデル量子化の翻訳

nnn112358nnn112358

原文
https://github.com/microsoft/onnxruntime/blob/gh-pages/docs/performance/model-optimizations/quantization.md

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は量子化空間における0を表します。浮動小数点の0値が量子化空間で正確に表現できることが重要です。これは多くのCNNでゼロパディングが使用されているためです。量子化後に0を一意に表現できない場合、精度エラーが発生します。

ONNX量子化の表現形式

量子化されたONNXモデルを表現する方法には2つあります:

  • オペレータ指向(QOperator):
    すべての量子化オペレータは、QLinearConv、MatMulIntegerなど、独自のONNX定義を持っています。
  • テンソル指向(QDQ; 量子化と逆量子化):
    このフォーマットでは、量子化と逆量子化のプロセスをシミュレートするために、元のオペレータ間にDeQuantizeLinear(QuantizeLinear(tensor))を挿入します。
    静的量子化では、QuantizeLinearとDeQuantizeLinearのオペレータも量子化パラメータを保持します。
    動的量子化では、量子化パラメータをその場で計算するためにComputeQuantizationParametersファンクションプロトが挿入されます。
  • 以下の方法で生成されたモデルはQDQフォーマットになります:
    1. quant_format=QuantFormat.QDQを指定して、以下で説明するquantize_staticによって量子化されたモデル
    2. TensorflowからコンバートされたまたはPyTorchからエクスポートされた量子化対応学習(QAT)モデル
    3. TFLiteやその他のフレームワークからコンバートされた量子化モデル

後者2つのケースでは、量子化ツールでモデルを量子化する必要はありません。ONNX Runtimeは量子化モデルとして直接実行できます。

下の図は、量子化されたConvに対するQOperatorフォーマットとQDQフォーマットの等価な表現を示しています。このエンドツーエンドの例で2つのフォーマットを実証しています。

基本的な最適化と拡張された最適化によるノードの変更

ONNXモデルの量子化

ONNX Runtimeは、32ビット浮動小数点モデルを8ビット整数モデルに変換する(量子化する)ためのPython APIを提供しています。これらのAPIには、前処理、動的/静的量子化、デバッグが含まれます。

前処理

前処理は、float32モデルを量子化に向けて準備するための変換です。以下の3つのオプションステップで構成されています:

  1. シンボリック形状推論: これはトランスフォーマーモデルに最も適しています。
  2. モデル最適化: このステップではONNX Runtimeのネイティブライブラリを使用して計算グラフを書き換え、計算ノードの統合、冗長性の排除を行い、実行時の効率を向上させます。
  3. ONNX形状推論。

これらのステップの目的は量子化の品質を向上させることです。量子化ツールは、テンソルの形状が既知の場合に最も効果的に機能します。シンボリック形状推論とONNX形状推論の両方がテンソルの形状を把握するのに役立ちます。シンボリック形状推論はトランスフォーマーベースのモデルで最も効果を発揮し、ONNX形状推論は他のモデルで機能します。

モデル最適化では、量子化ツールの作業を容易にする特定のオペレータフュージョンを実行します。例えば、BatchNormalizationが後続するConvolutionオペレータは、最適化中に1つに融合され、非常に効率的に量子化できます。

残念ながら、ONNX Runtimeの既知の問題として、モデル最適化は2GB以上のモデルサイズを出力できません。そのため、大規模なモデルでは最適化をスキップする必要があります。

前処理APIはPythonモジュールonnxruntime.quantization.shape_inferencequant_pre_process()関数にあります。shape_inference.pyを参照してください。前処理に利用可能な追加オプションとより詳細な制御について読むには、以下のコマンドを実行してください:

python -m onnxruntime.quantization.preprocess --help

モデル最適化は量子化中にも実行できます。ただし、これは履歴的な理由でデフォルトの動作となっていますが、推奨されません。量子化中のモデル最適化は、後のセクションで説明する量子化による精度低下のデバッグを困難にします。したがって、量子化中ではなく前処理中にモデル最適化を実行することをお勧めします。

動的量子化

モデルを量子化する方法には、動的と静的の2つがあります。動的量子化は、アクティベーションの量子化パラメータ(スケールとゼロポイント)を動的に計算します。これらの計算は推論のコストを増加させますが、通常は静的なものと比較してより高い精度を達成します。

動的量子化のPython APIは、モジュールonnxruntime.quantization.quantizeの関数quantize_dynamic()にあります。

静的量子化

静的量子化手法では、まずキャリブレーションデータと呼ばれる一連の入力を使用してモデルを実行します。これらの実行中に、各アクティベーションの量子化パラメータを計算します。これらの量子化パラメータは量子化モデルに定数として書き込まれ、すべての入力に対して使用されます。量子化ツールは、MinMax、エントロピー、パーセンタイルの3つのキャリブレーション手法をサポートしています。詳細についてはcalibrate.pyを参照してください。

静的量子化のPython APIは、モジュールonnxruntime.quantization.quantizeの関数quantize_static()にあります。詳細についてはquantize.pyを参照してください。

量子化デバッグ

量子化は損失のある変換です。モデルの精度に悪影響を与える可能性があります。この問題の解決策は、元の計算グラフと量子化されたグラフの重みとアクティベーションテンソルを比較し、最も差異が大きい箇所を特定し、これらのテンソルの量子化を避けるか、別の量子化/キャリブレーション手法を選択することです。これを量子化デバッグと呼びます。このプロセスを容易にするため、float32モデルとその量子化されたモデルの間で重みとアクティベーションテンソルを照合するためのPython APIを提供しています。

デバッグ用のAPIはモジュールonnxruntime.quantization.qdq_loss_debugにあり、以下の関数があります:

  • 関数create_weight_matching(): float32モデルとその量子化モデルを受け取り、これら2つのモデル間の対応する重みを照合する辞書を出力します。
  • 関数modify_model_output_intermediate_tensors(): float32または量子化モデルを受け取り、すべてのアクティベーションを保存するように拡張します。
  • 関数collect_activations(): modify_model_output_intermediate_tensors()で拡張されたモデルと入力データリーダーを受け取り、拡張されたモデルを実行してすべてのアクティベーションを収集します。
  • 関数create_activation_matching(): float32モデルとその量子化モデルの両方でcollect_activations(modify_model_output_intermediate_tensors())を実行して2セットのアクティベーションを収集すると想像できます。この関数はこれら2セットのアクティベーションを受け取り、対応するものを照合して、ユーザーが容易に比較できるようにします。

要約すると、ONNX Runtimeは、float32モデルとその量子化されたモデル間の対応する重みとアクティベーションテンソルを照合するためのPython APIを提供します。これにより、ユーザーは容易にそれらを比較して、最大の差異がある場所を特定できます。

ただし、量子化中のモデル最適化はこのデバッグプロセスを困難にします。計算グラフを大幅に変更する可能性があり、その結果、元のものと大きく異なる量子化モデルが生成される可能性があるためです。これにより、2つのモデルから対応するテンソルを照合することが困難になります。そのため、量子化プロセス中ではなく、前処理中にモデル最適化を実行することを推奨します。

  • 動的量子化:
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やトランスフォーマーベースのモデルには動的量子化を、CNNモデルには静的量子化を使用することが推奨されます。

どちらの後量子化手法も精度の目標を満たせない場合は、量子化対応学習(QAT)を使用してモデルを再学習することができます。ONNX Runtimeは現時点で再学習を提供していませんが、元のフレームワークでモデルを再学習し、ONNXに変換し直すことができます。

データ型の選択

量子化された値は8ビット幅で、符号付き(int8)または符号なし(uint8)のいずれかです。アクティベーションと重みの符号の有無を個別に選択できるため、データ形式には(アクティベーション: uint8, 重み: uint8)、(アクティベーション: uint8, 重み: int8)などがあります。
(アクティベーション: uint8, 重み: uint8)をU8U8、(アクティベーション: uint8, 重み: int8)をU8S8、同様にS8U8とS8S8を残りの2つの形式の省略形として使用しましょう。

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はU8S8に対してVPMADDUBSW命令を性能向上のために使用します。この命令は飽和の問題に悩まされる可能性があります:出力が16ビット整数に収まらず、収めるために制限(飽和)される場合があります。一般的に、これは最終結果に大きな影響を与えません。ただし、大幅な精度低下が発生した場合、飽和が原因である可能性があります。この場合、reduce_rangeまたはU8U8フォーマットを試すことができます。U8U8には飽和の問題がありません。

このような問題は、他のCPUアーキテクチャ(VNNIを搭載したx64およびArm®)では発生しません。

サポートされている量子化オペレータのリスト

サポートされているオペレータのリストについては、レジストリを参照してください。

量子化とモデルのopsetバージョン

モデルを量子化するにはopset10以上である必要があります。opset 10未満のモデルは、より新しいopsetを使用して元のフレームワークからONNXに再変換する必要があります。

トランスフォーマーベースのモデル

トランスフォーマーベースのモデルには、注意層の量子化のためのQAttentionなど、特定の最適化が存在します。これらの最適化を活用するには、モデルを量子化する前にTransformer Model Optimization Toolを使用してモデルを最適化する必要があります。

このノートブックでそのプロセスを説明しています。

GPUにおける量子化

GPUで量子化による性能向上を実現するには、ハードウェアのサポートが必要です。T4やA100のようなTensor Core int8計算をサポートするデバイスが必要です。古いハードウェアは量子化の恩恵を受けることができません。

ONNX RuntimeはGPUでの量子化にTensorRT Execution Providerを活用しています。CPU Execution Providerとは異なり、TensorRTは単精度モデルと入力のキャリブレーション結果を受け取り、独自のロジックで量子化方法を決定します。TensorRT EP量子化を活用する全体的な手順は以下の通りです:

  • CalibrationDataReaderを実装します。
  • キャリブレーションデータセットを使用して量子化パラメータを計算します。注: より良いキャリブレーションのためにモデルからすべてのテンソルを含めるには、まずsymbolic_shape_infer.pyを実行してください。詳細についてはこちらを参照してください。
  • 量子化パラメータをフラットバッファファイルに保存します。
  • モデルと量子化パラメータファイルを読み込み、TensorRT EPで実行します。

Yolo V3resnet50の2つのエンドツーエンド例を提供しています。

Int4/UInt4への量子化

ONNX Runtimeはモデル内の特定のオペレータを4ビット整数型に量子化することができます。オペレータにはブロック単位の重み専用量子化が適用されます。サポートされているオペレータタイプは以下の通りです:

  • MatMul:

    • 入力Bが定数の場合のみ、ノードは量子化されます
    • QOperatorまたはQDQフォーマットをサポートしています
    • QOperatorが選択された場合、ノードはMatMulNBitsノードに変換されます。重みBはブロック単位で量子化され、新しいノードに保存されます。HQQGPTQ、および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の指数かつ >= 16
  is_symmetric=True, # trueの場合はInt4に、falseの場合はuint4に量子化
  accuracy_level=4, # MatMulNbitsで使用、https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#attributes-35 を参照
  quant_format=quant_utils.QuantFormat.QOperator, 
  op_types_to_quantize=("MatMul","Gather"), # 量子化するオペレータタイプを指定
  quant_axes=(("MatMul", 0), ("Gather", 1),) # オペレータタイプごとに量子化する軸を指定

model = quant_utils.load_model_with_shape_infer(Path(model_fp32_path))
quant = matmul_4bits_quantizer.MatMul4BitsQuantizer(
  model, 
  nodes_to_exclude=None, # 量子化から除外するノードのリストを指定
  nodes_to_include=None, # 量子化に強制的に含めるノードのリストを指定
  algo_config=quant_config,)
quant.process()
quant.model.save_model_to_file(
  model_int4_path,
  True) # データを外部ファイルに保存

AWQとGTPQ量子化の使用方法については、Gen-AI model builderを参照してください。

よくある質問(FAQ)

なぜ性能向上が見られないのですか?

性能向上はモデルとハードウェアに依存します。量子化による性能向上には、計算とメモリの2つの側面があります。古いハードウェアには、int8での効率的な推論に必要な命令がないか、あっても限られています。また、量子化にはオーバーヘッド(量子化と逆量子化による)があるため、古いデバイスでは性能が低下することも珍しくありません。

VNNIを搭載したx86-64、Tensor Core int8サポートを備えたGPU、およびドット積命令を搭載したArm®ベースのプロセッサは、一般的に高い性能を得ることができます。

どの量子化手法を選択すべきですか?動的と静的のどちらがよいですか?

手法の選択セクションを参照してください。

reduce-rangeとチャネルごとの量子化はいつ使用すべきですか?

reduce-rangeは重みを7ビットに量子化します。これはAVX2およびAVX512(non-VNNI)マシンのU8S8フォーマットで飽和の問題を緩和するために設計されています。VNNIをサポートするマシンでは必要ありません。

チャネルごとの量子化は、重みの範囲が大きいモデルの精度を向上させることができます。精度の損失が大きい場合に試してください。AVX2およびAVX512マシンでは、通常、チャネルごとの量子化を有効にする場合、reduce-rangeも有効にする必要があります。

MaxPoolなどのオペレータが量子化されないのはなぜですか?

MaxPoolなどの特定のオペレータの8ビット型サポートは、ONNX opset 12で追加されました。モデルのバージョンを確認し、opset 12以上にアップグレードしてください。