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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [dev] - XXX. XX, XXXX

### Added
* Added `dpctl.SyclQueue.keep_args_alive` method, which keeps Python objects alive until the work on that queue using them completes [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
Comment thread
ndgrigorian marked this conversation as resolved.
Resolved
* Added `add_event` and `add_cleanup_event` methods 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)
* Added `SyclQueue_DeferRelease` C-API function, which defers a release callback until work submitted to a queue completes, and through which `dpctl::utils::keep_args_alive` keeps its arguments alive [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Changed
* Object lifetime during offload is now managed by a list of releases deferred until the events that gate them have completed, rather than by a `host_task`. `dpctl` keeps one thread for looking over the list and running the releases that have come due, started by the first release deferred and again in the child of a `fork`, which takes the GIL to do so and lets a release call into the interpreter and free USM allocations [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
* The event returned by `dpctl.SyclQueue.keep_args_alive`, `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 have been released [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Deprecated
* Deprecated `dpctl.SyclQueue._submit_keep_args_alive` in favor of `dpctl.SyclQueue.keep_args_alive`, which it now calls and which behaves identically [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`; they do not yet emit a `DeprecationWarning` [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

### Removed
* Removed the deprecated `dpctl.program` submodule, use `dpctl.compiler` instead [gh-2407](https://github.com/IntelPython/dpctl/pull/2407)

### Fixed
* Removed stale `select_host_device` and `has_host_device` names from `dpctl.__all__`, which made `from dpctl import *` fail with `AttributeError` [gh-2410](https://github.com/IntelPython/dpctl/pull/2410)
* 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
* Removed the `-fno-sycl-id-queries-fit-in-int` compile option, which none of the `dpctl` extensions need since `dpctl.tensor` moved to `dpnp` [gh-2413](https://github.com/IntelPython/dpctl/pull/2413)
Expand Down
22 changes: 22 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,25 @@

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
:meth:`dpctl.SyclQueue.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
:meth:`dpctl.SyclQueue.keep_args_alive` took for tasks that have since
completed, which ``dpctl`` otherwise does on a thread of its own. This is
best-effort: references that thread is already dropping are left to it, so
they may still be held when ``wait`` returns.

.. deprecated:: 0.24.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``.
2 changes: 1 addition & 1 deletion dpctl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ 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
# _sycl_queue includes _async_dec_ref.hpp
target_include_directories(${_trgt} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(DpctlCAPI INTERFACE ${_trgt}_headers)

Expand Down
207 changes: 207 additions & 0 deletions dpctl/_async_dec_ref.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//===--- _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 the functions that defer releasing what offloaded work
/// uses until that work has completed: decrementing the reference counts of an
/// array of Python objects, or calling a release function another module
/// passes in.
///
/// N.B.: The reference counts are dropped by the thread `dpctl` keeps for
/// running deferred releases, or by a thread that drains them.
///
//===----------------------------------------------------------------------===//

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

#include "Python.h"

#include "_deferred_release_watcher.hpp"
#include "syclinterface/dpctl_data_types.h"
#include "syclinterface/dpctl_sycl_type_casters.hpp"

namespace dpctl
{
namespace detail
{

/*!
* @brief Name of the kernel submitted by `submit_keep_alive_marker`.
*/
class keep_alive_marker;

/*!
* @brief An event that gates the release of objects used by work on `q`.
*
* Submits an empty kernel after `deps`. On an in-order queue the submission is
* also ordered after everything already submitted there, so the event covers
* uses of the objects that the caller never tracked.
*/
inline sycl::event
submit_keep_alive_marker(sycl::queue &q, const std::vector<sycl::event> &deps)
{
return q.single_task<keep_alive_marker>(deps, []() {});
}

} // namespace detail
} // namespace dpctl

namespace
{

/*!
* @brief Copy the events behind `nDepERefs` 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 The queue behind `QRef`, which must not be null.
*/
sycl::queue &unwrap_queue(DPCTLSyclQueueRef QRef)
{
sycl::queue *q = dpctl::syclinterface::unwrap<sycl::queue>(QRef);
if (!q) {
throw std::invalid_argument("A queue is required");
}
return *q;
}

/*!
* @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)
{
dpctl::detail::local_deferred_releases().defer(
std::move(depends), [obj_vec = std::move(obj_vec)]() {
for (PyObject *obj : obj_vec) {
Py_DECREF(obj);
}
});
}

} // namespace

/*!
* @brief Schedule DECREFs of `obj_array` for once `obj_array` is out of use.
*
* An empty kernel is submitted to `QRef` after `depERefs`, and the DECREFs wait
* for it. On an in-order queue submission also orders it after the work already
* submitted there, so the objects are covered regardless of providing dependent
* events. `QRef` should be the queue running the work that uses the objects.
* An event for the kernel is returned.
*
* Sets `*status` to 0 on success and 1 if scheduling threw, and returns nullptr
* on failure. A failed call has scheduled nothing, so the caller still owns the
* references it took.
*/
inline DPCTLSyclEventRef async_dec_ref(DPCTLSyclQueueRef QRef,
PyObject **obj_array,
size_t obj_array_size,
DPCTLSyclEventRef *depERefs,
size_t nDepERefs,
int *status)
{
using dpctl::syclinterface::wrap;

try {
sycl::queue &q = unwrap_queue(QRef);

std::vector<sycl::event> depends = unwrap_events(depERefs, nDepERefs);
std::vector<PyObject *> obj_vec(obj_array, obj_array + obj_array_size);

static constexpr int result_ok = 0;

const sycl::event marker =
dpctl::detail::submit_keep_alive_marker(q, depends);

// 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::move(obj_vec), {marker});

*status = result_ok;

return wrap<sycl::event>(e_ptr.release());
} catch (...) {
// no exception may escape into the calling Cython code
static constexpr int result_exception = 1;
*status = result_exception;
return nullptr;
}
}

/*!
* @brief Call `release(data)` once the work using what it releases is done.
*
* Submits an empty kernel to `QRef` after `depERefs`, as `async_dec_ref` does,
* and assigns its event to the `sycl::event` behind `markerERef`. `release` is
* called with the GIL held, on `dpctl`'s thread or on one that drains.
*
* @return 0 on success, and 1 if nothing was deferred, in which case `release`
* is never called.
*/
inline int defer_release(DPCTLSyclQueueRef QRef,
DPCTLSyclEventRef *depERefs,
size_t nDepERefs,
void (*release)(void *),
void *data,
DPCTLSyclEventRef markerERef)
{
using dpctl::syclinterface::unwrap;

try {
sycl::queue &q = unwrap_queue(QRef);
sycl::event *marker = unwrap<sycl::event>(markerERef);
if (!release || !marker) {
throw std::invalid_argument("A release and an event are required");
}

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

dpctl::detail::local_deferred_releases().defer(
{*marker}, [release, data]() { release(data); });

return 0;
} catch (...) {
// no exception may escape into the calling Cython code
return 1;
}
}
Loading
Loading