情報メインじゃない研究室で機械学習をすることになった人のための環境構築入門 ChainerMN 編

「ちょっと聞いて、ウチのGPUマジ遅いんだけど」
「マジ、えっ明美、分散処理してないの？」
「分散処理ってなに？ウチ聞いたことないんだけど」
「笑、遅れてる、じゃああたし忙しいから、じゃね」
　
そんな会話があったとかなかったとか。

#はじめに
今回はChainerMNを導入して、GPUやCPUを並列で用いることができるようにします。
昔はChainerMNというパッケージが必要でしたが、Chainer(v5)の中に完全に移行したそうです。

**このシリーズでは決してroot権限を使いません。**
**user権限でChainerMNをインストールします。**


※同じ研究室の人のために作ったものなので、汎用性がないかもしれません。
※私はそんなに強くないので、強い人の役には多分立ちません。

#必要なもの
- Chainer(v5)
- OpenMPI
- NCCL
- Cupy


では導入方法を説明致します。

#Chainer編
chainerのv5以降ではchainerの中にChainerMNが導入されています。
ゆえに、chainerのアップデート(インストール)を行いましょう。
**もし、chainerMNをかつてインストールしたことがあれば、必ずアンインストールしてから行って下さい。**

```
$ pip install -U chainer   (アップデート)
or
$ pip install chainer      (インストール)
```
chainerはとてもアップデートが多いですが、
アップデートしてもchainer v2ぐらいのコードならそこまで書き変えずに用いられる(若干嘘)ので、
アップデートについていった方がいいと思います。
ついて行きたくない人は、一番最後にchainerMNのインストールを行って下さい。

#OpenMPI編
OpenMPIの導入は比較的簡単です。
まず最初にOpenMPIをダウンロードします
https://www.open-mpi.org/software/ompi/v3.0/

ダウンロードしたtar.gzを解凍し、解凍された後のディレクトリに移ります。

```
$ tar xvf openmpi-3.0.3.tar
$ cd openmpi-3.0.3
```

インストール先のフォルダを--prefix= 以降に
**cudaがインストールされているフォルダを　--with-cuda= 以降に設定し、**以下のようにインストールします。
(user権限ではこれは必須)

```
$ ./configure --with-cuda=(Cudaへのパス) --prefix=(インストール先)
$ make -j4  
$ make install

(Cudaへのパス例):/usr/local/cuda-9.0/
(インストール先の例):/home/lab/ueno/openmpi-install-place
```
make installの際、なぜか、**Warning**が大量に表示されますが、実際の問題はないようです。

install の後、パスをしっかり通しておきます。

```bash:.bash_profileもしくは.bashrc
#==========
#openmpi
export PATH=/home/lab/ueno/openmpi-install-place/bin:$PATH
export LD_LIBRARY_PATH=/home/lab/ueno/openmpi-install-place/lib:$LD_LIBRARY_PATH
export LIBRARY_PATH=/home/lab/ueno/openmpi-install-place/lib:$LIBRARY_PATH
export MANPATH=/home/lab/ueno/openmpi-install-place/share/man:$MANPATH
#================================================================
```

#NCCLとCupy編
ncclはニックルと読むらしいですよ。

まず、これでもかというくらいCudaにパスを通します。(もしかしたらOpenMPIのインストールにも必要かも)

```bash:~/.bash_profileもしくは~/.bashrc
#cudapath(例)
export CUDA_PATH=/usr/local/cuda-9.0
export PATH=$CUDA_PATH/bin:$PATH
export CPATH=$CUDA_PATH/include:$CPATH
export LIBRARY_PATH=$CUDA_PATH/lib64:$LIBRARY_PATH
export LD_LIBRARY_PATH=$CUDA_PATH/lib64:$LD_LIBRARY_PATH
export DYLD_LIBRARY_PATH=/usr/local/cuda-9.0/lib:$DYLD_LIBRARY_PATH
export CUDA_ROOT=/usr/local/cuda-9.0
```


次にCupyをインストールしたいのですが、、、
Cupyをこのようにインストールできる人は幸せです。(私は幸せです。)

```
$ pip install cupy-cuda90
```

このコマンドでは、ncclも一緒にインストールできます。
確認のため、以下のコマンドで確認しましょう

