Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cuda_core/cuda/core/_cpp/rt/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ cuda/core/
├── context_scope.hpp # Scoped-context helpers (namespace detail)
├── internal.hpp # Registry, cleanup wrappers, deferred-cleanup item (namespace detail)
├── py_report.cpp, py_deferred_cleanup.cpp # Python-coupled bodies
├── host_launch.cpp # The stream-side host function launch record
└── context.cpp, stream.cpp, event.cpp, memory.cpp, program.cpp,
graph.cpp, graph_exec.cpp, texture.cpp # One resource family per file
```
Expand Down
4 changes: 4 additions & 0 deletions cuda_core/cuda/core/_cpp/rt/GRAPH_ATTACHMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ deleters call CUDA or run Python finalizers.
destructor callback only adds the attachment to the process-lifetime
`DeferredCleanupQueue` and requests a `Py_AddPendingCall`.

The host launch record of `host_launch.cpp` is the one producer that is not a
graph attachment;
its trampoline enqueues the record after the user function returns.

One pending call drains all queued attachments from Python's main thread. The
queue coalesces work because CPython's pending-call queue is bounded, and there
could be many more deferred cleanup items than allowed pending calls. If
Expand Down
8 changes: 8 additions & 0 deletions cuda_core/cuda/core/_cpp/rt/api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ StreamHandle get_per_thread_stream();
// token is created. Returns an empty handle for an empty h_context.
StreamHandle create_context_bound_legacy_stream(const ContextHandle &h_context);

// ============================================================================
// Host function launch
// ============================================================================

CUresult launch_host_function(
CUstream stream, CUhostFn fn, void *user_data, OpaqueHandle fn_owner, OpaqueHandle data_owner
) noexcept;

// ============================================================================
// Event handle functions
// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions cuda_core/cuda/core/_cpp/rt/driver_api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ namespace cuda_core::rt {
/* cuda-bindings requests 7000 (PTDS) or 2000 (legacy) */ \
X(cuStreamSynchronize, 7000) \
X(cuStreamIsCapturing, 10000) \
X(cuLaunchHostFunc, 10000) \
X(cuThreadExchangeStreamCaptureMode, 10010)

#define CUDA_CORE_DECLARE_DRIVER_FN(name, introduced) extern decltype(&name) p_##name;
Expand Down
57 changes: 57 additions & 0 deletions cuda_core/cuda/core/_cpp/rt/host_launch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

#include "py.hpp"
#include "api.hpp"
#include "driver_api.hpp"
#include "internal.hpp"
#include <new>
#include <utility>

namespace cuda_core::rt {

namespace {

struct HostLaunchRecord : detail::DeferredCleanupItem {
CUhostFn fn;
void *user_data;
OpaqueHandle fn_owner;
OpaqueHandle data_owner;

HostLaunchRecord(CUhostFn fn, void *user_data, OpaqueHandle fn_owner, OpaqueHandle data_owner) noexcept
: fn(fn), user_data(user_data), fn_owner(std::move(fn_owner)), data_owner(std::move(data_owner))
{
}
};

void
host_launch_trampoline(void *p) noexcept
{
auto *rec = static_cast<HostLaunchRecord *>(p);
rec->fn(rec->user_data);
// Owners may run Python finalizers or CUDA calls, so release them on the main thread.
detail::enqueue_cleanup(static_cast<detail::DeferredCleanupItem *>(rec));
}

} // namespace

CUresult
launch_host_function(
CUstream stream, CUhostFn fn, void *user_data, OpaqueHandle fn_owner, OpaqueHandle data_owner
) noexcept
{
retry_deferred_cleanup();
auto *record = new (std::nothrow) HostLaunchRecord(fn, user_data, std::move(fn_owner), std::move(data_owner));
if (record == nullptr) {
return CUDA_ERROR_OUT_OF_MEMORY;
}
CUresult status = DRIVER_CALL(cuLaunchHostFunc, stream, &host_launch_trampoline, record);
if (status != CUDA_SUCCESS) {
delete record;
return status;
}
return CUDA_SUCCESS;
}

} // namespace cuda_core::rt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This file was generated by stubgen-pyx v0.2.23 from cuda_core/cuda/core/graph/_host_callback.pyx
# This file was generated by stubgen-pyx v0.2.23 from cuda_core/cuda/core/_host_callback.pyx

import sys

Expand Down
64 changes: 63 additions & 1 deletion cuda_core/cuda/core/_launcher.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from cuda.core._stream import Stream
from cuda.core.graph import GraphBuilder
from cuda.core.typing import IsStreamType

__all__ = ['launch']
__all__ = ['launch', 'host_launch']

def launch(stream: Stream | GraphBuilder | IsStreamType, config: LaunchConfig, kernel: Kernel, *kernel_args) -> None:
"""Launches a :obj:`~_module.Kernel`
Expand All @@ -27,3 +27,65 @@ def launch(stream: Stream | GraphBuilder | IsStreamType, config: LaunchConfig, k
launching kernel.

"""
def host_launch(stream: Stream | GraphBuilder | IsStreamType, fn, *, user_data=None) -> None:
"""Enqueue a host function on a stream in stream order.

The function runs on a driver-owned thread once all work enqueued on the
stream before this call has completed. Work enqueued after this call waits
until the function returns. This call returns when the function is enqueued;
observe completion with :meth:`Stream.sync` or an :class:`Event` recorded
after it.

Passing a :class:`~graph.GraphBuilder` forwards to
:meth:`~graph.GraphBuilder.callback`.

Parameters
----------
stream : Stream or graph.GraphBuilder or IsStreamType
The stream establishing execution order, or a graph builder capturing
the callback.
fn : callable or ctypes function pointer
A Python callable taking no arguments, which runs with the GIL, or a
ctypes function pointer declared as ``CUhostFn``, for example
``ctypes.CFUNCTYPE(None, ctypes.c_void_p)``, which ``cuda.core``
passes to the driver as is. A pointer to compiled code runs without
the GIL; a ctypes callback that wraps a Python function acquires the
GIL through ctypes and is subject to the same deadlock rule as a
Python callable. ``cuda.core`` keeps ``fn`` alive until the function
has run and releases it afterwards on the main thread.
user_data : int or bytes-like, optional
Only for ctypes function pointers. An ``int`` is a raw pointer that
the caller must keep valid until the function has run. Bytes-like data
is copied; ``cuda.core`` owns the copy until the function has run and
releases it afterwards on the main thread.

Raises
------
TypeError
If ``stream`` is ``None`` or not a stream, or if ``fn`` is not
callable or its ctypes prototype is not ``CUhostFn``.
ValueError
If ``user_data`` is given for a Python callable.
RuntimeError
If ``stream`` is a :class:`Stream` that is capturing a graph, or a
:class:`~graph.GraphBuilder` that is closed or not building.

Notes
-----
An exception raised by a Python callable cannot propagate to the caller.
It is reported through :func:`sys.unraisablehook` and the stream continues.

.. warning::

The function must not call CUDA, directly or through a library. The
driver does not permit such calls. Such a call may fail with an error,
or it may deadlock.

A Python callable, or a ctypes callback that wraps a Python function,
acquires the GIL on the driver's thread. A thread
that holds the GIL while blocking inside a CUDA call waiting for this
stream therefore deadlocks with the callback. ``cuda.core`` and
``cuda.bindings`` release the GIL around CUDA calls. A library that
calls CUDA with the GIL held must not be used to wait on a stream with
a pending Python callback.
"""
91 changes: 89 additions & 2 deletions cuda_core/cuda/core/_launcher.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ from libc.stdint cimport uintptr_t

from cuda.bindings cimport cydriver

from cuda.core._host_callback cimport _resolve_host_callback
from cuda.core._launch_config cimport LaunchConfig
from cuda.core._kernel_arg_handler cimport ParamHolder
from cuda.core._module cimport Kernel
from cuda.core._rt cimport as_cu
from cuda.core._rt cimport as_cu, launch_host_function, OpaqueHandle
from cuda.core._stream cimport Stream_accept, Stream
from cuda.core._utils.cuda_utils cimport (
check_or_create_options,
Expand All @@ -24,7 +25,7 @@ if TYPE_CHECKING:
from cuda.core.graph import GraphBuilder
from cuda.core.typing import IsStreamType

__all__ = ['launch']
__all__ = ['launch', 'host_launch']


def launch(
Expand Down Expand Up @@ -69,6 +70,92 @@ def launch(
HANDLE_RETURN(cydriver.cuLaunchKernelEx(&drv_cfg, func_handle, args_ptr, NULL))


def host_launch(stream: Stream | GraphBuilder | IsStreamType, fn, *, user_data=None) -> None:
"""Enqueue a host function on a stream in stream order.

The function runs on a driver-owned thread once all work enqueued on the
stream before this call has completed. Work enqueued after this call waits
until the function returns. This call returns when the function is enqueued;
observe completion with :meth:`Stream.sync` or an :class:`Event` recorded
after it.

Passing a :class:`~graph.GraphBuilder` forwards to
:meth:`~graph.GraphBuilder.callback`.

Parameters
----------
stream : Stream or graph.GraphBuilder or IsStreamType
The stream establishing execution order, or a graph builder capturing
the callback.
fn : callable or ctypes function pointer
A Python callable taking no arguments, which runs with the GIL, or a
ctypes function pointer declared as ``CUhostFn``, for example
``ctypes.CFUNCTYPE(None, ctypes.c_void_p)``, which ``cuda.core``
passes to the driver as is. A pointer to compiled code runs without
the GIL; a ctypes callback that wraps a Python function acquires the
GIL through ctypes and is subject to the same deadlock rule as a
Python callable. ``cuda.core`` keeps ``fn`` alive until the function
has run and releases it afterwards on the main thread.
user_data : int or bytes-like, optional
Only for ctypes function pointers. An ``int`` is a raw pointer that
the caller must keep valid until the function has run. Bytes-like data
is copied; ``cuda.core`` owns the copy until the function has run and
releases it afterwards on the main thread.

Raises
------
TypeError
If ``stream`` is ``None`` or not a stream, or if ``fn`` is not
callable or its ctypes prototype is not ``CUhostFn``.
ValueError
If ``user_data`` is given for a Python callable.
RuntimeError
If ``stream`` is a :class:`Stream` that is capturing a graph, or a
:class:`~graph.GraphBuilder` that is closed or not building.

Notes
-----
An exception raised by a Python callable cannot propagate to the caller.
It is reported through :func:`sys.unraisablehook` and the stream continues.

.. warning::

The function must not call CUDA, directly or through a library. The
driver does not permit such calls. Such a call may fail with an error,
or it may deadlock.

A Python callable, or a ctypes callback that wraps a Python function,
acquires the GIL on the driver's thread. A thread
that holds the GIL while blocking inside a CUDA call waiting for this
stream therefore deadlocks with the callback. ``cuda.core`` and
``cuda.bindings`` release the GIL around CUDA calls. A library that
calls CUDA with the GIL held must not be used to wait on a stream with
a pending Python callback.
"""
from cuda.core.graph._graph_builder import GraphBuilder

if isinstance(stream, GraphBuilder):
return stream.callback(fn, user_data=user_data)

cdef Stream s = Stream_accept(stream, allow_stream_protocol=True)
cdef cydriver.CUstream c_stream = as_cu(s._h_stream)
cdef cydriver.CUstreamCaptureStatus status
with nogil:
HANDLE_RETURN(cydriver.cuStreamIsCapturing(c_stream, &status))
if status == cydriver.CU_STREAM_CAPTURE_STATUS_ACTIVE:
raise RuntimeError(
"host_launch cannot enqueue on a stream that is capturing a graph; "
"pass the GraphBuilder, or call GraphBuilder.callback")

cdef cydriver.CUhostFn c_fn
cdef void* c_user_data = NULL
cdef OpaqueHandle fn_owner, data_owner
_resolve_host_callback(fn, user_data, &c_fn, &c_user_data, &fn_owner, &data_owner)
with nogil:
HANDLE_RETURN(launch_host_function(c_stream, c_fn, c_user_data, fn_owner, data_owner))
return None


cdef _check_cooperative_launch(kernel: Kernel, config: LaunchConfig, stream: Stream):
dev = stream.device
num_sm = dev.properties.multiprocessor_count
Expand Down
3 changes: 3 additions & 0 deletions cuda_core/cuda/core/_rt.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ cdef StreamHandle create_stream_handle(
cdef StreamHandle create_stream_handle_ref(cydriver.CUstream stream) except+ nogil
cdef StreamHandle create_stream_handle_with_owner(cydriver.CUstream stream, object owner) except+ nogil
cdef void retry_deferred_cleanup() noexcept
cdef cydriver.CUresult launch_host_function(
cydriver.CUstream stream, cydriver.CUhostFn fn, void* user_data,
OpaqueHandle fn_owner, OpaqueHandle data_owner) noexcept nogil
cdef ContextHandle get_stream_context(const StreamHandle& h) noexcept nogil
cdef StreamHandle get_legacy_stream() except+ nogil
cdef StreamHandle get_per_thread_stream() except+ nogil
Expand Down
3 changes: 3 additions & 0 deletions cuda_core/cuda/core/_rt.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ cdef extern from "_cpp/rt/rt.hpp" namespace "cuda_core::rt":
cydriver.CUstream stream, object owner) except+ nogil
void initialize_deferred_cleanup "cuda_core::rt::initialize_deferred_cleanup" () except+
void retry_deferred_cleanup "cuda_core::rt::retry_deferred_cleanup" () noexcept
cydriver.CUresult launch_host_function "cuda_core::rt::launch_host_function" (
cydriver.CUstream stream, cydriver.CUhostFn fn, void* user_data,
OpaqueHandle fn_owner, OpaqueHandle data_owner) noexcept nogil
ContextHandle get_stream_context "cuda_core::rt::get_stream_context" (
const StreamHandle& h) noexcept nogil
StreamHandle get_legacy_stream "cuda_core::rt::get_legacy_stream" () except+ nogil
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/graph/_graph_builder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ from cuda.core.graph._graph_definition cimport (
GD_check_valid,
)
from cuda.core.graph._graph_node cimport GraphNode, GN_check_valid
from cuda.core.graph._host_callback cimport _resolve_host_callback
from cuda.core._host_callback cimport _resolve_host_callback
from cuda.core.graph._subclasses cimport (
ChildGraphNode,
EventRecordNode,
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/graph/_graph_node.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ from cuda.core._rt cimport (
)
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value

from cuda.core.graph._host_callback cimport (
from cuda.core._host_callback cimport (
_resolve_host_callback,
)

Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/graph/_subclasses.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ from cuda.core._rt cimport (
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value
from cuda.core._utils.version cimport cy_driver_version

from cuda.core.graph._host_callback cimport (
from cuda.core._host_callback cimport (
_is_py_host_trampoline,
_resolve_host_callback,
)
Expand Down
1 change: 1 addition & 0 deletions cuda_core/docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Devices and execution
Device
Host
launch
host_launch

:template: autosummary/cyclass.rst

Expand Down
3 changes: 3 additions & 0 deletions cuda_core/docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Kernels and launch configuration
partitions SMs across kernels with green contexts.
- :sample:`kernelNsysProfile <kernelNsysProfile/>`
annotates kernel launches with NVTX markers for Nsight Systems.
- :cuda-core-example:`host_launch.py <host_launch.py>`
orders a host function between two kernel launches on a stream and reads a
pinned buffer from it.

Memory management and interop
-----------------------------
Expand Down
8 changes: 8 additions & 0 deletions cuda_core/docs/source/release/1.3.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ Breaking Changes
New features
------------

- Added :func:`host_launch`, which enqueues a host function on a stream in stream
order: a Python callable, or a ctypes function pointer with the ``CUhostFn``
prototype, which ``cuda.core`` passes to the driver as is, so a pointer to
compiled code runs without the GIL. It is the stream-side counterpart of
:meth:`graph.GraphBuilder.callback` and accepts the same ``fn`` and
``user_data`` forms.
(`#2058 <https://github.com/NVIDIA/cuda-python/issues/2058>`__)

- Added incremental linking to :class:`Linker` through
``LinkerOptions(incremental=True)`` on nvJitLink 13.2 or newer. A partial
native result is returned as a CUBIN and can be passed directly to another
Expand Down
Loading
Loading