Skip to content

fix(rccl): return ncclInProgress from non-blocking group init when a rank is missing - #10796

Merged
mch merged 6 commits into
developfrom
users/mahughes/rocm-30165-nonblocking-init
Aug 28, 2026
Merged

mch merged 6 commits into
developfrom
users/mahughes/rocm-30165-nonblocking-init

Conversation

@mch

@mch mch commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

What

ncclCommInitRankConfig with ncclConfig_t.blocking = 0 could block inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init should
return at once with ncclInProgress so the caller can poll, time out, or abort.
On stock RCCL it instead waited for the missing rank and hung.

Why / root cause

In ncclGroupEndInternal (src/group.cc), the pending async jobs are moved out
of ncclAsyncJobs into groupJob->asyncJobs via ncclIntruQueueTransfer, which
empties ncclAsyncJobs. The subsequent non-blocking-vs-blocking decision still
inspected the now-empty ncclAsyncJobs, so a non-blocking init with pending work
mis-selected the blocking path and ran the jobs synchronously on the caller —
blocking forever when a peer rank is absent. The check must look at
groupJob->asyncJobs, which owns the jobs after the move.

The fix is a one-line change to inspect groupJob->asyncJobs in that predicate.

Testing

  • Ran the reproducer provided on the ticket on an 8xgfx950 machine: it hangs on
    develop (reproducing the bug) and passes with this fix, returning
    ncclInProgress promptly.
  • Added a host-only microtest (GroupEndInternalTest in rccl-UnitTestsMicro)
    that pins the branch selection: a non-blocking group carrying a pending init
    job returns ncclInProgress and publishes a group job on the communicator,
    while the blocking-group contrast test still runs synchronously and returns
    ncclSuccess.
  • Proved the guard is real: reverting the one-line fix in the hipified tree
    turns the two non-blocking assertions red and leaves the blocking test green.
  • Full test/host/run_host_tests.sh all pipeline passes (rccl-UnitTestsMicro
    33 tests, plus the HostUnitTests and init micro-test binaries).
  • The externals group.cc reaches are satisfied by module-organised fakes
    (comm_fakes, enqueue_stubs, reused transport_stubs, param_redirect.h).

This branch also cherry-picks the AMDSMI_FABRIC_DIRECT / HIP_FABRIC_API
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165

@mch
mch requested review from a team August 26, 2026 20:51
@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?

@mch
mch force-pushed the users/mahughes/rocm-30165-nonblocking-init branch from 05d508f to c71e91b Compare August 26, 2026 21:04
Comment thread projects/rccl/src/group.cc Resolved
@pvallem

pvallem commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Adjacent to this change, in ncclGroupCommJoin (src/include/group.h:134, not in the diff so no inline anchor):

That last line unconditionally sets ncclGroupBlocking = comm->config.blocking, wiping the ncclGroupBlocking = 1 that ncclAsyncLaunch sets for a destroyFlag job (group.cc:59-60). With the predicate fixed, ncclCommDestroy(nonblockingA); ncclAllReduce(..., nonblockingB, ...) in one group now takes the non-blocking branch, so commReclaim for A runs on the background thread after ncclGroupEnd returns.

Note a sticky guard (if (ncclGroupBlocking != 1)) is not the fix — I tried it, and it can't tell "1 because a teardown forced it" from "1 because the last comm is blocking", so it promotes a mixed collective group to blocking. The teardown forcing needs its own state (e.g. a thread_local bool consulted at the group.cc:898 predicate), reset in groupLocalResetJobState(). Own PR, presumably.

@pvallem

pvallem commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Host-only microtest for the ncclInProgress issue above. Red on current code, green with the fix.

TEST_F(GroupEndAsyncSettleTest, InitJobAlongsideCollective_SettlesCommAtSuccess) {
  ncclGroupStartInternal();
  ncclGroupCommJoin(collComm_.get(), ncclGroupTaskTypeCollective);
  QueueInitJobFor(initComm_.get());
  ncclGroupBlocking = 0;

  const ncclResult_t settled = EndGroupAndSettle(initComm_.get());

  // Anchor that the collective arm really ran -- otherwise this passes for the contrast case's reason.
  EXPECT_EQ(1, g_ncclLaunchFinishCalls);
  EXPECT_EQ(1, g_ncclTasksRegAndEnqueueCalls);
  EXPECT_EQ(ncclSuccess, COMPILER_ATOMIC_LOAD(&collComm_->asyncResult, std::memory_order_acquire));
  EXPECT_EQ(ncclSuccess, settled);
}

