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

344 lines
11 KiB
ReStructuredText

Secondary KV Storage
====================
LMCache multiprocess mode supports a two-tier storage architecture:
- **L1 (fast tier)** -- CPU memory by default, or an NVMe slab via GPUDirect
Storage (cuFile) when ``--gds-l1-path`` is set, managed by the L1 Manager.
All KV cache chunks live here during active use. (Byte-array L2 adapters are
unsupported under the GDS L1 tier, which exposes no L1 memory buffer.)
- **L2 (persistent)** -- Durable storage backends (NIXL-based or plain
file-system/raw-block). The StoreController asynchronously pushes data from L1
to L2, and the PrefetchController loads data from L2 back into L1 on
cache misses.
.. contents::
:local:
:depth: 2
Data Flow
---------
**Write path (L1 -> L2):**
1. vLLM stores KV cache chunks into L1 via the ``STORE`` RPC.
2. The ``StoreController`` detects new objects (via eventfd) and
asynchronously submits store tasks to each configured L2 adapter.
3. The L2 adapter writes the data to its backend (e.g., local SSD via GDS).
**Read path (L2 -> L1):**
1. A ``LOOKUP`` RPC checks L1 for prefix hits.
2. For keys not found in L1, the ``PrefetchController`` submits lookup
requests to L2 adapters.
3. If found in L2, the data is loaded back into L1 and read-locked for the
pending ``RETRIEVE`` RPC.
Adapter Types
-------------
LMCache ships several L2 storage backends, grouped by medium under
:doc:`Supported Backends <supported_storages>`. Select one or more with the
``--l2-adapter`` flag.
.. toctree::
:maxdepth: 2
supported_storages
Multiple Adapters (Cascade)
---------------------------
You can configure multiple L2 adapters by repeating the ``--l2-adapter``
argument. Adapters are used in the order they are specified. The
``StoreController`` pushes data to all configured adapters, and the
``PrefetchController`` queries adapters in order during lookups.
.. code-block:: bash
# SSD (fast, smaller) + NVMe GDS (larger capacity)
--l2-adapter '{"type": "nixl_store", "backend": "POSIX", "backend_params": {"file_path": "/data/ssd/l2", "use_direct_io": "false"}, "pool_size": 64}' \
--l2-adapter '{"type": "nixl_store", "backend": "GDS", "backend_params": {"file_path": "/data/nvme/l2", "use_direct_io": "true"}, "pool_size": 128}'
Store and Prefetch Policies
----------------------------
The **store policy** controls how keys flow from L1 to L2: which adapters
receive each key and whether keys are deleted from L1 after a successful
L2 store. The **prefetch policy** controls how keys flow from L2 back to
L1: when multiple adapters have the same key, the policy decides which
adapter loads it.
Select policies via CLI:
.. code-block:: bash
--l2-store-policy default \
--l2-prefetch-policy default
**Built-in policies:**
.. list-table::
:header-rows: 1
:widths: 15 15 70
* - Flag
- Name
- Behaviour
* - ``--l2-store-policy``
- ``default``
- Store all keys to all adapters. Never delete from L1.
* - ``--l2-store-policy``
- ``skip_l1``
- Buffer-only mode. Store all keys to all adapters, then
**delete them from L1** immediately. Pair with
``--eviction-policy noop`` to avoid useless LRU overhead.
* - ``--l2-prefetch-policy``
- ``default``
- For each key, pick the first (lowest-indexed) adapter that has it.
Prefetched keys are **temporary** (deleted after the reader finishes).
* - ``--l2-prefetch-policy``
- ``retain``
- Same load plan as ``default``, but prefetched keys are **retained**
permanently in L1. Useful when prefetched data is likely reused
by subsequent requests (e.g. shared system-prompt chunks).
Prefetch Concurrency
~~~~~~~~~~~~~~~~~~~~~
The ``--l2-prefetch-max-in-flight`` flag limits the number of concurrent
prefetch requests that the ``PrefetchController`` can have in flight at
any time. A higher value increases L2-to-L1 throughput but also
increases L1 memory pressure from in-flight data.
.. list-table::
:header-rows: 1
:widths: 30 15 55
* - Flag
- Default
- Description
* - ``--l2-prefetch-max-in-flight``
- ``8``
- Maximum number of concurrent prefetch requests.
Buffer-Only Mode
~~~~~~~~~~~~~~~~~
When L1 is used purely as a write buffer (all data lives in L2), use
``--l2-store-policy skip_l1`` together with ``--eviction-policy noop``.
This combination deletes keys from L1 as soon as they are stored to L2
and disables the LRU eviction tracker entirely, reducing memory and CPU
overhead.
.. code-block:: bash
--eviction-policy noop \
--l2-store-policy skip_l1 \
--l2-prefetch-policy default
Policies are extensible -- new policies can be added by creating a file
in ``storage_controllers/`` and calling ``register_store_policy()`` or
``register_prefetch_policy()`` at import time. See the design doc
``l2_adapters/design_docs/overall.md`` for details.
Serde (compression / quantization)
----------------------------------
Each adapter can optionally run a **serde** (serializer / deserializer)
that transforms data on the way in and out of L2 — e.g. fp8 quantization
for disk backends, or encryption for remote adapters. See
:doc:`KV Cache Compression </mp/serde>` for details and configuration.
.. toctree::
:maxdepth: 1
/mp/serde
Eviction
--------
LMCache supports eviction at both storage tiers so that each tier
can operate within a fixed capacity budget.
L1 Eviction
~~~~~~~~~~~
L1 eviction runs a single background thread that monitors overall L1
memory usage. When usage exceeds ``trigger_watermark``, the eviction
policy evicts a fraction of the least-recently-used keys.
**CLI flags:**
.. list-table::
:header-rows: 1
:widths: 30 15 55
* - Flag
- Default
- Description
* - ``--eviction-policy``
- *(required)*
- Policy name: ``LRU`` or ``noop``.
* - ``--eviction-trigger-watermark``
- ``0.8``
- L1 usage fraction [0, 1] above which eviction is triggered.
* - ``--eviction-ratio``
- ``0.2``
- Fraction of currently allocated L1 memory to evict per cycle.
**Example:**
.. code-block:: bash
--eviction-policy LRU \
--eviction-trigger-watermark 0.8 \
--eviction-ratio 0.2
L2 Eviction
~~~~~~~~~~~
L2 eviction is **per-adapter** and **opt-in**. Each adapter can
independently declare an eviction policy by adding an ``"eviction"``
sub-object to its ``--l2-adapter`` JSON spec. Adapters without an
``"eviction"`` key have no eviction controller.
When L2 eviction is enabled for an adapter, a dedicated background
thread monitors that adapter's ``get_usage()`` value. Once usage
exceeds ``trigger_watermark``, the policy evicts keys until usage
drops by ``eviction_ratio``.
**``"eviction"`` sub-object fields:**
.. list-table::
:header-rows: 1
:widths: 25 15 60
* - Field
- Default
- Description
* - ``eviction_policy``
- *(required)*
- Policy name: ``"LRU"`` or ``"noop"``.
* - ``trigger_watermark``
- ``0.8``
- Adapter usage fraction [0, 1] above which eviction is triggered.
* - ``eviction_ratio``
- ``0.2``
- Fraction of used capacity to evict per cycle.
**Example — nixl_store with LRU eviction:**
.. code-block:: bash
--l2-adapter '{
"type": "nixl_store",
"backend": "POSIX",
"backend_params": {"file_path": "/data/lmcache/l2", "use_direct_io": "false"},
"pool_size": 128,
"eviction": {
"eviction_policy": "LRU",
"trigger_watermark": 0.8,
"eviction_ratio": 0.2
}
}'
**Adapter support:**
.. list-table::
:header-rows: 1
:widths: 30 70
* - Adapter
- L2 Eviction Support
* - ``nixl_store``
- Full support. ``delete`` frees pool slots; pinned keys (in-flight
loads) are skipped and retried on the next cycle.
* - ``nixl_store_dynamic``
- Full support. ``delete`` removes data files from disk; pinned
keys are skipped. ``get_usage`` is byte-based
(``_total_bytes / max_capacity_bytes``).
* - ``mock``
- Full support. Useful for testing eviction behaviour without
real storage hardware.
* - ``raw_block``
- Full shared/global eviction support. ``delete`` recycles raw-block
slots; locked entries are skipped and retried on the next cycle.
* - ``s3``
- ``delete`` removes objects from the bucket and frees aggregate
byte accounting. ``get_usage`` reports ``usage_fraction == -1.0``
when ``max_capacity_gb`` is ``0`` (disabled); set a non-zero
``max_capacity_gb`` to enable the watermark-triggered eviction
controller.
* - ``hfbucket``
- ``delete`` removes objects from the bucket and frees aggregate
byte accounting. ``get_usage`` reports ``usage_fraction == -1.0``
when ``max_capacity_gb`` is ``0`` (disabled); set a non-zero
``max_capacity_gb`` to enable the watermark-triggered eviction
controller. Locked keys (in-flight loads) are skipped.
* - ``dax``
- Full support. ``delete`` removes unlocked keys from the in-memory
index immediately and recycles fixed slots once active read borrows
drain. Usage is slot-based.
* - ``mooncake_store``
- No eviction support (native connector adapter).
* - ``fs``
- No eviction support (``delete`` and ``get_usage`` are no-ops).
* - native connectors
- No eviction support.
.. note::
Each L2 adapter instance gets its own independent eviction
controller and policy. Two adapters of the same type can have
different watermarks or policies.
Combined L1 + L2 Eviction Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
--l1-size-gb 100 \
--eviction-policy LRU \
--eviction-trigger-watermark 0.8 \
--eviction-ratio 0.2 \
--l2-adapter '{
"type": "nixl_store",
"backend": "GDS",
"backend_params": {"file_path": "/data/nvme/l2", "use_direct_io": "true"},
"pool_size": 256,
"eviction": {
"eviction_policy": "LRU",
"trigger_watermark": 0.9,
"eviction_ratio": 0.1
}
}'
In this setup:
- L1 evicts from memory when it is 80 % full, reclaiming 20 % of
allocated memory per cycle.
- L2 (NIXL/GDS) evicts from the storage pool when 90 % of pool slots
are occupied, reclaiming 10 % per cycle.
- Both tiers use independent LRU policies, so each evicts its own
least-recently-used keys.
Verifying L2 Storage
--------------------
Set ``LMCACHE_LOG_LEVEL=DEBUG`` to see L2 activity in the server logs:
.. code-block:: bash
LMCACHE_LOG_LEVEL=DEBUG lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--l2-adapter '{"type": "nixl_store", "backend": "POSIX", "backend_params": {"file_path": "/data/lmcache/l2", "use_direct_io": "false"}, "pool_size": 64}'
Expected log messages when L2 is active:
.. code-block:: text
LMCache DEBUG: Submitted store task ...
LMCache DEBUG: L2 store task N completed ...
LMCache DEBUG: Prefetch request submitted: X total keys, Y L1 prefix hits, Z remaining for L2