leideng/QCFuse / srt /mem_cache /storage /lmcache /lmc_radix_cache.py
leideng's picture
download
raw
10 kB
from __future__ import annotations
import logging
import threading
from typing import TYPE_CHECKING, Optional
import torch
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
from sglang.srt.mem_cache.base_prefix_cache import MatchResult
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
try:
from lmcache.integration.sglang.sglang_adapter import (
LMCacheLayerwiseConnector,
LoadMetadata,
StoreMetadata,
)
except ImportError as e:
raise RuntimeError(
"LMCache is not installed. Please install it by running `pip install lmcache`"
) from e
if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.managers.schedule_batch import Req
logger = logging.getLogger(__name__)
class LayerTransferCounter:
"""Minimal adapter that lets the memory pool notify LMCache per-layer.
The KV pool calls `wait_until(layer_id)` after finishing a layer, which we
translate into a `load_kv_layerwise(layer_id)` call on the LMCache connector
within the provided CUDA stream.
"""
def __init__(
self,
num_layers: int,
load_stream: torch.cuda.Stream,
lmc_connector: LMCacheLayerwiseConnector,
printable: bool = False,
):
self.num_layers = num_layers
self.load_stream = load_stream
self.lmc_connector = lmc_connector
def wait_until(self, layer_id: int):
# Ensure ordering of the async loads wrt compute stream(s).
self.load_stream.synchronize()
with self.load_stream:
self.lmc_connector.load_kv_layerwise(layer_id)
class LMCRadixCache(RadixCache):
"""RadixCache + LMCache IO.
This subclass adds:
- LMCache connector setup (device/host buffers, TP rank/size)
- Two CUDA streams for async load/store
- Layer-wise transfer executor wiring to the KV cache
- Overridden `match_prefix` to fetch missing prefix chunks from LMCache
- Extended cache_finalization paths to store back into LMCache
- Eviction barrier that respects any in-flight host->device stores
"""
def __init__(
self,
req_to_token_pool: ReqToTokenPool,
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
page_size: int,
disable: bool = False,
enable_kv_cache_events: bool = False,
model_config: Optional["ModelConfig"] = None,
tp_size: int = 1,
rank: int = 0,
tp_group: Optional[torch.distributed.ProcessGroup] = None,
eviction_policy: str = "lru",
):
super().__init__(
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
page_size=page_size,
disable=disable,
enable_kv_cache_events=enable_kv_cache_events,
eviction_policy=eviction_policy,
)
kvcache = self.token_to_kv_pool_allocator.get_kvcache()
self.lmcache_connector = LMCacheLayerwiseConnector(
sgl_config=model_config,
tp_size=tp_size,
rank=rank,
# NOTE: The original implementation accessed private buffers via
# `_kvcache.k_buffer` / `.v_buffer`. We prefer public accessors when
# available; fall back to private fields if needed.
k_pool=getattr(
kvcache,
"k_buffer",
getattr(self.token_to_kv_pool_allocator._kvcache, "k_buffer"),
),
v_pool=getattr(
kvcache,
"v_buffer",
getattr(self.token_to_kv_pool_allocator._kvcache, "v_buffer"),
),
tp_group=tp_group,
)
self.load_stream = torch.cuda.Stream()
self.store_stream = torch.cuda.Stream()
self.layer_done_executor = LayerTransferCounter(
num_layers=(
model_config.num_hidden_layers if model_config is not None else 0
),
load_stream=self.load_stream,
lmc_connector=self.lmcache_connector,
)
kvcache.register_layer_transfer_counter(self.layer_done_executor)
self._in_flight_nodes: list[TreeNode] = []
self._node_lock = threading.Lock()
def reset(self): # type: ignore[override]
super().reset()
if hasattr(self, "_in_flight_nodes"):
with self._node_lock:
self._in_flight_nodes.clear()
def match_prefix(self, key: RadixKey, **kwargs) -> MatchResult: # type: ignore[override]
"""Match cached prefix; if there's a tail miss, prefetch from LMCache.
Reuses the base matching logic to obtain (value, last_node). If there
remains a *page-aligned* uncached suffix and there is room (or after
eviction), we allocate token slots and trigger an async LMCache load
into those slots, then materialize a new child node for the retrieved
chunk.
"""
if self.disable or not key:
return super().match_prefix(key, **kwargs)
if self.page_size != 1:
aligned_len = len(key) // self.page_size * self.page_size
key = key[:aligned_len]
base_res = super().match_prefix(key, **kwargs)
value: torch.Tensor = base_res.device_indices
last_node: TreeNode = base_res.last_device_node
if value.numel() == len(key):
return base_res
uncached_len = len(key) - value.numel()
if uncached_len == 0:
return base_res
chunk_size = self.lmcache_connector.chunk_size()
prefix_pad = value.numel() % chunk_size
if self.token_to_kv_pool_allocator.available_size() < uncached_len:
self.evict(uncached_len)
token_slots = self.token_to_kv_pool_allocator.alloc(uncached_len)
if token_slots is None:
return base_res
slot_mapping = torch.cat(
[
torch.full((value.numel(),), -1, dtype=torch.int64, device=self.device),
token_slots.detach().clone().to(torch.int64).to(self.device),
]
)
with torch.cuda.stream(self.load_stream):
num_retrieved = self.lmcache_connector.start_load_kv(
LoadMetadata(
token_ids=key.token_ids, # full page-aligned key
slot_mapping=slot_mapping,
offset=value.numel() - prefix_pad, # LMCache offset convention
)
)
logger.debug("num_retrieved_tokens: %s", num_retrieved)
if num_retrieved > 0:
self.token_to_kv_pool_allocator.free(
token_slots[(num_retrieved - prefix_pad) :]
)
else:
self.token_to_kv_pool_allocator.free(token_slots)
if num_retrieved > 0:
fetched = num_retrieved - prefix_pad
new_node = TreeNode()
start = value.numel()
end = start + fetched
new_node.key = key[start:end]
new_node.value = token_slots[:fetched]
new_node.parent = last_node
last_node.children[self.get_child_key_fn(new_node.key)] = new_node
last_node = new_node
value = torch.cat([value, token_slots[:fetched]])
self.evictable_size_ += fetched
self._record_store_event(new_node.parent)
self._record_store_event(new_node)
return MatchResult(
device_indices=value,
last_device_node=last_node,
last_host_node=last_node,
)
return base_res
def cache_finished_req(self, req: "Req", is_insert: bool = True) -> None: # type: ignore[override]
"""On request completion, insert device KV into radix and store to LMCache."""
super().cache_finished_req(req, is_insert=is_insert)
if not is_insert:
return
token_ids = (req.origin_input_ids + req.output_ids)[:-1]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, : len(token_ids)
]
_, new_last_node, _, _ = self.match_prefix(RadixKey(token_ids, req.extra_key))
assert new_last_node is not None
self.inc_lock_ref(new_last_node)
store_md = StoreMetadata(
last_node=new_last_node,
token_ids=token_ids,
kv_indices=kv_indices,
offset=0,
)
with torch.cuda.stream(self.store_stream):
self.lmcache_connector.store_kv(store_md)
with self._node_lock:
self._in_flight_nodes.append(new_last_node)
def evict(self, num_tokens: int) -> None: # type: ignore[override]
"""Before base eviction, wait for any outstanding stores and release locks."""
if self.disable:
return
self.store_stream.synchronize()
with self._node_lock:
for node in self._in_flight_nodes:
self.dec_lock_ref(node)
self._in_flight_nodes.clear()
super().evict(num_tokens)
def pretty_print(self): # type: ignore[override]
super().pretty_print()
try:
logger.debug(
"evictable=%d protected=%d", self.evictable_size_, self.protected_size_
)
except Exception: # pragma: no cover
pass
if __name__ == "__main__":
cache = LMCRadixCache(
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
page_size=1,
disable=False,
enable_kv_cache_events=False,
model_config=None,
tp_size=1,
rank=0,
tp_group=None,
)
cache.insert(RadixKey([1, 2, 3]), torch.tensor([10, 11, 12], dtype=torch.int64))
cache.insert(
RadixKey([1, 2, 3, 4]), torch.tensor([10, 11, 12, 13], dtype=torch.int64)
)
cache.pretty_print()

Xet Storage Details

Size:
10 kB
·
Xet hash:
77d729a32b0cdef898d275069948fc732bac074c15810b51dc9fd7e035bd43cd

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.