How a dead generation shard stops killing the run
PR #3591 ·
part 3/4 of #3454 ·
all links pinned at ab0ffe2f
Six pages, one question each — this one: what happens, in order.
#3590 gave the fleet a ledger: every shard has a state, and a dead one stops receiving traffic. That is enough to keep generating. It is not enough to keep training, because the weight refit is a collective — and a collective does not care that you stopped talking to a rank. Every remaining rank blocks inside NCCL waiting for one that will never arrive: no exception, no CPU burn, and Ray still reporting every actor healthy. This PR notices that from inside the stuck worker, breaks it, and rebuilds the communicator over whoever is left.
The four things this PR had to invent
Three of these are new files or new interface methods; the fourth is a rule about the ledger from #3590 that is easy to get backwards.
| Abstraction | What it is | Why it had to exist |
|---|---|---|
RefitAbortWatchdognew file |
A thread armed around the refit inside each worker. If the deadline passes it calls abort() on every group it was handed, and records that it fired. |
The stuck thread is inside NCCL and cannot report on itself, and the controller cannot see the difference between a slow refit and a wedged one. Somebody local has to hold a clock. |
abort()new method |
Tears down the communicator with ncclCommAbort and drops the rendezvous store. Safe to call twice, and safe on a group that never built a communicator. |
destroy is a collective every rank must call. A dead rank cannot call it. abort works whether or not the peers are alive, which makes it the only safe choice on a path that exists to handle dead peers. |
plan_refit_membershipnew file, pure |
Takes the set of surviving shards and returns the whole rank layout: new world size, and each survivor's new offset, compacted to a contiguous prefix with no holes. | The reshard destination mesh is arange(offset, offset + n), so a gap does not error — it silently misaligns every parameter. This is the part that must be exactly right and the part you cannot exercise without ≥3 GPUs, so it was split out as plain Python with no Ray in it. |
reconcile_communicatornew interface method |
One hook per weight-sync transport. Returns True rebuilt, False nothing was absent, None this transport owns no membership. |
Only two of the seven transports own an NCCL world. The other five inherit a no-op, so the controller can say "this transport cannot recover that way" instead of guessing. Adding an eighth NCCL transport costs one method. |
SUSPECT (failing probes, not yet condemned) and STALE (reloaded, holding old
weights) are both withheld from traffic while their processes are alive, and both join a refit
normally. Using the serving set would abort a run on one failed probe — and would abort it precisely
when a STALE shard is waiting to be refit, which is the recovery, not the failure. Hence
absent_shards()
over {DEAD, RESTARTING, RETIRED} only.
Does it work?
The abort itself is measured. On 2×A6000, with a peer SIGKILLed
mid-broadcast, a survivor blocked in the collective was released 0.15 s after
another thread called abort(). That is the one number on this page that came from
hardware, and it pins the only part that was genuinely uncertain — that ncclCommAbort
frees a thread already inside a collective.
End to end, nobody has watched it recover in CI. The recovery harness needs 3 GPUs
(2 generation shards + 1 trainer) and both CI lanes have 2, so on this PR's own green run every one
of the seven registered lines printed [recovery] SKIP: … found 2. and exited — seven
lines in 0.79 s on
H100 and
0.48 s on GB200,
both reported PASSED. The feature was validated by hand on ≥3-GPU hardware — the 7-digit job IDs in
the harness comments are an internal scheduler's, not GitHub Actions' — so the evidence exists; it
just is not in CI, and a green lane currently says nothing about shard-death recovery either way.
splitShare is set, and it defaults to 0
(init.cc:2196),
while ncclCommAbort sets only the parent's
(:2915). So step 4
does not release a rank blocked on a child, and the run wedges exactly as it did before — the
watchdog fires and nothing happens. Narrow (it needs the Python transfer path and a multi-member
replica group), but it is step 3 coming back after step 4 was supposed to end it. Raised on the PR
with a two-line fix: hand the children to the same cleanup before aborting the parent.