```
$ python -c 'import chainer; chainer.print_runtime_info()'
```
```
Platform: Linux-4.9.0-6-amd64-x86_64-with-debian-9.4
Chainer: 5.0.0
NumPy: 1.15.4
CuPy:
  CuPy Version          : 5.0.0
  CUDA Root             : /usr/local/cuda-9.0
  CUDA Build Version    : 9000
  CUDA Driver Version   : 9010
  CUDA Runtime Version  : 9000
  cuDNN Build Version   : 7301
  cuDNN Version         : 7301
  NCCL Build Version    : 2213
iDeep: 2.0.0.post3
```
一番下から二番目のNCCL Build Versionになんらかのversionが表示されている事が、
しっかりと nccl がインストールされていることの証明になります。



しかし、これではちゃんとインストールができない人がいると思います。
その場合は、

- GPUを使用している状態である。
- Cudaにしっかりパスが通っている。
- 逆にNCCLとCudnnにパスを通さない。
 
の3点に問題ないか確認してみましょう。
それでダメなら草の根を分ける必要があります。

##pip install cupy-cuda80 等を使わない場合
###NCCL
まず最初にNCCLをダウンロードします。
https://developer.nvidia.com/nccl/nccl-download
ダウンロードしたtar.gzを解凍し、名前をシンプルにしておきます。

```
$ tar xvf nccl-<version>.txz
$ mv nccl-<version>  NCCL
```
(ちなみにUbuntu 16.04とUbuntu14.04では、sudoを使う必要がありますが、
この二つはそもそも推奨環境なので、上記のpipでインストールできます。)

次にパスを通します。
下のパスのうち、どれかがなくても動く気もしなくもないですが、
全部あればとりあえず大丈夫だと思います。
私はずっとLIBRARY_PATHがなくてNCCLのインストールに行き詰まってました。

```bash:~/.bash_profileもしくは~/.bashrc
#nccl
export PATH=$PATH:/home/lab/ueno/NCCL
export NCCL_ROOT=/home/lab/ueno/NCCL
export CPATH=$NCCL_ROOT/include:$CPATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$NCCL_ROOT/lib
export LIBRARY_PATH=$NCCL_ROOT/lib/:$LIBRARY_PATH
```

ちなみに当然ですが、Cudaのパスもこれ以上ないくらいしっかり通しておきましょう。
こっちもしっかりやっとかないと、そもそもCupyをインストールできませんけど。


###Cupy

Cupyを入れ直しましょう。
（もし、ncclを初めてインストールしたなら絶対）

```
$ pip uninstall cupy
$ pip install cupy -—no-cache-dir
```
確認のため、以下のコマンドで確認しましょう

```
$ python -c 'import chainer; chainer.print_runtime_info()'
```
```
Chainer: 5.0.0
NumPy: 1.15.1
CuPy:
  CuPy Version          : 5.0.0
  CUDA Root             : /usr/local/cuda-9.0
  CUDA Build Version    : 9000
  CUDA Driver Version   : 9010
  CUDA Runtime Version  : 9000
  cuDNN Build Version   : 7005
  cuDNN Version         : 7005
  NCCL Build Version    : 2212
iDeep: 2.0.0.post3
```
一番下から二番目のNCCL Build Versionになんらかのversionが表示されている事が、
しっかりと nccl がインストールされていることの証明になります。

(cudnnをuser権限でインストールするためにはcudnnenvを使いましょう！)

これで下準備は完璧です。

#chainerMNの使い方
chainerMNはchainerv5をインストールした人なら、chainermnが勝手にインストールされ

```python:
import chainermn
```
でインポートできるようになります。

下に公式のchainerMNのプログラムのコピーを準備しました。
https://github.com/chainer/chainermn/tree/cc1048b82b1ae8acdc969f2ec3d0fc1441d5254d/examples/mnist


