CUDA Graph — capture, replay, and buffer reuse¶
TrackGenerator owns the production graph-captured path. Construction resolves the
selected generator, pre-allocates the persistent Track, all per-stage scratch groups,
and the [E] seed buffer. generate() always returns that same Track instance
with stable wp.array pointers; callers that need a snapshot use Track.clone().
Runtime facade¶
TrackGenerator acts as a runtime facade over the pipeline. Its public interface is:
__init__(config)— resolves generator, allocates all persistent buffers.generate(seeds)— runs or replays the pipeline, returns the stableTrack.Track.clone()— snapshot the current output arrays.
On the Warp cpu device, every generate() call runs _run_pipeline eagerly. On
cuda, the first call compiles and warms the kernels, captures _run_pipeline with
wp.ScopedCapture, stores the resulting wp.Graph, and then launches it. Subsequent
calls copy the current rng.seeds_warp values into the pre-allocated seed buffer and
replay the stored graph with wp.capture_launch.
This capture-then-replay path applies to generators whose GeneratorSpec.capturable is
True (the default; bezier, hull, polar, voronoi, and checkpoint
today). repulsive registers capturable=False — its growth loop transitions
coarse-to-fine stages and reads back a stall-convergence scalar from the host, both illegal
inside a capture region — so TrackGenerator.generate() runs it eagerly on cuda every
call, just like cpu, and never builds a wp.Graph for it. (The per-iteration
wp.Tape that used to be the interior blocker is gone — the gradient is now hand-written
analytic adjoints — so capture is now just a wiring exercise.) See
Repulsive Generator for the cost this forfeits.
Capture requirements¶
The capture works because every stage is pure Warp and fixed-shape:
A module global
_CAPTURINGmakes every wrapper’s_syncandwarp_relax’s finalwp.synchronizea no-op during capture. Host-blocking syncs are illegal inside capture, and the graph records stream ordering.The seed buffer address is stable; replay reuses the same buffer and reads the new seed contents on device.
output_mode="constant_spacing"captures too: per-trackcount[e]is device-side data, and count-aware kernels keep static launch dimensions viaN_max.Generator selection and
relax_enableare Python branches resolved before capture. The captured graph is fixed for thatTrackGenerator’s config.
Buffer reuse¶
All persistent buffers — the output Track arrays, per-stage scratch (generator scratch,
resample buffers, relax scratch including broadphase cache arrays), and the seed buffer —
are allocated once at construction and reused across every generate() call. There is no
per-call allocation on the hot path.
RelaxScratch holds the double buffer pair (positions A and B), the band and L0
arrays, and — in cached broadphase mode — the fixed-slot candidate list and overflow
counter. All addresses are stable across replays.
Performance characteristics¶
At large batches the pipeline is compute-bound (relaxation dominates), so graph replay is
approximately the same wall-clock as the eager call. The graph’s value is a single,
GPU-resident, deployable replayable unit — not a raw speedup — that eliminates kernel
launch overhead and driver overhead for the full pipeline in a single capture_launch
call.
Note
cpu-device runs are always eager. The graph capture path is cuda-only, and even
on cuda it only applies to capturable=True generators — repulsive runs
eagerly on both devices.