Skip to content

[None][perf] Cut per-iteration executor bookkeeping in the hang detector and profiler - #17531

Open
pranav-nvidia wants to merge 3 commits into
NVIDIA:mainfrom
pranav-nvidia:perf-executor-iteration-overhead
Open

[None][perf] Cut per-iteration executor bookkeeping in the hang detector and profiler#17531
pranav-nvidia wants to merge 3 commits into
NVIDIA:mainfrom
pranav-nvidia:perf-executor-iteration-overhead

Conversation

@pranav-nvidia

@pranav-nvidia pranav-nvidia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

The executor loop's hang-detector heartbeat did cross-thread work on every call. checkpoint() cancelled the pending watchdog task and scheduled a new one; both Future.cancel() and run_coroutine_threadsafe() post a call_soon_threadsafe, waking the detector's event-loop thread. With three checkpoints per iteration (loop top plus both pause() exits) that is roughly six cross-thread wakeups and three Task lifecycles per iteration, to re-arm a watchdog that will not fire for 300 s.

This keeps one long-lived watcher and moves a monotonic deadline instead, so checkpoint() becomes one clock read and one float store.

A second, smaller commit gates the profiler start-event record on the same condition the read side already uses (print_log or enable_iter_perf_stats), so events nobody reads are not recorded.

This is not Whisper-specific. It is a fixed per-iteration cost, so it matters in proportion to how short an iteration is — every host-bound workload pays it.

Measurements

whisper-tiny, SM120, fp16 greedy, 32-clip librispeech, median of 5 blocks x 5 iters after 10 warmup, 3 interleaved A/B rounds with medians reported.

baseline this PR delta
bs8 1044.7 1236.8 +18.4 %
bs32 1725.6 1930.8 +11.9 %

Both deltas exceed the observed run-to-run range (2.6-5.6 %). WER is 0.1250 in every arm.

Measured separately, the hang-detector commit accounts for +14.9 % / +11.6 % and the profiler commit for +2.4 % / +3.3 % (the latter is inside the noise band at bs32).

Behavioural notes

The watchdog's fire semantics are preserved deliberately, since its failure mode is a silent multi-rank hang:

  • start() leaves the detector disarmed; the first checkpoint() arms it, as before. The start-to-first-checkpoint window is not hang-eligible.
  • The watcher outlives a report, and outlives a report that raises. on_detected is the cross-rank hard kill and can itself fail on a degraded job; a watchdog that quietly stopped watching would be the exact failure it exists to catch.
  • A checkpoint() racing the watcher's disarm is not erased — the watcher only clears the deadline it observed lapse.
  • start() ignores a call on an already-active detector, so there is no path to two watchers sharing one deadline and propagating two hard kills.
  • pause() is unchanged relative to main, including its behaviour when nested.

One deliberate difference: the timeout is now measured from the checkpoint() call rather than from when the detector thread processes it. If a caller holds the GIL past the timeout, this reports on release where main would grant a fresh interval. Restoring main's arm epoch would require arming on the detector thread, which is precisely the cross-thread work this change removes.

Tests

Four tests added to tests/unittest/_torch/executor/test_hang_detector_kill.py, each pinning one production behaviour and each verified to fail when that behaviour is reverted: single-watcher reuse, disarmed-until-first-checkpoint, watcher survival across a raising callback, and the checkpoint-vs-disarm race.

Dev Engineer Review

  • The hang detector now uses one persistent watcher and a monotonic deadline.
  • checkpoint(), cancel_task(), and pause() update the deadline without repeated task cancellation or scheduling.
  • The watcher starts disarmed, reports each lapse once, survives callback errors, and prevents duplicate activation.
  • The follow-up fix disarms the detector before publishing its active state.
  • The profiler records timing data only when print_log or enable_iter_perf_stats is enabled.
  • The changes reduce cross-thread wakeups and per-iteration task overhead.
  • No configuration files or test-list files changed.
  • No public API declarations changed.
  • Error handling preserves watcher operation after reporting failures.
  • Timeout measurement starts at the checkpoint() call.

QA Engineer Review

  • Test changes are in tests/unittest/_torch/executor/test_hang_detector_kill.py.
  • Added coverage verifies watcher-task reuse, initial disarming, watcher survival after callback failure, and checkpoint/disarm race handling.
  • The hang-reporting test now calls _report_hang().
  • No corresponding test-db/ or qa/ test-list entry was added.
  • Verdict: needs follow-up because CI or manual QA coverage mapping is not shown.

checkpoint() cancelled the pending watchdog task and scheduled a new one, and
both calls post a call_soon_threadsafe, so the executor loop woke the detector
thread several times per iteration to re-arm a 300s watchdog. Keep one
long-lived watcher and move a monotonic deadline instead. The timeout is now
measured from the checkpoint call rather than from when the detector thread
gets to process it.

Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
The start events were recorded on every executor iteration while the read
side is already gated on print_log or enable_iter_perf_stats, so with
neither enabled the record calls were pure overhead.

Gate the record on the same condition.

Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
@pranav-nvidia

Copy link
Copy Markdown
Contributor Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65606 [ run ] triggered by Bot. Commit: 9d80c94 Link to invocation

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4f93a2b4-b2de-4faa-ae0a-691d3a5b9b38

📥 Commits

Reviewing files that changed from the base of the PR and between 9d80c94 and ab284c7.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/pyexecutor/hang_detector.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tensorrt_llm/_torch/pyexecutor/hang_detector.py

Walkthrough

The change replaces per-detection hang tasks with one persistent watcher and a shared deadline. It adds tests for watcher reuse, disarming, race handling, and callback failures. Profiler timing capture is gated by enabled statistics.

Changes

Persistent hang monitoring

Layer / File(s) Summary
Deadline lifecycle
tensorrt_llm/_torch/pyexecutor/hang_detector.py
HangDetector uses a disarmed monotonic deadline. start(), checkpoint(), cancel_task(), and pause() update shared watcher state.
Persistent watcher and validation
tensorrt_llm/_torch/pyexecutor/hang_detector.py, tests/unittest/_torch/executor/test_hang_detector_kill.py
The watcher reports elapsed deadlines once, preserves racing checkpoints, survives reporting exceptions, and remains active. Tests cover these behaviors and update the hang-reporting call.

Profiler timing capture

Layer / File(s) Summary
Timing capture gating
tensorrt_llm/_torch/pyexecutor/py_executor.py
CUDA timing events are recorded and completed only when logging or iteration performance statistics are enabled and the start timestamp is initialized.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HangDetector
  participant _watch
  participant _report_hang
  HangDetector->>_watch: schedule persistent watcher
  _watch->>HangDetector: observe shared deadline
  _watch->>_report_hang: report elapsed deadline
  _report_hang-->>_watch: return or raise
  _watch->>HangDetector: continue monitoring
Loading

Possibly related PRs

Suggested reviewers: zhaoyangwang-nvidia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the performance changes to the hang detector and profiler.
Description check ✅ Passed The description clearly explains the problem, solution, measurements, behavioral changes, and test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unittest/_torch/executor/test_hang_detector_kill.py (1)

70-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct lifecycle tests for HangDetector.

Coverage summary:

  • Added four tests and modified test_status_provider_errors_are_logged.
  • CI registration is sufficient. l0_sanity_check.yml lists the module, and l0_h100.yml and l0_cpu.yml cover its directory.
  • No QA-list entry is required for this unit-test change.
  • Coverage is insufficient for direct cancel_task(), stop(), repeated start(), and detection latency after pause() resumes. Add focused tests for these cases.
