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

192 lines
5.5 KiB
ReStructuredText

KV Cache Compression
====================
LMCache supports a **per-adapter serde** that transforms KV cache data on
its way to and from an L2 adapter. Typical uses: quantization (shrink
storage footprint), compression, encryption.
.. contents::
:local:
:depth: 2
When to use serde
-----------------
- **Save L2 storage or bandwidth.** fp8 quantization halves byte volume
vs. bf16 with minor accuracy loss — a good fit for disk / remote
adapters.
- **Encrypt at rest.** Wrap the raw bytes with authenticated encryption
before they land on disk.
- **Custom compression.** Anything lossless (lz4/zstd) or lossy
(CacheGen-style) can be plugged in via the ``Serializer`` /
``Deserializer`` ABCs.
Serde is **opt-in per adapter**: one ``--l2-adapter`` may use fp8 while
another stores raw bytes. When omitted, the adapter behaves exactly as
if serde did not exist (no extra allocations, no extra threads).
Configuring serde on an L2 adapter
----------------------------------
Add a ``"serde"`` sub-dict to any ``--l2-adapter`` JSON spec. The ``type``
field selects a registered serde; remaining keys are forwarded to the
serde factory.
.. code-block:: bash
lmcache server \
--l1-size-gb 100 \
--eviction-policy LRU \
--l2-adapter '{
"type": "fs",
"base_path": "/data/lmcache/l2",
"serde": {"type": "fp8", "fp8_dtype": "float8_e4m3fn"}
}'
.. list-table:: Built-in serde types
:header-rows: 1
:widths: 15 40 45
* - ``type``
- Description
- Config fields
* - ``fp8``
- Quantize each element to 8-bit float; dequantize on load.
Lossy but highly compressible.
- ``fp8_dtype`` (default ``float8_e4m3fn``; also accepts
``float8_e5m2``), ``max_workers`` (thread pool size,
default 1)
* - ``turboquant``
- Compress KV tensors with TurboQuant presets before L2 store and
reconstruct them on load.
- ``preset`` (default ``turboquant_k8v4``), ``head_dim`` (optional,
default 128), ``block_size`` (default 16), ``max_workers`` (thread
pool size, default 1)
TurboQuant serde
----------------
TurboQuant serde can be enabled by setting ``"type": "turboquant"`` in the
adapter serde config. If ``preset`` is omitted, TurboQuant serde defaults to
``turboquant_k8v4``.
.. code-block:: bash
lmcache server \
--l1-size-gb 100 \
--eviction-policy LRU \
--l2-adapter '{
"type": "fs",
"base_path": "/data/lmcache/l2",
"serde": {
"type": "turboquant",
"preset": "turboquant_k8v4",
"block_size": 16
}
}'
Supported presets:
.. list-table::
:header-rows: 1
:widths: 30 35 35
* - Preset
- Key path
- Value path
* - ``turboquant_k8v4``
- FP8 key
- 4-bit value quantization
* - ``turboquant_4bit_nc``
- 4-bit MSE key with norm correction
- 4-bit value quantization
* - ``turboquant_k3v4_nc``
- 3-bit MSE key with norm correction
- 4-bit value quantization
* - ``turboquant_3bit_nc``
- 3-bit MSE key with norm correction
- 3-bit value quantization
Writing a custom serde
----------------------
Implement the two sync ABCs (``Serializer``, ``Deserializer``) with your
transform logic, then register a factory keyed on a name you pick:
.. code-block:: python
# my_project/my_serde.py
from lmcache.v1.distributed.serde import (
AsyncSerdeProcessor,
Deserializer,
Serializer,
register_serde_factory,
)
class MySerializer(Serializer):
def serialize(self, src, dst) -> int:
# Write serialized bytes into dst; return bytes written.
...
def estimate_serialized_size(self, layout_desc) -> int:
# Upper bound on serialized byte size for this layout.
...
class MyDeserializer(Deserializer):
def deserialize(self, src, dst) -> None:
# Read serialized bytes from src, write into dst (KV-shaped).
...
def _create_mine(config: dict):
return AsyncSerdeProcessor(MySerializer(), MyDeserializer())
register_serde_factory("mine", _create_mine)
Reference it from your adapter config:
.. code-block:: json
{"type": "fs", "base_path": "/data", "serde": {"type": "mine"}}
Notes
-----
- **Buffer size.** ``estimate_serialized_size(layout)`` must return an
upper bound on the actual serialized output — include any safety
margin directly in the estimate (e.g., the built-in fp8 serializer
returns ``1.5 * num_elements``).
- **Failure handling.** If any step fails (serialize, store, load, or
deserialize), the whole submitted batch is reported as failed —
partial success within one batch is not surfaced. Failed keys are
cleaned up automatically.
- **Thread pool.** ``AsyncSerdeProcessor(max_workers=N)`` controls the
pool size. Transforms that release the GIL (e.g., torch ops)
benefit from ``N > 1``; pure-Python transforms do not.
Example
-------
An end-to-end script that starts an lmcache server with fp8 on a disk
adapter, runs vLLM, clears L1, and re-runs the same request to trigger
the L2 prefetch + fp8 deserialize path lives at
:file:`examples/serde/fp8/`. A pytest-based filesystem round-trip test
(no vLLM required) is at
:file:`tests/v1/distributed/serde/test_serde_fs_e2e.py`.
Compression methods
-------------------
Higher-level KV cache compression methods layered on top of the per-adapter
serde mechanism:
.. toctree::
:maxdepth: 1
cachegen