Skip to content

[TRTLLM-13409][fix] hard-kill all ranks when one rank's executor loop crashes - #16592

Open
JunyiXu-nv wants to merge 9 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-rank-crash-hard-kill
Open

[TRTLLM-13409][fix] hard-kill all ranks when one rank's executor loop crashes#16592
JunyiXu-nv wants to merge 9 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-rank-crash-hard-kill

Conversation

@JunyiXu-nv

@JunyiXu-nv JunyiXu-nv commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Dev Engineer Review

  • Implemented multi-rank “rank crash hard-kill” escalation with a configurable grace period (TLLM_RANK_CRASH_HARD_KILL_GRACE), where invalid/unset values default to 10s and negative values disable world hard-kill.
  • Ensured escalation logic is best-effort and non-throwing (does not raise if kill propagation fails/cancellation occurs), preserving the original executor exception for debugging.
  • Added a cancellable RankCrashKillWatchdog and integrated it into PyExecutor._event_loop_wrapper so the watchdog is armed before executor cleanup and the hard-kill is attempted afterward, preserving the original fire deadline.
  • Added unit coverage for single-rank exemption, grace timing/delay, disabling/invalid env handling, non-raising behavior, watchdog cancel behavior, deadline preservation, and crash vs clean-exit wiring.
  • No configuration or test-list files were changed.

QA Engineer Review

Test changes (unit)

  • tests/unittest/_torch/executor/test_hang_detector_kill.py
    • Added/extended coverage for:
      • test_rank_crash_kill_single_rank_is_noop
      • test_rank_crash_kill_fires_for_multi_rank
      • test_rank_crash_kill_sleeps_grace_before_kill
      • test_rank_crash_kill_disabled_by_negative_grace
      • test_rank_crash_kill_invalid_grace_uses_default
      • test_rank_crash_kill_never_raises
      • test_watchdog_kills_while_caller_blocks
      • test_watchdog_not_armed_for_single_rank
      • test_watchdog_not_armed_when_disabled
      • test_watchdog_cancel_prevents_the_kill
      • test_kill_keeps_original_deadline_on_handover
      • test_event_loop_wrapper_kills_world_on_crash
      • test_event_loop_wrapper_kills_world_when_cleanup_raises
      • test_event_loop_wrapper_kills_world_when_watchdog_cannot_arm
      • test_event_loop_wrapper_no_kill_on_clean_exit
      • test_event_loop_wrapper_no_kill_when_loop_raises_after_shutdown
      • test_event_loop_wrapper_no_kill_when_enclosing_context_manager_raises
    • CI list coverage:
      • Included in tests/integration/test_lists/test-db/l0_sanity_check.yml (module unittest/_torch/executor/test_hang_detector_kill.py)
  • tests/unittest/executor/test_proxy_fast_death.py
    • Modified:
      • test_pool_session_shutdown_never_blocks_after_release
    • CI list coverage:
      • Included in tests/integration/test_lists/test-db/l0_a10.yml (module unittest/executor/test_proxy_fast_death.py)

Verdict

  • Sufficient (relevant unit test modules are already covered in CI test-db via the test list entries above).

Description

When a rank's executor loop dies on an exception, the rank stops participating in collectives but nothing tells its peers: every peer blocks in its next collective until its own HangDetector fires 300s later, and the whole multi-GPU test session burns that long for an error that was already known (the A4 AutoDeploy catches are this signature: peers crash, the survivor wedges in ADP until the 300s backstop).

