GPU で困ったとき
GPU 向けのコードの基本は 第 9 章 で説明しています。
ここでは、ビルドや実行で問題が起きたときに確かめることをまとめます。
ソースのパスは、MISO を clone したフォルダからのパスです。
エラーの文面は CUDA 12.9 の nvcc と RTX 4090 で確かめたもので、版によって少し変わることがあります。
1. CMake の設定で止まる
症状 |
確かめること |
|---|---|
|
|
CPU 版から切り替えたら設定が反映されない |
同じビルドフォルダを使い回していないか。CPU 用と GPU 用でフォルダを分けるか、フォルダを消して作り直す |
|
自分の |
|
ビルドする計算機に GPU がない(ログインノードなど)。使う GPU の値( |
2. コンパイルできない
エラーの文面(抜粋) |
原因と対処 |
|---|---|
|
|
|
|
|
ラムダの中で、所有する配列( |
|
ラムダやカーネルから、 |
|
テンプレートのカーネルで |
g++ が |
カーネルを |
拡張ラムダ(__host__ __device__ を付けたラムダ)には、ここに挙げた以外にも置き場所や取り込み方の制約があります。
利用している版の NVIDIA CUDA C++ Programming Guide(https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html)の extended lambda の節を確かめてください。
3. 実行時に止まる
3.1 よくあるエラー
表示 |
原因の候補 |
|---|---|
|
ラムダが |
|
|
|
CUDA Toolkit がドライバより新しい。ドライバが対応する版の CUDA Toolkit を使う( |
|
MISO の |
illegal memory access の中で最も見つけにくいのは this の取り込みです。
CPU ビルドでは正しく動き、HMM が有効な一部の GPU 環境でも偶然動くためです(MISO issue #170)。
ラムダの中で使っている名前が、ローカル変数か引数かを、宣言まで戻って確かめてください(第 9 章 5 節)。
3.2 エラーの場所を突き止める
カーネルの起動は非同期なので、MISO_CUDA_CHECK が表示するファイルと行は、エラーに気づいた場所です。
エラーが起きたカーネルは、それより前に起動したものです(第 9 章 7 節)。
環境変数 CUDA_LAUNCH_BLOCKING=1 を付けて実行すると、カーネルの起動が終了まで待つようになり、
起動直後の MISO_CUDA_CHECK(cudaGetLastError()) でエラーが報告されます。
第 9 章 5.1 の例では、表示が次のように変わりました。
# run as is
CUDA Error: an illegal memory access was encountered .../miso/include/miso/array1d.hpp 109
# run with CUDA_LAUNCH_BLOCKING=1
CUDA Error: an illegal memory access was encountered .../miso/include/miso/execution.hpp 91
array1d.hpp は後の copy_from の中、execution.hpp は CUDA 版 for_each の起動直後の検査です。
さらに、for_each を呼んだ場所を調べるには、自分のコードの for_each の直後に MISO_CUDA_CHECK(cudaDeviceSynchronize()); を一時的に足して範囲を狭めます。
compute-sanitizer(CUDA Toolkit に含まれる)で実行すると、不正な読み書きをしたカーネルとスレッドが表示されます。
compute-sanitizer ./this_capture
mpiexec -np 2 compute-sanitizer ./build/mhd2d_vortex --config=config.yaml # with MPI
第 9 章 5.1 の例での表示(抜粋)です。
========= Invalid __global__ read of size 4 bytes
========= at void miso::for_each_kernel<Scaler::apply_wrong(miso::Array1DView<float>) const::[lambda(int) (instance 1)]>(miso::Range1D, T1)+0xd0
========= by thread (0,0,0) in block (0,0,0)
========= Access to 0x7fff1107e0a4 is out of bounds
apply_wrong の中のラムダを実行する for_each_kernel が、4 バイト(float のメンバ factor)を読もうとしたことが分かります。
0x7fff... で始まる番地は、Linux ではホストのスタック(ローカル変数を置く場所)によく使われる番地で、ホストのオブジェクトを読もうとした手がかりになります。
compute-sanitizer の下では実行がかなり遅くなるので、格子を小さくし、時間ステップ数を減らして試します。
4. MPI と組み合わせたとき
CUDA-aware MPI を使っているか。 MISO の CUDA 版のハロー交換は、デバイスのバッファの番地をそのまま
MPI_Isend・MPI_Irecvに渡します(miso/include/miso/mhd_halo_exchange.hpp)。 CUDA に対応していない MPI では、通信の中で止まるか、誤った値を受け取ります。NVIDIA HPC SDK に含まれる Open MPI など、CUDA に対応した MPI を使います。ランクと GPU の対応。 MISO はノード内のランク番号を GPU の数で割った余りで GPU を選びます(
miso/include/miso/env.hppのcuda::Env)。 ランク数が GPU の数より多いと、1 つの GPU を複数のランクで分け合います。完了待ちの順序。 GPU のカーネルが書いたバッファを MPI へ渡す前に
cudaDeviceSynchronizeで待ちます。 受信した値をカーネルで使う前にはMPI_Waitallで待ちます。2 つの完了待ちは別のものです。コンテナの中で実行するとき。 MISO の demo は
demo/shared/openmpi_config.envの環境変数を読み込んでからmpiexecを実行します。
5. CPU と GPU で数値が違うとき
まず、入力(config.yaml)、格子、MPI の分割、Real の型(float/double)を揃えます。
そのうえで、差の大きさと場所を調べます。
差の様子 |
確かめること |
|---|---|
総和・平均などの縮約値の最後の数桁 |
加算の順序の違い(Host 版は前から順、CUDA 版はブロック内のトーナメント)。誤りではない |
|
|
格子点ごとの値が丸め誤差程度に違う |
|
境界の近くだけ違う |
境界条件、ゴーストセルの更新、ハロー交換 |
特定の軸に沿ってずれる |
添字の順序、 |
最初のステップから大きく違う |
初期条件がデバイスへ写っているか( |
比較の方法と許す誤差の決め方は テストと計算結果の比較 を参照してください。 CPU と GPU の結果がビット単位で一致することは前提にしません。