笔记日期: 2026-06-21 笔记作者: Zhongzhu Zhou 论文标题: Tutti: Making SSD-Backed KV Cache Practical for Long-Context LLM Serving 作者: Shi Qiu, Yifan Hu, Xintao Wang, Wenhao Zhu, Jianqin Yan, Hao Chen, Kaiqiang Xu, Kai Chen, Yiming Zhang arXiv: 2605.03375 状态 / Venue: arXiv 预印本,2026 年 5 月
一句话总结
Tutti 把 KV 缓存的 I/O 控制路径从 CPU 迁移到 GPU,通过 GPU 原生对象存储、GPU io_uring 和时隙感知调度器三件套,让 NVMe SSD 在大多数场景下达到接近 DRAM 的首 Token 延迟,同时把每 GB 存储成本降低约 100 倍。
前置知识:读这篇论文需要先知道什么
1. 大模型推理的两个阶段:预填充与解码
Transformer 大模型处理一次请求分为两个截然不同的阶段:
预填充(Prefill):把输入提示词(prompt)中的所有 token 并行处理一遍,一次性计算出 Query ()、Key ()、Value () 矩阵。这个阶段是计算密集型的,其延迟用**首 Token 时间(Time to First Token, TTFT)**衡量——用户提交请求后多久收到第一个输出 token。
解码(Decode):逐个生成输出 token,每步自回归地基于已有 token 计算注意力。这个阶段用**Token 间延迟(Inter-Token Latency, ITL)**衡量——相邻两个输出 token 之间的时间间隔。
这两个阶段对性能的需求不同:预填充主要受计算能力限制,解码则更受内存带宽和调度策略影响。
2. KV 缓存:用内存换计算
解码阶段有一个浪费:每步生成新 token 时,都要对前面所有 token 的 、 向量重新计算一遍注意力。但这些向量是确定性的(取决于输入),完全可以存下来复用。这就是 **KV 缓存(Key-Value Cache)**的核心思路。
对于一个 层、 注意力头、头维度 的模型,存储 个 token 的 KV 缓存所需空间为:
以 Llama3-8B(,,,BF16 精度)为例,存储 128,000 个 token 的 KV 缓存大约需要 32 GB——接近 H100 GPU 80 GB 显存的一半。
KV 缓存不仅服务于单次对话,现代推理引擎还支持前缀缓存(Prefix Caching):多个请求共享同一系统提示词时,只计算一次 KV,供所有后续请求复用,可将预填充成本降低高达 90%。
3. 分页 KV 内存管理(PagedAttention)
由于不同请求的序列长度差异极大,静态预分配 KV 缓存会导致严重的内存碎片。受操作系统虚拟内存分页机制启发,vLLM 的 PagedAttention 将 KV 缓存切分成固定大小的块(Block)(通常每块 16–32 个 token),按需分配。
每个块的形状为:
关键点:同一序列的各个块在 GPU 显存中不连续,通过块表(Block Table)链接起来。这一”分散存储”特性正是 Tutti 要解决的核心问题——将非连续的 KV 块迁移到 SSD 并高效恢复。
4. KV 缓存分层存储:为什么需要 SSD
GPU HBM(高带宽内存)虽快,但容量有限(每块 H100 仅 80 GB)。随着上下文窗口扩展到数十万甚至百万 token,并发会话数增加,HBM 很快耗尽。为此,系统引入三层存储层次:
| 存储层 | 带宽 | 单服务器容量 | 价格($/GB/小时) |
|---|---|---|---|
| GPU HBM | ~3.35 TB/s | ~80 GB | 高(折算约 $0.06) |
| CPU DRAM | ~50–100 GB/s | ~2 TB | $0.0088 |
| NVMe SSD | ~7–30 GB/s | ~100 TB | $0.000082 |
DRAM 每 GB 约是 SSD 的 107 倍贵。如果 SSD 能达到 DRAM 的推理性能,成本将大幅下降。Tutti 的目标正是如此。
5. 为什么 DRAM 可行但 SSD 不行(在 Tutti 之前)
DRAM 为什么好用:
- 随机访问延迟极低(~100 ns),精细粒度 I/O 高效
cudaMemcpyAsync分层批量复制效率良好- 分层流水线可以把传输时间藏在注意力计算后面
SSD 为什么失败: 分页 KV 布局与 SSD 访问模式存在根本性不匹配。以 Qwen3-32B(,块大小 64 token)、128K-token 序列为例,迁移到 SSD 后:
恢复这个序列需要从 SSD 读取 256,000 个 ~80 KB 的随机散布对象。尽管 SSD 原始带宽高(约 30 GB/s),但为每个对象提交一次 I/O 请求的 CPU 软件开销彻底盖过了传输本身。
6. GPU 直通存储(GPU Direct Storage, GDS)及其局限
NVIDIA 的 GPU Direct Storage(GDS)尝试解决数据路径问题:建立 SSD 到 GPU HBM 的直接 DMA 通路,绕过 CPU DRAM 中转缓冲区。这听起来很完美,但 GDS 仍然要求CPU 发起每次 I/O 请求——它只解决了数据路径,没有解决 I/O 控制路径的 CPU 介入问题。即使用了 GDS,GPU 等待 I/O 的泡泡时间(Bubble Time)仍高达推理延迟的 70–80%。
7. Linux io_uring:异步 I/O 的艺术
Linux io_uring(内核 5.1 引入)是目前最先进的用户态异步 I/O 接口,其设计使用两个内核-用户态共享的环形缓冲区:
- 提交队列(Submission Queue, SQ):应用程序在此写入 I/O 请求
- 完成队列(Completion Queue, CQ):内核在此写入完成事件
这种无锁设计把 I/O 提交和回收的开销降到了极低。Tutti 的核心思路之一,就是在 GPU 上实现类似机制——gio_uring——让 GPU 自主向 NVMe 提交命令,无需 CPU 参与。
8. NVIDIA 绿色上下文(Green Contexts):GPU 资源隔离
NVIDIA Hopper 架构引入的**绿色上下文(Green Context)**功能允许在单块 GPU 内进行硬件级资源隔离:将 SM(流多处理器)划分为独立的”计算域”和”I/O 控制域”。没有这种隔离,一个长期运行的 I/O 轮询内核会占用 SM,阻塞后续的计算内核(因为 GPU 硬件调度器基本上不可抢占)。Tutti 利用绿色上下文确保 I/O 控制内核运行在专属 SM 上,不干扰计算。
现有方案如何工作,为何失败:以 LMCache 为例
在深入 Tutti 之前,理解 LMCache(Tutti 改进的目标系统)的工作原理很有价值。
LMCache 的三层架构
DRAM 层(LMCache-DRAM-LW):KV 块从 HBM 驱逐时,通过 cudaMemcpyAsync 批量传输到 CPU DRAM。恢复时逆向传输。分层流水线:GPU 计算第 层的同时,将第 层的 KV 从 DRAM 传入 HBM。有效性源于 DRAM-HBM 带宽(~50 GB/s)足以在下一层注意力完成前完成当前层 KV 传输。
SSD 层(LMCache-SSD):KV 块经由 CPU DRAM 中转存储到 NVMe SSD(通过文件系统)。恢复路径相反。问题双倍:DRAM 层的所有开销,加上更低的 SSD 带宽和每次 I/O 的高开销。
GDS 加速 SSD 层(LMCache-GDS):借助 NVIDIA cuFile 建立 SSD→HBM 直接 DMA 通路,绕过 DRAM 中转缓冲区。看似完美,但 cuFile 要求 CPU 发起每次 I/O 请求——对每个 KV 块都需要 CPU 准备 cuFile 传输描述符并调用 cuFileRead/cuFileWrite。面对 256,000 个对象,CPU 开销成为瓶颈。
实测数据的震撼
论文图 2 的数据令人印象深刻:在 vLLM v0.17.0、64K 序列、75% 命中率下,LMCache-GDS 的 GPU 气泡时间占总推理延迟的 72.3%。SSD 层的性能甚至比重新计算还差(9.4 秒 vs 重计算 9.4 秒,DRAM 仅需 1.7 秒)。
这一事实是 Tutti 的出发点:当 GDS 已经解决了数据路径,性能仍如此糟糕,问题一定出在 I/O 控制路径上。
论文概览:一张图看懂核心思路
graph TB
subgraph CPU_Centric["以 CPU 为中心(LMCache + GDS)"]
direction LR
A1["推理引擎\n(GPU)"] -->|"每块都要通知 CPU\n触发 I/O"| B1["CPU"]
B1 -->|"GDS DMA"| C1["NVMe SSD"]
C1 -->|"数据直传 HBM"| A1
end
subgraph GPU_Centric["以 GPU 为中心(Tutti)"]
direction LR
A2["推理引擎\n(GPU)"] -->|"每层只加载\n一次内核"| B2["CPU(轻量)"]
A2 -->|"gio_uring 直接\n发送 I/O 命令"| C2["NVMe SSD"]
C2 -->|"P2P DMA → HBM"| A2
end
style CPU_Centric fill:#ffcccc,stroke:#cc0000
style GPU_Centric fill:#ccffcc,stroke:#00cc00
图 1:CPU 中心 vs GPU 中心的 KV 缓存存储架构对比。 LMCache+GDS 中,CPU 是每个 KV 块 I/O 的必经之路;Tutti 中,CPU 仅在每层初始化时加载一次内核,后续 GPU 独立驱动所有 NVMe 命令。
三个根本瓶颈:现有 SSD KV 缓存为何失败
瓶颈一:分页布局导致海量碎片 I/O
分页 KV 布局虽然对显存管理极为友好,但迁移到 SSD 后问题浮现:要恢复一个序列的 KV,需要单独寻址和传输数十万个散布对象。即使 SSD 原始带宽足够,为每个对象单独提交 I/O 的 CPU 软件开销也会完全主导端到端延迟。
以实际数据为证:Qwen3-32B(64 层,块大小 64),128K-token 序列需要恢复 256,000 个对象。在不到 5 ms 的注意力计算时间窗口内,这显然是无法完成的。
瓶颈二:GDS 仍是 CPU 中心的 I/O 控制
GDS 把数据搬运从「HBM←DRAM←SSD」变成了「HBM←SSD(直通)」,但 I/O 命令的生命周期——准备 NVMe 描述符、向提交队列写入命令、等待完成队列的确认——仍然全部在 CPU 上串行发生。
GPU 等待 CPU 完成每个对象的 I/O 控制操作,形成严重的流水线气泡。测量结果显示,采用 GDS 的 LMCache 在 64K 序列、75% 命中率下,GPU 气泡时间占总推理时延的 73.0%(vLLM v0.17.0),恢复 KV 比重新计算还慢。
瓶颈三:读写同时进行导致带宽崩塌
在典型的分层流水线中,当前层写入新生成的 KV 到 SSD,同时下一层从 SSD 读取历史 KV——读写并发。但实验显示:
并发读写导致总带宽下降 60.1%!原因是大块读写争夺 NVMe 内部写缓存,相互阻塞。这是一个被现有系统普遍忽视的严重问题。
Tutti 的三个核心设计
设计一:GPU 原生对象存储(§3.1)
目标:让 GPU 在 NVMe 上高效存取 KV 对象,CPU 退出关键路径。这需要解决两个子问题:对象管理(索引、分配)和物理寻址(把 GPU 虚拟地址转换为 NVMe 可见的物理地址)。
GPU 文件池(GPU File Pool)
Tutti 在 GeminiFS(GPU 配套文件系统)基础上扩展了 GPU 文件池,核心设计决策:
- 每个 KV 内存块(覆盖 个 token)对应一个 GPU 文件
- 每个 GPU 文件包含 个对象(每层各一个 K 对象和一个 V 对象)
- 采用 Tensor-Stripe 布局:每个 GPU 文件按张量粒度而非细粒度存储页映射到若干 NVMe 文件
- 多个 GPU 文件以轮转方式分布在各 SSD 上,均衡 I/O 负载
这样,I/O 粒度自然对齐 KV 传输粒度(),避免了细粒度 NVMe I/O 与大块 KV 传输之间的失配。
CPU 负责的管理操作(哈希表、分配、索引)全部在非关键路径上执行——系统启动时预计算、空闲时维护,不影响推理延迟。
P2P 内存映射表:SGL vs PRP
第二个子问题是物理寻址:GPU 提交 NVMe I/O 命令时,需要提供目标 HBM 页的物理地址。NVMe 标准机制是物理区域页(Physical Region Pages, PRP):每 4 KB 一个指针。
对于 60 GB 的 KV 缓存池,PRP 所需空间计算如下:
按 64 KB PRP 列表页(每页 16 个指针):
3.75 GB 的宝贵 HBM 仅用于地址元数据,这是不可接受的。
Tutti 改用散布聚集列表(Scatter Gather Lists, SGL):每个条目仅 16 字节(物理地址 8 字节 + 长度 4 字节 + 标识符 4 字节),描述一段连续内存区域:
节省 250 倍!更重要的是,SGL 支持批量传输:一个 SGL 条目覆盖整个连续的 ~100 KB KV 对象,不再逐页提交细粒度 I/O 请求。
微基准测试结果:SGL 读取带宽 8.9 GB/s vs. PRP 的 0.29 GB/s,提升 31 倍;写入带宽 2.9 GB/s vs. 0.03 GB/s,提升 91 倍。
接口设计:CPU 开销从 降至
graph LR
subgraph HBM["GPU HBM"]
KB["KV 块池\n(分页,非连续)"]
P2P["P2P 映射表\n(SGL 条目,15 MB)"]
GFP["GPU 文件池\n(文件→块映射)"]
SQ["SQ 环形缓冲区"]
CQ["CQ 环形缓冲区"]
end
subgraph SSD["NVMe SSD"]
NF["NVMe 文件\n(GeminiFS 物理范围)"]
end
subgraph CPU["CPU(离关键路径)"]
HT["哈希表\n(块→GPU 文件 ID)"]
KM["内核加载器\n(每层一次)"]
end
KB --> P2P
P2P --> SQ
GFP --> SQ
SQ -->|"P2P DMA"| NF
NF -->|"直传 HBM"| KB
NF --> CQ
HT -.->|"启动时查表"| GFP
KM -.->|"每层一次"| SQ
style CPU fill:#fff3cd,stroke:#ffc107
style HBM fill:#d1ecf1,stroke:#17a2b8
style SSD fill:#d4edda,stroke:#28a745
图 2:GPU 原生 KV 缓存对象存储布局。 CPU 在关键路径之外管理元数据;GPU 通过 SGL 描述符和环形缓冲区直接驱动所有 NVMe I/O。
伪代码 1:层级批量存取接口
函数 retrieve_layer(layer_id, block_ids[], 输出 output_hbm[]):
输入: 层编号, 需要恢复的 KV 块 ID 列表
对于 block_ids 中的每个块 b:
gpu_file_id ← cpu_block_to_file[b] // CPU 哈希表(非关键路径)
sgl_entry ← p2p_table[b][layer_id] // 预计算的 P2P 映射
ioctx ← allocate_ioctx(gio_uring)
ioctx.sgl ← sgl_entry // 物理地址描述符
ioctx.offset ← gpu_file_id × layer_stride + layer_id × object_size
ioctx.len ← object_size
enqueue_to_sq(gio_uring, ioctx) // 入队,不等待
// GPU 同时并发执行所有 IOCTX
wait_cqe(gio_uring, block_ids) // 等待本层所有 I/O 完成
与传统 GDS 相比,CPU 只需在每层开始时加载一次 I/O 内核( 次),而不是为每个块单独通知( 次)。
设计二:GPU io_uring(gio_uring)(§3.2)
有了 GPU 文件池和 P2P 映射表,GPU 还需要一种机制能异步向 NVMe 提交命令并收割完成通知,同时不阻塞计算。这就是 gio_uring——Linux io_uring 在 GPU 上的镜像实现。
零拷贝环形缓冲区
gio_uring 使用一对驻留在 GPU HBM 但通过非缓存 mmap 映射到 CPU 虚拟地址空间的环形缓冲区:
- 提交队列(SQ):GPU 写入 I/O 命令,NVMe 控制器读取
- 完成队列(CQ):NVMe 控制器写入完成事件,GPU 轮询
每个 SQ 条目是一个I/O 控制块(IOCB),包含 2048 个 I/O 上下文(IOCTX)。IOCTX 记录:
- SGL 地址(8 字节物理指针)
- GPU 文件偏移(4 字节)
- I/O 长度(4 字节)
批量结构(每 IOCB 2048 个 IOCTX)与 GPU 最小调度单元对齐。H100 上(最小单元为 2 个 SM),每个 SM 支持 64 个 Warp × 32 线程 = 2,048 个并发线程,对应 2048 个 IOCTX。
SM 分区:通过 NVIDIA 绿色上下文实现精确隔离
仅靠多 CUDA 流并不够,因为长时间运行的 I/O 轮询内核在非抢占式 GPU 调度器下可能霸占 SM,阻塞计算内核。Tutti 利用绿色上下文将 GPU 资源在硬件层面隔离为:
- 计算域:运行注意力、GEMM、归一化等推理内核
- I/O 控制域:运行 gio_uring 的提交和轮询内核
I/O 控制内核在专属 SM 上运行,完全不受计算负载波动影响,提供确定性的 QoS,消除尾延迟和资源饥饿。
伪代码 2:gio_uring 异步 I/O 四步流程
阶段 1:初始化(CPU,系统启动时一次)
init_queue(depth):
在 HBM 中分配 SQ[depth] 和 CQ[depth]
通过非缓存 mmap 将 SQ/CQ 映射到 CPU 虚拟地址
向 NVMe 驱动预注册所有队列对
阶段 2:准备(CPU,每层每请求一次)
get_iocb(nums, event):
从 SQ 获取 nums 个可用 IOCB
用 CPU 侧虚拟地址填充 IOCTX(SGL、偏移、长度)
插入 CUDA 事件:确保 I/O 内核在依赖满足后才启动
返回 IOCB_ids
阶段 3:发射(GPU 内核,I/O 控制域 SM)
issue_io(IOCB_ids, SMs):
在指定 SM 分区上启动 I/O 内核
对于每个 IOCTX:
将 SGL 虚拟地址转为物理地址
将 NVMe 命令入队到 SQ
敲响 NVMe 门铃寄存器(doorbell)
监听 CQ(非阻塞,独立于计算流)
完成时:原子写入 IOCB_id 到 CQ
阶段 4:等待(GPU 计算内核)
wait_cqe(IOCB_ids):
检查 CQ 中的特定 IOCB_id
所有请求的 IOCB 完成后返回
全程无 CPU 参与
sequenceDiagram
participant CPU as CPU 运行时
participant IO_SM as I/O 控制 SM
participant Comp_SM as 计算 SM
participant NVMe as NVMe SSD
CPU->>IO_SM: 加载 I/O 内核(每层一次)
activate IO_SM
Note over Comp_SM: 第 N-1 层注意力计算(进行中)
IO_SM->>NVMe: 批量入队 NVMe 读命令(SGL + 门铃)
NVMe-->>IO_SM: 传输完成事件(CQ)
IO_SM->>IO_SM: 原子写入 IOCB_id 到 CQ
Note over Comp_SM: 第 N 层计算等待 KV 数据
Comp_SM->>IO_SM: wait_cqe(IOCB_ids)
IO_SM-->>Comp_SM: KV 数据已就绪(HBM)
activate Comp_SM
Note over Comp_SM: 第 N 层注意力计算(执行)
deactivate Comp_SM
deactivate IO_SM
图 3:gio_uring SM 分区执行模型。 I/O 控制与计算在隔离 SM 分区上并行运行,计算内核仅在 wait_cqe 时阻塞。
设计三:时隙感知 I/O 调度器(§3.3)
仅有 gio_uring 和 SM 分区还不够——两个干扰源依然存在。时隙感知调度器同时解决这两个问题。
问题回顾:读写并发导致带宽崩塌
如第一节所述,读写并发时带宽降低 60%。Tutti 的解决方案:完全不允许读写同时执行。读优先,写在计算间隙的时隙窗口中分批完成。
离线性能剖析:构建时隙查找表
关键洞察:预填充的计算时间可以精确预测——注意力的复杂度随前缀长度线性增长,而其他算子(线性投影、归一化)不受上下文长度影响。这种可预测性使得离线剖析可行。
Tutti 对模型每一层进行离线剖析,生成一张以 为索引的时隙查找表:
每个表项记录当前层计算中空闲 SM 的窗口时长和资源预算,供调度器在线查表决策,无需动态建模。
解耦的读写调度策略
伪代码 3:时隙感知调度(预填充阶段)
输入:请求,L_input 输入长度,L_prefix 缓存前缀长度
请求到达时:
priority ← READ // 读取在关键路径上,优先级最高
对于每一层 l = 0 到 L-1:
slack ← 时隙表[L_input][L_prefix][l]
// 优先调度读取(恢复历史 KV,在关键路径上)
如果 READ 队列非空:
n_read ← min(READ 队列长度, slack.max_IOCBs)
issue_io(read_iocbs[:n_read], slack.SM_budget)
// 只有读取已调度且时隙还有余量时,才调度写入
如果 WRITE 队列非空 且 slack.remaining > 0:
n_write ← min(WRITE 队列长度, slack.remaining_IOCBs)
issue_io(write_iocbs[:n_write], slack.SM_budget)
否则:
defer_writes_to_decode() // 推迟到解码阶段
// 执行当前层计算
attention_and_ffn(l)
wait_cqe(issued_iocb_ids)
伪代码 4:时隙感知调度(解码阶段)
对于每个解码步:
slack ← decode_slack_profile[current_length]
// 尽力消化剩余写入(解码阶段 GPU 利用率较低)
如果 WRITE 队列非空 且 slack.window_exists:
n_write ← min(WRITE 队列长度, slack.max_IOCBs)
issue_io(write_iocbs[:n_write], slack.SM_budget)
// 解码阶段无需读取(解码 KV 始终在 HBM 中)
generate_next_token()
调度器保证三条硬规则:
- 读取始终优先于写入(预填充阶段)
- 读写永不同时执行(防止带宽崩塌)
- I/O 内核的 SM 占用受
slack.SM_budget约束(不干扰计算)
graph LR
subgraph L0["第 0 层 (t=0..30ms)"]
R0["读取 KV\n(t=0..20ms)"]
C0["计算\n(t=0..30ms)"]
end
subgraph L1["第 1 层 (t=30..65ms)"]
R1["读取 KV\n(t=30..50ms)"]
C1["计算\n(t=30..65ms)"]
end
subgraph L2["第 2 层 (t=65..95ms)"]
R2["读取 KV\n(t=65..80ms)"]
C2["计算\n(t=65..95ms)"]
end
W["写入 KV 到 SSD\n(延迟, t=80..100ms)"]
C0 --> R1
C1 --> R2
R2 --> W
style R0 fill:#d1ecf1,stroke:#17a2b8
style R1 fill:#d1ecf1,stroke:#17a2b8
style R2 fill:#d1ecf1,stroke:#17a2b8
style C0 fill:#d4edda,stroke:#28a745
style C1 fill:#d4edda,stroke:#28a745
style C2 fill:#d4edda,stroke:#28a745
style W fill:#fff3cd,stroke:#ffc107
图 4:Tutti 时隙感知分层流水线。 读取在各层的时隙窗口内与计算重叠;写入被推迟到不与读取重叠的时隙,彻底消除带宽竞争。
关键公式推导
成本模型
论文定义每百万 token 服务成本:
使用云服务定价: = $5/小时/H100, = $0.0088/GB/小时, = $0.000082/GB/小时。DRAM 每 GB 成本是 SSD 的约 107 倍。
当 Tutti 使 SSD 的服务吞吐量匹配 DRAM 时,分子中 P_mem × S_mem 这一项被几乎消除。以 14 TB SSD 容量(等效约 140 GB DRAM 缓存)为例,SSD 成本约 /小时,而等量 DRAM 成本约 /小时×100 = /小时(14 TB DRAM 实际不存在),每 GB 效率悬殊。
GPU 气泡时间与交叉点分析
计算绑定与 I/O 绑定的临界点(“交叉点”)发生在:
对于第 层,前缀长度 ,新 token 数 :
注意力项随 线性增长,FFN 项不变。这解释了为何高缓存命中率(大 )时时隙窗口更大:更长的注意力计算为 I/O 提供了更多时间隐藏延迟。Tutti 的交叉点被推到 98.3% 命中率,而 LMCache-SSD 仅约 50%,说明前者在几乎所有实际部署场景下都能维持计算绑定状态。
实现细节
Tutti 核心 GPU 存储层使用约 8,000 行 C++ 编写,与 vLLM 的 KVConnector 接口集成需约 1,500 行 Python。关键实现选择:
-
启动时预分配:KV 缓存池在初始化时预先分配,生命期内保持稳定,允许 P2P 映射表一次计算、反复使用。
-
一次性热身剖析:推理开始前,Tutti 对特定模型和硬件配置进行时隙剖析,生成的剖析表可以在相同部署设置下跨推理进程复用。
-
多 GPU 独立队列:每个 GPU 通过本地守护进程维护独立的 NVMe 提交/完成队列对。Solidigm D7-PS1010 支持最多 256 个 I/O 队列,8 块 GPU 各 32 个队列绰绰有余。
-
分布式扩展通过 Mooncake:本地节点内,Tutti 负责高性能 HBM↔SSD 传输;跨节点范围,Mooncake 提供集群级 KV 元数据管理和位置路由。远端 KV 恢复目前通过 CPU 侧 RDMA(未来工作)。
实验结果分析
实验配置
- 服务器:64 核 Intel Xeon 6530,512 GB DRAM,2× H100 80 GB,4× Solidigm D7-PS1010 7.68 TB SSD
- 主要测试模型:Llama3-8B(单 GPU)
- 扩展性测试:GLM-4-9B-Chat-1M(双 GPU,张量并行)
- 工作负载:LEval(3K–200K token)和 LooGLE(多数 >100K token)
- 基线:HBM 独占、LMCache-DRAM、LMCache-SSD、LMCache-GDS
端到端性能(图 8)
| 系统 | 平均 TTFT(s)@ 1.5 req/s | 与 Tutti 对比 |
|---|---|---|
| HBM 独占 | 7.2 | 慢 8.3 倍 |
| LMCache-DRAM | 2.8 | 慢 3.2 倍 |
| LMCache-SSD | 6.5 | 慢 7.5 倍 |
| LMCache-GDS | 3.9 | 慢 4.5 倍 |
| Tutti | 0.87 | 基准 |
图 5:各系统 TTFT 对比(LEval,vLLM v0.17.0,1.5 req/s)。 Tutti 实现最低 TTFT,比 DRAM 低 69.1%,比 GDS 低 78.3%,在 1 秒 TTFT SLO 下支持比 GDS 多 2 倍的请求率。
主要结论:
- LEval(1.5 req/s,vLLM v0.17.0):Tutti 比 DRAM 低 69.1%,比 GDS 低 78.3%
- 1 秒 TTFT SLO 下:Tutti 比 DRAM 多支持 50% 请求,比 GDS 多 100%
- LooGLE(0.6 req/s,vLLM v0.17.0):Tutti TTFT 比 DRAM 低 93.2%,比 GDS 低 62.0%
- ITL(LEval,1.5 req/s):Tutti 比 DRAM 低 22.0%,比 GDS 低 24.4%
存储带宽微基准(图 9)
| 系统 | 读取带宽(128K token) | 写入带宽(128K token) |
|---|---|---|
| LMCache-DRAM | ~8.5 GB/s(有抖动) | ~18.4 GB/s |
| LMCache-GDS | ~11.9 GB/s | ~7 GB/s |
| LMCache-SSD | ~5 GB/s | ~4 GB/s |
| Tutti | ~25.9 GB/s | ~10 GB/s |
Tutti 读取带宽是 GDS 的 2.2 倍,近线性随上下文长度扩展。写入带宽受设备限制(单块 SSD 峰值约 10 GB/s)。
SGL vs PRP 带宽对比(图 10)
| 命令格式 | 单线程读取带宽 | 单线程写入带宽 |
|---|---|---|
| PRP | 0.287 GB/s | 0.032 GB/s |
| SGL | 8.891 GB/s | 2.922 GB/s |
| 提升倍数 | 31.0× | 91.3× |
SGL 命令降低了主机与 NVMe 设备之间的 PCIe 通信开销,稳定了队列进度,从而大幅提升带宽。
气泡时间分析与交叉点(图 13)
时隙感知调度器的有效性通过气泡时间分解实验验证:
| 系统 | 交叉点命中率(气泡>计算时间) |
|---|---|
| LMCache-SSD-LW | ~50% |
| LMCache-DRAM-LW | ~97.9% |
| Tutti | ~98.3% |
Tutti 的交叉点(98.3%)几乎与 DRAM(97.9%)持平,意味着在 98.3% 命中率以下,Tutti 的 GPU 气泡时间可以忽略不计(平均仅 25 ms,93.75% 命中率时低至 6 ms)。
成本分析(图 14)
在 LooGLE、0.5 req/s 场景下:
- Tutti vs. LMCache-SSD:成本降低 66.2%(更高吞吐量,相同 SSD 成本)
- Tutti vs. LMCache-GDS:成本降低 27%
多 GPU 扩展性(图 12)
在 GLM-4-9B-1M 双 GPU、640K 前缀长度下:
- Tutti:TTFT = 1.2 秒(成功完成)
- LMCache-GDS:OOM 失败(cuFile 暂存缓冲区耗尽显存)
LMCache-GDS 依赖 cuFile 为每次 GDS 传输分配 GPU 暂存缓冲区,长序列下这一开销累积至 OOM。Tutti 无需暂存缓冲区,直接通过 P2P DMA 管理 HBM,架构上更健壮。
一个具体请求的完整执行轨迹
为了让 Tutti 的设计更加具体,我们完整追踪一次在 Tutti-vLLM 上处理的请求:64K 共享前缀,Llama3-8B(32 层,块大小 16,BF16 精度)。
第一步:请求到达,前缀检索
vLLM 调度器收到请求,检查 KV 缓存中是否存在该 64K 前缀。根据论文 Table 1,LEval 工作负载的 SSD 命中率约为 84%,该前缀大概率在 SSD 中。
计算所需 I/O 规模:
在 LMCache-GDS 中,CPU 需要发起 256,000 次 cuFile 调用。在 Tutti 中,CPU 仅需每层加载一次 I/O 内核。
第二步:P2P 表查询和 IOCB 准备(CPU,每层一次)
对于第 层,CPU 运行时:
- 查询哈希表,获得 4,000 个块的 GPU 文件 ID
- 从预计算 P2P 表中取出 8,000 个 SGL 条目(每块各一个 K 和 V 条目)
- 将 8,000 个 IOCTX 填入 gio_uring SQ(仅 128 KB 元数据)
- 插入 CUDA 事件依赖
- 将 IOCB 句柄返回给 GPU 运行时
整个 CPU 操作在微秒级内完成。
第三步:GPU 发射第 0 层 I/O(I/O 控制 SM)
gio_uring I/O 内核在专属 SM 上:
- 将 8,000 个 SGL 条目并行转换为 NVMe 命令(Warp 级并行)
- 一次性将 8,000 条 NVMe 读取命令入队 SQ
- 敲响 NVMe 门铃寄存器一次
- NVMe 控制器开始 8,000 个并发 DMA 传输到 HBM
- 与此同时,计算 SM 开始执行第 0 层的嵌入和归一化
第 0 层的传输数据量:
以 25.9 GB/s 取回带宽,传输时间约 1.2 ms。H100 上 64K token 的第 0 层注意力约需 3.5 ms。时隙窗口:2.3 ms。
第四步:调度器检查时隙并发射写入
计算开始前,调度器查询 时隙表[64K][0][层=0]:
- 窗口时长:2.3 ms
- 可用 SM 预算:4 个 SM
- 最大 IOCB 数:~320 个写入 IOCB
如有上一请求解码阶段积压的 KV 写入,调度器在剩余 2.3 ms 内发射 320 个写入 IOCB,利用空闲资源消化写入积压。
第五步:计算与 I/O 完全重叠
第 0 层计算在计算 SM 分区执行。当计算到达 wait_cqe() 时,8,000 个读取 I/O 早已完成(1.2 ms vs 3.5 ms 计算时间)。零气泡。
此模式在全部 32 层重复。总预填充时间约为 ,I/O 完全隐藏在计算后。TTFT ≈ 112 ms(vs LMCache-GDS 的约 3.9 s)。
零气泡的充分条件:
Tutti 的交叉点(98.3% 命中率)意味着:在低于此命中率的所有工作点,该不等式成立,GPU 完全处于计算绑定状态。
Tutti 在 KV 缓存生态中的定位
理解 Tutti 的价值,需要了解它与同期相关工作的关系:
HBM 独占系统(vLLM、SGLang):最快但容量有限。LEval 上命中率仅 8%,频繁重计算拖慢速度。
DRAM 扩展系统(LMCache-DRAM、CachedAttention、HCache):利用 DRAM 扩展容量。带宽好(~50 GB/s),分层流水线有效。容量上限约 2 TB,不足以应对大规模多会话工作负载。
CacheBlend(EuroSys’25):专注于 RAG 工作负载的 KV 语义复用,混合缓存和计算 KV 条目。与 Tutti 互补——Tutti 管存储层,CacheBlend 管缓存匹配。
Strata:基于重要性评分的层次化上下文缓存,使用重要性驱动的驱逐策略。与 Tutti 正交(驱逐策略 vs 存储 I/O 效率),未来可组合。
IMPRESS(FAST’25):多层前缀 KV 存储,按重要性评分决定哪些 KV 留在哪一层。同样与 Tutti 正交,可与 Tutti 的 SSD 访问层结合。
BaM、GeminiFS、GoFS:通用 GPU 中心存储系统。Tutti 在 GeminiFS 基础上扩展,针对 KV 缓存场景的三个特有挑战(抽象失配、粒度差距、资源竞争)提供专有解决方案。
Tutti 的独特贡献:首个同时解决 I/O 控制路径(gio_uring)、物理寻址开销(SGL vs PRP)和 I/O-计算竞争(时隙感知调度器)的集成系统,已与生产级推理引擎(vLLM)深度集成。三者缺一不可:仅有 BaM 或 GeminiFS 不足以解决问题。
批判性分析:不足与可改进之处
(a) 方法和实验的弱点
0. 缺乏”禁用所有三项创新”的消融基线。 论文提供了 SGL vs PRP、时隙调度等单个组件的消融,但没有一个实验完全禁用三项创新,展示”朴素 GPU 中心”方案的基线性能。这使得读者难以判断哪项创新贡献最大——对未来想选择性实现 Tutti 子集的系统设计者尤为遗憾。
1. 缓存命中率分解不充分。 论文在 LEval/LooGLE 数据集的自然命中率下评估性能,但未研究命中率控制实验——即在人为设定的相同命中率下对比 Tutti 和 LMCache-GDS。生产环境中命中率随流量模式、会话组合、驱逐策略大幅波动,无法从现有实验直接推断冷启动或低命中率场景的性能。
1. 基线覆盖不足。 论文主要对比 LMCache(v0.4.2)。2026 年 5 月前后,KV 缓存服务领域已有 Strata、IMPRESS、HCache、CacheBlend、SGLang HiCache 等系统,均未被评估。尤其是 IMPRESS(FAST’25),专门针对分层 KV 存储的重要性排序,与 Tutti 目标高度重叠,其缺席令人存疑。
2. 模型规模偏小。 核心端到端实验只用了 Llama3-8B——8B 参数、32 层的小型模型,每层 I/O 开销相对较轻。对于 Charles 研究关注的 70B、405B 大模型,KV 缓存占比更高,分层流水线效果可能截然不同,但论文未作验证。“类 DRAM 效率”的断言应在更大模型上验证。
3. GDS 实现版本未说明。 NVIDIA 持续优化 GDS 和 cuFile,论文使用的 LMCache v0.4.2 可能未采用最新 GDS 优化。在 PCIe 6.0 或 CXL 内存普及后,GDS 与 Tutti 的差距可能收窄,但论文未作讨论。
4. 写入带宽瓶颈被轻描淡写。 Tutti 的写入带宽受限于单块 SSD 约 10 GB/s。在高新会话率(每请求都需要写出大量新 KV)的工作负载下,写入积压可能导致 HBM 满载时的驱逐阻塞,但论文未量化持续高驱逐率下的写入延迟。
5. 解码阶段 I/O 窗口短且不可预测。 调度器承认解码时隙”短且不可预测”,依靠”尽力而为”策略消化积压写入。在长解码输出(如代码生成、长文档)场景中,写入积压的量级及其对下一个请求的影响未被量化。
(b) 作者淡化或回避的局限
1. 热身剖析开销未报告。 Tutti 需要离线剖析每层的时隙窗口,剖析复杂度为 次模型前向计算。对于百万 token 上下文的模型,这可能需要数小时。论文提到”只需生成一次”,但未给出任何剖析时间测量值。
2. 远端检索路径未优化。 §3.4 明确提到跨节点 KV 恢复”使用 CPU 侧接口将 GPU 文件读入主机内存,再通过 RDMA 传输”。这条路径完全颠覆了 Tutti 消除 CPU 介入的核心设计——在集群级前缀缓存(跨节点复用)这一常见场景下,性能影响未被量化。
3. SSD 写入耐久性(Endurance)未讨论。 企业级 NVMe SSD 通常每天驱动写入量(DWPD)为 1–3 次。频繁的 KV 驱逐和恢复会加速 SSD 磨损。成本分析使用 $0.000082/GB/小时的价格,但未将 SSD 更换成本(因写入放大导致寿命缩短)纳入总拥有成本(TCO)计算。
4. CUDA 事件开销未分析。 gio_uring 使用 CUDA 事件序列化 I/O 和计算内核。对于非常短的解码步(低计算强度),事件同步开销可能不可忽视,但论文未提供相关测量值。特别是在高并发解码(hundreds of concurrent sequences)场景下,CUDA 事件的数量可能爆炸性增长,引入不可忽视的调度开销。
4a. 张量并行交互未分析。 张量并行模式下,每块 GPU 仅持有 KV 缓存的一个分片,Tutti 为每个 GPU 进程独立部署实例。但张量并行的 all-reduce 通信会占用 PCIe 带宽,与并发 SSD I/O 存在资源竞争,论文未分析这种交互对推理延迟的影响。
5. 多 SSD RAID 配置与定价不一致。 §4.1 使用”两块 SSD,29 GB/s 峰值带宽”,但单块 SSD 定价($0.000082/GB/小时)可能低于 RAID-0 双盘配置的实际成本(还需考虑控制器、电源、机架空间)。成本分析中应明确说明是按单 SSD 价格还是整套 RAID 配置报价,否则 “27% 成本降低” 的对比基线不透明。
(c) 具体改进建议
1. 在 70B+ 大模型上评估。 大模型 KV 缓存占比更高,HBM 耗尽更频繁,正是 Tutti 价值最大的场景。Llama3-70B 128K 序列的 KV 约 140 GB,远超 HBM,必须大量依赖 SSD 分层。在这种规模下验证”类 DRAM 效率”将大大增强论文说服力。
2. 纳入 Strata 和 IMPRESS 作为基线。 这两个系统代表了分层 KV 存储的当前前沿,与 Tutti 的目标直接重叠。加入它们将强化论文的定位,也将澄清 Tutti 的优势来源(GPU 原生 I/O,还是调度策略)。
3. 量化并优化远端检索路径。 GPU 发起的 RDMA(NVIDIA SHARP 或 GPUDirect RDMA)可以将消除 CPU 介入的原则延伸到节点间场景。论文已将此列为未来工作,加入初步基准测试将有助于评估机会。
4. 自适应时隙窗口大小。 当前调度器依赖离线剖析表,模型、硬件或 vLLM 版本更新后需要重新剖析。一个基于运行时测量动态估计时隙的在线自适应版本,将使系统更易部署。
5. 加入 SSD 磨损仿真。 用马尔可夫链对 KV 驱逐和恢复的写入放大进行建模,预测生产工作负载下的 SSD 寿命,可以为 TCO 分析提供更完整的图景。
可复现性说明
论文报告 Tutti 已实现并与 vLLM 集成。核心 GPU 存储层约 8,000 行 C++,vLLM 集成约 1,500 行 Python。论文提及”开源”但未给出代码仓库链接。复现关键要求:
- 硬件:H100 GPU(绿色上下文 SM 分区需要 Hopper 架构或更新),PCIe 5.0 用于 SGL 带宽
- 软件:GeminiFS(同一课题组,FAST’25),vLLM v0.12+ 或 v0.17+
- SSD:企业级 NVMe,顺序读取 >20 GB/s(论文使用 Solidigm D7-PS1010)
完整评估需要相当的硬件配置(2× H100,4 块企业级 SSD)。部分可复现的子实验(SGL vs PRP 带宽对比、gio_uring 吞吐量)只需单 H100 和 GPU 存储层代码。
从研究可复现性角度,论文提供的技术细节足够充分(SM 分区策略、SGL 格式、时隙表结构、vLLM 集成接口),具备在其他 GPU 中心存储系统上独立复现核心设计的条件。
更广泛的影响:Tutti 对 LLM 基础设施的意义
Tutti 的结果暗示了一种新的 LLM 服务基础设施设计哲学:
SSD 优先的 KV 存储层次:
- 第一层:SSD(近乎无限容量,在 Tutti 支持下达到类 DRAM 性能)
- 第二层:HBM(热工作集,高复用前缀)
- 可选 DRAM 层:多节点场景下 NVMe 非本地挂载时的中间缓冲
这一架构转变的经济意义显著。按当前云服务定价,配备 4× 7.68 TB SSD 的服务器(2.46/小时)提供的 KV 缓存容量超过 300 块 H100 的 HBM 总和,成本却不到 1/100。
**智能体工作负载(Agentic Workload)**是最具前景的应用场景:长时间运行的 AI 智能体跨多会话维护大型上下文历史,可以将 KV 状态廉价地持久化在本地 SSD 上,从而使万亿 token 上下文的多轮交互在经济上可行。Tutti 为这一场景提供了技术可行性证明。
总结
Tutti 完成了一次根本性的架构转变:通过 gio_uring 赋予 GPU 对 NVMe SSD 的自主 I/O 控制能力,打破了长期以来让 SSD 支持 KV 缓存实际不可用的 CPU 瓶颈。
这项工作的核心知识贡献,是识别出 CPU I/O 控制路径(而非数据路径)才是 SSD KV 缓存的根本瓶颈。GDS 已经解决了数据路径;Tutti 的洞察是,仅此不够,控制路径必须迁移到 GPU——gio_uring 和时隙感知调度器是这一洞察的工程实现。
三项核心创新形成完整闭环:
- GPU 原生对象存储(SGL 寻址)解决了物理寻址的内存开销和 I/O 粒度失配问题
- gio_uring(SM 分区、无锁环形缓冲区)消除了 I/O 控制路径上的 CPU 介入
- 时隙感知调度器(离线剖析、读写解耦)消除了带宽竞争和 SM 竞争
结果是,SSD 支持的 KV 缓存在几乎所有实际工作点上实现了接近 DRAM 的延迟,同时成本降低约 100 倍。交叉点分析(98.3% 命中率)表明,在 80–90% 命中率的典型长上下文部署场景下,Tutti 完全维持计算绑定状态,气泡时间几乎为零。
论文于 2026 年 5 月发布于 arXiv,凭借扎实的工程贡献和清晰的系统性能数据,是顶级系统会议(OSDI、EuroSys 或 ATC)的有力候选。代码一旦公开,将有望成为 GPU 中心 KV 缓存服务的参考实现,推动整个领域向 SSD 优先的长上下文服务架构演进。
对于关注 LLM 推理系统的研究者和工程师:Tutti 的贡献清晰、实现扎实、结果可信。核心思路(将 I/O 控制路径迁移到 GPU)具有良好的可推广性,未来可延伸到其他需要大规模 GPU-to-NVMe 数据访问的场景,如模型权重的动态卸载(weight offloading)或大规模激活存储。
建议持续关注代码仓库开源进展和后续 OSDI/EuroSys 版本(如有),届时可期待更完整的大模型规模验证和远端检索路径的优化方案。
这项工作最重要的意义或许不在于具体的性能数字,而在于它为 LLM 服务的经济学提供了一个新的可能性:用 100 倍便宜的 SSD 替代 DRAM 作为 KV 缓存的主要存储层,同时不牺牲服务质量——只要 I/O 子系统做得足够好。