Tutti SSD-Backed KV Cache

导言

把 KV Cache 放进 SSD 并不难,难的是让它取回来仍比重新 Prefill 更划算。GPU Direct Storage 已经能让数据绕过 Host DRAM,但每个 I/O 仍由 CPU 发起;Paged KV Cache 又会把一次长前缀恢复拆成海量小而散的请求。SSD 的标称带宽于是还没有用满,GPU 已经先停下来等数据。

Tutti 的核心不是再加一级缓存,而是重写 HBM 与 NVMe 之间的运行时路径:CPU 保留前缀索引和对象映射,却退出逐块 I/O 的数据与控制关键路径;GPU 通过对象化批处理、gio_uring 和 slack-aware 调度直接驱动本地 SSD。读完本文,应该能判断这三部分分别消除了什么瓶颈,以及论文的“接近 DRAM”结论在哪些条件下成立。

一句话结论:Tutti 让 SSD-backed KV Cache 变得实用,靠的不是把 SSD 伪装成慢内存,而是让存储 I/O 成为 GPU 执行计划的一部分。

SSD 慢在带宽之外

长上下文 Prefix Cache 有一个看似简单的收益公式。若某段前缀已经计算过,只要恢复已有 KV 的时间加上剩余后缀的 Prefill 时间,小于从头计算完整 Prompt 的时间,复用就值得:

$$
T_{lookup}+T_{restore}+T_{suffix}<T_{\mathrm{full\ prefill}}
$$

问题在 $T_{restore}$。vLLM、SGLang 一类引擎用 Paged KV Cache 把变长序列拆成不连续小块,便于显存分配、共享与回收;到了 SSD,这种显存布局会直接变成 I/O 碎片。

Tutti 论文 §2.2 给了一个很有冲击力的例子。对 64 层 Qwen3-32B,若每个 Block 保存 64 个 Token,那么恢复 128K Token 的 K/V 需要处理:

$$
N_{object}=2\times L\times \frac{N}{B}
=2\times64\times\frac{128\times1024}{64}
=262{,}144
$$

这里 2 是 Key 与 Value,$L$ 是层数,$N$ 是命中 Token 数,$B$ 是每个 Block 的 Token 数。论文给出的单对象大小约为 80 KB。一次逻辑上连续的前缀恢复,物理上变成约 25.6 万个散落对象。

传统 HBM—DRAM—SSD 路径还叠加了三类开销:

  • 数据路径开销:普通 SSD 路径先把数据读到 Host DRAM,再复制到 HBM。
  • 控制路径开销:CPU 为大量小 I/O 准备描述符、提交请求、处理完成并同步 GPU。
  • 流水碎片开销:按层搬运本可与计算重叠,却会进一步缩小单次 I/O 粒度、增加提交次数。

GDS 只解决了第一项。NVIDIA 官方说明明确区分了直接 DMA 数据路径与运行在 CPU 上的驱动控制路径:NVMe 可以直接读写 GPU Memory,但 cuFile 调用和文件系统、驱动仍由 Host 发起。因此 Tutti 论文观察到,LMCache-GDS 虽然绕过 bounce buffer,GPU bubble 仍超过总推理时延的 70%。

命中不一定比重算快

命中率越高,需要重算的后缀越短,可用于隐藏 I/O 的计算窗口反而越小。SSD 恢复速度若跟不上,缓存越“成功”,执行越容易从 compute-bound 变成 I/O-bound。这也是 Tutti 不能只追求容量或命中率的原因。

Tutti 换掉了什么

先看论文最重要的前后对比。左侧 LMCache 的普通路径需要 DRAM 中转;GDS 去掉数据中转后,CPU 仍逐次 initiate I/O。右侧 Tutti 中,CPU 只按层准备和加载 I/O Kernel,GPU 批量发起对象读写,SSD 与 HBM 直接交换数据。

![LMCache 与 Tutti 的数据路径和控制路径对比](https://pic.shaojiemike.top/shaojiemike/2026/08/f755a8ddedd814279096e59b1290b75e.png){ width=95% }
来自 Tutti 论文 Figure 1。它展示 CPU-centric LMCache、LMCache-GDS 与 GPU-centric Tutti 的关键路径差异;图中的控制面简化不代表 Tutti 删除了全部 CPU 元数据工作。

我更愿意把 Tutti 称为 CPU-prepared、GPU-executed 的 KV 对象存储。这个说法比“GPU 完全接管存储”更准确:

  • CPU 仍然保留 Prefix Hash、逻辑 Block 到 GPU File 的映射、分配与全局共享等不适合 SIMT 的元数据工作。
  • GPU 接管的是运行时关键路径:根据预先准备的 I/O Context 并行生成 NVMe 命令、提交请求、观察完成,并把数据直接搬到目标 HBM Block。
  • CPU 开销从逐层逐块变成逐层:论文将关键路径复杂度从 $O(L\times B)$ 降到 $O(L)$,其中 $B$ 是本次命中的 KV Block 数。

这三项设计并不是同义重复:对象存储解决“读什么、放哪里”,gio_uring 解决“谁来发起和收割完成”,slack-aware Scheduler 解决“什么时候发,才不会抢走推理资源”。

从散页到 GPU KV 对象

直接把传统文件系统或 GPU Block I/O 搬进 KV Cache 并不够。LLM 引擎需要按 Prefix Block 分配和共享,却希望按 Layer 批量恢复;SSD 又需要足够大的并发请求才能吃满带宽。Tutti 在 GeminiFS 之上增加 GPU-centric KV Object Store,把这几个粒度接起来。

GPU File Pool

运行时为每个逻辑 KV Block 关联一个 GPU File。一个 GPU File 内按 Layer 保存 Key Object 与 Value Object,因此共有 $2L$ 个对象;底层 NVMe File Pool 则预先在各块 SSD 上分配物理 Extent。Tensor-Stripe Layout 保留原始 Tensor 粒度,并用轮询方式把对象分散到多块本地 NVMe,避免运行时创建文件和重新分配 Extent。

这里有一个容易混淆的边界:Block 粒度没有消失。 Prefix 仍可按 Block 命中、共享和淘汰;变化发生在传输接口上,retrieve_layerstore_layer 会把同一 Layer 的多个 Block 组织成一批 GPU I/O Context,再由 GPU 并行发出。

P2P 映射与 SGL

NVMe Controller 不能直接理解 CUDA Virtual Address,需要 PCIe 可见的物理地址描述符。vLLM 会在启动时预分配相对稳定的 KV Memory Pool,Tutti 因而可以提前构建 P2P Memory Mapping Table,运行时不再逐请求做地址翻译。

论文没有沿用固定 4 KB 页的 PRP List,而是选择 NVMe Scatter Gather List(SGL)。在作者给出的 60 GB KV Pool 配置中:

描述方式 表达粒度 论文估算的 HBM 元数据 作用
PRP List 4 KB Physical Page 约 3.75 GB 通用,但中等大小 KV Object 需要大量 List Page
SGL 一个 16 B Descriptor 描述连续物理区间 约 15 MB 更贴合约 100 KB 的 KV I/O,并减少命令描述开销

这两个数字依赖论文的 60 GB Pool、4 KB Page 与 64 KB 分配假设,不是所有部署的固定比例。对应的单线程 500 MB Microbenchmark 中,SGL 的读写带宽分别为 8.891 GB/s 和 2.922 GB/s,PRP 为 0.287 GB/s 和 0.032 GB/s;这是描述符路径的隔离实验,不能直接当成端到端推理加速比

GPU io_uring 如何工作

有了对象和物理地址,GPU 还需要一种不阻塞计算的提交方式。Tutti 模仿 Linux io_uring 的 Submission/Completion 结构,实现了 GPU 侧的 gio_uring

  • SQ 与 CQ:一对 Lock-free Ring Buffer 驻留在 HBM,并通过 non-cached mmap 映射给 CPU。
  • IOCB:每个 Submission Entry 是一个 I/O Control Block,内含 2048 个 IOCTX,负责成批承载细粒度请求。
  • IOCTX:记录 SGL Address、GPU File Offset 与 Length,是 GPU 真正发给 NVMe 的轻量请求描述。
  • CUDA Event:约束目标 HBM Block、I/O Kernel 与 Attention Kernel 的依赖,防止异步执行读到未完成数据或提前覆盖仍在使用的 Block。

一次本地命中的教学化路径如下。它概括论文 §3.1—§3.2 的对象生命周期,不是公开源码摘录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CPU Runtime
1. 匹配可复用 Prefix,得到 logical_block_ids
2. 查询 logical block → GPU file 的 CPU-side 映射
3. 按 Layer 填充 IOCB,并提前 enqueue I/O Kernel

GPU I/O Control Domain
4. 等待目标 HBM Block 可写的 CUDA Event
5. 从 SQ 取得 IOCB;每个 GPU Thread 处理一个 IOCTX
6. 依据 SGL 向 NVMe SQ 提交 SSD → HBM P2P DMA
7. 观察完成后,把 IOCB ID 原子写入 CQ

GPU Compute Domain
8. 当前 Layer 在 wait_cqe() 后读取已经恢复的 K/V
9. 执行 Attention;下一 Layer 的恢复与本层计算重叠

仅使用不同 CUDA Stream 仍可能失效:长时间运行的 I/O Kernel 会占住 SM,而 GPU Kernel 调度通常不能像 CPU Thread 那样及时抢占。Tutti 用 NVIDIA Green Context 把 SM 划成 Compute Domain 与 I/O Control Domain,使 I/O Kernel 只能使用预留 SM。CUDA Programming Guide 也强调,Green Context 能限制工作使用指定 SM、减少相互干扰,但不能无条件保证并发;具体划分仍要按负载实验确定。

异步之后还要调度

把 I/O 放到 GPU 并不等于“越并行越快”。论文发现两种干扰:

  1. Read 与 Write 争用 SSD 内部资源。 在作者的 FIO 复现实验中,256 MB Read/Write 同时进行使总带宽下降 60%,而分开执行可以接近饱和。
  2. I/O 与模型 Kernel 争用 SM。 Embedding、Normalization、GEMM 等阶段可能占用接近 90% 的 GPU 资源;不受约束的 I/O Kernel 会抬高推理尾延迟。

Tutti 没有在请求到来时运行复杂优化器,而是离线 Profile 每层的空闲窗口,建立以 Input Length 与 Prefix Length 为键的 Lookup Table。表项记录可用窗口时长、SM Budget,以及不同 IOCB Count 的 I/O 执行特征。部署配置不变时,运行时只需查表。

调度优先级也很具体:

  1. Read 优先。 恢复历史 KV 位于 TTFT 关键路径;合适 Slack 存在时,发出能放入窗口的最大 IOCB Batch。
  2. 无窗口就立即读。 高命中导致剩余计算太少时,继续等只会让 Attention 更晚拿到 KV。
  3. Write 可以延后。 新生成 KV 的 store_layer 先留在 SQ;Prefill 没有安全窗口时推迟到 Decode,甚至后续请求的 Slack 中 Best-effort Flush。
  4. Read/Write 解耦。 不为了表面上的 Layer-wise Overlap 同时压满双向 I/O,避免 SSD 带宽塌陷。

这是一种很务实的工程取舍:用启动前 Profile 换运行时确定性。代价也很明确——模型版本、GPU/SSD 拓扑、Kernel 实现或资源分配改变后,原表不应被默认视为仍然有效。

KV 到底留在哪里

理解 Tutti 时,最好把元数据、KV 数据和执行状态分开。它们没有一起“迁移到 GPU”:

对象 主要位置 谁管理 生命周期与移动
Prefix Hash、Block Index CPU Memory vLLM / KVConnector 请求查询与共享管理;不随 KV Payload 写入 Attention Block
GPU File 与 NVMe File 映射 CPU Memory Tutti Runtime 启动预分配,运行时查询;CPU 保持 Engine-visible Mapping
P2P Mapping、SQ/CQ、IOCB GPU HBM Tutti gio_uring 映射表长期保留;IOCB 随 Layer Batch 循环使用
活跃 KV Block GPU HBM vLLM Block Manager Attention 前必须就绪;读取完成后由当前请求消费
本地历史 KV Object NVMe SSD Tutti / GeminiFS HBM Eviction 时写入,需要复用时 P2P DMA 回 HBM
集群 Replica Metadata CPU / Mooncake Control Plane Mooncake 记录空间与位置,不承载正常本地 KV Payload

单机本地路径是论文完成度最高的部分。多 GPU 时,每个 vLLM GPU Process 配一个 Tutti Instance 和独立 NVMe SQ/CQ Queue Pair,各自管理本 Rank 的 KV 分片;GPU 之间并不先聚合成一份完整 KV 再访问 SSD。

跨节点则要谨慎。论文 §3.4 用 Mooncake 负责全局空间、Replica Metadata 与 Location Lookup,但当前 Prototype 的 Remote Retrieval 仍通过 CPU Interface 把 GPU File 读到 Host Memory,再经 RDMA 发送。所以 Tutti 的强证据是 Local GPU↔Local NVMe Fast Path,不是端到端 GPU-driven Remote KV。

实验证明到哪里

截至 2026-08-26,本文使用的公开版本仍是 arXiv v1,下面讨论的是作者报告结果,不是独立复现。论文在一台 64 核 Intel Xeon 6530、512 GB Memory、两张 80 GB H100 与四块 7.68 TB Solidigm D7-PS1010 上评测;分层配置分配 256 GB Pinned DRAM,并为每张 GPU 提供 14 TB SSD Volume。端到端主实验使用单卡 Llama3-8B、LEval 与 LooGLE,并比较 vLLM 0.12.0/0.17.0 上的 HBM、LMCache-DRAM-LW、LMCache-SSD、LMCache-GDS 与 Tutti。请求按 Poisson Arrival 模拟,报告的是并发负载下的 Average TTFT/ITL

![Tutti 在 LEval 和 LooGLE 上的端到端 TTFT 与 ITL](https://pic.shaojiemike.top/shaojiemike/2026/08/03628e0ec68b2b78111e705c194e3037.png){ width=100% }
来自 Tutti 论文 Figure 8。上排为 vLLM 0.12.0,下排为 vLLM 0.17.0;违反 SLO 的数据点被省略,因此折线缺失不能解释为零时延。该图证明特定硬件、容量配置和合成到达过程下的端到端收益,不证明任意模型与远程 SSD 路径都有相同比例。

值得抓住的不是某一个最漂亮的百分比,而是三层证据彼此闭合:

  • 容量层:在论文配置中,HBM 在 LEval/LooGLE 的命中率为 8%/4%,DRAM 为 53%/24%,SSD 为 84%/86%。大容量确实减少了重算。
  • 数据路径层:双 SSD RAID-0 Microbenchmark 中,Tutti Retrieve 最高达到 25.9 GB/s,LMCache-GDS 约 11.9 GB/s;Tutti Store 在长序列约 9.8 GB/s。
  • 端到端层:新版 vLLM 的 LEval 高负载点上,Tutti 相比 DRAM 与 GDS 分别降低 69.1% 和 78.3% TTFT;在 1 秒 TTFT SLO 下,可承载 Request Rate 分别提高 50% 与 100%。

Slack-aware Pipeline 的边界同样重要。论文固定 32K Prompt、改变 Hit Rate 时,Tutti 的平均 Bubble 约 25 ms,在 93.75% Hit Rate 时为 6 ms,并把 compute-bound 到 I/O-bound 的 Crossover Point 推到 98.3%。但在固定 128K Input 的另一组实验中,当复用前缀超过 96K、工作负载接近纯 Retrieval 时,DRAM 重新领先,Tutti 最多落后 20.6%。

成本结论也不是 SSD 单价的同义反复。论文按 H100 5 美元/小时、DRAM 0.0088 美元/GB/小时、SSD 0.000082 美元/GB/小时计算单位 Token 成本;在 LooGLE 0.5 QPS 点,Tutti 相比 LMCache-GDS 降低约 27%。这个结果同时依赖更低存储价格与更高 GPU Utilization,换一组云价格、命中分布或写放大,比例都会变化。

什么时候值得采用

我的判断是,Tutti 最值得被看成一种有明确前提的 Local Storage Fast Path,而不是“DRAM 可以退出推理系统”的证明。部署前至少应过下面几道门:

  1. 先测复用价值。 用真实 Trace 比较 $T_{lookup}+T_{restore}+T_{suffix}$ 与 $T_{\mathrm{full\ prefill}}$,同时看 TTFT、ITL 和 P99,而不只看 Cache Hit Rate。
  2. 确认 I/O 能直达。 检查 GPU—NVMe PCIe Root Complex、P2P DMA、企业 SSD 的 SGL 支持、队列数与实际 Random I/O;论文数字来自 H100 和高端 PCIe 5.0 NVMe。
  3. 控制 Profile 漂移。 Model Revision、CUDA/vLLM Kernel、TP 拓扑、SM Partition 或 SSD 组合变化后重新生成 Slack Table。
  4. 把 Remote Path 单独验收。 本地命中和远端命中分别统计 Bytes、Host Staging、RDMA 与 Tail Latency,不能用 Figure 8 替远端路径背书。
  5. 补齐持久化成本。 评估 SSD Endurance、Write Amplification、Crash Recovery、Replica Durability 和 Metadata Rebuild;arXiv v1 没有给出这些生产指标。

代码证据边界

论文报告 Tutti 约有 8,000 行 C++,另用约 1,500 行 Python 接入多个 vLLM 版本,并把它称为 Open-source Solution。但截至本文检索的 arXiv v1 HTML、TeX Source 与参考文献,未给出 Tutti Repository URL。因此本文可以核对论文描述和图表,不能把 retrieve_layerstore_layergio_uring 的接口说明当成已完成源码审计

回到最初的问题

SSD-backed KV Cache 过去不实用,并不只是 SSD 比 DRAM 慢。更致命的是:Paged KV 把长前缀变成大量细碎对象,CPU 又站在每个 I/O 的控制路径上;GPU 算得越快,这段老路径就越刺眼。

Tutti 的回答是把对象、地址、队列和计算窗口一起设计:CPU 做它擅长的索引与准备,GPU 做它擅长的大规模并发发起,Scheduler 决定哪些 I/O 真能藏进推理空隙。论文实验有力支持,本地 H100—NVMe 配置下,这条路可以让 SSD 接近 DRAM-backed LMCache 的端到端表现。

还没有被证明的,是同样收益能自然延伸到任意 GPU、消费级 SSD、大模型、多租户干扰和跨节点 Remote KV。对工程决策而言,这个边界不是给 Tutti 泼冷水,反而让它最有价值的启示更清楚:不要只把 KV Cache 当成要搬运的 Tensor;要把它当成与 GPU 执行共同调度的存储对象。

参考资料

  1. Shi Qiu et al., Tutti: Making SSD-Backed KV Cache Practical for Long-Context LLM Serving, arXiv:2605.03375v1, 2026-05-05。本文的系统机制、实验数字与两张原图均来自该版本。
  2. NVIDIA, GPUDirect Storage Overview Guide。用于区分 GDS 的 Direct Data Path 与 CPU Driver Control Path。
  3. NVIDIA, CUDA Programming Guide: Green Contexts。用于核对 SM Resource Partition 的能力与并发边界。
  4. Shi Qiu et al., GeminiFS: A Companion File System for GPUs, FAST 2025。Tutti GPU File 与 NVMe File Path 的底层文件系统基础。
  5. Woosuk Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023。Paged KV Block Layout 的基础工作。
Author

Shaojie Tan

Posted on

2026-08-26

Updated on

2026-08-26

Licensed under