Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `DPCTLQueue_MemsetWithEvents` C-API function to support `dpctl.SyclQueue.memset_async` [gh-2361](https://github.com/IntelPython/dpctl/pull/2361)
* Added `dpctl.SyclQueue.fill` and `dpctl.SyclQueue.fill_async` methods [gh-2365](https://github.com/IntelPython/dpctl/pull/2365)
* Added `DPCTLQueue_Fill8/16/32/64/128WithEvents` C-API functions to support `dpctl.SyclQueue.fill_async` [gh-2365](https://github.com/IntelPython/dpctl/pull/2365)
* Added `dpctl.keep_args_alive` free function, and `add_event` method to the order manager [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* Added `add_cleanup_event` method and `cleanup_events` and `num_cleanup_events` properties to the order manager, for tracking the events that gate the release of objects used by offloaded tasks [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Deprecated
* Deprecated `dpctl.SyclQueue._submit_keep_args_alive` in favor of `dpctl.keep_args_alive` [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* Deprecated the order manager's `add_event_pair`, `host_task_events` and `num_host_task_events`, as `host_task` is no longer used for managing object lifetimes, in favor of `add_event`, `add_cleanup_event`, `cleanup_events` and `num_cleanup_events` [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Changed
* Bump minimum NumPy version to 1.26 [gh-2192](https://github.com/IntelPython/dpctl/pull/2192)
* Implemented a background thread that polls events to manage object lifetime during offload rather than use `host_task` [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* The references taken by `dpctl.keep_args_alive`, `dpctl::utils::keep_args_alive` and `dpctl.SyclQueue._submit_keep_args_alive` are now dropped by a thread that is running Python rather than by the background thread, which no longer calls into the interpreter at all. They are dropped the next time `dpctl` is entered from Python, including on a wait on the order manager, and are leaked rather than dropped if that never happens again [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* The event returned by `dpctl::utils::keep_args_alive` and `dpctl.SyclQueue._submit_keep_args_alive` is now for an empty kernel that gates the deferred release rather than for a `host_task` that performs it, so its completion means that the objects are no longer in use rather than that they were released [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* Rewrote USM Python examples into a single example [gh-2292](https://github.com/IntelPython/dpctl/pull/2292)
* Registered `DPCTL_PARTITION_AFFINITY_DOMAIN_UNKNOWN` enumerator when `DPCTLDevice_GetPartitionAffinityDomains` receives an unrecognized value from the SYCL runtime [gh-2324](https://github.com/IntelPython/dpctl/pull/2324)

Expand All @@ -30,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
* Fixed incorrect paths in `GetLevelZeroHeaders.cmake` [gh-2366](https://github.com/IntelPython/dpctl/pull/2366)
* A USM allocation that fails now drops the references held for offloaded tasks that have completed, and is attempted once more if that released anything, so that memory only waiting to be given up is not reported as unavailable [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Maintenance
* Updated pybind11 version used by `dpctl` and examples [gh-2357](https://github.com/IntelPython/dpctl/pull/2357)
Expand Down
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ option(
size of shared object with offloading sections"
OFF
)

find_package(IntelSYCL REQUIRED PATHS ${CMAKE_SOURCE_DIR}/cmake NO_DEFAULT_PATH)

set(_dpctl_sycl_target_compile_options)
Expand Down
8 changes: 8 additions & 0 deletions docs/doc_sources/api_reference/dpctl/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@
SyclQueueCreationError
SyclSubDeviceCreationError

.. rubric:: Lifetime management

.. autosummary::
:toctree: generated
:nosignatures:

keep_args_alive

.. rubric:: Utilities

.. autosummary::
Expand Down
20 changes: 20 additions & 0 deletions docs/doc_sources/api_reference/dpctl/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,23 @@

Thread-local object mapping each :class:`dpctl.SyclQueue` to an order
manager, used to ensure sequential ordering of offloaded tasks.

Record submitted tasks with ``add_event`` and use ``submitted_events``
as the dependency list of subsequent submissions. To keep Python objects
referenced by a task alive until it completes, use
:func:`dpctl.keep_args_alive`.

Record events that gate the release of objects used by a task with
``add_cleanup_event``, and find them in ``cleanup_events``. They are
waited on, but never become dependencies of later tasks.

Waiting with ``wait`` also drops the references that
:func:`dpctl.keep_args_alive` took for tasks that have since completed,
which is otherwise done the next time ``dpctl`` is called into.

.. deprecated:: 0.23.0
``add_event_pair``, ``host_task_events`` and ``num_host_task_events``
are deprecated. Tasks are no longer paired with a host task event, so
``add_event`` takes the computational event alone, and cleanup is
tracked by ``add_cleanup_event``, ``cleanup_events`` and
``num_cleanup_events``.
8 changes: 6 additions & 2 deletions dpctl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,12 @@ endforeach()
set(_cy_file ${CMAKE_CURRENT_SOURCE_DIR}/_sycl_queue.pyx)
get_filename_component(_trgt ${_cy_file} NAME_WLE)
build_dpctl_ext(${_trgt} ${_cy_file} "dpctl" SYCL)
# _sycl_queue include _host_task_util.hpp
target_include_directories(${_trgt} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# _sycl_queue includes _async_dec_ref.hpp, which includes
Comment thread
ndgrigorian marked this conversation as resolved.
# detail/keep_alive_watcher.hpp from the public include directory
target_include_directories(${_trgt} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/apis/include
)
target_link_libraries(DpctlCAPI INTERFACE ${_trgt}_headers)

add_subdirectory(compiler)
Expand Down
2 changes: 2 additions & 0 deletions dpctl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
SyclQueue,
SyclQueueCreationError,
WorkGroupMemory,
keep_args_alive,
)
from ._sycl_queue_manager import get_device_cached_queue
from ._sycl_timer import SyclTimer
Expand Down Expand Up @@ -114,6 +115,7 @@
"WorkGroupMemory",
"LocalAccessor",
"RawKernelArg",
"keep_args_alive",
]
__all__ += [
"get_device_cached_queue",
Expand Down
246 changes: 246 additions & 0 deletions dpctl/_async_dec_ref.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//===--- _async_dec_ref.hpp - Implements async DECREF ---------------------===//
//
// Data Parallel Control (dpctl)
//
// Copyright 2026 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file implements a utility function to decrement reference counts for a
/// given array of Python objects once a given array of sycl events has
/// completed.
///
/// N.B.: The reference counts are dropped by whichever thread next enters
/// `dpctl` from Python, and never by the thread that finds the events complete,
/// which must not touch Python. Waiting for a decrement to happen therefore
/// deadlocks, and nothing in `dpctl` does.
///
//===----------------------------------------------------------------------===//

#pragma once
#include <atomic>
#include <memory>
#include <stddef.h>
#include <stdexcept>
#include <sycl/sycl.hpp>
#include <utility>
#include <vector>

#include "Python.h"

#include "detail/keep_alive_watcher.hpp"
#include "syclinterface/dpctl_data_types.h"
#include "syclinterface/dpctl_sycl_type_casters.hpp"

namespace dpctl
{
namespace detail
{

namespace
{

/*!
* @brief The watcher, once there is one, for those who must not create it.
*/
std::atomic<KeepAliveWatcher *> created_watcher{nullptr};

} // namespace

/*!
* @brief The watcher of the `dpctl._sycl_queue` module, which everyone shares.
*
* `KeepAliveWatcher` befriends this, making it the only creator of a watcher.
*/
KeepAliveWatcher &local_keep_alive_watcher()
{
// deliberately leaked: the polling thread is detached and holds a bare
// `this`, so the watcher must outlive it
static KeepAliveWatcher *instance = []() {
KeepAliveWatcher *watcher = new KeepAliveWatcher();
created_watcher.store(watcher, std::memory_order_release);
return watcher;
}();
return *instance;
}

} // namespace detail
} // namespace dpctl

/*!
* @brief Address of the `KeepAliveWatcher`.
*
* Returns nullptr if the watcher could not be created.
*/
void *keep_alive_watcher_ptr()
{
try {
return static_cast<void *>(&dpctl::detail::local_keep_alive_watcher());
} catch (...) {
// nothing may escape into the calling Cython code, which is not
// prepared to handle a C++ exception
return nullptr;
}
}

/*!
* @brief Drop the references of the DECREFs that have come due.
*
* Does nothing until there is a watcher, and never creates one.
*
* Expects the caller to hold the GIL.
*
* @return Whether there were any.
*/
bool drain_retired_references()
{
// a watcher being created right now is left to the next drain
dpctl::detail::KeepAliveWatcher *watcher =
dpctl::detail::created_watcher.load(std::memory_order_acquire);
if (!watcher) {
return false;
}

try {
return watcher->drain_retired();
} catch (...) {
// nothing may escape into the calling Cython code, which is not
// prepared to handle a C++ exception
return false;
}
}

namespace
{

/*!
* @brief Copy `nDepERefs` event references into a vector.
*/
std::vector<sycl::event> unwrap_events(DPCTLSyclEventRef *depERefs,
size_t nDepERefs)
{
using dpctl::syclinterface::unwrap;

std::vector<sycl::event> depends;
depends.reserve(nDepERefs);
for (size_t ev_id = 0; ev_id < nDepERefs; ++ev_id) {
depends.push_back(*(unwrap<sycl::event>(depERefs[ev_id])));
}

return depends;
}

/*!
* @brief Schedule DECREFs of `obj_vec` for once `depends` have completed.
*/
void submit_dec_ref(std::vector<PyObject *> obj_vec,
std::vector<sycl::event> depends)
{
auto &watcher = dpctl::detail::local_keep_alive_watcher();

// the caller holds the GIL, so this is an opportunity to drain references
watcher.drain_retired();

watcher.submit(std::move(depends),
[obj_vec = std::move(obj_vec)]() mutable {
// handed to a thread that holds the GIL
dpctl::detail::local_keep_alive_watcher().retire(
[obj_vec = std::move(obj_vec)]() {
for (PyObject *obj : obj_vec) {
Py_DECREF(obj);
}
});
});
}

} // namespace

/*!
* @brief Schedule DECREFs of `obj_array` for once `depERefs` have completed.
*
* Sets `*status` to 0 on success and 1 if scheduling threw.
*/
void async_dec_ref(PyObject **obj_array,
size_t obj_array_size,
DPCTLSyclEventRef *depERefs,
size_t nDepERefs,
int *status)
{
try {
submit_dec_ref(
std::vector<PyObject *>(obj_array, obj_array + obj_array_size),
unwrap_events(depERefs, nDepERefs));

static constexpr int result_ok = 0;
*status = result_ok;
} catch (...) {
// nothing may escape into the calling Cython code, which is not
// prepared to handle a C++ exception
static constexpr int result_exception = 1;
*status = result_exception;
}
}

/*!
* @brief Queue-bound form of `async_dec_ref`.
*
* Returns an event for an empty kernel submitted to `QRef` after `depERefs`,
* which is what the DECREFs wait for. It has completed once `obj_array` is no
* longer in use, and, if `QRef` is in-order, once the work already submitted
* to the queue has completed as well. The DECREFs themselves are dropped
* afterwards, by whichever thread next enters `dpctl` from Python.
*
* Returns nullptr on failure, with `*status` set.
*/
DPCTLSyclEventRef async_dec_ref_event(DPCTLSyclQueueRef QRef,
PyObject **obj_array,
size_t obj_array_size,
DPCTLSyclEventRef *depERefs,
size_t nDepERefs,
int *status)
{
using dpctl::syclinterface::unwrap;
using dpctl::syclinterface::wrap;

try {
sycl::queue *q = unwrap<sycl::queue>(QRef);
if (!q) {
throw std::runtime_error("Queue reference is null");
}

const sycl::event marker = dpctl::detail::submit_keep_alive_marker(
*q, unwrap_events(depERefs, nDepERefs));

// allocated before scheduling, as failing afterwards could not be
// reported: the caller would drop a reference the scheduled DECREFs own
std::unique_ptr<sycl::event> e_ptr(new sycl::event(marker));

submit_dec_ref(
std::vector<PyObject *>(obj_array, obj_array + obj_array_size),
{marker});

static constexpr int result_ok = 0;
*status = result_ok;

return wrap<sycl::event>(e_ptr.release());
} catch (...) {
// nothing may escape into the calling Cython code, which is not
// prepared to handle a C++ exception
static constexpr int result_exception = 1;
*status = result_exception;
return nullptr;
}
}
Loading
Loading