すでにあるDockerfileをGPU対応させた話

## はじめに
この記事は、機械学習させようと思ったDockerfileおよびdocker-composeファイルがGPUに対応していなかったので、その原因と対策という話になります。
とはいえ、自分はdockerを調べ始めて1ヶ月ほどしかたっておらず、いくつか間違った知識があるかもしれないので、ご了承くださいm(_ _)m

### 環境
- Ubuntu 18.04
- docker 19.03.11
- docker-compose 1.17.1 -> 1.20.1
- nvidia-docker、cuda等はホストOSにインストール済み
- tensorflow 2.1.0

### 対象となる人
- dockerをGPU対応させたい人
- GPU対応できてるか知りたい人

## 使用したDockerfileとdocker-compose.yml
今回使おうとしたのはとある書籍のサンプルプロジェクトなので、すべてを載せることはできませんが、最低限の部分だけ記述しておきます。

```Dockerfile:Dockerfile
FROM continuumio/miniconda3:latest

COPY environment.yml /tmp/
RUN conda update -y -n base conda \
    && conda env create -f /tmp/environment.yml \
    && conda clean -y -t \
    && rm /tmp/environment.yml

ENV PATH /opt/conda/envs/tf/bin:$PATH
```

```yaml:docker-compose.yml
version: "3"
services:
  tf:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    container_name: tf
    image: tf
    ports:
      - "8888:8888"
    volumes:
      - ../:/tf
    command: /opt/conda/envs/tf/bin/jupyter notebook --ip='0.0.0.0' --port=8888 --no-browser
```

やりたいこととしては、minicondaを使用してenvironment.ymlからpython環境を作成し、docker-composeでjupyter notebookを起動させるといったものです。
environment.ymlにはtensorflow2.1やjupyterなどが含まれています。

しかし、このままではGPUを認識してくれません。

## GPUが認識されているかどうかの検証方法
GPUが使えるかどうかにはいくつか方法があるので、ここでまとめておきます。これらをdockerコンテナ内で実行することで、GPUが使えるか調べることができます。
### nvidia-smi
物理デバイスとしてGPUが認識できているか確認する方法です。dockerからGPUを使えるようになっていれば基本的にコマンドを受けつけてくれると思います。
### tf.config.list_physical_devices('GPU')
これはpythonで実行するやつです。tensorflowをimportしてから使用してください。
これを実行すると、GPUが使えればそのリストが返ってきます。この時、使用できない場合は様々なwarningが出てくるので、これを手掛かりに原因を掴むことができます。
### tf.test.is_gpu_available()
これもpythonの関数です。こちらはTrue/Falseで返ってくる以外は上の関数と同じです。
###※ lspci | grep -i nvidia
調べるとこの方法もよく出てくるのですが、docker内だとこのコマンドが使用できなかったです。最小限の構成だからでしょうか...？

## GPUが使えない原因
使いたいDockerコンテナに対し、上記のコマンドを試したところ、すべてGPUを認識していない結果となりました。
すごく調べて調査した結果、大きく分けて以下の3つの理由があることがわかりました。

- docker-composeがGPUに対応しきれていない
- CUDAやcudnnがdockerコンテナ内に含まれていない
- tensorflowがCUDAなどを認識していない

それでは、1つずつ詳しくみていきます。

## docker-composeのGPU対応
すでにnvidia-docker(nvidia-container-toolkit)がホストOSにインストールされている前提でお話しします。
dockerは19.03からgpusオプションに対応し、`--gpus all`とすることで物理的にGPUデバイスを認識しnvidia-smiが使用できるようになります。
しかし、docker-composeはgpusタグに対応していません。そこでruntimeタグを使用してGPU対応させます。

