Appearance
4. The GPU twin
RIPPLE's FNL backend runs the same realm on an NVIDIA GPU with OpenACC. It is a sibling of the CPU realm, not an extension of it: ripple_fnl_object extends the same common realm, keeps the state on the device, and replaces the procedures that touch q. The forest does not know which backend it drives.
The driver
src/app/ripple/fnl/adam_ripple_fnl.F90 is the CPU driver of chapter 1 with one type changed:
fortran
use :: adam_ripple_fnl_object, only : ripple_fnl_object
type(ripple_fnl_object), target :: realm(1) !< The realm.(the lines that differ, besides the header comment and the program/endprogram names; the rest of the file is the CPU driver's)
The device state
src/app/ripple/fnl/adam_ripple_fnl_object.F90, the type:
fortran
type, extends(ripple_common_object) :: ripple_fnl_object
!< RIPPLE FNL realm.
type(field_fnl_object) :: field_fnl !< Field helper: coordinates, maps, ghost exchange.
type(rk_fnl_object) :: rk_fnl !< Runge-Kutta helper: the stages on the device.
type(weno_fnl_object) :: weno_fnl !< WENO helper: the coefficients on the device.
real(R8P), pointer :: q_gpu(:,:,:,:,:)=>null() !< State [nb, 1-ngc:ni+ngc, ..., 1].
real(R8P), pointer :: dq_gpu(:,:,:,:,:)=>null() !< Residuals, same shape.
real(R8P), pointer :: flx_gpu(:,:,:,:,:)=>null() !< X-face fluxes [nb, 0:ni, 1:nj, 1:nk, 1].
real(R8P), pointer :: fly_gpu(:,:,:,:,:)=>null() !< Y-face fluxes [nb, 1:ni, 0:nj, 1:nk, 1].
real(R8P), pointer :: flz_gpu(:,:,:,:,:)=>null() !< Z-face fluxes [nb, 1:ni, 1:nj, 0:nk, 1].
real(R8P), pointer :: face_gpu(:,:)=>null() !< One face's skin or reflux mismatch [1, largest face].
real(R8P), allocatable :: face(:,:) !< Its host copy, the same extent.
contains
! ... the type-bound procedures (lines 50-68 of the source)
endtype ripple_fnl_objectThree things change from the CPU realm:
- The library's FNL helpers.
field_fnl,rk_fnlandweno_fnlare the device side of the library objects the common realm already owns: the ghost exchange (GPU-direct MPI), the Runge–Kutta stages, the WENO coefficients. They are components of the realm, initialized from the common objects by argument. - The layout is transposed,
(b, i, j, k, v)against the host(v, i, j, k, b): the block index is stride-1, so it is the innermost loop of every kernel and adjacent threads touch adjacent memory. - Device arrays are FUNDAL pointers, allocated by
dev_alloc, each call checked:
fortran
call dev_alloc(fptr_dev=self%q_gpu, lbounds=[1,1-ngc,1-ngc,1-ngc,1], ubounds=[nb,ni+ngc,nj+ngc,nk+ngc,1], &
init_value=0._R8P, ierr=ierr) ; call check('q_gpu')check stops the run with the array's name when ierr /= 0 (code 101 is a device out of memory). The host copy of a device buffer has the device buffer's extent: a copy between arrays of different extents goes through a host temporary and fails on the device (GPU cookbook).
Initialization
initialize_forest, whole:
fortran
subroutine initialize_forest(self, filename, realms_number, memory_avail, nv, verbose)
!< Initialize the realm: MPI and the device (once per process), the common data within the device budget, the FNL
!< helpers, the device data; initial AMR and initial condition on the host, then the state to the device.
class(ripple_fnl_object), intent(inout) :: self !< The realm.
character(*), intent(in) :: filename !< Input file name.
integer(I4P), intent(in), optional :: realms_number !< Unused: RIPPLE is single-realm.
real(R8P), intent(in), optional :: memory_avail !< Unused: the budget comes from the device.
integer(I4P), intent(in), optional :: nv !< Unused: nv = 1.
logical, intent(in), optional :: verbose !< Unused.
logical :: is_mpi !< MPI already initialized.
if (.not.mpih_fnl_is_initialized) then
call MPI_INITIALIZED(is_mpi, mpih_fnl%error)
call mpih_fnl%initialize(do_mpi_init=.not.is_mpi, do_device_init=.true., verbose=.true.)
mpih_fnl_is_initialized = .true.
mpih_fnl_device_sharers = count_device_sharers()
endif
! the blocks budget is the device memory, shared by the ranks bound to the same device
call self%initialize_ripple(filename=filename, memory_avail=real(mpih_fnl%dev_memory_total, R8P) / 1e9_R8P / &
real(mpih_fnl_device_sharers, R8P))
call self%field_fnl%initialize(grid=self%adam%grid, field=self%adam%field, maps=self%adam%maps, verbose=.true.)
call self%rk_fnl%initialize(grid=self%adam%grid, field=self%adam%field, rk=self%rk)
call self%weno_fnl%initialize(weno=self%weno)
call self%allocate_gpu
if (self%amr%markers_number > 0) call self%amr_update
call self%set_initial_conditions
call self%field_fnl%copy_cpu_gpu(field=self%adam%field, maps=self%adam%maps, verbose=.true.)
call self%copy_state(to_device=.true.)
call self%save_output(fields=.true.)
endsubroutine initialize_forestThe block budget is the device memory divided by the ranks that share the device; the initial AMR and the initial condition run on the host, then the maps and the state go to the device once. The backend also overrides finalize_mpi_forest, which the forest calls once after every realm is finalized, to close mpih_fnl.
A kernel
The face fluxes of chapter 2 on the device, compute_face_fluxes_dev in fnl/adam_ripple_fnl_kernels.F90, the loop nest:
fortran
ap = max(a, 0._R8P)
am = min(a, 0._R8P)
!$acc parallel loop independent gang vector collapse(4) DEVICEVAR(weno_a_gpu,weno_p_gpu,weno_d_gpu,q_gpu,fl_gpu) &
!$acc& firstprivate(di,dj,dk,ni,nj,nk,blocks_number,S,ap,am,weno_zeps) private(v,vr)
do k=1-dk, nk
do j=1-dj, nj
do i=1-di, ni
do b=1, blocks_number
!$acc loop seq
do m=1-S, S-1
v(2,m+S) = ap * q_gpu(b,i+m*di,j+m*dj,k+m*dk,1)
v(1,m+S) = am * q_gpu(b,i+(m+1)*di,j+(m+1)*dj,k+(m+1)*dk,1)
enddo
call weno_reconstruct_upwind_dev(S=S, weno_a=weno_a_gpu, weno_p=weno_p_gpu, weno_d=weno_d_gpu, &
weno_zeps=weno_zeps, weno_rmu=1._R8P, V=v, VR=vr)
fl_gpu(b,i,j,k,1) = vr(1) + vr(2)
enddo
enddo
enddo
enddoIt is the CPU loop with the block index innermost: host scalars are firstprivate, device arrays DEVICEVAR (FUNDAL's macro for deviceptr), and the WENO primitive is the library's device twin, weno_reconstruct_upwind_dev. RIPPLE's kernels carry OpenACC directives only (NVIDIA); FLUME's kernels also carry an !$omp twin of each directive for the AMD OpenMP-offload build (GPU cookbook).
Build and check: oracle r4
bash
fobis build --mode ripple-fnl-nvf --varset local_nvf # exe/adam_ripple_fnl
./src/tests/ripple/check.sh --legs r4 # after r1 and r2: it compares against their CPU runstext
16 blocks, largest |u_1 - u_2| = 1.665e-15
PASS [r4 periodic]
28 blocks, largest |u_1 - u_2| = 4.241e-14
PASS [r4 amr]
total of u dV: 482 rows, largest drift 1.388e-17 of max|u0| times the volume
PASS [r4 conservation]
RIPPLE oracles (r4) PASSEDThe FNL runs of periodic.ini and amr.ini agree with the CPU runs to round-off, and the device reflux conserves as the host one does. These runs were made on a WSL2 workstation, which checks correctness only: never time a GPU run there.
The seams layer
RIPPLE has one realm, so it stops at the staged layer. A forest of several realms glued by seams adds the third layer: fill_seam_from_peer_forest (or, across ranks, pack_seam_cells_forest and unpack_seam_cells_forest) fills a realm's seam ghosts from its peer, coupling_descriptor_forest reports what the forest checks before it lets two realms exchange stage data, and on FNL after_topology_build_forest moves the seam maps to the device once the forest has built them. FLUME implements all of them (src/app/flume/cpu/adam_flume_cpu_object.F90, src/app/flume/fnl/adam_flume_fnl_object.F90); the forest cookbook shows the inputs, and R11 the FNL side.
What you learned
- A GPU backend is a sibling realm: same common realm, device state, the library's FNL helpers as components.
- Device arrays are transposed with the block index stride-1; every
dev_allocis checked. - A kernel is the CPU loop with OpenACC directives and the library's device primitives; the oracle compares the two backends to round-off.
Previous: 3. AMR and the staged layer. This is the end of the build-your-own track.