Skip to content

Commit 0cfe451

Browse files
committed
fix(cuda.bindings): backport host-function graph replay fix to 12.9.x
Backport #3061 to the CUDA 12.9 maintenance branch. Regenerate the host-function wrappers for CUDA 12.9 and port callback assertions, graph replay coverage, and lifetime guidance. Signed-off-by: Andy Jost <ajost@nvidia.com>
1 parent 1635cf2 commit 0cfe451

7 files changed

Lines changed: 153 additions & 72 deletions

File tree

‎cuda_bindings/cuda/bindings/driver.pyx‎

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
# This code was automatically generated with version 12.9.0. Do not modify it directly.
5-
# CYTHON-BINDINGS-GENERATED-DO-NOT-MODIFY-THIS-FILE: format=1; content-sha256=5883cb5633a3d97ee7d7582823371bd1928ee4a2388183d7433daf02032ece87
5+
# CYTHON-BINDINGS-GENERATED-DO-NOT-MODIFY-THIS-FILE: format=1; content-sha256=7bd998915de52155f2e27696c9a4d514071b9c721d94e05f0a00c7a07394a961
66
from typing import Any, Optional
77
import cython
88
import ctypes
@@ -39995,19 +39995,6 @@ def cuLaunchCooperativeKernelMultiDevice(launchParamsList : Optional[tuple[CUDA_
3999539995
free(cylaunchParamsList)
3999639996
return (_CUresult(err),)
3999739997

39998-
ctypedef struct cuHostCallbackData_st:
39999-
cydriver.CUhostFn callback
40000-
void *userData
40001-
40002-
ctypedef cuHostCallbackData_st cuHostCallbackData
40003-
40004-
@cython.show_performance_hints(False)
40005-
cdef void cuHostCallbackWrapper(void *data) nogil:
40006-
cdef cuHostCallbackData *cbData = <cuHostCallbackData *>data
40007-
with gil:
40008-
cbData.callback(cbData.userData)
40009-
free(cbData)
40010-
4001139998
@cython.embedsignature(True)
4001239999
def cuLaunchHostFunc(hStream, fn, userData):
4001340000
""" Enqueues a host function call in a stream.
@@ -40087,18 +40074,8 @@ def cuLaunchHostFunc(hStream, fn, userData):
4008740074
cyhStream = <cydriver.CUstream><void_ptr>phStream
4008840075
cdef _HelperInputVoidPtrStruct cyuserDataHelper
4008940076
cdef void* cyuserData = _helper_input_void_ptr(userData, &cyuserDataHelper)
40090-
40091-
cdef cuHostCallbackData *cbData = NULL
40092-
cbData = <cuHostCallbackData *>malloc(sizeof(cbData[0]))
40093-
if cbData == NULL:
40094-
return (CUresult.CUDA_ERROR_OUT_OF_MEMORY,)
40095-
cbData.callback = cyfn
40096-
cbData.userData = cyuserData
40097-
4009840077
with nogil:
40099-
err = cydriver.cuLaunchHostFunc(cyhStream, <cydriver.CUhostFn>cuHostCallbackWrapper, <void *>cbData)
40100-
if err != cydriver.CUDA_SUCCESS:
40101-
free(cbData)
40078+
err = cydriver.cuLaunchHostFunc(cyhStream, cyfn, cyuserData)
4010240079
_helper_input_void_ptr_free(&cyuserDataHelper)
4010340080
return (_CUresult(err),)
4010440081

‎cuda_bindings/cuda/bindings/runtime.pyx.in‎

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
# This code was automatically generated with version 12.9.0. Do not modify it directly.
5-
# CYTHON-BINDINGS-GENERATED-DO-NOT-MODIFY-THIS-FILE: format=1; content-sha256=d6df3522c27bd5c4ff1ecf1bf0167f902aab2b381d9db0c473658a2922f5b6cd
5+
# CYTHON-BINDINGS-GENERATED-DO-NOT-MODIFY-THIS-FILE: format=1; content-sha256=e8e09d2b9dc5c50f5e7a6d85f94bd57c0c0776668393328be79deebdefce1c11
66
from typing import Any, Optional
77
import cython
88
import ctypes
@@ -24776,19 +24776,6 @@ def cudaFuncSetAttribute(func, attr not None : cudaFuncAttribute, int value):
2477624776

2477724777
{{if 'cudaLaunchHostFunc' in found_functions}}
2477824778

24779-
ctypedef struct cudaStreamHostCallbackData_st:
24780-
cyruntime.cudaHostFn_t callback
24781-
void *userData
24782-
24783-
ctypedef cudaStreamHostCallbackData_st cudaStreamHostCallbackData
24784-
24785-
@cython.show_performance_hints(False)
24786-
cdef void cudaStreamRtHostCallbackWrapper(void *data) nogil:
24787-
cdef cudaStreamHostCallbackData *cbData = <cudaStreamHostCallbackData *>data
24788-
with gil:
24789-
cbData.callback(cbData.userData)
24790-
free(cbData)
24791-
2479224779
@cython.embedsignature(True)
2479324780
def cudaLaunchHostFunc(stream, fn, userData):
2479424781
""" Enqueues a host function call in a stream.
@@ -24869,20 +24856,9 @@ def cudaLaunchHostFunc(stream, fn, userData):
2486924856
cystream = <cyruntime.cudaStream_t><void_ptr>pstream
2487024857
cdef _HelperInputVoidPtrStruct cyuserDataHelper
2487124858
cdef void* cyuserData = _helper_input_void_ptr(userData, &cyuserDataHelper)
24872-
24873-
cdef cudaStreamHostCallbackData *cbData = NULL
24874-
cbData = <cudaStreamHostCallbackData *>malloc(sizeof(cbData[0]))
24875-
if cbData == NULL:
24876-
return (cudaError_t.cudaErrorMemoryAllocation,)
24877-
cbData.callback = cyfn
24878-
cbData.userData = cyuserData
24879-
2488024859
with nogil:
24881-
err = cyruntime.cudaLaunchHostFunc(cystream, <cyruntime.cudaHostFn_t>cudaStreamRtHostCallbackWrapper, <void *>cbData)
24882-
if err != cyruntime.cudaSuccess:
24883-
free(cbData)
24860+
err = cyruntime.cudaLaunchHostFunc(cystream, cyfn, cyuserData)
2488424861
_helper_input_void_ptr_free(&cyuserDataHelper)
24885-
2488624862
return (_cudaError_t(err),)
2488724863
{{endif}}
2488824864

‎cuda_bindings/docs/source/overview.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,22 @@ kernelTypes = (
556556
kernelArgs = (kernelValues, kernelTypes)
557557
```
558558

559+
560+
(host-function-lifetime)=
561+
## Host functions
562+
563+
{func}`cuda.bindings.driver.cuLaunchHostFunc` and
564+
{func}`cuda.bindings.runtime.cudaLaunchHostFunc` enqueue a host function on a
565+
stream. The caller owns the memory behind `fn` and `userData`; the bindings
566+
do not keep the corresponding Python objects alive.
567+
568+
Keep strong references to the callback object (such as a `ctypes.CFUNCTYPE`
569+
instance) and the storage backing `userData` until the host function has
570+
completed, as confirmed by stream synchronization. A `CUhostFn` or
571+
`cudaHostFn_t` wrapper alone does not retain the underlying ctypes callback.
572+
573+
When a host function is captured into a CUDA graph, it runs on every launch of
574+
that graph. Keep both references alive until the graph and all graph executables
575+
containing that host node have been destroyed and all pending launches have
576+
completed. Releasing either object after the first invocation can cause later
577+
graph launches to access freed memory.

‎cuda_bindings/docs/source/release/12.9.9-notes.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,15 @@ Bugfixes
345345
classes in ``cudla``, ``cufile``, and ``nvml``.
346346
(`PR #2875 <https://github.com/NVIDIA/cuda-python/pull/2875>`_)
347347

348+
- ``driver.cuLaunchHostFunc`` and ``runtime.cudaLaunchHostFunc`` now pass the host
349+
function and its user data to CUDA unchanged. They used to route the call through
350+
an internal trampoline that freed its state after the first invocation, so a host
351+
function captured into a CUDA graph crashed on the second launch of that graph.
352+
(`Issue #790 <https://github.com/NVIDIA/cuda-python/issues/790>`_)
353+
Callers must retain the callback object and user data storage for the lifetime
354+
of the graph and its executables, including pending launches; see
355+
:ref:`host-function-lifetime`.
356+
348357
Known issues
349358
------------
350359

‎cuda_bindings/tests/pytest.ini‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
[pytest]
55
norecursedirs = cython
6+
markers =
7+
agent_authored(model): agent-authored test not yet materially human-reviewed
8+
human_reviewed: agent-authored test materially reviewed or rewritten by a human
9+
human_authored: test authored primarily by a human

‎cuda_bindings/tests/test_cuda.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,58 @@ def test_stream_capture():
779779
pass
780780

781781

782+
@pytest.mark.agent_authored(model="gpt-6")
783+
def test_cuda_launch_host_func_graph_replay():
784+
(err,) = cuda.cuInit(0)
785+
assert err == cuda.CUresult.CUDA_SUCCESS
786+
err, device = cuda.cuDeviceGet(0)
787+
assert err == cuda.CUresult.CUDA_SUCCESS
788+
err, ctx = cuda.cuCtxCreate(0, device)
789+
assert err == cuda.CUresult.CUDA_SUCCESS
790+
try:
791+
# A host function captured into a graph runs once per graph launch. The
792+
# bindings must hand the function pointer and the user data to CUDA
793+
# unchanged so that every launch reaches the same function (issue #790).
794+
counter = ctypes.c_int(0)
795+
796+
@ctypes.CFUNCTYPE(None, ctypes.c_void_p)
797+
def host_fn(userData):
798+
ctypes.c_int.from_address(userData).value += 1
799+
800+
callback = cuda.CUhostFn(_ptr=ctypes.addressof(host_fn))
801+
err, stream = cuda.cuStreamCreate(0)
802+
assert err == cuda.CUresult.CUDA_SUCCESS
803+
try:
804+
(err,) = cuda.cuStreamBeginCapture(stream, cuda.CUstreamCaptureMode.CU_STREAM_CAPTURE_MODE_GLOBAL)
805+
assert err == cuda.CUresult.CUDA_SUCCESS
806+
(err,) = cuda.cuLaunchHostFunc(stream, callback, ctypes.addressof(counter))
807+
assert err == cuda.CUresult.CUDA_SUCCESS
808+
err, graph = cuda.cuStreamEndCapture(stream)
809+
assert err == cuda.CUresult.CUDA_SUCCESS
810+
try:
811+
err, graphExec = cuda.cuGraphInstantiate(graph, 0)
812+
assert err == cuda.CUresult.CUDA_SUCCESS
813+
try:
814+
for launch in range(1, 6):
815+
(err,) = cuda.cuGraphLaunch(graphExec, stream)
816+
assert err == cuda.CUresult.CUDA_SUCCESS
817+
(err,) = cuda.cuStreamSynchronize(stream)
818+
assert err == cuda.CUresult.CUDA_SUCCESS
819+
assert counter.value == launch
820+
finally:
821+
(err,) = cuda.cuGraphExecDestroy(graphExec)
822+
assert err == cuda.CUresult.CUDA_SUCCESS
823+
finally:
824+
(err,) = cuda.cuGraphDestroy(graph)
825+
assert err == cuda.CUresult.CUDA_SUCCESS
826+
finally:
827+
(err,) = cuda.cuStreamDestroy(stream)
828+
assert err == cuda.CUresult.CUDA_SUCCESS
829+
finally:
830+
(err,) = cuda.cuCtxDestroy(ctx)
831+
assert err == cuda.CUresult.CUDA_SUCCESS
832+
833+
782834
def test_profiler():
783835
(err,) = cuda.cuInit(0)
784836
assert err == cuda.CUresult.CUDA_SUCCESS

‎cuda_bindings/tests/test_cudart.py‎

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,33 +1291,32 @@ def test_cudart_eglFrame():
12911291
assert int(frame.frame.pPitch[2].ysize) == 0
12921292

12931293

1294-
def cudart_func_stream_callback(use_host_api):
1294+
@pytest.mark.parametrize("use_host_api", [False, True], ids=["stream_callback", "host_function"])
1295+
@pytest.mark.agent_authored(model="gpt-6")
1296+
def test_cudart_func_callback(use_host_api):
12951297
class testStruct(ctypes.Structure):
12961298
_fields_ = [
12971299
("a", ctypes.c_int),
12981300
("b", ctypes.c_int),
12991301
("c", ctypes.c_int),
13001302
]
13011303

1304+
calls = []
1305+
stream_results = []
1306+
13021307
def task_callback_host(userData):
13031308
data = testStruct.from_address(userData)
1304-
assert data.a == 1
1305-
assert data.b == 2
1306-
assert data.c == 3
1307-
return 0
1309+
calls.append((userData, data.a, data.b, data.c))
13081310

13091311
def task_callback_stream(stream, status, userData):
1310-
data = testStruct.from_address(userData)
1311-
assert data.a == 1
1312-
assert data.b == 2
1313-
assert data.c == 3
1314-
return 0
1312+
stream_results.append((stream, status))
1313+
task_callback_host(userData)
13151314

13161315
if use_host_api:
1317-
callback_type = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
1316+
callback_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
13181317
target_task = task_callback_host
13191318
else:
1320-
callback_type = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
1319+
callback_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
13211320
target_task = task_callback_stream
13221321

13231322
# Construct ctype data
@@ -1333,19 +1332,64 @@ def task_callback_stream(stream, status, userData):
13331332
# Run
13341333
err, stream = cudart.cudaStreamCreate()
13351334
assertSuccess(err)
1336-
if use_host_api:
1337-
(err,) = cudart.cudaLaunchHostFunc(stream, callback, ctypes.addressof(c_data))
1335+
try:
1336+
if use_host_api:
1337+
(err,) = cudart.cudaLaunchHostFunc(stream, callback, ctypes.addressof(c_data))
1338+
else:
1339+
(err,) = cudart.cudaStreamAddCallback(stream, callback, ctypes.addressof(c_data), 0)
13381340
assertSuccess(err)
1339-
else:
1340-
(err,) = cudart.cudaStreamAddCallback(stream, callback, ctypes.addressof(c_data), 0)
1341+
(err,) = cudart.cudaStreamSynchronize(stream)
13411342
assertSuccess(err)
1342-
(err,) = cudart.cudaDeviceSynchronize()
1343-
assertSuccess(err)
13441343

1344+
# ctypes swallows callback exceptions, so assert on the test thread.
1345+
assert calls == [(ctypes.addressof(c_data), 1, 2, 3)]
1346+
if not use_host_api:
1347+
assert stream_results == [(int(stream), int(cudart.cudaError_t.cudaSuccess))]
1348+
finally:
1349+
(err,) = cudart.cudaStreamDestroy(stream)
1350+
assertSuccess(err)
1351+
1352+
1353+
@pytest.mark.agent_authored(model="claude-fable-5-1")
1354+
def test_cudart_host_func_graph_replay():
1355+
# A host function captured into a graph runs once per graph launch. The
1356+
# bindings must hand the function pointer and the user data to CUDA
1357+
# unchanged so that every launch reaches the same function (issue #790).
1358+
counter = ctypes.c_int(0)
13451359

1346-
def test_cudart_func_callback():
1347-
cudart_func_stream_callback(use_host_api=False)
1348-
cudart_func_stream_callback(use_host_api=True)
1360+
@ctypes.CFUNCTYPE(None, ctypes.c_void_p)
1361+
def host_fn(userData):
1362+
ctypes.c_int.from_address(userData).value += 1
1363+
1364+
callback = cudart.cudaHostFn_t(_ptr=ctypes.addressof(host_fn))
1365+
err, stream = cudart.cudaStreamCreate()
1366+
assertSuccess(err)
1367+
try:
1368+
(err,) = cudart.cudaStreamBeginCapture(stream, cudart.cudaStreamCaptureMode.cudaStreamCaptureModeGlobal)
1369+
assertSuccess(err)
1370+
(err,) = cudart.cudaLaunchHostFunc(stream, callback, ctypes.addressof(counter))
1371+
assertSuccess(err)
1372+
err, graph = cudart.cudaStreamEndCapture(stream)
1373+
assertSuccess(err)
1374+
try:
1375+
err, graphExec = cudart.cudaGraphInstantiate(graph, 0)
1376+
assertSuccess(err)
1377+
try:
1378+
for launch in range(1, 6):
1379+
(err,) = cudart.cudaGraphLaunch(graphExec, stream)
1380+
assertSuccess(err)
1381+
(err,) = cudart.cudaStreamSynchronize(stream)
1382+
assertSuccess(err)
1383+
assert counter.value == launch
1384+
finally:
1385+
(err,) = cudart.cudaGraphExecDestroy(graphExec)
1386+
assertSuccess(err)
1387+
finally:
1388+
(err,) = cudart.cudaGraphDestroy(graph)
1389+
assertSuccess(err)
1390+
finally:
1391+
(err,) = cudart.cudaStreamDestroy(stream)
1392+
assertSuccess(err)
13491393

13501394

13511395
@pytest.mark.skipif(

0 commit comments

Comments
 (0)