- はじめに -
ChainerMNがついに本家Chainerにマージされました。分散深層学習への本気度が伺えます。
節目という事で、Dockerを利用して複数ノードでChainerMNするために行った事のメモをTips形式で残しておこうという記事です。
私は半年くらい前からこの記事の内容を使っているのでアップデートがあるかもしれません。
加えて、最近はkubernetesを使うのが流暢で、PFNさんも公式ブログ書いてるし多分k8sが良いと思います。
ChainerMN on Kubernetes with GPUs
(私もk8s挑戦してるけどあんまり上手くいってなくて放置中です…)
この記事はk8s使えないけどDocker使えるような分散環境はあるみたいなニッチな需要を満たすかもしれないなあというレベル感の記事です。
- - はじめに -
- - 前提と参考文献 -
- - Docker環境構築 -
- - ノード間のsshの実現 -
- - mpiexecを用いたChainerMNの実行 -
- - エラー対応とか -
- mpiexec実行しても全く何も表示されない!
- orterun was unable to launch the specified application as it could not access or execute an executable ~~~
- mpiexecが動いてそうだけど学習前に止まる!
- ORTE does not know how to route a message to the specified daemon located on the indicated node ~~~
- クソ遅い!received unexpected process identifier!
- It appears as if there is not enough space for (the shared-memory backing
- Failed, NCCL error nvidia-sample.cu:88 'unhandled system error
- - おわりに -
- 前提と参考文献 -
記事前提として以下を想定しています。
また、私が環境構築した際に読んだ参考文献を示します。
- https://chainermn.readthedocs.io/en/latest/installation/index.html
- 高火力(時間課金)でChainerMNを試す - Fixstars Tech Blog /proc/cpuinfo
- chainerMNを試してみる - Qiita
- AWS GPU インスタンスにおける ChainerMN の分散効率に対する評価 - Qiita
- ChianerMNによる分散深層学習 - kumilog.net
ひとまず、上の5つに目を通せば動くような気がします。
加えて、細かなエラーでStackOverflowやOpenMPIの公式リファレンスを読んでいますがそちらは随時記載します。
Deep Learning分散の仕組みについては、秋葉さん、鈴木さんの記事とスライドに任せます。
- Docker環境構築 -
cuda周り以外はまっさらのUbuntu 16.04 のDocker想定。
apt-get upgrade apt-get update apt-get install python3-pip python3-dev apt-get install python-pip python-dev apt-get install wget git
バックエンドにOpenMPI、NCCLを利用する想定でインストールを進める。
OpenMPIのインストール
apt-get install infiniband-diags opensm ibverbs-utils infiniband-diags perftest wget https://download.open-mpi.org/release/open-mpi/v3.0/openmpi-3.0.1.tar.gz tar -zxvf openmpi-3.0.1.tar.gz cd openmpi-3.0.1 ./configure --with-cuda --prefix=$HOME/local/openmpi --with-openib make -j4 make install touch ~/.bashrc echo 'export LD_LIBRARY_PATH=$HOME/local/openmpi/lib:${LD_LIBRARY_PATH}' >> ~/.bashrc echo 'export PATH=$HOME/local/openmpi/bin:${PATH}' >> ~/.bashrc
NCCLなるGPU通信ライブラリのインストールは、以下のリンクからダウンロードする。
(NVIDIA Developerの登録を済ませておく必要がある)
https://developer.nvidia.com/nccl/nccl-download
ライセンス読んで「I Agree~」をチェックした後、debファイルを以下からダウンロードしてくる。
Docker内にダウンロードしてきたファイルを設置する。
# コンテナ内に送る sudo docker cp nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb [CONTAINER ID]:/hogehoge # docker attach後 dpkg -i nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb apt update apt install libnccl2 libnccl-dev echo 'export NCCL_ROOT=/usr/local/nccl' >> ~/.bashrc echo 'export CPATH=$NCCL_ROOT/include:$CPATH' >> ~/.bashrc echo 'export LD_LIBRARY_PATH=$NCCL_ROOT/lib/:$LD_LIBRARY_PATH' >> ~/.bashrc echo 'export LIBRARY_PATH=$NCCL_ROOT/lib/:$LIBRARY_PATH' >> ~/.bashrc source ~/.bashrc
追記:2018/09/19
エゴサしてたらNCCL情報が
ncclのインストール昔は大変でしたが、いまは nvidia/cuda:cudnn7... とかに入っているのでそっちを使うのがいいと思います。 cupy-cuda92 とかでも入ります / “複数ノードDockerでChain…” https://t.co/blilumnssI
— Kota Uenishi (๑•̀ㅂ•́)و✧ (@kuenishi) 2018年9月19日
この後一番陥りやすいのが相互にsshする環境の構築である。
複数ノードの場合「ノード間はssh-keyでパスワードなしでssh可」「dockerに入るにはパスワードなしで」を実現しなければならず、通信できない場合にOpenMPIが無反応だったりしてわからんってなるのでちゃんとやる。
apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev language-pack-ja apt-get -y install openssh-server ufw curl mkdir /var/run/sshd # ユーザを作って、そのユーザでsshできるようにする(vaaaaanquishの所をよしなに) useradd -m vaaaaanquish && echo "vaaaaanquish:vaaaaanquish" | chpasswd && gpasswd -a vaaaaanquish sudo mkdir -p /home/vaaaaanquish/.ssh chmod 700 /home/vaaaaanquish/.ssh passwd -d root passwd -d vaaaaanquish
鍵はDockerコンテナ内では生成せず、ノードからマウントしてくる形を取る(結局ノード間自体もsshできないと意味ないので)。
/etc/ssh/sshd_configを以下のように変更する(なければ作る)
ここで指定するPortは、Dockerからもアクセスするので今後統一して選べる所にする。
Port 2223
PermitEmptyPasswords yes
PasswordAuthentication no
PubkeyAuthentication yes
UsePAM no
また、rootから~/.ssh/configの最期の方に以下も追記する(なければ作る)
Host *
Port 2223
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
最後にcythonとChanierMNをインストールする。
cupyのcudaNNのバージョン等を絶対間違えないよう確認する
pip3 install cython
pip3 install cupy-cuda90
pip3 install chainermn
# Chainer試すためのMNISTパッケージもここで
pip3 install python-mnist
dockerコンテナからデタッチする前に、以下のファイルをルート配下辺りに「init.sh」のような名前で置いておく。
#!/bin/bash NCCL_SOCKET_IFNAME=^docker0 /usr/sbin/service ssh start /bin/bash
NCCLがDockerの作る仮想ソケットを利用してしまうらしいのでNCCL_SOCKET_IFNAME=^docker0は必須
参考:http://tabisurubiker.hatenadiary.jp/entry/2017/10/02/092952
Dockerコマンドでrunする際に実行するプログラムとして、このinit.shを実行、sshサーバを立ち上げて、bashで待機させるようにする。
待機しているdockerコンテナに対してOpenMPIがプロセス振ってくれるイメージ。
これで多分できたのでcommitしてsaveしてDockerコンテナは終了。
- ノード間のsshの実現 -
前述したようにノード間はsshできないとダメ。
実現する方法はいくつかあるがが、私はアホなので全ノードでssh-keygenして全ノードに配ればええやんと思ってスクリプト書きました。これで作ったkeyをマウントする形を取っている。ssh_generator.pyなる名前で以下のファイルを作って実行するだけ。自身をfabricで読んで各サーバに送る動作をpexpectで実行するアホみたいなスクリプトです。
# -*- coding: utf-8 -*- # must : pip install pexpect, fabric3 from fabric.api import * import pexpect import sys HOSTS = ['hogehoge', piyopiyo] # 利用予定のサーバ env.user = 'vaaaaanquish' # localからサーバにアクセスするユーザ env.key_filename = 'local key' # localからサーバにアクセスするためのkey env.password = PASSWORD # localからサーバにアクセスするためのkey pass def ssh_gen(): run('ssh-keygen -t rsa') def ssh_copy_id(y): run('ssh-copy-id -i /home/{}/.ssh/id_rsa.pub {}'.format(USER, y)) if __name__ == '__main__': for x in HOSTS: # 作るやつ print('\n[Making key : {}]'.format(x)) cmd= "fab -f ssh_generator.py -H {} ssh_gen".format(x) child = pexpect.spawn(cmd, encoding='utf-8') child.logfile = sys.stdout while 1: i=child.expect([ r'^.*(Enter file in which to save the key).*', r'.*(Enter passphrase).*', r'.*(Enter same passphrase again).*', r'.*(Overwrite).*\?', pexpect.EOF, pexpect.TIMEOUT], timeout=5) if i in [0,1,2]: child.sendline('') if i == 3: child.sendline('y') child.sendline('') if i in [4, 5]: break # 配るやつ print('\n[Sending key : {}]'.format(x)) for y in HOSTS: cmd= "fab -f ssh_generator.py -H {} ssh_copy_id:{}".format(x,y) child = pexpect.spawn(cmd, encoding='utf-8') child.logfile = sys.stdout while 1: i = child.expect([ r".*(password).*", pexpect.EOF, pexpect.TIMEOUT], timeout=5) if i == 0: child.sendline('{}'.format(PASSWORD)) child.sendline('') if i in [1,2]: break
これをローカルPCで実行し、鍵配り作業とする。各サーバの~/.ssh/配下に鍵がそれぞれ登録されます。
(基本的に使い終わったらこの鍵を削除しておかないと危ういのでちゃんとやりましょう)
踏み台等を利用する環境でも「env.gateway = [GATEWAY_SERVER]」のようにする事で解決可能です。
- mpiexecを用いたChainerMNの実行 -
以下ChinerMNのチュートリアルをpython-mnistで試していく。
Step 1: Communicators and Optimizers — ChainerMN 1.3.1 documentation
Dockerのrun
nvidia-docker runするがこの際には以下のようにオプションを付ける。
sudo nvidia-docker run -it -d \ --net=host -p 2223:2223 \ -v /tmp:/tmp \ -v /etc/hosts:/etc/hosts:ro \ -v /home/vaaaaanquish/.ssh:/home/vaaaaanquish/.ssh \ -e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 \ -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ -e NCCL_ROOT=/usr/local/nccl \ -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include \ -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs \ -e PYTHONPATH=/usr/bin/python3\ [docker_image_name] sh /init.sh
dockerのポートはssh_configに書いたものを指定する。
/etc/hostsはマウントしないとdockerから各ノード間で通信できないので必須。
また前述の通り、配布したsshもノード上の物をマウントして利用する。
環境変数はdocker内でbashrcに書いても良いし、個々で-eで指定しても良いですが今回はinit.shを最後に動かすようにしてるのでここで。
あとコレはHorovodなるpackageを使った時に起こりがちな症状ですが/tmpにファイル作る場合もあるので、/tmpも大人しくマウントしておくと良いです。
何か雑にこんなんで全ノードでdocker実行すればええんちゃうんですかね
from fabric.api import * env.hosts = [HOSTS] env.user = USER env.key_filename = KEY_PATH env.password = PASSWORD DOCKER_ENV = '-e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -e NCCL_ROOT=/usr/local/nccl -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs -e PYTHONPATH=/usr/bin/python3' @parallel def run_docker(): run("sudo nvidia-docker run -it -d --name {} --net=host -p 2223:2223 -v {}:{} -v /etc/hosts:/etc/hosts:ro -v /home/{}/.ssh:/home/{}/.ssh {} {} sh {}".format(P_NAME, LUSTERFS_MAUNT, LUSTERFS_HOME, USER, USER,DOCKER_ENV, DOCKER, INIT_SH))
mpiexecでChainerMNの実行
どこか1つのノードに対して「ssh root@hogehoge -p 2223」等としてdockerに入る。
docker内でmpiexecを実行する時、オプションで別のノードを指定することもできるが、 hostfile が あると便利なのでこれを用意する。
host.txtみたいな雑な名前で良い。書き方は以下の通り。
hogehoge_server_001 port=2223 cpu=2 hogehoge_server_002 port=2223 cpu=2 hogehoge_server_003 port=2223 cpu=2 hogehoge_server_004 port=2223 cpu=2
cpu=2って書いてるけど、これ実際はgpuの数っぽい。(プロセス数という意なのかも)
各サーバでport=2223とport=2224をそれぞれGPU割り当てて、Dockerマウントして使いたい!みたいなのはhostfileでは書けない。
いざ実行。前述example内のtrain.pyの画像path等をちょっと変更して動かしてみる。
mpiexec --allow-run-as-root --mca btl_tcp_if_include ib0 -x PATH=$PATH -x PYTHONPATH=$PYTHONOATH -x LD_LIBRARY_PATH=$LD_LIBRARY_PATH -x CPATH=$CPATH -x LIBRARY_PATH=$LIBRARY_PATH -x NCCL_ROOT=$NCCL_ROOT --hostfile host.txt -np 8 python3 train.py
dockerのrootで走ってもらう必要があるので--allow-run-as-rootが必須。
また、Infinibandを使っている時は--mca btl_tcp_if_include ib0して指定しないと掴んでくれなくて遅いので注意。
npの後ろにGPUと同じ数を書いて実行コマンドを叩くだけ。
学習が走ってそうだったら終わりです。
- エラー対応とか -
結構つまり所なエラーについて書いておきます。
OpenMPI、クソデコレーションされたエラー文吐く時もあれば、うんともすんとも言わないときもあるのでイライラせず対応していきましょう。
orterun was unable to launch the specified application as it could not access or execute an executable ~~~
→ 相互sshが上手くいってないので普通にsshで別ノードの指定ポートにパスワードなしで飛んでdockerコンテナ内に入れるかチェック
mpiexecが動いてそうだけど学習前に止まる!
→ chainerのサンプルでいうとGPU掴むところで止まってるんだと思います。
hostfileの書き方が怪しい場合と環境変数が上手く渡せてない場合が多いです。
この場合エラーも出ず止まったままになるのでサッサとctrl+cで抜けて確認。
ORTE does not know how to route a message to the specified daemon located on the indicated node ~~~
クソ遅い!received unexpected process identifier!
→ infiniband掴めてないのでmpiexec時にこれ忘れてる --mca btl_tcp_if_include ib0
もしくは環境とオプションが合ってない
It appears as if there is not enough space for (the shared-memory backing
→ /tmpとかファイルシステムに書き込む物を実行した時にそこマウントしてないと起こりがち
エラー文を読めばなんとか。
Failed, NCCL error nvidia-sample.cu:88 'unhandled system error
→ NCCLがDockerの作る仮想ソケットを利用してしまうので、上記init.shもしくはそれと同等のものが動いてるか確認
- おわりに -
最初にも書きましたが多分k8sとか色んなツールが出てきてるので、こんなコツコツやる必要もなくなって行くと思います。
ただこれ貯めといても仕方ないし書いときました。何かしらの参考になれば幸いです。
最近uberが出しているTF, Keras, PyTorchのOpenMPIトレーニングのwrapperとして扱えるhorovodも、以上の方法で使えます。
uber/horovod: Distributed training framework for TensorFlow, Keras, and PyTorch.
github.com
horovodとかdistTF、pytorch.distributedの知見も徐々にアウトプットしていこうと思っています。がんばります。