🤖 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/_torch/executor/test_hang_detector_kill.py` around lines 70 -
169, Add focused lifecycle tests for HangDetector covering direct cancel_task(),
stop(), repeated start() calls, and detection latency after pause() resumes.
Verify each method’s expected task/deadline state and that resumed monitoring
detects a subsequent hang within the configured timeout, using the existing test
patterns and symbols such as hd.task, hd.pause(), and hd.checkpoint().

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.

Inline comments:
In `@tensorrt_llm/_torch/pyexecutor/hang_detector.py`:
- Around line 360-379: Move the self._deadline = math.inf reset into the
self.lock region in HangDetector.start(), placing it immediately before
self.active = True. Remove the later reset after the lock so a concurrent
checkpoint() cannot arm the deadline and have it overwritten.
- Around line 401-406: Bound the sleep interval in the active watcher loop
around _deadline so disarmed states with an infinite remaining duration do not
sleep for the full timeout. Clamp asyncio.sleep to a fraction of self.timeout
while preserving deadline-based sleeping when armed, ensuring a checkpoint that
arms the detector during sleep is observed within the intended timeout latency.

---

Nitpick comments:
In `@tests/unittest/_torch/executor/test_hang_detector_kill.py`:
- Around line 70-169: Add focused lifecycle tests for HangDetector covering
direct cancel_task(), stop(), repeated start() calls, and detection latency
after pause() resumes. Verify each method’s expected task/deadline state and
that resumed monitoring detects a subsequent hang within the configured timeout,
using the existing test patterns and symbols such as hd.task, hd.pause(), and
hd.checkpoint().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: aad36bb0-6552-4098-9e32-abdac3dd9174

📥 Commits

Reviewing files that changed from the base of the PR and between 43c2386 and 9d80c94.

📒 Files selected for processing (3)
  • tensorrt_llm/_torch/pyexecutor/hang_detector.py
  • tensorrt_llm/_torch/pyexecutor/py_executor.py
  • tests/unittest/_torch/executor/test_hang_detector_kill.py

Comment thread tensorrt_llm/_torch/pyexecutor/hang_detector.py
Comment thread tensorrt_llm/_torch/pyexecutor/hang_detector.py
start() published `active` inside the lock but reset `_deadline` after
releasing it. A checkpoint from the executor thread in that window armed
the deadline, and the reset then erased it, leaving the loop unwatched
until the next checkpoint. Store the disarmed deadline inside the lock,
before `active` goes true.

Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
@pranav-nvidia

Copy link
Copy Markdown
Contributor Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65665 [ run ] triggered by Bot. Commit: ab284c7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65606 [ run ] completed with state ABORTED. Commit: 9d80c94

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65665 [ run ] completed with state SUCCESS. Commit: ab284c7
/LLM/main/L0_MergeRequest_PR pipeline #53386 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

@pranav-nvidia

Copy link
Copy Markdown
Contributor Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66000 [ run ] triggered by Bot. Commit: ab284c7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66000 [ run ] completed with state SUCCESS. Commit: ab284c7
/LLM/main/L0_MergeRequest_PR pipeline #53687 completed with status: 'UNSTABLE'

CI Report

⚠️ Multi-GPU Label Required:
Multi-GPU tests require the ci: full pre-merge approved label on this PR. Ask a member of NVIDIA/trt-llm-ci-approvers to add the label, then re-trigger CI with the same bot command (no rebase needed).

⚠️ 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

Link to invocation

@pranav-nvidia

Copy link
Copy Markdown
Contributor Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66044 [ run ] triggered by Bot. Commit: ab284c7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66044 [ run ] completed with state FAILURE. Commit: ab284c7
/LLM/main/L0_MergeRequest_PR pipeline #53727 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

@pranav-nvidia

Copy link
Copy Markdown
Contributor Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66087 [ run ] triggered by Bot. Commit: ab284c7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66087 [ run ] completed with state SUCCESS. Commit: ab284c7
/LLM/main/L0_MergeRequest_PR pipeline #53764 completed with status: 'SUCCESS'
Pipeline passed with automatic retried tests. Check the rerun report for details.

CI Report

Link to invocation

@zhaoyangwang-nvidia zhaoyangwang-nvidia 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.

Approve with nits.

# once. A checkpoint racing this branch installs a newer deadline;
# clearing that would leave the watcher alive but permanently
# disarmed, which is the failure this watchdog exists to catch.
if self._deadline == deadline:

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.

if self._deadline == deadline: self._deadline = math.inf is a non-atomic compare-and-set — the load/compare and the store are separate bytecodes, so a checkpoint() from the executor thread landing between them is still erased. That is the case the comment rules out, and test_a_checkpoint_racing_the_disarm_is_not_erased doesn't cover it either: it injects from _watch's own time.monotonic() call, i.e. between the local read and the comparison, not between the comparison and the store. The practical impact is small (a lapse only reaches here after timeout without a checkpoint, and the next checkpoint re-arms within an iteration), but since this is off the hot path, could these two lines take self.lock, or the comment be softened to best-effort?

self.task = asyncio.run_coroutine_threadsafe(self._detect_hang(), self.loop)
self._deadline = time.monotonic() + self.timeout

def cancel_task(self):

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.

cancel_task() no longer cancels a task — it only disarms the deadline, as the new docstring correctly says, so the name now points at the old design. Note it can't simply be renamed: run_precheck.py calls it and test_cache_transceiver_precheck_run.py::test_hang_detector_surface asserts the attribute exists. Consider adding disarm() as the real name and keeping cancel_task = disarm as an explicit compatibility alias.

@contextmanager
def pause(self):
"""Pause hang detection in scope."""
self._deadline = math.inf

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.

pause() previously called cancel_task(); inlining self._deadline = math.inf here makes four places that write the sentinel. pause() runs twice per iteration and is already orders of magnitude cheaper than the task cancel it replaces, so the call shouldn't affect the numbers — keeping it leaves "what disarming means" (and any future change to it, e.g. taking the lock) in one place.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants