feat(sdk): self-contained result rows + closed-loop latency refinement (report + opt-in SLA) - #1510
feat(sdk): self-contained result rows + closed-loop latency refinement (report + opt-in SLA)#1510tianhaox wants to merge 15 commits into
Conversation
…ssing estimate_closed_loop_latency(): the smallest faithful core of the quantitative queueing evaluator — a deterministic replay of the continuous-batching scheduler's budget arithmetic at pass granularity (fused-pass semantics; no RNG, no fitted constants, ~100 lines). Timing enters through two injected callables (prefill_ms/decode_ms), so nothing in the prediction pipeline changes. refine_closed_loop_ttft(): the non-invasive consumption pattern — takes an agg result DataFrame and returns a copy with additive ttft_refined / tpot_refined / throughput_refined columns; legacy columns untouched, unpriceable rows get NaN. Cross-validated against the full queueing evaluator on the same perf-DB timing (Qwen3-32B tp4, isl4096/osl256, C=2..64): TTFT/TPOT/throughput all within 0.2% (bit-identical up to C=8; the residual is the full evaluator's fused mixed-pass timing hook, which this core deliberately omits). Millisecond-scale per operating point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
Sweep result rows now record the operating-point step timings run_agg already computes (ColumnsAgg: mix_step_ms, genonly_step_ms, prefill_step_ms, num_mix_steps, num_genonly_steps; ColumnsDisagg: (p)prefill_step_ms — the raw solo context latency captured before the autoscale TTFT pre-correction). Pure bookkeeping: the scalars are the FFI-returned values, identical whichever engine step computed them; no parity-governed path is touched and no Rust change is needed. On top of that, sdk/closed_loop_ttft.py adds a post-processing tier: - estimate_closed_loop_latency: the fused continuous-batching pass calendar as one pure function (deterministic budget-arithmetic replay, no RNG, no fitted constants, ~ms per operating point); - estimate_disagg_closed_loop_latency: the P/D tandem counterpart (whole-prompt prefill batches behind a round-robin router, decode-attach handoff, per-iteration decode); - refine_closed_loop_latency(df): consumes ONLY the recorded row columns and returns a copy with additive ttft_refined / tpot_refined / throughput_refined (jointly consistent: they satisfy the closed-loop identity C/X = TTFT + (osl-1)*TPOT). Legacy columns untouched. Validated against the full queueing-model evaluator on identical timing: agg within 0.2% (exact callables) / ~3% (row scalars) over C=2..64; disagg tandem bit-identical at nine configuration points spanning 1P1D/2P1D/2P2D, serial and batched prefill, shallow to deep queueing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe change records aggregate and disaggregated timing metadata, adds deterministic fused and disaggregated latency estimators, and refines result DataFrames with recalculated TTFT, TPOT, and throughput values. ChangesClosed-loop latency refinement
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
src/aiconfigurator/sdk/closed_loop_ttft.py (4)
351-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog the swallowed exception before writing NaN.
The handler catches
KeyError,ValueError,TypeError, andRuntimeErrorand writes NaN. A missing column, a convergence failure, and a genuine bug all produce the same silent result. Add a debug log so the cause is recoverable.🔍 Proposed fix
- except (KeyError, ValueError, TypeError, RuntimeError): + except (KeyError, ValueError, TypeError, RuntimeError) as exc: + logger.debug("closed-loop refinement skipped a row: %s", exc) ttfts.append(float("nan")) tpots.append(float("nan")) xs.append(float("nan"))Add the module logger near the imports:
import logging logger = logging.getLogger(__name__)🤖 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 `@src/aiconfigurator/sdk/closed_loop_ttft.py` around lines 351 - 354, Add a module-level logger near the imports, then update the exception handler in the TTFT processing flow to log the caught exception at debug level before appending NaN values to ttfts, tpots, and xs. Preserve the existing caught exception types and NaN fallback behavior.
313-316: 🚀 Performance & Scalability | 🔵 TrivialConsider the cost of refining large sweep DataFrames.
Each row runs a full pass-by-pass simulation.
estimate_closed_loop_latencyallows up to200 * (warmup_generations + window_generations) * oslpasses, which is 409,600 passes forosl=256with the defaults. A sweep DataFrame with thousands of rows can take a long time.Document the expected per-row cost, or expose
warmup_generations/window_generationsonrefine_closed_loop_latencyso callers can trade accuracy for speed.🤖 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 `@src/aiconfigurator/sdk/closed_loop_ttft.py` around lines 313 - 316, The refine_closed_loop_latency loop currently performs an expensive full simulation for every DataFrame row. Expose warmup_generations and window_generations through refine_closed_loop_latency and pass them to estimate_closed_loop_latency, preserving existing defaults so callers can trade accuracy for runtime.
135-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPrefer identity-based removal over
list.remove.
slots.remove(s)compares list elements by value. Slots are plain lists, so two slots with identical contents compare equal. The current code is safe only because a finished slot never equals an unfinished one. That invariant is easy to break in a later change.Rebuild the list by identity instead.
🛡️ Proposed refactor
finished = [s for s in completers + decoders if s[1] >= osl] + if finished: + finished_ids = {id(s) for s in finished} + slots = [s for s in slots if id(s) not in finished_ids] for s in finished: completions += 1 if completions == warmup: steady_start = now if completions > warmup: steady_completions += 1 gaps.extend(s[5]) if s[5]: tpots.append(sum(s[5]) / len(s[5])) - slots.remove(s) pending.append((now + turnaround_ms, new_slot(now)))The same concern applies to
q.remove(r)at line 253 andrun.remove(r)at line 280.🤖 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 `@src/aiconfigurator/sdk/closed_loop_ttft.py` around lines 135 - 146, Replace value-based removals with identity-based filtering for the finished slot in the shown completion loop, and apply the same change to q.remove(r) and run.remove(r). Rebuild each list while excluding only the exact object instance being removed, preserving all other equal-valued entries.
151-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared quantile helper.
The project ignores Ruff
E731, so replacing the lambda is not required for lint compliance. The twoqimplementations are identical; move them to one module-level_quantilehelper to prevent drift. A one-click change is not suitable because the refactor changes scope and updates both estimators.🤖 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 `@src/aiconfigurator/sdk/closed_loop_ttft.py` at line 151, Extract the duplicated q logic from both estimators into one module-level _quantile helper in closed_loop_ttft.py, then update each estimator to call it. Preserve the current empty-input behavior and quantile index calculation exactly, removing the local q definitions to prevent future drift.tests/unit/sdk/test_closed_loop_ttft.py (1)
90-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd two cases that guard the production paths.
Both fixtures are hand-built dicts. Two realistic paths are untested.
_build_disagg_summary_dictand_rate_match_dictwrite(p)prefill_step_mswith a.get()default, so a row can carryNone. No test covers that row reachingrefine_closed_loop_latency.- The fixtures do not reference
common.ColumnsAggorcommon.ColumnsDisagg. If a column is renamed inaic-core/src/aiconfigurator_core/sdk/common.py, these tests still pass while every real row degrades to NaN.🧪 Proposed additional test
def test_disagg_row_without_prefill_step_ms_is_nan(self): df = pd.DataFrame( [ { "concurrency": 8, "isl": 4096, "osl": 64, "(p)workers": 1, "(d)workers": 1, "(p)bs": 1, "(d)bs": 64, "(p)prefill_step_ms": None, "tpot": 21.0, "ttft": 3600.0, } ] ) out = refine_closed_loop_latency(df) assert pd.isna(out["ttft_refined"].iloc[0]) assert out["ttft"].iloc[0] == 3600.0For point 2, assert that the required column names are present in
common.ColumnsAggandcommon.ColumnsDisagg.🤖 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/unit/sdk/test_closed_loop_ttft.py` around lines 90 - 150, Add coverage in TestRefineDataFrame for a disaggregated row whose “(p)prefill_step_ms” value is None, asserting refinement returns NaN while preserving legacy ttft. Also validate that all column names used by these fixtures are defined in common.ColumnsAgg and common.ColumnsDisagg, so renames cannot silently invalidate the tests.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 `@aic-core/src/aiconfigurator_core/sdk/common.py`:
- Around line 783-789: Split the inline comment after "power_w" from the
following operating-point step-timings comment so each comment starts on its own
line, preserving the existing field order and wording.
In `@docs/design/closed_loop_refinement.md`:
- Around line 48-59: Update the Validation section to cite reproducible evidence
for the reported agreement figures, including the nine-point disaggregated
results. Reference the script, notebook, or test that generates these
comparisons; if no such artifact exists, label the claims as one-off
measurements and include the measurement date and full configuration.
In `@src/aiconfigurator/sdk/closed_loop_ttft.py`:
- Around line 56-59: Update the slot-layout comment above new_slot to describe
the six fields actually returned: remaining_prefill, generated, arrival_ms,
first_token_ms, last_token_ms, and the gaps list at index 5; remove the
nonexistent gap_sum, gap_count, and gaps_list_or_None entries.
In `@src/aiconfigurator/sdk/inference_session.py`:
- Around line 272-274: Update the prefill_dict initialization in the inference
session flow to subtract prefill_dict.get("encoder_latency", 0.0) from ttft
before storing prefill_step_ms, while leaving the subsequent ttft autoscaling
correction unchanged.
In `@src/aiconfigurator/sdk/picking.py`:
- Line 122: Use a float NaN fallback for the prefill step field in both
rate-matching builders: update _build_disagg_summary_dict in
src/aiconfigurator/sdk/picking.py#L122-L122 and _rate_match_dict in
src/aiconfigurator/sdk/sweep.py#L163-L163 to convert prefill_step_ms with a
float("nan") default, preserving numeric column dtype when the field is absent.
---
Nitpick comments:
In `@src/aiconfigurator/sdk/closed_loop_ttft.py`:
- Around line 351-354: Add a module-level logger near the imports, then update
the exception handler in the TTFT processing flow to log the caught exception at
debug level before appending NaN values to ttfts, tpots, and xs. Preserve the
existing caught exception types and NaN fallback behavior.
- Around line 313-316: The refine_closed_loop_latency loop currently performs an
expensive full simulation for every DataFrame row. Expose warmup_generations and
window_generations through refine_closed_loop_latency and pass them to
estimate_closed_loop_latency, preserving existing defaults so callers can trade
accuracy for runtime.
- Around line 135-146: Replace value-based removals with identity-based
filtering for the finished slot in the shown completion loop, and apply the same
change to q.remove(r) and run.remove(r). Rebuild each list while excluding only
the exact object instance being removed, preserving all other equal-valued
entries.
- Line 151: Extract the duplicated q logic from both estimators into one
module-level _quantile helper in closed_loop_ttft.py, then update each estimator
to call it. Preserve the current empty-input behavior and quantile index
calculation exactly, removing the local q definitions to prevent future drift.
In `@tests/unit/sdk/test_closed_loop_ttft.py`:
- Around line 90-150: Add coverage in TestRefineDataFrame for a disaggregated
row whose “(p)prefill_step_ms” value is None, asserting refinement returns NaN
while preserving legacy ttft. Also validate that all column names used by these
fixtures are defined in common.ColumnsAgg and common.ColumnsDisagg, so renames
cannot silently invalidate the tests.
🪄 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: c5942aa3-9438-41bf-b01a-c8738c3186ba
📒 Files selected for processing (8)
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.pyaic-core/src/aiconfigurator_core/sdk/common.pydocs/design/closed_loop_refinement.mdsrc/aiconfigurator/sdk/closed_loop_ttft.pysrc/aiconfigurator/sdk/inference_session.pysrc/aiconfigurator/sdk/picking.pysrc/aiconfigurator/sdk/sweep.pytests/unit/sdk/test_closed_loop_ttft.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
- GitHub Check: Collect snapshot (new)
- GitHub Check: Collect snapshot (old)
- GitHub Check: Build wheels (macosx_arm64)
- GitHub Check: Build wheels (manylinux_2_28_x86_64)
- GitHub Check: Build wheels (manylinux_2_28_aarch64)
- GitHub Check: Build and Test (unit)
- GitHub Check: Build and Test (e2e)
- GitHub Check: Rust/Python engine-step parity
- GitHub Check: aic-core public API contract
- GitHub Check: Cargo Deny
🧰 Additional context used
📓 Path-based instructions (5)
**/*
⚙️ CodeRabbit configuration file
**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.
- Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
- If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.
Files:
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.pysrc/aiconfigurator/sdk/sweep.pysrc/aiconfigurator/sdk/inference_session.pydocs/design/closed_loop_refinement.mdsrc/aiconfigurator/sdk/picking.pyaic-core/src/aiconfigurator_core/sdk/common.pytests/unit/sdk/test_closed_loop_ttft.pysrc/aiconfigurator/sdk/closed_loop_ttft.py
aic-core/src/aiconfigurator_core/sdk/**
⚙️ CodeRabbit configuration file
aic-core/src/aiconfigurator_core/sdk/**: - Verify core SDK API changes remain compatible with legacy aiconfigurator.sdk imports, generator inputs, profiler data flow, and documented examples.
- Flag upper-layer dependencies or silent schema drift introduced into the minimal core distribution.
Files:
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.pyaic-core/src/aiconfigurator_core/sdk/common.py
src/aiconfigurator/sdk/**
⚙️ CodeRabbit configuration file
src/aiconfigurator/sdk/**: - Verify SDK API changes remain compatible with generator inputs, profiler data flow, and documented examples.
- Flag silent schema or field-name drift between SDK models and generator/module bridge code.
Files:
src/aiconfigurator/sdk/sweep.pysrc/aiconfigurator/sdk/inference_session.pysrc/aiconfigurator/sdk/picking.pysrc/aiconfigurator/sdk/closed_loop_ttft.py
docs/**
⚙️ CodeRabbit configuration file
docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.
- Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.
Files:
docs/design/closed_loop_refinement.md
tests/**
⚙️ CodeRabbit configuration file
tests/**: - Check that tests cover the changed behavior rather than only the happy path.
- Watch for fixtures or golden outputs that mask backend drift, support-matrix ordering changes, or CLI output regressions.
Files:
tests/unit/sdk/test_closed_loop_ttft.py
🧠 Learnings (2)
📚 Learning: 2026-08-03T13:45:40.375Z
Learnt from: tianhaox
Repo: ai-dynamo/aiconfigurator PR: 1460
File: collector/case_generator.py:2228-2254
Timestamp: 2026-08-03T13:45:40.375Z
Learning: For DeepSeek-V4 CSA top-k DELTA calibration in ai-dynamo/aiconfigurator, apply calibration data only when the runtime native num_heads exactly matches the calibration bucket (currently 64 or 128). Do not borrow calibration across head-count buckets: Flash and Pro DELTA values can diverge by up to 37% at long-context shapes. This exact-match rule applies to both Python and Rust consumers, including collector/case_generator.py.
Applied to files:
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.pysrc/aiconfigurator/sdk/sweep.pysrc/aiconfigurator/sdk/inference_session.pysrc/aiconfigurator/sdk/picking.pyaic-core/src/aiconfigurator_core/sdk/common.pytests/unit/sdk/test_closed_loop_ttft.pysrc/aiconfigurator/sdk/closed_loop_ttft.py
📚 Learning: 2026-05-01T00:39:37.334Z
Learnt from: simone-chen
Repo: ai-dynamo/aiconfigurator PR: 956
File: src/aiconfigurator/sdk/perf_database.py:4144-4151
Timestamp: 2026-05-01T00:39:37.334Z
Learning: In src/aiconfigurator/sdk/**/*.py, preserve upstream metadata for PerformanceResult.source: since PerformanceResult defaults source to "silicon", callers should not force-set result.source (e.g., PerfDatabase._query_silicon_or_hybrid should rely on the default and keep any existing source information rather than overwriting it). Only set source explicitly when you truly intend to change it.
Applied to files:
src/aiconfigurator/sdk/sweep.pysrc/aiconfigurator/sdk/inference_session.pysrc/aiconfigurator/sdk/picking.pysrc/aiconfigurator/sdk/closed_loop_ttft.py
🪛 GitHub Actions: Lint and Format / 1_Lint and Format (Ruff).txt
aic-core/src/aiconfigurator_core/sdk/common.py
[error] 783-783: Ruff E501: line too long (121 > 120). The 'ruff check .' command failed with exit code 1.
🪛 GitHub Actions: Lint and Format / Lint and Format (Ruff)
aic-core/src/aiconfigurator_core/sdk/common.py
[error] 783-783: Ruff E501: Line too long (121 > 120). The 'power_w' line exceeds the maximum allowed length.
🔇 Additional comments (6)
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py (1)
1644-1652: LGTM!src/aiconfigurator/sdk/inference_session.py (1)
733-736: LGTM!src/aiconfigurator/sdk/picking.py (1)
444-446: LGTM!src/aiconfigurator/sdk/sweep.py (1)
686-689: LGTM!tests/unit/sdk/test_closed_loop_ttft.py (1)
25-88: LGTM!aic-core/src/aiconfigurator_core/sdk/common.py (1)
818-821: 🗄️ Data Integrity & IntegrationKeep
(p)prefill_step_msin its current position.All
ColumnsDisaggproducers pass dictionaries, so the inserted field does not shift values for positional consumers.> Likely an incorrect or invalid review comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
…-goal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/design/closed_loop_refinement.md (1)
74-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClarify that the p99 comparison is estimator-only.
refine_closed_loop_latencyaddsttft_refined,tpot_refined, andthroughput_refined. It does not add a refined p99 column. The estimator computes p99 values, but the refinement function discards them. As written, this paragraph can imply that the PR produces refined TTFT p99, which conflicts with Lines 101-102.Suggested change
- throughput within 5.3% on 7 of 9 points and TTFT p99 within a few percent at low/moderate + throughput within 5.3% on 7 of 9 points and the estimator's TTFT p99 within a few percent at low/moderate🤖 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 `@docs/design/closed_loop_refinement.md` around lines 74 - 80, Update the serving-comparison paragraph to clarify that the TTFT p99 comparison reflects estimator-computed values only, not a refined p99 output from refine_closed_loop_latency. Avoid implying that the refinement adds a refined TTFT p99 column, and keep the existing throughput and latency findings unchanged.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.
Nitpick comments:
In `@docs/design/closed_loop_refinement.md`:
- Around line 74-80: Update the serving-comparison paragraph to clarify that the
TTFT p99 comparison reflects estimator-computed values only, not a refined p99
output from refine_closed_loop_latency. Avoid implying that the refinement adds
a refined TTFT p99 column, and keep the existing throughput and latency findings
unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: d81b5c00-9718-411d-bc40-77d923ebd3c5
📒 Files selected for processing (1)
docs/design/closed_loop_refinement.md
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
- GitHub Check: Collect snapshot (new)
- GitHub Check: Collect snapshot (old)
- GitHub Check: aic-core public API contract
- GitHub Check: Rust/Python engine-step parity
- GitHub Check: Build and Test (unit)
- GitHub Check: Build and Test (e2e)
- GitHub Check: Build wheels (macosx_arm64)
- GitHub Check: Cargo Deny
- GitHub Check: Build wheels (manylinux_2_28_aarch64)
- GitHub Check: Build wheels (manylinux_2_28_x86_64)
🧰 Additional context used
📓 Path-based instructions (2)
**/*
⚙️ CodeRabbit configuration file
**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.
- Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
- If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.
Files:
docs/design/closed_loop_refinement.md
docs/**
⚙️ CodeRabbit configuration file
docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.
- Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.
Files:
docs/design/closed_loop_refinement.md
🔇 Additional comments (3)
docs/design/closed_loop_refinement.md (3)
61-94: Add reproducible evidence for the validation claims.These lines add exact DES, real-serving, and error-mode measurements. The supplied context does not include a script, notebook, test, or dated artifact that reproduces them. Add the repository artifact and its configuration, or label the numbers as one-off measurements. The same evidence gap was raised for Lines 48-59 in the previous review, so this is a duplicate concern. A one-click change is not safe because the correct wording depends on the available validation artifact.
As per path instructions, documentation must match behavior and support-matrix or test evidence.
Source: Path instructions
96-100: LGTM!
104-113: 🗄️ Data Integrity & IntegrationConfirm the SLA target semantics before documenting this wiring.
refine_closed_loop_latencyprovides steady meanttft_refinedandtpot_refinedonly. It does not provide p50, p99, or transient values. The statement in Lines 111-112 supports mean-based SLA targets, but the phrase “an SLA on served latency” is broader. Confirm the existing SLA contract, then narrow the wording or add percentile outputs in the follow-up. No one-click change is safe because the correct metric depends on the SLA contract.Source: Path instructions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
filter_closed_loop_sla(df, ttft_ms, tpot_ms) refines the result rows and drops those whose refined steady closed-loop values exceed the targets; unpriceable rows keep the pipeline's legacy verdict. The pipeline filter itself is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
pick_under_closed_loop_sla filters the full per-operating-point summary on the refined values and keeps each deployment's best surviving row, so a deployment whose picked point violates the refined SLA falls back to its compliant lower-concurrency point instead of vanishing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
Task(refined_sla=True) makes sweep_agg and sweep_disagg enforce the TTFT/TPOT targets on the refined steady closed-loop values instead of the fixed-factor-corrected ones: agg points gate on the solo chunked prefill lower bound then price the candidate; disagg drops the 1.8 prefill gate to the solo bound (a true necessary condition) and walks each decode-parallel category best-first, keeping the first rate-matched combination whose refined values comply. Pricing is memoized on the operating-point inputs (targets don't change the refined value). Default off; legacy results are byte-identical (1647 sdk tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
_refined_tune_combo binary-searches the largest concurrency (never above the decode ladder's memory-validated inherited point) whose refined closed-loop TTFT/TPOT meet the targets, and reprices the row's latency/throughput columns from the tandem estimate — retiring the flat rate-match derate for refined rows. Top-K combos per decode-parallel category are tuned; ladder families with no compliant rung come back at their knee instead of vanishing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
The rate-matched row prices its decode step at the ladder point's batch, but the tandem's actual decode occupancy at the searched concurrency can sit far below it; cycle conservation turns the TPOT overestimate into a TTFT underestimate and the knee overshoots. The knee search now iterates the decode step to the Little fixed point on the decode ladder's (concurrency, tpot) rungs. Real 8xH20 arbitration: decode step error 29% -> 4%, knee pick TTFT error -29% (SLA violated) -> +14% (conservative, SLA met), throughput promise within 1.3%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
Profile showed 78% of tandem time in per-iteration O(C) min-scans over the prefill queues and decode pools. For osl >= 2 every dispatch comes from a decode completion and events process in non-decreasing time, so each queue is FIFO in visible time: the earliest entry is [0] and the eligible set is a leading run. Verified bit-identical on a 600-config grid and a full refined pareto sweep (1792 pricings, 44 rows byte-equal); worst-case pricing 1047 -> 235 ms, refined pareto sweep 129 -> 58 s. osl == 1 keeps the original scans (completions interleave within one event there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
- subtract colocated encoder latency at all four (p)prefill_step_ms stash sites so VL prefill worker duration is not overstated (text-only rows have encoder_latency=0 — numbers unchanged) - coerce (p)prefill_step_ms to float when present in both rate-match builders (numeric dtype); keep the None sentinel when absent (the parity test compares by equality, which a NaN default would break) - mark design-doc validation figures as one-off measurements with date and configuration; reference the in-repo semantic tests - fix the stale slot-layout comment (six fields) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
run_single_disagg rate-matched raw summary rows directly, so its output lacked (p)prefill_step_ms (not refinable) and its ttft was the raw solo value — neither the legacy 1.8 semantics nor the refined one. The row now stashes the raw solo context latency like the sweep sites, and with Task(refined_sla=True) both run_single_agg and run_single_disagg report the closed-loop repriced headline (reprice_closed_loop_row: consistent ttft/tpot/request_latency/throughput family; unpriceable rows pass through unchanged). Default output unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: tianhaox <tianhaox@nvidia.com>
jasonqinzhou
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES · 5.8/10 · high confidence
What this PR does
This PR makes aggregate and disaggregated result rows carry their operating-point timing inputs, adds deterministic closed-loop replay for refined TTFT, TPOT, and throughput, and optionally uses those values for sweep SLA enforcement and single-point headlines.
The implementation is unusually thoughtful about keeping the default path unchanged: the timing columns are additive, the replay is deterministic and memoized, the design notes distinguish the post-processing and in-sweep tiers, and the author backed the model with semantic tests plus queueing-evaluator and real-serving comparisons.
Why this score
The current head is 5.8/10 with REQUEST_CHANGES. The text-only single-replica path is well structured and its submitted tests and CI are green, but two confirmed P1 contract breaks make the opt-in results unsafe for supported configurations: multimodal rows are repriced without encoder latency or visual context tokens, and aggregate refined throughput loses the pp/attention-DP deployment scale. Two narrower P2 gaps also remain: osl=1 produces a NaN TPOT that rejects valid refined-SLA rows, and aggregate speculative-decoding progress is ignored by the replay.
| decode_max_seqs=int(row["(d)bs"]), | ||
| ) | ||
| else: | ||
| chunk_ref = max(1, min(int(row["ctx_tokens"]), int(row["isl"]) - int(row.get("prefix", 0) or 0))) |
There was a problem hiding this comment.
[P1] Do not apply the text-only replay to multimodal rows. BaseBackend.run_agg prices prefill/decode at text_isl + img_ctx_tokens but stores only the text isl in the result row, while prefill_step_ms intentionally excludes the encoder phase; the disagg row likewise stores raw context time after subtracting encoder_latency. Both branches here then ignore encoder_latency, and the agg branch also reconstructs the pass count from the text-only length. A focused probe shows that changing a row from encoder_latency=0 to 500 leaves the refined triple identical, and reprice_closed_loop_row overwrites the legacy TTFT with that lower value. With refined_sla=True this can admit a VLM deployment whose actual TTFT exceeds the target. Please either record/model the visual-token and encoder stages for both row shapes, or return NaN and preserve the legacy verdict for multimodal rows until that replay is supported; add aggregate and disaggregated VLM regressions.
| step = float(row["prefill_step_ms"]) | ||
| t_gen = float(row["genonly_step_ms"]) | ||
| r = estimate_closed_loop_latency( | ||
| concurrency=int(row["bs"]), |
There was a problem hiding this comment.
[P1] Preserve deployment-wide aggregate throughput scaling. run_agg models one local batch b and then scales both concurrency and output throughput by pp * attention_dp_size. Passing only row["bs"] here correctly replays one local scheduler for latency, but the returned throughput_rps is then published by refine_closed_loop_latency / reprice_closed_loop_row as deployment-wide throughput without restoring that scale. For a synthetic row with bs=4 and concurrency=8, the closed-loop identity holds for 4 / X but fails by 2x for the row's advertised 8 / X; run_single_agg(refined_sla=True) correspondingly halves seq/s and tokens/s. Please scale the replay throughput by concurrency / bs (and account for that factor in or after the cache) while leaving per-replica latency unchanged, with a PP/attention-DP regression.
| "ttft_steady_p50": q(ttfts, 0.5), | ||
| "ttft_steady_p99": q(ttfts, 0.99), | ||
| "ttft_transient_max": transient_max, | ||
| "tpot_mean": sum(tpots) / len(tpots) if tpots else float("nan"), |
There was a problem hiding this comment.
[P2] Define no-decode TPOT for osl=1 instead of returning NaN. osl=1 is a supported no-decode workload with existing backend coverage, but neither estimator records an inter-token gap, so both return tpot_mean=NaN. The row still has a finite refined TTFT, which makes _sweep_one_parallel_agg treat it as priced and then reject it because NaN <= tpot_target is false; reprice_closed_loop_row also turns the otherwise valid request latency into NaN because NaN * 0 remains NaN. A focused probe reproduced both behaviors. Please give the no-decode case an explicit finite TPOT/request-latency convention (for example TPOT 0), mirror it in the tandem estimator, and add aggregate/disaggregated osl=1 tests through the refined-SLA and reprice paths.
| # per-chunk prefill cost; decode priced at the recorded | ||
| # operating-point iteration time | ||
| prefill_ms=lambda b, i, p, _s=step, _c=chunk_ref: _s * (b * max(1, i - p)) / _c, | ||
| decode_ms=lambda b, c, _g=t_gen: _g, |
There was a problem hiding this comment.
[P2] Honor aggregate speculative-decoding progress in the replay. When nextn_accepted > 0, run_agg reduces decode_iterations and num_genonly_steps using decode_tokens_per_iteration; genonly_step_ms remains the cost of one verification iteration. This replay always advances one output token per genonly_step_ms and neither reads the recorded step counts nor carries decode_tokens_per_iteration, so an MTP row with fewer scheduler steps prices identically to a non-speculative row with the same scalars. That overstates refined TPOT/latency and can discard valid refined_sla points or corrupt the single-point headline. Please record the accepted-token progress in the self-contained row and advance the replay consistently (or fail open for such rows), with a SpeculativeDecodingProfile regression.
|
|
||
| ## Why a recursion instead of a formula | ||
|
|
||
| Closed-loop TTFT under continuous batching is not monotone in concurrency |
There was a problem hiding this comment.
Does “closed-loop” here mean an infinite request stream with a fixed concurrency/BS, where whenever one request finishes, a new request immediately arrives to keep the number of active requests constant? If so, the arrival times are derived from the simulated completion times rather than being explicit inputs—is that the intended workload model?
There was a problem hiding this comment.
yes. the closed loop refers to the concurrency based workload where a concurrency N is kept. The open loop is more like request rate based. there's no fixed budget of the slots for the concurrent requests.
What
Three pieces; default results unchanged (the third is opt-in):
1. Result rows become self-contained.
run_aggrecords the operating-point step timings it already computes into the summary row (ColumnsAgg:mix_step_ms,genonly_step_ms,prefill_step_ms,num_mix_steps,num_genonly_steps). Disagg rows record(p)prefill_step_ms— the raw solo context latency of one prefill worker, captured before the autoscale TTFT pre-correction.2. A post-processing refinement tier (
sdk/closed_loop_ttft.py):Under the hood: a deterministic replay of the scheduler's own budget arithmetic at pass granularity — the fused continuous-batching calendar for agg rows, the P/D tandem (round-robin router, whole-prompt prefill batches, decode-attach handoff) for disagg rows. No RNG, no fitted constants, ~ms per row. The three refined columns are one consistent timeline (they satisfy the closed-loop identity
C/X = TTFT + (osl−1)·TPOT), so consume them as a set.3. Opt-in refined-SLA enforcement in the sweep (
Task(refined_sla=True), default off):sweep_agg/sweep_disaggcompare the TTFT/TPOT targets against the refined steady closed-loop values instead of the fixed-factor-corrected ones. Agg candidate points gate on the solo chunked-prefill lower bound and are then priced; disagg drops the 1.8 prefill gate to the solo bound (a true necessary condition) and knee-tunes the top rate-matched combinations per decode-parallel category — a binary search for the largest compliant concurrency (never above the decode ladder's memory-validated point), with the decode step iterated to its occupancy-consistent value on the decode ladder (Little's law fixed point). Priced rows are repriced from the tandem, retiring the flat rate-match derate for refined rows. Post-processing helpersfilter_closed_loop_sla/pick_under_closed_loop_slacover the same semantics outside the pipeline. Pricing is memoized; overhead on an 8-GPU Qwen3-32B sweep: agg +~2s, disagg ~2min.Usage & cost
Default: everything off. Without touching anything, the only change is bookkeeping — result rows carry the extra step-timing columns; sweep results, SLA filtering, picking and runtimes are byte-identical to main (locked by the parity/unit suites).
Tier 1 — post-processing (call it when you want it):
Cost: ~ms per row (pure-Python deterministic replay; no DB or engine calls).
Tier 2 — in-sweep enforcement:
Task(refined_sla=True)— orrefined_sla: truein an experiment YAML (maps throughTask.from_yaml). Deliberately NOT applied topick_autoscale(the dynamo-planner profiling flow: its 1.8 is autoscaling headroom, a different semantic) or AFD frames (pass through unpriced pending their own validation). Measured on an 8-GPU Qwen3-32B sweep (isl4096/osl256, ttft 2000ms / tpot 30ms, H20 perf data, warm DB):All warm-process numbers (cold adds a one-time ~3-6s perf-DB load either way); pricing is memoized process-wide, so repeated sweeps over the same operating points pay it once:
run_single_agg/run_single_disagg)run_single_disagg's default output previously reported the raw solo TTFT with no correction at all)The disagg pareto cost is the concurrency knee search: each latency-target pair prices its own rate-matched candidates through the tandem (~1.8k pricings at 4–235ms each; operating-point results are memoized). Single-SLA-point tasks — the common "give me a deployment for this target" call — stay at seconds.
Why
The heuristic TTFT corrections on result rows are clamped/fitted and miss the real shape of closed-loop queueing — which is not even monotone in concurrency (replacement prefill chunks stretch passes; past the knee, larger decode batches eat the cycle and TTFT recedes). A recursion reproduces this because both effects emerge from iterating the budget arithmetic; a multiplier cannot. Derivation and scope notes:
docs/design/closed_loop_refinement.md.Validation (vs the full queueing-model evaluator, identical timing; Qwen3-32B, H20, isl4096/osl256)
Validation against real serving
Same estimators, nine closed-loop operating points against
trtllm-serve1.3.0rc20 (Qwen3-32B tp4, H20, chunked prefill ON, isl 1024/4096/8192 x C 4/16/48; predictions computed from the perf DB before the measurements ran):Reading: throughput within 5.3% on 7/9 points; the two misses (1024 C48, 4096/256 C48) trace to perf-DB timing in deep-churn regimes (real mixed passes cost up to ~2x the DB pricing; the same gap appears if you price those steps standalone), not to the recursion — refined TTFT errs on the conservative side there. The measured closed-loop TTFT correction factor (steady TTFT / solo no-queue TTFT) spans 2.8-14.4x across this small grid and is non-monotone in concurrency — which is why the refinement computes it from the operating point instead of applying any fixed multiplier.
8xH20 A/B arbitration: legacy pick vs refined pick
For the same task (8 GPUs, isl4096/osl256, TTFT<=2000ms, TPOT<=30ms) the legacy sweep picks 1P(tp4)+1D(tp4)@C5;
refined_sla=Truepicks 3P(tp2)+1D(tp2)@c16. Both were deployed on 8xH20 (TRT-LLM native disagg, ctx unchunked bs1, gen bs64; predictions frozen before measuring; steady-state protocol: 20 generations/slot, first 5 and last 1 discarded — the synchronized first round costs 1.5-5x the steady TTFT and multi-worker round-robin needs ~5 generations to relax).Arm A — the legacy pick's topology at three concurrencies the legacy model prices identically (solo x1.8 = 1077.5ms):
Arm B — the refined pick's topology:
Real-vs-real: the refined pick serves 91.8 vs 57.1 tok/s/gpu (+61%) under the same TTFT SLA — the legacy 1.8x gate had pre-killed the tp2-prefill family (solo x1.8 = 2070 > 2000) whose true closed-loop TTFT at the knee is 1201ms. X(C16)=X(C19) confirms the knee plateau: concurrency past the knee buys zero throughput and doubles TTFT.
Rust/Python parity
No parity-governed path is touched (
operations/**,perf_database.py,perf_interp/**,engine.py,rust_engine_step.pyall unchanged). The recorded scalars are the FFI-returned values — identical whichever engine step computed them — and the estimators live in the Python orchestration layer, which stays Python by the dedup plan.test_rate_match_parityextended coverage passes.Tests
1004 sdk unit tests green (13 new: estimator semantics incl. the closed-loop identity, P/D imbalance signatures, chunked-off admission, DataFrame refine with NaN tolerance, rate-match parity).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation