最新のTensorFlow 2(GPU版)をCentOS7上で極力root権限なしにソースからビルド＆インストールする

ネット上にはCentOS7かつ最新のTensorflow 2（GPU対応版）のインストール方法手順が書かれた記事がほとんどなかったので書いてみる。

Ubuntu 20と違ってCentOS 7.8にはお手軽にtensorflow-gpuを入れる手段がないので、ソースからビルドを行う。あとついでにgccやpythonもソースからビルドしてインストールする。

かなり難しいインストール作業で、トータルで5時間くらいかかる。

参考資料

- [tensorflow 公式ソースビルドガイド](https://www.tensorflow.org/install/source?hl=ja)
- [How to install the latest version of Tensorflow (2.3.0) on a machine where the default gcc is too old or too new and you don't have root access](https://gist.github.com/jakublipinski/40ba68994fe0092600a05b0060e7d445)　これがかなり参考になった。ちょっとミスがあるので修正が必要。

## マシン環境
- CentOS 7.8.2003
- GPU: NVIDIA RTX 3090 2枚差し (CUDA 11.1.105)

以下ではTensorFlow2のソースからのビルド＆インストールに必要となるソフトウェア類gcc, python, javaSDK, bazelを`/home/apps`以下に置いて適宜利用していくことにする。

### CUDA 11.1, cudnn 8, CUDA SDK
root権限を持っている場合はCUDA Toolkit 11.1を普通にインストールすればよい。rootを持っていない場合はcudaのインストーラでrunfileを使えばインストール先をホームディレクトリ以下に指定できるはず。cudnn 8も同様に公式サイトからダウンロードする。これについてはGPU版を使う上で避けて通れないので、なんらかの形でインストールされていることを前提にする。

参考：https://stackoverflow.com/questions/39379792/install-cuda-without-root。

### gcc 8.4.0
CentOS7の`/usr/bin/gcc`にデフォルトで存在するgcc 4.8.5は古すぎて使いたくなかったので（パフォーマンスも落ちる？）、ソースからビルドしたgcc 8.4.0を使う。

```
gccnum="8.4.0"
wget https://ftp.gnu.org/gnu/gcc/gcc-${gccnum}/gcc-${gccnum}.tar.xz
tar Jxvf gcc-${gccnum}.tar.xz
cd gcc-${gccnum}
./contrib/download_prerequisites
./configure --with-system-zlib --disable-multilib --disable-bootstrap --enable-languages=c,c++,fortran --prefix=/home/apps/compiler/gcc/${gccnum}
make -j8 && make install
```

`/home/apps/compiler/gcc/8.4.0`以下にインストールされた。

### python 3.8.7
```
module load gcc/8.4.0
wget https://www.python.org/ftp/python/3.8.7/Python-3.8.7.tar.xz
tar xf Python-3.8.7.tar.xz
cd Python-3.8.7
./configure --prefix=/home/apps/python/3.8.7 --enable-ipv6 --enable-loadable-sqlite-extensions --with-system-ffi --enable-optimizations
make -j8 altinstall
```

オプションはHomebrewで使われているものを流用したが、実質`--enable-optimizations`以外はあまり関係ないと思われる。`/home/apps/compiler/python/3.8.7`以下にインストールされた。

### Java SDK 15.0.2
bazelのビルドに必要らしい。`_linux-x64_bin.tar.gz`のものをダウンロードする。以下では`wget`でとってこようとしているが、素直にブラウザを使って公式ページ上からJava SDKをダウンロードしてきたほうがいい。

```
# wget https://download.oracle.com/otn-pub/java/jdk/15.0.2+7/hogehogehoghoge/jdk-15.0.2_linux-x64_bin.tar.gz
tar zxvf jdk-15.0.2_linux-x64_bin.tar.gz
mv jdk-15.0.2 /home/apps
export JAVA_HOME=/home/apps/jdk-15.0.2/
```

`/home/apps/jdk-15.0.2/`以下にインストールし、環境変数`JAVA_HOME`を設定しておく。

### bazel 3.1.0
バイナリ版のインストーラもあるけれど、あれだとうまくいかないみたいなので仕方なくソースビルドすることにする。ちなみに元GitHubのマスター（2.4.2以降も？）ではbazelの要求バージョンが3.7.2になっているかもしれないので、その場合はダウンロードするbazelのバージョン番号を変更して以下の操作を行う。

```
wget https://github.com/bazelbuild/bazel/releases/download/3.1.0/bazel-3.1.0-dist.zip
unzip bazel-3.1.0-dist.zip -d bazel-3.1.0
cd bazel-3.1.0
```

ここで、`third_party/grpc/bazel/generate_cc.bzl`ファイルに対して https://github.com/bazelbuild/bazel/pull/11860/files にかかれてあるように、`use_default_shell_env = True,`の一行を入れて修正する。終わったらコンパイルを行う。

```
# さっき入れたgcc 8.4.0をGCCROOTに設定する
GCCROOT=/home/apps/compiler/gcc/8.4.0
export GCC_HOST_COMPILER_PATH=$GCCROOT/bin/gcc
export GCC_HOST_COMPILER_PREFIX=$GCCROOT/bin
export CXX=$GCCROOT/bin/gcc
export CC=$GCCROOT/bin/gcc
export LD_LIBRARY_PATH=$GCCROOT/lib64
export LDFLAGS="-L$GCCROOT/lib -L$GCCROOT/lib64"
export CXXFLAGS="-L$GCCROOT/lib -L$GCCROOT/lib64"

export JAVA_HOME=/home/apps/jdk-15.0.2/
env BAZEL_LINKOPTS=-static-libstdc++:-static-libgcc BAZEL_LINKLIBS=-l%:libstdc++.a:-lm bash ./compile.sh
```

コンパイル時にBAZELのリンクオプションを付けておく。でき上がったbazelを`/home/apps/bazel/3.1.0/bin`に置く

```
mkdir -p /home/apps/bazel/3.1.0/bin
cp output/bazel /home/apps/bazel/3.1.0/bin
```

### bintools 
gcc 8.4.0のbinディレクトリには`gcc-ar`があっても`ar`がないので、bintoolsを入れることで解決できる（ような気がする）

```
wget https://ftp.gnu.org/gnu/binutils/binutils-2.36.tar.gz	
tar zxvf binutils-2.36.tar.gz	
cd binutils-2.36
./configure --prefix=/home/apps/binutils/2.36
make -j 16 MAKEINFO=true	
make install MAKEINFO=true
```

終わったら、先程インストールしたgcc 8.4.0のbinディレクトリに一時的にこのbinutilsでインストールした`ar`のシンボリックリンクを貼っておく

```
ln -s /home/apps/binutils/2.36/bin/ar /home/apps/compiler/gcc/8.4.0/bin/ar
```

こうすることで後のtensorflowのbazel build処理中のリンク処理がうまくいった。

### tensorflow 2.4.1
まずtensorflow 2.4.1をダウンロードする。

```
# tensorflow 2.4.1のダウンロード
$ git clone https://github.com/tensorflow/tensorflow.git -b v2.4.1 --recursive --depth 1
```

Tensorflowのコンパイル時のエラーを解決するためのワークアラウンド（応急処置回避策）を以下に示す。

protocが正しくないバージョンのstdc++にリンクされてしまう問題を次のようにして解決する。`third_party/gpus/crosstool/cc_toolchain_config.bzl.tpl`のファイルの543行目を以下のように変更する

```patch
- flag_group(flags = ["-lc++" if cpu == "darwin" else "-lstdc++"]),
+ flag_group(flags = ["-lc++" if cpu == "darwin" else "-l:libstdc++.a"]),
```

`third_party/gpus/crosstool/cc_toolchain_config.bzl.tpl`ファイルの241行目あたり、objcopyが上でインストールしたbinutilsのものを使うように変更する。

```patch
def _tool_paths(cpu, ctx):
    if cpu in ["local", "darwin"]:
        return [
            tool_path(name = "gcc", path = ctx.attr.host_compiler_path),
            tool_path(name = "ar", path = ctx.attr.host_compiler_prefix + (
                "/ar" if cpu == "local" else "/libtool"
            )),
            tool_path(name = "compat-ld", path = ctx.attr.host_compiler_prefix + "/ld"),
            tool_path(name = "cpp", path = ctx.attr.host_compiler_prefix + "/cpp"),
            tool_path(name = "dwp", path = ctx.attr.host_compiler_prefix + "/dwp"),
            tool_path(name = "gcov", path = ctx.attr.host_compiler_prefix + "/gcov"),
            tool_path(name = "ld", path = ctx.attr.host_compiler_prefix + "/ld"),
            tool_path(name = "nm", path = ctx.attr.host_compiler_prefix + "/nm"),
-           tool_path(name = "objcopy", path = ctx.attr.host_compiler_prefix + "/objcopy"),
+           tool_path(name = "objcopy", path = "/home/apps/binutils/2.36/bin/objcopy"),
            tool_path(name = "objdump", path = ctx.attr.host_compiler_prefix + "/objdump"),
            tool_path(name = "strip", path = ctx.attr.host_compiler_prefix + "/strip"),
        ]
```

システムデフォルトのgcc（`/bin/gcc`）を使わない場合（例えば上のように`/home/apps/`以下にインストールしたgcc 8.4.0を使おうとしているとき）、TensorFlow 2.4.1では以下の修正を `third_party/flatbuffers/build_defs.bzl` に適用しておく。将来的に2.4.2以降は修正されているはず。
https://github.com/tensorflow/tensorflow/pull/46550/files/60aea05061a01b18ff02582341ab374abf253437

`./configure`での設定に進む前に、gcc, python3.8, bazelへのPATHを通しておく。

```
$ export PATH="/home/apps/bazel/3.1.0/bin:$PATH"
$ echo $PATH
/home/apps/bazel/3.1.0/bin:/home/apps/python/3.8.7/bin:/home/apps/compiler/gcc/8.4.0/bin:/home/yoshitakam/.local/bin:/usr/bin:/home/moriwaki/bin:/usr/local/sbin:/usr/sbin
```

`/home/apps/bazel/3.1.0/bin:/home/apps/python/3.8.7/bin:/home/apps/compiler/gcc/8.4.0/bin`が入っていることを確認する（順不同）　終わったら`./configure`での設定へ。

```
# configureでbazel buildのための設定ファイルを作る
$ ./configure
You have bazel 3.1.0- (@non-git) installed.
Please specify the location of python. [Default is /bin/python3]: /home/apps/python/3.8.7/bin/python3.8


Found possible Python library paths:
  /home/apps/python/3.8.7/lib/python3.8/site-packages
Please input the desired Python library path to use.  Default is [/home/apps/python/3.8.7/lib/python3.8/site-packages]

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

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

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

Found CUDA 11.1 in:
    /usr/local/cuda-11.1/targets/x86_64-linux/lib
    /usr/local/cuda-11.1/targets/x86_64-linux/include
Found cuDNN 8 in:
    /usr/local/cuda-11.1/targets/x86_64-linux/lib
    /usr/local/cuda-11.1/targets/x86_64-linux/include


Please specify a list of comma-separated CUDA compute capabilities you want to build with.
You can find the compute capability of your device at: https://developer.nvidia.com/cuda-gpus. Each capability can be specified as "x.y" or "compute_xy" to include both virtual and binary GPU code, or as "sm_xy" to only include the binary code.
Please note that each additional compute capability significantly increases your build time and binary size, and that TensorFlow only supports compute capabilities >= 3.5 [Default is: 3.5,7.0]: 8.6


Do you want to use clang as CUDA compiler? [y/N]: N
nvcc will be used as CUDA compiler.

Please specify which gcc should be used by nvcc as the host compiler. [Default is /home/apps/compiler/gcc/8.4.0/bin/gcc]:


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


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 .bazelrc for more details.
        --config=mkl            # Build with MKL support.
        --config=mkl_aarch64    # Build with oneDNN support for Aarch64.
        --config=monolithic     # Config for mostly static monolithic build.
        --config=ngraph         # Build with Intel nGraph support.
        --config=numa           # Build with NUMA support.
        --config=dynamic_kernels        # (Experimental) Build kernels into separate shared objects.
        --config=v2             # Build TensorFlow 2.x instead of 1.x.
Preconfigured Bazel build configs to DISABLE default on features:
        --config=noaws          # Disable AWS S3 filesystem support.
        --config=nogcp          # Disable GCP support.
        --config=nohdfs         # Disable HDFS support.
        --config=nonccl         # Disable NVIDIA NCCL support.
Configuration finished
```

`CUDA compute capabilities`のところで入力する数字は https://developer.nvidia.com/cuda-gpus ここで確認する。RTX3090の場合は8.6になる。

`./configure`が終わったら、公式ガイドに従って次に`bazel build`を行うが、このとき以下のようなリンクオプションをたくさん付けておく。また、cudaを使いたい場合は必ず`--config=cuda`をつける。gcc-5以降の互換性を保ちたい場合は`--cxxopt="-D_GLIBCXX_USE_CXX11_ABI=0"`をつける。

```shell
env BAZEL_LINKOPTS=-static-libstdc++:-static-libgcc BAZEL_LINKLIBS=-l%:libstdc++.a:-lm BAZEL_CXXOPTS=-std=gnu++0x bazel build --config=opt --config=cuda --cxxopt="-D_GLIBCXX_USE_CXX11_ABI=0" //tensorflow/tools/pip_package:build_pip_package --action_env="LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" --verbose_failures
```
ビルドには8コアマシンで4時間くらいかかるので、ssh先でビルドが終わるまで監視するのがめんどくさい場合は、`nohup env BAZEL //(途中コマンド省略)// --verbose_failures &`としておけば良けば、sshが切れても処理が続けられる。他には`disown`コマンドを使う方法もある。

**ビルドに失敗したとき、やり直す前に`bazel clean --expunge`で一度ビルド途中のファイルを消去しておく。**

正しくビルドが成功すると、TensorFlowのディレクトリから見て`./bazel-bin/tensorflow/tools/pip_package/build_pip_package`というファイルが作られているはずなので、これを以下のコマンドでwheelファイル化させる。

```
# リリース版（2.4.1とかバージョン番号がついている方）はこちらのコマンド
./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
# githubのマスターから直接引っ張ってきた場合はnightly buildフラグを付けると良い
# ./bazel-bin/tensorflow/tools/pip_package/build_pip_package --nightly_flag /tmp/tensorflow_pkg
```

成功すると`/tmp/tensorflow_pkg`以下にwhlファイルが生成されているので、任意のディレクトリ上に移す（例：`/home/apps`）。

```
mv /tmp/tensorflow_pkg/tensorflow-2.4.1-cp38-cp38-linux_x86_64.whl /home/apps
```

あとはこれをpython3.8のpip3.8などでインストールすれば使えるようになる（依存パッケージ解決のためにインターネット環境が必要かも）。

```
/home/apps/python/3.8.7/bin/python3.8 -m pip install /home/apps/tensorflow-2.4.1-cp38-cp38-linux_x86_64.whl --user
```

`--user`の代わりに、`-t`オプションを付けて`-t /home/apps/tensorflow-gpu/2.4.1_python3.8_cuda11`とすれば、そのディレクトリにパッケージをインストールすることもでき、あとで環境変数`PYTHONPATH`を通せば使えるようになる。

## 動作テスト
 
最後にテストしてみる。`python3.8`起動前に、上記のpython3.8, gcc-8.4.0, cuda-11.1について環境変数`PATH`と`LD_LIBRARY_PATH`を通しておく。私はPATHの管理について[environment module](https://qiita.com/Ag_smith/items/f268ad27165a60aecd35)を使っています。

```shell
$ python3.8
Python 3.8.7 (default, Jan 29 2021, 03:36:15)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from tensorflow.python.client import device_lib

2021-01-29 15:47:38.281608: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudart.so.11.0
>>> device_lib.list_local_devices()
2021-01-29 15:47:44.310282: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  SSE3 SSE4.1 SSE4.2 AVX AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2021-01-29 15:47:44.313095: I tensorflow/compiler/jit/xla_gpu_device.cc:99] Not creating XLA devices, tf_xla_enable_xla_devices not set
2021-01-29 15:47:44.315158: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcuda.so.1
2021-01-29 15:47:44.553626: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1720] Found device 0 with properties:
pciBusID: 0000:01:00.0 name: GeForce RTX 3090 computeCapability: 8.6
coreClock: 1.695GHz coreCount: 82 deviceMemorySize: 23.70GiB deviceMemoryBandwidth: 871.81GiB/s
2021-01-29 15:47:44.554559: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1720] Found device 1 with properties:
pciBusID: 0000:81:00.0 name: GeForce RTX 3090 computeCapability: 8.6
coreClock: 1.695GHz coreCount: 82 deviceMemorySize: 23.70GiB deviceMemoryBandwidth: 871.81GiB/s
2021-01-29 15:47:44.554693: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudart.so.11.0
2021-01-29 15:47:44.560335: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcublas.so.11
2021-01-29 15:47:44.560419: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcublasLt.so.11
2021-01-29 15:47:44.568157: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcufft.so.10
2021-01-29 15:47:44.573367: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcurand.so.10
2021-01-29 15:47:44.591659: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcusolver.so.11
2021-01-29 15:47:44.597937: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcusparse.so.11
2021-01-29 15:47:44.600959: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudnn.so.8
2021-01-29 15:47:44.609122: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1862] Adding visible gpu devices: 0, 1
2021-01-29 15:47:44.609182: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudart.so.11.0
2021-01-29 15:47:45.879186: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1261] Device interconnect StreamExecutor with strength 1 edge matrix:
2021-01-29 15:47:45.879243: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1267]      0 1
2021-01-29 15:47:45.879253: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1280] 0:   N N
2021-01-29 15:47:45.879261: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1280] 1:   N N
2021-01-29 15:47:45.883077: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1406] Created TensorFlow device (/device:GPU:0 with 22428 MB memory) -> physical GPU (device: 0, name: GeForce RTX 3090, pci bus id: 0000:01:00.0, compute capability: 8.6)
2021-01-29 15:47:45.886622: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1406] Created TensorFlow device (/device:GPU:1 with 22428 MB memory) -> physical GPU (device: 1, name: GeForce RTX 3090, pci bus id: 0000:81:00.0, compute capability: 8.6)
[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {
}
incarnation: 12593410599457824982
, name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 23518436480
locality {
  bus_id: 4
  numa_node: 3
  links {
  }
}
incarnation: 9832514123427674549
physical_device_desc: "device: 0, name: GeForce RTX 3090, pci bus id: 0000:01:00.0, compute capability: 8.6"
, name: "/device:GPU:1"
device_type: "GPU"
memory_limit: 23518436480
locality {
  bus_id: 2
  numa_node: 1
  links {
  }
}
incarnation: 10036722414424779821
physical_device_desc: "device: 1, name: GeForce RTX 3090, pci bus id: 0000:81:00.0, compute capability: 8.6"
]
```

1ノードに2つあるRTX3090が正しく認識されているのでこれで使えるはず。