// Contrast: identical setup minus the collective comm. groupCommHeadMain[Collective] is nullptr,
// the drain's clear fires, and the same comm settles -- pinning that conjunct as the sole cause.
TEST_F(GroupEndAsyncSettleTest, InitJobAlone_SettlesCommAtSuccess) {
  ncclGroupStartInternal();
  QueueInitJobFor(initComm_.get());
  ncclGroupBlocking = 0;

  const ncclResult_t settled = EndGroupAndSettle(initComm_.get());

  EXPECT_EQ(0, g_ncclLaunchFinishCalls);
  EXPECT_EQ(ncclSuccess, settled);
}

EndGroupAndSettle runs ncclGroupEndInternal(), joins via ncclGroupJobComplete, then reads comm->asyncResult — so this asserts the group fully completed and the comm is still in progress, not just the return code. The first fails today with Expected: 0 / settled / Which is: 7; the second passes, so the only difference is the collective comm.

Dropping the groupCommHeadMain[ncclGroupTaskTypeCollective] == nullptr conjunct at group.cc:799 turns it green with nothing else moving (828 tests across the four host binaries, under --gtest_shuffle). That looks safe because where a comm is both an async-job comm and on the collective chain, the drain and the chain walk both pass the same ret, so the store is idempotent. Worth noting upstream calls ncclGroupCommJoin during init so the chain walk settles the comm there; RCCL has no such call in init.cc, which is why nothing settles it here.

Needs four supporting changes, the first being the one that matters: the ncclCommSetAsyncError fake in comm_fakes.cc currently returns ncclSuccess and stores nothing, so asyncResult never moves and the bug is invisible — it has to mirror init.cc:3567. Then ncclPrepareTasks, ncclTasksRegAndEnqueue, ncclLaunchPrepare and ncclLaunchFinish promoted off the ::abort() floor to succeeding seams with call counters; ncclMemoryStack::allocateSpilled and ncclMemoryStackDestruct ported from src/misc/utils.cc (the join/leave really push and pop comm->memScoped); and a test_categories_micro.yaml entry.

@pvallem

pvallem commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Host-only microtest for the group.h:134 clobber above, if useful. Red on current code, green once the guard is fixed.

// Already-joined path: L100-133 is skipped, so only the L134 store can move ncclGroupBlocking.
TEST_F(GroupBlockingGuardTest, DestroyFlagGuardSurvivesAlreadyJoinedCommJoin) {
  ncclGroupStartInternal();

  ncclGroupCommJoin(joinComm_.get(), kColl);
  ASSERT_EQ(joinComm_.get(), ncclGroupCommHead[kColl]);
  ASSERT_EQ(nullptr, joinComm_->groupNext[kColl]);  // sentinel consumed => next join skips the body

  LaunchDestroyJob();
  ASSERT_EQ(1, ncclGroupBlocking);

  ncclGroupCommJoin(joinComm_.get(), kColl);

  EXPECT_EQ(joinComm_.get(), ncclGroupCommHead[kColl]);
  EXPECT_EQ(nullptr, joinComm_->groupNext[kColl]) << "body must not have re-inserted the comm";
  EXPECT_EQ(1, ncclGroupBlocking) << "the teardown guard must be preserved across ncclGroupCommJoin";
}

// Contrast: with no teardown job in flight the same store is doing its legitimate job.
TEST_F(GroupBlockingGuardTest, CommJoinAdoptsCommBlockingModeWhenNoDestroyJob) {
  joinComm_->config.blocking = 1;  // distinguishable from both the -1 initial value and 0

  ncclGroupStartInternal();
  ASSERT_EQ(-1, ncclGroupBlocking);

  ncclGroupCommJoin(joinComm_.get(), kColl);

  EXPECT_EQ(joinComm_.get(), ncclGroupCommHead[kColl]);
  EXPECT_EQ(1, ncclGroupBlocking);
}

The first fails today with Expected: 1 / ncclGroupBlocking / Which is: 0; the second passes in both worlds, so the destroyFlag job is the only difference and a fix must not just delete the store. Two more in the suite: the same check on the first-join path, and one that re-joins a comm whose config.blocking changed and requires the refresh — without it, a fix that moves the store inside the L100 guard goes unnoticed.

