Adding a New Device Backend#
This guide explains how to add a new accelerator to LMCache in Multiprocess (MP) mode. Device vendors can choose either supported ownership model:
In-tree integration: contribute and maintain the backend under
lmcache/v1/platform/<device>/in the LMCache repository. This fits backends that should ship, test, and release with LMCache.External wheel integration: maintain the backend in a vendor repository and publish a wheel through the
lmcache.device_pluginsentry-point group. This fits backends that need an independent release cadence or own native-package distribution.
Both models implement the same DeviceSpec and DeviceOps interfaces
and use the same runtime detection and dispatch paths.
For basic users: read Part 1 only — a DeviceSpec
and a DeviceOps subclass are all you need to use the built-in torch
baseline ops.
For advanced users: continue to Part 2 for native ops and advanced transfer modes.
Part 1 — Basic Function Enabling#
For the majority of devices, a DeviceSpec class plus a minimal
DeviceOps subclass are sufficient. LMCache ships with a complete
torch baseline ops layer (lmcache/v1/platform/torch_ops.py) that
works on any device supporting standard PyTorch tensor operations —
no custom kernels required.
Prerequisites#
Your PyTorch backend should support:
Device Discovery & Status:
torch.<device>.is_available()→booltorch.<device>.device_count()→int
Device Context & Synchronization:
torch.<device>.set_device(device)→Nonetorch.<device>.current_device()→inttorch.<device>.synchronize()→None
Data Movement:
tensor.to(device)/tensor.cpu()(host↔device transfers)
Step 1: Choose the ownership model#
Option A — integrate in the LMCache repository#
Create a platform package directly in LMCache:
lmcache/v1/platform/foo/
├── __init__.py
└── device_ops.py
Define FooDeviceSpec in __init__.py. LMCache scans its built-in
platform packages, so this option needs no entry point or registration list.
Submit the code, tests, and device documentation in an LMCache PR; after
merge, the backend follows the LMCache release lifecycle.
Option B — maintain an external wheel#
Use a src layout so the wheel owns only its vendor namespace:
lmcache-foo-device/
├── pyproject.toml
└── src/
└── lmcache_foo/
├── __init__.py
├── device.py
└── device_ops.py
The project must declare one entry point in the
lmcache.device_plugins group. Its name is the lowercase
DeviceSpec.backend_name and its value points to the DeviceSpec class:
# pyproject.toml
[build-system]
requires = ["setuptools>=77", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lmcache-foo-device"
version = "0.1.0"
dependencies = ["lmcache"]
[project.entry-points."lmcache.device_plugins"]
foo = "lmcache_foo.device:FooDeviceSpec"
[tool.setuptools.packages.find]
where = ["src"]
Pin lmcache to the version range tested by your plugin before publishing
it. LMCache treats DeviceSpec and DeviceOps as the plugin interface;
incompatible interface changes should be caught by that dependency range.
Step 2: Implement FooDeviceSpec#
Create the spec in the location selected in Step 1:
In-tree:
lmcache/v1/platform/foo/__init__.py.External wheel:
src/lmcache_foo/device.py.
The implementation is the same in both layouts:
# SPDX-License-Identifier: Apache-2.0
"""LMCache platform registration for Foo devices."""
from __future__ import annotations
from typing import TYPE_CHECKING
from lmcache.v1.platform.base.device_spec import DeviceSpec
if TYPE_CHECKING:
from lmcache.v1.platform.base.device_ops import DeviceOps
class FooDeviceSpec(DeviceSpec):
"""Foo device specification for LMCache registry discovery."""
@property
def device_type(self) -> str:
return "foo"
@property
def backend_name(self) -> str:
return "foo"
@property
def torch_module_name(self) -> str:
return "foo"
@property
def ops_cls(self) -> type[DeviceOps]:
from .device_ops import FooDeviceOps
return FooDeviceOps
def is_available(self) -> bool:
"""Check backend availability without importing lmcache.__init__."""
try:
import torch
return hasattr(torch, "foo") and torch.foo.is_available()
except Exception:
return False
For an external wheel, the entry-point target must be the class itself, not a class instance or factory. For either model, the class must have a no-argument constructor; LMCache instantiates it once per process and caches it.
Step 3: Implement FooDeviceOps#
Create device_ops.py next to the spec package:
In-tree:
lmcache/v1/platform/foo/device_ops.py.External wheel:
src/lmcache_foo/device_ops.py.
# SPDX-License-Identifier: Apache-2.0
"""Foo ops backend."""
from __future__ import annotations
from typing import ClassVar
from lmcache.v1.platform.base.device_ops import DeviceOps
class FooDeviceOps(DeviceOps):
device_type: ClassVar[str] = "foo"
def ensure_native(self) -> None:
"""Keep the torch baseline until native ops are available."""
return None
Note
If you have no native extension yet, leave ensure_native as a no-op.
The torch baseline handles everything. See Part 2 when the backend also ships native code.
Key properties:
Property / Method |
Required |
Purpose |
|---|---|---|
|
yes |
Device type string (e.g. |
|
yes |
Unique LMCache selector for one concrete backend implementation |
|
yes |
Attribute on the |
|
no |
Returns the |
|
yes |
Returns |
|
no |
Called once on first use; override to bind native ops.
Base is a no-op (e.g. |
Note
The hasattr(torch, "foo") guard shown above is only needed for
out-of-tree PyTorch extensions (e.g. torch.musa, torch.xpu
when installed as a plug-in). For accelerators shipped inside
PyTorch itself (like torch.cuda) a plain
torch.foo.is_available() is enough.
Step 4: Install the selected integration#
For an in-tree backend, build or install LMCache normally from the branch containing the platform package. Once merged and released, users receive the backend with LMCache itself.
For an external backend, build the vendor project with any PEP 517 frontend, then install its wheel in the same Python environment as LMCache and the serving engine:
python -m pip install build
python -m build --wheel
python -m pip install dist/lmcache_foo_device-0.1.0-py3-none-any.whl
Restart every LMCache and serving-engine process after installing either integration. The platform registry is built once and cached for the lifetime of each process.
Neither model requires editing a global device-name list. All ops route
through torch_ops.py until the backend overrides them.
External wheel loading rules#
DeviceSpec.device_typeis the torch-facing device category (for example,"cuda"). Multiple backends may intentionally share onedevice_type.DeviceSpec.backend_nameis the LMCache-specific selector for one concrete backend implementation. It must be a unique, non-empty lowercase string.For external wheels, the entry-point name must match
DeviceSpec.backend_name.Duplicate
backend_namevalues are ignored after the first deterministic match.A plugin that cannot be imported, resolves to the wrong object type, or raises during construction is logged and skipped. Other devices remain usable.
Entry-point modules are imported while the platform registry initializes. Import base interfaces such as
lmcache.v1.platform.base.device_spec.DeviceSpecdirectly and keep ops, native libraries, IPC wrappers, and cache contexts behind lazy properties.Installed entry-point packages are executable Python code. Only install wheels from sources you trust.
Verification#
Start LMCache server. The worker below uses the engine-driven
transfer path, which the server only loads when
--supported-transfer-mode is engine_driven or auto (the
default is lmcache_driven):
lmcache server --l1-size-gb 10 --eviction-policy LRU --port 5555 \
--supported-transfer-mode engine_driven
Run vLLM with MP connector. If you want a specific torch device category, set
DEVICE_TYPE. Backends that share one device type are selected automatically
when exactly one reports available. If multiple backends report available, set
LMCACHE_DEVICE_BACKEND to select the exact implementation:
export DEVICE_TYPE=foo # optional; selects the torch-facing device type
export LMCACHE_DEVICE_BACKEND=foo # optional; disambiguates multiple available backends
vllm serve <your-model> \
--kv-transfer-config '{
"kv_connector": "LMCacheMPConnector",
"kv_connector_module_path": "lmcache.integration.vllm.lmcache_mp_connector",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"lmcache.mp.host": "tcp://localhost",
"lmcache.mp.port": "5555",
"lmcache.mp.mp_transfer_mode": "engine_driven"
}
}' \
--no-enable-prefix-caching \
--port 8000
Note
The default transfer mode is auto (CUDA → LMCache-driven, other
devices → engine-driven). The example above explicitly sets
engine_driven so that a new non-CUDA device works without
additional capability checks. For the lmcache_driven mode
(IPC zero-copy), see Advanced transfer mode.
Check the LMCache logs:
torch_dev=..., torch_device_type=foo
This confirms your DeviceSpec was discovered and the torch
baseline is active. When LMCACHE_DEVICE_BACKEND is set, LMCache also binds
the backend whose backend_name matches that value.
Debugging checklist:
[ ]
torch.foo.is_available()returnsTrue.[ ] For an external wheel,
importlib.metadata.entry_points(group="lmcache.device_plugins")includesfooand points toFooDeviceSpec.[ ] If another available backend shares
device_type="foo", setLMCACHE_DEVICE_BACKEND=footo disambiguate them.[ ] Set
DEVICE_TYPE=footo force the torch-facing device category if not picked up automatically.[ ] Engine-driven transfer works end-to-end (check the LMCache logs to confirm whether the SHM or Pickle sub-path is chosen — both should succeed).
[ ] Store/retrieve correctness is verified.
[ ] TP>1 / multi-worker behavior is verified.
Part 2 — Performance Optimization#
Once basic functionality is verified, add device-specific optimizations.
Device-specific ops via bind_native#
The DeviceOps base class delegates every op to the torch baseline
in lmcache/v1/platform/torch_ops.py. Each vendor may replace any
subset of these functions with a device-specific implementation.
How it works. When ensure_native() calls
self.bind_native(native_module), the method walks the module’s
public symbols and rebinds them as instance attributes — overriding the
base-class methods that delegate to torch_ops:
callers → lmcache.device_ops (DeviceOps instance)
│
bind_native() overlay:
├── native.multi_layer_kv_transfer ← vendor CUDA/SYCL kernel
├── native.calculate_cdf ← vendor kernel
└── (everything else) ← torch_ops baseline
Integration contract#
Regardless of how you build your ops module, the following contract must hold:
Same symbol names. Every function you override must be exposed under the exact name used in
torch_ops.py(e.g.multi_layer_block_kv_transfer).Same call signature. Positional/keyword arguments, argument order and semantics must match the baseline; callers invoke the
DeviceOpsinstance without knowing which backend answered.Importable Python module. Your native module must be importable via
import(how it gets there — a pure-Python file, a pybind11 extension, a ctypes wrapper, a RustPyO3module, etc. — is your choice).Partial override is allowed. You do not have to reimplement every function. Anything you leave out keeps using the torch baseline, so incremental optimization is supported.
Types from the native module are also bound.
bind_nativealso binds types (classes) — this is how native plan types likeStagingCopy,KernelGroupSpec, etc. overlay the stubs inops_types.py.
Implementation notes#
multi_layer_block_kv_transferandlmcache_memcpy_asyncare the hot entry points for both engine-driven and LMCache-driven transfer; other functions intorch_ops.pycan be overridden as needed.If you fall back to the generic path from inside a device-specific wrapper (e.g. when inputs are unsupported), call the corresponding
lmcache.v1.platform.torch_opsfunction directly to preserve semantics.ensure_native()is called once whenDeviceSpec.get_ops()first creates the singleton. It is safe to fail soft (log a warning and return without binding).
For concrete reference implementations, see:
lmcache/v1/platform/cuda/device_ops.py— binds the compiledlmcache.cuda_opspybind11 extension.lmcache/v1/platform/xpu/device_ops.py— binds the SYCLlmcache.xpu_opsextension.lmcache/v1/platform/musa/device_ops.py— method overrides without a separate native module.
Advanced transfer mode#
By default, the transfer mode is AUTO: the router dispatches
strictly by device_type — device_type == "cuda" goes to
LMCacheDrivenTransferContext (IPC zero-copy), everything else to
EngineDrivenTransferContext. A non-CUDA device that supports IPC
handle transfer can still opt into LMCache-driven explicitly (below).
Note
Under PyTorch a ROCm GPU reports device_type == "cuda". LMCache
registers a distinct RocmDeviceSpec with backend_name == "rocm"
while reusing the CUDA platform’s ops, cache context, and IPC wrapper.
CUDA and ROCm availability checks are mutually exclusive, so AUTO mode
selects the correct backend without extra configuration.
When the caller (or LMCACHE_MP_TRANSFER_MODE) explicitly requests
lmcache_driven, _build_lmcache_driven_context performs two hard
checks — both must succeed, otherwise the factory raises
ValueError (no silent degradation):
Your
DeviceSpecsubclass must bind aDeviceIPCWrappersubclass (exposing awrapclassmethod) viaipc_wrapper_cls.resolve_kv_wrapper_factory()reads that binding off the registered spec — no separate registry / auto-scan.DeviceSpec.is_handle_transfer_available()must returnTrue(the base-class default; override toFalseonly if your device lacks IPC handle transfer).
Separately, the LMCache-driven server module also requires a
BaseCacheContext subclass next to the backend (for example,
lmcache/v1/platform/foo/cache_context.py in-tree or
lmcache_foo/cache_context.py externally) and a matching
DeviceSpec.create_cache_context override that lazy-imports and
instantiates it. The platform-agnostic factory
lmcache.v1.platform.cache_context.create_cache_context dispatches
by device_type through the DeviceSpec registry and invokes
that hook; the default DeviceSpec.create_cache_context raises
NotImplementedError so a missing override surfaces loudly instead
of silently falling back. The cache context itself manages the KV
cache layout and pointers used for IPC transfer.
Host-side pinning via pin_memory_backend is optional and only
affects staging-buffer performance; it is not required to enable
LMCache-driven mode.
Event IPC capability#
The LMCache-driven multiprocess handle path also requires a platform
event-IPC backend. The capability is declared by
DeviceSpec.event_ipc_backend and is intentionally separate from
DeviceOps and DeviceIPCWrapper:
The base
DeviceSpecreturnsNone. A concrete device must explicitly opt in.CUDA-style event APIs can use
DefaultEventIPCBackend(event_module=..., device_type=...).Devices with a different event ABI should implement an
EventIPCBackendnext to their in-tree or external backend.Concrete device specs should cache the backend because request futures may query this property repeatedly.
The backend contract covers event creation, handle export/import, event
recording, stream wait, query, and synchronization.
check_event_support(device) must raise RuntimeError when those
operations are unavailable for the requested device. The default backend
checks for a CUDA-style Event type that accepts interprocess=True and
provides from_ipc_handle. A custom backend should instead validate the
equivalent prerequisites for its own event ABI.
For a CUDA-style device, bind and cache the default backend as follows:
from typing import TYPE_CHECKING
from lmcache.v1.platform.base.device_spec import DeviceSpec
if TYPE_CHECKING:
from lmcache.v1.platform.base.event_ipc import EventIPCBackend
class FooDeviceSpec(DeviceSpec):
_event_backend_cache: "EventIPCBackend | None" = None
@property
def event_ipc_backend(self) -> "EventIPCBackend":
backend = self._event_backend_cache
if backend is None:
import torch
from lmcache.v1.platform.base.event_ipc import (
DefaultEventIPCBackend,
)
backend = DefaultEventIPCBackend(
event_module=torch.foo,
device_type=self.device_type,
)
self._event_backend_cache = backend
return backend
Event IPC operations must preserve producer/consumer stream ordering without
adding a device-wide synchronization. query_event must remain
non-blocking so request futures can poll completion safely.
Note
If the device does not support Event IPC, leave the base
event_ipc_backend implementation unchanged so it returns None.
Engine-driven mode does not require this capability. LMCache-driven
mode raises an explicit error rather than falling back to CUDA or to the
accelerator active in the process.
The capability is checked during worker/server registration and before constructing a device-aware completion future. This keeps unsupported platforms from entering an asynchronous transfer path that cannot order KV-cache memory safely. The STORE and RETRIEVE message wire format is unchanged: the existing event handle bytes are still carried in the request and response payloads.
Override these methods in your DeviceSpec:
class FooDeviceSpec(DeviceSpec):
@property
def ipc_wrapper_cls(self):
"""Bind the DeviceIPCWrapper subclass for this device.
Lazy import so the accelerator-specific module is only
pulled in when the LMCache-driven path is actually used.
"""
from .ipc_wrapper import FooIPCWrapper
return FooIPCWrapper
def is_handle_transfer_available(self) -> bool:
"""Return True if your device supports IPC handle transfer."""
return True # base-class default; override to False if unsupported
@property
def pin_memory_backend(self):
"""Return a PinMemoryBackend subclass, or None.
Optional; only affects host staging performance.
"""
return None # default
def create_cache_context(self, *args, **kwargs):
"""Lazy-import and instantiate the BaseCacheContext for this device.
Required for LMCache-driven mode; the base-class default
raises ``NotImplementedError``.
"""
from .cache_context import FooCacheContext
return FooCacheContext(*args, **kwargs)
Opt into LMCache-driven mode by setting lmcache.mp.mp_transfer_mode
to lmcache_driven in the vLLM kv_connector_extra_config shown
in Part 1, or by exporting
LMCACHE_MP_TRANSFER_MODE=lmcache_driven. If either hard check
fails, the factory raises ValueError and refuses to construct the
context — switch back to engine_driven or auto.
References#
Topic |
Path |
|---|---|
Device spec base |
|
Device ops base |
|
Torch ops baseline |
|
Ops types and enums |
|
Event IPC base |
|
Backend loading ( |
|
Package-level |
|
Cache context base |
|
Cache context factory |
|
Reference |
|
Reference |
|
Reference |
|
Reference |
|
Reference |
|
Reference |
|
Engine-driven call site |
|