CUDAはなぜ"決定論的"でないのか
以前、研究においてCUDAを用いた実装を行う機会があったのですが、実験の再現性を確保するため、乱数シードを固定し、同一のGPU環境で何度も実行したにもかかわらず、出力結果が毎回わずかに異なるという現象に遭遇してしまいました。。。
当初は乱数の扱いやコーディングミスを疑ったが、調査を進めるうちに、これは実装の誤りではなく、CUDAが本質的に持つ「決定論性を保証しない」という性質に起因するものであることが分かったので簡易的にまとめてみます!
そもそも決定論的とは、、?
本題に入る前に、「決定論的(deterministic)」という言葉を整理しておく必要があります。
一般に、あるプログラムが決定論的であるとは、
- 同じ入力を与えたとき
- 同じ内部状態遷移を経て
- 常に同じ出力を返す
という性質を持つことを指すと思います。
多くの開発者にとって、この性質はほとんど意識されない前提になっているかと思います。
なぜなら、CPU上で動作する通常のプログラムは、ほぼ常に「決定論的に見える」からです。
CPUプログラムが決定論的に見える理由
CPU上で実行されるシングルスレッドプログラムでは、命令は基本的に上から下へ、順番に実行され、加算、代入、条件分岐といった操作は、明確な順序を持って処理されます。
そのため、
- 実行順序が固定されている
- 同じ命令列は同じ結果を生む
という感覚が自然に成立するかと思われます。
しかし、ここで重要なのは「CPUが決定論的だから」ではなく、正確には、CPUプログラムは、実行順序が暗黙的に固定されているため、決定論的に見えているというだけです。
並列性が入った瞬間に崩れる
この前提は、並列性が導入された瞬間に揺らぎ始めます。
例えば、複数スレッドが同じ変数を更新する場合を考えると、スレッドAとスレッドBが同時に値を読み、同時に書き戻すとき、その実行順序が保証されなければ、結果は一意に定まりません。
CPUにおいても、マルチスレッドプログラミングでは、ロックや同期を適切に行わなければ再現性は失われてしまいます。
つまり、「決定論性」はCPUが本質的に持つ性質ではなく、
- 実行順序を固定しているか
- その順序がプログラマにとって予測可能か
という設計の結果にすぎないと考えられます。
CUDAが前提としてるもの
CUDAは、最初からこの前提を置いていません。
GPUは数千〜数万のスレッドを同時に実行することを前提とした計算機であり、個々のスレッドの実行順序を一意に定めることを目的としておらず、どのスレッドが先に実行されるか、どのwarpが先にスケジューリングされるか、それらはハードウェアや実行状況に依存し、毎回同じとは限りません。
CUDAにおける「非決定性」とは、ランダムに振る舞っているという意味ではなく、実行順序を保証しない設計であるという、明確な前提があります。
決定論と再現性
ここで一度、混同されがちな2つの概念を分けておきます。
- 決定論(determinism)
- 再現性(reproducibility)
決定論とは、「理論的に一意の結果が定まる」ことを指す一方、再現性とは、「実際の実行結果が毎回一致する」ことを指します。
CUDAでは、決定論的なアルゴリズムを書いていても実行順序が変わることで再現性が失われるという状況が起こり得るということです。
再現性の対策
ここまでで、CUDAが「決定論的でない」とはどういう意味なのかを整理してきました。では実際に、研究や実験において再現性が必要な場合、どのように対処すればいいのでしょうか。
以下は、私自身が実際に用いた設定です。
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
ここでは、可能な限り再現性を高めるために、乱数シードの固定とcuDNNの挙動を制御しており、PyTorchでは、GPU計算の多くを NVIDIA の深層学習ライブラリである cuDNN に委ねています。
cuDNNには複数の計算アルゴリズムが存在し、デフォルトでは実行時に最も高速と判断されたものが自動的に選択されるようになっています。
しかし、この中には並列実行順序が不定となるアルゴリズムも含まれており、その結果、浮動小数点演算の丸め誤差の影響で、実行ごとに微小な差が生じることがあります。
そこで、torch.backends.cudnn.deterministic = True を指定することで、PyTorchは 決定論的なアルゴリズムのみを使用する ようになります。
また、torch.backends.cudnn.benchmark = False を設定することで、実行ごとに異なるアルゴリズムが選択されることを防ぎ、毎回同じ計算経路を通るように制御します。
ただし、これはCUDAを完全に正確に決定論的にする設定ではなく、「再現性を優先する」という選択を明示的に行っているに過ぎず、その代償として計算速度の低下が発生する可能性があります。CUDAが本来捨てた「順序」を、必要な場面だけで部分的に取り戻している、と考えるのが正確だと思います。
参考文献
-
NVIDIA Corporation, CUDA C Programming Guide
https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html -
NVIDIA Corporation, Floating Point and IEEE 754
https://docs.nvidia.com/cuda/floating-point/index.html -
PyTorch Team, Reproducibility
https://pytorch.org/docs/stable/notes/randomness.html -
NVIDIA Corporation, cuDNN Developer Guide
https://docs.nvidia.com/deeplearning/cudnn/developer-guide/index.html
Discussion