Tensorflow Lite v1.11.0 を自力でカスタマイズしてPython API にMultiThread機能を追加→オフィシャルの２．５倍にパフォーマンスアップ

**Bazel_bin**　[![GitHub stars](https://img.shields.io/github/stars/PINTO0309/Bazel_bin.svg?style=social&label=Stars)](https://github.com/PINTO0309/Bazel_bin)
<a href="https://github.com/PINTO0309/Bazel_bin"><img src="https://qiita-image-store.s3.amazonaws.com/0/194769/6ea145b9-b72f-dacb-252c-1d34ca733dc4.png" width=50%></a>
**Tensorflow-bin**　[![GitHub stars](https://img.shields.io/github/stars/PINTO0309/Tensorflow-bin.svg?style=social&label=Stars)](https://github.com/PINTO0309/Tensorflow-bin)
<a href="https://github.com/PINTO0309/Tensorflow-bin"><img src="https://qiita-image-store.s3.amazonaws.com/0/194769/5bb32ad2-904e-1a98-7239-88ffc0d03738.png" width=50%></a>

# １．はじめに
今回も、たぶん誰もチャレンジしないであろう不毛なことをします。
5G通信が実用化したらエッジで頑張る意味なんてきっとほとんど無くなるんでしょうね。
ただ、毎月固定費に近いかたちで支払わされる高額な通信費を、１円でも削りたいと思う方は少なからずいらっしゃるのではないでしょうか。

今回は、 Tenforflow Lite v1.11.0 の Python API をカスタマイズして MultiThread を実装し、RaspberryPi3のCPU単体での推論性能アップを目指します。
オフィシャルなエンハンスではありませんので、使用にあたっては自己責任でお願いします。
日本では誰にも見向きもされないゴミ扱いの代物です。
ちなみに、Tensorflow v1.12.0 は、RaspberryPi3のメモリとSWAP領域をフルに使い果たしてもリソースが足りませんでしたので断念しました。 **`matrix_square_root_op.cc`** のビルドだけで ３GB (MEM 1GB+SWAP 2GB) 以上のリソースを食い尽くしてビルドプロセスがアベンドします。

# ２．環境
下記リンク先にビルド済みのWheelを配置しました。
BazelとTensorflowのバージョンの組み合わせはほぼ決まっています。
むやみに、新しいBazel や 古いBazel を使用すると、十中八九ビルドに失敗します。

- **[Tensorflow v1.11.0](https://github.com/PINTO0309/Tensorflow-bin) (armv7l ネイティブビルド済み)**
- **[Tenforflow Lite v1.11.0](https://github.com/PINTO0309/Tensorflow-bin) (armv7l ネイティブビルド済み + マルチスレッド対応)**
- **[Bazel 0.17.2](https://github.com/PINTO0309/Bazel_bin) ネイティブビルド済み**
- RaspberryPi3 Model B+
- Python 3.5

# ３．ビルド作業
RaspberryPi3 Model B+ 上で Tensorflow v1.11.0 をネイティブビルドします。
これは、Tensorflow公式の pip パッケージがバグっていたり、Tensorflow Lite のミニマムビルドのオフィシャルチュートリアルがバグっていたり、クロスコンパイルのツールチェーンがバグっていたり、何故か MultiThread に対応していなくてパフォーマンスが中途半端だったり、などの各種問題をクリアするために実施しています。
今回もクロスコンパイルは行いません。 ラズパイ上での男気ネイティブビルドです。

## ３−１．Bazelのネイティブビルド
ビルド済みのバイナリは **[コチラ](https://github.com/PINTO0309/Bazel_bin)** に置きました。 自由に持ち去ってください。
下記は後学のための覚書ですので、無理して実施する必要はありません。

```bash:bazelのRaspberryPi3上でのネイティブビルド_スタンドアロン実行バイナリの生成
$ sudo apt update;sudo apt upgrade
$ sudo apt install -y build-essential openjdk-8-jdk pkg-config zip g++ zlib1g-dev unzip
$ cd ~
$ mkdir bazel;cd bazel
$ wget https://github.com/bazelbuild/bazel/releases/download/0.17.2/bazel-0.17.2-dist.zip
$ unzip bazel-0.17.2-dist.zip
$ rm bazel-0.17.2-dist.zip

#==== Only RaspberryPi ============================================================
$ nano bazel/scripts/bootstrap/compile.sh

#################################################################################
  run "${JAVAC}" -classpath "${classpath}" -sourcepath "${sourcepath}" \
      -d "${output}/classes" -source "$JAVA_VERSION" -target "$JAVA_VERSION" \
      -encoding UTF-8 ${BAZEL_JAVAC_OPTS} "@${paramfile}"
#################################################################################
↓
#################################################################################
  run "${JAVAC}" -classpath "${classpath}" -sourcepath "${sourcepath}" \
      -d "${output}/classes" -source "$JAVA_VERSION" -target "$JAVA_VERSION" \
      -encoding UTF-8 ${BAZEL_JAVAC_OPTS} "@${paramfile}" -J-Xmx500M
#################################################################################
#==== Only RaspberryPi ============================================================

$ sudo bash ./compile.sh   #<--- Execute it directly under the bazel folder
$ sudo cp output/bazel /usr/local/bin   #<--- Always execute after completion of build
```

## ３−２．Tensorflow Lite v1.11.0 のプログラムを改造してマルチスレッド化
現状のTensorflow LiteのPython APIはMultiThreadに対応していません。
RaspberryPi3は４コアを装備しているにもかかわらず、 Tensorflow Lite は **`１スレッド／１コア`** で超低速に動作してしまいます。
そこで、公式のTensorflowのソースプログラムを改造して **`４スレッド／４コア`** で動作できるようにします。
といっても、既に v1.13.0 あるいは v1.14.0 以降に実装されるであろう機能ですし、Pull Request に挙がっている内容ですので、公式にマージされる前に自前で先行実装してしまおう、というだけです。
現時点の v1.12.0 masterブランチを同じように改造することも簡単です。 ただ、各種ファイルのパスから「contrib」を抜く必要があります。
コンパイルに数十時間かかりますので、せっかちな方は **[コチラ - tensorflow-1.11.0-cp35-cp35m-linux_armv7l_jemalloc_multithread.whl](https://github.com/PINTO0309/Tensorflow-bin/blob/master/tensorflow-1.11.0-cp35-cp35m-linux_armv7l_jemalloc_multithread.whl)** をダウンロードしてご利用ください。
ダウンロード後に **`tensorflow-1.11.0-cp35-cp35m-linux_armv7l.whl`** へリネームし、 **`sudo -H pip3 install tensorflow-1.11.0-cp35-cp35m-linux_armv7l.whl`** でインストールできます。

```python:tensorflow/contrib/lite/examples/python/label_image.py
import argparse
import numpy as np
import time

from PIL import Image

from tensorflow.contrib.lite.python import interpreter as interpreter_wrapper
def load_labels(filename):
  my_labels = []
  input_file = open(filename, 'r')
  for l in input_file:
    my_labels.append(l.strip())
  return my_labels
if __name__ == "__main__":
  floating_model = False
  parser = argparse.ArgumentParser()
  parser.add_argument("-i", "--image", default="/tmp/grace_hopper.bmp", \
    help="image to be classified")
  parser.add_argument("-m", "--model_file", \
    default="/tmp/mobilenet_v1_1.0_224_quant.tflite", \
    help=".tflite model to be executed")
  parser.add_argument("-l", "--label_file", default="/tmp/labels.txt", \
    help="name of file containing labels")
  parser.add_argument("--input_mean", default=127.5, help="input_mean")
  parser.add_argument("--input_std", default=127.5, \
    help="input standard deviation")
  parser.add_argument("--num_threads", default=1, help="number of threads")
  args = parser.parse_args()

  interpreter = interpreter_wrapper.Interpreter(model_path=args.model_file)
  interpreter.allocate_tensors()
  input_details = interpreter.get_input_details()
  output_details = interpreter.get_output_details()
  # check the type of the input tensor
  if input_details[0]['dtype'] == np.float32:
    floating_model = True
  # NxHxWxC, H:1, W:2
  height = input_details[0]['shape'][1]
  width = input_details[0]['shape'][2]
  img = Image.open(args.image)
  img = img.resize((width, height))
  # add N dim
  input_data = np.expand_dims(img, axis=0)
  if floating_model:
    input_data = (np.float32(input_data) - args.input_mean) / args.input_std

  interpreter.set_num_threads(int(args.num_threads))
  interpreter.set_tensor(input_details[0]['index'], input_data)

  start_time = time.time()
  interpreter.invoke()
  stop_time = time.time()

  output_data = interpreter.get_tensor(output_details[0]['index'])
  results = np.squeeze(output_data)
  top_k = results.argsort()[-5:][::-1]
  labels = load_labels(args.label_file)
  for i in top_k:
    if floating_model:
      print('{0:08.6f}'.format(float(results[i]))+":", labels[i])
    else:
      print('{0:08.6f}'.format(float(results[i]/255.0))+":", labels[i])

  print("time: ", stop_time - start_time)
```

```python:tensorflow/contrib/lite/python/interpreter.py
# 最終行に下記２行を追加
  def set_num_threads(self, i):
    return self._interpreter.SetNumThreads(i)
```

```cpp:tensorflow/contrib/lite/python/interpreter_wrapper/interpreter_wrapper.cc
// 最終行近辺を下記のとおり修正
PyObject* InterpreterWrapper::ResetVariableTensors() {
  TFLITE_PY_ENSURE_VALID_INTERPRETER();
  TFLITE_PY_CHECK(interpreter_->ResetVariableTensors());
  Py_RETURN_NONE;
}

PyObject* InterpreterWrapper::SetNumThreads(int i) {
  interpreter_->SetNumThreads(i);
  Py_RETURN_NONE;
}

}  // namespace interpreter_wrapper
}  // namespace tflite
```

```cpp:tensorflow/contrib/lite/python/interpreter_wrapper/interpreter_wrapper.h
// ロジックの中盤あたりを下記のとおり修正
  // should be the interpreter object providing the memory.
  PyObject* tensor(PyObject* base_object, int i);

  PyObject* SetNumThreads(int i);

 private:
  // Helper function to construct an `InterpreterWrapper` object.
  // It only returns InterpreterWrapper if it can construct an `Interpreter`.
```

## ３−３．Tensorflow v1.11.0のネイティブビルド
例によって例のごとく、ネイティブビルドのため ２７時間以上 掛かります。 クロスコンパイルでは失敗します。
今回は改善要素として、ZRAMというツールを使用してRAMディスクを作成し、メモリ消費量を圧縮しつつ作業を行います。
メリットは、SDカードへのSWAP回数が大幅に減ることで、ビルド時間の大幅な短縮が見込まれます。
実測で全体の４０％ほどメモリ消費を抑えられるようです。
なお、Raspbianは 32bit カーネルですので、SWAP領域を 2048MB より大きく設定しても利用できません。でした。

```bash:TensorflowからCloneしてConfigure
$ sudo nano /etc/dphys-swapfile
CONF_SWAPFILE=2048
CONF_MAXSWAP=2048

$ sudo systemctl stop dphys-swapfile
$ sudo systemctl start dphys-swapfile

$ wget https://github.com/PINTO0309/Tensorflow-bin/raw/master/zram.sh
$ chmod 755 zram.sh
$ sudo mv zram.sh /etc/init.d/
$ sudo update-rc.d zram.sh defaults
$ sudo reboot

$ sudo apt-get install -y \
libhdf5-dev libc-ares-dev python3-pip \
python3-scipy openmpi-bin libopenmpi-dev

$ sudo -H pip3 install pip --upgrade
$ sudo -H pip3 install keras_applications==1.0.7 --no-deps
$ sudo -H pip3 install keras_preprocessing==1.0.9 --no-deps
$ sudo -H pip3 install h5py==2.9.0
$ sudo -H pip3 install -U --user six numpy wheel mock

$ cd ~
$ git clone -b v1.11.0 https://github.com/tensorflow/tensorflow.git
$ cd tensorflow
$ git checkout -b v1.11.0

$ ./configure
WARNING: --batch mode is deprecated. Please instead explicitly shut down your Bazel server using the command "bazel shutdown".
You have bazel 0.17.2- (@non-git) installed.
Please specify the location of python. [Default is /usr/bin/python]: /usr/bin/python3


Found possible Python library paths:
  /usr/local/lib
  /usr/lib/python3/dist-packages
  /usr/local/lib/python3.5/dist-packages
  /opt/movidius/caffe/python
Please input the desired Python library path to use.  Default is [/usr/local/lib] /usr/local/lib/python3.5/dist-packages

Do you wish to build TensorFlow with jemalloc as malloc support? [Y/n]: y
No jemalloc as malloc support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Google Cloud Platform support? [Y/n]: n
No Google Cloud Platform support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Hadoop File System support? [Y/n]: n
No Hadoop File System support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Amazon AWS Platform support? [Y/n]: n
No Amazon AWS Platform support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Apache Kafka Platform support? [Y/n]: n
No Apache Kafka Platform support will be enabled for TensorFlow.

Do you wish to build TensorFlow with XLA JIT support? [y/N]: n
No XLA JIT support will be enabled for TensorFlow.

Do you wish to build TensorFlow with GDR support? [y/N]: n
No GDR support will be enabled for TensorFlow.

Do you wish to build TensorFlow with VERBS support? [y/N]: n
No VERBS support will be enabled for TensorFlow.

Do you wish to build TensorFlow with nGraph support? [y/N]: n
No nGraph support will be enabled for TensorFlow.

Do you wish to build TensorFlow with OpenCL SYCL support? [y/N]: n
No OpenCL SYCL support will be enabled for TensorFlow.

Do you wish to build TensorFlow with CUDA support? [y/N]: n
No CUDA support will be enabled for TensorFlow.

Do you wish to download a fresh release of clang? (Experimental) [y/N]: n
Clang will not be downloaded.

Do you wish to build TensorFlow with MPI support? [y/N]: n
No MPI support will be enabled for TensorFlow.

Please specify optimization flags to use during compilation when bazel option "--config=opt" is specified [Default is -march=native]: 


Would you like to interactively configure ./WORKSPACE for Android builds? [y/N]: n
Not configuring the WORKSPACE for Android builds.

Preconfigured Bazel build configs. You can use any of the below by adding "--config=<>" to your build command. See tools/bazel.rc for more details.
    --config=mkl            # Build with MKL support.
    --config=monolithic     # Config for mostly static monolithic build.
Configuration finished
```

```bash:Tensorflow_v1.11.0をRaspberryPi3上でネイティブビルドするためのコマンド
$ sudo bazel build --config opt --local_resources 1024.0,0.5,0.5 \
--copt=-mfpu=neon-vfpv4 \
--copt=-ftree-vectorize \
--copt=-funsafe-math-optimizations \
--copt=-ftree-loop-vectorize \
--copt=-fomit-frame-pointer \
--copt=-DRASPBERRY_PI \
--host_copt=-DRASPBERRY_PI \
//tensorflow/tools/pip_package:build_pip_package
```

## ３−４．Tensorflow v1.11.0のインストール
wheel ファイルを自力で生成して、pipコマンドでTensorflowをインストールします。

```bash:wheelファイルの生成とpipコマンドによるインストール
$ su -s
# ./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
# exit
$ sudo pip3 install /tmp/tensorflow_pkg/tensorflow-1.11.0-cp35-cp35m-linux_armv7l.whl
$ sudo reboot
```

# ４．テスト
MobileNet V1 を使用してパフォーマンスがどれほど改善されるかを検証します。
起動スレッド数ごとの性能改善結果は下記のとおりです。
**`4 Thread`** で **`約2.5倍`** にパフォーマンスアップしました。
**`約60%`** の性能アップです。
ARMのCPU単体で **`6 FPS`** ほどの性能が出せるようになりました。

```bash:テストデータの生成_.tfliteファイルとlabels.txtの生成
$ cd ~;mkdir test
$ curl https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/examples/label_image/testdata/grace_hopper.bmp > ~/test/grace_hopper.bmp
$ curl https://storage.googleapis.com/download.tensorflow.org/models/mobilenet_v1_1.0_224_frozen.tgz | tar xzv -C ~/test mobilenet_v1_1.0_224/labels.txt
$ mv ~/test/mobilenet_v1_1.0_224/labels.txt ~/test/
$ curl http://download.tensorflow.org/models/mobilenet_v1_2018_02_22/mobilenet_v1_1.0_224_quant.tgz | tar xzv -C ~/test
$ cp tensorflow/tensorflow/contrib/lite/examples/python/label_image.py ~/test
```

```bash:mobilenet_v1のテスト_threads_x1
$ cd ~/test
$ python3 label_image.py \
--num_threads 1 \
--image grace_hopper.bmp \
--model_file mobilenet_v1_1.0_224_quant.tflite \
--label_file labels.txt

0.415686: 653:military uniform
0.352941: 907:Windsor tie
0.058824: 668:mortarboard
0.035294: 458:bow tie, bow-tie, bowtie
0.035294: 835:suit, suit of clothes
time:  0.4152982234954834
```

```bash:mobilenet_v1のテスト_threads_x2
$ cd ~/test
$ python3 label_image.py \
--num_threads 2 \
--image grace_hopper.bmp \
--model_file mobilenet_v1_1.0_224_quant.tflite \
--label_file labels.txt

0.415686: 653:military uniform
0.352941: 907:Windsor tie
0.058824: 668:mortarboard
0.035294: 458:bow tie, bow-tie, bowtie
0.035294: 835:suit, suit of clothes
time:  0.2455289363861084
```

```bash:mobilenet_v1のテスト_threads_x3
$ cd ~/test
$ python3 label_image.py \
--num_threads 3 \
--image grace_hopper.bmp \
--model_file mobilenet_v1_1.0_224_quant.tflite \
--label_file labels.txt

0.415686: 653:military uniform
0.352941: 907:Windsor tie
0.058824: 668:mortarboard
0.035294: 458:bow tie, bow-tie, bowtie
0.035294: 835:suit, suit of clothes
time:  0.19364309310913086
```

```bash:mobilenet_v1のテスト_threads_x4
$ cd ~/test
$ python3 label_image.py \
--num_threads 4 \
--image grace_hopper.bmp \
--model_file mobilenet_v1_1.0_224_quant.tflite \
--label_file labels.txt

0.415686: 653:military uniform
0.352941: 907:Windsor tie
0.058824: 668:mortarboard
0.035294: 458:bow tie, bow-tie, bowtie
0.035294: 835:suit, suit of clothes
time:  0.1647195816040039
```

# ５．おわりに
Pythonの GIL (グローバルインタープリターロック) の影響により、単純に４倍速にはなりませんでしたが、十分なパフォーマンスアップをすることができました。 めでたしめでたし。

# ６．参考リンク

**Tensorflow Lite のサンプルモデル一覧**
**https://www.tensorflow.org/lite/models**

**テスト用スクリプト**
**https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/examples/python/label_image.md**