[llvm] [libsycl] Integrate context to async handlers. (PR #224050)
Kseniya Tikhomirova via llvm-commits
llvm-commits at lists.llvm.org
Mon Sep 21 04:56:05 PDT 2026
https://github.com/KseniyaTikhomirova updated https://github.com/llvm/llvm-project/pull/224050
>From a7eac1a16aa4c3bc3d4ff4b6ee771d218761b2a9 Mon Sep 17 00:00:00 2001
From: "Tikhomirova, Kseniya" <kseniya.tikhomirova at intel.com>
Date: Tue, 15 Sep 2026 05:02:27 -0700
Subject: [PATCH 1/2] [libsycl] Integrate context to async handlers
Signed-off-by: Tikhomirova, Kseniya <kseniya.tikhomirova at intel.com>
---
libsycl/include/sycl/__impl/queue.hpp | 16 ++-
libsycl/src/detail/global_objects.cpp | 26 ++++-
libsycl/src/detail/global_objects.hpp | 19 ++--
libsycl/src/detail/queue_impl.hpp | 7 +-
libsycl/src/queue.cpp | 6 ++
libsycl/unittests/event/async_handler.cpp | 120 +++++++++++++++++++++-
6 files changed, 174 insertions(+), 20 deletions(-)
diff --git a/libsycl/include/sycl/__impl/queue.hpp b/libsycl/include/sycl/__impl/queue.hpp
index ef4b6c59fd8cb..32f1872a6750e 100644
--- a/libsycl/include/sycl/__impl/queue.hpp
+++ b/libsycl/include/sycl/__impl/queue.hpp
@@ -117,8 +117,8 @@ class _LIBSYCL_EXPORT queue : private detail::KernelSubmissionBase<queue> {
///
/// \param propList is a list of properties for queue construction.
explicit queue(const property_list &propList = {})
- : queue(detail::SelectDevice(default_selector_v),
- detail::defaultAsyncHandler, propList) {}
+ : queue(detail::SelectDevice(default_selector_v), async_handler{},
+ propList) {}
/// Constructs a SYCL queue instance with an async_handler using the device
/// returned by an instance of default_selector.
@@ -140,8 +140,8 @@ class _LIBSYCL_EXPORT queue : private detail::KernelSubmissionBase<queue> {
typename = detail::EnableIfDeviceSelectorIsInvocable<DeviceSelector>>
explicit queue(const DeviceSelector &deviceSelector,
const property_list &propList = {})
- : queue(detail::SelectDevice(deviceSelector), detail::defaultAsyncHandler,
- propList) {}
+ : queue(detail::SelectDevice(deviceSelector), async_handler{}, propList) {
+ }
/// Constructs a SYCL queue instance using the device identified by the
/// device selector provided.
@@ -162,7 +162,7 @@ class _LIBSYCL_EXPORT queue : private detail::KernelSubmissionBase<queue> {
/// \param syclDevice is an instance of SYCL device.
/// \param propList is a list of properties for queue construction.
explicit queue(const device &syclDevice, const property_list &propList = {})
- : queue(syclDevice, detail::defaultAsyncHandler, propList) {}
+ : queue(syclDevice, async_handler{}, propList) {}
/// Constructs a SYCL queue instance with an async_handler using the device
/// provided.
@@ -190,8 +190,7 @@ class _LIBSYCL_EXPORT queue : private detail::KernelSubmissionBase<queue> {
explicit queue(const context &syclContext,
const DeviceSelector &deviceSelector,
const property_list &propList = {})
- : queue(syclContext, detail::SelectDevice(deviceSelector),
- detail::defaultAsyncHandler, propList) {}
+ : queue(syclContext, detail::SelectDevice(deviceSelector), propList) {}
/// Constructs a SYCL queue instance with an async_handler that is associated
/// with syclContext, using the device identified by the device selector
@@ -223,8 +222,7 @@ class _LIBSYCL_EXPORT queue : private detail::KernelSubmissionBase<queue> {
/// \throw sycl::exception with sycl::errc::invalid if syclContext does not
/// contain syclDevice.
explicit queue(const context &syclContext, const device &syclDevice,
- const property_list &propList = {})
- : queue(syclContext, syclDevice, detail::defaultAsyncHandler, propList) {}
+ const property_list &propList = {});
/// Constructs a SYCL queue instance with an async_handler that is associated
/// with syclContext, using the device provided.
diff --git a/libsycl/src/detail/global_objects.cpp b/libsycl/src/detail/global_objects.cpp
index aa45961a1bee0..0ef13295d513d 100644
--- a/libsycl/src/detail/global_objects.cpp
+++ b/libsycl/src/detail/global_objects.cpp
@@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//
+#include <detail/context_impl.hpp>
#include <detail/global_objects.hpp>
#include <detail/platform_impl.hpp>
#include <detail/program_manager.hpp>
@@ -15,7 +16,9 @@
# include <windows.h>
#endif
+#include <cassert>
#include <tuple>
+#include <utility>
#include <vector>
_LIBSYCL_BEGIN_NAMESPACE_SYCL
@@ -67,9 +70,12 @@ InstanceWithLock<AsyncExceptionsContainer> &getAsyncExceptionList() {
void recordAsyncException(const std::shared_ptr<QueueImpl> &QueuePtr,
const std::exception_ptr &ExceptionPtr) {
+ assert(QueuePtr && "Queue impl ptr can't be nullptr");
+ AsyncExceptionKey Key{QueuePtr, QueuePtr->getContextWeakPtr()};
+
auto &[AsyncExceptions, AsyncExceptionsMutex] = getAsyncExceptionList();
std::lock_guard<SpinLock> Lock(AsyncExceptionsMutex);
- addAsyncException(AsyncExceptions[QueuePtr], ExceptionPtr);
+ addAsyncException(AsyncExceptions[std::move(Key)], ExceptionPtr);
}
void flushAsyncExceptions() {
@@ -86,13 +92,27 @@ void flushAsyncExceptions() {
if (Exceptions.size() == 0)
continue;
- if (std::shared_ptr<QueueImpl> Queue = EntryKey.lock();
+ // SYCL 2020 4.13.1.3. Priorities of async handlers: the handler the queue
+ // was constructed with comes first, the handler of the context enclosing
+ // the queue comes next.
+ const auto &[WeakQueue, WeakContext] = EntryKey;
+
+ if (std::shared_ptr<QueueImpl> Queue = WeakQueue.lock();
Queue && Queue->getAsyncHandler()) {
Queue->getAsyncHandler()(std::move(Exceptions));
continue;
}
- // If the queue is dead, use the default handler.
+ if (std::shared_ptr<ContextImpl> Context = WeakContext.lock();
+ Context && Context->get_async_handler()) {
+ Context->get_async_handler()(std::move(Exceptions));
+ continue;
+ }
+
+ // Neither the queue nor the context has a handler, or both of them are
+ // dead. A context constructed without an async_handler is given the default
+ // one at construction, so there is no need for a context to carry an empty
+ // handler: leaving it empty would end up here with an identical result.
defaultAsyncHandler(std::move(Exceptions));
}
}
diff --git a/libsycl/src/detail/global_objects.hpp b/libsycl/src/detail/global_objects.hpp
index fc598ad8ce29b..4200d8ab1b841 100644
--- a/libsycl/src/detail/global_objects.hpp
+++ b/libsycl/src/detail/global_objects.hpp
@@ -29,6 +29,7 @@
_LIBSYCL_BEGIN_NAMESPACE_SYCL
namespace detail {
+class ContextImpl;
class PlatformImpl;
class QueueImpl;
@@ -54,13 +55,19 @@ std::vector<std::unique_ptr<PlatformImpl>> &getPlatformCache();
// the SYCL shared library is first being unloaded.
void registerStaticVarShutdownHandler();
-// TODO: extend with context
-using AsyncExceptionKey = std::weak_ptr<QueueImpl>;
+using AsyncExceptionKey =
+ std::pair<std::weak_ptr<QueueImpl>, std::weak_ptr<ContextImpl>>;
struct AsyncExceptionKeyOwnerLess {
bool operator()(const AsyncExceptionKey &LHS,
const AsyncExceptionKey &RHS) const noexcept {
- return std::owner_less<std::weak_ptr<QueueImpl>>{}(LHS, RHS);
+ std::owner_less<std::weak_ptr<QueueImpl>> QueueLess;
+ if (QueueLess(LHS.first, RHS.first))
+ return true;
+ if (QueueLess(RHS.first, LHS.first))
+ return false;
+ return std::owner_less<std::weak_ptr<ContextImpl>>{}(LHS.second,
+ RHS.second);
}
};
@@ -71,12 +78,12 @@ using AsyncExceptionsContainer =
/// SpinLock.
InstanceWithLock<AsyncExceptionsContainer> &getAsyncExceptionList();
-/// Adds an exception to the list of unreported asynchronous exceptions.
+/// Adds an exception to the list of unreported asynchronous exceptions
+/// associated with the given queue and with the context enclosing it.
void recordAsyncException(const std::shared_ptr<QueueImpl> &QueuePtr,
const std::exception_ptr &ExceptionPtr);
-/// Reports all unreported asynchronous exceptions to available async_handler
-/// and clears the list.
+/// Reports all unreported asynchronous exceptions and clears the list.
void flushAsyncExceptions();
} // namespace detail
diff --git a/libsycl/src/detail/queue_impl.hpp b/libsycl/src/detail/queue_impl.hpp
index feaa128bc1629..b174fd015d3e4 100644
--- a/libsycl/src/detail/queue_impl.hpp
+++ b/libsycl/src/detail/queue_impl.hpp
@@ -64,6 +64,10 @@ class QueueImpl : public std::enable_shared_from_this<QueueImpl> {
/// \return the context implementation object this queue is associated with.
ContextImpl &getContext() { return *MContext; }
+ /// \return a weak pointer to the context implementation object this queue is
+ /// associated with.
+ std::weak_ptr<ContextImpl> getContextWeakPtr() const { return MContext; }
+
/// \return the device implementation object this queue is associated with.
DeviceImpl &getDevice() { return MDevice; }
@@ -114,7 +118,8 @@ class QueueImpl : public std::enable_shared_from_this<QueueImpl> {
void setKernelLaunchParams(std::vector<EventImplPtr> &&Events,
const ol_kernel_launch_size_args_t &Range);
- /// \return the async_handler associated with this queue.
+ /// \return the async_handler associated with this queue, empty if the queue
+ /// was constructed without one and its context has none either.
const async_handler &getAsyncHandler() const { return MAsyncHandler; }
/// Submits a memory copy operation from one USM or host pointer to another.
diff --git a/libsycl/src/queue.cpp b/libsycl/src/queue.cpp
index e3ab116229564..22a1e91079b05 100644
--- a/libsycl/src/queue.cpp
+++ b/libsycl/src/queue.cpp
@@ -22,6 +22,12 @@ queue::queue(const context &syclContext, const device &syclDevice,
asyncHandler, propList);
}
+queue::queue(const context &syclContext, const device &syclDevice,
+ const property_list &propList)
+ : queue(syclContext, syclDevice,
+ detail::getSyclObjImpl(syclContext)->get_async_handler(),
+ propList) {}
+
backend queue::get_backend() const noexcept { return impl->getBackend(); }
context queue::get_context() const {
diff --git a/libsycl/unittests/event/async_handler.cpp b/libsycl/unittests/event/async_handler.cpp
index 602789cffa1d3..a422781b37b04 100644
--- a/libsycl/unittests/event/async_handler.cpp
+++ b/libsycl/unittests/event/async_handler.cpp
@@ -11,6 +11,7 @@
#include <detail/event_impl.hpp>
#include <detail/global_objects.hpp>
+#include <sycl/__impl/context.hpp>
#include <sycl/__impl/detail/obj_utils.hpp>
#include <sycl/__impl/device.hpp>
#include <sycl/__impl/event.hpp>
@@ -38,6 +39,25 @@ EventImplPtr createEventImplWithHandle(detail::PlatformImpl &PlatformImpl) {
return detail::EventImpl::createEventWithHandle(Handle, PlatformImpl, {});
}
+async_handler makeRecordingHandler(std::vector<std::string> &Messages) {
+ return [&Messages](exception_list Exceptions) {
+ for (const auto &ExceptionPtr : Exceptions) {
+ try {
+ std::rethrow_exception(ExceptionPtr);
+ } catch (const sycl::exception &E) {
+ Messages.emplace_back(E.what());
+ } catch (...) {
+ ADD_FAILURE() << "Unexpected exception type in async_handler";
+ }
+ }
+ };
+}
+
+std::exception_ptr makeRuntimeException(const char *Message) {
+ return std::make_exception_ptr(
+ exception(make_error_code(errc::runtime), Message));
+}
+
template <typename FlushAction>
void runAsyncExceptionFlushTest(mock::MockLiboffload &Mock,
const char *ErrorMsg, FlushAction Flush) {
@@ -125,7 +145,105 @@ TEST(EventAsyncHandler, QueueThrowAsynchronous) {
});
}
-TEST(EventAsyncHandler, DeadQueueFallsBackToDefaultAsyncHandler) {
+TEST(EventAsyncHandler, QueueInheritsContextAsyncHandler) {
+ mock::MockWrapper Mock;
+ std::vector<std::string> ContextMessages;
+
+ device Device(default_selector_v);
+ context Ctx(Device, makeRecordingHandler(ContextMessages));
+ queue Q(Ctx, Device);
+
+ detail::recordAsyncException(detail::getSyclObjImpl(Q),
+ makeRuntimeException("inherited handler error"));
+ Q.throw_asynchronous();
+
+ EXPECT_THAT(ContextMessages,
+ ElementsAre(HasSubstr("inherited handler error")));
+}
+
+TEST(EventAsyncHandler, QueueHandlerHasPriorityOverContextHandler) {
+ mock::MockWrapper Mock;
+ std::vector<std::string> QueueMessages;
+ std::vector<std::string> ContextMessages;
+
+ device Device(default_selector_v);
+ context Ctx(Device, makeRecordingHandler(ContextMessages));
+ queue Q(Ctx, Device, makeRecordingHandler(QueueMessages));
+
+ detail::recordAsyncException(detail::getSyclObjImpl(Q),
+ makeRuntimeException("queue handler error"));
+ Q.throw_asynchronous();
+
+ EXPECT_THAT(QueueMessages, ElementsAre(HasSubstr("queue handler error")));
+ EXPECT_THAT(ContextMessages, IsEmpty());
+}
+
+TEST(EventAsyncHandler, DeadQueueReportsThroughContextHandler) {
+ mock::MockWrapper Mock;
+ std::vector<std::string> QueueMessages;
+ std::vector<std::string> ContextMessages;
+
+ device Device(default_selector_v);
+ context Ctx(Device, makeRecordingHandler(ContextMessages));
+ {
+ queue Q(Ctx, Device, makeRecordingHandler(QueueMessages));
+ detail::recordAsyncException(detail::getSyclObjImpl(Q),
+ makeRuntimeException("dead queue error"));
+ }
+
+ detail::flushAsyncExceptions();
+
+ EXPECT_THAT(ContextMessages, ElementsAre(HasSubstr("dead queue error")));
+ EXPECT_THAT(QueueMessages, IsEmpty());
+}
+
+TEST(EventAsyncHandler, ExceptionsAreGroupedPerQueueAndContext) {
+ mock::MockWrapper Mock;
+ std::vector<std::string> FirstMessages;
+ std::vector<std::string> SecondMessages;
+
+ device Device(default_selector_v);
+ context FirstCtx(Device, makeRecordingHandler(FirstMessages));
+ context SecondCtx(Device, makeRecordingHandler(SecondMessages));
+ queue FirstQ(FirstCtx, Device);
+ queue SecondQ(SecondCtx, Device);
+
+ detail::recordAsyncException(detail::getSyclObjImpl(FirstQ),
+ makeRuntimeException("first queue error"));
+ detail::recordAsyncException(detail::getSyclObjImpl(SecondQ),
+ makeRuntimeException("second queue error"));
+
+ detail::flushAsyncExceptions();
+
+ EXPECT_THAT(FirstMessages, ElementsAre(HasSubstr("first queue error")));
+ EXPECT_THAT(SecondMessages, ElementsAre(HasSubstr("second queue error")));
+}
+
+TEST(EventAsyncHandler, DeadQueueAndContextFallBackToDefaultAsyncHandler) {
+// EXPECT_DEATH is not supported on Windows.
+#if GTEST_HAS_DEATH_TEST
+ EXPECT_DEATH(
+ {
+ mock::MockWrapper Mock;
+ std::vector<std::string> ContextMessages;
+ {
+ device Device(default_selector_v);
+ context Ctx(Device, makeRecordingHandler(ContextMessages));
+ queue Q(Ctx, Device);
+ detail::recordAsyncException(
+ detail::getSyclObjImpl(Q),
+ makeRuntimeException("dead context async error"));
+ }
+
+ detail::flushAsyncExceptions();
+ },
+ "Default async_handler caught exceptions:\n\tdead context async error");
+#else
+ GTEST_SKIP() << "Death tests are not supported on this platform";
+#endif
+}
+
+TEST(EventAsyncHandler, DeadQueueReportsThroughDefaultContextHandler) {
// EXPECT_DEATH is not supported on Windows.
#if GTEST_HAS_DEATH_TEST
EXPECT_DEATH(
>From ab6087ae542c5874b6bd5e8a25518dbab0db268b Mon Sep 17 00:00:00 2001
From: "Tikhomirova, Kseniya" <kseniya.tikhomirova at intel.com>
Date: Mon, 21 Sep 2026 04:47:08 -0700
Subject: [PATCH 2/2] fix nit
Signed-off-by: Tikhomirova, Kseniya <kseniya.tikhomirova at intel.com>
---
libsycl/unittests/event/async_handler.cpp | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/libsycl/unittests/event/async_handler.cpp b/libsycl/unittests/event/async_handler.cpp
index a422781b37b04..0e05a44e15eda 100644
--- a/libsycl/unittests/event/async_handler.cpp
+++ b/libsycl/unittests/event/async_handler.cpp
@@ -149,7 +149,7 @@ TEST(EventAsyncHandler, QueueInheritsContextAsyncHandler) {
mock::MockWrapper Mock;
std::vector<std::string> ContextMessages;
- device Device(default_selector_v);
+ device Device;
context Ctx(Device, makeRecordingHandler(ContextMessages));
queue Q(Ctx, Device);
@@ -166,7 +166,7 @@ TEST(EventAsyncHandler, QueueHandlerHasPriorityOverContextHandler) {
std::vector<std::string> QueueMessages;
std::vector<std::string> ContextMessages;
- device Device(default_selector_v);
+ device Device;
context Ctx(Device, makeRecordingHandler(ContextMessages));
queue Q(Ctx, Device, makeRecordingHandler(QueueMessages));
@@ -183,7 +183,7 @@ TEST(EventAsyncHandler, DeadQueueReportsThroughContextHandler) {
std::vector<std::string> QueueMessages;
std::vector<std::string> ContextMessages;
- device Device(default_selector_v);
+ device Device;
context Ctx(Device, makeRecordingHandler(ContextMessages));
{
queue Q(Ctx, Device, makeRecordingHandler(QueueMessages));
@@ -202,7 +202,7 @@ TEST(EventAsyncHandler, ExceptionsAreGroupedPerQueueAndContext) {
std::vector<std::string> FirstMessages;
std::vector<std::string> SecondMessages;
- device Device(default_selector_v);
+ device Device;
context FirstCtx(Device, makeRecordingHandler(FirstMessages));
context SecondCtx(Device, makeRecordingHandler(SecondMessages));
queue FirstQ(FirstCtx, Device);
@@ -227,7 +227,7 @@ TEST(EventAsyncHandler, DeadQueueAndContextFallBackToDefaultAsyncHandler) {
mock::MockWrapper Mock;
std::vector<std::string> ContextMessages;
{
- device Device(default_selector_v);
+ device Device;
context Ctx(Device, makeRecordingHandler(ContextMessages));
queue Q(Ctx, Device);
detail::recordAsyncException(
More information about the llvm-commits
mailing list