Files
TheRock/compiler/amd-llvm/flang/docs/DoConcurrentConversionToOpenMP-atd.md
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

11 KiB

DO CONCURENT mapping to OpenMP

---
local:
---

This document seeks to describe the effort to parallelize do concurrent loops by mapping them to OpenMP worksharing constructs. The goals of this document are:

  • Describing how to instruct flang to map DO CONCURENT loops to OpenMP constructs.
  • Tracking the current status of such mapping.
  • Describing the limitations of the current implmenentation.
  • Describing next steps.

Usage

In order to enable do concurrent to OpenMP mapping, flang adds a new compiler flag: -fdo-concurrent-parallel. This flags has 3 possible values:

  1. host: this maps do concurent loops to run in parallel on the host CPU. This maps such loops to the equivalent of omp parallel do.
  2. device: this maps do concurent loops to run in parallel on a device (GPU). This maps such loops to the equivalent of omp target teams distribute parallel do.
  3. none: this disables do concurrent mapping altogether. In such case, such loops are emitted as sequential loops.

The above compiler switch is currently avaialble only when OpenMP is also enabled. So you need to provide the following options to flang in order to enable it:

flang ... -fopenmp -fdo-concurrent-parallel=[host|device|none] ...

Current status

Under the hood, do concurrent mapping is implemented in the DoConcurrentConversionPass. This is still an experimental pass which means that:

  • It has been tested in a very limited way so far.
  • It has been tested on simple synthetic inputs.

To describe current status in more detail, following is a description of how the pass currently behaves for single-range loops and then for multi-range loops.

Single-range loops

Given the following loop:

  do concurrent(i=1:n)
    a(i) = i * i
  end do

Mapping to host

Mapping this loop to the host, generates MLIR operations of the following structure:

%4 = fir.address_of(@_QFEa) ...
%6:2 = hlfir.declare %4 ...

omp.parallel {
  // Allocate private copy for `i`.
  %19 = fir.alloca i32 {bindc_name = "i"}
  %20:2 = hlfir.declare %19 {uniq_name = "_QFEi"} ...

  omp.wsloop {
    omp.loop_nest (%arg0) : index = (%21) to (%22) inclusive step (%c1_2) {
      %23 = fir.convert %arg0 : (index) -> i32
      // Use the privatized version of `i`.
      fir.store %23 to %20#1 : !fir.ref<i32>
      ...

      // Use "shared" SSA value of `a`.
      %42 = hlfir.designate %6#0
      hlfir.assign %35 to %42
      ...
      omp.yield
    }
    omp.terminator
  }
  omp.terminator
}

Mapping to device

Mapping the same loop to the device, generates MLIR operations of the following structure:

// Map `a` to the `target` region.
%29 = omp.map.info ... {name = "_QFEa"}
omp.target ... map_entries(..., %29 -> %arg4 ...) {
  ...
  %51:2 = hlfir.declare %arg4
  ...
  omp.teams {
    // Allocate private copy for `i`.
    %52 = fir.alloca i32 {bindc_name = "i"}
    %53:2 = hlfir.declare %52
    ...

    omp.distribute {
      omp.parallel {
        omp.wsloop {
          omp.loop_nest (%arg5) : index = (%54) to (%55) inclusive step (%c1_9) {
            // Use the privatized version of `i`.
            %56 = fir.convert %arg5 : (index) -> i32
            fir.store %56 to %53#1
            ...
            // Use the mapped version of `a`.
            ... = hlfir.designate %51#0
            ...
          }
          omp.terminator
        }
        omp.terminator
      }
      omp.terminator
    }
    omp.terminator
  }
  omp.terminator
}

Multi-range loops

The pass currently supports multi-range loops as well. Given the following example:

   do concurrent(i=1:n, j=1:m)
       a(i,j) = i * j
   end do

The generated omp.loop_nest operation look like:

omp.loop_nest (%arg0, %arg1)
    : index = (%17, %19) to (%18, %20)
    inclusive step (%c1_2, %c1_4) {
  fir.store %arg0 to %private_i#1 : !fir.ref<i32>
  fir.store %arg1 to %private_j#1 : !fir.ref<i32>
  ...
  omp.yield
}

It is worth noting that we have privatized versions for both iteration variables: i and j. These are locally allocated inside the parallel/target OpenMP region similar to what the single-range example in previous section shows.

Multi-range and perfectly-nested loops

Currently, on the FIR dialect level, the following 2 loops are modelled in exactly the same way:

do concurrent(i=1:n, j=1:m)
  a(i,j) = i * j
end do
do concurrent(i=1:n)
  do concurrent(j=1:m)
    a(i,j) = i * j
  end do
end do

Both of the above loops are modelled as:

