Skip to content

[TRTLLM-13409][fix] bound the perf-sanity harness so a stalled stage fails on its own - #17298

Open
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-perf-sanity-harness-bounds
Open

[TRTLLM-13409][fix] bound the perf-sanity harness so a stalled stage fails on its own#17298
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-perf-sanity-harness-bounds

Conversation

@JunyiXu-nv

@JunyiXu-nv JunyiXu-nv commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

The problem

Perf-sanity stages cannot fail on their own.

The benchmark client runs under subprocess.check_output() with no timeout, and every other harness wait is bounded by DEFAULT_TIMEOUT (10800s) — which sits above the pytest per-test marker (TIMEOUT (120) ⇒ 7200s). So no harness wait can expire first. A stall anywhere below the HTTP layer surfaces only as "the client is still running", and the stage burns its whole Slurm allocation until something external kills it — producing no results XML and no diagnostic.

The timeout stack is inverted:

wait bound can it fire?
/health readiness, agg min(self.timeout, AGG_SERVER_READY_TIMEOUT) = 30 min yes — #16403
/health readiness, disagg min(self.timeout, DISAGG_SERVER_READY_TIMEOUT) = 60 min yes — #16403
config-file rendezvous poll self.timeout = 10800 s no
hostname/port poll self.timeout = 10800 s no
wait_for_benchmark_ready self.timeout = 10800 s no
benchmark client subprocess none
client per-request HTTP (backend_request_func.py:18) 6 h no
disagg router per-request (-r 10800) 3 h no
pytest per-test marker 7200 s yes, last resort

Relationship to #16403

#16403 bounded the startup phase — "the server never becomes healthy" — with a
per-mode budget (disagg gets 60 min because its /health answers only after every
ctx/gen worker is up). That is a different phase from this PR, which bounds the
steady-state benchmark run after /health has already answered.

The two do not overlap, and the timeline says the remaining hangs are in the second
phase: with disagg readiness bounded at 60 min, a startup hang now fails at 60 min,
yet the stages measured below ran 1.7-2.3 h with servers healthy and the
benchmark running. Only the client phase can absorb that time.

An earlier revision of this description claimed the readiness polls were bounded at
10800 s. That was wrong -- it is true of the three file-rendezvous polls, but not of
/health, which #16403 already fixed.

Why now

