Skip to content

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

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

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

Conversation

@dlamd1dai

Copy link
Copy Markdown
Contributor

Summary

Host-path floating-point avg stays skipped when deviceImpl == 0, matching the skip that landed on develop after #11556 branched. The GIN device path still runs avg and still skips mulsum.

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

Test plan

  • reduce_scatter_perf builds with ENABLE_DEVICE_API and ENABLE_ROCSHMEM_GIN (RDC-linked like broadcast_perf)
  • rccl-UnitTestsGinSdmaReduceScatterPolicy passes (ctest -L unit)
  • rccl-UnitTestsGinSdmaReduceScatterGpu passes on a GPU
  • RCCL_TESTS_GIN_SDMA_RS=1 pytest test_ReduceScatterGinSdma.py with NCCL_GIN_TYPE=7
  • GIN CI runs rccl-gin-rocshmem-sdma-rs-d3, rccl-gin-rs-policy-ut, and rccl-gin-rs-pytest
  • Host -D 0 still skips floating avg and still runs mulsum

Made with Cursor

@therock-pr-bot

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?

@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Draft for the low-CTA GIN SDMA ReduceScatter tier, stacked on this branch so the diff is only that tier: dlamd1dai#17

It should move to ROCm/rocm-systems develop after this PR merges.

@amd-argus amd-argus Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARGUS

Findings

1 finding(s) that could not be posted as inline comments
  • projects/rccl/tools/ci/lib/gin.sbatch:205 (Low) gin.sbatch never asserts reduce_scatter_perf was built, so a break in the new RDC link is swallowed gin.sbatch builds rccl-tests with make -k || true at line 198 and then verifies only alltoall_perf (line 200) and broadcast_perf (line 205). reduce_scatter_perf, which this PR newly gives a -fgpu-rdc whole-archive rocSHMEM link, is not checked anywhere in the file, so a compile break in exactly the new path is swallowed by -k and surfaces only as a downstream SKIP ... perf binary not found in an explicitly non-gating job. Could a third copy of the lines 200-204 block cover it at build time?

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

Bottom line: Worth settling before approval: the warp-unroll arm at reduce_scatter.cu:388 ships with no test, and the GPU addressing test derives its expected values the same way as the code it checks.

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

Comment thread projects/rccl/tools/ci/run-gin-ci.sh Resolved
@dlamd1dai dlamd1dai self-assigned this Sep 28, 2026
@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in be3c3a6829 and 186dfb76b5:

  • removed the untested legacy warp-unroll path and its environment toggle
  • shared the ReduceScatter slice-offset helper between production and GPU-test code, with independent expected values and explicit policy coverage
  • asserted that reduce_scatter_perf was built after the non-gating make -k
  • gave ReduceScatter pytest a dedicated 3600-second outer budget
  • rejected whitespace-prefixed negative CTA pins
  • removed the inaccurate AllGather barrier comparison

Validation completed: shell syntax checks, policy-header compile/run smoke test, GIN config parser tests, and git diff --check.

@argus re-review

dlamd1dai and others added 3 commits September 28, 2026 13:22
Ship the single-tier LSA read-reduce path with its policy and GPU unit
tests, opt-in pytest, and GIN CI coverage. Anvil SDMA is GIN type 7 on
current develop.

AICOMRCCL-1803

Co-authored-by: Cursor <cursoragent@cursor.com>
The grid-stride schedule is the only path that ships, and the opt-in
warp-unroll branch had no test coverage.

AICOMRCCL-1803

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dlamd1dai
dlamd1dai force-pushed the users/dondai/gin-sdma-rs-d3 branch from 186dfb7 to 8e87145 Compare September 28, 2026 17:24

@amd-argus amd-argus Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARGUS

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

Bottom line: Worth settling before approval: the entry LSA barrier at reduce_scatter.cu:244 is memory_order_relaxed before the kernel reads every peer's sendbuff, where all_reduce.cu:167 uses acquire for the same read-reduce shape.

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


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High

This lsaBar.sync is cuda::memory_order_relaxed and is the only synchronization in ginReduceScatterBody before it reads every peer's sendbuff through ncclGetLsaPointer (line 272 onward). acquireOrderOf(relaxed) is relaxed (nccl_device/utility.h:351-355), so the wait-side load carries no acquire and those remote reads are not ordered after observing the peer arrived. all_reduce.cu:167 is the same LSA read-reduce shape and uses acquire; all_gather.cu:194 can stay relaxed because its tier only writes peers and publishes with release at :196. releaseOrderOf(acquire) is relaxed, so the arrive side costs nothing.

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

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 8efddab141, taken exactly as suggested.

