Skip to content
Merged
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: 6 additions & 4 deletions cuda_core/cuda/core/_memoryview.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,15 @@ cdef class StridedMemoryView:
data = cpython.PyCapsule_GetPointer(
self.metadata, DLPACK_VERSIONED_TENSOR_USED_NAME)
dlm_tensor_ver = <DLManagedTensorVersioned*>data
dlm_tensor_ver.deleter(dlm_tensor_ver)
if dlm_tensor_ver.deleter != NULL:
dlm_tensor_ver.deleter(dlm_tensor_ver)
elif cpython.PyCapsule_IsValid(
self.metadata, DLPACK_TENSOR_USED_NAME):
data = cpython.PyCapsule_GetPointer(
self.metadata, DLPACK_TENSOR_USED_NAME)
dlm_tensor = <DLManagedTensor*>data
dlm_tensor.deleter(dlm_tensor)
if dlm_tensor.deleter != NULL:
dlm_tensor.deleter(dlm_tensor)

def view(
self, layout : _StridedLayout | None = None, dtype : numpy.dtype | None = None
Expand Down Expand Up @@ -1176,7 +1178,7 @@ cdef object dtype_dlpack_to_numpy(DLDataType* dtype):

cpdef StridedMemoryView view_as_cai(obj, stream_ptr, view=None):
cdef dict cai_data = obj.__cuda_array_interface__
if cai_data["version"] < 3:
if cai_data.get("version", 0) < 3:
raise BufferError("only CUDA Array Interface v3 or above is supported")
if cai_data.get("mask") is not None:
raise BufferError("mask is not supported")
Expand Down Expand Up @@ -1232,7 +1234,7 @@ cpdef StridedMemoryView view_as_cai(obj, stream_ptr, view=None):

cpdef StridedMemoryView view_as_array_interface(obj, view=None):
cdef dict data = obj.__array_interface__
if data["version"] < 3:
if data.get("version", 0) < 3:
raise BufferError("only NumPy Array Interface v3 or above is supported")
if data.get("mask") is not None:
raise BufferError("mask is not supported")
Expand Down
112 changes: 32 additions & 80 deletions cuda_core/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

import ctypes
import functools
import math

# TODO: replace optional imports with pytest.importorskip
Expand Down Expand Up @@ -676,20 +677,26 @@ def test_from_array_interface_unsupported_strides(init_cuda):
StridedMemoryView.from_array_interface(b)


def _make_cuda_array_interface_obj(*, shape, strides, typestr="<f8", data=(0, False), version=3):
return type(
"SyntheticCAI",
(),
{
"__cuda_array_interface__": {
"shape": shape,
"strides": strides,
"typestr": typestr,
"data": data,
"version": version,
}
},
)()
def _make_interface_obj(attr_name, *, shape, strides, typestr="<f8", data=(0, False), version=3, **extra):
"""Create an object exposing an array-interface dict as ``attr_name``.

``version=None`` omits the key entirely, so tests can exercise interfaces
that don't declare a version. Extra kwargs are added to the interface dict.
"""
iface = {
"shape": shape,
"strides": strides,
"typestr": typestr,
"data": data,
}
if version is not None:
iface["version"] = version
iface.update(extra)
return type(f"Synthetic{attr_name}", (), {attr_name: iface})()


_make_cuda_array_interface_obj = functools.partial(_make_interface_obj, "__cuda_array_interface__")
_make_array_interface_obj = functools.partial(_make_interface_obj, "__array_interface__")


def test_from_cuda_array_interface_unsupported_strides(init_cuda):
Expand Down Expand Up @@ -902,54 +909,12 @@ def test_dlpack_export_unsupported_dtype_raises():
bad_view.__dlpack__()


class _FakeCAIv2:
"""Object with CUDA Array Interface v2 (unsupported)."""

def __init__(self):
self.__cuda_array_interface__ = {
"version": 2,
"shape": (5,),
"typestr": "<f4",
"data": (0, False),
}


class _FakeCAIWithMask:
"""Object with CUDA Array Interface that has a mask."""

def __init__(self):
self.__cuda_array_interface__ = {
"version": 3,
"shape": (5,),
"typestr": "<f4",
"data": (0, False),
"mask": np.ones(5, dtype=bool),
}


class _FakeArrayInterfacev2:
"""Object with NumPy Array Interface v2 (unsupported)."""

def __init__(self, arr):
iface = dict(arr.__array_interface__)
iface["version"] = 2
self.__array_interface__ = iface


class _FakeArrayInterfaceWithMask:
"""Object with NumPy Array Interface that has a mask."""

def __init__(self, arr):
iface = dict(arr.__array_interface__)
iface["mask"] = np.ones(arr.shape, dtype=bool)
self.__array_interface__ = iface


def test_cai_v2_rejected():
"""CUDA Array Interface v2 raises BufferError."""
@pytest.mark.parametrize("version", [2, None])
def test_cai_version_rejected(version):
"""CUDA Array Interface below v3 (or missing version) raises BufferError."""
from cuda.core._memoryview import view_as_cai

obj = _FakeCAIv2()
obj = _make_cuda_array_interface_obj(shape=(5,), strides=None, version=version)
with pytest.raises(BufferError, match="v3 or above"):
view_as_cai(obj, stream_ptr=-1)

Expand All @@ -958,38 +923,26 @@ def test_cai_mask_rejected():
"""CUDA Array Interface with mask raises BufferError."""
from cuda.core._memoryview import view_as_cai

obj = _FakeCAIWithMask()
obj = _make_cuda_array_interface_obj(shape=(5,), strides=None, mask=np.ones(5, dtype=bool))
with pytest.raises(BufferError, match="mask is not supported"):
view_as_cai(obj, stream_ptr=-1)


class _FakeCAIv3:
"""Valid CUDA Array Interface v3 object (for stream=None test)."""

def __init__(self):
self.__cuda_array_interface__ = {
"version": 3,
"shape": (5,),
"typestr": "<f4",
"data": (0, False),
}


def test_cai_stream_none_rejected():
"""CUDA Array Interface with stream=None raises BufferError."""
from cuda.core._memoryview import view_as_cai

obj = _FakeCAIv3()
obj = _make_cuda_array_interface_obj(shape=(5,), strides=None)
with pytest.raises(BufferError, match="stream=None is ambiguous"):
view_as_cai(obj, stream_ptr=None)


def test_array_interface_v2_rejected():
"""NumPy Array Interface v2 raises BufferError."""
@pytest.mark.parametrize("version", [2, None])
def test_array_interface_version_rejected(version):
"""NumPy Array Interface below v3 (or missing version) raises BufferError."""
from cuda.core._memoryview import view_as_array_interface

arr = np.zeros(5, dtype=np.float32)
obj = _FakeArrayInterfacev2(arr)
obj = _make_array_interface_obj(shape=(5,), strides=None, version=version)
with pytest.raises(BufferError, match="v3 or above"):
view_as_array_interface(obj)

Expand All @@ -998,8 +951,7 @@ def test_array_interface_mask_rejected():
"""NumPy Array Interface with mask raises BufferError."""
from cuda.core._memoryview import view_as_array_interface

arr = np.zeros(5, dtype=np.float32)
obj = _FakeArrayInterfaceWithMask(arr)
obj = _make_array_interface_obj(shape=(5,), strides=None, mask=np.ones(5, dtype=bool))
with pytest.raises(BufferError, match="mask is not supported"):
view_as_array_interface(obj)

Expand Down
50 changes: 50 additions & 0 deletions cuda_core/tests/test_utils_dlpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,56 @@ class _DLTensor(ctypes.Structure):
]


