Chainerチュートリアルを和訳する必要があったからかいてみた(2): GPU編

追記:

本記事はChainer 1.4以前の物になります。

現在の仕様とは大きく異なるので、参考程度にとどめてください。

    • -

前回に引き続き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