The part that convinced me this is a real bug rather than a defensible relaxation is that the function's own exit comment already asserts the guarantee the relaxed barrier does not provide:

The entry barrier already guarantees every peer's sendbuff is filled before any read.

That is the invariant the "NO exit barrier" argument rests on, and with acquireOrderOf(relaxed) returning relaxed at utility.h:351-355 the wait-side load carried no acquire, so the ncclGetLsaPointer reads from line 272 onward were not ordered after observing the peer's arrival. The comment described the intended design; the code did not implement it.

Your precedent comparison holds up on both sides. all_reduce.cu:167 is the same LSA read-reduce shape and acquires. all_gather.cu:194 can stay relaxed because all_gather.cu:169 uses the LSA pointer as a destination — that tier writes peers rather than reading them — and publishes with the release at :196. ReduceScatter is the read case, so it needs the acquire and has no exit barrier to lean on.

Since -D 3 exists for performance, it seemed worth measuring the cost rather than reasoning about it. At 1 MiB total, five repeats each on 8x MI350X: acquire 10.56, 10.56, 10.69, 10.86, 10.67 us against relaxed 11.13, 10.84, 11.00, 10.67, 10.64 us. The acquire arm is marginally faster on those means, which is to say the difference is entirely run-to-run noise — as expected, since releaseOrderOf(acquire) is relaxed so the arrive side is unchanged and the wait side gains one acquire per CTA per collective. A single earlier sample had suggested an 8% cost; that did not survive repetition.

Datacheck is clean at 4 MiB/rank for float and int32 (Out of bounds values : 0 OK), along with the six new narrowing-dtype cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

Comment thread projects/rccl/tools/ci/lib/gin.sbatch Outdated
-DROCSHMEM_INSTALL_DIR="${ROCSHMEM_INSTALL_DIR}"
make -j"${build_jobs}" rccl-UnitTestsFixtures rccl-UnitTestsGinSdmaBroadcastPolicy rccl-UnitTestsGinAnvilSdmaFactory
make -j"${build_jobs}" rccl-UnitTestsFixtures rccl-UnitTestsGinSdmaBroadcastPolicy \
rccl-UnitTestsGinSdmaReduceScatterPolicy rccl-UnitTestsGinAnvilSdmaFactory

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.

Medium

rccl-UnitTestsGinSdmaReduceScatterGpu is absent from this target list and has no gin-tests.json entry, so the 191 lines of GPU test this PR adds are never compiled, let alone executed: nothing in RCCL CI runs ctest, as projects/rccl/test/CMakeLists.txt:23 states. The sibling policy UT got both halves in this same PR (this line, plus the rccl-gin-rs-policy-ut entry). Could the GPU target join both? The "gpu"-only label is fine as it stands.

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 8efddab141. The GPU target joins both halves, as you suggested.

The gap holds up end to end: add_executable(rccl-UnitTestsGinSdmaReduceScatterGpu ...) sits at test/CMakeLists.txt:714 with its own add_test and LABELS "gpu", but the target appeared in neither gin.sbatch's build step nor gin-tests.json, and since nothing in RCCL CI runs ctest those 191 lines were compiled by no job and executed by none.

gin.sbatch now builds it alongside rccl-UnitTestsGinSdmaReduceScatterPolicy and fails the job with its own error message if the binary is missing, matching the treatment the policy UT already had. gin-tests.json gains an rccl-gin-rs-gpu-ut entry under kind fixtures, with a comment recording why an entry is needed at all despite the ctest registration:

{
  "_comment": "GPU-labelled in ctest, but RCCL CI does not run ctest, so it needs its own entry to execute at all.",
  "name": "rccl-gin-rs-gpu-ut",
  "kind": "fixtures",
  "bin": "rccl-UnitTestsGinSdmaReduceScatterGpu",
  "env": [],
  "args": ""
}

I left the "gpu" ctest label alone, as you said it reads fine — it still does the right thing for a local ctest -L unit.

One caveat on verification: I confirmed the JSON parses, the parser emits the new row, and gin.sbatch is syntactically valid, but I have not run the full GIN sbatch job, so the first real proof that this target builds under that job's configuration will come from CI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

(8, "min"),
(8, "max"),
])
@pytest.mark.parametrize("dtype", ["int32", "float"])

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.

Medium

This dtype list plus the hang guard's int64/half at line 195 means bfloat16, fp8_e4m3 and fp8_e5m2 never run through -D 3, and the rccl-gin-rocshmem-sdma-rs-d3 entry passes no -d, so it adds only float. Those three types are what gin_sdma_reduce.h's narrow-back-to-T-on-every-pairwise-step overloads exist for (gin_sdma_reduce.h:51 and :56 for rsum alone), and ReduceScatterRunTest does not exclude them on the device path. Could they join the ladder for sum and avg?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and fixed in 8efddab141. You are right that the three types the narrowing overloads exist for were the three the ladder never reached.

