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
criticalend 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's ifx/ifort has led on 2018 coarray features. The collectives (co_sum and 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: reductionatomiccritical.

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 mpi vs mpi_f08. Three interfaces exist, in ascending modernity: the legacy include 'mpif.h' (no compile-time checking — avoid in new code); use mpi (this book's default — handles are plain integers, ierr mandatory); and use mpi_f08 (from MPI-3, preferred for new code — handles become derived types type(MPI_Comm), type(MPI_Datatype), type(MPI_Status), type(MPI_Request), and ierr becomes a genuinely optional final argument, so call mpi_init() is valid). We teach use mpi because it is what most existing code uses; reach for mpi_f08 in 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 Fortran threadIdx%x and blockIdx%x count 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/gridDim are 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 nvfortran to 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 is nvfortran-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.