You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
cuda.core: graph slot table for node attachment lifetimes (#2280)
* cuda.core: add GraphBuilder.graph_definition property
Completes step 3 of #1330 by exposing the captured graph as an explicit
`GraphDefinition` view that shares ownership of the underlying `CUgraph`.
The handle-layer plumbing landed in PR #2008; this commit wires up the
user-facing surface and locks in the state-guard rules.
State semantics:
- PRIMARY builder: only valid after `end_building()`. Before
`begin_building()` no graph exists; during capture the driver is the
sole writer, so explicit access is unsafe.
- CONDITIONAL_BODY builder: valid both before `begin_building()` (the
body graph is allocated at conditional-node creation time) and after
`end_building()`. This enables a hybrid flow where a conditional body
is populated entirely via the explicit API, with no capture at all.
- FORKED builder: never valid. Forked builders share the primary's
graph; access through the primary instead.
Tests cover the happy path, both hybrid flows on conditional bodies
(populate-via-explicit-API and capture-then-augment), the three error
states (forked, capturing, primary pre-capture), and the
shared-ownership guarantee (the `GraphDefinition` survives the
builder's `close()`).
Co-authored-by: Cursor <cursoragent@cursor.com>
* cuda.core: add graph slot table infrastructure (phase 1)
Introduce OpaqueHandle and a per-graph slot table retained on the CUgraph
as a user object, preparing to replace ad-hoc per-resource user objects when
wiring graph node attachments in a follow-up change.
* cuda.core: wire graph node attachments to the slot table (phase 2)
Replace the per-resource CUDA user objects attached at each graph node
with the per-graph slot table from phase 1. Kernel, event-record,
event-wait, and host-callback nodes now store their owning handles in
node slots via graph_set_slot. Stream-captured callbacks map the
just-captured host node from cuStreamGetCaptureInfo and use the same
path; forked builders share the primary's graph handle so their
attachments reach the same table.
Refine the phase 1 surface to support this: the slot table is created
lazily on first attachment, so conditional-branch bodies (ref handles)
get one too, and graph_set_slot returns CUresult for HANDLE_RETURN-style
error checking. Removes _attach_user_object and the per-type heap-copy
deleters.
* cuda.core: rename graph host-callback module and retain kernel args
Rename graph/_utils to graph/_host_callback now that it holds only
host-callback machinery (the trampoline, _is_py_host_trampoline, and
_resolve_host_callback), matching the concept-named files around it, and
update the three cimport sites. Add _attach_host_callback_owners to share
the "callback -> slot 0, user_data -> slot 1" attachment between the eager
(GN_callback) and capture (add_callback) paths. Guard a zero-length
user_data copy against malloc(0) and hoist the per-call ctypes import.
Attach the kernel-argument tuple to the kernel node's slot 1 so the Python
objects backing the arguments -- notably device Buffers -- outlive the
graph. The driver copies argument values into the node at add time but does
not keep the referenced device memory alive, so without this a kernel node
could be left with a stale device pointer. This is the slot-table port of
the user-object fix from #2041 (currently only on main).
* cuda.core: accept Buffer in graph memcpy/memset and retain operands
GraphNode.memcpy/memset (and the GraphDefinition pass-throughs) now accept a
Buffer or a raw int for each address. A new _resolve_ptr helper reads the
device pointer from a Buffer and returns it as an owner; a raw int casts
through with no owner. GN_memcpy attaches a Buffer dst to slot 0 and src to
slot 1, and GN_memset attaches dst to slot 0, so buffers passed by value
outlive the graph. Raw ints behave exactly as before (caller owns the
lifetime), so this is backward compatible.
Document the stream-capture lifetime contract on GraphBuilder: operations
recorded during capture reference caller-owned memory and are not retained,
unlike explicit GraphDefinition construction. Host callbacks are the one
exception, retained on both the capture and explicit paths.
* cuda.core: add slot-table lifetime tests for Buffer memcpy/memset and capture callbacks
Cover GraphDefinition memset/memcpy with Buffer operands (including clone),
and GraphBuilder capture host callbacks retained after dropping Python refs.
* cuda.core: add explicit dst/src_owner for graph memcpy/memset
Keyword-only *_owner args retain arbitrary objects for raw pointer
operands; Buffer+owner combinations are rejected. Strengthen owner tests
with weakref retention checks and add src_owner rejection test.
* cuda.core: retain device allocations in graph memcpy/memset slots
Store DevicePtrHandle in slot table instead of Buffer wrappers so
reset/close cannot release memory while a graph still references it.
Add test-only weak_handle() for deterministic allocation lifetime
checks and extend graph lifetime tests accordingly.
* cuda.core: keep memset height/pitch positional; mark new graph tests
Address PR #2280 review feedback:
- Move the keyword-only "*" marker in GraphNode.memset and
GraphDefinition.memset to after height/pitch, so pre-existing positional
calls memset(dst, value, width, height, pitch) keep working. The new
dst_owner argument remains keyword-only. This avoids a public API break
across 1.x. memcpy is unchanged (its dst_owner/src_owner args are new,
so the existing "*" placement is non-breaking).
- Add @pytest.mark.agent_authored markers to the new graph tests in
test_graph_builder.py and test_graph_definition_lifetime.py.
* cuda.core: roll back graph node when owner-slot attachment fails
A node added via cuGraphAdd*Node is committed to the graph before its
owner slots are attached. If graph_set_slot fails (e.g. the driver lacks
cuUserObjectCreate, or a transient error), the node would remain in the
graph referencing Python-owned memory with nothing keeping it alive,
risking a later launch dereferencing freed memory.
Guard the slot-attachment at each explicit-add site (kernel, memset,
memcpy, event record/wait, host callback) with a try/except that destroys
the node (best effort) and re-raises. The capture-path callback in
_graph_builder is intentionally left alone: its node is created by
cuLaunchHostFunc during active capture, where destroying a capture
dependency would corrupt capture state.
* cuda.core: use if/elif chain in graph_definition guard
Convert the sequential guard checks in GraphBuilder.graph_definition to an
if/elif chain (splitting the final compound condition into a nested if).
Behavior is unchanged since each leading branch raises; the chain lets
Cython generate tighter branch code. Addresses a review nit on PR #2280.
* cuda.core: make graph_set_slot a no-op for null owners
Centralize null-owner handling in graph_set_slot: a null OpaqueHandle now
returns CUDA_SUCCESS without forcing slot-table (and user-object) creation.
This resolves the reviewer question about the asymmetric per-call-site NULL
checks -- optional owners are uniformly safe at the source, so callers no
longer need to guard them. Update the header doc accordingly.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Leo Fang <leof@nvidia.com>
0 commit comments