Skip to content

feat(rccl/gin-sdma): GIN SDMA Broadcast (-D3 hybrid Anvil-SDMA) - #11142

Merged
dlamd1dai merged 26 commits into
ROCm:developfrom
dlamd1dai:users/dondai/gin-stage3c-sdma-bc
Sep 9, 2026
Merged

dlamd1dai merged 26 commits into
ROCm:developfrom
dlamd1dai:users/dondai/gin-stage3c-sdma-bc

Conversation

@dlamd1dai

@dlamd1dai dlamd1dai commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the GIN Anvil-SDMA hybrid Broadcast (broadcast_perf -D 3, NCCL_GIN_TYPE=6), completing the SDMA collective set alongside the merged AllToAll (#7826, #10420) and AllGather (#10930).

JIRA ID : AICOMRCCL-1799

Size-adaptive tiering (-D 3), all selected by a single pure policy header:

tier range (default) shape
LL ≤ 2 KiB LatencyLayover slots
LSA ≤ 256 KiB direct xGMI stores from root
flat GIN > 256 KiB root gin.put() fan-out, receiver-side waitSignal(+1)
scatter + allgather ≥ 2 MiB distributes egress across all ranks instead of capping at root (busBw = B_root_egress / N)
pipelined ring ≥ 32 MiB edge-disjoint SM ring, ~128 CTAs
  • gin_sdma_broadcast_policy.h: pure, host-testable tier policy — threshold + LL-cap env resolution, scatter/ring chunk math, signal-count sizing. Compiles under GIN_SDMA_HOST_ONLY as plain C++ and is called from the __global__ kernels, so the host-tested decisions are exactly the ones that run on device.
  • broadcast.cu: the tiered kernels, devtime hook via the shared gin_sdma_devtime.h scaffold, rocSHMEM RDC link for broadcast_perf.
  • rccl-UnitTestsGinSdmaBroadcastPolicy host GTest (51 cases), plus opt-in pytest multi-segment regressions with 2 GiB / 4 GiB hang guards.
  • GIN CI matrix: broadcast_perf -D 3 (rccl-gin-rocshmem-sdma-bcast-d3) and pytest test_Broadcast.py -k gin_sdma (rccl-gin-bcast-pytest) via run-gin-ci.sh, both with NCCL_GIN_TYPE=6.

11 commits, 7 files, +2242/−50. Touches zero AllGather files.

Why this lives in rccl-tests rather than src/algorithms/gin/

#10386 recently added an in-library GIN SDMA AllToAll under projects/rccl/src/algorithms/gin/, so it is fair to ask why Broadcast is not there too.

These are different layers rather than competing homes. The in-library collective is host-side dispatch through collectives.cc: the application calls ncclAllToAll and RCCL selects a GIN implementation internally. This PR is the device-API path — the collective runs inside the caller's own kernel via ncclDevComm, which is what -D 3 in rccl-tests exercises and what the GIN device-kernel work item (AICOMRCCL-1796..1800) is chartered to deliver. It has no host-side entry point to dispatch from.

This also follows the merged AllGather precedent (#10930) exactly, keeping the two GIN-SDMA device collectives side by side in one tree so the shared policy/devtime scaffolding stays comparable. Promoting the device-API tiers into src/algorithms/gin/ later is a reasonable direction, but it should be one deliberate migration covering AllGather and Broadcast together, not a divergence introduced here.

Base

Rebased onto develop at 1c1419a9434 (conflict-free; gin-tests.json and rccl/test/CMakeLists.txt both picked up upstream's newer entries alongside this PR's). Previously stacked on the AllGather branch, now merged as #10930 — all inherited AllGather commits were dropped, along with three stale carries that would have regressed develop: the gin_anvil_sdma_oss7_device.cc conn-check TU that #10930 deliberately removed (unlinked __hip_fatbin in GIN CI, being restored properly in #11092), an alloc.h revert of the side-stream pooling, and pre-review versions of the AllGather sources.

Defects found and fixed while preparing this PR

1. -D 3 failed to compile in the default configuration (42eaaeb2927)

BroadcastRunColl case 3 was gated on the NCCL version alone, but everything it calls — testLaunchDeviceKernelThreshold{,Ctas,LL}, the Gin*BroadcastKernel symbols, buildBcastRingDecomp and the g_bcastBuilt* ring state — is defined inside defined(ENABLE_DEVICE_API) && NCCL_VERSION_CODE >= NCCL_VERSION(2,28,0) blocks. ENABLE_DEVICE_API defaults to OFF, so a stock rccl-tests configure against NCCL ≥ 2.28.7 compiled the call site with every callee preprocessed out.

Now gated on both macros, matching the merged all_gather.cu and BroadcastDeviceTime further down this same file. Verified by compiling both ways:

unpatched:  ENABLE_DEVICE_API=ON  -> compiles
            ENABLE_DEVICE_API=OFF -> error: use of undeclared identifier 'g_bcastBuiltN'
patched:    ENABLE_DEVICE_API=ON  -> compiles
            ENABLE_DEVICE_API=OFF -> compiles

2. Negative threshold env values silently pinned the LSA tier (02fc3917082)

parseSize() fed the raw string to strtoull(), which accepts a leading - and wraps it near SIZE_MAX. -1 happened to land on kThresholdUnset and was harmless, but -4K parsed as a ~16 EiB threshold, so every message compared as "below threshold", took the LSA tier, and the sweep still reported pass — the GIN/SDMA path would simply never run. Rejects the sign before parsing now, matching the AllGather policy header.

3. Broadcast CI and pytest selected the wrong GIN backend (956427cfbf1)

Both requested NCCL_GIN_TYPE=5. That is correct on the 2.30.7 line this work was developed against, but the enum was renumbered on develop:

develop:  GDAKI=3, ROCSHMEM_GDA=5, ANVIL_SDMA=6
2.30.7:   GDAKI=3, ROCSHMEM_GDA=4, ANVIL_SDMA=5

Plugin selection is a strict equality match on props.netDeviceType, so on develop a request for 5 resolves to the rocSHMEM GDA backend rather than Anvil-SDMA. The Broadcast CI entry would have silently exercised the wrong backend, while sitting directly between two AllToAll entries that already say 6. Now 6 everywhere, matching the merged test_AllGather.py, test_AllToAll.py and the AllToAll CI entries.

4. .getDevCommRequirements left null on 2.29+ without the device API (a23b7871504)

Wired under the same NCCL >= 2.29 || (ENABLE_DEVICE_API && >= 2.28) guard common.h declares the slot with. Same commit hardens BroadcastParseCtasEnv against a leading -, trailing junk (8foo parsed as 8) and a literal sentinel collision, matching gin_sdma_allgather::parseAllGatherCtasEnv().

Test plan

  • rccl-UnitTestsGinSdmaBroadcastPolicy — 51/51 pass (host-only; Host unit tests (CPU-only) CI green on prior run)
  • broadcast_perf -D 3 formal gate, 8× MI355X — PASS, 336 measured rows across all 8 roots, #wrong=0 out-of-place and in-place
  • Large-message sweep 128 MiB → 4 GiB (ring + scatter/allgather tiers) — PASS, #wrong=0
  • ENABLE_DEVICE_API on/off compile check (see above)
  • GIN CI: broadcast_perf entry in gin-tests.json via run-gin-ci.sh (rccl-gin-rocshmem-sdma-bcast-d3, NCCL_GIN_TYPE=6; GIN job re-running at 55e3f180b5e)
  • pytest test_Broadcast.py -k gin_sdma end-to-end — 8/8 pass on 8× MI355X SUT (44.6s); wired into GIN CI as rccl-gin-bcast-pytest (55e3f180b5e: exports RCCL_TESTS_GIN_SDMA_BCAST=1, BCAST_EXE, BCAST_GIN_TYPE=6; not device-API, which runs PROXY/type 2)

SUT evidence — 8× MI355X gfx950, smci355-ccs-aus-m03-17, Anvil-SDMA backend

Run against a docker image built from this commit's Broadcast sources. The image tree is the NCCL 2.30.7 validation line, where the Anvil-SDMA backend is NCCL_GIN_TYPE=5 (it is 6 on develop — see defect 3 above); the runs were confirmed to bind the intended backend via Assigned GIN plugin gin-anvil-sdma to comm in the init log, not by trusting the number.

RUN_POLICY_UT=0 gin-sdma-bcast-test.bash 8 128M
  -> PASS  BC-C1 host baseline + BC-C2 GIN, roots 0..7, 336 rows, #wrong=0
     per-root avg busbw 53.7 - 54.3 GB/s (no root privileged)

RUN_BC_C2=0 RUN_HOST_BASELINE=0 RUN_BC_C2_LARGE=1 gin-sdma-bcast-test.bash 8 128M
  -> PASS  BC-C2-L 128M..4G, root=0, #wrong=0

RCCL_TESTS_GIN_SDMA_BCAST=1 pytest test/test_Broadcast.py -k gin_sdma -v
  -> PASS  8 cases (256 MiB/2 GiB segmented × 3 dtypes + 2 GiB/4 GiB hang guards), 44.6s
size out-of-place in-place
512 MiB 341.7 GB/s 343.5 GB/s
1 GiB 356.8 GB/s 360.8 GB/s
2 GiB 366.5 GB/s 369.0 GB/s
4 GiB 373.4 GB/s 374.8 GB/s

Follow-up (not in this PR)

The three collectives now carry duplicated policy scaffolding: two __host__ __device__ attribute macros (GIN_SDMA_AG_HD / GIN_SDMA_HD), two namespaces (gin_sdma_allgather / gin_sdma), threshold precedence and 16-byte chunk alignment implemented twice, size-string parsing three times, and near-identical pytest launch/retry helpers. Worth consolidating once this lands and after the AICOMRCCL-2217..2221 stack, which already dedups the Anvil-SDMA env parsing and the pytest connectivity-gate retry.

Separately, GIN_SDMA_HD is defined unconditionally in gin_anvil_sdma_put_policy.h while the Broadcast header guards it with #ifndef. Safe today only because the expansions are identical; it wants an #ifndef on the canonical definition. Deliberately left out of this PR to keep the diff to Broadcast.

@therock-pr-bot

therock-pr-bot Bot commented Sep 2, 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?

@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.

Automated Review Guard for Upfront Scrutiny

Bottom line: Two device-side hazards to settle before merge: the ring constant tables are uploaded only for the first device in a process (broadcast.cu:1214), and every CTA snapshots the GIN signal before a per-block barrier, so a late CTA can wait forever (broadcast.cu:498).

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

Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl/test/CMakeLists.txt Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
… testable

Argus review on ROCm#11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
…sed kernels

Argus review on ROCm#11142. The load-bearing fix is the first one;
the rest remove the surface that made it hard to see.

Per-device ring tables. c_bcastNRings / c_bcastRingSucc / c_bcastRingPos /
c_bcastStream are __constant__, so they exist once per DEVICE, but the
"already built" gate (g_bcastBuiltN / g_bcastBuiltNRings) was process-wide. In a
-g N single-process multi-GPU run device 0 uploaded the decomposition and set
the flag, then devices 1..N-1 saw the flag set, skipped their upload, and ran
the ring against a zero-filled table: blockIdx.x % c_bcastNRings divides by
zero, and every rank reads position 0 and so believes it is the root. Key the
state on the current device instead (startColl calls cudaSetDevice immediately
before runColl on the device-API path). Distinct host threads own distinct
devices, so this also keeps -t N off a shared slot; the remaining shared
scratch -- buildBcastRingDecomp's backtracking object and the h_succ/h_pos
staging arrays -- becomes thread_local.

This was not caught because it needs -g N > 1: the GIN CI entry runs -g 1 under
mpirun, and the MI355X gates were MPI-based too.

g_bcastLLHandle / g_bcastLLReq become thread_local for the same -t N reason:
getDevCommRequirements runs once per thread and links g_bcastLLReq into that
thread's requirements list, and one shared node spliced into N lists corrupts
them. Note the handle itself did NOT need per-device treatment, contrary to the
review comment: ncclDevResourceHandle is a uint32 resource index, and every
devComm is created from one identical reqs, so the index is valid on all of
them.

Ring variants. Five ring kernels shipped but only GinRingSmTableBroadcastKernel
was reachable under defaults; the others needed env knobs nothing in the tree
sets, and the comments above them already recorded the A/B results that ruled
them out. Keep the table kernel and the coprime-stride fallback that N=4 and
N=6 genuinely need (no edge-disjoint decomposition exists there), and drop the
SDMA-put ring, the single-orientation SM ring, and the point-to-point ring along
with NCCL_GIN_ANVIL_BCAST_RING_{SM,MULTI,P2P}. The measurements that justify
the choice are kept in the consolidated comment. This also removes the
unvalidated ginHandles[0] cast the P2P kernel did without the null/magic checks
BroadcastGetSdmaThreshold performs, and collapses the devtime gate's disagreement
with the dispatch: builtNRings > 0 is now exactly the condition under which the
table kernel runs, so the timed kernel can no longer report a number for a
kernel that did not execute.

bcastRingCtas(). Used strtol(e, nullptr, 0), so "64foo" parsed as 64 and "010"
as octal 8 -- exactly what BroadcastParseCtasEnv exists to reject for the
sibling variable. Parse base 10 with a full-string check, and cache the result:
it was re-reading getenv at every call site, so the value that sized
lsaBarrierCount and the value that picked the launched grid were independent
reads of a mutable environment, and a grid larger than the barrier pool is the
documented corrupt-or-hang condition.

LL cap. The kernel evaluated eligibility against gin_sdma::kBroadcastLLMaxBytes
(the 64 KiB compile ceiling) while the host sized the pool and grid from the
env-resolved cap (2 KiB by default). They agreed only because bcastLLEligible
also requires msgBytes/8 <= nSlots, which re-imposed the cap via the slot count.
Pass the resolved cap so both sides evaluate the same bound.

Tier selection now goes through the shared helpers: the hybrid kernel switches
on bcastKernelTier instead of re-deriving the ladder inline (the header claimed
the kernels called it; they did not), the dispatch resolves one bcastLargeTier
instead of testing two predicates in an order nothing pinned, and both ring
kernels slice with ringStripe/ringChunk.

Also: drop the now-unused testLaunchDeviceKernelThreshold launcher and the
gin_anvil_sdma.h include that only signalPeer needed; rename the surviving
launcher's trailing parameter to kernelArg, since the ring dispatches were
passing nChunks into a slot declared sdmaThresholdOverride; remove the dead
#else arm on BC_HAVE_LL, whose condition is implied by this block's; and record
why the 2.28 requirements arm provisions no LL resource.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
… testable

Argus review on ROCm#11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit d64f37c)
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
…sed kernels

Argus review on ROCm#11142. The load-bearing fix is the first one;
the rest remove the surface that made it hard to see.

Per-device ring tables. c_bcastNRings / c_bcastRingSucc / c_bcastRingPos /
c_bcastStream are __constant__, so they exist once per DEVICE, but the
"already built" gate (g_bcastBuiltN / g_bcastBuiltNRings) was process-wide. In a
-g N single-process multi-GPU run device 0 uploaded the decomposition and set
the flag, then devices 1..N-1 saw the flag set, skipped their upload, and ran
the ring against a zero-filled table: blockIdx.x % c_bcastNRings divides by
zero, and every rank reads position 0 and so believes it is the root. Key the
state on the current device instead (startColl calls cudaSetDevice immediately
before runColl on the device-API path). Distinct host threads own distinct
devices, so this also keeps -t N off a shared slot; the remaining shared
scratch -- buildBcastRingDecomp's backtracking object and the h_succ/h_pos
staging arrays -- becomes thread_local.

This was not caught because it needs -g N > 1: the GIN CI entry runs -g 1 under
mpirun, and the MI355X gates were MPI-based too.

g_bcastLLHandle / g_bcastLLReq become thread_local for the same -t N reason:
getDevCommRequirements runs once per thread and links g_bcastLLReq into that
thread's requirements list, and one shared node spliced into N lists corrupts
them. Note the handle itself did NOT need per-device treatment, contrary to the
review comment: ncclDevResourceHandle is a uint32 resource index, and every
devComm is created from one identical reqs, so the index is valid on all of
them.

Ring variants. Five ring kernels shipped but only GinRingSmTableBroadcastKernel
was reachable under defaults; the others needed env knobs nothing in the tree
sets, and the comments above them already recorded the A/B results that ruled
them out. Keep the table kernel and the coprime-stride fallback that N=4 and
N=6 genuinely need (no edge-disjoint decomposition exists there), and drop the
SDMA-put ring, the single-orientation SM ring, and the point-to-point ring along
with NCCL_GIN_ANVIL_BCAST_RING_{SM,MULTI,P2P}. The measurements that justify
the choice are kept in the consolidated comment. This also removes the
unvalidated ginHandles[0] cast the P2P kernel did without the null/magic checks
BroadcastGetSdmaThreshold performs, and collapses the devtime gate's disagreement
with the dispatch: builtNRings > 0 is now exactly the condition under which the
table kernel runs, so the timed kernel can no longer report a number for a
kernel that did not execute.

bcastRingCtas(). Used strtol(e, nullptr, 0), so "64foo" parsed as 64 and "010"
as octal 8 -- exactly what BroadcastParseCtasEnv exists to reject for the
sibling variable. Parse base 10 with a full-string check, and cache the result:
it was re-reading getenv at every call site, so the value that sized
lsaBarrierCount and the value that picked the launched grid were independent
reads of a mutable environment, and a grid larger than the barrier pool is the
documented corrupt-or-hang condition.

LL cap. The kernel evaluated eligibility against gin_sdma::kBroadcastLLMaxBytes
(the 64 KiB compile ceiling) while the host sized the pool and grid from the
env-resolved cap (2 KiB by default). They agreed only because bcastLLEligible
also requires msgBytes/8 <= nSlots, which re-imposed the cap via the slot count.
Pass the resolved cap so both sides evaluate the same bound.

Tier selection now goes through the shared helpers: the hybrid kernel switches
on bcastKernelTier instead of re-deriving the ladder inline (the header claimed
the kernels called it; they did not), the dispatch resolves one bcastLargeTier
instead of testing two predicates in an order nothing pinned, and both ring
kernels slice with ringStripe/ringChunk.

Also: drop the now-unused testLaunchDeviceKernelThreshold launcher and the
gin_anvil_sdma.h include that only signalPeer needed; rename the surviving
launcher's trailing parameter to kernelArg, since the ring dispatches were
passing nChunks into a slot declared sdmaThresholdOverride; remove the dead
#else arm on BC_HAVE_LL, whose condition is implied by this block's; and record
why the 2.28 requirements arm provisions no LL resource.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 951b01b)
@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Review round: all 29 threads answered

Thanks @amd-argus — this was a genuinely useful review. I have replied to every thread individually. Summary of where things stand at head 951b01bddef:

15 findings are fixed, across two commits:

  • d64f37c09d0 — policy header and tests: ringStripe() / ringChunk() moved into the header and tested, bcastLargeTier() enum pinning ring-before-SAG precedence, parseSize overflow guards, dead alignChunkCount dropped with its tests, relational CTA-ladder assertions, external ddai-artifacts doc references inlined. Suite is now 48 cases, up from 38.
  • 951b01bddef — broadcast.cu: per-device ring state keyed by cudaGetDevice(), thread_local for the five pieces of shared host state, bcastRingCtas base-10 parse plus caching, env-resolved llMaxBytes passed to the kernel, tier dispatch switched to bcastLargeTier(), kernel switched to bcastKernelTier(), and the ring kernel set trimmed to the table kernel plus the coprime-stride fallback (−368/+194). Trimming dissolved two further findings, since the unvalidated rsCtx cast lived in the removed P2P kernel and the device-timing gate can no longer disagree with a two-kernel dispatch.

One finding is a confirmed blocker and is not yet fixed. The signalValue snapshot in GinHybridBroadcastKernel is a real race that can hang the default flat GIN path: only block 0 issues the puts, the entry barrier is per block index, so a non-root's block n can read the signal after the root's put has landed and then wait for a value nobody posts. Fixing by confining both the read and the wait to block 0, and auditing the same shape in GinScatterAllgatherBroadcastKernel. Details in that thread.

13 remain open and are tracked in their threads: the multi-node LSA-team gate, RCCL_TESTS_GIN_SDMA_BCAST never being exported from CI, the pytest #wrong / plugin-line assertions, _DATA_FAIL_RE matching the column header, ENABLE_DEVICE_API confirmation for the GIN CI image, the RCCL_SOURCE_DIR CMake guard question, moving BcastRingDecomp into the tested header, the sdmaThresholdOverride rename, baseCount deduplication, the CMake else() diagnostic, documenting ..._RING_STREAM as table-kernel-only, the commit-splitting point, and one brace suggestion.

The three red checks are all external

None of them are caused by this PR, and none can be cleared from this branch. I have filed both:

GIN / Single node gin testing is queued rather than failing. Worth noting it would not currently prove much either way, since RCCL_TESTS_GIN_SDMA_BCAST is unset and the three broadcast pytest cases skip — which is exactly the gap raised in review.

Hardware validation at this head

8× MI355X, rebuilt from this branch's sources rather than a prebuilt image: full root sweep (roots 0–7) over 128 B – 128 MiB with zero #wrong and zero out-of-bounds, 268 GB/s at 128 MiB. Ring tier separately confirmed at 32 MiB with NET/gin-anvil-sdma bound on all 8 ranks. Host policy unit tests 48/48.

dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
…to block 0

Both GIN Broadcast kernels sampled their completion signal on every CTA before
the entry barrier, while only block 0 ever issues the puts: the put loops walk
ranks (r < nRanks / r < N) with a grid-wide tid, so only tid < nRanks executes
a body, and nRanks is far below blockDim.x.

The entry barrier does not close that gap. ncclBarrierSession is constructed
with blockIdx.x, so it orders block n against block n of every peer and nothing
else. Block 0 of the root and block n of a non-root share no barrier edge, so
the root's put can land before that CTA has run readSignal. It then samples
base+1 and waits for base+2, which nobody posts, and the kernel hangs.

Moving the read after the barrier does not help, for the same reason. Confining
the read, the puts and the wait to block 0 does: block 0's read precedes the
block-0-to-block-0 barrier, which precedes the root's put. Kernel completion
still covers the wait, because the kernel does not retire until block 0 does.

GinScatterAllgatherBroadcastKernel had the identical shape at both of its
signals, so the same treatment is applied to the scatter wait and the gather
wait, along with the trailing flush -- block 0 issued every put, so it owns the
only dirty queues.

The put loops now stride by blockDim.x instead of grid-wide, so block 0 still
covers every rank if nRanks ever exceeds one block's width. Other CTAs continue
to run the grid-strided local copy and must still take the entry barrier, which
is a cross-rank barrier keyed by blockIdx.x.

Reported-by: amd-argus in ROCm#11142 review
Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 2, 2026
…to block 0

Both GIN Broadcast kernels sampled their completion signal on every CTA before
the entry barrier, while only block 0 ever issues the puts: the put loops walk
ranks (r < nRanks / r < N) with a grid-wide tid, so only tid < nRanks executes
a body, and nRanks is far below blockDim.x.

The entry barrier does not close that gap. ncclBarrierSession is constructed
with blockIdx.x, so it orders block n against block n of every peer and nothing
else. Block 0 of the root and block n of a non-root share no barrier edge, so
the root's put can land before that CTA has run readSignal. It then samples
base+1 and waits for base+2, which nobody posts, and the kernel hangs.

Moving the read after the barrier does not help, for the same reason. Confining
the read, the puts and the wait to block 0 does: block 0's read precedes the
block-0-to-block-0 barrier, which precedes the root's put. Kernel completion
still covers the wait, because the kernel does not retire until block 0 does.

GinScatterAllgatherBroadcastKernel had the identical shape at both of its
signals, so the same treatment is applied to the scatter wait and the gather
wait, along with the trailing flush -- block 0 issued every put, so it owns the
only dirty queues.

The put loops now stride by blockDim.x instead of grid-wide, so block 0 still
covers every rank if nRanks ever exceeds one block's width. Other CTAs continue
to run the grid-strided local copy and must still take the entry barrier, which
is a cross-rank barrier keyed by blockIdx.x.

Reported-by: amd-argus in ROCm#11142 review
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 89dd326)
@dlamd1dai

dlamd1dai commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Review round 2: test-coverage findings addressed (2df5f510db5)

@amd-argus this branch has moved three commits since the review at 21:51Z — requesting a re-review of the current head.

The review's strongest point was that the three GIN Broadcast pytest cases asserted only rc == 0 while presenting themselves as data checks. That is now measured rather than argued. On 8x MI355X, capturing NCCL debug the same way the test does:

run exit gin-anvil-sdma markers result rows
-D 3, GIN Anvil-SDMA 0 16 1
-D 0, GIN disabled 0 0 1

A host-path run with GIN switched off exits 0 and prints a full results table. The old assertion could not distinguish it from the path under test.

What changed

  • Every measured row must report #wrong == 0, and must report it as a number rather than N/A, so a run with data checking disabled is rejected instead of passing silently. At least one measured row must exist, and the out-of-bounds count must be 0.
  • The run must show the GIN Anvil-SDMA backend actually binding.
  • _DATA_FAIL_RE matched the literal Wrong, which appears in the #wrong column header of every run reaching the results table. It fired on healthy runs, making the connectivity-gate retry beneath it unreachable. It is replaced by _data_failed(), anchored on parsed column values. N/A counts as unchecked rather than failed, so it surfaces as an assertion instead of consuming the retry budget.
  • NCCL debug now goes to per-rank files instead of stdout. On stdout the ranks interleave and an INFO line splices into the middle of a results row (... 60.86 0smci355-...NCCL INFO ...), corrupting the table being parsed. The row regex also accepts the scientific notation getFloatStr falls back to, and no longer anchors the line end, where an algo/proto/nchannels group can follow.
  • NCCL_GIN_TYPE was baked in at 6. The ANVIL_SDMA enum is 6 on develop and a wrong value silently loads no plugin, so it is now RCCL_TESTS_BCAST_GIN_TYPE (default 6), matching the existing RCCL_TESTS_A2A_GIN_TYPE.
  • Nothing exported RCCL_TESTS_GIN_SDMA_BCAST, so the cases skipped wherever they ran. It is exported from run-device-api-ci.sh, which is the job that has both GPUs and a pytest runner. lib/gin-tests.json only understands the rocshmem, rccl-tests and fixtures kinds and cannot drive pytest at all, so the new GIN entry there could never have enabled them.

Validation — all 8 GIN cases pass in 44.6s on 8x MI355X (2.30.7 image), covering 256 MiB and 2 GiB across int32/int64/uint8 plus the 2 GiB and 4 GiB guards. Parsing is separately covered by 11 offline cases including the spliced-row and scientific-notation shapes.

Still open: the multi-node LSA-team gate, moving BcastRingDecomp into the policy header with tests, and the smaller cleanups (baseCount dedupe, sdmaThresholdOverride rename, CMake else() diagnostic, stream-knob documentation).

Not caused by this branch: the two build failures and the therock-pr-bot timeout are tracked as #11155 and #11156; both reproduce on develop independently of this PR.

Comment thread projects/rccl-tests/src/broadcast.cu Resolved

@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.

Automated Review Guard for Upfront Scrutiny

Findings

1 finding(s) that fall outside the diff and could not be anchored to a line
  • projects/rccl-tests/src/broadcast.cu:3 (Low) broadcast.cu copyright still reads 2019-2022 while every GIN sibling reads 2019-2026

Bottom line: The round-1 signal race is fixed and verified. What still blocks is coverage wiring: nothing exports RCCL_TESTS_GIN_SDMA_BCAST and no CI job builds rccl-UnitTestsGinSdmaBroadcastPolicy, so both new suites run in no job.

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

Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved

@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.

Automated Review Guard for Upfront Scrutiny

Bottom line: The ring's per-step barrier at broadcast.cu:908 asks for release only, so a forwarding rank gets no acquire before reading peer-written data; that should be settled before merge. The new CI pytest entry also selects zero tests.

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

Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
Comment thread projects/rccl-tests/src/broadcast.cu Resolved
@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Review round 3 addressed (ba186f2fa44)

Thanks again @amd-argus and @wenkaidu — replies are on the individual threads below. Summary of what landed in this commit vs what is intentionally deferred.

Fixed in ba186f2fa44

Finding Fix
-k gin_sdma selects nothing (GIN CI exit 5) -k GinSdma — pytest case-folds both sides, so gin_sdma ≠ GinSdma
bcastRingCtas int narrowing Clamp in long before (int) cast
parseSize leading whitespace Trim spaces/tabs before sign check; test for " -4K"
cudaMemcpyToSymbol vs launch stream cudaMemcpyToSymbolAsync(..., stream) for all four ring tables
bcastPoolCtas untested BcastPoolCtas.HybridPoolAndRingPeak
bcastRingChunks only clamps Interior case: 64 MiB / 8 CTAs → 128 chunks
bcastSagCtas env pin untested BcastPolicyCtas.SagEnvPinHonoredWithinPool
pytest import-time int() _env_int() falls back on bad values
4 GiB docstring Says ring path (SAG cannot run at 4 GiB with defaults)
Offline parsing guards Three host-only pytest cases for header / #wrong / sci notation
Brace style (buildBcastRingDecomp) Applied

Already fixed in earlier commits (replied on threads)

  • RCCL_TESTS_GIN_SDMA_BCAST / GIN-job pytest wiring (55e3f180b5e)
  • Policy UT runs in Host unit tests (CPU-only) (rccl-UnitTestsGinSdmaBroadcastPolicy, 51 cases) — not the GIN sbatch job, which only builds rccl-UnitTestsFixtures
  • ENABLE_DEVICE_API=ON in gin.sbatch — broadcast_perf -D 3 reaches case 3
  • Multi-node ring gate (32561e5cb43, @wenkaidu)
  • Signal race, per-device ring state, pytest assertions (89dd326a, 2df5f510db5)

Deferred (follow-up, not blocking this PR)

  • Duplicate ring bodies (ginRingSmTable vs coprime fallback) — refactor to one shared body; tracked
  • Shared env parsers (bcastRingCtas vs BroadcastParseCtasEnv, duplicated bcastRingMin lambdas) — consolidate with AllGather in AICOMRCCL-2217..2221 stack
  • BcastRingDecomp → policy header — host-testable, but large move; separate commit
  • SAG functional pytest case — ring tier is the default large path; SUT sweeps cover SAG; can add RING_MIN_BYTES=0 case later
  • Ring barrier memory order (acq_rel) — needs validation against NCCL barrier semantics on MI355X before changing
  • --device_timing + -g N>1 ring state — devtime path is opt-in; will key ring upload off launch device in follow-up
  • Dead sdmaThresholdOverride unset path — comment cleanup when threshold helper is unified
  • Launcher template consolidation — style/readability only

Requesting re-review at head ba186f2fa44.

dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 4, 2026
…omment

Two Argus review follow-ups on ROCm#11142. No functional change.

test_Broadcast.py: _read_debug_logs looped on `path`, shadowing the
module-level `path` bound at line 69. 2df5f51 applied the
errors="replace" half of the suggestion but left the loop variable, so the
finding was still live at head despite being marked resolved.

broadcast.cu: the GinHybridBroadcastKernel comment described a fallback to
the backend's rsCtx->sdmaThreshold when sdmaThresholdOverride is
TEST_SDMA_THRESHOLD_UNSET. That cannot happen from this caller: bcastThr
comes from testResolveSdmaThreshold (:1041), and resolveThreshold returns
kBroadcastSdmaThresholdDefault rather than the sentinel when both env vars
are unset, so BroadcastGetSdmaThreshold is unreachable. The sentinel arm is
kept for parity with AllGather; the comment now says that instead of
promising a fallback that never runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 4, 2026
_assert_bcast_ok checked that a -D 3 run worked, never that it worked the
way the test is named. Every tier produces a correct broadcast, so #wrong
stays 0 when a tier stops being reached and its messages fall through to
the next one: deleting the ScatterAG arm left
test_BroadcastGinSdmaScatterAllgather green with
GinScatterAllgatherBroadcastKernel covered by nothing. The two hang guards
and the flat segmented tests had the same hole.

BroadcastRunColl now names the kernel it launched as "#[bcast-tier] <name>"
and the four GIN-SDMA pytest regressions take an expected tier, asserting
exactly one ran so a run that switched tiers between roots cannot pass
either.

The hybrid arm asks gin_sdma::bcastKernelTier -- the policy the kernel
itself branches on -- rather than reporting "hybrid", which would leave
flat/LSA/LL indistinguishable and the flat tests as uncovered as before.
Reported once per distinct tier, since RunColl is called for warmup, every
iteration and every root; the dedupe is an atomic fetch_or so -t N host
threads cannot race it into printing twice. Opt-in via
RCCL_TESTS_BCAST_TIER_LOG, with the env read cached, so default stdout
stays byte-identical for the perf harness and the timed path is untouched.

The four new offline guards carry GinSdma in their names deliberately: the
rccl-gin-bcast-pytest job runs -k GinSdma, and run-gin-ci.sh expands its
args unquoted, so a filter containing a space is not available. One of them
reproduces the review's scenario directly -- the output a fall-through run
would print, which every other assertion accepts.

Addresses pvallem's review on ROCm#11142.

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

dlamd1dai commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Review round 4 addressed (4e7f2aaf41b)

Thanks @pvallem — this is a great catch, and a genuinely uncomfortable one, because every symptom of it looks like a pass.

You're right on all three counts. _assert_bcast_ok was checking that the run worked, never that it worked the way the test is named. All five -D 3 tiers produce a correct broadcast, so #wrong stays 0 when a tier stops being reached and its messages fall through to the next one. Deleting the ScatterAG arm really would have left test_BroadcastGinSdmaScatterAllgather green — it was proving "broadcast is correct at 256 MiB", which the flat kernel satisfies just as well, and GinScatterAllgatherBroadcastKernel was covered by nothing. Same argument applied to the two hang guards and to the "flat" segmented tests, so this was really four tests, not one.

Fixed the way you suggested: the dispatch names the tier it launched, and the tests take an expected one.

Host side — broadcast.cu

bcastReportTier() emits one #[bcast-tier] <name> line per distinct tier:

tier reported name
pipelined ring, edge-disjoint decomposition ring-table
pipelined ring, no decomposition (N=4, N=6) ring-multi
scatter + allgather scatter-allgather
flat GIN root fan-out flat-gin
direct xGMI LSA stores lsa-direct
LatencyLayover slots ll