Over the five days after the benchmark-fill-target fix (#16961), disagg hangs were 13 events / 143 GPU-h, of which 115 GPU-h were multi-hour stages with no identified cause — 1.7–2.3 h runtimes against 18–41 minute budgets, no results XML, killed externally.

This PR does not diagnose those. It makes the stage fail on its own, quickly, with the server-side context attached — so the next one is diagnosable instead of a silent multi-hour burn.

Changes

  • run_benchmark_client() runs the client under a deadline (default 3600s; TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC to change, 0 disables). On expiry it kills the client and raises, naming the elapsed time and the knob, with server-side keyword hits, a raw tail of each server log, and the partial client output attached. check_output semantics are otherwise preserved — combined stdout/stderr on success, CalledProcessError on nonzero exit. Applied to both the aggregated and disaggregated client paths.

  • stop_process() replaces the bare terminate(); wait() at the three server teardown sites, escalating to SIGKILL after a grace period. A rank wedged in a non-interruptible native call never runs the Python signal handler, and the unbounded wait() held the allocation until Slurm intervened.

  • The timeout path attaches a raw log tail in addition to the check_error() keyword scan. ERROR_KEYWORDS are Python exception names (RuntimeError, TimeoutError, …) and do not match [TRT-LLM] [E] lines, so a server that died the TRT-LLM way surfaced nothing at all.

A deliberate non-change

I did not add [TRT-LLM] [E] to ERROR_KEYWORDS, even though that is the obvious fix for the last point. ERROR_KEYWORDS also drives wait_for_endpoint_ready()'s fast-fail, where a benign [E] line during startup would begin failing currently-healthy runs. The raw tail gets the diagnostic to the reader without that risk. Worth doing separately with its own evaluation.

Tests

tests/unittest/others/test_perf_sanity_bounds.py — 12 tests, no GPU:

  • the client bound defaults to a finite value, honours the env override, treats 0 as "disabled", and falls back on a malformed value (a CI typo must not silently restore the unbounded behaviour);
  • stop_process reaps a cooperative process, escalates to SIGKILL against a child that ignores SIGTERM (the case the bare wait() could not survive), and no-ops on an already-dead process;
  • the client runner returns output on success, still raises CalledProcessError on nonzero exit, bounds a hanging client promptly while naming the knob, and surfaces both keyword hits and the raw tail.

All 12 pass locally. Note the module needs oyaml (declared in requirements-dev.txt).

Risk

The behaviour change is that a run exceeding one hour of client wall-clock now fails instead of hanging. The largest per-test budget in the perf-sanity lists is 120 minutes of pytest budget with real runtimes well under an hour, so this bounds the pathological case with headroom. If a legitimate long run trips it, TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC raises or disables it per-stage.

Dev Engineer Review

  • Added the configurable TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC timeout.
  • Set the default timeout to 3600 seconds.
  • Supports 0 to disable the timeout.
  • Preserves existing check_output() behavior.
  • Adds timeout diagnostics with elapsed time, configuration, server error keywords, server-log tails, and partial client output.
  • Replaces unbounded server teardown waits with stop_process().
  • Escalates from SIGTERM to SIGKILL after the grace period.
  • Keeps ERROR_KEYWORDS unchanged.
  • No configuration-file or test-list changes were identified.

QA Engineer Review

  • Added tests/unittest/others/test_perf_sanity_bounds.py.
  • Added coverage for timeout configuration, process termination, successful output, nonzero client exits, and timeout diagnostics.
  • The tests are outside tests/integration/test_lists/.
  • No corresponding test-db/ or qa/ coverage entry is reported.
  • Verdict: needs follow-up.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The perf sanity integration now bounds benchmark-client execution and server shutdown. It captures partial output and server diagnostics on timeout. New CPU-only regression tests cover configuration, termination, output, exit errors, and diagnostic failures.

Changes

Perf sanity execution bounds

Layer / File(s) Summary
Bounded client runner and diagnostics
tests/integration/defs/perf/test_perf_sanity.py
Adds configurable client timeouts, partial-output capture, server-log diagnostics, and explicit timeout errors.
Benchmark wiring and bounded shutdown
tests/integration/defs/perf/test_perf_sanity.py
Updates aggregated and disaggregated paths to use bounded client execution and SIGTERM/SIGKILL server shutdown.
Timeout and teardown regression coverage
tests/unittest/others/test_perf_sanity_bounds.py
Tests timeout configuration, process termination, output handling, nonzero exits, hangs, and server-log diagnostics.

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

Sequence Diagram(s)

sequenceDiagram
  participant BenchmarkPath
  participant run_benchmark_client
  participant ServerProcess
  participant ServerLogs
  BenchmarkPath->>run_benchmark_client: Start benchmark client with timeout
  run_benchmark_client->>ServerLogs: Inspect server errors and log tail
  run_benchmark_client-->>BenchmarkPath: Return output or raise diagnostic error
  BenchmarkPath->>ServerProcess: Stop with SIGTERM
  ServerProcess-->>BenchmarkPath: Exit or receive SIGKILL
Loading

Suggested labels: ci: full pre-merge approved

Suggested reviewers: bowenfu, mzweilz, mlefeb01

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% 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 clearly identifies the fix and states that stalled perf-sanity stages now fail independently.
Description check ✅ Passed The description clearly explains the problem, solution, risks, and relevant no-GPU tests; it omits the formal PR Checklist but is otherwise complete.
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 (3)
tests/integration/defs/perf/test_perf_sanity.py (2)

143-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the remaining parameters.

proc, cmd, env, and server_logs have no type annotations. Use subprocess.Popen[bytes], list[str], Mapping[str, str], and Sequence[str] | None.

As per coding guidelines: "Annotate every function, use None for procedures, avoid unnecessary Any and type: ignore, prefer built-in generic types and |".

Also applies to: 165-165

🤖 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/integration/defs/perf/test_perf_sanity.py` at line 143, Annotate the
remaining parameters in stop_process and the related function at the referenced
location: use subprocess.Popen[bytes] for proc, list[str] for cmd, Mapping[str,
str] for env, and Sequence[str] | None for server_logs. Preserve the existing
None return annotations and import any required typing symbols.

Source: Coding guidelines


143-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Terminate the full subprocess tree during teardown.

trtllm-serve can create worker descendants, but stop_process() signals only the direct child. Use cleanup_process_tree() from tests/integration/defs/trt_test_alternative.py for server teardown. Apply the same cleanup to the timeout path in run_benchmark_client() when the client can create descendants.

🤖 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/integration/defs/perf/test_perf_sanity.py` around lines 143 - 162,
Update stop_process() to use cleanup_process_tree() so SIGTERM/SIGKILL teardown
reaches the server process and all descendants, preserving the existing
grace-period behavior. Also replace direct client termination in
run_benchmark_client()’s timeout path with cleanup_process_tree() when the
client may create descendants.

Source: Learnings

tests/unittest/others/test_perf_sanity_bounds.py (1)

127-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the partial client output section.

The timeout message ends with --- last client output --- and the last 4000 characters of client stdout. No test asserts that a client which prints before it hangs keeps that output in the failure message. That section is the part that shows how far the run got, so a regression there would be silent.

A client such as print('phase 1', flush=True); time.sleep(120) covers it.

🤖 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/others/test_perf_sanity_bounds.py` around lines 127 - 149,
Extend test_client_hang_is_bounded_and_names_the_knob to run a hanging client
that emits identifiable flushed output before sleeping, then assert the
RuntimeError message contains that output after the --- last client output ---
section. Preserve the existing timeout, environment-variable, server-error, and
prompt-bound assertions.
🤖 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 `@tests/integration/defs/perf/test_perf_sanity.py`:
- Around line 206-212: Update the log-tail collection around the existing
open/read block to use collections.deque with maxlen=SERVER_LOG_TAIL_LINES,
importing deque with the standard-library imports. Iterate through the file and
retain only the final configured number of lines, preserving the existing
OSError handling and tail formatting.

In `@tests/unittest/others/test_perf_sanity_bounds.py`:
- Around line 46-49: Update test_client_timeout_defaults_to_one_hour to remove
the timeout environment variable with pytest’s monkeypatch fixture via
monkeypatch.delenv, preserving any pre-existing value after the test instead of
mutating os.environ directly.

---

Nitpick comments:
In `@tests/integration/defs/perf/test_perf_sanity.py`:
- Line 143: Annotate the remaining parameters in stop_process and the related
function at the referenced location: use subprocess.Popen[bytes] for proc,
list[str] for cmd, Mapping[str, str] for env, and Sequence[str] | None for
server_logs. Preserve the existing None return annotations and import any
required typing symbols.
- Around line 143-162: Update stop_process() to use cleanup_process_tree() so
SIGTERM/SIGKILL teardown reaches the server process and all descendants,
preserving the existing grace-period behavior. Also replace direct client
termination in run_benchmark_client()’s timeout path with cleanup_process_tree()
when the client may create descendants.

In `@tests/unittest/others/test_perf_sanity_bounds.py`:
- Around line 127-149: Extend test_client_hang_is_bounded_and_names_the_knob to
run a hanging client that emits identifiable flushed output before sleeping,
then assert the RuntimeError message contains that output after the --- last
client output --- section. Preserve the existing timeout, environment-variable,
server-error, and prompt-bound assertions.
🪄 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: 39b76602-bb68-49b8-aaea-c43884a8cf70

📥 Commits

Reviewing files that changed from the base of the PR and between 7608520 and e95cbfa.

📒 Files selected for processing (2)
  • tests/integration/defs/perf/test_perf_sanity.py
  • tests/unittest/others/test_perf_sanity_bounds.py

Comment thread tests/integration/defs/perf/test_perf_sanity.py
Comment thread tests/unittest/others/test_perf_sanity_bounds.py Outdated
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63989 [ run ] triggered by Bot. Commit: e95cbfa Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

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

@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-fix-perf-sanity-harness-bounds branch from e95cbfa to 8605b3a Compare August 5, 2026 10:21
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 3

🤖 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 `@tests/integration/defs/perf/test_perf_sanity.py`:
- Line 1571: Initialize server_proc before its try block and guard the
corresponding stop_process cleanup so it runs only when a process was created;
apply the same change to disagg_server_proc and its finally cleanup in
tests/integration/defs/perf/test_perf_sanity.py at lines 1571-1571 and
1599-1599.
- Line 141: Update the timeout normalization logic in
tests/integration/defs/perf/test_perf_sanity.py at lines 141-141 so negative
values are invalid and fall back to DEFAULT_BENCHMARK_CLIENT_TIMEOUT, while
preserving 0 as the only value that disables the deadline. Add a regression test
in tests/unittest/others/test_perf_sanity_bounds.py at lines 65-68 asserting
that a negative timeout does not disable the benchmark-client deadline.
- Around line 128-141: Complete the Python 3.10+ annotations for the new
performance sanity helpers and tests: in
tests/integration/defs/perf/test_perf_sanity.py lines 128-141, change
_benchmark_client_timeout to return int | None; at lines 144-164, annotate the
process parameter as subprocess.Popen[bytes]; at lines 166-235, annotate cmd as
Sequence[str], env as dict[str, str], and server_logs as list[str] | None. In
tests/unittest/others/test_perf_sanity_bounds.py lines 46-189, add -> None to
every test function and annotate fixture parameters with MonkeyPatch and Path,
importing those types as needed.
🪄 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: e10f96c1-7a52-4235-99e9-5a5ac1be4683

📥 Commits

Reviewing files that changed from the base of the PR and between f1f773f and 8605b3a.

📒 Files selected for processing (2)
  • tests/integration/defs/perf/test_perf_sanity.py
  • tests/unittest/others/test_perf_sanity_bounds.py

Comment on lines +128 to +141
def _benchmark_client_timeout() -> Optional[int]:
"""Effective client bound in seconds, or None when explicitly disabled."""
raw = os.environ.get(BENCHMARK_CLIENT_TIMEOUT_ENV_VAR_NAME)
if raw is None:
return DEFAULT_BENCHMARK_CLIENT_TIMEOUT
try:
value = int(raw)
except ValueError:
print_info(
f"{BENCHMARK_CLIENT_TIMEOUT_ENV_VAR_NAME}={raw!r} is not an integer; "
f"falling back to {DEFAULT_BENCHMARK_CLIENT_TIMEOUT}s"
)
return DEFAULT_BENCHMARK_CLIENT_TIMEOUT
return None if value <= 0 else value

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n \
  '^\s*def (_benchmark_client_timeout|stop_process|run_benchmark_client|test_)' \
  tests/integration/defs/perf/test_perf_sanity.py \
  tests/unittest/others/test_perf_sanity_bounds.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 1997


🏁 Script executed:

# Extract the function signatures and immediate context to verify annotations
sed -n '128,141p' tests/integration/defs/perf/test_perf_sanity.py
echo "---"
sed -n '144,164p' tests/integration/defs/perf/test_perf_sanity.py
echo "---"
sed -n '166,180p' tests/integration/defs/perf/test_perf_sanity.py
echo "---"
# Check imports at the top of both files
head -20 tests/integration/defs/perf/test_perf_sanity.py
echo "=== UNITTEST FILE ==="
head -20 tests/unittest/others/test_perf_sanity_bounds.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 4192


🏁 Script executed:

# Get full import block for integration test file
sed -n '1,50p' tests/integration/defs/perf/test_perf_sanity.py | grep -E '(^import|^from)'
echo "---"
# Get full import block for unittest file
sed -n '1,50p' tests/unittest/others/test_perf_sanity_bounds.py | grep -E '(^import|^from)'
echo "---"
# Check test function signatures in unittest file
sed -n '46,70p' tests/unittest/others/test_perf_sanity_bounds.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 2221


🏁 Script executed:

# Check how parameters are used to determine correct types
# For cmd, env, server_logs - look at their usage in run_benchmark_client
sed -n '166,185p' tests/integration/defs/perf/test_perf_sanity.py | grep -E '(cmd|env|server_logs)'
echo "---"
# For proc - check usage in stop_process
sed -n '144,164p' tests/integration/defs/perf/test_perf_sanity.py | grep -E 'proc\.'
echo "---"
# Check test invocations in unittest to see what types are passed
sed -n '114,150p' tests/unittest/others/test_perf_sanity_bounds.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 1933


🏁 Script executed:

# Check for imports of typing modules and how fixtures are used
sed -n '1,30p' tests/unittest/others/test_perf_sanity_bounds.py
echo "---"
# Check if there are other test functions with annotations in unittest
sed -n '76,110p' tests/unittest/others/test_perf_sanity_bounds.py
echo "---"
# Look for pathlib usage
grep -n "pathlib\|Path" tests/unittest/others/test_perf_sanity_bounds.py | head -10

Repository: NVIDIA/TensorRT-LLM

Length of output: 2914


Add complete annotations to all new functions using Python 3.10+ syntax.

All three helper functions and test functions lack required type annotations per coding guidelines.

  • tests/integration/defs/perf/test_perf_sanity.py#L128: Change Optional[int] to int | None.
  • tests/integration/defs/perf/test_perf_sanity.py#L144: Add proc: subprocess.Popen[bytes].
  • tests/integration/defs/perf/test_perf_sanity.py#L166: Add cmd: Sequence[str], env: dict[str, str], server_logs: list[str] | None.
  • tests/unittest/others/test_perf_sanity_bounds.py#L46-189: Add -> None to all test functions. Annotate fixture parameters: monkeypatch: MonkeyPatch (from _pytest.monkeypatch), tmp_path: Path (from pathlib).
📍 Affects 2 files
  • tests/integration/defs/perf/test_perf_sanity.py#L128-L141 (this comment)
  • tests/integration/defs/perf/test_perf_sanity.py#L144-L164
  • tests/integration/defs/perf/test_perf_sanity.py#L166-L235
  • tests/unittest/others/test_perf_sanity_bounds.py#L46-L189
🤖 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/integration/defs/perf/test_perf_sanity.py` around lines 128 - 141,
Complete the Python 3.10+ annotations for the new performance sanity helpers and
tests: in tests/integration/defs/perf/test_perf_sanity.py lines 128-141, change
_benchmark_client_timeout to return int | None; at lines 144-164, annotate the
process parameter as subprocess.Popen[bytes]; at lines 166-235, annotate cmd as
Sequence[str], env as dict[str, str], and server_logs as list[str] | None. In
tests/unittest/others/test_perf_sanity_bounds.py lines 46-189, add -> None to
every test function and annotate fixture parameters with MonkeyPatch and Path,
importing those types as needed.

Source: Coding guidelines

Comment thread tests/integration/defs/perf/test_perf_sanity.py
print_info(f"Server {self.disagg_serving_type} stopped")
server_proc.terminate()
server_proc.wait()
stop_process(server_proc, "server")

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Initialize process locals before entering try.

If open() or subprocess.Popen() fails, these finally blocks reference an unassigned local and mask the server-start failure with UnboundLocalError.

  • tests/integration/defs/perf/test_perf_sanity.py#L1571-L1571: initialize server_proc = None before try, then call stop_process only when it is assigned.
  • tests/integration/defs/perf/test_perf_sanity.py#L1599-L1599: initialize disagg_server_proc = None before try, then call stop_process only when it is assigned.
📍 Affects 1 file
  • tests/integration/defs/perf/test_perf_sanity.py#L1571-L1571 (this comment)
  • tests/integration/defs/perf/test_perf_sanity.py#L1599-L1599
🤖 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/integration/defs/perf/test_perf_sanity.py` at line 1571, Initialize
server_proc before its try block and guard the corresponding stop_process
cleanup so it runs only when a process was created; apply the same change to
disagg_server_proc and its finally cleanup in
tests/integration/defs/perf/test_perf_sanity.py at lines 1571-1571 and
1599-1599.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64030 [ run ] triggered by Bot. Commit: 8605b3a Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64030 [ run ] completed with state SUCCESS. Commit: 8605b3a
/LLM/main/L0_MergeRequest_PR pipeline #51960 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

@brnguyen2 brnguyen2 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.

Approving — the comments below are optional touch-ups, not blockers.

The harness change is right and the diagnostics you attach on expiry are the ones a triager actually needs. Main thing to fix before merge is the test wiring — as written, test_perf_sanity_bounds.py doesn't run anywhere in CI, so the bounds it pins are unprotected.

On the fixed 3600s: it isn't derived from anything the test declares. AggrTestCmds/the disagg cmds already carry self.timeout, and the PR description says real budgets are 18–41 min, so a stalled stage still overruns its own budget by up to 40 minutes before the bound fires. min(self.timeout, _benchmark_client_timeout()) — the same pattern already used for the readiness wait at test_perf_sanity.py:1241 — would make the bound track the test instead of a constant. Not a blocker, but worth considering while you're here.

if _INTEGRATION not in sys.path:
sys.path.insert(0, os.path.abspath(_INTEGRATION))

perf_sanity = pytest.importorskip("defs.perf.test_perf_sanity")

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.

Two problems that together mean this file never runs in CI:

  1. It's not in any tests/integration/test_lists/test-db/*.yml. The sibling file from [TRTLLM-13409][test] fail fast + surface server logs when a perf-sanity server dies or never becomes healthy #16403, unittest/others/test_http_utils_fail_fast.py, is listed in l0_cpu.yml:47 — add this one there too.
  2. It's missing pytestmark = pytest.mark.cpu_only. That sibling carries the marker with an explicit comment: the CPU-Generic stages select with -m cpu_only, so without it every test here is deselected and pytest exits 5.

Separately, importorskip is the wrong guard for a regression test. defs.perf.test_perf_sanity imports ..conftest, tensorrt_llm._utils, yaml, and test_common.* at module scope; if any of that breaks the whole file turns into a silent skip and the bounds you're pinning go unprotected while CI stays green. Import it directly (from defs.perf import test_perf_sanity) so a broken import is a failure.

# also drives wait_for_endpoint_ready()'s fast-fail, where a benign [E]
# during startup would start failing healthy runs. Keyword hits are
# best-effort; a raw tail is always attached.
keyword_hits = []

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.

keyword_hits is an unbounded list, but only the last 20 entries are ever used (keyword_hits[-20:]). The comment nine lines below explains that gen logs reach 77–232 MB and that materialising them risks an OOM on the already-failing rank — a log wedged in a retry loop emitting ConnectionRefusedError/TimeoutError per line is exactly the case that produces millions of hits here. Use deque(maxlen=20) for the same reason you used it for the tail.

return None if value <= 0 else value


def stop_process(proc, name: str, grace: int = SERVER_TERMINATE_GRACE_SEC) -> None:

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.

SIGKILL on server_proc reaches only the launcher. trtllm-serve forks MPI worker ranks for TP>1, and those don't get the signal — the old terminate(); wait() at least gave the parent a chance to reap them. The trade (a guaranteed hang for possible GPU-holding orphans) is probably still right inside a Slurm step whose cgroup gets cleaned up, but for a multi-client / multi-server-index test the next iteration runs in the same allocation and will hit "device in use".

Cheap fix: pass start_new_session=True at the three Popen sites (1232, 1558, 1586) and os.killpg(os.getpgid(proc.pid), SIGKILL) here on escalation.

f"falling back to {DEFAULT_BENCHMARK_CLIENT_TIMEOUT}s"
)
return DEFAULT_BENCHMARK_CLIENT_TIMEOUT
return None if value <= 0 else value

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.

The zero/negative convention here is the opposite of server_ready_timeout() at line 248, which treats <= 0 as "invalid, ignore, use the default". Here 0 means "disable the bound entirely" and a negative value disables it silently (no print_info, unlike the ValueError path). Two adjacent timeout knobs in the same file reading 0 in opposite directions is a foot-gun for whoever sets these in a Jenkins stage. At minimum log when a value <= 0 disables the bound.

tail_blob = "\n".join(tails) if tails else "<no server logs readable>"

raise RuntimeError(
f"Benchmark client made no progress for {elapsed:.0f}s "

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.

"made no progress for Ns" isn't what was measured — the client may well have been progressing, just slower than the budget (large ISL/OSL, high concurrency, an over-tight override). Someone triaging from this line will go looking for a stall that isn't there. "did not finish within {elapsed:.0f}s" states exactly what the harness knows.

@yufeiwu-nv
yufeiwu-nv removed their request for review August 5, 2026 23:32
…fails on its own

Perf-sanity stages could not fail by themselves. The benchmark client ran
under subprocess.check_output() with no timeout, and the file-rendezvous
polls are bounded by DEFAULT_TIMEOUT (10800s), above the pytest per-test
marker. NVIDIA#16403 bounded the /health readiness wait (30 min agg, 60 min
disagg), but that is the startup phase; nothing bounded the steady-state
benchmark run after /health had answered. A stall there surfaced only as
'the client is still running' until Slurm or Jenkins killed the stage, with
no results XML and no diagnostic.

Measured over the five days after NVIDIA#16961: 13 disagg hangs / 143 GPU-h, of
which 115 GPU-h were multi-hour stages with no identified cause -- 1.7-2.3h
runtimes against 18-41 minute budgets. Those exceed the 60-minute readiness
bound, so they are in the client phase.

- run_benchmark_client() runs the client under a deadline (default 3600s,
  TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC to change, 0 disables). On expiry it
  kills the client and raises naming the elapsed time and the knob, with
  server-side keyword hits, a tail of each server log, and the partial
  client output attached. check_output semantics are otherwise preserved.
  Applied to both the aggregated and disaggregated client paths.

- stop_process() replaces the bare terminate(); wait() at the three server
  teardown sites, escalating to SIGKILL after a grace period. A rank wedged
  in a non-interruptible native call never runs the Python signal handler.

- The timeout path attaches a raw log tail as well as the check_error()
  keyword scan, because ERROR_KEYWORDS are Python exception names and do
  not match '[TRT-LLM] [E]' lines. ERROR_KEYWORDS is deliberately left
  alone: it also drives wait_for_endpoint_ready()'s fast-fail, where a
  benign [E] during startup would begin failing healthy runs.

  The tail streams via deque(maxlen=N) rather than readlines(). Measured
  perf-sanity gen logs reach 77-232 MB, so materialising the whole file
  risked an OOM on the rank that is already failing, swallowing the very
  timeout report being assembled.

This bounds the stage regardless of why it stalled; it does not diagnose or
fix any particular hang.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
The 12 tests added with this change never ran: pipeline 51960 reports
l_test_count_passed=4, with the file absent from collection entirely.

The list entry was not the problem -- l0_cpu.yml covers the whole
unittest/others directory. The marker was. tests/unittest/conftest.py's
pytest_ignore_collect drops any file whose source lacks the literal
"pytest.mark.cpu_only" when pytest runs with -m cpu_only, which is how the
CPU-Generic stage invokes it. Add the marker, matching the sibling
unittest/others/test_http_utils_fail_fast.py.

Nothing here needs a GPU: the tests exercise the client-deadline resolution
and the SIGTERM->SIGKILL teardown escalation using subprocesses spawned
from sys.executable. The module-under-test import resolves in the stage
because the harness runs `python -m pytest` with cwd=tests/, which puts
tests/ on sys.path for `test_common`; verified by replaying that sys.path
locally, where the import advances past test_common and stops only at
tensorrt_llm, which the CPU stage has installed.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
@JunyiXu-nv
JunyiXu-nv force-pushed the dev-junyix-fix-perf-sanity-harness-bounds branch from 8605b3a to 5f9b978 Compare August 12, 2026 16:46
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@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: 4

🧹 Nitpick comments (1)
tests/integration/defs/perf/test_perf_sanity.py (1)

102-237: 📐 Maintainability & Code Quality | 🔵 Trivial

Test coverage summary.

Changed test-code file: tests/integration/defs/perf/test_perf_sanity.py. No test functions were added, modified, or removed in this file. The change adds three harness helpers: _benchmark_client_timeout, stop_process, and run_benchmark_client. Existing perf-sanity test entries are unchanged, so no updates to tests/integration/test_lists/test-db/ or tests/integration/test_lists/qa/ are required for this file.

Covered by tests/unittest/others/test_perf_sanity_bounds.py: timeout resolution, cooperative and stubborn process termination, already-exited processes, successful client output, and nonzero client exit.

Coverage gaps:

  1. No test asserts the behavior for a negative TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC.
  2. No test passes a nonexistent server-log path to run_benchmark_client on the timeout path. That case exercises the unguarded check_error() call.

Verdict: insufficient. Add the two cases above to tests/unittest/others/test_perf_sanity_bounds.py.

As per path instructions: "Always produce a test coverage summary, even if no issues are found" and the summary must list changed test functions, list-file status, and a coverage verdict.

🤖 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/integration/defs/perf/test_perf_sanity.py` around lines 102 - 237, Add
tests in test_perf_sanity_bounds.py for a negative
TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC value, verifying the intended timeout
resolution, and for run_benchmark_client timing out with a nonexistent
server-log path, verifying it reports the timeout without crashing in
check_error(). Preserve existing coverage and keep the test-list files
unchanged.

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 `@tests/integration/defs/perf/test_perf_sanity.py`:
- Around line 203-216: Update the server-log handling around check_error() so
unreadable or vanishing logs are caught without interrupting timeout diagnostic
assembly. Catch OSError from check_error(log_path) as well as the existing
open(log_path) call, preserve available keyword hits and continue processing
remaining logs. Add coverage for missing or unreadable server logs if the test
structure supports it.

In `@tests/unittest/others/test_perf_sanity_bounds.py`:
- Around line 134-156: Update test_client_hang_is_bounded_and_names_the_knob so
the simulated client emits a flushed progress line before sleeping, then assert
that this line appears in the RuntimeError message alongside the existing
timeout details.
- Line 51: Add return annotations of -> None to all 12 test functions in this
test module, including test_client_timeout_defaults_to_one_hour and the other
timeout, teardown, failure, and diagnostics tests. Annotate fixture parameters
with concrete types where applicable, using pytest.MonkeyPatch for monkeypatch
fixtures and pathlib.Path for path fixtures.

---

Nitpick comments:
In `@tests/integration/defs/perf/test_perf_sanity.py`:
- Around line 102-237: Add tests in test_perf_sanity_bounds.py for a negative
TRTLLM_PERF_SANITY_CLIENT_TIMEOUT_SEC value, verifying the intended timeout
resolution, and for run_benchmark_client timing out with a nonexistent
server-log path, verifying it reports the timeout without crashing in
check_error(). Preserve existing coverage and keep the test-list files
unchanged.
🪄 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: d97da3d6-0a24-4c1b-a906-67f5dc4ae3ad

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3cbe7 and 5f9b978.

📒 Files selected for processing (2)
  • tests/integration/defs/perf/test_perf_sanity.py
  • tests/unittest/others/test_perf_sanity_bounds.py

Comment on lines +203 to +216
for log_path in server_logs or []:
base = os.path.basename(log_path)
for line_idx, line in check_error(log_path):
keyword_hits.append(f"{base}:{line_idx}: {line}")
try:
with open(log_path, "r", errors="replace") as handle:
# deque(maxlen=) streams the file and keeps only the tail.
# readlines() would materialise the whole log first, and
# measured perf-sanity gen logs reach 77-232 MB -- risking
# an OOM on the rank that is already failing, which would
# swallow the very timeout report we are assembling.
tail = list(deque(handle, maxlen=SERVER_LOG_TAIL_LINES))
except OSError:
continue

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate error_utils and inspect check_error's file handling.
fd -t f 'error_utils.py' | while IFS= read -r f; do
  echo "=== $f ==="
  ast-grep run --pattern $'def check_error($$$):
    $$$' --lang python "$f"
done

Repository: NVIDIA/TensorRT-LLM

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -eu

f="tests/test_common/error_utils.py"
echo "=== $f outline ==="
ast-grep outline "$f" || true

echo "=== check_error definition and nearby code ==="
rg -n -A80 -B20 '(^|[[:space:]])def[[:space:]]+check_error|check_error[[:space:]]*=' "$f" || true

echo "=== target call site and related log helpers ==="
rg -n -A35 -B25 'check_error|get_server_logs|SERVER_LOG_TAIL_LINES' tests/integration/defs/perf/test_perf_sanity.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 28937


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import builtins
import os

# Probe the exact control flow in tests/test_common/error_utils.py without
# importing or executing repository code.
def check_error_probe(file_path, *, exists, opener):
    if not exists(file_path):
        return []
    with opener(file_path, "r", errors="replace"):
        return []

class Handle:
    def __enter__(self):
        return self
    def __exit__(self, *_):
        return False

print("missing-at-check: ", check_error_probe(
    "missing.log", exists=lambda _: False, opener=builtins.open
))

try:
    check_error_probe(
        "raced-away.log",
        exists=lambda _: True,
        opener=lambda *_args, **_kwargs: (_ for _ in ()).throw(
            FileNotFoundError("raced-away.log")
        ),
    )
except OSError as exc:
    print("exists-then-open failure:", type(exc).__name__, str(exc))

try:
    check_error_probe(
        "unreadable.log",
        exists=lambda _: True,
        opener=lambda *_args, **_kwargs: (_ for _ in ()).throw(
            PermissionError("unreadable.log")
        ),
    )
except OSError as exc:
    print("unreadable failure:", type(exc).__name__, str(exc))
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 289


🏁 Script executed:

#!/bin/bash
set -eu

echo "=== tests related to run_benchmark_client and timeout diagnostics ==="
rg -n -S -g '*.py' 'run_benchmark_client|BENCHMARK_CLIENT_TIMEOUT_ENV_VAR_NAME|server-side error keywords|<no server logs readable>' tests || true

echo "=== applicable integration test lists ==="
rg -n -S 'test_perf_sanity|perf_sanity' tests/integration/test_lists || true

Repository: NVIDIA/TensorRT-LLM

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu

f="tests/unittest/others/test_perf_sanity_bounds.py"
echo "=== test functions ==="
rg -n '^def test_|^async def test_' "$f"

echo "=== timeout-test coverage ==="
sed -n '1,220p' "$f"

Repository: NVIDIA/TensorRT-LLM

Length of output: 9058


Handle unreadable server logs in check_error().

check_error() handles an already-missing path, but open() can still raise OSError after os.path.exists() succeeds. Catch this exception so a log race or permission error does not replace the timeout diagnostic.

Test coverage: Existing timeout tests cover readable logs, but not missing or unreadable logs. Coverage is insufficient.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 207-207: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(log_path, "r", errors="replace")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 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/integration/defs/perf/test_perf_sanity.py` around lines 203 - 216,
Update the server-log handling around check_error() so unreadable or vanishing
logs are caught without interrupting timeout diagnostic assembly. Catch OSError
from check_error(log_path) as well as the existing open(log_path) call, preserve
available keyword hits and continue processing remaining logs. Add coverage for
missing or unreadable server logs if the test structure supports it.

Source: Linters/SAST tools

# ---------------------------------------------------------------------------


def test_client_timeout_defaults_to_one_hour(monkeypatch):

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="tests/unittest/others/test_perf_sanity_bounds.py"

printf '%s\n' '--- file inventory ---'
git ls-files "$file" CODING_GUIDELINES.md 'tests/integration/test_lists/*' | sed -n '1,160p'

printf '%s\n' '--- target file size ---'
wc -l "$file"

printf '%s\n' '--- target file ---'
cat -n "$file"

printf '%s\n' '--- coding guideline references ---'
if [ -f CODING_GUIDELINES.md ]; then
  rg -n -C 3 'Annotate every function|test|copyright|Python' CODING_GUIDELINES.md | sed -n '1,220p'
else
  printf '%s\n' 'CODING_GUIDELINES.md not found at repository root'
fi

printf '%s\n' '--- target module in test lists ---'
rg -n -C 2 'test_perf_sanity_bounds|perf_sanity_bounds' tests/integration/test_lists 2>/dev/null || true

printf '%s\n' '--- AST signature report ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("tests/unittest/others/test_perf_sanity_bounds.py")
tree = ast.parse(path.read_text())
for node in tree.body:
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_"):
        args = [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]
        annotations = {
            arg.arg: ast.unparse(arg.annotation)
            for arg in args
            if arg.annotation is not None
        }
        if node.args.vararg is not None:
            annotations["*" + node.args.vararg.arg] = (
                ast.unparse(node.args.vararg.annotation)
                if node.args.vararg.annotation is not None else None
            )
        if node.args.kwarg is not None:
            annotations["**" + node.args.kwarg.arg] = (
                ast.unparse(node.args.kwarg.annotation)
                if node.args.kwarg.annotation is not None else None
            )
        print(
            f"{node.lineno}: {node.name} "
            f"return={ast.unparse(node.returns) if node.returns else None} "
            f"params={annotations}"
        )
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 18051


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- annotation guideline ---'
rg -n -C 5 'Annotate every function|function annotations|return annotations|type annotations' CODING_GUIDELINES.md

printf '%s\n' '--- test review and test-list guidance ---'
rg -n -C 8 'test coverage|test list|test-list|tests/integration/test_lists|test functions' CODING_GUIDELINES.md .github 2>/dev/null | sed -n '1,260p'

printf '%s\n' '--- all test-list references to unit tests or perf sanity ---'
rg -n -i -C 2 'unittest|test_perf_sanity|perf.?sanity|sanity_bounds' tests/integration/test_lists 2>/dev/null | sed -n '1,260p'

printf '%s\n' '--- nearby unit-test signatures ---'
find tests/unittest -maxdepth 3 -type f -name '*.py' -print 2>/dev/null \
  | sort \
  | head -n 80 \
  | while IFS= read -r path; do
      rg -n '^def test_|^async def test_' "$path" 2>/dev/null | head -n 8 | sed "s#^#$path:#"
    done

Repository: NVIDIA/TensorRT-LLM

Length of output: 186


🏁 Script executed:

#!/bin/bash

printf '%s\n' '--- Python guideline section ---'
sed -n '479,530p' CODING_GUIDELINES.md

printf '%s\n' '--- test review instructions in repository ---'
rg -n -i -C 8 'test coverage|test list|test-list|tests/integration/test_lists|test functions' CODING_GUIDELINES.md .github 2>/dev/null \
  | sed -n '1,300p' || true

printf '%s\n' '--- all test-list references to unit tests or perf sanity ---'
rg -n -i -C 2 'unittest|test_perf_sanity|perf.?sanity|sanity_bounds' tests/integration/test_lists 2>/dev/null \
  | sed -n '1,300p' || true

printf '%s\n' '--- test-list file shapes ---'
for path in \
  tests/integration/test_lists/test-db/l0_cpu.yml \
  tests/integration/test_lists/test-db/l0_perf.yml \
  tests/integration/test_lists/test-db/l0_sanity_check.yml
do
  if [ -f "$path" ]; then
    printf '\n--- %s ---\n' "$path"
    cat -n "$path" | sed -n '1,180p'
  fi
done

printf '%s\n' '--- nearby unit-test signatures ---'
find tests/unittest -maxdepth 3 -type f -name '*.py' -print 2>/dev/null \
  | sort \
  | head -n 80 \
  | while IFS= read -r path; do
      matches=$(rg -n '^def test_|^async def test_' "$path" 2>/dev/null || true)
      if [ -n "$matches" ]; then
        printf '%s\n' "$matches" | head -n 8 | sed "s#^#$path:#"
      fi
    done

Repository: NVIDIA/TensorRT-LLM

Length of output: 50375


Add function annotations.

Add -> None to all 12 test functions. Annotate fixtures with concrete types, such as pytest.MonkeyPatch and pathlib.Path.

Test coverage: 12 tests cover timeout bounds, teardown escalation, client failures, and log diagnostics. Coverage verdict: sufficient.

🤖 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/others/test_perf_sanity_bounds.py` at line 51, Add return
annotations of -> None to all 12 test functions in this test module, including
test_client_timeout_defaults_to_one_hour and the other timeout, teardown,
failure, and diagnostics tests. Annotate fixture parameters with concrete types
where applicable, using pytest.MonkeyPatch for monkeypatch fixtures and
pathlib.Path for path fixtures.

Source: Coding guidelines

Comment on lines +92 to +104
ignores_sigterm = (
"import signal, time\nsignal.signal(signal.SIGTERM, signal.SIG_IGN)\ntime.sleep(60)\n"
)
proc = subprocess.Popen([sys.executable, "-c", ignores_sigterm])
# Let the child install its handler before we signal it.
time.sleep(1.0)

started = time.monotonic()
perf_sanity.stop_process(proc, "stubborn", grace=3)
elapsed = time.monotonic() - started

assert proc.poll() is not None, "SIGKILL should have reaped the process"
assert elapsed < 30, f"stop_process took {elapsed:.1f}s; it must not wait out the full sleep"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prove that SIGKILL escalation occurred.

The fixed one-second delay does not prove that the child installed its SIGTERM handler. If the child starts late, SIGTERM terminates it and all current assertions pass without testing escalation.

Make the child signal readiness after it installs the handler. Then assert that proc.returncode is -signal.SIGKILL.

🧰 Tools
🪛 ast-grep (0.45.1)

[error] 94-94: Command coming from incoming request
Context: subprocess.Popen([sys.executable, "-c", ignores_sigterm])
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.1)

