Which generation failures the run survives, and which end it
PR #3591 ·
checked against 3d9ce210 ·
see also what the review changed
Six pages, one question each — this one: which failures recover.
Two things decide the outcome, and you need both to predict it: what the ledger already thought of the shard when the weight sync began, and how the sync then went. There is no separate state for the refit. Its fate is written entirely in the shard's state, plus one hard rule — recover at most once per sync. Below is every combination.
On
nccl_reshard there is a second requirement: the fault must land outside the
bulk transfer. An abort mid-transfer orphans work on the trainers' own devices, and
no rebuild on a device in that state can succeed — so naming the shard no longer helps. Since
9a87c0123 the run detects this and ends in seconds with a named cause instead of
wedging. That turns both crash cells below into transport splits, not just the hang cell.
The same grid as a table, with what does the work
| Gen worker | Refit | Outcome | What actually happens |
|---|---|---|---|
| HEALTHY | succeeds | continues | The ordinary step. The reconcile at the top is a comparison, not a rebuild — each synchronizer remembers the membership it last built with. |
| HEALTHY | hangs | run ends | The watchdog aborts, but the probe finds every process alive and the ledger holds no suspect — so there is nobody to drop, no smaller group to rebuild, and the retry is never reached: recovery raises first. The code deliberately refuses to guess: "condemning the wrong one costs a healthy shard and leaves the real culprit in the rebuilt communicator — the same hang, one shard smaller." The error names the suspect list so you can see why it gave up. |
| HEALTHY | crashes | transport | RayActorError, then the post-abort probe sees the dead process, record_actor_death marks it DEAD, the group is rebuilt without it, and the refit is retried once. On packed broadcast that recovery completes. On nccl_reshard it never gets that far: the aborted bulk transfer has already orphaned work on the trainers' devices, sync_stream_within tags the abort, and the controller refuses the recovery and ends the run in seconds — see the box below. A death that lands between syncs recovers on both transports, because the shard is dropped before the next collective starts (the DEAD row). |
| SUSPECT | succeeds | continues | Common and harmless. A wedged engine can still take part in a collective; it just cannot generate. It keeps timing out requests and reaches DEAD on its own, after which it lands in the DEAD row. |
| SUSPECT | hangs | transport | The abort is evidence that something stopped participating; SUSPECT says which. Exactly one suspect and condemn_silent_participant marks it DEAD, then reconcile(force=True) shrinks the group and retries. Works on packed broadcast; ends the run on nccl_reshard — see the box below. With two or more suspects, or none, it refuses and ends. |
| SUSPECT | crashes | transport | The process death is visible, so no attribution is needed. Same split as HEALTHY × crashes: broadcast recovers, reshard mid-refit ends the run. |
| DEAD | any | continues | Excluded before the collective started, so it cannot affect this refit at all. Stays out until #3592 restarts it. |
| Not about one generation shard | |||
| a trainer dies | crashes | run ends | No remediation, by design. Trainers are never excluded — dropping one would change the parallelism the model was sharded for. This stack is about generation shards. |
| a 2nd shard fails | during the retry | run ends | Recover at most once: "a second failure is a real fault, not a membership problem, and retrying forever would recreate the wedge this exists to remove." The second shard is not even condemned, because the probe ran before the retry. |
| too few left | — | run ends | raise_if_exhausted once serving shards drop below min_healthy_shards. The right outcome: a run that cannot generate should stop. |
nccl_reshard.
Aborting a communicator does not retire work already sitting on a CUDA stream — and on the reshard
path those orphaned sends are on the trainers' devices, not the failed shard's. So nothing
you do to the remote rank retires them, and torch.cuda.synchronize() never returns.
Job 6485245 measured it on 4×GB200: both policy workers parked in synchronize
1801 s after their own abort had already logged. The author eliminated every
alternative by measurement: killing the shard outright (job 6523731 — trainers wedged anyway),
pinning the release to the caller's CUDA device (job 6582457 — still stuck, with a SIGKILLed peer),
and standing the deadline down on a confirmed death (lost the race). What landed instead
(9a87c0123): sync_stream_within tags its abort with a second marker,
the controller refuses to enter the recovery on that tag, and the run ends with a message saying
this is the known limit of the transport — exit in ~10 s for a killed shard, ~90 s for a frozen
one, against 1800 s wedges before. A plain abort without the tag still recovers, and a unit test
pins that boundary.
Does it work?
The abort is measured: on 2×A6000, a survivor blocked in a collective was released
0.15 s after another thread called abort(). The reshard limit is
measured too — job 6485245 for the stuck synchronize, and job 6523731 as the disproof of killing
the shard.
What is not measured is the whole grid end to end in CI. The recovery harness needs 3 GPUs and both lanes have 2, so those variants skip; the rows above were exercised by hand on larger hardware. The lane now registers the reshard-plus-frozen corner and makes a skip visible instead of letting it read as a pass, so at least the gap is legible.