Tiered KV Cache Offloading in vLLM

发表时间: 2026-09 · Blog post by Or Ozeri, Danny Harnik, Ronen Schaffer, Itay Etelis, Varun Sundar Rabindranath (blog.vllm.ai)

原文: https://blog.vllm.ai/blog/2026-09-10-tiered-kv-offloading

作者:Or Ozeri, Danny Harnik, Ronen Schaffer, Itay Etelis, Varun Sundar Rabindranath

速读

一句话结论
vLLM 引入了一种以主机内存为中心的 KV Cache 分层卸载架构,通过将显存中被驱逐的 KV 数据异步转移到 CPU 内存、文件系统、对象存储或对等节点,在多轮长上下文对话场景下避免了重复的 Prefill 计算,使集群的有效服务容量和吞吐量翻倍。

要解决什么问题
在处理长上下文模型和多轮对话时,系统会生成海量的 KV Cache。当加速器显存(如 GPU HBM)耗尽时,原有的做法只能将之前计算好的 KV 数据直接驱逐丢弃。这导致了一个严重的计算卡点:当后续请求再次需要这些历史上下文时,vLLM 必须从头开始重新计算(Prefill)所有的 KV 数据。这种强制的重复计算不仅极大地浪费了算力,还显著增加了请求的延迟。在机制层面,这意味着集群在面对大量并发的多轮对话时,有效服务容量会遭遇显存墙的硬性瓶颈,一旦超出显存容量,系统的吞吐量就会因为无休止的上下文重算而断崖式下跌。

怎么做的
核心思路是构建一个以主机内存(CPU DRAM)为中心的多级缓存卸载与重载机制,所有 KV 数据流转必须经过主机内存。这种设计通过“即时分配”绕开了显存占用瓶颈:当显存不足时,KV 数据块通过异步 DMA 拷贝到主机内存,一旦这段本地 PCIe 传输完成,显存会被立即释放,而无需等待数据写入更慢的下级存储;重载时,也只有当数据在主机内存准备就绪后,才会申请分配显存。
关键设计由以下几个部件构成:
第一,统一的 I/O 与规范化内存布局。操作的基本单元是数据块(Chunk),可通过超参调节大小。在多卡张量并行(如 $TP=8$)环境下,框架会将分散在各卡的 KV 分片聚合成一个单一的共享主机内存区域。该区域采用规范化的内存布局,将不同层的块和所有注意力头组合成连续的字节缓冲区。这种与配置无关的格式使得不同节点、不同并行度甚至不同注意力后端(如 FlashAttention、FlashInfer、Triton)之间可以直接共享 KV 数据,无需格式转换。
第二,作为标准 LRU/ARC 缓存的主机主层。主机内存不是简单的中转站,而是真正的缓存层,数据在这里驻留以直接响应未来的命中请求。当主机内存耗尽时,才会根据淘汰策略将数据下沉到所有配置的次级层。
第三,多样化的次级存储层。包括基于内容寻址的文件系统(支持本地或 NAS)、兼容 S3 的对象存储,以及基于 RDMA 的 P2P 节点间传输。P2P 层允许进行纯主机到主机的数据传输,这在 Prefill/Decode 分离架构中极为有效:Prefill 节点计算出 Chunk 后直接在主机层暴露,Decode 节点通过 RDMA 拉取,将原本零碎的 GPU 间传输合并为少量大型网络传输,并支持块级别的计算与传输重叠。
第四,混合模型支持。框架将所有 KV 格式标准化为统一的字节缓冲区,无论块内包含的是全注意力、滑动窗口还是状态空间(如 Mamba)的数据,在主机端都具有固定的字节大小,从而透明地支持各种现代模型架构。

效果如何
实验在两张 NVIDIA H100 GPU($TP=2$)上进行,测试模型为 Qwen/Qwen3.6-35B-A3B。次级存储层配置为基于本地 NVMe 的文件系统。测试负载为多轮对话(12K token 初始提示词,每轮追加 4K token,共 8 轮),最大请求并发度设为 64,主要测量 Prefill 阶段的吞吐量。对比基线为代表传统路线的“无卸载(仅依赖 HBM)”方法,以及代表中间路线的“仅 CPU 卸载”方法。
量化结果显示,在并发对话数较少(64 个以内)时,所有方法均表现良好;当并发达到 64 至 128 个时,HBM 耗尽,无卸载基线的吞吐量急剧下降,而仅 CPU 卸载能维持性能;当并发超过 128 个、CPU 缓存也耗尽时,文件系统卸载展现出绝对优势,它维持了极高的缓存命中率,其吞吐量达到了其他强制重算替代方案的 2 倍以上。
作者也指出了该方法的代价与局限:存储层的物理延迟客观上高于 CPU 内存,因此在极端并发下无法达到理论上的峰值吞吐量;但在大规模并发场景下,与从头进行全量重算相比,从存储层读取带来的延迟代价微乎其微,整体收益依然具有决定性优势。

