Horovod in Dockerを試す

モチベーション

一年ぐらい前から分散学習(Distributed Training)に興味を持っていた。そのため複数のTITAN-Vが使えるマシンを用意し、Horovodという分散学習フレームワークを試していた。この度ようやく分散学習サンプルを動かすことが出来たので、ここに投稿する。

情報源

  1. 本家のマニュアル - Horovod in Dockerという本家のマニュアル。docker起動のコマンドラインの記述は古いが、貴重な情報源。
  2. Dockerのマニュアル(日本語) - 自分のDocker環境はRootlessで構築していたので、Rootless Dockerの本家のマニュアル。
  3. Dockerのマニュアル(英語版) - 上記マニュアルの英語版のページ。

概要/経緯

Horovodは関連するミドルウェアも多いので、直接物理マインにインストールすることは避け、Dockerコンテナで動かす方針でこのプロジェクトを今年8月に開始した。DockerコンテナでHorovodを動かすことについては、情報源1.を参考に進めることにした。そのページを見てもらえば分かる通り、とても簡単そうに見える。

情報源でも書いたが、自分のDocker環境は、Rootless Dockerで実現していた。このことが、Hovovod in Dockerでサンプルを動かすまで長い時間が掛かった原因でもある。

Horovod in Dockerでのssh動作条件

Horovod in Dockerでは、1台目のマシン(以下、Primaryと呼び、2台目以降をSecondaryを呼ぶことにする)から、ssh通信でパスワード無しで認証させる必要がある。そのため、Primaryでssh-keygenを行なって、公開鍵(id_rsa.pub)をSecondarysに配布する。具体的な方法については後述する。

Horovod in Dockerの起動方法

初めに2台のマシンをPrimaryとSecondaryにして次の通りHorovodを実行した。

情報源1.に記載されている通り、sshの公開鍵については、nfs上に置き、-vでコンテナ内では/root/.sshとして見えるように設定した。具体的なコマンドラインは、次の通り。dockerをsudoで起動していないのは、rootless docker環境だから。

Secondary

Secondaryマシンでは、以下の通り、horovodのコンテナを起動し、ポート番号12345でssh通信を待つ。

$ docker run -it --gpus all --net=host -v /mnt/nfs2/ssh:/root/.ssh horovod/horovod:latest bash -c "/usr/sbin/sshd -p 12345; sleep infinity"
Primary

Primaryマシンでは、horovodコンテナを起動し、horovodrunでhorovod向けに書かれたコード(pytorch_mnist.py)を実行する。

$ docker run -it --gpus all --net=host -v /mnt/nfs2/ssh:/root/.ssh horovod/horovod:latest
root@ganymede:/horovod/examples/pytorch# horovodrun -np 2 -H 192.168.11.4:1,192.168.11.3:1 -p 12345 python pytorch_mnist.py
(途中略)
    raise RuntimeError('could not connect to some hosts via ssh')
RuntimeError: could not connect to some hosts via ssh

上記の通り、sshがつながっていないようであり、ここからsshとの格闘が始まった。 horovodrunの起動コマンドからわかるように、コンテナ内の/etc/hostsを変更せずに、IPアドレスでPrimary, Secondaryのマシンを指定している。

調査結果

Secondaryのコンテナにiproute2パッケージをインストールして、ssコマンド(旧netstat)でコンテナ内でポート番号12345をLISTENしていることを確認した。この時は、ganymedeがSecondaryである。

root@ganymede:/horovod/examples# apt update
root@ganymede:/horovod/examples# apt install iproute2
root@ganymede:/horovod/examples# apt install iputils-ping
root@ganymede:/horovod/examples# ip -br -4 address
root@ganymede:/etc/ssh# ss -nltu
Netid  State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
udp    UNCONN  0       0                    *:7946               *:*           
tcp    LISTEN  0       128            0.0.0.0:12345        0.0.0.0:*           
tcp    LISTEN  0       128                  *:7946               *:*           
tcp    LISTEN  0       128                  *:2377               *:*           
tcp    LISTEN  0       128               [::]:12345           [::]:*

ganymedeホスト上で、ssを使ってポート番号12345でLISTENしているか確かめたが、LISTENしてないことがわかった。

まとめると、コンテナではポート番号12345でLISTENするも、ホストではそのポート番号ではLISTENしていない。

マニュアルから分かったこと

ここに至って、情報源2.および3.を丹念に読むと、次のような記述があった!!

--net=host がホスト・ネットワーク名前空間上でポートをリッスンしません

これは予想されうる挙動で、デーモンは RootlessKit のネットワーク名前空間内にいるからです。かわりに docker run -p を使います。

–net=hostを使わず-pでホストとコンテナのポートをつなぐ

マニュアルに従って、–net=hostを使わず、-pでポートの繋ぎ変えるように次のように起動した。

$ docker run -it --gpus all -p12345:22 -v /mnt/nfs2/ssh:/root/.ssh horovod/horovod:latest

別ホストからssh接続すると次のようなエラーとなった。

$ ssh 192.168.11.4 -p 12345
kex_exchange_identification: Connection closed by remote host
Connection closed by 192.168.11.4 port 12345

ここで力尽きた。

ついにrootless dockerを諦める決断をし、全てのマシンのrootless dockerをアンインストールすることにした。

以下は、rootfulなdocker環境での実行である。

Horovod in Dockerを実行

sshの公開鍵と秘密鍵を作成する

次のとおり、公開鍵と秘密鍵をnfsマウントしている領域に作成する。

# ssh-keygen -t rsa
Enter file in which to save the key (/root/.ssh/id_rsa): /mnt/nfs2/ssh/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
(以下略)
# cat id_rsa.pub >> authorized_keys
# chmod 600 authorized_keys
# ls -l authorized_keys
-rw------- 1 root root  566 10月 13 15:33 authorized_keys

Horovod in Dockerを起動する

Secondarys
$ sudo docker run -it --gpus all --net=host -v /mnt/nfs2/ssh:/root/.ssh horovod/horovod:latest bash -c "/usr/sbin/sshd -p 12345; sleep infinity"
Primary
$ sudo docker run -it --gpus all --net=host -v /mnt/nfs2/ssh:/root/.ssh horovod/horovod:latest
# cd pytorch
# horovodrun -np 2 -H 192.168.11.3:1,192.168.11.4:1 -p 12345 python pytorch_mnist.py

実行時間を計測

SecondarysでHorovod in Docker(上記のコマンドラインのとおり)を起動した上で、次のとおり、1台から4台での実行時間(Real)を計測した。

# time horovodrun -np 1 -H localhost:1 python pytorch_mnist.py
(略)
[1,0]<stdout>:Test set: Average loss: 0.0551, Accuracy: 98.38%
[1,0]<stdout>:

real    5m19.646s
user    6m14.697s
sys 0m28.747s

# time horovodrun -np 2 -H 192.168.11.4:\
1,192.168.11.3:1 -p 12345 python pytorch_mnist.py
(略)
[1,0]<stdout>:Test set: Average loss: 0.0546, Accuracy: 98.26%
[1,0]<stdout>:

real    2m54.234s
user    3m46.072s
sys 0m36.676s

# time horovodrun -np 3 -H 192.168.11.4:1,192.168.11.3:1,192.168.11.5:1 -p 12345 python pytorch_mnist.py
(略)
[1,0]<stdout>:Test set: Average loss: 0.0576, Accuracy: 98.03%
[1,0]<stdout>:

real    2m7.043s
user    2m52.191s
sys 0m40.745s

# time horovodrun -np 4 -H 192.168.11.4:1,192.168.11.3:1,192.168.11.5:1,192.168.11.6:1 -p 12345 python pytorch_mnist.py
(略)
[1,0]<stdout>:Test set: Average loss: 0.0542, Accuracy: 98.27%
[1,0]<stdout>:

real    1m34.386s
user    2m33.140s
sys 1m3.089s

2回計測した結果をグラフにしてものが次のとおり。

mnist_exec_time

まとめ 〜 今後に向けて

今回、rootless docker環境で、dockerのhostネットワークを使おうとしてハマった。しかし、ssh認証、sshのログ(ssh -vvv IPアドレス -p ポート番号)の方法、ネットワークコマンド(ss等)を学ことが出来た。

計測した実行結果の評価については次のように考える。計測した4台のマシンはCPU/GPUの性能も異なるので、マシン台数と実行時間との関係について厳密に論じることはできないが、4台で実行すると1台だけより速くなることは事実である。

今回は、サンプルとして事前に添付されていたmnistを実行したが、今後は、horovod向けコードの記述を学び、もう少し実行時間が掛かる課題を取り上げて、horovod向けにコードを自分で書いて実行したい。