tensorflow for python3.6 on Ubuntu 20.04 on raspberry pi 4のビルド

# 概要

- Python3.6 on Ubuntu20.04 on raspberry pi 4では`pip install tensorflow`ができませんでした（2020/05/05時点）。
- そこでtensorflow for python3.6 on Ubuntu 20.04 on raspberry pi 4を自前でビルドしました。Cross build toolを用いてパブリッククラウド上でビルドし、生成したパッケージをraspberry piにインストールしました。簡単なサンプルが動作することを確認しました。

# Ubuntu20.04 on raspberry pi 4の悲しみ
Ubuntu20.04 on raspberry pi 4はOSとcpuアーキテクチャが新し目なためか、著名なライブラリでもビルド済みのパッケージが公開されていないことがまれによくあります。Tensorflowもその１つのようで、`pip install tensorflow`からの`import tensorflow`すると`ImportError: .../site-packages/tensorflow/python/_pywrap_tensorflow.so: wrong ELF class: ELFCLASS32`って怒られました（2020/05/05時点）。[公式のgithubリポジトリ](https://github.com/freedomtan/tensorflow/tree/369e2d8f5d91e29dc6c970283052c992c61d75ac)を見るとraspberry pi 0, 1, 2, 3向けにはあるようですが4はないです。そもそも0,1,2,3もビルド失敗してるっぽいですが…

Tensorflowのサブセット？のtfliteというものはpip installできる感じがあります（未検証）が使いませんでした。tfliteはフルセットのtensorflowとはパッケージが違うため、既存プログラムのimport部分を書き換える必要があったこと、フルセットとどのような差分があるのかtensorflow素人の私にはわからなかったことが理由です。

そこでTensorflowの自前ビルドを試みました。ビルドには以下の２つの方法があります。
- Native build: Raspberry pi 4ご自身でビルドいただく。
- Cross build: 別マシンでraspberry pi 4向けにビルドいただく。

Native buildは失敗し、cross buildは成功しました。Native buildはメモリとスワップがいっぱいになって止まってしまいました。MATEとかvscodeとか立ち上げてたのが良くなかったかもしれません。

# Cross build
[Tensorflow公式](https://www.tensorflow.org/install/source_rpi?hl=ja)でcross buildが推奨されています。（私が実体験したように）非力なCPUでのnative buildは時間がかかるためです。Tensorflowはdockerベースのcross build toolを提供しており、処理能力の高いマシンで別マシン向けのパッケージを生成できます。私は自宅には非力マシンしか持っていないため、パブリッククラウドの無料枠を使いました。

しかし、公式のソースコードそのままではraspberry pi 4 & python3.6向けにビルド出来ませんでした。Tensorflowは（よくわかっていませんが）C/C++とpythonで構成されているので、cross buildするためにはC/C++コンパイラとpythonバージョンをを動作させたい環境に合わせる必要があります。Ubuntu 20.04 on raspberry pi 4のCPUはarmv8(aarch64)、今回動かしたいpythonは3.6です。

そこで、armv8(aarch64) & python3.6に対応するためソースコードを修正しました。長くなったので修正の概要だけ記述します。まず、armv8(aarch64)に対応するため以下のように修正しました。

1. third_party/remote_config/remote_platform_configure.bzl
[こちら](https://github.com/freedomtan/tensorflow/commit/369e2d8f5d91e29dc6c970283052c992c61d75ac)を参考に、third_party/remote_config/remote_platform_configure.bzlを直しました。PRが出てるのでそのうち公式にも反映されそうです。

2. tensorflow/tools/ci_build/pi/build_raspberry_pi.sh
ビルド設定をarmv8(aarch64)に変えます。[このあたり](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0774j/index.html)を参考に、PI_COPTSの`--copt=march`や`--copt=mfpu`を書き換えました。

3. tensorflow/workspace.bzl
ビルド中に`external/eigen_archive/unsupported/Eigen/CXX11/../../../Eigen/src/Core/GenericPacketMath.h:145:10: error: invalid static_cast from type 'const __vector(4) int' to type '__vector(16) signed char'`というエラーが出たので、[こちら](https://github.com/tensorflow/tensorflow/issues/38077)を参考に tensorflow/workspace.bzlを修正しました。修正の意味はよくわかりませんが古いEigenに戻すようです。ARM用のC/C++コンパイラだと許されないキャストなんでしょうか…

次に、python3.6対応の修正です。Cross buildはubuntu16.04ベースです。なので、公式のままだとpython3.5向けのパッケージができるようです。Ubuntuのpythonバージョンを変えるのはおすすめ出来ないらしいので、pyenvで3.6に切り替えてごまかしました。

1. tensotflow/tools/ci_build/Dockerfile.pi-python3
Dockerfileのpip3をpyenv上で動かすよう修正しました。docker的には駄目なやり方な気がしますが…、最初にpyenvを入れて`pyenv install 3.6.x`しました。さらに`RUN install/install_hoge.sh`コマンドを全部bash動作にして、`RUN source ~/.bashrc && install/install_hige.sh`のようにしてpyenv上で動くようにしました。

2. tensorflow/tools/ci_build/install/install_python3.5_pip_packages.sh
python3.5を個別インストールするようになっていますが、1でpyenvで3.6を入れたのでコメントアウトしました。

最後に、armv8(aarch64)とpython3.6の両方に関わる修正です。tensorflow/tools/ci_build/install/install_pi_python37_toolchain.shです。追加リポジトリがraspberry pi 0,1,2,3を前提としてarmhfになっているのでarm64にしました。また、python-devが3.7なので3.6を入れるように修正しました。

## その他
必須ではないですがリモートでビルドしやすいようにtensorflow/tools/cu_build/ci_build.shを修正しました。公式では'docker --rm'なので、間違ってCtrl-Cしちゃったり、sshが切れちゃったりしたときにコンテナも終了してしまいます。そこで、'docker run -d'でコンテナをデーモンモードで起動させて、'docker logs -f'で出力して、Ctrl-Cしてもsshが切れてもコンテナが動き続けるようにしました。

# ビルド
以下でビルドしました。ci_build.shは、第１引数で指定されたDockerfileをビルドし、第２引数で指定したスクリプトをentrypointにしてコンテナを起動します。

```console
CI_DOCKER_EXTRA_PARAMS="-e CI_BUILD_PYTHON=python3 -e CROSSTOOL_PYTHON_INCLUDE_PATH=/usr/include/python3.6" \
        tensorflow/tools/ci_build/ci_build.sh PI-PYTHON36 \
        tensorflow/tools/ci_build/pi/build_raspberry_pi.sh
```

私の環境は4コアCPU、16GBメモリで、8時間くらいかかりました。進捗は、[ 完了したaction数 / ビルド全体のaction総数 ]という形で出ますが、ビルド進行中も総数がじわじわ増えるので完了までの目安にはなりませんでした。最終的なaction総数は22,699で、.whlのサイズは143MBでした。以下がビルド結果の抜粋です。

```
...
INFO: Elapsed time: 30729.504s, Critical Path: 475.54s
INFO: 17501 processes: 17501 local.
INFO: Build completed successfully, 22699 total actions
```

# 動作確認
出来上がった.whlファイルをraspberry pi 4にダウンロードして`pip insall`します。とりあえずインポートできるか確認しました。

```console
$ python -c "import sys; import tensorflow as tf; print(sys.version_info); print(tf.__version__)"
sys.version_info(major=3, minor=6, micro=8, releaselevel='final', serial=0)
2.1.0
```

インポートに２０秒位かかりますが動いていそうです。

バージョンが読めただけだと本当に動くのか怪しいので、もう少し踏み込んだ動作確認をしました。[Tensorflow公式のquickstart](https://www.tensorflow.org/tutorials/quickstart/beginner?hl=ja)を試しました。コードはそっくりそのままなので割愛します。

```console
$ python tf-test.py
1875/1875 [==============================] - 16s 8ms/step - loss: 0.0742 - accuracy: 0.9760
.....: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 31360000 exceeds 10% of free system memory.
313/313 - 1s - loss: 0.0771 - accuracy: 0.9776
```

公式と同じ結果になりました！