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 return, no exit, no outward branch

return (C1140), a branch to a target outside the construct (C1142) and an exit that belongs to the construct or to an outer construct (C1172) are not allowed; cycle is allowed and ends only the current iteration

11.1.7.5, 11.1.12, 11.2.1

No image control statements

sync all, sync memory, critical, lock and the other image control statements are not allowed (C1141)

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 advance=

an input/output statement with an advance= specifier is not allowed; records written by several iterations appear in a processor-dependent order, and one iteration shall not read or write a record another iteration wrote

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)

local(a, b)

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 allocatable, intent(in) or optional variable, no finalizable type, no allocatable ultimate component, no coarray, no assumed-size array (C1130)

local_init(a, b)

like local, but each iteration starts with the value (or pointer association) the outside variable had; the outside variable shall be defined

same as local (C1130)

shared(a, b)

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)

reduce(op: s)

a combiner variable, see reduce: a Fortran 2023 specifier

Fortran 2023 (C1131, C1132)

default(none)

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: 0 for +, ieor, ior; 1 for *; .true. for .and. and .eqv.; .false. for .or. and .neqv.; all bits set for iand; the least representable value for max and the largest for min;

  • the variable can appear in the block only in an intrinsic assignment of the form v = v op expr, v = expr op v or v = 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, where and nested forall

any permitted statements, including calls to pure procedures, if, select case, block, allocate of local variables

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

local, local_init, shared, default(none), reduce

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 do concurrent for threads; the generic loop auto-parallelizer is enabled with -ftree-parallelize-loops=n ("the number of threads is fixed at compile time"; with no =n the count comes from OMP_NUM_THREADS, and the option implies -pthread). -floop-parallelize-all uses Graphite dependence analysis

run (CPU)

LLVM Flang 23

-fopenmp -fdo-concurrent-to-openmp=host maps the construct to omp parallel do; =device to omp target teams distribute parallel do (also needs -fopenmp-targets and --offload-arch); =none leaves it sequential. The Flang manual lists constraints, below

host run; device checked against the documentation, not run

NVIDIA HPC SDK nvfortran

-stdpar (default -stdpar=gpu) offloads do concurrent to NVIDIA GPUs, -stdpar=multicore uses CPU threads; documented in the HPC Compilers User’s Guide 26.9

checked against the documentation, not run

Intel ifx

the Intel documentation lists -fopenmp-target-do-concurrent (Windows /Qopenmp-target-do-concurrent) and -fopenmp-do-concurrent-maptype-modifier among its OpenMP and offload options; option details were not re-verified here, read the page for your release

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

do concurrent

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

local, local_init, shared and default(none) came with Fortran 2018, and the compilers have implemented them (both used here accept all four)

Reductions

Fortran 2023 added reduce; before it the portable options were a hand-written reduction on an array of partial results, or OpenMP

forall

presented as the parallel assignment of Fortran 90/95 era; it has been obsolescent since Fortran 2018 and the standard calls it redundant with do concurrent

Compilers

older descriptions assume do concurrent is only a hint; compilers now map it to threads (-ftree-parallelize-loops, Flang’s -fdo-concurrent-to-openmp) and to GPUs (nvfortran -stdpar)

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

forall construct and statement

before Fortran 2008 (edition not confirmed here)

GNU Fortran 16.2 warns "Fortran 2018 obsolescent feature"; Flang 23 accepts silently

do concurrent with index list, mask and integer-type-spec

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

forall obsolescent

Fortran 2018 (Annex C.1, B.3.13)

reported by GNU Fortran 16.2

local, local_init, shared, default(none)

Fortran 2018 (Annex C.1)

GNU Fortran 16.2 and LLVM Flang 23

reduce(op: v)

Fortran 2023 (Introduction of J3/23-007r1)

GNU Fortran 16.2 and LLVM Flang 23

Automatic parallelization / offload of do concurrent

implementation feature, not in the standard

gfortran -ftree-parallelize-loops and Flang host run; Flang device, nvfortran -stdpar and ifx checked against the documentation, not run

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) + 1 is 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 a block inside the loop, and write default(none) so the compiler lists the forgotten ones.

  • Hand-written reductions. s = s + x(i) without reduce(+: s) is a violation; reduce with an assignment that is not of the allowed form is not diagnosed (see above).

  • Indirect subscripts with repeated values. a(ind(i)) = ... needs ind to be a permutation (or at least injective on the iteration space).

  • Impure calls and early exit. Both are compile-time errors; restructure with pure procedures, or use a plain do loop.

  • 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=host or -stdpar, the loop is a sequential loop (possibly vectorized).

  • Floating-point reductions. reduce may combine the partial results in any order; the last digits can change between thread counts.

  • forall as 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).