Skip to content

fix(mocker): align replay ITL and per-user throughput with AIPerf semantics - #12883

Open
davilu-nvidia wants to merge 1 commit into
ai-dynamo:mainfrom
davilu-nvidia:fix/replay-itl-aiperf-semantics
Open

fix(mocker): align replay ITL and per-user throughput with AIPerf semantics#12883
davilu-nvidia wants to merge 1 commit into
ai-dynamo:mainfrom
davilu-nvidia:fix/replay-itl-aiperf-semantics

Conversation

@davilu-nvidia

@davilu-nvidia davilu-nvidia commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Overview

The replay report prints an AIPerf-style metrics table, but its Inter Token Latency and Output Token Throughput Per User rows were computed over individual consecutive-token gaps pooled across all requests, while AIPerf defines both metrics per request:

itl  = (request_latency - ttft) / (output_sequence_length - 1)   # one value per request
otpu = 1 / itl                                                   # one value per request

Since DynoSim's headline use case is comparing a simulated report against a real AIPerf run field-for-field, the like-named metrics should use the same definition. With the per-gap definition, percentile rows are dominated by clean decode steps: rare-but-large scheduler stalls (e.g. a chunked prefill blocking decode for ~200 ms) sit just above p99 and disappear from the table, while on the AIPerf side each such stall is smeared into every affected request's average and shifts the whole distribution.

What changed

  • itl_* summary fields and the "Inter Token Latency" table row now follow AIPerf's request-level definition (identical to the existing per-request itl_ms in --per-request-jsonl)
  • output_token_throughput_per_user_* now follows AIPerf (1 / itl per request)
  • the raw per-gap distribution is preserved under new token_gap_* summary fields and a "Token Gap (ms)" table row — its tail is the only place engine-level stalls such as chunked-prefill interference remain visible before request-level averaging smooths them out
  • docs: report-schema section states the definitions explicitly
  • tests updated; test_replay_itl_uses_per_token_gaps renamed to test_replay_itl_matches_aiperf_request_semantics, covering both metrics

Validation against silicon

Setup: Qwen3-32B (bf16), 4x SGLang TP2 workers on GB200 behind dynamo.frontend --router-mode kv (this repo @ main), replaying the public Mooncake FAST25 conversation_trace.jsonl (first 20 min, filtered to input+output <= 40704, 3440 requests, ignore_eos) with AIPerf 0.9.0 in fixed-schedule mode; DynoSim replaying the identical trace with --router-mode kv_router --num-workers 4 and AIC gb200/sglang timing, KV capacity pinned to the measured max_total_num_tokens.

metric silicon (AIPerf) sim, old per-gap "ITL" sim, request-level ITL (this PR)
ITL p50 (ms) 12.1 8.0 (-34%) 9.6 (-21%)
ITL p90 (ms) 21.4 8.6 (-60%) 14.8 (-31%)
ITL p99 (ms) 237 24.2 (-90%) 129.8 (-45%)
TTFT p50/p90/p99 (ms) 357 / 1180 / 2021 388 / 1026 / 1683 unchanged
request throughput 2.832 rps 2.838 rps unchanged

