From fd15442f4fef55b5952825b62816f7f7fe079157 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Thu, 5 Nov 2026 17:26:01 +0800 Subject: [PATCH 1/2] fix(cuda.core): keep DLPack owner alive for derived StridedMemoryView Signed-off-by: Rui Luo --- cuda_core/cuda/core/_memoryview.pyx | 6 +++++ cuda_core/tests/test_utils_dlpack.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index 287a01a70bc..fd73cbb77ef 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -570,12 +570,18 @@ cdef class StridedMemoryView: """ Returns Buffer instance with the underlying data. If the SMV was created from a Buffer, it will return the same Buffer instance. + If the SMV was imported via DLPack, it will return a new instance owned by this view. Otherwise, it will create a new instance with owner set to the exporting object. """ cdef object buffer if self._buffer is None: if isinstance(self.exporting_obj, Buffer): buffer = self.exporting_obj + elif self.dl_tensor != NULL: + # This view holds the consumed capsule and calls its deleter in + # __dealloc__; the exporting object need not keep the memory alive. + # Not cached: a cached Buffer owned by self would form a cycle. + return Buffer.from_handle(self.ptr, 0, owner=self) else: buffer = Buffer.from_handle(self.ptr, 0, owner=self.exporting_obj) if self._buffer is None: diff --git a/cuda_core/tests/test_utils_dlpack.py b/cuda_core/tests/test_utils_dlpack.py index 4664b2da3f3..9489c977d3f 100644 --- a/cuda_core/tests/test_utils_dlpack.py +++ b/cuda_core/tests/test_utils_dlpack.py @@ -8,6 +8,8 @@ """ import ctypes +import gc +import weakref from pathlib import Path try: @@ -105,6 +107,41 @@ def test_dlpack_view_of_buffer_reuses_exporting_buffer(init_cuda): buffer.close() +class _EphemeralDLPackExporter: + """CPU exporter whose capsule is the only owner of a freshly created array.""" + + def __init__(self): + self.array_ref = None + + def __dlpack_device__(self): + return (int(DLDeviceType.kDLCPU), 0) + + def __dlpack__(self, **kwargs): + array = np.arange(4, dtype=np.int32) + self.array_ref = weakref.ref(array) + return array.__dlpack__(**kwargs) + + +@pytest.mark.agent_authored(model="claude-opus-5-5") +def test_dlpack_derived_view_keeps_capsule_owned_storage_alive(init_cuda): + """A derived view keeps capsule-owned storage alive until the last view is released.""" + exporter = _EphemeralDLPackExporter() + original = StridedMemoryView.from_dlpack(exporter, stream_ptr=-1) + derived = original.view(dtype=original.dtype) + ptr = original.ptr + del original + gc.collect() + + # Check liveness before reading, so a regression fails without touching freed memory. + assert exporter.array_ref() is not None + assert derived.ptr == ptr + assert np.from_dlpack(derived).tolist() == [0, 1, 2, 3] + + del derived + # No gc.collect(): the capsule must be released by reference counting, not a cycle. + assert exporter.array_ref() is None + + def test_dlpack_export_unversioned_capsule_and_deleter(): """``__dlpack__()`` with no ``max_version`` yields an *unversioned* unused DLPack capsule; dropping it unconsumed runs ``_smv_pycapsule_deleter`` on From 79525759d225d1c72b5ddba52c9efa74992d4a2d Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Fri, 6 Nov 2026 09:09:44 +0800 Subject: [PATCH 2/2] docs(cuda.core): add release note for derived view DLPack fix Signed-off-by: Rui Luo --- cuda_core/docs/source/release/1.3.0-notes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cuda_core/docs/source/release/1.3.0-notes.rst b/cuda_core/docs/source/release/1.3.0-notes.rst index e17192853d8..cad3a9fd911 100644 --- a/cuda_core/docs/source/release/1.3.0-notes.rst +++ b/cuda_core/docs/source/release/1.3.0-notes.rst @@ -354,6 +354,12 @@ Fixes and enhancements Python exception when an export or import fails, as required by the DLPack contract, instead of returning ``-1`` with no exception set. +- A view created with :meth:`~utils.StridedMemoryView.view` from a + ``StridedMemoryView`` imported through DLPack now keeps the imported + tensor alive. Previously, releasing the original view invoked the DLPack + deleter while the derived view still referenced the memory, which could + free a producer's temporary allocation too early. + - :meth:`~graph.MemcpyNode.update` and the executable view of a memcpy node now accept nodes recorded by stream capture. The driver stores the operands of a captured :meth:`Buffer.copy_from` or :meth:`Buffer.copy_to` as unified