docker-composeでgpu対応する方法は[こちらの記事](https://qiita.com/sht47/items/0706f19c67eb30e0954f)など、「docker-compose GPU」で検索かければたくさん出てきますが、やり方がいくつか異なっていたりして難しいと思います。
確認すべき要点は以下の通りです。

- docker-composeのバージョンを1.19以上にする
    - runtimeタグが使用できるようになります
- docker-compose.ymlのversionタグを確認する
    - docker-composeのバージョンとの対応によって使えない場合があります。最新版ならなんとかなるかもしれません。
- /etc/docker/daemon.jsonの中身を確認する
    - nvidia関連の設定が書かれていれば問題ありません
- docker-composeに`runtime: nvidia`を記述する
- environmentタグに`NVIDIA_VISIBLE_DEVICES=all`や`NVIDIA_DRIVER_CAPABILITIES=all`を記述する
    - NVIDIA_VISIBLE_DEVICESは使用するGPUデバイスを指定し、NVIDIA_DRIVER_CAPABILITIESはGPUの使用方法(？)を指定します。computeやutilityなど最低限の指定でも問題ないかもです。

これらの確認・変更を行って、docker-composeで立ち上げたコンテナ内でnvidia-smiを使用すると、うまくいけばnvidia-smiが使用できると思います。

## CUDAやcudnnの対応
ここからの問題に対する調査が非常に苦労しました。
dockerおよびdocker-composeでのGPU対応は、あくまで物理デバイスとしてのGPUを認識させるだけで、GPU計算ができるようになるわけではありません。
したがって、CUDAやcudnnがdockerコンテナにインストールされていなければなりません。
ほとんどのサンプルでは、nvidia/cudaというイメージや、tensorflowイメージなどを使用していますが、これらのコンテナにはCUDAが含まれています。しかし、minicondaのようなほとんどのイメージについてはGPUに対応していないのではないかと思います。

正直、この対策としては、ベースとなるDockerイメージによって異なると思います。
自分の環境では、minicondaベースをやめ、nvidia/cudaをベースにし、RUNでminicondaをインストールしました。
もしベースとなるイメージにcudaやcudnnが含まれているタグや関連するイメージがあれば、dockerhubからそれを選択するべきだと思います。
もしnvidia/cudaをベースにできないし、cudaのあるバージョンもないということであれば、Dockerfile内でcudaやcudnnを追加する方法しかないように思います。
Dockerfileにどのように書いたらいいかは分からないので、詳しい人に聞いてください...
愚直にやると、キャッシュ等が残ってしまい軽量とはいえなくなる可能性があります。

## tensorflowのCUDA認識
CUDAやcudnnのあるバージョンを見つけたところ悪いのですが、tensorflowには対応するCUDAやcudnnがインストールされている必要があります。少しでもバージョンが異なると動かない可能性があります。対応するCUDAのバージョンを含むDockerイメージにしてください。(参考:[Tensorflowのビルド構成](https://www.tensorflow.org/install/source?hl=ja#gpu))

さらにここから、場合によってはせっかく入ったのにtensorflowが見つけてくれないという場合があります。これは、LD_LIBRARY_PATHという環境変数にcuda関係のファイルへのパスが含まれていない可能性があります。
`find / -name libcu*`のように検索することで、全ファイルからlibcudartやlibcurandなどのファイルが見つかると思います。それらが入っているフォルダをLD_LIBRARY_PATHに追加してください。

先ほどの方法でtensorflowがGPUを認識したら成功です！おめでとうございます。

## おまけ：tensorRTについて
自分の環境ではそうなのですが、tensorflowをimportするタイミングでいくつかのwarningが出ます。そこではtensorRTが使えないといった旨が記載されています。
これは決してtensorflowが使えないというわけではなく、GPU計算をさらに高速化させるtensorRTが含まれていないことを意味しています。なので、もし出てしまった場合でも問題なくGPUを認識してくれると思います。

## おわりに
自分の経験をつらつらと書いただけなので参考にならなかったかもしれません...
そもそも、最初からtensorflow-gpuのdockerイメージをベースにすればこのような問題は起こりにくく、しかもgpuタグを付けなくてもGPUを認識してくれるので、できることならそのようなイメージから始めることをオススメします。