[KV Connector][NIXL] Reject NVFP4 KV cache with heterogeneous TP head split (#61865)

Signed-off-by: NickLucche <nicolo.lucchesi@mistral.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kevin H. Luu <khluu000@gmail.com>
This commit is contained in:
Nicolò Lucchesi
2026-10-15 13:57:37 -07:00
committed by GitHub
co-authored by Claude Opus 5.5 Kevin H. Luu
parent c414bf0295
commit 33212db2c2
2 changed files with 107 additions and 0 deletions
@@ -73,6 +73,7 @@ from vllm.v1.kv_cache_interface import (
KVCacheConfig,
KVCacheGroupSpec,
KVCacheLayout,
KVQuantMode,
MLAAttentionSpec,
compute_layer_kv_cache_shape_bytes,
)
@@ -1551,6 +1552,95 @@ class TestNixlHandshake:
with pytest.raises(AssertionError):
worker.add_remote_agent(bad_meta, remote_tp_size=2)
@pytest.mark.parametrize(
("kv_quant_mode", "local_tp", "remote_tp", "num_kv_heads", "rejected"),
[
(KVQuantMode.NVFP4, 2, 1, 8, True),
(KVQuantMode.NVFP4, 1, 2, 8, True),
(KVQuantMode.NVFP4, 2, 2, 8, False),
# Both sides hold one replicated head per rank: no head split.
(KVQuantMode.NVFP4, 16, 8, 8, False),
(KVQuantMode.NONE, 2, 1, 8, False),
],
)
@patch(
"vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker.NixlWrapper",
FakeNixlWrapper,
)
def test_handshake_rejects_nvfp4_kv_head_split(
self,
default_vllm_config,
dist_init,
kv_quant_mode,
local_tp,
remote_tp,
num_kv_heads,
rejected,
):
"""NVFP4 pages keep all heads' data before all heads' scales, so
heterogeneous-TP head splitting would silently corrupt the KV.
"""
block_size = maybe_update_block_size(16)
vllm_config = create_vllm_config(block_size=block_size)
kv_cache_config = KVCacheConfig(
num_blocks=1,
kv_cache_tensors=[],
kv_cache_groups=[
KVCacheGroupSpec(
["layer0"],
FullAttentionSpec(
block_size=block_size,
num_kv_heads=max(1, num_kv_heads // local_tp),
head_size=16,
dtype=torch.uint8,
kv_quant_mode=kv_quant_mode,
),
)
],
)
with patch(
"vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker.get_tensor_model_parallel_world_size", # noqa: E501
return_value=local_tp,
):
worker = FakeNixlConnectorWorker(
vllm_config,
"engine",
hand_shake_latency=0,
kv_cache_config=kv_cache_config,
)
worker.transfer_topo.total_num_kv_heads = num_kv_heads
worker.transfer_topo.local_physical_heads = max(1, num_kv_heads // local_tp)
local_len = 4096 * worker.block_size
remote_len = (
local_len
* max(1, num_kv_heads // remote_tp)
// worker.transfer_topo.local_physical_heads
)
worker.block_len_per_layer = [local_len]
worker.num_blocks = 1
worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
meta = NixlAgentMetadata(
engine_id=FakeNixlConnectorWorker.REMOTE_ENGINE_ID,
agent_metadata=FakeNixlWrapper.AGENT_METADATA,
kv_caches_base_addr=[0],
device_id=0,
num_blocks=1,
block_lens=[remote_len],
block_strides=[remote_len],
kv_cache_layout=worker.kv_cache_layout,
block_size=worker.block_size,
ssm_sizes=(0, 0),
attn_backend_name=worker.backend_name,
physical_blocks_per_logical_kv_block=1,
)
if rejected:
with pytest.raises(RuntimeError, match="NVFP4"):
worker.add_remote_agent(meta, remote_tp_size=remote_tp)
else:
worker.add_remote_agent(meta, remote_tp_size=remote_tp)
# NOTE: resource cleanup in mp backend is a bit finicky, so the order in which
# we put here is important. First run ray, it will clean up the resources, then
@@ -82,6 +82,7 @@ from vllm.utils.math_utils import cdiv
from vllm.utils.network_utils import make_zmq_path
from vllm.utils.torch_utils import async_tensor_h2d
from vllm.v1.kv_cache_interface import (
AttentionSpec,
CircularBufferSpec,
FullAttentionSpec,
KVCacheLayout,
@@ -2671,6 +2672,22 @@ class NixlBaseConnectorWorker:
"Heterogeneous TP head-dimension splitting requires contiguous heads. "
"Use a block-contiguous layout (e.g. LBHNC) on the prefill side."
)
# NVFP4 pages store all heads' data before all heads' scales, so a
# head's bytes are not a contiguous slice of the page.
if (
not self.use_mla
and self.transfer_topo.local_physical_heads
!= max(1, self.transfer_topo.total_num_kv_heads // remote_tp_size)
and any(
isinstance(spec, AttentionSpec) and spec.kv_quant_mode.is_nvfp4
for spec in self._layer_specs.values()
)
):
raise RuntimeError(
"NIXL KV-head re-sharding is not supported for NVFP4 KV cache "
f"(local TP {self.transfer_topo.tp_size}, remote TP "
f"{remote_tp_size}). Use matching TP sizes on prefill and decode."
)
# Per-region block_len validation enforcing the P/D invariant.
# REPLICATE regions (MLA, or a whole-model MLA / replicated-KV transfer)