[Bugfix][PD] Fix DSv4 Disaggregated (#41957)

Signed-off-by: NickLucche <nlucches@redhat.com>
Co-authored-by: ZhanqiuHu <zhu@redhat.com>
This commit is contained in:
Nicolò Lucchesi
2026-05-09 16:48:24 +00:00
committed by GitHub
co-authored by ZhanqiuHu
parent 3dda9aeb54
commit 171d59ae8d
5 changed files with 49 additions and 35 deletions
@@ -9,6 +9,8 @@ No GPU or NIXL required.
from __future__ import annotations
from types import SimpleNamespace
import pytest
from vllm.distributed.kv_transfer.kv_connector.v1.nixl.tp_mapping import (
@@ -33,12 +35,15 @@ def _compute_mapping(
num_kv_heads: int = 8,
group_spec_types: tuple[type, ...] = (FullAttentionSpec,),
) -> TPMapping:
return compute_tp_mapping(
transfer_topology = SimpleNamespace(
tp_rank=tp_rank,
tp_size=tp_size,
remote_tp_size=remote_tp_size,
is_mla=is_mla,
total_num_kv_heads=num_kv_heads,
)
return compute_tp_mapping(
transfer_topology=transfer_topology,
remote_tp_size=remote_tp_size,
group_spec_types=group_spec_types,
)
@@ -593,7 +593,7 @@ class TransferTopology:
return (
f"TransferTopology("
f"tp_ratio={self.tp_ratio(info.remote_tp_size)}, "
f"K={self.total_num_kv_heads}, "
f"num_kv_heads={self.total_num_kv_heads if not self.is_mla else 1}, "
f"local_tp={self.tp_size}, "
f"remote_tp={info.remote_tp_size}, "
f"local_rank={self.tp_rank}, "
@@ -10,6 +10,7 @@ import numpy as np
from vllm.distributed.kv_transfer.kv_connector.utils import (
BlockIds,
TransferTopology,
)
from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheSpec, MambaSpec
@@ -62,11 +63,8 @@ class TPMapping:
def compute_tp_mapping(
tp_rank: int,
tp_size: int,
transfer_topology: TransferTopology,
remote_tp_size: int,
is_mla: bool,
total_num_kv_heads: int,
group_spec_types: tuple[type[KVCacheSpec], ...],
) -> TPMapping:
"""Build the complete local-to-remote TP mapping.
@@ -74,13 +72,15 @@ def compute_tp_mapping(
Computes source ranks, head slot assignments, and the rank offset
factor in a single pass.
"""
tp_rank = transfer_topology.tp_rank
tp_size = transfer_topology.tp_size
total_num_kv_heads = transfer_topology.total_num_kv_heads
# --- Attention source ranks ---
if is_mla:
# All heads replicated across all ranks.
attn_ranks = [0]
elif tp_size >= remote_tp_size:
if transfer_topology.is_mla or tp_size >= remote_tp_size:
# D (local TP) > P (remote TP): multiple local ranks read different chunks from
# *one* remote rank, corresponding to different kv heads.
# For MLA, we only need one remote since cache is duplicated. When P TP=k*TP k,
# this will spread mla ranks to read from remote k*tp_rank.
attn_ranks = [tp_rank * remote_tp_size // tp_size]
else:
# P (remote TP) > D (local TP): one local rank
@@ -123,7 +123,7 @@ def compute_tp_mapping(
}
# --- Rank offset factor ---
if is_mla or tp_size <= remote_tp_size:
if transfer_topology.is_mla or tp_size <= remote_tp_size:
# We don't index into remote for reading, no offset needed.
rank_offset_factor = 0
elif tp_size > total_num_kv_heads:
@@ -10,6 +10,7 @@ import zmq
from vllm.platforms import current_platform
from vllm.utils.network_utils import make_zmq_socket
from vllm.v1.kv_cache_interface import KVCacheSpec, UniformTypeKVCacheSpecs
# Supported platforms and types of kv transfer buffer.
# {device: tuple of supported kv buffer types}
@@ -46,3 +47,11 @@ def zmq_ctx(socket_type: Any, addr: str) -> Iterator[zmq.Socket]:
finally:
if ctx is not None:
ctx.destroy(linger=0)
def get_representative_spec_type(spec: KVCacheSpec) -> type[KVCacheSpec]:
if isinstance(spec, UniformTypeKVCacheSpecs):
# All inner specs are the same type; pick any.
inner = next(iter(spec.kv_cache_specs.values()))
return type(inner)
return type(spec)
@@ -53,6 +53,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.tp_mapping import (
)
from vllm.distributed.kv_transfer.kv_connector.v1.nixl.utils import (
_NIXL_SUPPORTED_DEVICE,
get_representative_spec_type,
zmq_ctx,
)
from vllm.distributed.kv_transfer.kv_connector.v1.ssm_conv_transfer_utils import (
@@ -100,24 +101,24 @@ class NixlConnectorWorker:
num_blocks = dst_num_blocks
if block_size_ratio is not None:
num_blocks = int(num_blocks * block_size_ratio)
ratio = physical_blocks_per_logical
logical_blocks = num_blocks // ratio
num_fa_descs = num_fa_regions * num_blocks
# All-attention fast path: single vectorized broadcast.
if num_ssm_regions == 0:
# NOTE (NickLucche) With HMA, every kv group has the same number of layers
# and layers from different groups share the same kv tensor.
# eg block_ids=[[1, 2], [3]]->blocks [1, 2] need to be
# read across all regions, same for [3], but group0-group1 blocks will
# always differ (different areas). Therefore we can just flatten the
# block_ids and compute the descs ids for all groups at once.
block_arr = np.concatenate(block_ids)[None, :]
region_ids = np.arange(num_fa_regions)[:, None]
return (region_ids * num_blocks + block_arr).flatten()
# NOTE (NickLucche) With HMA, every kv group has the same number
# of layers and layers from different groups share the same kv
# tensor. Therefore we compute desc IDs per group using the
# right stride:
# FA descs have num_blocks entries per region (kernel granularity),
# SSM descs have logical_blocks entries per region (no kernel
# splitting).
# Compute desc ids per group using the right stride: FA descs have
# num_blocks entries per region (kernel granularity), SSM descs have
# logical_blocks entries per region (no kernel splitting).
logical_blocks = num_blocks // physical_blocks_per_logical
all_descs: list[np.ndarray] = []
for i, group in enumerate(block_ids):
group_arr = np.asarray(group)
@@ -426,8 +427,10 @@ class NixlConnectorWorker:
self._physical_blocks_per_logical_kv_block = 1
self._sync_block_size_with_kernel()
# Unwrap UniformTypeKVCacheSpecs to get the representative spec type
self._group_spec_types = tuple(
type(g.kv_cache_spec) for g in self.kv_cache_config.kv_cache_groups
get_representative_spec_type(g.kv_cache_spec)
for g in self.kv_cache_config.kv_cache_groups
)
# Per-engine TP mappings. Generated during handshake.
@@ -1259,12 +1262,9 @@ class NixlConnectorWorker:
logger.info("Transfer plan: %s", transfer_topo.describe(engine_id))
self.tp_mappings[engine_id] = compute_tp_mapping(
transfer_topo.tp_rank,
transfer_topo.tp_size,
transfer_info.remote_tp_size,
transfer_topo.is_mla,
transfer_topo.total_num_kv_heads,
self._group_spec_types,
transfer_topology=transfer_topo,
remote_tp_size=remote_tp_size,
group_spec_types=self._group_spec_types,
)
remote_agent_name = self.nixl_wrapper.add_remote_agent(
@@ -1391,7 +1391,8 @@ class NixlConnectorWorker:
)
# num_kv_heads > tp_size with P_TP > D_TP not supported for non-mamba.
# Mamba models can have replicated FA KV with tp_ratio < 0.
if not self._has_mamba:
# MLA models do not need to handle kv replication.
if not self.use_mla and not self._has_mamba:
assert not (
tp_ratio < 0 and self.transfer_topo.is_kv_replicated(remote_engine_id)
)
@@ -1915,9 +1916,9 @@ class NixlConnectorWorker:
# D may have to perform multiple reads from different remote ranks.
# MLA opt: when P TP > D TP, only a single read is executed for
# the first remote rank (cache is duplicated).
# the first remote rank (cache is duplicated)..
if self.use_mla and tp_ratio < 0:
read_specs = read_specs[:1]
assert len(read_specs) == 1
for i, spec in enumerate(read_specs):
remote_block_size = remote_info.remote_block_size
@@ -1959,11 +1960,10 @@ class NixlConnectorWorker:
if self.use_mla and tp_ratio < 0 and read_specs:
# ..but we still need to notify the other remote ranks that we
# have the blocks we need so they can update the request state.
notif_id = f"{req_id}:{self.world_size}".encode()
notif_id = f"{meta.remote.request_id}:{self.world_size}".encode()
remote_agents = self._remote_agents[meta.remote.engine_id]
read_ranks = {s.remote_rank for s in read_specs}
for rank_to_notify, agent in remote_agents.items():
if rank_to_notify not in read_ranks:
if rank_to_notify != read_specs[0].remote_rank:
self.nixl_wrapper.send_notif(agent, notif_msg=notif_id)
def _read_blocks(