From 4b6d8332bc45383502f774e97409058d74c05018 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Wed, 23 Sep 2026 14:43:58 +0800 Subject: [PATCH 1/2] fix(cuda.core): preserve DLPack C exchange errors Signed-off-by: Rui Luo --- cuda_core/cuda/core/_memoryview.pyx | 18 ++++++-- cuda_core/docs/source/release/1.3.0-notes.rst | 4 ++ cuda_core/tests/test_utils_dlpack.py | 42 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index 5ce1cc1faa8..9513e3a241c 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -901,6 +901,7 @@ cdef int _smv_managed_tensor_from_py_object_no_sync( DLManagedTensorVersioned** out, ) noexcept with gil: cdef DLManagedTensorVersioned* dlm_tensor_ver = NULL + cdef object caught = None if out == NULL: cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL") return -1 @@ -912,8 +913,11 @@ cdef int _smv_managed_tensor_from_py_object_no_sync( try: dlm_tensor_ver = _smv_allocate_dlm_tensor_versioned() _smv_fill_managed_tensor_versioned(dlm_tensor_ver, obj) - except Exception: + except Exception as exc: _smv_versioned_deleter(dlm_tensor_ver) + caught = exc + if caught is not None: + cpython.PyErr_SetObject(type(caught), caught) return -1 out[0] = dlm_tensor_ver return 0 @@ -925,6 +929,7 @@ cdef int _smv_managed_tensor_to_py_object_no_sync( ) noexcept with gil: cdef object capsule cdef object py_view + cdef object caught = None if out_py_object == NULL: cpython.PyErr_SetString(RuntimeError, b"out_py_object cannot be NULL") return -1 @@ -941,7 +946,10 @@ cdef int _smv_managed_tensor_to_py_object_no_sync( py_view = _smv_from_dlpack_capsule(capsule, capsule) cpython.Py_INCREF(py_view) out_py_object[0] = py_view - except Exception: + except Exception as exc: + caught = exc + if caught is not None: + cpython.PyErr_SetObject(type(caught), caught) return -1 return 0 @@ -950,6 +958,7 @@ cdef int _smv_dltensor_from_py_object_no_sync( void* py_object, DLTensor* out, ) noexcept with gil: + cdef object caught = None if out == NULL: cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL") return -1 @@ -959,7 +968,10 @@ cdef int _smv_dltensor_from_py_object_no_sync( return -1 try: _smv_setup_dltensor_borrowed(out, obj) - except Exception: + except Exception as exc: + caught = exc + if caught is not None: + cpython.PyErr_SetObject(type(caught), caught) return -1 return 0 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 12411e4b4d3..c58860c8425 100644 --- a/cuda_core/docs/source/release/1.3.0-notes.rst +++ b/cuda_core/docs/source/release/1.3.0-notes.rst @@ -169,3 +169,7 @@ Fixes and enhancements :attr:`graph.GraphNode.succ` views with an object that is not a :class:`~graph.GraphNode` is now a no-op instead of raising ``TypeError``, consistent with :class:`collections.abc.MutableSet` semantics. + +- The ``StridedMemoryView`` DLPack C exchange API now preserves the underlying + Python exception when an export or import fails, as required by the DLPack + contract, instead of returning ``-1`` with no exception set. diff --git a/cuda_core/tests/test_utils_dlpack.py b/cuda_core/tests/test_utils_dlpack.py index 70eaedc6118..724c2f9d95d 100644 --- a/cuda_core/tests/test_utils_dlpack.py +++ b/cuda_core/tests/test_utils_dlpack.py @@ -501,6 +501,33 @@ def test_dlpack_c_exchange_api_dltensor_from_py_object_scalar(): assert not out.strides +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_dlpack_c_exchange_api_managed_tensor_export_failure_sets_exception(): + """A view that DLPack cannot describe reports its BufferError through the + managed-tensor export entry point and leaves the output tensor NULL.""" + api = _get_exchange_api() + swapped = np.dtype(np.int32).newbyteorder("S") + view = StridedMemoryView.from_array_interface(np.zeros(3, dtype=swapped)) + out = ctypes.c_void_p(123) + + with pytest.raises(BufferError, match="Non-native-endian"): + api.managed_tensor_from_py_object_no_sync(id(view), ctypes.byref(out)) + assert not out.value + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_dlpack_c_exchange_api_dltensor_export_failure_sets_exception(): + """A view that DLPack cannot describe reports its BufferError through the + borrowed DLTensor export entry point.""" + api = _get_exchange_api() + swapped = np.dtype(np.int32).newbyteorder("S") + view = StridedMemoryView.from_array_interface(np.zeros(3, dtype=swapped)) + out = _DLTensor() + + with pytest.raises(BufferError, match="Non-native-endian"): + api.dltensor_from_py_object_no_sync(id(view), ctypes.byref(out)) + + def test_dlpack_c_exchange_api_managed_tensor_roundtrip(): """``managed_tensor_from_py_object_no_sync`` produces a managed tensor that ``managed_tensor_to_py_object_no_sync`` turns back into a StridedMemoryView. @@ -562,6 +589,21 @@ def test_dlpack_c_exchange_api_to_py_object_null_tensor(): assert not out_obj.value # set to NULL before the error +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_dlpack_c_exchange_api_import_failure_sets_exception(): + """Rejecting an unsupported DLPack device reports the underlying BufferError.""" + api = _get_exchange_api() + tensor = _DLManagedTensorVersioned() + tensor.version = _DLPackVersion(1, 0) + tensor.dl_tensor.device = _DLDevice(7, 0) # kDLVulkan is unsupported by cuda.core + tensor.dl_tensor.dtype = _DLDataType(0, 32, 1) + out_obj = ctypes.c_void_p(123) + + with pytest.raises(BufferError, match="device not supported"): + api.managed_tensor_to_py_object_no_sync(ctypes.byref(tensor), ctypes.byref(out_obj)) + assert not out_obj.value + + @pytest.mark.parametrize( "device_type", [ From 3e7c139510734434a54b9fef7938b38a879c7f69 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Tue, 29 Sep 2026 09:22:06 +0800 Subject: [PATCH 2/2] refactor(cuda.core): let Cython propagate DLPack exchange errors Declare the exchange API callbacks and their typedefs `except -1`, the contract dlpack.h documents, and raise instead of setting the error indicator by hand. KeyboardInterrupt and other BaseExceptions now also reach the consumer as -1 with the exception set. Co-authored-by: Andy Jost Signed-off-by: Rui Luo --- cuda_core/cuda/core/_dlpack.pxd | 12 +++-- cuda_core/cuda/core/_memoryview.pyx | 72 +++++++++-------------------- 2 files changed, 30 insertions(+), 54 deletions(-) diff --git a/cuda_core/cuda/core/_dlpack.pxd b/cuda_core/cuda/core/_dlpack.pxd index 328d98a4c2e..3f3551d1026 100644 --- a/cuda_core/cuda/core/_dlpack.pxd +++ b/cuda_core/cuda/core/_dlpack.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -90,26 +90,28 @@ cdef extern from "_include/dlpack.h": void (*SetError)(void* error_ctx, const char* kind, const char* message) noexcept ) + # dlpack.h: these entry points return -1 on failure with a Python + # exception set. `except -1` tells Cython so; it does not change the C type. ctypedef int (*DLPackManagedTensorFromPyObjectNoSync)( void* py_object, DLManagedTensorVersioned** out - ) + ) except -1 ctypedef int (*DLPackManagedTensorToPyObjectNoSync)( DLManagedTensorVersioned* tensor, void** out_py_object - ) + ) except -1 ctypedef int (*DLPackDLTensorFromPyObjectNoSync)( void* py_object, DLTensor* out - ) + ) except -1 ctypedef int (*DLPackCurrentWorkStream)( _DLDeviceType device_type, int32_t device_id, void** out_current_stream - ) + ) except -1 ctypedef struct DLPackExchangeAPIHeader: DLPackVersion version diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index 9513e3a241c..fcaa874f5e4 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -899,26 +899,21 @@ cdef int _smv_managed_tensor_allocator( cdef int _smv_managed_tensor_from_py_object_no_sync( void* py_object, DLManagedTensorVersioned** out, -) noexcept with gil: +) except -1 with gil: cdef DLManagedTensorVersioned* dlm_tensor_ver = NULL - cdef object caught = None if out == NULL: - cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL") - return -1 + raise RuntimeError("out cannot be NULL") out[0] = NULL cdef object obj = py_object if not isinstance(obj, StridedMemoryView): - cpython.PyErr_SetString(TypeError, b"py_object must be a StridedMemoryView") - return -1 + raise TypeError("py_object must be a StridedMemoryView") try: dlm_tensor_ver = _smv_allocate_dlm_tensor_versioned() _smv_fill_managed_tensor_versioned(dlm_tensor_ver, obj) - except Exception as exc: + except BaseException: + # Rollback must also run for KeyboardInterrupt. _smv_versioned_deleter(dlm_tensor_ver) - caught = exc - if caught is not None: - cpython.PyErr_SetObject(type(caught), caught) - return -1 + raise out[0] = dlm_tensor_ver return 0 @@ -926,53 +921,33 @@ cdef int _smv_managed_tensor_from_py_object_no_sync( cdef int _smv_managed_tensor_to_py_object_no_sync( DLManagedTensorVersioned* tensor, void** out_py_object, -) noexcept with gil: - cdef object capsule - cdef object py_view - cdef object caught = None +) except -1 with gil: if out_py_object == NULL: - cpython.PyErr_SetString(RuntimeError, b"out_py_object cannot be NULL") - return -1 + raise RuntimeError("out_py_object cannot be NULL") out_py_object[0] = NULL if tensor == NULL: - cpython.PyErr_SetString(RuntimeError, b"tensor cannot be NULL") - return -1 - try: - capsule = cpython.PyCapsule_New( - tensor, - DLPACK_VERSIONED_TENSOR_UNUSED_NAME, - _smv_pycapsule_deleter, - ) - py_view = _smv_from_dlpack_capsule(capsule, capsule) - cpython.Py_INCREF(py_view) - out_py_object[0] = py_view - except Exception as exc: - caught = exc - if caught is not None: - cpython.PyErr_SetObject(type(caught), caught) - return -1 + raise RuntimeError("tensor cannot be NULL") + cdef object capsule = cpython.PyCapsule_New( + tensor, + DLPACK_VERSIONED_TENSOR_UNUSED_NAME, + _smv_pycapsule_deleter, + ) + cdef object py_view = _smv_from_dlpack_capsule(capsule, capsule) + cpython.Py_INCREF(py_view) + out_py_object[0] = py_view return 0 cdef int _smv_dltensor_from_py_object_no_sync( void* py_object, DLTensor* out, -) noexcept with gil: - cdef object caught = None +) except -1 with gil: if out == NULL: - cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL") - return -1 + raise RuntimeError("out cannot be NULL") cdef object obj = py_object if not isinstance(obj, StridedMemoryView): - cpython.PyErr_SetString(TypeError, b"py_object must be a StridedMemoryView") - return -1 - try: - _smv_setup_dltensor_borrowed(out, obj) - except Exception as exc: - caught = exc - if caught is not None: - cpython.PyErr_SetObject(type(caught), caught) - return -1 + raise TypeError("py_object must be a StridedMemoryView") + _smv_setup_dltensor_borrowed(out, obj) return 0 @@ -980,10 +955,9 @@ cdef int _smv_current_work_stream( _DLDeviceType device_type, int32_t device_id, void** out_current_stream, -) noexcept with gil: +) except -1 with gil: if out_current_stream == NULL: - cpython.PyErr_SetString(RuntimeError, b"out_current_stream cannot be NULL") - return -1 + raise RuntimeError("out_current_stream cannot be NULL") # cuda.core has no global/current stream state today. out_current_stream[0] = NULL return 0