[sharded 7/7] CUDA graph capture: the contract, enforced and tested - #11
Open
caugonnet wants to merge 25 commits into
Open
[sharded 7/7] CUDA graph capture: the contract, enforced and tested#11caugonnet wants to merge 25 commits into
caugonnet wants to merge 25 commits into
Conversation
The sharded surface now splits explicitly along the capture boundary:
- The non-blocking elementwise family captures with the standard
fork/join event idiom over the shard streams; the per-place SM
confinement of the (green-context) shard streams survives inside the
instantiated graph, replays recompute from the current shard contents,
and contiguous (VMM-backed) arrays capture transparently, composing
per-shard stages with whole-array kernels in one graph.
- Everything that allocates containers, transfers host data or
synchronizes now REFUSES under an active capture with a thrown
std::runtime_error, detected by a safe query (new helper
places::stream_in_capture, including the legacy-stream probe for
global-mode captures elsewhere in the process) BEFORE any CUDA call
that would invalidate the capture or abort: container allocate /
allocate_contiguous, copy_from_host/copy_to_host/copy_between,
sharded_array::sync, place_group::sync/sync_stream, and the
synchronous algorithms (reduce/sum/min/max, scans, count/count_if,
histogram_even, copy_if/filter/remove_if, unique,
adjacent_difference, sort with either engine) -- all of which stage
per-place partials through the host. The refused capture stays VALID
and keeps accepting supported work.
- place_memory_resource stream-ordered allocation is deliberately left
capturable: an allocate on a capturing stream records a graph memory
node drawing from the place's pool. Tests pin both the balanced
(alloc/free enclosed in the graph, replays freely) and unbalanced
(relaunch fails while the allocation is live; freeing outside with
cudaFreeAsync re-arms it) semantics.
Tests: cudax/test/sharded/graph_capture/{elementwise_pipeline,
reduce_scan_capture, must_not_capture, graph_owned_memory,
contiguous_capture}.cu; docs: a 'CUDA graph capture' section in
sharded.rst documenting what captures, what must stay outside, and how
each refusal fails. 36/36 places+sharded tests green on 2 locality
domains (sm_103a, CUDA 13.4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scratch fix via sharded/sparse) to keep the stack level # Conflicts: # cudax/include/cuda/experimental/__places/place_group.cuh
…ract, from_device ownership docs)
The hand-rolled fork/join event choreography in the capture tests (the fork_to_shards/join_from_shards helpers and the per-shard make_stream_wait_for loops) is replaced by the containers' fork_from / join_into members; the capture section of sharded.rst now documents the members as the composition idiom with a caller stream or graph, keeping places::make_stream_wait_for for streams not owned by a container (the raw place_group stream sites are unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolutions: include union; the stream-capture query keeps its own section now that the stream helper moved out; sync() combines the capture guard with the new lazy iteration (no pool creation during synchronization). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolution: sync() = capture guard first, then the snapshot-under-lock/synchronize-unlocked body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolution: sync_stream is gone (its capture guard with it); the rst must-not-capture list and emphasis style updated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolutions + consolidation: sync docstring keeps both the capture-guard note and the per-shard spelling; ONE stream-level capture guard now lives in places (check_not_capturing beside stream_in_capture, place_group::sync uses it); the sharded layer keeps only the container-level convenience in reserved, delegating. rst: the redundant event-primitive sentence removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etween fix) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolution: sync() keeps its capture guard and iterates the new per-shard sync(shard_idx) member, which gets its own guard; must_not_capture pins the per-shard refusal; <string> included next to the guard that builds the message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #10 (
sharded/sort-va); review only the last commit here.This PR establishes and tests the CUDA-graph-capture contract of the sharded surface, and adds the small enforcement it needs. A new
CUDA graph capturesection insharded.rstis the user-facing statement of the contract.The contract
What captures. The non-blocking elementwise family (
fill,sequence,iota,tabulate,generate,for_each,transformwithblocking = false) is pure per-shard kernel launches on the shard streams, and captures with the standard fork/join event idiom (places::make_stream_wait_forin both directions around the recorded pipeline). Properties pinned by tests:allocate_contiguous) arrays are transparent to capture — the VMM mappings pre-exist the graph — and per-shard stages compose with whole-array kernels throughcontiguous_data()in one graph.What refuses, and how. Everything that allocates containers, transfers host data or synchronizes cannot be represented in a graph (the synchronous algorithms all stage per-place partials through the host). These now throw
std::runtime_errorwhen invoked under an active capture, detected by a safe query before any CUDA call that would invalidate the capture (or abort throughcuda_safe_call): containerallocate/allocate_contiguous,copy_from_host/copy_to_host/copy_between,sharded_array::sync,place_group::sync/sync_stream, andreduce/sum/min/max, the scans,count/count_if,histogram_even,copy_if/filter/remove_if,unique,adjacent_difference, andsort(either engine). The refused capture stays valid and keeps accepting supported work; everything refused works eagerly after the capture ends. The new helperplaces::stream_in_capture(stream)also probes the legacy stream, so the guards fire when a global-mode capture is active anywhere in the process (where these operations would invalidate it under the CUDA capture rules).Benign by construction (also tested): shard adoption and
slice(host-only bookkeeping, usable during capture), andplace_groupconstruction / lazy stream materialization (records nothing into the graph).Graph-owned memory.
place_memory_resourcestream-ordered allocation is deliberately left capturable: anallocateon a capturing stream records a graph memory node drawing from the place's pool. Tests pin both shapes — a balanced alloc/free pair enclosed in the capture instantiates and replays freely; an allocation not freed in-graph stays live after a launch (the captured pointer reads back), an immediate relaunch fails predictably, and freeing the pointer outside the graph withcudaFreeAsyncre-arms the launch. The documented user contract stays simple: allocate outside capture; capture computation only.Why the guards throw instead of letting the driver error surface
Under a global-mode capture, the first illegal call (
cudaStreamSynchronize, pinned-host allocation, ...) both returnscudaErrorStreamCaptureUnsupportedand invalidates the capture — and several of these paths run undercuda_safe_call, which aborts. The entry guards convert that into a clean, catchable refusal that leaves the capture usable.Replay smoke (2 locality domains, GB300, CUDA 13.4)
A 6-kernel captured pipeline replayed 1000x: ~12.4 us/iteration for the eager dispatch loop vs ~6.2 us/iteration for graph replay in the launch-bound regime (min of 3, stable across runs) — about a 2x cut in per-iteration launch overhead, the figure that matters for host-dispatch-bound loops.
Tests
cudax/test/sharded/graph_capture/{elementwise_pipeline, reduce_scan_capture, must_not_capture, graph_owned_memory, contiguous_capture}.cu— silent on success, following the suite style. 36/36 places+sharded tests green on 2 locality domains (sm_103a,-DCMAKE_CUDA_ARCHITECTURES=103a, NCCL off).Generated with Claude Code