chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# LMCache Connector for SGLang
|
||||
|
||||
This document describes how to use LMCache as KV Cache Management Backend for SGLang engine.
|
||||
For more details about LMCache, please refer to: https://lmcache.ai
|
||||
|
||||
## Install LMCache
|
||||
|
||||
### Method 1: with pip
|
||||
|
||||
```bash
|
||||
pip install lmcache
|
||||
```
|
||||
|
||||
### Method 2: from source
|
||||
|
||||
Clone LMCache project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/LMCache/LMCache
|
||||
```
|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
cd LMCache
|
||||
pip install -e . --no-build-isolation
|
||||
```
|
||||
|
||||
|
||||
## Use LMCache
|
||||
|
||||
LMCache supports two transport modes. **MP (multi-process, default)** issues a single blocking retrieve over ZMQ to a standalone daemon that owns the KV store and survives SGLang restarts. **IP (in-process)** uses an embedded layerwise connector — the cache lives and dies with the SGLang process. Mode selection is currently a code-level setting in `LMCRadixCache.__init__` (`self._mode`); only MP is reachable by default.
|
||||
|
||||
### MP mode (default): multi-process daemon
|
||||
|
||||
Uses `LMCacheMPConnector`. Daemon host/port come from the LMCache YAML config (`mp_host`, `mp_port`).
|
||||
|
||||
Terminal 1 — start the LMCache daemon:
|
||||
|
||||
```bash
|
||||
lmcache server \
|
||||
--host 127.0.0.1 --port 5556 \
|
||||
--l1-size-gb 4 \
|
||||
--eviction-policy LRU
|
||||
```
|
||||
|
||||
Use the bundled `example_config_mp.yaml` (or any YAML setting `mp_host` / `mp_port`):
|
||||
|
||||
Terminal 2 — start SGLang:
|
||||
|
||||
```bash
|
||||
python -m sglang.launch_server \
|
||||
--model-path MODEL \
|
||||
--enable-lmcache \
|
||||
--lmcache-config-file example_config_mp.yaml
|
||||
```
|
||||
|
||||
For full LMCache config options see https://docs.lmcache.ai/api_reference/configurations.html.
|
||||
|
||||
### IP mode: in-process
|
||||
|
||||
Uses `LMCacheLayerwiseConnector`. KV transfer happens per layer inside the SGLang process; the cache lives and dies with the server. To enable, edit `LMCRadixCache.__init__` and set `self._mode = LMCacheMode.IP`.
|
||||
|
||||
The LMCache config still controls chunk_size and storage; `mp_host` / `mp_port` are ignored on this path. Use the bundled `example_config_ip.yaml`:
|
||||
|
||||
```bash
|
||||
python -m sglang.launch_server \
|
||||
--model-path MODEL \
|
||||
--enable-lmcache \
|
||||
--lmcache-config-file example_config_ip.yaml
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Basic configurations
|
||||
chunk_size: 256
|
||||
|
||||
# CPU offloading configurations
|
||||
local_cpu: true
|
||||
use_layerwise: true
|
||||
max_local_cpu_size: 10 # number of CPU backend GB
|
||||
@@ -0,0 +1,3 @@
|
||||
# MP mode: SGLang dials the standalone `lmcache server` at this host/port.
|
||||
mp_host: 127.0.0.1
|
||||
mp_port: 5556
|
||||
@@ -0,0 +1,503 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
EvictParams,
|
||||
EvictResult,
|
||||
InitLoadBackParams,
|
||||
MatchPrefixParams,
|
||||
MatchResult,
|
||||
)
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
|
||||
from sglang.srt.runtime_context import get_server_args
|
||||
|
||||
try:
|
||||
from lmcache.integration.sglang.multi_process_adapter import LMCacheMPConnector
|
||||
from lmcache.integration.sglang.sglang_adapter import (
|
||||
LMCacheLayerwiseConnector,
|
||||
LoadMetadata,
|
||||
StoreMetadata,
|
||||
)
|
||||
from lmcache.integration.sglang.utils import lmcache_get_config
|
||||
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
|
||||
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _LMCacheLoadBackMarker:
|
||||
"""Carries the data ``init_load_back`` needs from the
|
||||
``match_prefix`` call in MP mode.
|
||||
"""
|
||||
|
||||
key: RadixKey # detached snapshot of the matched key (the live query key
|
||||
# aliases the req's growing fill_ids and must not be retained)
|
||||
value_numel: int # number of tokens already in radix at match time
|
||||
|
||||
|
||||
class LMCacheMode(enum.Enum):
|
||||
MP = enum.auto() # multi-process mode
|
||||
IP = enum.auto() # in-process mode
|
||||
|
||||
|
||||
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.
|
||||
|
||||
IP mode keeps the existing layerwise connector and
|
||||
its per-layer transfer hook: ``match_prefix`` kicks off the load via
|
||||
``start_load_kv`` and SGLang's per-layer KV-pool hook drives subsequent
|
||||
layers during forward.
|
||||
|
||||
MP mode uses ``LMCacheMPConnector`` with a two-phase
|
||||
load: ``match_prefix`` fires LOOKUP only (``connector.lookup_kv``) and
|
||||
returns ``host_hit_length`` on the ``MatchResult``; the SGLang
|
||||
scheduler then calls `init_load_back` at dispatch time,
|
||||
which fires the actual RETRIEVE (``connector.retrieve_kv``) into
|
||||
pre-allocated GPU slots.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: CacheInitParams,
|
||||
model_config: Optional[ModelConfig] = None,
|
||||
tp_size: int = 1,
|
||||
rank: int = 0,
|
||||
tp_group: Optional[torch.distributed.ProcessGroup] = None,
|
||||
):
|
||||
super().__init__(params)
|
||||
|
||||
cli_lmc_cfg = get_server_args().lmcache_config_file or ""
|
||||
|
||||
kvcache = self.token_to_kv_pool_allocator.get_kvcache()
|
||||
connector_kwargs = dict(
|
||||
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.device_group if tp_group is not None else None,
|
||||
)
|
||||
|
||||
self.load_stream = torch.cuda.Stream()
|
||||
self.store_stream = torch.cuda.Stream()
|
||||
|
||||
# MP is the default. To use the in-process layerwise connector,
|
||||
# set ``self._mode = LMCacheMode.IP`` here.
|
||||
self._mode = LMCacheMode.MP
|
||||
if self._mode is LMCacheMode.MP:
|
||||
if not cli_lmc_cfg:
|
||||
raise ValueError(
|
||||
"MP mode requires --lmcache-config-file (the YAML "
|
||||
"supplies mp_host / mp_port)."
|
||||
)
|
||||
lm_cfg = lmcache_get_config(cli_lmc_cfg)
|
||||
self.lmcache_connector = LMCacheMPConnector(
|
||||
page_size=params.page_size,
|
||||
host=lm_cfg.mp_host,
|
||||
port=lm_cfg.mp_port,
|
||||
**connector_kwargs,
|
||||
)
|
||||
elif self._mode is LMCacheMode.IP:
|
||||
self.lmcache_connector = LMCacheLayerwiseConnector(
|
||||
config_file=cli_lmc_cfg, **connector_kwargs
|
||||
)
|
||||
# Per-layer hook
|
||||
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()
|
||||
self._mp_load_back_markers: dict[str, _LMCacheLoadBackMarker] = {}
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
if hasattr(self, "_in_flight_nodes"):
|
||||
with self._node_lock:
|
||||
self._in_flight_nodes.clear()
|
||||
if hasattr(self, "_mp_load_back_markers"):
|
||||
self._mp_load_back_markers.clear()
|
||||
|
||||
def match_prefix(self, params: MatchPrefixParams) -> MatchResult:
|
||||
"""Dispatch to the mode-specific match_prefix.
|
||||
|
||||
MP mode → ``_mp_match_prefix`` (fires LOOKUP only).
|
||||
IP mode → ``_ip_match_prefix`` (single-shot ``start_load_kv``
|
||||
plus per-layer hook).
|
||||
"""
|
||||
key = params.key
|
||||
if self.disable or not key:
|
||||
return super().match_prefix(params)
|
||||
|
||||
if self.page_size != 1:
|
||||
aligned_len = len(key) // self.page_size * self.page_size
|
||||
key = key[:aligned_len]
|
||||
|
||||
base_res = super().match_prefix(params)
|
||||
value: torch.Tensor = base_res.device_indices
|
||||
last_node: TreeNode = base_res.last_device_node
|
||||
|
||||
if self._mode is LMCacheMode.MP:
|
||||
if params.req is None:
|
||||
return base_res
|
||||
return self._mp_match_prefix(key, base_res, value, last_node, params.req)
|
||||
elif self._mode is LMCacheMode.IP:
|
||||
return self._ip_match_prefix(key, base_res, value, last_node)
|
||||
return base_res
|
||||
|
||||
def _mp_match_prefix(
|
||||
self,
|
||||
key: RadixKey,
|
||||
base_res: MatchResult,
|
||||
value: torch.Tensor,
|
||||
last_node: TreeNode,
|
||||
req: Req,
|
||||
) -> MatchResult:
|
||||
"""MP LOOKUP
|
||||
|
||||
Returns a ``MatchResult`` with ``host_hit_length`` set when
|
||||
LMCache has tokens beyond radix. Otherwise releases
|
||||
the held read locks and returns the radix-only result.
|
||||
"""
|
||||
token_ids = key.raw_token_ids()
|
||||
matched = self.lmcache_connector.lookup_kv(token_ids, req.rid)
|
||||
if matched <= value.numel():
|
||||
# Release the read locks; keep the pending session for end_session.
|
||||
self.lmcache_connector.release_pending(req.rid)
|
||||
return base_res
|
||||
|
||||
if token_ids is key.token_ids:
|
||||
token_ids = token_ids[:]
|
||||
self._mp_load_back_markers[req.rid] = _LMCacheLoadBackMarker(
|
||||
key=RadixKey(token_ids, key.extra_key, key.is_bigram),
|
||||
value_numel=int(value.numel()),
|
||||
)
|
||||
return MatchResult(
|
||||
device_indices=value,
|
||||
last_device_node=last_node,
|
||||
last_host_node=last_node,
|
||||
best_match_node=last_node,
|
||||
host_hit_length=matched - int(value.numel()),
|
||||
)
|
||||
|
||||
def _ip_match_prefix(
|
||||
self,
|
||||
key: RadixKey,
|
||||
base_res: MatchResult,
|
||||
value: torch.Tensor,
|
||||
last_node: TreeNode,
|
||||
) -> MatchResult:
|
||||
"""IP mode: ``start_load_kv`` + per-layer hook.
|
||||
|
||||
Allocates slots for the page-aligned uncached tail and kicks off
|
||||
the layerwise load. Returns ``base_res`` if there's nothing to
|
||||
fetch or alloc/load fails.
|
||||
"""
|
||||
if value.numel() == len(key):
|
||||
return base_res
|
||||
|
||||
uncached_len = len(key) - value.numel()
|
||||
if uncached_len == 0:
|
||||
return base_res
|
||||
|
||||
token_ids = key.raw_token_ids()
|
||||
result = self._load_back(
|
||||
key=key,
|
||||
value_numel=int(value.numel()),
|
||||
uncached_len=uncached_len,
|
||||
last_node=last_node,
|
||||
load_fn=lambda sm, pp: self._ip_load_back(
|
||||
token_ids=token_ids,
|
||||
value_numel=int(value.numel()),
|
||||
slot_mapping=sm,
|
||||
prefix_pad=pp,
|
||||
),
|
||||
)
|
||||
if result is None:
|
||||
return base_res
|
||||
new_slots, new_node = result
|
||||
return MatchResult(
|
||||
device_indices=torch.cat([value, new_slots]),
|
||||
last_device_node=new_node,
|
||||
last_host_node=new_node,
|
||||
best_match_node=new_node,
|
||||
)
|
||||
|
||||
def init_load_back(
|
||||
self, params: InitLoadBackParams
|
||||
) -> Tuple[torch.Tensor, Optional[TreeNode]]:
|
||||
"""MP RETRIEVE.
|
||||
|
||||
Called by the scheduler when ``match_prefix`` returned
|
||||
``host_hit_length > 0``. Uses the cached LOOKUP result to
|
||||
allocate slots and fire RETRIEVE, inserts the resulting
|
||||
TreeNode into the radix tree, and returns
|
||||
``(new_indices, new_last_node)``.
|
||||
"""
|
||||
req = params.req
|
||||
marker = self._mp_load_back_markers.pop(req.rid)
|
||||
last_node: TreeNode = params.best_match_node
|
||||
|
||||
result = self._load_back(
|
||||
key=marker.key,
|
||||
value_numel=marker.value_numel,
|
||||
uncached_len=params.host_hit_length,
|
||||
last_node=last_node,
|
||||
load_fn=lambda sm, pp: self._mp_load_back(
|
||||
marker=marker,
|
||||
request_id=req.rid,
|
||||
slot_mapping=sm,
|
||||
prefix_pad=pp,
|
||||
),
|
||||
)
|
||||
if result is None:
|
||||
# Either alloc failed (locks still held by lookup_kv) or
|
||||
# retrieve returned nothing (locks already released by
|
||||
# retrieve_kv). release_pending is idempotent on locks_held.
|
||||
self.lmcache_connector.release_pending(req.rid)
|
||||
return (
|
||||
torch.empty((0,), dtype=torch.int64, device=self.device),
|
||||
last_node,
|
||||
)
|
||||
return result
|
||||
|
||||
def _load_back(
|
||||
self,
|
||||
*,
|
||||
key: RadixKey,
|
||||
value_numel: int,
|
||||
uncached_len: int,
|
||||
last_node: TreeNode,
|
||||
load_fn, # Callable[[torch.Tensor, int], int] — (slot_mapping, prefix_pad) -> num_retrieved
|
||||
) -> Optional[Tuple[torch.Tensor, TreeNode]]:
|
||||
"""Alloc slots, run ``load_fn``, attach a TreeNode for what was loaded.
|
||||
|
||||
Returns ``(slots, new_node)`` on success, ``None`` if alloc fails
|
||||
or the load returned zero (slots are freed in either case).
|
||||
"""
|
||||
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(EvictParams(num_tokens=uncached_len))
|
||||
|
||||
token_slots = self.token_to_kv_pool_allocator.alloc(uncached_len)
|
||||
if token_slots is None:
|
||||
return None
|
||||
|
||||
slot_mapping = torch.empty(
|
||||
value_numel + token_slots.numel(),
|
||||
dtype=torch.int64,
|
||||
device=self.device,
|
||||
)
|
||||
slot_mapping[:value_numel].fill_(-1)
|
||||
slot_mapping[value_numel:].copy_(token_slots)
|
||||
|
||||
num_retrieved = load_fn(slot_mapping, prefix_pad)
|
||||
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(priority=last_node.priority)
|
||||
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[new_node.key.child_key(self.page_size)] = new_node
|
||||
self.evictable_size_ += fetched
|
||||
self._update_leaf_status(last_node)
|
||||
self._update_leaf_status(new_node)
|
||||
|
||||
self._record_store_event(new_node.parent)
|
||||
self._record_store_event(new_node)
|
||||
|
||||
return token_slots[:fetched], new_node
|
||||
|
||||
return None
|
||||
|
||||
def _mp_load_back(
|
||||
self,
|
||||
*,
|
||||
marker: _LMCacheLoadBackMarker,
|
||||
request_id: str,
|
||||
slot_mapping: torch.Tensor,
|
||||
prefix_pad: int,
|
||||
) -> int:
|
||||
"""MP non-layerwise loader: fire ``retrieve_kv`` and wait for the
|
||||
load_stream so the compute stream observes the writes.
|
||||
"""
|
||||
self.load_stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(self.load_stream):
|
||||
n = self.lmcache_connector.retrieve_kv(
|
||||
LoadMetadata(
|
||||
token_ids=marker.key.token_ids,
|
||||
slot_mapping=slot_mapping,
|
||||
offset=marker.value_numel - prefix_pad,
|
||||
prefix_pad=prefix_pad,
|
||||
request_id=request_id,
|
||||
)
|
||||
)
|
||||
torch.cuda.current_stream().wait_stream(self.load_stream)
|
||||
return n
|
||||
|
||||
def _ip_load_back(
|
||||
self,
|
||||
*,
|
||||
token_ids: list[int],
|
||||
value_numel: int,
|
||||
slot_mapping: torch.Tensor,
|
||||
prefix_pad: int,
|
||||
) -> int:
|
||||
"""IP layerwise loader: kick off ``start_load_kv`` on ``self.load_stream``.
|
||||
|
||||
``start_load_kv`` enqueues the first layer's transfer; the
|
||||
``LayerTransferCounter`` hook drives the rest during forward.
|
||||
"""
|
||||
with torch.cuda.stream(self.load_stream):
|
||||
return self.lmcache_connector.start_load_kv(
|
||||
LoadMetadata(
|
||||
token_ids=token_ids,
|
||||
slot_mapping=slot_mapping,
|
||||
offset=value_numel - prefix_pad,
|
||||
)
|
||||
)
|
||||
|
||||
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
|
||||
"""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:
|
||||
if self._mode is LMCacheMode.MP:
|
||||
self._mp_load_back_markers.pop(req.rid, None)
|
||||
self.lmcache_connector.end_session(req.rid)
|
||||
return
|
||||
|
||||
global_server_args = get_server_args()
|
||||
topk = global_server_args.speculative_eagle_topk
|
||||
enable_kv_committed_len = topk is None or topk == 1
|
||||
if enable_kv_committed_len:
|
||||
kv_committed_len = req.kv_committed_len
|
||||
else:
|
||||
kv_committed_len = len(req.origin_input_ids) + max(
|
||||
len(req.output_ids) - 1, 0
|
||||
)
|
||||
|
||||
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
|
||||
kv_indices = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, :kv_committed_len
|
||||
]
|
||||
|
||||
# Use super() to avoid a redundant LOOKUP — we only need new_last_node from radix.
|
||||
match_result = super().match_prefix(
|
||||
MatchPrefixParams(key=RadixKey(token_ids, req.extra_key))
|
||||
)
|
||||
new_last_node = match_result.last_device_node
|
||||
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,
|
||||
request_id=req.rid,
|
||||
)
|
||||
with torch.cuda.stream(self.store_stream):
|
||||
self.lmcache_connector.store_kv(store_md)
|
||||
if self._mode is LMCacheMode.MP:
|
||||
# MP store_kv blocks until the daemon's signal event fires, so the slots are safe to evict immediately.
|
||||
self._mp_load_back_markers.pop(req.rid, None)
|
||||
self.dec_lock_ref(new_last_node)
|
||||
self.lmcache_connector.end_session(req.rid)
|
||||
elif self._mode is LMCacheMode.IP:
|
||||
# Layerwise store is async on store_stream; defer the unlock to evict()'s store_stream.synchronize().
|
||||
with self._node_lock:
|
||||
self._in_flight_nodes.append(new_last_node)
|
||||
|
||||
def evict(self, params: EvictParams) -> EvictResult:
|
||||
"""Before base eviction, wait for any outstanding stores and release locks."""
|
||||
if self.disable:
|
||||
return EvictResult()
|
||||
|
||||
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()
|
||||
|
||||
return super().evict(params)
|
||||
|
||||
def pretty_print(self):
|
||||
super().pretty_print()
|
||||
try:
|
||||
logger.debug(
|
||||
"evictable=%d protected=%d", self.evictable_size_, self.protected_size_
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
@@ -0,0 +1,118 @@
|
||||
try:
|
||||
from lmcache.integration.sglang.sglang_adapter import (
|
||||
LMCacheLayerwiseConnector,
|
||||
LoadMetadata,
|
||||
StoreMetadata,
|
||||
)
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"LMCache is not installed. Please install it by running `pip install lmcache` in the root directory of LMCache"
|
||||
)
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.configs.model_config import ModelConfig
|
||||
|
||||
|
||||
def test_load_store_metadata():
|
||||
model_config = ModelConfig(
|
||||
model_path="Qwen/Qwen3-4B",
|
||||
)
|
||||
|
||||
# Generate Dummy KV Cache
|
||||
head_num = model_config.num_key_value_heads
|
||||
head_dim = model_config.head_dim
|
||||
layer_num = model_config.num_hidden_layers
|
||||
buffer_size = 256
|
||||
input_id_len = 16
|
||||
|
||||
k_buffer = [
|
||||
torch.randn(buffer_size, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
v_buffer = [
|
||||
torch.randn(buffer_size, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
|
||||
connector = LMCacheLayerwiseConnector(
|
||||
model_config, 1, 0, k_buffer, v_buffer, config_file="example_config_ip.yaml"
|
||||
)
|
||||
|
||||
fake_token_ids = torch.randint(0, model_config.vocab_size, (input_id_len,)).tolist()
|
||||
fake_kv_indices = torch.randint(0, buffer_size, (input_id_len,))
|
||||
offset = 0
|
||||
|
||||
store_metadata = StoreMetadata(
|
||||
last_node=None,
|
||||
token_ids=fake_token_ids,
|
||||
kv_indices=fake_kv_indices,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
load_metadata = LoadMetadata(
|
||||
token_ids=fake_token_ids,
|
||||
slot_mapping=fake_kv_indices,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
current_stream = torch.cuda.current_stream()
|
||||
|
||||
retrieve_token_num = connector.start_load_kv(load_metadata)
|
||||
assert retrieve_token_num == 0
|
||||
|
||||
connector.store_kv(store_metadata)
|
||||
current_stream.synchronize()
|
||||
|
||||
# check retrieve
|
||||
gt_key_buffer = [
|
||||
torch.zeros(input_id_len, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
gt_value_buffer = [
|
||||
torch.zeros(input_id_len, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
|
||||
for i in range(layer_num):
|
||||
gt_key_buffer[i] = k_buffer[i][fake_kv_indices]
|
||||
gt_value_buffer[i] = v_buffer[i][fake_kv_indices]
|
||||
|
||||
# clear the k_buffer and v_buffer
|
||||
for _ in range(layer_num):
|
||||
k_buffer[i].zero_()
|
||||
v_buffer[i].zero_()
|
||||
|
||||
retrieve_token_num = connector.start_load_kv(load_metadata)
|
||||
assert retrieve_token_num == input_id_len
|
||||
|
||||
for i in range(layer_num):
|
||||
current_stream.synchronize()
|
||||
connector.load_kv_layerwise(i)
|
||||
|
||||
current_stream.synchronize()
|
||||
test_key_buffer = [
|
||||
torch.zeros(input_id_len, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
test_value_buffer = [
|
||||
torch.zeros(input_id_len, head_num, head_dim, dtype=torch.bfloat16).cuda()
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
|
||||
for i in range(layer_num):
|
||||
test_key_buffer[i] = k_buffer[i][fake_kv_indices]
|
||||
test_value_buffer[i] = v_buffer[i][fake_kv_indices]
|
||||
|
||||
for i in range(layer_num):
|
||||
assert torch.allclose(test_key_buffer[i], gt_key_buffer[i])
|
||||
assert torch.allclose(test_value_buffer[i], gt_value_buffer[i])
|
||||
|
||||
print("================================================")
|
||||
print("TEST_LOAD_STORE_METADATA PASSED!")
|
||||
print("================================================")
|
||||
connector.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_load_store_metadata()
|
||||
Reference in New Issue
Block a user