KV Cache 压缩#

LMCache 支持 按适配器独立配置的 serde,在 KV Cache 数据进出 L2 适配器时对其进行转换。典型用途:量化(缩小存储占用)、压缩、加密。

何时使用 serde#

  • 节省 L2 存储或带宽。 fp8 量化相比于 bf16 将字节体积减半,且准确度损失较小——非常适合磁盘/远程适配器。

  • 静态加密。 在原始字节写入磁盘之前,使用经过身份验证的加密进行包装。

  • 自定义压缩。 任何无损(lz4/zstd)或有损(CacheGen 风格)都可以通过 Serializer / Deserializer ABC 插入。

Serde 是 按适配器可选启用 的:一个 --l2-adapter 可以使用 fp8,而另一个则存储原始字节。若省略,适配器行为与未使用 serde 时完全一致(无额外内存分配,无额外线程)。

在 L2 适配器上配置 serde#

向任何 --l2-adapter JSON 规范添加一个 "serde" 子字典。type 字段选择一个已注册的 serde;其余键将转发到 serde 工厂。

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"}
    }'
内置 serde 类型#

type

描述

配置字段

fp8

将每个元素量化为 8 位浮点数;在加载时反量化。虽然有损,但高度可压缩。

fp8_dtype(默认 float8_e4m3fn;也接受 float8_e5m2),max_workers(线程池大小,默认 1)

turboquant

在 L2 存储之前使用 TurboQuant 预设压缩 KV 张量,并在加载时重构它们。

preset``(默认 ``turboquant_k8v4),``head_dim``(可选,默认 128),``block_size``(默认 16),``max_workers``(线程池大小,默认 1)

aesgcm

AES-GCM authenticated encryption of KV bytes at rest in L2, keyed per cache_salt.

key_provider (default hkdf), master_key_path (required for hkdf), aes_bits (128 default, or 256), max_workers (thread pool size, default 1)

TurboQuant 序列化与反序列化#

TurboQuant serde 可以通过在适配器 serde 配置中设置 "type": "turboquant" 来启用。如果省略 preset,TurboQuant serde 默认使用 turboquant_k8v4

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
        }
    }'

支持的预设:

预设

键路径

值路径

turboquant_k8v4

FP8 键

4位值量化

turboquant_4bit_nc

带有范数校正的 4 位 MSE 键

4位值量化

turboquant_k3v4_nc

带有范数校正的 3 位 MSE 密钥

4位值量化

turboquant_3bit_nc

带有范数校正的 3 位 MSE 密钥

3位值量化

Encryption serde#

The aesgcm serde encrypts KV bytes with AES-GCM before they land in L2 and decrypts them on load, so a party who can read the remote storage (bucket / disk / RESP) cannot recover cache contents. Each tenant's data is encrypted under a distinct key derived from its cache_salt.

lmcache server \
    --l1-size-gb 100 \
    --eviction-policy LRU \
    --l2-adapter '{
        "type": "s3",
        "bucket": "my-kv-cache",
        "serde": {
            "type": "aesgcm",
            "key_provider": "hkdf",
            "master_key_path": "/etc/lmcache/keys/master"
        }
    }'

Provide the master key as a file (e.g. a mounted Kubernetes Secret). The hkdf provider reads it once at startup and derives a per-cache_salt key via HKDF-SHA256; the master key is never written to L2. aes_bits defaults to 128 (already unbreakable and slightly faster); set 256 if a compliance mandate requires it.

警告

The hkdf provider derives every tenant's key from one shared master key, so any server holding the master can decrypt any tenant's data ("fleet vs. outside" trust). It is not per-tenant access isolation.

编写自定义 serde#

实现两个同步 ABC(SerializerDeserializer),并使用您的转换逻辑,然后注册一个以您选择的名称为键的工厂:

# 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, key) -> int:
        # Write serialized bytes into dst; return bytes written.
        # ``key`` is the object's ObjectKey; ignore it unless your
        # transform is keyed per object (e.g. encryption keyed on
        # cache_salt).
        ...

    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, key) -> None:
        # Read serialized bytes from src, write into dst (KV-shaped).
        # ``key`` mirrors serialize; ignore it unless keyed per object.
        ...

def _create_mine(config: dict):
    return AsyncSerdeProcessor(MySerializer(), MyDeserializer())

register_serde_factory("mine", _create_mine)

从您的适配器配置中引用它:

{"type": "fs", "base_path": "/data", "serde": {"type": "mine"}}

注意事项#

  • 缓冲区大小。 estimate_serialized_size(layout) 必须返回实际序列化输出的上限——安全余量应直接计入估算值(例如,内置的 fp8 序列化器返回 1.5 * num_elements)。

  • Raw-byte output. If your serde writes bytes directly (rather than through MemoryObj.tensor like the quantizers), reach the buffer via MemoryObj.byte_array and cast it to the native format first: memoryview(dst.byte_array).cast("B"). byte_array is a ctypes-backed view with format "<B", and CPython does not support slice assignment into a non-native format (dst[i:j] = ... raises NotImplementedError: memoryview: unsupported format <B). The built-in aesgcm serde is an example.

  • 失败处理。 如果任何步骤失败(序列化、存储、加载或反序列化),整个提交的批次均报告为失败——批次内的部分成功不会对外呈现。失败的键会自动清理。

  • 线程池。 AsyncSerdeProcessor(max_workers=N) 控制池的大小。释放全局解释器锁(GIL)的变换(例如,torch 操作)可以从 N > 1 中受益;纯 Python 变换则不然。

示例#

一个端到端的脚本,它在磁盘适配器上启动一个带有 fp8 的 lmcache 服务器,运行 vLLM,清除 L1,然后重新运行相同的请求以触发 L2 预取 + fp8 反序列化路径,位于 examples/serde/fp8/。一个基于 pytest 的文件系统往返测试(不需要 vLLM)位于 tests/v1/distributed/serde/test_serde_fs_e2e.py

压缩方法#

构建于按适配器 serde 机制之上的高阶 KV Cache 压缩方法: