MPI with mpi_f08
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and the MPI standard, and was checked on
2026-10-19. MPI is not part of the Fortran standard: it is a separate specification maintained by the MPI Forum, and
its documents page lists MPI 5.0, approved on 5 June 2025, as the latest version. The
examples were compiled with mpifort from MPICH 4.2.1 (a wrapper around GNU Fortran 16.2) and run with mpirun. That
MPICH reports itself as implementing MPI 4.1 (see the first example), so everything shown that is newer than 4.1 is
described but not run. No example needs more than four ranks, and every example except the two deliberately wrong ones
compiles without a warning under mpifort -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all. LLVM Flang 23 was not used: the container’s
MPI library and its mpi_f08 module files were built by gfortran, and module files from one Fortran compiler cannot be
read by another (see Modules and Submodules), so the
MPI examples are labelled gfortran-only. The output shown is real output.
Message passing is a different model from the ones in Coarrays, Images and Synchronization, OpenMP and OpenACC and do concurrent and GPU Offload: a job is a set of independent processes, called ranks, each with its own memory, that cooperate only by sending messages. Nothing is shared unless it is sent, which is why MPI scales from a laptop to a cluster of thousands of nodes and why every exchange is written out explicitly. The running example is the domain decomposition of heatflow's temperature grid: each rank owns a strip of rows, exchanges one row of ghost cells (the halo) with each neighbour on every step, and reduces the final maximum. The serial kernel is in Modules and Submodules; the grid itself is the allocatable array of Allocatable Arrays and Dynamic Memory.
The three Fortran bindings
MPI defines three ways to call it from Fortran (MPI 5.0 section 19.1):
| Binding | How you get it | What it gives you |
|---|---|---|
|
|
Handles are |
|
|
Handles are default |
|
|
Handles are default |
The recommendation is simple: use mpi_f08 in new code. A plain use mpi_f08 is acceptable because every MPI name
starts with MPI_ and a program must not declare names beginning with that prefix (section 2.6.2). Keep the use mpi_f08
statement in the one module or program unit that talks to MPI, as described in
Intrinsic Modules and iso_fortran_env: it is
a vendor-supplied module, not an intrinsic one, so it is not portable across installations and its .mod files are tied to the compiler that built the
MPI library. mpi_f08 names are written with a capital letter after the prefix in the specification (MPI_Comm_rank), but
Fortran is case insensitive and mpi_comm_rank works in all three bindings.
Two consequences of the strong typing matter from the start. First, an integer cannot stand for a handle, and the
compiler says so. Example 7 passes an integer where MPI_Comm_rank wants a communicator:
program wrong_handle
use mpi_f08
implicit none
integer :: comm, rank
call MPI_Init()
comm = 91 ! an integer is not a communicator handle in mpi_f08
call MPI_Comm_rank(comm, rank)
call MPI_Finalize()
end program wrong_handle
Real gfortran 16.2 diagnostic through mpifort:
07_wrong_handle.f90:7:32:
7 | call MPI_Comm_rank(comm, rank)
| 1
Error: There is no specific subroutine for the generic 'mpi_comm_rank' at (1)
Second, the check stops at the handle. The buffer arguments are type(*), so the compiler cannot compare the buffer with
the MPI datatype that describes it: describing a real(dp) array as MPI_REAL is a runtime error that nothing reports.
Describing data: kinds and derived datatypes shows the consequence.
Starting, ranking and finishing
Every MPI program begins with MPI_Init and ends with MPI_Finalize. Between them, a communicator (a group of ranks
plus a private communication context) names who is talking; MPI_COMM_WORLD is the one containing all the ranks started
together. MPI_Comm_rank returns the caller’s number, from 0, and MPI_Comm_size the number of ranks. With mpi_f08
the ierror argument is optional, so the calls are subroutines with the results in the last argument: there is no ierr
variable to declare or forget to check.
program hello_mpi
use mpi_f08
implicit none
integer :: rank, nprocs, version, subversion, namelen
character(len=MPI_MAX_PROCESSOR_NAME) :: node
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
call MPI_Get_version(version, subversion)
call MPI_Get_processor_name(node, namelen)
print '(a,i0,a,i0,a,i0,a,i0,a)', 'rank ', rank, ' of ', nprocs, ' (MPI ', version, '.', subversion, ')'
call MPI_Finalize()
end program hello_mpi
Built with mpifort 01_hello.f90 and run with mpirun -n 4 ./a.out:
rank 0 of 4 (MPI 4.1)
rank 1 of 4 (MPI 4.1)
rank 2 of 4 (MPI 4.1)
rank 3 of 4 (MPI 4.1)
The four lines are written by four different processes to the same terminal, so their order is not defined; here they
happened to come out sorted. The program also shows that this MPICH implements MPI 4.1 although MPI 5.0 is the current
specification: MPI_Get_version reports the version the library implements, which can lag the published standard.
MPI_Get_library_version returns a free-form string identifying the implementation and is the one to print in bug
reports. Started without mpirun the same executable runs as a single rank (rank 0 of 1), which the fpm example below
relies on.
Rules worth knowing (MPI 5.0 chapter 11):
-
MPI_Initmust be called once, before almost every other MPI routine (the exceptions includeMPI_Initialized,MPI_FinalizedandMPI_Get_version), andMPI_Finalizeonce, after all communication has completed. AfterMPI_Finalizeno MPI call is allowed. -
MPI_Finalizeis collective: a rank that stops early (anerror stop, a crash, anexitin one branch) can leave the others blocked.MPI_Abort(comm, code)terminates all ranks;error stopin one rank is not guaranteed to do so (see Error Handling and Program Termination). -
Every rank is a separate process with its own Fortran units. Guard messages with
if (rank == 0), as the examples do; whether and in which order the output of the ranks is merged is the launcher’s business. -
The newer sessions model (
MPI_Session_init) is an alternative toMPI_Initfor libraries that must not depend on a global initialization. It is not shown because the installed MPICH was only exercised through the world model. -
MPI 5.0 also adds a standard application binary interface (chapter 20, with Fortran in 20.4), which would allow one compiled program to run against different implementations. It is described by the specification and not tested here.
Handles are values of derived types
In mpi_f08 every MPI object is represented by a derived type whose only component is an integer, MPI_VAL, so
a handle can be assigned, passed and compared but not confused with an integer or with a handle of another kind. The
operators == and /= are overloaded for each handle type, which is how a communicator is tested against
MPI_COMM_NULL. The next program splits MPI_COMM_WORLD into two groups, compares handles and frees one.
program handles
use mpi_f08
implicit none
type(MPI_Comm) :: world, half
type(MPI_Datatype) :: t
type(MPI_Op) :: op
integer :: rank, subrank
call MPI_Init()
world = MPI_COMM_WORLD
t = MPI_DOUBLE_PRECISION
op = MPI_SUM
call MPI_Comm_rank(world, rank)
! two groups: world ranks {0, 1} and {2, 3, ...}; key = rank keeps the order
call MPI_Comm_split(world, merge(0, 1, rank < 2), rank, half)
call MPI_Comm_rank(half, subrank)
print '(a,i0,a,i0,a,l1,a,l1)', 'world rank ', rank, ' -> sub rank ', subrank, &
', handles equal: ', world == half, ', sub is null: ', half == MPI_COMM_NULL
call MPI_Comm_free(half)
print '(a,i0,a,l1)', 'rank ', rank, ' after free, sub is null: ', half == MPI_COMM_NULL
call MPI_Finalize()
end program handles
Real output with mpirun -n 4 (the lines of the four ranks arrive in any order):
world rank 0 -> sub rank 0, handles equal: F, sub is null: F
rank 0 after free, sub is null: T
world rank 1 -> sub rank 1, handles equal: F, sub is null: F
rank 1 after free, sub is null: T
world rank 2 -> sub rank 0, handles equal: F, sub is null: F
rank 2 after free, sub is null: T
world rank 3 -> sub rank 1, handles equal: F, sub is null: F
rank 3 after free, sub is null: T
Points to remember:
-
MPI_Comm_split(comm, color, key, newcomm)is collective overcomm; ranks with the samecolorland in the same new communicator, ordered bykey. A rank passingMPI_UNDEFINEDas colour getsMPI_COMM_NULL. -
MPI_Comm_freesets the handle to the null value, so a freed handle compares equal toMPI_COMM_NULL, as the second line of each pair shows. Request handles (type(MPI_Request)) behave the same way afterMPI_Wait, which is what letsMPI_Waitallskip finished entries. -
Predefined handles such as
MPI_COMM_WORLD,MPI_DOUBLE_PRECISIONandMPI_SUMare named constants or variables of the corresponding handle type. Store them in a variable of that type (type(MPI_Datatype) :: t), not an integer. -
The integer value is reachable as
handle%MPI_VALfor code that has to talk to C, where the conversion functionsMPI_Comm_f2candMPI_Comm_c2ftranslate between the C and Fortran handle values; see Interoperability with C. -
Handles of the
mpimodule andmpif.hare plain integers; code that stores them inintegervariables and moves tompi_f08must change the declarations (MPI 5.0 gives advice in section 2.5.1).
Point-to-point communication
MPI_Send and MPI_Recv move a buffer between two ranks of a communicator. The arguments are the buffer, the count
of elements, the MPI datatype, the peer rank, a tag that lets a receiver select among messages, the communicator and, for
a receive, a type(MPI_Status) that records where the message came from. The Fortran buffer is passed as written; because
type(*), dimension(..) accepts any array or scalar, no loc, no flattening and no explicit shape is needed.
The messages are matched by the triple (source, tag, communicator), and the messages between a fixed pair of ranks do
not overtake each other. A receive that names a smaller count than the message is an error (MPI_ERR_TRUNCATE); a larger
count is allowed and MPI_Get_count tells how much arrived. That is the basis of the usual idiom for a message of unknown
size: probe, ask the count, allocate, receive.
program p2p
use mpi_f08
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer :: rank, nprocs, n, r, cnt
real(real64), allocatable :: buf(:)
type(MPI_Status) :: st
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
if (rank == 0) then
do r = 1, nprocs - 1
buf = [(real(r*100 + n, real64), n=1, r + 2)]
call MPI_Send(buf, size(buf), MPI_DOUBLE_PRECISION, r, 1, MPI_COMM_WORLD)
end do
else
! size unknown: probe, ask for the count, allocate, then receive
call MPI_Probe(0, 1, MPI_COMM_WORLD, st)
call MPI_Get_count(st, MPI_DOUBLE_PRECISION, cnt)
allocate (buf(cnt))
call MPI_Recv(buf, cnt, MPI_DOUBLE_PRECISION, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE)
print '(a,i0,a,i0,a,*(f6.0))', 'rank ', rank, ' got ', cnt, ' values:', buf
end if
call MPI_Finalize()
end program p2p
Real output with mpirun -n 4:
rank 1 got 3 values: 101. 102. 103.
rank 2 got 4 values: 201. 202. 203. 204.
rank 3 got 5 values: 301. 302. 303. 304. 305.
Rank 0 sends a different number of values to each other rank, and the receivers size their allocatable array
(see Allocatable Arrays and Dynamic Memory)
from the probed count. MPI_STATUS_IGNORE says that the status is not wanted; it is a special constant, not a variable,
and has the matching MPI_STATUSES_IGNORE for arrays of statuses. Useful members of type(MPI_Status) are MPI_SOURCE,
MPI_TAG and MPI_ERROR, as the ring exchange shows:
program ring
use mpi_f08
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer :: rank, nprocs, right, left
real(real64) :: out, in
type(MPI_Status) :: st
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
right = mod(rank + 1, nprocs)
left = mod(rank - 1 + nprocs, nprocs)
out = 10.0_real64*rank
call MPI_Sendrecv(out, 1, MPI_DOUBLE_PRECISION, right, 7, &
in, 1, MPI_DOUBLE_PRECISION, left, 7, MPI_COMM_WORLD, st)
print '(a,i0,a,f5.1,a,i0,a,i0)', 'rank ', rank, ' received ', in, ' from ', st%MPI_SOURCE, ' tag ', st%MPI_TAG
call MPI_Finalize()
end program ring
rank 0 received 30.0 from 3 tag 7
rank 1 received 0.0 from 0 tag 7
rank 2 received 10.0 from 1 tag 7
rank 3 received 20.0 from 2 tag 7
MPI_Sendrecv posts the send and the receive together and is the safe way to write a ring or a halo swap: the pattern
"everybody MPI_Sends to the right, then everybody MPI_Recvs from the left" works for small messages because they are
buffered, and deadlocks for large ones, because MPI_Send may not return until the receiver has started the matching
receive. Which of the two happens depends on the message size and the implementation, so a program that is correct only
for small messages is not correct. Wildcards MPI_ANY_SOURCE and MPI_ANY_TAG exist for receives; MPI_PROC_NULL as a
peer turns a send or receive into a no-operation, which removes the special cases at the ends of a decomposition (the
heatflow example below uses it). The send modes are blocking (MPI_Send), synchronous (MPI_Ssend), buffered
(MPI_Bsend) and ready (MPI_Rsend); MPI_Send is the one to start from.
Collective communication
A collective operation is called by every rank of the communicator, with consistent arguments, and the library chooses the algorithm. They replace long hand-written send and receive loops and are usually faster. The common ones are:
| Routine | Effect |
|---|---|
|
no rank returns until all have called it (rarely needed for correctness) |
|
one root’s buffer is copied to every rank |
|
split the root’s array into equal pieces, one per rank, and the reverse; |
|
every rank receives all pieces, or a personalised piece from every rank |
|
combine one value per rank with |
|
prefix reductions |
The example distributes a root’s array, reduces it two ways and gathers the partial sums.
program collectives
use mpi_f08
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer :: rank, nprocs
integer :: nloc, i
real(real64), allocatable :: whole(:), part(:)
real(real64) :: mine, total, biggest
integer :: cfg(2)
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
cfg = 0
if (rank == 0) cfg = [nprocs*2, 3]
call MPI_Bcast(cfg, 2, MPI_INTEGER, 0, MPI_COMM_WORLD)
nloc = cfg(1)/nprocs
if (rank == 0) then
whole = [(real(i, real64), i=1, cfg(1))]
else
allocate (whole(0))
end if
allocate (part(nloc))
call MPI_Scatter(whole, nloc, MPI_DOUBLE_PRECISION, part, nloc, MPI_DOUBLE_PRECISION, 0, MPI_COMM_WORLD)
mine = sum(part)
call MPI_Reduce(mine, total, 1, MPI_DOUBLE_PRECISION, MPI_SUM, 0, MPI_COMM_WORLD)
call MPI_Allreduce(maxval(part), biggest, 1, MPI_DOUBLE_PRECISION, MPI_MAX, MPI_COMM_WORLD)
call MPI_Gather(mine, 1, MPI_DOUBLE_PRECISION, whole, 1, MPI_DOUBLE_PRECISION, 0, MPI_COMM_WORLD)
print '(a,i0,a,*(f5.0))', 'rank ', rank, ' part:', part
if (rank == 0) then
print '(a,f6.1,a,*(f5.0))', 'total=', total, ' partial sums:', whole(1:nprocs)
end if
print '(a,i0,a,f4.0)', 'rank ', rank, ' sees max ', biggest
call MPI_Finalize()
end program collectives
Real output with mpirun -n 4 (lines from different ranks interleave in any order):
rank 0 part: 1. 2.
total= 36.0 partial sums: 3. 7. 11. 15.
rank 0 sees max 8.
rank 1 part: 3. 4.
rank 1 sees max 8.
rank 2 part: 5. 6.
rank 2 sees max 8.
rank 3 part: 7. 8.
rank 3 sees max 8.
Things that surprise newcomers:
-
The send buffer of
MPI_Scatterand the receive buffer ofMPI_Gatherare significant only at the root, but they must still be valid Fortran arguments on the other ranks, which is why the non-root ranks above allocate a zero-size array rather than pass an unallocated one. Passing an unallocatedallocatablearray to atype(*)dummy is an error. -
maxval(part)as the send buffer ofMPI_Allreduceis an expression, not a variable. The compiler passes a temporary, which is fine for a blocking call but must never be done for a non-blocking one (see below). -
MPI_IN_PLACEas the send buffer makes the receive buffer both input and output, saving a copy. -
A reduction that adds floating-point values is not reproducible between rank counts, because the order of the additions depends on the algorithm and the number of ranks. See Floating-Point and IEEE Arithmetic for why that is expected and not a bug.
-
MPI 4.0 added non-blocking and persistent collectives (
MPI_Iallreduce,MPI_Allreduce_init); the sameasynchronousrules as for non-blocking point-to-point calls apply.
Non-blocking communication and the asynchronous attribute
MPI_Isend and MPI_Irecv return immediately with an MPI_Request; the transfer proceeds while the program does other
work, and MPI_Wait, MPI_Waitall or MPI_Test completes it. Until completion the buffer belongs to MPI: it must not be
modified (send) or read (receive). The cost of the freedom is that the Fortran compiler does not know any of this.
program nonblocking
use mpi_f08
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 4
integer :: rank, nprocs, up, down
real(real64), asynchronous :: send_up(n), recv_down(n)
type(MPI_Request) :: reqs(2)
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
up = mod(rank + 1, nprocs)
down = mod(rank - 1 + nprocs, nprocs)
send_up = real(rank, real64)
recv_down = -1.0_real64
call MPI_Irecv(recv_down, n, MPI_DOUBLE_PRECISION, down, 0, MPI_COMM_WORLD, reqs(1))
call MPI_Isend(send_up, n, MPI_DOUBLE_PRECISION, up, 0, MPI_COMM_WORLD, reqs(2))
! ... independent work could overlap with the transfer here ...
call MPI_Waitall(2, reqs, MPI_STATUSES_IGNORE)
print '(a,i0,a,f4.0)', 'rank ', rank, ' received ', recv_down(1)
call MPI_Finalize()
end program nonblocking
rank 0 received 3.
rank 1 received 0.
rank 2 received 1.
rank 3 received 2.
The buffers are declared with the asynchronous attribute (J3/23-007r1 8.5.4). It is the same attribute that marks
variables involved in asynchronous input/output (see
Derived-Type and Asynchronous I/O; the book
chapter on it is MRCB section 8.11), and it tells the compiler that the variable "might be associated with a pending
input/output storage sequence" and may change at any time, which disables code motion across calls. The problem it
solves is the one in the next two programs. They are identical except for the attribute, and both send three values to
the right neighbour and receive three from the left, using persistent requests (MPI_Recv_init, MPI_Send_init, then
MPI_Startall and MPI_Waitall on every step). The counts are of kind MPI_COUNT_KIND, the argument kind that selects the large-count
variants of the routines through the generic names (MPI 5.0 section 19.2).
program persistent
use mpi_f08
use, intrinsic :: iso_fortran_env, only: real64, int64
implicit none
integer, parameter :: n = 3
integer(MPI_COUNT_KIND), parameter :: big = 3_int64
integer :: rank, nprocs, right, left, step
real(real64) :: out(n), in(n)
type(MPI_Request) :: req(2)
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
right = mod(rank + 1, nprocs)
left = mod(rank - 1 + nprocs, nprocs)
! a count of kind MPI_COUNT_KIND selects the large-count variant (MPI_Send_init_c, ...)
call MPI_Recv_init(in, big, MPI_DOUBLE_PRECISION, left, 0, MPI_COMM_WORLD, req(1))
call MPI_Send_init(out, big, MPI_DOUBLE_PRECISION, right, 0, MPI_COMM_WORLD, req(2))
do step = 1, 3
out = real(rank*10 + step, real64)
call MPI_Startall(2, req)
call MPI_Waitall(2, req, MPI_STATUSES_IGNORE)
if (rank == 0) print '(a,i0,a,f5.1)', 'step ', step, ': rank 0 received ', in(1)
end do
call MPI_Request_free(req(1))
call MPI_Request_free(req(2))
call MPI_Finalize()
end program persistent
Without optimization (-O0) this prints what one expects. Compiled the way mpifort does it by default (-O2) and run with mpirun -n 4:
step 1: rank 0 received 0.0
step 2: rank 0 received 0.0
step 3: rank 0 received 0.0
Source: MPI 5.0 report: Chapter 3.7 Nonblocking Communication, 19.1.8 Additional Support for Fortran Register-Memory-Synchronization and 19.1.17 Problems with Code Movement and Register Optimization, J3/23-007r1: 8.5.4 ASYNCHRONOUS attribute
The values never arrive in the program’s view of in: the compiler saw that in is not modified between its
initialization and the print, because the call that completes the receive has no visible connection to it, and kept it
in a register or folded the value. The program is wrong, not the library, and no compiler warning exists. Adding the
attribute to both buffers is the whole fix:
real(real64), asynchronous :: out(n), in(n)
With that single change (file 14b_persistent_async.f90) the same build prints:
step 1: rank 0 received 31.0
step 2: rank 0 received 32.0
step 3: rank 0 received 33.0
The rules, from MPI 5.0 section 19.1.17 and J3/23-007r1 8.5.4:
-
Declare every buffer that is the argument of a non-blocking or persistent call
asynchronousin every scoping unit that touches it, including a dummy argument that receives the buffer: a scoping unit that does not declare the attribute is free to optimize as if the buffer could not change. -
MPI_ASYNC_PROTECTS_NONBLOCKINGandMPI_SUBARRAYS_SUPPORTEDare logical constants ofmpi_f08that tell whether the library has made the interfaceasynchronousand whether it supports non-contiguous array sections as non-blocking buffers. Both are true in the MPICH used here (example 13). If they are false,asynchronousalone is not enough. -
The older fallback is
call MPI_F_SYNC_REG(buf)after the completing call, and again before the starting call when the buffer is modified first, to force the compiler to assume the buffer is read and written there. It is also needed formpiandmpif.hcode. Thevolatileattribute and thetargetattribute are discussed as further options, and the standard callsvolatilepoorly performing (section 19.1.17). -
Do not pass an expression or a section that needs a temporary, such as
maxval(part)or a non-contiguous array on a library withoutMPI_SUBARRAYS_SUPPORTED, to a non-blocking call: the temporary is freed when the call returns, long before the transfer completes.
Array sections
An array section such as a(2, :) is not contiguous in column-major order (see
Arrays Fundamentals and
Array Arguments: Assumed Shape, Size and Rank).
With mpi_f08 and a library that sets MPI_SUBARRAYS_SUPPORTED, it can be passed directly, and the compiler passes a
descriptor so that the library gathers the elements itself, with no copy that could be freed too early.
program subarrays
use mpi_f08
implicit none
integer, parameter :: nx = 4, ny = 3
integer :: rank, i
integer, asynchronous :: a(nx, ny)
integer :: b(ny)
type(MPI_Request) :: req
type(MPI_Status) :: st
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
if (rank == 0) then
print '(a,l1)', 'MPI_SUBARRAYS_SUPPORTED = ', MPI_SUBARRAYS_SUPPORTED
print '(a,l1)', 'MPI_ASYNC_PROTECTS_NONBLOCKING = ', MPI_ASYNC_PROTECTS_NONBLOCKING
end if
if (rank == 0) then
a = reshape([(i, i=1, nx*ny)], [nx, ny])
! a(2, :) is not contiguous: stride nx elements
call MPI_Isend(a(2, :), ny, MPI_INTEGER, 1, 0, MPI_COMM_WORLD, req)
call MPI_Wait(req, MPI_STATUS_IGNORE)
else if (rank == 1) then
call MPI_Recv(b, ny, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, st)
print '(a,*(i3))', 'rank 1 received row 2:', b
end if
call MPI_Finalize()
end program subarrays
MPI_SUBARRAYS_SUPPORTED = T
MPI_ASYNC_PROTECTS_NONBLOCKING = T
rank 1 received row 2: 2 6 10
With the mpi module or mpif.h a section is passed by sequence association, which makes the compiler copy it into a
contiguous temporary; that is harmless for a blocking call and a bug for a non-blocking one, because the temporary may be
released before the transfer completes (MPI 5.0 section 19.1.12). The traditional remedy is a derived datatype, shown next,
and it remains the better choice for strided data on the performance-sensitive path.
Describing data: kinds and derived datatypes
An MPI datatype tells the library what the bytes of a buffer are. For intrinsic types the names are
MPI_INTEGER, MPI_REAL, MPI_DOUBLE_PRECISION, MPI_COMPLEX, MPI_LOGICAL and MPI_CHARACTER, for the default kinds,
plus optional size-specific names (MPI_INTEGER8 and MPI_REAL8, for example). The Fortran standard does not say that real64 is
the same as the default double precision, so code that uses kinds from
Intrinsic Modules and iso_fortran_env should
ask MPI for the matching datatype with MPI_Type_create_f90_real, MPI_Type_create_f90_integer or
MPI_Type_create_f90_complex, passing the result of precision/range or selected_int_kind (MPI 5.0 section 19.1.9).
The heatflow kinds are used below.
program kinds
use mpi_f08
use heatflow_kinds, only: dp, sp, ip, lp
implicit none
type(MPI_Datatype) :: tdp, tsp
integer :: rank, sz
real(dp) :: x, total
integer(lp) :: big, bigsum
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
! ask for the MPI datatype matching the Fortran kind instead of assuming double precision
call MPI_Type_create_f90_real(precision(1.0_dp), range(1.0_dp), tdp)
call MPI_Type_create_f90_real(precision(1.0_sp), range(1.0_sp), tsp)
call MPI_Type_size(tdp, sz)
if (rank == 0) print '(a,i0,a,l1)', 'dp has ', sz, ' bytes, equals MPI_DOUBLE_PRECISION: ', tdp == MPI_DOUBLE_PRECISION
call MPI_Type_size(tsp, sz)
if (rank == 0) print '(a,i0,a,l1)', 'sp has ', sz, ' bytes, equals MPI_REAL: ', tsp == MPI_REAL
x = rank + 1.0_dp
call MPI_Allreduce(x, total, 1, tdp, MPI_SUM, MPI_COMM_WORLD)
big = huge(0_ip) + int(rank, lp) ! needs a 64-bit integer type
call MPI_Allreduce(big, bigsum, 1, MPI_INTEGER8, MPI_SUM, MPI_COMM_WORLD)
if (rank == 0) print '(a,f5.1,a,i0)', 'sum=', total, ' bigsum=', bigsum
call MPI_Finalize()
end program kinds
dp has 8 bytes, equals MPI_DOUBLE_PRECISION: F
sp has 4 bytes, equals MPI_REAL: F
sum= 10.0 bigsum=8589934594
The datatypes are the right size, and a reduction with them gives the expected 10.0 and 4 * 2147483647 + 6 for the
64-bit sum (MPI_INTEGER8 stands for integer(lp) here, which is correct only because lp is int64). The two F
lines are the point: the datatype that MPI_Type_create_f90_real returns is a distinct handle and so compares unequal to
MPI_DOUBLE_PRECISION, even though it describes the same layout. Compare sizes with MPI_Type_size, not handles.
Mistakes in the datatype are not diagnosed. This program sums integers while describing them as MPI_REAL, and the
result is not an integer sum:
program wrong_datatype
use mpi_f08
implicit none
integer :: rank, mine, total
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
mine = 10000000 + rank
! integer buffers, but a REAL datatype: type(*) buffers are not type checked
call MPI_Allreduce(mine, total, 1, MPI_REAL, MPI_SUM, MPI_COMM_WORLD)
if (rank == 0) print '(a,i0,a)', 'sum computed as ', total, ' (expected 40000006)'
call MPI_Finalize()
end program wrong_datatype
Real output with mpirun -n 4, which compiles without a warning:
sum computed as 26777217 (expected 40000006)
(With small integers the same mistake can look correct, because their bit patterns are tiny subnormal reals that add like integers, which is why this example uses 10000000 plus the rank.)
For data that is not contiguous, build a derived datatype and commit it. MPI_Type_vector(count, blocklength, stride,
oldtype, newtype) describes count blocks of blocklength elements, with the start of consecutive blocks stride
elements apart; MPI_Type_create_subarray describes a rectangular piece of a larger array, and
MPI_Type_create_struct a derived type (which should be sequence or bind(c) so its layout is defined, see
Derived Types). The program below sends one grid row of a
column-major array, whose elements are nx apart, and stores it into another row of the receiver’s array.
program column_type
use mpi_f08
use heatflow_kinds, only: dp
implicit none
integer, parameter :: nx = 5, ny = 4
integer :: rank, i, j
real(dp) :: a(nx, ny)
type(MPI_Datatype) :: row_t
type(MPI_Status) :: st
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
a = 0.0_dp
! one grid row a(i, :) is ny elements, nx apart in memory
call MPI_Type_vector(ny, 1, nx, MPI_DOUBLE_PRECISION, row_t)
call MPI_Type_commit(row_t)
if (rank == 0) then
a = reshape([(real(i, dp), i=1, nx*ny)], [nx, ny])
call MPI_Send(a(2, 1), 1, row_t, 1, 0, MPI_COMM_WORLD) ! row 2, from its first element
else if (rank == 1) then
call MPI_Recv(a(4, 1), 1, row_t, 0, 0, MPI_COMM_WORLD, st) ! stored into row 4
do i = 1, nx
print '(a,*(f5.0))', 'a(i,:) =', (a(i, j), j=1, ny)
end do
end if
call MPI_Type_free(row_t)
call MPI_Finalize()
end program column_type
Real output on rank 1 (mpirun -n 2):
a(i,:) = 0. 0. 0. 0.
a(i,:) = 0. 0. 0. 0.
a(i,:) = 0. 0. 0. 0.
a(i,:) = 2. 7. 12. 17.
a(i,:) = 0. 0. 0. 0.
The starting address passed to MPI_Send and MPI_Recv is the first element of the row (a(2, 1), a(4, 1)), and the
count is one instance of the type. A datatype must be committed with MPI_Type_commit before use and released with
MPI_Type_free afterwards. A derived type with allocatable or pointer components cannot be described this way, since
its components are not at fixed offsets: send the components separately.
Process topologies
The decomposition arithmetic (who is my neighbour?) can be left to MPI. MPI_Dims_create chooses a balanced grid of ranks
for a given count, MPI_Cart_create builds a communicator with that structure (and may reorder the ranks to match the
network when reorder is true), MPI_Cart_coords converts a rank to coordinates, and MPI_Cart_shift returns the source
and destination of a shift along a dimension, with MPI_PROC_NULL at a non-periodic edge (MPI 5.0 section 8.5).
program cartesian
use mpi_f08
implicit none
integer :: nprocs, rank, dims(2), coords(2), lo, hi
logical :: periods(2)
type(MPI_Comm) :: cart
call MPI_Init()
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
dims = 0
call MPI_Dims_create(nprocs, 2, dims)
periods = [.false., .false.]
call MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, .true., cart)
call MPI_Comm_rank(cart, rank)
call MPI_Cart_coords(cart, rank, 2, coords)
call MPI_Cart_shift(cart, 1, 1, lo, hi) ! neighbours along dimension 2 (y)
print '(a,i0,a,i0,a,i0,a,i0,a,i0)', 'rank ', rank, ' at (', coords(1), ',', coords(2), ') of ', dims(1), ' x ', dims(2)
print '(a,i0,a,i0,a,i0,a,l1)', ' y-neighbours of rank ', rank, ': lower ', lo, ', upper ', hi, &
', lower is MPI_PROC_NULL: ', lo == MPI_PROC_NULL
call MPI_Comm_free(cart)
call MPI_Finalize()
end program cartesian
Real output with mpirun -n 4:
rank 0 at (0,0) of 2 x 2
y-neighbours of rank 0: lower -1, upper 1, lower is MPI_PROC_NULL: T
rank 1 at (0,1) of 2 x 2
y-neighbours of rank 1: lower 0, upper -1, lower is MPI_PROC_NULL: F
rank 2 at (1,0) of 2 x 2
y-neighbours of rank 2: lower -1, upper 3, lower is MPI_PROC_NULL: T
rank 3 at (1,1) of 2 x 2
y-neighbours of rank 3: lower 2, upper -1, lower is MPI_PROC_NULL: F
Coordinates and dimension numbers are counted from zero, unlike Fortran array indices, and the value of MPI_PROC_NULL
(-1 here) is implementation defined, so always compare with the constant. The .true. argument of MPI_Cart_create is
reorder.
Error handling
By default an MPI error is fatal: the default handler of a communicator is MPI_ERRORS_ARE_FATAL and aborts the job
(MPI_ERRORS_RETURN is the other predefined one, and MPI_Comm_create_errhandler builds your own). Programs that want to react set MPI_ERRORS_RETURN
and pass the optional ierror argument, then compare with MPI_SUCCESS. The error code is implementation specific;
MPI_Error_class maps it to a portable class such as MPI_ERR_RANK, MPI_ERR_TRUNCATE, MPI_ERR_COUNT or
MPI_ERR_TYPE, and MPI_Error_string produces text.
program errors
use mpi_f08
implicit none
integer :: rank, ierr, eclass
integer :: buf(1)
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
! by default errors abort the job; ask for error codes instead
call MPI_Comm_set_errhandler(MPI_COMM_WORLD, MPI_ERRORS_RETURN)
buf = rank
call MPI_Send(buf, 1, MPI_INTEGER, 99, 0, MPI_COMM_WORLD, ierr) ! no rank 99
if (ierr /= MPI_SUCCESS) then
call MPI_Error_class(ierr, eclass)
print '(a,i0,a,l1)', 'rank ', rank, ' MPI_Send failed, class is MPI_ERR_RANK: ', eclass == MPI_ERR_RANK
end if
call MPI_Finalize()
end program errors
Real output with mpirun -n 2:
rank 0 MPI_Send failed, class is MPI_ERR_RANK: T
rank 1 MPI_Send failed, class is MPI_ERR_RANK: T
Printing the string from MPI_Error_string gave a multi-line stack with addresses on this MPICH, which is why the example
prints the class. After an error with MPI_ERRORS_RETURN the standard does not promise that the communicator is still
usable; for most programs the right reaction is to report and MPI_Abort. Errors in Fortran itself (an out-of-bounds
access, an unallocated array) are not MPI errors and are handled as in
Error Handling and Program Termination and
Testing, Debugging and Profiling.
Domain decomposition of heatflow
The heatflow grid has nx points in x and ny rows in y. A one-dimensional decomposition gives each rank ny / nprocs rows
stored as u(nx, 0:nloc + 1): rows 1:nloc are owned, and rows 0 and nloc + 1 are the halo that mirrors the
neighbours' edge rows. Because the second index is the slow one in column-major order, a whole row u(:, j) is
contiguous, so no derived datatype is needed. Every step the halo is refreshed, the interior is updated and the boundary
rows of the first and last rank are held at zero.
The version in the parallel/ directory of the heatflow project posts one MPI_Sendrecv per direction and is the
simplest correct one; the listing below is the non-blocking variant that overlaps communication with computation. Both
give the same result.
program mpi_halo
use mpi_f08
use heatflow_kinds, only: dp
implicit none
integer, parameter :: nx = 41, ny = 40, nsteps = 200
real(dp), parameter :: alpha = 1.0_dp, dt = 5.0e-5_dp
real(dp), parameter :: dx = 1.0_dp/(nx - 1), dy = 1.0_dp/(ny - 1)
integer :: rank, nprocs, nloc, up, down, n
real(dp), allocatable, asynchronous :: u(:, :)
real(dp), allocatable :: unew(:, :)
real(dp) :: local_max, global_max
type(MPI_Request) :: req(4)
call MPI_Init()
call MPI_Comm_rank(MPI_COMM_WORLD, rank)
call MPI_Comm_size(MPI_COMM_WORLD, nprocs)
nloc = ny/nprocs
allocate (u(nx, 0:nloc + 1), unew(nx, 0:nloc + 1))
u = 0.0_dp
unew = 0.0_dp
if (rank == 0) u((nx + 1)/2, nloc/2) = 100.0_dp
down = merge(MPI_PROC_NULL, rank - 1, rank == 0)
up = merge(MPI_PROC_NULL, rank + 1, rank == nprocs - 1)
do n = 1, nsteps
! 1. post the halo exchange
call MPI_Irecv(u(:, 0), nx, MPI_DOUBLE_PRECISION, down, 0, MPI_COMM_WORLD, req(1))
call MPI_Irecv(u(:, nloc + 1), nx, MPI_DOUBLE_PRECISION, up, 1, MPI_COMM_WORLD, req(2))
call MPI_Isend(u(:, nloc), nx, MPI_DOUBLE_PRECISION, up, 0, MPI_COMM_WORLD, req(3))
call MPI_Isend(u(:, 1), nx, MPI_DOUBLE_PRECISION, down, 1, MPI_COMM_WORLD, req(4))
! 2. update the interior rows, which need no halo, while the messages travel
call sweep(2, nloc - 1)
! 3. finish the exchange, then update the two rows that need the halo
call MPI_Waitall(4, req, MPI_STATUSES_IGNORE)
if (nloc >= 2) call sweep(1, 1)
call sweep(nloc, nloc)
u(2:nx - 1, 1:nloc) = unew(2:nx - 1, 1:nloc)
if (rank == 0) u(:, 1) = 0.0_dp
if (rank == nprocs - 1) u(:, nloc) = 0.0_dp
end do
local_max = maxval(u(:, 1:nloc))
call MPI_Reduce(local_max, global_max, 1, MPI_DOUBLE_PRECISION, MPI_MAX, 0, MPI_COMM_WORLD)
if (rank == 0) print '(a,i0,a,f8.3)', 'ranks=', nprocs, ' max=', global_max
call MPI_Finalize()
contains
subroutine sweep(jlo, jhi)
integer, intent(in) :: jlo, jhi
integer :: i, j
do j = jlo, jhi
do i = 2, nx - 1
unew(i, j) = u(i, j) + alpha*dt*( &
(u(i - 1, j) - 2.0_dp*u(i, j) + u(i + 1, j))/dx**2 + &
(u(i, j - 1) - 2.0_dp*u(i, j) + u(i, j + 1))/dy**2)
end do
end do
end subroutine sweep
end program mpi_halo
Built with mpifort heatflow_kinds.f90 09_halo_nonblocking.f90 and run with mpirun -n 4:
ranks=4 max= 0.380
Source: MPI 5.0 report: Chapter 3 Point-to-Point Communication and Chapter 6 Collective Communication
This is the same value the MPI_Sendrecv version in the project, the coarray version and the OpenMP version
(Coarrays, Images and Synchronization and
OpenMP and OpenACC) report for four ranks or images. The hot
spot is placed at row nloc / 2 of rank 0, so the number depends on how many rows a rank owns: the same program printed
0.512 with one rank and 0.509 with two when it was tried, which is a property of the test setup and not an error. The
important details are:
-
uisallocatable, asynchronous, and the four requests are completed by oneMPI_Waitallbefore the edge rows (which read the halo) are updated. The interior update touches only rows that no outstanding message reads or writes. -
u(:, 0)and the other row sections are contiguous, so passing them to non-blocking calls does not make a copy, even withoutMPI_SUBARRAYS_SUPPORTED. -
downandupareMPI_PROC_NULLat the ends, so the same four calls run on every rank. -
Each rank reduces its own
maxvalandMPI_Reducecombines them on rank 0; only rank 0 prints. -
The internal procedure
sweepuses host association foruandunew. That is fine here, but anasynchronoushost variable used in an internal procedure keeps the attribute, which is another reason internal procedures are convenient for this pattern.
Building and running
An MPI installation provides compiler wrappers that add the include and library paths and the MPI library to a normal
Fortran compiler: mpifort (also installed as mpif90) for Fortran, plus mpicc and mpicxx. mpifort -show prints
the real command line. On the MPICH used here it was:
gfortran -O2 -fstack-protector-strong -fstack-clash-protection -mbranch-protection=standard -fallow-invalid-boz -fallow-argument-mismatch -Wl,-z,relro -Wl,-z,now -I/usr/lib/aarch64-linux-gnu/mpich/include -I/usr/lib/aarch64-linux-gnu/mpich/include -L/usr/lib/aarch64-linux-gnu/mpich/lib -lmpichfort -lmpich
The wrapper is a distribution’s choice (this one adds -O2, which is why the optimization example above misbehaves
without asynchronous, and -fallow-argument-mismatch, which hides errors for the mpi module and mpif.h, though not
for mpi_f08). Programs are started with mpirun -n 4 ./a.out or mpiexec -n 4 ./a.out; both are the launcher of the
installed implementation, and the options beyond -n differ between MPICH and Open MPI. The same
-std=f2023 -Wall -Wextra -pedantic -fcheck=all options used in the rest of the section can be passed to mpifort, and
every example compiled with them (the deliberately wrong ones excepted). A run with 16 ranks failed in this container
during MPI_Init (a UCX out-of-memory error), so the examples use at most four; use no more ranks than the machine has
cores unless the launcher documentation says oversubscription is supported.
Two build-system integrations were tried and worked:
CMake has a FindMPI module whose Fortran component reports whether mpi_f08 is usable:
cmake_minimum_required(VERSION 3.20)
project(mpi_demo LANGUAGES Fortran)
find_package(MPI REQUIRED COMPONENTS Fortran)
message(STATUS "mpi_f08 module found: ${MPI_Fortran_HAVE_F08_MODULE}")
add_executable(hello main.f90)
target_link_libraries(hello PRIVATE MPI::MPI_Fortran)
-- Found MPI_Fortran: /usr/lib/aarch64-linux-gnu/mpich/lib/libmpichfort.so (found version "4.1")
-- Found MPI: TRUE (found version "4.1") found components: Fortran
-- mpi_f08 module found: TRUE
Source: CMake: FindMPI
and mpirun -n 2 build/hello printed the two ranks. The version reported is that of the MPI standard the library
implements, 4.1.
fpm has an mpi metapackage (0.13.0 was used):
name = "mpi_demo"
[dependencies]
mpi = "*"
[fortran]
implicit-typing = true
implicit-external = true
The two [fortran] entries are not optional: without them fpm refused to build with "metapackage does require
implicit-typing" (and the same for implicit-external), because the metapackage also has to support mpif.h. They loosen
the checks for the whole package, so a package that wants strict implicit none (type, external) enforcement in its own
sources must enforce it with its own compiler flags. fpm run --runner "mpirun -n 2" started two ranks, and plain fpm run ran one.
See Building with Make, CMake and Meson and
Fortran Package Manager (fpm) for the surrounding
build structure, and Compilers and Compiler Options
for the options mentioned above.
A last practical constraint is that the mpi_f08 module files are produced by one specific Fortran compiler. Open MPI and
MPICH each ship modules built for the compiler they were configured with, and using a different compiler (or even a
different major version of the same one) typically gives an error when the module is read. For that reason, and for the ABI, build
the whole program, MPI library and mpifort included, with a matching toolchain, and treat the MPI 5.0 ABI (chapter 20) as a possible
future way around it.
MPI and coarrays compared
Both are SPMD models in which every process runs the same program, but the abstraction differs:
| Aspect | MPI with mpi_f08 |
Coarrays (Fortran 2023) |
|---|---|---|
Where it lives |
a library and launcher with a Fortran binding, not in the language |
part of the language, enabled by the compiler ( |
Data movement |
explicit messages: matching send and receive, or collectives |
a coarray reference |
Synchronization |
implied by message matching and collectives |
|
Process model |
ranks, communicators, topologies, dynamic process creation, sessions |
images and teams |
Typical strength |
any system size, any hardware, mature tools, I/O library, hybrid with OpenMP |
short code for regular data, a halo exchange is one assignment |
Compiler diagnostics |
handles are typed, buffers are not type checked against datatypes |
ordinary Fortran type checking of the coarray reference |
Availability here |
MPICH with |
gfortran with |
The same strip decomposition is a single statement per halo with coarrays and about a dozen with MPI, and the result of
the heatflow test was identical (max = 0.380 with four ranks or images). MPI remains the choice when the program must
run on a cluster, interoperate with C or Python MPI code, or use the MPI parallel I/O and one-sided interfaces. Coarray
runtimes are often built on MPI, and OpenCoarrays used the MPICH installed in this container. Combine MPI with OpenMP for
the shared-memory level inside a node (the thread-support level is requested with MPI_Init_thread).
Common mistakes
-
Using
mpif.h. It is deprecated and has no interfaces; usempi_f08. -
Missing
asynchronouson non-blocking buffers. Works at-O0, fails at-O2with stale values and no diagnostic. -
Deadlock from ordering sends and receives. Use
MPI_Sendrecv, non-blocking calls, or order the pairs consistently. -
Wrong datatype for the buffer. Not diagnosed; use
MPI_Type_create_f90_realfor non-default kinds. -
Modifying a buffer before the request completes.
-
Passing an expression or a temporary to a non-blocking routine.
-
Calling a collective on only some ranks of the communicator. The job hangs.
-
Printing from all ranks and expecting an order. There is none.
-
A message longer than the receive count. Truncation error; use
MPI_ProbeandMPI_Get_count. -
Ranks that exit early. Use
MPI_Abort, noterror stop, to stop the whole job. -
Comparing handles that came from different creation routines. Compare properties, not handles.
-
Assuming the library implements the newest published MPI. Check
MPI_Get_version; here it was 4.1 while the specification is at 5.0.
See also Performance and Memory Layout for why a contiguous halo row matters, and Common Gotchas and Pitfalls.