The old rows made the simulator look 60-90% optimistic on decode latency; the apples-to-apples comparison shows the actual model error is 21-45%, which matches the physical accounting (engine logs show a 15.7% chunked-prefill duty cycle; the mocker reproduces ~104 s of blocked stream-time vs ~145 s on silicon, plus AIC's GB200 decode entries running ~7% fast).

Breaking change note

mean/median/p*/max_itl_ms and *_output_token_throughput_per_user change meaning (per-gap -> per-request). Consumers that want the old data should read the new *_token_gap_ms fields.


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Replay reports now include a Token Gap (ms) metric, showing pooled gaps between consecutive generated tokens.
    • Latency reports now distinguish request-level ITL from raw token-gap statistics for clearer performance analysis.
    • Per-user output-token throughput and related latency calculations now follow consistent request-level definitions.
  • Documentation

    • Updated the replay CLI reference with token-gap reporting and clarified definitions for ITL and output-token throughput.

…antics

The replay report prints an AIPerf-style table, but its Inter Token
Latency and Output Token Throughput Per User rows were computed over
individual consecutive-token gaps pooled across requests, while AIPerf
defines both metrics per request:

  itl  = (request_latency - ttft) / (output_sequence_length - 1)
  otpu = 1 / itl

Comparing a DynoSim report against a real AIPerf run therefore
overstated the simulation error dramatically. In a GB200 validation run
(Qwen3-32B TP2 x4 workers, Mooncake FAST25 conversation trace via the
dynamo frontend + SGLang workers, measured with AIPerf 0.9.0), the
per-gap ITL percentiles made the sim look 60-90% optimistic at
p90/p99, while the request-level comparison shows 31/45%:

                      silicon   sim (per-gap)   sim (request-level)
  ITL p50 (ms)          12.1        8.0              9.6
  ITL p90 (ms)          21.4        8.6             14.8
  ITL p99 (ms)         237.0       24.2            129.8

itl_* and output_token_throughput_per_user_* now follow the AIPerf
definitions. The raw per-gap distribution is still valuable - its tail
is the only place engine stalls such as chunked-prefill interference
are visible before request-level averaging smooths them out - so it is
kept under new token_gap_* summary fields and a Token Gap table row.

Signed-off-by: davilu <davilu@nvidia.com>
@davilu-nvidia
davilu-nvidia requested review from a team as code owners August 8, 2026 07:28
@copy-pr-bot

copy-pr-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@davilu-nvidia
davilu-nvidia temporarily deployed to external_collaborator August 8, 2026 07:28 — with GitHub Actions Inactive
@davilu-nvidia
davilu-nvidia temporarily deployed to external_collaborator August 8, 2026 07:28 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

👋 Hi davilu-nvidia! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor fix documentation Improvements or additions to documentation labels Aug 8, 2026

@devin-ai-integration devin-ai-integration 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.

Devin Review found 3 potential issues.

Open in Devin Review

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.

🔍 New token_gap fields escape the canonical validation and handoff-conformance checks

validate_report_finite checks ttft/ttst/tpot/itl/e2e/output_token_throughput_per_user (lib/mocker/src/replay/offline/canonical.rs:464-473) and the offline handoff conformance comparator compares latency.itl.distribution plus latency.itl.max_ms (lib/mocker/src/replay/offline/single.rs:717-753). Both are now looking at the request-level distribution, which is a copy of tpot; the per-gap distribution moved to token_gap and is compared/validated nowhere. Net effect: the serialized *_token_gap_ms fields are unvalidated, and the single-worker vs. generic-runtime conformance check silently lost its only per-gap-timing assertion (a divergence in individual token spacing that preserves the request average would no longer be caught).

(Refers to lines 1290-1291)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1336 to +1343
tpot: build_distribution_stats(tpots.clone()),
itl: {
let distribution = build_distribution_stats(tpots);
TraceInterTokenLatencyStats {
max_ms: distribution.max_ms,
distribution,
}
},

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.

🔍 ITL now duplicates TPOT field-for-field

Because e2e_ms is computed as last_token_ms - arrival_time_ms and ttft_ms as first_token_ms - arrival_time_ms (lib/mocker/src/replay/collector.rs:1255-1256), AIPerf's (request_latency - ttft) / (osl - 1) is algebraically identical to TraceRequestStats::mean_tpot_ms (lib/mocker/src/replay/collector.rs:721-730). The new itl distribution is therefore built from exactly the same tpots vector as tpot, so every *_itl_ms field in the report is now bit-identical to the corresponding *_tpot_ms field, and the extra tpots.clone() exists solely to feed two identical distributions. This is functionally fine but worth confirming: consumers that used to read both rows for distinct information now get duplicate data, and the clone could be avoided by computing the stats once and copying the struct.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1353 to +1355
output_token_throughput_per_user: build_distribution_stats(
output_tokens_per_s_per_user,
),

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.

🔍 Downstream Spica scoring docs/comments still describe the old per-gap definition

mean_output_token_throughput_per_user is the objective metric for OptimizationTarget.THROUGHPUT_PER_USER (aisimulate/src/aisimulate/spica/score.py:82-84), whose comment states it is the "mean of per-token-gap 1000/itl"; the same wording appears in docs/fern/pages/developer-guide/knowledge-base/modular-components/ai-simulate-experimental/spica-experimental/optimization-goals.md:50. After this change the value is the mean of per-request 1000/ITL, so both the comment and the doc are stale, and previously recorded sweep results are no longer comparable with new ones for this target.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Replay latency reporting now separates request-level ITL from pooled token gaps. It serializes token-gap statistics, computes per-user throughput from request-level TPOT, updates report display and documentation, and revises tests.

Changes

Replay latency metrics

Layer / File(s) Summary
Metric contracts and serialized output
lib/mocker/src/replay/collector.rs, docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx
Latency statistics now distinguish request-level ITL from pooled token gaps. Serialized reports include token-gap distribution and maximum values. Documentation defines the updated formulas and semantics.
Collection and report finalization
lib/mocker/src/replay/collector.rs
Token finalization aggregates consecutive-token gaps. Report finalization derives ITL and per-user output-token throughput from request-level values.
Report display and metric validation
components/src/dynamo/replay/reporting.py, lib/mocker/src/replay/collector.rs, lib/mocker/src/replay/mod.rs
The report table adds a Token Gap row. Tests verify request-level ITL, pooled token gaps, maximum gaps, and per-user throughput.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and validation, but it omits the required Related Issues section and reviewer-start guidance. Add the required Related Issues section and identify the specific files or sections where reviewers should start.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: aligning replay ITL and per-user throughput with AIPerf semantics.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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

🤖 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 `@lib/mocker/src/replay/collector.rs`:
- Around line 1275-1277: Define a single AIPerf-compatible contract for zero ITL
and apply it in the throughput calculation near output_tokens_per_s_per_user:
ensure every request receives the documented throughput value when tpot_ms is
0.0, and add a same-timestamp-token test covering that behavior in
lib/mocker/src/replay/collector.rs (anchor lines 1275-1277). Update
docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx (lines
352-357) to precisely describe the selected zero-ITL behavior; both sites must
remain consistent.
🪄 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: fea0473f-6b9f-4408-9511-b57c48528a3e

📥 Commits

Reviewing files that changed from the base of the PR and between a765194 and 86d746e.

📒 Files selected for processing (4)
  • components/src/dynamo/replay/reporting.py
  • docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx
  • lib/mocker/src/replay/collector.rs
  • lib/mocker/src/replay/mod.rs

Comment on lines +1275 to +1277
if tpot_ms > 0.0 {
output_tokens_per_s_per_user.push(1000.0 / tpot_ms);
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define one zero-ITL throughput contract across collection and documentation.

A request can have zero ITL when multiple tokens receive the same completion timestamp. The current collector excludes that request from per-user throughput, while the documentation promises a throughput value per request.

  • lib/mocker/src/replay/collector.rs#L1275-L1277: define and implement the AIPerf-compatible behavior for tpot_ms == 0.0, then add a same-timestamp token test.
  • docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx#L352-L357: document the selected zero-ITL behavior precisely.
📍 Affects 2 files
  • lib/mocker/src/replay/collector.rs#L1275-L1277 (this comment)
  • docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx#L352-L357
🤖 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 `@lib/mocker/src/replay/collector.rs` around lines 1275 - 1277, Define a single
AIPerf-compatible contract for zero ITL and apply it in the throughput
calculation near output_tokens_per_s_per_user: ensure every request receives the
documented throughput value when tpot_ms is 0.0, and add a same-timestamp-token
test covering that behavior in lib/mocker/src/replay/collector.rs (anchor lines
1275-1277). Update
docs/fern/pages/reference/components/dynosim-replay-cli-reference.mdx (lines
352-357) to precisely describe the selected zero-ITL behavior; both sites must
remain consistent.

@datadog-official

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 2 Pipeline jobs failed

Pre Merge | rust-tests (.)   View in Datadog   GitHub Actions

🔧 Fix in code. This looks caused by changes in this PR. Assertion failed in mod.rs:261: expected output_token_throughput_per_user.min_ms and max_ms to be close to 30.0, but they were not.

Pre Merge | pre-merge-status-check   View in Datadog   GitHub Actions

📋 Copy prompt for your agent
CI on my pull request is failing. Help me find and fix the root cause of each failing job below — they were flagged as caused by changes in this PR, so focus on the diff. For each job, explain the failure and propose a fix.

Branch: fix/replay-itl-aiperf-semantics

Pre Merge | rust-tests (.)
Commit: 86d746ecd4c307f97a6a35f74bf7e4d7263438c3
Error (code / test):
Assertion failed in mod.rs:261: expected output_token_throughput_per_user.min_ms and max_ms to be close to 30.0, but they were not.
CI job: https://github.com/ai-dynamo/dynamo/actions/runs/31246251600/job/93075254778

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 86d746e | Docs | Datadog PR Page | Give us feedback!

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

Labels

documentation Improvements or additions to documentation external-contribution Pull request is from an external contributor fix size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant