Coverage for cuda/core/_host_callback.pyx: 72.04%
93 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-05 01:11 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-05 01:11 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2#
3# SPDX-License-Identifier: Apache-2.0
5from cpython.buffer cimport (
6 PyBUF_SIMPLE,
7 PyBUF_WRITABLE,
8 PyBuffer_Release,
9 PyObject_CheckBuffer,
10 PyObject_GetBuffer,
11 Py_buffer,
12)
13from libc.stdint cimport uintptr_t
14from libc.stdlib cimport malloc
15from libc.string cimport memcpy as c_memcpy
17from cuda.bindings cimport cydriver
19from cuda.core._memory._buffer cimport Buffer, Buffer_check_open
20from cuda.core._rt cimport (
21 OpaqueHandle,
22 make_opaque_malloc,
23 make_opaque_py,
24)
26import sys
27import ctypes as ct
28from _ctypes import CFuncPtr
30import numpy
32from cuda.core._utils.pycompat import BufferProtocol
34__all__ = ['HOST_COPY_FN', 'pack_host_copy_args']
37# CUhostFn is `void (CUDA_CB *)(void*)`. CUDA_CB is __stdcall on Windows and
38# empty elsewhere, but ctypes only honors that distinction when it builds a
39# callback on 32-bit x86 Windows, which cuda.core does not support: on win-64
40# and ARM64 both CFUNCTYPE and WINFUNCTYPE produce a FFI_DEFAULT_ABI thunk. The
41# declared result and argument types are all that remain worth checking.
42_CUHOSTFN_HINT = (
43 "ctypes.CFUNCTYPE(None, ctypes.c_void_p)"
44 if sys.platform != "win32"
45 else "ctypes.CFUNCTYPE(None, ctypes.c_void_p) or "
46 "ctypes.WINFUNCTYPE(None, ctypes.c_void_p)"
47)
50def _cuhostfn_type_error(detail):
51 """Build the rejection message for a non-conforming ctypes callback."""
52 return TypeError( 1TUVWXrYZ
53 f"host callback {detail}; CUDA requires a callback matching CUhostFn " 1TUVWXrYZ
54 f"(void (*)(void*)), declared as {_CUHOSTFN_HINT}. " 1TUVWXrYZ
55 "Alternatively, pass a Python callable."
56 )
59def _validate_ctypes_host_callback(fn):
60 """Reject ctypes callbacks whose declared prototype is not CUhostFn.
62 ``restype`` and ``argtypes`` are the prototype the caller declared, and are
63 what CUDA calls through. A function pointer taken from a shared library
64 keeps ctypes' defaults -- a ``c_int`` result and unspecified arguments --
65 until the caller declares otherwise, so it must be declared to be accepted.
66 """
67 restype = fn.restype 1TijkEFGHUVWXrIJKLMNOlPQhmnoexyabczRuvdAYZ
68 argtypes = fn.argtypes 1TijkEFGHUVWXrIJKLMNOlPQhmnoexyabczRuvdAYZ
69 if restype is not None or argtypes is None or tuple(argtypes) != (ct.c_void_p,): 1TijkEFGHUVWXrIJKLMNOlPQhmnoexyabczRuvdAYZ
70 raise _cuhostfn_type_error( 1TUVWXrYZ
71 f"has prototype restype={restype!r}, argtypes={argtypes!r}") 1TUVWXrYZ
74cdef void _py_host_trampoline(void* data) noexcept with gil:
75 (<object>data)()
78cdef bint _is_py_host_trampoline(cydriver.CUhostFn fn) noexcept nogil:
79 return fn == <cydriver.CUhostFn>_py_host_trampoline 123456789!h#$%
82ctypedef struct _HostCopyArgs:
83 uintptr_t dst
84 uintptr_t src
85 size_t nbytes
88cdef void _host_copy(void* data) noexcept nogil:
89 cdef _HostCopyArgs* args = <_HostCopyArgs*>data
90 if args.nbytes:
91 c_memcpy(<void*>args.dst, <const void*>args.src, args.nbytes)
94cdef object _host_copy_fn_pointer():
95 return ct.CFUNCTYPE(None, ct.c_void_p)(<uintptr_t><cydriver.CUhostFn>_host_copy)
98HOST_COPY_FN: CFuncPtr = _host_copy_fn_pointer()
101cdef uintptr_t _host_copy_address(object obj, size_t nbytes, bint writable, str name) except? 0:
102 cdef Buffer buf
103 cdef Py_buffer view
104 # These export the buffer protocol over their own storage, so a value
105 # meant as an address would otherwise pack the address of that storage.
106 if isinstance(obj, (bool, numpy.integer, ct._SimpleCData)): 2a b c f p S w t s RbSbq ' B C D 0 g
107 raise TypeError(f"{name} must not be a {type(obj).__name__}; pass int(addr) for an address") 1BCD
108 if isinstance(obj, int): 2a b c f p S w t s RbSbq ' B C D 0 g
109 return <uintptr_t>obj 2c S s RbSb
110 if isinstance(obj, Buffer): 1abfpwtsq'BCD0g
111 buf = <Buffer>obj 1afpwtg
112 Buffer_check_open(buf) 1afpwtg
113 # A Buffer without a memory resource could only be checked with a
114 # driver query, which is not allowed on a host callback thread.
115 if buf._memory_resource is not None and not buf._memory_resource.is_host_accessible: 1afptg
116 raise ValueError(f"{name} is a Buffer that is not host-accessible") 1t
117 if nbytes > buf._size: 1afpg
118 raise ValueError(f"nbytes ({nbytes}) exceeds the size of {name} ({buf._size})") 1p
119 return <uintptr_t>buf.handle 1afg
120 if not PyObject_CheckBuffer(obj): 1abfpwtsq'BCD0g
121 raise TypeError( 10
122 f"{name} must be an int, a Buffer, or a buffer-protocol object, got {type(obj).__name__}") 10
123 PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE | PyBUF_WRITABLE if writable else PyBUF_SIMPLE) 1abfpwtsq'BCDg
124 try: 1abfpwtsqBCDg
125 if nbytes > <size_t>view.len: 1abfpwtsqBCDg
126 raise ValueError(f"nbytes ({nbytes}) exceeds the size of {name} ({view.len})") 1p
127 return <uintptr_t>view.buf 1abfpwtsqBCDg
128 finally:
129 PyBuffer_Release(&view)
132def pack_host_copy_args(
133 dst: int | Buffer | BufferProtocol, src: int | Buffer | BufferProtocol, nbytes: int) -> bytes:
134 """Pack the arguments of :data:`HOST_COPY_FN` into ``user_data`` bytes.
136 The result is a record of three native-width fields in this order:
137 ``uintptr_t dst``, ``uintptr_t src``, ``size_t nbytes``, with no padding.
138 ``struct.pack("@PPN", dst, src, nbytes)`` builds the same record, and
139 ``struct.unpack("@PPN", data)`` reads one back. A caller that keeps such a
140 record in memory of its own can pass its address as an ``int``
141 ``user_data`` instead.
143 ``dst`` and ``src`` are not retained. The caller must keep both alive,
144 and must not resize them (a ``bytearray``, for example), until the copy
145 has run.
147 Parameters
148 ----------
149 dst : int | :class:`Buffer` | :obj:`collections.abc.Buffer`
150 The destination. Only an ``int`` is a host address. A buffer-protocol
151 object must be contiguous and writable.
152 src : int | :class:`Buffer` | :obj:`collections.abc.Buffer`
153 The source. Only an ``int`` is a host address. A buffer-protocol
154 object must be contiguous.
155 nbytes : int
156 The number of bytes to copy. Zero packs a copy that does nothing.
158 Returns
159 -------
160 bytes
161 The packed record, to pass as ``user_data``.
163 Raises
164 ------
165 TypeError
166 If ``dst`` or ``src`` is not an ``int``, a :class:`Buffer`, or a
167 buffer-protocol object, or is a ``bool``, a NumPy integer scalar, or a
168 ctypes scalar such as ``ctypes.c_void_p``. A buffer-protocol object
169 that cannot export a contiguous view, or a writable one for ``dst``,
170 raises its own error.
171 ValueError
172 If ``nbytes`` exceeds the size of ``dst`` or ``src``, if a
173 :class:`Buffer` comes from a memory resource that is not
174 host-accessible, if an address is null, or if the ranges overlap.
175 OverflowError
176 If ``nbytes`` or an ``int`` address is negative or does not fit in a
177 native-width field.
178 RuntimeError
179 If ``dst`` or ``src`` is a closed :class:`Buffer`.
181 Notes
182 -----
183 This function makes no CUDA calls, so a Python host callback can call it,
184 for example to fill a record that a later :data:`HOST_COPY_FN` reads
185 through an ``int`` ``user_data``. It checks host accessibility only for a
186 :class:`Buffer` with a memory resource. For an ``int`` or a :class:`Buffer`
187 without one, the caller must ensure that the memory is host-accessible.
188 """
189 cdef size_t n = nbytes 2a b c f p S w t s WbXbRbSbq ' B C D 0 g
190 cdef _HostCopyArgs args
191 args.dst = _host_copy_address(dst, n, True, "dst") 2a b c f p S w t s RbSbq ' B C D 0 g
192 args.src = _host_copy_address(src, n, False, "src") 1abcfpSwtsqBCDg
193 args.nbytes = n 1abcfSsqg
194 if n: 1abcfSsqg
195 if args.dst == 0 or args.src == 0: 1abfSsqg
196 raise ValueError("dst and src must not be null") 1s
197 if (args.dst - args.src if args.dst > args.src else args.src - args.dst) < n: 1abfSqg
198 raise ValueError("dst and src overlap") 1q
199 return (<char*>&args)[:sizeof(args)] 1abcfSqg
202cdef void _resolve_host_callback(
203 object fn, object user_data,
204 cydriver.CUhostFn* out_fn, void** out_user_data,
205 OpaqueHandle* out_fn_owner, OpaqueHandle* out_data_owner) except *:
206 """Resolve a Python callable or ctypes CFuncPtr into a C callback pair and
207 the owners that keep it alive.
209 On return ``*out_fn`` / ``*out_user_data`` are ready to pass to
210 ``cuGraphAddHostNode`` or ``cuLaunchHostFunc``. ``*out_fn_owner`` owns the
211 callback object; ``*out_data_owner`` owns a copied ``user_data`` buffer and
212 is left null otherwise. The caller attaches both owners to the graph node.
214 ctypes callbacks are validated against the ``CUhostFn`` ABI before their
215 address is passed to CUDA.
216 """
217 cdef bint is_host_copy
218 if isinstance(fn, ct._CFuncPtr): 2* T i j + ( k E F G H U V W X r 2 TbI , - J . K / L : M ; N = O ? @ [ 3 ] ^ l P 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkbQ ! h # lbm 1 mbn nbob$ o pbqb% e ) rbsbtbubvbwbxbx yby a b c z R u v zbd A AbBbCbUbVbY Z DbEbFbGbHbIbJbKbLbMbNbObPbQb
219 _validate_ctypes_host_callback(fn) 1TijkEFGHUVWXrIJKLMNOlPQhmnoexyabczRuvdAYZ
220 out_fn[0] = <cydriver.CUhostFn><uintptr_t>ct.cast(fn, ct.c_void_p).value 1ijkEFGHrIJKLMNOlPQhmnoexyabczRuvdA
221 is_host_copy = out_fn[0] == <cydriver.CUhostFn>_host_copy 1ijkEFGHrIJKLMNOlPQhmnoexyabczRuvdA
222 if user_data is None: 1ijkEFGHrIJKLMNOlPQhmnoexyabczRuvdA
223 out_user_data[0] = NULL 1EFGHrIJKLMNOPQR
224 elif isinstance(user_data, int): 1ijklhmnoexyabczuvdA
225 out_user_data[0] = <void*><uintptr_t>user_data 1xyzA
226 else:
227 buf = bytes(user_data) 1ijklhmnoeabcuvd
228 if is_host_copy and len(buf) != sizeof(_HostCopyArgs): 1ijklhmnoeabcuvd
229 raise ValueError( 1uv
230 f"HOST_COPY_FN user_data must be {sizeof(_HostCopyArgs)} bytes " 1uv
231 f"from pack_host_copy_args, got {len(buf)}") 1uv
232 if len(buf): 1ijklhmnoeabcd
233 out_user_data[0] = malloc(len(buf)) 1ijklhmnoeabcd
234 if out_user_data[0] == NULL: 1ijklhmnoeabcd
235 raise MemoryError("failed to allocate user_data buffer")
236 c_memcpy(out_user_data[0], <const char*>buf, len(buf)) 1ijklhmnoeabcd
237 out_data_owner[0] = make_opaque_malloc(out_user_data[0]) 1ijklhmnoeabcd
238 else:
239 out_user_data[0] = NULL
240 if is_host_copy and out_user_data[0] == NULL: 1ijkEFGHrIJKLMNOlPQhmnoexyabczRdA
241 raise ValueError("HOST_COPY_FN requires user_data from pack_host_copy_args or a nonzero address") 1zR
242 else:
243 if not callable(fn): 2* + ( 2 Tb, - . / : ; = ? @ [ 3 ] ^ 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkb! # lb1 mbnbob$ pbqb% e ) rbsbtbubvbwbxbybzbd AbBbCbUbVbDbEbFbGbHbIbJbKbLbMbNbObPbQb
244 raise TypeError( 21 Ub
245 f"callback must be callable, got {type(fn).__name__}") 21 Ub
246 if user_data is not None: 2* + ( 2 Tb, - . / : ; = ? @ [ 3 ] ^ 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkb! # lb1 mbnbob$ pbqb% e ) rbsbtbubvbwbxbybzbd AbBbCbVbDbEbFbGbHbIbJbKbLbMbNbObPbQb
247 raise ValueError( 2( Tbe ) Vb
248 "user_data is only supported with ctypes function pointers")
249 out_fn[0] = <cydriver.CUhostFn>_py_host_trampoline 2* + ( 2 , - . / : ; = ? @ [ 3 ] ^ 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkb! # lb1 mbnbob$ pbqb% ) rbsbtbubvbwbxbybzbd AbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQb
250 out_user_data[0] = <void*>fn 2* + ( 2 , - . / : ; = ? @ [ 3 ] ^ 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkb! # lb1 mbnbob$ pbqb% ) rbsbtbubvbwbxbybzbd AbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQb
252 out_fn_owner[0] = make_opaque_py(fn) 2* i j + ( k E F G H r 2 I , - J . K / L : M ; N = O ? @ [ 3 ] ^ l P 4 5 _ ` { | } ~ abbbcbdbeb6 fb7 gb8 9 hbibjbkbQ ! h # lbm 1 mbn nbob$ o pbqb% e ) rbsbtbubvbwbxbx yby a b c zbd A AbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQb