Skip to content

feat(rccl-tests): GIN SDMA ReduceScatter (-D 3) - #11556

Closed
dlamd1dai wants to merge 18 commits into
ROCm:developfrom
dlamd1dai:users/dondai/gin-stage3e-sdma-rs
Closed

dlamd1dai wants to merge 18 commits into
ROCm:developfrom
dlamd1dai:users/dondai/gin-stage3e-sdma-rs

Conversation

@dlamd1dai

@dlamd1dai dlamd1dai commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a GIN Anvil-SDMA ReduceScatter device path (reduce_scatter_perf -D 3): single-tier LSA read-reduce, host-testable policy, GPU unit tests, and opt-in pytest/devtime smoke.
  • Wire ReduceScatter into GIN CI (reduce_scatter_perf -D 3, policy gtest, test_ReduceScatterGinSdma.py) and device-api CI devtime smoke.

Keep RS single-tier for this PR. A size-hybrid SDMA put-partials path is not required at 4 GiB: the CU LSA pull is already at the xGMI/host-kernel ceiling through 2 GiB (~98–99% of -D 0), and 4 GiB is the same bandwidth-bound regime (not a new latency/algorithm cliff). Phase-2 pipelined SDMA is a later ~5–9% large-message bet, not a 4 GiB correctness or perf gate.

JIRA ID: AICOMRCCL-1803
https://amd-hub.atlassian.net/browse/AICOMRCCL-1803

Test plan

Validated on 8× AMD Instinct MI355X (gfx950), HIP 7.13.99004 / ROCm 7.13.0

Commit validated: e99ea5c351c. NCCL_GIN_TYPE=6 (NCCL_GIN_TYPE_ANVIL_SDMA on this
develop-based branch). Plugin load confirmed in-log:
NCCL INFO GIN/Plugin: Assigned plugin gin-anvil-sdma type 6 to comm.
All results below exercise the single-tier LSA read-reduce path only — this PR ships
no hybrid/low-CTA tier, and RS_CTAS / NCCL_GIN_ANVIL_RS_CTAS was never set.

  • projects/rccl-tests/src/reduce_scatter.cu compiles and links with zero warnings
    or errors
    ; reduce_scatter_perf built; BUILD SANITY OK (GIN host plugins in
    librccl.so, Anvil-SDMA factory linked).
  • Single-tier -D 3 datacheck, np=8, 128 B → 2 GiB (-b 128 -e 2G -f 2), op sum:
    float, int32, half — 25/25 size rows each, #wrong = 0 in both the
    out-of-place and in-place columns, # Out of bounds values : 0 OK. No hangs; every
    run reached the 2 GiB row and printed its closing summary. Peak 410 GB/s bus bw.
  • New host-side LSA guard does not misfire: on a single-node all-P2P comm the world
    team equals the LSA team (lsaSize=8 == commProperties.nRanks), and
    GinReduceScatterKernel requires CUDA P2P connectivity across all ranks was never
    emitted at np=2, np=4 or np=8. Since the guard runs before the deviceImpl switch,
    the successful -D 3 data rows prove it returned testSuccess.
    Not covered: the guard's reject path (world ⊋ LSA team) needs a multi-node comm.
  • fp8 skip restructure, -o all -d all on both paths, no testNotImplemented and
    no ncclDevFuncId not found abort on either:
    • -D 0 (default host path): 408 rows, all #wrong = 0. fp8 avg runs; fp8 prod
      and mulsum skipped; mulsum still covered for all non-fp8 types.
    • -D 3 (GIN device path): 348 rows, all #wrong = 0. mulsum skipped for every
      type; fp8 prod skipped; fp8 avg runs.
    • Row counts match the intended skips exactly (68 × 6 = 408; 58 × 6 = 348).
  • Unit tests (built via their CMake targets with -DBUILD_TESTS=ON -DGIN_ANVIL_UNIT_TESTS=ON): rccl-UnitTestsGinSdmaReduceScatterPolicy 19/19
    passed
    ; rccl-UnitTestsGinSdmaReduceScatterGpu 4/4 passed on a real GPU (no
    skip). ctest -L unit in rccl-tests: 1/1 passed.
    Counts are as of e99ea5c351c. The policy suite has since grown to 25 tests with the
    ReduceScatterPolicyPool (pool/grid clamp) and ReduceScatterPolicyEnv (env parse
    rejection) cases; those have not yet been run on hardware — see the MI300X section.
  • Negative control: NCCL_GIN_TYPE=5 on this branch does not silently pass — it aborts
    (rc=6, "GIN support is not enabled for this communicator") with zero data rows.
  • GIN CI job (rccl-gin-rocshmem-sdma-rs-d3, rccl-gin-rs-policy-ut,
    rccl-gin-rs-pytest) — failed at 61a394d42e2 for a cause outside the RS device
    path, now fixed on this branch.
    The job never reached any test: librccl failed to
    build with anvil_device.hpp: attempt to use a poisoned identifier (15 errors,
    gfx950) followed by ERROR: no librccl.so under .../.ci-out/rccl-install/lib. Root
    cause is the upstream pair described under blockers below — ENABLE_ROCSHMEM_GIN
    defaults NCCL_GIN_ANVIL_SDMA_ENABLE to 1 while RCCL_POISON_HIP_ATOMICS also
    defaults ON. 61a394d42e2 carried the poison but not the isolation fix; the branch
    now includes 264938bdf36 and 97377b539e8, so this needs a re-run to confirm.
  • Device-api CI ReduceScatter devtime smoke — PASSED on the self-hosted runner
    (device-API / Single node device-api testing, 25m37s, run 34733479064). This job
    runs device-api.sbatch -> run-device-api-ci.sh, so it exercises both edits to
    that script. From the job log:
    test_ReduceScatterDevtime.py Mode1Augment / Mode2DeviceOnly /
    Mode2WithTimedCheck -> 3 passed in 67.94s, followed by
    All device-api benchmark and devtime smoke runs succeeded. -- that final summary
    line is itself the proof the run_devtime_smoke call no longer aborts the script
    under set -euo pipefail. RCCL_TESTS_RS_GIN_TYPE defaulting to 2 is also
    confirmed correct here: device-api.sbatch builds librccl without rocSHMEM, so the
    Anvil-SDMA plugin is compiled out and only the proxy type can load.
    reduce_scatter.cu additionally compiled clean in this job under
    ENABLE_DEVICE_API=1 without rocSHMEM -- a second, independent compile of the
    commit in a different configuration from the MI355X build.

