chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:24:33 +08:00
commit f213ec8976
2101 changed files with 494002 additions and 0 deletions
@@ -0,0 +1,686 @@
Adding Native Backends
======================
.. _native-connectors-overview:
Overview
--------
Native connectors are high-performance C++ storage backends that integrate with LMCache
through pybind11. They work in **both** LMCache operating modes:
- **Non-MP mode** (single process): via ``ConnectorClientBase`` (asyncio integration)
- **MP mode** (multiprocess): via ``NativeConnectorL2Adapter`` (L2 adapter interface)
Write the connector once, get both modes for free.
The framework lives in ``csrc/storage_backends/`` with the Redis RESP connector as the
reference implementation.
Architecture
~~~~~~~~~~~~
.. code-block:: text
Non-MP mode:
CacheEngine -> RemoteBackend -> ConnectorClientBase -> native client (C++)
(asyncio event loop)
MP mode:
StoreController / PrefetchController
|
NativeConnectorL2Adapter (Python bridge)
+-- 3 eventfds (store, lookup, load)
+-- completion demux thread
+-- ObjectKey <-> string serialization
+-- client-side lock tracking
|
native client (C++)
+-- 1 eventfd, worker threads, GIL-free I/O
**Design principles:**
1. **GIL release** at the pybind layer for true concurrency between native threads
2. **Batching with tiling**: work for a batched request is split evenly among threads
3. **eventfd-based completions**: the kernel wakes Python -- no polling
4. **Non-blocking submission**: submission queue / completion queue architecture
Step 1: C++ Connector
---------------------
Create your connector directory (e.g., ``csrc/storage_backends/mybackend/``) and
inherit from ``ConnectorBase<YourConnectionType>``. You need to override 4 required methods
(and optionally ``do_single_delete`` to support eviction).
**connector.h:**
.. code-block:: cpp
// csrc/storage_backends/mybackend/connector.h
#pragma once
#include "../connector_base.h"
namespace lmcache {
namespace connector {
// Per-thread connection state
struct MyConn {
int fd = -1;
// your connection fields
};
class MyConnector : public ConnectorBase<MyConn> {
public:
MyConnector(std::string host, int port, int num_workers)
: ConnectorBase(num_workers), host_(host), port_(port) {
start_workers(); // IMPORTANT: call at END of constructor
}
protected:
// 1. Create a connection (called once per worker thread)
MyConn create_connection() override {
MyConn conn;
// connect to server...
return conn;
}
// 2. GET: read value for key into buf
void do_single_get(MyConn& conn, const std::string& key,
void* buf, size_t len,
size_t chunk_size) override {
// send GET command, recv response into buf
}
// 3. SET: write data from buf under key
void do_single_set(MyConn& conn, const std::string& key,
const void* buf, size_t len,
size_t chunk_size) override {
// send SET command with data from buf
}
// 4. EXISTS: check if key exists
bool do_single_exists(MyConn& conn,
const std::string& key) override {
// send EXISTS, return true/false
}
// 5. DELETE: remove key (optional, has default no-op)
bool do_single_delete(MyConn& conn,
const std::string& key) override {
// send DELETE, return true if deleted, false if not found
}
// Optional: clean shutdown
void shutdown_connections() override {
// close sockets, free resources
}
private:
std::string host_;
int port_;
};
} // namespace connector
} // namespace lmcache
**What ConnectorBase gives you for free:**
- Worker thread pool with per-thread connections
- Submission queue (lock-free enqueue) and completion queue
- Automatic tiling: batch operations are split across workers
- eventfd signaling on completion (kernel wakes Python)
- Graceful shutdown (stop flag, drain, join)
.. important::
Always call ``start_workers()`` at the **end** of your derived constructor,
after all member variables are initialized. Worker threads call
``create_connection()`` immediately, so the object must be fully constructed.
**Reference:** ``csrc/storage_backends/redis/connector.h`` and ``connector.cpp``
Step 2: Pybind Module
---------------------
Use the ``LMCACHE_BIND_CONNECTOR_METHODS`` macro, which binds all 7 methods
(``event_fd``, ``submit_batch_get/set/exists/delete``, ``drain_completions``, ``close``)
with proper GIL release and Python buffer protocol handling.
.. code-block:: cpp
// csrc/storage_backends/mybackend/pybind.cpp
#include <pybind11/pybind11.h>
#include "../connector_pybind_utils.h"
#include "connector.h"
namespace py = pybind11;
PYBIND11_MODULE(lmcache_mybackend, m) {
py::class_<lmcache::connector::MyConnector>(m, "LMCacheMyBackendClient")
.def(py::init<std::string, int, int>(),
py::arg("host"), py::arg("port"),
py::arg("num_workers"))
LMCACHE_BIND_CONNECTOR_METHODS(
lmcache::connector::MyConnector);
}
The pybind utilities automatically:
- Extract buffer pointers from Python ``memoryview`` objects under the GIL
- Release the GIL before calling into C++
- Convert C++ ``Completion`` structs to Python tuples ``(future_id, ok, error, result_bools)``
**Reference:** ``csrc/storage_backends/redis/pybind.cpp``
Step 3: Build System
--------------------
Register your C++ sources in ``setup.py`` alongside the existing Redis extension:
.. code-block:: python
# In cuda_extension() and rocm_extension():
mybackend_sources = [
"csrc/storage_backends/mybackend/pybind.cpp",
"csrc/storage_backends/mybackend/connector.cpp",
]
# Add to ext_modules list:
cpp_extension.CppExtension(
"lmcache.lmcache_mybackend",
sources=mybackend_sources,
include_dirs=[
"csrc/storage_backends",
"csrc/storage_backends/mybackend",
],
extra_compile_args={"cxx": ["-O3", "-std=c++17"]},
),
Then rebuild:
.. code-block:: bash
pip install -e .
Step 4: Python Client (Non-MP Mode)
------------------------------------
Inherit from ``ConnectorClientBase`` which provides asyncio event loop integration,
future management, and both sync and async methods.
.. code-block:: python
# lmcache/v1/storage_backend/native_clients/mybackend_client.py
from .connector_client_base import ConnectorClientBase
from lmcache.lmcache_mybackend import LMCacheMyBackendClient
class MyBackendClient(ConnectorClientBase[LMCacheMyBackendClient]):
def __init__(self, host: str, port: int,
num_workers: int, loop=None):
native = LMCacheMyBackendClient(host, port, num_workers)
super().__init__(native, loop)
This gives you ``batch_get``, ``batch_set``, ``batch_exists`` (async), and their
synchronous variants, all with automatic eventfd-driven completion handling.
**Reference:** ``lmcache/v1/storage_backend/native_clients/resp_client.py``
Step 5: L2 Adapter (MP Mode)
-----------------------------
To use your connector as an L2 adapter in MP mode, create a single Python module that
defines the config class, factory function, and self-registers both. The
``NativeConnectorL2Adapter`` bridge handles all the complexity (eventfd demuxing,
key serialization, locking).
Create a new file in the L2 adapters package:
.. code-block:: python
# lmcache/v1/distributed/l2_adapters/mybackend_l2_adapter.py
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from lmcache.v1.distributed.internal_api import L1MemoryDesc
from lmcache.v1.distributed.l2_adapters.base import (
L2AdapterInterface,
)
from lmcache.v1.distributed.l2_adapters.config import (
L2AdapterConfigBase,
register_l2_adapter_type,
)
from lmcache.v1.distributed.l2_adapters.factory import (
register_l2_adapter_factory,
)
class MyBackendL2AdapterConfig(L2AdapterConfigBase):
def __init__(self, host: str, port: int,
num_workers: int = 8,
max_capacity_gb: float = 0):
self.host = host
self.port = port
self.num_workers = num_workers
self.max_capacity_gb = max_capacity_gb
@classmethod
def from_dict(cls, d: dict) -> "MyBackendL2AdapterConfig":
host = d.get("host")
if not isinstance(host, str) or not host:
raise ValueError("host must be a non-empty string")
port = d.get("port")
if not isinstance(port, int) or port <= 0:
raise ValueError("port must be a positive integer")
num_workers = d.get("num_workers", 8)
max_capacity_gb = d.get("max_capacity_gb", 0)
return cls(host=host, port=port,
num_workers=num_workers,
max_capacity_gb=max_capacity_gb)
@classmethod
def help(cls) -> str:
return (
"MyBackend L2 adapter config fields:\n"
"- host (str): server hostname (required)\n"
"- port (int): server port (required)\n"
"- num_workers (int): worker threads (default 8)"
)
def _create_mybackend_l2_adapter(
config: L2AdapterConfigBase,
l1_memory_desc: "Optional[L1MemoryDesc]" = None,
) -> L2AdapterInterface:
from lmcache.lmcache_mybackend import LMCacheMyBackendClient
from lmcache.v1.distributed.l2_adapters \
.native_connector_l2_adapter import (
NativeConnectorL2Adapter,
)
assert isinstance(config, MyBackendL2AdapterConfig)
native_client = LMCacheMyBackendClient(
config.host, config.port, config.num_workers
)
return NativeConnectorL2Adapter(
native_client,
max_capacity_gb=config.max_capacity_gb,
)
# Self-register -- runs automatically when the module
# is imported by the L2 adapter auto-discovery mechanism
register_l2_adapter_type("mybackend", MyBackendL2AdapterConfig)
register_l2_adapter_factory("mybackend", _create_mybackend_l2_adapter)
.. note::
The L2 adapter package uses ``pkgutil.iter_modules`` to auto-discover all modules
in ``lmcache/v1/distributed/l2_adapters/``. Simply creating the file above is
sufficient -- no changes to ``__init__.py`` or any other existing file are needed.
**Usage from the command line:**
.. code-block:: bash
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--l2-adapter '{"type": "mybackend", "host": "10.0.0.1", "port": 9000}'
How NativeConnectorL2Adapter Bridges the Gap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The C++ connector has 1 eventfd and mixed completions. MP mode's ``L2AdapterInterface``
requires 3 separate eventfds and typed results. The bridge handles this transparently:
.. list-table::
:header-rows: 1
:widths: 30 25 45
* - L2 Adapter Method
- Native Call
- Extra Logic
* - ``submit_store_task(keys, objs)``
- ``submit_batch_set``
- ObjectKey to string, MemoryObj to memoryview
* - ``submit_lookup_and_lock_task(keys)``
- ``submit_batch_exists``
- + client-side lock refcount
* - ``submit_load_task(keys, objs)``
- ``submit_batch_get``
- ObjectKey to string, MemoryObj to memoryview
* - ``submit_unlock(keys)``
- *(none)*
- client-side lock decrement
* - ``pop_completed_store_tasks()``
- via ``drain_completions``
- demux by op type
* - ``query_lookup_and_lock_result()``
- via ``drain_completions``
- exists results to Bitmap, apply locks
* - ``query_load_result()``
- via ``drain_completions``
- ok/fail to Bitmap
A background demux thread polls the native eventfd, calls ``drain_completions()``,
looks up each ``future_id`` to determine its operation type, routes the result to
the correct completion dict, and signals the corresponding Python eventfd.
Third-Party Native Connector Plugins (``native_plugin``)
---------------------------------------------------------
.. _native-plugin-overview:
The steps above describe how to add a native connector **inside** the LMCache source tree.
If you want to ship a connector as a **separate, pip-installable package** (e.g. a proprietary
storage backend), use the ``native_plugin`` L2 adapter type instead. It dynamically loads your
connector at runtime -- no LMCache source modifications required.
How It Works
~~~~~~~~~~~~
The ``native_plugin`` adapter type loads a third-party **connector object** (pybind-wrapped C++
or pure Python) and wraps it with the built-in ``NativeConnectorL2Adapter`` bridge. This means
you only need to implement the 6 connector methods -- the Python-side demux/lock bridging logic
is reused from LMCache.
.. list-table:: ``plugin`` vs ``native_plugin``
:header-rows: 1
:widths: 25 35 40
* - Aspect
- ``plugin``
- ``native_plugin``
* - What is loaded
- A full ``L2AdapterInterface`` subclass
- A **connector** object (lower level)
* - Bridging logic
- Provided by the plugin itself
- Reused from ``NativeConnectorL2Adapter``
* - Third-party effort
- Must implement all abstract methods + 3 eventfds
- Only 6 connector methods
Required Connector Interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The dynamically loaded connector instance must expose the following methods (identical to the
pybind ``LMCACHE_BIND_CONNECTOR_METHODS`` contract):
.. code-block:: python
class NativeConnectorProtocol:
def event_fd(self) -> int: ...
def submit_batch_get(
self,
keys: list[str],
memoryviews: list[memoryview],
) -> int: ...
def submit_batch_set(
self,
keys: list[str],
memoryviews: list[memoryview],
) -> int: ...
def submit_batch_exists(
self,
keys: list[str],
) -> int: ...
def submit_batch_delete(
self,
keys: list[str],
) -> int: ...
def drain_completions(
self,
) -> list[tuple[int, bool, str, list[bool] | None]]: ...
def close(self) -> None: ...
The factory validates the first 6 methods at creation time and raises ``TypeError`` if
any are missing. ``submit_batch_delete`` is **optional** -- if absent, the adapter's
``delete()`` method will be a no-op (eviction will not remove keys from the backend).
Configuration
~~~~~~~~~~~~~
.. code-block:: json
{
"type": "native_plugin",
"module_path": "my_ext_package",
"class_name": "MyConnectorClient",
"adapter_params": {
"host": "localhost",
"port": 1234
}
}
.. list-table:: ``NativePluginL2AdapterConfig`` fields
:header-rows: 1
:widths: 20 10 10 60
* - Field
- Type
- Required
- Description
* - ``module_path``
- ``str``
- yes
- Dotted Python import path of the module containing the connector class.
* - ``class_name``
- ``str``
- yes
- Name of the connector class inside ``module_path``.
* - ``adapter_params``
- ``dict``
- no
- Forwarded as ``**kwargs`` to the connector class constructor.
* - ``max_capacity_gb``
- ``float``
- no
- Maximum L2 storage capacity in GB for client-side usage tracking. Required for L2 eviction. Default 0 (disabled).
Loading Flow
~~~~~~~~~~~~
.. code-block:: text
CLI / config JSON
|
v
NativePluginL2AdapterConfig.from_dict(d)
|
v
_create_native_plugin_l2_adapter(config, ...)
|
+-- importlib.import_module(config.module_path)
+-- getattr(module, config.class_name)
+-- connector_cls(**config.adapter_params)
+-- validate 6 required methods
+-- NativeConnectorL2Adapter(native_client)
|
v
L2AdapterInterface instance (ready for use)
Step-by-Step: Building an External Native Connector Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. **Create a Python package** with a C++ pybind11 extension that inherits from
``ConnectorBase<T>`` (same base class as built-in connectors).
Project layout:
.. code-block:: text
my_ext_connector/
+-- csrc/
| +-- connector.h # C++ connector class
| +-- connector.cpp # C++ implementation
| +-- pybind.cpp # pybind11 bindings
+-- src/
| +-- my_ext_connector/
| +-- __init__.py # re-export the factory class
| +-- connector.py # Python factory wrapper
+-- pyproject.toml
+-- setup.py # build C++ extension
2. **Implement the C++ connector** inheriting from ``ConnectorBase<T>`` and override
the 4 required methods (``create_connection``, ``do_single_get``, ``do_single_set``,
``do_single_exists``) and optionally ``do_single_delete`` for eviction support.
3. **Create pybind11 bindings** using the ``LMCACHE_BIND_CONNECTOR_METHODS`` macro:
.. code-block:: cpp
#include <pybind11/pybind11.h>
#include "connector_pybind_utils.h"
#include "connector.h"
namespace py = pybind11;
PYBIND11_MODULE(_native, m) {
py::class_<MyFSConnector>(m, "MyFSConnector")
.def(py::init<std::string, int>(),
py::arg("base_path"),
py::arg("num_workers"))
LMCACHE_BIND_CONNECTOR_METHODS(MyFSConnector);
}
4. **Write a Python factory class** that selects the backend and returns the native
connector instance:
.. code-block:: python
from my_ext_connector._native import MyFSConnector
class MyConnectorClient:
def __new__(
cls,
base_path: str = "/tmp/my_ext",
num_workers: int = 2,
):
return MyFSConnector(base_path, num_workers)
5. **Build and install** the package:
.. code-block:: bash
cd my_ext_connector
pip install -e .
6. **Configure LMCache** to use it:
.. code-block:: bash
--l2-adapter '{
"type": "native_plugin",
"module_path": "my_ext_connector",
"class_name": "MyConnectorClient",
"adapter_params": {
"base_path": "/tmp/my_ext",
"num_workers": 2
}
}'
Reference Implementation
~~~~~~~~~~~~~~~~~~~~~~~~~
See ``examples/lmc_external_native_connector/`` for a complete, pip-installable example
connector plugin that demonstrates:
- C++ pybind11-wrapped connectors inheriting from ``ConnectorBase<T>`` (same as built-in
Redis/FS).
- Two backends: filesystem (``ExampleFSConnector``) and in-memory
(``ExampleMemoryConnector``), both in C++.
- A thin Python factory class (``ExampleNativeConnector``) that selects the backend via a
``"backend"`` parameter.
- Worker thread pool with eventfd notification (inherited from ``ConnectorBase``).
- Build via ``pip install -e .`` using pybind11 + setuptools.
Checklist
---------
Use this checklist when adding a new native connector:
1. C++ connector inheriting ``ConnectorBase<T>`` with 4 required + 1 optional (``do_single_delete``) method overrides
2. Pybind module using ``LMCACHE_BIND_CONNECTOR_METHODS``
3. ``setup.py`` entry for the new ``CppExtension``
4. Python client inheriting ``ConnectorClientBase`` (non-MP mode)
5. L2 adapter module with config class + factory self-registration (MP mode)
6. Unit tests (see ``tests/v1/distributed/test_native_connector_l2_adapter.py``)
7. Rebuild with ``pip install -e .`` and verify both modes work
For **external** native connector plugins (``native_plugin``):
1. Separate pip-installable package with C++ pybind11 extension
2. Connector class exposing the 6 required methods (+ optional ``submit_batch_delete`` for eviction)
3. Python factory class for backend selection
4. ``pip install -e .`` and configure via ``--l2-adapter`` JSON
5. Unit tests (see ``examples/lmc_external_native_connector/tests/``)
Built-in Aerospike backend (optional)
-------------------------------------
LMCache ships an optional in-tree Aerospike native connector (same
``ConnectorBase`` harness as Redis). It is compiled only when
``BUILD_AEROSPIKE=1`` or ``AEROSPIKE_INCLUDE_DIR`` is set during
``pip install -e .``.
**Build:**
.. code-block:: bash
See ``.github/workflows/aerospike_integration.yml`` for installing the C client into ``.deps/``
source .deps/aerospike-client-c.env
BUILD_AEROSPIKE=1 pip install -e .
The ``aerospike-client-c.env`` file simply points the build at the C client
headers and shared libraries you unpacked into ``.deps/``. Adjust the paths to
match where the client was installed. Example:
.. code-block:: bash
# .deps/aerospike-client-c.env
export AEROSPIKE_INCLUDE_DIR="${PWD}/.deps/aerospike-install/usr/include"
export AEROSPIKE_LIBRARY_DIR="${PWD}/.deps/aerospike-install/usr/lib"
# Needed at build and runtime so the loader can find libaerospike (and
# libyaml if you built it locally):
export LD_LIBRARY_PATH="${AEROSPIKE_LIBRARY_DIR}:${PWD}/.deps/libyaml-install/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}"
Setting ``AEROSPIKE_INCLUDE_DIR`` is enough to enable the extension, so
``BUILD_AEROSPIKE=1`` is optional once the env file is sourced. Multiple include
or library directories can be passed as ``;``-separated lists.
**MP mode:**
.. code-block:: bash
--l2-adapter '{"type": "aerospike", "hosts": "127.0.0.1:3000", "namespace": "lmcache", "set_name": "kv_chunks", "num_workers": 8}'
Implementation: ``csrc/storage_backends/aerospike/``,
``lmcache/v1/distributed/l2_adapters/aerospike_l2_adapter.py``.
Additional Resources
--------------------
- Framework source: ``csrc/storage_backends/``
- ``ConnectorBase`` template: ``csrc/storage_backends/connector_base.h``
- ``IStorageConnector`` interface: ``csrc/storage_backends/connector_interface.h``
- Pybind utilities: ``csrc/storage_backends/connector_pybind_utils.h``
- Redis reference implementation: ``csrc/storage_backends/redis/``
- Aerospike implementation (optional): ``csrc/storage_backends/aerospike/``
- Architecture README: ``csrc/storage_backends/README.md``
- External native connector example:
``examples/lmc_external_native_connector/``
- Native plugin adapter source:
``lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py``
- Design document:
``lmcache/v1/distributed/l2_adapters/design_docs/plugin.md``
- RESP backend user guide: :doc:`RESP (Native Redis/Valkey) <../../kv_cache/storage_backends/resp>`
@@ -0,0 +1,294 @@
Remote Storage Plugins
========================
.. warning::
This page documents the behavior of LMCache's in-process mode (deprecated). Please consider using :doc:`LMCache MP mode </mp/index>` for better feature support and performance.
LMCache supports built-in remote storage connectors for Redis, InfiniStore,
MooncakeStore, S3, Hugging Face Buckets, and more.
The remote storage plugin system provides the ability to add custom storage connectors through dynamic loading. This enables extending remote storage capabilities without modifying core code.
.. note::
The ``remote_url`` configuration is **deprecated** and will be removed in a future release.
Please use ``remote_storage_plugins`` instead.
Connector Definition Requirements
---------------------------------
A custom remote storage connector requires two classes:
1. **ConnectorAdapter**: Handles URL scheme matching and connector instantiation
- Inherit from ``ConnectorAdapter``
- Set the URL scheme in the constructor (e.g., ``mystore://``)
- Implement the ``create_connector`` method
2. **RemoteConnector**: Implements the actual storage operations
- Inherit from ``RemoteConnector``
- Implement all abstract methods: ``exists``, ``exists_sync``, ``get``, ``put``, ``list``, ``close``
.. note::
The ``ConnectorAdapter`` constructor receives no arguments from LMCache. The scheme should be set by calling the parent constructor with the scheme string.
The ``create_connector`` method receives a ``ConnectorContext`` object containing the URL, event loop, local CPU backend, config, metadata, and ``plugin_name``.
Plugin Naming Convention
-------------------------
Plugin names follow the format ``{type}`` or ``{type}.{instance}``:
- ``{type}`` — a single instance of that connector type (e.g. ``fs``, ``mooncakestore``)
- ``{type}.{instance}`` — a named instance, allowing **multiple instances of the same type** (e.g. ``fs.primary``, ``fs.backup``)
The framework extracts the *type* portion (everything before the first ``.``) to locate the matching ``ConnectorAdapter``. The full plugin name is used as the configuration key prefix.
Using Built-in Connectors via Plugins
-------------------------------------
Built-in connectors (``fs``, ``mooncakestore``, ``hfbucket``, etc.) can be used
directly via ``remote_storage_plugins`` without specifying ``module_path`` or
``class_name``. Their configuration is placed under ``extra_config``:
.. code-block:: yaml
chunk_size: 64
local_cpu: False
max_local_cpu_size: 5
remote_storage_plugins: ["fs"]
extra_config:
remote_storage_plugin.fs.base_path: /tmp/lmcache
Multiple instances of the same connector type:
.. code-block:: yaml
remote_storage_plugins: ["fs.primary", "fs.backup"]
extra_config:
remote_storage_plugin.fs.primary.base_path: /data/cache1
remote_storage_plugin.fs.backup.base_path: /data/cache2
Mixing different connector types:
.. code-block:: yaml
remote_storage_plugins: ["fs.local", "mooncakestore"]
extra_config:
remote_storage_plugin.fs.local.base_path: /data/cache
remote_storage_plugin.mooncakestore.master_server_address: "localhost:50051"
Built-in Hugging Face Buckets example:
.. code-block:: yaml
remote_storage_plugins: ["hfbucket"]
extra_config:
remote_storage_plugin.hfbucket.bucket_handle: hf://buckets/my-org/lmcache-kv/prod
remote_storage_plugin.hfbucket.token_env: HF_TOKEN
How to Integrate Custom Remote Storage with LMCache
---------------------------------------------------
1. Install your connector package in the LMCache environment
2. Add ``remote_storage_plugins`` and its related ``module_path`` and ``class_name`` to the ``extra_config`` section of LMCache configuration as follows:
.. code-block:: yaml
chunk_size: 64
local_cpu: False
max_local_cpu_size: 5
remote_storage_plugins: ["mystore"]
extra_config:
remote_storage_plugin.mystore.module_path: <module_path>
remote_storage_plugin.mystore.class_name: <adapter_class_name>
An example configuration for a custom remote storage connector:
.. code-block:: yaml
chunk_size: 64
local_cpu: False
max_local_cpu_size: 5
remote_storage_plugins: ["mystore"]
extra_config:
remote_storage_plugin.mystore.module_path: my_package.my_connector
remote_storage_plugin.mystore.class_name: MyStoreConnectorAdapter
Multiple instances of a custom connector:
.. code-block:: yaml
remote_storage_plugins: ["mystore.region_a", "mystore.region_b"]
extra_config:
remote_storage_plugin.mystore.region_a.module_path: my_package.my_connector
remote_storage_plugin.mystore.region_a.class_name: MyStoreConnectorAdapter
remote_storage_plugin.mystore.region_b.module_path: my_package.my_connector
remote_storage_plugin.mystore.region_b.class_name: MyStoreConnectorAdapter
.. note::
- ``remote_url`` is **deprecated**; use ``remote_storage_plugins`` instead
- ``remote_storage_plugin.<plugin_name>`` uses the full plugin name (including instance suffix) as the key prefix
- Multiple remote storage plugins can be loaded simultaneously
- Built-in connectors do not require ``module_path`` / ``class_name``
ConnectorAdapter Implementation
-------------------------------
The ``ConnectorAdapter`` class is responsible for:
- Defining the URL scheme it handles
- Creating the appropriate ``RemoteConnector`` instance
.. code-block:: python
from lmcache.v1.storage_backend.connector import (
ConnectorAdapter,
ConnectorContext,
)
from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
from lmcache.v1.storage_backend.connector import extract_plugin_type
PLUGIN_TYPE = "mystore"
class MyStoreConnectorAdapter(ConnectorAdapter):
"""Adapter for MyStore remote storage."""
def __init__(self) -> None:
# Register the URL scheme this adapter handles
super().__init__("mystore://")
def can_parse(self, url: str) -> bool:
"""Match legacy URL or plugin://{type}[.{instance}] format."""
if url.startswith(self.schema):
return True
if url.startswith("plugin://"):
pname = url[len("plugin://"):]
return extract_plugin_type(pname) == PLUGIN_TYPE
return False
def create_connector(self, context: ConnectorContext) -> RemoteConnector:
"""Create and return a MyStoreConnector instance."""
# Access context properties as needed:
# - context.url: the full remote URL
# - context.loop: asyncio event loop
# - context.config: LMCacheEngineConfig
# - context.metadata: LMCacheMetadata
# - context.plugin_name: plugin instance name
# (e.g. "mystore", "mystore.region_a")
return MyStoreConnector(
context.config,
context.metadata,
plugin_name=context.plugin_name,
)
RemoteConnector Implementation
------------------------------
The ``RemoteConnector`` class defines the interface for remote storage operations. Your implementation must provide the following abstract methods:
.. code-block:: python
from typing import List, Optional
from lmcache.utils import CacheEngineKey
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.metadata import LMCacheMetadata
from lmcache.v1.memory_management import MemoryObj
from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
class MyStoreConnector(RemoteConnector):
"""Custom connector for MyStore remote storage."""
def __init__(
self,
config: LMCacheEngineConfig,
metadata: Optional[LMCacheMetadata]
):
super().__init__(config, metadata)
# Initialize your connection here
async def exists(self, key: CacheEngineKey) -> bool:
"""Check if a key exists in the remote store."""
raise NotImplementedError
def exists_sync(self, key: CacheEngineKey) -> bool:
"""Synchronous version of exists."""
raise NotImplementedError
async def get(self, key: CacheEngineKey) -> Optional[MemoryObj]:
"""Retrieve a memory object by key. Return None if not found."""
raise NotImplementedError
async def put(self, key: CacheEngineKey, memory_obj: MemoryObj):
"""Store a memory object with the given key."""
raise NotImplementedError
async def list(self) -> List[str]:
"""List all keys in the remote store."""
raise NotImplementedError
async def close(self):
"""Close the connection to the remote store."""
raise NotImplementedError
Optional Methods
----------------
The ``RemoteConnector`` base class also provides optional methods that can be overridden for enhanced functionality:
- ``support_ping()`` / ``ping()``: Health check support
- ``support_batched_get()`` / ``batched_get()``: Batch retrieval operations
- ``support_batched_put()`` / ``batched_put()``: Batch storage operations
- ``support_batched_contains()`` / ``batched_contains()``: Batch existence checks
- ``remove_sync()``: Synchronous key removal
Implementation Example
----------------------
A complete remote storage connector implementation would include both the adapter and connector classes in a single module or package. Here's a minimal working example structure:
.. code-block:: text
my_connector_package/
├── __init__.py
├── adapter.py # Contains MyStoreConnectorAdapter
└── connector.py # Contains MyStoreConnector
The adapter module (``adapter.py``):
.. code-block:: python
from lmcache.v1.storage_backend.connector import (
ConnectorAdapter,
ConnectorContext,
extract_plugin_type,
)
from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
from .connector import MyStoreConnector
PLUGIN_TYPE = "mystore"
class MyStoreConnectorAdapter(ConnectorAdapter):
def __init__(self) -> None:
super().__init__("mystore://")
def can_parse(self, url: str) -> bool:
if url.startswith(self.schema):
return True
if url.startswith("plugin://"):
pname = url[len("plugin://"):]
return extract_plugin_type(pname) == PLUGIN_TYPE
return False
def create_connector(self, context: ConnectorContext) -> RemoteConnector:
return MyStoreConnector(
context.config,
context.metadata,
plugin_name=context.plugin_name,
)
Configuration would then reference the adapter:
.. code-block:: yaml
remote_storage_plugins: ["mystore"]
extra_config:
remote_storage_plugin.mystore.module_path: my_connector_package.adapter
remote_storage_plugin.mystore.class_name: MyStoreConnectorAdapter
@@ -0,0 +1,89 @@
Runtime Plugins
===============
The LMCache runtime plugin system provides the ability to extend functionality by running custom scripts alongside LMCache processes. Plugins can be written in Python and Bash for now, and are managed by the ``RuntimePluginLauncher`` class.
Key Use Cases
-------------
- Start metric reporters for centralized monitoring
- Implement log reporters for log collection systems
- Report process-level metrics to alerting systems
- Implement health checks and service discovery
- Custom cache management operations
Configuration
-------------
Runtime plugins are configured through environment variables and configuration files:
Environment Variables:
- ``LMCACHE_RUNTIME_PLUGIN_ROLE``: Process role (e.g., ``SCHEDULER``, ``WORKER``)
- ``LMCACHE_RUNTIME_PLUGIN_CONFIG``: JSON string containing plugin configuration
- ``LMCACHE_RUNTIME_PLUGIN_WORKER_ID``: Current worker ID
- ``LMCACHE_RUNTIME_PLUGIN_WORKER_COUNT``: Total worker count in cluster
Configuration File (``lmcache.yaml``):
.. code-block:: yaml
runtime_plugin_locations:
- "/path/to/plugins"
extra_config:
custom_setting: value
Runtime Plugin Naming Convention
--------------------------------
Plugin filenames determine execution targets:
Role-Specific Plugins:
- Format: ``<ROLE>[_<WORKER_ID>][_<DESCRIPTION>].<EXTENSION>``
- Examples:
- ``scheduler_foo_plugin.py``: Runs only on ``SCHEDULER``
- ``worker_0_test.sh``: Runs only on worker ID 0
- ``all_plugin.sh``: Runs on all workers
Notes:
- Role names are case-insensitive
- Worker ID must be numeric when specified
- To target a specific worker ID, the filename must have at least three parts separated by underscores (e.g., `worker_<ID>_<DESCRIPTION>.ext`). A file named `worker_<DESCRIPTION>.ext` will run on all workers.
Execution Model
---------------
1. **Interpreter Detection**:
- Uses shebang line (e.g., ``#!/opt/venv/bin/python``)
- Fallback interpreters:
- ``.py````python``
- ``.sh````bash``
2. **Output Handling**:
- Stdout/stderr captured continuously
- Logged with plugin name prefix
3. **Process Management**:
- Launched as subprocesses
- Terminated when parent process exits
Example Runtime Plugins
-----------------------
Python Plugin (``scheduler_foo_plugin.py``):
.. literalinclude:: ../../../../examples/plugins/scheduler_foo_plugin.py
:language: python
:linenos:
Bash Plugin (``all_plugin.sh``):
.. literalinclude:: ../../../../examples/plugins/all_plugin.sh
:language: bash
:linenos:
Best Practices
--------------
1. Keep runtime plugins lightweight and efficient
2. Use descriptive naming conventions
3. Implement graceful error handling
4. Include shebang for portability
5. Validate configuration inputs
6. Add timeout mechanisms for long operations
@@ -0,0 +1,248 @@
Storage Plugins
===============
LMCache supports out of the box storage backends like Mooncake, S3 and NIXL.
The LMCache storage plugin system provides the ability to add custom storage backends through dynamic loading or plug and play capability. In other words, extending cache storage capabilities without modifying core code.
Backend Definition Requirements
-------------------------------
1. Inherit from ``StoragePluginInterface``
2. Implement all the abstract methods of the parent interface of ``StoragePluginInterface``- ``StorageBackendInterface``
3. Package as an installable Python module
.. note::
The interface constructor is the instantiation contract that the LMCache loading system will use when loading custom storage backends.
If you wish to implement a constructor, it should have the same parameter signature and call the interface constructor.
How to Integrate the Backend with LMCache
-----------------------------------------
1. Install your backend package in the LMCache environment
2. Add ``storage_plugins`` and its related ``module_path`` and ``class_name`` to ``extra_config`` section of LMCache configuration as follows:
.. code-block:: yaml
chunk_size: 64
local_cpu: False
max_local_cpu_size: 5
storage_plugins: <backend_name>
extra_config:
storage_plugin.<backend_name>.module_path: <module_path>
storage_plugin.<backend_name>.class_name: <class_name>
An example configuration for a logging backend is as follows:
.. code-block:: yaml
chunk_size: 64
local_cpu: False
max_local_cpu_size: 5
storage_plugins: "log_backend"
extra_config:
storage_plugin.log_backend.module_path: lmc_external_log_backend.lmc_external_log_backend
storage_plugin.log_backend.class_name: ExternalLogBackend
.. note::
- Storage backends are initialized in order during LMCache startup - earlier backends have higher priority during cache lookups
- ``storage_plugin.<backend_name>`` distinguishes the different dynamic loaded backends
Backend Implementation Example
------------------------------
A sample custom backend implementation can be viewed at https://github.com/opendataio/lmc_external_log_backend/
MP-Mode L2 Adapter Plugins (``plugin``)
----------------------------------------
.. _plugin-l2-adapter-overview:
The storage plugin system described above applies to **non-MP mode** (single-process). For
**MP mode** (multiprocess), LMCache provides the ``plugin`` L2 adapter type, which dynamically
loads a third-party ``L2AdapterInterface`` implementation at runtime.
Overview
~~~~~~~~
The ``plugin`` adapter type lets you ship a full L2 adapter as a **separate, pip-installable
package**. At startup, LMCache imports your module, instantiates your adapter class, and uses it
just like a built-in adapter -- no LMCache source modifications required.
.. note::
If your storage backend is a native C++ connector (pybind-wrapped), consider using the
``native_plugin`` type instead (see :doc:`native_connectors`). It reuses the built-in
bridging logic so you only need to implement 6 connector methods rather than the full
``L2AdapterInterface``.
Configuration
~~~~~~~~~~~~~
.. code-block:: json
{
"type": "plugin",
"module_path": "my_plugin.adapter",
"class_name": "MyL2Adapter",
"adapter_params": {
"host": "localhost",
"capacity": 1000
}
}
.. list-table:: ``PluginL2AdapterConfig`` fields
:header-rows: 1
:widths: 22 10 10 58
* - Field
- Type
- Required
- Description
* - ``module_path``
- ``str``
- yes
- Dotted Python import path of the module containing the adapter class.
* - ``class_name``
- ``str``
- yes
- Name of the class inside ``module_path`` that implements ``L2AdapterInterface``.
* - ``adapter_params``
- ``dict``
- no
- Forwarded to the adapter class constructor (as a typed config or raw dict).
* - ``config_class_name``
- ``str``
- no
- Explicit config class name; when omitted the factory auto-discovers it.
Config Class Auto-Discovery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The factory automatically resolves the adapter's config class using the following priority
chain (first match wins):
1. **Explicit** ``config_class_name`` field in JSON config.
2. **Convention**: adapter class name + ``"Config"`` suffix (e.g. ``MyL2Adapter`` looks for
``MyL2AdapterConfig``).
3. **Attribute**: ``config_class_name`` attribute on the adapter class itself.
4. **Fallback**: no config class found -- pass raw ``adapter_params`` dict.
Each candidate is looked up in the loaded module and validated as an ``L2AdapterConfigBase``
subclass. This means most plugins that follow the naming convention will **automatically**
receive a typed config instance without any extra configuration.
Plugin Contract
~~~~~~~~~~~~~~~
A plugin adapter class **must**:
1. Subclass ``L2AdapterInterface`` from ``lmcache.v1.distributed.l2_adapters.base``.
2. Implement all abstract methods: ``submit_store_task``, ``pop_completed_store_tasks``,
``submit_lookup_and_lock_task``, ``query_lookup_and_lock_result``, ``submit_unlock``,
``submit_load_task``, ``query_load_result``, ``close``, and all three event-fd getters.
3. Provide **three distinct event fds** (store / lookup / load). The controllers build
``fd -> adapter`` maps; duplicates will misroute events.
4. Be **thread-safe**: ``StoreController`` and ``PrefetchController`` call adapter methods
from different threads concurrently.
5. Accept ``**kwargs`` in ``__init__`` to stay forward-compatible.
A plugin adapter class **should**:
1. Create its own asyncio event loop and background thread if it needs async I/O.
2. Use ``create_event_notifier()`` from ``lmcache.v1.platform`` for the three event fds
(cross-platform: ``os.eventfd`` on Linux, ``os.pipe`` fallback elsewhere).
3. Clean up all resources (event fds, threads, connections) in ``close()``.
Loading Flow
~~~~~~~~~~~~
.. code-block:: text
CLI / config JSON
|
v
PluginL2AdapterConfig.from_dict(d)
| validates module_path, class_name, adapter_params
|
v
_create_plugin_adapter(config, ...)
|
+-- importlib.import_module(config.module_path)
+-- getattr(module, config.class_name)
+-- issubclass check against L2AdapterInterface
|
+-- _resolve_config_class(module, config, adapter_cls)
| +-- 1. config.config_class_name (explicit)
| +-- 2. class_name + "Config" (convention)
| +-- 3. adapter_cls.config_class_name (attribute)
| +-- 4. None (fall back to raw dict)
|
+-- [if config class found]
| +-- adapter_cls(cfg_cls.from_dict(adapter_params))
|
+-- [otherwise]
+-- adapter_cls(adapter_params)
|
v
L2AdapterInterface instance (ready for use)
Minimal Example
~~~~~~~~~~~~~~~
.. code-block:: python
# my_plugin/adapter.py
import asyncio
import threading
from lmcache.native_storage_ops import Bitmap
from lmcache.v1.distributed.l2_adapters.base import (
L2AdapterInterface,
L2TaskId,
)
from lmcache.v1.platform import create_event_notifier
class MyL2Adapter(L2AdapterInterface):
def __init__(self, params, **_kw):
self._store_efd = create_event_notifier()
self._lookup_efd = create_event_notifier()
self._load_efd = create_event_notifier()
# ... set up connection, background thread, etc.
# implement all abstract methods ...
def close(self) -> None:
self._store_efd.close()
self._lookup_efd.close()
self._load_efd.close()
Launch via CLI:
.. code-block:: bash
--l2-adapter '{
"type": "plugin",
"module_path": "my_plugin.adapter",
"class_name": "MyL2Adapter",
"adapter_params": {"host": "localhost"}
}'
Reference Implementation
~~~~~~~~~~~~~~~~~~~~~~~~~
See ``examples/lmc_external_l2_adapter/`` for a complete, pip-installable example plugin
(``InMemoryL2Adapter``) that demonstrates:
- FIFO eviction with configurable capacity.
- Simulated bandwidth delay for realistic testing.
- Background asyncio event loop with proper shutdown.
- Full test suite covering store, lookup, load, batch operations, and eviction behavior.
Additional Resources
~~~~~~~~~~~~~~~~~~~~
- Plugin adapter source: ``lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py``
- Native plugin adapter: ``lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py``
- Design document: ``lmcache/v1/distributed/l2_adapters/design_docs/plugin.md``
- L2 adapter base interface: ``lmcache/v1/distributed/l2_adapters/base.py``