「ちょっと聞いて、ウチの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 の後、パスをしっかり通しておきます。
#==========
#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のインストールにも必要かも)
#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のインストールに行き詰まってました。
#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が勝手にインストールされ
import chainermn
でインポートできるようになります。
下に公式のchainerMNのプログラムのコピーを準備しました。
https://github.com/chainer/chainermn/tree/cc1048b82b1ae8acdc969f2ec3d0fc1441d5254d/examples/mnist
#!/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()
#!/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
##ふとしたつぶやき
Jupyterlabを使ってchainerMNで分散処理をするにはどうしたらいいんだろう...