Repository navigation
Conversation
✅ All Policy Checks Passed
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
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 |
There was a problem hiding this comment.
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 withmake -k || trueat line 198 and then verifies onlyalltoall_perf(line 200) andbroadcast_perf(line 205).reduce_scatter_perf, which this PR newly gives a-fgpu-rdcwhole-archive rocSHMEM link, is not checked anywhere in the file, so a compile break in exactly the new path is swallowed by-kand surfaces only as a downstreamSKIP ... perf binary not foundin 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.
|
Addressed the review findings in
Validation completed: shell syntax checks, policy-header compile/run smoke test, GIN config parser tests, and @argus re-review |
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>
186dfb7 to
8e87145
Compare
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| lsaBar.sync(ncclCoopCta(), cuda::memory_order_relaxed); | |
| lsaBar.sync(ncclCoopCta(), cuda::memory_order_acquire); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved by 8efddab141.
| -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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved by 8efddab141.
| (8, "min"), | ||
| (8, "max"), | ||
| ]) | ||
| @pytest.mark.parametrize("dtype", ["int32", "float"]) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved by 8efddab141.
| loop = devtimeLoopMid; | ||
| if (devtimeSkipMid >= 0) skip = devtimeSkipMid; | ||
| else skip = (skip < 2) ? skip : 2; | ||
| } |
There was a problem hiding this comment.
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.
| } | |
| int loop = 0; | |
| int skip = 0; | |
| gin_devtime::resolveLoopSkip(totalBytesCta, loop, skip); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved by 8efddab141.
| pytest_prefix="RS" | ||
| hw_cases="${GIN_PYTEST_RS_HW_CASES}" | ||
| outer_timeout="${GIN_PYTEST_RS_TIMEOUT}" | ||
| bench_timeout="${outer_timeout}" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved by f3606768a3.
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>
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
|
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 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. |
Summary
develop. The shared pytest harness stays on rccl-tests: share GIN-SDMA pytest harness #11894, the device-api rocSHMEM CI build stays on ci(rccl): build device-api with rocSHMEM GIN #11895, and the poison-isolation change stays closed with rccl: isolate GIN-SDMA TUs from HIP atomics poison (AICOMRCCL-2221) [1/3] #11893.reduce_scatter_perf -D 3): single-tier LSA read-reduce, host policy unit test, GPU addressing test, opt-in pytest, and GIN CI entries (rccl-gin-rocshmem-sdma-rs-d3,rccl-gin-rs-policy-ut,rccl-gin-rs-pytest).test_ReduceScatterDevtime.pyand the device-api job hook) waits until ci(rccl): build device-api with rocSHMEM GIN #11895 lands.develop,NCCL_GIN_TYPE_ANVIL_SDMAis 7 (type 6 is rocSHMEM GDA). The new CI entry and the pytest default use 7. Hardware runs recorded on feat(rccl-tests): GIN SDMA ReduceScatter (-D 3) #11556 used 6 because that number was Anvil SDMA on that older base.Host-path floating-point
avgstays skipped whendeviceImpl == 0, matching the skip that landed ondevelopafter #11556 branched. The GIN device path still runsavgand still skipsmulsum.JIRA ID: AICOMRCCL-1803
https://amd-hub.atlassian.net/browse/AICOMRCCL-1803
Test plan
reduce_scatter_perfbuilds withENABLE_DEVICE_APIandENABLE_ROCSHMEM_GIN(RDC-linked likebroadcast_perf)rccl-UnitTestsGinSdmaReduceScatterPolicypasses (ctest -L unit)rccl-UnitTestsGinSdmaReduceScatterGpupasses on a GPURCCL_TESTS_GIN_SDMA_RS=1pytesttest_ReduceScatterGinSdma.pywithNCCL_GIN_TYPE=7rccl-gin-rocshmem-sdma-rs-d3,rccl-gin-rs-policy-ut, andrccl-gin-rs-pytest-D 0still skips floatingavgand still runsmulsumMade with Cursor