[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