主要贡献

长上下文模型和多轮对话会生成海量的KV缓存(KV caches)。当加速器内存(例如GPU HBM)被填满时,先前计算的KV数据会被驱逐。在下一个需要该数据的请求到来时,vLLM必须从头开始重新计算。

为了解决这一问题,vLLM自v0.22版本起引入了分层KV缓存卸载(Tiered KV cache offloading)框架。该框架将由于容量不足被驱逐的KV数据保存在主机内存、外部存储以及远程节点中。通过从较低层级重新加载数据而非重新计算,该机制显著节省了计算资源、降低了延迟,并提升了集群的有效服务容量。

此外,借助二级存储层,KV数据实现了跨节点共享,这不仅支持了缓存的水平扩展,还允许新实例从共享存储中热启动,并支持在节点之间传输KV数据以实现分离式服务(disaggregated serving)或负载均衡。

设计原则与核心架构

以主机为中心的设计原则
该框架的核心设计原则是:所有的KV数据都必须流经主机内存(CPU DRAM)。在执行卸载操作时,KV数据首先从加速器移动到主机。随后,数据从主机传播到二级存储层(如文件系统、对象存储或远程节点)。在重新加载时,数据流向反转:二级存储层将数据提升至主机内存,然后再加载到加速器中。

所有的KV数据流经主机主层。二级存储层扩展了主机DRAM无法容纳的容量。在卸载时,数据块级联到所有层。在重新加载时,拥有该数据块的第一个层负责提供数据。
所有的KV数据流经主机主层。二级存储层扩展了主机DRAM无法容纳的容量。在卸载时,数据块级联到所有层。在重新加载时,拥有该数据块的第一个层负责提供数据。

这种设计带来了以下几个关键优势:

快速释放加速器与即时分配
从加速器到主机的复制过程利用了快速的本地PCIe传输。一旦该复制完成,加速器内存就会立即被释放,而无需等待任何二级存储层的传输启动。后续的存储写入、网络发送以及远程RDMA操作都直接基于主机副本异步进行,不再触及加速器内存。在重新加载时,只有当数据在主机中准备就绪后,才会分配加速器内存,而不是在等待层级数据传输时提前预留。这些机制共同创造了一种即时分配(just-in-time allocation)模式:加速器内存仅在实际需要时才被占用。

在t2时刻加速器内存被释放——即主机复制完成的瞬间。二级存储层的写入操作从主机副本异步继续进行。
在t2时刻加速器内存被释放——即主机复制完成的瞬间。二级存储层的写入操作从主机副本异步继续进行。

整合的I/O操作
在多加速器配置中(例如tensor_parallel_size=8),每个设备持有KV缓存的一个分片(shard)。该框架将所有的分片整合到一个单一的共享主机内存区域中。这种整合有效减少了I/O操作的数量并增大了单次操作的数据量,从而显著提升了存储和网络吞吐量。

多个加速器分片汇聚到一个共享的主机区域。二级存储层处理更少、更大的I/O操作——从而提高了存储和网络吞吐量。
多个加速器分片汇聚到一个共享的主机区域。二级存储层处理更少、更大的I/O操作——从而提高了存储和网络吞吐量。

规范的内存布局
主机区域使用一种规范的内存布局:每个页面存储一个层的一个块,来自所有张量并行(TP)rank的KV头被收集到一个单一的连续区域中。由于这种设计,定位任何数据块只需进行简单的偏移量计算。固定的主机端布局确保了即使在不同节点的GPU内存布局不同的情况下(例如不同的加速器类型、不同的注意力后端如FlashAttention/FlashInfer/Triton,或不同的并行配置),也能实现正确的共享。由于布局与配置无关,具有不同设置的节点可以直接共享KV数据,无需重新映射或格式转换。例如,TP=2的节点和TP=4的节点对于相同的KV数据会生成完全相同的主机端数据块表示。

