Skip to content

Commit 8845a79

Browse files
rluo8Andy-Jost
andauthored
fix(cuda.core): preserve DLPack C exchange errors (#2939)
* fix(cuda.core): preserve DLPack C exchange errors Signed-off-by: Rui Luo <[email protected]> * 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 <[email protected]> Signed-off-by: Rui Luo <[email protected]> --------- Signed-off-by: Rui Luo <[email protected]> Co-authored-by: Andy Jost <[email protected]>
1 parent 265959d commit 8845a79

4 files changed

Lines changed: 76 additions & 42 deletions

File tree

‎cuda_core/cuda/core/_dlpack.pxd‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

@@ -90,26 +90,28 @@ cdef extern from "_include/dlpack.h":
9090
void (*SetError)(void* error_ctx, const char* kind, const char* message) noexcept
9191
)
9292

93+
# dlpack.h: these entry points return -1 on failure with a Python
94+
# exception set. `except -1` tells Cython so; it does not change the C type.
9395
ctypedef int (*DLPackManagedTensorFromPyObjectNoSync)(
9496
void* py_object,
9597
DLManagedTensorVersioned** out
96-
)
98+
) except -1
9799

98100
ctypedef int (*DLPackManagedTensorToPyObjectNoSync)(
99101
DLManagedTensorVersioned* tensor,
100102
void** out_py_object
101-
)
103+
) except -1
102104

103105
ctypedef int (*DLPackDLTensorFromPyObjectNoSync)(
104106
void* py_object,
105107
DLTensor* out
106-
)
108+
) except -1
107109

108110
ctypedef int (*DLPackCurrentWorkStream)(
109111
_DLDeviceType device_type,
110112
int32_t device_id,
111113
void** out_current_stream
112-
)
114+
) except -1
113115

114116
ctypedef struct DLPackExchangeAPIHeader:
115117
DLPackVersion version

‎cuda_core/cuda/core/_memoryview.pyx‎

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -899,79 +899,65 @@ cdef int _smv_managed_tensor_allocator(
899899
cdef int _smv_managed_tensor_from_py_object_no_sync(
900900
void* py_object,
901901
DLManagedTensorVersioned** out,
902-
) noexcept with gil:
902+
) except -1 with gil:
903903
cdef DLManagedTensorVersioned* dlm_tensor_ver = NULL
904904
if out == NULL:
905-
cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL")
906-
return -1
905+
raise RuntimeError("out cannot be NULL")
907906
out[0] = NULL
908907
cdef object obj = <object>py_object
909908
if not isinstance(obj, StridedMemoryView):
910-
cpython.PyErr_SetString(TypeError, b"py_object must be a StridedMemoryView")
911-
return -1
909+
raise TypeError("py_object must be a StridedMemoryView")
912910
try:
913911
dlm_tensor_ver = _smv_allocate_dlm_tensor_versioned()
914912
_smv_fill_managed_tensor_versioned(dlm_tensor_ver, <StridedMemoryView>obj)
915-
except Exception:
913+
except BaseException:
914+
# Rollback must also run for KeyboardInterrupt.
916915
_smv_versioned_deleter(dlm_tensor_ver)
917-
return -1
916+
raise
918917
out[0] = dlm_tensor_ver
919918
return 0
920919
921920
922921
cdef int _smv_managed_tensor_to_py_object_no_sync(
923922
DLManagedTensorVersioned* tensor,
924923
void** out_py_object,
925-
) noexcept with gil:
926-
cdef object capsule
927-
cdef object py_view
924+
) except -1 with gil:
928925
if out_py_object == NULL:
929-
cpython.PyErr_SetString(RuntimeError, b"out_py_object cannot be NULL")
930-
return -1
926+
raise RuntimeError("out_py_object cannot be NULL")
931927
out_py_object[0] = NULL
932928
if tensor == NULL:
933-
cpython.PyErr_SetString(RuntimeError, b"tensor cannot be NULL")
934-
return -1
935-
try:
936-
capsule = cpython.PyCapsule_New(
937-
<void*>tensor,
938-
DLPACK_VERSIONED_TENSOR_UNUSED_NAME,
939-
_smv_pycapsule_deleter,
940-
)
941-
py_view = _smv_from_dlpack_capsule(capsule, capsule)
942-
cpython.Py_INCREF(py_view)
943-
out_py_object[0] = <void*>py_view
944-
except Exception:
945-
return -1
929+
raise RuntimeError("tensor cannot be NULL")
930+
cdef object capsule = cpython.PyCapsule_New(
931+
<void*>tensor,
932+
DLPACK_VERSIONED_TENSOR_UNUSED_NAME,
933+
_smv_pycapsule_deleter,
934+
)
935+
cdef object py_view = _smv_from_dlpack_capsule(capsule, capsule)
936+
cpython.Py_INCREF(py_view)
937+
out_py_object[0] = <void*>py_view
946938
return 0
947939
948940
949941
cdef int _smv_dltensor_from_py_object_no_sync(
950942
void* py_object,
951943
DLTensor* out,
952-
) noexcept with gil:
944+
) except -1 with gil:
953945
if out == NULL:
954-
cpython.PyErr_SetString(RuntimeError, b"out cannot be NULL")
955-
return -1
946+
raise RuntimeError("out cannot be NULL")
956947
cdef object obj = <object>py_object
957948
if not isinstance(obj, StridedMemoryView):
958-
cpython.PyErr_SetString(TypeError, b"py_object must be a StridedMemoryView")
959-
return -1
960-
try:
961-
_smv_setup_dltensor_borrowed(out, <StridedMemoryView>obj)
962-
except Exception:
963-
return -1
949+
raise TypeError("py_object must be a StridedMemoryView")
950+
_smv_setup_dltensor_borrowed(out, <StridedMemoryView>obj)
964951
return 0
965952
966953
967954
cdef int _smv_current_work_stream(
968955
_DLDeviceType device_type,
969956
int32_t device_id,
970957
void** out_current_stream,
971-
) noexcept with gil:
958+
) except -1 with gil:
972959
if out_current_stream == NULL:
973-
cpython.PyErr_SetString(RuntimeError, b"out_current_stream cannot be NULL")
974-
return -1
960+
raise RuntimeError("out_current_stream cannot be NULL")
975961
# cuda.core has no global/current stream state today.
976962
out_current_stream[0] = NULL
977963
return 0

