二级 KV 存储#

LMCache MP 模式(多进程)支持两级存储架构:

  • L1 (快速层) -- 默认情况下为 CPU 内存,或通过 GPUDirect Storage (cuFile) 的 NVMe 块,当设置 --gds-l1-path 时,由 L1 管理器管理。所有 KV Cache 块在活跃使用期间都存放在这里。(在 GDS L1 层下,不支持字节数组 L2 适配器,因为它不暴露任何 L1 内存缓冲区。)

  • L2 (持久性) -- 持久存储后端(基于 NIXL 或普通文件系统/原始块)。StoreController 异步将数据从 L1 推送到 L2,而 PrefetchController 在缓存未命中时将数据从 L2 加载回 L1。

数据流#

写入路径 (L1 -> L2):

  1. vLLM 通过 STORE RPC 将 KV Cache 块存储到 L1。

  2. StoreController 通过 eventfd 检测新对象,并异步将存储任务提交到每个已配置的 L2 适配器。

  3. L2 适配器将数据写入其后端(例如,通过 GDS 的本地 SSD)。

读取路径 (L2 -> L1):

  1. LOOKUP RPC 检查 L1 中的前缀缓存命中情况。

  2. 对于在 L1 中未找到的键,PrefetchController 向 L2 适配器提交查找请求。

  3. 如果在 L2 中找到,数据将被加载回 L1,并为待处理的 RETRIEVE RPC 加读锁。

适配器类型#

LMCache 提供了几种 L2 存储后端,按介质分组在 支持的后端 下。使用 --l2-adapter 标志选择一个或多个。

多个适配器(级联)#

您可以通过重复 --l2-adapter 参数来配置多个 L2 适配器。适配器按指定的顺序使用。StoreController 将数据推送到所有配置的适配器,而 PrefetchController 在查找期间按顺序查询适配器。

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

存储和预取策略#

存储策略 控制键从 L1 流向 L2 的方式:哪些适配器接收每个键,以及在成功存储到 L2 后是否从 L1 删除键。 预取策略 控制键从 L2 流回 L1 的方式:当多个适配器具有相同的键时,策略决定哪个适配器加载它。

通过 CLI 选择策略:

--l2-store-policy default \
--l2-prefetch-policy default

内置策略:

标志

名称

行为

--l2-store-policy

default

将所有键存储到所有适配器中。永不从 L1 中删除。

--l2-store-policy

skip_l1

仅缓冲区模式。将所有键存储到所有适配器,然后立即 从 L1 中删除它们。与 --eviction-policy noop 配对以避免无用的 LRU 开销。

--l2-prefetch-policy

default

对于每个键,选择第一个(索引最低的)拥有该键的适配器。预取的键是临时的(在读取完成后删除)。

--l2-prefetch-policy

retain

default 相同的加载计划,但预取的键将永久保留在 L1 中。当预取的数据可能被后续请求重用时(例如,共享系统提示块),此策略非常有用。

预取并发性#

--l2-prefetch-max-in-flight 标志限制了 PrefetchController 在任意时刻可以并发进行的预取请求数量。较大的值会提升 L2 到 L1 的吞吐量,但也会增加在途数据对 L1 内存的压力。

标志

默认

描述

--l2-prefetch-max-in-flight

8

最大并发预取请求数。

仅缓冲区模式#

当 L1 仅作为写缓冲区使用(所有数据存储在 L2 中)时,使用 --l2-store-policy skip_l1 以及 --eviction-policy noop。此组合会在数据存储到 L2 后立即从 L1 中删除键,并完全禁用 LRU 逐出跟踪器,从而减少内存和 CPU 开销。

--eviction-policy noop \
--l2-store-policy skip_l1 \
--l2-prefetch-policy default

策略是可扩展的——可以通过在 storage_controllers/ 中创建文件并在导入时调用 register_store_policy()register_prefetch_policy() 来添加新策略。有关详细信息,请参阅设计文档 l2_adapters/design_docs/overall.md

序列化与反序列化(压缩 / 量化)#

每个适配器可以选择性地运行一个 serde(序列化器/反序列化器),在数据进出 L2 时进行转换——例如,针对磁盘后端的 fp8 量化,或针对远程适配器的加密。有关详细信息和配置,请参见 KV Cache Compression

逐出#

LMCache 支持在两个存储层次上进行逐出,以便每个层次都可以在固定的容量预算内运行。

L1 逐出#

L1 逐出运行一个后台线程,监控整体 L1 内存使用情况。当使用量超过 trigger_watermark 时,逐出策略会逐出一部分最近最少使用的键。

命令行标志:

标志

默认

描述

--eviction-policy

(必需)

策略名称:LRUnoop

--eviction-trigger-watermark

0.8

触发逐出的 L1 使用比例 [0, 1]。

--eviction-ratio

0.2

每个周期逐出的当前分配的 L1 内存的比例。

示例:

--eviction-policy LRU \
--eviction-trigger-watermark 0.8 \
--eviction-ratio 0.2

L2 逐出#

L2 逐出是按适配器独立配置可选启用的。每个适配器可以通过在其 --l2-adapter JSON 规范中添加 "eviction" 子对象来独立声明逐出策略。没有 "eviction" 键的适配器不启用逐出控制器。

当为适配器启用 L2 逐出时,一个专用的后台线程会监控该适配器的 get_usage() 值。一旦使用量超过 trigger_watermark,该策略将逐出键,直到使用量降低 eviction_ratio

``"eviction"`` 子对象字段:

字段

默认

描述

eviction_policy

(必需)

策略名称:"LRU""noop"

trigger_watermark

0.8

触发逐出的适配器使用比例 [0, 1]。

eviction_ratio

0.2

每个周期逐出的已用容量的比例。

示例 — 使用 LRU 逐出的 nixl_store:

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

适配器支持:

适配器

L2 逐出支持

nixl_store

完全支持。delete 释放池槽;固定的键(正在进行的加载)会被跳过,并在下一个周期重试。

nixl_store_dynamic

完全支持。delete 从磁盘中删除数据文件;被固定的键会被跳过。get_usage 是基于字节的 (_total_bytes / max_capacity_bytes)。

mock

完全支持。适用于在无真实存储硬件的情况下测试逐出行为。

raw_block

完全支持共享/全局逐出。delete 回收原始块槽;被锁定的条目会被跳过,并在下一个周期重试。

s3

delete 从存储桶中移除对象并释放聚合字节计数。当 max_capacity_gb0(禁用)时,get_usage 报告 usage_fraction == -1.0;设置非零的 max_capacity_gb 以启用基于水印的逐出控制器。

hfbucket

delete 从桶中移除对象并释放聚合字节计数。当 max_capacity_gb0(禁用)时,get_usage 报告 usage_fraction == -1.0;设置非零的 max_capacity_gb 以启用基于水印的逐出控制器。锁定的键(正在加载的)会被跳过。

dax

完全支持。delete 会立即从内存索引中移除未锁定的键,并在活跃读取借用排空后回收固定槽。使用量以槽数为单位计算。

mooncake_store

不支持逐出(原生连接器适配器)。

fs

不支持逐出(deleteget_usage 均为空操作)。

原生连接器

不支持逐出。

备注

每个 L2 适配器实例都有自己独立的逐出控制器和策略。两个相同类型的适配器可以有不同的水位线或策略。

L1 + L2 逐出示例#

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

在此设置中:

  • 当 L1 的内存使用达到 80% 时,它会逐出内存,每个周期回收 20% 的分配内存。

  • L2 (NIXL/GDS) 在存储池中当 90% 的池槽被占用时进行逐出,每个周期回收 10%。

  • 两个层级使用独立的 LRU 策略,因此每个层级逐出其自身的最近最少使用的键。

验证 L2 存储#

设置 LMCACHE_LOG_LEVEL=DEBUG 以在服务器日志中查看 L2 活动:

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

L2 运行时的预期日志消息:

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