Two fixture details if you adapt it: groupNext[type] must be preset to group.h's 0x1 sentinel or the join silently no-ops, and comm->memScoped needs real backing storage since ncclGroupCommJoin calls ncclMemoryStackPush (a zeroed stack spills into allocateSpilled, not linked into rccl-UnitTestsMicro).

No shared-fake changes needed — new fixture in group-test.cc plus a test_categories_micro.yaml entry. Probably belongs with the guard fix rather than here.

@pvallem

pvallem commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

One more on the newly-live non-blocking path, at group.cc:952 (outside the diff, so no inline anchor).

The branch publishes comm->groupJob = groupJob at L905/L919 before creating the thread at L928. If STDTHREADCREATE_GOTO throws, fail: runs groupCleanup then delete groupJob at L954 — but nothing nulls those pointers back out. comm->groupJob is assigned in exactly three places tree-wide (group.cc:905, group.cc:919, init.cc:4447) and only the last clears it, gated on *asyncError == ncclSuccess. A later ncclCommAbort runs setCommAbortFlags first, so ncclCommEnsureReady (init.cc:660-661) calls ncclGroupJobAbort on freed memory.

ASan on a probe of that path:

==266268==ERROR: AddressSanitizer: heap-use-after-free on address 0x74038d1e00ac
READ of size 1 at 0x74038d1e00ac thread T0
0x74038d1e00ac is located 108 bytes inside of 160-byte region [0x74038d1e0040,0x74038d1e00e0)

Offset 108 of the 160-byte ncclGroupJob is nonBlockingInit — the first field ncclGroupJobComplete reads (group.cc:964). Alloc and free are the new and the L954 delete in the same frame.

Only reachable if thread construction throws, so it's latent — but this fail path used to be live only for preconnect groups and now runs for every non-blocking init.

A microtest, red today (STDTHREADCREATE_GOTO is a macro, so it can be redefined in the test TU to force the throw; the test compares the pointer and never dereferences it):

TEST_F(GroupEndAsyncSettleTest, ThreadCreateFailure_LeavesNoDanglingGroupJob) {
  // ... one async job, no collective comm; g_forceThreadCreateFailure armed after adoption
  EXPECT_TRUE(adoptedBeforeFailure);                       // the comm did take the job
  EXPECT_EQ(ncclSystemError, ncclGroupEndInternal());      // the fail path really ran
  EXPECT_EQ(ncclSystemError, initComm_->asyncResult);      // groupCleanup processed this comm
  EXPECT_EQ(nullptr, initComm_->groupJob);                 // <-- fails today: 0x338b31c0
}

One thing worth calling out about the fix: nulling only the groupCommHead chains isn't enough. An init/split comm is never on those chains — it's only reachable via groupJob->asyncJobs — so the fix has to walk both lists. I mutated the fix to the chains-only version and the test still catches it.

Separately, the new GroupEndInternalTest::TearDown does if (comm_ && comm_->groupJob) ncclGroupJobComplete(...), which treats non-null as valid. The PR's three tests never reach fail: so it's fine today, but any future test that does — an OOM injection on NEW_NOTHROW_GOTO, or an error-returning ncclCommSetAsyncError hook — turns it into a silent UAF in the harness.

@alex-breslow-amd alex-breslow-amd 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.

From what I can tell, this looks good. Going to stamp before I forget. Please follow up on Praneeth's feedback.

Copilot AI 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.

Pull request overview

This PR fixes a non-blocking communicator-init hang in RCCL by ensuring ncclGroupEndInternal correctly selects the non-blocking path when pending async init work exists, returning ncclInProgress promptly (so callers can poll/timeout/abort). It also adds a host-only microtest suite (plus supporting fakes/stubs and build/test wiring) to lock in the intended blocking vs non-blocking behavior.

Changes:

  • Fix ncclGroupEndInternal non-blocking predicate to inspect groupJob->asyncJobs after the async-job queue transfer.
  • Add GroupEndInternalTest host-only microtests that assert non-blocking groups return ncclInProgress and publish a group job, while blocking groups run synchronously.
  • Extend host microtest build/test infrastructure (CMake sources/defines, new fakes/stubs, and micro test category inclusion).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