Single-rank worlds are exempt (no peers to unblock), and the kill helper never raises (it runs in a finally where an exception would mask the loop's original error).

Test Coverage

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

@JunyiXu-nv
JunyiXu-nv requested review from a team as code owners July 20, 2026 05:22
@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-feat-rank-crash-hard-kill branch from 03e5e9e to cf7fdc1 Compare July 20, 2026 05:23
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds configurable rank-crash hard-kill handling with a grace period, optional watchdog, and defensive error handling. Executor crash cleanup now starts the watchdog before cleanup and invokes direct world termination afterward, with tests covering policy and ordering.

Changes

Rank crash handling

Layer / File(s) Summary
Configurable rank-crash kill policy
tensorrt_llm/_torch/pyexecutor/hang_detector.py
Adds environment-based grace parsing, optional disabling, world-level hard-kill behavior, and daemon watchdog startup.
Executor crash integration
tensorrt_llm/_torch/pyexecutor/py_executor.py
Tracks event-loop crashes, starts the watchdog before cleanup, and invokes direct hard-kill handling after cleanup.
Kill behavior validation
tests/unittest/_torch/executor/test_hang_detector_kill.py, tests/unittest/executor/test_proxy_fast_death.py
Tests grace handling, watchdog applicability and cancellation, kill error suppression, crash cleanup ordering, exception preservation, clean exits, and shutdown state setup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PyExecutor_event_loop_wrapper
  participant RankCrashKillWatchdog
  participant executor_loop_cleanup
  participant hard_kill_on_rank_crash
  PyExecutor_event_loop_wrapper->>RankCrashKillWatchdog: Arm after event-loop crash
  PyExecutor_event_loop_wrapper->>executor_loop_cleanup: Execute local cleanup
  RankCrashKillWatchdog->>hard_kill_on_rank_crash: Trigger after grace period
  PyExecutor_event_loop_wrapper->>hard_kill_on_rank_crash: Invoke after cleanup with original deadline
Loading

Suggested reviewers: schetlur-nv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change and follows the required [ticket][type] summary format.
Description check ✅ Passed The description explains the problem and solution, and the checklist is checked, but Test Coverage is left blank.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60270 [ run ] triggered by Bot. Commit: cf7fdc1 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60270 [ run ] completed with state SUCCESS. Commit: cf7fdc1
/LLM/main/L0_MergeRequest_PR pipeline #48629 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60547 [ run ] triggered by Bot. Commit: cf7fdc1 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60547 [ run ] completed with state FAILURE. Commit: cf7fdc1
/LLM/main/L0_MergeRequest_PR pipeline #48859 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60580 [ run ] triggered by Bot. Commit: cf7fdc1 Link to invocation

@nv-xtf nv-xtf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — One non-blocking question inline about kill reachability if cleanup blocks on a PP send handle.

Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60580 [ run ] completed with state FAILURE. Commit: cf7fdc1
/LLM/main/L0_MergeRequest_PR pipeline #48890 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-feat-rank-crash-hard-kill branch from cf7fdc1 to ec5a475 Compare July 22, 2026 03:53
@JunyiXu-nv
JunyiXu-nv requested review from a team as code owners July 22, 2026 03:53
@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-feat-rank-crash-hard-kill branch from ec5a475 to c0c1a43 Compare July 22, 2026 07:34
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62397 [ run ] triggered by Bot. Commit: 50eeaca Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62397 [ run ] completed with state SUCCESS. Commit: 50eeaca
/LLM/main/L0_MergeRequest_PR pipeline #50558 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Pipeline #50558 — one timeout, on a path this PR does touch

Now that the scaffolding blocker is waived, this run got a clean signal. One root failure across 24 stages (reported twice, as the wrapper and the inner test):

[B300-PyTorch-1] test_unittests_v2[unittest/_torch/executor/test_overlap_scheduler.py::
                 test_overlap_scheduler_consistency[no_reuse-python_scheduler-TRTLLMSampler]]
                 results-timeout.xml -> "Test terminated unexpectedly"

I am not claiming this is unreachable, because it isn't. test_overlap_scheduler.py exercises the overlap scheduler loop in py_executor.py, and this PR modifies exactly that loop — including the _event_loop_completed sentinel at its normal-exit break. The failure is a timeout, which is the shape a sentinel bug would produce. Given this PR previously carried a real blocker of mine on the same file, that deserved checking rather than dismissal.

Measured ambient rate:

Stage-runs where test_overlap_scheduler_consistency executed 48
Stage-runs where it failed 1 — build #50609

Build #50609 is an unrelated PR, and the scan window has moved past #50558, so this count contains no data from this PR at all. That makes it an independent occurrence of the same test timing out without this change present — which is the evidence I wanted before attributing the failure to ambient noise rather than to the sentinel.

That is sufficient to re-run rather than to go re-read the sentinel logic. If it recurs here, I will treat that as a suspected regression in this PR and investigate the overlap loop directly instead of re-running again.

Re-running.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62510 [ run ] triggered by Bot. Commit: 50eeaca Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62510 [ run ] completed with state SUCCESS. Commit: 50eeaca
/LLM/main/L0_MergeRequest_PR pipeline #50658 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Pipeline #50658 — the overlap-scheduler timeout did not recur

I said on the previous run that a recurrence of test_overlap_scheduler_consistency would mean I stop re-running and go read the sentinel logic instead. Recording the outcome of that test: it did not recur. #50658 has zero failing testcases across all 14 stages that uploaded.

What failed instead: eight stages were cut mid-test with one unfinished test each, and all eight are multi-GPU:

DGX_B200-4_GPUs-{1,2,3}, DGX_B200-8_GPUs-{1,2,4},
DGX_B300-4_GPUs-1, DGX_B200-4_GPUs-AutoDeploy-1

One unfinished test apiece across a coherent stage group is the signature of a single cancellation, not eight independent failures.

I checked whether this PR's own hard-kill caused it, since "multi-GPU stages die simultaneously" is exactly what a mis-firing peer-rank kill would look like, and this PR is the one that adds that kill. It did fire — once — and here is where:

TEST: unittest._torch.executor.test_hang_detector_kill::test_watchdog_kills_while_caller_blocks   status=PASS
  [TRT-LLM] [E] [_torch] Executor loop crashed on this rank; hard-killing all 2 ranks in 0s
  (peers cannot make progress without this rank). Set TLLM_RANK_CRASH_HARD_KILL_GRACE=-1 to disable.

That is this PR's own regression test exercising the feature, on a stage (B300-PyTorch-1) that is not among the eight cut ones, and the test passed. So the kill is behaving as designed and is not the cause of the cancellation.

For the record on this PR's recent history, since the pattern matters more than any single run: #50428 aborted with no test failures, #50558 failed on one timeout now shown to be ambient (1 failure in 48 runs, the other on an unrelated PR), and #50658 has zero test failures. Three consecutive pipelines have ended without a single failing test attributable to this change.

Re-running.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62530 [ run ] triggered by Bot. Commit: 50eeaca Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62530 [ run ] completed with state SUCCESS. Commit: 50eeaca
/LLM/main/L0_MergeRequest_PR pipeline #50679 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Pipeline #50679 — one timeout, in code with no path to this change

One root failure across 19 stages (reported twice, as wrapper and inner test):

[DGX_H100-2_GPUs-PyTorch-Others-2]
  test_unittests_v2[unittest/_torch/multi_gpu/test_linear.py::test_row_linear[2-unbalanced]]
  results-timeout.xml -> "Test terminated unexpectedly"

Unreachable from this PR. tests/unittest/_torch/multi_gpu/test_linear.py imports torch, mpi4py.MPI, tensorrt_llm.quantization.utils.fp4_utils and the autotuner — and contains zero references to py_executor, PyExecutor, GenerationExecutor or LLM(. It exercises a tensor-parallel Linear module directly over raw MPI. This PR changes hang_detector.py and py_executor.py; neither is on that path.

Ambient rate — thin, and I will not dress it up. Across the last 60 pipelines this parametrisation appears in only 3 stage-runs, with 0 failures. That is too small a sample to call anything, and it contains no independent sighting of this failure. The weight here rests entirely on the unreachability argument above, not on the rate.

Pattern across this PR's recent runs, which I think is the more informative signal: #50428 aborted with no test failures; #50558 failed on test_overlap_scheduler_consistency (since measured at 1-in-48, with the other occurrence on an unrelated PR); #50658 had zero failing testcases with eight multi-GPU stages cancelled; #50679 fails on this. Four consecutive pipelines, four different causes, none reproducing. A defect in this change would reproduce the same failure.

For context on the environment: I measured that roughly 1 in 5 recent pipelines has at least one test killed by the harness timeout, spread across unrelated areas, and roughly 1 in 6 ends with zero failing tests at all.

Re-running.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62649 [ run ] triggered by Bot. Commit: 50eeaca Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62649 [ run ] completed with state SUCCESS. Commit: 50eeaca
/LLM/main/L0_MergeRequest_PR pipeline #50789 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Pipeline #50789 — stale test file on this branch, not a regression

I said on the previous run that a recurrence in test_row_linear[2-unbalanced] would mean I stop re-running and investigate. It recurred, so here is the investigation.

The two failures are not the same failure:

pipeline mode
#50679 results-timeout.xml -> "Test terminated unexpectedly"
#50789 Failed: DID NOT RAISE <class 'AssertionError'> at test_linear.py:315

The second is behavioural, and it is explained by this branch's base, not by its code:

$ contents/tests/unittest/_torch/multi_gpu/test_linear.py?ref=50eeaca72c
290:        with pytest.raises(AssertionError):
315:        with pytest.raises(AssertionError):     <- exactly where CI's traceback points

$ contents/tests/unittest/_torch/multi_gpu/test_linear.py?ref=main
(no match)

Those lines were removed from main by 7982aa9 (#16844)"Test-only fix — drop the if hidden_size % 2 != 0: with pytest.raises(...)", tracked as https://nvbugs/6501376. This branch predates that fix, so it is running the known-bad version of a test it does not touch.

So the correct action is a rebase onto current main, not another /bot run. I am not re-triggering, because re-running would reproduce the same stale test.

For completeness on the earlier timeout in #50679: that is a different mode and remains unexplained by this; the broader question of whether test_row_linear is also flaky under timeout is separate from the assertion failure above.

Worth noting the general lesson, since it applies to any long-lived branch here: CI runs the branch's own copy of files the branch does not modify, so a stale base means stale tests — including tests that main has since fixed.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Confirming datapoint for the stale-base diagnosis above. Scanning the last 120 pipelines for this test across every stage that ran it:

test_row_linear (any param): ran=7 failed=1
   FAIL (50789, 'results-DGX_H100-2_GPUs-PyTorch-Others-2.tar.gz')   <- this PR

One failure in seven runs, and it is ours. That is precisely the pattern the stale base predicts — other PRs are based on a main that carries #16844's fix and pass; this branch carries the pre-fix copy and fails. It is the opposite of an ambient flake: an ambient flake would show failures spread across unrelated PRs, which is what I measured for the earlier test_overlap_scheduler_consistency (1-in-48, the other occurrence on an unrelated PR).

So: rebase, don't re-run.

… crashes

When a rank's executor loop dies on an exception, the rank stops
participating in collectives but nothing tells its peers: every peer
blocks in its next collective until its own HangDetector fires 300s
later, and the whole multi-GPU test session burns that long for an
error that was already known (the A4 AutoDeploy catches are this
signature: peers crash, the survivor wedges in ADP until the 300s
backstop).

Escalate at error time instead: after the loop's local cleanup has
woken rank-local waiters, hard-kill the world via the ST-1 propagation
path. A grace period (TLLM_RANK_CRASH_HARD_KILL_GRACE, default 10s,
negative disables) lets the cleaner error paths win the race first --
the stashed error reaches rank-local response waiters, the init-phase
ready handshake returns the real exception to the proxy, and the worker
future completes with the original error -- so the client reports the
actual failure rather than a bare worker death.

Single-rank worlds are exempt (no peers to unblock), and the kill
helper never raises (it runs in a finally where an exception would mask
the loop's original error).

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…blocks or raises

The kill sat after _executor_loop_cleanup() in the finally block, so it
was skippable in exactly the situations it exists for: cleanup blocking
without bound (unbounded wait() on a PP send handle wedged by the crash)
or cleanup raising (aborting the finally before the kill). Either way
peers fall back to burning 300s in their own HangDetectors.

Arm a daemon watchdog thread BEFORE cleanup that fires the kill at
crash + grace regardless of cleanup progress, and nest the post-cleanup
kill in its own finally so a raising cleanup cannot skip it.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…pool-session shutdown test

The test builds MpiPoolSession via __new__ (a real spawn is neither
needed nor wanted), but MpiPoolSession.shutdown() now reads n_workers
(added by NVIDIA#16456 while the test was in flight) and _wait_shutdown, both
set only in __init__. Provide them explicitly and drop the waive.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…rand peers

The kill was armed for ANY exception escaping the executor loop wrapper,
including one raised on the way out of an already-shutting-down loop.
A hardware A/B on 2x GB200 (mpirun -n 2 trtllm-llmapi-launch trtllm-bench
--tp 2) showed it turning a benign late exception -- raised after
event_loop() returned, i.e. after the shutdown request was processed and
all work was done -- into a whole-job SIGKILL: mpirun exit 137 at
crash+10s, where the same injection on the merge-base logged the error,
let the worker thread die, and completed the benchmark with exit 0. It
fired during the memory-profiling dry run, before the benchmark even
started: PyExecutor is constructed and shut down twice per process and
that first shutdown is a normal lifecycle event.

Three changes:

- crashed = not self.is_shutdown. Once is_shutdown is set every rank has
  processed the shutdown broadcast, so no peer is waiting on this one and
  there is nothing to escalate.
- Scope the flag to event_loop() itself via an inner try. Teardown of the
  enclosing host-profiler / GC context managers is not a stranded-peer
  condition either.
- Make the watchdog cancellable and give it an explicit deadline. It was
  a bare daemon thread that slept the grace and killed with no cancel
  path, so once armed it fired at crash+10s even if the process went on
  to shut down cleanly and would have exited 0. It also double-armed: the
  watchdog and the post-cleanup hard_kill_on_rank_crash each ran their
  own timer. The watchdog now covers only the window where cleanup may
  block forever; once cleanup returns the caller cancels it and carries
  the kill inline on the watchdog's ORIGINAL deadline, so exactly one
  timer is live at any moment and the handover cannot push the kill out
  by a second grace.

A genuine mid-loop crash still arms the watchdog and still hard-kills the
world -- the behavior the kill exists for is unchanged.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…bed kill

If cancel() ever regresses, the watchdog thread outlives monkeypatch
teardown and SIGKILLs the pytest process instead of failing the test.
Join it inside the test, while propagate_hard_kill is still stubbed.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…is_shutdown

The previous commit keyed the decision off `not self.is_shutdown`, on the
premise that is_shutdown means "every rank has processed the shutdown
broadcast". That premise is false, and the mistake silently disabled the
kill for this feature's most common trigger.

`_handle_errors` sets `self.is_shutdown = True` rank-locally whenever an
error is fatal, and `classify_error` returns `immediate_fatal` for a CUDA
illegal address / device-side assert / launch failure, bypassing the error
budget entirely. The only "broadcast" that follows is
`enqueue_shutdown_request()`, which pushes into this process's own queue --
peers are told nothing. `should_stop_processing` additionally requires empty
active/waiting queues, so the loop keeps entering collectives after the flag
is set.

So: rank 3 of 4 takes cudaErrorIllegalAddress in the forward pass,
_handle_errors sets is_shutdown and returns None, and the next unguarded
statement (guided_decoder.execute(batch_outputs['logits'])) raises TypeError
on None. The wrapper computed crashed=False, armed nothing, and ranks 0-2
blocked in their next NCCL collective for the full 300s -- exactly the
failure this PR exists to remove.

Gate on an explicit completion sentinel instead. `_event_loop_completed` is
set at the three executor loops' normal-exit `break` sites and nowhere else,
so it answers the actual question: did event_loop() reach its own
termination? A raise after that point (profiler/hang-detector __exit__, or
the enclosing context managers) is a teardown error; anything else -- now
including a failure before the loop ever started, which the previous inner
try narrowed away -- strands peers and escalates.

Also corrects two overstated claims from the previous commit. The
watchdog/post-cleanup "double-arm" never produced an extra or later kill:
earliest-wins gave crash+grace before, and max(cleanup_end, crash+grace) =
crash+grace after. cancel() cannot spare a rank that exits cleanly either --
it is followed one line later by the same kill on the same deadline. The
handover machinery is kept because one timer is clearer than two, but all
the protection against a spurious SIGKILL rests on the predicate above.

Log fixes: the disarm message drops from ERROR to DEBUG (it printed
"cancelled before the grace elapsed" moments before the world was SIGKILLed,
which reads during triage as "the kill was called off"), and the countdown
now logs the remaining time rather than the full grace on the handover path.

Tests: a rank-local-fatal regression test that fails against the previous
predicate; a pre-loop-failure test; coverage for the already-elapsed
deadline (the max(0.0, ...) clamp is the only thing keeping a negative sleep
from raising into the blanket except and dropping the kill); and an AST
check that every `break` in the three loops is preceded by the sentinel, so
a new normal-exit path cannot make clean shutdowns look like crashes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… process-wide sleep patch

monkeypatching time.sleep is process-wide, so any background thread that
sleeps during these two tests appends its own entry into the asserted
list. Assert the ordering these tests are about instead of exact list
equality.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…le on the deadline clamp

Five guard-quality defects from review. None changed runtime behavior on a
healthy run, but two recreated the conditions under which the predicate has
already regressed twice.

1. The AST guard enforced the wrong invariant. It required EVERY own `break`
   in the three loops to be preceded by the sentinel, but only a break
   exiting the outer `while True` terminates the event loop, and
   _executor_loop / _executor_loop_overlap already contain inner `for` loops.
   Worse, its failure message told the reader "every normal exit must set
   it", so a contributor who added an inner-loop break would have been
   instructed to set the sentinel while the loop was still running -- making
   every later rank-local crash look like a clean shutdown and silently
   disabling the kill, the same class as the previous blocker. Now: exactly
   one loop-terminating break per loop, located by recursing through
   if/try/with but stopping at nested for/while/def.

   Mutation-tested: inner `for _z in (): break` without the sentinel PASSES;
   a missing sentinel on the outer break FAILS; a second outer break FAILS.

2. The load-bearing reset was untested. Deleting
   `self._event_loop_completed = False` from _event_loop_wrapper left the
   whole suite green while a second loop run on the same executor misread a
   genuine crash as a clean shutdown (verified: run-2 events go from
   [watchdog, cleanup, cancel, kill] to [cleanup]). The one "initialization"
   test only grepped __init__ -- the site that is NOT load-bearing -- which
   invited deleting the real one as redundant. Added a behavioral
   two-invocation test plus a source assertion on the wrapper.

3. The rationale on the max(0.0, ...) deadline clamp was false. The comment
   and docstring claimed it was the only thing preventing a negative sleep
   from raising into the blanket except and dropping the kill. It is not:
   _wait_out_kill_grace guards with `remaining > 0`, and Event.wait() returns
   immediately for a negative timeout (probe: _wait_out_kill_grace(-100, None)
   -> True, zero sleep calls). A false "this guard protects X" comment is how
   the previous regressions got through, and someone trusting it could drop
   the real guard as redundant. Corrected both, moved the load-bearing note
   onto the `> 0` guard, and added a test asserting sleep is never called
   with a negative argument.

4. Hardening the two grace-ordering tests against the process-wide time.sleep
   patch weakened them: membership-only assertions accepted sleeping the
   grace TWICE before killing -- exactly the crash + 2*grace bug this series
   introduced with its two independent timers. Pinned the counts to 1 while
   still tolerating an unrelated thread's sleep. The handover test now pins
   time.monotonic and asserts the exact remaining duration instead of racing
   a 0.3s real sleep against a 0.55s bound on a loaded CI node.

5. The AST guard checked neither the assigned value nor the target object:
   `self._event_loop_completed = False` before the break passed, and so did
   `self.dist._event_loop_completed = True`. A value inversion turns every
   clean shutdown into a whole-job SIGKILL and was invisible. Now requires
   Constant True assigned to an attribute of Name('self'); both mutants fail.

Also documents why _executor_loop_pp sets the sentinel before its Stage-5
drain: reaching there means every rank observed should_stop_processing, so
the drain consumes only a rank-local queue and a raise in it strands nobody.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…p, not walk order

_executor_loop_pp contains four while loops, one of them (the Stage-5
drain) a sibling of the event loop under the same with-block. Selecting
by ast.walk order happened to pick the right one, but a body reorder
would silently repoint the guard at the drain. Select by the `True`
test and assert uniqueness instead.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-feat-rank-crash-hard-kill branch from 50eeaca to dd070b7 Compare July 30, 2026 10:13
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (a146f66)

The previous failure was not a flake and not this PR's code — the branch was carrying a stale copy of tests/unittest/_torch/multi_gpu/test_linear.py, whose pytest.raises(AssertionError) at line 315 was removed from main by #16844 (7982aa9701, https://nvbugs/6501376). Re-running could not have fixed that; only a rebase could. Confirmed after rebase: grep -c "pytest.raises(AssertionError)" tests/unittest/_torch/multi_gpu/test_linear.py0.

One conflict, resolved by dropping an obsolete waive. The rebase conflicted in waives.txt, where this branch re-added:

unittest/disaggregated/test_kv_transfer.py::test_transfer_worker_v2[tp4_pp1_to_tp2_pp2] SKIP (https://nvbugs/6426834)

That waive is obsoletemain fixed the underlying bug in 7d3a4e9796 (#16708, "Deflake test_kv_transfer: cap NIXL progress threads, pin UCX env"), same NVBug. Re-adding it would have skipped a test main has since repaired, which is the same staleness class as the test_linear.py problem above. I dropped it and kept main's block. Duplicate check across the whole file comes back clean.

Net effect: the diff against main is now exactly the four files this PR owns, and waives.txt is no longer touched at all.

tensorrt_llm/_torch/pyexecutor/hang_detector.py           | 198 +
tensorrt_llm/_torch/pyexecutor/py_executor.py             |  68 +-
tests/unittest/_torch/executor/test_hang_detector_kill.py | 664 +-
tests/unittest/executor/test_proxy_fast_death.py          |   4 +

All nine commits preserved, no squashing. 50eeacadd070b7.

Apologies for the stale approval — the rebase was unavoidable to get a valid CI signal.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62761 [ run ] triggered by Bot. Commit: dd070b7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62761 [ run ] completed with state SUCCESS. Commit: dd070b7
/LLM/main/L0_MergeRequest_PR pipeline #50891 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants