cudax: __sharded — placed containers and algorithms for the in-process rung - #4
Open
caugonnet wants to merge 27 commits into
Open
cudax: __sharded — placed containers and algorithms for the in-process rung#4caugonnet wants to merge 27 commits into
caugonnet wants to merge 27 commits into
Conversation
…es layer Sharded arrays partition one logical 1D array across in-process places (devices or sub-device locality domains) while keeping a common address space. Algorithms follow the cooperation-scope recipe CUDA already uses from warps to blocks to devices: run the previous scope's primitive locally on each place, combine through what the scope shares. Containers: - shard<T>: one placed piece (data/size/capacity/global_offset + data_place/exec_place/reference stream + index math) - sharded_array<T>: allocation from explicit specs or a place_group, adoption of existing shards, allocate_like, host transfer helpers, slicing with place correspondence, shard visitation, validation, copy_between/resharding - allocate_contiguous: shards as views into ONE contiguous VA range with per-place physical backing (VMM via places::localized_array); logical boundaries exact, physical ownership granule-approximate; fixed-size contract (size-mutating operations must refuse such arrays) Algorithms (cuda::experimental::sharded): - elementwise: fill, sequence, iota, tabulate, generate, for_each, transform (in-place/unary/binary) — no cross-place stage - reduce/sum/min/max: per-place CUB DeviceReduce + combine - inclusive_scan/exclusive_scan: per-place CUB DeviceScan + in-place prefix fold over the shared address space - adjacent_difference: local differences + one boundary element/shard Temporaries come from each shard's place via place_group resources. Also: public umbrella cuda/experimental/sharded.cuh, tests (containers + contiguous contract + algorithm correctness), a sharded_reduce example over locality domains, docs stub, header-test wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ous; silent tests The contiguous backing places each spec's physical blocks at its exec place's affine data place; a non-affine data_place in a spec was silently ignored. Throw std::invalid_argument up front instead, with a negative test. Also strip success-path printf chatter from the sharded tests (CCCL practice: tests are silent unless they fail or skip).
…/ view) A contiguous array owns its memory -- through the VMM backing rather than per-shard allocations -- yet reported is_owning()==false because the flag conflated shard-level deallocation policy with ownership. Split the enum: owning_shards frees per shard, owning_backing releases via the localized_array backing, view owns nothing. is_owning() is now true for both owning kinds; clear() semantics unchanged.
…ilent-tests pass) The printf removal in the silent-tests commit left its argument list behind, breaking compilation of containers/sharded_array.cu; the loud path is the EXPECT itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…if, unique -- size-mutating contract enforced Completes the initial algorithm family with the read-only pair and the size-mutating pair, per-place CUB + combine like reduce/scan: - count/count_if: per-place DeviceReduce::TransformReduce (0/1 transform), per-place counts summed; read-only, available on contiguous arrays. - histogram_even: per-place DeviceHistogram::HistogramEven, per-bin sum of the per-place histograms; read-only. Invalid num_bins/levels refused with std::invalid_argument. - copy_if/filter/remove_if: per-place in-place DeviceSelect::If (stable compaction to the front of each shard), then shard sizes and offsets are updated; capacities unchanged (reset_sizes_to_capacity reuses buffers). - unique: per-place in-place DeviceSelect::Unique, then duplicates straddling shard boundaries are trimmed with an O(1) size decrement per boundary (compared against the previous NON-empty shard, so runs spanning empty shards are handled). THE CONTRACT, now enforced rather than documented-only: the size-mutating algorithms throw std::invalid_argument on contiguous (allocate_contiguous) arrays -- shrinking shard sizes would leave gaps between shards' valid elements, falsifying the read-as-one-array contract of contiguous_data(), and compacting across the gaps would migrate elements across the placement the caller asked for. Read-only algorithms remain available on every array. Tests (silent on success): count/histogram vs host references incl. a contiguous-array read-only case; copy_if with empty-result shards and a keep-nothing case; remove_if/filter; unique vs std::unique with runs straddling shard boundaries plus the one-value-everywhere degenerate chain; the three refuse-on-contiguous negative tests verifying the array is left untouched. VMM-gated cases skip loudly when the capability is absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sharded_array<T>::adopt(shards) is the named form of the adopting constructor (which stays): a zero-copy wrap of caller-owned memory. The name states the contract -- the container becomes ownership::view and the caller owes the memory's lifetime; from_* factories, by contrast, build owned storage by copying or transforming their inputs. The two-word rule is documented in sharded.rst's container section, and the container test asserts adopt() matches the constructor (is_view, per-shard data identity). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_array Bridge a caller stream and the per-shard streams without host round-trips: fork_from(stream) records one event on the caller stream and makes every shard stream wait on it; join_into(stream) records an event on each shard stream and makes the caller stream wait on all. Both are ordering declarations, not synchronizations (the host returns immediately), and both are capture-safe: inside an active CUDA graph capture the record/wait pairs become graph dependencies, making them the composition idiom between a captured caller stream (or graph) and the per-shard work. Events come from a small pool owned by the container (disableTiming, lazily created, reused across calls): join events are per shard, created in the shard's execution context; fork events are keyed by the caller stream's device. Container ownership (rather than a place_group resource cache) is deliberate: shards do not reference the group that created their streams, and adopted arrays never had one, so a group cache would leave the adopted path on transient events. Lazy creation is mutex-guarded; recording reuses the pooled events, so concurrent fork/join calls on one container must be ordered externally (documented). Tests (containers/fork_join.cu): eager producer -> fork -> per-shard consumers -> join -> reader chain with no host sync between stages, the same chain on an adopted array over foreign caller-owned streams, the members inside a graph capture (instantiate + relaunch x3), and degenerate no-ops (empty container, shard stream == caller stream). sharded.rst gains the composition section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in event pool cudaStreamGetDevice requires CUDA 12.8+; cuda::stream_ref::device() provides the same answer (green-context streams report their underlying device) on every toolkit cudax supports, via the versioned driver-API loader below CTK 13.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ring places::make_stream_wait_for is retired in favor of the equivalent cuda::stream_ref::wait(stream_ref) (RAII event, driver-API wait). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rved namespace, factory cut, stream helper retired) into sharded/dev
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… docs style - New sharded::contiguous_backing_supported(device): the public answer to 'can this machine back allocate_contiguous' (the VMM attribute the localized_array backing requires; allocate_contiguous throws where it reports false). The three tests drop their hand-rolled driver probes and ask the library. - rst: emphasis instead of capitals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fork_from is the capture-composition idiom, but querying a capturing stream's device (cuStreamGetDevice) returns CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED on some drivers (caught by the T4 CI arms; newer drivers permit it, which is why local runs passed). cudaStreamIsCapturing is the one stream query legal everywhere: under capture the recorded event only becomes a graph dependency node, so the current device is the right home for it; outside capture the stream's device is queried as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, per-shard sync spelling
- namespace detail -> reserved across the __sharded headers (the STF
convention, kept deliberately).
- allocate() keeps zero-size shards instead of dropping them: shard
positions correspond to spec positions — and to group places in the
place_group overloads — even when a size is zero, the same way
compaction leaves emptied shards in place. Host-copy paths skip
empties; validate() already tolerated them. Tests: allocation shape +
round-trip across an empty shard, and reduce over an array with an
allocation-empty shard.
- sync() documents the per-shard spelling
(cuda::stream_ref{arr.shard(i).stream}.sync()).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… correctness, API polish
- scan (CRITICAL fix): init and identity are now distinct. Inclusive
custom-op scans need no identity at all — the cross-shard prefixes
are folds of the preceding non-empty shards' totals (has-prefix
chain), so non-additive operators (multiplies, max) fold correctly.
Custom-op exclusive scans take init AND identity explicitly: locals
run with the identity, the init seeds the global sequence exactly
once through the prefix chain (it used to be folded per shard).
Empty shards contribute nothing (presence flags, not identity
pre-fill). Tests pin the multiplies cross-shard product, the
exclusive nonzero-init sequence, a custom-op exclusive scan with an
explicit identity, and a scan across allocation-empty shards.
- adjacent_difference: rejects input/output aliasing (one thread's
write races another's read); the predecessor boundary walks back to
the previous NON-EMPTY shard. Regression test: {nonzero, 0, ...,
nonzero} shards + the aliasing refusal.
- sharded_array::sync(shard_idx): the per-shard synchronization is now
a member (exec scope applied, same as every shard operation) and
sync() iterates it; the docstring recommends the member rather than
a raw stream_ref spelling that would bypass the exec scope.
- release() refuses ownership::owning_backing (a VMM mapping cannot be
handed over through raw pointers; it used to leave the caller with
pointers into a range the destructor unmaps).
- allocate_contiguous keeps zero-size shards (zero-length views at the
running offset) — the same place-correspondence invariant allocate()
documents.
- fork_join: the current device is restored on the event-creation
error path; cuda::std::size_t + <cuda/std/cstddef>.
- unique: host staging laid out _Tp-first with max alignment (an
over-aligned _Tp used to land on an 8-byte boundary).
- _CCCL_HOST_API on the public algorithm surface ([[nodiscard]] on the
value-returning ones); shard<T> members _CCCL_HOST_DEVICE_API +
[[nodiscard]] noexcept (device code calls contains/to_local/
to_global); missing std includes; the rst usage example is
self-contained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alue type; example includes - The exclusive scan's identity-prefix shortcut imposed an undeclared operator== requirement on _Tp; removed — the fold is a cheap elementwise pass the value-type contract should not pay for. - sharded_reduce example: direct <cstddef>, std::size_t. 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.
The framing: CUB's cooperation-scope ladder, extended
CUB's structural invariant: the primitive at scope N = scope N−1's primitive run locally + a combine using what scope N shares.
WarpReduce,WarpScan, ...BlockReduce,BlockScan, ...DeviceReduce,DeviceScan, ...__sharded)__multi_gpuMGMN)Each rung owns its combine. Sharded containers exist exactly at the highest rung that still shares an address space; where nothing is shared, the MGMN communicator algorithms take over. The two surfaces are scoped by rung and compose: hierarchical multi-process + placed-per-process is ordinary rung nesting, the same way
DeviceReducerunsBlockReducerunsWarpReduce.What this PR adds
A new sibling capability
cudax/include/cuda/experimental/__sharded/(public umbrellacuda/experimental/sharded.cuh, namespacecuda::experimental::sharded), peer to__multi_gpu, matching the ladder 1:1. It builds only on the places layer (andplace_groupfrom the previous PR in this stack) — no other dependencies.Container tier
shard<T>: one placed piece —data/size/capacity/global_offset+data_place/exec_place/reference stream + global↔local index mathsharded_array<T>: allocation from explicit(size, data_place, exec_place, stream)specs or from aplace_group(even split or explicit sizes; reference streams from the group's pools); adoption of existing shards (non-owning views);allocate_like; host transfer (from_host,copy_from_host,copy_to_host);slicewith place correspondence preserved (empty shards keep their position);each_shardvisitation with exec-place activation;sync; validation;copy_betweenfor arbitrary re-shardingallocate_contiguous): shards become views into ONE contiguous VA range whose physical pages are owned per place (VMM viaplaces::localized_array). Logical shard boundaries are exact (shard(i).data == contiguous_data() + global_offset); physical ownership snaps to the allocation granularity (typically 2 MiB).contiguous_data()hands the whole array to unmodified single-pointer consumers. The range is mapped once, so shard sizes are fixed: size-mutating operations must refuse such arrays (the contract is documented on the method and enforced with a throw by the size-mutating algorithmscopy_if/filter/remove_ifandunique). One VA also means one physical home per byte — the container holds partitioned data only; replicated operands belong to the binding tier (STFlogical_data).Algorithm tier (native, per-place CUB + combine)
fill,sequence,iota,tabulate,generate,for_each,transform(in-place / unary / binary, with cross-array stream ordering and layout checks)reduce/sum/min/max: per-placecub::DeviceReducethrough the group's stream + memory-resource environments, then a combine of the P partialsinclusive_scan/exclusive_scan(+ sum aliases): per-placecub::DeviceScan, host prefix of the P shard totals, in-place prefix fold into each shard over the shared address spaceadjacent_difference: local differences + exactly one boundary element per shardcount/count_if: per-placecub::DeviceReduce::TransformReduce(predicate mapped to 0/1), per-place counts summed — read-only, so available on every array including contiguous oneshistogram_even: per-placecub::DeviceHistogram::HistogramEven, per-bin sum of the per-place histograms — read-only; invalid bins/levels refused withstd::invalid_argumentcopy_if/filter/remove_if: per-place in-placecub::DeviceSelect::If(stable compaction to the front of each shard), then shard sizes and global offsets are updated; capacities are unchanged, soreset_sizes_to_capacity()reuses the buffersunique(std::uniquesemantics across shard boundaries): per-place in-placecub::DeviceSelect::Unique, then duplicates straddling shard boundaries are trimmed with an O(1) size decrement per boundary (compared against the previous non-empty shard, so runs spanning empty shards are handled)The size-mutating algorithms (
copy_if/filter/remove_if,unique) enforce the contiguous-backing contract: they throwstd::invalid_argumentonallocate_contiguousarrays, leaving them untouched — shrinking shard sizes would leave gaps between shards' valid elements, falsifying the read-as-one-array contract ofcontiguous_data(), and compacting across the gaps would migrate elements across the placement the caller asked for.Every algorithm takes
(place_group&, containers...)— the mapping-tier signature; algorithm temporaries are drawn from each shard's own place.Tests (
cudax/test/sharded/)containers/sharded_array.cu: allocation (specs / group / uniform), index math, host roundtrips,copy_betweenincl. misaligned resharding, adoption + slicing,allocate_likelayout+placement preservation, contract throws (size-count mismatch,check_compatible)containers/contiguous.cu: exact-boundary + density checks, per-shard placement correspondence, per-shard writes read by one whole-range kernel throughcontiguous_data()(and the reverse), backing release onclear()algorithms/{elementwise,reduce_scan}.cu: correctness vs host references over multiple places (locality domains), custom operators, layout-mismatch throws, empty-input behavioralgorithms/count_histogram.cu:count/count_ifandhistogram_evenvs host references, including a contiguous-array case showing read-only algorithms stay available there; invalid-argument throwsalgorithms/compaction.cu:copy_ifvs host reference incl. empty-result shards and a keep-nothing case,remove_if/filter,uniquevsstd::uniquewith runs straddling shard boundaries plus the one-value-everywhere degenerate chain, and the refuse-on-contiguous negative tests (copy_if,remove_if,unique) verifying the array is left untouchedinclude_only.cu+ header-test targets for the new headersExample
cudax/examples/places/sharded_reduce.cu:place_group::by_locality_domains()→sharded_array::allocate→iota→sum, 256M elements.Verification
GB300 node (sm_103a, CUDA 13.4, 2 locality domains): 26/26 places+sharded tests and both examples pass.
Sequenced next
__multi_gpucommunicator concept — already validated out of tree:cudax::reduceand the MGMN HSS sort run unmodified over it — as the bridge between the two rungsOpened on the fork for design review with Andrei. Main review points: the
__sharded-as-sibling placement, the container/algorithm split, and the contiguous-backing contract.🤖 Generated with Claude Code
Update:
sharded_array<T>gainsfork_from(cudaStream_t)/join_into(cudaStream_t)— ordering declarations (not synchronizations; the host returns immediately) bridging a caller stream and the per-shard streams via a small container-owned event pool (lazily created disableTiming events, reused across calls; correct for adopted arrays with foreign streams). Both are capture-safe and documented as the composition idiom with a caller stream or graph. Tests cover the eager no-host-sync chain, an adopted-array variant, and a capture variant.