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
6 changes: 6 additions & 0 deletions cuda_core/cuda/core/_memoryview.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions cuda_core/docs/source/release/1.3.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,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
Expand Down
37 changes: 37 additions & 0 deletions cuda_core/tests/test_utils_dlpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"""

import ctypes
import gc
import weakref
from pathlib import Path

try:
Expand Down Expand Up @@ -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
Expand Down
Loading