Re-validated on 8× AMD Instinct MI300X (gfx942), ROCm 7.0.1 — logs retained

The MI355X figures above predate the barrier-pool clamp and CTA-parser hardening, and that
host has since been repartitioned to CPX/NPS2, where the Anvil-SDMA context cannot be
created at all (the plugin is assigned, no context created line follows, and -D 3
correctly refuses with rc=6). So the pool-clamp work was reproduced on MI300X in SPX
instead. Anvil-SDMA supports MI300X as a first-class path (anvil.cpp carries an explicit
mi300xOamMap for the MI300X xGMI SDMA layout).

Image built from 740d74051e3 for gfx942; NCCL_GIN_TYPE=6; device path proven by
Assigned plugin gin-anvil-sdma type 6 and GIN anvil-sdma: context created on all
8 ranks.

  • Datacheck, int32/sum, -D 3 -V 8, out-of-place and in-place — 28/28 rows,
    #wrong = 0 in every column, Out of bounds values : 0 OK in all four runs.

    -b is the total send buffer (ReduceScatterGetCollByteCount divides by
    nranks), so -b 8M -e 2G is 1 MiB → 256 MiB per rank:

    | config | `-b`/`-e`/`-f` | rows | nonzero `#wrong` |
    |---|---|---|---|
    | baseline (no pin) | `8M`/`2G`/`2` | 9 | 0 |
    | `NCCL_GIN_ANVIL_RS_CTAS=64` | `8M`/`2G`/`2` | 9 | 0 |
    | `NCCL_GIN_ANVIL_RS_CTAS=-2` | `8M`/`2G`/`4` | 5 | 0 |
    | `NCCL_GIN_ANVIL_RS_CTAS=8foo` | `8M`/`2G`/`4` | 5 | 0 |
    
    Baseline bus bw rises 120 → 214 GB/s across the ladder. The `RS_CTAS=64` run is the
    pool-clamp regression case: pre-fix it launched 64 CTAs against a 48-slot pool; it now
    completes clean at 120 → 189 GB/s. The consistent ~12% deficit versus baseline is the
    expected cost of pinning, since the clamped 48 is not what the size-adaptive ladder
    selects at these sizes.
    
  • Parser discriminator, 16 MiB/rank (-b 128M), int32/sum — bandwidth
    separates an honoured pin from a rejected one, all with #wrong = 0:

    | `NCCL_GIN_ANVIL_RS_CTAS` | busbw (GB/s) | reading |
    |---|---|---|
    | unset | 206.84 | ladder baseline |
    | `1` | **19.64** | valid pin, honoured — 10.5× collapse |
    | `-2` | 208.45 | rejected, falls back to ladder |
    | `8foo` | 207.56 | rejected, falls back to ladder |
    | `64` | 180.78 | parsed, then clamped to the 48-slot pool |
    
    `-2` and `8foo` landing within 1% of the unset baseline, while a genuinely valid `1`
    collapses to 19.64 GB/s, is the evidence that the hardened parser ignores them rather
    than silently pinning.
    
  • Pre-fix negative control not re-run here. The RS_CTAS=128 hang was observed on
    MI355X before the clamp; no pre-fix binary was built for gfx942, so that control is
    not part of this reproduction.

  • Policy unit tests not run on MI300X — the image is an ONLY_FUNCS rccl-tests
    build without BUILD_TESTS, so rccl-UnitTestsGinSdmaReduceScatterPolicy is absent.

Caveats on this reproduction: the image is gfx942, not the gfx950 target; it was built
with the device linker ON and with rocSHMEM taken from this same monorepo branch (the
Dockerfile default gin-stage2-gda-sdma-only declares gin_anvil::sdma where this branch
expects ::sdma_anvil); and the policy header carried a local patch equivalent to
61b1613ea2c, so the binary is not a byte-clean build of any single pushed commit.

Build-configuration blockers hit (pre-existing; not introduced by this PR)

