Skip to content

cuda.core: retain graph attachments with per-node user objects#2357

Open
Andy-Jost wants to merge 18 commits into
NVIDIA:mainfrom
Andy-Jost:graph-node-attachment-user-objects
Open

cuda.core: retain graph attachments with per-node user objects#2357
Andy-Jost wants to merge 18 commits into
NVIDIA:mainfrom
Andy-Jost:graph-node-attachment-user-objects

Conversation

@Andy-Jost

@Andy-Jost Andy-Jost commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix the graph attachment lifetime problem described in #2350. Each resource-bearing node version now has an immutable attachment owned through its own CUDA user object, so source-graph mutation cannot release resources still used by clones, executable graphs, or in-flight launches.

Implementing that ownership change also required cuda-core's metadata to follow CUDA's graph topology and mutation boundaries. Each graph hierarchy keeps root, child, and conditional graph state coherent; clone and embed paths copy and rekey that metadata, and prepare/commit operations publish it only after the corresponding CUDA mutation succeeds. Together, these pieces make cuda-core's bookkeeping match the lifetimes CUDA actually enforces.

Changes

  • Replace the mutable graph-wide slot table with complete per-node attachment bundles and user objects.
  • Track canonical root, child, and conditional graphs in a shared hierarchy while keeping attachment and node-handle metadata scoped to each graph.
  • Copy and rekey attachment metadata when CUDA embeds cloned child graphs, and invalidate borrowed graph/node handles when CUDA destroys their backing objects.
  • Preserve captured host callback owners when CUDA commits a callback but its node cannot be recovered.
  • Keep graph mutation and metadata updates failure-aware via prepare/commit attachment transactions and failure-atomic deletion.
  • Add contributor design documentation, release notes, and regressions across graph definitions, clones, executable updates, and launches.