```python:train_mnist.py

#!/usr/bin/env python
from __future__ import print_function

import argparse

import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions
from mpi4py import MPI

import chainermn


class MLP(chainer.Chain):

    def __init__(self, n_units, n_out):
        super(MLP, self).__init__(
            # the size of the inputs to each layer will be inferred
            l1=L.Linear(784, n_units),  # n_in -> n_units
            l2=L.Linear(n_units, n_units),  # n_units -> n_units
            l3=L.Linear(n_units, n_out),  # n_units -> n_out
        )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)


def main():
    parser = argparse.ArgumentParser(description='ChainerMN example: MNIST')
    parser.add_argument('--batchsize', '-b', type=int, default=100,
                        help='Number of images in each mini-batch')
    parser.add_argument('--communicator', type=str,
                        default='hierarchical', help='Type of communicator')
    parser.add_argument('--epoch', '-e', type=int, default=20,
                        help='Number of sweeps over the dataset to train')
    parser.add_argument('--gpu', '-g', action='store_true',
                        help='Use GPU')
    parser.add_argument('--out', '-o', default='result',
                        help='Directory to output the result')
    parser.add_argument('--resume', '-r', default='',
                        help='Resume the training from snapshot')
    parser.add_argument('--unit', '-u', type=int, default=1000,
                        help='Number of units')
    args = parser.parse_args()

    # Prepare ChainerMN communicator.

    if args.gpu:
        if args.communicator == 'naive':
            print("Error: 'naive' communicator does not support GPU.\n")
            exit(-1)
        comm = chainermn.create_communicator(args.communicator)
        device = comm.intra_rank
    else:
        if args.communicator != 'naive':
            print('Warning: using naive communicator '
                  'because only naive supports CPU-only execution')
        comm = chainermn.create_communicator('naive')
        device = -1

    if comm.mpi_comm.rank == 0:
        print('==========================================')
        print('Num process (COMM_WORLD): {}'.format(MPI.COMM_WORLD.Get_size()))
        if args.gpu:
            print('Using GPUs')
        print('Using {} communicator'.format(args.communicator))
        print('Num unit: {}'.format(args.unit))
        print('Num Minibatch-size: {}'.format(args.batchsize))
        print('Num epoch: {}'.format(args.epoch))
        print('==========================================')

    model = L.Classifier(MLP(args.unit, 10))
    if device >= 0:
        chainer.cuda.get_device(device).use()
        model.to_gpu()

    # Create a multi node optimizer from a standard Chainer optimizer.
    optimizer = chainermn.create_multi_node_optimizer(
        chainer.optimizers.Adam(), comm)
    optimizer.setup(model)

    # Split and distribute the dataset. Only worker 0 loads the whole dataset.
    # Datasets of worker 0 are evenly split and distributed to all workers.
    if comm.rank == 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)

    train_iter = chainer.iterators.SerialIterator(train, args.batchsize)
    test_iter = chainer.iterators.SerialIterator(test, args.batchsize,
                                                 repeat=False, shuffle=False)

    updater = training.StandardUpdater(train_iter, optimizer, device=device)
    trainer = training.Trainer(updater, (args.epoch, 'epoch'), out=args.out)

    # Create a multi node evaluator from a standard Chainer evaluator.
    evaluator = extensions.Evaluator(test_iter, model, device=device)
    evaluator = chainermn.create_multi_node_evaluator(evaluator, comm)
    trainer.extend(evaluator)

    # Some display and output extensions are necessary only for one worker.
    # (Otherwise, there would just be repeated outputs.)
    if comm.rank == 0:
        trainer.extend(extensions.dump_graph('main/loss'))
        trainer.extend(extensions.LogReport())
        trainer.extend(extensions.PrintReport(
            ['epoch', 'main/loss', 'validation/main/loss',
             'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
        trainer.extend(extensions.ProgressBar())

    if args.resume:
        chainer.serializers.load_npz(args.resume, trainer)

    trainer.run()


if __name__ == '__main__':
    main()
```
```python:train_mnist_model_parallel.py
#!/usr/bin/env python
# coding: utf-8

import argparse

import chainer
import chainer.cuda
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions

import chainermn
import chainermn.datasets
import chainermn.functions


chainer.disable_experimental_feature_warning = True


class MLP0SubA(chainer.Chain):
    def __init__(self, comm, n_out):
        super(MLP0SubA, self).__init__(
            l1=L.Linear(784, n_out))

    def __call__(self, x):
        return F.relu(self.l1(x))


class MLP0SubB(chainer.Chain):
    def __init__(self, comm):
        super(MLP0SubB, self).__init__()

    def __call__(self, y):
        return y


class MLP0(chainermn.MultiNodeChainList):
    # Model on worker 0.
    def __init__(self, comm, n_out):
        super(MLP0, self).__init__(comm=comm)
        self.add_link(MLP0SubA(comm, n_out), rank_in=None, rank_out=1)
        self.add_link(MLP0SubB(comm), rank_in=1, rank_out=None)


class MLP1Sub(chainer.Chain):
    def __init__(self, n_units, n_out):
        super(MLP1Sub, self).__init__(
            l2=L.Linear(None, n_units),
            l3=L.Linear(None, n_out))

    def __call__(self, h0):
        h1 = F.relu(self.l2(h0))
        return self.l3(h1)


class MLP1(chainermn.MultiNodeChainList):
    # Model on worker 1.
    def __init__(self, comm, n_units, n_out):
        super(MLP1, self).__init__(comm=comm)
        self.add_link(MLP1Sub(n_units, n_out), rank_in=0, rank_out=0)


def main():
    parser = argparse.ArgumentParser(
        description='ChainerMN example: pipelined neural network')
    parser.add_argument('--batchsize', '-b', type=int, default=100,
                        help='Number of images in each mini-batch')
    parser.add_argument('--epoch', '-e', type=int, default=20,
                        help='Number of sweeps over the dataset to train')
    parser.add_argument('--gpu', '-g', action='store_true',
                        help='Use GPU')
    parser.add_argument('--out', '-o', default='result',
                        help='Directory to output the result')
    parser.add_argument('--unit', '-u', type=int, default=1000,
                        help='Number of units')
    args = parser.parse_args()

    # Prepare ChainerMN communicator.
    if args.gpu:
        comm = chainermn.create_communicator('hierarchical')
        device = comm.intra_rank
    else:
        comm = chainermn.create_communicator('naive')
        device = -1

    if comm.rank == 0:
        print('==========================================')
        if args.gpu:
            print('Using GPUs')
        print('Num unit: {}'.format(args.unit))
        print('Num Minibatch-size: {}'.format(args.batchsize))
        print('Num epoch: {}'.format(args.epoch))
        print('==========================================')

    if comm.rank == 0:
        model = L.Classifier(MLP0(comm, args.unit))
    elif comm.rank == 1:
        model = MLP1(comm, args.unit, 10)

    if device >= 0:
        chainer.cuda.get_device(device).use()
        model.to_gpu()

    optimizer = chainer.optimizers.Adam()
    optimizer.setup(model)

    # Iterate dataset only on worker 0.
    train, test = chainer.datasets.get_mnist()
    if comm.rank == 1:
        train = chainermn.datasets.create_empty_dataset(train)
        test = chainermn.datasets.create_empty_dataset(test)

    train_iter = chainer.iterators.SerialIterator(
        train, args.batchsize, shuffle=False)
    test_iter = chainer.iterators.SerialIterator(
        test, args.batchsize, repeat=False, shuffle=False)

    updater = training.StandardUpdater(train_iter, optimizer, device=device)
    trainer = training.Trainer(updater, (args.epoch, 'epoch'), out=args.out)
    trainer.extend(extensions.Evaluator(test_iter, model, device=device))

    # Some display and output extentions are necessary only for worker 0.
    if comm.rank == 0:
        trainer.extend(extensions.dump_graph('main/loss'))
        trainer.extend(extensions.LogReport())
        trainer.extend(extensions.PrintReport(
            ['epoch', 'main/loss', 'validation/main/loss',
             'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
        trainer.extend(extensions.ProgressBar())

    trainer.run()


if __name__ == '__main__':
    main()
```


後はこれを実行してみて下さい。

実行の仕方は、gpuなら、

```
$ mpiexec -n 4 python train_mnist.py --gpu
$ mpiexec -n 2 python train_mnist_model_parallel.py --gpu
```
cpuなら、

```
$ mpiexec -n 4 python train_mnist.py
$ mpiexec -n 2 python train_mnist_model_parallel.py
```
のような感じです。
どちらも、-n の後が、使うGPU/CPUの数です。

自分のプログラムに適用するには、
https://docs.chainer.org/en/v5.0.0/chainermn/tutorial/index.html
を参考にして下さい。

汎用的な書き換え方法に関してはまた今度追記します。


##reference

https://qiita.com/minhal/items/0bd75f2a80fe8af473af

https://github.com/chainer/chainermn/tree/cc1048b82b1ae8acdc969f2ec3d0fc1441d5254d/examples/mnist

##ふとしたつぶやき

Jupyterlabを使ってchainerMNで分散処理をするにはどうしたらいいんだろう...