From 0445a5f75d61ce54259e8f38e2cff362d785ae6f Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 10 Aug 2026 21:26:01 +0300 Subject: [PATCH] cuda.core: account for DLTensor.byte_offset in from_dlpack DLPack places a tensor's first element at data + byte_offset, but view_as_dlpack set StridedMemoryView.ptr from data alone. A producer that reports the allocation base in data and expresses a slice as byte_offset therefore produced a view pointing byte_offset bytes before the tensor, with nothing raised. The offset was also lost permanently on a round-trip, because the __dlpack__ re-export writes ptr back out as data with byte_offset = 0. The capsule-consuming importer in the same module already folds byte_offset in, so the two import paths disagreed about the same capsule. This makes view_as_dlpack match it. Closes #2592 Signed-off-by: Vyron Vasileiadis --- cuda_core/cuda/core/_memoryview.pyx | 2 +- cuda_core/docs/source/release/1.2.0-notes.rst | 12 ++++++ cuda_core/tests/test_utils_dlpack.py | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index bbe5a887700..843aa5b2265 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -1102,7 +1102,7 @@ cdef StridedMemoryView view_as_dlpack(obj, stream_ptr, view=None): cdef StridedMemoryView buf = StridedMemoryView() if view is None else view buf.dl_tensor = dl_tensor buf.metadata = capsule - buf.ptr = (dl_tensor.data) + buf.ptr = (dl_tensor.data) + (dl_tensor.byte_offset) buf.device_id = device_id buf.is_device_accessible = is_device_accessible buf.readonly = is_readonly diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index 120d2c2a253..4b0d6ddc7c9 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -73,6 +73,18 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- :meth:`~utils.StridedMemoryView.from_dlpack`, and + :meth:`~utils.StridedMemoryView.from_any_interface` on a DLPack producer, now + add ``DLTensor.byte_offset`` to ``ptr``. DLPack places a tensor's first + element at ``data + byte_offset``, but ``ptr`` was taken from ``data`` alone, + so a producer that reported an allocation base in ``data`` and expressed a + slice as an offset yielded a view pointing ``byte_offset`` bytes before the + tensor, with no error raised. The offset was also lost permanently on a + round-trip, since the ``__dlpack__`` re-export writes ``ptr`` back out as + ``data`` with ``byte_offset = 0``. The capsule-consuming path was already + correct. + (`#2592 `__) + Deprecation Notices ------------------- diff --git a/cuda_core/tests/test_utils_dlpack.py b/cuda_core/tests/test_utils_dlpack.py index 9c2ff64a52f..e8796ddd7d6 100644 --- a/cuda_core/tests/test_utils_dlpack.py +++ b/cuda_core/tests/test_utils_dlpack.py @@ -251,6 +251,46 @@ def __dlpack__(self, stream=None, max_version=None, **kwargs): producer_deleter(dlm) +@pytest.mark.agent_authored(model="claude-opus-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_honours_byte_offset(max_version, capsule_name, managed_cls): + """DLPack puts a tensor's first element at ``data + byte_offset``, so a producer + may report the allocation base in ``data`` and express a slice as an offset. + ``view.ptr`` must account for it, as the capsule-consuming path already does.""" + src = np.arange(9, dtype=np.int32) + # View only the first 8 elements, so shifting by one element below stays inside + # the allocation and a regression fails an assertion instead of reading OOB. + base = StridedMemoryView.from_any_interface(src[:8], stream_ptr=-1) + capsule = base.__dlpack__(max_version=max_version) + dlm = ctypes.cast(_PyCapsule_GetPointer(capsule, capsule_name), ctypes.POINTER(managed_cls)) + assert dlm.contents.dl_tensor.data == src.ctypes.data + assert dlm.contents.dl_tensor.byte_offset == 0 + + # Re-describe the same 8 elements as src[1:9]. byte_offset is the only field + # written: shape and strides share one producer-owned block, so leave them alone. + dlm.contents.dl_tensor.byte_offset = src.itemsize + + 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) + assert view.ptr == src.ctypes.data + src.itemsize + assert view.shape == (8,) + assert np.array_equal(np.from_dlpack(view), src[1:]) + + _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)