Repository navigation
feat(inference): router replay with KV cache offload - #3831
Merged
Merged
Conversation
This was referenced Oct 4, 2026
faresobeid
marked this pull request as ready for review
October 4, 2026 01:43
This was referenced Oct 4, 2026
faresobeid
force-pushed
the
feat/router-replay-kv-offload
branch
from
October 4, 2026 10:54
880bd10 to
afc159c
Compare
This was referenced Oct 4, 2026
faresobeid
force-pushed
the
feat/router-replay-v2-pd
branch
from
October 5, 2026 01:25
1be0816 to
2b415a3
Compare
faresobeid
force-pushed
the
feat/router-replay-kv-offload
branch
from
October 5, 2026 01:25
afc159c to
49af414
Compare
faresobeid
force-pushed
the
feat/router-replay-v2-pd
branch
from
October 6, 2026 12:43
2b415a3 to
4a4ce60
Compare
faresobeid
force-pushed
the
feat/router-replay-kv-offload
branch
from
October 6, 2026 12:43
49af414 to
996c97b
Compare
This was referenced Oct 6, 2026
faresobeid
force-pushed
the
feat/router-replay-v2-pd
branch
from
October 6, 2026 14:53
4a4ce60 to
801f8af
Compare
faresobeid
force-pushed
the
feat/router-replay-kv-offload
branch
from
October 6, 2026 14:53
996c97b to
9da6745
Compare
vLLM 0.31 captures routed experts through the AuxOutput connector and rejects it with P/D connectors: a decode instance never computes a remotely prefilled prompt, so its worker would materialize missing rows (BlockObjectStoreError), and the first forward starts mid-block. Carry our upstream AuxOutput fix as a plugin patch: - the config check skips the P/D connector rejection for NixlConnector (alone or inside MultiConnector); every other check still runs; - requests that arrive with do_remote_prefill emit rows only from their first forward (num_computed_tokens then); the floor survives preemption; - the worker does not buffer or publish the partially known first block. The pinned router drops as many leading decode rows as prefill returned, so the decode server left-pads its rows to routed_experts_prompt_start (the same payload size as 0.30's full-length decode rows). Remove the config rejection added by the 0.31 bump.
vLLM 0.31 keeps routing rows in a local store keyed by block hash. No connector moves rows with the KV, so a block filled by an offload load has no rows here, and a request that needs them fails the engine step (BlockObjectStoreError). - Load clamp: native (OffloadingConnector._max_loadable_tokens) and Mooncake (get_num_new_matched_tokens) loads stop at block_floor(routed_experts_prompt_start). vLLM never returns rows before that position, so multi-turn reloads keep their hits. Decode- side P/D requests are not limited (they emit from their first forward). - Loaded-block cut: loaded blocks then serve local prefix hits. A later request with a lower prompt start would need their rows, so its local hit is cut at the first loaded block it needs rows from. Clamping is simpler than restoring rows with offloaded blocks: the row store lives in the worker, the clamp is a scheduler-side bound, and neither connector has a hook to carry extra per-block objects. Remove validate_router_replay_without_kv_offload.
faresobeid
force-pushed
the
feat/router-replay-kv-offload
branch
from
October 6, 2026 16:18
9da6745 to
28c9c7a
Compare
faresobeid
force-pushed
the
feat/router-replay-v2-pd
branch
from
October 6, 2026 16:18
801f8af to
a827863
Compare
…rward Anchor the routing rows at the end (start = prompt + completion - 1 - rows), so a P/D decode response no longer zero-pads its rows to the prompt start. The router cuts decode rows by position since v0.2.3 (router#61).
A request resumed after preemption emits routing rows only from num_tokens-1, which its first forward always recomputes, so it never reads stored rows.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1dc6dc3. Configure here.
…ked /generate path The start moves into _PackedOutputs (#3900), so routed_experts.py and serving_tokens.py stay as on main.
samsja
pushed a commit
that referenced
this pull request
Oct 14, 2026
Router replay now works with disaggregated P/D on vLLM 0.31, together with sampling replay. It replaces main's config rejection of router replay + P/D. **Problem on 0.31.** vLLM 0.31 captures routed experts through the AuxOutput connector (V2 runner only, vllm#45635) and rejects P/D connectors (`_verify_aux_output_compatibility`). The rejection is real: a decode (D) instance never computes a remotely prefilled prompt, so its worker would try to materialize the prompt rows from its block store (`BlockObjectStoreError`), and D's first forward (token `prompt_len-1`) starts mid-block, which the routed-experts buffer rejects. **Fix (plugin patch `monkey_patch_routed_experts_with_pd_connectors`, our upstream AuxOutput patch 02):** - Config: skip only the P/D connector check when the KV transfer config has `NixlConnector` (alone or inside `MultiConnector`). Every other AuxOutput check still runs. - Scheduler: a request on D (`kv_transfer_params` with a `remote_engine_id` and no `do_remote_decode`; NIXL clears `do_remote_prefill` but keeps that) emits rows only from its first forward on D (`num_computed_tokens` at that point: normally `prompt_len-1`, lower after a failed KV load, where D recomputes those rows itself). The floor survives preemption and is dropped when the request finishes. - Worker: the partially known first block is not buffered or published; later blocks D computes in full are published as usual. **Router v0.2.3 required (router#61); the decode `start` comes from #3900 (merged).** On a decode instance the rows start at its first forward, so the response carries only the rows D computed. #3900's packed `/generate` anchors the rows at the end: `start = len(prompt) + len(completion) - 1 - len(rows)` (equal to `routed_experts_prompt_start` without P/D). This PR does not touch `routed_experts.py`/`serving_tokens.py`, so it and #3900 (and #3920/#3794 on it) land in either order; P/D router replay needs both. Router v0.2.3 cuts decode rows by position (`(prefill.start + prefill.seq_len) - decode.start`) and also handles uint16 routing (models with more than 256 experts). This PR pins `vllm-router` to v0.2.3 in `pyproject.toml`/`uv.lock`. Without #3900, the merge would drop real completion rows. Also: removes `validate_disaggregated_no_routed_experts` (both validators on main), docs. Stack: **this PR** → #3831 (router replay with KV offload) → #3832 (P/D per-role KV offload) → #4023 (draft, HiSparse on decode). ## Checks run - `pytest -m "not gpu" tests/unit/test_configs.py tests/unit/inference` on this head: 214 passed. `ruff check` / `ruff format --check`: clean. - New tests (`test_routed_experts_pd.py`): the scheduler floor (first forward, preemption, cleanup) on vLLM's real `AuxOutputSchedulerConnector`, and the worker skip on the real `RoutedExpertsBuffer`. `test_combined_replay_uses_v2_runner` is parametrized over single-node and P/D. - vLLM's own `tests/distributed/aux_output_connector/test_store.py` (v0.31.0) with this patch applied plus patch 02's two new tests: 66 passed. Without the patch the two new tests fail. ## GPU validation (2026-10-06, vLLM 0.31.0, 1x 8xH200, Qwen3-30B-A3B TP1, `VLLM_BATCH_INVARIANT=1`, router v0.2.2 with zero-padded decode rows) - 1P1D (NIXL) vs colocated, router replay + sampling masks: 10/10 identical at concurrency 1 (prompts 64-6000 tokens, 128 completion tokens) and 20/20 at concurrency 10 with a fresh `cache_salt`. Identical means tokens, prompt and completion routing rows (100%) and sampling masks; rows = prompt + completion - 1. - Forced decode preemption (D with 1300 blocks, 20 requests, 1000-4000 prompts, up to 1024 tokens, concurrency 10): 6 preemptions on D, 20/20 identical to colocated. - All engines log `Using V2 Model Runner`; no `BlockObjectStoreError`. Re-run (job 2900, 2026-10-06, same setup, launched from per-role configs written by #3832's `write_pd_configs`): 1P1D (NIXL, no offload) vs colocated, router replay + masks: 10/10 identical at concurrency 1 and 20/20 at concurrency 10 with a fresh `cache_salt`; 0 errors. Not run: injected NIXL load failure (`kv_load_failure_policy=recompute`), MTP on decode, an `rl` run with a P/D config. ## GPU validation with router v0.2.3 (2026-10-14) 1x 8xH200, vLLM 0.31.0, Qwen3-30B-A3B TP1, `VLLM_BATCH_INVARIANT=1`, router replay + sampling masks. prime-rl = #3830 (978146c) merged with #3900 (20a3225); router = `release/v0.2.3` (ff818da, this PR + version bump), wheel built locally. 1P1D (NIXL) through the router vs a colocated server. Identical means: same tokens, same routing rows for prompt and completion (100%, rows = prompt + completion - 1, merged `start` = 0), same sampling masks, same logprobs. - Concurrency 1 (prompts 64-6000 tokens, 128 completion tokens): 10/10 identical. - Concurrency 10, fresh `cache_salt` on the colocated side: 20/20 identical. - uint16: no knob exists (vLLM picks uint8 for <=256 experts), so a test-only `sitecustomize` forced vLLM's capturer to `uint16` on P, D and the colocated server. The responses carry `dtype: uint16`. Concurrency 1: 10/10 identical; concurrency 10: 20/20 identical. - Forced decode preemption (D with `num_gpu_blocks_override=1300`, 1000-4000 token prompts, up to 1024 tokens, concurrency 10): 6 preemptions on D, 20/20 identical. - Router logs: no errors or warnings during the runs (only the shutdown warning). Engines: all `Using V2 Model Runner`, no `BlockObjectStoreError`. - Negative control, router v0.2.2 with the same P/D pair: 8/10 requests fail (HTTP 500 `decode routed_experts has 128 rows, expected at least <prompt_len>`). The 2 prompts of 64 tokens return 128 rows instead of 191, and the completion rows are wrong. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Changes expert-routing capture on NIXL P/D via vLLM monkey patches and removes prior safety guards; training correctness depends on router 0.2.3 merge behavior and patch edge cases (preemption, failed KV load). > > **Overview** > **Router replay** and **sampling replay** are now allowed on **disaggregated prefill/decode (P/D)** deployments, where they were previously blocked by config validation and vLLM’s AuxOutput + NIXL incompatibility. > > Config validators that rejected `enable_return_routed_experts` with `deployment.type = "disaggregated"` are removed from inference and RL configs. Docs now describe P/D behavior: prompt routing from prefill, completion routing from decode, and that sampling capture can run alongside router replay under P/D. > > A startup **vLLM monkey patch** (`monkey_patch_routed_experts_with_pd_connectors`) bypasses only the P/D connector check in `_verify_aux_output_compatibility`, raises an emit floor on decode so routed-expert rows start at the instance’s first forward, and skips buffering a partial first block after remote prefill. **`vllm-router` is pinned to v0.2.3** so the router can merge prefill/decode expert rows correctly. > > Unit tests cover scheduler floors, worker buffering, and V2 runner setup for both single-node and disaggregated configs. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e586ef8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: faresobeid <111092724+faresobeid@users.noreply.github.com>
# Conflicts: # docs/inference.md # packages/prime-rl-configs/src/prime_rl/configs/rl.py # src/prime_rl/inference/patches.py
samsja
approved these changes
Oct 19, 2026
samsja
pushed a commit
that referenced
this pull request
Oct 19, 2026
Disaggregated P/D with per-role KV offload, so P/D + router replay + sampling replay + KV offload can be configured from the stack alone. **Stacked on #3831.** Supersedes the parts of #3616 needed here. **Per-role KV offload** - `inference.deployment.prefill_kv_cache_offload` / `decode_kv_cache_offload`: default `"inherit"` (use `inference.kv_cache_offload`), `None` (`"None"` in TOML) disables offload for that role, or a full offload config. - Mooncake `cpu.num_bytes = 0` connects a role to the shared store without starting a `mooncake_client` on its nodes (no storage contributed; the vLLM process still uses its staging buffer). Native offload still needs a positive size. - Under P/D the Mooncake connector gets `save_decode_cache = true`: decode also writes the KV it generates, so the next turn's prefill can load the previous completion. Existing keys are skipped by the store, so decode re-saving the prompt costs one existence check. Writes are not clamped; prefill loads are clamped at `routed_experts_prompt_start` by #3831, which keeps multi-turn completion blocks loadable. - `_mooncake_store.sh.j2` runs per role: one master on the head node; storage clients only on roles with capacity. Roles must agree on a Mooncake disk path (one master, one `-root_fs_dir`). - Mooncake keys carry model + TP/PCP/DCP/PP rank + content hash only, so both roles must keep the same KV layout (TP size, `kv_cache_dtype`, block size) to share the store. Documented. **Launcher.** One engine config per role (`inference-{prefill,decode}.json`, `InferenceConfig.for_pd_role` / `write_pd_configs`); each node picks its file by `$ROLE`. Inference nodes always run `ulimit -l unlimited` (NIXL/RDMA registers pinned host pools). Example (all storage on prefill; decode connects without capacity): ```toml [inference.deployment.prefill_kv_cache_offload] type = "mooncake" [inference.deployment.prefill_kv_cache_offload.cpu] num_bytes = 1_000_000_000_000 [inference.deployment.decode_kv_cache_offload] type = "mooncake" [inference.deployment.decode_kv_cache_offload.cpu] num_bytes = 0 ``` Resulting connectors: prefill `MultiConnector[Nixl, MooncakeStore(save_decode_cache)]`, decode `MultiConnector[Nixl, MooncakeStore(save_decode_cache)]`. Left out (not needed for this use case): GLM-5.3 example configs, monitoring dashboard and tooling, trainer DRAM contribution to the store, NUMA/UCX per-rank binding, router timeout/retry fields, lease TTL and staging-buffer fields, explicit `kv_transfer_config` passthrough. ## Checks run - `pytest -m "not gpu" tests/unit/test_configs.py tests/unit/inference`: 223 passed. Ruff: clean. Test: `test_pd_role_connectors`. - Dry run of `examples/advanced/glm-5.3/swe.toml` + router replay + `top_p = 0.95` (sampling masks) + prefill Mooncake (1 TB + disk) + decode Mooncake (`cpu.num_bytes = 0`): per-role JSON configs build `[Nixl, MooncakeStore]` for both roles; `bash -n` on the generated `rl.sbatch` passes; only prefill nodes start a storage client. ## GPU validation (2026-10-06, vLLM 0.31.0, 1x 8xH200, configs written by `write_pd_configs`, router v0.2.2) - Qwen3-30B-A3B TP1, `VLLM_BATCH_INVARIANT=1`, 1P1D NIXL, prefill Mooncake (RDMA, 40 GB) + decode Mooncake `cpu.num_bytes = 0` with `save_decode_cache`, router replay + sampling masks, 6 two-turn conversations (turn 2 start = `len(p1 + c1) - 1`) after evicting prefill's GPU cache, vs a colocated no-offload reference: **6/6 identical** (tokens, 264 rows, start, masks). Prefill external hits **16880** = sum of block_floor(start), vs 16512 when only prefill writes (prompt-only maximum, earlier run with the same conversations): the turn-1 completion blocks were loaded from decode-saved KV. No errors. - 1P1D without offload, same configs path: 10/10 and 20/20 (concurrency 10, fresh salt) identical to colocated. Not validated: multi-node Mooncake role launch on SLURM (template rendered and `bash -n` only). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes disaggregated inference launch paths, KV connector composition, and Mooncake SLURM wiring; misconfiguration could break multi-turn prefix reuse, though config validation and unit tests cover the main connector shapes. > > **Overview** > Adds **per-role KV cache offload** for disaggregated prefill/decode via `inference.deployment.prefill_kv_cache_offload` and `decode_kv_cache_offload` (`inherit`, disabled, or a full offload block). Resolvers expose `kv_cache_offload_by_role`, `for_pd_role()`, and emit **`inference-{prefill,decode}.json`** so each node loads the right connector set. > > **Mooncake** now allows `cpu.num_bytes = 0` to join the shared store without starting a storage client (native offload still requires positive CPU bytes). Under P/D, the Mooncake connector gets **`save_decode_cache`** so decode-written KV is available on the next prefill turn; validation requires a **single disk path** across Mooncake roles sharing one master. > > SLURM templates take a **`mooncake_offloads`** map (replacing global offload flags), role-aware `_mooncake_store.sh.j2`, and **`inference-$ROLE.json`** in P/D jobs; inference nodes always run **`ulimit -l unlimited`** for NIXL/RDMA pinned memory. Docs cover the typical prefill-storage / decode-connect pattern and KV layout constraints across roles. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ee5dcc6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
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.

Router replay now works with
inference.kv_cache_offloadon vLLM 0.31, both native (OffloadingConnector) and Mooncake (MooncakeStoreConnector), with or without P/D. Per-role P/D offload (decode-side Mooncake withsave_decode_cache) is in #3832; decode writes are not clamped, and prefill loads of decode-saved blocks go through the same clamp.Problem on 0.31. vLLM 0.31 keeps routing rows in a local store keyed by block hash. No connector moves rows with the KV, so a block filled by an offload load has no rows on this instance. A request that needs them fails the engine step with
BlockObjectStoreError. Upstream allows offload connectors and fails closed.1. Load clamp. External loads stop at
block_floor(routed_experts_prompt_start). vLLM never returns rows before that position, so the request never reads the loaded blocks' rows.OffloadingConnector._max_loadable_tokens.MooncakeStoreConnector.get_num_new_matched_tokensand shrinkload_specs[rid].kvpool_cached_tokens(and cleartail_key_boundaries), soupdate_state_after_alloc's assert holds.num_output_tokens > 0) and decode-side P/D requests are not limited: they emit rows only from their first forward, which always recomputes that token, so they keep their offload hits.Why clamp instead of restoring rows. Restoring rows with offloaded blocks would need each connector to carry an extra per-block object, and the row store lives in the worker (TP rank 0) while load decisions are made in the scheduler. Neither connector has a hook for that. The clamp is a scheduler-side bound and keeps the hits that matter (multi-turn reloads).
2. Loaded-block cut (still needed on 0.31). Loaded blocks enter the GPU prefix cache. A later request with a lower
routed_experts_prompt_startthat hits them locally (for example a first turn hitting a prefix reloaded by another rollout's later turn) needs their rows and would crash the engine. The scheduler marks blocks covered bynum_external_computed_tokens(unmarked when reallocated), and_get_local_prefix_cache_hitcuts such hits at the first loaded block the request needs rows from, then recomputes. Requests with output and decode-side P/D requests are skipped. It also covers non-P/D requests on a decode instance that hit NIXL-loaded blocks.Hits kept: multi-turn reloads (verifiers sets
routed_experts_prompt_start = path_len - 1). Hits lost: offloaded prefixes past it (first turns, shared prompts), which are recomputed.validate_router_replay_without_kv_offloadis deleted; docs updated. vLLM 0.31 allows offload connectors with routed-experts capture, and the P/D routed-experts bypass on main covers NIXL insideMultiConnector.Checks run
pytest -m "not gpu" tests/unit/orchestrator tests/unit/test_configs.py tests/unit/inferenceon this head: 316 passed. Ruff: clean.KVCacheManager.GPU validation (2026-10-06, vLLM 0.31.0, 1x 8xH200, Qwen3-30B-A3B TP1, BI, native offload 40 GB CPU, 2000 GPU blocks; compared with a no-offload reference)
BlockObjectStoreError("auxiliary output object does not exist")cpu.num_bytes = 0+save_decode_cache), same conversations, re-run on this stack (job 2900)Tokens were identical everywhere.
Note
Medium Risk
Changes vLLM scheduler and KV offload connector behavior for routed-expert capture; incorrect bounds could cause
BlockObjectStoreErroror silent prefix-cache misses, though unit tests cover the core logic.Overview
Router replay can now run together with
inference.kv_cache_offload(native and Mooncake). The RL config no longer rejects that combination, and the inference docs describe the new behavior instead of calling the mix unsupported.Because offloaded KV blocks do not carry routed-expert rows in vLLM’s local store, prime-rl adds two vLLM scheduler patches: clamp external loads so they stop at the block before
routed_experts_prompt_start(with exceptions for P/D decode and resumed requests), and cut local prefix-cache hits at externally loaded blocks when a request would need missing routing rows. Multi-turn reloads keep safe offload hits; shared or first-turn prefixes may be recomputed.Unit tests cover
_routed_experts_load_limit, loaded-block tracking, and prefix-hit cutting on vLLM’sKVCacheManager.Reviewed by Cursor Bugbot for commit b7ef693. Bugbot is set up for automated code reviews on this repo. Configure here.