projects/rccl/src/group.cc One-line fix to non-blocking selection logic after async-job queue transfer.
projects/rccl/test/host/group-test.cc New host-only microtests exercising ncclGroupEndInternal blocking vs non-blocking behavior.
projects/rccl/test/host/fakes/comm_fakes.h New header providing a controllable seam for ncclCommSetAsyncError.
projects/rccl/test/host/fakes/comm_fakes.cc New comm/utility/profiling/roctx fakes used by host-only microtests.
projects/rccl/test/host/fakes/collective_stubs.cc New fail-loud stubs to satisfy collective/transport symbols not expected to run in these tests.
projects/rccl/test/host/CMakeLists.txt Wire new group microtest + fakes into rccl-UnitTestsMicro and pass GROUP_CC_PATH.
projects/rccl/test/test_categories_micro.yaml Include GroupEndInternalTest.* in the microtest category.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@mch
mch force-pushed the users/mahughes/rocm-30165-nonblocking-init branch from c71e91b to 45a3166 Compare August 27, 2026 16:06

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

LGTM with nits

Comment thread projects/rccl/src/group.cc Resolved
Comment thread projects/rccl/test/host/group-test.cc Resolved
@github-actions

Copy link
Copy Markdown
Contributor

RCCL Perf-Regression Gate: ⚠️ NO VERDICT (not measured)

This run did not produce a usable answer. It is neither a PASS nor a regression — treat the perf gate as not run for this change.

Why:

  • this report predates the trustworthiness flag, or a group could not be scored

Mode: detect (reference vs candidate)
Thresholds: small 17.2% · mid 12.3% · large 12.9%
Keys compared: 0 · Confirmed regressions: 0 · Inconclusive: 0
Provenance: n/a

Per-collective breakdown
group keys regressions inconclusive
all_gather_perf-d=bfloat16-default 0 0 0
all_gather_perf-d=float-default 0 0 0
all_reduce_perf-d=bfloat16-default 0 0 0
all_reduce_perf-d=float-default 0 0 0
broadcast_perf-d=bfloat16-default 0 0 0
broadcast_perf-d=float-default 0 0 0
reduce_scatter_perf-d=bfloat16-default 0 0 0
reduce_scatter_perf-d=float-default 0 0 0

@mch
mch force-pushed the users/mahughes/rocm-30165-nonblocking-init branch from d0d605f to 4071f51 Compare August 27, 2026 21:13
@mch

mch commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@pvallem I addressed the UAF comment in this PR, and created a follow up ticket for the group.h:134 clobber.

mch added 6 commits August 27, 2026 23:03
…ing rank

ncclGroupEndInternal moves pending async jobs out of ncclAsyncJobs into
groupJob->asyncJobs via ncclIntruQueueTransfer, which empties ncclAsyncJobs.
The subsequent non-blocking-vs-blocking decision still inspected the now-empty
ncclAsyncJobs, so ncclCommInitRankConfig with config.blocking = 0 fell through
to the blocking path and waited for a missing rank instead of returning
ncclInProgress.

Inspect groupJob->asyncJobs (which owns the jobs after the move) when deciding
the path, so a non-blocking init with pending jobs returns ncclInProgress.

Co-authored-by: Pi Agent
Add a host-only microtest (rccl-UnitTestsMicro) that pins the branch
selection in ncclGroupEndInternal: a non-blocking group (config.blocking
== 0) carrying pending async init jobs must return ncclInProgress and
publish a group job on the communicator, rather than running the jobs
synchronously on the caller. That is the guarantee
ncclCommInitRankConfig(blocking=0) relies on so a caller can poll / time
out / abort when a peer rank never appears.

group-test.cc #includes the hipified src/group.cc so the file-static
launch helpers are reachable, and installs its group-specific behaviour
(the ncclCommSetAsyncError result) from the test fixture. The externals
group.cc references are satisfied by fakes organised by the module being
faked, not by a per-unit file:

  * comm_fakes.{cc,h} -- the controllable ncclCommSetAsyncError seam plus
    the comm/util globals and recorder/nvtx/profiler instrumentation that
    are real in the init unit (init.cc / utils.cc) and so cannot live in
    the shared nccl_fakes.cc.
  * collective_stubs.cc -- self-contained fail-loud floor for the
    collective launch/registration pipeline and the transport connect
    entry points the tested path never executes. Kept independent of the
    init binary's transport_stubs.cc, whose NVLS/P2P stubs now route
    through test-driven seam globals defined in the init test's own TUs.
  * param_redirect.h -- routes group.cc's NCCL_PARAM sites through
    g_loadParam (the shared mechanism p2p-test already uses), so the
    production ncclLoadParam signature is irrelevant.

