Edited at

ChainerでGPUを使うと毎回結果が変わる理由と対策

More than 1 year has passed since last update.

本記事はChainerやTensorFlowでGPUを使うと毎回結果が変わる理由と対策 (まとめ)の詳細編であり、Chainerについて具体的な対策について検証します。


関連Issues(Chainer)

https://github.com/pfnet/chainer/pull/1321


検証環境

Chainer v1.20.0

Python 3.5.2

CUDA 8.0

cuDNN 5.1

NVIDIA Tesla/GeForce いろいろ


まずは乱数種の指定

GPUの演算誤差だけでなく乱数を用いていることも演算結果が安定しない原因です。

そこで乱数種を指定して発生する乱数を固定します。


乱数種の指定対象

Chainerの場合、下記の3種類の乱数に配慮する必要があります。


  • Python

  • NumPy

  • CuPy

CuPyはPFN社謹製のGPU対応版NumPyと考えればよいでしょう。


参考実装

def set_random_seed(seed):

# set Python random seed
random.seed(seed)

# set NumPy random seed
np.random.seed(seed)

# set Chainer(CuPy) random seed
cp.random.seed(seed)

seedに同じ値を設定すれば発生する乱数値が安定します。


それではちょっとしたCNNで実験してみます

実験用データセットはみんな大好きMNIST。使用するモデルはCNNを用いている小さめのものを選びました。


テストに用いたモデル

class CNN(chainer.Chain):

def __init__(self):
super(CNN, self).__init__(
conv1=L.Convolution2D(1, 32, 5, stride=1),
conv2=L.Convolution2D(32, 64, 5, stride=1),
l1=L.Linear(None, 512),
l2=L.Linear(None, 10),
)

def __call__(self, x):
x_4d = x.data.reshape(len(x.data), 1, 28, 28)
h = F.relu(self.conv1(x_4d))
h = F.max_pooling_2d(h, 2, stride=1)
h = F.relu(self.conv2(h))
h = F.max_pooling_2d(h, 2, stride=1)
h = F.relu(self.l1(h))
return self.l2(h)


ちなみにChainerのMNISTを用いたExampleはCNNになっていないため、TensorFlowのExampleであるDeep MNIST for Expertsに使われているモデル(Conv2層+FC1層)を使用しています。


まずCPUで5回実行しました


CPUによる実行結果(10Epoch目の結果だけを抽出)

$ python mnist_chainer.py

epoch main/loss validation/main/loss main/accuracy validation/main/accuracy elapsed_time
10 0.00639321 0.0375724 0.998116 0.99164 4494.79
10 0.00639321 0.0375724 0.998116 0.99164 4487.52
10 0.00639321 0.0375724 0.998116 0.99164 4498.98
10 0.00639321 0.0375724 0.998116 0.99164 4493.16
10 0.00639321 0.0375724 0.998116 0.99164 4492.28

期待通り5回とも同じ結果になっています。


次にGPUで5回実行しました


GPUによる実行結果(10Epoch目の結果だけを抽出)

$ python mnist_chainer.py

epoch main/loss validation/main/loss main/accuracy validation/main/accuracy elapsed_time
10 0.00759941 0.0510452 0.997615 0.988754 77.9142
10 0.00894238 0.0384938 0.997499 0.99164 78.9828
10 0.00753692 0.0475569 0.997615 0.988256 78.1221
10 0.00674504 0.0442383 0.998166 0.99164 77.9893
10 0.00766461 0.0411011 0.997465 0.991143 80.4075

やはりGPUでは結果が不安定(非決定的)になってしまいますね。


GPUを使用しても演算結果を安定(決定的に)させる方法

手っ取り早くて効果的なのは環境変数CHAINER_CUDNNに0を設定することです。

Chainerの環境変数についてはこちらを参照のこと。


環境変数を指定した、GPUによる実行結果(10Epoch目の結果だけを抽出)

$ export CHAINER_CUDNN=0

$ python mnist_chainer.py
epoch main/loss validation/main/loss main/accuracy validation/main/accuracy elapsed_time
10 0.00971611 0.0498366 0.997232 0.98955 175.727
10 0.00971611 0.0498366 0.997232 0.98955 174.934
10 0.00971611 0.0498366 0.997232 0.98955 175.237
10 0.00971611 0.0498366 0.997232 0.98955 175.662
10 0.00971611 0.0498366 0.997232 0.98955 174.704

あっさり解決しました。演算結果が不安定(非決定的)になる原因はcuDNNだったようです。

しかし、演算結果は安定したものの演算時間が約2.2倍になってしまいました。


演算性能をほとんど低下させずに安定(決定)的な演算を行う方法

個人的には過去の試験結果との比較を行うような試験はそれほど高頻度で行うわけではないため、演算時間が2.2倍程度なら十分許容出来ます。

しかし、ケースによっては出来るだけ処理性能を落とさずに安定(決定)的な結果を得たいこともあるでしょう。

その場合、実装に若干の修正が必要になりますが、下記のようなパラメータ追加で対応可能です。


性能低下を抑えて安定(決定)的な演算結果を得られるよう修正

class CNN(chainer.Chain):

def __init__(self):
super(CNN, self).__init__(
conv1=L.Convolution2D(1, 32, 5, stride=1, deterministic=True),
conv2=L.Convolution2D(32, 64, 5, stride=1, deterministic=True),
l1=L.Linear(None, 512),
l2=L.Linear(None, 10),
)

def __call__(self, x):
x_4d = x.data.reshape(len(x.data), 1, 28, 28)
h = F.relu(self.conv1(x_4d), use_cudnn=False)
h = F.max_pooling_2d(h, 2, stride=1, use_cudnn=False)
h = F.relu(self.conv2(h), use_cudnn=False)
h = F.max_pooling_2d(h, 2, stride=1, use_cudnn=False)
h = F.relu(self.l1(h), use_cudnn=False)
return self.l2(h)


記述のポイントは下記の3点です。


  • L.Convolution2Dにdeterministic=Trueを指定

  • F.max_pooling_2dにuse_cudnn=Falseを指定

  • F.reluにuse_cudnn=Falseを指定

Convolution2DではcuDNNを使用するもののdeterministic(決定的)なアルゴリズムを使用するよう指定しています。(本記事冒頭のIssuesにある通り、v1.18.0にて追加されました)

max_pooling_2dとreluではdeterministicの指定が出来ないためcuDNNを使用しないよう指定しています。

(max_pooling_2dでcuDNNを使用すると結果が非決定的になります)

尚、この実装の場合は、環境変数CHAINER_CUDNNは指定しない、または1を指定する必要があります。


速度を気にしつつ再度実行します


GPUによる実行結果(10Epoch目の結果だけを抽出)

$ export CHAINER_CUDNN=1

$ python mnist_chainer_deterministic.py
epoch main/loss validation/main/loss main/accuracy validation/main/accuracy elapsed_time
10 0.00820688 0.0408237 0.997649 0.991242 80.8627
10 0.00820688 0.0408237 0.997649 0.991242 80.2272
10 0.00820688 0.0408237 0.997649 0.991242 80.7561
10 0.00820688 0.0408237 0.997649 0.991242 80.8799
10 0.00820688 0.0408237 0.997649 0.991242 80.7954

期待通り、ほとんど演算性能を落とさずに安定(決定)的な演算結果を得ることが出来ました。

上記実行例では僅か2.5%の速度低下で安定(決定)的な演算結果を得られました。


結論

ChainerでGPU演算を用いて安定(決定)的な演算を行いたい場合は下記2つの方法がある。


  • 環境変数CHAINER_CUDNNに0を設定しcuDNNを無効にする

  • LinkやFunction系APIの引数でuse_cudnn=Falseやdeterministic=Trueを個別指定する


余談

TensorFlow, Torch, Caffeなど他のフレームワークでもこの問題に取り組んでいるようですが、Chainerが一番美しく解決されているようです。