[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