Appendix G: Parallel Programming Reference
A side-by-side quick reference to Fortran's four parallel programming models — coarrays, OpenMP, MPI, and GPU offload (OpenACC and CUDA Fortran). It is the companion card to Part VIII: where Chapters 31–35 teach each model in depth, this appendix collects the syntax, the argument orders, and the compile-and-run commands in one place so you can look them up without re-reading a chapter. For the underlying toolchain (installing OpenMPI, OpenCoarrays, and the compiler flags themselves) see Appendix C.
Two things are worth saying once, up front. First, all four models parallelize the same computation
in this book — the five-point heat-equation stencil frozen in
Chapter 24 and carried behind
the stable step interface. Coarrays (Chapter 32),
OpenMP (Chapter 33), MPI
(Chapter 34), and a GPU
(Chapter 35) are four deliveries of
one physics; the answer is identical, only the machine and the notation change. Section G.6 tabulates that
correspondence directly. Second, an honesty note carried over from Chapter 35: the GPU examples were written
to be exactly correct but, lacking a GPU and nvfortran, were not machine-checked, and a few
coarray features (notably teams) are only partially supported by current gfortran — those caveats are
flagged where they occur.
G.1 The four models at a glance
| Model | Memory model | Scales to | Fortran mechanism | Standard / library | Chapter |
|---|---|---|---|---|---|
| Coarrays | partitioned global (PGAS) | one node or a cluster | in the language ([*], sync, co_*) |
Fortran 2008 / 2018 | 32 |
| OpenMP | shared memory | cores of one node | !$omp directives + omp_lib |
OpenMP standard | 33 |
| MPI | distributed, private per process | a whole cluster | use mpi library calls |
MPI standard (a library) | 34 |
| GPU | separate device memory | one (or a few) GPUs | !$acc directives / CUDA Fortran |
OpenACC / NVIDIA CUDA | 35 |
The one-line build-and-run for each (details in the per-model sections below):
$ gfortran -fcoarray=single -std=f2018 -Wall prog.f90 -o prog && ./prog # coarrays, 1 image (test)
$ caf -std=f2018 prog.f90 -o prog && cafrun -n 4 ./prog # coarrays, 4 images
$ gfortran -std=f2018 -fopenmp -Wall prog.f90 -o prog && OMP_NUM_THREADS=4 ./prog # OpenMP
$ mpif90 -std=f2018 -Wall -O2 prog.f90 -o prog && mpirun -np 4 ./prog # MPI, 4 processes
$ nvfortran -acc -Minfo=accel prog.f90 -o prog && ./prog # OpenACC on the GPU
G.2 Coarrays (Fortran 2008/2018)
Coarrays add parallelism to the language itself: declare a variable with a codimension [*] and it
exists as one independent copy per image, any of which can reach any other's copy with a coindexed
reference a[q].
Declaration and image queries
| Construct | Meaning |
|---|---|
real(dp) :: a[*] |
scalar coarray — one copy per image |
real(dp) :: v(n)[*] |
array coarray — an n-element array on every image |
real(dp), allocatable :: u(:)[:] |
allocatable coarray; allocate(u(n)[*]) sets local shape + coshape |
real(dp) :: g(nx,ny)[np,*] |
corank-2 coarray — images laid out in an np-by-(N/np) grid |
this_image() |
the calling image's number, 1 .. num_images() |
num_images() |
total number of images N |
this_image(u) |
this image's cosubscripts for coarray u (corank > 1) |
image_index(u, [i,j]) |
the image number for cosubscripts [i,j] (inverse of this_image(u)) |
The () bounds are the local shape (identical on every image); the [] bounds are the coshape
(the arrangement of images). The trailing * means "as many images as the program was launched with."
Remote (coindexed) access
x = a[q] ! GET: read image q's copy of a
a[q] = x ! PUT: write into image q's copy
col = u(:,1)[q] ! remote array section (one contiguous column from image q)
An unbracketed a is always this image's local copy (fast); a bracketed a[q] may be a network
transfer — so communicate rarely and in bulk (whole columns, not element-by-element).
Synchronization
| Statement | Effect |
|---|---|
sync all |
global barrier — every image waits for every image (the default) |
sync images(q) |
pairwise sync with image q |
sync images(*) |
sync with all other images |
critical … end critical |
mutual exclusion — one image at a time in the block |
lock(lck) … unlock(lck) |
lock-based exclusion; lck is type(lock_type) from iso_fortran_env, itself a coarray |
The correctness rule: a remote read must be ordered against remote writes by a synchronization —
the pattern write → sync all → read is race-free; without the barrier the program is undefined.
use, intrinsic :: iso_fortran_env, only: lock_type
type(lock_type) :: lck[*]
lock(lck)
! ... exclusive access to a shared resource ...
unlock(lck)
Collective subroutines (Fortran 2018)
Every image must call a collective, with matching arguments; the result is written back to the argument on
every image (or, with result_image, on just one).
| Call | Effect |
|---|---|
call co_sum(x [, result_image]) |
x becomes the sum across all images |
call co_max(x [, result_image]) |
x becomes the maximum across all images |
call co_min(x [, result_image]) |
x becomes the minimum across all images |
call co_broadcast(x, source_image) |
copy source_image's x to every image |
call co_reduce(x, operation [, result_image]) |
reduce with a user-supplied pure 2-argument function |
co_sum(x, result_image=1) leaves the sum only on image 1 (the analogue of MPI's mpi_reduce);
co_sum(x) leaves it on all (the analogue of mpi_allreduce).
Teams (brief)
Teams partition the images into named subsets that each behave, inside a change team block, as a
self-contained image set (this_image()/num_images() become team-relative).
use, intrinsic :: iso_fortran_env, only: team_type
type(team_type) :: t
form team(team_number, t) ! each image chooses a team number
change team(t)
! this_image() / num_images() are now RELATIVE to this image's team
end team
⚠️ gfortran caveat — teams are the least-supported corner of coarrays. Teams (
form team/change team) are fully standard (Fortran 2018) but incomplete or absent in current gfortran + OpenCoarrays — a program using them may fail to compile or link. Treat teams as a model to recognize, not one to depend on with gfortran today; Intel'sifx/iforthas led on 2018 coarray features. The collectives (co_sumand friends) are well supported with OpenCoarrays. Verify on your exact toolchain.
Compile and run
$ gfortran -fcoarray=single -std=f2018 -Wall prog.f90 -o prog && ./prog # 1 image, for testing logic
$ caf -std=f2018 -Wall prog.f90 -o prog # OpenCoarrays wrapper
$ cafrun -n 4 ./prog # run on 4 images
gfortran alone runs coarray code only as a single degenerate image (-fcoarray=single, where
num_images() is 1 and every sync all is a no-op). Multi-image execution on gfortran requires the
external OpenCoarrays runtime: caf wraps gfortran -fcoarray=lib + the library, and cafrun -n N
launches N images (wrapping mpirun). Intel builds coarrays in (ifx -coarray, image count via the
FOR_COARRAY_NUM_IMAGES environment variable) with no separate launcher.
G.3 OpenMP (shared-memory threads)
OpenMP parallelizes a serial loop by annotating it with a directive (a specially formatted comment,
!$omp …). Without -fopenmp the directives are ignored and the same source builds a correct serial
program; with it, the loop's iterations are split across a team of threads that all share one memory.
Directives
| Directive | Effect |
|---|---|
!$omp parallel` … `!$omp end parallel |
fork a team; every thread runs the block |
!$omp do` … `!$omp end do |
share a loop's iterations across the team (inside a parallel region) |
!$omp parallel do` … `!$omp end parallel do |
fork and share the loop in one directive (most common) |
!$omp sections` / `!$omp section … !$omp end sections |
give different code blocks to different threads (task parallelism) |
!$omp single` … `!$omp end single |
exactly one thread runs the block (implicit barrier at end) |
!$omp master` … `!$omp end master |
the master thread (0) runs the block; no implicit barrier |
!$omp workshare` … `!$omp end workshare |
parallelize whole-array Fortran statements |
!$omp simd` / `!$omp do simd / !$omp parallel do simd |
vectorize a loop (SIMD), optionally combined with threading |
!$omp task` … `!$omp end task ; !$omp taskwait |
queue irregular/recursive work for the team; wait for it |
Data-sharing clauses
| Clause | Meaning |
|---|---|
shared(list) |
one instance, visible to all threads (correct for read-only data) |
private(list) |
each thread gets its own uninitialized copy (loop indices, scratch) |
firstprivate(list) |
private, but each copy initialized to the pre-region value |
lastprivate(list) |
private, and the last iteration's value is copied back out |
default(none) |
remove implicit scoping — you must name an attribute for every variable (use always) |
reduction(op:var) |
private per thread, combined with op at the end (the race-free way to sum/max/count) |
schedule(kind[,chunk]) |
how iterations are handed out (see below) |
num_threads(n) |
set the team size for this region |
collapse(n) |
fold n nested loops into one iteration space |
Reduction operators: +, *, -, max, min, .and., .or., .eqv., .neqv., iand, ior,
ieor. The inner loop index and every scratch temporary are your responsibility to scope — only the
!$omp do loop variable is made private automatically.
Synchronization and scheduling
| Construct | Effect |
|---|---|
!$omp barrier |
all threads wait here (work-sharing constructs carry an implicit one) |
!$omp critical [(name)] |
one thread at a time in the block (general, relatively slow) |
!$omp atomic |
one indivisible x = x op expr update (cheap; single statement only) |
schedule(static[,chunk]) |
equal chunks decided up front — reproducible; best for uniform work |
schedule(dynamic[,chunk]) |
threads grab chunks at run time — self-balancing; best for lumpy work |
schedule(guided[,chunk]) |
like dynamic but chunk size shrinks over the loop |
For combining per-thread results prefer, in order: reduction → atomic → critical.
Runtime library and environment (use omp_lib)
| Routine / variable | Returns / sets |
|---|---|
omp_get_thread_num() |
this thread's id, 0 .. team_size-1 (master is 0) |
omp_get_num_threads() |
number of threads in the current team |
omp_get_max_threads() |
threads a parallel region would use |
omp_set_num_threads(n) |
set the default team size |
omp_get_wtime() |
wall-clock time in seconds (a real(dp)), for timing regions |
OMP_NUM_THREADS (env) |
default team size at run time — the usual way to set it |
OMP_SCHEDULE, OMP_PROC_BIND (env) |
default schedule; thread-to-core binding |
Compile and run
$ gfortran -std=f2018 -fopenmp -Wall prog.f90 -o prog
$ OMP_NUM_THREADS=4 ./prog
The -fopenmp flag both enables the directives and links the runtime that provides omp_lib. Omit it and
your "parallel" program is silently serial.
G.4 MPI (distributed-memory processes)
MPI runs many processes, each with private memory, coordinating by explicit messages. It is a
library (Open MPI, MPICH), not part of Fortran; a program uses it with use mpi and mpi_* calls.
Every mpi_* call takes ierr (an integer status) as its last argument.
The skeleton
use mpi ! or: use mpi_f08 (modern — see below)
implicit none
integer :: ierr, rank, nprocs
integer :: status(MPI_STATUS_SIZE)
call mpi_init(ierr) ! start the runtime
call mpi_comm_rank(MPI_COMM_WORLD, rank, ierr) ! my rank, 0 .. nprocs-1
call mpi_comm_size(MPI_COMM_WORLD, nprocs, ierr) ! how many processes
! ... work ...
call mpi_finalize(ierr) ! shut the runtime down
Point-to-point communication (mind the argument order)
call mpi_send(buf, count, datatype, dest, tag, comm, ierr)
! buf count datatype dest tag comm ierr
call mpi_recv(buf, count, datatype, source, tag, comm, status, ierr)
! buf count datatype source tag comm status ierr
call mpi_sendrecv(sendbuf, sendcount, sendtype, dest, sendtag, & ! send half
recvbuf, recvcount, recvtype, source, recvtag, & ! recv half
comm, status, ierr)
call mpi_isend(buf, count, datatype, dest, tag, comm, request, ierr) ! non-blocking
call mpi_irecv(buf, count, datatype, source, tag, comm, request, ierr) ! non-blocking
call mpi_wait(request, status, ierr) ! complete one
call mpi_waitall(count, requests, statuses, ierr) ! complete several
Six things must line up for delivery: buffer, count, datatype, dest/source, tag, communicator.
A paired send-then-receive on both ranks can deadlock on large messages (it "works" for small ones
that MPI buffers) — use mpi_sendrecv (one call, cannot deadlock) or order the calls so one side receives
first. With mpi_isend/mpi_irecv, do not touch the buffer until the matching mpi_wait returns.
Collective communication
call mpi_bcast(buffer, count, datatype, root, comm, ierr) ! one -> all
call mpi_scatter(sendbuf, sendcount, sendtype, & ! one -> all (pieces)
recvbuf, recvcount, recvtype, root, comm, ierr)
call mpi_gather (sendbuf, sendcount, sendtype, & ! all -> one
recvbuf, recvcount, recvtype, root, comm, ierr)
call mpi_reduce(sendbuf, recvbuf, count, datatype, op, root, comm, ierr) ! combine -> root
call mpi_allreduce(sendbuf, recvbuf, count, datatype, op, comm, ierr) ! combine -> all
call mpi_barrier(comm, ierr) ! all wait
Every process in the communicator must make the matching call, or the collective hangs. In mpi_gather
and mpi_scatter, recvcount/sendcount is the count per process, not the total. mpi_reduce has a
root; mpi_allreduce does not (everyone gets the result).
Datatypes and reduction operations
| Fortran type | MPI datatype |
|---|---|
integer |
MPI_INTEGER |
default real |
MPI_REAL (a.k.a. MPI_REAL4) |
real(dp) (double) |
MPI_DOUBLE_PRECISION — equivalently MPI_REAL8 |
complex(dp) |
MPI_DOUBLE_COMPLEX |
logical |
MPI_LOGICAL |
character |
MPI_CHARACTER |
Reduction ops: MPI_SUM, MPI_PROD, MPI_MAX, MPI_MIN, MPI_LAND, MPI_LOR, MPI_MAXLOC,
MPI_MINLOC. Useful constants: MPI_COMM_WORLD, MPI_PROC_NULL (a no-op rank, for grid edges),
MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_STATUS_SIZE, MPI_SUCCESS.
🔧
use mpivsmpi_f08. Three interfaces exist, in ascending modernity: the legacyinclude 'mpif.h'(no compile-time checking — avoid in new code);use mpi(this book's default — handles are plainintegers,ierrmandatory); anduse mpi_f08(from MPI-3, preferred for new code — handles become derived typestype(MPI_Comm),type(MPI_Datatype),type(MPI_Status),type(MPI_Request), andierrbecomes a genuinely optional final argument, socall mpi_init()is valid). We teachuse mpibecause it is what most existing code uses; reach formpi_f08in fresh code.
Compile and run
$ mpif90 -std=f2018 -Wall -O2 prog.f90 -o prog # mpif90 (or mpifort) = gfortran + MPI flags
$ mpirun -np 4 ./prog # launch 4 processes (mpiexec is equivalent)
mpif90 is not a different compiler — it is gfortran with MPI's include/link paperwork filled in, so all
the optimization flags of Appendix C still apply. -np N may
exceed the physical core count (processes time-share) for testing a decomposition.
G.5 GPU: OpenACC and CUDA Fortran
Two roads onto the graphics processor: OpenACC directives (portable, incremental — the easy path) and CUDA Fortran explicit kernels (NVIDIA-only, more control). The governing fact for both is that the device has its own memory: the dominant cost is moving data across the host↔device bridge, so the rule is move data once, compute on it many times.
OpenACC directives
| Directive | Effect |
|---|---|
!$acc parallel loop [clauses] |
run this loop's (independent) iterations on the device |
!$acc kernels` … `!$acc end kernels |
hand the region to the compiler to auto-parallelize |
!$acc parallel loop collapse(2) |
fold nested loops into one iteration space for the GPU |
!$acc parallel loop reduction(op:var) |
GPU reduction (same spirit as OpenMP's) |
!$acc data …` … `!$acc end data |
keep named arrays resident on the device across many kernels |
!$acc update self(a)` / `!$acc update host(a) |
refresh the host's copy from the device (mid-run, no teardown) |
!$acc update device(a) |
refresh the device's copy from the host |
!$acc wait |
wait for asynchronous device work |
OpenACC data clauses (on !$acc data` or `!$acc parallel loop)
| Clause | At entry | At exit | Use for |
|---|---|---|---|
copyin(a) |
host → device | (nothing) | inputs the device only reads |
copyout(a) |
allocate on device | device → host | outputs the device only writes |
copy(a) |
host → device | device → host | arrays read and written |
create(a) |
allocate on device | (nothing) | device-only scratch |
present(a) |
(assert already resident) | (nothing) | data an enclosing !$acc data region already moved |
The canonical pattern — one data region around a whole time loop, present on each step's kernel — is what
makes an offload win instead of embarrass:
!$acc data copy(u) create(u_new) ! field RESIDENT across ALL steps: one copy in, one out
do step = 1, nsteps
!$acc parallel loop collapse(2) present(u, u_new) ! no per-step transfer
do j = 2, ny-1
do i = 2, nx-1
u_new(i,j) = u(i,j) + r*( u(i-1,j) + u(i+1,j) + u(i,j-1) + u(i,j+1) - 4.0_dp*u(i,j) )
end do
end do
! ... commit u_new into u ...
end do
!$acc end data
CUDA Fortran (use cudafor)
| Construct | Meaning |
|---|---|
attributes(global) subroutine k(...) |
a kernel — called from host, runs on device; must be a module procedure |
real(dp), device :: x_d(n) |
an array in device memory; x_d = x copies host → device, y = y_d copies back |
real(dp), value :: a |
pass a scalar argument by value into a kernel |
call k<<<nblocks, tpb>>>(...) |
launch: a grid of nblocks blocks × tpb threads each |
threadIdx%x, blockIdx%x, blockDim%x, gridDim%x |
built-in thread/block indices and sizes |
attributes(global) subroutine saxpy_kernel(a, x, y, n)
real(dp), value :: a
integer, value :: n
real(dp) :: x(n), y(n) ! dummy arrays live in device memory
integer :: i
i = (blockIdx%x - 1)*blockDim%x + threadIdx%x ! ONE-based global index
if (i <= n) y(i) = a*x(i) + y(i) ! guard the tail
end subroutine saxpy_kernel
⚠️ CUDA Fortran thread indices are ONE-based. In CUDA C the index is
blockIdx.x*blockDim.x + threadIdx.x(0-based); in CUDA FortranthreadIdx%xandblockIdx%xcount from 1, so the correct global index is(blockIdx%x - 1)*blockDim%x + threadIdx%x. Copy the C formula verbatim and you are off by one at every block boundary. (blockDim/gridDimare counts, not indices — they are not shifted.)
Compile and run
$ nvfortran -acc -Minfo=accel prog.f90 -o prog # OpenACC on the GPU (-Minfo=accel reports what offloaded)
$ nvfortran -acc=multicore prog.f90 -o prog # OpenACC targeting CPU cores instead
$ gfortran -fopenacc prog.f90 -o prog # OpenACC via gfortran (offload maturity trails nvfortran)
$ nvfortran -cuda prog.f90 -o prog # CUDA Fortran (NVIDIA only; a *.cuf file enables it too)
A note on these GPU examples. As stated in Chapter 35, this book has no GPU and no
nvfortranto compile against, so the OpenACC and CUDA Fortran syntax above is written to be exactly correct but was not machine-checked, and any performance figure is an illustrative order of magnitude, never a promise. CUDA Fortran has no gfortran equivalent — it isnvfortran-only.
G.6 The same idea in four dialects
The deepest payoff of Part VIII is seeing that the four models are four spellings of one small set of ideas. Learn to read across a row and the next model stops being a fresh mountain.
| Operation | Coarrays (32) | OpenMP (33) | MPI (34) | OpenACC (35) |
|---|---|---|---|---|
| Unit of parallelism | image | thread | process (rank) | GPU thread |
| "Which am I?" | this_image() (1..N) |
omp_get_thread_num() (0..N−1) |
mpi_comm_rank(comm, r, ierr) (0..N−1) |
index from threadIdx/blockIdx |
| "How many?" | num_images() |
omp_get_num_threads() |
mpi_comm_size(comm, n, ierr) |
grid × block dimensions |
| Memory | private per image (PGAS) | shared | private per process | host + separate device |
| Sum across all | call co_sum(x) |
reduction(+:x) |
mpi_allreduce(x,r,1,type,MPI_SUM,comm,ierr) |
reduction(+:x) |
| Max across all | call co_max(x) |
reduction(max:x) |
mpi_allreduce(...,MPI_MAX,...) |
reduction(max:x) |
| Broadcast one→all | call co_broadcast(x, k) |
(shared memory — none needed) | mpi_bcast(x,1,type,root,comm,ierr) |
(host owns the value) |
| Barrier | sync all |
!$omp barrier` | `mpi_barrier(comm, ierr)` | (implicit at region end; `!$acc wait) |
||
| Mutual exclusion | critical / lock |
!$omp critical` / `!$omp atomic |
(private memory — no shared race) | !$acc atomic |
| Neighbour halo | u(:,1) = u(:,n)[q] |
(shared array — no exchange) | mpi_sendrecv(...) |
(device-resident field) |
The row that recurs three times over — co_sum, reduction(+:s), mpi_allreduce(...,MPI_SUM,...) (and
OpenACC's reduction(+:s)) — is one operation in four dialects: combine a value across all the parallel
workers. Recognizing it is much of what makes the later models feel like variations.
G.7 Which model when?
| If your situation is… | Reach for | Because | Chapter |
|---|---|---|---|
| One node's cores, shared RAM, least ceremony | OpenMP | a directive on the loop you already have; no decomposition, no halos | 33 |
| A structured grid; you want native Fortran, one notation from laptop to cluster | Coarrays | parallelism in the language (PGAS); halo is a coindexed a[q] |
32 |
| Many nodes of a cluster; maximum maturity and scale; joining existing HPC code | MPI | the HPC lingua franca; scales to millions of cores; explicit and tunable | 34 |
| Large, regular, data-parallel arrays and you have a GPU | OpenACC (then CUDA Fortran) | a throughput device for the stencil sweep; directives first, kernels if you must | 35 |
| The biggest runs: many nodes, each with many cores and/or a GPU | Hybrid MPI + OpenMP (+ GPU) | MPI across nodes, OpenMP/OpenACC within each node | 34, 33, 35 |
Portability, honestly weighed. OpenMP is the most universal for shared memory — built into gfortran with
just -fopenmp. Coarray syntax is standard and portable, but multi-image execution on gfortran depends
on the external OpenCoarrays install (Intel's ifx builds it in), and teams are weakly supported on
gfortran today (G.2). MPI is a library you install (Open MPI / MPICH) — ubiquitous on clusters, not part
of the language. OpenACC is most mature on nvfortran (gfortran's -fopenacc is catching up); CUDA Fortran
is NVIDIA-only. When in doubt for a single node, OpenMP is the smallest, most portable step; for a
cluster, MPI is the safe default; coarrays are the elegant native alternative that spans both.
All four solve the same problem. Every model in this appendix parallelizes the identical five-point heat
stencil of Chapter 24, behind
the same frozen step interface, and each chapter's Project Checkpoint reaches the same deterministic
field — only the schedule and the machine differ. Coarrays and MPI use the same 1D domain decomposition and
the same halo (coindexed read vs. explicit mpi_sendrecv); OpenMP and OpenACC parallelize the same interior
sweep (threads vs. GPU cores). The physics is invariant; the parallelism is the wardrobe. If a parallel run
ever disagrees with the serial answer, you have a bug — a scoping mistake, a missing synchronization, or an
un-exchanged halo — not a faster result.