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

mpi_f08

use mpi_f08

Handles are bind(c) derived types (type(MPI_Comm), type(MPI_Request) and so on), so passing the wrong kind of handle is a compile-time error. Buffers are type(*), dimension(..) (assumed-type, assumed-rank), so any array can be passed and a non-contiguous section is handled correctly. The ierror argument is optional. Large-count (_c) variants are available through the same generic names. It needs Fortran 2008 with TS 29113, which is part of Fortran 2018 and later (MPI 5.0 section 2.6.2).

mpi

use mpi

Handles are default integer. Few procedures have explicit interfaces that check argument types. ierror is a required last argument. MPI 5.0 calls these older bindings inconsistent with the Fortran standard in several respects (section 19.1.1).

mpif.h

include 'mpif.h'

Handles are default integer, there are no interfaces at all, and the include file is deprecated in MPI 5.0.

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_Init must be called once, before almost every other MPI routine (the exceptions include MPI_Initialized, MPI_Finalized and MPI_Get_version), and MPI_Finalize once, after all communication has completed. After MPI_Finalize no MPI call is allowed.

  • MPI_Finalize is collective: a rank that stops early (an error stop, a crash, an exit in one branch) can leave the others blocked. MPI_Abort(comm, code) terminates all ranks; error stop in 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 to MPI_Init for 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 over comm; ranks with the same color land in the same new communicator, ordered by key. A rank passing MPI_UNDEFINED as colour gets MPI_COMM_NULL.

  • MPI_Comm_free sets the handle to the null value, so a freed handle compares equal to MPI_COMM_NULL, as the second line of each pair shows. Request handles (type(MPI_Request)) behave the same way after MPI_Wait, which is what lets MPI_Waitall skip finished entries.

  • Predefined handles such as MPI_COMM_WORLD, MPI_DOUBLE_PRECISION and MPI_SUM are 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_VAL for code that has to talk to C, where the conversion functions MPI_Comm_f2c and MPI_Comm_c2f translate between the C and Fortran handle values; see Interoperability with C.

  • Handles of the mpi module and mpif.h are plain integers; code that stores them in integer variables and moves to mpi_f08 must 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

MPI_Barrier

no rank returns until all have called it (rarely needed for correctness)

MPI_Bcast

one root’s buffer is copied to every rank

MPI_Scatter, MPI_Gather

split the root’s array into equal pieces, one per rank, and the reverse; MPI_Scatterv/MPI_Gatherv take per-rank counts and displacements

MPI_Allgather, MPI_Alltoall

every rank receives all pieces, or a personalised piece from every rank

MPI_Reduce, MPI_Allreduce

combine one value per rank with MPI_SUM, MPI_MAX, MPI_MIN, MPI_PROD, MPI_LAND, …​ and deliver the result to the root or to everyone

MPI_Scan, MPI_Exscan

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_Scatter and the receive buffer of MPI_Gather are 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 unallocated allocatable array to a type(*) dummy is an error.

  • maxval(part) as the send buffer of MPI_Allreduce is 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_PLACE as 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 same asynchronous rules 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

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 asynchronous in 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_NONBLOCKING and MPI_SUBARRAYS_SUPPORTED are logical constants of mpi_f08 that tell whether the library has made the interface asynchronous and 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, asynchronous alone 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 for mpi and mpif.h code. The volatile attribute and the target attribute are discussed as further options, and the standard calls volatile poorly 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 without MPI_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.

sequenceDiagram participant R0 as rank 0 participant R1 as rank 1 participant R2 as rank 2 loop every time step R0->>R1: row nloc (Isend) R1->>R0: row 1 (Isend) R1->>R2: row nloc (Isend) R2->>R1: row 1 (Isend) Note over R0,R2: update interior rows while messages travel Note over R0,R2: Waitall, then update the two edge rows end R0->>R0: Reduce(MAX) to rank 0

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

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:

  • u is allocatable, asynchronous, and the four requests are completed by one MPI_Waitall before 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 without MPI_SUBARRAYS_SUPPORTED.

  • down and up are MPI_PROC_NULL at the ends, so the same four calls run on every rank.

  • Each rank reduces its own maxval and MPI_Reduce combines them on rank 0; only rank 0 prints.

  • The internal procedure sweep uses host association for u and unew. That is fine here, but an asynchronous host 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 (-fcoarray= options and a runtime library)

Data movement

explicit messages: matching send and receive, or collectives

a coarray reference a(i)[k] reads or writes another image’s memory after synchronization

Synchronization

implied by message matching and collectives

sync all, sync images, events, locks and teams

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 mpifort worked; Flang 23 was not tried with it

gfortran with -fcoarray=lib worked; Flang 23 rejects coarrays

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; use mpi_f08.

  • Missing asynchronous on non-blocking buffers. Works at -O0, fails at -O2 with 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_real for 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_Probe and MPI_Get_count.

  • Ranks that exit early. Use MPI_Abort, not error 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.