Skip to content

[None][infra] Echo test output to the console on post-merge multi-GPU stages - #16935

Open
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-hang-observability-v2
Open

[None][infra] Echo test output to the console on post-merge multi-GPU stages#16935
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-hang-observability-v2

Conversation

@JunyiXu-nv

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

Copy link
Copy Markdown
Collaborator

Problem

Multi-GPU CI stages already run pytest with -s, but the S3 log plugin then points fd 1/2 at session spool files and publishes each test's slice only once that test finishes. A test that wedges never finishes — pytest's --timeout=3600 --timeout-method=thread ends the process with os._exit — so nothing that test, or any MPI worker rank that inherited those fds, wrote ever reaches the stage log.

Concretely: RTXPro6000D-4_GPUs-PyTorch-Post-Merge-2 in run 2220 produced a 749 KB node log with zero [TRT-LLM] lines for a 60-minute wedge. The stack dump that identified the root cause was only recoverable by downloading and unpacking results-<stage>.tar.gz by hand.

For calibration, zero [TRT-LLM] is the norm rather than a hang signal: 61 of 63 sampled multi-GPU stage logs contain none, including 42 of 42 that passed.

Change

  1. SessionSpoolEchoer — makes --s3-echo-stdout work with the default session capture mode by tailing the spool files onto the saved real fds from one session-scoped thread. Capture, per-test slicing and upload are untouched; only the console copy is best-effort, and the spool is still written synchronously by the producer, so an abrupt kill cannot lose more than one poll interval.

    Deliberately not switching stages to timestamped mode: measured 0/5 runs preserved pytest-timeout's dump in that mode versus 5/5 in session mode.

  2. FDRedirector reader fix--s3-capture-mode=timestamped already accepted --s3-echo-stdout, but its reader called read(4096) on a buffered text stream (os.fdopen(pipe_read, "r", ...)), which blocks until 4096 characters or EOF. The trailing partial chunk — exactly where a hang dump lands, since the writer then stops producing — was never drained. Measured: 183/200 chatter lines echoed but 0/3 detector lines. Now reads the raw pipe and decodes incrementally.

  3. shouldEchoTestOutputToConsole(stageName) — post-merge ∧ >1 GPU ∧ ¬PerfSanity, reusing the existing parseTaskConfigFromStageName. Simulated against all 132 declared stages: 22 enabled, 13 PerfSanity excluded.

Test evidence

Unit: 3 new tests in tests/unittest/test_s3_output.py; 23/23 pass.

A/B on real hardware (GB200, 4 GPUs, SLURM + pyxis): the shipped HangDetector running in a child process that inherits pytest's fd 1/2 — the shape of an MPI worker rank — terminated by a real os.kill(SIGKILL). Same container and same pytest flags CI builds; before uses pristine origin/main test_common.

Before — stage log 687 B, ends mid-line, zero detector output. Spool (2,096 B) holds the whole dump.

After — stage log 3,275 B:

[TRT-LLM] [E] [_torch] Hang detected after 20 seconds.
[TRT-LLM] [E] [_utils] Thread 281452144357760 stack trace:
  File ".../pyexecutor/hang_detector.py", line 126, in _detect_hang
    print_all_stacks()
[TRT-LLM] [E] Hang detected on rank 1 in PyExecutor; hard-killing and propagating to peer ranks.
[TRT-LLM] [E] HangDetector: self-SIGKILL; relying on the launcher to propagate to peer ranks.

Spool unchanged at 2,097 B — capture/upload intact. The echo won the race against SIGKILL with zero injected delay.

Running scan_hangkill_v2.py's verbatim patterns over the two real logs: before hangkill=False; after hangkill=True, timeout_s=[20], kill_path=self-SIGKILL.

Cost

Roughly 340–450 KB of extra console per executed test. Deliberately not stated as a multiplier: the same measurement over two post-merge runs of the same 15 stages gave 14.8x and 0.75x, because one executed 71% of its tests and the other 5% (the rest reused). Console size is dominated by shell trace and barely moves; captured output does not.

Note this measurement method reads 0 captured bytes for stages that were hard-killed before publishing, so it is a lower bound precisely on the stages this targets.

