do concurrent and GPU Offload
|
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 was checked on 2026-10-19. Every example
that runs on a CPU was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2)
and run, and, unless the text says otherwise, also built and run with LLVM Flang 23; the output shown is real output. The
examples that need a parallelizing option (-ftree-parallelize-loops, -fopenmp -fdo-concurrent-to-openmp=host) were compiled with
-O2 and that option instead of the strict -std=f2023 set, and the timings are from a 10-core aarch64 Linux container, so
they show the relative effect only. No GPU is available here: the GPU offload material (NVIDIA -stdpar=gpu, Flang
-fdo-concurrent-to-openmp=device, Intel ifx offload options) is checked against the documentation, not run, and is labelled
that way where it appears. The loop construct is defined in 11.1.7 of the standard: the syntax is 11.1.7.2, the loop control
11.1.7.4.2 and the extra rules for do concurrent are 11.1.7.5. forall is 10.2.4, and its obsolescent status is Annex B.3.13.
do concurrent is a promise to the compiler: the iterations of this loop do not depend on one another, so they may be executed in any
order, interleaved, vectorized or run on different threads or on a GPU. It is the only standard Fortran statement whose job is to
state that promise, and it is the reason why the loop is the first thing to try before reaching for OpenMP or OpenACC.
The examples use the temperature grid of the running example, heatflow, as the data: the explicit FTCS step of the heat equation
updates every interior point from its four neighbours in the old grid, which is exactly the independence a do concurrent
loop asks for. The stencil kernels and the sequential do forms are in
Control Flow, the explicit-thread alternative is
OpenMP and OpenACC, the distributed-memory alternatives are
Coarrays, Images and Synchronization and
MPI with mpi_f08, and the array-syntax forms that often make an
explicit loop unnecessary are in Array Intrinsics and Reductions.
The contract: independent iterations
A do concurrent construct is a do construct whose loop control is the word concurrent followed by a concurrent header and,
optionally, locality specifiers (R1123, R1125 to R1132):
[name:] do concurrent ( [integer-type-spec ::] index = lower:upper[:step] [, index = ...] [, mask] ) [locality-specs]
block
end do [name]
The block is executed once for every active combination of the index values (11.1.7.4.2), and the standard places no order on the combinations. The rules that make this safe are collected in 11.1.7.5; the note after them says why they exist: they "are designed to ensure there are no data dependencies between iterations of the loop". What follows is the programmer-level reading of them.
| Rule | Detail | Clause |
|---|---|---|
No carried dependence |
if an iteration defines a variable (or becomes undefined for it), no other iteration may reference, define or undefine it: this is what lets the loop be reordered or parallelized |
11.1.7.5 |
Only pure procedures |
a reference to an impure procedure shall not appear in the construct (C1143), and the restrictions apply to every procedure invoked from the loop (Note 2) |
11.1.7.5 |
No |
|
11.1.7.5, 11.1.12, 11.2.1 |
No image control statements |
|
11.1.7.5 |
No deallocation of polymorphic entities |
a statement that might deallocate a polymorphic entity shall not appear (C1144) |
11.1.7.5 |
IEEE state is fixed |
the procedures that get or set the halting mode, status or modes, the rounding mode or the underflow mode shall not be referenced (C1145, C1146) |
11.1.7.5 |
No |
an input/output statement with an |
11.1.7.5 |
Mask must be pure |
a procedure referenced in the mask expression, including a defined operation, shall be pure (C1123) |
11.1.7.2 |
Index limits are independent |
a limit or step shall not use an index of the same header (C1125), so the iteration space is a plain rectangle (or a masked subset) |
11.1.7.2 |
A conforming program obeys these rules. The compilers check the ones that are visible in the source (impure calls, exit, image
control statements, locality specifications) and cannot check the one that matters most, the dependence between iterations: that
one remains the programmer’s responsibility, as the pitfalls at the end of this page show.
The heatflow stencil
The explicit FTCS update of the heatflow grid, written with do concurrent over both indices, followed by a second construct that
computes the residual with a reduce specifier (described in reduce: a Fortran 2023 specifier). The sequential version of
the same loop is ftcs_sweep in heatflow_kernels; the data comes from heatflow_grid.
program do_concurrent_stencil
use heatflow_kinds, only: dp
use heatflow_grid, only: grid_t, make_grid
implicit none
type(grid_t) :: g
real(dp), allocatable :: unew(:, :)
real(dp), parameter :: alpha = 1.0_dp, dt = 5.0e-5_dp
real(dp) :: residual
integer :: i, j, n
g = make_grid(41, 41, 1.0_dp, 1.0_dp)
call g%set_hot_spot(100.0_dp)
unew = g%u
do n = 1, 200
do concurrent (j = 2:g%ny - 1, i = 2:g%nx - 1)
unew(i, j) = g%u(i, j) + alpha*dt*( &
(g%u(i - 1, j) - 2.0_dp*g%u(i, j) + g%u(i + 1, j))/g%dx**2 + &
(g%u(i, j - 1) - 2.0_dp*g%u(i, j) + g%u(i, j + 1))/g%dy**2)
end do
residual = 0.0_dp
do concurrent (j = 2:g%ny - 1, i = 2:g%nx - 1) reduce(max: residual)
residual = max(residual, abs(unew(i, j) - g%u(i, j)))
end do
g%u(2:g%nx - 1, 2:g%ny - 1) = unew(2:g%nx - 1, 2:g%ny - 1)
end do
print '(a,es10.3,a,f8.3)', 'residual=', residual, ' max=', g%max_temperature()
end program do_concurrent_stencil
Output (identical with gfortran and with Flang):
residual= 2.514E-03 max= 0.499
The interior points are updated from g%u into unew; the old grid is only read inside the loop, and every iteration writes a
different element of unew. That is the pattern to look for: read from one array, write to another, no iteration writes what
another reads. The copy back (g%u(...) = unew(...)) is a whole-array assignment outside the loop, which is ordinary Fortran.
The mathematical background is the usual one: with \( \alpha \) the diffusivity and \( \Delta t \) the step,
\[ u^{n+1}_{i,j} = u^n_{i,j} + \alpha \Delta t \left( \frac{u^n_{i-1,j} - 2u^n_{i,j} + u^n_{i+1,j}}{\Delta x^2} + \frac{u^n_{i,j-1} - 2u^n_{i,j} + u^n_{i,j+1}}{\Delta y^2} \right), \]
and every right-hand side depends only on level \( n \).
The concurrent header
The header is a list of one or more concurrent controls, each index = lower:upper[:step], followed by an optional scalar logical
mask (R1125, R1126). The rules are those of a counted do loop, applied to every index:
-
the limits and the step are scalar integer expressions evaluated once, before any iteration, and the step shall not be zero;
-
an index variable is a construct entity: its scope is the construct, it has no other attributes, and if the header has no type-spec it has the type and kind of the variable of that name in the enclosing scope, which then shall be an integer (19.4);
-
an optional
integer-type-spec ::in the header makes the indices of that integer kind (a Fortran 2008 feature, according to the GNU Fortran Fortran 2008 status page); -
the iterations are the combinations of the index values for which the mask is true, and the mask may use the indices;
-
an empty range executes nothing, as for a counted
do.
program header_forms
use, intrinsic :: iso_fortran_env, only: int64
implicit none
integer, parameter :: n = 6
integer :: i, j, hits
integer :: a(n, n)
! several index ranges, a step and a mask: one iteration per active (i, j) combination
a = 0
do concurrent (j = 1:n, i = 1:n, i > j)
a(i, j) = 10*i + j
end do
print '(a,i0)', 'strictly lower triangle entries set: ', count(a /= 0)
! a step: the same rules as for a counted do
a = 0
do concurrent (i = n:1:-2)
a(i, 1) = i
end do
print '(a,6i3)', 'column 1 with step -2: ', a(:, 1)
! a type-spec makes the index variables a different kind, local to the construct
hits = 0
do concurrent (integer(int64) :: k = 1:5) reduce(+: hits)
hits = hits + int(k)
end do
print '(a,i0)', 'sum with 64-bit index: ', hits
! an empty range executes nothing, as for a counted do
hits = -1
do concurrent (i = 5:1)
hits = 0
end do
print '(a,i0)', 'after the empty range hits = ', hits
end program header_forms
Output (identical with gfortran and with Flang):
strictly lower triangle entries set: 15
column 1 with step -2: 0 2 0 4 0 6
sum with 64-bit index: 15
after the empty range hits = -1
The first construct sets the strictly lower triangle of a 6 by 6 array (15 elements) with a mask; the second uses a negative step; the
third declares the index as integer(int64), so the header is independent of the default integer kind (it also shows reduce for an
accumulating sum); the fourth never executes. Compare with the array syntax where (...) or forall: the mask of do concurrent
selects iterations, and the block can contain any permitted statements, not only one assignment.
Locality: local, local_init, shared and default(none)
By default, a variable that appears in the block has unspecified locality, and the standard gives the rules for it in 11.1.7.5: a variable referenced in an iteration must have been defined earlier in that iteration or must not be defined by any other iteration, and so on. Fortran 2018 added the locality specifiers, which say it explicitly (Annex C.1 lists "the locality of a variable used in a DO CONCURRENT construct can be explicitly specified" among the Fortran 2018 features):
| Specifier | Meaning | Restrictions (clause 11.1.7.2 and 11.1.7.5) |
|---|---|---|
|
each iteration gets its own copy of the variable, undefined at the start of the iteration (a pointer has undefined association status); the outside variable is inaccessible by that name inside the construct and unchanged afterwards |
no |
|
like |
same as |
|
the name refers to the outside variable; if an iteration defines it, no other iteration may reference or define it |
the variables named must be in the innermost enclosing construct or scoping unit (C1126) |
|
a combiner variable, see reduce: a Fortran 2023 specifier |
Fortran 2023 (C1131, C1132) |
|
every variable of an enclosing scope used in the block must be named in one of the specifiers (C1134); at most once per header (C1129) |
A name can appear in one specifier only (C1128), cannot be an index of the same header (C1127), and a variable used by the header (in a
limit, step or mask) shall not be local (C1133). A construct or statement entity declared inside the block (for example in a
block construct) without the save attribute is a different entity in each iteration, which is the cleanest way to get a
temporary without any specifier.
module scratch_kernels
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
integer, parameter :: dp = real64
public :: laplacian
contains
pure function laplacian(u, i, j, dx, dy) result(l)
real(dp), intent(in) :: u(:, :), dx, dy
integer, intent(in) :: i, j
real(dp) :: l
l = (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 function laplacian
end module scratch_kernels
program locality
use, intrinsic :: iso_fortran_env, only: real64
use scratch_kernels, only: laplacian
implicit none
integer, parameter :: dp = real64, n = 5
real(dp) :: u(n, n), unew(n, n), tmp, offset, scale
integer :: i, j
u = 0.0_dp
u(3, 3) = 1.0_dp
unew = u
! local: tmp is a fresh temporary in every iteration; the outside tmp is not touched
tmp = -99.0_dp
scale = 0.1_dp
offset = 5.0_dp
do concurrent (j = 2:n - 1, i = 2:n - 1) local(tmp) shared(unew, u, scale)
tmp = laplacian(u, i, j, 0.25_dp, 0.25_dp)
unew(i, j) = u(i, j) + scale*tmp
end do
print '(a,f8.3)', 'tmp after the construct (unchanged): ', tmp
print '(a,f8.4)', 'centre after one step: ', unew(3, 3)
! local_init: every iteration starts from the value the outside variable has
do concurrent (j = 2:n - 1, i = 2:n - 1) local_init(offset) shared(unew, u)
offset = offset + 1.0_dp
unew(i, j) = u(i, j) + offset
end do
print '(a,f8.3,a,f8.3)', 'offset after the construct (unchanged): ', offset, ' unew(2,2)=', unew(2, 2)
! default(none): every variable of the host scope used in the block must be classified
do concurrent (j = 2:n - 1, i = 2:n - 1) default(none) shared(u, unew) local(tmp)
tmp = 2.0_dp*u(i, j)
unew(i, j) = tmp
end do
print '(a,f8.3)', 'doubled centre: ', unew(3, 3)
end program locality
Output (identical with gfortran and with Flang):
tmp after the construct (unchanged): -99.000
centre after one step: -5.4000
offset after the construct (unchanged): 5.000 unew(2,2)= 6.000
doubled centre: 2.000
tmp keeps its outside value -99.0 after the first construct because the iterations used their own copies. The laplacian
function is pure, which is the price of admission for calling it from the loop. The third construct shows default(none): every
outside variable used in the block (u, unew, tmp) is named; the loop indices need no entry. The second construct shows local_init: offset starts at 5.0
in every iteration, so each unew(i, j) gets u + 6.0; had offset been unspecified or shared, the loop
would have updated one variable from every iteration, which is a violation.
The standard’s own example for local (Note 5 of 11.1.7.6) is a loop that modifies x inside the construct and still prints the original 1.0 afterwards.
default(none) is the specifier that catches mistakes. Forgetting one name is a compile-time error:
program default_none_misuse
implicit none
integer :: i
real :: a(10), factor
a = 1.0
factor = 3.0
do concurrent (i = 1:10) default(none) shared(a)
a(i) = factor*a(i)
end do
print *, a
end program default_none_misuse
GNU Fortran 16.2 (-std=f2023):
10_default_none.f90:9:17:
8 | do concurrent (i = 1:10) default(none) shared(a)
| 2
9 | a(i) = factor*a(i)
| 1
Error: Variable 'factor' at (1) not specified in a locality spec of DO CONCURRENT at (2) but required due to DEFAULT (NONE)
LLVM Flang 23:
error: Semantic errors in 10_default_none.f90
10_default_none.f90:9:12: error: Variable 'factor' from an enclosing scope referenced in DO CONCURRENT with DEFAULT(NONE) must appear in a locality-spec
a(i) = factor*a(i)
^^^^^^
10_default_none.f90:4:18: Declaration of 'factor'
real :: a(10), factor
^^^^^^
reduce: a Fortran 2023 specifier
A reduction (a sum, a maximum, a count) updates one variable from every iteration, which would violate the independence rules. Fortran
2023 added the reduce(operation: variables) locality specifier to express it (the Introduction of J3/23-007r1 lists it among the
Fortran 2023 execution-control features). The operation is one of +, *, .and., .or., .eqv., .neqv., or the intrinsic
functions iand, ieor, ior, max, min (R1131, R1132, C1122). The semantics (11.1.7.5):
-
each iteration works on a construct entity of the variable’s type, initialized, before the iterations begin, from Table 11.1:
0for+,ieor,ior;1for*;.true.for.and.and.eqv.;.false.for.or.and.neqv.; all bits set foriand; the least representable value formaxand the largest formin; -
the variable can appear in the block only in an intrinsic assignment of the form
v = v op expr,v = expr op vorv = func(expr, v)(with the same variable in each occurrence); -
at the end the outside variable is combined with the iteration results "in any order", which is why a floating-point sum can differ in the last bits between runs and between compilers;
-
the variable cannot be
intent(in),optional,volatile,asynchronous, coindexed or assumed-size (C1131), and shall be of a type suitable for the operation (C1132).
program reductions
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64, n = 8
real(dp) :: u(n, n), total, hottest, coldest
integer :: i, j, flags, all_bits, parity
logical :: any_neg
do j = 1, n
do i = 1, n
u(i, j) = real(i*j, dp) - 20.0_dp
end do
end do
total = 0.0_dp
hottest = -huge(1.0_dp)
coldest = huge(1.0_dp)
any_neg = .false.
flags = 0
all_bits = -1
parity = 0
do concurrent (j = 1:n, i = 1:n) reduce(+: total) reduce(max: hottest) reduce(min: coldest) &
reduce(.or.: any_neg) reduce(ior: flags) reduce(iand: all_bits) reduce(ieor: parity)
total = total + u(i, j)
hottest = max(hottest, u(i, j))
coldest = min(coldest, u(i, j))
any_neg = any_neg .or. u(i, j) < 0.0_dp
flags = ior(flags, i)
all_bits = iand(all_bits, i + 8)
parity = ieor(parity, i*j)
end do
print '(a,f9.3)', 'sum = ', total
print '(a,f9.3)', 'max = ', hottest
print '(a,f9.3)', 'min = ', coldest
print '(a,l1)', 'any negative = ', any_neg
print '(a,3i5)', 'ior, iand, ieor = ', flags, all_bits, parity
print '(a,f9.3)', 'check with intrinsic sum: ', sum(u)
end program reductions
Output (identical with gfortran and with Flang):
sum = 16.000
max = 44.000
min = -19.000
any negative = T
ior, iand, ieor = 15 0 80
check with intrinsic sum: 16.000
The same result as sum(u), maxval(u) and minval(u) of the array intrinsics, with the difference that the block can compute the
value during the loop instead of reading a stored array: the stencil of the first example does it for the residual
(reduce(max: residual)). Before reduce, the only portable way to write that loop was to store per-iteration values in an array and
reduce it afterwards, or to use OpenMP.
The compilers accept the form of the assignment but do not check it. Writing the combiner in a form the standard does not allow is not diagnosed by gfortran 16.2 or Flang 23, which print the same value:
program reduce_misuse
implicit none
integer :: i
real :: s, a(10)
a = 1.0
s = 0.0
do concurrent (i = 1:10) reduce(+: s)
s = s * 2.0 + a(i)
end do
print *, s
end program reduce_misuse
1023.00000
s = s * 2.0 + a(i) is not of the form s = s op expr, so the program is not conforming; run sequentially, the loop computes
1023 (that is, 1024 - 1), and a parallel execution would give another value. Do not rely on the compiler to find this.
What the compiler rejects
Because the rules are in the grammar and in the constraints, the compilers diagnose the violations they can see. Each of the next four fragments is deliberately wrong and was compiled with both compilers; the diagnostics are real.
An impure procedure. bump is not pure (it is an internal subroutine without the prefix):
program impure_call
implicit none
integer, parameter :: n = 4
real :: a(n)
integer :: i
a = 1.0
do concurrent (i = 1:n)
call bump(a(i))
end do
print *, a
contains
subroutine bump(x)
real, intent(inout) :: x
x = x + 1.0
end subroutine bump
end program impure_call
GNU Fortran 16.2:
06_impure.f90:9:19:
9 | call bump(a(i))
| 1
Error: Subroutine call to 'bump' in DO CONCURRENT block at (1) is not PURE
LLVM Flang 23:
error: Semantic errors in 06_impure.f90
06_impure.f90:9:5: error: Impure procedure 'bump' may not be referenced in DO CONCURRENT
call bump(a(i))
^^^^^^^^^^^^^^^
The fix is to make the procedure pure (or elemental) and, if it needs to modify its argument, to give the argument intent(inout) as above;
a pure subroutine may change its dummy arguments but no other data. Pure procedures are covered in
Procedures, Functions and Subroutines.
Leaving the construct. An exit that belongs to the construct is rejected (C1172), as is a return (C1140):
program leave_early
implicit none
integer :: i
real :: a(10)
a = [(real(i), i = 1, 10)]
do concurrent (i = 1:10)
if (a(i) > 5.0) exit
a(i) = 0.0
end do
print *, a
end program leave_early
GNU Fortran 16.2:
07_exit_return.f90:8:24:
8 | if (a(i) > 5.0) exit
| 1
Error: EXIT statement at (1) leaves DO CONCURRENT construct
LLVM Flang 23:
error: Semantic errors in 07_exit_return.f90
07_exit_return.f90:8:21: error: EXIT must not leave a DO CONCURRENT statement
if (a(i) > 5.0) exit
^^^^
07_exit_return.f90:7:3: The construct that was left
do concurrent (i = 1:10)
^^^^^^^^^^^^^^^^^^^^^^^^
A search with early termination is not a do concurrent loop: use a counted do with exit, or the findloc, any and all intrinsics.
cycle is allowed, because it ends only the iteration.
An image control statement. sync memory (and the others of C1141) is rejected:
program image_control
implicit none
integer :: i
real :: a(10)
a = 1.0
do concurrent (i = 1:10)
a(i) = 2.0*a(i)
sync memory
end do
print *, a
end program image_control
LLVM Flang 23 diagnoses it directly:
error: Semantic errors in 08_sync.f90
08_sync.f90:9:5: error: An image control statement is not allowed in DO CONCURRENT
sync memory
^^^^^^^^^^^
08_sync.f90:7:3: Enclosing DO CONCURRENT statement
do concurrent (i = 1:10)
^^^^^^^^^^^^^^^^^^^^^^^^
GNU Fortran 16.2 stops earlier by default, because coarray features are disabled (a fatal error, nothing about do concurrent); with
-fcoarray=single it reports the rule itself:
08_sync.f90:9:15:
9 | sync memory
| 1
Error: Image control statement SYNC at (1) in DO CONCURRENT block
A missing locality declaration under default(none) was shown in the previous section.
The dependence the compiler cannot see
Everything above is checked syntactically. The central rule, no dependence between iterations, is not:
program carried_dependency
implicit none
integer, parameter :: n = 8
real :: a(n)
integer :: i
a = 1.0
! NOT conforming: iteration i reads what iteration i-1 defines
do concurrent (i = 2:n)
a(i) = a(i - 1) + 1.0
end do
print '(8f5.1)', a
end program carried_dependency
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0
Both compilers accept this program without a diagnostic and, in the builds tried here (including gfortran -O2 -ftree-parallelize-loops=4), printed the cumulative
values, but the program is not conforming: iteration i references a(i - 1), which iteration i - 1 defines. A compiler is free to run the
iterations in any order or at once, so a build with -ftree-parallelize-loops, an OpenMP mapping or a GPU can print anything, and the result
can change from one run to the next. Treat do concurrent as a statement that you verify, as you would a pragma. The rewrite is the
one the forall section below shows: read from a copy.
Two kinds of dependence are easy to miss. The first is an indirect subscript: a(ind(i)) = i is independent only if ind has no repeated
values, as Note 4 of 11.1.7.6 says explicitly ("the user knows that there are no repeated values in the index array"). The second is a
variable shared silently, usually a scratch variable declared outside the loop: that is exactly what local and default(none) are for.
forall versus do concurrent
forall (10.2.4) was added, before do concurrent, to give array assignment a parallel meaning. It was declared obsolescent in Fortran 2018
(Annex C.1: "FORALL is now specified to be obsolescent"), and B.3.13 gives the reason: the
constructs were intended to allow highly efficient parallel execution, but "experience indicates that they are too complex and have too many restrictions for
compilers to take advantage of them", they are "redundant with the DO CONCURRENT construct", and many of the manipulations for which they might be used are better done with pointers, especially with pointer rank remapping.
The two statements differ in meaning, not only in syntax:
forall |
do concurrent |
|
|---|---|---|
Body |
only assignments, pointer assignments, |
any permitted statements, including calls to pure procedures, |
Evaluation |
all right-hand sides are evaluated, then all assignments are performed, as for array assignment |
no ordering at all: the iterations are independent, so the result must not depend on it |
Many-to-one assignment |
not allowed (10.2.4.4) |
not allowed either (independence), but then not diagnosed |
Mask |
one scalar mask in the header |
one scalar mask in the header |
Locality, reductions |
none |
|
Status |
obsolescent since Fortran 2018 |
current |
program forall_vs_do_concurrent
implicit none
integer, parameter :: n = 6
integer :: a(n), b(n), i
integer, parameter :: ind(3) = [3, 1, 2]
! forall: every right-hand side is evaluated before any assignment, so this is a shift
a = [(i, i = 1, n)]
forall (i = 2:n) a(i) = a(i - 1)
print '(a,6i3)', 'forall shift: ', a
! the same effect with do concurrent needs a second array: iterations must be independent
a = [(i, i = 1, n)]
b = a
do concurrent (i = 2:n)
b(i) = a(i - 1)
end do
print '(a,6i3)', 'do concurrent shift: ', b
! many-to-one assignment: forbidden in forall; allowed in do concurrent only when indices do not repeat
a = 0
do concurrent (i = 1:3)
a(ind(i)) = i
end do
print '(a,6i3)', 'permuted store: ', a
end program forall_vs_do_concurrent
Output (identical with gfortran and with Flang; the warning comes from gfortran at compile time):
Warning: Fortran 2018 obsolescent feature: FORALL construct at (1)
05_forall.f90:9:34:
9 | forall (i = 2:n) a(i) = a(i - 1)
| 1
Warning: Fortran 2018 obsolescent feature: FORALL construct at (1)
forall shift: 1 1 2 3 4 5
do concurrent shift: 1 1 2 3 4 5
permuted store: 2 3 1 0 0 0
The forall form a(i) = a(i - 1) is well defined because the right-hand sides are evaluated first, giving a shift. The same text in
a do concurrent would be wrong (and it is the program of the previous section). The correct do concurrent form reads from one array and writes to another.
The third construct is the only kind of many-to-one-looking assignment that is legal: the subscript ind(i) is a permutation, so the
stores never collide.
The mechanical conversion of old forall code is therefore not a rename: check that the body does not rely on the "evaluate everything first"
rule, and, for a body of a single array assignment, prefer plain array syntax. See
Legacy Features and Modernization for the other obsolescent features.
What compilers do with it
The standard does not require any parallel execution: a compiler that runs a do concurrent loop as a plain sequential loop
conforms. The value of the construct is that it makes the independence known, so the compiler can vectorize without a dependence
analysis and, with the right option, parallelize or offload. The table summarizes what the vendors document; the sections after it
show what was run here.
| Compiler | Option and behavior | Checked here |
|---|---|---|
GNU Fortran 16.2 |
no special treatment of |
run (CPU) |
LLVM Flang 23 |
|
host run; device checked against the documentation, not run |
NVIDIA HPC SDK |
|
checked against the documentation, not run |
Intel |
the Intel documentation lists |
not run |
gfortran: -ftree-parallelize-loops
A compute-bound kernel: every element needs thousands of trigonometric operations, with a local scratch scalar and an inner counted loop,
followed by a reduce(+).
program compute_bound
use, intrinsic :: iso_fortran_env, only: real64, int64
implicit none
integer, parameter :: dp = real64, n = 200000, work = 2000
real(dp), allocatable :: x(:), y(:)
real(dp) :: total, v
integer(int64) :: t0, t1, rate
integer :: i, k
allocate (x(n), y(n))
x = [(0.001_dp*real(i, dp), i = 1, n)]
call system_clock(t0, rate)
do concurrent (i = 1:n) local(v, k) shared(x, y)
v = x(i)
do k = 1, work
v = sin(v) + 0.5_dp*cos(v*real(k, dp)*1.0e-3_dp)
end do
y(i) = v
end do
total = 0.0_dp
do concurrent (i = 1:n) reduce(+: total)
total = total + y(i)
end do
call system_clock(t1)
print '(a,es16.9,a,f7.3,a)', 'total=', total, ' time=', real(t1 - t0, dp)/rate, ' s'
end program compute_bound
Built and run on 10 cores (the line total= is the same in every build; the times are what changes):
gfortran -std=f2023 -fcheck=all 13_compute.f90 # strict check build: result total=1.447292643E+05, 7.8 s
gfortran -O2 13_compute.f90 # sequential: total=1.447292643E+05, 5.8 s and 7.5 s
gfortran -O2 -ftree-parallelize-loops=4 13_compute.f90 # 4 threads: total=1.447292643E+05, 2.1 s and 2.2 s
The result is the same to all printed digits, and the run time falls by a factor of about three with four threads. The auto-parallelizer
does not need do concurrent to do its job, but it needs to prove independence for an ordinary do loop, and do concurrent removes the proof obligation for
the loop nest it covers. Note that the thread count is fixed at compile time with =4.
Flang: mapping to OpenMP on the host
The same file, built with LLVM Flang:
flang -O2 13_compute.f90 # sequential: total=1.447292643E+05, 6.1 s
flang -O2 -fopenmp -fdo-concurrent-to-openmp=host 13_compute.f90 # OMP_NUM_THREADS=4: 1.8 s; OMP_NUM_THREADS=1: 5.9 s
The binary built with -fdo-concurrent-to-openmp=host calls the OpenMP runtime (nm shows kmpc/GOMP symbols), and the thread count is set at run
time through OMP_NUM_THREADS. The Flang documentation states the current limits of this conversion: only perfectly nested loops are collapsed into one
multi-dimensional OpenMP loop (a statement between two headers parallelizes only the outer loop), there is no data-dependence analysis (so the
independence promise is trusted), non-rectangular ranges such as j = i:n are not handled, local is supported and reduce support is partial (as stated for LLVM 22), and GPU
support is "basic and still in progress". It is a moving target: read the page for your Flang release.
Not every loop gets faster
The stencil of the first example, enlarged to 3000 by 3000 points and 40 steps (12_bench.f90), is limited by memory bandwidth, not by arithmetic.
On the same machine the results were the same in every build (residual= 2.503E-02, sum= 100.0000) but the times were:
gfortran -O2 0.31 s
gfortran -O2 -ftree-parallelize-loops=4 0.48 s
flang -O2 0.46 s
flang -O2 -fopenmp -fdo-concurrent-to-openmp=host (OMP_NUM_THREADS=4) 0.47 s
Parallel execution of a memory-bound loop does not pay for the thread start-up and the extra traffic; measure before you assume. The
guidance of
Performance and Memory Layout about column-major order and
cache use applies first: do concurrent (j = ..., i = ...) places the first (fastest-varying) index in the inner position of the generated loops only if the
compiler chooses to, so keep array accesses stride-one in i as in the examples.
GPU offload (checked against the documentation, not run)
There is no GPU in the environment used to prepare this page, so the following commands were not run; they are taken from the vendors' manuals, which are cited below.
NVIDIA HPC SDK. nvfortran -stdpar=gpu file.f90 compiles every do concurrent loop of the file for the GPU, -stdpar=multicore for the CPU threads. The
User’s Guide (version 26.9) states, for calls inside the loop, that PURE routines can generally be called, and that external routines cannot unless they have an OpenACC
routine directive or a CUDA device attribute. With the default managed memory (-gpu=mem:managed), globals accessed by called routines may not be updated
correctly on the CPU side, and an assumed-size array indexed through another array may give wrong updates or memory violations; with -gpu=mem:unified the
guide states there are no limitations of the kind. The compiler can be combined with OpenACC directives (-acc) for data management and with CUDA Fortran (-cuda).
The heatflow stencil of the first example is a candidate as it stands: pure arithmetic, array accesses indexed by the loop indices, no calls.
LLVM Flang. flang -fopenmp -fdo-concurrent-to-openmp=device -fopenmp-targets=<triple> --offload-arch=<arch> file.f90 converts the construct into an OpenMP target teams distribute parallel do. Trying it
on the example compiler here stops at the first requirement, because the device libraries are not installed:
flang-23: error: cannot find ROCm device library; provide its path via '--rocm-path' or '--rocm-device-lib-path', or pass '-nogpulib' to build without ROCm device library
(for -fopenmp-targets=amdgcn-amd-amdhsa --offload-arch=gfx90a). An invalid value of the option, such as -fdo-concurrent-to-openmp=bogus, is
rejected with invalid value 'bogus' in '-fdo-concurrent-to-openmp=bogus'; the accepted values are none, host and device.
Intel ifx. The options named in the table above control the offload of do concurrent through the OpenMP target runtime; the details are in the Intel Fortran Compiler documentation for your release.
In all three cases the same source is used, and the portability argument for do concurrent over directives is that the program text does not change; only the build command does.
Which loop gets offloaded, how data moves and what the memory model is are properties of the compiler and the hardware that you must test on the target.
The same idea in other languages
do concurrent is Fortran’s data-parallel loop: independent iterations declared by the programmer, scheduled by the implementation. Rust’s
Rayon expresses it with parallel iterators whose closure cannot share mutable state, which the borrow checker enforces and the Fortran compiler cannot
(see Data Parallelism with Rayon); C does it with explicit threads and
the programmer’s own partitioning (see Threads) or with OpenMP pragmas, which is the Fortran
OpenMP and OpenACC model. The difference to remember: Fortran puts the promise in the language,
Rust proves it in the type system, and C leaves it to the developer.
Where the books are outdated
| Topic | What changed |
|---|---|
|
introduced in Fortran 2008; the books that cover only that edition show the bare construct, with no way to say which variables are private |
Locality specifiers |
|
Reductions |
Fortran 2023 added |
|
presented as the parallel assignment of Fortran 90/95 era; it has been obsolescent since Fortran 2018 and the standard calls it redundant with |
Compilers |
older descriptions assume |
Standard version provenance and compiler support
| Feature | Introduced | Compiler support checked on 2026-10-19 |
|---|---|---|
|
before Fortran 2008 (edition not confirmed here) |
GNU Fortran 16.2 warns "Fortran 2018 obsolescent feature"; Flang 23 accepts silently |
|
Fortran 2008 (the GNU Fortran Fortran 2008 status page lists the header type-spec as a Fortran 2008 feature that its compiler did not then recognize; it works in 16.2) |
GNU Fortran 16.2 and LLVM Flang 23 |
|
Fortran 2018 (Annex C.1, B.3.13) |
reported by GNU Fortran 16.2 |
|
Fortran 2018 (Annex C.1) |
GNU Fortran 16.2 and LLVM Flang 23 |
|
Fortran 2023 (Introduction of J3/23-007r1) |
GNU Fortran 16.2 and LLVM Flang 23 |
Automatic parallelization / offload of |
implementation feature, not in the standard |
gfortran |
Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. See History, Standards and Compilers for the per-compiler support table.
Pitfalls
-
A carried dependence.
a(i) = a(i - 1) + 1is not allowed and not diagnosed; sequential builds hide it, parallel builds expose it. -
A shared scratch variable. A temporary declared outside the loop and assigned inside it is shared by default and is a race in a parallel run; use
local, or declare it in ablockinside the loop, and writedefault(none)so the compiler lists the forgotten ones. -
Hand-written reductions.
s = s + x(i)withoutreduce(+: s)is a violation;reducewith an assignment that is not of the allowed form is not diagnosed (see above). -
Indirect subscripts with repeated values.
a(ind(i)) = ...needsindto be a permutation (or at least injective on the iteration space). -
Impure calls and early exit. Both are compile-time errors; restructure with
pureprocedures, or use a plaindoloop. -
Expecting speed from every loop. Memory-bound stencils may run slower in parallel (0.31 s versus 0.48 s above); compute-bound kernels with more arithmetic per element can gain a factor close to the number of cores.
-
Assuming the compiler parallelizes. Without
-ftree-parallelize-loops,-fopenmp -fdo-concurrent-to-openmp=hostor-stdpar, the loop is a sequential loop (possibly vectorized). -
Floating-point reductions.
reducemay combine the partial results in any order; the last digits can change between thread counts. -
forallas a find-and-replace. The evaluation order differs (see above); convert by reading the body. -
GPU data movement. An offloaded loop moves data; globals, pointers, assumed-size arrays and external procedures are the usual trouble spots (see the NVIDIA notes above).
See also Best Practices and Style and Common Gotchas and Pitfalls.
References
-
GCC manual: Options That Control Optimization (-ftree-parallelize-loops, -floop-parallelize-all) and GNU Fortran manual: Fortran 2008 status
-
NVIDIA HPC Compilers User’s Guide: -stdpar and Stdpar Fortran
-
Intel Fortran Compiler Developer Guide and Reference: DO CONCURRENT and the OpenMP offload options