diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fd31e5a50..babc03f0c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This release is compatible with NumPy 2.5. * Added `dpnp.tensor.broadcast_shapes` to align with the 2025.12 version of the Python array API [#3009](https://github.com/IntelPython/dpnp/pull/3009) * Added support for free-threaded Python builds [gh-3026](https://github.com/IntelPython/dpnp/pull/3026) * Added `dpnp.broadcast` class implementation [#2901](https://github.com/IntelPython/dpnp/pull/2901) +* Added `UsmNDArray_RemoveQueueRef` C API function to release a queue reference obtained from `UsmNDArray_GetQueueRef` [#3042](https://github.com/IntelPython/dpnp/pull/3042) ### Changed @@ -89,6 +90,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3037](https://github.com/IntelPython/dpnp/pull/3037) * Fixed `dpnp.all` and `dpnp.any` aborting when reducing over an empty axis (e.g. an array with a zero-length dimension) [#3021](https://github.com/IntelPython/dpnp/pull/3021) * Released the GIL before the blocking OneMKL DFT calls in the FFT extension [#3040](https://github.com/IntelPython/dpnp/pull/3040) +* Fixed a per-call `sycl::queue` leak in `usm_ndarray::get_queue()`/`get_device()` [#3042](https://github.com/IntelPython/dpnp/pull/3042) ### Security diff --git a/dpnp/include/dpnp4pybind11.hpp b/dpnp/include/dpnp4pybind11.hpp index d80dac8be2b..caed73b7316 100644 --- a/dpnp/include/dpnp4pybind11.hpp +++ b/dpnp/include/dpnp4pybind11.hpp @@ -88,6 +88,7 @@ class dpnp_capi int (*UsmNDArray_GetElementSize_)(PyUSMArrayObject *); int (*UsmNDArray_GetFlags_)(PyUSMArrayObject *); DPCTLSyclQueueRef (*UsmNDArray_GetQueueRef_)(PyUSMArrayObject *); + void (*UsmNDArray_RemoveQueueRef_)(DPCTLSyclQueueRef); py::ssize_t (*UsmNDArray_GetOffset_)(PyUSMArrayObject *); PyObject *(*UsmNDArray_GetUSMData_)(PyUSMArrayObject *); void (*UsmNDArray_SetWritableFlag_)(PyUSMArrayObject *, int); @@ -186,8 +187,9 @@ class dpnp_capi UsmNDArray_GetNDim_(nullptr), UsmNDArray_GetShape_(nullptr), UsmNDArray_GetStrides_(nullptr), UsmNDArray_GetTypenum_(nullptr), UsmNDArray_GetElementSize_(nullptr), UsmNDArray_GetFlags_(nullptr), - UsmNDArray_GetQueueRef_(nullptr), UsmNDArray_GetOffset_(nullptr), - UsmNDArray_GetUSMData_(nullptr), UsmNDArray_SetWritableFlag_(nullptr), + UsmNDArray_GetQueueRef_(nullptr), UsmNDArray_RemoveQueueRef_(nullptr), + UsmNDArray_GetOffset_(nullptr), UsmNDArray_GetUSMData_(nullptr), + UsmNDArray_SetWritableFlag_(nullptr), UsmNDArray_MakeSimpleFromMemory_(nullptr), UsmNDArray_MakeSimpleFromPtr_(nullptr), UsmNDArray_MakeFromPtr_(nullptr), USM_ARRAY_C_CONTIGUOUS_(0), @@ -215,6 +217,7 @@ class dpnp_capi this->UsmNDArray_GetElementSize_ = UsmNDArray_GetElementSize; this->UsmNDArray_GetFlags_ = UsmNDArray_GetFlags; this->UsmNDArray_GetQueueRef_ = UsmNDArray_GetQueueRef; + this->UsmNDArray_RemoveQueueRef_ = UsmNDArray_RemoveQueueRef; this->UsmNDArray_GetOffset_ = UsmNDArray_GetOffset; this->UsmNDArray_GetUSMData_ = UsmNDArray_GetUSMData; this->UsmNDArray_SetWritableFlag_ = UsmNDArray_SetWritableFlag; @@ -489,8 +492,16 @@ class usm_ndarray : public py::object PyUSMArrayObject *raw_ar = usm_array_ptr(); auto const &api = detail::dpnp_capi::get(); + // UsmNDArray_GetQueueRef_ returns an owning copy; free it through + // RemoveQueueRef (dpctl's DPCTLQueue_Delete) DPCTLSyclQueueRef QRef = api.UsmNDArray_GetQueueRef_(raw_ar); - return *(reinterpret_cast(QRef)); + auto qref_deleter = [](sycl::queue *q) { + detail::dpnp_capi::get().UsmNDArray_RemoveQueueRef_( + reinterpret_cast(q)); + }; + std::unique_ptr q_ptr{ + reinterpret_cast(QRef), qref_deleter}; + return *q_ptr; } sycl::device get_device() const @@ -498,8 +509,16 @@ class usm_ndarray : public py::object PyUSMArrayObject *raw_ar = usm_array_ptr(); auto const &api = detail::dpnp_capi::get(); + // UsmNDArray_GetQueueRef_ returns an owning copy; free it through + // RemoveQueueRef (dpctl's DPCTLQueue_Delete) DPCTLSyclQueueRef QRef = api.UsmNDArray_GetQueueRef_(raw_ar); - return reinterpret_cast(QRef)->get_device(); + auto qref_deleter = [](sycl::queue *q) { + detail::dpnp_capi::get().UsmNDArray_RemoveQueueRef_( + reinterpret_cast(q)); + }; + std::unique_ptr q_ptr{ + reinterpret_cast(QRef), qref_deleter}; + return q_ptr->get_device(); } int get_typenum() const diff --git a/dpnp/tensor/_usmarray.pyx b/dpnp/tensor/_usmarray.pyx index 9f85c71d546..78de01b936c 100644 --- a/dpnp/tensor/_usmarray.pyx +++ b/dpnp/tensor/_usmarray.pyx @@ -1850,6 +1850,12 @@ cdef api c_dpctl.DPCTLSyclQueueRef UsmNDArray_GetQueueRef(usm_ndarray arr): return arr.get_queue_ref() +cdef api void UsmNDArray_RemoveQueueRef(c_dpctl.DPCTLSyclQueueRef QRef): + """Delete a DPCTLSyclQueueRef previously returned by + UsmNDArray_GetQueueRef""" + c_dpctl.DPCTLQueue_Delete(QRef) # safe on NULL + + cdef api Py_ssize_t UsmNDArray_GetOffset(usm_ndarray arr): """Get offset of zero-index array element from the beginning of the USM allocation""" diff --git a/dpnp/tests/tensor/test_usm_ndarray_capi.py b/dpnp/tests/tensor/test_usm_ndarray_capi.py index 5ceb25fe7ec..d0e44f6712d 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_capi.py +++ b/dpnp/tests/tensor/test_usm_ndarray_capi.py @@ -229,8 +229,19 @@ def test_pyx_capi_get_queue_ref(): fn_restype=ctypes.c_void_p, fn_argtypes=(ctypes.py_object,), ) + remove_queue_ref_fn = _pyx_capi_fnptr_to_callable( + X, + "UsmNDArray_RemoveQueueRef", + b"void (DPCTLSyclQueueRef)", + fn_restype=None, + fn_argtypes=(ctypes.c_void_p,), + ) queue_ref = get_queue_ref_fn(X) # address of a copy, should be unequal assert queue_ref != X.sycl_queue.addressof_ref() + # UsmNDArray_GetQueueRef returns an owning copy; + # UsmNDArray_RemoveQueueRef must free it and be a no-op on NULL + remove_queue_ref_fn(queue_ref) + remove_queue_ref_fn(None) def test_pyx_capi_make_from_memory():