I confirmed the device path accepts them — RS_BF16_CASE and RS_FP8_CASE are wired into SPECIALIZE_REDUCE_KERNEL, and the only exclusion there is prod for the two fp8 types. So rather than widen the existing dtype list, which would have dragged all three through prod, min and max as well, I added a separate case limited to the ops that actually narrow:

@_rs_skip
@pytest.mark.parametrize("op", ["sum", "avg"])
@pytest.mark.parametrize("dtype", ["bfloat16", "fp8_e4m3", "fp8_e5m2"])
def test_ReduceScatterGinSdmaNarrowingDtypes(request, dtype, op):

at 4 MiB/rank, which is the mid-band 48-CTA probe the ladder already runs sum and avg at. That is 6 new launches, so GIN_PYTEST_RS_HW_CASES goes from 15 to 21 and the comment above it now itemises all three groups.

All six pass on hardware. On 8x MI350X gfx950 under ROCm 7.13, reduce_scatter_perf -b 4194304 -e 4194304 -R 2 -D 3 -A 1 -c 1:

bfloat16   sum   Out of bounds values : 0 OK
bfloat16   avg   Out of bounds values : 0 OK
fp8_e4m3   sum   Out of bounds values : 0 OK
fp8_e4m3   avg   Out of bounds values : 0 OK
fp8_e5m2   sum   Out of bounds values : 0 OK
fp8_e5m2   avg   Out of bounds values : 0 OK

That was worth running before adding them rather than after: fp8 through a pairwise fold that renarrows on every step is exactly the shape where a tolerance problem would have shown up, and it would have landed as a CI failure rather than a review comment.

I did not add a -d to the rccl-gin-rocshmem-sdma-rs-d3 entry. That one is the perf probe and adding dtypes there multiplies bench time without checking anything the pytest does not now cover, but say the word if you would rather have the coverage in both places.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low

These eleven lines reproduce gin_devtime::resolveLoopSkip (gin_sdma_devtime.h:34-46) statement for statement, including both skip clamps, and gin_sdma_devtime.h is already included at line 20. all_gather.cu:332 calls the helper. A later change to the shared tier ladder would silently miss ReduceScatter.

Suggested change
}
int loop = 0;
int skip = 0;
gin_devtime::resolveLoopSkip(totalBytesCta, loop, skip);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and taken verbatim in 8efddab141. The eleven lines were a statement-for-statement copy of gin_devtime::resolveLoopSkip, including both skip clamps, from a header already included at line 20, and your point about the shared tier ladder drifting is the real risk — all_gather.cu:332 calls the helper, so ReduceScatter was the one caller that would have silently kept the old ladder.

int loop = 0;
int skip = 0;
gin_devtime::resolveLoopSkip(totalBytesCta, loop, skip);

The parameter is named perPeerBytes in the helper and the value passed is totalBytesCta; the thresholds compare against whatever is passed, so this is behaviour-identical, but the naming mismatch is worth being aware of if the helper ever grows a per-peer-specific clamp.

Exercised on hardware rather than just compiled, since this block only runs under --device_timing and would otherwise have shipped unverified:

#[rs-devtime] size      4194304 B  ctas 32  loop 10 skip 10  devtime      12.46 us  algbw   336.62 GB/s  busbw   294.54 GB/s

4 MiB total is under the 8 MiB mid-band threshold, so loop 10 skip 10 is the default arm resolving correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

const size_t totalBytes = count * wordSize(type) * (size_t)nRanksGlobal;
double sec = devUs * 1.0e-6;
double algBw = (double)totalBytes / 1.0e9 / sec;
double busBw = algBw * ((double)(nRanksGlobal - 1) / (double)nRanksGlobal);

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.

Low

These two lines restate the algBw/busBw formula that this PR's own gin_sdma_reducescatter::bandwidthGBps (gin_sdma_reducescatter_policy.h:184) computes and that ReduceScatterGetBw was rewritten to call at line 137, so the printed number is the one copy ReduceScatterPolicyBandwidth does not cover. all_gather.cu:362 calls its helper for exactly this augment line.

Suggested change
double busBw = algBw * ((double)(nRanksGlobal - 1) / (double)nRanksGlobal);
double algBw = 0.0, busBw = 0.0;
gin_sdma_reducescatter::bandwidthGBps(count, (int)wordSize(type), sec, nRanksGlobal, &algBw, &busBw);

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and taken verbatim in 8efddab141.

double algBw = 0.0, busBw = 0.0;
gin_sdma_reducescatter::bandwidthGBps(count, (int)wordSize(type), sec, nRanksGlobal, &algBw, &busBw);

