03. GPU Version: Moving Arrays and Loops to the GPU
You rewrite the program of the 02. MPI version into heat1d_gpu, which can be built for either the GPU or the CPU. GPU memory, MISO_LAMBDA, and capturing this are explained in Chapter 9, and the points to watch in this assignment in Chapter 10. Copy the source of the MPI version to 03_gpu.cpp and rewrite it from there.
What you build
Switching the place of computation
At the top of the source, write
using Backend = backend::CUDA;if__CUDACC__is defined, andusing Backend = backend::Host;otherwise.Change the arrays used in the computation to
Array1D<Real, Backend>, and the first argument offor_eachandreducetoBackend{}.Write every lambda with
MISO_LAMBDA, and useutil::sinand the like for math functions (std::sincannot be used on the GPU).
Ghost-cell exchange
Create a struct Halo that bundles the exchange. Its members are as follows.
Member |
Role |
|---|---|
|
The number of physical cells on this rank |
|
The left and right neighboring ranks ( |
|
The communicator used for communication |
|
Send and receive arrays on the computing side (2 elements; |
|
Send and receive arrays on the host (2 elements) |
Allocate the arrays in the constructor’s member initializer list (Chapter 4), and do not reallocate them at every step.
The member function void apply(Array1D<Real, Backend> &u) processes in the following order.
With a
for_each(Range1D{0, 1}), packu[1]intosend[0]andu[n]intosend[1].Copy to the host with
h_send.copy_from(send);.Perform the same two
MPI_Sendrecvcalls as in the MPI version, usingh_sendandh_recv. The first call sendsh_send[0]to the left neighbor and receives intoh_recv[1]from the right neighbor. The second call sendsh_send[1]to the right neighbor and receives intoh_recv[0]from the left neighbor. You can pass the addresses ash_send.data()andh_send.data() + 1.Copy to the computing side with
recv.copy_from(h_recv);.With a
for_each(Range1D{0, 1}), setu[0] = recv[0]if there is a left neighbor andu[0] = -u[1]if not, and do the same for the right side.
Do not use member variables (such as n, left, and right) inside a lambda. Before the lambda, copy them into local variables, as in const int n_ = n; and const bool has_left = (left != MPI_PROC_NULL);, and use those inside the lambda.
Error and output
Compute the error with
reduce(Backend{}, ...). The return value is a host value, so you can pass it directly toMPI_Allreduce.Before writing the output, create
Array1D<Real> u_host(n + 2);, copy to the host withu_host.copy_from(u);, and then callMPI_Gather.At the start of the first printed line, add
[gpu]for the GPU build and[cpu]for the CPU build. Also distinguish the file names, as inresult_N64_P2_gpu.txt/result_N64_P2_cpu.txt.
Build setup
For the CPU, as with the MPI version, just create heat1d_gpu from 03_gpu.cpp and link MISO::MISO.
For the GPU, add to CMakeLists.txt so that the following two things happen when you give -DMISO_USE_CUDA=ON to cmake.
Before bringing in MISO with
add_subdirectory, enable CUDA (enable_language(CUDA)). Before that, decide the generation of the target GPU,CMAKE_CUDA_ARCHITECTURES(nativeif not specified).Make
03_gpu.cppbe compiled as a CUDA source (set_source_files_properties(03_gpu.cpp PROPERTIES LANGUAGE CUDA)).
The part you add looks, for example, like the following.
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 has the same name as the setting in MISO’s miso/CMakeLists.txt, so CUDA is also enabled on the MISO side, and options such as --expt-extended-lambda are added. Create the GPU build in a separate folder from the CPU build.
cmake -B build-gpu -S . -DMISO_DIR=/path/to/MISO -DMISO_USE_CUDA=ON
cmake --build build-gpu -j
What to check
CPU build (possible even without a GPU)
With
cmp, check that the results ofmpiexec -np P ./build/heat1d_gpu 64(\(P = 1, 2, 4\)) matchresult_N64_P<P>.txtof the MPI version bit for bit. If they match, the rewrite toBackendand the exchange through the send and receive arrays are written correctly.
GPU build
Error: Run \(N = 32, 64, 128, 256, 512\) on 1 rank, and check that
max error (exact)agrees with the table of the serial version to the 4th or 5th digit, and thatmax error (discrete)is at most \(10^{-11}\).Number of ranks: Run with \(P = 1, 2, 4\), and check that the files from the GPU builds match one another bit for bit. Even with a single GPU, multiple ranks can share the same GPU (
miso::Envselects the GPU to use from the rank number within the node).Comparison with the CPU: Compute the maximum difference over all cells from the CPU result for the same \(N\) and \(P\).
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}'
Treat the results as matching if the maximum difference is about \(10^{-12}\) or less. The last digits can differ because of FMA and differences in the implementation of math functions, so we do not require bit-for-bit agreement (it may still match). The printed
max error (discrete)can also differ from the CPU in the last digits (for example1.249e-14and1.247e-14).
If an illegal memory access was encountered is printed at run time, first check whether a lambda contains a member variable or a view of a host array (Array1D<Real>). For other errors, see the reference “GPU Troubleshooting”.
Criteria for moving on
Once the CPU build matches the MPI version and, if you can use a GPU, you have checked 2 to 4 above with the GPU build, the heat conduction Practice is complete. Move on to Chapter 11 and read MISO’s time evolution.