[error] 95-95: subprocess call: check for execution of untrusted input

(S603)

Comment on lines +134 to +156
def test_client_hang_is_bounded_and_names_the_knob(monkeypatch, tmp_path):
"""The regression that mattered: a client that never returns.

Before, this ran until Slurm killed the stage hours later. It must now
raise promptly, name the env var, and carry the server-side errors.
"""
monkeypatch.setenv(perf_sanity.BENCHMARK_CLIENT_TIMEOUT_ENV_VAR_NAME, "2")
server_log = tmp_path / "trtllm-serve.CTX_0.0.log"
server_log.write_text("some line\n[TRT-LLM] [E] Error in event loop: boom\n")

started = time.monotonic()
with pytest.raises(RuntimeError) as excinfo:
perf_sanity.run_benchmark_client(
[sys.executable, "-c", "import time; time.sleep(120)"],
dict(os.environ),
[str(server_log)],
)
elapsed = time.monotonic() - started

msg = str(excinfo.value)
assert perf_sanity.BENCHMARK_CLIENT_TIMEOUT_ENV_VAR_NAME in msg
assert "made no progress" in msg
assert elapsed < 60, f"bound did not fire promptly ({elapsed:.1f}s)"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert partial client output in the timeout report.

This test verifies the timeout message but not the partial-output contract. Make the client print a flushed progress line before sleeping, then assert that the line is included in RuntimeError.