简化的二级存储层
将所有数据路由通过主机内存使得二级存储层极易构建和操作。它们在每个vLLM实例中仅作为一个单一进程运行,使用标准的基于CPU的库(如POSIX I/O、S3 SDKs、RDMA verbs)来传输数据,并且永远不会触及加速器内存或相关的API。这彻底消除了跨多进程协调或理解特定加速器内存布局的复杂性。

方法细节

卸载与重载的工作原理
操作的基本单位是数据块(chunk)——它是一段包含一组token的固定大小的KV数据。默认情况下,一个数据块映射到一个单一的加速器块。系统提供了一个可配置的blocks_per_chunk参数,允许设置更大的数据块,从而为主机和二级存储层提供更大吞吐的I/O操作。

卸载路径(Offload path)
当发生卸载时,新的KV数据块通过异步DMA从加速器移动到主机。此时,加速器内存会立即被释放——这一过程在任何二级存储层传输开始之前就已经完成。接着,分层管理器会从主机副本中读取数据,并同时将数据块级联下发到所有已配置的二级存储层。主机主层是一个严格的LRU/ARC缓存,而不是一个短暂的暂存缓冲区。数据块会保留在主机内存中,并直接服务于未来的缓存命中。只有当主机容量耗尽时,最近最少使用的数据块才会被驱逐——即便如此,这些数据块依然存活在成功接收它们的二级存储层中。

重载路径(Reload path)
在处理请求时,调度器首先检查主机缓存——如果数据块存在,则直接命中。在主机未命中时,系统会按配置的顺序依次查询二级存储层;第一个拥有该数据块的层将负责提供数据。该层会异步地将数据块提升回主机内存;在此期间,调度器会收到一个RETRY信号,并在下一个调度周期重新检查状态。值得注意的是,在同一个请求中,不同的数据块可以由不同的层提供服务——例如,一个数据块可能来自文件系统,而另一个数据块可能来自远程节点。

文件系统层(Filesystem)
该层将每个KV数据块作为文件存储在本地或网络存储上。其采用了基于内容的寻址命名方式——相同的token序列会映射到相同的键,因此匹配的输入会自动共享缓存数据。当多个vLLM实例共享同一个存储挂载点(例如网络附加存储NAS,或同一节点上的多个实例)时,它们无需任何额外配置即可自动共享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"}]
        }
    }'

对象存储层(Object Storage)
该层通过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"
        }]
    }
}'

点对点层(Peer-to-Peer, P2P)
P2P层支持通过网络实现跨实例的KV缓存共享。该层使用ZMQ进行节点间的协调,并使用RDMA(通过NIXL)进行大容量数据传输。所有的传输都是主机对主机(host-to-host)的——两端都不涉及加速器内存。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}]
    }
}'

P2P层有两个关键的用例:

  1. Prefill/Decode分离(Prefill/Decode disaggregation):Prefill实例计算KV数据块并将其存放在其主机层中。Decode实例通过RDMA从Prefill实例的主机内存中拉取这些数据块。相比于基于GPU的P/D方法,其关键优势在于:整合的I/O将许多小的单GPU传输转化为更少、更大的RDMA操作,从而显著提高了网络吞吐量。此外,通过分块Prefill(chunked prefill),每个完成的Prefill数据块会立即可供传输——计算和数据移动得以重叠,有效减少了首字延迟(time-to-first-token)。
  2. 负载均衡(Load balancing):可以将KV数据块从过载的vLLM实例传输到有可用容量的实例。任何节点都可以从任何对等节点拉取数据块。

混合模型支持(Hybrid Model Support)
该框架与vLLM的混合内存分配器深度集成。能够透明地处理结合了不同层类型的模型(如全注意力、滑动窗口、MLA、Mamba)。规范的布局将所有的KV格式标准化为统一的字节缓冲区表示。无论包含哪种层类型,每个数据块在主机上都有固定的字节大小。不同的层类型将不同数量的token打包到同一个数据块中——例如,Mamba状态层每个数据块覆盖的token数量远多于全注意力层,因此它们被卸载的频率较低。这意味着:

  • 滑动窗口层(Sliding window layers)仅重新加载其窗口内的token,而不是完整的历史记录。
  • 状态空间层(State-space layers,如Mamba)会将其状态与注意力KV一起卸载和重载。
    该框架支持最先进的混合架构,包括DeepSeek V4、GLM 5.3、Nemotron 3等。

添加新的二级存储层
二级存储层的接口非常精简,仅包含四个核心方法:

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()时,该层直接写入该区域。不需要中间拷贝或序列化——该层直接操作主层的内存。每个二级存储层还独立管理自己的驱逐策略。官方提供了一个完整的内存参考实现,并且支持树外(Out-of-tree)二级存储层——只需在层配置中指定module_path,vLLM就会加载自定义的SecondaryTierManager实现,而无需对vLLM自身代码进行任何修改。

补充细节

可观测性(Observability)
该框架通过vLLM标准的/metrics端点暴露了丰富的Prometheus指标:

  • 主机缓存利用率(Host cache utilization):主层的当前填充率。
  • 传输吞吐量(Transfer throughput):加速器与主机之间传输的字节数和耗时。
  • 各层延迟(Per-tier latencies):每个层的查找和数据传输耗时。
  • 各层命中率(Per-tier hit rates):哪些层正在为当前工作负载提供服务。
    此外,二级存储层可以定义自定义指标(包括计数器、直方图、仪表盘),这些指标会被自动注册和暴露,完全不需要修改框架底层代码。

KV事件(KV Events)
当数据块在不同层之间移动时,框架会发出结构化的KV事件,详细报告哪些数据块被存储或驱逐、来自哪个层、以及具有何种局部性(本地或远程)。二级存储层也可以发出自己的事件。这些事件使得外部编排系统能够做出智能的路由决策。例如,llm-d和Dynamo等项目通过消费KV事件来将请求路由到最有可能产生缓存命中的实例——与无缓存感知的调度机制相比,这实现了显著更高的吞吐量和更低的延迟。此外,llm-d还利用这些事件来编排对等节点之间的P2P KV传输。

实验环境

  • 模型参数:Qwen/Qwen3.6-35B-A3B
  • 硬件配置:2× NVIDIA H100 GPU (张量并行度 TP=2)
  • 存储层配置:基于本地NVMe的文件系统后端(Filesystem backend)
  • 工作负载:多轮对话(multi-turn conversations),包含12K tokens的初始Prompt,每轮生成4K tokens,共进行8轮。
  • 最大请求并发数:64
  • 测试指标:仅测量Prefill实例的吞吐量(在Prefill-Decode分离架构下进行评估)。

实验结果

实验的核心目的在于验证KV缓存卸载能够通过从更廉价的存储层重新加载KV数据,来避免昂贵的重复Prefill计算。实验在不同并发对话规模下对比了无缓存(No Caching)、CPU卸载(CPU Offloading)和存储卸载(Storage Offloading)的表现:

  • 最多约64个并发对话:此时GPU的HBM足以容纳整个工作集,所有的缓存方法都表现良好,吞吐量相近。
  • 64–128个并发对话:HBM被占满。如果不使用卸载技术(No Caching),吞吐量会急剧下降。而引入CPU卸载可以有效维持较高的性能表现。
  • 超过128个并发对话:此时CPU缓存也被占满。存储卸载(Storage offloading)机制继续提供高缓存命中率,与替代方案相比,其吞吐量翻了一倍以上。
不同并发规模下的吞吐量对比图。存储层的延迟高于CPU内存,因此未能达到峰值吞吐量。但在大规模并发下,相比于完全重新计算,基于存储的缓存命中具有决定性优势。
不同并发规模下的吞吐量对比图。存储层的延迟高于CPU内存,因此未能达到峰值吞吐量。但在大规模并发下,相比于完全重新计算,基于存储的缓存命中具有决定性优势。

分析表明,虽然存储(NVMe)的延迟高于CPU内存,导致其未能达到理论峰值吞吐量,但在大规模并发场景下,系统面临的选择是"基于存储的缓存命中"与"完全重新计算"。实验结果决定性地证明了存储卸载在维持系统高吞吐量方面的巨大优势。

结论

本文介绍了vLLM中引入的分层KV缓存卸载框架,该框架通过将KV数据流经主机内存并扩展至文件系统、对象存储和P2P网络,成功解决了长上下文和多轮对话中因GPU显存瓶颈导致的重复计算问题。其以主机为中心的设计实现了GPU显存的快速释放与即时分配、I/O操作的整合,并提供了兼容多种底层模型架构(如全注意力、滑动窗口、Mamba等)的规范内存布局。实验证明,在大规模并发场景下,存储卸载不仅显著提升了系统的有效服务容量,还成倍提高了整体吞吐量。该机制为Prefill/Decode分离架构的发展与跨节点负载均衡提供了坚实的基础设施支持。