Three details worth flagging, since they are all places I could have made it weaker:

  • It reports the launched kernel, not just largeTier. You pointed at largeTier at :1087, which covers ring vs SAG. But the fall-through arm is GinHybridBroadcastKernel, which picks LL / LSA / flat itself, so reporting "hybrid" there would have left the three indistinguishable and test_BroadcastGinSdmaLargeSegmented exactly as uncovered as before. So the hybrid arm asks gin_sdma::bcastKernelTier() — the same policy function the kernel branches on — and the ring arm reports table vs multi after the decomposition is built. Every name above maps to one kernel.
  • Once per distinct tier, not once per launch. RunColl is called for warmup, every iteration and every root; one line per launch would flood the results table it shares stdout with. The dedupe is an atomic fetch_or so -t N host threads can't race it into printing twice.
  • Opt-in via RCCL_TESTS_BCAST_TIER_LOG, so default stdout stays byte-identical for the perf harness, and the env read is cached because RunColl is on the timed path. Happy to make it unconditional if you'd rather every run be self-describing — see the question at the bottom.

The leading # keeps it clear of the results table (_ROW_RE is anchored on a leading size field), matching the existing #[bcast-devtime] side channel.

Test side — test_Broadcast.py

_assert_bcast_ok(..., expect_tier=) now asserts a tier line exists and that exactly one tier ran and it was an allowed one:

  • test_BroadcastGinSdmaLargeSegmented → flat-gin
  • test_BroadcastGinSdmaScatterAllgather → scatter-allgather
  • test_BroadcastGinSdma2GiBHangGuard / 4GiB → ring-table or ring-multi

Exactly-one rather than "contains", so a run that took the expected tier for some roots and a different one for the rest doesn't slip through. The ring accepts either name because the decomposition depends on rank count. A missing tier line is a hard failure rather than a skip, so an older binary can't make these pass vacuously.

Four offline guards come with it (no GPU needed), one of which is literally your scenario: it builds the output a fall-through run would produce — correct data, #wrong 0, everything else passing — and asserts the SAG expectation rejects it.

One thing I found on the way

The CI job rccl-gin-bcast-pytest runs -k GinSdma, which never collected the existing offline parsing guards (test_bcast_row_regex_*, test_bcast_data_failed_*) — they run nowhere today. I'd have widened the filter to -k "GinSdma or bcast_", but run-gin-ci.sh:178 expands ${args} unquoted, so the quotes don't survive and pytest gets -k '"GinSdma' plus two stray words. So the new guards are named into the existing filter instead (test_BroadcastGinSdmaTierGuard*) and need no CI change. Glad to rename the four pre-existing ones the same way, or fix the runner's arg handling properly, if you have a preference — didn't want to fold either into this PR uninvited.

…inSdma

The four offline #wrong / scientific-notation cases used snake_case names, so GIN CI's unquoted pytest filter never ran them.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread projects/rccl/tools/ci/run-gin-ci.sh Resolved
…EOUT

GNU timeout killing pytest does not reap the mpirun session; pytest killpg only runs if the inner communicate timeout fires first.

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

Copy link
Copy Markdown
Contributor Author

CI / merge notes at 5745db9144a

Review is complete (thanks @wenkaidu @pvallem). All threads are resolved. The two nits from this afternoon are on this head: parser guards renamed into -k GinSdma (f752ebfe22b) and the inner Broadcast pytest budget capped under GIN_PYTEST_TIMEOUT (5745db9144a).

Merge is still blocked on checks, not on review. Two of the reds are not this PR:

mci/rocm-libraries/precheckin(rccl) — unrelated AlltoAll on gfx950

Build 16 matches build 15: compile/package and gfx942 / gfx1100 tests pass; only precheckin: Test ubuntu24-therock && 8gfx950 fails.

The failing case is host rccl-UnitTests --gtest_filter=AlltoAll.OutOfPlace (MP 2 ranks, ncclFloat32, 2 097 152 elements). Rank 0 expected 0.500000, got 0.000000 at index 1 048 576 (start of the other rank’s half). Immediate rerun failed the same way; --stop-on-rerun-failure stopped the suite. That is in-library ncclAlltoAll, not -D 3 Broadcast and not rccl-UnitTestsGinSdmaBroadcastPolicy. This branch does not change AlltoAll sources.

I am not pushing a no-op to retrigger it (that would re-queue GIN). Next step is a Jenkins rebuild of the same SHA: rebuild. If it stays red, I will merge past it as a pre-existing gfx950 AlltoAll miss.

therock-pr-bot — missing pre-commit check on develop

The policy comment on this PR is already ✅. The GitHub check still fails because it waits for a check named pre-commit, and .github/workflows/pre-formatting.yml only runs on PRs targeting testbranch. Same on unrelated PRs; tracked as #11156. Not fixable from this branch.

Checks that do cover this change

  • Host unit tests, device-API, rccl-ext-plugins, TheRock package builds (gfx94X and gfx950): pass
  • GIN / Single node gin testing: queued (serial runner)
  • TheRock gfx94X Test single-node / rocprofv3: queued after the successful builds
  • precheckin(rccl-tests): still running; gfx950 and gfx1100 tests already passed there

Plan: Jenkins-rebuild Math CI without a git push, wait for GIN + TheRock tests, then merge with an override for the bot (and for Math CI AlltoAll if the rebuild repeats build 15/16).

@dlamd1dai
dlamd1dai merged commit 1508be5 into ROCm:develop Sep 9, 2026
52 of 53 checks passed
dlamd1dai added a commit that referenced this pull request Sep 9, 2026
… testable

Argus review on #11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit that referenced this pull request Sep 9, 2026
…sed kernels

Argus review on #11142. The load-bearing fix is the first one;
the rest remove the surface that made it hard to see.

Per-device ring tables. c_bcastNRings / c_bcastRingSucc / c_bcastRingPos /
c_bcastStream are __constant__, so they exist once per DEVICE, but the
"already built" gate (g_bcastBuiltN / g_bcastBuiltNRings) was process-wide. In a
-g N single-process multi-GPU run device 0 uploaded the decomposition and set
the flag, then devices 1..N-1 saw the flag set, skipped their upload, and ran
the ring against a zero-filled table: blockIdx.x % c_bcastNRings divides by
zero, and every rank reads position 0 and so believes it is the root. Key the
state on the current device instead (startColl calls cudaSetDevice immediately
before runColl on the device-API path). Distinct host threads own distinct
devices, so this also keeps -t N off a shared slot; the remaining shared
scratch -- buildBcastRingDecomp's backtracking object and the h_succ/h_pos
staging arrays -- becomes thread_local.

This was not caught because it needs -g N > 1: the GIN CI entry runs -g 1 under
mpirun, and the MI355X gates were MPI-based too.

g_bcastLLHandle / g_bcastLLReq become thread_local for the same -t N reason:
getDevCommRequirements runs once per thread and links g_bcastLLReq into that
thread's requirements list, and one shared node spliced into N lists corrupts
them. Note the handle itself did NOT need per-device treatment, contrary to the
review comment: ncclDevResourceHandle is a uint32 resource index, and every
devComm is created from one identical reqs, so the index is valid on all of
them.

Ring variants. Five ring kernels shipped but only GinRingSmTableBroadcastKernel
was reachable under defaults; the others needed env knobs nothing in the tree
sets, and the comments above them already recorded the A/B results that ruled
them out. Keep the table kernel and the coprime-stride fallback that N=4 and
N=6 genuinely need (no edge-disjoint decomposition exists there), and drop the
SDMA-put ring, the single-orientation SM ring, and the point-to-point ring along
with NCCL_GIN_ANVIL_BCAST_RING_{SM,MULTI,P2P}. The measurements that justify
the choice are kept in the consolidated comment. This also removes the
unvalidated ginHandles[0] cast the P2P kernel did without the null/magic checks
BroadcastGetSdmaThreshold performs, and collapses the devtime gate's disagreement
with the dispatch: builtNRings > 0 is now exactly the condition under which the
table kernel runs, so the timed kernel can no longer report a number for a
kernel that did not execute.

bcastRingCtas(). Used strtol(e, nullptr, 0), so "64foo" parsed as 64 and "010"
as octal 8 -- exactly what BroadcastParseCtasEnv exists to reject for the
sibling variable. Parse base 10 with a full-string check, and cache the result:
it was re-reading getenv at every call site, so the value that sized
lsaBarrierCount and the value that picked the launched grid were independent
reads of a mutable environment, and a grid larger than the barrier pool is the
documented corrupt-or-hang condition.

LL cap. The kernel evaluated eligibility against gin_sdma::kBroadcastLLMaxBytes
(the 64 KiB compile ceiling) while the host sized the pool and grid from the
env-resolved cap (2 KiB by default). They agreed only because bcastLLEligible
also requires msgBytes/8 <= nSlots, which re-imposed the cap via the slot count.
Pass the resolved cap so both sides evaluate the same bound.

Tier selection now goes through the shared helpers: the hybrid kernel switches
on bcastKernelTier instead of re-deriving the ladder inline (the header claimed
the kernels called it; they did not), the dispatch resolves one bcastLargeTier
instead of testing two predicates in an order nothing pinned, and both ring
kernels slice with ringStripe/ringChunk.

