Skip to content

refactor(replay): unify planner runtime boundary [DYN-3850] - #12334

Merged
PeaBrane merged 6 commits into
mainfrom
codex/computelab-planner-replay-20260728
Jul 29, 2026
Merged

refactor(replay): unify planner runtime boundary [DYN-3850]#12334
PeaBrane merged 6 commits into
mainfrom
codex/computelab-planner-replay-20260728

Conversation

@PeaBrane

@PeaBrane PeaBrane commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make normal and planner offline trace/synthetic replay share the same prepared-workload executor. Planner mode supplies an optional ReplayScalingPolicy; normal mode supplies None.
  • Keep request routing monomorphized. The scaling policy is the only dynamic callback, and it runs only at the scaling cadence after same-timestamp request and worker events settle.
  • Move scaling application into the event runtime with explicit active/starting/draining lifecycle semantics, atomic two-pool validation, startup cancellation before active drain, and independent disaggregated prefill/decode targets.
  • Make ReplayPlannerAdapter the context-managed Python owner and accept an injectable EngineProtocol, leaving that protocol as the future local-versus-IPC planner boundary.
  • Remove the planner bridge, handle, and hook integration layer while preserving the public CLI/Python APIs, report schemas, planner diagnostics, goodput fields, workload preparation modes, and online replay path.

Reviewer guide

  1. The semantic core is in lib/mocker/src/replay/offline/{executor,scaling,agg,disagg}.rs and the shared entrypoint dispatch.
  2. lib/bindings/python/rust/llm/replay.rs contains the optional Python scaling-policy boundary; the no-policy path still releases the GIL.
  3. components/src/dynamo/planner/offline/replay_adapter.py owns planner bootstrap, execution, report finalization, and cleanup through EngineProtocol.
  4. The remaining changes are public API plumbing, lifecycle/report accounting, and focused Rust/Python regression coverage.

Online/live replay is intentionally unchanged. The independently investigated KVBM replay ordering fix and its campaign harness are not part of this diff; the narrow fix is tracked separately in #12326.

Validation

  • Byte parity: 11/11 completed semantic rows produced one stable digest per revision across two separate processes and then matched byte-for-byte between baseline and candidate:

    • 3 canonical 5,000-row native rows: vLLM aggregated KV, vLLM disaggregated KV + KVBM, and SGLang aggregated KV.
    • 2 public Python no-policy rows: trace and synthetic.
    • 6 real-planner rows: vLLM/SGLang aggregated plus disaggregated prefill and decode scaling.
    • Canonicalization removed only wall-clock execution time, its derived throughput fields, and the planner HTML output path. Simulated tick/FPM times, scaling events, worker lifecycle IDs/counts, diagnostics, traffic data, and request results remained in the comparison.
  • Planner lifecycle: aggregated vLLM and SGLang exercised 1 → 2 → 3 → 2 → 1; disaggregated prefill exercised 1 → 2 → 1; disaggregated decode exercised 1 → 2 → 3 → 2 → 1. Every fixture completed every request and retained ordered scaling events plus active/starting/draining observations.

  • Paired performance: all 8/8 rows passed after 5 warmups and 30 randomized adjacent pairs with a fixed-seed, one-sided 95% bootstrap over 200,000 draws. Median candidate/baseline ratios and 95% upper bounds were:

    Row Median ratio 95% upper
    vLLM aggregated KV 0.99828 1.01335
    vLLM disaggregated KV 0.99650 1.00561
    SGLang aggregated KV 0.99239 1.00314
    SGLang disaggregated KV 1.00910 1.02083
    Python no-policy trace 0.99069 0.99628
    Python no-policy synthetic 0.98940 0.99904
    Planner vLLM aggregated 0.99472 1.00100
    Planner vLLM disaggregated decode 1.00340 1.00626

    The pass gate was an upper bound of 1.05; the worst observed upper bound was 1.02083.

  • Size: native replay binary ratio 1.00130 (.text 1.00172); PyO3 extension ratio 0.99897 (.text 0.99941), both below the 1.05 gate.

  • Local suites: 176 offline replay Rust tests passed; 4 one-worker KV-router validator tests passed; 104 Python binding replay tests passed with 9 expected skips; 24 planner offline tests passed; all changed files passed pre-commit.

The frozen native SGLang-disaggregated 5,000-row semantic row remains a bounded wall-time blocker, not a parity exception: its baseline process stayed CPU-active through independent 30- and 45-minute limits without emitting a report. The same configuration completed 1,000/1,000 requests in 0.528 seconds during qualification.