Review guide

  1. Start with cuda_core/cuda/core/_cpp/GRAPH_ATTACHMENTS.md for the ownership model, graph hierarchy, common operations, and invariants.
  2. Review resource_handles.cpp/.hpp for per-node user objects, canonical graph boxes, clone metadata import, handle invalidation, and deferred cleanup.
  3. Review _resource_handles.* and graph/*.pyx for the Cython integration and CUDA-operation ordering.
  4. Review the graph lifetime, mutation, builder, and update tests for correctness evidence and failure coverage.

Test coverage

  • pre-commit run --all-files
  • Full local cuda-core test suite on GPU

Related Work

@Andy-Jost Andy-Jost added this to the cuda.core next milestone Jul 14, 2026
@Andy-Jost Andy-Jost added P0 High priority - Must do! feature New feature or request cuda.core Everything related to the cuda.core module labels Jul 14, 2026
@Andy-Jost Andy-Jost self-assigned this Jul 14, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test

@Andy-Jost
Andy-Jost force-pushed the graph-node-attachment-user-objects branch from d7e8d3e to 945672d Compare July 15, 2026 00:42
@github-actions

Copy link
Copy Markdown

@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test

@Andy-Jost
Andy-Jost force-pushed the graph-node-attachment-user-objects branch 4 times, most recently from 807cac8 to 6d7ae74 Compare July 15, 2026 23:10
@Andy-Jost
Andy-Jost force-pushed the graph-node-attachment-user-objects branch from 6d7ae74 to e7391d3 Compare July 17, 2026 23:38
@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test

@Andy-Jost
Andy-Jost force-pushed the graph-node-attachment-user-objects branch from e7391d3 to 8babae5 Compare July 18, 2026 00:10
Andy-Jost added 10 commits July 20, 2026 08:06
Preserve the validated lifetime and failure scenarios as executable specifications for the metadata redesign, alongside independent cleanup clarifications.
Establish canonical per-graph boxes, stable hierarchy ownership, and Level 1 graph identity before rebuilding attachment operations.
Give root and child graph handles stable per-graph boxes while sharing one hierarchy control block and registry identity.
Replace the mutable graph-wide slot table with complete per-node user objects and intrusive deferred cleanup.
Copy and rekey source attachment metadata into embedded graph hierarchies before publishing their canonical boxes.
Unregister and tombstone child graph boxes after CUDA destroys their owner while preserving stable storage for existing handles.
Allow graph-level anonymous attachments so a captured host callback stays safe when CUDA commits it but its node cannot be recovered.
Verify anonymous capture retention and proactive invalidation of child and conditional graph views.
Orient contributors to versioned node ownership, graph hierarchy metadata, clone propagation, invalidation, and deferred cleanup.
Make node identity atomic, preserve tombstoned graph identity, retry deferred cleanup safely, and cover final-reference launch ownership.
Document corrected graph attachment lifetime and callback-safe resource release.
Retain attachment owners and preallocate metadata before CUDA graph mutations so failures cannot leave nodes with unretained resources.
Carry rollback through the prepared attachment deleter so consumer extension modules do not depend on an out-of-line C++ symbol.
@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test

Preserve the general no-invalidation guarantee while documenting borrowed child graphs as a narrow driver-owned exception, and tighten implementation comments for reviewers.
@Andy-Jost
Andy-Jost marked this pull request as ready for review July 20, 2026 19:20
@Andy-Jost
Andy-Jost requested review from leofang and mdboom and removed request for leofang July 20, 2026 19:22
@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test

@mdboom mdboom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some clarifying questions about the design doc. Once I get my head around that I'll come back and look more closely at the code.

The Cython graph code creates CUDA nodes and then calls these operations to keep
the C++ metadata synchronized with the driver.

## Common operations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put some of this concrete procedural content as comments next to the relevant code? It's sort of something you need to flip back-and-forth between to understand...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I removed this stuff, except for the section on deferred cleanup. Key procedural details were moved into brief code comments. The document is overall much shorter and more focused.

Comment on lines +5 to +10
CUDA user objects tie externally managed resource lifetimes to CUDA graphs.
cuda-core uses them to keep resources referenced by graph node parameters
alive. It also keeps non-owning metadata that maps each node to the
user-object attachment backing its current parameters. This mapping lets
cuda-core update the correct user-object references during graph changes and
reproduce the node associations in graph clones.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the motivation is missing here, and it jumps right into the solution.

The motivation is that we must keep all resources used by graph parameters alive for the lifetime of any graph that may use them.

The solution, is therefore to use CUDA user objects as a way to keep user resources alive.

Do I have that right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mermaid graph would go a long way here, showing all of the objects and how the relate with owning and non-owning pointers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new section to address this. I also added a simple Mermaid diagram showing the Hierarchy/Box/Handle relationship and cleaned up the text in that section.

Comment on lines +17 to +22
A definition can be cloned, embedded as a child graph, or instantiated as a
`CUgraphExec`. Each operation copies the definition's current nodes and
parameters. The source and copy then have independent lifetimes: changing the
source does not change an existing clone or executable. An embedded clone is
owned by its parent node and is destroyed when that node is removed or
replaced.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we talking about definition in an abstract sense here, or CUgraph specifically? If the latter, maybe:

Suggested change
A definition can be cloned, embedded as a child graph, or instantiated as a
`CUgraphExec`. Each operation copies the definition's current nodes and
parameters. The source and copy then have independent lifetimes: changing the
source does not change an existing clone or executable. An embedded clone is
owned by its parent node and is destroyed when that node is removed or
replaced.
A `CUgraph` can be cloned, embedded as a child graph, or instantiated as a
`CUgraphExec`. Each operation copies the definition's current nodes and
parameters. The source and copy then have independent lifetimes: changing the
source does not change an existing clone or executable. An embedded clone is
owned by its parent node and is destroyed when that node is removed or
replaced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clarified

Several versions of one logical node's parameters may therefore remain live at
the same time. Without version-specific ownership, replacing or deleting a node
could release memory, kernels, callbacks, or other resources that an existing
clone, executable, or launch still uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is meant by "version" here? Isn't it just different "instances"? If not, maybe define "version" here.

In compiler runtimes, versioned data structures are usually used for caching -- you can compare a version number on a dictionary, for example, to see if it's been mutated since the last time you cached something about it. I don't see anything like that happening in the code (unless I missed it), so I feel like "instance" is a more precise term.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an introduction that should clarify

clone, executable, or launch still uses.

cuda-core prevents this by giving each parameter version an immutable
`NodeAttachment` owned through a graph-retained CUDA user object. Non-owning

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity -- this is immutable on the C++ side, but may reference mutable data on the Python side (most Python data is). I don't think that matters to the implementation, I just found it confusing as to what layer is being talked about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified to

a NodeAttachment whose owner bundle is immutable after publication (although the resources it keeps alive may themselves be mutable).

Transitive mutability is a real problem without a good solution I can see. Example: someone could embed a cuda.core.Buffer in a kernel parameter bundle and then close the buffer.

Comment on lines +104 to +105
`GraphHierarchy::graveyard`. Keeping this tombstone at a stable address lets
existing aliasing handles safely observe that the graph is invalid. Removing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this happen? I only see graphs being put in the graveyard. Must be something more subtle going on?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This occurs in invalidate_child_graph_state. The object moved to the graveyard is a GraphBox, which contains the destroyed child graph’s CUgraph and related state. Each GraphHandle aliases that CUgraph field while sharing ownership of the containing GraphHierarchy.

When a parent node containing a child graph is destroyed, CUDA also destroys the embedded child graph, invalidating its raw handle. This PR sets the GraphBox’s CUgraph field to null so existing aliases reflect that invalidation, then moves the retired box to the graveyard so the aliased field’s address remains valid.

This machinery is necessary because CUDA couples removal of an embedded child graph from its parent with destruction of that graph. If the API allowed the graph to be detached and its ownership transferred to the caller, cuda.core could use an ordinary owning handle and call cuGraphDestroy after its final reference was released.

-----
Definitions that view the same root, child, or conditional graph hierarchy
share underlying graph state. Mutations anywhere in that hierarchy must be
externally synchronized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only real issue my agent found was that self-referencing a graph may create a cycle that the Python garbage collector may be unable to break. We could add a comment to that effect, but I sort of think it seems very unlikely someone would do that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. The general problem is that Python's cyclic garbage collector cannot trace references through the driver, so reference cycles involving retained user objects result in leaks. This limitation is very difficult to remove, so for this PR I opted to update the docs.

I noted the limitation in GRAPH_ATTACHMENTS.md and the docstrings (example here) for API functions that retain Python objects on graphs:

GraphNode.callback and GraphBuilder.callback
GraphNode.launch for retained kernel arguments
GraphNode.memset and GraphNode.memcpy for retained buffers and explicit owners

Reframe the design around parameter versions and driver-managed lifetimes, and keep mutation ordering guidance beside the implementation.
Replace the ASCII sketch with Mermaid and illustrate the GraphHandle alias and control-block relationships for reviewers.
Document that driver-held Python references are opaque to cyclic GC and identify affected graph APIs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.core Everything related to the cuda.core module feature New feature or request P0 High priority - Must do!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cuda.core: retain graph node attachments as per-node CUDA user objects

2 participants