Skip to content

[None][test] Restrict gen-worker per-iter mean to steady-state iterations - #16298

Merged
chenfeiz0326 merged 3 commits into
NVIDIA:mainfrom
chenfeiz0326:chenfeiz/perf-sanity-gen-metric-max-tokens
Jul 13, 2026
Merged

[None][test] Restrict gen-worker per-iter mean to steady-state iterations#16298
chenfeiz0326 merged 3 commits into
NVIDIA:mainfrom
chenfeiz0326:chenfeiz/perf-sanity-gen-metric-max-tokens

Conversation

@chenfeiz0326

@chenfeiz0326 chenfeiz0326 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

The disagg gen_only Mean Gen Worker Per-Iter Device Step Time metric in
tests/integration/defs/perf/test_perf_sanity.py currently averages
prev_device_step_time over every log line whose iter field is >= 5.
Near the end of a run, num_generation_tokens shrinks as sequences finish,
so those tail iterations have systematically shorter device step times and
drag the mean below the steady-state cost we actually want to track.

This PR restricts the mean, per gen_server_{i}.log, to iterations at the
mode (most-frequent) num_generation_tokens value observed for
iter >= 5 rows in that file, with ties broken toward the larger ngen:

  1. Single pass over each gen_server_{i}.log builds a
    Dict[num_generation_tokens, (count, Welford mean of prev_device_step_time)]
    over iter >= 5 rows with a numeric prev_device_step_time, and
    independently counts every such row into total_count.
  2. After the settle poll drains (see below), the per-file bucket with the
    most rows is selected (ties → largest ngen) and its mean is taken as
    that file's contribution. The per-file means are then averaged.

num_generation_tokens is captured by a separate regex applied to the
same line — the name is stable, but its position within the line relative
to prev_device_step_time is not, so chaining the two into one pattern
would silently drop any row whose states dict was printed first.

total_count is the number of iter >= 5 rows with a numeric
prev_device_step_time across all files — not the size of the mode
bucket. This keeps the settle signal monotonic across polls (rows only ever
get appended as NFS flushes), so an in-flight ngen shift while the tail is
still flushing cannot hold the count flat for two consecutive polls and
trigger a premature return on a non-steady-state sample.

Note on iter >= 5: iter here is the literal value of the iter field
in the log line, not a running line count. Gen-worker logs emit many
iter = 0 prefill/scheduling lines and a warmup iter = 1 line before
generation begins, and < 5 drops all of them before we ever look at
num_generation_tokens.

Method comparison

Aspect Old method New method
Row filter for the per-file mean iter >= 5 iter >= 5 and num_generation_tokens == mode(ngen) (ties → largest ngen)
Regex-captured fields (iter, prev_device_step_time) (iter, prev_device_step_time) from _DEVICE_STEP_TIME_RE plus num_generation_tokens from an independent _NUM_GEN_TOKENS_RE applied to the same line (position-independent)
Passes over each gen_server_{i}.log 1 1 (per-ngen Welford dict built in the same pass; halves I/O on 10k+ line logs)
Rows included from a typical genonly run Steady-state gen iters plus the shrinking-batch tail Steady-state gen iters only
Robustness to a one-off ngen spike N/A (no filter) Mode ignores it (== max would have collapsed the file mean to 1–2 samples)
Memory per file O(1) Welford O(distinct ngen), a small constant — steady-state + a shrinking tail
Steady-state bias Tail pulls the mean downward (fewer gen tokens → shorter step time) Removed — only mode-ngen iters contribute
Settle signal for parse_gen_worker_device_step_time Row count of the filtered set Row count of all iter >= 5 rows — monotonic across polls, so a mid-flush ngen shift cannot spoof "stable"
Aggregation across gen workers Per-file mean, then average across files Same

Impact on real logs

Sanity-checked on two real gen worker logs:

Log old mean (all iter>=5) new mean (mode ngen only) ngen filter
ds-r1 disagg genonly rep3 53.98 ms (253 iters) 53.30 ms (252 iters) 512
gpt_oss-120b postqwen r1 39.41 ms (11013 iters) 41.37 ms (9684 iters) 1024