The remote campaign used the same temporary deterministic replay fixture in both revisions to isolate this planner refactor. That fixture, the KVBM ordering change, and the parity driver are excluded from this PR.


Open in Devin Review

Summary by CodeRabbit

  • New Features
    • Added scaling-policy support to offline trace and synthetic replay.
    • Replay can now apply dynamic worker scaling decisions for aggregated and disaggregated workloads.
    • Added scaling diagnostics, snapshots, and transition reporting.
  • Bug Fixes
    • Improved worker accounting while workers are starting or draining.
    • Scaling policies now reject unsupported replay modes and trace formats with clear errors.
    • Replay cleanup is handled automatically, including when errors occur.
  • Tests
    • Added coverage for scale-up/scale-down behavior, callback errors, validation, cleanup, and replay equivalence.

Signed-off-by: PeaBrane <yanrpei@gmail.com>
@datadog-official

datadog-official Bot commented Jul 29, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 2 Pipeline jobs failed

PR | backend-status-check   View in Datadog   GitHub Actions

PR | trtllm-runtime / Multi-GPU Test cuda13.1, amd64   View in Datadog   GitHub Actions

ℹ️ Info

🎯 Code Coverage (details)
Patch Coverage: 24.72%
Overall Coverage: 38.01% (-6.82%)

Useful? React with 👍 / 👎

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

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Signed-off-by: PeaBrane <yanrpei@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Offline replay scaling policy

