440 lines
17 KiB
Python
440 lines
17 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""CPU-only cache context for platforms without CUDA GPUs.
|
|
|
|
This module sits next to :mod:`lmcache.v1.platform.cuda.cache_context`
|
|
under :mod:`lmcache.v1.platform` and provides the same public API as
|
|
:class:`~lmcache.v1.platform.cuda.cache_context.GPUCacheContext` but
|
|
keeps all tensors on CPU. Stream / Event objects are provided by
|
|
:class:`~lmcache.v1.platform.cpu.stub_cpu_device.StubStream` so
|
|
CPU-only hosts never import ``cupy`` or instantiate a real CUDA
|
|
stream object.
|
|
|
|
The platform-agnostic dispatcher ``create_cache_context`` lives in
|
|
:mod:`lmcache.v1.platform.cache_context`.
|
|
"""
|
|
|
|
# Future
|
|
from __future__ import annotations
|
|
|
|
# Standard
|
|
from collections.abc import Sequence
|
|
from typing import TYPE_CHECKING
|
|
import os
|
|
|
|
# Third Party
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.logging import init_logger
|
|
from lmcache.utils import EngineType
|
|
from lmcache.v1.gpu_connector.utils import (
|
|
LayoutHints,
|
|
get_group_data_ptrs,
|
|
normalize_and_discover_per_layer_formats,
|
|
)
|
|
from lmcache.v1.kv_layer_groups import KVLayerGroupsManager
|
|
from lmcache.v1.multiprocess.custom_types import KVCache
|
|
from lmcache.v1.multiprocess.group_view import engine_group_layer_indices
|
|
from lmcache.v1.platform.base_cache_context import BaseCacheContext
|
|
from lmcache.v1.platform.cpu.stub_cpu_device import StubStream
|
|
|
|
if TYPE_CHECKING:
|
|
# First Party
|
|
from lmcache.v1.multiprocess.group_view import EngineGroupInfo
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
class CPUCacheContext(BaseCacheContext):
|
|
"""CPU-only cache context with the same public API as
|
|
:class:`GPUCacheContext`.
|
|
|
|
All tensors live on CPU. CUDA streams and cupy streams are
|
|
replaced by :class:`StubStream` no-op objects so callers can keep
|
|
using ``stream.synchronize()`` / ``wait_event(...)`` etc. without
|
|
branching on the active backend.
|
|
|
|
KV cache tensors are reconstructed from the
|
|
:class:`CpuShmTensorWrapper` instances sent by the client over
|
|
POSIX shared memory -- the server does **not** allocate the KV
|
|
cache itself. This mirrors the GPU-mode CUDA-IPC flow where the
|
|
client owns the buffers and the server only maps them.
|
|
"""
|
|
|
|
device_type = "cpu"
|
|
|
|
def __init__(
|
|
self,
|
|
kv_caches: KVCache,
|
|
lmcache_tokens_per_chunk: int = 256,
|
|
layout_hints: LayoutHints | None = None,
|
|
engine_group_infos: "Sequence[EngineGroupInfo]" = (),
|
|
engine_type: EngineType = EngineType.VLLM,
|
|
separate_object_groups: bool = True,
|
|
full_sw_kv: bool = False,
|
|
) -> None:
|
|
if not kv_caches:
|
|
raise ValueError(
|
|
"CPUCacheContext requires a non-empty list of "
|
|
"CpuShmTensorWrapper; the legacy server-side "
|
|
"self-allocation path has been removed."
|
|
)
|
|
|
|
# First Party
|
|
from lmcache.v1.platform.cuda.cache_context import (
|
|
unwrap_kv_cache_tensors,
|
|
)
|
|
|
|
unwrapped = unwrap_kv_cache_tensors(kv_caches)
|
|
self.device_ = torch.device("cpu")
|
|
|
|
# Discover layout & build KV layer groups via the same path
|
|
# GPUCacheContext uses, so we don't need to hand-roll any
|
|
# PageBufferShapeDesc here. ``layout_hints`` / ``engine_type``
|
|
# are forwarded so the signature matches GPUCacheContext.
|
|
(
|
|
kv_caches_normalized,
|
|
engine_kv_formats,
|
|
) = normalize_and_discover_per_layer_formats(
|
|
unwrapped,
|
|
engine_group_layer_indices(engine_group_infos),
|
|
engine_type,
|
|
layout_hints,
|
|
)
|
|
kv_caches_list: list[torch.Tensor] = list(kv_caches_normalized)
|
|
num_layers_val = len(engine_kv_formats)
|
|
kv_layer_groups_manager = KVLayerGroupsManager(
|
|
kv_caches_list,
|
|
engine_kv_formats=engine_kv_formats,
|
|
engine_group_infos=engine_group_infos,
|
|
lmcache_tokens_per_chunk=lmcache_tokens_per_chunk,
|
|
separate_object_groups=separate_object_groups,
|
|
)
|
|
# Set full_sw_kv before the temp buffer / object groups are sized so
|
|
# both use the full (un-windowed) per-chunk geometry (mirrors
|
|
# GPUCacheContext).
|
|
if full_sw_kv:
|
|
kv_layer_groups_manager.enable_full_sw_kv()
|
|
|
|
# Pre-allocated block IDs buffer (CPU).
|
|
_MAX_BLOCK_IDS = 1_000_000
|
|
block_ids_buffer = torch.empty(_MAX_BLOCK_IDS, dtype=torch.long)
|
|
|
|
super().__init__(
|
|
kv_caches=kv_caches_list,
|
|
device=self.device_,
|
|
num_layers=num_layers_val,
|
|
kv_layer_groups_manager=kv_layer_groups_manager,
|
|
block_ids_buffer=block_ids_buffer,
|
|
lmcache_tokens_per_chunk=lmcache_tokens_per_chunk,
|
|
)
|
|
|
|
# Per-group KV pointer tensors (CPU). Reuse the same helper
|
|
# GPUCacheContext relies on so the layout matches exactly.
|
|
self.group_kv_pointers_: list[torch.Tensor] = [
|
|
torch.tensor(
|
|
get_group_data_ptrs(
|
|
self.kv_caches_,
|
|
self.get_engine_kv_format(idx),
|
|
group.layer_indices,
|
|
),
|
|
dtype=torch.long,
|
|
)
|
|
for idx, group in enumerate(self.kv_layer_groups_manager_.kv_layer_groups)
|
|
]
|
|
|
|
self.kv_cache_pointers_ = torch.tensor(
|
|
[t.data_ptr() for t in self.kv_caches_], dtype=torch.long
|
|
)
|
|
|
|
# Temporary buffer for transfers (same layout as
|
|
# GPUCacheContext but on CPU).
|
|
self._max_batch_size = 4
|
|
self.tmp_chunk_group_offsets_: list[int] = [0]
|
|
for group_idx, group in enumerate(
|
|
self.kv_layer_groups_manager_.kv_layer_groups
|
|
):
|
|
shape = self.get_kv_buffer_shape(lmcache_tokens_per_chunk, group_idx)
|
|
byte_size = shape.numel() * group.dtype.itemsize
|
|
self.tmp_chunk_group_offsets_.append(
|
|
self.tmp_chunk_group_offsets_[-1] + byte_size
|
|
)
|
|
self.tmp_chunk_bytes_ = self.tmp_chunk_group_offsets_[-1]
|
|
# Buffer lives on CPU; keep the attribute name aligned with the
|
|
# context to avoid GPU-prefixed naming bleeding into a CPU-only
|
|
# class. The public ``get_tmp_gpu_buffer_flat`` method name is
|
|
# preserved so ``server.py`` can duck-type across backends.
|
|
self.tmp_cpu_buffer_ = torch.empty(
|
|
self.tmp_chunk_bytes_ * self.max_batch_size,
|
|
dtype=torch.uint8,
|
|
)
|
|
|
|
# Mock streams. ``StubStream`` already implements the small
|
|
# subset of the API server-side code uses (``synchronize``,
|
|
# ``wait_event``, ``record_event`` ...), so we never import
|
|
# cupy or instantiate a real CUDA stream object here.
|
|
self.cuda_stream_: StubStream = StubStream(device="cpu")
|
|
self.cupy_stream_: StubStream = self.cuda_stream_
|
|
self.high_priority_cuda_stream_: StubStream = StubStream(
|
|
device="cpu", priority=0
|
|
)
|
|
self.high_priority_cupy_stream_: StubStream = self.high_priority_cuda_stream_
|
|
|
|
# Sanity-check: warn if /dev/shm looks too small for the
|
|
# registered KV cache. Only meaningful on Linux where
|
|
# /dev/shm is the default tmpfs backing POSIX SHM.
|
|
self._check_shm_capacity()
|
|
|
|
logger.info(
|
|
"CPUCacheContext: %d layers, %d blocks, dtype=%s (shm-backed)",
|
|
self.num_layers_,
|
|
self.num_blocks,
|
|
self.kv_caches_[0].dtype,
|
|
)
|
|
|
|
# -- Internal helpers --
|
|
|
|
_SHM_PATH = "/dev/shm"
|
|
|
|
def _check_shm_capacity(self) -> None:
|
|
"""Warn if /dev/shm free space is smaller than the KV cache."""
|
|
if not os.path.isdir(self._SHM_PATH):
|
|
return
|
|
try:
|
|
st = os.statvfs(self._SHM_PATH)
|
|
except OSError:
|
|
return
|
|
free_bytes = st.f_bavail * st.f_frsize
|
|
kv_bytes = sum(t.numel() * t.element_size() for t in self.kv_caches_)
|
|
if kv_bytes > free_bytes:
|
|
logger.warning(
|
|
"Insufficient /dev/shm space for CPU KV cache: "
|
|
"need %d bytes but only %d bytes available. "
|
|
"Consider increasing the size of /dev/shm "
|
|
"(e.g. mount -o remount,size=<N>G /dev/shm).",
|
|
kv_bytes,
|
|
free_bytes,
|
|
)
|
|
|
|
def close(self) -> None:
|
|
"""Release resources. No-op for CPU context (no GDS staging buffer)."""
|
|
pass
|
|
|
|
# -- Properties (same API as GPUCacheContext) --
|
|
|
|
@property
|
|
def max_batch_size(self) -> int:
|
|
"""Returns the maximum number of concurrent batches."""
|
|
return self._max_batch_size
|
|
|
|
@property
|
|
def kv_pointers(self) -> torch.Tensor:
|
|
"""Returns a tensor of KV cache data pointers."""
|
|
return self.kv_cache_pointers_
|
|
|
|
@property
|
|
def stream(self) -> StubStream:
|
|
"""Returns the (mock) CUDA stream."""
|
|
return self.cuda_stream_
|
|
|
|
@property
|
|
def cupy_stream(self) -> StubStream:
|
|
"""Returns the (mock) external stream."""
|
|
return self.cupy_stream_
|
|
|
|
@property
|
|
def high_priority_stream(self) -> StubStream:
|
|
"""Returns the (mock) high-priority CUDA stream."""
|
|
return self.high_priority_cuda_stream_
|
|
|
|
@property
|
|
def high_priority_cupy_stream(self) -> StubStream:
|
|
"""Returns the (mock) high-priority external stream."""
|
|
return self.high_priority_cupy_stream_
|
|
|
|
@property
|
|
def block_size(self) -> int:
|
|
"""Returns the block size (tokens per block)."""
|
|
return self.kv_layer_groups_manager_.kv_layer_groups[0].shape_desc.bs
|
|
|
|
@property
|
|
def group_slots_per_blocks(self) -> list[int]:
|
|
"""Per-group physical slot count (``shape_desc.bs``) in group
|
|
order."""
|
|
return [
|
|
group.shape_desc.bs
|
|
for group in self.kv_layer_groups_manager_.kv_layer_groups
|
|
]
|
|
|
|
def blocks_for_tokens(self, num_logical_tokens: int, group_idx: int) -> int:
|
|
"""Number of blocks that span *num_logical_tokens* for a group.
|
|
|
|
Mirrors :meth:`GPUCacheContext.blocks_for_tokens`.
|
|
"""
|
|
group = self.kv_layer_groups_manager_.kv_layer_groups[group_idx]
|
|
physical_slots = (
|
|
num_logical_tokens * group.slots_per_block // group.tokens_per_block
|
|
)
|
|
return physical_slots // group.shape_desc.bs
|
|
|
|
def get_group_kv_pointers(self, group_idx: int) -> torch.Tensor:
|
|
"""Returns the KV cache pointer tensor for the given group."""
|
|
return self.group_kv_pointers_[group_idx]
|
|
|
|
def get_kernel_group_kv_pointers(self, kernel_group_idx: int) -> torch.Tensor:
|
|
"""Returns the KV pointer tensor for the given kernel group.
|
|
|
|
Mirrors :meth:`GPUCacheContext.get_kernel_group_kv_pointers`.
|
|
"""
|
|
return self.group_kv_pointers_[kernel_group_idx]
|
|
|
|
def get_kernel_group_shape_dtype(
|
|
self,
|
|
num_tokens: int,
|
|
kernel_group_idx: int,
|
|
) -> tuple[torch.Size, torch.dtype]:
|
|
"""Returns the shape and dtype for the given kernel group index and
|
|
number of tokens.
|
|
|
|
Mirrors :meth:`GPUCacheContext.get_kernel_group_shape_dtype` so
|
|
callers such as ``lmcache_driven_transfer.get_layout_desc`` can duck-type
|
|
across GPU and CPU backends.
|
|
|
|
Args:
|
|
num_tokens: Number of tokens.
|
|
kernel_group_idx: Index of the kernel group.
|
|
|
|
Returns:
|
|
A ``(shape, dtype)`` tuple for the given kernel group.
|
|
"""
|
|
group = self.kv_layer_groups_manager_.kv_layer_groups[kernel_group_idx]
|
|
compress_ratio = group.tokens_per_block // group.slots_per_block
|
|
if num_tokens % compress_ratio != 0:
|
|
raise ValueError(
|
|
"num_tokens (%d) is not a multiple of compress_ratio (%d) "
|
|
"for kernel_group_idx %d"
|
|
% (num_tokens, compress_ratio, kernel_group_idx)
|
|
)
|
|
num_slots = num_tokens // compress_ratio
|
|
sd = group.shape_desc
|
|
shape = torch.Size(
|
|
(sd.kv_size, group.num_layers, num_slots, group.hidden_dim_size)
|
|
)
|
|
return shape, group.dtype
|
|
|
|
def get_tmp_gpu_buffer_flat(self, chunk_idx: int) -> torch.Tensor:
|
|
"""Returns the flat uint8 temp buffer for the given chunk."""
|
|
if chunk_idx >= self.max_batch_size:
|
|
raise ValueError(
|
|
"chunk_idx %d >= max_batch_size %d" % (chunk_idx, self.max_batch_size)
|
|
)
|
|
start = chunk_idx * self.tmp_chunk_bytes_
|
|
return self.tmp_cpu_buffer_[start : start + self.tmp_chunk_bytes_]
|
|
|
|
def get_temp_kernel_group_buffer(
|
|
self, batch_idx: int, kernel_group_idx: int
|
|
) -> torch.Tensor:
|
|
"""Returns the typed temp buffer for the given batch and kernel group.
|
|
|
|
Mirrors :meth:`GPUCacheContext.get_temp_kernel_group_buffer`.
|
|
|
|
Args:
|
|
batch_idx: Batch slot index (0 <= batch_idx < max_batch_size).
|
|
kernel_group_idx: Index of the kernel group.
|
|
|
|
Returns:
|
|
A typed tensor view with the correct shape and dtype.
|
|
"""
|
|
if batch_idx >= self.max_batch_size:
|
|
raise ValueError(
|
|
"batch_idx %d >= max_batch_size %d" % (batch_idx, self.max_batch_size)
|
|
)
|
|
group = self.kv_layer_groups_manager_.kv_layer_groups[kernel_group_idx]
|
|
shape = self.get_kv_buffer_shape(
|
|
self.lmcache_tokens_per_chunk, kernel_group_idx
|
|
)
|
|
g_start = self.tmp_chunk_group_offsets_[kernel_group_idx]
|
|
g_end = self.tmp_chunk_group_offsets_[kernel_group_idx + 1]
|
|
chunk = self.tmp_chunk_bytes_
|
|
return (
|
|
self.tmp_cpu_buffer_[
|
|
batch_idx * chunk + g_start : batch_idx * chunk + g_end
|
|
]
|
|
.view(group.dtype)
|
|
.view(shape)
|
|
)
|
|
|
|
def get_temp_object_group_buffer(
|
|
self, batch_idx: int, object_group_idx: int
|
|
) -> torch.Tensor:
|
|
"""Returns the flat uint8 temp buffer for the given batch and object
|
|
group.
|
|
|
|
Mirrors :meth:`GPUCacheContext.get_temp_object_group_buffer`.
|
|
|
|
Args:
|
|
batch_idx: Batch slot index (0 <= batch_idx < max_batch_size).
|
|
object_group_idx: Index of the object group.
|
|
|
|
Returns:
|
|
A flat uint8 tensor view covering the object group's byte range.
|
|
"""
|
|
if batch_idx >= self.max_batch_size:
|
|
raise ValueError(
|
|
"batch_idx %d >= max_batch_size %d" % (batch_idx, self.max_batch_size)
|
|
)
|
|
manager = self.kv_layer_groups_manager_
|
|
object_group = manager.object_groups[object_group_idx]
|
|
kg_indices = object_group.kernel_group_indices
|
|
# Object group spans from the first to the last kernel group's range.
|
|
g_start = self.tmp_chunk_group_offsets_[kg_indices[0]]
|
|
g_end = self.tmp_chunk_group_offsets_[kg_indices[-1] + 1]
|
|
chunk = self.tmp_chunk_bytes_
|
|
return self.tmp_cpu_buffer_[
|
|
batch_idx * chunk + g_start : batch_idx * chunk + g_end
|
|
]
|
|
|
|
def get_tmp_chunk_gpu_buffer(self, group_idx: int = 0) -> torch.Tensor:
|
|
"""Returns a typed view of the temp buffer for one chunk."""
|
|
group = self.kv_layer_groups_manager_.kv_layer_groups[group_idx]
|
|
shape = self.get_kv_buffer_shape(self.lmcache_tokens_per_chunk, group_idx)
|
|
start = self.tmp_chunk_group_offsets_[group_idx]
|
|
end = self.tmp_chunk_group_offsets_[group_idx + 1]
|
|
return self.tmp_cpu_buffer_[start:end].view(group.dtype).view(shape)
|
|
|
|
def get_tmp_chunk_gpu_buffer_batched(
|
|
self, batch_size: int, group_idx: int = 0
|
|
) -> list[torch.Tensor]:
|
|
"""Returns a list of non-overlapping temp buffer views."""
|
|
if batch_size > self.max_batch_size:
|
|
raise ValueError(
|
|
"batch_size %d > max_batch_size %d" % (batch_size, self.max_batch_size)
|
|
)
|
|
group = self.kv_layer_groups_manager_.kv_layer_groups[group_idx]
|
|
shape = self.get_kv_buffer_shape(self.lmcache_tokens_per_chunk, group_idx)
|
|
g_start = self.tmp_chunk_group_offsets_[group_idx]
|
|
g_end = self.tmp_chunk_group_offsets_[group_idx + 1]
|
|
chunk = self.tmp_chunk_bytes_
|
|
return [
|
|
self.tmp_cpu_buffer_[i * chunk + g_start : i * chunk + g_end]
|
|
.view(group.dtype)
|
|
.view(shape)
|
|
for i in range(batch_size)
|
|
]
|
|
|
|
def cache_size_per_token(self) -> int:
|
|
"""Returns cache size per *logical* token in bytes,
|
|
summed across all groups.
|
|
|
|
Mirrors :meth:`GPUCacheContext.cache_size_per_token`.
|
|
"""
|
|
total = 0
|
|
for group_idx, group in enumerate(
|
|
self.kv_layer_groups_manager_.kv_layer_groups
|
|
):
|
|
compress_ratio = group.tokens_per_block // group.slots_per_block
|
|
numels = self.get_kv_buffer_shape(compress_ratio, group_idx).numel()
|
|
slot_bytes = numels * group.dtype.itemsize
|
|
total += slot_bytes // compress_ratio
|
|
return total
|