From 9ea7be5f37d4c51ded0b9c552e81dbd975a9bfac Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sun, 9 Aug 2026 13:37:31 -0700 Subject: [PATCH] fix(core): honour DLTensor.byte_offset in StridedMemoryView.from_dlpack `view_as_dlpack` -- the path behind `StridedMemoryView.from_dlpack()`, `StridedMemoryView.from_any_interface()` on a DLPack producer, and the deprecated `StridedMemoryView(obj, stream_ptr)` constructor -- built the view pointer from `data` alone: buf.ptr = (dl_tensor.data) `byte_offset` is a mandatory DLTensor field, and a producer may legally leave the allocation base in `data` and encode a slice in `byte_offset`. The sibling capsule-consuming helper in the same file already does the right thing (`_smv_from_dlpack_capsule`, _memoryview.pyx:866): view.ptr = (dl_tensor.data) + (dl_tensor.byte_offset) Nothing downstream recovers the offset -- `layout_from_dlpack` reads only ndim/shape/strides/dtype -- so `view.ptr` was short by exactly `byte_offset` bytes, and every consumer that reads `ptr` alone (`as_tensor_map()`, the `__dlpack__` re-export, which writes `byte_offset = 0` and uses `ptr` as `data`, and the 16-byte alignment check in _tensor_map.pyx) silently operated on the wrong memory. No exception was raised. Test builds a real capsule, rewrites it to describe src[1:] as base + byte_offset, and asserts `ptr`, for both the versioned and unversioned capsule flavours. It uses the CPU device path, so it needs no GPU. --- cuda_core/cuda/core/_memoryview.pyx | 8 +++- cuda_core/docs/source/release/1.2.0-notes.rst | 7 +++ cuda_core/tests/test_utils_dlpack.py | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index bbe5a887700..2ea0839ea90 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -1102,7 +1102,13 @@ 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) + # byte_offset is a mandatory DLTensor field and a producer may leave the + # allocation base in ``data`` and encode a slice in ``byte_offset``. It + # must be folded into ``ptr`` here (as _smv_from_dlpack_capsule does), + # because every consumer -- as_tensor_map(), the __dlpack__ re-export + # (which writes ``byte_offset = 0`` and uses ``ptr`` as ``data``), the + # alignment checks -- reads ``ptr`` alone. + 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..7ef665b7f99 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,13 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- :meth:`StridedMemoryView.from_dlpack` (and therefore + :meth:`StridedMemoryView.from_any_interface` on a DLPack producer) now folds + the ``DLTensor.byte_offset`` field into ``ptr``. A producer that reports the + allocation base in ``data`` and encodes a slice in ``byte_offset`` produced a + view whose ``ptr`` was short by exactly ``byte_offset`` bytes, with no error + raised. The capsule-consuming path already handled it. + Deprecation Notices ------------------- diff --git a/cuda_core/tests/test_utils_dlpack.py b/cuda_core/tests/test_utils_dlpack.py index 9c2ff64a52f..008a3e25a4f 100644 --- a/cuda_core/tests/test_utils_dlpack.py +++ b/cuda_core/tests/test_utils_dlpack.py @@ -251,6 +251,51 @@ 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): + """``byte_offset`` must be folded into ``ptr``. + + It is a mandatory ``DLTensor`` field, and a producer is free to leave the + allocation base in ``data`` and encode a slice in ``byte_offset``. The + capsule-consuming helper does add it; the ``__dlpack__``-consuming one used + to drop it, so ``ptr`` pointed ``byte_offset`` bytes before the data with + no error anywhere -- and every consumer (``as_tensor_map``, the + ``__dlpack__`` re-export, the alignment checks) reads ``ptr`` alone. + """ + src = np.arange(8, dtype=np.int32) + offset = src.itemsize # skip exactly one element + 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)) + assert dlm.contents.dl_tensor.data == src.ctypes.data + assert dlm.contents.dl_tensor.byte_offset == 0 + + # Re-describe the same allocation as src[1:], the way a producer that + # reports the base pointer plus an offset would. + dlm.contents.dl_tensor.byte_offset = offset + dlm.contents.dl_tensor.shape[0] = src.size - 1 + + 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.shape == (src.size - 1,) + assert view.ptr == src.ctypes.data + offset + + _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)