Skip to content

CUDA Graph API #107

Open
Open

Description

@mtrsl

Are there any plans to implement a safe interface for the CUDA Graph API? Maybe DeviceOperation could be used for some graph-like stuff, but it doesn't seem like a full replacement

Activity

  1. added
    enhancementNew feature or request
    host-apisHost-side runtime APIs (cuda-host, cuda-core, cuda-async)
    on Jun 6, 2026
  2. nihalpasham commented on Sep 3, 2026

    @nihalpasham
    Collaborator

    Short answer: yes, and it arrives through the shared host crates, not oxide's own.

    // sketch of the shared cuda_async::cuda_graph API once oxide launchers plug in
    let graph = CudaGraph::scope(&stream, |s| {
        s.record(kernels.gemm_async(cfg, &x, &w0, &mut h))?;   // recorded, not run
        s.record(kernels.relu_async(cfg, &mut h))
    })?;
    for batch in inputs {
        graph.update(memcpy_into(&mut x, &batch))?;  // new input, same buffer
        graph.launch().sync_on(&stream)?;            // replay as one launch
    }
    • What is left on my side: oxide's launchers implement a different trait (same job) from the one record accepts today, so the shared crate needs a small bridge for them, plus an oxide example with buffers allocated outside the capture. That pair is what this issue waits on.

    Which workload do you want to replay (decode-step loop, iterative solver, something else), so the example matches it?

  3. mtrsl commented on Sep 6, 2026

    @mtrsl
    Author

    Thanks for the update. A common base for cuda-oxide and cutile-rs sounds great.

    I am more likely to use explicitly defined graphs than captured graphs. No particular workload in mind currently, but I've previously encoded the dependencies between compartmental model components in a CUDA graph allowing for parallel model updates

  4. midagedev commented on Sep 21, 2026

    @midagedev
    Contributor

    We are running the dense block of a DeepSeek-V2-Lite decode step (33 launches, one token) as one captured graph on an RTX 3090 with cuda-oxide launchers, MoE blocks next, so this thread is the one we depend on. Two things, one small.

    The small one. The only capture entry points today are on the Tile-side cuda_core::runtime::Stream (begin_capture/end_capture, runtime.rs); the simt::CudaStream that #[cuda_module] launchers take has none, so our graph wrapper calls cuStreamBeginCapture_v2 / cuStreamEndCapture through cuda_core::sys on the raw handle. We have the parity patch on a branch — begin_capture / end_capture / is_capturing on simt::CudaStream, calling the same cudarc_shim::stream helpers runtime::Stream uses, with a GPU test (records a host function, checks the status transitions, one node in the graph, end_capture without begin_capture is a driver error, the default stream refuses):

    NVlabs/cutile-rs@main...midagedev:cutile-rs:feat/simt-stream-capture

    It stops at the CUgraph, exactly where runtime::Stream stops; nothing about instantiation or launch. Before opening a PR: is this a shape you want at all, given the bridge you describe (oxide launchers → cuda_async::CudaGraph::record)? If the plan is for oxide code to capture only through cuda-async, the patch is redundant and we will drop it. If it is useful as the low-level half, does it belong in a PR of its own, or in cutile-rs#257 (which edits the same simt/stream.rs; #270 is the graph half in cuda-async, disjoint from this)?

    The data point, in case it helps the API design. On the 3090, one captured graph with touch (empty) kernels costs 0.76 µs per node; our block-0 FFN half went from 8 nodes at 104 µs to 4 fused nodes at 84.5 µs, and only 2.9 µs of that 19.7 µs was node overhead — the rest was small-kernel bodies (norm, quantize, swiglu, residual add) that stopped occupying the GPU alone. So for us the graph is worth having for the per-launch CPU cost, not for the GPU-side gap; the fusion decisions happen elsewhere. What did shape the code around the graph is that capture freezes launch scalars: the KV row index, the live key count, and the MoE expert ids picked by the router all had to move into device buffers read by the kernels so one graph can be replayed at every position and with every routing (update_with() in #270 covers the input-buffer case; the "a kernel reads its own index from a device buffer" case needs nothing from the API, but an example of it would have saved us a day).

    All numbers are from our repo's gates on the 3090 under a machine lease; happy to share the harness. The patch and this note were drafted with Claude; the measurements and the review are ours.

  5. billylindeman commented on Sep 28, 2026

    @billylindeman

    Is there a timeline for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthost-apisHost-side runtime APIs (cuda-host, cuda-core, cuda-async)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions