Files
2026-07-13 12:24:33 +08:00

217 lines
7.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Platform-agnostic cache-context factory.
The concrete implementations live in their respective sub-packages:
* :class:`~lmcache.v1.platform.cuda.cache_context.GPUCacheContext` --
CUDA-backed.
* :class:`~lmcache.v1.platform.cpu.cache_context.CPUCacheContext` --
CPU-only fallback (POSIX-SHM-backed KV tensors).
:func:`create_cache_context` keeps the dispatch out of the call site
in :mod:`lmcache.v1.multiprocess.server`. Selection is data-driven:
each backend sub-package ships its own
:class:`~lmcache.v1.platform.base_cache_context.BaseCacheContext`
subclass under ``platform/<backend>/cache_context.py`` and declares
the ``torch.device.type`` it handles via the
:attr:`BaseCacheContext.device_type` ClassVar. The first
:func:`create_cache_context` call discovers those subclasses with
:func:`lmcache.v1.utils.subclass_discovery.discover_subclasses` and
memoises the resulting ``device_type -> class`` map. Adding a new
accelerator therefore requires *zero* edits to this module -- just
drop a new ``platform/<backend>/cache_context.py`` whose subclass
sets ``device_type``.
"""
# Future
from __future__ import annotations
# Standard
from collections.abc import Sequence
from typing import TYPE_CHECKING
# First Party
from lmcache.logging import init_logger
from lmcache.utils import EngineType
from lmcache.v1.gpu_connector.utils import LayoutHints
from lmcache.v1.multiprocess.custom_types import KVCache
from lmcache.v1.platform.base_cache_context import BaseCacheContext
from lmcache.v1.utils.subclass_discovery import discover_subclasses
if TYPE_CHECKING:
# First Party
from lmcache.v1.multiprocess.group_view import EngineGroupInfo
logger = init_logger(__name__)
# ``device_type -> BaseCacheContext`` subclass. Populated lazily on
# the first :func:`create_cache_context` call by scanning the
# ``platform`` package for ``cache_context`` leaf modules at depth 2
# (i.e. ``platform/<backend>/cache_context.py``). Tests substitute
# entries via :func:`snapshot_backends` / :func:`restore_backends`.
#
# The value type is the loose ``type`` (rather than
# ``type[BaseCacheContext]``) so callers can instantiate it with the
# concrete subclass' positional ``__init__`` signature without mypy
# resolving the abstract-base ``__init__`` instead.
_BACKENDS: dict[str, type] = {}
_BACKENDS_DISCOVERED: bool = False
def _discover_backends_once() -> None:
"""Populate :data:`_BACKENDS` on first use.
Walks ``lmcache.v1.platform`` two levels deep (``platform`` ->
``<backend>`` -> ``cache_context``) and indexes every concrete
:class:`BaseCacheContext` subclass by its ``device_type``
ClassVar. Subclasses with an empty ``device_type`` are skipped
with a warning so a missing override surfaces loudly instead of
silently shadowing a real backend.
"""
global _BACKENDS_DISCOVERED
if _BACKENDS_DISCOVERED:
return
# First Party
import lmcache.v1.platform as platform_pkg
for cls in discover_subclasses(
platform_pkg,
BaseCacheContext, # type: ignore[type-abstract]
module_filter=lambda short_name: short_name == "cache_context",
levels=[2, 2],
):
device_type = getattr(cls, "device_type", "")
if not device_type:
logger.warning(
"Skipping %s: empty device_type ClassVar; concrete "
"BaseCacheContext subclasses must override it.",
cls.__name__,
)
continue
existing = _BACKENDS.get(device_type)
if existing is not None and existing is not cls:
logger.warning(
"Multiple cache-context classes claim device_type=%r "
"(%s vs %s); keeping the first.",
device_type,
existing.__name__,
cls.__name__,
)
continue
_BACKENDS[device_type] = cls
_BACKENDS_DISCOVERED = True
def _resolve_backend(device_type: str) -> type:
_discover_backends_once()
cls = _BACKENDS.get(device_type)
if cls is None:
raise ValueError(
"No cache-context class registered for device type %r. "
"Make sure ``lmcache.v1.platform.<backend>.cache_context`` "
"ships a BaseCacheContext subclass with the matching "
"``device_type`` ClassVar." % device_type
)
return cls
def snapshot_backends() -> dict[str, type]:
"""Return a shallow copy of the backend table.
Pair with :func:`restore_backends` in test fixtures so installing
fakes for one test does not leak into the next.
"""
_discover_backends_once()
return dict(_BACKENDS)
def restore_backends(state: dict[str, type]) -> None:
"""Replace the backend table with *state*.
Marks the table as already-discovered so further calls do not
re-trigger filesystem scanning and overwrite the test's fakes.
"""
global _BACKENDS_DISCOVERED
_BACKENDS.clear()
_BACKENDS.update(state)
_BACKENDS_DISCOVERED = True
def _detect_device_type(kv_caches: KVCache) -> str:
"""Return the ``torch.device.type`` describing *kv_caches*.
All wrappers in *kv_caches* must materialize tensors on the same
device type; mixed-device batches are not supported by any
downstream cache-context implementation.
"""
device_types = {w.to_tensor().device.type for w in kv_caches}
if len(device_types) != 1:
raise ValueError(
"create_cache_context requires all kv_caches to share one "
"device type, got %r" % sorted(device_types)
)
return next(iter(device_types))
def create_cache_context(
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,
) -> BaseCacheContext:
"""Create the appropriate cache context for *kv_caches*.
The signature mirrors :class:`GPUCacheContext` so callers can
forward their kwargs verbatim and stay agnostic of the active
backend.
Selection is driven by ``tensor.device.type`` of *kv_caches*:
on first use the platform package is scanned for
``BaseCacheContext`` subclasses and the one whose
``device_type`` ClassVar matches is instantiated. ``"cuda"``,
``"cpu"``, future ``"xpu"`` ... all resolve through the same
code path -- no ``isinstance`` / ``if-elif`` chain.
Args:
kv_caches: KV cache tensor wrappers from the serving engine.
Must be non-empty.
lmcache_tokens_per_chunk: Number of tokens per LMCache chunk.
layout_hints: Optional hints for KV format detection.
Forwarded verbatim to the concrete context constructor.
engine_group_infos: Engine-neutral KV cache group metadata.
engine_type: Which serving engine produced the caches.
separate_object_groups: When True (default), split kernel groups into
one object group per sliding-window size; when False, a single
full-attention object group.
full_sw_kv: When True, sliding-window groups store/transfer full
per-chunk KV (no sub-chunk window cutting) so chunks stay valid for
reuse at any position; see
:meth:`KVLayerGroupsManager.enable_full_sw_kv`.
Returns:
A concrete cache context instance.
Raises:
ValueError: If *kv_caches* is empty, mixes device types, or
targets a device type with no registered backend.
"""
if not kv_caches:
raise ValueError("create_cache_context requires a non-empty kv_caches list")
device_type = _detect_device_type(kv_caches)
cls = _resolve_backend(device_type)
return cls(
kv_caches,
lmcache_tokens_per_chunk,
layout_hints,
engine_group_infos,
engine_type,
separate_object_groups,
full_sw_kv,
)