[HiSparse]Fix DeepSeek V4 HiSparse PD Transfers with Separate Host and Device KV Indices (#31901)

Co-authored-by: jackyYang6 <82102811+jackyYang6@users.noreply.github.com>
This commit is contained in:
huangtingwei
2026-08-03 20:35:04 +08:00
committed by GitHub
co-authored by jackyYang6
parent d48ab2d386
commit 3953788596
3 changed files with 201 additions and 13 deletions
+27 -1
View File
@@ -1249,6 +1249,32 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
page_indices = kv_to_page_indices(kv_indices, kv_transfer_page_size).astype( page_indices = kv_to_page_indices(kv_indices, kv_transfer_page_size).astype(
np.int32 np.int32
) )
device_page_indices = None
if (
self.scheduler.enable_hisparse
and isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool)
and not _is_fake_transfer(decode_req.req, self.scheduler.server_args)
):
# alloc_logical_only() already allocated the shared logical pages
# used by C4 indexer and C128 KV. These device buffers do not use
# the C4 sparse physical-slot mapping; carry their logical page IDs
# alongside the independently allocated C4 host page IDs.
full_kv_indices = self.req_to_token_pool.req_to_token[
decode_req.req.req_pool_idx,
prefix_len:origin_input_len,
]
device_page_indices = kv_to_page_indices(
full_kv_indices,
page_size,
).astype(np.int32)
if self.transfer_backend != TransferBackend.MOONCAKE:
raise NotImplementedError(
"DSV4 HiSparse direct PD transfer currently requires "
"the Mooncake backend"
)
metadata_kwargs = {"decode_prefix_len": total_prefix_len}
if device_page_indices is not None:
metadata_kwargs["device_kv_indices"] = device_page_indices
if ( if (
self.transfer_queue.enable_staging self.transfer_queue.enable_staging
and hasattr(decode_req.kv_receiver, "require_staging") and hasattr(decode_req.kv_receiver, "require_staging")
@@ -1263,7 +1289,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
page_indices, page_indices,
decode_req.metadata_buffer_index, decode_req.metadata_buffer_index,
state_indices, state_indices,
decode_prefix_len=total_prefix_len, **metadata_kwargs,
) )
if decode_req.is_rebootstrap: if decode_req.is_rebootstrap:
self.kv_manager.submit_prefill_recompute( self.kv_manager.submit_prefill_recompute(
@@ -85,6 +85,7 @@ class TransferInfo:
required_dst_info_num: int required_dst_info_num: int
is_dummy: bool is_dummy: bool
decode_prefix_len: Optional[int] = None decode_prefix_len: Optional[int] = None
dst_device_kv_indices: Optional[npt.NDArray[np.int32]] = None
# Note: always put the optional staging field at the final (it will be set through 'STAGING_RSP' pkg when needed) # Note: always put the optional staging field at the final (it will be set through 'STAGING_RSP' pkg when needed)
staging: Optional[StagingTransferInfo] = None staging: Optional[StagingTransferInfo] = None
@@ -113,6 +114,11 @@ class TransferInfo:
decode_prefix_len=( decode_prefix_len=(
int(msg[8].decode("ascii")) if len(msg) > 8 and msg[8] != b"" else None int(msg[8].decode("ascii")) if len(msg) > 8 and msg[8] != b"" else None
), ),
dst_device_kv_indices=(
np.frombuffer(msg[9], dtype=np.int32)
if len(msg) > 9 and msg[9] != b""
else None
),
) )
@@ -623,6 +629,8 @@ class MooncakeKVManager(CommonKVManager):
force_flat: bool = False, force_flat: bool = False,
src_layer_ids: Optional[List[int]] = None, src_layer_ids: Optional[List[int]] = None,
dst_layer_ids: Optional[List[int]] = None, dst_layer_ids: Optional[List[int]] = None,
dst_device_data_indices: Optional[npt.NDArray[np.int32]] = None,
dst_device_data_ptrs: Optional[set[int]] = None,
) -> int: ) -> int:
""" """
Generic KV cache transfer supporting both MHA and MLA architectures. Generic KV cache transfer supporting both MHA and MLA architectures.
@@ -632,10 +640,18 @@ class MooncakeKVManager(CommonKVManager):
even on a non-MLA backend, for K-only state buffers (e.g. MiniMax sparse even on a non-MLA backend, for K-only state buffers (e.g. MiniMax sparse
index) whose per-layer list must not be half-split into K/V. index) whose per-layer list must not be half-split into K/V.
""" """
# Group by indices for optimization # Host and device buffers may use different destination page spaces.
# Build both transfer plans once, then select per destination buffer.
prefill_kv_blocks, dst_kv_blocks = group_concurrent_contiguous( prefill_kv_blocks, dst_kv_blocks = group_concurrent_contiguous(
prefill_data_indices, dst_data_indices prefill_data_indices, dst_data_indices
) )
device_prefill_kv_blocks = device_dst_kv_blocks = None
if dst_device_data_indices is not None:
device_prefill_kv_blocks, device_dst_kv_blocks = (
group_concurrent_contiguous(
prefill_data_indices, dst_device_data_indices
)
)
layers_params = None layers_params = None
@@ -701,7 +717,18 @@ class MooncakeKVManager(CommonKVManager):
src_ptr: int, dst_ptr: int, item_len: int src_ptr: int, dst_ptr: int, item_len: int
) -> List[Tuple[int, int, int]]: ) -> List[Tuple[int, int, int]]:
transfer_blocks = [] transfer_blocks = []
for prefill_index, decode_index in zip(prefill_kv_blocks, dst_kv_blocks): if dst_device_data_ptrs and int(dst_ptr) in dst_device_data_ptrs:
assert (
device_prefill_kv_blocks is not None
and device_dst_kv_blocks is not None
)
src_blocks, dst_blocks = (
device_prefill_kv_blocks,
device_dst_kv_blocks,
)
else:
src_blocks, dst_blocks = prefill_kv_blocks, dst_kv_blocks
for prefill_index, decode_index in zip(src_blocks, dst_blocks):
src_addr = src_ptr + int(prefill_index[0]) * item_len src_addr = src_ptr + int(prefill_index[0]) * item_len
dst_addr = dst_ptr + int(decode_index[0]) * item_len dst_addr = dst_ptr + int(decode_index[0]) * item_len
length = item_len * len(prefill_index) length = item_len * len(prefill_index)
@@ -750,7 +777,19 @@ class MooncakeKVManager(CommonKVManager):
dst_kv_indices: npt.NDArray[np.int32], dst_kv_indices: npt.NDArray[np.int32],
executor: concurrent.futures.ThreadPoolExecutor, executor: concurrent.futures.ThreadPoolExecutor,
dst_layer_ids: Optional[List[int]] = None, dst_layer_ids: Optional[List[int]] = None,
dst_device_kv_indices: Optional[npt.NDArray[np.int32]] = None,
): ):
dst_device_kv_ptrs = None
if dst_device_kv_indices is not None:
compression_ratios = self.kv_args.mla_compression_ratios
assert compression_ratios is not None
if len(dst_kv_ptrs) == len(self.kv_args.kv_data_ptrs):
start = self.kv_args.prefill_start_layer
end = self.kv_args.prefill_end_layer
assert end is not None
compression_ratios = compression_ratios[start:end]
c4_layer_num = sum(ratio == 4 for ratio in compression_ratios)
dst_device_kv_ptrs = set(dst_kv_ptrs[c4_layer_num:])
return self._send_kvcache_generic( return self._send_kvcache_generic(
mooncake_session_id=mooncake_session_id, mooncake_session_id=mooncake_session_id,
src_data_ptrs=self.kv_args.kv_data_ptrs, src_data_ptrs=self.kv_args.kv_data_ptrs,
@@ -761,6 +800,8 @@ class MooncakeKVManager(CommonKVManager):
executor=executor, executor=executor,
src_layer_ids=self.kv_args.kv_layer_ids, src_layer_ids=self.kv_args.kv_layer_ids,
dst_layer_ids=dst_layer_ids, dst_layer_ids=dst_layer_ids,
dst_device_data_indices=dst_device_kv_indices,
dst_device_data_ptrs=dst_device_kv_ptrs,
) )
def send_kvcache_dcp( def send_kvcache_dcp(
@@ -1557,12 +1598,22 @@ class MooncakeKVManager(CommonKVManager):
is_dcp_transfer = ( is_dcp_transfer = (
target_rank_registration_info.requires_dcp_relayout target_rank_registration_info.requires_dcp_relayout
) )
chunked_dst_device_kv_indice = None
if is_dcp_transfer: if is_dcp_transfer:
if req.dst_device_kv_indices is not None:
raise RuntimeError(
"HiSparse destination device indices are not "
"supported by PD DCP relayout"
)
chunked_dst_kv_indice = req.dst_kv_indices chunked_dst_kv_indice = req.dst_kv_indices
else: else:
chunked_dst_kv_indice = req.dst_kv_indices[ chunked_dst_kv_indice = req.dst_kv_indices[
kv_chunk.index_slice kv_chunk.index_slice
] ]
if req.dst_device_kv_indices is not None:
chunked_dst_device_kv_indice = (
req.dst_device_kv_indices[kv_chunk.index_slice]
)
# NOTE: This is temporarily a workaround to deal with the case where the prefill_kv_indices # NOTE: This is temporarily a workaround to deal with the case where the prefill_kv_indices
# is mismatched with the dst_kv_indices when page size > 1, this should never happen. # is mismatched with the dst_kv_indices when page size > 1, this should never happen.
@@ -1577,6 +1628,12 @@ class MooncakeKVManager(CommonKVManager):
: len(chunked_dst_kv_indice) : len(chunked_dst_kv_indice)
] ]
) )
if chunked_dst_device_kv_indice is not None:
chunked_dst_device_kv_indice = (
chunked_dst_device_kv_indice[
: len(kv_chunk.prefill_kv_indices)
]
)
skip_kv, skip_state = self._get_dsa_cache_transfer_skip_flags( skip_kv, skip_state = self._get_dsa_cache_transfer_skip_flags(
target_rank_registration_info target_rank_registration_info
@@ -1620,7 +1677,8 @@ class MooncakeKVManager(CommonKVManager):
target_rank_registration_info.dst_kv_ptrs, target_rank_registration_info.dst_kv_ptrs,
chunked_dst_kv_indice, chunked_dst_kv_indice,
executor, executor,
target_rank_registration_info.dst_kv_layer_ids, dst_layer_ids=target_rank_registration_info.dst_kv_layer_ids,
dst_device_kv_indices=chunked_dst_device_kv_indice,
) )
elif ( elif (
self.enable_staging self.enable_staging
@@ -2244,6 +2302,7 @@ class MooncakeKVReceiver(CommonKVReceiver):
aux_index: Optional[int] = None, aux_index: Optional[int] = None,
state_indices: Optional[List] = None, state_indices: Optional[List] = None,
decode_prefix_len: Optional[int] = None, decode_prefix_len: Optional[int] = None,
device_kv_indices: Optional[npt.NDArray[np.int32]] = None,
): ):
if self.bootstrap_infos is None: if self.bootstrap_infos is None:
self.kv_mgr.record_failure( self.kv_mgr.record_failure(
@@ -2282,6 +2341,11 @@ class MooncakeKVReceiver(CommonKVReceiver):
), ),
str(self.required_dst_info_num).encode("ascii"), str(self.required_dst_info_num).encode("ascii"),
str(decode_prefix_len or 0).encode("ascii"), str(decode_prefix_len or 0).encode("ascii"),
(
np.asarray(device_kv_indices, dtype=np.int32).tobytes()
if not is_dummy and device_kv_indices is not None
else b""
),
] ]
) )
except zmq.ZMQError: except zmq.ZMQError:
@@ -1,7 +1,9 @@
import unittest import unittest
from concurrent.futures import ThreadPoolExecutor
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import MagicMock from unittest.mock import MagicMock
import numpy as np
import torch import torch
from sglang.srt.mem_cache.allocator.hisparse import ( from sglang.srt.mem_cache.allocator.hisparse import (
@@ -78,8 +80,8 @@ class TestDeepSeekV4HiSparseAllocator(CustomTestCase):
fill_len = 512 fill_len = 512
swa_tail_len = 128 swa_tail_len = 128
kv_loc = torch.arange(fill_len, dtype=torch.int64) kv_loc = torch.arange(512, 512 + fill_len, dtype=torch.int64)
host_indices = torch.arange(1000, 1000 + fill_len, dtype=torch.int64) host_indices = torch.arange(1000, 1128, dtype=torch.int64)
req = SimpleNamespace( req = SimpleNamespace(
rid="req-0", rid="req-0",
@@ -108,18 +110,17 @@ class TestDeepSeekV4HiSparseAllocator(CustomTestCase):
req_to_token_pool = ReqToTokenPool() req_to_token_pool = ReqToTokenPool()
allocator = SimpleNamespace( allocator = SimpleNamespace(
device=torch.device("cpu"), device=torch.device("cpu"),
page_size=64, page_size=256,
available_size=MagicMock(return_value=fill_len), available_size=MagicMock(return_value=fill_len),
alloc_extend_swa_tail=MagicMock(return_value=kv_loc), alloc_extend_swa_tail=MagicMock(return_value=kv_loc),
alloc_logical_only=MagicMock(return_value=kv_loc), alloc_logical_only=MagicMock(return_value=kv_loc),
) )
regular_host_alloc = MagicMock(return_value=host_indices)
coordinator = SimpleNamespace( coordinator = SimpleNamespace(
mem_pool_host=SimpleNamespace( mem_pool_host=SimpleNamespace(alloc_paged_token_slots=regular_host_alloc),
alloc_paged_token_slots=MagicMock(return_value=host_indices)
),
req_to_host_pool=object(), req_to_host_pool=object(),
req_to_host_pool_allocated_len=object(), req_to_host_pool_allocated_len=object(),
host_token_len=MagicMock(side_effect=lambda length: length), host_token_len=MagicMock(side_effect=lambda token_len: token_len // 4),
) )
queue = DecodePreallocQueue.__new__(DecodePreallocQueue) queue = DecodePreallocQueue.__new__(DecodePreallocQueue)
queue.req_to_token_pool = req_to_token_pool queue.req_to_token_pool = req_to_token_pool
@@ -138,7 +139,7 @@ class TestDeepSeekV4HiSparseAllocator(CustomTestCase):
result = queue._pre_alloc(req) result = queue._pre_alloc(req)
self.assertIs(result, host_indices) self.assertTrue(torch.equal(result, host_indices))
allocator.alloc_extend_swa_tail.assert_called_once() allocator.alloc_extend_swa_tail.assert_called_once()
allocator.alloc_logical_only.assert_not_called() allocator.alloc_logical_only.assert_not_called()
_, kwargs = allocator.alloc_extend_swa_tail.call_args _, kwargs = allocator.alloc_extend_swa_tail.call_args
@@ -149,7 +150,104 @@ class TestDeepSeekV4HiSparseAllocator(CustomTestCase):
self.assertEqual(req.kv_committed_len, fill_len) self.assertEqual(req.kv_committed_len, fill_len)
self.assertEqual(req.extend_range.length, fill_len) self.assertEqual(req.extend_range.length, fill_len)
self.assertEqual(len(req_to_token_pool.writes), 1) self.assertEqual(len(req_to_token_pool.writes), 1)
coordinator.mem_pool_host.alloc_paged_token_slots.assert_called_once() coordinator.host_token_len.assert_called_once_with(fill_len)
regular_host_alloc.assert_called_once_with(
coordinator.req_to_host_pool,
coordinator.req_to_host_pool_allocated_len,
req.req_pool_idx,
0,
len(host_indices),
)
self.assertTrue(torch.equal(req_to_token_pool.writes[0][1], kv_loc))
# C4 indexer/C128 use the logical allocator's full-page IDs. They do not
# use either the independently allocated host pages or C4 sparse slots.
np.testing.assert_array_equal(
np.unique(kv_loc.numpy() // allocator.page_size),
np.array([2, 3]),
)
def test_mooncake_uses_separate_host_and_device_page_indices(self):
from sglang.srt.disaggregation.mooncake.conn import MooncakeKVManager
manager = object.__new__(MooncakeKVManager)
manager.is_mla_backend = True
manager.is_hybrid_mla_backend = False
manager.enable_custom_mem_pool = False
manager._transfer_data = MagicMock(return_value=0)
with ThreadPoolExecutor(max_workers=1) as executor:
ret = manager._send_kvcache_generic(
mooncake_session_id="session",
src_data_ptrs=[1000, 2000, 3000],
dst_data_ptrs=[10000, 20000, 30000],
item_lens=[100, 100, 100],
prefill_data_indices=np.array([1, 2], dtype=np.int32),
dst_data_indices=np.array([7, 8], dtype=np.int32),
executor=executor,
dst_device_data_indices=np.array([21, 22], dtype=np.int32),
dst_device_data_ptrs={20000, 30000},
)
self.assertEqual(ret, 0)
manager._transfer_data.assert_called_once_with(
"session",
[
(1100, 10700, 200),
(2100, 22100, 200),
(3100, 32100, 200),
],
)
def test_mooncake_derives_device_buffers_from_local_pp_layout(self):
from sglang.srt.disaggregation.mooncake.conn import MooncakeKVManager
manager = object.__new__(MooncakeKVManager)
manager.kv_args = SimpleNamespace(
kv_data_ptrs=[1000, 2000, 3000],
kv_item_lens=[100, 100, 100],
kv_layer_ids=[],
mla_compression_ratios=[4, 128, 4, 128],
prefill_start_layer=0,
prefill_end_layer=2,
)
manager._send_kvcache_generic = MagicMock(return_value=0)
executor = MagicMock()
manager.send_kvcache(
"session",
np.array([1], dtype=np.int32),
[10000, 20000, 30000],
np.array([7], dtype=np.int32),
executor,
dst_device_kv_indices=np.array([21], dtype=np.int32),
)
kwargs = manager._send_kvcache_generic.call_args.kwargs
self.assertEqual(kwargs["dst_device_data_ptrs"], {20000, 30000})
def test_mooncake_transfer_metadata_carries_device_page_indices(self):
from sglang.srt.disaggregation.mooncake.conn import TransferInfo
host_pages = np.array([7, 8], dtype=np.int32)
device_pages = np.array([21, 22], dtype=np.int32)
info = TransferInfo.from_zmq(
[
b"9",
b"127.0.0.1",
b"12345",
b"session",
host_pages.tobytes(),
b"0",
b"",
b"1",
b"0",
device_pages.tobytes(),
]
)
np.testing.assert_array_equal(info.dst_kv_indices, host_pages)
np.testing.assert_array_equal(info.dst_device_kv_indices, device_pages)
if __name__ == "__main__": if __name__ == "__main__":