class _DLManagedTensor(ctypes.Structure):
_fields_ = [
("dl_tensor", _DLTensor),
("manager_ctx", ctypes.c_void_p),
("deleter", ctypes.c_void_p),
]


class _DLManagedTensorVersioned(ctypes.Structure):
_fields_ = [
("version", _DLPackVersion),
("manager_ctx", ctypes.c_void_p),
("deleter", ctypes.c_void_p),
("flags", ctypes.c_uint64),
("dl_tensor", _DLTensor),
]


@pytest.mark.agent_authored(model="cursor-grok-4.5")
@pytest.mark.parametrize(
"max_version, capsule_name, managed_cls",
[
pytest.param(None, b"dltensor", _DLManagedTensor, id="unversioned"),
pytest.param((1, 0), b"dltensor_versioned", _DLManagedTensorVersioned, id="versioned"),
],
)
def test_from_dlpack_null_deleter_dealloc(max_version, capsule_name, managed_cls):
"""``__dealloc__`` must tolerate a capsule whose deleter is NULL."""
src = np.arange(6, dtype=np.int32)
base = StridedMemoryView.from_any_interface(src, stream_ptr=-1)
capsule = base.__dlpack__(max_version=max_version)
dlm = ctypes.cast(_PyCapsule_GetPointer(capsule, capsule_name), ctypes.POINTER(managed_cls))
# Steal the producer deleter so __dealloc__ sees NULL, then invoke it ourselves.
producer_deleter = ctypes.CFUNCTYPE(None, ctypes.POINTER(managed_cls))(dlm.contents.deleter)
dlm.contents.deleter = None

class _Export:
def __dlpack_device__(self):
return base.__dlpack_device__()

def __dlpack__(self, stream=None, max_version=None, **kwargs):
if capsule_name == b"dltensor" and max_version is not None:
raise TypeError("force unversioned")
return capsule

view = StridedMemoryView.from_dlpack(_Export(), stream_ptr=-1)
del view # __dealloc__ must not call the NULL deleter
producer_deleter(dlm)


_FN_FROM_PY = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p))
_FN_TO_PY = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p))
_FN_DLTENSOR_FROM_PY = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
Expand Down
Loading