Layer / File(s) Summary
Scaling contracts and replay wiring
lib/mocker/src/replay/offline/*, lib/mocker/src/replay/*
Planner hooks and ticks are replaced with scaling policies, snapshots, decisions, scaling events, prepared executors, and updated validation.
Aggregated and disaggregated scaling runtimes
lib/mocker/src/replay/offline/agg.rs, lib/mocker/src/replay/offline/disagg.rs, lib/mocker/src/replay/offline/components/*
Replay runtimes schedule scaling ticks, construct settled snapshots, apply capacity decisions, and track non-draining worker groups.
Scaling-aware replay entrypoints
lib/mocker/src/replay/entrypoints.rs, lib/mocker/src/replay/offline/entrypoints.rs
Trace, workload, request, concurrency, loaded-trace, and disaggregated replay paths accept optional scaling policies.
Python adapter and replay integration
lib/bindings/python/rust/llm/replay.rs, components/src/dynamo/replay/*, components/src/dynamo/planner/offline/replay_adapter.py
Python callbacks are adapted to Rust scaling policies, while planner replay uses a context-managed adapter through unified replay APIs.
Replay integration and scaling tests
lib/bindings/python/tests/replay/*, components/src/dynamo/planner/tests/offline/*
Tests cover policy plumbing, cleanup, callback errors, replay restrictions, GIL behavior, and aggregated/disaggregated scaling transitions.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and omits the required Related Issues section. Rewrite the PR description using the repository template, including Overview, Details, Where should the reviewer start?, and a completed Related Issues section.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main replay refactor and boundary unification.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 💡
  • Fix failing CI checks

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: 4

🧹 Nitpick comments (5)
components/src/dynamo/planner/offline/replay_adapter.py (1)

314-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Initialize _pending_tick in __init__ instead of probing with hasattr.

_pending_tick is created only inside start(), so initial_tick_ms has to guess at object state. Declaring self._pending_tick: Optional[ScheduledTick] = None in __init__ and testing is None makes the lifecycle explicit and keeps type checkers happy.

🤖 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 `@components/src/dynamo/planner/offline/replay_adapter.py` around lines 314 -
318, Initialize self._pending_tick as Optional[ScheduledTick] = None in the
class __init__, then update initial_tick_ms to check whether _pending_tick is
None instead of using hasattr. Preserve the existing start() behavior and ensure
the tick is non-None before accessing at_s.
components/src/dynamo/replay/main.py (1)

585-602: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider constructing the adapter inside the scope's try.

_prepare_planner_replay builds the ReplayPlannerAdapter (which opens an event loop in __init__) and then runs the AIC bootstrap. If any bootstrap step raises an exception type not caught there, the adapter is never entered and its loop is never closed. Wrapping the post-construction bootstrap in the with adapter: scope — or a try/except: adapter.close(); raise — closes that gap.

🤖 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 `@components/src/dynamo/replay/main.py` around lines 585 - 602, Update
_planner_replay_adapter so exceptions during _prepare_planner_replay’s
post-construction bootstrap still close the ReplayPlannerAdapter event loop.
Construct or retain the adapter within a try/finally or with-scope that
guarantees adapter.close() before re-raising, while preserving normal
context-manager cleanup and yielded adapter behavior.
lib/bindings/python/tests/replay/test_replay_policy_plumbing.py (1)

52-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting finalize runs before close.

finalize() now depends on the adapter still being open (it calls self._recorder.finalize() before close() shuts the loop down). Recording the order in _FakePlannerAdapter — e.g. set self.finalized_before_close = not self.closed inside finalize — would lock in that contract, which is exactly what the try/finally removal in replay_adapter.py changed.

Also applies to: 101-104

🤖 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/bindings/python/tests/replay/test_replay_policy_plumbing.py` around lines
52 - 67, Update _FakePlannerAdapter to record whether finalize is called while
the adapter remains open, such as by setting a finalized_before_close flag from
finalize based on closed; initialize the flag in __init__ and assert it in the
affected replay tests to enforce finalize-before-close ordering.
lib/bindings/python/src/dynamo/_core.pyi (1)

2587-2601: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document scaling_policy in the docstrings.

The parameter is offline-only (ValueError otherwise) and must expose initial_tick_ms() -> float and on_tick(metrics) -> dict. A one-line mention (or a Protocol alias instead of Any) makes the contract discoverable from the stub.

Also applies to: 2631-2639

🤖 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/bindings/python/src/dynamo/_core.pyi` around lines 2587 - 2601, Update
the replay function docstrings around the scaling_policy parameter to document
that it is offline-only and raises ValueError otherwise, and that the policy
must provide initial_tick_ms() -> float and on_tick(metrics) -> dict. Apply the
same documentation to the additional affected docstring, or replace Any with an
appropriate Protocol alias that exposes this contract.
components/src/dynamo/replay/api.py (1)

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

Document why _planner_replay_adapter is imported inside the function.

dynamo.replay.main imports from this module, so a top-level import would cycle — and the late lookup is what makes monkeypatch.setattr(replay_main, "_planner_replay_adapter", ...) work in test_replay_policy_plumbing.py. A one-line comment prevents a future "move imports to top" cleanup from breaking both.

As per coding guidelines: "Keep imports at the top of the file; always flag import statements inside function bodies, methods, or classes as they hide dependencies and make modules harder to understand".

♻️ Proposed comment
+        # Imported lazily: dynamo.replay.main imports this module, and tests
+        # monkeypatch the attribute on the module object.
         from dynamo.replay.main import _planner_replay_adapter

Also applies to: 188-188

🤖 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 `@components/src/dynamo/replay/api.py` at line 103, Add concise comments at
both in-function imports of _planner_replay_adapter explaining that the local
import avoids the circular dependency from dynamo.replay.main importing this
module and preserves monkeypatch.setattr behavior in
test_replay_policy_plumbing.py. Keep the imports local.

Source: Coding guidelines

🤖 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 `@components/src/dynamo/planner/offline/replay_adapter.py`:
- Around line 251-258: Update __exit__ cleanup handling so KeyboardInterrupt and
asyncio.CancelledError raised by close() are always re-raised, including when an
exception from the managed body is already in flight; only log ordinary cleanup
failures and preserve the existing return behavior.

In `@components/src/dynamo/replay/main.py`:
- Around line 895-903: Update the planner trace replay call to pass the
trace-specific arguments for shared-prefix ratio and prefix-group count, using
the corresponding trace option symbols rather than args.shared_prefix_ratio and
args.num_prefix_groups. Keep the remaining run_trace_replay arguments unchanged
and align these values with the non-planner trace replay call.

In `@lib/bindings/python/tests/replay/test_replay_planner_load_modes.py`:
- Around line 199-220: Update
test_normal_replay_releases_gil_for_background_python_thread to avoid the fixed
20 ms sleep and wall-clock duration assumption: have the background thread loop
and count iterations until a stop signal is set, signal that stop after replay
completes, join the thread, and assert the counter recorded any progress.

In `@lib/mocker/src/replay/offline/agg.rs`:
- Around line 266-267: Remove the stale SLA-threshold doc comment above
with_scaling_policy, leaving only the documentation describing the scaling
policy and tick-scoped FPM collection.

---

Nitpick comments:
In `@components/src/dynamo/planner/offline/replay_adapter.py`:
- Around line 314-318: Initialize self._pending_tick as Optional[ScheduledTick]
= None in the class __init__, then update initial_tick_ms to check whether
_pending_tick is None instead of using hasattr. Preserve the existing start()
behavior and ensure the tick is non-None before accessing at_s.

In `@components/src/dynamo/replay/api.py`:
- Line 103: Add concise comments at both in-function imports of
_planner_replay_adapter explaining that the local import avoids the circular
dependency from dynamo.replay.main importing this module and preserves
monkeypatch.setattr behavior in test_replay_policy_plumbing.py. Keep the imports
local.

In `@components/src/dynamo/replay/main.py`:
- Around line 585-602: Update _planner_replay_adapter so exceptions during
_prepare_planner_replay’s post-construction bootstrap still close the
ReplayPlannerAdapter event loop. Construct or retain the adapter within a
try/finally or with-scope that guarantees adapter.close() before re-raising,
while preserving normal context-manager cleanup and yielded adapter behavior.

In `@lib/bindings/python/src/dynamo/_core.pyi`:
- Around line 2587-2601: Update the replay function docstrings around the
scaling_policy parameter to document that it is offline-only and raises
ValueError otherwise, and that the policy must provide initial_tick_ms() ->
float and on_tick(metrics) -> dict. Apply the same documentation to the
additional affected docstring, or replace Any with an appropriate Protocol alias
that exposes this contract.

In `@lib/bindings/python/tests/replay/test_replay_policy_plumbing.py`:
- Around line 52-67: Update _FakePlannerAdapter to record whether finalize is
called while the adapter remains open, such as by setting a
finalized_before_close flag from finalize based on closed; initialize the flag
in __init__ and assert it in the affected replay tests to enforce
finalize-before-close ordering.
🪄 Autofix (Beta)

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: 27fb3f18-19f9-4b03-90ed-19ce349fba8b

📥 Commits

Reviewing files that changed from the base of the PR and between 5f2102a and 02dbb47.

📒 Files selected for processing (26)
  • components/src/dynamo/mocker/__init__.py
  • components/src/dynamo/planner/offline/replay_adapter.py
  • components/src/dynamo/planner/tests/offline/test_replay_adapter_fpm.py
  • components/src/dynamo/replay/api.py
  • components/src/dynamo/replay/main.py
  • lib/bindings/python/rust/lib.rs
  • lib/bindings/python/rust/llm/replay.rs
  • lib/bindings/python/src/dynamo/_core.pyi
  • lib/bindings/python/tests/replay/test_replay_planner_load_modes.py
  • lib/bindings/python/tests/replay/test_replay_planner_scaling.py
  • lib/bindings/python/tests/replay/test_replay_policy_plumbing.py
  • lib/mocker/src/replay/collector.rs
  • lib/mocker/src/replay/entrypoints.rs
  • lib/mocker/src/replay/mod.rs
  • lib/mocker/src/replay/offline/agg.rs
  • lib/mocker/src/replay/offline/components/engine.rs
  • lib/mocker/src/replay/offline/disagg.rs
  • lib/mocker/src/replay/offline/disagg_tests.rs
  • lib/mocker/src/replay/offline/entrypoints.rs
  • lib/mocker/src/replay/offline/events.rs
  • lib/mocker/src/replay/offline/executor.rs
  • lib/mocker/src/replay/offline/mod.rs
  • lib/mocker/src/replay/offline/runtime_utils.rs
  • lib/mocker/src/replay/offline/scaling.rs
  • lib/mocker/src/replay/planner_handle.rs
  • lib/mocker/src/replay/validate.rs
💤 Files with no reviewable changes (3)
  • lib/mocker/src/replay/planner_handle.rs
  • lib/bindings/python/rust/lib.rs
  • components/src/dynamo/mocker/init.py

Comment thread components/src/dynamo/planner/offline/replay_adapter.py Outdated
Comment thread components/src/dynamo/replay/main.py
Comment thread lib/bindings/python/tests/replay/test_replay_planner_load_modes.py Outdated
Comment thread lib/mocker/src/replay/offline/agg.rs Outdated
Signed-off-by: PeaBrane <yanrpei@gmail.com>
Comment thread components/src/dynamo/planner/offline/replay_adapter.py Outdated
Comment thread components/src/dynamo/planner/offline/replay_adapter.py Outdated
PeaBrane added 3 commits July 28, 2026 18:49
Signed-off-by: PeaBrane <yanrpei@gmail.com>
…ner-replay-20260728

Signed-off-by: PeaBrane <yanrpei@gmail.com>
Signed-off-by: PeaBrane <yanrpei@gmail.com>
@PeaBrane
PeaBrane enabled auto-merge (squash) July 29, 2026 16:45
@PeaBrane
PeaBrane merged commit ce54943 into main Jul 29, 2026
249 of 253 checks passed
@PeaBrane
PeaBrane deleted the codex/computelab-planner-replay-20260728 branch July 29, 2026 18:24
@PeaBrane PeaBrane changed the title refactor(replay): unify planner runtime boundary refactor(replay): unify planner runtime boundary [DYN-3850] Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants