Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8522585
fix(router): stop charging decode for old prompts
PeaBrane Mar 25, 2026
3561be4
Merge origin/main and keep tracker sane
PeaBrane Mar 25, 2026
42dd884
refactor replay timing without moving the clock
PeaBrane Mar 25, 2026
b4d281c
feat: teach replay to disagg without drama
PeaBrane Mar 25, 2026
a9dd460
merge main and keep replay honest
PeaBrane Mar 25, 2026
b03b956
fix: tighten replay guardrails and docs
PeaBrane Mar 25, 2026
e41b87e
fix: centralize replay validation before it forks
PeaBrane Mar 25, 2026
e0f4342
feat: let disagg replay stop kv cling
PeaBrane Mar 25, 2026
d85a264
cache replay turns and sort stats once
PeaBrane Mar 25, 2026
00e5b26
docs: clarify free subsumes prefill cleanup
PeaBrane Mar 25, 2026
cdfb4ba
fix replay mypy's identity crisis
PeaBrane Mar 25, 2026
253a427
refactor prefill router without moving guardrails
PeaBrane Mar 25, 2026
3eb2051
feat: plumb replay planner profile args
PeaBrane Mar 25, 2026
5b103d7
Merge main and keep prefill tracking intact
PeaBrane Mar 25, 2026
aab6f78
keep planner args from vanishing
PeaBrane Mar 25, 2026
d39ab78
fail fast when AIC ghosts its DB
PeaBrane Mar 25, 2026
aebd263
clarify when mocker actually needs AIC
PeaBrane Mar 25, 2026
62f5072
fix mocker mypy kwargs tantrum
PeaBrane Mar 25, 2026
7193e9d
split kv_router so it stops hoarding
PeaBrane Mar 25, 2026
a15b8a0
fix tests header before legal notices bite
PeaBrane Mar 25, 2026
2ae18e1
fix metrics test import after split
PeaBrane Mar 25, 2026
d88451b
merge mocker config tests before pytest riots
PeaBrane Mar 25, 2026
10bfc3f
test replay planner profile paths
PeaBrane Mar 25, 2026
61c4585
merge main without reviving flat prefill router
PeaBrane Mar 25, 2026
02e4b1e
model decode handoff without prefill fibs
PeaBrane Mar 25, 2026
77ac5e6
refactor replay state without moving the clock
PeaBrane Mar 25, 2026
6bd84f8
batch online trace arrivals before passes bolt
PeaBrane Mar 25, 2026
d09a1fc
Revert "batch online trace arrivals before passes bolt"
PeaBrane Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"router_track_active_blocks",
"router_track_output_blocks",
"router_assume_kv_reuse",
"router_track_prefill_tokens",
"router_snapshot_threshold",
"router_reset_states",
"router_ttl_secs",
Expand All @@ -51,6 +52,7 @@ class KvRouterConfigBase(ConfigBase):
router_track_active_blocks: bool
router_track_output_blocks: bool
router_assume_kv_reuse: bool
router_track_prefill_tokens: bool
router_snapshot_threshold: int
router_reset_states: bool
router_ttl_secs: float
Expand Down Expand Up @@ -173,6 +175,18 @@ def add_arguments(self, parser) -> None:
),
obsolete_flag="--assume-kv-reuse",
)
add_negatable_bool_argument(
g,
flag_name="--router-track-prefill-tokens",
env_var="DYN_ROUTER_TRACK_PREFILL_TOKENS",
default=True,
dest="router_track_prefill_tokens",
help=(
"KV Router: Include prompt-side prefill tokens in active load accounting. "
"Use --no-router-track-prefill-tokens to ignore prompt tokens in router "
"prefill-token load, queue pressure, and active_prefill_tokens metrics."
),
)
add_argument(
g,
flag_name="--router-snapshot-threshold",
Expand Down
5 changes: 4 additions & 1 deletion components/src/dynamo/router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ python -m dynamo.router \
- `--endpoint`: Full endpoint path for workers in the format `namespace.component.endpoint` (e.g., `dynamo.prefill.generate`)

**Router Configuration:**
All router options use the `--router-*` prefix (e.g., `--router-block-size`, `--router-kv-overlap-score-weight`, `--router-temperature`, `--router-kv-events` / `--no-router-kv-events`, `--router-replica-sync`, `--router-snapshot-threshold`, `--router-reset-states`, `--router-track-active-blocks` / `--no-router-track-active-blocks`). Legacy names without the prefix (e.g., `--block-size`, `--kv-events`) are still accepted but deprecated. For detailed descriptions, see the [Router Guide](/docs/components/router/router-guide.md).
All router options use the `--router-*` prefix (e.g., `--router-block-size`, `--router-kv-overlap-score-weight`, `--router-temperature`, `--router-kv-events` / `--no-router-kv-events`, `--router-replica-sync`, `--router-snapshot-threshold`, `--router-reset-states`, `--router-track-active-blocks` / `--no-router-track-active-blocks`, `--router-track-prefill-tokens` / `--no-router-track-prefill-tokens`). Legacy names without the prefix (e.g., `--block-size`, `--kv-events`) are still accepted but deprecated. For detailed descriptions, see the [Router Guide](/docs/components/router/router-guide.md).

## Architecture

Expand Down Expand Up @@ -74,6 +74,9 @@ python -m dynamo.vllm --model MODEL_NAME --block-size 64 --disaggregation-mode p
> **Why `--no-router-track-active-blocks` for prefill routing?**
> Active block tracking is used for load balancing across decode (generation) phases. For prefill-only routing, decode load is not relevant, so disabling this reduces overhead and simplifies the router state.
>
> **When should I use `--no-router-track-prefill-tokens`?**
> Use it on decode-only routers that should ignore already-completed prompt work. This keeps `active_prefill_tokens`, queue pressure, and load estimates focused on decode-side work after a prefill-to-decode handoff.
>
> **Why `--router-block-size` is required for standalone routers:**
> Unlike the frontend router which can infer block size from the ModelDeploymentCard (MDC) during worker registration, standalone routers cannot access the MDC and must have the block size explicitly specified. This is a work in progress to enable automatic inference.

Expand Down
1 change: 1 addition & 0 deletions components/src/dynamo/router/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ async def worker(runtime: DistributedRuntime):
f"router_track_active_blocks={config.router_track_active_blocks}, "
f"router_track_output_blocks={config.router_track_output_blocks}, "
f"router_assume_kv_reuse={config.router_assume_kv_reuse}, "
f"router_track_prefill_tokens={config.router_track_prefill_tokens}, "
f"router_ttl_secs={config.router_ttl_secs}, "
f"router_max_tree_size={config.router_max_tree_size}, "
f"router_prune_target_ratio={config.router_prune_target_ratio}"
Expand Down
30 changes: 26 additions & 4 deletions docs/benchmarks/mocker-trace-replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ The dedicated replay CLI exposes:
- `--replay-mode offline|online`
- `--router-mode round_robin|kv_router`
- `--num-workers`
- `--num-prefill-workers`
- `--num-decode-workers`
- `--replay-concurrency`
- `--arrival-interval-ms`
- `--arrival-speedup-ratio`
Expand All @@ -125,6 +127,8 @@ The dedicated replay CLI exposes:
- `--num-prefix-groups`
- `--inter-turn-delay-ms`
- `--extra-engine-args` (JSON string)
- `--prefill-engine-args` (JSON string)
- `--decode-engine-args` (JSON string)
- `--router-config` (JSON string)
- `--report-json`

Expand Down Expand Up @@ -164,6 +168,19 @@ as `block_size`, `engine_type`, `dp_size`, `speedup_ratio`, and `decode_speedup_
`--extra-engine-args`, not as top-level replay CLI flags. Unspecified fields fall back to the same
defaults used by `MockEngineArgs::default()` and `KvRouterConfig::default()`.

Offline disagg replay uses staged engine args instead of `--extra-engine-args`:

- `--prefill-engine-args` for the prefill worker config
- `--decode-engine-args` for the decode worker config
- `--num-prefill-workers` and `--num-decode-workers` for pool sizes

For offline disagg replay, the staged JSON must set `worker_type` explicitly:

- `--prefill-engine-args` must use `worker_type: "prefill"`
- `--decode-engine-args` must use `worker_type: "decode"`

The staged configs must also use the same `block_size`.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
### Synthetic Replay

Synthetic replay bypasses trace loading and generates in-memory requests with fixed input/output
Expand Down Expand Up @@ -320,16 +337,20 @@ If `--report-json` is not provided, `python -m dynamo.replay` writes a timestamp

Shared replay constraints:

- aggregated mode
- `extra_engine_args.engine_type` must be `vllm` or `sglang`
- `extra_engine_args.dp_size` must be `1`
- aggregated replay requires the existing aggregated args path
- disagg replay requires both `prefill_engine_args` and `decode_engine_args`
- disagg replay requires `router_mode=kv_router`
- replay `dp_size` must be `1`
- disagg replay requires matching `block_size` in `prefill_engine_args` and `decode_engine_args`

Additional offline constraints:

- offline `kv_router` requires `num_workers > 1`
- single-worker offline replay is still a dedicated fast path for `vllm`, but it now supports both
flat request replay and workload-driven multi-turn replay
- `sglang` still goes through the shared multi-worker replay runtime even when `num_workers=1`
- offline disagg replay is a separate two-stage runtime with prefill and decode worker pools

Additional online constraints:

Expand All @@ -343,12 +364,13 @@ If you violate those constraints, replay fails immediately with a validation err
either a trace file, or all of `--input-tokens`, `--output-tokens`, and `--request-count`
- `--replay-concurrency` works with both trace replay and synthetic replay
- mocker compute-speed knobs such as `speedup_ratio` still affect simulated timing when passed via
`--extra-engine-args`
the engine-args JSON for the chosen replay mode
- `--arrival-speedup-ratio` affects trace timestamps, not worker compute speed
- `--arrival-interval-ms` only applies to synthetic replay
- `--turns-per-session`, `--shared-prefix-ratio`, `--num-prefix-groups`, and
`--inter-turn-delay-ms` only apply to synthetic replay
- `--extra-engine-args` and `--router-config` are JSON strings on the standalone replay CLI
- `--extra-engine-args`, `--prefill-engine-args`, `--decode-engine-args`, and `--router-config`
are JSON strings on the standalone replay CLI
- offline replay does not need planner runtime setup, router registration, or external event transport
- the replay block size should match the trace block size, because token synthesis expands `hash_ids`
using the configured block size
Expand Down
1 change: 1 addition & 0 deletions docs/components/router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For Kubernetes, set `DYN_ROUTER_MODE=kv` on the Frontend service. Workers automa
| `--no-router-kv-events` | enabled | Fall back to approximate routing (no event consumption from workers) |
| `--router-queue-threshold` | `4.0` | Backpressure queue threshold; enables priority scheduling via `nvext.agent_hints.priority` |
| `--router-queue-policy` | `fcfs` | Queue scheduling policy: `fcfs` (tail TTFT), `wspt` (avg TTFT), or `lcfs` (comparison-only reverse ordering) |
| `--no-router-track-prefill-tokens` | disabled | Ignore prompt-side prefill tokens in router load accounting; useful for decode-only routing paths |

### Standalone Router

Expand Down
9 changes: 9 additions & 0 deletions docs/components/router/router-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ The main KV-aware routing arguments (frontend uses the same `--router-*` flag na

- `--no-router-assume-kv-reuse`: When tracking active blocks, disables the assumption of KV cache reuse. By default (`router_assume_kv_reuse=true`), the router computes actual block hashes for sequence tracking to deduplicate blocks and optimize load balancing. When disabled via this flag, the router generates random hashes for sequence blocks, treating each request's blocks as unique. This is useful in disaggregated setups where prefill transfers blocks to decode workers that may already have those blocks cached, but the engine cannot coordinate transfers to avoid duplication. Without this flag, the router's load balancing heuristics would undercount decode blocks when duplicates exist.

- `--no-router-track-prefill-tokens`: Disables prompt-side prefill token accounting in the router's active load model. By default (`router_track_prefill_tokens=true`), the router counts uncached prompt tokens toward `active_prefill_tokens`, queue pressure, and potential prefill-token load. Disable this for decode-only routing paths where prompt processing has already happened elsewhere and the decode router should ignore transferred prompt load. In normal live disaggregated serving, the decode-stage override applies this behavior automatically.

- `--router-replica-sync`: Disabled by default. Enables NATS-based synchronization of local routing decisions between router replicas. When enabled, routers share their active sequence information and local predictions of block usage, improving routing consistency across instances. Note that this does not sync the radix tree or cached KV block states themselves - in JetStream mode those are synchronized through JetStream events; in local indexer mode (default) each router queries workers directly.

### KV Indexer / Approx KV Indexer
Expand All @@ -280,6 +282,8 @@ Use `--no-router-kv-events` when you are not confident that your backend engine

Use `--no-router-assume-kv-reuse` in disaggregated setups where the decode worker does not reuse transferred KV cache blocks. By default the router assumes KV blocks transferred from prefill to decode will be deduplicated on the decode side, but vLLM and SGLang decode workers currently do not support this — only TensorRT-LLM does. Without this flag, the router undercounts decode blocks when duplicates exist, leading to inaccurate load estimates.

Use `--no-router-track-prefill-tokens` when a router is serving decode-only traffic and prompt processing has already completed elsewhere. This keeps decode routing decisions focused on decode-side load instead of briefly charging prompt tokens to the decode worker after handoff. The built-in live disaggregated decode path applies the equivalent per-request override automatically.

Use `--router-track-output-blocks` **(experimental)** when your workload is output-heavy and you want the router to account for output-side KV cache growth in load balancing. This is useful in two scenarios: (1) workloads with long output sequences and little multi-turn reuse, where output blocks dominate the KV cache footprint; (2) agentic schedulers (e.g. NAT or other LLM routers) that can accurately predict the expected output sequence length per request. When enabled, the router adds placeholder blocks as tokens are generated. If you additionally pass `nvext.agent_hints.osl` (expected output sequence length in tokens) per request, the router applies fractional decay to output blocks — each output block's weight starts at 1.0 and decays linearly toward 0.0 as generation approaches the expected OSL. This lets the router predict that a request nearing completion will soon free its blocks, effectively modeling the future load trajectory rather than just the current snapshot. Without `osl`, output blocks are added at full weight with no decay. The flag requires `--router-track-active-blocks` (the default).

The `--router-queue-threshold` (default: 4.0) controls when incoming requests are held in a priority queue. The router holds requests while all workers exceed the given fraction of `max_num_batched_tokens`, releasing them as capacity frees up. This defers the routing decision so it is made with the freshest load metrics, rather than dispatching into an already-saturated system. It also enables priority scheduling via `nvext.agent_hints.priority`. Set to None to disable queueing entirely.
Expand Down Expand Up @@ -310,6 +314,11 @@ The prefill router is automatically created when:
- **Seamlessly integrated** into the request pipeline between preprocessing and decode routing
- **Falls back gracefully** to decode-only mode if prefill fails or no prefill workers are available

**Key characteristics of the decode routing stage in disaggregated mode:**
- **Disables overlap scoring** (`overlap_score_weight=0`) because decode routing should not chase prefix reuse
- **Disables KV reuse assumption** (`assume_kv_reuse=false`) unless the backend can truly deduplicate transferred blocks
- **Disables prefill-token tracking** (`track_prefill_tokens=false`) so decode-side load reflects decode work rather than already-completed prompt work

### Setup Example

When both workers are registered, requests are automatically routed.
Expand Down
48 changes: 44 additions & 4 deletions docs/mocker/mocker.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ python -m dynamo.mocker \

The mocker supports replaying Mooncake-style traces through the dedicated replay CLI, which exposes
`offline|online`, `round_robin|kv_router`, `arrival_speedup_ratio`, closed-loop concurrency
admission, and synthetic workload generation directly:
admission, synthetic workload generation, and offline disaggregated prefill/decode replay directly:

The replay CLI defaults to `--replay-mode offline` and `--router-mode round_robin`. Engine settings
such as `block_size`, `engine_type`, and compute speedups still belong in `--extra-engine-args`.
The replay CLI defaults to `--replay-mode offline` and `--router-mode round_robin`. Aggregated
replay uses `--extra-engine-args`. Offline disagg replay instead uses
`--prefill-engine-args` plus `--decode-engine-args`, together with
`--num-prefill-workers` and `--num-decode-workers`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```bash
python -m dynamo.replay /path/to/mooncake_trace.jsonl \
Expand Down Expand Up @@ -197,6 +199,31 @@ Replay supports aggregated `vllm` and `sglang` engine configs. Internally replay
`block_size`; for `sglang`, `sglang.page_size` is still accepted as a compatibility alias as long
as it matches `block_size` when both are provided.

Offline replay also supports disaggregated `kv_router` mode. In that mode:

- `--prefill-engine-args` must describe a prefill worker
- `--decode-engine-args` must describe a decode worker
- `--router-mode` must be `kv_router`
- only offline replay is supported

Example:

```bash
python -m dynamo.replay \
--input-tokens 4096 \
--output-tokens 256 \
--request-count 100 \
--replay-mode offline \
--router-mode kv_router \
--replay-concurrency 32 \
--num-prefill-workers 2 \
--num-decode-workers 6 \
--prefill-engine-args '{"worker_type":"prefill","block_size":512}' \
--decode-engine-args '{"worker_type":"decode","block_size":512}' \
--router-config '{"router_queue_policy":"wspt"}' \
--report-json /tmp/replay-report.json
```

## Performance Modeling Setup

By default, the mocker uses hardcoded polynomial formulas to estimate prefill and decode timing. For more realistic simulations, pass `--planner-profile-data` with either:
Expand Down Expand Up @@ -234,13 +261,26 @@ python -m dynamo.mocker \

The AIC model automatically uses `--model-path` and `--engine-type` to select the appropriate performance data. Available systems include `h200_sxm`, `h100_sxm`, etc. (see AIC SDK documentation for the full list).

When using `python -m dynamo.replay`, there are no dedicated AIC flags. Pass the equivalent fields directly via `--extra-engine-args`:
When using `python -m dynamo.replay`, there are no dedicated AIC flags. For aggregated replay,
pass the equivalent fields via `--extra-engine-args`:

```bash
python -m dynamo.replay /path/to/trace.jsonl \
--extra-engine-args '{"aic_backend":"vllm","aic_system":"h200_sxm","aic_model_path":"nvidia/Llama-3.1-8B-Instruct-FP8","aic_tp_size":1}'
```

For offline disagg replay, pass the staged engine configs instead:

```bash
python -m dynamo.replay /path/to/trace.jsonl \
--replay-mode offline \
--router-mode kv_router \
--prefill-engine-args '{"worker_type":"prefill","aic_backend":"vllm","aic_system":"h200_sxm","aic_model_path":"nvidia/Llama-3.1-8B-Instruct-FP8","aic_tp_size":1,"block_size":512}' \
--decode-engine-args '{"worker_type":"decode","aic_backend":"vllm","aic_system":"h200_sxm","aic_model_path":"nvidia/Llama-3.1-8B-Instruct-FP8","aic_tp_size":1,"block_size":512}' \
--num-prefill-workers 2 \
--num-decode-workers 6
```

The `aic_backend` field enables the AIC perf model and should match `engine_type` (`"vllm"` or `"sglang"`). The `aic_model_path` field is the equivalent of `--model-path` in `dynamo.mocker`.

Example `--reasoning` configuration:
Expand Down
1 change: 1 addition & 0 deletions lib/bench/kv_router/active_sequences_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ async fn apply_entry(
token_sequence: Some(block_hashes),
isl,
overlap: 0,
track_prefill_tokens: true,
expected_output_tokens: Some(output_length as u32),
worker,
lora_name: None,
Expand Down
14 changes: 13 additions & 1 deletion lib/bindings/c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ impl RouterHandles {
let config_override = if is_disaggregated {
Some(RouterConfigOverride {
overlap_score_weight: Some(0.0),
assume_kv_reuse: Some(false),
track_prefill_tokens: Some(false),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
..Default::default()
})
} else {
Expand Down Expand Up @@ -573,6 +575,9 @@ fn kv_router_config_from_env() -> KvRouterConfig {
if let Some(v) = env_bool("DYN_ROUTER_TRACK_OUTPUT_BLOCKS") {
cfg.router_track_output_blocks = v;
}
if let Some(v) = env_bool("DYN_ROUTER_TRACK_PREFILL_TOKENS") {
cfg.router_track_prefill_tokens = v;
}
if let Some(v) = env_f64("DYN_ROUTER_QUEUE_THRESHOLD") {
cfg.router_queue_threshold = Some(v);
}
Expand All @@ -584,6 +589,7 @@ fn kv_router_config_from_env() -> KvRouterConfig {
router_replica_sync = cfg.router_replica_sync,
router_track_active_blocks = cfg.router_track_active_blocks,
router_track_output_blocks = cfg.router_track_output_blocks,
router_track_prefill_tokens = cfg.router_track_prefill_tokens,
router_queue_threshold = ?cfg.router_queue_threshold,
"KvRouterConfig initialized (DYN_* env overrides applied)"
);
Expand Down Expand Up @@ -862,6 +868,12 @@ pub unsafe extern "C" fn add_request(

tokio::time::timeout(timeout_duration, async {
let worker = WorkerWithDpRank::new(worker_id, dp_rank);
let router_config_override = RouterConfigOverride {
overlap_score_weight: Some(0.0),
assume_kv_reuse: Some(false),
track_prefill_tokens: Some(false),
..Default::default()
};

// Compute overlap_blocks using the public method
let overlap_blocks = match decode_router
Expand All @@ -884,7 +896,7 @@ pub unsafe extern "C" fn add_request(
None,
worker,
None, // lora_name
None, // router_config_override
Some(&router_config_override),
)
.await;

Expand Down
Loading
Loading