Repository navigation
feat: model client response overhead in replay - #635
Closed
dreamtalen wants to merge 2 commits into
Closed
dreamtalen wants to merge 2 commits into
dreamtalen wants to merge 2 commits into
Conversation
Engine replay recorded every token and every finished request at the end of the engine pass that produced it, and a closed-loop client sent its next request at that same instant. A served request also pays the server's response path before the client receives its tokens: the serving engine's server processes detokenize each pass's outputs, format streamed chunks and write them to the connections. - engine.detokenization (workers, base_ms, per_output_us) mirrors engine.tokenization. Every token-emitting worker (aggregated, or decode) has `workers` detokenizer servers (vLLM --api-server-count), shared by its attention-DP ranks, and each request belongs to one of them by a stable hash of its id. When a pass ends, each server takes the pass's outputs that belong to it as one job of base_ms + per_output_us x outputs, after its earlier jobs, and the client receives them when the job ends. Token times, latency metrics, throughput and request completion use the client's receive time, and a request that waits on a finished response is released when the client receives its end. - A rejection carries no output: it skips detokenization and reaches the client when the engine rejects the request, in both topologies. - The SGLang host-loop TTFT breakdown adds mean_detokenization_ms, the first token's emission to its receipt by the client (zero without detokenization), so the stages still sum to TTFT. - Off by default and keeps outputs request for request. Offline token replay on --stack engine only; worker scaling, AFD, encoder pools and agentic profiles are rejected. Rust: Replayer::with_detokenization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
With engine.detokenization a closed-loop client sends the request that waited on a finished response as soon as it receives the end of that response. A real load generator first parses the response and returns the slot to its scheduler, which takes time that grows with the response length. - traffic.load.response_overhead (base_ms, per_output_token_us) releases a request that waits on a finished response (the next request of a concurrency slot, a session's next turn, a dependent agentic request) that long after the client receives the end of the response. It is a cost of the load generator, not an authored wait: a session's inter_turn_delay_ms follows it. A rejection is received when the engine rejects the request and also waits for the overhead. - Builds on engine.detokenization but also works alone: without detokenization the client receives every output and terminal at its pass end, and acts on a finished request after the overhead. - Worker time and gpu_hours include the overhead between requests but, like duration_ms, end with the last response. - Off by default and keeps outputs request for request. Offline token replay on --stack engine only; online replay, worker scaling, AFD, encoder pools, agentic profiles and materialized requests without source_type are rejected. Rust: Replayer::with_response_overhead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
dreamtalen
marked this pull request as draft
October 29, 2026 05:00
Base automatically changed from
yongmingd/replay-response-detokenization
to
main
October 29, 2026 18:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why and what changed
With
engine.detokenizationa closed-loop client sends the request that waited on a finished response as soon as it receives the end of that response. A real load generator such as AIPerf first parses the response's chunks and returns the concurrency slot to its scheduler, which takes time that grows with the response length. We found this gap while running the paper's vLLM replay experiments.traffic.load.response_overhead(base_ms,per_output_token_us) releases a request that waits on a finished response (a concurrency slot's next request, a session's next turn, a dependent agentic request) that long after the client receives the end of the response.inter_turn_delay_ms: a session's next turn is sent at the response end, plus the response overhead, plusinter_turn_delay_ms.base_ms.engine.detokenizationbut also works alone: without detokenization the client receives every output and terminal at its pass end, and acts on a finished request after the overhead.gpu_hoursinclude the overhead between requests but, likeduration_ms, end with the last response.--stack engineonly; online replay, worker scaling, AFD, analytical EPD, agentic profiles and SDK workloads withoutsource_typeare rejected. Rust:Replayer::with_response_overhead.Review map
crates/core/src/replay/components/client_path.rs(ResponseOverhead,ClientStep::{Receive, Act}), thencrates/core/src/replay/agg.rsanddisagg.rs(terminal release,client_overlap,only_response_overhead_in_flight,commit_response_overhead_tail), thencrates/core/tests/replay_detokenization.rs.traffic.load.response_overheadYAML block,Replayer::with_response_overhead.Evidence
cargo fmt --all -- --check;cargo test --workspace(core 2,373 passed) andcargo test --workspace --features embed-python,replay-benchwithAIC_REQUIRE_EMBEDDED_ROUND_TRIP=1(core 2,461 passed) pass with no failures;cargo test --manifest-path crates/tests/public-api/Cargo.tomlpasses (21). Perfmodel parity suites pass with goldens unchanged (test_engine_step_parity.py304,test_compile_engine_parity.py85).ruff check,ruff format --checkandscripts/ci/check_packaged_legal_files.pypass. Pythontests/andpython/aisimulate/tests -m unitfail exactly the same tests as main on that host (204 and 72; they fail on main too) and run 29 new cases.the_client_releases_its_next_request_after_receiving_and_response_overhead,a_longer_response_has_a_longer_response_overhead,a_rejected_request_waits_for_the_response_overhead,a_rejection_holds_one_slot_until_the_client_acts_on_it(concurrency 2),worker_time_ends_with_the_last_response_not_its_response_overhead,zero_detokenization_cost_and_response_overhead_keep_pass_end_delivery, invalid configs (all in aggregated and disaggregated replay);ClientPathunit tests (overhead alone keeps outputs at the pass end, Receive then Act, atomic rejection of a non-finite action time); Python compiler/runner cases, rejected modes and a native end-to-end replay.engine.detokenization. Response end → next request start at concurrency 1 (54 requests, 64–2048 output tokens) fitsbase_ms1.40 +per_output_token_us3.65 (RMSE 0.31 ms). Higher-concurrency points measure 0.2–3.4 ms above the c1 fit (AIPerf's scheduler contends with in-flight streams); this PR keeps the c1 fit.engine.detokenizationon both sides, without → with response overhead (plus the measured 0.255 ms request send leg inengine.tokenization):wall_time_ms, release builds of f45d14a (main) and this PR on an idle VM, paired alternating runs. Off: Exp1 1k c1 / 1k c256 / 8k c64 median ratio to main 0.949 / 0.965 / 1.006 (5 runs). Synthetic 512/128 closed loops of 1k / 4k / 16k and a 400 req/s Poisson overload (3 runs, every request completed, same simulated outcome across variants), host cost per output token main → this PR off → with detokenization and response overhead: 0.49 → 0.51 → 0.53, 0.66 → 0.67 → 0.61, 0.72 → 0.68 → 0.66, 0.72 → 0.67 → 0.67 µs.turnaround. Fixed:gpu_hourstail, docs rows, a request held by both the disaggregated flow and the client is counted once against the concurrency limit, leftover naming. Documented: requests ending without output waitbase_ms.Modeling or data provenance
Tracking
engine.detokenization); Exp1 numbers also include feat: model vLLM async-scheduling admission in replay #624.🤖 Generated with Claude Code