docker (nvidia-docker) を使ってマルチノードで ChainerMN を実行する方法(仮)

docker (nvidia-docker) を使ってマルチノードで ChainerMN を実行する方法(仮)

編集履歴: (2017/12/14) `nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04` を使う場合 `nccl-repo-ubuntu1604-2.0.5-ga-cuda9.0_3-1_amd64.deb` をダウンロードして配置しなくても `apt-get install libnccl2 libnccl-dev` できたので変更

# シングルノード

シングルノードなら特に難しい所はない。docker コンテナ内に OpenMPI や ChainerMN をインストールして、nvidia-docker run で mpiexec すれば良い。

## Dockerfile

```docker
FROM nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04

RUN apt-get update \
  && apt-get install -y --no-install-recommends python3-dev python3-pip \
  && pip3 install setuptools && pip3 install --upgrade pip \
  && apt-get install -y --no-install-recommends git m4 autoconf automake libtool flex \
  && apt-get install -y --no-install-recommends libnccl2 libnccl-dev \
  && apt-get install -y --no-install-recommends ssh \
  && apt-get clean
RUN git clone --depth=1 -b v3.0.0 https://github.com/open-mpi/ompi.git /tmp/ompi \
  && cd /tmp/ompi \
  && ./autogen.pl \
  && ./configure --with-cuda \
  && make -j4 \
  && make install \
  && rm -rf /tmp/ompi

ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
ENV CPATH=/usr/local/include:$CPATH
ENV CUDA_PATH=/usr/local/cuda
ENV PATH=$CUDA_PATH/bin:$PATH
ENV CPATH=$CUDA_PATH/include:$CPATH
ENV LD_LIBRARY_PATH=$CUDA_PATH/lib64:$CUDA_PATH/lib:$LD_LIBRARY_PATH

RUN pip3 install cupy==3.0.0a1 chainer==4.0.0a1
RUN pip3 install cython && pip3 install chainermn
```

## ビルド

```
mkdir docker
vim docker/Dockerfile
cd docker
nvidia-docker build -t chainermn .
```

## 実行

docker コンテナ内で mpiexec する

```
$ git clone https://github.com/chainer/chainermn # examples が欲しいだけ
$ nvidia-docker run -v $(pwd):/mnt $HOME/.chainer:/root/.chainer chainermn \
  mpiexec --allow-run-as-root -n 8 \
  python3 /mnt/chainermn/examples/mnist/train_mnist.py --gpu
```

-n 8 はGPU数にあわせて調整

# マルチノード

mpiexec すると、ホストそれぞれに ssh して、mpiexec の引数のコマンドを実行する。
ホストAのコンテナから、ホストBのコンテナにsshできるようにする必要がある。

## Dockerfile

```docker
FROM nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04

RUN apt-get update \
  && apt-get install -y --no-install-recommends python3-dev python3-pip \
  && pip3 install setuptools && pip3 install --upgrade pip \
  && apt-get install -y --no-install-recommends git m4 autoconf automake libtool flex \
  && apt-get install -y --no-install-recommends libnccl2 libnccl-dev \
  && apt-get install -y --no-install-recommends ssh \
  && apt-get clean
RUN git clone --depth=1 -b v3.0.0 https://github.com/open-mpi/ompi.git /tmp/ompi \
  && cd /tmp/ompi \
  && ./autogen.pl \
  && ./configure --with-cuda \
  && make -j4 \
  && make install \
  && rm -rf /tmp/ompi

ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
ENV CPATH=/usr/local/include:$CPATH
ENV CUDA_PATH=/usr/local/cuda
ENV PATH=$CUDA_PATH/bin:$PATH
ENV CPATH=$CUDA_PATH/include:$CPATH
ENV LD_LIBRARY_PATH=$CUDA_PATH/lib64:$CUDA_PATH/lib:$LD_LIBRARY_PATH

RUN apt-get install -y --no-install-recommends openssh-server \
  && mkdir /var/run/sshd \
  && sed -i 's/Port 22/Port 10022/' /etc/ssh/sshd_config \
  && ssh-keygen -t rsa -N '' -f /root/.ssh/id_rsa \
  && cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys \
  && chmod 600 /root/.ssh/authorized_keys \
  && echo 'Host *' > /root/.ssh/config \
  && echo '  Port 10022' >> /root/.ssh/config \
  && echo '  StrictHostKeyChecking no' >> /root/.ssh/config \
  && echo '  UserKnownHostsFile /dev/null' >> /root/.ssh/config

RUN echo 'export CUDA_PATH="/usr/local/cuda"\n\
export PATH="$CUDA_PATH/bin:$PATH"\n\
export CPATH="$CUDA_PATH/include:$CPATH"\n\
export LD_LIBRARY_PATH="$CUDA_PATH/lib64:$CUDA_PATH/lib:$LD_LIBRARY_PATH"\n\
export PATH="/usr/local/nvidia/bin:$PATH"\n\
export LD_LIBRARY_PATH="/usr/local/nvidia/lib:/usr/local/nvidia/lib64:$LD_LIBRARY_PATH"\n\
export NCCL_SOCKET_IFNAME=^docker0' > /etc/profile.d/chainermn.sh

RUN pip3 install cupy==3.0.0a1 chainer==4.0.0a1
RUN pip3 install cython && pip3 install chainermn

EXPOSE 10022
```

