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

161 lines
5.7 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Base class for device IPC wrappers.
:class:`DeviceIPCWrapper` is the abstract base for KV-cache IPC wrapper
implementations. Every concrete wrapper (e.g. :class:`~.cpu.shm.CpuShmTensorWrapper`,
:class:`~.cuda.ipc_wrapper.CudaIPCWrapper`) subclasses it so they share
the single msgspec ext code (1) -- pickle preserves the concrete
subclass identity across the wire so ``to_tensor`` dispatches correctly
on the receiving side.
"""
# Future
from __future__ import annotations
# Standard
from typing import Any, Tuple
import pickle
import threading
# Third Party
import torch
# First Party
from lmcache import torch_dev
class DeviceIPCWrapper:
"""Base class for KV-cache IPC wrapper.
Holds the device-agnostic mechanism shared by all transports: the
interface fields (``dtype``/``shape``/``stride``/``storage_offset``/
``device_uuid``), UUID<->ordinal discovery via the ``torch_dev``
abstraction, equality, and pickle-based (de)serialization.
Every wire-level wrapper subclasses this so they share the single
msgspec ext code (1) registered for ``DeviceIPCWrapper``: pickle
preserves the concrete subclass identity across the wire so
``to_tensor`` dispatches correctly on the receiving side.
Subclasses implement ``__init__`` (populate the interface fields from a
tensor) and ``to_tensor`` (reconstruct the tensor from the handle).
Concrete subclasses set ``_is_default_wrapper = True`` (a ``ClassVar``)
to mark themselves as the default factory for their ``device_type``;
auto-discovery in :mod:`lmcache.v1.platform._registry` reads it via
``getattr(cls, "_is_default_wrapper", False)`` so the attribute is
intentionally not declared on the base class.
"""
# Interface fields populated by each concrete subclass's
# ``__init__``. Declared here so the base-class ``__eq__`` (and
# type-checkers) can see them; ``handle`` is intentionally typed as
# ``Any`` because each backend stores a different opaque payload
# (``tuple`` for CUDA shared-storage IPC, ``None`` for the SHM /
# raw-CUDA paths that override ``to_tensor``).
handle: Any
dtype: torch.dtype
shape: Tuple[int, ...]
stride: Tuple[int, ...]
storage_offset: int
device_uuid: str
_discovered_device_mapping: dict[str, int] = {}
_device_mapping_lock = threading.Lock()
@classmethod
def _get_device_uuid(cls, device_index: int) -> str:
"""Get the UUID of a device given its index."""
return str(torch_dev.get_device_properties(device_index).uuid)
@classmethod
def _discover_devices(cls) -> None:
"""Discover all available accelerator devices and map their UUIDs
to the physical device ordinals.
"""
if not torch_dev.is_available():
return
num_devices = torch_dev.device_count()
with DeviceIPCWrapper._device_mapping_lock:
if DeviceIPCWrapper._discovered_device_mapping:
return # Already discovered
for i in range(num_devices):
device_uuid = cls._get_device_uuid(i)
DeviceIPCWrapper._discovered_device_mapping[device_uuid] = i
@classmethod
def _get_device_index_from_uuid(cls, device_uuid: str) -> int:
"""Get the physical device ordinal from its UUID."""
cls._discover_devices()
with DeviceIPCWrapper._device_mapping_lock:
device_index = DeviceIPCWrapper._discovered_device_mapping.get(
device_uuid, None
)
if device_index is None:
raise RuntimeError(
f"Device UUID {device_uuid} not found in the discovered "
"devices. Please make sure the process can see all the "
"accelerator devices"
)
return device_index
def to_tensor(self) -> torch.Tensor:
"""Reconstruct the tensor in this process from the IPC handle.
Subclasses implement the transport-specific reconstruction.
"""
raise NotImplementedError
def __eq__(self, other: object) -> bool:
# ``isinstance`` first so type-checkers can narrow ``other`` to
# ``DeviceIPCWrapper`` before we touch its attributes; the
# exact-type check that follows then enforces that, e.g., a
# ``CudaIPCWrapper`` is never considered equal to a
# ``RawCudaIPCWrapper`` even though they share a base class.
if not isinstance(other, DeviceIPCWrapper):
return False
if type(self) is not type(other):
return False
return (
self.handle == other.handle
and self.dtype == other.dtype
and self.shape == other.shape
and self.stride == other.stride
and self.storage_offset == other.storage_offset
and self.device_uuid == other.device_uuid
)
@staticmethod
def Serialize(obj: "DeviceIPCWrapper") -> bytes:
"""Pickle ``obj`` for the multiprocess wire.
Pickle (rather than msgspec) is used so the concrete subclass
identity round-trips: every wrapper shares the single msgspec
ext code (1), and the receiver relies on the unpickled type to
dispatch ``to_tensor`` correctly.
Args:
obj: The wrapper instance to serialize.
Returns:
The pickled bytes payload.
"""
return pickle.dumps(obj)
@staticmethod
def Deserialize(data: bytes) -> "DeviceIPCWrapper":
"""Inverse of :meth:`Serialize`.
Args:
data: The pickled bytes payload produced by :meth:`Serialize`.
Returns:
The reconstructed wrapper instance, with its concrete
subclass identity preserved.
"""
return pickle.loads(data)