[Unified Cache][7/N] Support MTP, EAGLE, and DSpark draft KV caches in the external linker (#37914)

Co-authored-by: hzh0425 <hzh0425@apache.org>
This commit is contained in:
huangtingwei
2026-09-13 18:22:06 +08:00
committed by GitHub
co-authored by hzh0425
parent cebca698e2
commit a7cf4a6fbc
4 changed files with 183 additions and 31 deletions
@@ -1300,7 +1300,9 @@ class _DeepSeekV4Strategy(StackStrategy):
_build_deepseek_v4_device_pool_group,
)
return _build_deepseek_v4_device_pool_group(kvcache, page_size)
return _build_deepseek_v4_device_pool_group(
kvcache, page_size, params.mtp_draft_device_pools
)
def build(
self,
@@ -1572,7 +1574,9 @@ class _DsaStrategy(StackStrategy):
_build_dsa_device_pool_group,
)
return _build_dsa_device_pool_group(kvcache, page_size)
return _build_dsa_device_pool_group(
kvcache, page_size, params.mtp_draft_device_pools
)
def build(
self,
@@ -26,7 +26,7 @@ class DevicePoolEntry:
indices_from_pool: PoolName,
device_pool: Any,
components: Sequence[Sequence[torch.Tensor]],
layer_mapping: dict[int, int],
layer_mapping: dict[int, int | Sequence[int]],
page_size: int,
rows_are_pages: bool,
packed: bool = True,
@@ -127,14 +127,16 @@ class DevicePoolEntry:
return self._rows(indices)
def get_prepared_layer_range_meta(self, locations: list[int], layer: int):
buffer_index = self.layer_mapping.get(layer)
if buffer_index is None:
mapped = self.layer_mapping.get(layer)
if mapped is None:
return None
buffer_indices = [mapped] if isinstance(mapped, int) else list(mapped)
items = []
for component, offsets in zip(self.buffer_meta, self._component_offsets):
base_ptr, row_stride, size = component[buffer_index]
items.append((base_ptr, row_stride, size, offsets[buffer_index]))
for buffer_index in buffer_indices:
base_ptr, row_stride, size = component[buffer_index]
items.append((base_ptr, row_stride, size, offsets[buffer_index]))
ptrs, sizes, offsets = [], [], []
for row in locations:
@@ -234,8 +236,28 @@ def _deepseek_v4_state_views(state_pools: list[Any], global_layers: list[int]):
return views
def _with_packed_draft_mapping(
layer_mapping: dict[int, int],
*,
target_device_layer_num: int,
draft_layer_num: int,
) -> dict[int, int | tuple[int, ...]]:
"""Attach draft depth N to the same transfer layer as target layer N."""
if draft_layer_num > len(layer_mapping):
raise ValueError(
"Packed draft layers exceed the target transfer layer count: "
f"{draft_layer_num} > {len(layer_mapping)}."
)
result: dict[int, int | tuple[int, ...]] = dict(layer_mapping)
for depth in range(draft_layer_num):
result[depth] = (layer_mapping[depth], target_device_layer_num + depth)
return result
def _build_deepseek_v4_device_pool_group(
kvcache: Any, page_size: int
kvcache: Any,
page_size: int,
mtp_draft_device_pools: tuple[Any, ...] = (),
) -> DevicePoolGroup:
from sglang.srt.mem_cache.deepseek_v4_memory_pool import HiSparseC4DevicePool
from sglang.srt.mem_cache.hybrid_cache.hybrid_pool_assembler import (
@@ -256,13 +278,23 @@ def _build_deepseek_v4_device_pool_group(
"DeepSeek V4 SWA page size must match the tree page size: "
f"{kvcache.swa_page_size} != {page_size}."
)
draft_swa_buffers = [
buffer
for pool in mtp_draft_device_pools
for buffer in pool.swa_kv_pool.kv_buffer
]
swa_mapping = _with_packed_draft_mapping(
mappings.swa,
target_device_layer_num=len(kvcache.swa_kv_pool.kv_buffer),
draft_layer_num=len(draft_swa_buffers),
)
entries.append(
DevicePoolEntry(
name=PoolName.SWA,
indices_from_pool=PoolName.SWA,
device_pool=kvcache.swa_kv_pool,
components=[kvcache.swa_kv_pool.kv_buffer],
layer_mapping=mappings.swa,
components=[[*kvcache.swa_kv_pool.kv_buffer, *draft_swa_buffers]],
layer_mapping=swa_mapping,
page_size=page_size,
rows_are_pages=True,
)
@@ -336,21 +368,41 @@ def _build_deepseek_v4_device_pool_group(
)
def _build_dsa_device_pool_group(kvcache: Any, page_size: int) -> DevicePoolGroup:
def _build_dsa_device_pool_group(
kvcache: Any,
page_size: int,
mtp_draft_device_pools: tuple[Any, ...] = (),
) -> DevicePoolGroup:
if kvcache.page_size != page_size:
raise ValueError(
"DSA KV page size must match the tree page size: "
f"{kvcache.page_size} != {page_size}."
)
num_layers = kvcache.layer_num
identity = {layer: layer for layer in range(num_layers)}
if any(pool.page_size != page_size for pool in mtp_draft_device_pools):
raise ValueError("DSA MTP page size must match the tree page size.")
draft_kv_buffers = [
buffer for pool in mtp_draft_device_pools for buffer in pool.kv_buffer
]
draft_indexer_buffers = [
buffer
for pool in mtp_draft_device_pools
for buffer in pool.index_k_with_scale_buffer
]
if len(draft_kv_buffers) != len(draft_indexer_buffers):
raise ValueError("DSA MTP KV and indexer draft layer counts must match.")
layer_mapping = _with_packed_draft_mapping(
{layer: layer for layer in range(num_layers)},
target_device_layer_num=num_layers,
draft_layer_num=len(draft_kv_buffers),
)
entries = [
DevicePoolEntry(
name=PoolName.KV,
indices_from_pool=PoolName.KV,
device_pool=kvcache,
components=[kvcache.kv_buffer],
layer_mapping=identity,
components=[[*kvcache.kv_buffer, *draft_kv_buffers]],
layer_mapping=layer_mapping,
page_size=page_size,
rows_are_pages=False,
),
@@ -358,8 +410,8 @@ def _build_dsa_device_pool_group(kvcache: Any, page_size: int) -> DevicePoolGrou
name=PoolName.INDEXER,
indices_from_pool=PoolName.KV,
device_pool=kvcache,
components=[kvcache.index_k_with_scale_buffer],
layer_mapping=identity,
components=[[*kvcache.index_k_with_scale_buffer, *draft_indexer_buffers]],
layer_mapping=layer_mapping,
page_size=page_size,
rows_are_pages=True,
),
@@ -253,6 +253,7 @@ class BaseSpecWorker(ABC):
spec_algorithm = target_model_runner.spec_algorithm
if not (
get_memory().enable_hierarchical_cache
or get_memory().enable_unified_cache_external_linker
or get_disagg().disaggregation_decode_retraction_backup == "host_pool"
):
return HiCacheDraftPlan()
@@ -276,6 +277,11 @@ class BaseSpecWorker(ABC):
device_pools=draft_pools,
)
if get_memory().enable_unified_cache_external_linker:
raise NotImplementedError(
"The external linker only supports packed draft KV caches."
)
return HiCacheDraftPlan(
mode=HiCacheDraftMode.SIDECAR,
# Preserve the legacy non-packed HiCache behavior: multi-layer