fir.do_loop %arg0 = %11 to %12 step %c1 unordered {
  ...
  fir.do_loop %arg1 = %14 to %15 step %c1_1 unordered {
    ...
  }
}

Consequently, from the DoConcurrentConversionPass' perspective, both loops are treated in the same manner. Under the hood, the pass detects perfectly-nested loop nests and maps such nests as if they were multi-range loops.

Non-perfectly-nested loops

One limitation that the pass currently have is that it treats any intervening code in a loop nest as being disruptive to detecting that nest as a single unit. For example, given the following input:

do concurrent(i=1:n)
  x = 41
  do concurrent(j=1:m)
    a(i,j) = i * j
  end do
end do

Since there at least one statement between the 2 loop header (i.e. x = 41), the pass does not detect the i and j loops as a nest. Rather, the pass in that case only maps the i loop to OpenMP and leaves the j loop in its origianl form. In theory, in this example, we can sink the intervening code into the j loop and detect the complete nest. However, such transformation is still to be implemented in the future.

The above also has the consequence that the j variable will not be privatized in the OpenMP parallel/target region. In other words, it will be treated as if it was a shared variable. For more details about privatization, see the "Data environment" section below.

Data environment

By default, variables that are used inside a do concurernt loop nest are either treated as shared in case of mapping to host, or mapped into the target region using a map clause in case of mapping to device. The only exceptions to this are:

  1. the loop's iteration variable(s) (IV) of perfect loop nests. In that case, for each IV, we allocate a local copy as shown the by the mapping examples above.
  2. any values that are from allocations outside the loop nest and used exclusively inside of it. In such cases, a local privatized value is created in the OpenMP region to prevent multiple teams of threads from accessing and destroying the same memory block which causes runtime issues. For an example of such cases, see flang/test/Transforms/DoConcurrent/locally_destroyed_temp.f90.

Non-perfectly-nested loops' IVs

For non-perfectly-nested loops, the IVs are still treated as shared or map entries as pointed out above. This might not be consistent with what the Fortran specficiation tells us. In particular, taking the following snippets from the spec (version 2023) into account:

§ 3.35

construct entity entity whose identifier has the scope of a construct

§ 19.4

A variable that appears as an index-name in a FORALL or DO CONCURRENT construct, or ... is a construct entity. A variable that has LOCAL or LOCAL_INIT locality in a DO CONCURRENT construct is a construct entity. ... The name of a variable that appears as an index-name in a DO CONCURRENT construct, FORALL statement, or FORALL construct has a scope of the statement or construct. A variable that has LOCAL or LOCAL_INIT locality in a DO CONCURRENT construct has the scope of that construct.

From the above quotes, it seems there is an equivalence between the IV of a do concurrent loop and a variable with a LOCAL locality specifier (equivalent to OpenMP's private clause). Which means that we should probably localize/privatize a do concurernt loop's IV even if it is not perfectly nested in the nest we are parallelizing. For now, however, we do not do that as pointed out previously. In the near future, we propose a middle-ground solution (see the Next steps section for more details).

Next steps

Delayed privatization

So far, we emit the privatization logic for IVs inline in the parallel/target region. This is enough for our purposes right now since we don't localize/privatize any sophisticated types of variables yet. Once we have need for more advanced localization through do concurrent's locality specifiers (see below), delayed privatization will enable us to have a much cleaner IR. Once delayed privatization's implementation upstream is supported for the required constructs by the pass, we will move to it rather than inlined/early privatization.

Locality specifiers for do concurrent

Locality specifiers will enable the user to control the data environment of the loop nest in a more fine-grained way. Implementing these specifiers on the FIR dialect level is needed in order to support this in the DoConcurrentConversionPass.

Such specified will also unlock a potential solution to the non-perfectly-nested loops' IVs issue described above. In particular, for a non-perfectly nested loop, one middle-ground proposal/solution would be to:

  • Emit the loop's IV as shared/mapped just like we do currently.
  • Emit a warning that the IV of the loop is emitted as shared/mapped.
  • Given support for LOCAL, we can recommend the user to explicitly localize/privatize the loop's IV if they choose to.

More advanced detection of loop nests

As pointed out earlier, any intervening code between the headers of 2 nested do concurrent loops prevents us currently from detecting this as a loop nest. In some cases this is overly conservative. Therefore, a more flexible detection logic of loop nests needs to be implemented.

Data-dependence analysis

Right now, we map loop nests without analysing whether such mapping is safe to do or not. We probalby need to at least warn the use of unsafe loop nests due to loop-carried dependencies.

Non-rectangular loop nests

So far, we did not need to use the pass for non-rectangular loop nests. For example:

do concurrent(i=1:n)
  do concurrent(j=i:n)
    ...
  end do
end do

We defer this to the (hopefully) near future when we get the conversion in a good share for the samples/projects at hand.