[llvm] [Offload] Add OFFLOAD_FORCE_SYNC_OPS force-synchronization escape hatch (PR #222635)

Jan Patrick Lehr via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 10 05:58:34 PDT 2026


https://github.com/jplehr created https://github.com/llvm/llvm-project/pull/222635

- Add OF_ForceSyncOps BoolEnvar and forceSyncOps() on GenericDeviceTy with actual spelling: OFFLOAD_FORCE_SYNC_OPS
- Add shouldForceSync predicate and drain external async info objects in AsyncInfoWrapperTy::finalize() when the flag is set

This is a more general adaptation of the downstream OMPX_FORCE_SYNC_REGIONS that has been proven helpful in the past.

Assisted-by: Claude Code

>From 0ced5d21776256585139530f6ebd93249c49367e Mon Sep 17 00:00:00 2001
From: JP Lehr <JanPatrick.Lehr at amd.com>
Date: Fri, 21 Aug 2026 05:40:31 -0500
Subject: [PATCH] [Offload] Add OFFLOAD_FORCE_SYNC_OPS force-synchronization
 escape hatch

- Add OF_ForceSyncOps BoolEnvar and forceSyncOps() on GenericDeviceTy
  with actual spelling: OFFLOAD_FORCE_SYNC_OPS
- Add shouldForceSync predicate and drain external async info objects in
  AsyncInfoWrapperTy::finalize() when the flag is set

Assisted-by: Claude Code
---
 .../common/include/PluginInterface.h             | 16 ++++++++++++++++
 .../common/src/PluginInterface.cpp               |  7 +++++++
 offload/test/lit.cfg                             |  4 ++++
 offload/test/unit/lit.cfg.py                     |  4 ++++
 offload/unittests/OffloadAPI/common/Fixtures.hpp | 11 +++++++++++
 .../unittests/OffloadAPI/memory/olMemFill.cpp    |  3 +++
 .../OffloadAPI/queue/olDestroyQueue.cpp          |  1 +
 .../OffloadAPI/queue/olLaunchHostFunction.cpp    |  1 +
 8 files changed, 47 insertions(+)

diff --git a/offload/plugins-nextgen/common/include/PluginInterface.h b/offload/plugins-nextgen/common/include/PluginInterface.h
index 29513661867b1..39a834af07edc 100644
--- a/offload/plugins-nextgen/common/include/PluginInterface.h
+++ b/offload/plugins-nextgen/common/include/PluginInterface.h
@@ -103,6 +103,15 @@ template <typename... ArgsTy>
                                     ArgsTy... Args);
 } // namespace Plugin
 
+/// Decide whether an operation must be synchronized eagerly because the
+/// force-synchronization escape hatch (OFFLOAD_FORCE_SYNC_OPS) is enabled. Only
+/// external async info objects are affected; local ones are always synchronized
+/// on finalization. A pending error suppresses synchronization.
+inline bool shouldForceSync(bool ForceSyncOps, bool IsLocalAsyncInfo,
+                            bool HasQueue, bool HasError) {
+  return ForceSyncOps && !IsLocalAsyncInfo && HasQueue && !HasError;
+}
+
 /// Class that wraps the __tgt_async_info to simply its usage. In case the
 /// object is constructed without a valid __tgt_async_info, the object will use
 /// an internal one and will synchronize the current thread with the pending
