daisukeの技術ブログ

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

gemmlowpライブラリのソースコードを読んで量子化を学ぶ(サンプルソースの実行)

今回もAIの量子化について学んでいきます。GoogleのQAT量子化の論文 に出てくる gemmlowp ライブラリ について見ていきます。

それでは、やっていきます!

はじめに

「AIモデルの量子化」の記事一覧です。良かったら参考にしてください。

AIモデルの量子化の記事一覧

それではやっていきます!

gemmlowpのGitHub

gemmlowp は、(TensorFlow Lite のベースになった?今も使われてる?)TensorFlow Lite のリファレンスコードから呼び出されてる Google の GitHub の行列演算ライブラリです。

gemmlowp のリポジトリは、以下です。

github.com

GitHub にあるドキュメントを、順番に見ていきます。

gemmlowpのREADME.mdを読む

gemmlowp に関する概略の内容が書かれています。

特徴をまとめておきます。

  • 必須は、C++11 で、一部機能には pthreads、sysconf が必須
  • gcc と Clang をサポート
  • 最適化は、NEON を備えた ARM 32bit と 64bit と、SSE 4.1 を備えた Intel x86 32bit と 64bit
  • gemmlowp は、ヘッダのみのライブラリ
  • 使用例は、doc/quantization_example.cc
  • eight_bit_int_gemm/ は非推奨

他のドキュメントは、doc/ にあるようです。

まずは、使用例(doc/quantization_example.cc)を動かしていきながら、必要に応じて、ドキュメントを参照します。

gemmlowpのdoc/quantization_example.ccを実行する

quantization_example.cc を見ていきます。

ビルドと実行

先頭に、このソースコードは、doc/quantization.md に示されている理論を実装したものとあります。

また、x86 でのビルド方法と実行方法が書かれています。

では、早速、ビルドして実行してみます。

いくつか、コンパイルで警告が出ましたが、ビルドが完了して、実行も出来ました。

$ git clone https://github.com/google/gemmlowp.git
$ cd gemmlowp/
$ g++ doc/quantization_example.cc -I . --std=c++11 -msse4.1 -lpthread -o quantization_example
$ ./quantization_example

実行の結果が出力されました。チュートリアルのような感じになっているので、少しずつ説明していきます。

実行ログの分析

リファレンスとなる浮動小数点数の行列の積

最初に、2行4列の行列と、4行3列の行列の積を浮動小数点数で計算しているようです。結果は、2行3列の行列になります。この後、量子化による計算を行い、この結果と比べて、どれぐらい差が出るかを見るのだと思います。

ちなみに、LHS は、Left Hand Side の略で左側で、RHS は、Right Hand Side の略で右側です。

First, let us make some float matrices LHS and RHS, and compute their product.

Here is the float LHS matrix:
0.629   0.812   -0.746  0.827
-0.729  0.67    0.938   -0.558

Here is the float RHS matrix:
0.265   -0.443  0.915
-0.384  -0.623  0.993
-0.805  0.0938  0.93
0.0944  0.986   0.935

Here is the float product (LHS * RHS) matrix obtained by ordinary float matrix multiplication, i.e. as far as we are concerned, the REFERENCE RESULT:
0.534   -0.0396 1.46
-1.26   -0.557  0.348
この後の量子化で行列の積を計算することの説明文

この後は説明文が続いてます。

Now we embark on reproducing this result using quantized arithmetic. The code below splits into two parts: quantization code that only needs to run offline (e.g. to generate a quantized neural network workload), and actual runtime quantized code, which is typically performance-critical and where we typically do not want to use any floating-point arithmetic. We want to clearly distinguish between the two.

The below is OFFLINE QUANTIZATION CODE. We still use some floating-point arithmetic in the process of generating the quantized workload to be run on-device.

Now, let us choose quantization parameters for these matrices. You might ask, what good is quantization if we need to pick quantization parameters for the result before we can run the quantized computation to obtain the result? The idea is that we target applications such as neural networks, where unknown results are only allowed to vary within preexisting bounds. In practice, the bounds for the results are typically learned during the neural network training process. The min and max of the result do not have to be exact. If they are too broad, we just get lower quantization accuracy. If they are too narrow, we just get clamping at the bounds.

ChatGPTで翻訳します。

それでは、量子化された算術を使ってこの結果を再現する作業に取り掛かりましょう。

以下のコードは二つの部分に分かれています。一つはオフライン量子化コードで、これはオフラインでのみ実行する必要があり(例えば量子化されたニューラルネットワークのワークロードを生成するため)、もう一つは実際のランタイム量子化コードです。

ランタイム量子化コードは通常、パフォーマンスが重要で、浮動小数点演算は使用しないようにします。これら二つを明確に区別する必要があります。

以下はオフライン量子化コードです。このプロセスでは、デバイス上で実行するための量子化ワークロードを生成する際に、一部の浮動小数点演算を使用します

では、これらの行列に対して量子化パラメータを選びましょう。結果を得るための量子化計算を実行する前に、結果の量子化パラメータを選ぶ必要があるなら、量子化には何の意味があるのかと疑問に思うかもしれません。ポイントは、ニューラルネットワークのようなアプリケーションを対象としていることで、未知の結果は既存の範囲内でのみ変動が許されるということです。実際には、結果の範囲は通常、ニューラルネットワークのトレーニングプロセス中に学習されます。結果の最小値と最大値は正確である必要はありません。範囲が広すぎると量子化精度が低下し、狭すぎると境界でのクランプが発生するだけです。

スケールとゼロポイントの算出

LHS の行列の最小値と最大値を求めて、スケールとゼロポイントを求めます。

論文では、整数を  q、実数を  r、スケールを  S、ゼロポイントを  Z として、 r = S(q − Z) という関係を示していました。

まず、スケールは、ソースコードを見たところ、以下の計算で求めています。

リアル空間の取り得る範囲を 255 に分割して、量子化空間の 1 あたりのリアル空間の範囲のイメージです。

const float qmin = 0;
const float qmax = 255;
const double scale = (max - min) / (qmax - qmin);

ゼロポイントは、ソースコードを見たところ、以下の計算で求めています。最終的にゼロポイントに採用するのは、nudged_zero_point です。

ゼロポイントは、リアル空間の 0 の量子化空間の値です。

論文でも述べられていたように、ゼロポイントを整数にすることで、量子化空間からリアル空間に戻すときに、正確に 0 を表現できるようになります。

まず、initial_zero_point は、単純に、(0 - min) / scale + qmin ですね。

リアル空間の量をスケールで割ると量子化空間の量になります。本来は、これを整数に四捨五入する必要があります。また、この式は、量子化空間が 0 から 255 の場合と、-128 から 127 の両方に対応しています。

次の条件式は、そもそもリアル空間の 0 が min より小さい場合は、ゼロポイントは、qmin に設定するしかありません。

同様に、リアル空間の 0 が max より大きい場合は、ゼロポイントは、qmax に設定するしかありません。

これらのどちらにも当てはまらない場合は、四捨五入します。

const double initial_zero_point = qmin - min / scale;
std::uint8_t nudged_zero_point = 0;
if (initial_zero_point < qmin) {
  nudged_zero_point = qmin;
} else if (initial_zero_point > qmax) {
  nudged_zero_point = qmax;
} else {
  nudged_zero_point = static_cast<std::uint8_t>(std::round(initial_zero_point));
}

これらの実装から、LHS、RHS、計算結果の最小値、最大値、スケール、ゼロポイントが求まっています。

計算結果のこれらの値が、なぜ必要なのか、というのが先ほどの説明文で書かれていました。

ニューラルネットワークの学習と推論で量子化を適用する場合を想定していて、学習時に最小値と最大値を取得しておき、学習が完了したら、最小値と最大値からスケールとゼロポイントを求めます。推論を高速化したいので、推論は、求めたこれらの値を使って、量子化して計算を高速化します。

ここでは、それらの簡単な流れをやってみましょう、ということです。

For LHS, we have min = -0.746, max = 0.938, scale = 0.0066, zero_point = 113
For RHS, we have min = -0.805, max = 0.993, scale = 0.00705, zero_point = 114
For the result, we have min = -1.26, max = 1.46, scale = 0.0107, zero_point = 118
スケールとゼロポイントを使ってLHSとRHSを量子化する

LHS と RHS を量子化した結果です。例えば、LHS の 0.629 の場合、 r = S(q − Z) を変形すると、 q = Z + r/S なので、q = 113 + 0.629 / 0.0066 = 208.303... = 208 となります。

Quantized uint8 LHS matrix:
208     236     0       238
3       214     255     29

Quantized uint8 RHS matrix:
152     51      244
60      26      255
0       127     246
127     254     247

End of OFFLINE QUANTIZATION CODE.

先ほどの説明文で言うところの、オフライン量子化コードと書かれたところは、ここまでになります。ここまでは、浮動小数点演算を使っていました。

行列の積を量子化で計算する

この先は、ランタイム量子化コードで、処理性能が重要になる部分であり、浮動小数点演算は使用しません。

The below is ON-DEVICE RUNTIME QUANTIZED CODE. This is the part that is performance-critical and may only use quantized arithmetic.

Quantized uint8 result matrix obtained by quantized multiplication:
168     115     255
0       66      151

End of ON-DEVICE RUNTIME QUANTIZED CODE.

ランタイム量子化コードで計算された結果です。この計算過程については、複雑な処理を含むので、次回に、デバッガを使って、追いかけたいと思います。

ここまでがランタイム量子化コードとなります。

次に表示されているのは、ランタイム量子化コードで計算された値を、リアル空間に戻した(逆量子化した)ものと、リファレンスで浮動小数点数で計算した結果との差分です。

Here is the actual float product (LHS * RHS) matrix obtained by dequantizing the above uint8 result, i.e. as far as we are concerned, the ACTUAL RESULT:
0.533   -0.032  1.46
-1.26   -0.554  0.352

Difference between ACTUAL and REFERENCE float results:
-0.000675       0.00764 -0.000674
-0.000674       0.0022  0.00369

例えば、168 の場合、 r = S(q − Z) なので、r = 0.0107 * (168 - 118) = 0.535 となります。少し値が異なりますね、ここについても、次回に、デバッガで確認したいと思います。

差分についても、表示されてる値では、0.533 - 0.534 = -0.001 です。実際は桁数を指定して表示しているので、もっと細かい精度で計算したら一致するのかもしれません。ここも次回に、デバッガで確認したいと思います。

おわりに

今回は、TensorFlow Lite C++ の量子化の推論を理解するために、gemmlowpライブラリのサンプルソースを実行して、結果を理解しました。

次回は、VSCodeのデバッガで、詳細に理解していきたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です。

最後までお読みいただき、ありがとうございました。