192 lines
5.5 KiB
ReStructuredText
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
|