[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 24 00:57:26 PDT 2026


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

>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 1/2] [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 29513661867b14..39a834af07edc3 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 573369dc3ba577..322bfc5184dca7 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 37255f14ec1245..ce389fc3e34e27 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 39f159999c3d89..67020c5a3f8d8b 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 a05be01648ebc8..2e6c3be9ec3071 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 467a551c48c94c..e15a3c6c79a603 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 aa9e372ede2c87..beaa4ccf91e594 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 1dedf7ecaaefdb..d9802947378d70 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.

>From 2a5331b09973ea37d4e8055163783cf0b247d431 Mon Sep 17 00:00:00 2001
From: JP Lehr <JanPatrick.Lehr at amd.com>
Date: Thu, 24 Sep 2026 02:55:03 -0500
Subject: [PATCH 2/2] Review: Update env var name

---
 offload/plugins-nextgen/common/include/PluginInterface.h | 2 +-
 offload/test/lit.cfg                                     | 4 ++--
 offload/test/unit/lit.cfg.py                             | 4 ++++
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/offload/plugins-nextgen/common/include/PluginInterface.h b/offload/plugins-nextgen/common/include/PluginInterface.h
index 39a834af07edc3..594da1627a122f 100644
--- a/offload/plugins-nextgen/common/include/PluginInterface.h
+++ b/offload/plugins-nextgen/common/include/PluginInterface.h
@@ -1410,7 +1410,7 @@ struct GenericDeviceTy : public DeviceAllocatorTy {
 
   /// 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);
+  BoolEnvar OF_ForceSyncOps = BoolEnvar("OFFLOAD_FORCE_BLOCKING", false);
 
   /// Return whether all device operations should be forced synchronous.
   bool forceSyncOps() const { return OF_ForceSyncOps; }
diff --git a/offload/test/lit.cfg b/offload/test/lit.cfg
index ce389fc3e34e27..f042b60d2f1a0c 100644
--- a/offload/test/lit.cfg
+++ b/offload/test/lit.cfg
@@ -40,8 +40,8 @@ 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']
+if 'OFFLOAD_FORCE_BLOCKING' in os.environ:
+    config.environment['OFFLOAD_FORCE_BLOCKING'] = os.environ['OFFLOAD_FORCE_BLOCKING']
 
 # Architectures like gfx942 may or may not be APUs so an additional environment
 # variable is required as some tests can be APU specific.
diff --git a/offload/test/unit/lit.cfg.py b/offload/test/unit/lit.cfg.py
index 67020c5a3f8d8b..00a815ed51ed4a 100644
--- a/offload/test/unit/lit.cfg.py
+++ b/offload/test/unit/lit.cfg.py
@@ -42,6 +42,10 @@ def prepend_executable_path(path):
 if "OFFLOAD_FORCE_SYNC_OPS" in os.environ:
     config.environment["OFFLOAD_FORCE_SYNC_OPS"] = os.environ["OFFLOAD_FORCE_SYNC_OPS"]
 
+# Allow running the whole suite with all device operations forced synchronous.
+if "OFFLOAD_FORCE_BLOCKING" in os.environ:
+    config.environment["OFFLOAD_FORCE_BLOCKING"] = os.environ["OFFLOAD_FORCE_BLOCKING"]
+
 # 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



More information about the llvm-commits mailing list