The gpt_oss case shows the failure mode this fix addresses: the tail
included 1329 iterations with num_generation_tokens ranging 1–960, all
with shorter device step times, pulling the mean ~5% below the real
steady-state cost. The new metric reports the correct steady-state value.

Review defects addressed

Three defects were flagged on the earlier revision of this PR; the current
code addresses each:

  • Non-monotonic settle counter. In the earlier revision total_count
    was the size of the filtered (mode/== max) bucket, so a mid-run
    max_ngen jump could hold the count flat for two consecutive polls and
    return a mean computed on a non-steady-state sample. Fixed by making
    total_count count every iter >= 5 row with a numeric device time
    (monotonic across polls) and moving the ngen bucket selection after
    the settle poll drains.
  • Strict == max with no sample-size protection. Replaced by mode
    ngen (ties → largest ngen). A single-iter spike above the sustained
    batch used to lock max_ngen onto that spike and collapse the mean to
    1–2 samples; the mode is unaffected.
  • Regex order-coupling. The previous single regex required
    num_generation_tokens to appear after prev_device_step_time on
    the line. num_generation_tokens is now captured by a separate regex
    applied to the same line, so ordering does not matter.

Test plan

  • Run existing disagg gen_only perf-sanity cases end-to-end and verify
    the reported d_mean_gen_worker_per_iter_device_step_time matches
    the manual computation on the collected gen_server logs.
  • Confirm regression baseline shifts (if any) are within noise for
    workloads whose tail is small, and produce a higher, more accurate
    value for workloads with long tails.
  • Synthetic unit-level checks for scanner behavior: order-flipped
    states dict captured, iter<5 dropped, N/A device-time dropped,
    settle counter monotonic across a mid-flush max_ngen jump, mode
    wins over a one-off ngen spike, tie-break resolves to the larger
    ngen, and rows missing num_generation_tokens are still counted
    toward the settle signal.

@chenfeiz0326
chenfeiz0326 requested a review from a team as a code owner July 13, 2026 04:15
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The performance sanity test now averages numeric device step times only from iterations at least 5 that match each gen-worker log’s maximum num_generation_tokens, using a two-pass scan.

Changes

Steady-state parsing

Layer / File(s) Summary
Two-pass steady-state scan
tests/integration/defs/perf/test_perf_sanity.py
The log regex captures num_generation_tokens and excludes nonnumeric step times; scanning first finds per-file maxima, then computes filtered means and averages them across workers. Docstrings describe the updated behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly matches the change: restricting the gen-worker per-iter mean to steady-state iterations.
Description check ✅ Passed The description is detailed and covers rationale plus test coverage, though the section headings don't exactly match the template.
✨ 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.

Caution

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

⚠️ Outside diff range comments (1)
tests/integration/defs/perf/test_perf_sanity.py (1)

201-242: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compare each file’s observed max before declaring the logs settled. parse_gen_worker_device_step_time only waits for total_count to repeat, but that count can stay unchanged while a later poll sees a higher num_generation_tokens maximum. That can stop the loop on a stale batch and return the wrong steady-state mean. Return each file’s max_ngen from _scan_gen_worker_device_step_time and require both the count and maxima to stabilize.

🤖 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 201 - 242, The
polling logic in parse_gen_worker_device_step_time must stabilize per-file
maxima as well as total_count. Update _scan_gen_worker_device_step_time to
return each file’s observed max_ngen alongside its means and count, then track
and compare those maxima between polls; continue polling whenever either the
total count or any file’s max_ngen changes, and only compute the final
steady-state result after both remain unchanged.
🧹 Nitpick comments (1)
tests/integration/defs/perf/test_perf_sanity.py (1)

201-242: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit test coverage for the new two-pass scan.

No tests exercise _scan_gen_worker_device_step_time's new max-num_generation_tokens filtering (ties at the max, all-N/A lines, iter < 5 exclusion, tail rows with lower token counts than the plateau, multi-file averaging). Given the algorithm's added complexity (two passes, dynamic per-file max, Welford accumulation), targeted tests using tmp_path-backed fake gen_server_{i}.log files would materially increase confidence without much effort. A file such as tests/unittest/.../test_perf_sanity_device_step_time.py (or a small test class colocated in this module) mocking log content per scenario would suffice; coverage for this specific logic currently appears insufficient.

As per path instructions, "Act as a QA engineer reviewing test changes and coverage for TensorRT-LLM... suggest concrete list file names and whether coverage is sufficient, insufficient, or needs follow-up outside the PR."

🤖 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 201 - 242, Add
targeted unit tests for _scan_gen_worker_device_step_time using tmp_path-backed
gen_server_{i}.log fixtures, covering max-num_generation_tokens filtering, ties
at the maximum, all-N/A input, exclusion of iter values below 5, lower-token
tail rows, and multi-file mean aggregation. Keep the tests focused on the
two-pass scan and Welford results, colocated in the existing module or a
dedicated test_perf_sanity_device_step_time.py file.

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/integration/defs/perf/test_perf_sanity.py`:
- Around line 201-242: The polling logic in parse_gen_worker_device_step_time
must stabilize per-file maxima as well as total_count. Update
_scan_gen_worker_device_step_time to return each file’s observed max_ngen
alongside its means and count, then track and compare those maxima between
polls; continue polling whenever either the total count or any file’s max_ngen
changes, and only compute the final steady-state result after both remain
unchanged.

---

Nitpick comments:
In `@tests/integration/defs/perf/test_perf_sanity.py`:
- Around line 201-242: Add targeted unit tests for
_scan_gen_worker_device_step_time using tmp_path-backed gen_server_{i}.log
fixtures, covering max-num_generation_tokens filtering, ties at the maximum,
all-N/A input, exclusion of iter values below 5, lower-token tail rows, and
multi-file mean aggregation. Keep the tests focused on the two-pass scan and
Welford results, colocated in the existing module or a dedicated
test_perf_sanity_device_step_time.py file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 681b7445-649c-4204-b0b6-742ad836b80f

📥 Commits

Reviewing files that changed from the base of the PR and between 4a86ff5 and 5935833.

📒 Files selected for processing (1)
  • tests/integration/defs/perf/test_perf_sanity.py

…ions

The disagg gen_only Mean Gen Worker Per-Iter Device Step Time metric
currently averages every iter >= 5. Tail iterations near the end of a
run have a shrinking num_generation_tokens as sequences finish, so their
device step time is systematically lower than the steady-state cost and
drags the mean down.

Filter to steady state by, for each gen_server_{i}.log:

1. Scanning iter >= 5 rows once to find the maximum
   num_generation_tokens in that file (the steady-state batch);
2. Averaging prev_device_step_time only over rows whose
   num_generation_tokens equals that max.

The scanner stays O(1) memory (two file passes, Welford mean in the
second). The regex now also captures num_generation_tokens. The
settle-detection contract is unchanged: total_count is the number of
included rows, and once it is non-zero and stable across two polls we
return the metric.

Signed-off-by: chenfeiz0326 <chenfeiz@nvidia.com>
@chenfeiz0326
chenfeiz0326 force-pushed the chenfeiz/perf-sanity-gen-metric-max-tokens branch from 5935833 to 15b122d Compare July 13, 2026 05:40
@chenfeiz0326

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast --stage-list "DGX_B200-16_GPUs-2_Nodes-PyTorch-Disagg-PerfSanity-CTX1-NODE1-GPU4-GEN1-NODE1-GPU8-Post-Merge-2"

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58901 [ run ] triggered by Bot. Commit: 15b122d Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58901 [ run ] completed with state SUCCESS. Commit: 15b122d
/LLM/main/L0_MergeRequest_PR pipeline #47441 (Partly Tested) completed with status: 'SUCCESS'

CI Report

Link to invocation

…egex

Rework gen-worker per-iter mean parsing to address three issues raised
in review of the previous approach:

1. Settle contract: total_count is again monotonic. The previous
   iteration counted only rows whose num_generation_tokens equaled the
   file's running max, so a mid-flush max_ngen jump could hold the count
   flat for two consecutive polls and trigger early return on a
   non-steady-state sample. total_count now sums every iter >= 5 row
   with a numeric prev_device_step_time (rows are append-only across
   polls) while the per-ngen bucket dict is used only after the poll
   loop settles.

2. Bucket selection: mode ngen with tie-break to the larger value
   replaces strict == max. A single-iter ngen spike could previously
   collapse the mean to 1-2 samples (ds-r1: 253 -> 252 iters, mean
   53.98 -> 53.30 ms). The mode is the sustained batch by definition
   and is robust to that spike; ties resolve to the larger ngen so we
   still pick the upper of any tied clusters when they exist.

3. Regex order coupling: num_generation_tokens is now captured by a
   separate regex applied to the same line, not chained after
   prev_device_step_time. num_generation_tokens is always present in
   the states dict and its name is stable, but its position within the
   line is not, so the previous chained pattern silently dropped rows
   whose states dict was printed first.

The scan is now a single pass building
per_file_by_ngen: List[Dict[int, Tuple[int, float]]] (count + Welford
mean per ngen bucket), removing the second read of large gen logs.

Signed-off-by: chenfeiz0326 <chenfeiz@nvidia.com>
@chenfeiz0326
chenfeiz0326 force-pushed the chenfeiz/perf-sanity-gen-metric-max-tokens branch from 7676afe to 74998f7 Compare July 13, 2026 06:53
@chenfeiz0326

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast --stage-list "DGX_B200-16_GPUs-2_Nodes-PyTorch-Disagg-PerfSanity-CTX1-NODE1-GPU4-GEN1-NODE1-GPU8-Post-Merge-2"

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58919 [ run ] triggered by Bot. Commit: 74998f7 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58919 [ run ] completed with state FAILURE. Commit: 74998f7
/LLM/main/L0_MergeRequest_PR pipeline #47456 (Partly Tested) 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

Signed-off-by: Chenfei Zhang <chenfeiz@nvidia.com>
@chenfeiz0326
chenfeiz0326 force-pushed the chenfeiz/perf-sanity-gen-metric-max-tokens branch from c4018bf to b8dedde Compare July 13, 2026 08:55

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

Approved

@chenfeiz0326
chenfeiz0326 enabled auto-merge (squash) July 13, 2026 09:17
@chenfeiz0326

Copy link
Copy Markdown
Collaborator Author

/bot skip --comment "Only update gen only test's metric calculation, no need to run the whole CI pipeline"

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58945 [ skip ] triggered by Bot. Commit: b8dedde Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #58945 [ skip ] completed with state SUCCESS. Commit: b8dedde
Skipping testing for commit b8dedde

Link to invocation

@chenfeiz0326
chenfeiz0326 merged commit 11a880f into NVIDIA:main Jul 13, 2026
8 checks passed
chenfeiz0326 added a commit to chenfeiz0326/TensorRT-LLM that referenced this pull request Jul 24, 2026
The disagg gen_only perf-sanity regression metric
mean_gen_worker_per_iter_device_step_time is parsed from each
gen_server_{i}.log by parse_gen_worker_device_step_time(). Since NVIDIA#16298
introduced steady-state ngen bucketing, a gen worker whose
prev_device_step_time lines are all present and numeric can still yield a
None metric if num_generation_tokens fails to match _NUM_GEN_TOKENS_RE on
every row (absent from the states dict, or a non-bare-int render). A None
metric then trips PerfSanityTestConfig.check_test_failure() with a
misleading "missing 'prev_device_step_time'" RuntimeError even though the
data was logged and the workload passed (nvbugs 6487036 / 6487040).

Fix: _scan_gen_worker_device_step_time now also accumulates a per-file
un-bucketed all-iter Welford mean over every iter>=5 numeric
prev_device_step_time row. _mean_at_mode_ngen prefers the steady-state
mode-ngen bucket (unchanged for healthy logs) but falls back to that
all-iter mean for any worker whose buckets are empty, so a present metric
is never dropped to None. The fallback path logs a print_info. Returns
None only when no usable row exists in any worker at all.

Verified behavior-preserving on a real passing gen_server_0.log (v32
gen_only mtp3 on lyris GB200: 12.91 ms before and after). Adds
test_gen_worker_device_step_time_parser.py (10 CPU-only cases covering the
mode-bucket path, iter<5 / N/A skips, the missing / unparsable
num_generation_tokens fallback, mixed-worker averaging, and the None
cases).

Signed-off-by: Chenfei Zhang <chenfeiz@nvidia.com>
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