Debian GNU/Linux Stretch + CUDA9.1 の環境で TensorFlow 1.2 をビルドする 

# はじめに

Debian GNU/Linux Stretch 環境に [backports](https://backports.debian.org/) から CUDA9.1[^whynot_cuda8] をインストールして、コンパイル済みの TensorFlow パッケージを利用しようと、以下のコマンドを実行すると「libcublas.so.9.0 が見つかりませんよ」というエラーになりました。

[^whynot_cuda8]: backports を利用しない Debian GNU/Linux Stretch 環境では、CUDA は 8.0 になります。CUDA8.0 に対応した TensorFlow は相当古いバージョンになるため、今回動かそうとしているプログラムは動きませんでした。なお、Debian GNU/Linux Buster 環境であれば CUDA9.2 になりますから、基本的には、このページの手順でコンパイル可能です。

```shell-session
$ pip3 install --user tensorflow-gpu
$ python3 train.py
（中略）
ImportError: libcublas.so.9.0: cannot open shared object file: No such file or directory
```

なるほど、CUDA9.1 向けに TensorFlow をビルドしなければならないのだなと軽く考えたのが、運の尽き。それがこんな泥沼の1週間になろうとは…

# 前提となる環境

 * Debian GNU/Linux Stretch を使っています。
 * CUDA は backports.debian.org から CUDA9.1 をインストールしています。
 * cuDNN や NCCL は、NVidia が公開している Ubuntu 用のパッケージをインストールしています。

正確な環境設定手順は、https://gist.github.com/tsuchm/bf61370ccb2fac639c811d77d0c61fa7 で公開している Ansible Playbook の通りです。

# 基本的な手順

手順は、[公式ページ](https://www.tensorflow.org/install/source)で解説されている通り…のはずなのですが、後述のようにトラブルシュートが必要になります。

## bazel のインストール

最初に、[bazel をインストール](https://docs.bazel.build/versions/master/install.html)します。ただし、最新の 0.19.1 を使って `bazel build` すると、以下のようなエラーになります。

```
ERROR: Config value cuda is not defined in any .rc file
```

このエラーを回避するには、https://stackoverflow.com/questions/52041661/geting-error-config-value-cuda-is-not-defined-in-any-rc-file-when-trying-to-tr に説明されている通り、0.16 よりも以前のバージョン（本稿では 0.15.2 を使用）をインストールする必要があります。

## 依存パッケージのインストール

ビルド時に必要なパッケージをインストールしておきます。

```shell-session
$ sudo apt install python3-numpy python3-pip python3-six python3-all-dev python3-wheel swig
$ pip3 install --user --upgrade mock
$ pip3 install --user --upgrade keras_applications==1.0.5 --no-deps
$ pip3 install --user --upgrade keras_preprocessing==1.0.3 --no-deps
```

## TensorFlow のソースをダウンロード・ビルド

TensorFlow のソースをダウンロードします。2018年11月現在の最新リリース版は 1.12 系列ですから、1.12 系列のブランチの最新版をビルドするために、ブランチを移動します。

```shell-session
$ git clone https://github.com/tensorflow/tensorflow.git
$ cd tensorflow
$ git checkout r1.12
$ ./configure
```

`configure` スクリプトを実行して、設定ファイル `.tf_configure.bazelrc` を生成します。注意点としては、

 * Python のバイナリを `/usr/bin/python3` と明示的に指定しないと、Python2 用にビルドされてしまいます。
 * 2018年11月現在の Debian GNU/Linux Stretch + backports では CUDA9.1 が `/usr` 以下にインストールされていますから、そのように指定します。

このコマンドの結果、以下のような設定ファイルが生成されます。

```config:.tf_configure.bazelrc
build --action_env PYTHON_BIN_PATH="/usr/bin/python3"
build --action_env PYTHON_LIB_PATH="/usr/lib/python3/dist-packages"
build --python_path="/usr/bin/python3"
build --define with_ignite_support=true
build --define with_xla_support=true
build --action_env TF_NEED_OPENCL_SYCL="0"
build --action_env TF_NEED_ROCM="0"
build --action_env TF_NEED_CUDA="1"
build --action_env CUDA_TOOLKIT_PATH="/usr"
build --action_env TF_CUDA_VERSION="9.1"
build --action_env CUDNN_INSTALL_PATH="/usr/lib/x86_64-linux-gnu"
build --action_env TF_CUDNN_VERSION="7"
build --action_env NCCL_INSTALL_PATH="/usr/lib/x86_64-linux-gnu"
build --action_env NCCL_HDR_PATH="/usr/include"
build --action_env TF_NCCL_VERSION="2"
build --action_env TF_CUDA_COMPUTE_CAPABILITIES="3.5,7.0"
build --action_env TF_CUDA_CLANG="0"
build --action_env GCC_HOST_COMPILER_PATH="/usr/bin/x86_64-linux-gnu-gcc-6"
build --config=cuda
test --config=cuda
build:opt --copt=-march=x86-64
build:opt --copt=-O3
build:opt --host_copt=-march=native
build:opt --define with_default_optimizations=true
build:v2 --define=tf_api_version=2
```

次に、以下のコマンドで、TensorFlow 全体をビルドします。

```shell-session
$ bazel build --config=opt --verbose_failures //tensorflow/tools/pip_package:build_pip_package
```

なお、`.tf_configure.bazelrc` に `build --config=cuda` という指定があるので、bazel コマンドの実行時には `--config=cuda` オプションを指定しなくとも、GPU バージョンがビルドされるはずです。

次に、以下のコマンドで、`/tmp/tensorflow_pkg/` 配下に pip パッケージを作成します。

```shell-session
$ ./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
```

最後に、以下のコマンドで、作成された pip パッケージをインストールします。

```shell-session
$cd  pip3 install --user /tmp/tensorflow_pkg/tensorflow-1.12.0-cp35-cp35m-linux_x86_64.whl 
```

# トラブルシュート

TensorFlow は、ビルドスクリプト内部で protobuf などのライブラリをダウンロード・ビルドしています。そして、このライブラリと、システム側にインストールしているライブラリのバージョンが **完全に一致** していないと各種のトラブルの原因になります。通常であれば、インストール対象で使われているバージョンよりも新しいバージョンがシステムにインストールされていれば良いことが大半なのですが、TensorFlow のビルドプロセスでは、 **完全に一致** している必要があります。

基本的には、`workspace.bzl` というファイルに、バージョンやダウンロード URL が記述されていますので、これを逐一確認していくことになります。

## protobuf のバージョン

protobuf のバージョンが一致していないと、以下のようなエラーでビルドに失敗します。これは、https://stackoverflow.com/questions/46235376/tensorflow-protobuf-version-mismatch で紹介されている問題と同じです。

```
bazel-out/k8-opt/genfiles/tensorflow/core/framework/types.pb.h:17:2: error: #error This file was generated by an older version of protoc which is
bazel-out/k8-opt/genfiles/tensorflow/core/framework/types.pb.h:18:2: error: #error incompatible with your Protocol Buffer headers. Please
bazel-out/k8-opt/genfiles/tensorflow/core/framework/types.pb.h:19:2: error: #error regenerate this file with a newer version of protoc.
```

システムにインストールされている protobuf のバージョンは、以下のコマンドで確認できます。

```shell-session
$ dpkg -l libprotobuf-dev
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)保持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)トリガ待ち/(T)トリガ保留
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前                                                  バージョン                      アーキテクチャ                  説明
+++-=====================================================-===============================-===============================-================================================================================================================
ii  libprotobuf-dev:amd64                                 3.6.1-4~bpo9+1                  amd64                           protocol buffers C++ library (development files) and proto files
```

それに対して、`tensorflow/workspace.bzl` 中の protobuf をダウンロードしている箇所を確認すると、以下のように 3.6.0 が指定されています。

```python
    PROTOBUF_URLS = [
        "https://mirror.bazel.build/github.com/google/protobuf/archive/v3.6.0.tar.gz",
        "https://github.com/google/protobuf/archive/v3.6.0.tar.gz",
    ]
    PROTOBUF_SHA256 = "50a5753995b3142627ac55cfd496cebc418a2e575ca0236e29033c67bd5665f4"
    PROTOBUF_STRIP_PREFIX = "protobuf-3.6.0"
```

そこで、3.6.1 をダウンロードするように `tensorflow/workspace.bzl` の URL と SHA256SUM などを書き換えます。

```patch:
--- a/tensorflow/workspace.bzl
+++ b/tensorflow/workspace.bzl
@@ -381,11 +381,11 @@ def tf_workspace(path_prefix = "", tf_repo_name = ""):
     )
 
     PROTOBUF_URLS = [
-        "https://mirror.bazel.build/github.com/google/protobuf/archive/v3.6.0.tar.gz",
-        "https://github.com/google/protobuf/archive/v3.6.0.tar.gz",
+        "https://github.com/protocolbuffers/protobuf/archive/v3.6.1.tar.gz",
+        "https://github.com/protocolbuffers/protobuf/archive/v3.6.1.tar.gz",
     ]
-    PROTOBUF_SHA256 = "50a5753995b3142627ac55cfd496cebc418a2e575ca0236e29033c67bd5665f4"
-    PROTOBUF_STRIP_PREFIX = "protobuf-3.6.0"
+    PROTOBUF_SHA256 = "3d4e589d81b2006ca603c1ab712c9715a76227293032d05b26fca603f90b3f5b"
+    PROTOBUF_STRIP_PREFIX = "protobuf-3.6.1"
 
     tf_http_archive(
         name = "protobuf_archive",
```

なお、URL は、たとえ同一であっても、2回以上指定しておく必要があります。

なお、Debian GNU/Linux Buster 環境で Python3.7 向けにビルドしようとすると、以下のようにコンパイルエラーが発生します。

```
external/protobuf_archive/python/google/protobuf/pyext/extension_dict.cc: In function 'PyObject* google::protobuf::python::extension_dict::_FindExtensionByName(google::protobuf::python::ExtensionDict*, PyObject*)':
external/protobuf_archive/python/google/protobuf/pyext/extension_dict.cc:56:45: error: invalid conversion from 'const char*' to 'char*' [-fpermissive]
        ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \
                      ~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
external/protobuf_archive/python/google/protobuf/pyext/extension_dict.cc:184:7: note: in expansion of macro 'PyString_AsStringAndSize'
   if (PyString_AsStringAndSize(arg, &name, &name_size) < 0) {
       ^~~~~~~~~~~~~~~~~~~~~~~~
```

おそらく protobuf-3.6.1 が Python3.7 と非互換なのだろうと思われます。https://gist.github.com/tsuchm/8d54fdabb2ba760da91fa67ca5842a2d に記載の通り、Debian GNU/Linux Buster で使われている protobuf-3.6.1.3 を使ってビルドするように指定すれば、回避できます。

## ICU (International Components for Unicode) のバージョン

ICU のバージョンが一致していないと、以下のようなエラーでビルドに失敗します。

```
ImportError: /home/user/.cache/bazel/_bazel_tsuchiya/fb08a882423ce1af8fd909db27d4a772/execroot/org_tensorflow/bazel-out/host/bin/tensorflow/create_tensorflow.python_api_1.runfiles/org_tensorflow/tensorflow/python/_pywrap_tensorflow_internal.so: undefined symbol: uscript_getScript_57
```

システムにインストールされている ICU のバージョンは、以下のコマンドで確認できます。

```shell-session
$ dpkg -l icu-devtools
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)保持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)トリガ待ち/(T)トリガ保留
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前                                                  バージョン                      アーキテクチャ                  説明
+++-=====================================================-===============================-===============================-================================================================================================================
ii  icu-devtools                                          57.1-6+deb9u2                   amd64                           Development utilities for International Components for Unicode
```

そこで、システムで使っているバージョンの ICU を使うように、`third_party/icu/workspace.bzl` を以下のように書き換えます。

```patch
--- a/third_party/icu/workspace.bzl
+++ b/third_party/icu/workspace.bzl
@@ -5,11 +5,11 @@ load("//third_party:repo.bzl", "third_party_http_archive")
 def repo():
     third_party_http_archive(
         name = "icu",
-        strip_prefix = "icu-release-62-1",
-        sha256 = "e15ffd84606323cbad5515bf9ecdf8061cc3bf80fb883b9e6aa162e485aa9761",
+        strip_prefix = "icu-release-57-1",
+        sha256 = "f86486f6825fdb01da28349a3d956db409e297dc2f15144c2456cfa0c6f8cd8d",
         urls = [
-            "https://mirror.bazel.build/github.com/unicode-org/icu/archive/release-62-1.tar.gz",
-            "https://github.com/unicode-org/icu/archive/release-62-1.tar.gz",
+            "https://mirror.bazel.build/github.com/unicode-org/icu/archive/release-57-1.tar.gz",
+            "https://github.com/unicode-org/icu/archive/release-57-1.tar.gz",
         ],
         build_file = "//third_party/icu:BUILD.bazel",
     )
```

## コンパイラのバージョン

[公式ページ](https://www.tensorflow.org/install/source)に説明されている通り、公式には GCC 4.8 でビルドが確認されています。そのため、以下のように `.tf_configure.bazelrc` に指定して、GCC 4.8 を使うと安全でしょう。

```config:.tf_configure.bazelrc
build --action_env GCC_HOST_COMPILER_PATH="/usr/bin/x86_64-linux-gnu-gcc-4.8"
```

GCC 6 を使ってビルドしていて PyString が未定義であるなどのエラーが出た場合には、`--cxxopt` オプションが役立つかもしれません。

```shell-session
$ bazel build --config=opt --verbose_failures //tensorflow/tools/pip_package:build_pip_package --cxxopt="-D_GLIBCXX_USE_CXX11_ABI=0"
```

## Quota Exceeded

TensorFlow は protobuf や各種の周辺ライブラリを全て、一度ダウンロードして展開・ビルドします。そのため、`~/.cache/bazel/` というディレクトリ配下にかなりの容量（約18GB）を必要とします。quota がかかっている共用マシンなどでは、quota exceeded で中断する場合があります。回避するには、環境変数 `TEST_TMPDIR` または `--output-base` オプションに、十分な空き容量があるディレクトリを指定しましょう。

## TensorFlow が GPU を使っているかチェック

以下のような `listgpu.py` というファイルを用意して実行します。

```python:listgpu.py
from tensorflow.python.client import device_lib
device_lib.list_local_devices()
```

```shell-session
$ python3 listgpu.py
```

これで GPU がきちんと列挙されなければ、少なくとも TensorFlow から GPU が見えていません。