fix: recover stuck workers when an MPExecutor subprocess dies#82
Merged
Conversation
MPExecutor.run() waited for the subprocess result with an unbounded res_q.get(), so a child that died without replying (OOM kill, CUDA abort) left the parent blocked forever while holding the executor lock: the task never reached a terminal state and the worker stayed BUSY. Poll the result queue with a timeout and check subprocess liveness, surfacing a dead child as a retryable error so the worker recovers and the task can be retried. Also restart the subprocess after an unexpected (non-ExecutionError) failure, since a corrupted engine's GPU memory is only reclaimed once the owning process exits; controlled errors keep the subprocess warm. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
timzsu
requested changes
Jun 22, 2026
8 tasks
A controlled ExecutionError raised with retryable=True by an inner executor was re-raised as non-retryable in the parent, since the flag was never carried in the subprocess error payload. Record it alongside is_execution_error and restore it when re-raising, so retry routing sees the executor's original intent. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Assert the controlled-error path stays non-retryable alongside the existing retryable-propagation test, and set retryable explicitly in the executor-init-failure payload so it matches the run-failure payload shape. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
This was referenced Jun 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
A worker would get stuck
BUSYindefinitely when an executor running insideMPExecutor(e.g. vLLM) died hard — an out-of-memory kill, a CUDAabort(), or a native segfault.MPExecutor.run()waited for the subprocess's result with an unboundedres_q.get(), but a child that dies that way never posts a result, so the parent thread blocked forever while holding the executor lock.run()never returned, the runner'sfinally: set_idle()never ran, and the worker stayedBUSY(withstale=false, so no health/staleness path reclaimed it) — silently unable to accept further work.Changes
src/worker/executors/mp_executor.py— wait for the subprocess result with liveness-checked polling instead of an unbounded blocking get, so a dead child surfaces as a retryable failure rather than a hang; restart the subprocess after an unexpected failure to drop a possibly-corrupted engine; share the teardown logic across the run, await, and cleanup paths.tests/worker/test_mp_executor_lifecycle.py— cover the dead-child, unexpected-failure, and controlled-error paths.Design
run()now classifies how a task ended and reacts accordingly: a child that died silently is reported as a retryable error (a hard crash is usually transient, e.g. OOM under contention); a controlledExecutionErrorkeeps the subprocess warm to avoid a needless engine reload; an unexpected exception restarts the subprocess, since a corrupted engine's GPU memory is only reclaimed once the process exits.Test Plan
End-to-end on a local root + single GPU worker on freshly built images carrying this branch, using a fault-injection repro: submit a vLLM inference task, then
kill -9theMPExecutorchild subprocess mid-run to faithfully simulate an OOM kill / CUDA abort, and observe the worker and task state before and after the fix.Test Result
End-to-end: before the fix, killing the child left
worker=BUSYandtask=DISPATCHEDfor the full 120s observation window. After the fix, the dead child is reported asmp(vllm) subprocess exited (code -9), the task fails retryably, is re-dispatched, and runs toDONE, and the worker returns toIDLE.Pre-submission Checklist
pre-commit runon the changed files and fixed any issues.[BREAKING]and described migration steps above.