TensorFlow C++(libtensorflow_cc.so) で推論アプリを CMake でビルドする(r1.8 対象)

漢なら, TensorFlow C++ API で推論やりたいですよね! やりましょう!

現時点(2018 年 5 月 28 日)の最新版は `r1.8` です.

TensorFlow Lite というのも出始めてきていますが, 対応する Op がまだまだという問題がありますので, 
本体の `libtensorflow` を使うことにします.

昨今では学習したモデルをモバイルで動かしたい需要が増えてきていますが, 流石に python をモバイル上で動かすというのは面倒なので C++ でコアを書き, 周りは Kotlin, Swift, ReactNative あたりで叩きたいですよね.

ここでの目的は TensorFlow C++ API(CPU 版) を PC Linux(Ubuntu 16.04, x86)で動かすことにより, モバイルでの Tensorflow C++ API での推論の開発とデバッグをやりやすくすることです.

`libtensorflow` をビルドする方法のは, 主に以下の三つがあります.

* bazel : 標準のビルド. しかし bazel を使っているのは TensorFlow くらいであるし, bazel 内部は Java が動いていたりしてなるべく使いたくない...
* cmake : Contrib で提供. Windows 向けで, PC Linux 環境向けには用意されていない. 一応ビルドはできるが, PC 環境だと `libtensorflow` を作ってくれないので自前で `.o` をリストアップしてユーザプロジェクトでリンクしないといけなくて大変. また後述するリンクの問題の対応が入っていないため実質使い物にならない.
* makefile : Contrib で提供. モバイル(Android, iOS, RaspberryPi)向けで, PC Linux(x86) 環境向けには用意されていない. 一応ビルドはできるが, これも後述する問題により Op のリストを自前でまとめないとならず大変.

従って現状 Bazel 一択です.

**そして, いろいろ試したところ Bazel で `libtensorflow_cc` を `-config monolithic` でビルドが唯一の解です**

Docker で `libtensorflow` をビルドしてくれるのもあります. こちらも検討したいですね.

https://github.com/FloopCZ/tensorflow_cc

## libtensorflow リンク時の問題


自前のアプリのビルドには Bazel は使いたくありませんから,  CMake で書き, `libtensorflow` をリンクするようにしたいですね.

しかし, とりあえず bazel や makefile でビルドした `libtensorflow.so` をリンクして動かしても, Session の初期化時に, `No session factory registered for the given session options` で失敗とよくわからないエラーが出たり,  Op が無いとか言われてしまいます...:dizzy_face:

原因はこれです.

contrib/makefile: No session factory registered for the given session options #3308
https://github.com/tensorflow/tensorflow/issues/3308

TensorFlow では, ライブラリのロード時にグローバルコンストラクタで Op を初期化みたいな変な C++ テクニックを使っています.
**こういう変な C++ テクニックつかうの, 本当にやめてほしいですね!** :no_good:

少なくとも, Linux 環境では, makefile ベースでビルドした場合は, この問題を解決するために, リンク時にその辺りの情報を保持するために `-Wl,--allow-multiple-definition -Wl,--whole-archive` を指定する必要があります!

さらに, `-fPIC` or `-fPIE` で **position independent にしてコンパイルすると確実かもしれません!**

CMake ですと

```
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
```

で有効になります.

上の makefile での問題は, Op を全部入れるとライブラリが肥大化するので, 選別した Op だけ使うようにしていて, 必要なら Op を追加してね, とあります. 従って makefile で作った `libtensorflow-core.a` だと実行時に Op が足りないというエラーも出ますので注意ください. これを直すのも面倒ですね.

## Bazel で libtensorflow_cc をビルドする.

`libstensorflow_cc.so` を **monolithic 有効でビルドします**. monolithic 有効にしないとアプリとリンクしたときにうまく動きません.

```
$ bazel build --config opt --config monolithic tensorflow:libtensorflow_cc.so
```

## アプリ側での CMake 設定.

`TENSORFLOW_DIR`(git repo ディレクトリ)と, `TENSORFLOW_EXTERNAL_DIR`(third party パッケージ), `TENSORFLOW_BUILD_DIR`(ビルドディレクトリ)を cmake のパラメータとして設定できるようにします.

bazel だと, external パッケージの場所は, repo ディレクトリ名が `tensorflow` ではなく別の名前, たとべあ `tensorflow-cmake` だと `bazel-tensorflow-cmake` と repo 名がプレフィックスとして付くので注意ください.

以下のようにして bootstrap するのを想定します.

