自然言語処理編 - LSTM モデル推論の効率化 その弐
前の記事では、推論の効率化を行った。 この記事では、Float16 変換と量子化による推論の効率化を体験する。
ONNX モデルの Float16 変換
モデルを float32 から float16 にする。 モデルのサイズが半分程度になる。 単純な変換のみなので、非常にお手軽な手段となる。
必要なパッケージをインストールする
pip install onnx onnxconverter-common
コンバートする。
convert2onnx_float16.py
import os
import onnx
from onnxconverter_common import float16
# データディレクトリ
DATA_DIR = '.'
# モデルファイル名
MODEL_FILE = os.path.join(DATA_DIR, 'lstm-layer2-bidirect.onnx')
# FP16 ONNX モデルファイル名
ONNX_FP16_FILE = os.path.join(DATA_DIR, 'lstm-layer2-bidirect-float16.onnx')
# ONNX モデルを読み込む
onnx_model = onnx.load(MODEL_FILE)
# FP16 モデルに変換する
onnx_model = float16.convert_float_to_float16(onnx_model)
# 変換したモデルをファイルに保存する
onnx.save(onnx_model, ONNX_FP16_FILE)
print(f'ONNX fp16 model size: {os.path.getsize(ONNX_FP16_FILE) / 1024 / 1024:.2f} MB')
onnx_model = onnx.load(ONNX_FP16_FILE)
onnx.checker.check_model(onnx_model)
print(f'ONNX fp16 model is valid.')
変換ログ
ONNX fp16 model size: 8.79 MB
推論をしてみる。
推論は、infer_onnx.py
のモデルファイル名(MODEL_FILE)を
lstm-layer2-bidirect-float16.onnx
に変更するだけで実行できる。
以下のような実行結果となる。
CPU で実行した結果。 正答率は変化無しだが、推論速度が、42% 程低下しているようだ。 そういう仕様なのか、変換パラメータの問題なのか、環境の問題なのか原因は未調査。
前回(ONNX 変換のみ): 全体経過時間: 7.5133 s (58665 times/s)
Using providers: ['CPUExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428195, Total: 440767, Accuracy: 0.9715
全体経過時間: 12.8086 s (34412 times/s)
ループ回数: 27
初回時間: 0.7488 s
次回以降平均時間: 0.4638 s
T4 GPU で実行した結果。 こちらは、推論速度が、75% 程向上している。
前回(ONNX 変換のみ): 全体経過時間: 3.2806 s (134355 times/s)
Using providers: ['CUDAExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428202, Total: 440767, Accuracy: 0.9715
全体経過時間: 1.8700 s (235707 times/s)
ループ回数: 27
初回時間: 0.1328 s
次回以降平均時間: 0.0668 s
ONNX モデルの量子化
ONNX モデルの量子化 とは、32 ビット浮動小数点モデルを 8 ビット整数モデルに変換すること。 量子化には、動的量子化と静的量子化がある。 公式ドキュメントには、以下のようにある。
一般的に、RNN およびトランスフォーマーベースのモデルには動的量子化を使用し、 CNN モデルには静的量子化を使用することをお勧めします。
今回のモデルは、RNN 系なので、動的量子化が適していると思われるが、両方試してみる。
ONNX量子化表現形式には、以下のような種類があるようだ。
- 演算子指向 (QOperator)
- テンソル指向 (QDQ; 量子化と逆量子化)
データタイプには、activations と weights の組み合わせで、 それぞれ符号付き (int8) または、符号なし (uint8) のいずれかを選択でき、 以下のような組み合わせがあるようだ。
- U8U8(activations: uint8, weights: uint8)
- U8S8(activations: uint8, weights: int8)
- S8S8(activations: int8, weights: int8)
CPU 上の ONNX ランタイム量子化では、U8U8、U8S8、S8S8 を選択可能。 QDQ の場合は、S8S8 がデフォルトとなり、パフォーマンスと精度のバランスが取れている。 QOperator の場合、S8S8 は x86-64 CPU で遅くなり、 GPU 上で ONNX ランタイムの量子化処理を行う場合は、S8S8 のみサポートされているようだ。
動的量子化
convert2onnx_dynamic.py
import os
import onnx
from onnxruntime.quantization import quantize_dynamic
from onnxruntime.quantization.preprocess import quant_pre_process
# データディレクトリ
DATA_DIR = '.'
# モデルファイル名
MODEL_FP32 = os.path.join(DATA_DIR, 'lstm-layer2-bidirect.onnx')
# 前処理済みモデルファイル名
MODEL_PRE_PROCESS = os.path.join(DATA_DIR, 'lstm-layer2-bidirect-pre-process.onnx')
# 量子化済みモデルファイル名
MODEL_QUANT = os.path.join(DATA_DIR, 'lstm-layer2-bidirect-quant-dynamic.onnx')
# 量子化するための前処理済みのモデルを生成する
quant_pre_process(MODEL_FP32, MODEL_PRE_PROCESS)
print(f'ONNX pre-processed model size: {os.path.getsize(MODEL_PRE_PROCESS) / 1024 / 1024:.2f} MB')
# 動的量子化済みモデルファイルを生成する
quantize_dynamic(MODEL_PRE_PROCESS, MODEL_QUANT)
print(f'ONNX quantized model size: {os.path.getsize(MODEL_QUANT) / 1024 / 1024:.2f} MB')
model = onnx.load(MODEL_QUANT)
onnx.checker.check_model(model)
print(f'ONNX model is valid.')
変換ログ
ONNX pre-processed model size: 17.56 MB
ONNX quantized model size: 4.41 MB
推論をしてみる。
推論は、infer_onnx.py
のモデルファイル名(MODEL_FILE)を
lstm-layer2-bidirect-quant-dynamic.onnx
に変更するだけで実行できる。
以下のような実行結果となる。
CPU で実行した場合。 正答率は変化無しで、推論速度が、多少改善しているようだ。
Using providers: ['CPUExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428282, Total: 440767, Accuracy: 0.9717
全体経過時間: 7.6324 s (57749 times/s)
ループ回数: 27
初回時間: 0.4474 s
次回以降平均時間: 0.2763 s
T4 GPU で実行した場合。 なぜか、とんでもなく遅い。 Colab 環境の問題なのか、量子化方法の問題なのか未調査。
Using providers: ['CUDAExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428327, Total: 440767, Accuracy: 0.9718
全体経過時間: 91.9086 s (4796 times/s)
ループ回数: 27
初回時間: 4.1985 s
次回以降平均時間: 3.3735 s
静的量子化
import os
import numpy as np
import onnx
import onnxruntime
from onnxruntime.quantization import (
CalibrationDataReader,
quantize_static,
QuantType,
QuantFormat
)
from onnxruntime.quantization.preprocess import quant_pre_process
# データディレクトリ
DATA_DIR = '.'
# バッチサイズ
BATCH_SIZE = 64
# テスト用訓練データ出力ファイル名
X_TEST_FILE = os.path.join(DATA_DIR, 'lstm-optim-x-test.npy')
# モデルファイル名
MODEL_FP32 = os.path.join(DATA_DIR, 'lstm-layer2-bidirect.onnx')
# 前処理済みモデルファイル名
MODEL_PRE_PROCESS = os.path.join(DATA_DIR, 'lstm-layer2-bidirect-pre-process.onnx')
# 量子化済みモデルファイル名
MODEL_QUANT = os.path.join(DATA_DIR, 'lstm-layer2-bidirect-quant-static.onnx')
class MyCalibrationDataReader(CalibrationDataReader):
"""キャリブレーション用のデータを読み込む
Attributes:
enum_data (iter): データのインデックスを返すイテレータ
data (any): データ配列
input_name (str): 入力データ名
"""
def __init__(self):
"""コンストラクタ
"""
self.enum_data = None
# データを詠み込む
self.data = np.load(X_TEST_FILE)
# モデルを読み込む
_session = onnxruntime.InferenceSession(MODEL_FP32, None)
# 入力パラメータ名を取得する
self.input_name = _session.get_inputs()[0].name
def get_next(self) -> dict or None:
"""次のバッチデータを取得する
Returns (dict or None): バッチデータを返す。データが無い場合は None を返す。
"""
if self.enum_data is None:
# バッチデータのインデックスを返すイテレータを生成する
self.enum_data = iter(range(0, len(self.data), BATCH_SIZE))
try:
# バッチデータのインデックスを取得する
index = next(self.enum_data)
# データからバッチデータを切り出す
return {self.input_name: self.data[index:index + BATCH_SIZE]}
except StopIteration:
return None
def rewind(self) -> None:
"""データを先頭に巻き戻す"""
self.enum_data = None
def main() -> None:
"""静的量子化モデルに変換する"""
# 量子化するための前処理済みのモデルを生成する
quant_pre_process(MODEL_FP32, MODEL_PRE_PROCESS)
print(f'ONNX pre-processed model size: {os.path.getsize(MODEL_PRE_PROCESS) / 1024 / 1024:.2f} MB')
# キャリブレーション用データを準備する
calibration_data_reader = MyCalibrationDataReader()
# 静的量子化済みモデルファイルを生成する
quantize_static(
MODEL_PRE_PROCESS,
MODEL_QUANT,
calibration_data_reader,
quant_format=QuantFormat.QDQ,
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8
)
print(f'ONNX quantized model size: {os.path.getsize(MODEL_QUANT) / 1024 / 1024:.2f} MB')
model = onnx.load(MODEL_QUANT)
onnx.checker.check_model(model)
print(f'ONNX model is valid.')
if __name__ == '__main__':
main()
変換ログ
ONNX pre-processed model size: 17.56 MB
ONNX quantized model size: 5.55 MB
ONNX model is valid.
推論をしてみる。
推論は、infer_onnx.py
のモデルファイル名(MODEL_FILE)を
lstm-layer2-bidirect-quant-static.onnx
に変更するだけで実行できる。
以下のような実行結果となる。
CPU で実行した場合。 正答率は変化無しで、推論速度が、多少改善しているようだ。
Using providers: ['CPUExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428507, Total: 440767, Accuracy: 0.9722
全体経過時間: 9.3587 s (47097 times/s)
ループ回数: 27
初回時間: 0.5572 s
次回以降平均時間: 0.3385 s
T4 GPU で実行した場合。 静的量子化モデルの場合は、量子化前と正答率も、処理時間もさほど変わらない。
Using providers: ['CUDAExecutionProvider']
Loading datas...
Start inference...
Profile file:
OK: 428507, Total: 440767, Accuracy: 0.9722
全体経過時間: 3.4794 s (126680 times/s)
ループ回数: 27
初回時間: 0.2613 s
次回以降平均時間: 0.1238 s
まとめ
効率化の結果を表にまとめる。 (カッコ内の数字は変換のみとの比率) モデルの変換処理で、なにも調整をしていないため、 これらの比較結果には、あまり意味が無い。 前の記事の参考資料にも記載がある通り、 条件によっては、推論時間が 10 倍になる場合もある。 改善策については、ここでは踏み入らないこととする。
ONNX モデル | サイズ(MB) | CPU 正答率 | GPU 正答率 | CPU 推論時間(秒) | GPU 推論時間(秒) |
---|---|---|---|---|---|
変換のみ | 17.56 | 0.9717 | 0.9715 | 7.5133 | 3.2806 |
float16 | 8.79 (50.1%) | 0.9715 | 0.9715 | 12.8086 (170.5%) | 1.8700 (57.0%) |
動的量子化 | 4.41 (25.1%) | 0.9717 | 0.9718 | 7.6324 (101.6%) | 91.9086 (2801.5%) |
静的量子化 | 5.55 (31.6%) | 0.9722 | 0.9722 | 9.3587 (124.6%) | 3.4794 (106.1%) |