[Openmp-commits] [llvm] [openmp] [OpenMP][DeviceRTL] Fix the chunked static distribution dropping iterations (PR #216117)

via Openmp-commits openmp-commits at lists.llvm.org
Thu Aug 13 09:54:10 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-offload

Author: Spencer Bryngelson (sbryngelson)

<details>
<summary>Changes</summary>

`StaticLoopChunker` drops loop iterations whenever a non-zero thread chunk is used, so a chunked static distribution silently computes the wrong answer.

Two things are wrong. `NormalizedLoopNestChunked` starts a thread at `BId * BlockChunk + TId`, which does not account for the chunk, so with `ThreadChunk = C` the threads start one index apart and then each run `C` consecutive iterations: they overlap at the front and leave the rest of the space unvisited. And `DistributeFor` defaults `BlockChunk` to `NumThreads`, which cannot hold one chunk of `C` iterations for each of `NumThreads` threads, so every thread past the block chunk gets nothing at all.

The path is only reachable with a non-zero thread chunk. clang lowers worksharing loops to `__kmpc_for_static_init_4` and never reaches these entries, and flang passes a hardcoded `0` for both chunks, so nothing in tree exercises it today and no in-tree behaviour changes. It becomes reachable as soon as a frontend forwards a `schedule(static, C)` chunk, which is what #<!-- -->214303 needs.

### Testing

New `offload/test/offloading/distribute_for_thread_chunk.c` records which thread ran each iteration for `NumIters=32`, `NumThreads=8`, `ThreadChunk=4`, and checks both that no iteration is skipped and that the mapping is the one `schedule(static,4)` prescribes. It calls the entry directly for the same reason as `distribute_for_no_loop_num_threads.c`: no C or C++ construct reaches it.

On gfx90a, thread recorded per iteration:

| | result |
|---|---|
| expected | `0 0 0 0 1 1 1 1 2 2 2 2 ... 7 7 7 7` |
| before | 17 of 32 iterations unwritten, 10 misplaced |
| after | 0 unwritten, 0 misplaced, mapping exactly as expected |

The default path is unchanged: with `ThreadChunk = 0` the mapping is identical before and after, and `distribute_for_no_loop_num_threads.c` still reports `unwritten: 0`.

`__kmpc_for_static_loop` reaches the same helper with `BlockChunk` hardcoded to `0`, which makes `KernelIteration` zero so the induction variable never advances. That entry is left alone here; it is a separate defect and no frontend passes it a chunk either.

The analysis, the reduced test case and this fix were produced with Claude; I reviewed and verified them.


---
Full diff: https://github.com/llvm/llvm-project/pull/216117.diff


2 Files Affected:

- (added) offload/test/offloading/distribute_for_thread_chunk.c (+75) 
- (modified) openmp/device/src/Workshare.cpp (+7-5) 


``````````diff
diff --git a/offload/test/offloading/distribute_for_thread_chunk.c b/offload/test/offloading/distribute_for_thread_chunk.c
new file mode 100644
index 0000000000000..bd3e8afcde31f
--- /dev/null
+++ b/offload/test/offloading/distribute_for_thread_chunk.c
@@ -0,0 +1,75 @@
+// Chunked static distribution: thread T owns iterations [T*chunk, T*chunk+chunk)
+// within each block chunk. The index used to start each thread has to account
+// for the chunk, and the block chunk has to cover one chunk per thread.
+//
+// The entry is called directly because no C/C++ construct reaches it: clang
+// emits __kmpc_for_static_init_4, only flang emits this one.
+//
+// RUN: %libomptarget-compile-run-and-check-generic
+// REQUIRES: gpu
+
+#include <omp.h>
+#include <stdio.h>
+
+#define N 32
+#define NT 8
+#define CHUNK 4
+
+struct Args {
+  int *Tid;
+};
+
+#pragma omp begin declare target
+extern void __kmpc_distribute_for_static_loop_4u(
+    void *Loc, void (*Fn)(unsigned, void *), void *Arg, unsigned NumIters,
+    unsigned NumThreads, unsigned BlockChunk, unsigned ThreadChunk,
+    unsigned char OneIterationPerThread);
+
+__attribute__((noinline)) static void body(unsigned I, void *A) {
+  ((struct Args *)A)->Tid[I] = omp_get_thread_num();
+}
+#pragma omp end declare target
+
+// For the host fallback copy only; the device uses the runtime's definition.
+#ifndef __AMDGCN__
+void __kmpc_distribute_for_static_loop_4u(
+    void *Loc, void (*Fn)(unsigned, void *), void *Arg, unsigned NumIters,
+    unsigned NumThreads, unsigned BlockChunk, unsigned ThreadChunk,
+    unsigned char OneIterationPerThread) {
+  for (unsigned I = 0; I < NumIters; ++I)
+    Fn(I, Arg);
+}
+#endif
+
+int main(void) {
+  static int Tid[N];
+  for (int I = 0; I < N; ++I)
+    Tid[I] = -1;
+
+#pragma omp target teams map(tofrom : Tid[0 : N]) num_teams(1) thread_limit(NT)
+  {
+#pragma omp parallel num_threads(NT)
+    {
+      struct Args A;
+      A.Tid = Tid;
+      __kmpc_distribute_for_static_loop_4u(0, body, &A, N, NT,
+                                           /*BlockChunk=*/0,
+                                           /*ThreadChunk=*/CHUNK,
+                                           /*OneIterationPerThread=*/0);
+    }
+  }
+
+  int Unwritten = 0, Misplaced = 0;
+  for (int I = 0; I < N; ++I) {
+    if (Tid[I] == -1)
+      ++Unwritten;
+    else if (Tid[I] != (I / CHUNK) % NT)
+      ++Misplaced;
+  }
+
+  // CHECK: unwritten: 0
+  // CHECK: misplaced: 0
+  printf("unwritten: %d\n", Unwritten);
+  printf("misplaced: %d\n", Misplaced);
+  return 0;
+}
diff --git a/openmp/device/src/Workshare.cpp b/openmp/device/src/Workshare.cpp
index a16ef93707989..fdb64c01eb41f 100644
--- a/openmp/device/src/Workshare.cpp
+++ b/openmp/device/src/Workshare.cpp
@@ -732,7 +732,7 @@ template <typename Ty> class StaticLoopChunker {
     Ty KernelIteration = NumBlocks * BlockChunk;
 
     // Start index in the chunked space.
-    Ty IV = BId * BlockChunk + TId;
+    Ty IV = BId * BlockChunk + TId * ThreadChunk;
     ASSERT(IV >= 0, "Bad index");
 
     // Cover the entire iteration space, assumptions in the caller might allow
@@ -900,14 +900,16 @@ template <typename Ty> class StaticLoopChunker {
     if (OneIterationPerThread)
       NumThreads = static_cast<Ty>(mapping::getMaxTeamThreads());
 
-    // If the block chunk is not specified we pick a default now.
-    if (BlockChunk == 0)
-      BlockChunk = NumThreads;
-
     // If the thread chunk is not specified we pick a default now.
     if (ThreadChunk == 0)
       ThreadChunk = 1;
 
+    // If the block chunk is not specified we pick a default now. It has to
+    // cover one chunk for every thread, otherwise the threads past the block
+    // chunk get no iterations at all.
+    if (BlockChunk == 0)
+      BlockChunk = NumThreads * ThreadChunk;
+
     // If we know we have more threads (across all blocks) than iterations we
     // can indicate that to avoid an outer loop.
     if (OneIterationPerThread)

``````````

</details>


https://github.com/llvm/llvm-project/pull/216117


More information about the Openmp-commits mailing list