Repository navigation
Conversation
A single-layer bucket, typically a speculative drafter layer, drove the hybrid grouping heuristic to a group size of 1, giving one KV cache group per layer. Fall back to the multi-layer group size with the least padding in that case; other models keep their existing grouping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Replace the min / 1.5x-max heuristic with a search over group sizes from the smallest bucket (at least 2) to the largest, minimizing padding with full attention padding weighted over sliding window / mamba padding. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
…se bytes Bound the hybrid KV cache group size below by min_kv_cache_group_layers (default 3) and cost padding layers by each spec's max_memory_usage_bytes instead of fixed full vs. other weights. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Co-authored-by: lucamotz <luca.motz@icloud.com> Signed-off-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>
KV transfer peers (e.g. P/D) may run different TP sizes, which change per-rank bytes and so the byte-based group size. With a KV connector, cost padding by full attention vs. bounded caches instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Co-authored-by: OpenAI Codex <noreply@openai.com> Signed-off-by: Tomas Ruiz <tomas.ruiz.te@gmail.com>
Co-authored-by: OpenAI Codex <noreply@openai.com> Signed-off-by: Tomas Ruiz <tomas.ruiz.te@gmail.com>
…arch Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Replace the full-attention-only field comparison with one helper: layers share a KV cache group if their specs merge, or if they are attention specs with the same block size, token-slot semantics and page size. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Equal page bytes across different head shapes depend on TP (e.g. Qwen3.5 4x256 target vs 8x128 DFlash drafter match at TP<=4 but not TP8), so KV transfer peers with different TP sizes could bucket layers differently. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
…e_kv_cache_group_specs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Page bytes change with TP but the number of pages each layer needs, at its own page size before unification, doesn't. Replaces the full vs. bounded split, and needs no special case for Mamba (about one page). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Co-authored-by: Luca Motz <luca.motz@icloud.com> Signed-off-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>
Co-authored-by: Luca Motz <luca.motz@icloud.com> Signed-off-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>
kv_postprocess_blksize_on_receive and the tail zeroing in post_process_device_kv_on_receive index the attention cache as [B, N, H, D], but since vllm-project#44455 packed K and V into the content dim the layer views are [B, H, N, 2*D]. With a larger block size on D, the regroup scrambled every received block on LBHNC, or failed to reshape with fewer KV heads than block_size_ratio, and the tail zeroing cleared KV heads instead of the untransferred tokens. Drop the two permutes and zero along the token dim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
… views With HMA, attention groups alias the same KV tensors, but the receive post-process applied every group's block ids to every attention layer view. A block received with a block-size ratio was regrouped once per aliasing group, which scrambles it with more than one KV head per rank (e.g. gpt-oss). Post-process each group's blocks only through its own layers' views. The cached flat list of attention views goes: the caller already skips mamba groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
With a block-size ratio, a received block holds the remote sub-blocks back to back. On a head-major layout (LBHNC, BLHNC) they must be regrouped per head, but on a token-major layout (LBNHC, BLNHC) they are already in token order, and the regroup scrambled them. Regroup only on layouts whose heads are contiguous within a block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
With enable_permute_local_kv, a token-major (LBNHC) decoder receives the prefiller's head-major (LBHNC) bytes. Both permute helpers still indexed the cache as a physical [B, N, H, D] tensor, but since vllm-project#44455 it is a logical [B, H, N, 2*D] view, so they scrambled every received block. View the received bytes in memory order before reading them as the remote head-major blocks, and use kv_postprocess_blksize_and_layout_on_receive at every block-size ratio. kv_postprocess_layout_on_receive was its ratio-1 case, with the same bug and a 5-D branch dead since vllm-project#51718. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
… sizes With a block-size ratio, pull front-clips the local (D) sub-blocks to the remote (P) count, and _apply_prefix_caching then end-trims P to D's count. Both are right only when the two lists start and end at the same token, which different block sizes break: - a partial prefix hit on D, or sliding-window clipping, starts the lists at different tokens; - each side pads its list to its own logical block past the last token. Every received page then shifts by whole remote blocks. The same end-trim shifts pages for attention-only models whose kernel blocks match but whose logical blocks differ. Drop each side's padding past the request's last token (the remote_num_tokens P already sends) and pair the lists from there backwards. The receive post-process zeroes exactly D's padding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…izes With DCP, NIXL sets the KV interleave to each side's own logical block size, so ranks own different tokens once P and D block sizes differ, and the DCP read path pairs blocks by logical position. A different kernel block then pulls misaligned tokens and leaves owned ones unwritten, and equal kernel blocks under different logical blocks fail an assert in the read. Reject DCP peers whose logical block size differs at handshake. Different kernel blocks under an equal logical block size stay supported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
The push connector cannot convert or re-pair blocks for a peer whose kernel block size or kernel blocks per logical block differ, and D registers P for notifications without checking either. Depending on the sizes, D then waits for a WRITE that never comes (P rejects its handshake, or fails to build the WRITE, after D sent the registration), or P writes head-sliced or misaligned blocks and D's receive post-process fails on the block-size ratio assert or on the empty push remote block ids. Reject such peers in _validate_remote_parallel_config, which _nixl_handshake runs for P's and both of D's handshakes (registration and heartbeat) before registering the peer, so the request fails on D like any failed handshake. This subsumes the PP-only check in add_remote_agent and leaves push's block_size_ratio > 1 branch in _xfer_blocks unreachable, so it is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luca Motz <luca.motz@icloud.com>
P/D decode recomputes the last prompt token. NIXL and Mooncake loaded all N prompt tokens and the scheduler then rewound one token, after every group was laid out for position N. A sliding window laid out for N drops the block holding position N - W whenever N % block_size == block_size - 1, so the recomputed token attended to the null block; Mamba avoided this only through a Mamba-specific backoff. Make that backoff unconditional: the prefiller stops at N - 1 and the decoder loads N - 1 tokens and computes the last one, as the local prefix cache already does. Both sides then lay out every group for the position decode resumes at. Document the contract in get_num_new_matched_tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…s at its end Bidirectional transfer lets the prefiller pull KV the decoder kept from an earlier turn. The workers pair the two sides' blocks from the end of the sequence, so a pull that stops before the decoder's KV ends shifts every block by one or more positions: a prompt shorter than the decoder's context (e.g. a regenerated turn), or, now that the prefiller stops one token short, a prompt the decoder's KV fully covers. Pull only when the decoder's KV ends before the prefiller's last prompt token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…ultimodal prompts Now that every prefiller stops one token short, two prompt shapes that only Mamba and multi-module MTP models used to hit reach all models: - A mixed token-id / embeddings prompt kept its embeddings and mask at full length, which the worker rejects. Cut all three. - A prompt ending in a multimodal item leaves the item running past the cut prompt. With disable_chunked_mm_input and a prefill lookahead (EAGLE), the scheduler rolled back before the item every step and never scheduled it. Covering the rest of the prompt now counts as covering the item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…-side hit Now that decode loads N - 1 tokens, a decode-side prefix hit of N - 1 tokens leaves nothing to receive. Push mode then staged no registration, but kept heartbeating the prefiller, which held its blocks until the decode request ended, and left an empty recv whose metadata the worker never drops. Stop the heartbeat so the prefiller reclaims the blocks when its lease lapses, and finish the remote prefill there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…t would split Cutting the last prompt token on the prefiller can split a multimodal item the prompt ends with. Models parse an item from its whole placeholder: Qwen3-VL looks up each video frame's end marker and fails to start the request, and with disable_chunked_mm_input and a lookahead (EAGLE) the scheduler rolled back before an image running past the cut prompt forever. Back off to the start of such an item on both sides instead: the prefiller drops the item, the decoder loads the same prefix and computes the item. The rule lives in two shared helpers that NIXL and Mooncake both use, which also cut mixed token-id / embeddings prompts consistently. This replaces the scheduler change, which is no longer needed since an item never runs past the cut prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
When a multimodal item starts at the beginning of the prompt and the cut would split it, there is no prefix both sides can stop at. Falling back to the full prompt made decode load all N tokens and rewind one, the layout bug the backoff exists to avoid. Transfer nothing instead: the decoder computes the whole prompt and the prefiller's own result goes unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
…ed load When decode has nothing to load (a decode-side prefix hit of N - 1 tokens, a 1-token prompt, or no safe prefix to cut), Mooncake still sends P an empty pull so P releases its blocks. The failure path already skipped reporting such a pull, but the success path reported it as finished_recving for a request that never waited on a load, tripping the scheduler's assert. Skip it there too. Also update the HiSparse NIXL import test for a 1-token prompt, which now imports nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Signed-off-by: Luca Motz <luca.motz@icloud.com>
Lucas Wilkinson's hybrid KV cache group planning, unchanged. Signed-off-by: Luca Motz <luca.motz@icloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a KV connector configured, vllm-project#58207 plans KV cache groups TP-invariantly, since the connector's peers (e.g. P and D) may run different TP sizes. Ask the connector instead: requires_matching_kv_cache_groups() defaults to True. CPU offloading keeps KV inside the engine, so OffloadingConnector with CPUOffloadingSpec and SimpleCPUOffloadConnector return False, and MultiConnector asks the connectors it wraps. Signed-off-by: Luca Motz <luca.motz@icloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apes P and D may group the same layers differently, and across TP a layer's KV heads per rank and tokens per page can change by a different factor than another layer's (e.g. a target and a drafter across a KV-head replication boundary). The model-wide head mapping and one engine-wide block size ratio then don't fit every layer. The handshake (version 15) advertises each region member's KV cache group, KV heads and tokens per page. Where the model-wide mapping doesn't fit, layers transfer in group pairs, split by (local group, remote group, geometry): each local page receives its own KV heads from the remote ranks owning them (a read takes each head once, a write every replica), in token units both pages are multiples of, through prepared lists per remote rank. Pull and push both use them; a push decode registers the token count both sides pair from and computes its own pairs to zero what the write leaves unwritten. Matched plans the model-wide mapping fits keep today's path. MLA, CSA-linear and PP push layer routing don't use group pairs, so their peers must group each layer alike, which the handshake now checks. Plans can differ even when both sides plan TP-invariantly: PP raises the in-flight tokens the planner costs sliding windows with. Signed-off-by: Luca Motz <luca.motz@icloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group pairs map different peer plans, so NixlConnector no longer needs TP-invariant planning, except for MLA. NixlPushConnector keeps it: a PP producer routes layers by name, without group pairs. Signed-off-by: Luca Motz <luca.motz@icloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a layer's pages on the two engines don't divide each other, group pairs transfer them in small token units. Log once per remote engine the --block-size that gives both engines the same block size, and where to set it. Signed-off-by: Luca Motz <luca.motz@icloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 tasks done
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
1 task done
5 of 6 tasks
This branch has not been deployed
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.
Overview
NIXL can now transfer KV between P and D whose hybrid KV cache group plans differ, so NixlConnector no longer needs #58207's TP-invariant planning, except for MLA. Motivated by RFC #58638. Stacked on #58207 at
e55909cad0, #58860, #59746, #59747 and #59330; only the last four commits are new.Claims
requires_matching_kv_cache_groups()defaults to True, so every other connector keeps [Core] Pick hybrid KV cache group size by worst-case padding bytes; add --min-kv-cache-group-layers #58207's planning.--max-num-batched-tokens, PP and async scheduling.Validation
pytest tests/v1/core/test_kv_cache_utils.py pytest tests/v1/kv_connector/unit/test_nixl_connector.py tests/v1/kv_connector/unit/test_nixl_connector_hma.py \ tests/v1/kv_connector/unit/test_nixl_push_connector.py tests/v1/kv_connector/unit/test_nixl_desc_geometry.py \ tests/v1/kv_connector/unit/test_tp_mapping.py tests/v1/kv_connector/unit/test_nixl_heartbeat.py \ tests/v1/kv_connector/unit/test_bidirectional_kv_transfer.py tests/v1/kv_connector/unit/test_handshake_pp_aggregation.py \ tests/v1/kv_connector/unit/test_nixl_simple_cpu_offload.py tests/v1/kv_connector/unit/test_transfer_topology_sharded.pyOn 6× H100, pull with independent plans is bitwise equal to TP-invariant push in both directions and at 262144 tokens. Under load, it shows no TTFT, TPOT or throughput cost against TP-invariant pull beyond run-to-run spread. On 2× DGX Spark, smaller models in pull and push, including D prefix hits and an MTP drafter, are bitwise equal to matching plans too.
CPU tests and byte oracle
test_pull_between_different_group_plans(new) builds P and D workers for sliding-window and GDN hybrids whose plans put 2 vs 4 layers in a group, runs the realregister_kv_caches,add_remote_agentand pull read path, and replays the recorded NIXL descriptors on CPU tensors: every D head and token holds P's bytes for the same layer, head and token, and nothing else is written.test_kv_transfer_group_planning_is_tp_invariantgets one row per connector;test_layer_handshake_rejects_unsupported_geometrygets a row for a PP push producer whose plan differs.77126176e8on main74c5cbcd7b: the 10 NIXL files pass with 709 tests andtest_kv_cache_utils.pywith 187, except 3 tests that also fail on main in this environment.GPU: Qwen3.6-35B-A3B-FP8 + DFlash, 6× H100 SXM
07c43a220con mainb1f229fb75.Qwen/Qwen3.6-35B-A3B-FP8+z-lab/Qwen3.6-35B-A3B-DFlash(7 speculative tokens),TRITON_ATTN,--enforce-eager, greedy, 4 code prompts of 1100–12000 tokens, P TP2 on GPUs 0–1 and D TP4 on 2–5 and the reverse,NCCL_NVLS_ENABLE=0andVLLM_USE_DEEP_GEMM=0. Each NixlConnector pull with independent plans is compared with a NixlPushConnector run, which keeps TP-invariant plans, on the same image: texts and logprobs are bitwise equal on all 4 prompts, DFlash accepted tokens are equal, and D reads every prompt token but the last over NIXL.These runs set
--block-size 592on the TP4 engine, as the new log line suggests. Without it, TP4 picks block 304 and outputs stay bitwise equal, but D'sadd agenthandshake step takes 11–15× longer and NIXL transfers about 6× longer.Perf A/B: independent vs TP-invariant pull under load, 6× H100 SXM
Arm B is
07c43a220con mainb1f229fb75(plans 17 / 5); arm A is the same build with "Let pull peers plan KV cache groups independently" reverted (TP-invariant, 10 / 10). P TP2 → D TP4, max_model_len 262144,--block-size 592on TP4, the flags above.vllm bench servethrough the P/D proxy with random 8192-token prompts, run order A B B A, each run restarting both engines; greedy outputs are bitwise equal across all four runs. B − A pools both runs per arm, with a 95% bootstrap CI in ms.D's NIXL transfers move the same bytes in both arms; under load their mean time is 77.6 vs 78.9 ms, and post-processing takes about 0.4 ms longer per transfer in B. Eager mode, so absolute numbers are not production numbers.
GB10: smaller models, pull and push, 2× DGX Spark
Build
98132d0d55: this branch's four commits on maind0d6e5f3a2. P and D on one Spark, or the TP2 side across both Sparks;--enforce-eager, greedy. Each case differs between P and D in plan, block size or TP and is compared with the same setup on matching plans and pages, on the same image: texts and logprobs are bitwise equal, and D reads every prompt token but the last over NIXL.rc_mlx5, with the reads into GPU memory emulated in software.Details
kv_transfer_config is not Nonecheck. MultiConnector asks the connectors it wraps; OffloadingConnector withCPUOffloadingSpecand SimpleCPUOffloadConnector return False, since their KV doesn't leave the engine.use_mla. NixlPushConnector stays TP-invariant, since a PP push producer routes layers by name. MLA, CSA-linear and PP push producers reject peers with different plans at the handshake.--block-sizethat makes P and D pages divide, once per remote engine.Not covered: MLA, where a GQA draft under an MLA target stays with #58471 and #59071; DCP; GPUDirect RDMA across nodes. The NIXL compatibility doc isn't updated yet.
The four commits change +700/−51 production lines. The hook can go as a small PR on top of #58207 and the NIXL commits after it, if that is easier to review. No open NIXL PR, and none citing #58638 or #58207, transfers between different KV cache group plans.
AI assistance (Claude Code) was used for the analysis, patch and tests. I reviewed every changed line.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code