@@ -1399,6 +1408,13 @@ struct GenericDeviceTy : public DeviceAllocatorTy {
   BoolEnvar OMPX_TrackAllocationTraces =
       BoolEnvar("OFFLOAD_TRACK_ALLOCATION_TRACES", false);
 
+  /// Environment flag that forces every device operation to be synchronized,
+  /// draining the queue after each operation. Debugging escape hatch.
+  BoolEnvar OF_ForceSyncOps = BoolEnvar("OFFLOAD_FORCE_SYNC_OPS", false);
+
+  /// Return whether all device operations should be forced synchronous.
+  bool forceSyncOps() const { return OF_ForceSyncOps; }
+
   /// Array of images loaded into the device. Images are automatically
   /// deallocated by the allocator.
   llvm::SmallVector<DeviceImageTy *> LoadedImages;
diff --git a/offload/plugins-nextgen/common/src/PluginInterface.cpp b/offload/plugins-nextgen/common/src/PluginInterface.cpp
index 573369dc3ba57..322bfc5184dca 100644
--- a/offload/plugins-nextgen/common/src/PluginInterface.cpp
+++ b/offload/plugins-nextgen/common/src/PluginInterface.cpp
@@ -66,6 +66,13 @@ void AsyncInfoWrapperTy::finalize(Error &Err) {
   if (AsyncInfoPtr == &LocalAsyncInfo && LocalAsyncInfo.Queue && !Err)
     Err = Device.synchronize(&LocalAsyncInfo);
 
+  // With the force-synchronization escape hatch enabled, also drain external
+  // async info objects after each operation.
+  else if (shouldForceSync(Device.forceSyncOps(),
+                           AsyncInfoPtr == &LocalAsyncInfo,
+                           AsyncInfoPtr->Queue != nullptr, (bool)Err))
+    Err = Device.synchronize(AsyncInfoPtr, /*ReleaseQueue=*/false);
+
   // Invalidate the wrapper object.
   AsyncInfoPtr = nullptr;
 }
diff --git a/offload/test/lit.cfg b/offload/test/lit.cfg
index 37255f14ec124..ce389fc3e34e2 100644
--- a/offload/test/lit.cfg
+++ b/offload/test/lit.cfg
@@ -39,6 +39,10 @@ if 'OMP_TARGET_OFFLOAD' in os.environ:
 if 'HSA_ENABLE_SDMA' in os.environ:
     config.environment['HSA_ENABLE_SDMA'] = os.environ['HSA_ENABLE_SDMA']
 
+# Allow running the whole suite with all device operations forced synchronous.
+if 'OFFLOAD_FORCE_SYNC_OPS' in os.environ:
+    config.environment['OFFLOAD_FORCE_SYNC_OPS'] = os.environ['OFFLOAD_FORCE_SYNC_OPS']
+
 # Architectures like gfx942 may or may not be APUs so an additional environment
 # variable is required as some tests can be APU specific.
 config.environment['IS_APU'] = os.environ.get('IS_APU', '0')
diff --git a/offload/test/unit/lit.cfg.py b/offload/test/unit/lit.cfg.py
index 39f159999c3d8..67020c5a3f8d8 100644
--- a/offload/test/unit/lit.cfg.py
+++ b/offload/test/unit/lit.cfg.py
@@ -38,6 +38,10 @@ def prepend_executable_path(path):
 if config.operating_system == "Windows" and config.library_dir:
     prepend_executable_path(config.library_dir)
 
+# Allow running the whole suite with all device operations forced synchronous.
+if "OFFLOAD_FORCE_SYNC_OPS" in os.environ:
+    config.environment["OFFLOAD_FORCE_SYNC_OPS"] = os.environ["OFFLOAD_FORCE_SYNC_OPS"]
+
 # test_source_root: The root path where tests are located.
 # test_exec_root: The root path where tests should be run.
 config.test_exec_root = config.unittest_dir
diff --git a/offload/unittests/OffloadAPI/common/Fixtures.hpp b/offload/unittests/OffloadAPI/common/Fixtures.hpp
index a05be01648ebc..2e6c3be9ec307 100644
--- a/offload/unittests/OffloadAPI/common/Fixtures.hpp
+++ b/offload/unittests/OffloadAPI/common/Fixtures.hpp
@@ -14,6 +14,7 @@
 #include <thread>
 
 #include "Environment.hpp"
+#include "Shared/EnvironmentVar.h"
 
 #pragma once
 
@@ -142,6 +143,16 @@ template <typename Fn> inline void threadify(Fn body) {
   }
 }
 
+/// Skip the current test when OFFLOAD_FORCE_SYNC_OPS is enabled. Tests using
+/// ManuallyTriggeredTask enqueue a host task that blocks until a later
+/// `trigger`; forcing operations synchronous makes the enqueue wait for that
+/// task inline, so it can never be triggered and times out.
+#define SKIP_IF_FORCE_SYNC_OPS()                                               \
+  do {                                                                         \
+    if (BoolEnvar("OFFLOAD_FORCE_SYNC_OPS", false))                            \
+      GTEST_SKIP() << "incompatible with OFFLOAD_FORCE_SYNC_OPS";              \
+  } while (0)
+
 /// Enqueues a task to the queue that can be manually resolved.
 // It will block until `trigger` is called.
 struct ManuallyTriggeredTask {
diff --git a/offload/unittests/OffloadAPI/memory/olMemFill.cpp b/offload/unittests/OffloadAPI/memory/olMemFill.cpp
index 467a551c48c94..e15a3c6c79a60 100644
--- a/offload/unittests/OffloadAPI/memory/olMemFill.cpp
+++ b/offload/unittests/OffloadAPI/memory/olMemFill.cpp
@@ -23,6 +23,7 @@ struct olMemFillTest : OffloadQueueTest {
     // Block/enqueue tests ensure that the test has been enqueued to a queue
     // (rather than being done synchronously if the queue happens to be empty)
     if constexpr (Block) {
+      SKIP_IF_FORCE_SYNC_OPS();
       ASSERT_SUCCESS(Manual.enqueue(Queue));
     }
 
@@ -103,6 +104,7 @@ TEST_P(olMemFillTest, SuccessLargeEnqueue) {
   constexpr size_t Size = 1024;
   void *Alloc;
   ManuallyTriggeredTask Manual;
+  SKIP_IF_FORCE_SYNC_OPS();
   ASSERT_SUCCESS(Manual.enqueue(Queue));
 
   ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, Size, &Alloc));
@@ -157,6 +159,7 @@ TEST_P(olMemFillTest, SuccessLargeByteAlignedEnqueue) {
   constexpr size_t Size = 17 * 64;
   void *Alloc;
   ManuallyTriggeredTask Manual;
+  SKIP_IF_FORCE_SYNC_OPS();
   ASSERT_SUCCESS(Manual.enqueue(Queue));
 
   ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, Size, &Alloc));
diff --git a/offload/unittests/OffloadAPI/queue/olDestroyQueue.cpp b/offload/unittests/OffloadAPI/queue/olDestroyQueue.cpp
index aa9e372ede2c8..beaa4ccf91e59 100644
--- a/offload/unittests/OffloadAPI/queue/olDestroyQueue.cpp
+++ b/offload/unittests/OffloadAPI/queue/olDestroyQueue.cpp
@@ -20,6 +20,7 @@ TEST_P(olDestroyQueueTest, Success) {
 
 TEST_P(olDestroyQueueTest, SuccessDelayedResolution) {
   ManuallyTriggeredTask Manual;
+  SKIP_IF_FORCE_SYNC_OPS();
   ASSERT_SUCCESS(Manual.enqueue(Queue));
   ASSERT_SUCCESS(olDestroyQueue(Queue));
   Queue = nullptr;
diff --git a/offload/unittests/OffloadAPI/queue/olLaunchHostFunction.cpp b/offload/unittests/OffloadAPI/queue/olLaunchHostFunction.cpp
index 1dedf7ecaaefd..d9802947378d7 100644
--- a/offload/unittests/OffloadAPI/queue/olLaunchHostFunction.cpp
+++ b/offload/unittests/OffloadAPI/queue/olLaunchHostFunction.cpp
@@ -47,6 +47,7 @@ TEST_P(olLaunchHostFunctionTest, SuccessSequence) {
 
 TEST_P(olLaunchHostFunctionKernelTest, SuccessBlocking) {
   SKIP_KNOWN_FAILURE(LevelZero{"driver issue"});
+  SKIP_IF_FORCE_SYNC_OPS();
 
   // Verify that a host kernel can block execution - A host task is created that
   // only resolves when Block is set to false.



More information about the llvm-commits mailing list