‎cuda_core/docs/source/release/1.3.0-notes.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,7 @@ Fixes and enhancements
169169
:attr:`graph.GraphNode.succ` views with an object that is not a
170170
:class:`~graph.GraphNode` is now a no-op instead of raising ``TypeError``,
171171
consistent with :class:`collections.abc.MutableSet` semantics.
172+
173+
- The ``StridedMemoryView`` DLPack C exchange API now preserves the underlying
174+
Python exception when an export or import fails, as required by the DLPack
175+
contract, instead of returning ``-1`` with no exception set.

‎cuda_core/tests/test_utils_dlpack.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,33 @@ def test_dlpack_c_exchange_api_dltensor_from_py_object_scalar():
501501
assert not out.strides
502502

503503

504+
@pytest.mark.agent_authored(model="gpt-5.6-sol")
505+
def test_dlpack_c_exchange_api_managed_tensor_export_failure_sets_exception():
506+
"""A view that DLPack cannot describe reports its BufferError through the
507+
managed-tensor export entry point and leaves the output tensor NULL."""
508+
api = _get_exchange_api()
509+
swapped = np.dtype(np.int32).newbyteorder("S")
510+
view = StridedMemoryView.from_array_interface(np.zeros(3, dtype=swapped))
511+
out = ctypes.c_void_p(123)
512+
513+
with pytest.raises(BufferError, match="Non-native-endian"):
514+
api.managed_tensor_from_py_object_no_sync(id(view), ctypes.byref(out))
515+
assert not out.value
516+
517+
518+
@pytest.mark.agent_authored(model="gpt-5.6-sol")
519+
def test_dlpack_c_exchange_api_dltensor_export_failure_sets_exception():
520+
"""A view that DLPack cannot describe reports its BufferError through the
521+
borrowed DLTensor export entry point."""
522+
api = _get_exchange_api()
523+
swapped = np.dtype(np.int32).newbyteorder("S")
524+
view = StridedMemoryView.from_array_interface(np.zeros(3, dtype=swapped))
525+
out = _DLTensor()
526+
527+
with pytest.raises(BufferError, match="Non-native-endian"):
528+
api.dltensor_from_py_object_no_sync(id(view), ctypes.byref(out))
529+
530+
504531
def test_dlpack_c_exchange_api_managed_tensor_roundtrip():
505532
"""``managed_tensor_from_py_object_no_sync`` produces a managed tensor that
506533
``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():
562589
assert not out_obj.value # set to NULL before the error
563590

564591

592+
@pytest.mark.agent_authored(model="gpt-5.6-sol")
593+
def test_dlpack_c_exchange_api_import_failure_sets_exception():
594+
"""Rejecting an unsupported DLPack device reports the underlying BufferError."""
595+
api = _get_exchange_api()
596+
tensor = _DLManagedTensorVersioned()
597+
tensor.version = _DLPackVersion(1, 0)
598+
tensor.dl_tensor.device = _DLDevice(7, 0) # kDLVulkan is unsupported by cuda.core
599+
tensor.dl_tensor.dtype = _DLDataType(0, 32, 1)
600+
out_obj = ctypes.c_void_p(123)
601+
602+
with pytest.raises(BufferError, match="device not supported"):
603+
api.managed_tensor_to_py_object_no_sync(ctypes.byref(tensor), ctypes.byref(out_obj))
604+
assert not out_obj.value
605+
606+
565607
@pytest.mark.parametrize(
566608
"device_type",
567609
[

0 commit comments

Comments
 (0)