Reverting the one-line fix in the hipified tree turns the two non-blocking
assertions red while the blocking-path contrast test stays green,
confirming the tests guard the fix and the setup is not over-broad.

Co-authored-by: Pi Agent
The non-blocking branch of ncclGroupEndInternal publishes
comm->groupJob = groupJob on every participating comm before creating the
background thread. If STDTHREADCREATE_GOTO fails, the fail: path runs
groupCleanup and deletes groupJob but never clears comm->groupJob. Since
comm->groupJob is only cleared elsewhere on success (init.cc, gated on
asyncError == ncclSuccess), a later ncclCommAbort -> ncclCommEnsureReady
-> ncclGroupJobAbort dereferences the freed group job (heap-use-after-free).

Previously this fail path was only live for preconnect groups; the
non-blocking init predicate fix makes it run for every non-blocking
ncclCommInitRankConfig, so the latent UAF became reachable.

Add groupJobUnlinkComms to null comm->groupJob for every comm that adopted
this exact groupJob before it is destroyed, walking both the async-jobs
list (an init/split comm is only reachable there, never on the
groupCommHead chains) and the groupCommHead chains. Scoped to the
ncclGroupEndInternal fail path only -- groupLaunch's own groupCleanup runs
on the background thread after a successful thread create, where
comm->groupJob is legitimately in use and must survive for the caller to
join/abort.

Cover it with a host-only microtest that arms a redefinable
STDTHREADCREATE_GOTO to force the thread-create failure and asserts
comm->groupJob is cleared. Red before the fix, green after.

Co-authored-by: Pi Agent
@mch
mch force-pushed the users/mahughes/rocm-30165-nonblocking-init branch from 4071f51 to fd4447c Compare August 27, 2026 23:03
@mch
mch merged commit 7621e6c into develop Aug 28, 2026
32 of 36 checks passed
@mch
mch deleted the users/mahughes/rocm-30165-nonblocking-init branch August 28, 2026 13:58
@systems-assistant

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

LoveDoLove pushed a commit to LoveDoLove-Forked-Projects/pytorch that referenced this pull request Oct 2, 2026
## Summary

Enable nccl2 communicator reconfigure on ROCm by dropping the `USE_ROCM` compile-time exclusion around `ncclCommRevoke` and `supportsReconfigure()`.

Version gates, defined once as `NCCL_HAS_COMM_REVOKE` in `nccl2/NcclApi.hpp`:

- CUDA: unchanged, NCCL 2.28.0
- ROCm: RCCL 2.30.7 (ROCm 10.1)

The constructor rejects `enable_reconfigure` on builds below the gate with `TORCH_CHECK(!enable_reconfigure || supportsReconfigure())`.

## Why RCCL 2.30.7 and not 2.30.4