The framing in your note is what makes it worth doing: ReduceScatterGetBw was already rewritten to call the helper at line 137 and ReduceScatterPolicyBandwidth covers it, so this augment line was the single remaining copy and therefore the one place the formula could drift unnoticed. all_gather.cu:362 does the same thing for its own augment line, so this also matches the sibling.

totalBytes is still computed just above because the snprintf prints it, so that line is unchanged.

Verified on hardware through --device_timing, same run as the resolveLoopSkip thread:

#[rs-devtime] size      4194304 B  ctas 32  loop 10 skip 10  devtime      12.46 us  algbw   336.62 GB/s  busbw   294.54 GB/s

294.54 / 336.62 = 0.875 = 7/8, which is the (nranks-1)/nranks correction at 8 ranks, so the helper reproduces what the inline pair produced.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low

This cites parseAllGatherCtasEnv as precedent for leaving the host helpers unguarded, but gin_sdma_allgather_policy.h:158 opens #if !defined(__CUDA_ARCH__) && !defined(__HIP_DEVICE_COMPILE__) with that function inside it at :171, and all_gather.cu:148-155 works around the guard with its own device branch. The unguarded choice here may well be the better one; it is the precedent that is wrong.

Suggested change
// gin_sdma_allgather_policy.h's parseAllGatherCtasEnv relies on.
// arch. Plain `inline` is already host-only. Note gin_sdma_allgather_policy.h
// does the opposite: it guards parseAllGatherCtasEnv, and all_gather.cu works
// around that guard with a __HIP_DEVICE_COMPILE__ branch.

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and taken in 8efddab141. Tracing the citation through: gin_sdma_allgather_policy.h:158 does open #if !defined(__CUDA_ARCH__) && !defined(__HIP_DEVICE_COMPILE__) with parseAllGatherCtasEnv inside it at :171, and all_gather.cu:148-155 does carry a __HIP_DEVICE_COMPILE__ branch to work around that guard. So the comment's factual claim about the precedent was simply wrong.

The reasoning above it still stands on its own — HIP parses the whole translation unit in the device pass including bodies it will not codegen, so guarding these would break name lookup in ReduceScatterParseCtasEnv — it just should not have been attributed to a precedent that does the opposite. The comment now says so explicitly, which is more useful than silence: someone comparing the two headers will otherwise assume one of them is a mistake.

Thanks for checking the citation and not just the conclusion — a wrong attribution wrapped in correct reasoning is exactly the kind of thing that survives review, because it reads plausible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

Comment thread projects/rccl/tools/ci/run-gin-ci.sh Outdated
pytest_prefix="RS"
hw_cases="${GIN_PYTEST_RS_HW_CASES}"
outer_timeout="${GIN_PYTEST_RS_TIMEOUT}"
bench_timeout="${outer_timeout}"

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.

Low

bench_timeout is decided twice: line 215 binds it to GIN_PYTEST_TIMEOUT for every pytest kind, and this line re-binds it for ReduceScatter only. Broadcast is correct only because its outer_timeout at line 247 happens to equal line 215's value. A third pytest file that sets outer_timeout and misses this line gets apply_gin_pytest_inner_budget sizing against its own cap while timeout enforces Broadcast's, inverting the invariant lines 195-203 protect. Could this binding move to just after the esac?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and moved as suggested in 8efddab141.

Your description of the failure mode is what makes this worth fixing now rather than later: the bug is latent, not active. Broadcast happens to be correct only because its outer_timeout equals the value line 215 binds, so today both files agree by coincidence. A third pytest file with its own outer_timeout that forgot this line would have apply_gin_pytest_inner_budget sizing its inner budget against its own cap while timeout enforced Broadcast's — which inverts exactly the invariant lines 195-203 exist to protect, and would surface as an orphaned mpirun session rather than a clean failure.

The binding now happens once, immediately after the esac, so it is structurally impossible for a file to set outer_timeout and miss it:

    esac
    # Bind once, after the esac, so every pytest file enforces the same cap it
    # sizes its inner budget against. Per-file re-binding inside the case would
    # silently pair one file's outer_timeout with another's `timeout`.
    bench_timeout="${outer_timeout}"

The per-file line in the ReduceScatter arm is gone. Behaviour for both existing files is unchanged, since each now gets its own outer_timeout where before Broadcast got the identical value by a different route.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low

Nothing in the PR sets NCCL_GIN_ANVIL_RS_CTAS, so the clamp composed here is only covered as arithmetic: gin_sdma_reducescatter_policy_test.cpp:166 pins the -V 8 plus RS_CTAS=64 regression, but swapping this call for a bare reduceScatterCtas(...) would reintroduce that out-of-bounds lsaBarrier index and every test would still pass. One pytest case carrying the pin through RCCL_TESTS_RS_XENV closes it, though it costs a launch and a GIN_PYTEST_RS_HW_CASES bump, so it may read better as a follow-up.

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I am taking your suggestion that this reads better as a follow-up — so this one is not addressed in 8efddab141.

Your analysis is right and worth restating because the gap is subtle: gin_sdma_reducescatter_policy_test.cpp:166 pins -V 8 plus RS_CTAS=64 as arithmetic, but nothing in the PR ever sets NCCL_GIN_ANVIL_RS_CTAS against a real launch. Swapping the composed clamp for a bare reduceScatterCtas(...) would reintroduce the out-of-bounds lsaBarrier index and the whole suite would still pass, because the only thing testing the clamp is a unit test of the clamp.

I am deferring it rather than bundling it for two reasons. It costs a launch and another GIN_PYTEST_RS_HW_CASES bump, and this revision already raises that count from 15 to 21 for the narrowing-dtype coverage; I would rather land that increase, see the job's wall-clock, and then decide the budget for this one. Second, the pin belongs with the --device_timing smoke case you raised on the sibling thread — both are "wire an existing untested path through RCCL_TESTS_RS_XENV", and they share the plumbing.

I will file them together. If you would rather have this one in the current PR, say so and I will add it now; it is a small change, the hesitation is purely about CI budget.

// timing). loop/skip come from --devtime_loop/--devtime_skip (default 10/10); size-
// tier overrides via --devtime_loop_mid/_large and --devtime_skip_mid/_large.
#if defined(ENABLE_DEVICE_API) && NCCL_VERSION_CODE >= NCCL_VERSION(2,28,7)
testResult_t ReduceScatterDeviceTime(struct threadArgs* args, ncclDataType_t type, ncclRedOp_t op, int root, int in_place, double* outDeltaSec) {

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.

Low

ReduceScatterDeviceTime and GinReduceScatterTimedKernel at line 369 are reachable only through --device_timing, which neither the pytest nor any gin-tests.json entry passes, so this whole block ships unexecuted. --devtime_check 1 already validates a timed kernel's output through the normal datacheck and returns an error (common.cu:1217-1228), and run-device-api-ci.sh runs an AllToAll devtime smoke on the same pattern. Worth one case as a follow-up?

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, worth a case, and I am taking it as the follow-up you propose rather than folding it in here — so this is not addressed in 8efddab141.

You are right that the block ships unexecuted. I should record that it became partly untrue by accident this round: I ran --device_timing 1 --devtime_check 1 by hand on 8x MI350X to validate the two shared-helper dedups raised on the other threads, since both resolveLoopSkip and bandwidthGBps live inside ReduceScatterDeviceTime:

#[rs-devtime] size      4194304 B  ctas 32  loop 10 skip 10  devtime      12.46 us  algbw   336.62 GB/s  busbw   294.54 GB/s
# Out of bounds values : 0 OK

So GinReduceScatterTimedKernel does run correctly and --devtime_check 1 does validate its output through the normal datacheck, exactly as you described from common.cu:1217-1228. But that was a manual run on my machine, which is not coverage — nothing in gin-tests.json or the pytest passes --device_timing, so the path is still one refactor away from silently breaking, and the fact that I had to reach for it by hand rather than point at an existing case rather makes your argument.

run-device-api-ci.sh running an AllToAll devtime smoke on the same pattern is a good template to copy. I will file this together with the NCCL_GIN_ANVIL_RS_CTAS pin from the sibling thread, since both are the same shape — push an existing untested path through RCCL_TESTS_RS_XENV and account for the extra launch in GIN_PYTEST_RS_HW_CASES.


_rs_enabled = os.environ.get("RCCL_TESTS_GIN_SDMA_RS", "") not in ("", "0", "false", "False")

RS_NP = int(os.environ.get("RCCL_TESTS_RS_NP", "0"))

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.

Low

RS_NP here, and RS_TIMEOUT_S and RS_CONN_RETRIES at lines 82-83, parse env vars with a bare int() at module scope, so a value like 900s raises at import and pytest reports a collection error for the whole file instead of skipping. test_Broadcast.py:158 already has _env_int(name, default) for exactly this, used for the BCAST vars at lines 169, 177 and 178. Could these three use it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed in 8efddab141 using the existing helper as you suggested.

The failure mode is worse than a bad value being ignored: a bare int() at module scope raises during import, so pytest reports a collection error for the entire file rather than skipping or falling back. A value like 900s — plausible given GIN_PYTEST_RS_TIMEOUT is itself written with the s suffix — would take out every ReduceScatter test rather than one.

test_Broadcast.py:158 already had exactly the right helper, so I copied it rather than inventing a variant, and all three vars now use it:

RS_NP = _env_int("RCCL_TESTS_RS_NP", 0)
RS_TIMEOUT_S = _env_int("RCCL_TESTS_RS_TIMEOUT_S", 900)
RS_CONN_RETRIES = _env_int("RCCL_TESTS_RS_CONN_RETRIES", 5)

RS_NP keeps its existing <= 0 fallback chain (ROCR_VISIBLE_DEVICES, then HIP_VISIBLE_DEVICES, then rocminfo) underneath, so a bad value now lands on device autodetection instead of aborting collection. The int() inside that rocminfo fallback is left as-is, since it parses command output rather than an env var and is already inside a try/except.

Longer term the duplication between the two files is worth pulling into a shared conftest helper, but that is a wider change than this PR and I would rather not drag test_Broadcast.py into it here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by 8efddab141.

Conflicts in tools/ci/lib/gin-tests.json and tools/ci/lib/gin.sbatch:
develop added the ConnCheck host/device unit tests to the same test list
and build step this branch added the ReduceScatter policy test to. Both
sets of entries are independent, so all are kept.

@amd-argus amd-argus Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARGUS

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

Bottom line: Worth settling before approval: the relaxed lsaBar.sync at reduce_scatter.cu:244, still open from the previous round.

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

return x;
}

template <class T> __device__ __forceinline__ T combine(int op, T a, T b) {

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.

Medium

gin_sdma_reduce::combine, preOp and postOp have no caller outside reduce_scatter.cu, so the bit-match-with-verifiable.cu contract stated at the top of this header is checked only by the -D 3 datacheck on an 8-GPU GIN node, which run-gin-ci.sh runs non-gating. Reverting the half/bf16 narrow-on-every-step to a float accumulator would leave every gtest green. Could a kernel in gin_sdma_reducescatter_gpu_test.cpp fold these ops over fixed inputs per (op, T) against expected values written out in the test?

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in f360676. Of the three findings in this round, this is the one with teeth.

ReduceOpsNarrowEveryStep in gin_sdma_reducescatter_gpu_test.cpp folds {B, 1, 1, 1, 1, 1, 1, 1} over eight ranks for float, int32, half, bfloat16, fp8_e4m3 and fp8_e5m2 under sum/prod/max/min/avg, against expected values written out in the test as you suggested.

The choice of inputs is what makes it non-vacuous. B is picked so that one ulp of B exceeds 2, which puts B+1 strictly inside the ulp — never on a tie, so the result does not depend on ties-to-even — and therefore pins the step-wise sum at B. A float accumulator would instead reach B+7 and narrow once, landing an ulp or two higher. float and int32 are the controls: neither narrows, so they see the full B+7 and stay green under either implementation.

T B step-wise sum / avg float accumulator
half 4096 4096 / 512 4104 / 513
bfloat16 512 512 / 64 520 / 65
fp8_e4m3 32 32 / 4 40 / 5
fp8_e5m2 32 32 / 4 40 / 5

I computed the expected column independently before building, rather than reading it back off a run, so the test is asserting a derived contract and not just pinning current behaviour. On gfx950 all five ReduceScatterGpu cases pass. As a negative control I rebuilt the same test with the fold accumulating in float and narrowing once: it fails exactly those eight low-precision sum and avg cases at exactly the predicted values, and float/int32 stay green.

The test also pins ncclSum=0 .. ncclAvg=4, since combine switches on the raw int while SPECIALIZE_REDUCE_KERNEL hands it an ncclRedOp_t. fp8 prod is left out because that macro never dispatches it there.

Two things worth flagging on the plumbing. The header asks to be included after common.h, but common.h is rccl-tests' MPI harness and this target has no business linking it, so the test reproduces only what the header actually reads: rccl.h (for NCCL_VERSION_CODE, ncclRedOp_t and __half, which is why the target gains ${PROJECT_BINARY_DIR}/include, mirroring RCCL_COMMON_INCLUDE_DIRS at test/CMakeLists.txt:163), the HAVE_BF16/HAVE_FP8 guards, rccl_float8.h, and ENABLE_DEVICE_API — opened locally, since this is pure arithmetic and the coverage should not blink out depending on how librccl was configured.

And rccl_float8.h typedefs different underlying types per arch: fnuz on gfx942, IEEE on gfx950, and a software struct on everything else. 32, 4 and 1 are exact in all three and the mantissa widths and RNE behaviour are identical, so the expectations hold across them. I compile-checked gfx90a, gfx942 and gfx1100 to cover the fallback and fnuz paths; only gfx950 was available to run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by f3606768a3.

// (16/eltSize elements). eltSize must be a power-of-two divisor of 16
// (1,2,4,8,16), matching wordSize() of the rccl-tests element types. Mirrors
// ReduceScatterGetCollByteCount. Returns 0 if inputs are degenerate.
GIN_SDMA_RS_HD inline size_t sliceBaseCount(size_t count, size_t eltSize, int nranks) {

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.

Low

sliceBaseCount, sliceBytes, reduceScatterSliceOffset and pickSdmaThreshold are character-for-character copies of chunkBaseCount, chunkBytes, allGatherRecvSliceOffset and pickSdmaThreshold in gin_sdma_allgather_policy.h, and gin_sdma_broadcast_policy.h is a third copy of the same family. Divergence between copies is silent: the ReduceScatter unit test locks one behaviour while AllGather drifts, and nothing goes red. Consolidating means editing AllGather, so this reads as a follow-up rather than something to fix in this PR.

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and your framing is right: this belongs in a follow-up rather than here.

The duplication is real: sliceBaseCount, sliceBytes, the slice-offset helper and pickSdmaThreshold are the AllGather versions with the names changed, and Broadcast makes a third copy. But consolidating means editing gin_sdma_allgather_policy.h and gin_sdma_broadcast_policy.h, which drags all_gather.cu and broadcast.cu into a PR whose hardware coverage is entirely ReduceScatter — I would be changing the two collectives I am least able to regression-test in this branch.

I will file it together with the other follow-ups from this round (the NCCL_GIN_ANVIL_RS_CTAS pytest pin and the --device_timing smoke case). The shape I have in mind is a single gin_sdma_slice_policy.h holding the count/bytes/offset arithmetic and the threshold pick, with the three per-collective headers reduced to their CTA ladders, which are the part that genuinely differs.

)
self.assertEqual(parts[5], "-k GinSdma -v")

def test_rs_pytest_kind_preserves_args(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low

test_rs_pytest_kind_preserves_args can never fail on its own: it differs from test_pytest_kind_preserves_args only in the name/bin strings and a one- instead of two-entry env list, and parse_gin_config.py branches on neither bin nor kind (parse_config at parse_gin_config.py:49 copies both through, format_rows at :78 joins them positionally). Could the existing case be parametrized over both filenames instead?

@dlamd1dai dlamd1dai Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in f360676.

Tracing it through: parse_config copies bin and kind straight through, and format_rows joins the fields positionally. Neither branches on either value, so there is no code path test_rs_pytest_kind_preserves_args could reach that test_pytest_kind_preserves_args did not — it could only ever have failed as a duplicate.

Parametrized as you suggested. Since the file is unittest rather than pytest, the two filenames now run through a subTest loop in the single case. I kept the differing env list lengths (two entries and one) so the -x-prefixed join is still pinned at both arities, and added an assertion on parts[1] so the name field is covered too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by f3606768a3.

dlamd1dai and others added 2 commits September 28, 2026 21:28
ginReduceScatterBody reads every peer's sendbuff through ncclGetLsaPointer,
and the only synchronization before those reads was a relaxed lsaBar.sync.
acquireOrderOf(relaxed) is relaxed, so observing a peer's arrival did not
order the remote reads after the writes that filled its sendbuff -- the
invariant the function's own exit comment already claims. Acquire here, as
all_reduce.cu does for the same read-reduce shape; all_gather.cu can stay
relaxed because its LSA tier writes peers and publishes with a release.
releaseOrderOf(acquire) is relaxed, so the arrive side is unchanged.

rccl-UnitTestsGinSdmaReduceScatterGpu was defined in test/CMakeLists.txt
with its own add_test but appeared in neither gin.sbatch nor gin-tests.json,
and RCCL CI does not run ctest, so it never compiled or ran. It now builds
alongside the policy UT and has its own entry.

The dtype ladder covered only int32 and float, so bfloat16 and the two fp8
types never reached -D 3 even though gin_sdma_reduce.h carries per-op
narrowing overloads specifically for them. Adds a case for all three under
sum and avg (prod is excluded for fp8; min/max do not narrow) and bumps
GIN_PYTEST_RS_HW_CASES from 15 to 21 to match.

Also: drop two copies of shared logic in ReduceScatterDeviceTime in favour
of gin_devtime::resolveLoopSkip and gin_sdma_reducescatter::bandwidthGBps;
bind bench_timeout once after the esac so a pytest file cannot size its
inner budget against one cap while `timeout` enforces another; parse the
three RS_* env vars with _env_int so a bad value skips instead of failing
collection for the whole file; and correct the comment that cited
parseAllGatherCtasEnv as precedent for leaving the host helpers unguarded
when gin_sdma_allgather_policy.h in fact guards it.

Verified on 8x MI350X gfx950 under ROCm 7.13: reduce_scatter_perf -D 3
datachecks clean (Out of bounds values : 0) for float and int32 plus all
six new bfloat16/fp8_e4m3/fp8_e5m2 sum and avg cases, and the
--device_timing path exercises both dedups. The acquire costs nothing
measurable: 1 MiB total over five repeats each, acquire 10.56-10.86 us
against relaxed 10.64-11.13 us.
gin_sdma_reduce.h exists only to bit-match verifiable.cu, whose low-precision
folds round back to T after every pairwise step instead of accumulating in
float. reduce_scatter.cu is its sole caller, so swapping those overloads for a
float accumulator would leave every gtest green and surface only as a datacheck
mismatch in the -D 3 pytest job, which does not gate.

Adds ReduceOpsNarrowEveryStep to gin_sdma_reducescatter_gpu_test.cpp: for
float, int32, half, bfloat16 and both fp8 types it folds {B, 1 x 7} over eight
ranks under sum/prod/max/min/avg and compares against expected values written
out in the test. B is picked so one ulp of B exceeds 2, which puts B+1 strictly
inside the ulp -- never on a tie -- and pins the step-wise sum at B; a float
accumulator would reach B+7 and narrow once, landing an ulp or two higher. The
test also pins the ncclSum=0..ncclAvg=4 numbering that combine() switches on.
fp8 prod is left out because SPECIALIZE_REDUCE_KERNEL never dispatches it.

The header asks to be included after rccl-tests' common.h, which is an MPI
harness this target should not link, so the test reproduces only what the
header reads: rccl.h (hence the new ${PROJECT_BINARY_DIR}/include on the
target), the HAVE_BF16 / HAVE_FP8 guards, rccl_float8.h, and ENABLE_DEVICE_API.

Also folds test_rs_pytest_kind_preserves_args into
test_pytest_kind_preserves_args as a subTest loop. parse_config copies bin and
kind through untouched and format_rows joins positionally, so neither branches
on either field and the second standalone copy could not have failed on its own.

Verified on gfx950: all five ReduceScatterGpu cases pass against expectations
computed independently beforehand, not read back off the run. Negative control
-- accumulate in float and narrow once -- fails exactly the eight low-precision
sum and avg cases at exactly the predicted values (half 4104/513, bfloat16
520/65, both fp8 40/5) while float and int32 stay green. Compiles for gfx90a,
gfx942 and gfx1100, covering the software fp8 fallback and the fnuz typedefs.

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

@amd-argus amd-argus Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARGUS

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

Bottom line: Looks approval ready. One open item if you want it here: the avg skip added at reduce_scatter.cu:572 has no test that can fail on it, since all 21 pytest launches assert exit status only.

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

if (run_ops[j] == ncclAvg) {
// integer avg (SumPostDiv) is fine. Skip floating avg on the host path until
// that parity lands. The GIN device path (-D 3) implements avg itself.
if (deviceImpl == 0 && run_ops[j] == ncclAvg) {

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.

Medium

The deviceImpl == 0 && qualifier added here is not covered by anything that can fail on it. Dropping it makes the four avg launches (float, bfloat16, fp8_e4m3, fp8_e5m2) continue past their only op x type pair, return testSuccess and exit 0, and the pytest asserts exit status alone at test_ReduceScatterGinSdma.py:201. Could the skip matrix become a pure predicate in gin_sdma_reducescatter_policy.h, pinned by the policy UT that already runs as rccl-gin-rs-policy-ut?

@dlamd1dai

Copy link
Copy Markdown
Contributor Author

Superseded by #12747 and closing in favour of it. Same commits, same content; only the head branch moved.

Why. This PR was opened from a fork, and the required RCCL PR gate check can never pass that way. Fork pull requests get no secrets, so the RCCL coco PR workflow skips its Setup job, the coco smoke never runs, and the gate fails closed within seconds. Clearing it here would need a maintainer to dispatch the workflow by hand and then override the check, on every push. #12747 is branched in ROCm/rocm-systems, where Setup succeeds and coco dispatches to both the Ruby and OCI clusters on its own.

Please keep reading this thread. 14 of the 18 review threads here are still unresolved, and inline conversations do not carry across to a new pull request. Argus will re-review #12747 and should re-surface anything still unfixed, but the replies and reasoning written here only exist on this PR, so check them before merging the replacement.

No further work is needed on this branch. Closing rather than leaving it open so it does not keep consuming CI or show up as a duplicate for reviewers; it stays readable and can be reopened.

@dlamd1dai dlamd1dai closed this Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant