Skip to content

fix(perf): exclude non-stream requests from TTFT/TPOT/ITL metrics - #1499

Merged
Yunnglin merged 2 commits into
modelscope:mainfrom
qiumuyang:fix/perf-mixed-stream-metrics
Jul 21, 2026
Merged

Yunnglin merged 2 commits into
modelscope:mainfrom
qiumuyang:fix/perf-mixed-stream-metrics

Conversation

@qiumuyang

Copy link
Copy Markdown
Contributor

Summary

When a benchmark run mixes stream: true and stream: false requests, streaming-specific metrics (TTFT / TPOT / ITL) are now computed over stream requests only. Pure-stream and pure-non-stream runs are unchanged.

Motivation

For non-stream requests, first_chunk_latency = query_latency (entire request time) and time_per_output_token = 0. When mixed with real stream requests this inflates TTFT and deflates TPOT.

Mixed runs are reachable via workload_trace (per-request stream from original trace) and line_by_line (JSONL body stream field overrides CLI flag).

Example

8 streaming (TTFT ~50 ms) + 2 non-streaming (latency ~2.5 s):

Metric Before After (stream only)
Avg TTFT (8×0.05 + 2×2.5) / 10 = 0.54 s 0.05 s
Avg TPOT deflated by two 0 contributions stream-only mean

Changes

File What changed
benchmark_util.py Add is_stream flag and is_stream_body() helper; stream-only accumulators for TTFT/TPOT/ITL with non-stream fallback; expose stream/non-stream counts.
default_api.py Set is_stream = True in SSE branch; body-based fallback on failure.
openai_responses_api.py Set is_stream = True in SSE branch; body-based fallback on failure.
http_client.py Body-based is_stream on timeout/connection-error paths.
db_util.py Persist is_stream column; percentile TTFT/TPOT/ITL/decode use stream rows only (fallback for pure non-stream).
perf_constants.py STREAM_REQUESTS / NON_STREAM_REQUESTS constants.
perf_models.py Count fields in BenchmarkSummary; conditional display when mixed.

Testing

$ pytest tests/perf/test_stream_metrics.py -q
9 passed

…d percentiles

Detect is_stream from response Content-Type (success) or request body
(error/timeout). Compute streaming metrics over stream requests only;
pure-non-stream runs fall back to prior behavior. Persist is_stream in
SQLite for percentile filtering.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces stream and non-stream metric bucketing to prevent non-stream requests from polluting streaming-specific metrics like TTFT, TPOT, and ITL. It adds an is_stream flag to BenchmarkData, updates MetricsAccumulator and database utilities to calculate separate averages and percentiles, and includes comprehensive unit tests. The review feedback points out a potential AttributeError in the new is_stream_body helper if the request body is not a dictionary (e.g., when it is None or a string on failure paths), and suggests adding a defensive type check.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread evalscope/perf/utils/benchmark_util.py Outdated
Comment on lines +15 to +20
def is_stream_body(body: dict) -> bool:
"""Best-effort stream classification from the request body.

Used only when no response is available (error/timeout paths).
"""
return body.get('stream') is True

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.

medium

The helper function is_stream_body assumes that body is always a dictionary. However, if request generation fails or connection testing fails, body can be None or a non-dictionary type (such as a string). This will cause an AttributeError when calling body.get('stream').

We should add a defensive check to ensure body is a dictionary before calling .get().

Suggested change
def is_stream_body(body: dict) -> bool:
"""Best-effort stream classification from the request body.
Used only when no response is available (error/timeout paths).
"""
return body.get('stream') is True
def is_stream_body(body: Any) -> bool:
"""Best-effort stream classification from the request body.
Used only when no response is available (error/timeout paths).
"""
if isinstance(body, dict):
return body.get('stream') is True
return False

ITL is self-filtering: non-stream requests contribute an empty
inter_chunk_latency list, so all_inter_token_latencies is already
stream-only by construction. Remove the redundant stream-only ITL
accumulator (and its now-dead all-request counterpart usage) in both the
online accumulator and the DB percentile path, keeping TTFT/TPOT/decode on
the stream subset where non-stream values genuinely pollute.

Also guard is_stream_body against non-dict bodies (None/str) on the
error/timeout paths to avoid AttributeError.

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

LGTM. Bucketing TTFT/TPOT by the actually-streamed response is the right fix, and the pure-non-stream fallback keeps existing runs unchanged.

Pushed two small refinements to the branch (e1517dad):

  1. Dropped the redundant ITL stream bucket — non-stream requests contribute an empty ITL list, so it's already stream-only by construction (TTFT/TPOT/decode correctly stay on the stream subset).
  2. Hardened is_stream_body against non-dict bodies on error/timeout paths.

Tests (9 passed) + make lint clean. Thanks!

@Yunnglin
Yunnglin merged commit fbfb625 into modelscope:main Jul 21, 2026
3 checks passed
@qiumuyang
qiumuyang deleted the fix/perf-mixed-stream-metrics branch July 21, 2026 10:50
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.

2 participants