Building librccl with -DENABLE_ROCSHMEM_GIN=ON at this branch head needs two flag
workarounds. Both live in RCCL library / rocSHMEM code; this PR touches no file under
projects/rccl/src/ and none under projects/rocshmem/, so neither originates here.

  1. -DRCCL_POISON_HIP_ATOMICS=OFF — the default-ON poison from upstream a171d93c8d1
    (fix(rccl): replace __hip_atomic builtins with Clang scoped atomics #11321) force-includes poison_hip_atomics.h into RCCL's GIN sources, which pull in
    rocSHMEM headers that still use __hip_atomic_* (1656 × "attempt to use a poisoned
    identifier" in rocshmem/src/atomic.hpp and src/sdma/anvil_device.hpp).
    Upstream's own projects/rccl/CMakeLists.txt concedes the conflict — it warns that
    "ENABLE_ROCSHMEM and ENABLE_ROCSHMEM_GIN are incompatible with RCCL_POISON_HIP_ATOMICS"
    — but only warns, because gin_device_common.h on develop defaults
    NCCL_GIN_ANVIL_SDMA_ENABLE to 1 whenever ENABLE_ROCSHMEM_GIN is set, pulling
    anvil_device.hpp into every TU that includes gin__funcs.h.
    This branch no longer needs the flag workaround: 264938bdf36 makes the backend
    opt-in per TU (default 0) and 97377b539e8 parses the external Anvil header ahead of the
    poison while keeping it active for all following RCCL code, with
    test_poison_hip_atomics.py locking both directions of that boundary. This is also the
    cause of the GIN CI failure noted above, which was at a commit predating those two.
  2. Drop install.sh --no-device-linker — the librocshmem_gin_host.a extraction
    (ld -r + ar, projects/rccl/src/CMakeLists.txt) fails the RDC offload link with
    lld: error: ...rocshmem_host.bc: Invalid encoding. The surrounding CMake comment only
    documents the device-linker (default) path, so ENABLE_DEVICE_LINKER=OFF appears
    untested for ENABLE_ROCSHMEM_GIN.

These were not bisected against pristine upstream/develop; the attribution is inferred
from file provenance and this PR's file list. Consequently the validated librccl is not
bit-identical to a default-flag CI build, though nothing in the RS device path depends on
either flag.

Made with Cursor

@therock-pr-bot

therock-pr-bot Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

✅ All Policy Checks Passed

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —

🎉 All policy checks passed!

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

🚫 Please fix the failed policies before requesting reviews.

The following policy checks failed:

  • ❌ PR Description

The Not ready to Review label has been added to this PR.
Once all policies pass, the label will be removed automatically.

@amd-argus amd-argus Bot 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.

ARGUS

This PR changes 2,182 lines across 15 files. A change this size is hard to review
well, for a person and for this bot alike: reviewers lose the thread, defects hide in the
volume, and the feedback arrives too late to be cheap to act on. Splitting it into a
stacked pull request
gives each piece a reviewable diff and keeps the history. This review covers the whole change
regardless.

Bottom line: Worth settling before approval: in the low-CTA SDMA tier only one CTA per rank waits for the inbound puts, so sibling CTAs reduce scratch that has not landed (reduce_scatter.cu:302), and the LSA peer index at reduce_scatter.cu:410 is a world rank.

Comment @argus review to re-run, or @argus re-review after pushing.

}
__syncthreads();

if (blockIdx.x == (devComm.rank % peerStride))

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.

Only the CTA with blockIdx.x == rank % peerStride waits for the inbound puts, but ncclLsaBarrierSession at line 307 is a cross-rank rendezvous at one barrier index (nccl_device/impl/lsa_barrier__funcs.h:75-79,121-123), not a join across sibling CTAs on this rank. The other CTAs go straight into the reduce at line 317 and read scratchBase with nothing ordering those reads after this rank's puts, and gin.flush is source-side only (nccl_device/gin.h:310-313). Could every CTA wait the signal, or a grid-wide sync go here?

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 was the right call on the most serious item, and it no longer applies to
the PR: the low-CTA SDMA tier came from c588410b5e4, which I dropped. Branch head is now
61a394d42e2, where grep -cE 'sdmaScratchFits|LowCtaSdma|scratchCap' reduce_scatter.cu returns 0.

The shipped kernel is single-tier (direct LSA read-reduce): every CTA joins the one entry
ncclLsaBarrierSession and reads peers' send buffers directly, so there is no staged scratch
whose landing needs ordering.

Worth recording that your analysis matched the hardware. A sweep of that tier did reproduce
in-place data corruption, which is consistent with the ordering hole you describe — sibling
CTAs reducing scratch that hadn't landed, with gin.flush being source-side only. When the
tier returns in its own PR it will carry either a grid-wide sync or a per-CTA signal wait, and
get reviewed on its own.

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.

Correction to my reply above — on the evidence I cited, not on your finding.

Your reading of c588410b5e4 is confirmed correct by inspection. At that SHA only one block waits:

  if (blockIdx.x == (devComm.rank % peerStride))
    gin.waitSignal(ncclCoopCta(), signalIndex, signalValue + (uint64_t)nRanks);
  gin.flush(ncclCoopCta());

and every block then reads scratchBase in a grid-stride loop (tid = threadIdx.x + blockIdx.x * blockDim.x) after an ncclLsaBarrierSession indexed by blockIdx.x — which, as you said, is a cross-rank rendezvous at one barrier index, not a join across sibling CTAs on this rank. So the hole is real in the code you reviewed.

What I overstated was the hardware corroboration. I wrote that a sweep "did reproduce in-place data corruption, which is consistent with the ordering hole you describe." That sweep ran a different implementation — the hybrid tier on our wip branch, where each block waits for its own nRanks arrivals rather than one block waiting for all — so it is not evidence for the single-waiter hole at this SHA.

A follow-up investigation also re-tallied that corpus and weakened it further: exactly 1 nonzero #wrong row out of 383 datacheck rows (~0.26%), and 105 targeted reruns at the implicated CTA counts reproduced nothing. The earlier ~5% figure I had been working from was the deadlock rate, carried over by mistake. Root cause there is undetermined, and because each row runs the two placements as separate invocations, the in-place specificity rests on n=1 and cannot be called in-place-only.

So your finding stands on its own code reading, which is the part that matters here, and the tier is out of this PR either way. I shouldn't have attached unrelated hardware data to it.

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.

Resolved by 61a394d42e.

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.

Agreed on the analysis, and the hazard is real — but the code it describes is the low-CTA SDMA tier that this PR does not ship. The shipped GinReduceScatterKernel is single-tier LSA read-reduce with a per-CTA barrier session (reduce_scatter.cu:271), so no CTA reads scratch another CTA is still filling. Leaving this open deliberately: AICOMRCCL-1803 scopes the -D4 hybrid LSA+GIN variant, and this constraint has to be honoured when that tier lands.

Comment thread projects/rccl/test/CMakeLists.txt Resolved
Comment thread projects/rccl-tests/src/common.h Resolved
@dlamd1dai
dlamd1dai force-pushed the users/dondai/gin-stage3e-sdma-rs branch from c588410 to 08d1413 Compare September 13, 2026 01:56
@dlamd1dai

Copy link
Copy Markdown
Contributor Author

@argus re-review

dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 13, 2026
Addresses the argus review on ROCm#11556.

reduce_scatter.cu registered barrier/lsaBarrier/signal pools of
max(-V, reduceScatterMaxCtas()) but computed the launched grid with
reduceScatterCtas(), which caps an NCCL_GIN_ANVIL_RS_CTAS pin at 128.
With -V 8 and RS_CTAS=64 that launched 64 CTAs against 48 pool slots,
so blockIdx.x 48..63 indexed past devComm.lsaBarrier.

Mirror the AllGather sibling: reduceScatterPoolCtas() is now both the
pool size and the ceiling reduceScatterGridCtas() clamps the grid to,
so "grid <= pool" is decided in one place. Four new policy unit tests
lock the invariant at every band edge (23/23 pass).

ReduceScatterParseCtasEnv now rejects a leading '-' and trailing
garbage like the AllGather/Broadcast parsers, so RS_CTAS=-2 no longer
wraps to a huge unsigned and "8foo" no longer silently pins 8.

The two ReduceScatter unit-test targets move out of the AllGather
EXISTS guard into their own, keyed on gin_sdma_reducescatter_policy.h;
the shared --offload-arch list is hoisted out so both guards can use
it. Also adds Broadcast to the device-timing hook comment and the
copyright year.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dlamd1dai dlamd1dai self-assigned this Sep 13, 2026
@dlamd1dai

Copy link
Copy Markdown
Contributor Author

@argus re-review

Pushed 61a394d42e2. Since the review you commented on (c588410b5e4), two things changed that affect a good number of your findings:

  1. The low-CTA SDMA tier commit was dropped, which removes the single-CTA-wait race you flagged as the bottom-line blocker, along with sdmaScratchFits, reduceScatterLaunchCtas, the scratch globals and the 4 low-CTA pytest launches.
  2. The remaining defects are fixed: the LSA-vs-world peer index (ncclTeamLsa gate, as in all_gather.cu), the barrier-pool overrun under NCCL_GIN_ANVIL_RS_CTAS, the ReduceScatterParseCtasEnv validation, the fp8 prod/mulsum skip, the unbuilt RS policy UT target, the device-API gin-type default, and the aborted FAILED RUNS summary.

Each of your 28 inline comments has a threaded reply stating which of those applies and, where I deferred, why. Two are worth your attention on re-read: the pool-overrun fix added reduceScatterPoolCtas/reduceScatterGridCtas plus 4 tests for the invariant you identified at gin_sdma_reducescatter_policy_test.cpp:91, and the CMake guard split needed the --offload-arch hoist you warned about.

@amd-argus amd-argus Bot 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.

ARGUS

This PR changes 2,126 lines across 16 files. A change this size is hard to review
well, for a person and for this bot alike: reviewers lose the thread, defects hide in the
volume, and the feedback arrives too late to be cheap to act on. Splitting it into a
stacked pull request
gives each piece a reviewable diff and keeps the history. This review covers the whole change
regardless.

Bottom line: Worth settling before approval: test_ReduceScatterGinSdma.py treats -b as a per-rank size when it is the total, so the CTA-ladder rows and the 2 GiB hang guard run at 1/8th their documented sizes.

Comment @argus review to re-run, or @argus re-review after pushing.

dlamd1dai and others added 14 commits September 13, 2026 15:53
Move MPI launcher assembly, connectivity-gate retry, and Broadcast debug-log
capture into gin_sdma_harness.py so AllGather, AllToAll, and Broadcast GIN-SDMA
pytest harnesses share one implementation and stop drifting.

Co-authored-by: Cursor <cursoragent@cursor.com>
Build device-api CI with ENABLE_DEVICE_API + ENABLE_ROCSHMEM_GIN via cmake,
dedupe rocshmem RDC link blocks, move GPU AllGather gtest to gpu-only label,
add AllGather policy header missing STATUS, and link rocSHMEM into
FixturesDebug for GIN unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
ENABLE_ROCSHMEM_GIN was defaulting NCCL_GIN_ANVIL_SDMA_ENABLE to 1, so
non-GIN fat objects pulled rocSHMEM anvil_device.hpp and failed under
RCCL_POISON_HIP_ATOMICS. Keep SDMA opt-in on GIN kernel TUs and rccl-tests
RDC binaries.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Device-api CI builds one --rocshmem-gin librccl.so, which leaves GIN
rocSHMEM host symbols to the executable. Every binary the matrix launches
now whole-archive RDC-links rocSHMEM, not only the GIN collectives.

all_reduce_perf still does not build under ENABLE_ROCSHMEM_GIN (same as
gin.sbatch), so it is not RDC-linked, not required after `make -k`, and
the LSA -D 1/-D 2 suites that launched it are dropped until it builds.
The runner's perf-dir sentinel moves to alltoall_perf.

Harness: extract gin_perf_argv()/gin_hang_msg() so the three collectives
share one argv vector and the AllToAll -A 1 gap is explicit; make
detect_ngpus() fail when rocminfo is missing instead of reporting zero
GPUs; pin CONN_GATE_RE with a non-gate failure case.

Co-authored-by: Cursor <cursoragent@cursor.com>
Parse rocSHMEM's external anvil_device.hpp before RCCL poisons the
__hip_atomic_* builtins, while retaining the poison for all following RCCL
code. Share the GIN-SDMA backend definitions between regular and DeviceLinker
compilation so both pipelines opt in consistently.

Restore NCCL_GIN_ANVIL_SDMA_ENABLE=1 on stub-backed tests and let Debug
fixtures link rocSHMEM without requiring GIN_ANVIL_UNIT_TESTS.

Co-authored-by: Cursor <cursoragent@cursor.com>
Compile a guarded stand-in for rocSHMEM's anvil_device.hpp with a
__hip_atomic_load call and verify that the explicitly opted-in external header
is parsed before RCCL activates its atomics poison.

Co-authored-by: Cursor <cursoragent@cursor.com>
After parsing the external Anvil stub, spell __hip_atomic_load directly in the
RCCL probe and require the compiler to reject it. This ensures the external
header exception does not disable the poison for following RCCL code.

Co-authored-by: Cursor <cursoragent@cursor.com>
…age3e wip

Port the single-tier LSA read-reduce kernel, policy/reduce headers, host and GPU
unit tests, opt-in pytest regression/devtime smoke, and reduce_scatter_perf
rocSHMEM link fix from users/dondai/gin-stage3e-sdma-rs-nccl-2.30.7-wip,
reconciled to stage3e conventions (shared CLI devtime, ncclCommQueryProperties
devComm reqs, whole-archive rocSHMEM link). Wire ReduceScatter devtime into
device-api CI alongside AllToAll.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nventions

Add initCommConfig and update getDevCommRequirements version guards after cherry-pick onto develop.

Co-authored-by: Cursor <cursoragent@cursor.com>
Dispatch pytest matrix entries by collective so RS uses reduce_scatter_perf
and RCCL_TESTS_GIN_SDMA_RS, matching the Broadcast GinSdma job.

Co-authored-by: Cursor <cursoragent@cursor.com>
Four correctness fixes from the amd-argus review of this PR.

1. reduce_scatter.cu: gate -D 3 on world == LSA team.
   The read-reduce calls ncclGetLsaPointer(sendwin, sendoffset, s) with s over
   [0, devComm.nRanks), feeding a WORLD rank where an LSA-team index is expected
   (ncclGetLsaPointer resolves lsaFlatBase + peer*stride4G). Those coincide only
   when the world team is exactly the LSA team, so a comm whose LSA team is a
   subset read the wrong peer or outside the aperture. Benign on a single-node
   all-P2P comm, which is why the datacheck never caught it. Added the same
   nRanks != ncclTeamLsa(comm).nRanks gate all_gather.cu already carries.

2. reduce_scatter.cu: restore the unconditional fp8 prod/mulsum skip.
   It had been moved inside `if (deviceImpl != 0)`, which re-enabled fp8 prod and
   mulsum on the DEFAULT -D 0 host path that upstream excluded on purpose
   (upstream/develop reduce_scatter.cu:121-125). The exclusion is unconditional
   again; fp8 avg stays supported, per that upstream comment -- note this is
   deliberately narrower than all_reduce.cu, which also excludes fp8 avg. The
   device path keeps its separate mulsum-for-any-type skip, which is what it
   actually needs since SPECIALIZE_REDUCE_KERNEL returns nullptr there.

3. run-device-api-ci.sh: stop aborting before the failure summary.
   run_devtime_smoke ends in `return ${failed}` and was called bare under
   `set -euo pipefail`, so any smoke failure exited the script at that call and
   the FAILED RUNS summary (and its exit 1) never ran. Call it with `|| true`;
   FAILED_RUNS already carries the failures and the summary owns the exit code.

4. run-device-api-ci.sh / gin.sbatch: two config mismatches.
   RCCL_TESTS_RS_GIN_TYPE defaulted to 6 (Anvil-SDMA), but device-api.sbatch
   builds librccl with a plain `install.sh -lj` so the plugin is compiled out of
   the #ifdef ENABLE_ROCSHMEM_GIN entirely. Default to 2 (proxy), matching the
   AllToAll smoke in the same script. And gin-tests.json runs
   rccl-UnitTestsGinSdmaReduceScatterPolicy while gin.sbatch built only
   Fixtures + GinSdmaBroadcastPolicy, so that entry was a permanent SKIP scored
   as a failure; it is now built and existence-checked like its sibling.

Not addressed here (style/coverage, not correctness): the repeated three-line
fold, the rsum/rprod widen-narrow duplication, the tautological
gin_sdma_reducescatter_gpu_test.cpp, and the devtime pytest duplication.

GIN_PYTEST_RS_HW_CASES=11 is correct for this branch: the pytest collects 11
cases (8 CTA ladder + 3 hang guard). The review counted 15 against the dropped
low-CTA commit, which added 4 more launches.

Validated: bash -n on all three scripts; policy header compiles with
GIN_SDMA_HOST_ONLY; RS pytest collects 11 tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Addresses the argus review on ROCm#11556.

reduce_scatter.cu registered barrier/lsaBarrier/signal pools of
max(-V, reduceScatterMaxCtas()) but computed the launched grid with
reduceScatterCtas(), which caps an NCCL_GIN_ANVIL_RS_CTAS pin at 128.
With -V 8 and RS_CTAS=64 that launched 64 CTAs against 48 pool slots,
so blockIdx.x 48..63 indexed past devComm.lsaBarrier.

Mirror the AllGather sibling: reduceScatterPoolCtas() is now both the
pool size and the ceiling reduceScatterGridCtas() clamps the grid to,
so "grid <= pool" is decided in one place. Four new policy unit tests
lock the invariant at every band edge (23/23 pass).

ReduceScatterParseCtasEnv now rejects a leading '-' and trailing
garbage like the AllGather/Broadcast parsers, so RS_CTAS=-2 no longer
wraps to a huge unsigned and "8foo" no longer silently pins 8.

The two ReduceScatter unit-test targets move out of the AllGather
EXISTS guard into their own, keyed on gin_sdma_reducescatter_policy.h;
the shared --offload-arch list is hoisted out so both guards can use
it. Also adds Broadcast to the device-timing hook comment and the
copyright year.

Co-authored-by: Cursor <cursoragent@cursor.com>
parseReduceScatterCtasEnvString and parseReduceScatterUnrollMinBytesString
move out of reduce_scatter.cu into gin_sdma_reducescatter_policy.h, behind the
host-only guard, so the rejection cases can be locked by the policy unit test
without compiling the HIP TU. reduce_scatter.cu keeps the caching wrappers.

UNROLL_MIN also gains an overflow guard: a MiB value above (size_t)-1 >> 20
would wrap when shifted, so it now falls back to 0 (tier off) rather than
landing on an arbitrary threshold.

PoolCoversTheSelfSelectedLadderAtEveryBandEdge now asserts on
reduceScatterCtas rather than reduceScatterGridCtas. The clamped form could not
exceed its own bound, so the old assertion held by construction; the ladder
form tests the property the name describes. reduceScatterGridCtas stays covered
by the adjacent pin/clamp tests.

Also retires the stale reducescatter-gin-sdma-phase2.md references in favour of
AICOMRCCL-1803 for the follow-up put-partials tier.

Co-authored-by: Cursor <cursoragent@cursor.com>
ReduceScatterGetCollByteCount divides the -b count by nranks to get the
per-rank output slice, so the pytest helpers were understating every size by a
factor of nranks. At NP=8 the "256 MiB/rank" hang guard was really 32 MiB/rank
and the devtime smoke at 4 MiB never reached the mid CTA band it was meant to
exercise. The helpers now take the per-rank slice and multiply by RS_NP, and
the devtime smoke moves to 32 MiB so it lands inside [8 MiB, 48 MiB).

The datacheck regex was matching the literal "#wrong" column header via the
bare "Wrong" alternative, so a clean run could be read as a failure. It now
requires "#wrong = <nonzero>".

Adds avg and prod to the CTA-ladder matrix, skips the 2 GiB-total guard below
RS_NP=8 since that case exists to exercise the 8-way grid-stride path, and
raises GIN_PYTEST_RS_HW_CASES from 11 to 15 to match the new launch count.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dlamd1dai
dlamd1dai force-pushed the users/dondai/gin-stage3e-sdma-rs branch from 61a394d to 6031206 Compare September 22, 2026 22:10
dlamd1dai and others added 3 commits September 22, 2026 18:29
_assert_datacheck_clean returned early on a clean row and failed on a nonzero
one, but output matching none of its patterns fell off the end and passed. A
run that died before printing a table row therefore passed on no evidence.
The nonzero check now runs first so a nonzero row wins over any clean row
printed beside it, and the absence of all three patterns is an explicit
failure.

Also adds the missing 2026 copyright year to the two ReduceScatter gtest
files, matching gin_sdma_reducescatter_policy.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
…mments

The no-exit-barrier rationale claimed sendbuff is never written by the kernel,
which does not hold in place: ReduceScatterGetCollByteCount sets
recvInplaceOffset to the slice base, so recvbuff aliases into the sendbuff
allocation. The safety argument is that rank R writes only slice R and no peer
reads it, so state that instead.

SPECIALIZE_REDUCE_KERNEL is defined in reduce_scatter.cu, not common.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
dbc66d2 moved the CTA/unroll env parsers into the policy header behind
`#if !defined(__CUDA_ARCH__) && !defined(__HIP_DEVICE_COMPILE__)`. HIP parses
the whole translation unit in the device pass, including the bodies of host
functions it will not codegen, so the guard made name lookup fail inside
ReduceScatterParseCtasEnv and ReduceScatterUnrollMinBytes:

  error: no member named 'parseReduceScatterCtasEnv' in namespace
  'gin_sdma_reducescatter'
  2 errors generated when compiling for gfx942.

That broke reduce_scatter_perf on every arch, not just gfx942. Plain `inline`
is already host-only, which is what gin_sdma_allgather_policy.h's
parseAllGatherCtasEnv relies on, so the guard is dropped and a comment records
why it must not come back.

Verified with hipcc --offload-arch=gfx942 -fsyntax-only: the guarded form fails
with the errors above, the unguarded form compiles clean.

Co-authored-by: Cursor <cursoragent@cursor.com>

@amd-argus amd-argus Bot 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.

ARGUS

Findings

1 finding(s) that could not be posted as inline comments
  • projects/rccl/tools/ci/lib/device-api-tests.json:41 (Medium) The gating device-api matrix gained a -D 3 AllGather suite at type 6 but no ReduceScatter counterpart The matrix gains gin-d3-allgather at NCCL_GIN_TYPE=6 but no ReduceScatter counterpart, so the gating device-api job never runs this PR's kernel on Anvil-SDMA, even though reduce_scatter_perf is a hard requirement of that job (device-api.sbatch:160-170 exits 1 without it). The only type-6 RS sweep is rccl-gin-rocshmem-sdma-rs-d3 in gin-tests.json:66-71, and gin.sbatch:249 turns that job's failures into a non-gating sentinel. Could the RS suite join the matrix next to the AllGather one?

This PR changes 3,730 lines across 29 files. A change this size is hard to review
well, for a person and for this bot alike: reviewers lose the thread, defects hide in the
volume, and the feedback arrives too late to be cheap to act on. Splitting it into a
stacked pull request
gives each piece a reviewable diff and keeps the history. This review covers the whole change
regardless.

Bottom line: Worth settling before approval: the entry LSA barrier at reduce_scatter.cu:272 syncs relaxed, so the peer read-reduce that follows has no acquire edge.

Comment @argus review to re-run, or @argus re-review after pushing.

memset(reqs, 0, sizeof(*reqs));
switch(deviceImpl) {
#if NCCL_VERSION_CODE >= NCCL_VERSION(2,28,7)
case 3: { // single-tier LSA read-reduce: barriers only, no scratch

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.

This case 3 returns true unconditionally, so the lsaSize == nRanks gate added at reduce_scatter.cu:167 and the ginType == NCCL_GIN_TYPE_NONE check at :175 both apply only to the >= 2.29.0 overload. ginReduceScatterBody is the same either way and indexes ncclGetLsaPointer(sendwin, sendoffset, s) over [0, devComm.nRanks), which is unchecked arithmetic (lsaFlatBase + peer*stride4G, nccl_device/impl/core__funcs.h:114), so lsaSize < nRanks reads (s - lsaSize) * 4 GiB past the aperture. all_gather.cu's ungated 2.28 arm is safe for a different reason: AllGatherLsaDirect takes lsaRanks and loops lp < lsaRanks (all_gather.cu:163-172, called with lsa.nRanks at :195), so it is bounded in the kernel. Gate: only builds against RCCL 2.28.7 to 2.28.x, and the in-tree RCCL is 2.30.7 (projects/rccl/makefiles/version.mk), so nothing here compiles this arm.


ncclTeam lsa = ncclTeamLsa(devComm);
ncclLsaBarrierSession<ncclCoopCta> lsaBar { ncclCoopCta(), devComm, lsa, devComm.lsaBarrier, blockIdx.x };
lsaBar.sync(ncclCoopCta(), cuda::memory_order_relaxed);

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.

lsaBar.sync(..., cuda::memory_order_relaxed) is the only ordering construct before the peer loads begin at reduce_scatter.cu:316, and it supplies no acquire edge: arrive fences with releaseOrderOf(relaxed) and stores the inbox relaxed, waitInternal loads it with acquireOrderOf(relaxed) (nccl_device/impl/lsa_barrier__funcs.h:72,78,126), and sync is arrive plus wait (:166-169). The comment at :508-510 leans on this barrier to guarantee every peer's sendbuff is filled before any read. The closest sibling is allReduceLsaKernel, which folds peers' sendbuffs the same way and enters with acquire (all_reduce.cu:167 before its fold at :176-179); alltoall.cu:353 does too. The relaxed entries in all_gather.cu:194 and broadcast.cu write to peers and carry the memory_order_release exit sync this kernel deliberately omits.

Suggested change
lsaBar.sync(ncclCoopCta(), cuda::memory_order_relaxed);
lsaBar.sync(ncclCoopCta(), cuda::memory_order_acquire);

RCCL_TESTS_GIN_SDMA_DEVTIME=1 \
RCCL_TESTS_RS_EXE="${rs_bin}" \
RCCL_TESTS_RS_NP="${NP}" \
RCCL_TESTS_RS_GIN_TYPE="${RCCL_TESTS_RS_GIN_TYPE:-2}" \

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.

RCCL_TESTS_RS_GIN_TYPE:-2 binds the proxy backend, while the comment at run-device-api-ci.sh:225-227 says type 6 is in this build and that the default matches gin-tests.json. Three other places say 6: gin-tests.json:69, test_ReduceScatterDevtime.py:41 and its actual default at :83. We asked for 2 last round on the premise that device-api.sbatch built librccl without rocSHMEM; this PR changes that at device-api.sbatch:126 (--rocshmem-gin), so the premise no longer holds and the one gating -D 3 ReduceScatter run never touches Anvil-SDMA.

Suggested change
RCCL_TESTS_RS_GIN_TYPE="${RCCL_TESTS_RS_GIN_TYPE:-2}" \
RCCL_TESTS_RS_GIN_TYPE="${RCCL_TESTS_RS_GIN_TYPE:-6}" \

loop = devtimeLoopMid;
if (devtimeSkipMid >= 0) skip = devtimeSkipMid;
else skip = (skip < 2) ? skip : 2;
}

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.

This block reproduces gin_devtime::resolveLoopSkip (gin_sdma_devtime.h:34-46, already included at reduce_scatter.cu:20) except that it compares totalBytesCta, the message size summed across ranks, against the 8 MiB and 64 MiB tier constants. AllGatherDeviceTime calls the shared helper with per-rank chunkBytes (all_gather.cu:329-332), and the flag help text says "loop at per-peer >= 8 MiB" (common.cu:2315), so at NP=8 --devtime_loop_mid arms at one eighth of the documented threshold here. Both tier vars default to 0 so nothing arms today.

Suggested change
}
int loop = 0, skip = 0;
gin_devtime::resolveLoopSkip(count * wordSize(type), loop, skip);

*busBw = baseBw * factor;
switch(deviceImpl) {
case 3: { // GinReduceScatterKernel: single-tier LSA read-reduce (no scratch)
if (commProperties.ginType == NCCL_GIN_TYPE_NONE) {

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.

This rejects the comm when commProperties.ginType == NCCL_GIN_TYPE_NONE, but ginReduceScatterBody (reduce_scatter.cu:265-515) constructs no ncclGin, issues no put, waits no signal and calls no flush; its only synchronization is the entry ncclLsaBarrierSession at :271. On a P2P-complete node with GIN disabled, reduce_scatter_perf -D 3 therefore refuses a configuration the kernel would run correctly. Is the gate there to hold the door open for the put-partials tier under AICOMRCCL-1803? Please check.

const int rsNRanks = (rsDc != nullptr) ? rsDc->nRanks : 1;
const size_t rsTotalBytes = count * (size_t)wordSize(type) * (size_t)rsNRanks;
static const size_t rsCtasEnv = ReduceScatterParseCtasEnv("NCCL_GIN_ANVIL_RS_CTAS");
const int rsGridCtas = gin_sdma_reducescatter::reduceScatterGridCtas(

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.

This three-line grid-CTA selection is repeated verbatim at reduce_scatter.cu:639-641, differing only in the total-bytes variable name (rsTotalBytes versus totalBytesCta), and both copies also declare their own function-local static parse of the same env var. The AllGather sibling collapses exactly this into AllGatherResolveLaunchCtas at all_gather.cu:157-160, which both its call sites use. Could a ReduceScatterResolveLaunchCtas(totalBytes) do the same here, leaving one place where pool and grid are reconciled?

--cmake-options "-DROCSHMEM_INSTALL_DIR=${ROCSHMEM_INSTALL_DIR}"
)
rccl_prefix="${workdir}/projects/rccl/build/release"
rccl_cfg="$(ls "${rccl_prefix}"/lib*/cmake/rccl/*[Cc]onfig.cmake 2>/dev/null | head -n1)"

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.

set -euo pipefail is in force from device-api.sbatch:29, and the exit status of a bare var=$(pipeline) assignment is the command substitution's, which under pipefail is ls's. An unmatched glob makes ls exit 2, so the script terminates at this line and the block at :131-135, the ERROR: no RCCL cmake config message and the ls -laR of what actually exists, never runs. ls stderr is already discarded by 2>/dev/null, so a half-built RCCL ends the job with nothing useful in build-%j.err. Could it take || true?

Suggested change
rccl_cfg="$(ls "${rccl_prefix}"/lib*/cmake/rccl/*[Cc]onfig.cmake 2>/dev/null | head -n1)"
rccl_cfg="$(ls "${rccl_prefix}"/lib*/cmake/rccl/*[Cc]onfig.cmake 2>/dev/null | head -n1 || true)"

_detected_ngpus = detect_ngpus()
except RuntimeError:
_detected_ngpus = 0
ngpus = max(1, _detected_ngpus)

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.

detect_ngpus's docstring says it raises precisely to stop this: a missing binary used to look like zero GPUs because rocminfo | grep | wc -l still exits 0, and callers that clamp with max(1, ...) then silently collect a one-GPU matrix (gin_sdma_harness.py:51-57). Here the raise is caught and the clamp applied anyway, so on a box with no rocminfo and neither visible-devices var ngpus_single collapses to ["1"] where the pre-PR int(math.log2(0)) failed at collection. test_Broadcast.py:45-49 does the same. Could the RuntimeError propagate here instead?

Suggested change
ngpus = max(1, _detected_ngpus)
ngpus = detect_ngpus()

// functions it will not codegen, so hiding these there breaks name lookup in
// ReduceScatterParseCtasEnv/ReduceScatterUnrollMinBytes and fails the
// reduce_scatter_perf build on every arch. Plain `inline` is already host-only;
// it is what gin_sdma_allgather_policy.h's parseAllGatherCtasEnv relies on.

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.

This says plain inline is what gin_sdma_allgather_policy.h's parseAllGatherCtasEnv relies on, but that function sits inside #if !defined(__CUDA_ARCH__) && !defined(__HIP_DEVICE_COMPILE__) at gin_sdma_allgather_policy.h:158, and all_gather.cu:148-155 adds a second device-pass guard at the call site. The reasoning in the two lines above stands on its own; only the appeal to the sibling is false, and a maintainer who trusts it will remove the wrong guard.

Suggested change
// it is what gin_sdma_allgather_policy.h's parseAllGatherCtasEnv relies on.
// reduce_scatter_perf build on every arch. Plain `inline` is already host-only
// (gin_sdma_allgather_policy.h:158 instead excludes its parsers from that pass).

rc, out = _run_devtime(request, device_timing_mode=2)
assert rc == 0, "reduce_scatter_perf exited {} (mode -B 2)".format(rc)
_assert_datacheck_clean(out)
assert "WARN --device_timing=2: no in-kernel device-time" not in out, (

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.

When the timed kernel produces nothing, common.cu:1189-1196 prints this WARN, sets skipMetricRow and the process still exits 0, so assert rc == 0 and _assert_datacheck_clean both pass and only the message wording stands between a silent no-op and a green test. The --devtime_check block that returns testInternalError (common.cu:1216-1220) sits inside the deviceDeltaSec > 0 branch, so the mode-3 test at line 191 has the same hole. Mode 1 at line 175 has the real check, a positive #[rs-devtime] value. Worth dropping these two rather than repairing them?

@dlamd1dai

Copy link
Copy Markdown
Contributor Author

The ReduceScatter slice of this PR is now #12332, rebased onto current develop.

Left on their existing PRs: the shared pytest harness (#11894) and the device-api rocSHMEM CI build (#11895). The poison-isolation change stays closed with #11893. The device-api ReduceScatter devtime smoke waits until #11895 lands.

@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Closing this PR. It conflicts with develop, and the work that should land is on the replacement PRs:

The poison-isolation change stays closed with #11893. The device-api ReduceScatter devtime smoke waits until #11895 lands.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant