Android NDK 環境で TensorFlow Lite GPU Delegate を使う方法

#1. はじめに
　TensorFlow Lite を GPUで高速化する **GPU Delegate機能** を Android で使う方法について書きます。

　GPU Delegate に関する公式情報はいくつかあるのですが（例えば[ここ](https://www.tensorflow.org/lite/models/pose_estimation/overview) や [ここ] (https://www.tensorflow.org/lite/performance/gpu)）、それらは「手軽に試す」ことに焦点があてられており、
　　・TensorFlow Lite ライブラリや GPU Delegate ライブラリは、ビルド済みバイナリをダウンロードして使う
　　・アプリは Java や Kotlin で書かれている
というものが大半でした。

　これに対し本記事では、次の前提でアプリを作る方法について書きます。
　　■TensorFlow Lite ライブラリとGPU Delegate ライブラリは、ビルド済みバイナリを使うのではなく、自前ビルドしたものを使いたい（最新のソースコードを使いたい。自分でコード修正したい。）
　　■アプリは C++ で書きたい（処理速度を少しでも速くするため。（すみません。嘘です。本当は私が Java や Kotlin を使えないからです））

　また本記事では、一通りビルド手順を説明した後、GPU Delegate 有無による性能比較を [TensorFlow/Examples にある Style Transfer モデル](https://github.com/tensorflow/examples/tree/master/lite/examples/style_transfer/android) で計測した結果についても触れます。



<img width="640" alt="gl2style_transfer.png名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265135/196e4076-0264-a24e-ba1f-758888f0ab5b.png">
　上図は Style Transfer 実行例です。
　任意の入力画像を、「Matrix」や「ムンクの叫び」といった「特徴的な画風」に変換できるというものです。とっておきのお気に入り画像を、サイバー電脳空間や近代芸術印象派の世界感で鑑賞するのも味わい深くて一興です。しらんけど。

　なお、アプリのソースコードについては触れませんが、下記に置いていますので、ご興味のある方はご覧ください。（ご意見・コメント大歓迎です！）
https://github.com/terryky/android_tflite




#2. 作業環境

下記の環境で作業を行いました。

|  項目                |  バージョン  |
| -------------------- | ----          |
| ホストPC             | x86_64 Ubuntu 18.04 |
| Android端末          |  XPERIA XZ1 (Android Pi API Level 28)|
| AndroidNDK バージョン | r20b         |
| TensorFlow バージョン | r2.2 ブランチ (acf4951a2f5fdc181ed14c163381c0cf135d9ee6)|



　
#3. ビルド手順
　それではアプリをビルドする方法について説明していきます。
　作業は大きく次の２つのステップで行います。
　　１）コマンドラインから ```Bazel``` で TensorFlow Lite ライブラリ と GPUDelegate ライブラリをビルドする
　　２）Android Studio で、１）で生成したライブラリのリンク設定を行いアプリをビルド。実機へ転送して実行

　以下、ステップごとに説明します。

##3.1 TensorFlow Lite ライブラリ、GPUDelegate ライブラリのビルド

### 3.1.1 事前準備
#### 3.1.1.1 Android NDK, Android SDK のインストール
　NDKやSDKは、Android 開発されたことのある人ならインストール済だと思います。
　まだの方は、Android Studio の GUI でインストールしても良いですし、明示的に [ここ](https://developer.android.com/ndk/downloads)から直接ダウンロードしてインストールしてもかまいません。

#### 3.1.1.2 Bazel のインストール
　TensorFlow のバージョンに合った ```Bazel``` をインストールします。今回は TensorFlow の r2.2 ブランチで作業するため、Bazel 2.0.0 をインストールします。

```sh
$ wget https://github.com/bazelbuild/bazel/releases/download/2.0.0/bazel-2.0.0-installer-linux-x86_64.sh
$ chmod 755 bazel-2.0.0-installer-linux-x86_64.sh
$ sudo ./bazel-2.0.0-installer-linux-x86_64.sh
```

### 3.1.2 ソースコード取得、configure

　TensorFlowのソースコードを git で取得し、r2.2 ブランチに切り替えます。

```sh
$ cd work
$ git clone https://github.com/tensorflow/tensorflow.git
$ cd tensorflow 
$ git checkout r2.2
```

　次に ```configure``` を行い、「Android NDK がどこにインストールされているか」や「ターゲットの Android API レベルをいくつにするか」といった Android 特有のビルド環境の設定を行います。
　これらの設定情報は、```configure``` 実行時にインタラクティブに入力しても良いのですが、事前に環境変数で設定しておけば、configure フェーズで入力せずに済むようです。

　下記は私の環境での設定例です。環境に合わせてご指定ください。

```sh
$ export ANDROID_NDK_HOME=${HOME}/Android/android-ndk-r20b
$ export ANDROID_NDK_API_LEVEL="27"
$ export ANDROID_BUILD_TOOLS_VERSION="29.0.3"
$ export ANDROID_SDK_API_LEVEL="27"
$ export ANDROID_SDK_HOME=${HOME}/Android/Sdk
$ export ANDROID_API_LEVEL="27"

$ ./configure

(必要に応じてオプション選択。Enterキー連射(default 設定)でも大丈夫)
```

### 3.1.3 TensorFLow Lite ライブラリ, GPU Delegate ライブラリのビルド

　TensorFlow Lite 本体と GPUDelegate を実行するモジュールはそれぞれ独立したライブラリとなっており、ビルドも個々に行います。ターゲットの Android 実機にあわせて aarch64用のライブラリをクロスビルドすることになりますが、```bazel``` コマンドを ```--config=android_arm64``` オプションつきで叩くだけで難なくビルドできます。（[ラズパイ向けにビルドした時](https://qiita.com/terryky/items/fa18bd10cfead076b39f)と比べると何て楽ちんなんでしょう）
 

```sh
$ bazel build -s -c opt --cxxopt='--std=c++11' --config=android_arm64 //tensorflow/lite:libtensorflowlite.so
$ bazel build -s -c opt --cxxopt='--std=c++11' --config=android_arm64 //tensorflow/lite/delegates/gpu:libtensorflowlite_gpu_delegate.so
```

　ビルド完了すると、bazel-bin フォルダ以下に下記の共有ライブラリが出来上がります。
　GPUDelegate 用の ```libtensorflowlite_gpu_delegate.so``` は、約 60MB もの巨大なライブラリです。OpenGLES 用のGPUカーネルと OpenCL 用のGPUカーネルを両方抱え込んでいるためですが、「メモリの少ない組み込み用途向けにどちらか一方を選択できるようにしようぜ」、という議論が [issue で進行中](https://github.com/tensorflow/tensorflow/pull/36180#issuecomment-601701253) なので、改善されていくのではと思います。 

```sh
$ ls -l bazel-bin/tensorflow/lite/
-r-xr-xr-x  1 terryky terryky  2610368  3月 20 13:42 libtensorflowlite.so*
$ ls -l bazel-bin/tensorflow/lite/delegates/gpu/
-r-xr-xr-x 1 terryky terryky  59657040  3月 20 13:43 libtensorflowlite_gpu_delegate.so*
```

#### (2020/07/19 ライブラリのファイルサイズに関する追記ここから)
　```libtensorflowlite_gpu_delegate.so``` が60MBもの巨大なファイルサイズになっていることについて、@iwatake2222 さんから **「bazelでbuild時に、```--strip always``` オプションをつけると5.5MByte程度にまでファイルサイズが低減される」** という貴重なご指摘を頂きました。ありがとうございます。

　ご指摘をうけ改めて ```libtensorflowlite_gpu_delegate.so``` のセクション情報を確認すると、50MB級の巨大なセクションデータ（```.debug_xxx``` という名前のセクション）が含まれており、これがファイルサイズ肥大要因のようです。

<details>
<summary>その時のセクション情報を見る</summary>
<div>

```sh
$ objdump -h bazel-bin/tensorflow/lite/libtensorflowlite_gpu_delegate.so

libtensorflowlite_gpu_delegate_arm.so:     ファイル形式 elf64-little

セクション:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .hash         0000acfc  00000000000001c8  00000000000001c8  000001c8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .gnu.hash     0000c8b0  000000000000aec8  000000000000aec8  0000aec8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .dynsym       00028d70  0000000000017778  0000000000017778  00017778  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .dynstr       0007b37d  00000000000404e8  00000000000404e8  000404e8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .gnu.version  00003674  00000000000bb866  00000000000bb866  000bb866  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .gnu.version_r 00000060  00000000000beee0  00000000000beee0  000beee0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .rela.dyn     00023fa0  00000000000bef40  00000000000bef40  000bef40  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .rela.plt     0000e2e0  00000000000e2ee0  00000000000e2ee0  000e2ee0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .plt          00009760  00000000000f11c0  00000000000f11c0  000f11c0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  9 .text         0027bc58  00000000000fa920  00000000000fa920  000fa920  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 10 malloc_hook   00000280  0000000000376578  0000000000376578  00376578  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .rodata       0002c970  0000000000376800  0000000000376800  00376800  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 12 .eh_frame_hdr 0000c744  00000000003a3170  00000000003a3170  003a3170  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 13 .eh_frame     00039ec0  00000000003af8b8  00000000003af8b8  003af8b8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 14 .gcc_except_table 00022df8  00000000003e9778  00000000003e9778  003e9778  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 15 .note.android.ident 00000098  000000000040c570  000000000040c570  0040c570  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .init_array   00000030  000000000041ca00  000000000041ca00  0040ca00  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 17 .fini_array   00000010  000000000041ca30  000000000041ca30  0040ca30  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 18 .data.rel.ro  0000c458  000000000041ca40  000000000041ca40  0040ca40  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .dynamic      00000240  0000000000428e98  0000000000428e98  00418e98  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .got          00005f28  00000000004290d8  00000000004290d8  004190d8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .data         00000260  000000000042f000  000000000042f000  0041f000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .bss          00004a90  000000000042f280  000000000042f280  0041f260  2**6
                  ALLOC
 23 .comment      0000012f  0000000000000000  0000000000000000  0041f260  2**0
                  CONTENTS, READONLY
 24 .debug_aranges 000003e0  0000000000000000  0000000000000000  0041f38f  2**0
                  CONTENTS, READONLY, DEBUGGING
 25 .debug_info   022d2089  0000000000000000  0000000000000000  0041f76f  2**0
                  CONTENTS, READONLY, DEBUGGING
 26 .debug_abbrev 00089b0e  0000000000000000  0000000000000000  026f17f8  2**0
                  CONTENTS, READONLY, DEBUGGING
 27 .debug_line   002f25d8  0000000000000000  0000000000000000  0277b306  2**0
                  CONTENTS, READONLY, DEBUGGING
 28 .debug_str    00aefe38  0000000000000000  0000000000000000  02a6d8de  2**0
                  CONTENTS, READONLY, DEBUGGING
 29 .debug_loc    00ec7390  0000000000000000  0000000000000000  0355d716  2**0
                  CONTENTS, READONLY, DEBUGGING
 30 .debug_macinfo 00000142  0000000000000000  0000000000000000  04424aa6  2**0
                  CONTENTS, READONLY, DEBUGGING
 31 .debug_ranges 00452380  0000000000000000  0000000000000000  04424be8  2**0
                  CONTENTS, READONLY, DEBUGGING
```
</div>
</details>

　どうやら、```--config=android_arm64``` 指定でビルドした時は、```-g``` オプション付きでソースファイルがコンパイルされ、生成されたデバッグ情報がそのままライブラリにリンクされるようです。デバッグ情報が不要な場合は、```-g``` オプションがつかないようにするか、@iwatake2222さんのご指摘通り ```strip``` でこのデバッグ情報をそぎ落とすことでファイルサイズを小さくすることができます。


#### (2020/07/19 ライブラリのファイルサイズに関する追記ここまで)






　なお、bazel-bin フォルダ以下には、アプリのビルドに必要なヘッダファイル (flatbuffers, absl) もダウンロードされた状態になっているので、削除せずそのままにしておいてください。

　以降、このライブラリとヘッダファイルを使ってアプリをビルドしていきます。

## 3.2 アプリのビルド

　Android Studio を使ってビルドを行います。
　NDK の ```cmake``` によるビルドなので、```CMakeLists.txt``` に必要な項目を書いていきます。

　ポイントは、
　・```libtensorflowlite*.so``` は共有ライブラリなので、アプリ実行ファイルと一緒にパッキングしてAndroid実機へ転送するために、```jniLibs``` フォルダへコピーする必要があることと、
　・include path として、「TensorFlow を git clone した場所」と「bazel がダウンロードした(flatbuffers, absl の) ヘッダファイルの場所」を設定すること
　です。

　特に、後者の「bazel がヘッダファイルをダウンロードする場所」は、TensorFlow のバージョンによって若干変化するようなので注意してください。

```sh:CMakeLists.txt
# TensorFlow ソースコードを git clone したディレクトリ
get_filename_component(tfliteDir /home/hogehoge/work/tensorflow ABSOLUTE)

# bazel ビルドした生成物が格納されたディレクトリ(TensorFlowバージョンによって違うっぽい)
#get_filename_component(bazelgenDir ${tfliteDir}/bazel-genfiles ABSOLUTE) #for tf2.1
get_filename_component(bazelgenDir ${tfliteDir}/bazel-bin ABSOLUTE) #for tf2.2

# アプリがローカルでライブラリを保持するディレクトリ
set(jnilibDir ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs)

# bazel でビルドした TensorFlow Lite ライブラリをアプリローカルへコピー
file(COPY ${bazelgenDir}/tensorflow/lite/libtensorflowlite.so
	DESTINATION ${jnilibDir}/arm64-v8a)

file(COPY ${bazelgenDir}/tensorflow/lite/delegates/gpu/libtensorflowlite_gpu_delegate.so
    DESTINATION ${jnilibDir}/arm64-v8a)

# アプリとリンクするライブラリファイルを宣言
add_library(lib_tflite SHARED IMPORTED)
set_target_properties(lib_tflite PROPERTIES IMPORTED_LOCATION
	${jnilibDir}/arm64-v8a/libtensorflowlite.so)

add_library(lib_tflite_gpu_delegate SHARED IMPORTED)
set_target_properties(lib_tflite_gpu_delegate PROPERTIES IMPORTED_LOCATION
    ${jnilibDir}/arm64-v8a/libtensorflowlite_gpu_delegate.so)


# include パスの追加。bazel がダウンロードしたヘッダファイルも併記。
include_directories(${tfliteDir}/
                    ${bazelgenDir}/external/flatbuffers/include
                    ${bazelgenDir}/external/com_google_absl
                    )

# アプリとリンク
target_link_libraries(native-activity
    android
    native_app_glue
    lib_tflite
    lib_tflite_gpu_delegate
    log)
```

#4. GPU Delegate 有無による性能比較
　以上の手順によりアプリをビルドすることができたはずです。

　ここでは、出来上がった Style Transfer アプリを実際に XPERIA XZ1 で動かした際の結果について触れておきます。
　GPU Delegate 有無による性能比較のため、条件を変えて下記３種類で実験しました。


|   実行条件  | 説明                                   | 使用したモデル |
| ------------| -------------------------------------- | -----------------|
| (1) **GPU** (float) | GPUDelegate **有効**。GPUで fp16 精度で演算  | [fp16モデル](https://storage.googleapis.com/download.tensorflow.org/models/tflite/arbitrary_style_transfer/style_transfer_f16_384.tflite)|
| (2) CPU (float) | GPUDelegate無効。CPUで float 精度で演算 | [fp16モデル](https://storage.googleapis.com/download.tensorflow.org/models/tflite/arbitrary_style_transfer/style_transfer_f16_384.tflite)|
| (3) CPU (int)   | GPUDelegate無効。CPUで int8 精度で演算  | [int8量子化モデル](https://storage.googleapis.com/download.tensorflow.org/models/tflite/arbitrary_style_transfer/style_transfer_quantized_384.tflite)| |


##4.1 比較
　生成された画像は下記貼付の通りです。
　いずれも味わい深い絵画調の画像となっており、演算精度による生成画像の違いは感じられません。（少なくとも私は違いが判りません）

**(1) GPU**
<img width="640" alt="GPU" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265135/ef8b0fde-86f2-2ba2-32e1-0872772d21e0.png">

**(2) CPU (float)**
<img width="640" alt="CPU(float)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265135/0d9efc7b-2f0a-4ec4-d473-577d8e1f558c.png">

**(3) (CPU int)**
<img width="640" alt="CPU(int)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265135/82371aab-d3e8-6bed-3280-36d7bbf5a7b3.png">


　生成画像にはほとんど差がありませんでしたが、処理速度は大きく差がみられました。
　GPUによる性能改善効果が明確に表れています。


|                      | TensorFlow Lite 処理時間（※１） | 前／後処理含むトータル処理時間（※２）|
| -------------------- | -------------------|----------------------|
| (1) **GPU**          | **約 40 [ms]**     | 約 50 [ms]           |
| (2) CPU (float)      | 約 1050 [ms]       | 約 1060 [ms]         |
| (3) CPU (float)      | 約 450 [ms]        | 約 460 [ms]          |

　※１）transferモデルの invoke()時間であり、predictモデルのinvoke()時間は計測対象外です。
　※２）前処理とは、transferモデルが期待する入力画像サイズ(384x384)にリサイズする処理です。
　※２）後処理とは、transfer モデルの出力結果を画面表示する処理です。キャプチャ画像に出ている文字描画時間や、表示バッファスワップ待ち時間を含みます。

この結果を見ると、積極的に GPU Delegate を使っていきたくなりますね。



#5. おわりに
　今回使った SytleTransfer のモデルは **GPUDelegateによる性能改善効果絶大** でした。
　CPUとの性能差が大きすぎるので、アプリのバグを疑って何度か見直したのですが、どうやら間違ってないような気がするので、多分本当に速いんだと思います。（ちなみに TensorFlow r2.1 時点では、このモデルに含まれるOperationの一部が GPU Delegate 未対応で CPUへフォールバックされるため、ここまで速くなりません。r2.2 ブランチ以降でのみ高速化の恩恵が得られます。）

　Style Transfer のモデルが GPU Delegate 時に大幅に性能向上する理由としては、もともとStyle Transfer モデルがGPUに有利なOperation構成となっているか、あるいは「GPUで処理することを想定した何らかの最適化」が施されているのではないかと思うのですが、学習済みモデルをダウンロードして使っただけということもあり、あまりよくわかっていません。継続調査していこうと思います。

　世の中エッジ用AIアクセラレータが百花繚乱状態ですが、スマホには初めから比較的強力なGPUが搭載されており、APIもほぼ統一(OpenGLES/OpenCL/Vulkan or Metal) されています。今後、GPUを最大限に活かすような、GPU aware なモデル最適化テクニックが広く共有されれば良いな、と思います。


#6. ソースコード
　今回計測に用いた StyleTransfe のアプリソースコードは下記で公開しています。
　https://github.com/terryky/android_tflite