vLLM 发布分层 KV 缓存卸载框架
Tiered KV Cache Offloading in vLLM
vLLM 推出分层 KV 缓存卸载框架,将淘汰的 KV 数据经主机内存级联到文件系统、对象存储或远程节点,避免重复预填充。该框架自 v0.22 起可用,所有 KV 数据统一流经主机内存,二级层以单进程方式运行且不接触加速器内存。官方基准显示,在 Qwen3.6-35B-A3B 双 H100 的多轮对话负载下,超过 128 路并发时存储层卸载的吞吐比无卸载方案高出一倍以上。
vLLM 官方详解分层 KV 缓存卸载的架构与三种二级存储,并给出并发规模下的吞吐对比数据。
长上下文模型和多轮对话会生成海量的 KV 缓存。 当加速器内存(例如 GPU HBM)被占满时,先前计算出的 KV 数据会被逐出。 在下一次需要它的请求中,vLLM 必须从头重新计算。
分层 KV 缓存卸载会将逐出的 KV 数据保留下来,跨越主机内存、存储和远程对等节点。 vLLM 无需重新计算,而是从较低层级重新加载数据——节省算力、降低延迟,并提升集群的有效服务容量。
借助次级层级,KV 数据也变得可在节点间共享——从而实现缓存的水平扩展、从共享存储热启动新实例,以及在节点间传输 KV 数据以支持分离式服务或负载均衡。
该框架自 v0.22 起已在 vLLM 中可用,详细使用指南见此处。
以主机为中心的设计
核心设计原则:所有 KV 数据都流经主机内存(CPU DRAM)。
卸载时,KV 数据先从加速器移动到主机。 从主机出发,它再传播到次级层级——文件系统、对象存储或远程对等节点。 重新加载时,流程相反:次级层级将数据提升到主机内存,然后再加载到加速器。
这一设计带来了几个关键优势:
快速释放加速器内存,即时分配
从加速器复制到主机是快速的本地 PCIe 传输。 此复制一完成,加速器内存即被释放——早于任何次级层级传输开始。 存储写入、网络发送和远程 RDMA 都从主机副本继续进行,不再触碰加速器内存。 重新加载时,只有在数据于主机中准备就绪后才会分配加速器内存——而不是在等待层级传输期间提前预留。 两者结合,形成了一种即时分配模式:加速器内存仅在活跃需要时才被占用。
整合 I/O
在多加速器配置中(例如 tensor_parallel_size=8),每个设备持有 KV 缓存的一个分片。
该框架将所有分片整合到单个共享的主机内存区域。
规范内存布局
主机区域使用规范内存布局:每个页存储某一层的一个块,来自各 TP 秩的所有 KV 头被汇聚到一个连续的单一区域中。定位任何数据块都只是简单的偏移计算。 固定的主机侧布局确保了即使各节点的 GPU 内存布局不同,也能正确共享——不同的加速器类型、注意力后端(FlashAttention、FlashInfer、Triton)或并行配置都映射到相同的规范表示。 由于该布局与配置无关,不同配置的节点可以直接共享 KV 数据——无需重新映射或格式转换。 对于相同的 KV 数据,TP=2 节点和 TP=4 节点会生成完全相同的主机侧数据块。
简单的次级层级
将所有数据经由主机内存路由,使二级存储层易于构建和运维。 它们每个 vLLM 实例只有一个进程,使用标准的基于 CPU 的库(POSIX I/O、S3 SDK、RDMA verbs)传输数据,并且从不接触加速器内存或 API。 无需跨多个进程协调,也无需理解特定于加速器的内存布局。
卸载与重新加载的工作原理
操作单元是块(chunk)——覆盖一组 token 的固定大小 KV 数据片段。
默认情况下,一个块映射到单个加速器 block。
一个可配置的 blocks_per_chunk 参数允许使用更大的块,从而向主机和二级存储层产生更大的 I/O。
卸载路径
新的 KV 块通过异步 DMA 从加速器移动到主机。 加速器内存会立即释放——在任何二级存储层传输开始之前。 随后,分层管理器从主机副本读取,将块同时级联到所有已配置的二级存储层。
主机主存储层是一个真正的 LRU/ARC 缓存,而不是暂存缓冲区。 块保留在主机内存中,直接服务未来的命中。 只有当主机容量耗尽时,最近最少使用的块才会被驱逐——即便那时,它们仍会存留在接收它们的任一二级存储层中。
重新加载路径
调度器首先检查主机缓存——如果块在那里,就是立即命中。
主机未命中时,按配置的顺序查询二级存储层;第一个持有该块的存储层为其提供服务。
该存储层会异步地将块提升回主机内存;在此期间,调度器收到一个 RETRY,并在下一个周期重新检查。
同一请求中的不同块可以由不同的存储层提供服务——例如,一个块来自文件系统,另一个来自远程对等节点。
二级存储层
文件系统
将每个 KV 块作为文件存储在本地或网络存储上。 使用内容寻址命名——相同的 token 序列映射到相同的键,因此匹配的输入会自动共享缓存数据。
当多个 vLLM 实例共享同一存储挂载点(例如网络附加存储,或同一节点上的多个实例)时,它们自动共享 KV 数据,无需额外配置。
亮点:
- 非阻塞查找
- 原子写入
- 独立的读/写线程池
vllm serve Qwen/Qwen3.6-35B-A3B \
--kv-transfer-config '{
"kv_connector_extra_config": {
"spec_name": "TieringOffloadingSpec",
"cpu_bytes_to_use": 107374182400,
"secondary_tiers": [{"type": "fs", "root_dir": "/mnt/kv-cache"}]
}
}'对象存储
通过 NIXL 将 KV 块存储在兼容 S3 的对象存储中。 与文件系统层相同的内容寻址方案。 提供了一种经济高效的网络存储选项——通常每 GB 比高性能文件存储更便宜,同时仍能实现跨实例的共享访问。
--kv-transfer-config '{
"kv_connector_extra_config": {
"spec_name": "TieringOffloadingSpec",
"cpu_bytes_to_use": 107374182400,
"secondary_tiers": [{
"type": "obj",
"bucket": "my-kv-cache",
"endpoint_override": "http://minio:9000"
}]
}
}'点对点(P2P)
支持通过网络进行跨实例 KV 缓存共享。 使用 ZMQ 进行协调,使用 RDMA(通过 NIXL)进行批量数据传输。 所有传输都是主机到主机——两端均不涉及加速器内存。
P2P 层不决定从哪个对等节点拉取 KV 数据——那是编排层的职责(例如 llm-d 这样的路由器)。编排器通过请求的 kv_transfer_params 驱动跨节点传输。示例和更多细节可在使用指南中找到。
--kv-transfer-config '{
"kv_connector_extra_config": {
"spec_name": "TieringOffloadingSpec",
"cpu_bytes_to_use": 107374182400,
"secondary_tiers": [{"type": "p2p", "host": "10.0.0.1", "port": 5710}]
}
}'两个关键用例:
预填充/解码分离
预填充实例计算 KV 块,并使其在其主机层中可用。 解码实例通过 RDMA 从预填充器的主机内存中拉取它们。
相比基于 GPU 的 P/D 方案,一个关键优势是:整合 I/O 将许多小的每 GPU 传输合并为更少、更大的 RDMA 操作——大幅提升网络吞吐量。 此外,借助 分块预填充,每个完成的预填充块立即可用于传输——计算与数据移动重叠,减少首 token 时间。
负载均衡
将 KV 块从过载的 vLLM 实例传输到有可用容量的实例。 任何节点都可以从任何对等节点拉取块。
有关使用 llm-d 进行 P2P KV 缓存共享的更多信息,请参阅这篇博客文章。
混合模型支持
该框架与 vLLM 的混合内存分配器集成。 结合不同层类型的模型——全注意力、滑动窗口、MLA、Mamba——均被透明处理。
规范布局将所有 KV 格式归一化为统一的字节缓冲区表示。 每个块在主机上具有固定的字节大小,无论其包含哪些层类型。 不同层类型将不同数量的 token 打包到同一个块中——例如,Mamba 状态层每个块覆盖的 token 数量远多于全注意力层,因此它们被卸载的频率更低。
这意味着:
- 滑动窗口层仅重新加载其窗口内的 token,而非完整历史
- 状态空间层(Mamba)将其状态与注意力 KV 一起卸载和重新加载
该框架支持最先进的混合架构,包括 DeepSeek V4、GLM 5.3、Nemotron 3 等。
可观测性
该框架通过 vLLM 的标准 /metrics 端点暴露 Prometheus 指标:
- 主机缓存利用率——主层的当前填充率
- 传输吞吐量——加速器 ↔ 主机传输的字节数和时间
- 各层延迟——每层的查找和数据传输耗时
- 各层命中率——哪些层正在为你的工作负载提供服务
次级层可以定义自定义指标(计数器、直方图、仪表),这些指标会自动注册并暴露——无需修改框架。
KV 事件
当块在层之间移动时,框架会发出结构化的 KV 事件,报告哪些块被存储或驱逐、来自哪一层以及具有何种局部性(本地 vs. 远程)。 次级层可以发出自己的事件。
这些事件使外部编排系统能够做出智能路由决策。 诸如 llm-d 和 Dynamo 等项目消费 KV 事件,以将请求路由到最有可能缓存命中的实例——与缓存无感知调度相比,实现显著更高的吞吐量和更低的延迟。 此外,llm-d 使用这些事件来编排对等节点之间的 P2P KV 传输。
添加新的次级层
次级层接口非常精简——四个核心方法:
class SecondaryTierManager(ABC):
def lookup(self, key, req_context) -> LookupResult:
"""Does this tier have a chunk? Returns HIT, MISS, or RETRY."""
def submit_store(self, job_metadata: JobMetadata) -> None:
"""Start async store from host to this tier."""
def submit_load(self, job_metadata: JobMetadata) -> None:
"""Start async load from this tier to host."""
def get_finished_jobs(self) -> Iterable[JobResult]:
"""Poll completed transfers."""每个层在构造时都会获得一个指向共享主机区域的直接 memoryview。
当调用 submit_store() 时,该层直接从该区域读取 KV 数据。
当调用 submit_load() 时,该层写入其中。
无需中间副本或序列化——该层直接操作主层的内存。
每个次级层还独立管理自己的驱逐策略。
完整的内存参考实现可在 vllm/v1/kv_offload/tiering/example/ 获取。
支持树外二级层级——在层级配置中指定 module_path,vLLM 即可加载你的自定义 SecondaryTierManager 实现,无需对 vLLM 本身做任何代码改动。
性能——扩展到更多用户
KV 缓存卸载的主要优势:通过从更廉价的层级重新加载 KV 数据,避免代价高昂的重复预填充。
在并发对话较少时,所有缓存方法都能实现高吞吐——加速器内存足以容纳全部内容。 随着对话池增长,每个层级都会出现容量限制:
- 最多约 64 个对话——HBM 足以容纳工作集;所有缓存方法表现良好。
- 64–128 个对话——HBM 被填满;若不进行卸载,吞吐量会急剧下降。CPU 卸载可维持性能。
- 超过 128 个对话——CPU 缓存也被填满。存储卸载仍能保持高缓存命中率,吞吐量相比其他方案提升一倍以上。
存储的延迟高于 CPU 内存,因此无法达到峰值吞吐。 但在大规模场景下,选择在于存储支持的缓存命中与完全重新计算之间——存储明显胜出。
基准测试设置:
- 模型:Qwen/Qwen3.6-35B-A3B,运行于 2× NVIDIA H100(TP=2)
- 存储层级:本地 NVMe 上的文件系统后端
- 工作负载:多轮对话,初始提示 12K token + 每轮 4K token,共 8 轮
- 最大请求并发数:64
- 仅测量预填充器吞吐量(预填充-解码分离)
完整性能结果与复现脚本可在 neuralmagic/fs-offload-experiments 获取。
致谢
我们感谢 Liran Schour、Chang Guo、Srinivas Krovvidi、Rotem Shavitt、Effi Ofer、Omer Paz、Kfir Toledo 和 Michal Malka 对分层 KV 缓存卸载框架设计与实现的贡献,也感谢所有其他贡献代码、评审和反馈的社区成员。
来源:vLLM Blog · vllm.ai