This protects the diagnostic behavior required when a benchmark client stalls.

🤖 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/others/test_perf_sanity_bounds.py` around lines 134 - 156,
Update test_client_hang_is_bounded_and_names_the_knob so the simulated client
emits a flushed progress line before sleeping, then assert that this line
appears in the RuntimeError message alongside the existing timeout details.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65610 [ run ] triggered by Bot. Commit: 5f9b978 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65610 [ run ] completed with state FAILURE. Commit: 5f9b978
/LLM/main/L0_MergeRequest_PR pipeline #53337 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

@BowenFu BowenFu 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.

Changes required: terminate the complete server process tree.\n\nNVBug demonstrates the exact gap here: after the pytest thread timeout, MPI worker PIDs retained about , and later GLM cases OOMed in the same allocation. The current sends / only to ; descendants can therefore outlive the launcher.\n\nPlease start each server in its own session/process group and terminate that group, or use a verified process-tree cleanup helper. Add a regression where a descendant ignores and prove both parent and descendant are gone. CI pipeline failed on an unrelated invalid test-list entry.\n\nThis is required before merge.

@BowenFu
BowenFu dismissed their stale review August 13, 2026 14:04

Replacing this review because shell formatting stripped its inline identifiers.

@BowenFu BowenFu 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.

Changes required: terminate the complete server process tree.

NVBug 6526562 demonstrates the exact gap here: after the pytest thread timeout, MPI worker PIDs retained about 255.74 GiB, and later GLM cases OOMed in the same allocation. The current stop_process() sends SIGTERM/SIGKILL only to server_proc; descendants can therefore outlive the launcher.

Please start each server in its own session/process group and terminate that group, or use a verified process-tree cleanup helper. Add a regression where a descendant ignores SIGTERM and prove both parent and descendant are gone. CI pipeline #53337 failed on an unrelated invalid test-list entry.

This is required before merge.

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