Skip to content

Commit bbf7d44

Browse files
committed
Migrate host_tasks to a thread pool based approach
Inspired by comment in: AdaptiveCpp/AdaptiveCpp#1915 we move away from use of host_task, which enables compatibility with AdaptiveCpp
1 parent ac092b2 commit bbf7d44

5 files changed

Lines changed: 177 additions & 57 deletions

File tree

dpctl/CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,12 @@ endforeach()
199199
set(_cy_file ${CMAKE_CURRENT_SOURCE_DIR}/_sycl_queue.pyx)
200200
get_filename_component(_trgt ${_cy_file} NAME_WLE)
201201
build_dpctl_ext(${_trgt} ${_cy_file} "dpctl" SYCL)
202-
# _sycl_queue include _host_task_util.hpp
203-
target_include_directories(${_trgt} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
202+
# _sycl_queue includes _async_dec_ref.hpp, which includes
203+
# detail/keep_alive_pool.hpp from the public include directory
204+
target_include_directories(${_trgt} PRIVATE
205+
${CMAKE_CURRENT_SOURCE_DIR}
206+
${CMAKE_CURRENT_SOURCE_DIR}/apis/include
207+
)
204208
target_link_libraries(DpctlCAPI INTERFACE ${_trgt}_headers)
205209

206210
add_subdirectory(program)
Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===--- _host_tasl_util.hpp - Implements async DECREF =//
1+
//===--- _async_dec_ref.hpp - Implements async DECREF ---------------------===//
22
//
33
// Data Parallel Control (dpctl)
44
//
@@ -19,11 +19,11 @@
1919
//===----------------------------------------------------------------------===//
2020
///
2121
/// \file
22-
/// This file implements a utility function to schedule host task to a sycl
23-
/// queue depending on given array of sycl events to decrement reference counts
24-
/// for the given array of Python objects.
22+
/// This file implements a utility function to decrement reference counts for a
23+
/// given array of Python objects once a given array of sycl events has
24+
/// completed.
2525
///
26-
/// N.B.: The host task attempts to acquire GIL, so queue wait, event wait and
26+
/// N.B.: The deferred work acquires the GIL, so queue wait, event wait and
2727
/// other synchronization mechanisms should be called after releasing the GIL to
2828
/// avoid deadlocks.
2929
///
@@ -33,9 +33,12 @@
3333
#include <exception>
3434
#include <stddef.h>
3535
#include <sycl/sycl.hpp>
36+
#include <utility>
37+
#include <vector>
3638

3739
#include "Python.h"
3840

41+
#include "detail/keep_alive_pool.hpp"
3942
#include "syclinterface/dpctl_data_types.h"
4043
#include "syclinterface/dpctl_sycl_type_casters.hpp"
4144

@@ -49,16 +52,21 @@ DPCTLSyclEventRef async_dec_ref(DPCTLSyclQueueRef QRef,
4952
using dpctl::syclinterface::unwrap;
5053
using dpctl::syclinterface::wrap;
5154

52-
sycl::queue *q = unwrap<sycl::queue>(QRef);
55+
// `QRef` is kept in the signature for API compatibility
56+
(void)QRef;
5357

5458
std::vector<PyObject *> obj_vec(obj_array, obj_array + obj_array_size);
5559

5660
try {
57-
sycl::event ht_ev = q->submit([&](sycl::handler &cgh) {
58-
for (size_t ev_id = 0; ev_id < nDepERefs; ++ev_id) {
59-
cgh.depends_on(*(unwrap<sycl::event>(depERefs[ev_id])));
60-
}
61-
cgh.host_task([obj_array_size, obj_vec]() {
61+
std::vector<sycl::event> depends;
62+
depends.reserve(nDepERefs);
63+
for (size_t ev_id = 0; ev_id < nDepERefs; ++ev_id) {
64+
depends.push_back(*(unwrap<sycl::event>(depERefs[ev_id])));
65+
}
66+
67+
dpctl::detail::KeepAlivePool::get().submit(
68+
std::move(depends),
69+
[obj_array_size, obj_vec = std::move(obj_vec)]() {
6270
const bool initialized = Py_IsInitialized();
6371
#if PY_VERSION_HEX < 0x30d0000
6472
const bool finalizing = _Py_IsFinalizing();
@@ -75,12 +83,12 @@ DPCTLSyclEventRef async_dec_ref(DPCTLSyclQueueRef QRef,
7583
PyGILState_Release(gstate);
7684
}
7785
});
78-
});
7986

8087
static constexpr int result_ok = 0;
8188

8289
*status = result_ok;
83-
auto e_ptr = new sycl::event(ht_ev);
90+
// return a dummy event for API compatibility
91+
auto e_ptr = new sycl::event();
8492
return wrap<sycl::event>(e_ptr);
8593
} catch (const std::exception &e) {
8694
static constexpr int result_std_exception = 1;

dpctl/_sycl_queue.pyx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import collections.abc
9292
import logging
9393

9494

95-
cdef extern from "_host_task_util.hpp":
95+
cdef extern from "_async_dec_ref.hpp":
9696
DPCTLSyclEventRef async_dec_ref(
9797
DPCTLSyclQueueRef, PyObject **,
9898
size_t, DPCTLSyclEventRef *, size_t, int *
@@ -1184,16 +1184,18 @@ cdef class SyclQueue(_SyclQueue):
11841184
working on Python objects collected in ``args``.
11851185
Returns:
11861186
dpctl.SyclEvent
1187-
The event associated with the submission of host task.
1187+
An already-complete event. No task is submitted to the queue,
1188+
so there is nothing to wait for here; ``events`` are what say
1189+
when the work reading ``args`` is done.
11881190
1189-
Increments reference count of ``args`` and schedules asynchronous
1190-
``host_task`` to decrement the count once dependent events are
1191+
Increments reference count of ``args`` and schedules the matching
1192+
decrement to run on a background thread once dependent events are
11911193
complete.
11921194
11931195
.. note::
1194-
The ``host_task`` attempts to acquire Python GIL, and it is
1195-
known to be unsafe during interpreter shutdown sequence. It is
1196-
thus strongly advised to ensure that all submitted ``host_task``
1196+
The deferred decrement attempts to acquire Python GIL, which is
1197+
known to be unsafe during the interpreter shutdown sequence. It
1198+
is thus strongly advised to ensure that all dependent events
11971199
complete before the end of the Python script.
11981200
"""
11991201
cdef size_t nDE = len(dEvents)
@@ -1236,7 +1238,7 @@ cdef class SyclQueue(_SyclQueue):
12361238
with nogil:
12371239
DPCTLEvent_Wait(htERef)
12381240
DPCTLEvent_Delete(htERef)
1239-
raise RuntimeError("Could not submit keep_args_alive host_task")
1241+
raise RuntimeError("Could not schedule keep_args_alive cleanup")
12401242

12411243
return SyclEvent._create(htERef)
12421244

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//===--- keep_alive_pool.hpp - keeps owners alive during offload ----------===//
2+
//
3+
// Data Parallel Control (dpctl)
4+
//
5+
// Copyright 2026 Intel Corporation
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
//===----------------------------------------------------------------------===//
20+
///
21+
/// \file
22+
/// A fixed-size pool of threads that wait on SYCL events and then run a
23+
/// callable for maintaining Python object lifetime during offloaded tasks.
24+
///
25+
//===----------------------------------------------------------------------===//
26+
27+
#pragma once
28+
29+
#include <condition_variable>
30+
#include <cstddef>
31+
#include <exception>
32+
#include <functional>
33+
#include <mutex>
34+
#include <queue>
35+
#include <thread>
36+
#include <utility>
37+
#include <vector>
38+
39+
#include <sycl/sycl.hpp>
40+
41+
namespace dpctl
42+
{
43+
namespace detail
44+
{
45+
46+
class KeepAlivePool
47+
{
48+
public:
49+
/*!
50+
* @brief Number of waiter threads.
51+
*/
52+
static constexpr std::size_t num_threads = 4;
53+
54+
static KeepAlivePool &get()
55+
{
56+
// deliberately leaked: workers are detached and hold a bare `this`, so
57+
// the pool must outlive them
58+
static KeepAlivePool *instance = new KeepAlivePool();
59+
return *instance;
60+
}
61+
62+
/*!
63+
* @brief Run `task` once every event in `depends` has completed.
64+
*
65+
* `task` must own everything it releases -- move USM `shared_ptr` copies,
66+
* `sycl::buffer` handles, or `PyObject *` references into it. It runs on a
67+
* pool thread without the GIL, so it must take the GIL itself if it touches
68+
* Python.
69+
*/
70+
void submit(std::vector<sycl::event> depends, std::function<void()> task)
71+
{
72+
{
73+
std::lock_guard<std::mutex> lock(queue_mutex_);
74+
tasks_.emplace(std::move(depends), std::move(task));
75+
}
76+
condition_.notify_one();
77+
}
78+
79+
KeepAlivePool(const KeepAlivePool &) = delete;
80+
KeepAlivePool &operator=(const KeepAlivePool &) = delete;
81+
~KeepAlivePool() = delete;
82+
83+
private:
84+
KeepAlivePool()
85+
{
86+
for (std::size_t i = 0; i < num_threads; ++i) {
87+
std::thread(&KeepAlivePool::run, this).detach();
88+
}
89+
}
90+
91+
void run()
92+
{
93+
for (;;) {
94+
std::pair<std::vector<sycl::event>, std::function<void()>> item;
95+
{
96+
std::unique_lock<std::mutex> lock(queue_mutex_);
97+
condition_.wait(lock, [this] { return !tasks_.empty(); });
98+
item = std::move(tasks_.front());
99+
tasks_.pop();
100+
}
101+
102+
try {
103+
sycl::event::wait(item.first);
104+
} catch (const std::exception &) {
105+
// run the task anyway: an async error must not strand the
106+
// task or else it may leak
107+
}
108+
109+
try {
110+
item.second();
111+
} catch (const std::exception &) {
112+
// a throwing task must not take down the worker or later
113+
// tasks will be lost
114+
}
115+
}
116+
}
117+
118+
std::queue<std::pair<std::vector<sycl::event>, std::function<void()>>>
119+
tasks_;
120+
std::mutex queue_mutex_;
121+
std::condition_variable condition_;
122+
};
123+
124+
} // namespace detail
125+
} // namespace dpctl

dpctl/apis/include/dpctl4pybind11.hpp

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#pragma once
2727

28+
#include "detail/keep_alive_pool.hpp"
2829
#include "dpctl_capi.h"
2930

3031
#include <atomic>
@@ -823,46 +824,26 @@ sycl::event keep_args_alive(sycl::queue &q,
823824
}
824825
}
825826

826-
bool use_depends = true;
827-
sycl::event host_task_ev;
828-
829-
if (n_usm_owners_held > 0) {
830-
host_task_ev = q.submit([&](sycl::handler &cgh) {
831-
if (use_depends) {
832-
cgh.depends_on(depends);
833-
use_depends = false;
834-
}
835-
else {
836-
cgh.depends_on(host_task_ev);
837-
}
838-
cgh.host_task([shp_usm = std::move(shp_usm)]() {
839-
// no body, but shared pointers are captured in
840-
// the lambda, ensuring that USM allocation is
841-
// kept alive
842-
});
843-
});
844-
}
827+
if (n_usm_owners_held > 0 || n_objects_held > 0) {
828+
dpctl::detail::KeepAlivePool::get().submit(
829+
depends, [n_usm_owners_held, shp_usm = std::move(shp_usm),
830+
n_objects_held, shp_arr = std::move(shp_arr)]() mutable {
831+
for (std::size_t i = 0; i < n_usm_owners_held; ++i) {
832+
shp_usm[i].reset();
833+
}
845834

846-
if (n_objects_held > 0) {
847-
host_task_ev = q.submit([&](sycl::handler &cgh) {
848-
if (use_depends) {
849-
cgh.depends_on(depends);
850-
use_depends = false;
851-
}
852-
else {
853-
cgh.depends_on(host_task_ev);
854-
}
855-
cgh.host_task([n_objects_held, shp_arr = std::move(shp_arr)]() {
856-
py::gil_scoped_acquire acquire;
835+
if (n_objects_held > 0) {
836+
py::gil_scoped_acquire acquire;
857837

858-
for (std::size_t i = 0; i < n_objects_held; ++i) {
859-
shp_arr[i]->dec_ref();
838+
for (std::size_t i = 0; i < n_objects_held; ++i) {
839+
shp_arr[i]->dec_ref();
840+
}
860841
}
861842
});
862-
});
863843
}
864844

865-
return host_task_ev;
845+
// return dummy event for API compatibility
846+
return sycl::event{};
866847
}
867848

868849
/*! @brief Check if all allocation queues are the same as the

0 commit comments

Comments
 (0)