Horovod in Dockerで(Proxy環境上に)分散学習環境を構築してみた

[Fujitsu Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fujitsu) 18日目の記事です。

:warning: 以下の記事は限定共有で公開していた記事です。旧バージョンのHorovodで構築した時の方法のため、現行バージョンではそぐわない可能性が有ります。(構築を実施した環境が廃止されてしまっているため、再検証が難しくなってしまいました。)最新版のHorovodの環境での構築は今後随時記事を投稿予定です。

以前業務でUberが開発した分散学習用OSS[Horovod](https://github.com/horovod/horovod)を用いた分散学習環境を構築したところ、Dockerベースでの構築が一筋縄でいかなかったため、構築方法をまとめて公開します。

:bangbang: 掲載の構築方法は2019年9月時点で最新バージョンのHorovod v0.18.1では未検証です。
:warning: 構築した環境がCUDA9.0までの対応だったため[Horovod v0.16.4](https://github.com/horovod/horovod/archive/v0.16.4.tar.gz)で試した方法です。
:rage: 社内Proxy環境上で公式README記載の方法で構築しても分散学習が動作しませんでした。
:pray: Horovod最新版での構築出来た or 出来なかった等の情報をコメントで頂けると幸いです。
:clap: RedHat系OSやWindowsでの構築情報なども大歓迎です。
:grin: 本記事のDockerコマンドは全てv1.13から導入された新しいコマンド体系を採用しています。

# 各マシンのスペック情報

今回の環境構築に用いた各GPUマシンのスペックは以下の通りです。今回はこの2台を同一ネットワーク上で接続し、利用します。

|項目|メインマシン|スレーブマシン|
|:-----:|:-----:|:----:|
|OS|Ubuntu 16.04.5 LTS|Ubuntu 16.04.5 LTS|
|CPU|Xeon(R) W-2102 <br> 2.90GHz|Xeon(R) W-2102 <br> 2.90GHz|
|Memory|64GB|64GB|
|Disk|(SSD) 256GB + (HD) 1TB|(HD) 500GB|
|GPU|NVIDIA<br>Quadro P4000|NVIDIA<br>GeForce GTX 1060|
|CUDA|9.0|9.0|

# 前準備：メインマシン、スレーブマシンのセットアップ

メインマシン、スレーブマシン共にOSのインストールやNVIDAのビデオドライバーCUDAのセットアップを行います。NVIDAのドライバーは以下のサイトからダウンロード出来ます。今回は別の環境の影響でCUDA9.0をインストールしました。今後、CUDA9.0以降のバージョンでも構築を試みたいと思います。CUDAやNVIDA Tool Kitのインストール方法は他のページに譲るとします。

https://developer.nvidia.com/cuda-downloads

以下のサイトには便利なシェルの実装が紹介されています。

https://qiita.com/kazetof/items/c2f8b2751d3ed9600a57

<font color="red"><strong>以降の行程はrootアカウントまたはdockerを実行できる環境で実施して下さい。</strong></font>

## 通信用ポートの利用状況の確認とrootログインの設定
この行程はメインマシン、スレーブマシン双方で実施します。後程分散学習環境を構築する時の準備です。

## ポート利用状況の確認
本家のREADMEで12345ポートを用いて双方向通信をしていたので、12345ポートを本記事でも用います。念のため、ポート利用状況を以下のコマンドで確認し、使用済みの場合は別のポート番号を使用してください。

```bash
lsof -i:12345
```

ポートが利用されていない場合、何も表示されません。

### `/etc/ssh/sshd_config`の編集
以下の様に編集します。

* RootLoginを許可します。

```config
PermitRootLogin yes
```

### sshの再起動
以下のコマンドを実行します。

```bash
/etc/init.d/ssh restart
```
<details><summary>実行結果</summary><div>

```bash
# /etc/init.d/ssh restart
[ ok ] Restarting ssh (via systemctl): ssh.service.
```
</div></details>

# Horovod用Dockerファイルのダウンロードとビルド
まずはメインマシンで構築作業を行います。メインマシンで構築したコンテナはtarファイル化し、後程スレーブマシンに転送します。

## ダウンロード用ディレクトリを作成

```bash
mkdir horovod-docker
```

## ソースファイルのダウンロード及びDockerファイルの取り出し

```bash
cd horovod-docker
wget https://github.com/horovod/horovod/archive/v0.16.4.tar.gz
tar xvf v0.16.4.tar.gz
cp horovod-0.16.4/Dockerfile ./
```

<details><summary>取り出したDockerfileの中身</summary><div>

```dockerfile

FROM nvidia/cuda:9.0-devel-ubuntu16.04

# TensorFlow version is tightly coupled to CUDA and cuDNN so it should be selected carefully
ENV TENSORFLOW_VERSION=1.12.0
ENV PYTORCH_VERSION=1.1.0
ENV TORCHVISION_VERSION=0.2.2.post3
ENV CUDNN_VERSION=7.4.1.5-1+cuda9.0
ENV NCCL_VERSION=2.3.7-1+cuda9.0
ENV MXNET_VERSION=1.4.1

# Python 2.7 or 3.5 is supported by Ubuntu Xenial out of the box
ARG python=2.7
ENV PYTHON_VERSION=${python}

RUN apt-get update && apt-get install -y --allow-downgrades --allow-change-held-packages --no-install-recommends \
        build-essential \
        cmake \
        git \
        curl \
        vim \
        wget \
        ca-certificates \
        libcudnn7=${CUDNN_VERSION} \
        libnccl2=${NCCL_VERSION} \
        libnccl-dev=${NCCL_VERSION} \
        libjpeg-dev \
        libpng-dev \
        python${PYTHON_VERSION} \
        python${PYTHON_VERSION}-dev

RUN ln -s /usr/bin/python${PYTHON_VERSION} /usr/bin/python

RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
    python get-pip.py && \
    rm get-pip.py

# Install TensorFlow, Keras, PyTorch and MXNet
RUN pip install 'numpy<1.15.0' tensorflow-gpu==${TENSORFLOW_VERSION} keras h5py torch==${PYTORCH_VERSION} torchvision==${TORCHVISION_VERSION} mxnet-cu90==${MXNET_VERSION}

# Install Open MPI
RUN mkdir /tmp/openmpi && \
    cd /tmp/openmpi && \
    wget https://www.open-mpi.org/software/ompi/v4.0/downloads/openmpi-4.0.0.tar.gz && \
    tar zxf openmpi-4.0.0.tar.gz && \
    cd openmpi-4.0.0 && \
    ./configure --enable-orterun-prefix-by-default && \
    make -j $(nproc) all && \
    make install && \
    ldconfig && \
    rm -rf /tmp/openmpi

# Install Horovod, temporarily using CUDA stubs
RUN ldconfig /usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs && \
    HOROVOD_GPU_ALLREDUCE=NCCL HOROVOD_WITH_TENSORFLOW=1 HOROVOD_WITH_PYTORCH=1 HOROVOD_WITH_MXNET=1 pip install --no-cache-dir horovod && \
    ldconfig

# Install OpenSSH for MPI to communicate between containers
RUN apt-get install -y --no-install-recommends openssh-client openssh-server && \
    mkdir -p /var/run/sshd

# Allow OpenSSH to talk to containers without asking for confirmation
RUN cat /etc/ssh/ssh_config | grep -v StrictHostKeyChecking > /etc/ssh/ssh_config.new && \
    echo "    StrictHostKeyChecking no" >> /etc/ssh/ssh_config.new && \
    mv /etc/ssh/ssh_config.new /etc/ssh/ssh_config

# Download examples
RUN apt-get install -y --no-install-recommends subversion && \
    svn checkout https://github.com/horovod/horovod/trunk/examples && \
    rm -rf /examples/.svn

WORKDIR "/examples"

```
</div></details>

※ 最新版のHorovodのDockerfileをダウンロードするには以下のコマンドを実行します。

```bash
wget -O horovod-docker/Dockerfile https://raw.githubusercontent.com/uber/horovod/master/Dockerfile
```

## Dockerfileの編集

以下のようにDockerfileを書き換えます。このまま実行すると最新版のHorovodがインストールされてしまい、イメージファイルビルド時に実行エラーが発生します。

```diff
# Install Horovod, temporarily using CUDA stubs
RUN ldconfig /usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs && \
    HOROVOD_GPU_ALLREDUCE=NCCL HOROVOD_WITH_TENSORFLOW=1 HOROVOD_WITH_PYTORCH=1 
- HOROVOD_WITH_MXNET=1 pip install --no-cache-dir horovod && \
+ HOROVOD_WITH_MXNET=1 pip install --no-cache-dir horovod==0.16.4 && \
    ldconfig
```
examplesもv0.16.4対応の物をダウンロードするためにDockerファイルを書き換えます。また、Proxy環境上でSubversionのProxy設定を別途やる必要が無くなります。

```diff
# Download examples
- RUN apt-get install -y --no-install-recommends subversion && \
-    svn checkout https://github.com/horovod/horovod/trunk/examples && \
-    rm -rf /examples/.svn
-
- WORKDIR "/examples"
+ RUN cd / && \
+    git clone https://github.com/uber/horovod.git -b v0.16.4 && \
+    rm -rf /horovod/.git
+    
+ WORKDIR "/horovod/examples"
```

その他実行環境に応じてDokcerファイルに必要事項を追記してください。因みに引数として渡したProxy情報等はビルド後消えてしまいます。

## Dockerファイルのビルド
Dockerファイルをビルドし、イメージファイルを作ります。記載のコマンドは社内Proxyを考慮したコマンドを記載しています。イメージタグ名(本記事ではhorovod-docker-2019-September)は任意の名前を付けてください。デフォルトでは最新版を表すlatestタグを記載している資料が多いです。今回は別バージョンとの共存の必要性から別名を付けています。

```bash
docker image build --force-rm --build-arg http_proxy=${http_proxy} --build-arg https_proxy=${https_proxy} -t horovod:horovod-docker-2019-September ./
```

イメージファイルの確認

```bash
# docker image ls | grep horovod-docker 
horovod                                        horovod-docker-2019-September   c8ed3f918584        9 minutes ago       7.79GB
```

# Dockerコンテナの起動

以下のコマンドを実行します。古いネットの記事ではnvidia-dockerで実行と記載されている場合が有りますが、現在nvidia-dockerの実行は--runtime=nvidiaを渡す仕様に変わっています。

```bash
docker container run -it --rm -p 12345:12345 -d --runtime=nvidia --name horovod-docker horovod:horovod-docker-2019-September
```

Dockerファイル内にProxy情報を直接書き込むのはセキュリティー上好ましくありません。故に、Dockerファイルには直接書き込まず、両マシン上で、`/etc/environment`に書き込んでおき、`docker container run --volume(-vオプション)`オプションで渡す方法も有ります。

```bash
docker container run -it --rm -p 12345:12345 -d --runtime=nvidia --volume=/etc/environment:/etc/environment --name horovod-docker horovod:horovod-docker-2019-September
```

起動確認

```bash
# docker container ls | grep horovod-docker 
4724b9e94a6b        horovod:horovod-docker-2019-September   "/bin/bash"              16 seconds ago      Up 15 seconds
                    horovod-docker
```

# メインマシンだけでサンプルソースを実行

分散環境の構築前に、メインマシンだけでサンプルソースが動くか試します。

## コンテナにログイン

```bash
docker container exec -it horovod-docker /bin/bash
```

## サンプルソースの実行

```bash
mpirun --allow-run-as-root -np 1 -H localhost:1 python keras_mnist_advanced.py
```

<details><summary>実行結果</summary><div>

```
/horovod/examples# time mpirun --allow-run-as-root -np 1 -H localhost:1 python keras_mnist_advanced.py
Using TensorFlow backend. 
2019-09-03 07:20:46.807102: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 AVX512F FMA
2019-09-03 07:20:47.020836: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1432] Found device 0 with properties:  
name: Quadro P4000 major: 6 minor: 1 memoryClockRate(GHz): 1.48
pciBusID: 0000:65:00.0
totalMemory: 7.93GiB freeMemory: 7.83GiB
2019-09-03 07:20:47.020868: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1511] Adding visible gpu devices: 0
2019-09-03 07:20:47.409998: I tensorflow/core/common_runtime/gpu/gpu_device.cc:982] Device interconnect StreamExecutor with strength 1 edge matrix: 
2019-09-03 07:20:47.410040: I tensorflow/core/common_runtime/gpu/gpu_device.cc:988]      0
2019-09-03 07:20:47.410049: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1001] 0:   N
2019-09-03 07:20:47.410260: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1115] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 7553 MB memory) -> physical GPU (device: 0, name: Quadro P4000, pci bus id: 0000:65:00.0, compute capability: 6.1)
x_train shape: (60000, 28, 28, 1) 
60000 train samples
10000 test samples
Epoch 1/24 
468/468 [==============================] - 18s 37ms/step - loss: 0.4606 - acc: 0.8546 - val_loss: 0.0462 - val_acc: 0.9849 
Unexpected end of /proc/mounts line `overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/UL43O2ONW7PVU55RLVWO56PBSQ:/var/lib/docker/overlay2/l/D5I5BQJ5RWNQ6FOEOK4BIIMZCC:/var/lib/docker/overlay2/l/LRIO2ZYIYXXJ4LZBTBY5BKDG36:/var/lib/docker/overlay2/l/XFBQUFCZ7Y2WHPIKP47NKS6CNM:/var/lib/docker/overlay2/l/PZ5BRRWRC3GUEK5YHRIFJIH5ZO:/var/lib/docker/overlay2/l/VL232KYNXUQN3UIWEVTKXO462I:/var/lib/docker/overlay2/l/R5FK4HKNKG7NMBGFAQCEWEBLS3:/var/lib/docker/overlay2/l/XRL2S7KJWAVAD7HKLTR4WULHOJ:/var/lib/docker/overlay2/l/6TONSZZSENN7D'
Unexpected end of /proc/mounts line `J6ZDKP4O6MCCQ:/var/lib/docker/overlay2/l/LTSGLK6CENJ2256EOESRU7LEBU:/var/lib/docker/overlay2/l/6CRSESMM5NBGI7GFGH3PD33OZA:/var/lib/docker/overlay2/l/3WV2PP5HRFZQJ5QGSMDTIAFCEM:/var/lib/docker/overlay2/l/ULGPD6V4SZOHFTVZBI7K5ADWCY:/var/lib/docker/overlay2/l/LNMJHOYTMLCW4DBSCSGFJMUYPB:/var/lib/docker/overlay2/l/KUVWQZCTBIQPJCDH2UYZWPJOEL:/var/lib/docker/overlay2/l/LBPVPLDYLSA73BULYASYSAY6CN:/var/lib/docker/overlay2/l/SN7RTYBEOAVPDPK7X2RJZFYFS5:/var/lib/docker/overlay2/l/UCVZKXBTZXMANZHSKPD5JVUTW5:/var/lib/do'
Epoch 2/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.1734 - acc: 0.9486 - val_loss: 0.0403 - val_acc: 0.9870 
Epoch 3/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.1340 - acc: 0.9608 - val_loss: 0.0282 - val_acc: 0.9902 
Epoch 4/24 
468/468 [==============================] - 16s 35ms/step - loss: 0.1159 - acc: 0.9653 - val_loss: 0.0281 - val_acc: 0.9905 
Epoch 5/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.1048 - acc: 0.9691 - val_loss: 0.0325 - val_acc: 0.9894 
 
Epoch 5: finished gradual learning rate warmup to 1.
Epoch 6/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0927 - acc: 0.9717 - val_loss: 0.0238 - val_acc: 0.9919 
Epoch 7/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0879 - acc: 0.9732 - val_loss: 0.0249 - val_acc: 0.9921 
Epoch 8/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0839 - acc: 0.9752 - val_loss: 0.0242 - val_acc: 0.9920 
Epoch 9/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0812 - acc: 0.9764 - val_loss: 0.0231 - val_acc: 0.9922 
Epoch 10/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0799 - acc: 0.9766 - val_loss: 0.0225 - val_acc: 0.9925 
Epoch 11/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0800 - acc: 0.9768 - val_loss: 0.0254 - val_acc: 0.9920 
Epoch 12/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0782 - acc: 0.9775 - val_loss: 0.0244 - val_acc: 0.9922 
Epoch 13/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0753 - acc: 0.9783 - val_loss: 0.0221 - val_acc: 0.9922 
Epoch 14/24 
468/468 [==============================] - 16s 33ms/step - loss: 0.0748 - acc: 0.9782 - val_loss: 0.0233 - val_acc: 0.9927 
Epoch 15/24 
468/468 [==============================] - 16s 33ms/step - loss: 0.0697 - acc: 0.9787 - val_loss: 0.0236 - val_acc: 0.9926 
Epoch 16/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0719 - acc: 0.9794 - val_loss: 0.0257 - val_acc: 0.9919 
Epoch 17/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0699 - acc: 0.9796 - val_loss: 0.0211 - val_acc: 0.9930 
Epoch 18/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0658 - acc: 0.9813 - val_loss: 0.0210 - val_acc: 0.9936 
Epoch 19/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0661 - acc: 0.9808 - val_loss: 0.0210 - val_acc: 0.9935 
Epoch 20/24 
468/468 [==============================] - 16s 33ms/step - loss: 0.0661 - acc: 0.9811 - val_loss: 0.0200 - val_acc: 0.9946 
Epoch 21/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0654 - acc: 0.9816 - val_loss: 0.0224 - val_acc: 0.9932 
Epoch 22/24 
468/468 [==============================] - 16s 34ms/step - loss: 0.0630 - acc: 0.9814 - val_loss: 0.0197 - val_acc: 0.9939 
Epoch 23/24 
468/468 [==============================] - 16s 33ms/step - loss: 0.0637 - acc: 0.9822 - val_loss: 0.0215 - val_acc: 0.9938 
Epoch 24/24 
468/468 [==============================] - 15s 33ms/step - loss: 0.0605 - acc: 0.9830 - val_loss: 0.0214 - val_acc: 0.9934 
Test loss: 0.0218829112259677 
Test accuracy: 0.9934

real    6m24.112s
user    6m0.316s
sys     0m19.704s

```
</div></details>

メインマシン単独での実行では、約6分30秒程の時間を要しました。いよいよ分散学習環境を構築して分散学習を実施します。

サンプル実行後、コンテナからログアウトします。

```bash
exit
```

# 分散学習環境の構築

メインマシン単独でサンプルソースを実行しました。続いて、分散学習環境を構築しhorovodによる分散学習を実現します。

## 前準備：コンテナイメージの保存及びスレーブマシンへの転送

スレーブマシン側でもコンテナイメージファイルが必要になります。0からイメージファイルを作るのではなく、メインマシン側でビルドしたイメージファイルをtarファイルにし、転送します。

### コンテナイメージファイルのtarファイル化

以下のコマンドを実行します。

```bash
docker image save horovod:horovod-docker-2019-September > ./horovod-docker-2019-September.tar
```

### 双方向通信の設定
ssh認証でメインマシンとスレーブマシン間で双方向通信が出来るようにしておきます。

参考
[【秘密鍵/公開鍵】コピペで出来る！鍵認証の設定方法]("https://qiita.com/nnahito/items/dbe6fbfe347cd66ae7e6")

### tarファイルをスレーブマシンに転送
以下のコマンドを実行します。

```bash
scp ./horovod-docker-2019-September.tar <USER>@<SLAVEMACHINE_IP>:/root/
```

## コンテナイメージのロード @ スレーブマシーン

スレーブマシン側で以下のコマンドを実行

```bash
docker image load < horovod-docker-2019-September.tar
```

イメージファイルの確認 @ スレーブマシン

```bash
# docker image ls | grep horovod-docker 
horovod                                           horovod-docker-2019-September   c8ed3f918584        About an hour ago   7.79GB
```

# スレーブマシンでコンテナにログイン
双方向通信、ポート開放を終えたら再度コンテナにログインします。
まず、スレーブマシン側でコンテナにログインし、スレーブマシン用のコマンドを実行します。

## コンテナにログイン

```bash
docker container run -it --network=host --runtime=nvidia --volume=/mnt/share/ssh:/root/.ssh horovod:horovod-docker-2019-September bash -c "/usr/sbin/sshd -p 12345; sleep infinity"
```

これでスレーブ側のコンテナが実行状態のままになります。Proxy情報などの環境情報をオプションで渡す場合は以下のコマンドを実行します。

```bash
docker container run -it --network=host --runtime=nvidia --volume=/mnt/share/ssh:/root/.ssh --volume=/etc/environment:/etc/environment  horovod:horovod-docker-2019-September bash -c "/usr/sbin/sshd -p 12345; sleep infinity"
```

# メインマシンでコンテナにログイン
メインマシンでもコンテナにログインします。メインマシンはもう少し設定が必要です。

```bash
docker container exec -it horovod-docker /bin/bash
```

## hostfileの作成
ログイン後、作業ディレクトリ(/horovod/examples)直下にhostfileを作成します。以下の内容を記載し、作成します。

```config
MAIN_IP slots=1
SLAVE_IP slots=1
```
MAIN_IPにはメインマシンのIPアドレス、SLAVE_IPにはスレーブマシンのIPアドレスを記載します。

## NCCL関連の設定

* configファイルの内容の確認

```bash
# cat /etc/nccl.conf
cat: /etc/nccl.conf: No such file or directory
```

* 以下の内容を追記

```bash
echo "NCCL_DEBUG=INFO" >> /etc/nccl.conf
echo "NCCL_SOCKET_IFNAME=enp0s31f6" >> /etc/nccl.conf
```

## File Openのマックスサイズの変更

```bash
echo "root soft nofile 2048" >> /etc/security/limits.conf 
echo "root hard nofile 2048" >> /etc/security/limits.conf
```

## .bashrcに以下の内容を追加し有効化

```
vim ~/.bashrc
```

```bash

MPI_PATH=/usr/local/openmpi
PATH=/usr/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/bin:/usr/local/clang/bin
LD_LIBRARY_PATH=/usr/local/lib:/opt/clBLAS/lib64:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/openmpi/lib
HTTPS_PROXY=<HTTPS_PROXY>:<PORT>
HTTP_PROXY=<HTTP_PROXY>:<PORT>
```

* 下の２行は必要な場合のみ記載して下さい。

```bash
source ~/.bashrc
```

## ソースの実行 @ メインマシン

```bash
 mpirun --allow-run-as-root -np 2 -hostfile ./hostfile  -mca plm_rsh_args "-p 12345"  --mca pml ob1 --mca btl tcp,self python keras_mnist_advanced.py
```

# Infrastructure as Codeを目指して
本記事で扱った内容を反映したDockerfileの内容は以下の様になります。hostファイル作成部分の設定値についていは上述の行程を参考に修正してください。

<details><summary>Dockerfile</summary><div>

```dockerfile

FROM nvidia/cuda:9.0-devel-ubuntu16.04

# TensorFlow version is tightly coupled to CUDA and cuDNN so it should be selected carefully
ENV TENSORFLOW_VERSION=1.12.0
ENV PYTORCH_VERSION=1.1.0
ENV TORCHVISION_VERSION=0.2.2.post3
ENV CUDNN_VERSION=7.4.1.5-1+cuda9.0
ENV NCCL_VERSION=2.3.7-1+cuda9.0
ENV MXNET_VERSION=1.4.1
# Add IP info
ENV MAIN_IP=
ENV SLAVE_IP=
# Add Proxy info if necessary
ENV HTTP_PROXY=
ENV HTTPS_PROXY=
ENV NO_PROXY=
ENV HTTP_PROXY2=
ENV HTTPS_PROXY2=

# Python 2.7 or 3.5 is supported by Ubuntu Xenial out of the box
ARG python=2.7
ENV PYTHON_VERSION=${python}

RUN apt-get update && apt-get install -y --allow-downgrades --allow-change-held-packages --no-install-recommends \
        build-essential \
        cmake \
        git \
        curl \
        vim \
        wget \
        ca-certificates \
        libcudnn7=${CUDNN_VERSION} \
        libnccl2=${NCCL_VERSION} \
        libnccl-dev=${NCCL_VERSION} \
        libjpeg-dev \
        libpng-dev \
        python${PYTHON_VERSION} \
        python${PYTHON_VERSION}-dev

RUN ln -s /usr/bin/python${PYTHON_VERSION} /usr/bin/python

RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
    python get-pip.py && \
    rm get-pip.py

# Install TensorFlow, Keras, PyTorch and MXNet
RUN pip install 'numpy<1.15.0' tensorflow-gpu==${TENSORFLOW_VERSION} keras h5py torch==${PYTORCH_VERSION} torchvision==${TORCHVISION_VERSION} mxnet-cu90==${MXNET_VERSION}

# Install Open MPI
RUN mkdir /tmp/openmpi && \
    cd /tmp/openmpi && \
    wget https://www.open-mpi.org/software/ompi/v4.0/downloads/openmpi-4.0.0.tar.gz && \
    tar zxf openmpi-4.0.0.tar.gz && \
    cd openmpi-4.0.0 && \
    ./configure --enable-orterun-prefix-by-default && \
    make -j $(nproc) all && \
    make install && \
    ldconfig && \
    rm -rf /tmp/openmpi

# Install Horovod, temporarily using CUDA stubs
RUN ldconfig /usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs && \
    HOROVOD_GPU_ALLREDUCE=NCCL HOROVOD_WITH_TENSORFLOW=1 HOROVOD_WITH_PYTORCH=1 HOROVOD_WITH_MXNET=1 pip install --no-cache-dir horovod && \
    ldconfig

# Install OpenSSH for MPI to communicate between containers
RUN apt-get install -y --no-install-recommends openssh-client openssh-server && \
    mkdir -p /var/run/sshd

# Allow OpenSSH to talk to containers without asking for confirmation
RUN cat /etc/ssh/ssh_config | grep -v StrictHostKeyChecking > /etc/ssh/ssh_config.new && \
    echo "    StrictHostKeyChecking no" >> /etc/ssh/ssh_config.new && \
    mv /etc/ssh/ssh_config.new /etc/ssh/ssh_config

# Download examples
RUN cd / && \
   git clone https://github.com/uber/horovod.git -b v0.16.4 && \
   rm -rf /horovod/.git

# Shell for single machine
RUN echo "#!/bin/bash" > /horovod/examples/exec_horovod_single_machine.sh && \
echo "mpirun --allow-run-as-root -np 1 -H localhost:1 python \$1" >> /horovod/examples/exec_horovod_single_machine.sh && \
chmod +x /horovod/examples/exec_horovod_single_machine.sh

# Shell for main machine
RUN echo "#!/bin/bash" > /horovod/examples/exec_horovod_main_machine.sh && \
echo "mpirun --allow-run-as-root -np 2 -hostfile ./hostfile  -mca plm_rsh_args \"-p 12345\" --mca pml ob1 --mca btl tcp,self python keras_mnist_advanced.py" >> /horovod/examples/exec_horovod_main_machine.sh && \
chmod +x /horovod/examples/exec_horovod_main_machine.sh

# hostfile for main machine
RUN echo ${MAIN_IP} " slots=1" > /horovod/examples/hostfile && \
echo ${SLAVE_IP} " slots=1" >> /horovod/examples/hostfile

# Add info into NCCL conf
RUN echo "NCCL_DEBUG=INFO" >> /etc/nccl.conf && \
echo "NCCL_SOCKET_IFNAME=enp0s31f6" >> /etc/nccl.conf

# Add info of Max File Open size
RUN echo "root soft nofile 2048" >> /etc/security/limits.conf && \
echo "root hard nofile 2048" >> /etc/security/limits.conf

# Add Open MPI info into bashrc
RUN echo "MPI_PATH=/usr/local/openmpi" >> ~/.bashrc && \
echo "PATH=/usr/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/bin:/usr/local/clang/bin" >> ~/.bashrc && \
echo "LD_LIBRARY_PATH=/usr/local/lib:/opt/clBLAS/lib64:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/openmpi/lib" >> ~/.bashrc && \
echo "HTTPS_PROXY=${HTTPS_PROXY2}" >> ~/.bashrc && \
echo "HTTP_PROXY=${HTTP_PROXY2}" >> ~/.bashrc
 
WORKDIR "/horovod/examples"

```
</div></details>

コンテナログイン後に、

* メインマシンのみで実行 : `./exec_horovod_single_machine.sh`
* 分散学習メインマシン : `./exec_horovod_main_machine.sh`

# Reference
https://tech.ckme.co.jp/ssh_config.shtml

https://qiita.com/namahoge/items/542ba95fd3892b57d6fb

https://qiita.com/iaoiui/items/fc318fa75cce3227b638

https://qiita.com/matsu911/items/72e0bb6224a30d205739

https://qiita.com/rock619/items/2bbbe379ae0fdd2ae00f

https://qiita.com/sonoshou/items/cc2b740147ba1b8da1f3