```
# source code directory of tensorflow
TF_DIR=`pwd`/../tensorflow-cmake

# external source code directory of tensorflow
TF_EXTERNAL_DIR=`pwd`/../bazel-tensorflow-cmake

# bazel build directory of tensorflow where `libtensorflow.so` exists.
# Please specify absolute path, otherwise cmake cannot find lib**.a
TF_BUILD_DIR=`pwd`/../tensorflow-cmake/bazel-bin/tensorflow

cmake -DTENSORFLOW_DIR=${TF_DIR} \
      -DTENSORFLOW_EXTERNAL_DIR=${TF_EXTERNAL_DIR} \
      -DTENSORFLOW_BUILD_DIR=${TF_BUILD_DIR} \
      -DSANITIZE_ADDRESS=On \
      -Bbuild \
      -H.
```

### include の設定

eigen, protocol buffers あたりのヘッダーパスを設定します.

`array_ops.h` などの一部ファイルは libtensorflow のビルド時に作成されますので, それらへのパスを通します. bazel ビルドの場合は `bazel-genfiles` にあります.

`tensorflow/third_party` は実体の無いヘッダーがあるだけなので, ここにパスを通してもヘッダが見つからない(or ヘッダファイルが循環している)エラーがでるので注意です!


```
target_include_directories(myapp
    PUBLIC ${TENSORFLOW_DIR}

    # for array_ops.h
    PUBLIC ${TENSORFLOW_DIR}/bazel-genfiles

    # headers for external packages
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/protobuf_archive/src
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/eigen_archive
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/nsync/public

    # this project
    PUBLIC ${CMAKE_SOURCE_DIR}/src
)
```


### 補足: リンカの設定

Bazel で `monolithic` 有効の場合は以下のリンカ設定は不要のようですが, 参考のために記録しておきます.

Makefile ベース, もしくは `monolithic` オフで bazel でビルドした場合は, `libtensorflow**.so` をリンクする前に `-Wl,--allow-multiple-definition -Wl,--whole-archive` をつけます.

```
# Fix for "No session factory registered for the given session" error in the runtime.
if (UNIX AND NOT APPLE)
  # https://github.com/tensorflow/tensorflow/issues/3308
  TARGET_LINK_LIBRARIES(myapp -Wl,--allow-multiple-definition -Wl,--whole-archive "${TENSORFLOW_DIR}/bazel-bin/tensorflow/libtensorflow_cc.so" -Wl,--no-whole-archive)
endif ()
```

できれば `libtensorflow_cc.so` のリンク行のあとに `-Wl,--no-whole-archive` をいれて後続にリストアップするライブラリのリンク設定をリセットしたいですが, これは課題とします.

最終的に, ユーザアプリでの `CMakeLists.txt` は以下のようになります.

```
make_minimum_required(VERSION 3.5.1)

project(myapp)

# threads
find_package(Threads)

# C++11(or C++14)
set (CMAKE_CXX_STANDARD 11)

# PIC
set (CMAKE_POSITION_INDEPENDENT_CODE ON)

set (CORE_SOURCE
    ${CMAKE_SOURCE_DIR}/src/main.cc
    )

link_directories(
    ${TENSORFLOW_BUILD_DIR}
    )

add_executable( myapp
    ${CORE_SOURCE}
    )

target_include_directories(myapp
    PUBLIC ${TENSORFLOW_DIR}

    # for array_ops.h
    PUBLIC ${TENSORFLOW_DIR}/bazel-genfiles

    # headers for external packages
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/protobuf_archive/src
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/eigen_archive
    PUBLIC ${TENSORFLOW_EXTERNAL_DIR}/external/nsync/public

    # this project
    PUBLIC ${CMAKE_SOURCE_DIR}/src
)

target_link_libraries( myapp
    tensorflow_cc
    ${CMAKE_THREAD_LIBS_INIT}
    ${CMAKE_DL_LIBS}
    )

...
```

これでとりあえずユーザアプリ(Linux + C++)で推論を動かすことができるようになるはずです!(少なくとも freeze された .pb を読み込めるところまで確認しました)

Happy TensorFlow C++ Inference! :nerd:

## 参考情報

Exporting trained TensorFlow models to C++ the RIGHT way!
https://medium.com/@hamedmp/exporting-trained-tensorflow-models-to-c-the-right-way-cf24b609d183

TensorFlow : Mobile : TensorFlow ライブラリを統合する
http://tensorflow.classcat.com/2017/12/13/tensorflow-mobile-linking_libs/
(No session factory registered for the given session options について)

Tensorflowをc++のcmakeしてるプロジェクトで使いたい
https://qiita.com/Tacha-S/items/5bb18c631868b6b15517

## TODO

* 学習部分もモバイル(エッジ)で動かしたい.
* TensorFlow C++, ビルドがめんどくさいし Bazel 使いたくないので PyTorch 1.0 に早く移行したい. PyTorch で幸せになりたい.
* 優秀な TensorFlow C++ 若人が, 日々切磋琢することで, 人類史上最速で優秀な C++ 機械学習若人へと昇華なされるスキームを確立する旅に出たい.