[Unified Cache][AMD] Support DeepSeek-V4 unified KV in direct external linkers (#38269)
Co-authored-by: amd-danli103 <danli103@amd.com> Co-authored-by: Duyi-Wang <duyi.wang@amd.com> Co-authored-by: TianDi101 <tiandi920722@gmail.com>
This commit is contained in:
co-authored by
amd-danli103
Duyi-Wang
TianDi101
parent
0bae67648a
commit
822e73ccdd
@@ -431,8 +431,9 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
|
||||
def free_swa(self, free_index: torch.Tensor):
|
||||
"""Release the SWA peers of an arbitrary slot set and clear their mapping.
|
||||
Synchronizes at page_size > 1; kv-row segments go through free_swa_segment()."""
|
||||
if free_index.numel() == 0:
|
||||
No-op for a per-request ring, which owns no paged SWA peers. Otherwise
|
||||
synchronizes at page_size > 1; kv-row segments use free_swa_segment()."""
|
||||
if self._swa_req_ring or free_index.numel() == 0:
|
||||
return
|
||||
|
||||
if self.page_size == 1:
|
||||
@@ -455,8 +456,9 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
|
||||
def free_swa_segment(self, free_index: torch.Tensor, *, start_pos: int):
|
||||
"""free_swa() for a kv-row segment; same start-alignment contract as
|
||||
free_segment(), and fixed-shape at every page size."""
|
||||
if free_index.numel() == 0:
|
||||
free_segment(), and fixed-shape at every page size. No-op for a
|
||||
per-request ring, as in free_swa()."""
|
||||
if self._swa_req_ring or free_index.numel() == 0:
|
||||
return
|
||||
self._free_swa_pages(free_index, start_pos=start_pos)
|
||||
|
||||
|
||||
@@ -239,34 +239,34 @@ def _build_deepseek_v4_device_pool_group(
|
||||
) -> DevicePoolGroup:
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import HiSparseC4DevicePool
|
||||
from sglang.srt.mem_cache.hybrid_cache.hybrid_pool_assembler import (
|
||||
_dsv4_compressed_region_buffers,
|
||||
_dsv4_indexer_regions,
|
||||
_resolve_deepseek_v4_layer_mappings,
|
||||
)
|
||||
|
||||
mappings = _resolve_deepseek_v4_layer_mappings(kvcache)
|
||||
if getattr(kvcache, "_unified_kv", False) or isinstance(
|
||||
kvcache.c4_kv_pool, HiSparseC4DevicePool
|
||||
):
|
||||
raise ValueError(
|
||||
"The direct external linker does not support unified-KV or HiSparse."
|
||||
)
|
||||
if kvcache.swa_page_size != page_size:
|
||||
raise ValueError(
|
||||
"DeepSeek V4 SWA page size must match the tree page size: "
|
||||
f"{kvcache.swa_page_size} != {page_size}."
|
||||
)
|
||||
if isinstance(kvcache.c4_kv_pool, HiSparseC4DevicePool):
|
||||
raise ValueError("The direct external linker does not support HiSparse.")
|
||||
|
||||
entries = [
|
||||
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,
|
||||
page_size=page_size,
|
||||
rows_are_pages=True,
|
||||
mappings = _resolve_deepseek_v4_layer_mappings(kvcache)
|
||||
is_unified_kv = getattr(kvcache, "_unified_kv", False)
|
||||
entries = []
|
||||
if not is_unified_kv:
|
||||
if kvcache.swa_page_size != page_size:
|
||||
raise ValueError(
|
||||
"DeepSeek V4 SWA page size must match the tree page size: "
|
||||
f"{kvcache.swa_page_size} != {page_size}."
|
||||
)
|
||||
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,
|
||||
page_size=page_size,
|
||||
rows_are_pages=True,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
def add(name, source, pool, buffers, layer_mapping):
|
||||
if layer_mapping:
|
||||
@@ -282,11 +282,14 @@ def _build_deepseek_v4_device_pool_group(
|
||||
)
|
||||
)
|
||||
|
||||
c4_buffers, _ = _dsv4_compressed_region_buffers(kvcache, 4)
|
||||
c128_buffers, _ = _dsv4_compressed_region_buffers(kvcache, 128)
|
||||
|
||||
add(
|
||||
PoolName.DEEPSEEK_V4_C4,
|
||||
PoolName.KV,
|
||||
kvcache.c4_kv_pool,
|
||||
kvcache.c4_kv_pool.kv_buffer,
|
||||
c4_buffers,
|
||||
mappings.c4,
|
||||
)
|
||||
for region in _dsv4_indexer_regions(kvcache, page_size):
|
||||
@@ -301,29 +304,30 @@ def _build_deepseek_v4_device_pool_group(
|
||||
PoolName.DEEPSEEK_V4_C128,
|
||||
PoolName.KV,
|
||||
kvcache.c128_kv_pool,
|
||||
kvcache.c128_kv_pool.kv_buffer,
|
||||
c128_buffers,
|
||||
mappings.c128,
|
||||
)
|
||||
add(
|
||||
PoolName.DEEPSEEK_V4_C4_STATE,
|
||||
PoolName.SWA,
|
||||
kvcache.compress_state_pools,
|
||||
_deepseek_v4_state_views(
|
||||
if not is_unified_kv:
|
||||
add(
|
||||
PoolName.DEEPSEEK_V4_C4_STATE,
|
||||
PoolName.SWA,
|
||||
kvcache.compress_state_pools,
|
||||
mappings.c4_state_global_layers,
|
||||
),
|
||||
mappings.c4_state,
|
||||
)
|
||||
add(
|
||||
PoolName.DEEPSEEK_V4_C4_INDEXER_STATE,
|
||||
PoolName.SWA,
|
||||
kvcache.indexer_compress_state_pools,
|
||||
_deepseek_v4_state_views(
|
||||
_deepseek_v4_state_views(
|
||||
kvcache.compress_state_pools,
|
||||
mappings.c4_state_global_layers,
|
||||
),
|
||||
mappings.c4_state,
|
||||
)
|
||||
add(
|
||||
PoolName.DEEPSEEK_V4_C4_INDEXER_STATE,
|
||||
PoolName.SWA,
|
||||
kvcache.indexer_compress_state_pools,
|
||||
mappings.c4_state_global_layers,
|
||||
),
|
||||
mappings.c4_state,
|
||||
)
|
||||
_deepseek_v4_state_views(
|
||||
kvcache.indexer_compress_state_pools,
|
||||
mappings.c4_state_global_layers,
|
||||
),
|
||||
mappings.c4_state,
|
||||
)
|
||||
return DevicePoolGroup(
|
||||
entries,
|
||||
mappings.transfer_layer_num,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Callable, Optional, Sequence
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.mem_cache.allocator.swa import is_swa_req_ring
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
DecLockRefParams,
|
||||
EvictParams,
|
||||
@@ -308,11 +309,9 @@ class SWAComponent(TreeComponent):
|
||||
ct = self.component_type
|
||||
state = {"len": float("inf")}
|
||||
|
||||
# unified_kv never caches the SWA ring (per-request, not content-stable),
|
||||
# so SWA bookkeeping must not gate the match here.
|
||||
swa_device_only_hicache = (
|
||||
not self.tree_core.has_swa_host_pool and self.tree_core.enable_hicache
|
||||
)
|
||||
# A per-request SWA ring is not stored in tree nodes, so its bookkeeping
|
||||
# must not gate prefix matching.
|
||||
swa_req_ring = is_swa_req_ring(self.cache.token_to_kv_pool_allocator)
|
||||
|
||||
def validator(node: UnifiedTreeNode) -> bool:
|
||||
cd = node.component_data[ct]
|
||||
@@ -320,7 +319,7 @@ class SWAComponent(TreeComponent):
|
||||
# — load_back will restore SWA from host before use.
|
||||
if cd.value is None and (match_device_only or cd.host_value is None):
|
||||
state["len"] = 0
|
||||
if swa_device_only_hicache and (node.backuped or not node.evicted):
|
||||
if swa_req_ring and (node.backuped or not node.evicted):
|
||||
return True
|
||||
return False
|
||||
state["len"] += len(node.key)
|
||||
|
||||
@@ -25,6 +25,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.allocator.swa import is_swa_req_ring
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
DecLockRefParams,
|
||||
InsertParams,
|
||||
@@ -160,6 +161,15 @@ class UnifiedCacheLinkerWrapper:
|
||||
|
||||
self.cache = cache
|
||||
self.cache_linker = cache_linker
|
||||
swa = cache.components.get(ComponentType.SWA)
|
||||
self._skip_swa = swa is not None and is_swa_req_ring(
|
||||
cache.token_to_kv_pool_allocator
|
||||
)
|
||||
self._components = tuple(
|
||||
component
|
||||
for component in cache._components_tuple
|
||||
if not (self._skip_swa and component is swa)
|
||||
)
|
||||
# rid -> what match found, consumed by the next init_load_back.
|
||||
self.hit_markers: dict[str, ExternalCacheHitMarker] = {}
|
||||
# Loads in flight, each pinning its inserted endpoint until DMA completes.
|
||||
@@ -192,7 +202,7 @@ class UnifiedCacheLinkerWrapper:
|
||||
return result
|
||||
|
||||
lookup_transfers = []
|
||||
for component in cache._components_tuple:
|
||||
for component in self._components:
|
||||
transfer = component.build_external_linker_transfer(
|
||||
LinkerTransferPhase.LOOKUP, None, tail_hashes
|
||||
)
|
||||
@@ -290,7 +300,7 @@ class UnifiedCacheLinkerWrapper:
|
||||
|
||||
# Build per-component linker transfers.
|
||||
component_transfers: list[tuple[TreeComponent, PoolTransfer]] = []
|
||||
for component in cache._components_tuple:
|
||||
for component in self._components:
|
||||
transfer = component.build_external_linker_transfer(
|
||||
LinkerTransferPhase.LOAD, None, tail_hashes
|
||||
)
|
||||
@@ -313,6 +323,20 @@ class UnifiedCacheLinkerWrapper:
|
||||
prefix_len,
|
||||
)
|
||||
|
||||
# Components omitted from the linker do not run their PREPARE hook.
|
||||
# Keep a non-restorable SWA range as tombstones instead of rebuilding
|
||||
# it from an uninitialized FULL-to-SWA mapping during cache.insert().
|
||||
if self._skip_swa:
|
||||
if req.kv is None:
|
||||
from sglang.srt.managers.schedule_batch import ReqKvInfo
|
||||
|
||||
req.kv = ReqKvInfo(
|
||||
kv_allocated_len=prefix_len,
|
||||
swa_evicted_seqlen=prefix_len,
|
||||
)
|
||||
else:
|
||||
req.kv.swa_evicted_seqlen = max(req.kv.swa_evicted_seqlen, prefix_len)
|
||||
|
||||
# Insert the newly loaded tail into the tree.
|
||||
prefix_indices = torch.cat(
|
||||
[req.prefix_indices.to(torch.int64), full_transfer.device_indices]
|
||||
@@ -484,6 +508,8 @@ class UnifiedCacheLinkerWrapper:
|
||||
node_id
|
||||
)
|
||||
if transfers is not None:
|
||||
if self._skip_swa:
|
||||
transfers = [t for t in transfers if t.name != PoolName.SWA]
|
||||
self._offload_node(node_id, transfers)
|
||||
|
||||
def _offload_node(self, node_id: NodeId, transfers: list[PoolTransfer]) -> None:
|
||||
|
||||
Reference in New Issue
Block a user