Limitations, stated plainly

  • A TP=2 run with a real model was not completed: MPI_Comm_spawn does not work under pyxis/enroot here, and mpirun -n 1 reroutes worker stdout to mpirun, which is the exact property under test. The chain "MPI worker's fd 1 == pytest's fd 1" rests on code inspection (logger.py:188 StreamHandler(sys.stdout)) plus the Jenkins invocation pattern, not on a TP=2 run.
  • pytest-timeout's own dump is written <250 ms before os._exit, so it usually lands in the spool but not the console. Unchanged by this PR; the spool copy is not regressed.
  • One test, `TestLlama3_1_8BInstruct::test_kv_cache_v2_nixl_python", reportedly emits ~39 MB of stdout (~83% of its stage). Capping it is a worthwhile separate change; deliberately left out to keep this PR to one concern.

Dev Engineer Review

  • Added post-merge multi-GPU–only stdout echo gating in jenkins/L0_Test.groovy via new shouldEchoTestOutputToConsole(stageName) helper, excluding PerfSanity, and honoring a new DISABLE_POST_MERGE_STDOUT_ECHO / disableStdoutEchoOnPostMerge kill switch to fully disable console echo in those stages.
  • Implemented session-scoped console echo for S3 spool capture in tests/test_common/s3_output.py with a new SessionSpoolEchoer thread that tails session spool files and writes new bytes to the original console fds.
  • Extended capture APIs to support echo-to-console (SessionCapture(..., echo_to_console=False)), exposed SessionFDSpool.console_fd, and plumbed the early option through tests/test_common/s3_output_hooks.py (--s3-echo-stdout).
  • Fixed incremental decoding in FD-based capture (FDRedirector) to avoid UTF-8 boundary corruption, and ensured echoed output uses raw bytes when echoing to the original fd.
  • Relaxed --s3-echo-stdout validation constraints to only reject --s3-capture-mode=direct, and updated CLI help text accordingly.
  • Follow-up hardening covered broken pipe/read/write error handling (dropping failed readers to avoid EPIPE storms), safer shutdown/closure coordination, best-effort thread startup, and limiting write size to prevent oversized pipe writes; added a session-wide console byte budget (128 MiB) and a build parameter to disable echo on post-merge stages.

QA Engineer Review

Test code added/updated:

  • tests/unittest/test_s3_output.py
    • Added:
      • test_session_capture_echoes_output_to_console_before_the_test_ends
      • test_session_capture_does_not_echo_to_console_by_default
      • test_session_echo_drops_a_reader_whose_console_is_gone
      • test_session_echo_stops_after_the_byte_budget
      • test_session_capture_survives_an_echo_thread_that_cannot_start
      • test_session_echo_writes_are_atomic_sized
      • test_fd_redirector_echoes_a_partial_chunk_without_waiting_for_more
  • tests/unittest/tools/test_test_to_stage_mapping.py
    • Updated validation logic to accept --s3-echo-stdout when either ENABLE_S3_ECHO_STDOUT is set or when the new shouldEchoTestOutputToConsole(stageName) post-merge / non-PerfSanity / GPU-gated rules allow it, while still enforcing the disable kill switch.

Coverage mapping (per provided instructions):

  • These test changes are within tests/ (outside tests/integration/test_lists/), so they are covered by tests/integration/test_lists/test-db/ only if separately mapped by CI; no explicit updates to those lists are indicated in the provided change summary.
  • Verdict: needs follow-up — CBTS coverage data is unavailable.

… stages

Multi-GPU CI stages already run pytest with `-s`, but the S3 log plugin then
points fd 1 and 2 at session spool files and publishes each test's slice only
once that test finishes. A test that wedges never finishes: pytest's
`--timeout=3600 --timeout-method=thread` ends the process with `os._exit`, so
nothing that test -- or any MPI worker rank that inherited those fds -- wrote
reaches the stage log. On `RTXPro6000D-4_GPUs-PyTorch-Post-Merge-2` in run 2220
that left a 749 KB node log with zero `[TRT-LLM]` lines for a 60-minute wedge.
That is not specific to hangs: 61 of 63 sampled multi-GPU stage logs contain no
`[TRT-LLM]` line at all, including 42 of 42 that passed.

The output is not destroyed -- the spool ships inside `results-<stage>.tar.gz`
-- but recovering it means knowing to download and unpack an artifact, which is
not how a stage failure gets triaged. The change is about diagnostic reach: put
the evidence where the failure is read.

Teach `--s3-echo-stdout` to work with the default session capture mode by
tailing the spool files onto the real console from a single session-scoped
thread. Capture, per-test slicing and uploading are unchanged; only the console
copy is best-effort, and the spool is still written synchronously by the
producer, so an abrupt kill cannot lose more than one poll interval.

`--s3-capture-mode=timestamped` already accepted `--s3-echo-stdout`, but its
reader called `read(4096)` on a buffered *text* stream, which blocks until it
has 4096 characters or sees EOF. The trailing partial chunk -- exactly where a
hang dump lands, since the writer then stops producing -- was never drained.
Read the raw pipe instead and decode incrementally.

Enable the flag for post-merge multi-GPU stages only (22 stages). Those are
where a lost stage costs the most GPU-hours per occurrence; pre-merge keeps its
current, quieter console. PerfSanity stages are excluded. Measured cost is
roughly 340-450 KB of extra console per executed test, so the increase tracks
how much of a run actually executes rather than being a fixed multiple.

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

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Jenkins selectively enables --s3-echo-stdout for eligible multi-GPU post-merge stages. Pytest capture propagates the option to session capture, which tails S3 spool files and echoes output live. FD redirection uses binary reads with incremental UTF-8 decoding, with new tests covering timing and defaults.

Changes

S3 console echo

Layer / File(s) Summary
Stage eligibility selection
jenkins/L0_Test.groovy, tests/unittest/tools/test_test_to_stage_mapping.py
Adds kill-switch, stage-name, performance-stage, and GPU-count checks before appending --s3-echo-stdout; tests enforce the scoped gating.
Capture option plumbing
tests/test_common/s3_output_hooks.py, tests/test_common/s3_output.py
Parses and propagates the echo flag, enables session spool echoing, updates mode validation, and revises CLI help text.
Session spool echo
tests/test_common/s3_output.py, tests/unittest/test_s3_output.py
Tails spool files to console descriptors with bounded output and failure handling, with tests for live output, reader removal, byte limits, startup failures, and atomic writes.
Binary pipe echo and decoding
tests/test_common/s3_output.py, tests/unittest/test_s3_output.py
Uses unbuffered binary pipe reads with incremental UTF-8 decoding and tests prompt echoing of partial chunks.

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

Sequence Diagram(s)

sequenceDiagram
  participant Jenkins
  participant PytestHooks
  participant SessionCapture
  participant SessionSpoolEchoer
  participant ConsoleFD
  Jenkins->>PytestHooks: pass --s3-echo-stdout
  PytestHooks->>SessionCapture: set echo_to_console
  SessionCapture->>SessionSpoolEchoer: start spool tailing
  SessionSpoolEchoer->>ConsoleFD: write new spool bytes
  SessionCapture->>SessionSpoolEchoer: stop and drain before closing spool fds
Loading

Suggested reviewers: brnguyen2

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: echoing test output to the console on post-merge multi-GPU stages.
Description check ✅ Passed It clearly explains the problem, solution, testing, and limitations, though it does not follow the template's exact section headings.
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: 1

🧹 Nitpick comments (1)
tests/unittest/test_s3_output.py (1)

458-560: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage summary (QA).

  • Test functions added: test_session_capture_echoes_output_to_console_before_the_test_ends, test_session_capture_does_not_echo_to_console_by_default, test_fd_redirector_echoes_a_partial_chunk_without_waiting_for_more (plus helper _drain_console_pipe).
  • Test list registration: these are standard tests/unittest/ pytest tests, which TensorRT-LLM CI collects and runs as a whole rather than via explicit entries in tests/integration/test_lists/test-db or qa/ (those lists gate GPU-specific integration test selection). No new list entries are required.
  • Verdict: sufficient — the three tests directly cover the PR's stated goals (live echo during a hang, default no-echo behavior, and prompt partial-chunk echo for the binary pipe change). One gap: none of the new tests exercise a broken echo destination (e.g., os.write to a closed/broken console fd) — this matters given the major issue raised on FDRedirector._reader_loop in tests/test_common/s3_output.py; consider adding that regression test once the write-failure handling is fixed there.
🤖 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/test_s3_output.py` around lines 458 - 560, Add a regression
test covering a broken echo destination for FDRedirector._reader_loop, such as
closing the original console file descriptor before captured output is written.
Verify the reader handles the write failure without hanging or crashing and
still completes cleanup, using the existing FDRedirector test patterns.

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/test_common/s3_output.py`:
- Around line 382-388: Update `_reader_loop` to isolate `os.write(self.saved_fd,
raw)` failures from the primary log-file capture: catch echo-specific `OSError`
without terminating the loop, while preserving per-test log writing. Replace the
single echo write with full-buffer retry behavior equivalent to
`SessionSpoolEchoer._write_all`, ensuring partial writes continue until all
bytes are flushed or the echo destination fails.

---

Nitpick comments:
In `@tests/unittest/test_s3_output.py`:
- Around line 458-560: Add a regression test covering a broken echo destination
for FDRedirector._reader_loop, such as closing the original console file
descriptor before captured output is written. Verify the reader handles the
write failure without hanging or crashing and still completes cleanup, using the
existing FDRedirector test patterns.
🪄 Autofix (Beta)

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: 3dfbb1d7-60f8-4a21-ab42-8ab2b961ac5b

📥 Commits

Reviewing files that changed from the base of the PR and between cfebf19 and 23a3210.

📒 Files selected for processing (4)
  • jenkins/L0_Test.groovy
  • tests/test_common/s3_output.py
  • tests/test_common/s3_output_hooks.py
  • tests/unittest/test_s3_output.py

Comment thread tests/test_common/s3_output.py
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62129 [ run ] triggered by Bot. Commit: 23a3210 Link to invocation

@JunyiXu-nv
JunyiXu-nv marked this pull request as draft July 28, 2026 06:20
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Converting to draft — an adversarial review found a blocker plus four real defects. Fixes in progress; I'll mark ready once they land and CI is clean.

Blocker. This PR breaks an existing pre-merge test on every PR, not just post-merge ones: tests/unittest/tools/test_test_to_stage_mapping.py::test_s3_stdout_echo_requires_explicit_opt_in asserts that every line containing --s3-echo-stdout has if (ENABLE_S3_ECHO_STDOUT) within the 3 preceding lines. The new line at L0_Test.groovy:1254 is guarded by if (!ENABLE_S3_ECHO_STDOUT && shouldEchoTestOutputToConsole(stageName)), which does not contain that substring. Reproduced locally: 1 failed, 5 passed. unittest/tools runs on A10-PyTorch-1..4 pre-merge (l0_a10.yml). The test encodes a deliberate invariant this PR intentionally changes, so it needs updating in the same commit with the rationale — not a regex workaround.

Also confirmed, with reproductions:

  1. The pytest-timeout stack dump is still lost — 20/20 runs. timeout_timer flushes and calls os._exit(1) while the 0.25 s poll thread is sleeping. Not a regression (the timestamped path loses it too), but the Groovy comment claiming the wedge now leaves a trace overstates it. What is gained is everything printed up to ~0.25 s before the kill, including Hang detected lines — which is the real value and should be how it's described.
  2. EPIPE becomes a self-feeding warning storm into the spool_drain catches OSError, logs, and retries forever at ~4 warnings/s; each warning goes to stderr → the spool → read back → EPIPE again. Over a 4 h stage that is ~57k junk lines injected into the upload artifact this PR promises not to regress.
  3. 5 s shutdown stall plus an uncaught ValueError traceback on the consolestop() closes readers while the thread may still be running; read() on a closed FileIO raises ValueError, which _drain does not catch.
  4. Thread-start failure can abort the pytest session silentlyself._echoer.start() sits outside the try/except that unwinds spools, so a RuntimeError("can't start new thread") leaves fd 1/2 pinned to the spool and pytest's own error written into it.

Open question for whoever reviews next: the gate enables full stdout+stderr echo on 22 stages, 3 of which are Slurm multi-node where the job log is tail -f'd over SSH into the Jenkins console. There is currently no byte cap and no kill switch short of another merge. A cap or a Jenkins param may be warranted.

Verified non-issues, so nobody re-checks them: incremental UTF-8 decode is correct across split multi-byte boundaries (0 replacement chars); no thread leak under threadleak = True (session-scoped thread pre-exists every test); no double --s3-echo-stdout; gate arithmetic is exactly 22 of 128 stages with no null/throw path.

…ariant test

Follow-up to the console echo change; five defects found in review.

Update `test_s3_stdout_echo_requires_explicit_opt_in`, which asserted that
every `--s3-echo-stdout` line is guarded by `if (ENABLE_S3_ECHO_STDOUT)`. That
invariant is what this feature deliberately widens, so the test now encodes the
new rule instead: echo must be reachable *only* from the build parameter or
from `shouldEchoTestOutputToConsole()`, and that gate must keep checking
post-merge, multi-GPU, not-PerfSanity, and the kill switch. Verified by
mutation that it still fails on each of those four regressions.

`SessionSpoolEchoer` fixes:

- A broken console (EPIPE) was retried every poll forever. Each warning went to
  stderr, into the spool being read, and fed itself: 13 warnings in 3s, growing
  without bound, injected into the very artifact this must not regress. Drop
  the reader on the first read or write error and log once; 2 warnings total.
- `stop()` closed the readers even when the join timed out, so a still-running
  thread hit `ValueError: closed file`, which is not an `OSError`, escaped
  `_drain`, and printed a `threading.excepthook` traceback on the real stderr
  where a triager reads. Catch `(OSError, ValueError)`, add a `_closed` flag the
  thread checks, and leave the readers open when the thread outlives the join --
  leaking two descriptors at process exit is the cheaper bug. Join timeout cut
  to 2s: a graceful exit needs one poll interval, anything longer is a stuck
  write and stalling teardown on it buys nothing.
- `start()` was outside the try/except that unwinds the spools, so
  `RuntimeError("can't start new thread")` propagated out of
  `pytest_load_initial_conftests` before the capture cleanup was registered,
  leaving fd 1 and 2 pinned to the spool and pytest's own traceback inside it --
  a silent nonzero exit. A best-effort diagnostic must never do that.
- Writes of up to 1 MiB are not atomic on a pipe and spliced pytest's progress
  markers. Cap each write at PIPE_BUF.
- Bound total echoed bytes (128 MiB) and add the `disableStdoutEchoOnPostMerge`
  build parameter, so console volume has a ceiling and an off switch that does
  not need a merge.

Also correct the overstated claim in the Groovy comment and the class
docstring. Echo recovers everything written up to roughly a poll interval
before the process dies, which covers a HangDetector report. It does not
recover pytest-timeout's own stack dump, written immediately before
`os._exit`; that still reaches only the spool, exactly as it does today.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
@JunyiXu-nv
JunyiXu-nv marked this pull request as ready for review July 28, 2026 06:38
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

All six review items addressed in 20a0db5fb8; back out of draft.

Blocker fixed properly. The invariant test was rewritten to encode the new rule rather than working around the old regex, and renamed test_s3_stdout_echo_is_opt_in_or_scoped_to_post_merge_multi_gpu: every --s3-echo-stdout must be guarded by either ENABLE_S3_ECHO_STDOUT or shouldEchoTestOutputToConsole(stageName), and the gate body must still check Post-Merge, PerfSanity, system_gpu_count and the kill switch.

I independently mutation-tested the replacement to confirm it is protective rather than a rubber stamp:

Mutation Result
unmodified passes
drop the Post-Merge restriction from the gate fails (caught)
add an unguarded --s3-echo-stdout elsewhere in the file fails (caught)

Honest-claim correction (item 2). Rather than shim os._exit, the Groovy comment and class docstring now state the limit explicitly: the echo recovers everything written up to ~one poll interval before death — which covers a HangDetector report, since the detector logs and dumps stacks before killing — but it does not recover pytest-timeout's own dump, which still reaches only the spool. Wrapping a third-party plugin's kill path to win the less useful half of the dump was not worth the risk.

Other fixes: reader dropped on first read/write error, so the EPIPE storm is gone (measured 13 warnings → 2, spool injection 1633 B → 296 B, thread now exits); (OSError, ValueError) caught with a _closed flag and readers left open on join timeout, so no end-of-stage traceback; echoer.start() wrapped so a thread-exhaustion RuntimeError can no longer abort the session; writes sliced to PIPE_BUF (4096), which removed the spliced progress markers.

Log-volume concern addressed both ways: a 128 MiB session byte budget that stops echoing with a warning naming results-<stage>.tar.gz, plus a disableStdoutEchoOnPostMerge build parameter so it can be turned off without another merge.

Verified locally: test_s3_output.py + test_test_to_stage_mapping.py = 33 passed, 3 skipped. The GB200 A/B was re-run after the rework (the write path changed materially) and still shows 3,273 B vs 687 B with the full Hang detected report and hard-killing and propagating to peer ranks.

One caveat left on record by the author: a session-wide cap means a pathological test (e.g. the ~39 MB test_kv_cache_v2_nixl_python) consumes budget a later wedge then cannot use. 128 MiB is ~10x a normal stage so it should not bite, but capping that test at source remains the better fix and is a separate change.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62137 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62129 [ run ] completed with state ABORTED. Commit: 23a3210

Link to invocation

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_common/s3_output.py (1)

512-519: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Echo write is still unguarded; an EPIPE here kills the per-test log capture too.

os.write(self.saved_fd, raw) sits inside the same try that drives log_file.write, so a BrokenPipeError/OSError on the console copy falls through to except Exception at Line 548 and ends the reader loop — the timestamped log file stops being written as well. SessionSpoolEchoer._echo deliberately isolates this per reader; the same isolation is missing here. Partial writes (Line 516) are also only warned about instead of being flushed to completion, unlike _write_all.

🐛 Proposed fix: isolate and fully flush the echo write
                 if self.echo_to_original and self.saved_fd is not None:
-                    ret = os.write(self.saved_fd, raw)
-                    if ret != len(raw):
-                        logger.warning(
-                            f"Partial write to original FD {self.target_fd}: {ret} != {len(raw)}"
-                        )
+                    try:
+                        view = memoryview(raw)
+                        while view:
+                            view = view[os.write(self.saved_fd, view) :]
+                    except OSError as e:
+                        logger.warning(
+                            f"Echo to original FD {self.target_fd} failed, disabling echo: {e}"
+                        )
+                        self.echo_to_original = False
🤖 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/test_common/s3_output.py` around lines 512 - 519, Update the echo path
in the surrounding reader method so writing to self.saved_fd cannot terminate
log-file capture: isolate the console copy from log_file.write, handle
BrokenPipeError/OSError without propagating it to the outer reader-loop
exception handler, and replace the single os.write call with the existing
_write_all-style behavior so partial writes are fully flushed while preserving
the warning for genuine write failures.
🧹 Nitpick comments (1)
tests/unittest/test_s3_output.py (1)

539-659: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

QA coverage summary (tests/** path instruction):

  1. Test functions added: test_session_echo_drops_a_reader_whose_console_is_gone, test_session_echo_stops_after_the_byte_budget, test_session_capture_survives_an_echo_thread_that_cannot_start, test_session_echo_writes_are_atomic_sized, test_fd_redirector_echoes_a_partial_chunk_without_waiting_for_more (plus helper _drain_console_pipe).
  2. These are unittest-level tests for tests/test_common/s3_output.py internals, not integration tests; inclusion in tests/integration/test_lists/test-db/ or qa/ doesn't directly apply — confirm they remain picked up by the generic unittest collection stage instead.
  3. Verdict: insufficient for one specific hardening claim — the PR objectives call out "shutdown stalls and traceback risk" (the _JOIN_TIMEOUT_SECONDS branch in SessionSpoolEchoer.stop() where a still-alive thread is left with open readers rather than closed to avoid a ValueError escaping into threading.excepthook), but none of the shown tests exercise that branch. The rest of the hardening (EPIPE drop, byte budget, thread-start failure, atomic write chunking) is sufficient.

Want me to draft a test for the join-timeout branch (e.g. monkeypatching Thread.join/is_alive so stop() observes a still-running thread) and open a follow-up?

🤖 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/test_s3_output.py` around lines 539 - 659, Extend the unittest
coverage for SessionSpoolEchoer.stop so it exercises the _JOIN_TIMEOUT_SECONDS
path when the worker thread remains alive after join. Use the existing
SessionSpoolEchoer symbols and controlled thread behavior to verify stop closes
or safely handles open readers without raising, while preserving the existing
shutdown behavior for threads that terminate normally.

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.

Outside diff comments:
In `@tests/test_common/s3_output.py`:
- Around line 512-519: Update the echo path in the surrounding reader method so
writing to self.saved_fd cannot terminate log-file capture: isolate the console
copy from log_file.write, handle BrokenPipeError/OSError without propagating it
to the outer reader-loop exception handler, and replace the single os.write call
with the existing _write_all-style behavior so partial writes are fully flushed
while preserving the warning for genuine write failures.

---

Nitpick comments:
In `@tests/unittest/test_s3_output.py`:
- Around line 539-659: Extend the unittest coverage for SessionSpoolEchoer.stop
so it exercises the _JOIN_TIMEOUT_SECONDS path when the worker thread remains
alive after join. Use the existing SessionSpoolEchoer symbols and controlled
thread behavior to verify stop closes or safely handles open readers without
raising, while preserving the existing shutdown behavior for threads that
terminate normally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a0b3c96d-6f9c-4a16-bddb-a1da7e57199a

📥 Commits

Reviewing files that changed from the base of the PR and between 23a3210 and 20a0db5.

📒 Files selected for processing (4)
  • jenkins/L0_Test.groovy
  • tests/test_common/s3_output.py
  • tests/unittest/test_s3_output.py
  • tests/unittest/tools/test_test_to_stage_mapping.py

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62137 [ run ] completed with state SUCCESS. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50315 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 #50315 (on the post-fix commit 20a0db5) failed because a Jenkins agent went offline mid-run, not because of a test:

  • The SBSA single-GPU child run 5363 finished ABORTED, not FAILURE — 21 nodes SUCCESS, 6 ABORTED, zero FAILUREs.
  • On GB300-PyTorch-1, all four tests that ran passed (TestGPTOSS::test_w4_1gpu ×2, TestQwen3_8B::test_fp8_block_scales, TestQwen3_8B::test_w4a8_mxfp4). Then at 07:46:44:
test_unittests_v2[unittest/_torch/thop/parallel] <- Cannot contact aws-cmh-slurm-1-login-01...
08:00:04  Sending interrupt signal to process
08:00:24  After 20s process did not stop
08:00:35  Stage is interrupted, skip to upload test result
  • The orchestrator then reported ERROR: SBSA single-GPU test failed purely because the child result was not SUCCESS, which blocked [Test-SBSA-Multi-GPU].

This PR is not implicated:

  • Zero occurrences of s3-echo-stdout, SessionSpoolEchoer, s3_output or spool anywhere in the stage log.
  • GB300-PyTorch-1 does not contain Post-Merge, so shouldEchoTestOutputToConsole() returns false for it and this PR adds no flag to that stage at all.
  • The interrupt came 46 minutes into a 3600 s pytest, well short of its own timeout, immediately after the agent became uncontactable.

Re-running CI.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62158 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

yufeiwu-nv added a commit to yufeiwu-nv/TensorRT-LLM that referenced this pull request Jul 28, 2026
…n model YAML (NVIDIA#16935)

Signed-off-by: yufeiwu-nv <230315618+yufeiwu-nv@users.noreply.github.com>
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62158 [ run ] completed with state FAILURE. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50333 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 #50333 failed on DGX_B200-PyTorch-9 in TestDeepSeekV3Lite::test_nvfp4[moe_backend=TRTLLM-mtp_nextn=0-fp8kv=True-attention_dp=False-cuda_graph=True-...] (pytest exit 1 → srun.real: task 0: Exited with exit code 1).

This PR cannot affect that stage:

  • DGX_B200-PyTorch-9 contains neither Post-Merge nor _GPUs, so shouldEchoTestOutputToConsole() returns false for it and this PR adds no flag to that stage at all.
  • Zero occurrences of s3-echo-stdout, SessionSpoolEchoer, s3_output or spool echo anywhere in the stage log.
  • The failing test is a DeepSeekV3Lite NVFP4 accuracy case — semantically unrelated to console echo.

Being straight about what I cannot show: that exact test parametrization, and the wider TestDeepSeekV3Lite::test_nvfp4 family, have 0 indexed runs in the last 7 days, so unlike earlier failures on this PR I have no ambient baseline to point at. What I can say is that the stage is independently unhealthy — 93 FAILURE / 547 non-skip runs ≈ 17% over 7 days, spread across Slurm issue (32), Flaky test (15), Test assertion failure (14) and Timeout (14).

So the case here is structural rather than statistical: the change is gated off for this stage and its code never executes there.

Re-running CI.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62180 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62180 [ run ] completed with state FAILURE. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50351 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 #50351 failed on GB200-4_GPUs-PyTorch-3 in TestGPTOSS::test_eagle3_4gpus[v2_kv_cache-cutlass-one_model-overlap_scheduler]:

AssertionError: Reference accuracy is 64.000, threshold is 55.734.
Expected accuracy >= threshold, but got 55.556.

A 0.18-point miss against the threshold — a borderline accuracy flake, not a functional failure.

Not caused by this PR:

  • Zero occurrences of s3-echo-stdout, SessionSpoolEchoer, s3_output or spool in the stage log. GB200-4_GPUs-PyTorch-3 also contains no Post-Merge, so shouldEchoTestOutputToConsole() is false for it and this PR adds no flag there.
  • Ambient rate over 7 days: 948 runs — 859 PASSED, 82 SKIPPED, 7 FAILED (0.74%), spread across 4 post-merge runs plus PRs 16787, 16861 and this one. This PR accounts for one of seven.
  • A console-echo change cannot move a model accuracy score.

Re-running CI.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62221 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62221 [ run ] completed with state SUCCESS. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50389 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 #50389 failed on DGX_B200-PyTorch-2 in test_nvfp4_kv[v2_kv_cache=True-attn_backend=TRTLLM-torch_compile=True]:

RuntimeError: it->second->use_count > 0 INTERNAL ASSERT FAILED at
"/opt/pytorch/pytorch/c10/cuda/CUDACachingAllocator.cpp":3045,
please report a bug to PyTorch.

A PyTorch CUDA caching-allocator internal assert — a GPU memory refcount invariant inside PyTorch itself. Not caused by this PR:

  • Mechanically impossible from this change. The PR adds a thread that tails a spool file onto a console fd. It cannot affect CUDA allocator refcounts. DGX_B200-PyTorch-2 also contains no Post-Merge, so shouldEchoTestOutputToConsole() is false and no echo is even enabled there.
  • Known ambient flake. Over 7 days test_nvfp4_kv ran 1168 times — 1056 PASSED, 82 SKIPPED, 30 FAILED (2.6%) — spread across 16 distinct PRs: 16346 (×6), post-merge (×3), 15550 (×3), 16455 (×3), 14047 (×2), 16862 (×2), 16902 (×2), plus 16021, 16399, 16402, 16749, 16752, 16762, 16768, 16908 and this one at ×1 each.

For transparency, this is the fourth consecutive failure on this PR, each on a different stage with a different root cause: a Jenkins agent going offline mid-run; TestDeepSeekV3Lite::test_nvfp4; TestGPTOSS::test_eagle3_4gpus missing an accuracy threshold by 0.18; and now this allocator assert. Each has been checked individually against its own ambient baseline rather than waved through, and in every case the PR's code is either mechanically incapable of the effect or gated off for the stage entirely.

Re-running CI.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62239 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62239 [ run ] completed with state FAILURE. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50408 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 #50408 failed on three stages — DGX_H100-PyTorch-1, -3 and -6 — all at the node-allocation step, before any test ran:

Request Node Via Slurm
sbatch: error: Batch job submission failed: Requested node configuration is not available

Three Request Node Via Slurm nodes failed with the identical error in the same run. A PR cannot influence whether the cluster has nodes matching a reservation's configuration, and no test executed on any of the three stages.

This is the same failure that dominated CI earlier today — 138 of 348 stage failures in a 24-hour window carried this exact sbatch error against reservation sla_res_fw190_d580. It had cleared by mid-morning (0 occurrences in an hour) and appears to have returned.

For accuracy: OpenSearch shows 0 Slurm issue stage failures in the last two hours, but its classifier lags 0.5–40 h and this run finished ~15 minutes ago, so that is an indexing artefact rather than evidence of rarity. The direct error string in the stage log is the primary evidence here.

Re-running CI.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62251 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62251 [ run ] completed with state FAILURE. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50420 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 #50420 — diagnosis

One test failed across the whole pipeline, on B300-PyTorch-2:

test_cute_dsl_moe::test_nvfp4_gather_grouped_gemm_act_fusion_blackwell[128-1-32-128-relu2]
Exception: Mismatch percentage is 0.054688 for rtol 0.000100

The other three stages that uploaded results (DGX_B200-PyTorch-2, DGX_H100-PyTorch-1, DGX_H100-PyTorch-6) contain zero failing testcases and empty unfinished_test.txt; the remaining stages never uploaded, consistent with fail-fast cancelling them after the B300 failure.

Why this PR cannot reach it. The diff is jenkins/L0_Test.groovy, tests/test_common/s3_output.py, tests/test_common/s3_output_hooks.py and two test files. That is Jenkins wiring plus a pytest stdout-capture plugin. The failure is a numerical-tolerance exception raised inside the test body of an FP4 grouped-GEMM CUDA kernel test. Changing how stdout is captured cannot change kernel arithmetic.

Why this test flakes. check_accuracy(..., atol=1e-4, rtol=1e-4, percent=0.95) fails once ≥5% of elements exceed tolerance. The reported 0.054688 is exactly 7/128, and this is the smallest configuration in the sweep (num_tokens=128, top_k=1), so the tolerance has roughly one-element resolution: 6/128 = 4.69% passes, 7/128 = 5.47% fails. A single extra mismatching element flips the result, which is exactly the shape of a low-rate flake in a quantized GEMM whose reduction order is not run-to-run stable.

Measured ambient rate. I scanned the last 60 L0_MergeRequest_PR pipelines' archived results for every B300/B200 stage:

Stage-runs where this test executed 19
Stage-runs where it failed 1 (this one, #50420)

So ~5%, on a single observed failure — the interval around that number is wide, and I want to be straight that "1 in 19" is an estimate from one event, not a well-sampled rate. What it does establish is that ours is not a systematically reproducing failure: 18 other executions of the same parameter set passed in the same window.

The earlier failure on this PR (#50408) uploaded no test results at all, so it is a different class and not a repeat of this one.

Re-running.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62298 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62298 [ run ] completed with state FAILURE. Commit: 20a0db5
/LLM/main/L0_MergeRequest_PR pipeline #50467 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 #50467 — blocked by the same repo-wide unittest/scaffolding failure

Not re-triggering. This is the second consecutive run on this PR to fail, and the cause is not this PR.

The only failing testcase in the entire pipeline:

[DGX_H100-PyTorch-3]  test_unittests_v2[unittest/scaffolding]

Nothing else failed anywhere. The previous run (#50420) failed on an unrelated one-in-nineteen CuTe DSL numerical flake, documented above; this run is a different, systematic cause.

Measured across the last 50 pipelines, for every PyTorch stage that ran this wrapper:

scaffolding wrapper observed in 10 stage-runs; failed in 10
  50445, 50453, 50457, 50458, 50459 (x2, incl. retry), 50463, 50464, 50467, 50485

Ten out of ten, across nine pipelines, eight of them unrelated PRs, with #50459 failing both its original attempt and its retry. The denominator is not biased toward failures — stages in this window do upload results when they pass (#50460 uploaded twelve stages with zero failing tests); there were simply no passing runs of this wrapper to find.

As on #16592, the inner failing test names are not recoverable from the artifacts: no results-sub-unittests-unittest-scaffolding.xml is produced, and the stage's inner-s3/unittest-scaffolding spool is empty. Which is, with some irony, precisely the observability gap this PR exists to close.

Re-running would reproduce it and cost hours for no new information, so I am leaving this for a human decision rather than looping. Not filing a bug — this is not this PR's to own.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

#16978 has merged, so full:DGX_H100/unittest/scaffolding SKIP is now on main (waives.txt:185) and the collection error that blocked this PR no longer fails the stage.

Re-running. The underlying cause — mcp 2.0.0 removed mcp.server.fastmcp, and mcp was unpinned in requirements.txt — is fixed properly in #16980; the waive is the interim unblock.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62395 [ run ] triggered by Bot. Commit: 20a0db5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

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

CI Report

Link to invocation

# Echo first: the whole point of echoing is live visibility, and
# the writer may be killed at any moment.
if self.echo_to_original and self.saved_fd is not None:
ret = os.write(self.saved_fd, raw)

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 writing to the Jenkins console fails, this exception stops the whole reader thread. Could we catch the console write error here, disable echo, and continue writing to the log file?

if self._closed:
return
try:
while True:

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 stdout output is very large, this could hide an important error or hang report written to stderr.

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.

4 participants