Multi-GPU and MPI¶
GeoSWE scales across multiple GPUs with mpi4py. The domain is split into
strips, each rank owns one subgrid, and ranks exchange ghost (“halo”) rows each
step. One MPI rank drives one GPU.
Running¶
pip install "geoswe[gpu,mpi]" # "geoswe[gpu-rocm,mpi]" on AMD GPUs
mpirun -n 4 python -m mpi4py my_run.py # 4 ranks -> 4 GPUs
python -m mpi4py installs mpi4py’s abort hook, so a rank that raises takes the
job down instead of leaving the others waiting in the next collective until the
scheduler’s wall clock kills them. geoswe.runlib.driver.main and
geoswe.runlib.replay.main do that for failures inside themselves; the hook
covers the rest of your script.
Under Slurm the scheduler can hand each rank its own device, on either vendor:
srun -n 4 --gpus-per-task=1 --gpu-bind=closest python -m mpi4py my_run.py. The
pinning line in the script below then sees one device per rank and is a no-op.
In the script, pass the communicator and a process grid to the solver:
from mpi4py import MPI
import numpy as np
import cupy as cp
from geoswe import Mesh2D, Config, Solver2D
comm = MPI.COMM_WORLD
cp.cuda.Device(comm.rank % cp.cuda.runtime.getDeviceCount()).use() # pin a GPU
NX, NY_GLOBAL, dx = 2000, 2000, 10.0 # NY_GLOBAL must divide by the rank count
NY_LOCAL = NY_GLOBAL // comm.size
j0 = comm.rank * NY_LOCAL # this rank's first global row
# each rank builds ONLY its own subgrid (no global array)
mesh = Mesh2D(nx=NX, ny=NY_LOCAL, dx=dx, dy=dx, ngh=4)
cfg = Config(dtype="float32") # ... and the rest of your configuration
jj = np.arange(j0, j0 + NY_LOCAL) # global y index of each local row
bed_local = np.broadcast_to(0.002 * dx * jj, (NX, NY_LOCAL)).copy()
q0_local = np.zeros((3, NX, NY_LOCAL)); q0_local[0] = 1.0
s = Solver2D(mesh, cfg, q0_local, bed_local, comm=comm, dims=[1, comm.size])
s.step(dt=float(s.cfl_dt())) # cfl_dt() does the global all-reduce
dims=[Px, Py]sets the process grid;[1, N]is a 1-D split in \(y\). PassNoneto let MPI choose.cfl_dt()performs a global all-reduce so every rank advances with the same stabledt(lockstep).Halo exchange of the conserved state happens inside
step(); you do not call it explicitly.
A complete, runnable weak/strong scaling benchmark is
examples/ex05_scaling_bench.py. It also runs on one GPU (mpirun -n 1, or
plain python). It pins rank r to GPU r modulo the number of visible GPUs,
so with more ranks than GPUs the ranks share devices.
Scaling¶
GeoSWE’s per-step cost is dominated by the fused right-hand-side kernel; the halo
exchange is a small, fixed overhead that does not grow with the per-rank domain.
The result is near-ideal scaling: in the synthetic benchmark, weak scaling
(fixed work per GPU) holds 99.5 % efficiency at 16 H100 GPUs and 10.24 billion
cells (98.8 % at 32 Blackwell MIG slices and 20.48 billion cells), and strong
scaling (fixed total problem) reaches 15.5x on 16 H100 GPUs for the compressed
path. The only cost that does not shrink with the per-rank domain is the
inter-rank communication (the scalar dt all-reduce plus the halo), so
strong-scaling efficiency tapers once each rank holds too few cells, the classic
surface-to-volume trade-off. The application runs carry 0.4 to 1.1 billion
active cells per rank and sit deep in the weak-scaling regime. See
benchmarks.
Tip
On hardware without GPU peer access (e.g. MIG slices), force host-staged halo
exchange with SWE_HALO_CUDA_AWARE=0. With =1 the halo buffers go straight to
MPI as device pointers, with no host round-trip, which is what the published
multi-GPU benchmark runs use. See the configuration reference.
That variable, and every other GEOSWE_*/SWE_* setting, is read from each
rank’s own environment, so a launch that spans nodes has to forward it (-x
per variable with Open MPI). A rank that misses one either fails the run or
quietly computes something else, depending on the variable; see
performance, which also covers measuring where a run’s
per-step time goes before changing anything.
On AMD GPUs the same variable selects GPU-aware MPI. With HPE Cray MPICH that
also needs MPICH_GPU_SUPPORT_ENABLED=1 and an mpi4py linked against the GPU
transport library; GeoSWE falls back to host staging, with a warning, when Cray
MPICH is running without its GPU support. See AMD GPUs.
Large domains¶
For continental-scale problems that do not fit a dense grid, combine MPI with the compressed active-cell mesh, which partitions the active cells evenly across ranks and supports checkpoint/resume across job-time limits.