Skip to content

[KV Connector] Support NIXL heterogeneous P/D block sizes for hybrid models - #49612

Merged
njhill merged 11 commits into
vllm-project:mainfrom
njhill:het-tp-kvzero
Jul 28, 2026
Merged

njhill merged 11 commits into
vllm-project:mainfrom
njhill:het-tp-kvzero

Conversation

@njhill

@njhill njhill commented Jul 23, 2026

Copy link
Copy Markdown
Member

Hybrid (mamba) models previously asserted out heterogeneous block sizes entirely — yet they are the models where P/D block sizes most readily diverge, since the mamba-padded attention block size varies with TP sharding (#41037). Lift the restriction:

  • Mamba state blocks are indivisible, so their descriptors always use local page geometry and their desc ids are never ratio-expanded; attention descriptors remain expanded to remote-block granularity.
  • A shared per-group mapping (pull READ and push WRITE) expands attention groups to remote granularity and clips to the transferred coverage, while mamba groups map 1:1.
  • The receive post-process permutes only attention-layer caches (a lazily cached list) and zeroes everything past the transferred coverage — the untransferred token tail of the last partially covered block plus any blocks beyond it — whose zeroing the scheduler skipped for the load. The clip happens at remote-block granularity with a block-size ratio, and at kernel-block granularity when equal kernel pages meet differing logical block sizes, so mamba hybrids need this zeroing even when the ratio is 1 (including MLA).
  • The multi-read hetero-TP path (P_TP > D_TP with sharded SSM state, [PD][Bugfix] Fix NIXL hybrid MLA+mamba heterogeneous TP #49297) builds its per-source split handles from the remote- granularity descriptors, keyed by (tp_ratio, remote_block_size); replicated and single-source attention descriptors pass through whole, and SSM chunking is unaffected by the ratio.

Still unsupported, now rejected loudly at handshake time: head-sharded attention reads combined with a block-size mismatch, and host-buffer mode with a block-size ratio.

Note this is an alternative to #45575, which inflates the block-size on one side to make it common between P and D.

Fixes #41037.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@njhill njhill added the ready ONLY add when PR is ready to merge/full CI is needed label Jul 23, 2026
…models

Hybrid (mamba) models previously asserted out heterogeneous block
sizes entirely — yet they are the models where P/D block sizes most
readily diverge, since the mamba-padded attention block size varies
with TP sharding (vllm-project#41037). Lift the restriction:

- Mamba state blocks are indivisible, so their descriptors always use
  local page geometry and their desc ids are never ratio-expanded;
  attention descriptors remain expanded to remote-block granularity.
- A shared per-group mapping (pull READ and push WRITE) expands
  attention groups to remote granularity and clips to the transferred
  coverage, while mamba groups map 1:1.
- The receive post-process permutes only attention-layer caches (a
  lazily cached list) and zeroes the untransferred token tail of the
  last local block, whose zeroing the scheduler skipped for the load.
- The multi-read hetero-TP path (P_TP > D_TP with sharded SSM state,
  vllm-project#49297) builds its per-source split handles from the remote-
  granularity descriptors, keyed by (tp_ratio, remote_block_size);
  replicated and single-source attention descriptors pass through
  whole, and SSM chunking is unaffected by the ratio.

Still unsupported, now rejected loudly at handshake time: head-sharded
attention reads combined with a block-size mismatch, and host-buffer
mode with a block-size ratio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
njhill added 9 commits July 25, 2026 14:21
…-strict count tolerance

Adds an end-to-end descriptor-geometry harness for hybrid MLA+SSM models
under heterogeneous P/D block geometry (TP-sharded KDA-style state making
the mamba-aligned logical block size differ between P and D while kernel
pages stay equal): a recording NIXL wrapper resolves every prepared
transfer's local and remote descriptor ids back to byte ranges, and a
sweep over four geometries and 38 prompt lengths asserts three
invariants per read: every local write lands within the requesting
request's own blocks (a violation would corrupt a co-resident request
mid-decode), every local/remote descriptor pair is token-aligned, and
the matched tokens are fully covered.

The sweep found one real defect: _apply_prefix_caching's count-mismatch
tolerance used max(local_ppl, remote_ppl) as a strict bound, but
allocation rounding legitimately leaves up to ppl-1 trailing dead kernel
blocks per side (e.g. 12-token local vs 8-token remote blocks at N=25
gives |9-6| == max_padding), crashing legitimate transfers with an
AssertionError. Widen the bound to the sum of the ratios, inclusive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TQHxUrXWp7k5kk1oz95Cy

Signed-off-by: Nick Hill <nickhill123@gmail.com>
With equal kernel pages but differing logical block sizes (hybrid
heterogeneous TP, e.g. TP8 prefill block 768 -> TP1 decode block 5760),
the pull transfer is front-trimmed to min(local, remote) kernel blocks.
The scheduler excluded the matched-range blocks from alloc-time KV
zeroing (it would race the RDMA write), and the existing receive
tail-zeroing only runs for block_size_ratio > 1 and non-MLA models, so
the untransferred tail of the last local logical block kept stale
recycled bytes. Once decode grew into that tail, requests emitted
garbage mid-response; only the prefill-block < decode-block direction
is affected, matching observed failures in that direction only.

Adds a receive-time zeroing of the clipped attention kernel blocks and
extends the descriptor-geometry suite with a KimiLinear-scale geometry
(5760/768, kernel 64, tp_ratio=-8) plus a sentinel-based
transferred-or-zeroed invariant that reproduced the bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVJLHN2a1DP6iBJjayotrs

Signed-off-by: Nick Hill <nickhill123@gmail.com>
Receive-side zeroing of blocks the transfer didn't write lived in two
places with two different notions of granularity: the block-size-ratio
tail inside post_process_device_kv_on_receive (sub-block units, non-MLA
only, last covered block only) and _zero_untransferred_hetero_ppl_tail
(whole kernel blocks, mamba only). Neither knew about the other, so a
hybrid running both regimes at once - differing kernel block sizes and
differing physical_blocks_per_logical, reachable with prefix caching off
- compared local kernel blocks against remote sub-blocks and left whole
untransferred blocks unzeroed.

Fold both into post_process_device_kv_on_receive, which now takes the
covered sub-block count in the finest (remote-block) granularity and
zeroes everything past it: the token tail of the last partially covered
block, then whole blocks beyond. MLA enters the path too, with the
block-size/layout conversion gated separately, so its clipped blocks are
also zeroed. The invariant is now uniform: every byte of the request's
local attention blocks is either transferred or zeroed.

Also switch the block-id H2D copies to async_tensor_h2d - a pageable
torch.tensor(..., device=cuda) copy is host-blocking and stream-ordered,
so it can stall the engine loop behind queued forward kernels - and
build the tensor only when a consumer needs it.

tests/v1/kv_connector/unit/test_nixl_desc_geometry.py and
test_nixl_connector_hma.py: 236 passed on GB200 (1 unrelated failure,
test_fewer_blocks_with_hma, needs HF auth for a gated repo).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Nick Hill <nickhill123@gmail.com>
…ry suite

The desc-geometry suite only ever ran the two heterogeneous regimes in
isolation: differing physical_blocks_per_logical with equal kernel block
sizes, or (elsewhere) a block-size ratio alone. The combination - local
kernel 8 / remote kernel 4 (ratio 2) with ppl 3 vs 2 - is what the two
former receive-side zeroing paths each handled only half of.

Generalize the harness to take a separate remote kernel block size: the
remote's advertised kernel page scales down by the ratio, and descriptor
resolution now works in absolute token offsets with an explicit desc
page, so local sub-block descs pair correctly against whole remote pages.
Coverage is checked at the finest transfer granularity (remote kernel).

Against the previous two-path implementation the new case fails 11 of 16
prompt lengths on the transferred-or-zeroed invariant, e.g. N=29 leaves
192 stale bytes per region in local block 3; all 16 pass with the unified
path.

tests/v1/kv_connector/unit/test_nixl_desc_geometry.py and
test_nixl_connector_hma.py: 252 passed on GB200 (1 unrelated failure,
test_fewer_blocks_with_hma, needs HF auth for a gated repo).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Nick Hill <nickhill123@gmail.com>
It holds the by-remote-block-size map of src_blocks_data and sits beside
src_xfer_handles_by_block_size; both siblings are public-styled. No
references outside base_worker.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Nick Hill <nickhill123@gmail.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
async_tensor_h2d's result was discarded, leaving indices None: the
has_stale zeroing path then failed its assertion and the convert path
would pass None to the postprocess kernels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVJLHN2a1DP6iBJjayotrs

Signed-off-by: Nick Hill <nickhill123@gmail.com>
The geometry suite fakes a CUDA platform for the worker but builds its
VllmConfig with the real one, and KVTransferConfig.kv_buffer_device
defaults to current_platform.device_type. On the cpu-small CI queue
(image-build-cpu) that resolves to "cpu" while the worker's faked
device_type is "cuda", so use_host_buffer flips on. Host xfer buffers
are allocated per layer, so the HMA shared-tensor pooling this suite
relies on stops deduplicating: the 6 layers register as 6 regions
instead of 2, tripping the region-count check in add_remote_agent
("assert len(kv_caches_base_addr) == len(block_len_per_layer)"), and the
new block_size_ratio > 1 cases additionally hit "Heterogeneous block
sizes are not supported with host buffer".

Pin kv_buffer_device to the faked device type. The other nixl cpu_test
files are unaffected because they don't fake the platform, so their
device_type is "cpu" and use_host_buffer is forced off.

Verified against a forced CpuPlatform (which reproduces the CI failures
exactly - 17 failed - without this change): 196 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Nick Hill <nickhill123@gmail.com>
@mergify

mergify Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @njhill.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 27, 2026
Conflict in nixl base_worker._build_mamba_local: vllm-project#49988 added a
descriptor-layout diagram to its docstring and an
`assert block_size_ratio == 1`, while this branch had dropped the
function's block_size_ratio parameter entirely (mamba state pages are
indivisible, so their descriptors are ratio-independent). Kept the
ratio-free signature and body, kept main's diagram, and dropped the
assert -- lifting that restriction is what this branch is for.

The rest of vllm-project#49988 (K/V packed into one region per layer,
get_backend_aware_kv_block_len removed) merged cleanly and subsumes this
branch's _build_fa_local edits; no callers of the removed helper remain.
@mergify mergify Bot removed the needs-rebase label Jul 28, 2026
@njhill
njhill merged commit 601fa9a into vllm-project:main Jul 28, 2026
102 checks passed
@njhill
njhill deleted the het-tp-kvzero branch July 28, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kv-connector ready ONLY add when PR is ready to merge/full CI is needed v1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] _align_hybrid_block_size produces TP-dependent block sizes, currently unsupported when local and remote kernel block size mismatch

2 participants