前回に引き続き2回目、今回は2章を飛ばし3章のGPUの使い方について訳しました。
また、複数GPUを使うのは必要に迫られていないので取り急ぎ省略しました。
ChainerでGPUを使う
この章では、以下の事を学びます。
- ChainerとCuPyの関係
- CuPyの基本
- ChainerでのSingle-GPUの使い方
- model-parallel計算におけるMulti-GPUの使い方
- data-parallel計算におけるMulti-GPUの使い方
この章を読んだ後、以下の事ができるようになります。
- CUDAの使えるGPUをChainerで使う
- Chainerでmodel-parallel計算を書く
- Chainerでdata-parallel計算を書く
ChainerとCuPyの関係
Note v1.3.0のリリースから、ChainerはGPUバックエンドをPyCUDAから CuPyに変更しました。CuPyはChainerで使っていたPuCUDAの機能を すべてカバーしていますが、インタフェースは異なります。
ChainerはGPU計算のバックエンドにCuPyを使用しています。特に、cupy.ndarrayクラスはChainerの為に実装されたGPU配列です。CuPyはNumPyの機能のサブセットを同じインタフェースでサポートしています。なのでCPUとGPUで共通のコードを書く事が可能です。さらにPyCUDA風のユーザー定義カーネル生成もサポートしていますので、GPU専用の速い実装も書けます。
Note chainer.cudaモジュールは沢山の重要なシンボルをCuPyからインポート しています。例えば、cupyのネームスペースはchainer内において cuda.cupyを参照しています。また、chainer.cudaモジュールはたとえ CUDAをインストールしていなくてもインポートできます。
ChainerはGPU向けに確保されたメモリプールを使用します。前のセクションで見た通り、Chainerは学習や評価するイテレーションの中で沢山の配列を初期化し、破棄します。この振る舞いはCUDAアーキテクチャに良く向いておらず、つまりCUDA上でのメモリの確保と開放(i.e.cudaMallcやcudaFree関数)はCPUの場合とGPUの場合において性能が発揮しきれない方法で同期しています。そこで、計算中のメモリの確保と開放を避けるために、ChainerはCuPyのメモリプールを標準のメモリ確保アロケーターとして使用します。ChainerはデフォルトのアロケーターをCuPyのものに変更したため、ユーザーはCuPyの関数を既存のメモリアロケーターの管理を避け直接的に使用する事が可能です。
cuda.ndarrayの基礎
Note CuPyは明示的な初期化が必要ありません。なので、cuda.init()関数は v1.3.0から廃止されました。
CuPyはNumPyインタフェースのサブセットを実装したGPU配列のバックエンドです。cupy.ndarrayクラスはこのコアで、GPU版のnumpy.ndarrayと同質です。CuPyは沢山の関数をcupy.ndarrayオブジェクトに実装しています。こちらでサポートしているサブセットのリファレンスが見られます。NumPyを理解する事はほとんどのCuPyの機能を理解する助けになるでしょう。こちらでNumPyの学習用ドキュメントが見られます。
cupy.ndarrayとnumpy.ndarrayで主に異なることは、cupy.ndarrayの場合領域がデバイス上で確保済みだということです。この確保はデフォルトでは現在のデバイス(カレントデバイス)に対して行われます。カレントデバイスは、cupy.cuda.Deviceオブジェクトで以下のように変更可能です。
with cupy.cuda.Device(1): x_on_gpu1 = cupy.array([1, 2, 3, 4, 5])
CuPyのほとんどの処理はカレントデバイスで行います。カレントデバイスではないデバイス上の配列の処理でエラーが発生するので注意してください。
Chainerは自動でデバイスを切り替え、選ぶいくつかの便利な関数を提供します。例えば、chainer.cuda.to_gpu()関数はnumpy.ndarrayオブジェクトを特定のデバイスへコピーします。
x_cpu = np.ones((5, 4, 3), dtype=np.float32) x_gpu = cuda.to_gpu(x_cpu, device=1)
これは、以下のCuPyを使ったコードと同じことです。
x_cpu = np.ones((5, 4, 3), dtype=np.float32) with cupy.cuda.Device(1): x_gpu = cupy.array(x_cpu)
GPUデバイス上からCPUへ移動したい場合はchainer.cuda.to_cpu()を用いて以下のようにできます。
x_cpu = cuda.to_cpu(x_gpu)
これは、CuPyを用いて以下のようにするのと同じです。
with x_gpu.device:
x_cpu = x_gpu.get()
Note 上記のようなwith句は適切なCUDAデバイスを使用するために必要な ものです。もし、1つのデバイスのみを使用している場合、デバイスを 切り替える必要はありません。 chainer.cuda.to_cpu()とchainer.cuda.to_gpu()関数は自動的に現在の デバイスを正しく切り替えてくれます。
Chainerはさらにchainer.cuda.get_device()という便利な関数をデバイスを選択するために提供しています。この関数は整数、CuPy配列、NumPy配列またはNone(この場合現在のデバイスを指します)を受け、適切なデバイスオブジェクトを返します。もし引数がNumPy配列の場合、ダミーデバイスオブジェクトが返ります。ダミーデバイスオブジェクトはなにもしないwith句をサポートします。ここにいくつかの例を示します。
cuda.get_device(1).use() x_gpu1 = cupy.empty((4, 3), dtype='f') # 'f' indicates float32 with cuda.get_device(1): x_gpu1 = cuda.empty((4, 3), dtype='f') with cuda.get_device(x_gpu1): y_gpu1 = x_gpu + 1
このようにNumPy配列を受け取るので、適切なデバイス切り替えをしてくれる関数をNumPyとCuPyともに適用可能なものとして書く事ができます。
def add1(x): with cuda.get_device(x): return x + 1
CuPyのNumPyとの互換性はCPU/GPU共通コードを書く事を可能にします。これはchainer.cuda.get_array_module()関数により簡単に実現できます。この関数はnumpy、cupyモジュールを引数に応じて返します。CPU/GPU共通関数は以下のように定義できます。
# Stable implementation of log(1 + exp(x)) def softplus(x): xp = cuda.get_array_module(x) return xp.maximum(0, x) + xp.log1p(xp.exp(-abs(x)))
Single GPUでニューラルネットワークを走らせる
Single-GPUの使い方は非常にシンプルです。FunctionSetをGPUへ移して、事前に配列をGPUへ入れておくだけです。このサブセクションでは、ChainerのMNIST exampleのコードを使って話を進めます。
FunctionSetオブジェクトはto_gpu()メソッドを用いて指定されたGPUへ移動できます。GPU版のパラメータと勾配がoptimizerへ渡っているか確認してください。
model = FunctionSet( l1 = F.Linear(784, 100), l2 = F.Linear(100, 100), l3 = F.Linear(100, 10), ).to_gpu() optimizer = optimizers.SGD() optimizer.setup(model)
このto_gpu()メソッドはFunctionSet自体を返します。デバイスは指定しなくてもかまいません。その場合はカレントデバイスを使用します。
それから、ミニバッチのすべての計算をGPUへ移しましょう。
batchsize = 100 datasize = len(x_train) for epoch in range(20): print('epoch %d' % epoch) indexes = np.random.permutation(datasize) for i in range(0, datasize, batchsize): x_batch = cuda.to_gpu(x_train[indexes[i : i + batchsize]]) y_batch = cuda.to_gpu(y_train[indexes[i : i + batchsize]]) optimizer.zero_grads() loss, accuracy = forward(x_batch, y_batch) loss.backward() optimizer.update()
これはほとんど元々のexampleとおなじコードですが、cuda.to_gpu()関数をミニバッチ配列に挿入しました。
Model-parallel Computation on Multiple GPUs
略
Data-parallel Computation on Multiple GPUs
略