2020年1月19日日曜日

TensorFlow Lite for microのHello worldサンプルをM5StickVで動かす(その1)

更新


LCDを表示するぶぶんを「その2」に記載した。


なお、ここで説明した時のソースは以下のcommitとなる。



目的


TensorFlow Lite for microのサンプル(Hello world)をM5StickV向けにビルド、動作させる。今回は、サンプルをそのまま動作させて、シリアルで結果が得られるところまでを確認する。


動機


前回前々回のブログでTensorFlow Lite for microをRISC-Vプロセッサ向けにビルド・Qemuで動作させることを試した。やっぱり、実際のモノ(マイコンとか、実機)で動かしてみたいと思った。そういえば、M5StickVのCPUはRISC-V 64 bitなので、もしかしたらうまくいくかもしれないと思い、試してみた。


ビルドまでの道のり


ここでは、前々回のブログで用いたリポジトリ(本家からのfork)を使う。このため、最新では異なることがあるので注意。

ビルドできるまでのいろいろなことを記載する。


コンパイラ


M5StickV向けのコンパイラは、以下で公開しているものを使った。
バージョンは、「v8.2.0-20190409」である。


Kendryte K210 standalone SDK


non OS向けのSDKが公開されている。
リンカスクリプトやBSP、ドライバのSDKが付属し、cmakeを利用してビルドできる。このリポジトリのsrc配下にターゲットとなるソースを配置し、CMakeLists.txtを編集することで、目的のバイナリがビルドできる。


kflash_gui


M5StickVのバイナリ書き込みツール。
(メインで使っている)Fedora 30ではうまく起動できなかったため、Windows版を利用する。


TensorFlow Lite for microの構成


ビルドに必要なソース・ヘッダファイルを確認した。ここでは、「hello world」サンプルをもとにする。

Makefile


大本のMakefileは以下。ビルド時にターゲットCPUとビルドするアプリケーション(プロジェクト)を指定する。
前回指定したRISC-V 32bit、hello worldのサンプルの場合、この辺りも参照する。
riscv32_mcu/Makefile.inc は、#33972 のRISC-Vターゲットでビルドが失敗する問題を解消するためである。これは、今時点(2020.1.19)で取り込まれていないため注意。


3rd party ライブラリ


TensorFlow以外のライブラリは以下を使用している。
上記以外では、ターゲット固有のSDKやtoolchainであるため、ここでは省略。なお、gemmlowp、flatbuffersともヘッダーファイルだけを使用する。


ファイル構成


hello worldのビルドに必要なソース・ヘッダファイルの構成は以下。microがどのようなファイルを必要としているのか、見渡せるようにする。本体と比べれば、ファイル数は少ない。

    tensorflow
    ├ tensorflow
    |  ├ core
    |  |  └ public
    |  |      └ version.h
    |  └ lite
    |      ├ c
    |      |  ├ builtin_op_data.h
    |      |  ├ common.c
    |      |  └ common.h
    |      ├ core
    |      |  └ api
    |      |      ├ error_reporter.cc
    |      |      ├ error_reporter.h
    |      |      ├ flatbuffer_conversions.cc
    |      |      ├ flatbuffer_conversions.h
    |      |      ├ op_resolver.cc
    |      |      ├ op_resolver.h
    |      |      ├ tensor_utils.cc
    |      |      └ tensor_utils.h
    |      ├ kernels
    |      |  ├ internal
    |      |  |  ├ optimized
    |      |  |  |  └ neon_check.h
    |      |  |  ├ reference
    |      |  |  |  ├ integer_ops
    |      |  |  |  |  ├ add.h
    |      |  |  |  |  ├ conv.h
    |      |  |  |  |  ├ depthwise_conv.h
    |      |  |  |  |  ├ fully_connected.h
    |      |  |  |  |  ├ mul.h
    |      |  |  |  |  ├ pooling.h
    |      |  |  |  |  └ softmax.h
    |      |  |  |  ├ add.h
    |      |  |  |  ├ arg_min_max.h
    |      |  |  |  ├ binary_function.h
    |      |  |  |  ├ ceil.h
    |      |  |  |  ├ comparisons.h
    |      |  |  |  ├ concatenation.h
    |      |  |  |  ├ conv.h
    |      |  |  |  ├ depthwiseconv_float.h
    |      |  |  |  ├ depthwiseconv_uint8.h
    |      |  |  |  ├ dequantize.h
    |      |  |  |  ├ floor.h
    |      |  |  |  ├ fully_connected.h
    |      |  |  |  ├ logistic.h
    |      |  |  |  ├ maximum_minimum.h
    |      |  |  |  ├ mul.h
    |      |  |  |  ├ neg.h
    |      |  |  |  ├ pad.h
    |      |  |  |  ├ pooling.h
    |      |  |  |  ├ prelu.h
    |      |  |  |  ├ process_broadcast_shapes.h
    |      |  |  |  ├ quantize.h
    |      |  |  |  ├ round.h
    |      |  |  |  ├ softmax.h
    |      |  |  |  └ strided_slice.h
    |      |  |  ├ common.h
    |      |  |  ├ compatibility.h
    |      |  |  ├ quantization_util.cc
    |      |  |  ├ quantization_util.h
    |      |  |  ├ round.h
    |      |  |  ├ scoped_profiling_label_wrapper.h
    |      |  |  ├ strided_slice_logic.h
    |      |  |  ├ tensor.h
    |      |  |  ├ tensor_ctypes.h
    |      |  |  └ types.h
    |      |  ├ kernel_util.cc
    |      |  ├ kernel_util.h
    |      |  ├ op_macros.h
    |      |  └ padding.h
    |      ├ micro
    |      |  ├ examples
    |      |  |  └ hello_world
    |      |  |      ├ constants.cc
    |      |  |      ├ constants.h
    |      |  |      ├ main.cc
    |      |  |      ├ main_functions.cc
    |      |  |      ├ main_functions.h
    |      |  |      ├ output_handler.cc
    |      |  |      ├ output_handler.h
    |      |  |      ├ sine_model_data.cc
    |      |  |      └ sine_model_data.h
    |      |  ├ kernels
    |      |  |  ├ activations.cc
    |      |  |  ├ activation_utils.h
    |      |  |  ├ add.cc
    |      |  |  ├ all_ops_resolver.cc
    |      |  |  ├ all_ops_resolver.h
    |      |  |  ├ arg_min_max.cc
    |      |  |  ├ ceil.cc
    |      |  |  ├ comparisons.cc
    |      |  |  ├ concatenation.cc
    |      |  |  ├ conv.cc
    |      |  |  ├ depthwise_conv.cc
    |      |  |  ├ dequantize.cc
    |      |  |  ├ elementwise.cc
    |      |  |  ├ floor.cc
    |      |  |  ├ fully_connected.cc
    |      |  |  ├ logical.cc
    |      |  |  ├ logistic.cc
    |      |  |  ├ maximum_minimum.cc
    |      |  |  ├ micro_ops.h
    |      |  |  ├ micro_utils.h
    |      |  |  ├ mul.cc
    |      |  |  ├ neg.cc
    |      |  |  ├ pack.cc
    |      |  |  ├ pad.cc
    |      |  |  ├ pooling.cc
    |      |  |  ├ prelu.cc
    |      |  |  ├ quantize.cc
    |      |  |  ├ reshape.cc
    |      |  |  ├ round.cc
    |      |  |  ├ softmax.cc
    |      |  |  ├ split.cc
    |      |  |  ├ strided_slice.cc
    |      |  |  ├ svdf.cc
    |      |  |  └ unpack.cc
    |      |  ├ memory_planner
    |      |  |  ├ greedy_memory_planner.cc
    |      |  |  ├ greedy_memory_planner.h
    |      |  |  ├ linear_memory_planner.cc
    |      |  |  ├ linear_memory_planner.h
    |      |  |  └ memory_planner.h
    |      |  ├ riscv64_mcu
    |      |  |  ├ debug_log.cc
    |      |  |  └ README.md
    |      |  ├ tools
    |      |  |  └ make
    |      |  |      └ downloads
    |      |  |          ├ flatbuffers
    |      |  |          |  ├ include
    |      |  |          |  |  └ flatbuffers
    |      |  |          |  |      ├ base.h
    |      |  |          |  |      ├ flatbuffers.h
    |      |  |          |  |      └ stl_emulation.h
    |      |  |          |  └ LICENSE.txt
    |      |  |          └ gemmlowp
    |      |  |              ├ fixedpoint
    |      |  |              |  ├ fixedpoint.h
    |      |  |              |  └ fixedpoint_sse.h
    |      |  |              ├ internal
    |      |  |              |  └ detect_platform.h
    |      |  |              └ LICENSE
    |      |  ├ compatibility.h
    |      |  ├ debug_log.h
    |      |  ├ debug_log_numbers.cc
    |      |  ├ debug_log_numbers.h
    |      |  ├ memory_helpers.cc
    |      |  ├ memory_helpers.h
    |      |  ├ micro_allocator.cc
    |      |  ├ micro_allocator.h
    |      |  ├ micro_error_reporter.cc
    |      |  ├ micro_error_reporter.h
    |      |  ├ micro_interpreter.cc
    |      |  ├ micro_interpreter.h
    |      |  ├ micro_mutable_op_resolver.cc
    |      |  ├ micro_mutable_op_resolver.h
    |      |  ├ micro_optional_debug_tools.cc
    |      |  ├ micro_optional_debug_tools.h
    |      |  ├ micro_utils.cc
    |      |  ├ micro_utils.h
    |      |  ├ simple_memory_allocator.cc
    |      |  ├ simple_memory_allocator.h
    |      |  ├ test_helpers.cc
    |      |  └ test_helpers.h
    |      ├ schema
    |      |  └ schema_generated.h
    |      ├ string_type.h
    |      ├ string_util.h
    |      ├ type_to_tflitetype.h
    |      └ version.h
    ├ AUTHORS
    └ LICENSE


デバッグログの出力


HWやtoolchain固有の部分であり、DebugLog関数を実装することになる。RISC-V向けには、RISC-V 32bitをそのまま流用することで、シリアルにログが出力される。


ビルドプロジェクト


今回は、TensorFlow Lite for microのMakefileを使用するのではなく、Kendryte K210 standalone SDKのcmakeを使ってのビルド方法にした(そのほうが楽そうだった)。

ビルド用のプロジェクトは以下で公開している。



TensorFlow Lite for microのhello worldで使用するソース・ヘッダファイルのみをsrc配下に格納する。
※tensorflowすべてのファイルを格納してしまうと、SDK内でヘッダファイルの競合が発生するため、必要なファイルのみとした。

CMakeLists.txtには、必要なヘッダのパス、ソースファイルの一覧を記載する。また、前々回のブログでも記載した、std::roundの参照エラー(#35302)の回避のために、以下の定義を追加しておく。あと、Kendryte K210 standalone SDKが便利なマクロを用意してくれるので助かる。

M5StickV向けのビルドで発生するビルドエラーの回避


1か所、ビルドエラーが発生するためにソースを修正する。
std::minの第二引数「(1ll << 31) - 1.0」がfloatであるといわれて、第一引数「beta * input_scale * (1 << (31 - input_integer_bits))」のdoubleと型が不一致だと怒られてしまう。第二引数「(1ll << 31) - 1.0」はdoubleではないのか?(しかも、前々回のブログではエラーとならなかった...)のでもしかしたら、toolchainによるものかも?

とりあえず、doubleにキャストすることで回避する。


ビルド


kflash_guiが(自分の環境だと)Windowsでしか動かないのでWindows 10でビルドする。

Windows版のCMakeをインストールする。

Kendryte RISC-V GNU Compiler Toolchainをダウンロードして適当なディレクトリで展開する。

PowerShellで操作。
> git clone https://github.com/NobuoTsukamoto/m5stickv-tensorflow-lite-micro.git
> $env:Path="\kendryte-toolchain\bin;C:\Program Files\CMake\bin;" + $env:Path
> cd m5stickv-tensorflow-lite-micro\src\micro
> mkdir build
> cd build
> cmake -G "MinGW Makefiles" ../../../..
> make

ビルド後、buildディレクトリに「micro」と「micro.bin」のファイルができる。
「micro.bin」ファイルをkflash_guiでM5StickVに焼けばOK。


結果


Teratermで接続すると、シリアルにログが出力される。



次回


つぎは、M5StickVのLCDに結果表示するために行ったことを記載する。


2020年1月4日土曜日

TensorFlow lite for micro で RISC-Vターゲットのサンプルをqemuで動かす

目的


前回のブログ「TensorFlow lite for micro で RISC-Vターゲットのサンプルをビルドする」で生成した、Hello worldのバイナリを実行する。
ただ、実機("Hifive1" SiFive FE310 development board)は持っていないため、qemuで動作させる。


qemuでの実行


qemuの準備


RISC-Vターゲットのqemuを準備する。Fedora 31にはパッケージ(qemu-system-riscv 4.1.1)が存在するが、今回はver.4.2をソースビルドする。

ソースビルド


依存関係のインストール。追加で必要になったのは以下(その他はTensorFlowなどのビルドで入っているかも?)。

$ sudo dnf install glib2-devel pixman-devel

ソースファイルをダウンロード。

$ wget https://download.qemu.org/qemu-4.2.0.tar.xz
$ tar xf qemu-4.2.0.tar.xz
$ cd qemu-4.2.0/

configure, makeを行う。ターゲットはriscv32-softmmuのみでよいはず。後のことも考えて、その他のRISC-Vターゲットもビルドする。

$ ./configure --target-list=riscv64-softmmu,riscv32-softmmu,riscv64-linux-user,riscv32-linux-user
$ make -j$(nproc)
$ sudo make install


バイナリのqemuでの実行


前回のブログで作成したTensorFlow lite for microのRISC-Vターゲットのバイナリ(Hello worldのサンプル)を実行する。
オプションの指定は以下。
  • machineオプション: sifive_e
  • kernelオプション: 作成したELFファイル
    tensorflow/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world

実行。

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel tensorflow/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_worl

コンソールにログが出力される。Hello worldのサンプルは正弦波を出力するモデルである。x_value, y_valueにそれなりの値が永遠と出力される。




2020年1月3日金曜日

TensorFlow lite for micro で RISC-Vターゲットのサンプルをビルドする

目的

  • TensorFlow Lite for microをやってみる。
  • はじめのサンプル"Hello world"をビルドして、ビルド方法やビルドしているものをなんとなく見てみる。
  • ターゲットはRISC-V、最終的にはRISC-Vプロセッサで動かしたい。


動機


去年よりTensorFlow Lit for microをやってみたいと思ってた。また、RISC-Vにも興味がある。


ビルドする前に


2019.1.3時点で、TensorFlowのmaster(commit 68cbb78)では、RISC-V("Hifive1" SiFive FE310 development board)向けのビルド手順がREADMEから削除されている。いつの時点からなのか、理由は不明。このあたりまでは、手順があった。experimentalが外れたときに削除されたのかも?

また、現時点ではRISC-Vのターゲットではビルドができない。PRはあるがまだ取り込まれていない。
#35302はレビュー待ちの状態なので、そのうち取り込まれると思われる。
#33972はコンフリクトが発生(microがexperimentalでなくなったため)しているため、いつ取り込まれるかは不明。。。

この2つはRISC-Vターゲットの固有のものなので、少し内容をおさえておく。

#35302


RISC-Vターゲットでのみビルドエラーが発生する。C++11にはstd::roundがあるが、RISC-V toolchainには何故か存在せず、::roundが使える(おそらく、toolcainのバグ?)。このため、tflite::TfLiteRound を使うことにする。tflite::TfLiteRoundはstd::roundが定義されていない場合は、::roundを使用するようになっているだけである。

std::roundが使えず、::roundが使える理由はおそらく、このtweetのリンク先と同じだろう(RISC-V toolchainのことではないが)。



#33972


もとのissueはこちら。2つの問題がある。
  • __wrap__funsのundefined referenceが発生する。
    lite/micro/tools/make/targets/mcu_riscv_makefile.incにある変数が上書きされてしまうためである。このため、example/xxxx配下に変数が上書きされないよう、makfile.incを作ることで回避。
  • __dso_handleが定義されていない。
    RISC-V toolcainには__dso_handleが定義されていない。lite/experimental/micro/arduino/abi.cc に定義してビルドすることで回避している(fno-use-cxa-atexitオプションで回避できるのだろうか?)。


ビルド


ようやく本題。

手順は古いREADMEを参照しながら。#35302、#33972については自分のリポジトリに取り込んだ。このため、この手順は公式のものでなく、今後変更される可能性は大。


まずは、githubからリポジトリをclone。ブランチは"micro_riscv"。

$ git clone -b micro_riscv https://github.com/NobuoTsukamoto/tensorflow.git

ビルド用のDocker imageをビルドし、コンテナを立ち上げる。

$ sudo docker build -t riscv_build -f ./tensorflow/tensorflow/lite/micro/testing/Dockerfile.riscv ./tensorflow/tensorflow/lite/micro/testing/
$ sudo docker run -it -v ./tensorflow/micro/tensorflow:/workspace riscv_build:latest bash

コンテナ内でhello worldのサンプルのビルドを行う。RISC-V toolchainはビルド時にダウンロードする(ダウンロードするtoolchainはここを参照)。toolchainへのパスを通しておく。

# export PATH=${PATH}:/workspace/tensorflow/lite/micro/tools/make/downloads/riscv_toolchain/bin/
# cd /workspace
# make -f tensorflow/lite/micro/tools/make/Makefile TARGET=riscv32_mcu hello_world_bin

ビルド時に気になるメッセージがあるが、バイナリが生成されているので気にしない(たぶん、問題ない?)。

/workspace/tensorflow/lite/micro/tools/make/downloads/riscv_toolchain/bin/../lib/gcc/riscv64-unknown-elf/8.1.0/../../../../riscv64-unknown-elf/bin/ld: tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world: section `.gcc_except_table._ZN9__gnu_cxx27__verbose_terminate_handlerEv' can't be allocated in segment 1
LOAD: .data .gcc_except_table._ZN10__cxxabiv111__terminateEPFvvE .gcc_except_table.__gxx_personality_v0 .gcc_except_table.__cxa_call_unexpected .gcc_except_table._ZN9__gnu_cxx27__verbose_terminate_handlerEv

ビルドが終了するとtensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin 配下にELFと.binファイルが生成される。readelfコマンドで見るとRISC-V 32bit向けであることがわかる。

# ls -alh tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin
drwxr-xr-x. 2 root root 4.0K Jan  3 08:56 .
drwxr-xr-x. 5 root root 4.0K Jan  3 06:14 ..
-rwxr-xr-x. 1 root root 4.0M Jan  3 06:14 hello_world
-rwxr-xr-x. 1 root root 1.5G Jan  3 06:14 hello_world.bin
# readelf -h tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x20400000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4175496 (bytes into file)
  Flags:                             0x1, RVC, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         37
  Section header string table index: 36


Renodeによるテスト


Renodeのエミュレータによるバイナリのテストを行う。まず、テスト用バイナリの生成を行う。hello_world_test が生成される。

# make -f tensorflow/lite/micro/tools/make/Makefile TARGET=riscv32_mcu hello_world_test

スクリプトはmicro_speech用なのでhello_worldに書き換える。
/workspace/tensorflow/lite/micro/testing/sifive_fe310.resc ファイルを修正する。10行目のパスの実行ファイルを"micro_speech_test"から"hello_world_test"に変更する。
$bin?=@/workspace/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world_test

Renodeでバイナリを実行する。~~~ALL TESTS PASSED~~~が出力されればOK。

# renode -P 5000 --disable-xwt -e 's@/workspace/tensorflow/lite/micro/testing/sifive_fe310.resc'
08:56:34.2849 [INFO] Loaded monitor commands from: /opt/renode/./scripts/monitor.py
08:56:34.3117 [INFO] Monitor available in telnet mode on port 5000
08:56:34.6272 [INFO] Including script: /workspace/tensorflow/lite/micro/testing/sifive_fe310.resc
08:56:34.6453 [INFO] System bus created.
08:56:35.1569 [DEBUG] Segment size automatically calculated to value 64KiB (3)
08:56:35.3093 [DEBUG] Segment size automatically calculated to value 16MiB
08:56:35.3691 [WARNING] Previous log file detected and renamed to: /tmp/renode_riscv_log.txt.1
08:56:35.5157 [DEBUG] sysbus: Loading ELF /workspace/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world_test.
08:56:35.5244 [INFO] sysbus: Loading segment of 135576 bytes length at 0x20400000.
08:56:35.5396 [DEBUG] sysbus: Segment loaded.
08:56:35.5397 [INFO] sysbus: Loading segment of 2152 bytes length at 0x20421198.
08:56:35.5399 [DEBUG] sysbus: Segment loaded.
08:56:35.5784 [INFO] cpu: Setting PC value to 0x20400000.
08:56:35.7980 [INFO] SiFive-FE310: Machine started.
08:56:35.8604 [WARNING] sysbus: [cpu: 0x20400AA8] ReadDoubleWord from non existing peripheral at 0x10000070.
08:56:35.8615 [WARNING] sysbus: [cpu: 0x20400AB6] WriteDoubleWord to non existing peripheral at 0x10000070, value 0x0.
08:56:35.8616 [WARNING] sysbus: [cpu: 0x20400AC2] (tag: 'PRCI_HFROSCCFG') WriteDoubleWord to non existing peripheral at 0x10008000, value 0x40100004.
08:56:35.8618 [WARNING] sysbus: [cpu: 0x20400AC8] (tag: 'PRCI_HFROSCCFG') ReadDoubleWord from non existing peripheral at 0x10008000, returning 0xFFFFFFFF.
08:56:35.8620 [WARNING] sysbus: [cpu: 0x20400ACE] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8622 [WARNING] sysbus: [cpu: 0x20400AD6] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFEFFFF.
08:56:35.8624 [WARNING] sysbus: [cpu: 0x20400AD8] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8625 [WARNING] sysbus: [cpu: 0x20400AE8] (tag: 'PRCI_HFROSCCFG') WriteDoubleWord to non existing peripheral at 0x10008000, value 0x40100004.
08:56:35.8627 [WARNING] sysbus: [cpu: 0x20400AEE] (tag: 'PRCI_HFROSCCFG') ReadDoubleWord from non existing peripheral at 0x10008000, returning 0xFFFFFFFF.
08:56:35.8629 [WARNING] sysbus: [cpu: 0x20400AF4] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8631 [WARNING] sysbus: [cpu: 0x20400AFC] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFEFFFF.
08:56:35.8632 [WARNING] sysbus: [cpu: 0x20400B04] WriteDoubleWord to non existing peripheral at 0x10014000, value 0x8.
08:56:35.8634 [WARNING] sysbus: [cpu: 0x20400B0E] WriteDoubleWord to non existing peripheral at 0x1000800C, value 0x100.
08:56:35.8651 [WARNING] sysbus: [cpu: 0x20400B18] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0x405F1.
08:56:35.8652 [WARNING] sysbus: [cpu: 0x20400B1A] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8653 [WARNING] sysbus: [cpu: 0x20400B24] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFBFFFF.
08:56:35.8989 [WARNING] sysbus: [cpu: 0x20400B42] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8990 [WARNING] sysbus: [cpu: 0x20400B4C] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8991 [WARNING] sysbus: [cpu: 0x20400B52] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFFFFFF.
08:56:35.8994 [WARNING] gpioInputs: Unhandled read from offset 0x3C.
08:56:35.8998 [WARNING] gpioInputs: Unhandled write to offset 0x3C, value 0x0.
08:56:35.8998 [WARNING] gpioInputs: Unhandled read from offset 0x38.
08:56:35.8999 [WARNING] gpioInputs: Unhandled write to offset 0x38, value 0x30000.
08:56:36.0392 [DEBUG] uart0: [+0.67s host +0.4ms virt 0.4ms virt from start] core freq at 170833 Hz
08:56:36.0416 [DEBUG] uart0: [+2.47ms host +0s virt 0.4ms virt from start]   Testing LoadModelAndPerformInference
08:56:36.0771 [DEBUG] uart0: [+35.5ms host +0.7ms virt 1.1ms virt from start]   1/1 tests passed
08:56:36.0776 [DEBUG] uart0: [+0.52ms host +0s virt 1.1ms virt from start]   ~~~ALL TESTS PASSED~~~
08:56:36.0777 [DEBUG] uart0: [+83?s host +0s virt 1.1ms virt from start]   
08:56:36.0798 [DEBUG] uart0: [+2.14ms host +0s virt 1.1ms virt from start]   
08:56:36.0808 [DEBUG] uart0: [+0.99ms host +0s virt 1.1ms virt from start]   Progam has exited with code:0x00000000
09:02:45.0257 [INFO] SiFive-FE310: Machine paused.
09:02:45.0330 [DEBUG] SiFive-FE310: Disposing sysbus.cpu.
09:02:45.0352 [DEBUG] SiFive-FE310: Disposing sysbus.
09:02:45.0358 [DEBUG] SiFive-FE310: Disposing sysbus.maskRom.
09:02:45.0360 [DEBUG] SiFive-FE310: Disposing sysbus.otp.
09:02:45.0361 [DEBUG] SiFive-FE310: Disposing sysbus.dtim.
09:02:45.0362 [DEBUG] SiFive-FE310: Disposing sysbus.qspi0Xip.
09:02:45.0371 [INFO] SiFive-FE310: Disposed.


その他


今回、ビルドする際の情報を記載する。

RISC-V toolchainについて


デフォルトで使用しているtoolchainはSiFiveのtoolchain(最新でない)である。別途、riscv-gnu-toolchainでビルドしたものも使用できるはず。なお、Newlib cross-compilerの32bit向けが必要。


ボード向けのBSPなど


これもダウンロードスクリプトでダウンロードしている。Makefileでもこのあたりこのあたり(各サンプルのMakefile)でヘッダや必要なモジュールを加えているので参考になりそう。


Renodeについて


まだ、あまりわかっていない。オープンソースの"virtual development tool "。RISC-Vだけでなくて、その他アーキテクチャもサポートする仮想開発ツール?もう少し調べてみよう。

今回、dockerのコンテナ内でビルドしたのはRenodeを使用するためである。Renodeを使わなければ、dockerは不要。


最後に


TensorFlow Lite for microのRISC-V向けのhello worldのバイナリを作成した。次はこのバイナリを動作させてみたい。

2019年12月29日日曜日

2019年の振り返り

今年はいろいろとやった気がするので、振り返ってみようと思う。やっぱり、Edge TPUとJetson Nanoを中心にとても楽しい一年だったと思う。


エッジ向けAIデバイス


今年はエッジ向けAIデバイスがブームになったと思う。個人では3つ購入して遊んでみた。


個人でこのような素晴らしいデバイスを購入できるようになったのはとても良いと思う。また、AIデバイスによってOn-DeviceでDeep Learningができるようになることは、とても大きい可能性があると思う。


Edge TPU


3月に発売を知って、真っ先に買ってみた。
画像認識、物体検出だけでなくて、セグメンテーションモデルやポーズ推定も動作するのは素晴らしい。まだまだ、使い方はたくさんあると思うので、来年も継続してやってみたい。

こんなことをやってみた。

ブログ(いくつか)


コード

また、TensorFlow Liteについても、ちょっとだけわかった気がする(あくまで気がする)。ラズパイなどのARM CPUでそれなりに動作できるのはとても素晴らしいと思う。モバイルも含めていろいろとできることが増えるなぁと感じる。
TensorFlow Lite for MicrocontrollersやAndoridも気になる分野なので来年は絶対、チャレンジする。

インフルエンサーのかたがつぶやくとものすごい広まることも知った(数日で1,000ビュー超えるなんて初めて...)。


Jetson Nano


最初は我慢していたけど、物欲に負けて7月に購入。
TF-TRT(TensorFlow integration with TensorRT)を中心にやってみた。また、もっと推論は早くなるはず、と指摘いただいて試してみた結果、原因が判明したこともよかった。

ブログ
コード

CUDAはいろいろと使い方がたくさんある。まだ、TensorRTも試せていないので、来年は手を出したいと思う。


M5STICKV


RISC-V CPUとKPUプロセッサで\3,000ちょっとはとても安い!これも物欲に負けて購入。まだ十分使えていないので、来年はやってみる!これを使ってやってみたいことはたくさんある。


MediaPipe


気になっていて、ようやく試せた。MLのアプリケーション作るのは面倒!だけど、フレームワークにして楽にしちゃおう!という感じだと思うけど、これからこういったアプローチは増えてくるのだろうか?来年もウォッチしようと思う。

ブログ
コード



コンペ


今年の前半は、いくつか画像のコンペをチャレンジしてみた。まあ、メダルは取れたけど、やっぱり金じゃないのはくやしい。上位を目指したい。時間が取れなくてチャレンジだけで終わってしまったのもいくつかる。来年はどうしよう?もう一回チャレンジしてみようかな?


釣り


まぁ、今年も行ける回数は少なかったよね(しょうがない)。しかも数年ぶりにボウズもあった(涙)来年も、数回は行きたいね。







ラーメン


一応、長野はラーメン激戦区。お店は14回訪問。月1回ぐらいのペース?今度はブログにラーメンを書いてみようかな。旨い!しかないけど。。。

久々の阿吽。塩。健康診断の後の一杯は最高

土鍋や竹さん。いつもおいしい!

県外でのラーメンは1回だけ。東京ラーメンストリートの斑鳩


最後に


書かなかったけど、Yoctoもっと詳しくなりたいし、カメラもうまくなりたい。jax colab tpuもある、ラズパイ4も使い倒したい... スノーボードも復活したいし...
やりたいことはたくさんある。

ただ、今年は楽しい一年だったと思う。
一番は、Twitterでいろいろとアドバイスやいいねをいただくフォロワーさんが増えたことがうれしかった(ほんと、ありがとうございます)。いろいろとリプライをもらったり、ここに記載できないものがたくさんありすぎて、感謝。やっぱり、色々もらえるとモチベーションもちがうんだなぁとわかった。

来年も、面白いことをやってきたいと思う。

2019年12月27日金曜日

MediaPipeでEdge TPU


目的


ここ最近、MediaPipeのサンプルをEdge TPUで動作させてみて、オリジナルから変えてみたこと、ハマったこと、感じたことをまとめてみる。


動機


前のブログでも書いたとおり、MediaPipeは気になっていた。

そこにGoogle Coral DevBoard向けのサンプルがでてきた。
DevBoardを持っておらず購入しようか迷ったが、USB Acceleratorでもできるのは?と思いやって見た結果、Jetson Nano + Edge TPU、Raspberry Pi + Edge TPUで動かすことができた。





メリットとデメリット


思いつくままにメリットとデメリットを記載したい。ただし、所感であり誤りも含んでいると思うので注意。

論文、ドキュメントは以下を参照。


メリット


モジュールの再利用性と非同期処理のサポートが特徴と感じる。

これはMediaPipeが目的でもあるMLアプリケーションのプロトタイプモデルをすばやく構築するために再利用性を高めることと、MLアプリケーションには必要になる非同期処理(重い推論と他の処理を分離)を意識していることからだと思われる。

  • モジュールの再利用
  • 非同期処理のサポート
  • フロー制御のサポート

モジュールの再利用がしやすい


Calculatorとしてコンポーネントを分割することで、再利用しやすい作りとなっている。このコンポーネントをCalculatorと呼んでいる。特に推論を行う前処理、後処理は同じものを再利用したい。AnnotationOverlayCalculator(bonding boxの描画)やPacketResamplerCalculator(フレームの間引き)などがそれに当たる。あと、CPU⇔GPU間のメモリ割当もサポートしているため、AndroidのGPU delegateも簡単に構築できる。


非同期処理のサポート


一般に、推論部分は一番重い(処理時間がかかる)。

このため、GPU(もしくはAccelerator、CPUなど)で推論している間に他の処理をCPUで行うことで、トータルの処理時間を短縮させたい。一般的には、カメラ入力、推論、描画は非同期に行って描画を高速にしたい(見せたい)ということもある。しかし、一般的に非同期処理を実装するのは難易度が高い。

MediaPipeにはtimestampと呼ばれる概念で同期を取ることができる。timestampはデータ(packetと呼ばれる)が生成されるたびにインクリメントされていく。Calculatorは分散で処理していて、データ(packetと呼ばれる)に付属するtimestampによって、データの到達順が管理され、Calculator間のデータの同期が取られる。


フロー制御のサポート


推論など重い処理を担当するCalculatorは、データのボトルネックとなってしまう。適切な制御を行わないとオーバーフローやデッドロックの原因になってしまう。このため、MediaPipeは2つのフロー制御を持っている。
  • The back-pressure mechanism
    ストリームでバッファリングされたPacketが制限に達すると、上位ノードの実行を抑制する。
  • 専用ノードの挿入
    PacketResamplerCalculatorを使ったノードの場合は、Packetを間引きする(たとえば、フレームレートを1/10に間引くなど)。
    FlowLimiterCalculatorを使ったノードの場合は、処理が終わるまでの間のPacketを受け捨てる。

デメリット


まだ、ドキュメントやツール類が追いついていないところにデメリットを感じてしまう。このあたりは今後改善されるかもしれない。
  • Bazelによるビルド
  • どんなCalculatorがあるのか?
  • デバッグツールがない?
  • 実装がC++

Bazelによるビルド


Bazelを多少は理解していないといけないところは辛い。また、graphとBUILDの対応は手動で行う必要がある(graphからビルドするCalculatorのパス・ファイル名を割り出さないといけない)。もう少しスマートな方法がないのだろうか?


どんなCalculatorがあるのか?


これは、ドキュメントが整備されていないのが原因。今は、ソースから何があるのかを拾い出さないといけない。ドキュメントが整備されれば解消されるとは思う。


デバッグツールがない?


Calculatorが非同期で動作するということは、デバッグが大変である。論文を見る限りツールがありそうだがまだすべてが公開されていないようである。ブレークポイントを設定してのデバッグは大変かもしれない...


実装がC++


現状はアプリケーション、CalculatorともC++での実装が必要。C++は敷居が高いと思われる。少なくともアプリケーションはPythonで書きたいのでは?


Edge TPU向けのサンプルを作成したポイント


ここでは、Coral Dev Board Setup (experimental)からEdge TPU USB Acceleratorを使ったサンプルを作成したのポイントを残す。
作成したサンプルは2つ。

Jetson Nano + Edge TPU


こちらは、ほとんどCoral Dev Board Setup (experimental)から変更はない。
Jetson Nano側にEdge TPUのRuntimeをインストールするぐらいである。OpenCVもJetpackにインストール済みのライブラリを使用すればよかった。

Raspberry Pi + Edge TPU


Raspbianは32bit OSであるため、クロスコンパイル時の指定を"--cpu=armv7a"にするぐらいである。ほぼ変更なく動作できるのは、MediaPipeの強みの一つだと思う。


Object Detection and Tracking


AndroidのデモをRaspberry Pi + Edge TPUで動かそうと思ったが、ハマってしまった... 最初の検出は動作するのだが、2回目以降の検出がされない... 原因は、Raspberry Pi(もとはCoarl)のメイン処理とPacketResamplerCalculatorにあった。

もともと、Object Detection and TrackingのGraphは、入力ストリーム(カメラ or ビデオ)を間引いて推論を行っている(トラッキングや表示は間引かない)。これは、時間のかかる・リソース消費が大きい推論を全フレームで行わず、トラッキングで補完することで、リアルタイム・リソース消費を軽くしている。このフレームの間引きをPacketResamplerCalculatorが担当し、デフォルトでは0.5フレームにしている(フロー制御の一つ)。

PacketResamplerCalculatorはデータのtimestampをチェックしてtimestampが指定したフレームレートの時間に達したら、後続のCalculatorに流すようになっている。このため、timestampは経過時間(実時間)を期待している。

timestampはデータの生成の際(メイン処理)に付与され、増加している。Raspberry Pi(もとはCoarl)のメイン処理では、timestampが+1しかされていなかった。このため、timestampの時間の流れが非常にゆっくりになってしまったのが原因だった...(timestampはμs単位なので、非常に長ーく待っていれば、2回目の推論がくる...)

  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) break;  // End of video.
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

    // Send image packet into the graph.
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp++))));
    // ★↑ここでframe_timestampが+1されていることが原因...

    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get();

    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }


このため、実時間(前回データ生成時との時間の差)を与えることで、問題が解消できたのである。修正のコードは下記。githubにもアップしている。std::chronoで前回からの時間差をとっている(ただ、この修正だと、ビデオファイル入力ではまずい... ビデオファイル入力の場合は、フレームレートから算出しないといけない)。

  std::chrono::system_clock::time_point start;
  start = std::chrono::system_clock::now();
  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) break;  // End of video.
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

    // Get timestamp.
    // ★ここで時間差(μs)を取得する。
    auto end = std::chrono::system_clock::now();
    frame_timestamp = std::chrono::duration_cast(end - start).count();
    
    // Send image packet into the graph.
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp))));

    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get();

    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }


最後に


MediaPipeを使うとMLアプリケーションの開発がとても簡単になるように思える。
ただ、Calculator(コンポーネント)の実装やデバッグまで考えると難易度が高いようにも思える。
もう少し使われるようになるのか?2020年もウォッチしてみたいと思う。


2019年11月30日土曜日

MediaPipeのObject detectionのサンプルでSSDLite MobileNet V3を試す

目的


MediaPipeのAndroidのObject detectionのサンプルを動かしたときの備忘録を残す。
今回の手順は公式にもあるので、あまり参考にはならない。


動機


MediaPipeについてはTLに流れてから、ずーっと気になっていた。
今回、モバイルに最適化されたSSDLite MobileNet V3が登場したので試してみた。





Xperia Z5でSSDLite MobileNet V3 Small をGPU実行したときのスクリーンキャプチャである。



手順


手順としては、以下となる。
  1. Android用の環境を準備(Bazel、Android関連のインストール)
  2. MediaPipe用にモデルをエクスポート
  3. モデルをMediaPipeのプロジェクトへの組み込み、ビルド
  4. Andoridへのインストール


Android用の環境を準備


Linux環境(筆者はFedora 31)にBazel、Andorid Studio(Android SDK)、Android NDKをインストールする。インストールの方法は、「TensorFlow Lite GPU delegateのAndroid用ライブラリをビルドする」と同じである。


MediaPipe用にモデルをエクスポート


TF-Liteモデルをエクスポートするのだが、通常のTF-Liteモデルのエクスポート手順とは異なる。これは、PostProssessing(NonMaxSuppression)の処理をMediaPipeが行うためである。
手順は公式でも記載されているが、ここではSSDLite MobileNet V3 Smallのpre-trainedモデルで行う。

まず、tensorflow / models をcloneし、object detection apiのインストールを行う(TF1.15でmodelsはmasterで試している)。

Tensorflow detection model zooからssd_mobilenet_v3_small_cocoをダウンロードして展開する。

$ wget http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v3_small_coco_2019_08_14.tar.gz
$ tar xf ssd_mobilenet_v3_small_coco_2019_08_14.tar.gz

MediaPipeの手順に沿ってモデルをfreeze graphする。ここで注意することは、通常の手順と異なる(パラメータが異なる)ことである。具体的には、export_tflite_ssd_graph.pyの引数に--add_postprocessing_op=falseを指定する。

$ CONFIG_FILE=xxxx/ssd_mobilenet_v3_small_coco_2019_08_14/pipeline.config
$ CHECKPOINT_PATH=xxx/ssd_mobilenet_v3_small_coco_2019_08_14/model.ckpt
$ OUTPUT_DIR=path_to_output_dir
$ python object_detection/export_tflite_ssd_graph.py --pipeline_config_path=$CONFIG_FILE --trained_checkpoint_prefix=$CHECKPOINT_PATH --output_directory=$OUTPUT_DIR --add_postprocessing_op=false

出力ディレクトリには2つのファイルが作成される。
  • tflite_graph.pb
  • tflite_graph.pbtxt

これをtoco(tflite_convert)で変換する。
input_arrays, output_arraysの名前に注意。また、input_shapesはSSDLite MobileNetV3は320, 320である。
$ tflite_convert --graph_def_file=$OUTPUT_DIR/tflite_graph.pb \
  --output_file=$OUTPUT_DIR/ssdlite_object_detection.tflite \
  --input_format=TENSORFLOW_GRAPHDEF \
  --output_format=TFLITE \
  --inference_type=FLOAT \
  --input_shapes=1,320,320,3 \
  --input_arrays=normalized_input_image_tensor \
  --output_arrays=raw_outputs/box_encodings,raw_outputs/class_predictions


モデルをMediaPipeのプロジェクトへの組み込み、ビルド


mediapipeのリポジトリをcloneして、作成したTF-Liteモデルをmediapipe/mediapipe/model配下にコピーする

$ git clone https://github.com/google/mediapipe.git
$ cp $OUTPUT_DIR/ssdlite_object_detection.tflite mediapipe/mediapipe/model/ssdlite_object_detection.tflite

ANDROID_NDK_HOME, ANDROID_HOMEを設定し、ビルド。
(ANDROID_NDK_HOME, ANDROID_HOMEはインストールした環境にあわせて変更)
$ export ANDROID_HOME=$HOME/Android/Sdk
$ export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk-bundle

CPU版
$ bazel build -c opt --config=android_arm64 mediapipe/examples/android/src/java/com/google/mediapipe/apps/objectdetectioncpu

GPU版
$ bazel build -c opt --config=android_arm64 mediapipe/examples/android/src/java/com/google/mediapipe/apps/objectdetectiongpu


Andoridへのインストール


あとはadbコマンドを使ってインストールする。
$ adb install bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/objectdetectiongpu/objectdetectiongpu.apk

メニューに Object Detection CPU / Object Detection GPUのアイコンがあるので、タップすればOK。
(再度、インストールする場合は、一度アンインストールしてから)。

2019年11月24日日曜日

Jetson NanoでTF-TRTを試す(Object detection)

目的


Object detectionのモデルについて、TF-TRT(TensorFlow integration with TensorRT)を使ってFP16に最適化したモデルを生成し、Jetson Nanoでどの程度最適化の効果ががあるのかを確認する。


動機


Jetson NanoでTF-TRTを試す(Image Classification)その2では、モデルの生成時、 Relu6(x )を relu(x) - relu(x - 6)に置き換えをやめることにより、処理時間が短縮されることを確認した。
また、2019年10月にSSDLite MobileNet V3のモデルが公開された。
このため、Object detectionのモデルについて、TF−TRTの変換および、効果を確認する。


まとめ

  • 前回と同様、Relu6の置き換えを行うことで推論の処理時間短縮になることを確認した。
  • 本家リポジトリが行っているGraphの書き換え(NonMaxSuppressionのCPU実行)が推論の処理時間の短縮になっていることを確認した。
  • Jetson Nanoでは、SSDLite MobileNet V3 Large、Smallは、精度(mAP)が同等のモデルと比べた場合、推論の処理時間が遅いことがわかった。
  • (個人的には)SSD MobileNet V1 FPNが動作することに驚いた。
    ターゲットにもよるがこれは使える可能性がある。


環境


2019.11.23時点の環境で確認。

  • JetPack 4.2.2 (r32.2.1)
  • TensorRT: 5.1.6
  • TensorFlow: 1.14.0+nv19.10
  • Python:3.6.8


モデルの作成


変換用のスクリプトは、本家よりForkしたリポジトリにアップしている。



今回は、pre-trained models(Tensorflow detection model zoo)をダウンロードしてFP16のモデルへの変換と、自前で学習したモデルの変換を行うようにした。

インストール方法については、本家と同様の手順である。
モデルの変換および、ベンチマークの際は、CUIモード&jetson_clocksで実行している。


TF-TRTモデルの変換


本家リポジトリでは、チェックポイントからGraphを作成する際に大きく2つ変更を行っている。これが最終的な推論の速度にも影響している。

  • Relu6(x )を relu(x) - relu(x - 6)に置き換え
    これは、前回と同様。
    本家のリポジトリでは置き換えが有効になっている。
    現在はRelu6のままTF-TRTモデルに変換したほうが推論の処理時間の短縮につながるため、置き換えをしない。
  • NonMaxSuppressionをCPUで実行するように書き換え
    "Optimize NVIDIA GPU performance for efficient model inference"に詳細が解説されているが、NonMaxSupperssionはGPUで実行するコストが高い。
    (GPUに対応していないOpeがCPUで実行されるため、CPU-GPU間でデータ転送を頻繁に行ってしまう。制御文が含まれるOpeはGPUでの実行が遅い。)
    本家のリポジトリではNonMaxSuppressionをCPUで実行するようにGraphを書き換えている。今回は書き換えを行う・行わないでの違いを確認する。

TF-TRTモデルに変換する際のTrtGraphConverterのパラメータは前回と同様である。
    converter = trt.TrtGraphConverter(
        input_graph_def=frozen_graph,
        nodes_blacklist=output_names, #output nodes
        max_batch_size=1,
        is_dynamic_op=False,
        max_workspace_size_bytes = 1 << 25,
        precision_mode=trt.TrtPrecisionMode.FP16,
        minimum_segment_size=50)



Pre-trained modelsの変換


リポジトリをClone後、"tf_trt_models/examples/detection"ディレクトリに移動して変換用スクリプトを実行する。

python3 convert.py --model="pre-trained model prefix" --output="path to output dir"

--modelで変換したいpre-trained modelを指定する。サポートしているpre-trained modelsは、こちらを参照。
--outputにTF-TRTで変換したFP16のモデルが出力される。モデルのファイル名は"model_frozen_16.pb"である。


自前で学習したモデルの変換


TensorFlow Object detection APIで学習したモデルを変換する。学習後、Exporting a trained model for inferenceに従って、モデルをエクスポートする。
エクスポート後のディレクトリ(model.ckpt.*、pipeline.configファイルを含む)をJetson Nanoにコピーする。

python3 convert.py --path="path to export model dir" --output="path to output dir"

--pathフラグでコピーしたディレクトリのパスを指定する。


ベンチマーク


ベンチマークスクリプトはここ
初回の推論と、2回目以降の推論100回の平均をベンチマークする。

なお、画像サイズはモデルのinputと合わせるように推論前にリサイズしている(推論の時間に含めてない)。

対象のモデルはpre-trained models(Tensorflow detection model zoo)で公開されているSSDのモデルとする。

  • ssd_mobilenet_v1_0.75_depth_coco
  • ssd_mobilenet_v1_coco
  • ssd_mobilenet_v1_fpn_coco
  • ssdlite_mobilenet_v2_coco 
  • ssd_mobilenet_v2_coco
  • ssd_mobilenet_v3_small_coco
  • ssd_mobilenet_v3_large_coco
  • ssd_inception_v2_coco
  • ssd_resnet_50_fpn_coco


モデルサイズ


前回でも判明していたが、モデルサイズは変換後(FP16)のほうがサイズが大きくなる。
なお、ssd_inception_v2、ssd_resnet_50_fpnは変換ができなかった(変換中にKilledとなってしまう)。おそらく、メモリが不足していると思われる。

Model
モデルサイズ [MB]
FP32FP16
ssd_mobilenet_v1_07.5_depth1926
ssd_mobilenet_v12842
ssd_mobilenet_v1_fpn130135
ssdlite_mobilenet_v21930
ssd_mobilenet_v267100
ssdlite_mobilenet_v3_small813
ssdlite_mobilenet_v3_large1321
ssd_inception_v2980
ssd_resnet_50_fpn_coco1290



Jetson Nanoでの初回の推論


変換前(FP32)、変換後(FP16)について、NonMaxSupperssionをCPUで実行する・市しないで比較した。
表・グラフ中で

  • FP32 nms_gpu
    変換前かつ、NonMaxSupperssionの書き換えを行わない
  • FP32nms_cpu
    変換前かつ、NonMaxSupperssionをCPU実行に書き換え
  • FP16 nms_gpu
    FP16に変換かつ、NonMaxSupperssionの書き換えを行わない
  • FP32nms_cpu
    FP16に変換かつ、NonMaxSupperssionをCPU実行に書き換え
である。

結果から、
  • FP32よりFP16のモデルのほうが処理時間が短くなっている。
  • 初回の推論ではNonMaxSupperssionの効果は確認できない。
ことがわかる。


Model
Inference time [ms]
FP32 nms_gpuFP32 nms_cpuFP16 nms_gpuFP16 nms_cpu
ssd_mobilenet_v1_0.75_depth30694301951042010076
ssd_mobilenet_v1377183852873279294
ssd_mobilenet_v1_fpn1023241052941863021492
ssd_mobilenet_v247367661181154516043
ssdlite_mobilenet_v282924145826152566360
ssdlite_mobilenet_v3_small246232439298458208
ssdlite_mobilenet_v3_large380773842550125431
ssd_inception_v20000
ssd_resnet_50_fpn0000

※ssd_inception_v2, ssd_resnet_50_fpnは実行時にKilledとなってしまう。

結果をグラフ化してみる。
ssdlite_mobilenet_v2のFP32 nms_gpuの場合、突出して処理時間がかかっているため、対数目盛とした。また、ssd_inception_v2, ssd_resnet_50_fpnは除く。



Jetson Nanoでの2回目以降の推論


2回目以降の推論の処理時間を比較する。

  • FP32よりFP16のモデルのほうが処理時間が短くなっている。
  • NonMaxSupperssionをCPU実行に書き換えたほうが処理時間が短くなっている(ssd_mobilenet_v1_fpnのFP32 は除く)。
  • ssdslite_mobilenet_v3_smallは、ssd_mobilenet_v1_0.75_depthよりも処理時間がかかり、ssd_mobilenet_v1とほぼ同等の処理時間である(ssd_mobilenet_v1_0.75_depthとはmAPがほぼ同等)。
  • ssdlite_mobilenet_v3_largeは、ssdlite_mobilenet_v2よりも処理時間がかかる(ssdlite_mobilenet_v2とはmAPがほぼ同等)。
ことが確認できる。

MobileNet V3の論文では、MobileNet V2より早い処理時間となっている。
もともと論文にもあるとおり、MobileNet V3はモバイルCPU向けであり、
  • TF-Liteモデルでのベンチマークであること。
    (6.1.2 Measurement setupを参照)
  • TF-LiteでH-swishのOpeを最適化していること。
    (6.1.2 Measurement setupおよび、Figure 9を参照)

から、GPU実行やTF-TRTのOpeの最適化がないことが影響していると考えられる(後述も参照。このため、今後Opeの最適化があれば十分早くなる可能性あり?)。ただ、これはあくまでも私感であり、間違っている可能性がある。

Model
Inference time [ms]
FP32 nms_gpuFP32 nms_cpuFP16 nms_gpuFP16 nms_cpu
ssd_mobilenet_v1_0.75_depth83545742
ssd_mobilenet_v199726348
ssd_mobilenet_v1_fpn8351146474460
ssd_mobilenet_v21031836955
ssdlite_mobilenet_v216066757252
ssdlite_mobilenet_v3_small119599647
ssdlite_mobilenet_v3_large1298310964
ssd_inception_v20000
ssd_resnet_50_fpn0000

※ssd_inception_v2, ssd_resnet_50_fpnは実行時にKilledとなってしまう。

結果をグラフ化してみる。
ssdlite_mobilenet_v2のFP32 nms_gpuの場合、突出して処理時間がかかっているため、対数目盛とした。また、ssd_inception_v2, ssd_resnet_50_fpnは除く。

もう少しわかりやすいように、ssdlite_mobilenet_v2のFP32 nms_gpuを除いたものも掲載する。

ssdlite_mobilenet_v2のFP32 nms_gpuの処理時間をを除いた


問題点・不明な事象...


今回のベンチマークを行うにあたり、2つのよくわからない事象にあたった。。。書き残しておく(これは筆者の手順ミスの可能性もあるので注意)。


SSDLite MobileNet v3 LargeのPre-trained Modelで検出ができない


これは、Jetson NanoやTF-TRTの問題ではない。
Pre-trainedのモデル(ssd_mobilenet_v3_large_coco)のチェックポイントからモデルをfreeze graph(エクスポート)するしたもを利用するとなぜかオブジェクトを検出しない。。。
ssd_mobilenet_v3_small_cocoはうまくいく(オブジェクトを検出する)。
エクスポートする手順は変わっていないはずなのだが?


MobileNet V3 LargeとSmallの違いはネットワークぐらいで、使っているOpeは同じだと思っていたので、理由がさっぱりわからない。。。

なお、Google colabで再現するnotebookを作成して試してみても再現する(TensorFlowのバージョンは1.12, 1.14, 1.15で確認)。


自分で再学習した場合は、オブジェクトを検出するようになる。
再学習したチェックポイントは、一応、公開しておく(ただし、十分学習できていない。また、都合により削除する場合あるので注意)。


TF-TRTで変換したモデルが再現できない(SSDLite MobileNet V3 Large、Small)


今回、SSDLite MobileNet V3 Large、SmallのTF-TRTモデルの変換を行っていた中で、一度だけFP16のモデルの変換に成功?したのか、推論の処理時間が早くなったモデルが生成できた。

下の表のFP16 nms_cpu(最速?)の列は、何故か推論の処理時間が早くなったモデルである。FP16 nms_cpu(ベンチ)の列は、ベンチマークで記載したものである。


Model
Input size
Inference time [ms]
FP16 nms_cpu
(最速?)
FP16 nms_cpu
(ベンチ)
ssdlite_mobilenet_v3_small320×3203347
ssdlite_mobilenet_v3_large320×3205164


  • ssdlite_mobilenet_v3_smallはmAPが同等のssd_mobilenet_v1よりも早い。
  • ssdlite_mobilenet_v3_largeはssdlite_mobilenet_v2とほぼ同じ。

その後、何度か再現(モデルの変換)を試みたが再現できなかった。
当初、モデルの変換の際に誤って違うモデルを選んだのかと思ったのだが、モデルの構造をみると、

TRTEngineOpのinput_shapeが320×320となっている。MobileNet V3以外でinputが320×320はないため間違いとは思えない。。。

TrtGraphConverterのパラメータをいじっても再現しない。もし、これが再現できるのであれば、SSDLite MobileNet V3は、推論の処理時間とmAPをみても十分使えるものになると考えられる。

こssdlite_mobilenet_v3_smallのモデルについて公開してこうと思う(筆者の都合により削除する場合あるので注意)。


最後に


TF-TRTについてObject detectionモデルの変換についてまとめてみた。とりあえず、Image classification、Object detectionのモデルについてはやってみた。今後は、新しいモデルの登場、TF-TRTのバージョンアップがあればやってみようと思う。