[https://nvbugs/6435642][fix] detect killed MPI executor workers#16338
Conversation
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
792a8c7 to
67d5ab1
Compare
|
/bot run --disable-fail-fast --stage-list "A10-PyTorch-1, A10-PyTorch-2" |
|
PR_Github #59029 [ run ] triggered by Bot. Commit: |
|
PR_Github #59029 [ run ] completed with state |
|
/bot run --disable-fail-fast |
|
PR_Github #59050 [ run ] triggered by Bot. Commit: |
📝 WalkthroughWalkthroughThe proxy now monitors local worker process liveness, workers report process identities during initialization, and fatal shutdown can occur when a process dies while its MPI future remains pending. Tests cover pidfd, procfs, identity filtering, and proxy health behavior. ChangesWorker liveness monitoring
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Worker as MPI worker leader
participant Proxy as GenerationExecutorProxy
participant Monitor as WorkerProcessMonitor
participant Process as Local worker process
Worker->>Proxy: ready status with worker identities
Proxy->>Monitor: register identities
Monitor->>Process: open pidfd or read procfs state
Process-->>Monitor: exit event or changed process state
Monitor-->>Proxy: dead worker identity
Proxy->>Proxy: record fatal error and call pre_shutdown()
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/unittest/executor/test_fatal_error_health_check.py (1)
523-624: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTwo coverage gaps in
TestWorkerProcessMonitor.Well-covered overall, but missing:
- A test where
os.pidfd_openraises a genericOSError(notProcessLookupError) — should fall back to procfs registration (lines 122-123 inworker_process_monitor.py), currently untested.- A test for the zombie-state (
"Z") death branch specifically (only start-time mismatch is exercised today, in both the pidfd post-open-validation andfind_dead_workerprocfs paths).As per path instructions, "suggest concrete list file names and whether coverage is sufficient, insufficient, or needs follow-up outside the PR": add these two cases to
tests/unittest/executor/test_fatal_error_health_check.py::TestWorkerProcessMonitor.🧪 Suggested additional tests
def test_pidfd_open_generic_oserror_falls_back_to_procfs(self, identity): if identity.start_time is None: identity = identity._replace(start_time=100) monitor = WorkerProcessMonitor() with patch.object(_monitor_mod.os, "pidfd_open", side_effect=OSError("EMFILE"), create=True): monitor.register([identity]) assert identity in monitor._procfs_identities def test_zombie_state_is_detected(self, identity): if identity.start_time is None: identity = identity._replace(start_time=100) monitor = WorkerProcessMonitor() with ( patch.object(_monitor_mod.os, "pidfd_open", None, create=True), patch.object(_monitor_mod, "_read_process_state", return_value=("Z", identity.start_time)), ): monitor.register([identity]) assert monitor.find_dead_worker() == identity🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/executor/test_fatal_error_health_check.py` around lines 523 - 624, Add two cases to TestWorkerProcessMonitor: verify a generic OSError from os.pidfd_open falls back to procfs registration and retains the identity in _procfs_identities, and verify a procfs state of "Z" is reported by find_dead_worker as the dead identity. Preserve the existing start-time setup and mocking patterns used by the neighboring tests.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/unittest/executor/test_fatal_error_health_check.py`:
- Around line 523-624: Add two cases to TestWorkerProcessMonitor: verify a
generic OSError from os.pidfd_open falls back to procfs registration and retains
the identity in _procfs_identities, and verify a procfs state of "Z" is reported
by find_dead_worker as the dead identity. Preserve the existing start-time setup
and mocking patterns used by the neighboring tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 00a922fa-3189-4f1f-8116-5cbf65686d17
📒 Files selected for processing (4)
tensorrt_llm/executor/proxy.pytensorrt_llm/executor/worker.pytensorrt_llm/executor/worker_process_monitor.pytests/unittest/executor/test_fatal_error_health_check.py
JunyiXu-nv
left a comment
There was a problem hiding this comment.
Overall LGTM. CC @QiJune for viz
|
PR_Github #59050 [ run ] completed with state
|
|
/bot run --disable-fail-fast --stage-list "DGX_B200-2_GPUs-PyTorch-1, DGX_B200-4_GPUs-PyTorch-Ray-1, Build-SBSA" |
|
PR_Github #59123 [ run ] triggered by Bot. Commit: |
|
PR_Github #59123 [ run ] completed with state
|
|
/bot run --disable-fail-fast --stage-list "DGX_B200-2_GPUs-PyTorch-1, DGX_B200-4_GPUs-PyTorch-Ray-1" |
|
PR_Github #59231 [ run ] triggered by Bot. Commit: |
|
PR_Github #59231 [ run ] completed with state |
|
/bot run --disable-fail-fast |
|
PR_Github #59284 [ run ] triggered by Bot. Commit: |
|
PR_Github #59284 [ run ] completed with state |
…is dead PR NVIDIA#16338 lets the proxy detect workers killed abruptly (MPI_Abort from the PyExecutor HangDetector, SIGKILL, the OOM killer) via OS process monitoring, and PR NVIDIA#15816 unblocks pending requests with a sticky EngineDeadError. But teardown still hangs: shutdown() blocks forever on f.result() for mpi4py futures that never complete after an abrupt kill, then on the result-dispatcher join (the worker that would send the shutdown sentinel is dead), then on joining the dead MPI pool. In CI the detection therefore only moves the client-side hang from generate() into LLM.__exit__, and hang-killed stages still burn pytest's full --timeout=3600 after the GPUs were freed at +300s (e.g. L0_Test-SBSA-Multi-GPU run 2744 GB300-4_GPUs-PyTorch-Post-Merge-2/3 on a post-NVIDIA#15816 main commit). Once the engine is known dead (_engine_dead, set by all detection paths): - give the futures one short collective grace instead of unbounded per-future result() waits, and skip the ones still pending - bound the result-dispatcher join (daemon thread; leaked, not awaited) - shut the owned MPI session down without waiting on the dead pool Orderly-shutdown semantics are unchanged when the engine is alive. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…is dead PR NVIDIA#16338 lets the proxy detect workers killed abruptly (MPI_Abort from the PyExecutor HangDetector, SIGKILL, the OOM killer) via OS process monitoring, and PR NVIDIA#15816 unblocks pending requests with a sticky EngineDeadError. But teardown still hangs: shutdown() blocks forever on f.result() for mpi4py futures that never complete after an abrupt kill, then on the result-dispatcher join (the worker that would send the shutdown sentinel is dead), then on joining the dead MPI pool. In CI the detection therefore only moves the client-side hang from generate() into LLM.__exit__, and hang-killed stages still burn pytest's full --timeout=3600 after the GPUs were freed at +300s (e.g. L0_Test-SBSA-Multi-GPU run 2744 GB300-4_GPUs-PyTorch-Post-Merge-2/3 on a post-NVIDIA#15816 main commit). Once the engine is known dead (_engine_dead, set by all detection paths): - give the futures one short collective grace instead of unbounded per-future result() waits, and skip the ones still pending - bound the result-dispatcher join (daemon thread; leaked, not awaited) - shut the owned MPI session down without waiting on the dead pool Orderly-shutdown semantics are unchanged when the engine is alive. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…le-device builds PR NVIDIA#16338 added `isinstance(self.mpi_session, MpiPoolSession)` in GenerationExecutorProxy. On single-device builds (ENABLE_MULTI_DEVICE=False) the MPI machinery in llmapi.mpi_session is not imported, so MpiPoolSession is not a usable type and the check raised 'isinstance() arg 2 must be a type', failing test_modeling_out_of_tree[True] on GB10. Guard on MpiPoolSession being a type before using it; worker-process monitoring only applies to the MPI pool case. Signed-off-by: Bowen Fu <5812640+BowenFu@users.noreply.github.com>
…is dead PR NVIDIA#16338 lets the proxy detect workers killed abruptly (MPI_Abort from the PyExecutor HangDetector, SIGKILL, the OOM killer) via OS process monitoring, and PR NVIDIA#15816 unblocks pending requests with a sticky EngineDeadError. But teardown still hangs: shutdown() blocks forever on f.result() for mpi4py futures that never complete after an abrupt kill, then on the result-dispatcher join (the worker that would send the shutdown sentinel is dead), then on joining the dead MPI pool. In CI the detection therefore only moves the client-side hang from generate() into LLM.__exit__, and hang-killed stages still burn pytest's full --timeout=3600 after the GPUs were freed at +300s (e.g. L0_Test-SBSA-Multi-GPU run 2744 GB300-4_GPUs-PyTorch-Post-Merge-2/3 on a post-NVIDIA#15816 main commit). Once the engine is known dead (_engine_dead, set by all detection paths): - give the futures one short collective grace instead of unbounded per-future result() waits, and skip the ones still pending - bound the result-dispatcher join (daemon thread; leaked, not awaited) - shut the owned MPI session down without waiting on the dead pool Orderly-shutdown semantics are unchanged when the engine is alive. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…is dead PR NVIDIA#16338 lets the proxy detect workers killed abruptly (MPI_Abort from the PyExecutor HangDetector, SIGKILL, the OOM killer) via OS process monitoring, and PR NVIDIA#15816 unblocks pending requests with a sticky EngineDeadError. But teardown still hangs: shutdown() blocks forever on f.result() for mpi4py futures that never complete after an abrupt kill, then on the result-dispatcher join (the worker that would send the shutdown sentinel is dead), then on joining the dead MPI pool. In CI the detection therefore only moves the client-side hang from generate() into LLM.__exit__, and hang-killed stages still burn pytest's full --timeout=3600 after the GPUs were freed at +300s (e.g. L0_Test-SBSA-Multi-GPU run 2744 GB300-4_GPUs-PyTorch-Post-Merge-2/3 on a post-NVIDIA#15816 main commit). Once the engine is known dead (_engine_dead, set by all detection paths): - give the futures one short collective grace instead of unbounded per-future result() waits, and skip the ones still pending - bound the result-dispatcher join (daemon thread; leaked, not awaited) - shut the owned MPI session down without waiting on the dead pool Orderly-shutdown semantics are unchanged when the engine is alive. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…etcher Move _isinstance_transparent_shim to _session_utils and use it at the prefetcher's seam install too: when the prefetcher owns the seams (reuse disabled or manual opt-in), it had the same hazard the shim removes for reuse — a bare function at the seam makes any isinstance(x, MpiPoolSession) against the patched module attribute raise TypeError (the NVIDIA#16338 breakage class). Adds an install-side test asserting the patched attribute stays a real type that answers for the real class. Signed-off-by: qgai <qgai@nvidia.com>
…k robust to test-infra patching tests/test_common/session_reuse.py (NVIDIA#15777) rebinds the executor proxy module's MpiPoolSession attribute to a pool-caching factory function. The worker-process-monitor registration added in NVIDIA#16338 does isinstance(self.mpi_session, MpiPoolSession) against that module global, which raises 'TypeError: isinstance() arg 2 must be a type' whenever the session-reuse harness is active (e.g. the DGX AutoDeploy CI stages). The factory still hands out real MpiPoolSession instances, so check against a private un-patched alias of the class instead; this keeps worker-process monitoring active for cached pools. Signed-off-by: handongl <15242213+HandongLi-01@users.noreply.github.com>
…robust to test-infra patching tests/test_common/session_reuse.py (NVIDIA#15777) rebinds the executor proxy module's MpiPoolSession attribute to a pool-caching factory that returns delegating wrapper objects. The worker-process-monitor registration added in NVIDIA#16338 does isinstance(self.mpi_session, MpiPoolSession) against that module global, which raises 'TypeError: isinstance() arg 2 must be a type' whenever the session-reuse harness is active (e.g. the DGX AutoDeploy CI stages). Neither the module global (a function under patching) nor the returned object (a wrapper, not an MpiPoolSession subclass) is reliable for isinstance checks, so record pool-session mode from the construction path instead: the pool-constructor branch always yields a (possibly wrapped) pool session, and an externally provided session is checked against an un-patched alias of the class. This both fixes the TypeError and keeps worker-process monitoring active for cache-reused pools. Signed-off-by: handongl <15242213+HandongLi-01@users.noreply.github.com>
What changed
MpiPoolSessionworkers with pidfds, with a/procidentity check as fallback.GenerationExecutorProxy.check_health()and the background error monitor, then initiate the existing nonblocking shutdown path.Why
NVBug 6435642 tracks a failure mode where the
mpi4py.futures.serverprocess is killed by OOM/SIGKILL while its future remains pending and no error reaches the proxy error queue. The parent TRT-LLM/Dynamo worker therefore remains alive and continues reporting healthy even though inference capability is lost.PR #12718 added the fatal-error propagation and shutdown foundation, but hard-killed MPI workers can bypass its future/error-queue checks. Active OS process-liveness monitoring closes that gap for the locally spawned
MpiPoolSessiontopology.Impact
When an engine-core MPI worker is killed,
check_health()now returnsFalse, records a fatal error containing the worker rank and PID, and allows embedding runtimes such as Dynamo to remove and recover the failed worker instead of routing requests that will hang.Validation
pytest -q tests/unittest/executor/test_fatal_error_health_check.py --confcutdir=tests/unittest/executor -k 'not TestOpenAIHealthEndpoint'str | Noneannotation inscripts/check_test_list.py.NVBug: 6435642
Summary by CodeRabbit
New Features
Tests