03. GPU 版:配列とループを GPU へ移す
02. MPI 版 のプログラムを、GPU でも CPU でもビルドできる heat1d_gpu に書き換えます。
GPU のメモリ、MISO_LAMBDA、this の取り込みは 第 9 章、この課題での注意点は 第 10 章 で説明しています。
MPI 版のソースをコピーして 03_gpu.cpp とし、そこから書き換えます。
作るもの
計算場所の切り替え
ソースの先頭で、
__CUDACC__が定義されていればusing Backend = backend::CUDA;、そうでなければusing Backend = backend::Host;とします。計算に使う配列を
Array1D<Real, Backend>に、for_each・reduceの第 1 引数をBackend{}に変えます。ラムダはすべて
MISO_LAMBDAで書き、数学関数はutil::sinなどを使います(std::sinは GPU で使えません)。
ゴーストセルの交換
交換をまとめた構造体 Halo を作ります。メンバは次のとおりです。
メンバ |
役割 |
|---|---|
|
このランクの物理セル数 |
|
左右の隣のランク(なければ |
|
通信に使うコミュニケータ |
|
計算する側の送受信用の配列(2 要素。 |
|
ホストの送受信用の配列(2 要素) |
配列はコンストラクタの初期化子リストで確保し(第 4 章)、ステップごとに確保し直しません。
メンバ関数 void apply(Array1D<Real, Backend> &u) は、次の順に処理します。
for_each(Range1D{0, 1})で、u[1]をsend[0]に、u[n]をsend[1]に詰めます。h_send.copy_from(send);でホストへ写します。MPI 版と同じ 2 回の
MPI_Sendrecvを、h_sendとh_recvで行います。 1 回目はh_send[0]を左隣へ送り、右隣からh_recv[1]に受け取ります。2 回目はh_send[1]を右隣へ送り、左隣からh_recv[0]に受け取ります。 アドレスはh_send.data()、h_send.data() + 1のように渡せます。recv.copy_from(h_recv);で計算する側へ写します。for_each(Range1D{0, 1})で、左隣があればu[0] = recv[0]、なければu[0] = -u[1]とし、右側も同様にします。
ラムダの中でメンバ変数(n、left、right など)を使わないでください。
ラムダの前で const int n_ = n;、const bool has_left = (left != MPI_PROC_NULL); のようにローカル変数にコピーし、ラムダの中ではそれを使います。
誤差と出力
誤差は
reduce(Backend{}, ...)で求めます。戻り値はホストの値なので、そのままMPI_Allreduceに渡せます。書き出す前に、
Array1D<Real> u_host(n + 2);を作り、u_host.copy_from(u);でホストへ写してからMPI_Gatherします。表示の 1 行目の先頭に、GPU 用のビルドなら
[gpu]、CPU 用なら[cpu]を付けます。 ファイル名もresult_N64_P2_gpu.txt/result_N64_P2_cpu.txtのように区別します。
ビルドの設定
CPU 用には、MPI 版と同じく heat1d_gpu を 03_gpu.cpp から作り、MISO::MISO をリンクするだけです。
GPU 用には、cmake に -DMISO_USE_CUDA=ON を与えたとき、次の 2 つが行われるように CMakeLists.txt に書き足します。
add_subdirectoryで MISO を取り込む前に、CUDA を使えるようにする(enable_language(CUDA))。 その前に、対象の GPU の世代CMAKE_CUDA_ARCHITECTURESを決めておきます(指定がなければnative)。03_gpu.cppを CUDA のソースとしてコンパイルさせる(set_source_files_properties(03_gpu.cpp PROPERTIES LANGUAGE CUDA))。
書き足す部分は、たとえば次の形になります。
option(MISO_USE_CUDA "Enable CUDA support" OFF)
if(MISO_USE_CUDA)
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
set(CMAKE_CUDA_ARCHITECTURES native CACHE STRING "CUDA architectures")
endif()
enable_language(CUDA)
set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()
MISO_USE_CUDA は MISO の miso/CMakeLists.txt の設定と同じ名前なので、MISO 側でも CUDA が有効になり、--expt-extended-lambda などが付きます。
GPU 用のビルドは、CPU 用とは別のフォルダに作ります。
cmake -B build-gpu -S . -DMISO_DIR=/path/to/MISO -DMISO_USE_CUDA=ON
cmake --build build-gpu -j
確かめること
CPU 用のビルド(GPU がなくてもできます)
mpiexec -np P ./build/heat1d_gpu 64(\(P = 1, 2, 4\))の結果が、MPI 版のresult_N64_P<P>.txtとビット単位で一致することをcmpで確かめます。 一致すれば、Backendへの書き換えと、送受信用の配列を経由する交換が正しく書けています。
GPU 用のビルド
誤差:\(N = 32, 64, 128, 256, 512\) を 1 ランクで実行し、
max error (exact)が 逐次版 の表と 4〜5 桁目まで一致すること、max error (discrete)が \(10^{-11}\) 以下であることを確かめます。ランク数:\(P = 1, 2, 4\) で実行し、GPU 用のビルドどうしでファイルがビット単位で一致することを確かめます。 GPU が 1 枚でも、複数のランクで同じ GPU を共有して実行できます(
miso::Envが、ノード内のランク番号から使う GPU を選びます)。CPU との比較:同じ \(N\)・\(P\) の CPU の結果と、全セルの最大の差を求めます。
paste result_N64_P1_cpu.txt result_N64_P1_gpu.txt \ | awk '{d = $2 - $4; if (d < 0) d = -d; if (d > m) m = d} END {print m}'
最大の差が \(10^{-12}\) 程度以下なら一致とみなします。 FMA や数学関数の実装の違いで最後の桁が違うことがあるので、ビット単位の一致は求めません(一致することもあります)。
max error (discrete)の表示も、CPU と最後の桁が違うことがあります(たとえば1.249e-14と1.247e-14)。
実行時に an illegal memory access was encountered と表示されたら、まずラムダの中にメンバ変数や、ホストの配列(Array1D<Real>)のビューがないかを調べます。
その他のエラーは 参考資料「GPU で困ったとき」 を参照してください。
次へ進む目安
CPU 用のビルドで MPI 版と一致し、GPU を使える場合は GPU 用のビルドで上の 2〜4 を確かめたら、熱伝導の実習は完了です。 第 11 章 へ進み、MISO 本体の時間発展を読みます。