Communicators & reductions¶
The MPI stub¶
NuMPI.MPI is mpi4py's MPI module if mpi4py can be imported. Otherwise it is
NuMPI.MPIStub, an implementation of the parts of mpi4py's interface that
NuMPI and the codes built on it use, for a single process. NuMPI._has_mpi4py
tells which one is in use.
The stub provides
COMM_WORLDandCOMM_SELF, both withrank == 0andsize == 1(Get_rank(),Get_size(),Barrier()/barrier());- the collectives
bcast,gather,allgather,scatter,reduce,allreduce, and the buffer-basedReduce,AllreduceandAllgather, which with a single process copy or return their input; FilewithOpen,Set_view,Read,Read_all,Write,Write_allandClose, so that MPI I/O works serially, including on Python file objects;- the datatypes (
_typedict,Create_contiguous,Create_subarray, …), reduction operations (SUM,MAX,MIN, …) and file modes (MODE_RDONLY,MODE_WRONLY,MODE_CREATE, …) these need.
Code that only uses these runs serially without an MPI installation:
from NuMPI import MPI
comm = MPI.COMM_WORLD
if comm.rank == 0:
print(f"Running on {comm.size} process(es)")
Communicators¶
NuMPI's functions take a communicator argument (comm or communicator).
Three kinds are accepted:
| Communicator | Requires |
|---|---|
mpi4py communicators, e.g. MPI.COMM_WORLD or the result of Split |
mpi4py |
the stub's COMM_WORLD/COMM_SELF |
nothing |
muGrid's Communicator, e.g. muGrid.Communicator.world() |
muGrid, but not mpi4py |
Communicators other than mpi4py's are used through a small protocol, so any object that implements it works:
allreduce(array, op)withopone of"sum","max","min", returning an array of the same shape and data type on every process;compare(other), returning"identical","congruent","similar"or"unequal";- the attributes
sizeandrank.
Reductions¶
Reduction(comm) wraps a communicator into an object with numpy-like
reductions over a distributed array: each process passes its local data, and
every process receives the global result. Reduction() without a
communicator reduces over a single process.
| Method | Result |
|---|---|
pnp.sum(arr, axis=None) |
global sum |
pnp.max(arr, axis=None) |
global maximum |
pnp.min(arr, axis=None) |
global minimum |
pnp.mean(arr) |
global mean, \(\sum_p \sum_i a_i / \sum_p n_p\) |
pnp.dot(a, b) |
global dot product of two distributed vectors |
pnp.any(arr), pnp.all(arr) |
logical reductions |
Positional and keyword arguments (e.g. axis) are passed to the local numpy
operation before the global reduction. The mean is the mean over all elements
of all processes, so it is correct also when processes hold different numbers
of elements.
import numpy as np
from NuMPI import MPI
from NuMPI.Tools import Reduction
comm = MPI.COMM_WORLD
pnp = Reduction(comm)
local = np.random.default_rng(comm.rank).random(100 + comm.rank)
rms = np.sqrt(pnp.mean(local ** 2)) # same value on every process
NuMPI.Tools.Reduction.same_reduction_group(pnp_a, pnp_b) tells whether two
reduction wrappers reduce over the same group of processes. It compares the
communicators, not the wrapper objects, so two Reduction instances of the
same communicator count as the same group.
Distributed and global quantities¶
NuMPI's parallel algorithms operate on distributed arrays: every process holds a slice of the global data, and scalar quantities (energies, norms, tolerances, Lagrange multipliers) are global, i.e. reduced and the same on every process. The optimizers rely on this split, and user code that feeds them must respect it.
Domain decompositions¶
NuMPI.Tools.Subdivision.suggest_subdivisions(nb_dims, nb_procs) suggests the
number of subdivisions along each of nb_dims dimensions for nb_procs
processes, from the prime factors of nb_procs:
from NuMPI.Tools.Subdivision import suggest_subdivisions
suggest_subdivisions(2, 12) # [2, 6]
suggest_subdivisions(3, 4) # [2, 2, 1]
The FFT engines determine their own decomposition; use the
engine's subdomain_locations and nb_subdomain_grid_pts when data is
transformed with it.