Which process owns each piece of the refit recovery
PR #3591 ·
all links pinned at ab0ffe2f
Six pages, one question each — this one: which process owns what.
The recovery machinery is spread across three kinds of process, and the piece that does the actual rescuing lives in the least obvious one. The ledger of shard health and the decision to rebuild are both in the driver, with no GPU. The communicators are in the GPU workers. The watchdog that breaks the hang is a thread inside each GPU worker process, created fresh for every refit and owned by nothing — which is why you cannot find it by looking for a long-lived object. The router, despite being the thing that routes traffic away from a dead shard, takes no part in the refit at all.
Why the watchdog cannot live in the driver
This is the design choice that makes the rest work, and it is worth being explicit about because the driver is where you would naturally put a timeout.
The driver cannot tell a slow refit from a wedged one. From outside, both look like
ray.get not returning yet, and a large model's refit legitimately takes seconds. The
driver also cannot break the hang even if it guessed right: freeing a rank stuck in NCCL means
calling abort() on that rank's own communicator object, which only exists
inside that worker's process.
And the stuck thread cannot rescue itself — it is blocked inside a NCCL call and will not run
another line of Python until that call returns. So the only place a clock can usefully sit is
a second thread in the same process: close enough to touch the communicator,
separate enough to still be running. That is what
RefitAbortWatchdog
is, and why it is created at the call site rather than owned by anything.
| Question | Answer |
|---|---|
| Does it live in the driver? | No. Six worker-side sites, four training and two generation. |
Does VllmGeneration own it? | No. That object is driver-side and holds the membership, not the watchdog. |
| Does the generation Ray actor own it? | Not quite. It is one level deeper — in the vLLM TP worker process, inside VllmInternalWorkerExtension. |
| Does the router own it? | No. The router is not in the refit path at all. |
| What holds a reference? | Nothing. It is a with-block local, joined when the block exits. |
Does it work?
The one thing this arrangement depends on is that a second thread calling
ncclCommAbort can actually free a thread already blocked inside a collective. That was
measured: on 2×A6000, with a peer SIGKILLed mid-broadcast, the blocked survivor was
released 0.15 s after another thread called abort(). If that had not
worked, the in-process thread design would have bought nothing.
End to end, no CI job has watched this recover. The recovery harness needs 3 GPUs and both lanes
have 2, so on the PR's own green run every registered variant printed
[recovery] SKIP: … found 2. and exited. The feature was exercised by hand on larger
hardware, so the evidence exists — it is just not in CI, and a green lane says nothing about it.
abort() before the parent goes.