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 5ce1cc1faa8..fcaa874f5e4 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -899,22 +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 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: + except BaseException: + # Rollback must also run for KeyboardInterrupt. _smv_versioned_deleter(dlm_tensor_ver) - return -1 + raise out[0] = dlm_tensor_ver return 0 @@ -922,45 +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 +) 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: - 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: +) 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: - return -1 + raise TypeError("py_object must be a StridedMemoryView") + _smv_setup_dltensor_borrowed(out, obj) return 0 @@ -968,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 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", [