シングルノード用の Dockerfile に対して、sshd のセットアップと、ssh したときに必要な環境変数を設定できるように `/etc/profile.d/chainermn.sh` ファイルの作成を追加している。

ホストに向けたMPI通信をコンテナに簡易にフォワードするため、またネットワーク速度向上のため、docker run 時には `--net=host` を指定する。ホストの sshd 22 番ポートと被らないように、コンテナの sshd は 10022 番ポートで起動するようにしている。

## ビルド

```
mkdir docker
vim docker/Dockerfile
cd docker
nvidia-docker build -t chainermn .
```

※ ここでビルドしたイメージを全ホストで利用すること。今回の Dockerfile では、それぞれのホストで docker build すると、別の ssh 鍵ができてコンテナ間 ssh できなくなってしまう。

## 実行

**従属ホスト(ホストB)**

コンテナで sshd を起動だけしておく

```
$ git clone https://github.com/chainer/chainermn # examples が欲しいだけ
$ nvidia-docker run --rm -it --net=host \
  -v $(pwd):/mnt -v $HOME/.chainer:/root/.chainer chainermn \
  /bin/bash -lc '/etc/init.d/ssh start && /bin/bash'
```

**マスターホスト(ホストA)**

hostfile を用意。cpu=N に並列実行数を指定する。

```txt:hostfile
ホストA cpu=8
ホストB cpu=8
```

実行

```
$ git clone https://github.com/chainer/chainermn # examples が欲しいだけ
$ nvidia-docker run --rm -it --net=host \
  -v $(pwd):/mnt -v $HOME/.chainer:/root/.chainer chainermn \
  /bin/bash -lc '/etc/init.d/ssh start && \
  mpiexec --allow-run-as-root --mca btl_tcp_if_exclude docker0,lo --hostfile /mnt/hostfile \
  /bin/bash -lc "python3 /mnt/chainermn/examples/mnist/train_mnist.py --gpu"'
```

今回は、実行ファイル(chainermn の examples)を各ホストに配置しているが、Docker イメージに含める方法もあるとは思う。chainermn のルールとして、全ホストに実行ファイルがないとダメ。

## 便利スクリプト

全従属ホストでコンテナを立ち上げて、マスターホストで mpiexec を実行する便利スクリプト。
git clone https://github.com/chainer/chainermn は実行したいスクリプトに合わせてご編集ください。
python3 /mnt/chainermn/examples/mnist/train_mnist.py --gpu も実行したいスクリプトに合わせてご編集ください。

```bash:run_chainermn.sh
#!/bin/bash

PREPARE_SCRIPT=/tmp/run_chainermn_prepare.sh
SLAVE_SCRIPT=/tmp/run_chainermn_slave.sh
MASTER_SCRIPT=/tmp/run_chainermn_master.sh
 
cat <<'EOF' > $PREPARE_SCRIPT
git clone https://github.com/chainer/chainermn chainermn
EOF
 
cp $PREPARE_SCRIPT $SLAVE_SCRIPT
cat <<'EOF' >> $SLAVE_SCRIPT
nvidia-docker run --rm -it --net=host \
  -v $(pwd):/mnt -v $HOME/.chainer:/root/.chainer chainermn \
  /usr/sbin/sshd -D
EOF

cp $PREPARE_SCRIPT $MASTER_SCRIPT
cat <<'EOF' >> $MASTER_SCRIPT
nvidia-docker run --rm -it --net=host \
  -v $(pwd):/mnt -v $HOME/.chainer:/root/.chainer chainermn \
  /bin/bash -lc '/etc/init.d/ssh start && \
  mpiexec --allow-run-as-root --mca btl_tcp_if_exclude docker0,lo --hostfile /mnt/hostfile \
  /bin/bash -lc "python3 /mnt/chainermn/examples/mnist/train_mnist.py --gpu"'
EOF
 
master_host=$(hostname)
slave_hosts=$(grep -v ${master_host} hostfile | awk '{print $1}')
hosts=$(cat hostfile | awk '{print $1}')
n_hosts=$(cat hostfile | wc -l)
 
echo "$slave_hosts" | xargs -I{} -P${n_hosts} echo scp $SLAVE_SCRIPT {}:/tmp/chainermn.sh
echo "$slave_hosts" | xargs -I{} -P${n_hosts} scp $SLAVE_SCRIPT {}:/tmp/chainermn.sh
echo cp $MASTER_SCRIPT /tmp/chainermn.sh
cp $MASTER_SCRIPT /tmp/chainermn.sh
 
function cleanup() {
  echo "$hosts" | xargs -I{} -P${n_hosts} echo ssh {} docker stop chainermn
  echo "$hosts" | xargs -I{} -P${n_hosts} ssh {} docker stop chainermn
}
trap cleanup EXIT

echo "$hosts" | xargs -I{} -P${n_hosts} echo ssh -tt {} /bin/bash /tmp/chainermn.sh
echo "$hosts" | xargs -I{} -P${n_hosts} ssh -tt {} /bin/bash /tmp/chainermn.sh
```