RCCL 2.30.4 (ROCm 7.14 and 10.0) is the first RCCL with `ncclCommRevoke`, but it hangs on the missing-rank nonblocking init path that reconfigure uses (`ncclGroupEndInternal` still inspects emptied `ncclAsyncJobs` after jobs were moved to `groupJob->asyncJobs`). A reconfigure with an absent rank blocks there instead of honoring `ReconfigureOptions::timeout`. The fix is [ROCm/rocm-systems#10796](ROCm/rocm-systems#10796) (`7621e6c`, 2026-08-28), which ships in ROCm 10.1 / RCCL 2.30.7. On RCCL 2.30.4, `supportsReconfigure()` stays false and `enable_reconfigure=True` fails in the constructor, as before this PR.

2.30.7 is not an exact marker for the fix: the 2.30.7 sync reached rocm-systems `develop` on 2026-08-14 ([ROCm/rocm-systems#9924](ROCm/rocm-systems#9924)), so 10.1 nightlies built between 2026-08-14 and 2026-08-28 report 2.30.7 without it. No released ROCm is affected.

## Tests

`Nccl2FaultToleranceTest` skips as a class on ROCm when `torch.cuda.nccl.version() < (2, 30, 7)` and otherwise runs in full, including `test_reconfigure_timeout_is_retryable`. The class also needs 3 GPUs (`world_size = 3`). In upstream CI the ROCm 10.0 jobs skip the class; `rocm-preview` (ROCm 10.2.0a20260916, RCCL 2.30.7) runs it on 3+ GPU runners.

## Test plan

- [x] Local 3-GPU MI355X (`gfx950`), ROCm `10.2.0a20260916` / RCCL 2.30.7, at `e795687abe21`
  - `test/distributed/test_c10d_fault_tolerance.py`: 34 tests, OK, 3 expected Gloo skips
  - `Nccl2FaultToleranceTest.test_reconfigure_timeout_is_retryable`: 3/3 ok, about 5 s each
  - With the RCCL version patched to `(2, 30, 4)`, the class is skipped with "nccl2 reconfigure requires RCCL 2.30.7 or later"
- [x] Preprocessor check of `NCCL_HAS_COMM_REVOKE`: undefined for RCCL 2.30.4 and CUDA NCCL 2.27.7, defined for RCCL 2.30.7 / 2.31.2 and CUDA NCCL 2.28.0 / 2.30.4
- [x] Local 3-GPU MI355X against `rocm==10.1.0a20260902` / RCCL 2.30.7 at `ccbac3faaec`: timeout test ok in 4.647 s (hung on RCCL 2.30.4 until OS timeout)
- [x] CI on `e795687abe21`
- [x] [UTs pass on the ROCk CI for gfx942 with 8 GPU runner](https://github.com/ROCm/TheRock/actions/runs/36291283110/job/108541764587) (earlier head)
- [x] [UTs pass on ROCk CI for gfx950 with 8 GPU runner](https://github.com/ROCm/TheRock/actions/runs/36291292204/job/108652014622) (earlier head)

Pull Request resolved: pytorch#194774
Approved by: https://github.com/pragupta, https://github.com/kapilsh, https://github.com/jeffdaily
corey-derochie-amd pushed a commit that referenced this pull request Oct 8, 2026
…rank is missing (#10796)

## What

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

## Why / root cause

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

## Testing

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
corey-derochie-amd pushed a commit that referenced this pull request Oct 9, 2026
…rank is missing (#10796)

## What

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

## Why / root cause

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

## Testing

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
cyyever pushed a commit to BakeLab/pytorch that referenced this pull request Oct 11, 2026
## Summary

Enable nccl2 communicator reconfigure on ROCm by dropping the `USE_ROCM` compile-time exclusion around `ncclCommRevoke` and `supportsReconfigure()`.

Version gates, defined once as `NCCL_HAS_COMM_REVOKE` in `nccl2/NcclApi.hpp`:

- CUDA: unchanged, NCCL 2.28.0
- ROCm: RCCL 2.30.7 (ROCm 10.1)

The constructor rejects `enable_reconfigure` on builds below the gate with `TORCH_CHECK(!enable_reconfigure || supportsReconfigure())`.

## Why RCCL 2.30.7 and not 2.30.4

RCCL 2.30.4 (ROCm 7.14 and 10.0) is the first RCCL with `ncclCommRevoke`, but it hangs on the missing-rank nonblocking init path that reconfigure uses (`ncclGroupEndInternal` still inspects emptied `ncclAsyncJobs` after jobs were moved to `groupJob->asyncJobs`). A reconfigure with an absent rank blocks there instead of honoring `ReconfigureOptions::timeout`. The fix is [ROCm/rocm-systems#10796](ROCm/rocm-systems#10796) (`7621e6c`, 2026-08-28), which ships in ROCm 10.1 / RCCL 2.30.7. On RCCL 2.30.4, `supportsReconfigure()` stays false and `enable_reconfigure=True` fails in the constructor, as before this PR.

2.30.7 is not an exact marker for the fix: the 2.30.7 sync reached rocm-systems `develop` on 2026-08-14 ([ROCm/rocm-systems#9924](ROCm/rocm-systems#9924)), so 10.1 nightlies built between 2026-08-14 and 2026-08-28 report 2.30.7 without it. No released ROCm is affected.

## Tests

`Nccl2FaultToleranceTest` skips as a class on ROCm when `torch.cuda.nccl.version() < (2, 30, 7)` and otherwise runs in full, including `test_reconfigure_timeout_is_retryable`. The class also needs 3 GPUs (`world_size = 3`). In upstream CI the ROCm 10.0 jobs skip the class; `rocm-preview` (ROCm 10.2.0a20260916, RCCL 2.30.7) runs it on 3+ GPU runners.

## Test plan

- [x] Local 3-GPU MI355X (`gfx950`), ROCm `10.2.0a20260916` / RCCL 2.30.7, at `e795687abe21`
  - `test/distributed/test_c10d_fault_tolerance.py`: 34 tests, OK, 3 expected Gloo skips
  - `Nccl2FaultToleranceTest.test_reconfigure_timeout_is_retryable`: 3/3 ok, about 5 s each
  - With the RCCL version patched to `(2, 30, 4)`, the class is skipped with "nccl2 reconfigure requires RCCL 2.30.7 or later"
- [x] Preprocessor check of `NCCL_HAS_COMM_REVOKE`: undefined for RCCL 2.30.4 and CUDA NCCL 2.27.7, defined for RCCL 2.30.7 / 2.31.2 and CUDA NCCL 2.28.0 / 2.30.4
- [x] Local 3-GPU MI355X against `rocm==10.1.0a20260902` / RCCL 2.30.7 at `ccbac3faaec`: timeout test ok in 4.647 s (hung on RCCL 2.30.4 until OS timeout)
- [x] CI on `e795687abe21`
- [x] [UTs pass on the ROCk CI for gfx942 with 8 GPU runner](https://github.com/ROCm/TheRock/actions/runs/36291283110/job/108541764587) (earlier head)
- [x] [UTs pass on ROCk CI for gfx950 with 8 GPU runner](https://github.com/ROCm/TheRock/actions/runs/36291292204/job/108652014622) (earlier head)

Pull Request resolved: pytorch#194774
Approved by: https://github.com/pragupta, https://github.com/kapilsh, https://github.com/jeffdaily
corey-derochie-amd pushed a commit that referenced this pull request Oct 14, 2026
…rank is missing (#10796)

## What

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

## Why / root cause

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

## Testing

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
corey-derochie-amd pushed a commit that referenced this pull request Oct 14, 2026
…rank is missing (#10796)

## What

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

## Why / root cause

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

## Testing

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
corey-derochie-amd pushed a commit that referenced this pull request Oct 15, 2026
…rank is missing (#10796)

## What

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

## Why / root cause

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

## Testing

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
corey-derochie-amd pushed a commit that referenced this pull request Oct 15, 2026
…rank is missing (#10796)

`ncclCommInitRankConfig` with `ncclConfig_t.blocking = 0` could block
inside the
call instead of returning promptly. In the simplest case — a 2-rank
communicator where rank 1 never starts — rank 0's non-blocking init
should
return at once with `ncclInProgress` so the caller can poll, time out,
or abort.
On stock RCCL it instead waited for the missing rank and hung.

In `ncclGroupEndInternal` (`src/group.cc`), the pending async jobs are
moved out
of `ncclAsyncJobs` into `groupJob->asyncJobs` via
`ncclIntruQueueTransfer`, which
empties `ncclAsyncJobs`. The subsequent non-blocking-vs-blocking
decision still
inspected the now-empty `ncclAsyncJobs`, so a non-blocking init with
pending work
mis-selected the blocking path and ran the jobs synchronously on the
caller —
blocking forever when a peer rank is absent. The check must look at
`groupJob->asyncJobs`, which owns the jobs after the move.

The fix is a one-line change to inspect `groupJob->asyncJobs` in that
predicate.

- Ran the reproducer provided on the ticket on an 8xgfx950 machine: it
hangs on
  `develop` (reproducing the bug) and passes with this fix, returning
  `ncclInProgress` promptly.
- Added a host-only microtest (`GroupEndInternalTest` in
`rccl-UnitTestsMicro`)
that pins the branch selection: a non-blocking group carrying a pending
init
job returns `ncclInProgress` and publishes a group job on the
communicator,
while the blocking-group contrast test still runs synchronously and
returns
  `ncclSuccess`.
- Proved the guard is real: reverting the one-line fix in the hipified
tree
turns the two non-blocking assertions red and leaves the blocking test
green.
- Full `test/host/run_host_tests.sh all` pipeline passes
(`rccl-UnitTestsMicro`
  33 tests, plus the HostUnitTests and init micro-test binaries).
- The externals `group.cc` reaches are satisfied by module-organised
fakes
(`comm_fakes`, `enqueue_stubs`, reused `transport_stubs`,
`param_redirect.h`).

This branch also cherry-picks the `AMDSMI_FABRIC_DIRECT` /
`HIP_FABRIC_API`
init-target defines so the host micro-test binaries build cleanly.

JIRA ID : ROCM-30165
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.

5 participants