Also: drop the now-unused testLaunchDeviceKernelThreshold launcher and the
gin_anvil_sdma.h include that only signalPeer needed; rename the surviving
launcher's trailing parameter to kernelArg, since the ring dispatches were
passing nChunks into a slot declared sdmaThresholdOverride; remove the dead
#else arm on BC_HAVE_LL, whose condition is implied by this block's; and record
why the 2.28 requirements arm provisions no LL resource.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit that referenced this pull request Sep 9, 2026
…to block 0

Both GIN Broadcast kernels sampled their completion signal on every CTA before
the entry barrier, while only block 0 ever issues the puts: the put loops walk
ranks (r < nRanks / r < N) with a grid-wide tid, so only tid < nRanks executes
a body, and nRanks is far below blockDim.x.

The entry barrier does not close that gap. ncclBarrierSession is constructed
with blockIdx.x, so it orders block n against block n of every peer and nothing
else. Block 0 of the root and block n of a non-root share no barrier edge, so
the root's put can land before that CTA has run readSignal. It then samples
base+1 and waits for base+2, which nobody posts, and the kernel hangs.

Moving the read after the barrier does not help, for the same reason. Confining
the read, the puts and the wait to block 0 does: block 0's read precedes the
block-0-to-block-0 barrier, which precedes the root's put. Kernel completion
still covers the wait, because the kernel does not retire until block 0 does.

GinScatterAllgatherBroadcastKernel had the identical shape at both of its
signals, so the same treatment is applied to the scatter wait and the gather
wait, along with the trailing flush -- block 0 issued every put, so it owns the
only dirty queues.

The put loops now stride by blockDim.x instead of grid-wide, so block 0 still
covers every rank if nRanks ever exceeds one block's width. Other CTAs continue
to run the grid-strided local copy and must still take the entry barrier, which
is a cross-rank barrier keyed by blockIdx.x.

Reported-by: amd-argus in #11142 review
Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit that referenced this pull request Sep 9, 2026
…omment

Two Argus review follow-ups on #11142. No functional change.

test_Broadcast.py: _read_debug_logs looped on `path`, shadowing the
module-level `path` bound at line 69. 2df5f51 applied the
errors="replace" half of the suggestion but left the loop variable, so the
finding was still live at head despite being marked resolved.

broadcast.cu: the GinHybridBroadcastKernel comment described a fallback to
the backend's rsCtx->sdmaThreshold when sdmaThresholdOverride is
TEST_SDMA_THRESHOLD_UNSET. That cannot happen from this caller: bcastThr
comes from testResolveSdmaThreshold (:1041), and resolveThreshold returns
kBroadcastSdmaThresholdDefault rather than the sentinel when both env vars
are unset, so BroadcastGetSdmaThreshold is unreachable. The sentinel arm is
kept for parity with AllGather; the comment now says that instead of
promising a fallback that never runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit that referenced this pull request Sep 9, 2026
_assert_bcast_ok checked that a -D 3 run worked, never that it worked the
way the test is named. Every tier produces a correct broadcast, so #wrong
stays 0 when a tier stops being reached and its messages fall through to
the next one: deleting the ScatterAG arm left
test_BroadcastGinSdmaScatterAllgather green with
GinScatterAllgatherBroadcastKernel covered by nothing. The two hang guards
and the flat segmented tests had the same hole.

BroadcastRunColl now names the kernel it launched as "#[bcast-tier] <name>"
and the four GIN-SDMA pytest regressions take an expected tier, asserting
exactly one ran so a run that switched tiers between roots cannot pass
either.

The hybrid arm asks gin_sdma::bcastKernelTier -- the policy the kernel
itself branches on -- rather than reporting "hybrid", which would leave
flat/LSA/LL indistinguishable and the flat tests as uncovered as before.
Reported once per distinct tier, since RunColl is called for warmup, every
iteration and every root; the dedupe is an atomic fetch_or so -t N host
threads cannot race it into printing twice. Opt-in via
RCCL_TESTS_BCAST_TIER_LOG, with the env read cached, so default stdout
stays byte-identical for the perf harness and the timed path is untouched.

The four new offline guards carry GinSdma in their names deliberately: the
rccl-gin-bcast-pytest job runs -k GinSdma, and run-gin-ci.sh expands its
args unquoted, so a filter containing a space is not available. One of them
reproduces the review's scenario directly -- the output a fall-through run
would print, which every other assertion accepts.

Addresses pvallem's review on #11142.

Co-authored-by: Cursor <cursoragent@cursor.com>
alex-breslow-amd added a commit that referenced this pull request Sep 10, 2026
This reverts the 26 commits landed by PR #11142
(1d5d2fc..1508be5), restoring the 11 affected files to their
state at e9702b4.

Reverted commits:
  1d5d2fc rccl-tests/gin-sdma: port Broadcast -D 3 from stage3c-sdma-bcast
  16da5c0 rccl/gin-sdma: add Broadcast policy unit test and GIN CI entry
  a847f26 rccl-tests/gin-sdma: align broadcast_perf with NCCL 2.30.7 testEngine API.
  47d85d5 rccl-tests/gin-sdma: add 4 GiB Broadcast GIN hybrid hang guard pytest.
  94f6e7a style(rccl-tests): apply black formatting to test_Broadcast.py.
  d6f62cb fix(rccl-tests/gin-sdma): reject negative Broadcast threshold env values
  f002876 fix(rccl-tests/gin-sdma): align Broadcast with merged AllGather conventions
  db9ad6d style(rccl-tests/gin-sdma): match merged AllGather testEngine init form
  d13edf8 fix(rccl-tests/gin-sdma): gate Broadcast RunColl case 3 on ENABLE_DEVICE_API
  71c05f9 fix(rccl-tests/gin-sdma): select Anvil-SDMA with NCCL_GIN_TYPE=6 on develop
  25df448 style(rccl-tests/gin-sdma): escape the regex backslash in test_Broadcast.py
  e64f47c rccl-tests/gin-sdma: make the shipped Broadcast tiling and tier order testable
  185b45f rccl-tests/gin-sdma: fix Broadcast per-device ring state and trim unused kernels
  dc19eaf fix(rccl-tests/gin-sdma): confine Broadcast GIN signal read and wait to block 0
  9ea1e59 test(rccl-tests/gin-sdma): make Broadcast GIN pytest cases assert and run in CI
  72d015a fix(rccl-tests/gin-sdma): gate the Broadcast ring tier on the LSA team
  7bba6ea ci(rccl): drop the Broadcast GIN pytest smoke from the device-api job
  6f1f937 ci(rccl/gin): run Broadcast GIN pytest from the GIN CI job
  d8d6678 fix(rccl/gin-sdma): address Argus round-3 review findings
  9eaec96 fix(rccl/gin-sdma): SAG pytest, policy UT in GIN CI, ring acq_rel barriers
  eab9239 fix(rccl/gin-sdma): HIP MemcpyToSymbolAsync needs explicit memcpy kind
  c81e6d8 fix(rccl/gin-sdma): template bcastMemcpyConstantAsync for scalar symbols
  90f2b91 fix(rccl/gin-sdma): unshadow debug log path, correct dead threshold comment
  0e06d47 rccl-tests/gin-sdma: assert which Broadcast tier ran
  b3affe7 test(rccl-tests/gin-sdma): collect Broadcast parser guards under -k GinSdma
  1508be5 ci(rccl/gin): keep Broadcast pytest inner budget under GIN_PYTEST_TIMEOUT

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

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 16, 2026
… testable

Argus review on ROCm#11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 16, 2026
…sed kernels

Argus review on ROCm#11142. The load-bearing fix is the first one;
the rest remove the surface that made it hard to see.

Per-device ring tables. c_bcastNRings / c_bcastRingSucc / c_bcastRingPos /
c_bcastStream are __constant__, so they exist once per DEVICE, but the
"already built" gate (g_bcastBuiltN / g_bcastBuiltNRings) was process-wide. In a
-g N single-process multi-GPU run device 0 uploaded the decomposition and set
the flag, then devices 1..N-1 saw the flag set, skipped their upload, and ran
the ring against a zero-filled table: blockIdx.x % c_bcastNRings divides by
zero, and every rank reads position 0 and so believes it is the root. Key the
state on the current device instead (startColl calls cudaSetDevice immediately
before runColl on the device-API path). Distinct host threads own distinct
devices, so this also keeps -t N off a shared slot; the remaining shared
scratch -- buildBcastRingDecomp's backtracking object and the h_succ/h_pos
staging arrays -- becomes thread_local.

This was not caught because it needs -g N > 1: the GIN CI entry runs -g 1 under
mpirun, and the MI355X gates were MPI-based too.

g_bcastLLHandle / g_bcastLLReq become thread_local for the same -t N reason:
getDevCommRequirements runs once per thread and links g_bcastLLReq into that
thread's requirements list, and one shared node spliced into N lists corrupts
them. Note the handle itself did NOT need per-device treatment, contrary to the
review comment: ncclDevResourceHandle is a uint32 resource index, and every
devComm is created from one identical reqs, so the index is valid on all of
them.

Ring variants. Five ring kernels shipped but only GinRingSmTableBroadcastKernel
was reachable under defaults; the others needed env knobs nothing in the tree
sets, and the comments above them already recorded the A/B results that ruled
them out. Keep the table kernel and the coprime-stride fallback that N=4 and
N=6 genuinely need (no edge-disjoint decomposition exists there), and drop the
SDMA-put ring, the single-orientation SM ring, and the point-to-point ring along
with NCCL_GIN_ANVIL_BCAST_RING_{SM,MULTI,P2P}. The measurements that justify
the choice are kept in the consolidated comment. This also removes the
unvalidated ginHandles[0] cast the P2P kernel did without the null/magic checks
BroadcastGetSdmaThreshold performs, and collapses the devtime gate's disagreement
with the dispatch: builtNRings > 0 is now exactly the condition under which the
table kernel runs, so the timed kernel can no longer report a number for a
kernel that did not execute.

bcastRingCtas(). Used strtol(e, nullptr, 0), so "64foo" parsed as 64 and "010"
as octal 8 -- exactly what BroadcastParseCtasEnv exists to reject for the
sibling variable. Parse base 10 with a full-string check, and cache the result:
it was re-reading getenv at every call site, so the value that sized
lsaBarrierCount and the value that picked the launched grid were independent
reads of a mutable environment, and a grid larger than the barrier pool is the
documented corrupt-or-hang condition.

LL cap. The kernel evaluated eligibility against gin_sdma::kBroadcastLLMaxBytes
(the 64 KiB compile ceiling) while the host sized the pool and grid from the
env-resolved cap (2 KiB by default). They agreed only because bcastLLEligible
also requires msgBytes/8 <= nSlots, which re-imposed the cap via the slot count.
Pass the resolved cap so both sides evaluate the same bound.

Tier selection now goes through the shared helpers: the hybrid kernel switches
on bcastKernelTier instead of re-deriving the ladder inline (the header claimed
the kernels called it; they did not), the dispatch resolves one bcastLargeTier
instead of testing two predicates in an order nothing pinned, and both ring
kernels slice with ringStripe/ringChunk.

Also: drop the now-unused testLaunchDeviceKernelThreshold launcher and the
gin_anvil_sdma.h include that only signalPeer needed; rename the surviving
launcher's trailing parameter to kernelArg, since the ring dispatches were
passing nChunks into a slot declared sdmaThresholdOverride; remove the dead
#else arm on BC_HAVE_LL, whose condition is implied by this block's; and record
why the 2.28 requirements arm provisions no LL resource.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 16, 2026
…to block 0

Both GIN Broadcast kernels sampled their completion signal on every CTA before
the entry barrier, while only block 0 ever issues the puts: the put loops walk
ranks (r < nRanks / r < N) with a grid-wide tid, so only tid < nRanks executes
a body, and nRanks is far below blockDim.x.

The entry barrier does not close that gap. ncclBarrierSession is constructed
with blockIdx.x, so it orders block n against block n of every peer and nothing
else. Block 0 of the root and block n of a non-root share no barrier edge, so
the root's put can land before that CTA has run readSignal. It then samples
base+1 and waits for base+2, which nobody posts, and the kernel hangs.

Moving the read after the barrier does not help, for the same reason. Confining
the read, the puts and the wait to block 0 does: block 0's read precedes the
block-0-to-block-0 barrier, which precedes the root's put. Kernel completion
still covers the wait, because the kernel does not retire until block 0 does.

GinScatterAllgatherBroadcastKernel had the identical shape at both of its
signals, so the same treatment is applied to the scatter wait and the gather
wait, along with the trailing flush -- block 0 issued every put, so it owns the
only dirty queues.

The put loops now stride by blockDim.x instead of grid-wide, so block 0 still
covers every rank if nRanks ever exceeds one block's width. Other CTAs continue
to run the grid-strided local copy and must still take the entry barrier, which
is a cross-rank barrier keyed by blockIdx.x.

Reported-by: amd-argus in ROCm#11142 review
Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 16, 2026
…omment

Two Argus review follow-ups on ROCm#11142. No functional change.

test_Broadcast.py: _read_debug_logs looped on `path`, shadowing the
module-level `path` bound at line 69. 2df5f51 applied the
errors="replace" half of the suggestion but left the loop variable, so the
finding was still live at head despite being marked resolved.

broadcast.cu: the GinHybridBroadcastKernel comment described a fallback to
the backend's rsCtx->sdmaThreshold when sdmaThresholdOverride is
TEST_SDMA_THRESHOLD_UNSET. That cannot happen from this caller: bcastThr
comes from testResolveSdmaThreshold (:1041), and resolveThreshold returns
kBroadcastSdmaThresholdDefault rather than the sentinel when both env vars
are unset, so BroadcastGetSdmaThreshold is unreachable. The sentinel arm is
kept for parity with AllGather; the comment now says that instead of
promising a fallback that never runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
dlamd1dai added a commit to dlamd1dai/rocm-systems that referenced this pull request Sep 16, 2026
_assert_bcast_ok checked that a -D 3 run worked, never that it worked the
way the test is named. Every tier produces a correct broadcast, so #wrong
stays 0 when a tier stops being reached and its messages fall through to
the next one: deleting the ScatterAG arm left
test_BroadcastGinSdmaScatterAllgather green with
GinScatterAllgatherBroadcastKernel covered by nothing. The two hang guards
and the flat segmented tests had the same hole.

BroadcastRunColl now names the kernel it launched as "#[bcast-tier] <name>"
and the four GIN-SDMA pytest regressions take an expected tier, asserting
exactly one ran so a run that switched tiers between roots cannot pass
either.

The hybrid arm asks gin_sdma::bcastKernelTier -- the policy the kernel
itself branches on -- rather than reporting "hybrid", which would leave
flat/LSA/LL indistinguishable and the flat tests as uncovered as before.
Reported once per distinct tier, since RunColl is called for warmup, every
iteration and every root; the dedupe is an atomic fetch_or so -t N host
threads cannot race it into printing twice. Opt-in via
RCCL_TESTS_BCAST_TIER_LOG, with the env read cached, so default stdout
stays byte-identical for the perf harness and the timed path is untouched.

The four new offline guards carry GinSdma in their names deliberately: the
rccl-gin-bcast-pytest job runs -k GinSdma, and run-gin-ci.sh expands its
args unquoted, so a filter containing a space is not available. One of them
reproduces the review's scenario directly -- the output a fall-through run
would print, which every other assertion accepts.

Addresses pvallem's review on ROCm#11142.

Co-authored-by: Cursor <cursoragent@cursor.com>
anugodavar pushed a commit that referenced this pull request Sep 17, 2026
… testable

Argus review on #11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
anugodavar pushed a commit that referenced this pull request Sep 17, 2026
…sed kernels

Argus review on #11142. The load-bearing fix is the first one;
the rest remove the surface that made it hard to see.

Per-device ring tables. c_bcastNRings / c_bcastRingSucc / c_bcastRingPos /
c_bcastStream are __constant__, so they exist once per DEVICE, but the
"already built" gate (g_bcastBuiltN / g_bcastBuiltNRings) was process-wide. In a
-g N single-process multi-GPU run device 0 uploaded the decomposition and set
the flag, then devices 1..N-1 saw the flag set, skipped their upload, and ran
the ring against a zero-filled table: blockIdx.x % c_bcastNRings divides by
zero, and every rank reads position 0 and so believes it is the root. Key the
state on the current device instead (startColl calls cudaSetDevice immediately
before runColl on the device-API path). Distinct host threads own distinct
devices, so this also keeps -t N off a shared slot; the remaining shared
scratch -- buildBcastRingDecomp's backtracking object and the h_succ/h_pos
staging arrays -- becomes thread_local.

This was not caught because it needs -g N > 1: the GIN CI entry runs -g 1 under
mpirun, and the MI355X gates were MPI-based too.

g_bcastLLHandle / g_bcastLLReq become thread_local for the same -t N reason:
getDevCommRequirements runs once per thread and links g_bcastLLReq into that
thread's requirements list, and one shared node spliced into N lists corrupts
them. Note the handle itself did NOT need per-device treatment, contrary to the
review comment: ncclDevResourceHandle is a uint32 resource index, and every
devComm is created from one identical reqs, so the index is valid on all of
them.

Ring variants. Five ring kernels shipped but only GinRingSmTableBroadcastKernel
was reachable under defaults; the others needed env knobs nothing in the tree
sets, and the comments above them already recorded the A/B results that ruled
them out. Keep the table kernel and the coprime-stride fallback that N=4 and
N=6 genuinely need (no edge-disjoint decomposition exists there), and drop the
SDMA-put ring, the single-orientation SM ring, and the point-to-point ring along
with NCCL_GIN_ANVIL_BCAST_RING_{SM,MULTI,P2P}. The measurements that justify
the choice are kept in the consolidated comment. This also removes the
unvalidated ginHandles[0] cast the P2P kernel did without the null/magic checks
BroadcastGetSdmaThreshold performs, and collapses the devtime gate's disagreement
with the dispatch: builtNRings > 0 is now exactly the condition under which the
table kernel runs, so the timed kernel can no longer report a number for a
kernel that did not execute.

bcastRingCtas(). Used strtol(e, nullptr, 0), so "64foo" parsed as 64 and "010"
as octal 8 -- exactly what BroadcastParseCtasEnv exists to reject for the
sibling variable. Parse base 10 with a full-string check, and cache the result:
it was re-reading getenv at every call site, so the value that sized
lsaBarrierCount and the value that picked the launched grid were independent
reads of a mutable environment, and a grid larger than the barrier pool is the
documented corrupt-or-hang condition.

LL cap. The kernel evaluated eligibility against gin_sdma::kBroadcastLLMaxBytes
(the 64 KiB compile ceiling) while the host sized the pool and grid from the
env-resolved cap (2 KiB by default). They agreed only because bcastLLEligible
also requires msgBytes/8 <= nSlots, which re-imposed the cap via the slot count.
Pass the resolved cap so both sides evaluate the same bound.

Tier selection now goes through the shared helpers: the hybrid kernel switches
on bcastKernelTier instead of re-deriving the ladder inline (the header claimed
the kernels called it; they did not), the dispatch resolves one bcastLargeTier
instead of testing two predicates in an order nothing pinned, and both ring
kernels slice with ringStripe/ringChunk.

Also: drop the now-unused testLaunchDeviceKernelThreshold launcher and the
gin_anvil_sdma.h include that only signalPeer needed; rename the surviving
launcher's trailing parameter to kernelArg, since the ring dispatches were
passing nChunks into a slot declared sdmaThresholdOverride; remove the dead
#else arm on BC_HAVE_LL, whose condition is implied by this block's; and record
why the 2.28 requirements arm provisions no LL resource.

Co-authored-by: Cursor <cursoragent@cursor.com>
anugodavar pushed a commit that referenced this pull request Sep 17, 2026
…to block 0

Both GIN Broadcast kernels sampled their completion signal on every CTA before
the entry barrier, while only block 0 ever issues the puts: the put loops walk
ranks (r < nRanks / r < N) with a grid-wide tid, so only tid < nRanks executes
a body, and nRanks is far below blockDim.x.

The entry barrier does not close that gap. ncclBarrierSession is constructed
with blockIdx.x, so it orders block n against block n of every peer and nothing
else. Block 0 of the root and block n of a non-root share no barrier edge, so
the root's put can land before that CTA has run readSignal. It then samples
base+1 and waits for base+2, which nobody posts, and the kernel hangs.

Moving the read after the barrier does not help, for the same reason. Confining
the read, the puts and the wait to block 0 does: block 0's read precedes the
block-0-to-block-0 barrier, which precedes the root's put. Kernel completion
still covers the wait, because the kernel does not retire until block 0 does.

GinScatterAllgatherBroadcastKernel had the identical shape at both of its
signals, so the same treatment is applied to the scatter wait and the gather
wait, along with the trailing flush -- block 0 issued every put, so it owns the
only dirty queues.

The put loops now stride by blockDim.x instead of grid-wide, so block 0 still
covers every rank if nRanks ever exceeds one block's width. Other CTAs continue
to run the grid-strided local copy and must still take the entry barrier, which
is a cross-rank barrier keyed by blockIdx.x.

Reported-by: amd-argus in #11142 review
Co-authored-by: Cursor <cursoragent@cursor.com>
anugodavar pushed a commit that referenced this pull request Sep 17, 2026
…omment

Two Argus review follow-ups on #11142. No functional change.

test_Broadcast.py: _read_debug_logs looped on `path`, shadowing the
module-level `path` bound at line 69. 2df5f51 applied the
errors="replace" half of the suggestion but left the loop variable, so the
finding was still live at head despite being marked resolved.

broadcast.cu: the GinHybridBroadcastKernel comment described a fallback to
the backend's rsCtx->sdmaThreshold when sdmaThresholdOverride is
TEST_SDMA_THRESHOLD_UNSET. That cannot happen from this caller: bcastThr
comes from testResolveSdmaThreshold (:1041), and resolveThreshold returns
kBroadcastSdmaThresholdDefault rather than the sentinel when both env vars
are unset, so BroadcastGetSdmaThreshold is unreachable. The sentinel arm is
kept for parity with AllGather; the comment now says that instead of
promising a fallback that never runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
anugodavar pushed a commit that referenced this pull request Sep 17, 2026
_assert_bcast_ok checked that a -D 3 run worked, never that it worked the
way the test is named. Every tier produces a correct broadcast, so #wrong
stays 0 when a tier stops being reached and its messages fall through to
the next one: deleting the ScatterAG arm left
test_BroadcastGinSdmaScatterAllgather green with
GinScatterAllgatherBroadcastKernel covered by nothing. The two hang guards
and the flat segmented tests had the same hole.

BroadcastRunColl now names the kernel it launched as "#[bcast-tier] <name>"
and the four GIN-SDMA pytest regressions take an expected tier, asserting
exactly one ran so a run that switched tiers between roots cannot pass
either.

The hybrid arm asks gin_sdma::bcastKernelTier -- the policy the kernel
itself branches on -- rather than reporting "hybrid", which would leave
flat/LSA/LL indistinguishable and the flat tests as uncovered as before.
Reported once per distinct tier, since RunColl is called for warmup, every
iteration and every root; the dedupe is an atomic fetch_or so -t N host
threads cannot race it into printing twice. Opt-in via
RCCL_TESTS_BCAST_TIER_LOG, with the env read cached, so default stdout
stays byte-identical for the perf harness and the timed path is untouched.

The four new offline guards carry GinSdma in their names deliberately: the
rccl-gin-bcast-pytest job runs -k GinSdma, and run-gin-ci.sh expands its
args unquoted, so a filter containing a space is not available. One of them
reproduces the review's scenario directly -- the output a fall-through run
would print, which every other assertion accepts.

Addresses pvallem's review on #11142.

Co-authored-by: Cursor <cursoragent@cursor.com>
corey-derochie-amd pushed a commit that referenced this pull request Oct 14, 2026
… testable

Argus review on #11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
corey-derochie-amd pushed a commit that referenced this pull request Oct 15, 2026
… testable

Argus review on #11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
corey-derochie-amd pushed a commit that referenced this pull request Oct 15, 2026
… testable

Argus review on #11142 pointed out that several of the policy
suite's assertions covered code the kernels do not run, while the code they do
run was untested. Address that in the header rather than the tests:

* Add ringStripe()/ringChunk(). The default large tier is the SM ring, and its
  CTA-stripe and pipeline-chunk splits were open-coded at each kernel that walks
  them, appearing in no test. The one tiling the suite did prove exactly
  (sagChunk) is used only by the scatter+allgather tier. New cases assert both
  splits partition their range exactly, stay contiguous from zero, stay balanced
  to within one element, and compose to cover the buffer for an indivisible
  (prime) element count.

* Add bcastLargeTier(). Under the shipped defaults bcastUseRing (>=32 MiB) and
  bcastUseScatterAllgather (>=2 MiB) are BOTH true for every large message, so
  which tier runs is fixed by the order the dispatch tests them and by nothing
  else. Each predicate was asserted in isolation, and "both true" satisfies all
  of those assertions, so swapping the two blocks would not have failed a test.
  Returning the resolved tier as an enum lets the 1 MiB / 2 MiB / 32 MiB
  boundaries and the ring opt-out pin the precedence directly.

* Reject overflowing size values in parseSize(). strtoull saturates at
  ULLONG_MAX instead of failing, and the suffix multiply can then wrap a huge
  value back down: "17179869184G" is 2^64 bytes, which wrapped to 0, and 0 is
  meaningful here (it force-enables GIN). That is the same silent mis-tiering
  shape as the negative-value defect fixed earlier in this branch. Also pin that
  "0x10" reads as 0 rather than 16, since the parser is base 10 by contract.

* Drop alignChunkCount(). It had no caller in broadcast.cu: both the SAG and
  ring paths slice with sagChunk, which deliberately does an unaligned even
  split with the remainder folded into the last rank, so the header was shipping
  an alignment rule the kernels contradict, plus two test cases and eight
  assertions backing it.

* Make the CTA-ladder assertions relational. Eight of them compared
  bcastHybridCtas' result against the very constant it returns on that branch,
  so they pinned which branch was taken but not the ladder's shape. Add a case
  asserting LL < LSA, SDMA < LSA, and every grid within [1, pool].

Also inline the basis for the header's defaults instead of pointing at
ddai-artifacts/docs/gin-anvil-sdma-broadcast-design-plan.md, which is not in the
repo and left the bare section numbers in broadcast.cu unresolvable.

Host policy suite: 48/48 pass (was 38; -2 dead-helper cases, +12 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

3 participants