hostfile を用意して、実行

```
bash run_chainermn.sh
```

Ctrl-C するとリモートホストの docker コンテナが stop される (ちょっと時間かかるのでお待ちください)


# トラブルシューティング

See also [AWS GPU インスタンスにおける ChainerMN の分散効率に対する評価#トラブルシューティング](https://qiita.com/sonots/items/22384bbc61284f2fdf94#%E3%81%8A%E3%81%BE%E3%81%91-%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)


## Unexpected end of /proc/mounts line `overlay

コンテナ内で mpiexec すると以下のような warning が出る

```
Unexpected end of /proc/mounts line `overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/ETGN4JWRCAAOR5SDTEX4LJN7Z6:/var/lib/docker/overlay2/l/6GKXZXZ25OYNPLCCTBNRKHDQG6:/var/lib/docker/overlay2/l/OUCL4BE45M72TBGXLVGOUGULMH:/var/lib/docker/overlay2/l/DHW4TE7ZW55P5UOZBX547LDZ77:/var/lib/docker/overlay2/l/YSIOMV4IYP3T2YVN77UFZFQ5XI:/var/lib/docker/overlay2/l/F5AUK2J5LITCBCVWGBCJD3LETY:/var/lib/docker/overlay2/l/ZPR6AAOCLV6SOQA7I6DIDMRY7G:/var/lib/docker/overlay2/l/UE6YIXVHWKAVHG2UJAMUWEUE5Y:/var/lib/docker/overlay2/l/UY4QMTUE5XDUS'
Unexpected end of /proc/mounts line `INR6Y7546V2HV:/var/lib/docker/overlay2/l/F6JQBSMJYZOOQ3QIHYUG3VVHMW:/var/lib/docker/overlay2/l/VIP7WZGLNKZMRDKH6KBINRIJSW:/var/lib/docker/overlay2/l/TDLKNGLFEVYF4SI73USXIW4A4A:/var/lib/docker/overlay2/l/VZ6RLPGCUQFQ4K5HHWOSZXXK75:/var/lib/docker/overlay2/l/UVHFPFXQRL3XJS32BZRUMP6QEP:/var/lib/docker/overlay2/l/STHMTEHKNZWTCGWFDSYT6J3ND4:/var/lib/docker/overlay2/l/L7ANIC2YQAOSWMBVRR6QZS5SZ2:/var/lib/docker/overlay2/l/DC7BAJG5WLKXZVJJM73DGCNIKE:/var/lib/docker/overlay2/l/4W423XWKPJV3WINYECSJQ7JBY3:/var/lib/do'
Unexpected end of /proc/mounts line `cker/overlay2/l/FJF6DA25TZWDVGOMCZ7J7PRT7Q:/var/lib/docker/overlay2/l/4PHONTLPJTZAVDXCRN4W2BCQPW:/var/lib/docker/overlay2/l/RU7ZODNEXSEKWG5J4UU53AYGDB:/var/lib/docker/overlay2/l/6VF7XXRRSZC2HH24Z3JJRVZBVA:/var/lib/docker/overlay2/l/M3WQNRNV5IA3KXFBHV5GY3KD6U:/var/lib/docker/overlay2/l/ML2ZZUHFRV5HELDLE5O5NDWPLX:/var/lib/docker/overlay2/l/T535AZCEOGAX5BT2XGDEBQ3RH3:/var/lib/docker/overlay2/l/4SZPXUFR4LU2WRUTITUPSOPIJE,upperdir=/var/lib/docker/overlay2/333f260a32ed9645c209c166c9e2d6ce807ec87b0ee351a86dc365fa'
```


https://stackoverflow.com/questions/46138549/docker-openmpi-and-unexpected-end-of-proc-mounts-line この問題らしいが、無視して動作したので無視した。

## MCA parameter "plm_rsh_agent" was set to a path

```
The value of the MCA parameter "plm_rsh_agent" was set to a path
that could not be found:

  plm_rsh_agent: ssh : rsh

Please either unset the parameter, or check that the path is correct
```

apt-get install ssh して解決

# 今後の発展

マルチノードでの docker コンテナの管理を、docker swarn や kubernetes のようなコンテナクラスタ管理ツールを使って行えるようにすると立ち上げるのが簡単になりそう。

一方で、うまく MPI や GPU を扱えるようにするには設定が難しそう (少なくも GKE のようなマネージドサービスを使うのは難しそう)。https://github.com/NLKNguyen/alpine-mpich が参考になりそう (docker swarn を使っている)。