1. はじめに
TensorFlow Lite を GPUで高速化する GPU Delegate機能 を Android で使う方法について書きます。
GPU Delegate に関する公式情報はいくつかあるのですが(例えばここ や [ここ] (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/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. ビルド手順
それではアプリをビルドする方法について説明していきます。
作業は大きく次の2つのステップで行います。
1)コマンドラインから Bazel
で TensorFlow Lite ライブラリ と GPUDelegate ライブラリをビルドする
2)Android Studio で、1)で生成したライブラリのリンク設定を行いアプリをビルド。実機へ転送して実行
以下、ステップごとに説明します。
3.1 TensorFlow Lite ライブラリ、GPUDelegate ライブラリのビルド
3.1.1 事前準備
3.1.1.1 Android NDK, Android SDK のインストール
NDKやSDKは、Android 開発されたことのある人ならインストール済だと思います。
まだの方は、Android Studio の GUI でインストールしても良いですし、明示的に ここから直接ダウンロードしてインストールしてもかまいません。
3.1.1.2 Bazel のインストール
TensorFlow のバージョンに合った Bazel
をインストールします。今回は TensorFlow の r2.2 ブランチで作業するため、Bazel 2.0.0 をインストールします。
$ 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 ブランチに切り替えます。
$ cd work
$ git clone https://github.com/tensorflow/tensorflow.git
$ cd tensorflow
$ git checkout r2.2
次に configure
を行い、「Android NDK がどこにインストールされているか」や「ターゲットの Android API レベルをいくつにするか」といった Android 特有のビルド環境の設定を行います。
これらの設定情報は、configure
実行時にインタラクティブに入力しても良いのですが、事前に環境変数で設定しておけば、configure フェーズで入力せずに済むようです。
下記は私の環境での設定例です。環境に合わせてご指定ください。
$ 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
オプションつきで叩くだけで難なくビルドできます。(ラズパイ向けにビルドした時と比べると何て楽ちんなんでしょう)
$ 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 で進行中 なので、改善されていくのではと思います。
$ 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
という名前のセクション)が含まれており、これがファイルサイズ肥大要因のようです。
その時のセクション情報を見る
$ 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
どうやら、--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 のバージョンによって若干変化するようなので注意してください。
# 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 有無による性能比較のため、条件を変えて下記3種類で実験しました。
実行条件 | 説明 | 使用したモデル |
---|---|---|
(1) GPU (float) | GPUDelegate 有効。GPUで fp16 精度で演算 | fp16モデル |
(2) CPU (float) | GPUDelegate無効。CPUで float 精度で演算 | fp16モデル |
(3) CPU (int) | GPUDelegate無効。CPUで int8 精度で演算 | int8量子化モデル |
4.1 比較
生成された画像は下記貼付の通りです。
いずれも味わい深い絵画調の画像となっており、演算精度による生成画像の違いは感じられません。(少なくとも私は違いが判りません)
生成画像にはほとんど差がありませんでしたが、処理速度は大きく差がみられました。
GPUによる性能改善効果が明確に表れています。
TensorFlow Lite 処理時間(※1) | 前/後処理含むトータル処理時間(※2) | |
---|---|---|
(1) GPU | 約 40 [ms] | 約 50 [ms] |
(2) CPU (float) | 約 1050 [ms] | 約 1060 [ms] |
(3) CPU (float) | 約 450 [ms] | 約 460 [ms] |
※1)transferモデルの invoke()時間であり、predictモデルのinvoke()時間は計測対象外です。
※2)前処理とは、transferモデルが期待する入力画像サイズ(384x384)にリサイズする処理です。
※2)後処理とは、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