目次
2.2 トレーニング後の動的量子化 トレーニング後の動的量子化
2.3 トレーニング後の静的量子化 トレーニング後の静的量子化
3.2 Pytorch と Tensorflow が混合精度トレーニングを適用する方法
1. モデルの量子化とは何ですか
PyTorch モデルの量子化公式ドキュメント:量子化 — PyTorch 2.0 ドキュメント
現在の深層学習フレームワークのほとんどは、重みパラメーターの保存に fp32 を使用します。たとえば、Python float のタイプは倍精度浮動小数点数 fp64 で、PyTorch Tensor のデフォルトのタイプは単精度浮動小数点数 fp32 です。
fp32 の使用には主に次の問題があります。
- モデルのサイズが大きく、トレーニング中のグラフィックス カードのメモリ要件が高くなります。
- モデルのトレーニングが遅い
- モデル推論が遅い
PyTorch は int8 量子化をサポートしているため、モデル サイズが 4 分の 1、メモリ帯域幅要件が 4 分の 1 に削減されます。int8 計算のハードウェア サポートは、通常、FP32 計算より 2 ~ 4 倍高速です。量子化は主に推論を高速化するための手法であり、量子化演算子の順方向パスのみをサポートします。簡単に言うと、深層学習における量子化とは、もともと浮動小数点数で保存されていたテンソルを保存するために使用するビット数を減らし、もともと浮動小数点数で行われていた計算を完了するために使用するビット数を減らすことを指します。
データの種類 | 範囲 |
---|---|
float16 | -65504~65504 |
float32 | -2^31 ~ 2^31-1 |
あなた8 | -2^7 ~ 2^7-1 (-128 ~ 127) |
uint8 | 0 ~ 2^8-1 (0 ~ 255) |
モデル量子化の利点:
- ストレージのオーバーヘッドと帯域幅の要件が軽減される
- 計算速度の高速化(メモリアクセスの減少と int8 計算の高速化により、計算速度が 2 ~ 4 倍速くなります)
- エネルギー消費と設置面積の削減
- 許容できる精度の損失(つまり、量子化はモデルの重みにノイズを導入することと同じです。幸いなことに、CNN 自体はノイズの影響を受けません。モデルのトレーニング プロセス中に、アナログ量子化によって導入された重みにノイズが追加されることも、オーバーフィッティングを防止します。ビット数未満の量子化モデルでは、精度が大幅に低下することはありません)
量子化されたモデルの場合、そのテンソル演算の一部またはすべては、量子化前に float 型ではなく int 型を使用して計算されます。もちろん、量子化には基盤となるハードウェアのサポートも必要です。x86 CPU (AVX2 をサポート)、ARM CPU、Google TPU、Nvidia Volta/Turing/Ampere、Qualcomm DSP などの主流のハードウェアはすべて、量子化をサポートしています。
注: 深層学習のモデル圧縮の主流の方法には、量子化ベースの方法、モデルの枝刈りおよび知識の蒸留、およびモデル圧縮の最も広く使用されている形式であるモデルの量子化が含まれます。
2. Pytorch モデルの量子化
PyTorch は現在、次の 3 つの方法で量子化をサポートしています。
- トレーニング後の動的量子化、モデルトレーニング後の動的量子化
- トレーニング後の静的量子化、モデルのトレーニング後の静的量子化
- QAT (量子化認識トレーニング)、モデルトレーニングにおけるオープン量子化
2.1 テンソルの量子化
>>> x = torch.rand(2,3, dtype=torch.float32)
>>> x
tensor([[0.6839, 0.4741, 0.7451],
[0.9301, 0.1742, 0.6835]])
>>> xq = torch.quantize_per_tensor(x, scale = 0.5, zero_point = 8, dtype=torch.quint8)
tensor([[0.5000, 0.5000, 0.5000],
[1.0000, 0.0000, 0.5000]], size=(2, 3), dtype=torch.quint8,
quantization_scheme=torch.per_tensor_affine, scale=0.5, zero_point=8)
>>> xq.int_repr()
tensor([[ 9, 9, 9],
[10, 8, 9]], dtype=torch.uint8)
>>> xdq = xq.dequantize()
>>> xdq
tensor([[0.5000, 0.5000, 0.5000],
[1.0000, 0.0000, 0.5000]])
- quantize_per_tensor 関数: 指定されたスケールと zp を使用して、浮動小数点数テンソルを量子化テンソルに変換します。
- dequantize 関数: quantize_per_tensor の対義語、量子化テンソルを浮動小数点数テンソルに変換します。
xdq と x の値がずれているという事実は、次の 2 つの真実を伝えます。
- 量子化の精度が失われます。
- 適切なスケールと zp を選択すると、精度の損失を効果的に減らすことができます (たとえば、scale = 0.0036、zero_point = 0)。
PyTorch では、適切なスケールと ZP を選択する作業は、さまざまなオブザーバーによって実行されます。テンソル量子化は、テンソルごととチャネルごとの2 つのモードをサポートします。テンソルごととは、テンソル内のすべての値が同じ方法でスケーリングおよびオフセットされることを意味し、チャネルごととは、テンソルの特定の次元 (通常はチャネルの次元) の値が特定の方法でスケーリングおよびオフセットされることを意味します。つまり、テンソルにはスケールとオフセット (ベクトルを形成するため) のさまざまな方法が多数あるため、量子化時に発生するエラーはテンソルごとよりも少なくなります。PyTorch は現在、conv2d()、conv3d()、linear() のチャネルごとの量子化をサポートしています。
2.2 トレーニング後の動的量子化トレーニング後の動的量子化
これは量子化の最も単純な形式で、重みが事前に量子化され、アクティベーションが推論中に動的に量子化されます。このアプローチは、行列の乗算を計算するのではなく、メモリから重みをロードすることによってモデルの実行時間が支配される場合に使用されます。動的量子化をモデル全体に適用するには、torch.quantization.quantize_dynamic() 関数を 1 回呼び出すだけで済みます。
torch.quantization.quantize_dynamic(model, qconfig_spec=None, dtype=torch.qint8, mapping=None, inplace=False)
quantize_dynamic API は、float モデルを動的量子化モデル、つまり重みのみが量子化されるモデルに変換します。dtype パラメーターは、float16 または qint8 の値を取ることができます。モデル全体を変換する場合、デフォルトでは次の操作のみが変換されます。
- 線形
- LSTM
- LSTMCell
- RNNCell
- GRUCell
なぜ?動的量子化では重みパラメータのみが定量化されるため、これらの層には一般に多数のパラメータがあり、モデル全体におけるパラメータの割合が非常に高いため、限界利益が高くなります。他のレイヤーで動的量子化を実行することには、実際的な意味はほとんどありません。
デフォルトの動作による quantize_dynamic 呼び出しの例は次のとおりです。
#原始网络
Net(
(conv): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1), bias=False)
(fc): Linear(in_features=3, out_features=2, bias=False)
(relu): ReLU()
)
#quantize_dynamic 后
Net(
(conv): Conv2d(1, 1, kernel_size=(1, 1), stride=(1, 1), bias=False)
(fc): DynamicQuantizedLinear(in_features=3, out_features=2, dtype=torch.qint8, qscheme=torch.per_tensor_affine)
(relu): ReLU()
)
Linear を除いて、他の op は変更されていないことがわかります。Linear は DynamicQuantizedLinear に変換され、DynamicQuantizedLinear は torch.nn.quantized.dynamic.modules.linear.Linear クラスです。そうです、quantize_dynamic API の本質は、モデル内の演算のタイプを取得することです。演算のタイプがディクショナリ DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS のキーに属している場合、この演算はキーに対応する値に置き換えられます。
# Default map for swapping dynamic modules
DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS = {
nn.GRUCell: nnqd.GRUCell,
nn.Linear: nnqd.Linear,
nn.LSTM: nnqd.LSTM,
nn.LSTMCell: nnqd.LSTMCell,
nn.RNNCell: nnqd.RNNCell,
}
概要: トレーニング後の動的量子化は、動的量子化または重みのみの量子化と呼ばれ、モデル内の一部の演算パラメーターを事前に INT8 に量子化し、その後、操作中に入力を動的に INT8 に量子化し、結果を float32 に再量子化します。現在のオペレーションが出力されるときのタイプ。動的量子化は、デフォルトでは Linear および RNN バリアントにのみ適用されます。
2.3 トレーニング後の静的量子化トレーニング後の静的量子化
これは最も一般的に使用される量子化形式で、重みが事前に量子化され、キャリブレーション中のモデルの動作の観察に基づいてアクティベーション テンソル スケール ファクターとバイアスが事前計算されます。CNN は典型的な使用例であり、メモリ帯域幅と計算量の節約の両方が重要な場合に、トレーニング後の量子化がよく行われます。
動的量子化との類似点と相違点:
- ネットワークの重みパラメータが float32 から int8 に変換される点も同じです。
- 違いは、トレーニング セットまたはトレーニング セットの分布に類似したデータをモデルに供給する必要があり(バックプロパゲーションがないことに注意してください)、その後、アクティベーションの量子化パラメーター (スケールと zp) が次に従って計算されることです。各オペ入力の分布特性 - これは Calibrate (校正) と呼ばれます。
静的量子化には、op forward 後の後処理であるアクティベーション、つまりポストプロセスが含まれます。なぜ静的定量化にはアクティベーションが必要なのでしょうか? 静的量子化の前向き推論プロセスは (開始 +1) から (終了 -1) までの INT 計算であるため、アクティベーションでは、1 つの演算の入力が次の演算の入力に適合していることを確認する必要があります。
トレーニング後の量子化を実行する一般的な手順は次のとおりです。
- ステップ 1 - モデルの準備: QuantStub および DeQuantStub モジュールを追加して、アクティベーションを明示的に量子化および量子化解除する場所を指定します。モジュールが再利用されないようにし、再量子化が必要な操作をモジュールのスキーマに変換します。
- ステップ 2 - conv+relu または conv+batchnorm+relu などの組み合わせ操作を融合して、モデルの精度とパフォーマンスを向上させます。
- ステップ 3 - 対称量子化または非対称量子化、および MinMax または L2Norm キャリブレーション手法の選択など、量子化方法の構成を指定します。
- ステップ 4 - torch.quantization.prepare() モジュールをプラグインして、キャリブレーション中に活性化テンソルを観察します。
- ステップ 5 - キャリブレーション データセットを使用してモデルに対してキャリブレーション操作を実行します。
- ステップ 6 - torch.quantization.convert() モジュールを使用してモデルを変換します。これには、各活性化テンソルに使用されるスケールとバイアス値の計算と保存、キー演算子の量子化実装の置き換えなどが含まれます。
具体的な参考資料: PyTorch の定量化 - について知る
動的量子化と静的量子化の最大の違いは次のとおりです。
- 静的に量子化された浮動小数点入力は、QuantStub によって int に変更される必要があり、出力されるまで int になります。
- 動的に量子化された float 入力は動的に計算されたスケールと zp を int に量子化し、op 出力は再び float に変換されます。
2.4トレーニング中の量子化を意識したトレーニング
まれに、トレーニング後の量子化では十分な精度が得られない場合がありますが、torch.quantization.FakeQuantize() モジュールをプラグインしてトレーニング時の量子化を実行できます。計算はFP32で行われますが、INT8の量子化効果をシミュレートするために値は四捨五入されています。具体的な定量手順は次のとおりです。
- ステップ 1 - モデルの準備: QuantStub および DeQuantStub モジュールを追加して、アクティベーションを明示的に量子化および量子化解除する場所を指定します。モジュールが再利用されないようにし、再量子化が必要な操作をモジュールのスキーマに変換します。
- ステップ 2 - conv+relu または conv+batchnorm+relu などの組み合わせ操作を融合して、モデルの精度とパフォーマンスを向上させます。
- ステップ 3 - 対称量子化または非対称量子化、および MinMax または L2Norm キャリブレーション手法の選択など、擬似量子化方法の構成を指定します。
- ステップ 4 - torch.quantization.prepare_qat() モジュールを挿入します。これはトレーニング中のアナログ量子化に使用されます。
- ステップ 5 - モデルをトレーニングまたは微調整します。
- ステップ 6 - torch.quantization.convert() モジュールを使用してモデルを変換します。これには、各活性化テンソルに使用されるスケールとバイアス値の計算と保存、キー演算子の量子化実装の置き換えなどが含まれます。
具体的な参考資料: PyTorch の定量化 - について知る
QAT の概要:
# 原始的模型,所有的tensor和计算都是浮点
previous_layer_fp32 -- linear_fp32 -- activation_fp32 -- next_layer_fp32
/
linear_weight_fp32
# 训练过程中,fake_quants发挥作用
previous_layer_fp32 -- fq -- linear_fp32 -- activation_fp32 -- fq -- next_layer_fp32
/
linear_weight_fp32 -- fq
# 量化后的模型进行推理,权重和输入都是int8
previous_layer_int8 -- linear_with_activation_int8 -- next_layer_int8
/
linear_weight_int8
3. 混合精度トレーニング 自動的に混合精度
低精度の計算を使用してモデルを最適化します。推論プロセス中、モデル最適化のより成熟したスキームは fp16 量子化と int8 量子化です。トレーニングスキームは混合精度トレーニングであり、その基本的な考え方は非常にシンプルです。精度は半分になります。 (fp32→fp16)、トレーニング時間は半分になります。単精度浮動小数点数 float32 (32 ビット、4 バイト) に比べ、半精度浮動小数点数 float16 は 2 バイトの 16 ビットしかありません。
トレーニング中に FP32 を FP16 に置き換えると、次の 2 つの利点があります。
- ビデオ メモリ使用量の削減: FP16 のビデオ メモリ使用量は FP32 の半分であり、より大きなバッチ サイズを使用できます。
- トレーニングの高速化: FP16 を使用すると、モデルのトレーニング速度をほぼ 2 倍にすることができます
トレーニング プロセス中に、半精度計算を直接使用すると、 2 つの問題が発生します。
- 丸め誤差: 十分に小さい浮動小数点数に対して実行される演算は、値を 0 に丸めます。バックプロパゲーションでは、勾配更新値の多くまたはほとんどが非常に小さくなります。バックプロパゲーションにおける丸め誤差は、累積によってこれらの数値が 0 または 0 になる可能性があります。 nan、不正確な勾配更新が発生し、ネットワークの収束に影響を及ぼします。
- オーバーフロー エラー (Grad オーバーフロー / アンダーフロー) : 精度の低下 (小数点以下 8 桁よりも小数点以下 16 桁の方がはるかに正確です) により、取得された値が fp16 の有効ダイナミック レンジより大きくなったり、小さくなったりします。つまり、オーバーフローまたはアンダーフローです
混合精度 (Automatically Mixed Precision、AMP) を使用すると、わずか数行のコードでメモリ使用量を半分にし、トレーニング速度を 2 倍にすることができます。AMP テクノロジーは、2017 年に Baidu と NIVDIA のチームによって提案され (Mixed Precision Training)、その結果は ICLR で公開されました。PyTorch 1.6 が登場する前は、誰もが NVIDIA の apex ライブラリを使用して AMP トレーニングを実装していました。バージョン 1.6 以降では、PyTorch にはすぐに使える AMP が付属しています。
混合精度トレーニングは、半精度浮動小数点数を使用して、精度の損失を可能な限り低減しながらトレーニングを高速化するもので、FP16、つまり半精度浮動小数点数を使用して重みと勾配を保存することで、トレーニングを高速化します。メモリ使用量を削減します。元の FP32 ニューラル ネットワーク計算の代わりに FP16 を使用する場合の最大の問題は、精度の低下です。
3.1 解決策
1) 重量ごとに FP32 のコピーを保管します。
FP16 トレーニングを実現するには、モデルの重みと入力データを FP16 に変換する必要があり、FP16 の勾配はバックプロパゲーション中に取得されます。このとき直接更新すると、勾配 * 学習率の値が小さいことが多いため、モデルの重みとの乖離が大きくなり、丸め誤差が発生する可能性があります。
したがって、解決策は次のとおりです。モデルの重み、活性化値、勾配およびその他のデータを FP16 に保存し、 更新用にFP32 モデルの重みのコピーを維持します。FP16 の勾配がバックプロパゲーションによって取得された後、FP32 に変換されてスケーリングされず、最後に FP32 のモデル重みが更新されます。更新プロセス全体が FP32 環境で実行されるため、丸め誤差は発生しません。
FP32 重みバックアップは、バックプロパゲーションの丸め誤差の問題を解決します。
2)ロススケーリング
アンダーフローの問題を解決するには、計算された損失値をスケーリングしますが、連鎖則の存在により、損失のスケーリングが各勾配に作用します。次に、スケーリングされた勾配が FP16 の有効範囲に変換されます。このように、FP16 を使用すると、オーバーフローすることなく勾配を保存できます。さらに、更新する前に、スケーリングされたグラデーションを FP32 に変換し、その後、グラデーションをスケーリングされていない (スケールされていない) 状態に戻す必要があります。
スケーリング係数 (loss_scale) は通常、inf または nan が発生しない限り、フレームワークによって自動的に決定され、loss_scale が大きいほど優れています。トレーニングが進むにつれてネットワークの勾配がどんどん小さくなり、loss_scale が大きいほど FP16 の表現範囲を最大限に活用できるためです。
3) 改良された演算方法: FP16 * FP16 + FP32
FP16 環境で動作が不安定なモジュールについては、FP32 精度で動作するように強制します。たとえば、バッチ平均を計算する必要がある BN レイヤーは FP32 で実行する必要があります。そうしないと、丸めエラーが発生します。BN 層を例として取り上げ、その重みを FP32 に変換し、入力を FP16 から FP32 に変換すると、モジュール全体が FP32 で動作することが保証されます。
3.2 Pytorch と Tensorflow が混合精度トレーニングを適用する方法
Pytorch は、NVIDIA のオープンソース フレームワークAPEXを使用して、混合進行状況と分散トレーニングをサポートできます。
model, optimizer = amp.initialize(model, optimizer, opt_level="O1")
with amp.scale_loss(loss, optimizer) as scaled_loss:
scaled_loss.backward()
Tensorflow はさらにシンプルで、すでに公式サポートがあり、トレーニング前に文を追加するだけです。
export TF_ENABLE_AUTO_MIXED_PRECISION=1
# 或者
import os
os.environ['TF_ENABLE_AUTO_MIXED_PRECISION'] = '1'
参照: