导言
我之前在 verl 里见过两种 vLLM offload 级别,后来又接触到 vLLM 的 KV offloading。再往 Mooncake 看,项目里似乎已经有 GDS。于是看到 vLLM Issue #48504 还要做 “GDS + KV offloading” 时,一个很自然的困惑出现了:这些机制是不是在重复做同一件事?
真正需要拆开的不是两个实现,而是三种不同的 offload 语义,以及两类不同的 GPU Direct。本文沿着实际数据路径,解释当前 vLLM 会自动做什么、Mooncake 怎样通过 Connector、Store 与 Transfer Engine 的解耦透明接入 GDS,以及 #48504 为什么仍然有独立价值。
时间与证据边界
本文整理的是截至 2026-08-31 的公开状态,依据 vLLM、verl 和 Mooncake 的官方文档、源码与 GitHub Issue/PR。开放 PR 的状态与后续设计可能继续变化。
同一个词,三套语义¶
困惑首先来自 “offload” 这个词。它在 verl、vLLM 引擎和 KV Cache 系统里都出现,但管理的对象、触发时机和生命周期并不相同。
| 机制 | 管理对象 | 主要目的 | 是否保留可复用 KV |
|---|---|---|---|
verl 调用的 vLLM sleep(level=1/2) |
模型权重与运行时显存 | 训练与 rollout 共卡时让出显存 | 否,sleep/wake 会重置 prefix cache |
| vLLM Model Runner V1/V2 | 模型执行实现 | 演进执行器与 Kernel 调用路径 | 不是 KV offload 协议 |
vLLM OffloadingConnector |
可按前缀复用的 KV block | 把 GPU prefix cache 扩展到更大、更慢的层级 | 是 |
如果我在 verl 里看到的是 sleep(level=1/2),那么它们的差别是:
- Level 1:把模型权重卸载到 CPU,丢弃 KV Cache。
- Level 2:进一步丢弃 GPU 中的权重与 KV Cache,后续依靠 verl 的权重同步恢复模型。
这是一种引擎生命周期切换,服务于训练 Actor 与推理 Rollout 之间的显存交接,并不保存下一次请求可以复用的 prompt KV。verl 会根据 LoRA、MTP、NPU 等情况选择 sleep level。verl 实现 与 vLLM sleep 语义 可以对应上这条路径。
Model Runner V1/V2 又是另一回事。它们描述模型怎样执行,不是两代 KV offloading。vLLM 的 Q3 2026 Roadmap 也把 Model Runner V2 迁移与 KV offload 分别列为独立工作。
当前 KV offloading 怎样运行¶
vLLM 原生 OffloadingConnector 更像 GPU prefix cache 的分层扩展。Scheduler 侧的 OffloadingManager 负责 block 查找、命中、分配和淘汰决策,Worker 侧的 OffloadingWorker 负责异步搬运;KV 则以 prefix block hash 为 key。vLLM KV Offloading 文档 给出的当前层级是:
新产生的可复用 KV block
├── 继续留在 GPU prefix cache
└── 异步复制到 CPU primary
└── FS / Object Store / P2P secondary
后续请求命中:secondary → CPU → GPU
默认情况下,系统只 offload prompt/prefill KV;配置后也可以存 decode KV。再次遇到相同前缀时,vLLM 会查找最长可用命中,并异步把 block 提升回 GPU。
自动不等于按水位换出
当前机制已经会在启用后自动 store、lookup 和 load,但它不是等到 HBM 紧张才把冷 block swap 到下层。它更接近 write-through cache extension:合格 KV 在产生后被异步复制,原副本暂时仍可留在 GPU;GPU prefix cache 何时淘汰它,是另一套缓存决策。
因此,今天的“自动”仍有边界:容量、存储层和策略需要显式配置,系统不会自动发现 SSD,也不会在每个请求到来时自动判断应走 GDS、CPU bounce 还是重新 Prefill。
GDS RFC 补的是哪条路径¶
当前 multi-tier 设计有一个关键约束:CPU 是 primary,也是 secondary tier 的 gateway。 文件系统、对象存储和 P2P secondary 接收的是 CPU buffer,看不到 GPU pointer。这一点不是某个传输后端恰好没优化,而是当前 manager 接口的边界。Multi-tier KV Offloading RFC 也明确把 secondary 直接访问 GPU 排除在原始范围之外。
下面这张图只看数据面,最容易看出三条路径的差别。
图:根据会话内容整理的自绘示意图。图中强调的是 CPU 是否位于文件 I/O 数据路径,以及缓存控制面由谁拥有;它不表示所有 Mooncake 部署都使用同一种 SSD 后端。
Issue #48504 想补齐的正是第二列:
- 把 GPU block 到 pointer/span 的解析下沉为公共 worker 能力。
- 完成原生
FilesystemSpec的 load 与 store。 - 为文件系统提供 CPU bounce、直接 GDS、NIXL GDS 等可插拔 transport。
- 后续通过
MultiConnector把 tiered offload 与 GDS 文件连接器组合起来。
最终的数据通路可以从:
缩短为:
这也是为什么不能简单地“给现有 secondary tier 加一个 GDS 开关”:现有接口只拿到 CPU memory view,而 GDS 后端需要 GPU 地址、文件映射和相应的完成语义。
这里的关键不是“vLLM 此前完全无法使用 GDS”,而是此前可以通过 Mooncake 等 external connector 获得这类能力,#48504 则把它内生化到 vLLM 原生 OffloadingConnector / FilesystemSpec 框架。所谓“把能力从 Mooncake 上移”可以作为架构上的直观说法,但不是把 Mooncake 的代码直接抽进 vLLM:RFC 初版选择的是 NIXL GDS,外部 Connector 的数据格式与控制面也不因此被替换。
Mooncake 为什么不是重复实现¶
这里还要再拆开两种经常被统称为 GPU Direct 的技术:
- GPUDirect RDMA:GPU 与远端内存之间通过 NIC 直接传输。
- GPUDirect Storage:GPU 与文件或 NVMe 之间通过 cuFile 等路径直接传输。
vLLM 当前的 Mooncake 接入也有两个方向:MooncakeConnector 面向 disaggregated prefill 的 P→D KV 传输;MooncakeStoreConnector 则把 Mooncake 当成外部的分布式、按 hash 寻址的 KV Cache,由 Mooncake 管理全局元数据、placement、复制和淘汰。vLLM Mooncake Store 文档 描述的是后一种边界。
这里需要纠正一个过于保守的判断:Connector、Store 与 Transfer Engine 是分层解耦的。 Connector 只表达 Put/Get 与 buffer,Store 管理对象、placement、replica 和生命周期,真正的数据搬运再交给 Transfer Engine。因而上层 Connector 不必感知每一种存储传输实现:当 Store 把本地 SSD 映射为 File Segment,并由 TENT selector 选择 GdsTransport 时,同一条 MooncakeStoreConnector 路径可以透明使用 GDS。
vLLM MooncakeStoreConnector
│ Put / Get + buffer
▼
Mooncake Store
│ object / placement / replica / lifecycle
▼
Transfer Engine / TENT
├── GdsTransport ───────→ File Segment / Local SSD
└── Host staging 等路径 ─→ Local SSD
Mooncake TENT 已经实现可选择的 GDS transport;我此前对其 File Segment、transport selector、GdsTransport 与 cuFile batch 路径的梳理见 Mooncake TENT GDS。与此同时,公开的 SSD Offload Design 也描述了 ClientBuffer 或 pinned host staging。这两者并不矛盾:前者证明架构和后端支持 GDS,后者说明 host staging 仍是可用的另一条实现或部署路径。
| 判断层次 | 结论 | 还需要确认什么 |
|---|---|---|
| 架构是否支持 GDS | 支持 | Connector、Store、TE 的接口允许底层 transport 替换 |
| GDS backend 是否已经实现 | 已经实现且可选择 | TENT 的 File Segment 与 GdsTransport 路径 |
所有 enable_offload 部署是否默认走 GDS |
不能据此断言 | Store storage backend、selector 与运行配置 |
| 已选 GDS 是否必然形成理想 direct DMA | 仍有条件 | GPU buffer、cuFile 注册、文件系统、驱动、硬件与对齐约束 |
支持、默认与实际路径是三件事
“Mooncake Store 能通过 TENT GDS 使用 GDS”在架构与实现层面成立;但某个具体部署是否默认选择它,以及最终 I/O 是否没有 host bounce,仍要检查 storage mapping、transport selector、buffer 类型和运行环境。不能因为默认配置尚未确认就否定能力,也不能因为仓库中已有 GdsTransport 就把每次 SSD I/O 都视作 direct DMA。
所以,两种方案可能得到相似的 SSD ⇄ GPU 物理数据路径,但它们的数据搬运控制面不同:
- Mooncake Store:Connector 发出 Put/Get 并传递 buffer;Store 拥有对象、placement、replica 与生命周期;Transfer Engine/TENT 选择 transport 并搬运数据。本地 SSD 可以落到 GDS,也可以使用 host staging。
- #48504 原生 GDS:vLLM
OffloadingManager拥有OffloadKey、命中与调度;FilesystemSpec拥有文件布局、分配、提交与生命周期;NIXL GDS 等 transport 负责在文件和 GPU 之间搬运。
因此,更准确的表述是:#48504 将此前可通过 Mooncake 等 external connector 获得的 GDS 能力,原生化到 vLLM 的 KV offloading 框架。 它并不是从 Mooncake 抽取代码,而是把接口、文件生命周期和调度所有权放回 vLLM。
二者不是替代关系。#48504 甚至设想让 GDS 文件 connector 与现有 tiered connector 组合:先查 CPU/tiered cache,未命中时再从文件系统直接 GDS load。组合时需要明确写入所有权,避免两个 connector 对同一文件命名空间重复写入。
三个工作项怎样拼起来¶
截至本文日期,相关工作项都还在演进,但职责已经可以分开:
| 工作项 | 真正解决的问题 | 与 GDS 的关系 |
|---|---|---|
| PR #44865 | 重构 Scheduler 到 Worker 的传输描述,显式表达 per-group、方向和对齐信息 | 是前置数据描述重构,不改变实际复制或存储方法 |
| Issue #48504 | 完整的 Filesystem load/store 与可插拔 GDS transport | 是目标 RFC |
| PR #54327 | 文件系统 secondary tier 的容量、准入、淘汰、SQLite 元数据和重启恢复 | 管“磁盘里留什么”,不改变 CPU 中转数据路径 |
换成一句更直观的话:#44865 描述要搬哪些 KV,#54327 管理磁盘留下哪些 KV,#48504 决定 KV 怎样在文件与 GPU 之间搬。
自动卸载还缺什么¶
现在可以更准确地回答“vLLM 将来是否会自动卸载”:
- 已经自动化的部分:启用 connector 后,合格 KV 的 store、prefix lookup、load 和 promotion 由运行时自动完成。
- 尚未自动化的部分:存储层发现、容量配置、请求级路径选择,以及把传输成本与重算成本放在一起决策。
- 路线图中的方向:生产级 distributed/multi-tier offload,以及 prefetch、eviction、selective offloading 和 agent hints。
- #48504 初版边界:CUDA、NIXL GDS、单节点 TP、相同模型/dtype/block/并行配置;使用静态开关,不做逐请求自适应 GDS routing,也不处理 resharding。
如果准备参与 #48504,更连贯的实现顺序是:公共 GPU pointer/span 解析 → 完整 FilesystemSpec → CPU bounce 基线 → NIXL/cuFile GDS transport → MultiConnector 组合与性能验证。 PR #54327 更适合关注磁盘缓存策略,而不是直接作为 GDS 数据面的切入口。
回到最初问题¶
最初看起来像两个重复建设:Mooncake 已经有 GDS,vLLM 为什么还要 GDS + KV offloading?拆开控制面和数据面以后,答案就清楚了:
- verl sleep level 1/2 是让出推理引擎显存,不是可复用 KV 的分层缓存。
- 当前 vLLM 原生 KV offloading 已能自动存取,但 FS secondary 仍必须经过 CPU primary。
- Mooncake 的 Connector、Store 与 Transfer Engine 分层解耦;TENT 已有可选择的 GDS transport,所以 Mooncake Store 在架构与实现上支持透明接入 GDS,但是否默认启用取决于 backend 与配置。
-
48504 不是 vLLM 第一次可能用到 GDS,而是要在 vLLM 自己的 offloading 生命周期内建立原生
FS/NVMe ⇄ GPU路径。¶
因此,判断一项“GDS 已接入”的说法时,最好把问题分成三个层次:架构与 backend 是否支持、部署是否选择了它、实际 DMA 路径是否仍经过 host staging。 再继续检查谁管理 KV 生命周期,就能分清 Mooncake 外部 Connector 与 vLLM 原生 offloading 的能力边界。
参考资料¶
- vLLM KV Offloading 使用文档
- vLLM Multi-tier KV Offloading RFC
- vLLM GDS + KV Offloading RFC #48504
- vLLM PR #44865
- vLLM PR #54327
- vLLM Mooncake Store Connector
- Mooncake SSD Offload Design
- Mooncake Transfer Engine Design
- Mooncake TENT GDS 源码梳理
