chainerMNを試してみる

# chainerMNとは

> ChainerMN は Chainer の追加パッケージで、Chainer を用いた学習を分散処理により高速化できます。柔軟で直感的に利用できる Chainer の利便性をそのままに、学習時間を大幅に短縮できます。1 ノード内の複数の GPU を活用することも、複数のノードを活用することもできます。既存の学習コードから数行の変更で ChainerMN を利用可能です。
https://research.preferred.jp/2017/05/chainermn-beta-release/ より引用

# はじめに

普段は普通のchainerを使ってニューラルネットワークのモデルを調整している者です。しかしディープラーニングで実際に学習させながら試行錯誤するとなるとどうしても時間がかかる。そんな折にchainerMNに関する記事を読んで，簡単に早くできる(かもしれない)なら試してみるしかない！と試してみた次第です。

# インストール

本稿では，chainerインストール済みの以下のスペックのLinuxサーバ複数台の上に実行環境を作ることを想定して記載します。

* 計算機スペック
  * os
     * Ubuntu 16.04
  * cpu
     * Intel(R) Core(TM) i7-6950X(20コア)
  * gpu
     * GeForce GTX 1080 Ti 2枚 


* 次のものが必要です
  * mpi
     * OpenMPI
  * NCCL
  * chainer

## MPI

まずはMPIをインストールします。MPIは複数のプロセスやコンピューターを使って並列コンピューティングを行うためのモジュールです。フリーで使えるMPIとしては，[OpenMPI](https://www.open-mpi.org/)や[MPICH](http://www.mpich.org/)などがあるようです。

連携して動作させるコンピューター間では同一のMPIをインストールし，複数の種類のMPIを混在させないようにする必要があります。

OpenMPIをダウンロード
https://www.open-mpi.org/software/ompi/v3.0/

ダウンロードしたtar.gzを解凍したフォルダでインストールを行います。
今回は念のためにインストールするディレクトリを$HOME以下にしておきます

```console
$ ./configure --with-cuda --prefix=$HOME/local/openmpi
$ make -j4  
$ make install
```

PATHを通しておきます

```~/.bashrc
export LD_LIBRARY_PATH=$HOME/local/openmpi/lib:${LD_LIBRARY_PATH}
export PATH=$HOME/local/openmpi/bin:${PATH}
```

## NCCL

次にNCCLをインストールします。NCCLはNVIDIAのマルチGPUの集合通信ライブラリです。

以下から.debをダウンロード
https://developer.nvidia.com/nccl

```console
$ sudo dpkg -i nccl-repo-ubuntu1604-2.1.2-ga-cuda8.0_1-1_amd64.deb
$ sudo apt update
$ sudo apt install libnccl2 libnccl-dev
```

こちらもPATHを通しておきます

```~/.bashrc
export NCCL_ROOT=/usr/local/nccl
export CPATH=$NCCL_ROOT/include:$CPATH
export LD_LIBRARY_PATH=$NCCL_ROOT/lib/:$LD_LIBRARY_PATH
export LIBRARY_PATH=$NCCL_ROOT/lib/:$LIBRARY_PATH
```

## chainerMN

最後にpipでchainerMNをインストールします

```console
pip install cython
pip install chainermn
```

# chainerからの変更点

chainerでモデルを構成していて，trainerを使って学習しているコードからであれば、数行の変更で分散学習になります。

chainerMNの[チュートリアル](https://chainermn.readthedocs.io/en/stable/tutorial/step1_communicators_optimizers.html)にあるように，chainerのサンプルの[train_mnist.py](https://github.com/chainer/chainer/blob/master/examples/mnist/train_mnist.py)を変更すると以下のようになります。

### 1. chainerMNをインポート

```python
# 追記
import chainermn
```
### 2. communicatorを作る

communicatorはプロセス間で情報のやり取りをするためのオブジェクトです

```python
# 追記
comm = chainermn.create_communicator(args.communicator)
```

### 3. gpuデバイスの確保

gpuを使う場合は以下のように書き換える 

```python
# 変更前
chainer.cuda.get_device_from_id(args.gpu).use()

# 変更後
device = comm.intra_rank #プロセスのランクでGPUデバイスを割り振る
chainer.cuda.get_device(device).use()
```

### 4. optimizerの設定

```python
# 変更前
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

# 変更後
optimizer = chainermn.create_multi_node_optimizer(
    chainer.optimizers.Adam(), comm)
optimizer.setup(model)
```

### 5. datasetの設定

```python
# 変更前
train, test = chainer.datasets.get_mnist()

# 変更後
if comm.rank == 0: # ランク0のプロセスでのみデータセットを作る
        train, test = chainer.datasets.get_mnist()
    else:
        train, test = None, None
train = chainermn.scatter_dataset(train, comm, shuffle=True)
test = chainermn.scatter_dataset(test, comm, shuffle=True)
```

### 6. evaluatorの設定

```python
# 変更前
evaluator = extensions.Evaluator(test_iter, model, device=device)

# 変更後
evaluator = extensions.Evaluator(test_iter, model, device=device)
evaluator = chainermn.create_multi_node_evaluator(evaluator, comm)
trainer.extend(evaluator)
```

### 7. 出力系のextensionsをランク0のプロセスでのみ動くようにする

printなどを使うとmpirunのプロセス数だけ表示される。必要ならランク0でのみ呼ばれるようにする

## 単一ノードで複数プロセスを動かすケース

ここでいうノードとは，サーバ1台，端末1台などのことを言います。この節で取り扱うのはサーバ1台で複数のプロセスを走らせたり，複数枚のGPUを使うケースです。

単一のノードでchainerMNを使ったコードを実行するにはmpirunを使います。-npオプションの数字が実際に実行するプロセス数になります。

```console
mpirun -np 4 python train_mnist.py
```

## 複数ノードで動かすケース

複数ノードでMPIを使うには、そのための環境を作る必要があります。主な作業を列挙すると，

* MPIでつなぐノード間でパスフレーズなしでSSHログインできるようする
* ファイアーウォールなどを切る
* NFSなどで実行ファイルや実行環境をノード間で共有する
* ホストファイルの用意

となります。具体的な方法については，長くなりすぎるので省略させてください。私は以下のページを参照して環境を構築しました。

> MPIの環境構築や基本コマンドのまとめ
> https://qiita.com/kkk627/items/49c9c35301465f6780fa

> NFSサーバーを構築したメモ
> https://qiita.com/salty-vanilla/items/5bc95ad47af6bf051bf7

また，各ノードの~/.bashrcファイルに以下のような記述があると，複数ノード間向けにmpirunを起動したときにorted command not foundと出てエラーになります。mpirun実行時には，該当部分をコメントアウトするなどの対応が必要でした。

```
# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac
```

複数ノードでchianerMNを動かすには以下のように実行します。--hostfileオプションには，hostファイルへのパスを指定します。

```console
mpirun --hostfile host -np 4 python train_mnist.py
```

# 実験

今回は試しにgithubにあるchainermnの[サンプル](https://github.com/chainer/chainermn/blob/cc1048b82b1ae8acdc969f2ec3d0fc1441d5254d/examples/mnist/train_mnist.py)のmnistをプロセス数とノード数を変えながら実行して実行時間を計測してみました。mnistのデータ数は1000，batch_sizeは100，epochs数は20です。

なお，lossやaccuracyなどの評価値はどれもほとんど差がなかったのでここでは触れないです。

#### CPUのみの学習

|プロセス数|ノード数|所用時間(s)|
|:-:|:-:|:-:|
|1|1|2670.08|
|2|1|2117.98|
|4|1|687.73|
|6|1|614.60|
|10|1|583.00|
|2|2|2504.24|
|4|2|567.44|
|6|2|554.23|
|10|2|512.01|

#### GPUを使った学習

chainerのサンプルでGPUを使うには--gpuオプションを付けて実行します。

```console
mpirun -np 2 python train_mnist.py --gpu
```

|プロセス数|ノード数|所要時間(s)|
|:-:|:-:|:-:|
|1|1|80.64|
|2|1|45.10|
|2|2|418.23|
|4|2|252.48|

# まとめ

ということで，chinaerMNの環境を構築してmnistを学習させたときの処理時間を計測してみました。

CPUを使っても，GPUを使っても単一ノード，複数ノードそれぞれの中では，プロセス数が増えるにしたがって所要時間が短くなることが確認できました。
ただ複数ノードを使うと，単一ノードと同数のプロセス数でも遅くなるケースや，プロセス数を増やしても投入したプロセス数ほど高速化できていないケースもありました。今回の設定だと，ノードあたりのGPUの数が少なかったり，データセットのepochあたりのイテレーター数が少なかったりしたので，ノード間の同期やepoch更新時のオーバヘッドに時間を取られて，あまり分散学習の恩恵を得られていないと思われます。

ともあれchainerMNの使い方はわかったので，今度はもっとデータ数の大きいデータセットとGPUの数が多いマシンが使える案件で試してみたいです。


### 参考にしたサイト

https://chainermn.readthedocs.io/en/stable/installation/guide.html

http://xkumiyu.hatenablog.com/entry/chainermn#ChainerMNのインストール

https://qiita.com/kkk627/items/49c9c35301465f6780fa

https://qiita.com/salty-vanilla/items/5bc95ad47af6bf051bf7