正在获取所在地区…
正在读取天气
音乐
暂未播放
FlatCake 完全拆解:把 DRAM 与 SSD 拉平成同一层 KV 存储
背景:前缀缓存把存储推上了关键路径
前缀缓存(prefix caching)如今几乎是所有 LLM 推理引擎的标配。它的出发点很简单:同一个系统提示词、同一段多轮对话的历史、同一个 Agent 会话此前追加过的工具输出,在很多请求之间是共享的,那么这些 token 的 key/value 张量(下称 KV 块)就不必再算一遍。把 KV 块留在 HBM 里,下一次请求命中前缀时直接接管,prefill 只需要计算未命中的后缀。
问题出在容量上。随着负载形态从“短小、全局共享的系统提示词”,演化到多轮对话,再演化到智能体式编程会话(每轮都往上下文里追加工具调用结果、检索到的文件和命令输出),活跃的 KV 工作集很快就达到几十 GB、持续几个小时。FlatCake 的论文里给出一组生产轨迹的统计:他们分析的 Claude Code 生产轨迹(claude-code-weka-0621)中,每个请求平均输入 218,922 个 token,平均跨越 3,421 个块。
GPU HBM 装不下,主机 DRAM 也装不下,于是 NVMe SSD 被推上前台。当前的主流做法是把 GPU HBM、主机 DRAM、NVMe SSD 组织成经典的分层存储(tiered storage):
- HBM 是 L1,最快的介质;
- 主机 DRAM 是 L2,作为 SSD 与 GPU 之间的中间缓存与暂存层;
- NVMe SSD 是 L3,容量最大、带宽最低。
这套结构之所以成立,靠两个前提:
- 时间过滤:基于最近性(recency)的 DRAM 缓存能捕捉住活跃工作集,SSD 只服务冷 miss,一直待在关键路径之外。
- 暂存与合并:因为增量淘汰会把磁盘上的物理布局打得七零八落,把 SSD 读取先暂存到主机 DRAM 缓冲区里再做拼装,是隐藏设备延迟、重整碎片读的有效手段。
FlatCake 这篇论文(arXiv:2610.38355,2026 年 10 月 28 日提交,作者来自复旦大学、北京大学、MetaX、中科院计算所等单位)的核心判断是:真实生产负载同时打破了这两个前提。DRAM 既当不好过滤器,也当不好暂存层,于是整条恢复路径(把 SSD 上的 KV 块搬回 GPU 去算注意力)反而变成了新的瓶颈。
这篇拆解沿着论文的论证线走:先看生产轨迹暴露出的两类复用模式,再看两层瓶颈各自怎么形成,然后逐模块拆 FC-GVA、FC-MAS、FC-CIO 三个组件,最后看消融与跨模型实验说明了什么。
两类复用模式:时间过滤为什么失效
论文统计了来自三个提供方的八条生产轨迹,覆盖 Mooncake、Qwen 百炼与 Claude Code,并在无限缓存容量下计算理想命中率。结果跨度极大:从 36.6% 到 98.3%。但这八条轨迹并不是均匀分布,而是落在两个截然不同的复用区间里。
第一个区间:一次性块为主,复用距离极长
七条前缀缓存与对话轨迹中,被访问过一次就再也不碰的块占 53.2%–94.0%,其中 Qwen 的 to-B API 轨迹高达 94.0%,Mooncake 的 ToolAgent 轨迹为 78.5%。同时,可复用块的复用距离(以中间隔了多少个请求计)呈现重尾分布:Qwen to-B API 的平均复用距离为 2,550 个请求,P99 达到 54,728;Mooncake ToolAgent 则是 489 与 4,085。
访问本身是倾斜的——访问量最多的前 10% 的块承担了 34%–65% 的全部访问——但这并不足以救回 DRAM。原因在于:如果每个新生成的块都先进入 DRAM,那些只用一次的块会持续把本来可复用的会话前缀挤到 SSD 上,等这个前缀下一轮再被请求时,它早就不在 DRAM 里了。
论文还引用了 Bidaw 的观察来强化这一点:即使让快速层容纳 40.1% 的块总量,LRU、FIFO 和队列增强的淘汰策略所得到的 DRAM 命中率都只有约 20%。也就是说,换淘汰策略几乎无济于事,问题出在“先入 DRAM”这个结构本身。
第二个区间:密集复用,但工作集本身超容量
Claude Code 轨迹呈现出完全相反的形态:只有 16.6% 的块是一次性的,无限容量下命中率 98.31%;复用距离的平均值仅为 0.85 个请求、P99 为 14——同一会话的下一轮、并发拉起的子 Agent 几乎立刻就会复用同一份前缀。
按时间过滤的标准,这是一份“友好”的负载,理应被 DRAM 完美留住。但它的体积不行。同一个请求平均 3,421 个块,而 Mooncake 对话轨迹只有 23 个块。论文按 DeepSeek-V4-Flash 的压缩 FP8 表示测算了每个 GPU rank 上的历史 KV 占用:单请求中位数 0.52 GiB、平均 0.80 GiB;八个 rank 合计中位数 4.1 GiB、平均 6.4 GiB,P90 达到 16.0 GiB。
这意味着即使在深度压缩之后的 KV 表示下,一个会话的历史就能吃掉单卡一大块 HBM,而并发的多轮会话叠起来,活跃工作集很快就超过主机 DRAM 容量。

图源:FlatCake 论文(arXiv:2610.38355),Figure 2。
上图是论文的动机图。(a) 是七条非 Agent 轨迹上按块排序的累计访问份额,展示了访问倾斜;(b) 是 DeepSeek-V4-Flash 各轨迹上每个 GPU rank 的 FP8 历史 KV footprint,竖线是 P10–P90 区间,虚线是 128K token 的参照线;(c) 则把 TTFT 拆成若干段,对比“分层存储从 SSD 恢复”与“完全重算”两种情况随上下文长度的变化。三张图合起来说明:工作集既大又分散,(c) 里随着上下文变长,从 SSD 恢复的代价会一路爬升并越过完全重算。
两类模式的共同结果
两种模式看似相反,落点却相同:大部分缓存命中的 KV 必须由 NVMe SSD 来服务。既然这样,让块在 DRAM 与 SSD 之间来回迁移,只带来了数据搬运,却没能让热数据真正留在 DRAM 里。分层存储的第一条前提——时间过滤——就此失效,SSD 被推到了关键读路径上。
两个瓶颈:暂存与布局
既然 SSD 必须承担大部分命中,端到端性能就由 SSD 恢复路径决定了。论文引用 Chen 等人的统计:存储 I/O 占了 TTFT 的 51%–98%。但原始 NVMe 带宽并不是首要限制——Ren 等人的实验里,把 SSD 模拟在 DRAM 中、带宽给到 16.9 GiB/s 时,KV 卸载只跑出 2.0 GiB/s。瓶颈在于分层机制本身。
瓶颈一:主机暂存导致并发内存争用
在分层结构里,把 SSD 上的前缀恢复回 GPU,需要先把块从 SSD 拷进主机暂存缓冲区,再逐层搬到 HBM;同时后台还有“晋升”流程(把 SSD 块拉进 DRAM 作为 L2 缓存)。
即使把 SSD→DRAM 与 DRAM→GPU 两跳做成分块流水,单请求的传输时间可以压到 max(TSSD→DRAM,TDRAM→GPU),推理引擎仍然必须为每个在途前缀持有一份主机暂存缓冲区,直到 H2D 传输完成。于是暂存内存的需求随上下文长度与并发数线性增长:
Mstage=O(context×concurrency)这个式子里的每一项都可以从论文给出的数字里看到:上下文是几百 KB 到数 MB 级的 KV(一个 218K token 的请求在 FP8 下就是 0.8 GiB 量级),并发数是流水线上同时在恢复的请求数。在高负载下,并发的恢复请求会把这块有界的暂存池耗尽,SGLang 的 HiCache 在预取被阻塞时会撤销预取、回退到 GPU 上完全重算——本来就存在 SSD 上的块,因为拿不到暂存缓冲区,变成了昂贵的计算 miss。
这就是论文端到端实验中那颗“命中率崩塌”的炸弹:命中率并不是因为块被淘汰而下降,而是因为块还在,只是恢复动作被撤销了。
瓶颈二:按淘汰顺序落盘带来的碎片
第二条路是绕开主机 DRAM,用批量 GPUDirect Storage(GDS)直接把 SSD 读进 GPU。这条路上 GDS 是有效的传输原语,但它解决不了磁盘层的碎片。
关键约束是:一个 NVMe 读命令只能对应磁盘上一段连续的 LBA 区间。GDS 的 scatter-gather 列表允许把数据分散写入不连续的 GPU 页,但前提是源字节在 SSD 上位于一段连续 LBA 里。
而分层存储的 SSD 空间是按增量淘汰填充的:块先在 DRAM 中,LRU/FIFO 淘汰时才落盘,同一会话的块会分散在多个不连续的偏移上;再加上粗粒度的 FIFO 桶回收,桶一旦回收就会横切多个会话,把有效前缀一起作废。于是恢复一个已经分散的前缀,需要发出 O(layers×blocks) 个小读:
Nreads=O(L×B)其中 L 是层数,B 是前缀跨越的块数。这也解释了为什么 SSD 带宽提升 3.3 倍,吞吐只涨了 1.33 倍——读请求的数量和 CPU 提交开销没有减少。
为什么分层会掉进这个陷阱
论文的归因很简洁:分层存储是为可变的页设计的。一条被写入的页,它未来会被谁访问、它的内容在物理空间上应当和谁挨在一起,写入时都是未知的,因此只能靠“先放进快的层,再看要不要淘汰”。
而 LLM 的 KV 块恰好具备三个与通用页截然不同的性质:
- 一次写入、不可变:块生成后从不原地修改,因此没有脏状态需要回写;
- 会话范围整体复用:一轮生成出的块属于一个已知的会话前缀,下一轮会被跨层整体恢复;
- 写入时就已知理想的磁盘布局:同一会话、同一层的 token 在后续轮次总是被一起读。
也就是说,KV 块最理想的磁盘空间分组在创建时就已经确定。如果写入时直接按“会话连续、层优先”的顺序放到目标介质上,并且此后永不迁移,那么 SSD 恢复天然就是顺序的,也就不再需要主机 DRAM 来做缓存或拼装。
核心思想:把两种介质拉到同一层

图源:FlatCake 论文(arXiv:2610.38355),Figure 1。
上图很直接地展示了两种组织的差别。左边 (a) 是分层:主机 DRAM 作为 L2 位于 NVMe SSD 这个 L3 之上,两者之间有一条标着“LRU evict”的红色箭头,意味着块只有被淘汰时才越过这条线到达 SSD,之后要想再被读,就得沿原路回去。右边 (b) 是 FlatCake:主机 DRAM 与 NVMe SSD 被放在同一层,共用同一个统一的地址空间,两条箭头分别从扁平接口指向两者,两者之间没有任何箭头——不回迁、不晋升、不淘汰传输。
一句话概括 FlatCake 的核心思想:
KV 块是写一次、不可变、按会话范围复用的。因此可以在创建时就直接把它放到目标介质上,此后永不迁移;DRAM 与 SSD 只是两种对等介质,由同一个 64 位地址空间寻址。
这个“扁平”并不只是画图上的概念,它带来三个具体的结构性变化:
- 没有跨层迁移:块要么一开始就住在 DRAM,要么一开始就住在 SSD。省掉了“SSD→DRAM 晋升”与“DRAM→SSD 淘汰”这两笔在所有分层设计里都要发生的数据搬运,也省掉了相应的读/写放大。
- 地址即路由:块的物理位置终生不变,可以把介质、节点、偏移直接编码进 64 位地址句柄里,用位掩码就能判断“这次访问该走哪条传输通道”,不需要逐级查页表、也不需要遍历 LRU 链。
- 回收不跨界:SSD 的空间按会话区间回收,而不是按固定的桶回收,因此不会出现“回收一个桶、作废 k 个会话前缀”的情况。
FlatCake 把这三个变化落到三个组件上,分别回答请求生命周期上的三个问题:
- Q1 写入路径:怎样在创建时就把块绑定到对等介质,并以 O(1) 完成路由?——FC-GVA(Global Virtual Address Manager)。
- Q2 读控制路径:在没有 DRAM 暂存的情况下,怎样把快的 DRAM 命中与慢的 SSD 恢复调度开?——FC-MAS(Medium-Aware Request Scheduler)。
- Q3 读数据路径:怎样把碎片化的块请求合并成大的顺序 GDS 传输,并直接流入一块有界的 GPU 窗口?——FC-CIO(Coalescing Direct I/O Engine)。
系统总览

图源:FlatCake 论文(arXiv:2610.38355),Figure 3。
上图分为 (a) 控制路径与 (b) 数据路径两部分。
(a) 中,请求按顺序经过三个阶段:❶ FC-GVA 把块键(block key)映射成 64 位 GVA,并完成写时一次性的介质放置;❷ FC-MAS 把请求分成 DRAM-Ready 与 SSD-Preparing 两个队列;❸ FC-CIO 负责把合并后的 SSD 读任务交给 GDS。图中还能看到 GVA 的位域:最高 4 位是介质类型,接着 12 位是节点,剩下的 48 位是偏移。
(b) 中,上层是 prefill 与 decode 的 GPU,它们的 KV 槽位在 HBM 里;下侧是两块对等介质——钉住内存(pinned)的主机 DRAM 与 NVMe SSD。DRAM 命中的数据通过 cudaMemcpyAsync 直接 H2D;SSD 命中的数据由 FC-CIO 经 GDS 流进一块有界的 GPU 暂存窗口,再由 scatter 内核散进分页的 KV 槽位。
去掉中间层 DRAM 后,原来由它隐式承担的职责——淘汰时决定谁落 SSD、预暂存以隐藏 SSD 延迟、在主机缓冲里拼装碎片块——必须由上面的三个组件显式接过来。这也是论文用三个 Q 来组织设计的原因。
FC-GVA:全局虚拟地址管理
在分层结构里,存储层并不需要在写入时决定一个块的长期归属:所有块先进 DRAM,LRU 反应式地把冷块推到 SSD。FC-GVA 要回答的问题正是:如果没有反应式淘汰,怎样在创建时一次性决定块的归属,并且让读路径以 O(1) 找到它?
写时作用域直放
FC-GVA 依赖两个领域性质:
复用作用域在请求到达时就可观测。 一个前缀属于全局共享的系统提示词、短轮次对话,还是几 GB 级的 Agent 会话,在块被生成的那一刻就是已知的——这正是通用分页系统缺少的信息。
平坦结构里误判的代价是不对称的。 在分层结构里,把一个本该留在 DRAM 的块放到 SSD,后果是“SSD 命中 + 可能的暂存争用”;反方向也一样,错放可能引发回迁风暴。但在平坦结构里,两种介质都是可直接访问的对等介质,SSD 上的作用域又是连续布局的(见 FC-CIO),把可复用的块放在 SSD 上不会造成 miss,也不会引发迁移——只是这次命中以 SSD 带宽而不是 DRAM 带宽来服务。
因此 FC-GVA 的策略相当直接:用写时可见的引擎信号把每个新块直接放到 DRAM 或 SSD 上。论文给出的分类见下方的表格(原论文 Table 2)。
| 复用作用域 | 写时信号 | 目标介质 |
|---|---|---|
| 共享提示词 | 前缀引用计数 >1 | DRAM(m=0) |
| 短轮次对话 | 轮次 >1,且此前回复较短 | DRAM(m=0) |
| Agent 会话 / 长上下文 | 上下文 ≥Lagent 或回复 ≥τ | SSD(m=1) |
| 一次性 / 溢出 | 轮次 =1,或 DRAM 已满 | SSD(m=1) |
这张表与前面的两类复用模式一一对应:占用小、复用距离短的共享提示词与短轮次对话留在 DRAM;体积大的 Agent 会话、期望复用距离很长的回复、不共享的单轮 API 请求,以及 DRAM 满时到达的写入,全部直接放 SSD。
一个值得注意的细节:当多轮或 Agent 会话在后续轮次追加新 token 时,FC-GVA 会把新生成的块绑定到同一个会话作用域,保证这个会话的历史始终分组在同一块介质上。跨节点时,由一个轻量协调器跟踪 DRAM 与 SSD 的聚合容量,在写入时分配 GVA;因为 KV 流量以读为主,容量协调不在关键读路径上。
64 位 GVA 与两级寻址
块创建后永不迁移,物理位置也就终生不变,因此可以直接把它烘进地址句柄里。FC-GVA 用两级结构完成键到地址的翻译:
第一级:一张驻留 DRAM 的哈希表,把块键(会话 ID、层号、token 区间)映射到不可变的 64 位 GVA,一次 O(1) 查找即可。这里有一个并发细节:为了避免读后写冒险又不给读加锁,FC-GVA 在异步写提交之前就分配好块的 GVA,但只有在 DMA 或 GDS 写入的回调完成后,才把这个键→GVA 条目发布到第一级索引里。这样调度线程即使看到某个键,也一定意味着它背后的数据已经落位。
第二级:GVA 本身分成三个固定位域:
- MediaType
[63:60]:4 位,0 表示 DRAM,1 表示 SSD,其余编码留给未来介质; - NodeID
[59:48]:12 位,最多 4096 个节点; - Offset
[47:0]:48 位字节偏移,单节点最多 256 TiB。
用寄存器级的位掩码取出 MediaType 与 NodeID,就能把访问直接路由到相应的传输后端:本地 DRAM 走 cudaMemcpyAsync,远端 DRAM 走 RDMA,本地 SSD 走 GDS 的 cuFile,远端 SSD 走 NVMe-oF。这里没有逐层的页表,也没有需要加锁的 LRU 链。
另一个细节是位域的位置选择:把 MediaType 放在最高位,使得 DRAM 与 SSD 的地址落在不相交的数值区间里。于是“这个请求是否需要 SSD”可以退化为一次整数比较——260 就成了一条天然的分界线:GVA 小于它的,所需介质是 DRAM;大于等于它的,就需要 SSD 参与。FC-MAS 正是利用这一点做分类(见下一节)。
会话粒度的 SSD 回收
写时直放还有一个附带好处:回收变简单了。因为块从不原地修改,释放 SSD 容量不需要任何脏块回写。FC-GVA 用原子引用计数跟踪每个会话作用域的活跃读者;当 SSD 利用率越过高水位时,回收最老的、无人引用的会话区间,把它们的 GPU File 交还空闲池——这是 O(1) 的元数据操作。
对比之下,Mooncake 这类分层系统按固定大小的桶增量分配 SSD 页,并按 FIFO 顺序回收桶。回收一个桶会横切 k 个不同的会话。如果受影响会话 i 再次被请求的概率是 pi,那么一次桶回收至少作废一个活跃会话前缀的概率是:
Pinvalidate=1−i=1∏k(1−pi)当 k 增大时,这个概率逼近 1。也就是说,在桶回收的粒度下,作废活跃前缀几乎不可避免,一旦被作废的前缀再次被请求,就只能重算。按整个会话区间回收则完全消除了这种跨会话作废,同时保留了 FC-CIO 所需的连续磁盘布局。
FC-MAS:介质感知的请求调度
分层系统把 SSD 命中在准入之前先预暂存进主机 DRAM,以此对调度器隐藏 SSD 延迟;代价就是上一个瓶颈里那个 O(context×concurrency) 的主机内存争用。FC-MAS 去掉预暂存,但这样一来,本地 DRAM 与 NVMe SSD 之间约 5 倍的带宽差就直接暴露在调度器面前。
调度器因此面临一个两难:
- 共享 FCFS 队列:一个几 GB 的 SSD 恢复会堵住排在后面的、只需一次亚毫秒级
cudaMemcpyAsync的 DRAM 命中请求。这就是跨介质的队头阻塞。 - 严格 DRAM 优先:在 DRAM 请求持续到达的情况下,大的 SSD 会话会被无限期饿死。

图源:FlatCake 论文(arXiv:2610.38355),Figure 4。
上图用六个请求(三个的 KV 在 SSD、三个在 DRAM)对比了两种调度。(a) 是共享 FCFS:请求 2、4、6 是快速的 DRAM 命中,却被迫排在慢速的 SSD 恢复 1、3、5 之后,时间线上能看到明显的空转。(b) 是 FC-MAS 的双队列:DRAM-Ready 队列里的请求立即派发,SSD 预取在后台并行推进,时间线被压短。FC-MAS 用三个机制同时解决这两难。
双队列分离
请求到达时,FC-MAS 先给它打上到达时间戳 arrival_ts,解析出其前缀块的 GVA,然后按其中最慢的介质给请求分类:
- 若所有缓存块都在 DRAM 中(maxjGVAj<260),请求进入 DRAM-Ready Queue,立即派发到 GPU;
- 只要有任何块在 SSD 上(maxjGVAj≥260),请求进入 SSD-Preparing Queue。
注意第二类里包含混合请求:既共享一个钉在 DRAM 上的系统提示词,又带着一段住在 SSD 上的会话历史。这种请求按最慢介质归类是正确的——它的完成时间由 SSD 恢复决定,而不是由 DRAM 提示词决定。
对进入 SSD 队列的请求,FC-MAS 把它那些 DRAM 驻留的前缀块钉住不动,同时由 FC-CIO 异步地把 SSD 块预取进 GPU HBM。预取与 DRAM 命中的 GPU 执行重叠起来。
按体积感知的预取优先级
SSD 队列内部也有公平性问题:一个大的 Agent 恢复可能在 NVMe 阵列上挡住几个小的 SSD 请求。FC-MAS 借用最高响应比优先(Highest Response Ratio Next,HRRN)的思路来排序,但把“服务时间”替换成“SSD KV 传输量”:
Priorityi=1+KV_sizeiwait_timei式中 wait_timei 是请求 i 在 SSD 队列中已经等待的时间,KV_sizei 是它需要从 SSD 传输的 KV 体积。
这个式子的含义值得细品。当等待时间相同时,体积小的请求优先级更高,从而降低平均排队延迟——这是 HRRN 的经典性质。而随着等待时间线性增长,多 GB 级别的恢复其优先级最终一定会超过所有刚到的小请求,因此保证不会被饿死。系数 1 保证优先级始终大于 1,也就是任何已等待请求的优先级都高于任何刚到达请求的响应比。选择“体积”而不是“预计服务时间”作为分母,是因为在 GDS 批量提交下,KV 传输体积与实际占用带宽几乎成正比,而精确估算服务时间需要额外建模。
按到达时间重新准入
一旦 FC-CIO 完成某个请求的 SSD 预取,它的 KV 块就驻留在 GPU HBM 里了。此时这个请求已经“准备好了”,本该进入 DRAM-Ready 队列。如果按常规把它追加到队尾,那么在预取期间不断到达的 DRAM 命中会一直排在它前面,SSD 会话依旧会被饿死。
FC-MAS 的处理是:按原始到达时间戳插入。因为它在 SSD 预取上花掉的时间已经算进了它的队列资历,重新插入后它会排在新到达的 DRAM 命中之前被派发。这把两条队列之间的公平性统一到了同一条时间轴上——先来的先走,不管它来自哪条介质。
FC-CIO:合并式直接 I/O 引擎
去掉主机 DRAM 暂存,解决了主机内存争用,但也把碎片读以前用来重整的那个中间缓冲去掉了。FC-CIO 要回答的是:没有主机缓冲,怎样把块级的前缀请求变成顺序的 NVMe 传输,并流进一块有界的 GPU HBM 预算?
前面说过,在分层后端上直接打开 GDS 并不管用:GDS 的 scatter-gather 列表只能分散到不连续的 GPU 页,源字节仍须位于一段连续 LBA。当块按 LRU 淘汰顺序落盘时,一个会话的块散落在磁盘各处,于是又是 O(L×B) 个小读。FC-CIO 的思路是把写时磁盘布局、读时描述符合并、层流水线 GPU 流式传输三者协同设计成一条生产者—消费者流水线。
写时:层优先的会话作用域布局
先看消费顺序。prefill 时,第 l 层的注意力要消费这个层上所有缓存前缀的 token,然后执行才推进到第 l+1 层。磁盘布局应当匹配这个顺序。
当 FC-GVA 把一个会话作用域分配到 SSD 时,FC-CIO 把它的块组织成一个逻辑 GPU File,与 NVMe File Pool 中一个预先分配的、单 extent 的文件一一对应。在每一轮的写批次内部,块按层优先(layer-major)排列:属于第 l 层的所有 token 块占据 SSD 上一段物理连续字节;同一会话后续轮次的新增内容以层优先的段追加到同一个 GPU File 末尾。
与按 LRU 增量淘汰填充不同——后者会把并发会话的页按淘汰顺序交错写入——这种写时布局保证:恢复某一层的前缀时,目标是连续的 LBA 跨度。理想情况下 NVMe 读命令数从 O(L×B) 降到:
Nreadsideal=O(L)读时:排序—合并
即使磁盘上连续,推理引擎的前缀查询仍是按页粒度在基数树(radix tree)上进行的,交给存储后端的是几十万个块描述符,它们可能跨越多个轮次、多个作用域,并且到达时不按偏移排序。
为了在发出 GDS 调用之前恢复出连续 LBA 区间,FC-CIO 在提交层做一次轻量的排序—合并:
- 按
(gpu_file_id, offset)对块描述符排序,让物理相邻的块在列表里挨在一起; - 线性扫描,把相邻区间合并成单个大的读描述符。
还有一个细节处理的是“部分命中”与“子块对齐”留下的缝隙:相邻两段之间存在小的、本次并未请求的空洞。FC-CIO 引入可配置的字节阈值 Δgap=64 KiB,把小于这个阈值的缝隙桥接过去——多读一小段顺序字节,换取消除 NVMe 命令被拆分带来的开销。这是一个典型的用小额超额读换命令开销的权衡:64 KiB 的额外顺序读在 NVMe 上几乎不增加时间,却能让一条读命令覆盖更大的跨度。
合并后的描述符按轮转方式分配到工作线程,每个线程用自己的 cuFileHandle、CUDA stream 和 cuFileBatchIOSubmit 提交批次。

图源:FlatCake 论文(arXiv:2610.38355),Figure 5。
上图三部分对应 FC-CIO 的三个环节。(a) 是写时层优先布局:GPU File 内的块按层排列,映射到 NVMe File Pool 里预先分配的一段连续大区间。(b) 是读时排序—合并:五个块描述符按文件与偏移排序后合并成两条 I/O 命令——这正是上面描述的过程。(c) 是三种恢复路径:(i) 带晋升的 SSD 恢复(分层路径),(ii) 经主机缓冲的 DRAM 恢复,(iii) 层流水线的 GDS 恢复——SSD 数据直接流进有界的 GPU HBM 窗口,可选择 scatter,也可直接绕过(bypass)。
层流水线的直接 GPU 流式传输
最后一个问题出在目的地。SGLang 的分页 KV 缓存把 256 token 的 HBM 页非连续地分配,如果直接对各个 KV 页槽位发 GDS 读,又会把每层的连续区间拆回逐页 DMA;反过来,如果把整个几 GB 的前缀先暂存到主机 DRAM,又回到 O(context×concurrency) 的争用。
FC-CIO 的第三条路(图 5(c)(iii)):把合并后的层区间经 GDS 流进一块小型的、双缓冲的 GPU HBM 暂存窗口——大小为每个活跃流两层——然后在 GPU 上把块散进分页 KV 槽位:
- 当计算流正在为第 l 层执行注意力与 FFN kernel 时,I/O 流异步地为第 l+1 层执行
cuFileBatchIOSubmit和一个 GPU scatter kernel; - 这样,暂存内存被限制在一个常数级的窗口里:
对比前面分层结构的 O(context×concurrency),这是这笔设计收益中最关键的一项。
还有一个行为上的差异:当并发恢复在重负载下把窗口占满时,后来的预取会在 CUDA stream 事件上短暂等待,而不是撤销在途预取、去重算已经缓存好的 token。这正是端到端实验里命中率不崩塌的机制来源——系统选择了“等”,而不是“放弃”。
这条流水线需要两侧同步:I/O 流上的合并层读完成时,scatter kernel 从连续暂存缓冲拷到不连续的目的槽位,通过 cudaEventRecord 与 cudaStreamWaitEvent 与计算流对齐。
实现中的工程细节
论文的实现基于 SGLang v0.5.19,规模约为 8.8K 行 C++17/CUDA 与 7.1K 行 Python,挂在 SGLang 标准的 HiCacheStorage 接口(exists / get / put)后面,不改模型 runner,也不改注意力 kernel。这一点很重要:它说明扁平化的收益并不依赖对计算侧的侵入。
几个实现上的选择值得单独讲:
钉住的 DRAM 池与大页。 引擎初始化时,本地 DRAM 后端用 mmap 加 MAP_POPULATE 与 2 MB 大页(MAP_HUGETLB)预分配主机 KV 池,并用 cudaHostRegister 对整个区域注册一次。启动时预触页与钉住大页,消除了页面错误陷入、TLB 抖动,以及每次传输都要临时 pin 的开销。
预分配的 NVMe 文件池与 GDS 对齐。 为了防止写路径上出现文件系统元数据分配与 extent 碎片,FC-CIO 在启动时用 fallocate 在 XFS/RAID-0 阵列上预分配一组固定大小的单 extent 文件,以 O_DIRECT 打开,并 cuFileHandleRegister。GPU File 内所有逻辑块偏移与 GPU 暂存缓冲切片都对齐到 4 KiB 边界,满足 libcufile 的 DMA 对齐要求,避免驱动内部的 bounce buffering。这一条与写时布局是一体的——单 extent 的文件本身就是层优先顺序布局得以成立的物理基础。
双缓冲 GPU 暂存与页 scatter。 对每个请求的 GPU 缓冲动态调用 cuFileBufRegister 会带来毫秒级的 BAR1 与 IOMMU 映射开销,所以 FC-CIO 在初始化时分配并注册一块固定的每 GPU HBM 暂存窗口。恢复时在这个已注册窗口内两块 ping-pong 层缓冲上做 GDS 流水读。
无锁的一级 GVA 索引。 键→GVA 索引实现为基于 64 位原子 CAS 的并发开放寻址哈希表。因为 GVA 发布之后不可变,会话回收又是异步地释放整个作用域,调度线程查询前缀命中的路径是 wait-free 的读,不与后台的写完成 worker 争用。
实验与结果
实验设置
主实验平台是 8 块 NVIDIA B200(每块 180 GB HBM3e)的服务器,3 TB 主机 DRAM,8 块 3.84 TB 三星 PM9A3 NVMe 组成 RAID 0。所有对比系统被配置成相同的 KV 预算:1 TiB 主机 DRAM 与 4 TiB NVMe SSD。主机 DRAM 到 GPU 的 H2D 带宽为 25–32 GB/s,GDS cuFile 每个 worker 为 5.5–6.5 GB/s。
模型是 DeepSeek-V4-Flash-0731,一个 284B 参数的 MoE 模型,43 层混合 MLA,1M token 上下文窗口,KV 缓存使用 fp8_e4m3、256 token 的页,每个 rank 每 token 占 7.5 KiB。负载是重放 Claude Code 的生产轨迹 claude-code-weka-0621:98,827 个请求、216 亿输入 token、393 个多轮会话,按记录的到达顺序、在 0.1–1.0 QPS 的到达率下重放,不做缓存预热。
对照系统有三个:
- +L2 Host DRAM:SGLang HiCache 的两层 HBM–DRAM LRU 缓存,不做 SSD 卸载;
- +L3 Mooncake:三层 HBM–DRAM–SSD,SSD 上的 KV 块先经主机 DRAM 暂存再 H2D,并晋升进 DRAM 层;
- FlatCake:FC-GVA、FC-MAS、FC-CIO 三件套。
跨上下文长度的实验则用 Llama-3.1-70B(TP4,4 块 A100),并额外对照 Recompute-only(不做卸载、全部重算)与 Mooncake-GDS(保留分层淘汰顺序的落盘,但把 SSD 读路径换成 GDS)。
端到端缩放与命中率稳定性

图源:FlatCake 论文(arXiv:2610.38355),Figure 6。
上图四个面板分别是 (a) 平均 TTFT、(b) 中位数 TTFT、(c) 总 token 吞吐、(d) 交付命中率,横轴是到达率。图上有一条淡灰色细线标出源轨迹的前缀复用上限 94.72%。
TTFT。 在整个负载范围内 FlatCake 的平均与中位数 TTFT 都最低。峰值到达率 1.0 QPS 时,FlatCake 平均 TTFT 为 7.57 s;+L3 Mooncake 为 12.24 s,+L2 Host DRAM 为 17.51 s,对应约 1.6 倍与 2.3 倍的差距。中位数(P50)的分离更明显:FlatCake 为 6.26 s,Mooncake 为 11.45 s,Host DRAM 为 18.39 s——这是 1.8 倍与 2.9 倍的差距。中位数差距大于平均值,说明分层方案的延迟分布被长尾拖得更歪,大量的请求落在了慢路径上。
吞吐。 所有配置在 0.25 QPS 以内都能跟上负载,超过之后分层开始饱和。1.0 QPS 时 FlatCake 跑出 98.3K tokens/s,Mooncake 为 61.6K,Host DRAM 为 43.2K。
为什么分层会退化——交付命中率。 这是整组实验里最关键的一组数据。FlatCake 在所有到达率下维持 94.16%–94.57% 的交付命中率,与 94.72% 的上限只差 0.6 个百分点以内。+L2 Host DRAM 在所有到达率下都停在 53.30%,因为多 GB 的 Agent 会话根本装不进主机 DRAM。+L3 Mooncake 在低负载 0.1 QPS 时能到 87.9%,但到 0.5 QPS 及以上时跌到 64.26%——尽管它有 4 TiB 的 NVMe 容量,相比仅用 DRAM 缓存也只多回收了 11 个百分点。
这里的因果链值得清楚地写出来,它把前面的两个瓶颈串在了一起:
- 分层恢复必须把一个在途前缀完整地暂存进主机 DRAM 缓冲,直到 H2D 完成;
- QPS 上升,并发的长上下文恢复耗尽了引擎有界的在途暂存预算;
- 暂存内存饱和时,SGLang 撤销被阻塞的预取、回退到 GPU 上重算;
- 本来在 SSD 上的块,就因为暂存池满了而变成了计算 miss;
- 交付命中率下降,TTFT 拉长。
FlatCake 把合并后的 SSD 读直接流进固定大小的 GPU 暂存窗口;窗口满载时,后来的恢复会在流同步上短暂等待,而不是撤销预取。于是峰值负载下命中率仍有 94.16%。
交付命中率(delivered hit rate)的口径是:只统计在 prefill 开始前已经传输到 GPU HBM 的块。这一定义很关键——它衡量的不是“块是否还在缓存里”,而是“这次请求实际拿到了多少缓存”。分层方案在低负载时命中率高,恰恰是因为暂存池还够用;一旦不够用,块就在,但交付不了。
各机制的效果拆解

图源:FlatCake 论文(arXiv:2610.38355),Figure 7。
上图对比了 (a) +L3 Mooncake 与 (b) FlatCake 的命中按介质(GPU HBM、主机 DRAM、NVMe SSD)叠加的构成。三组趋势:
- FlatCake 的 SSD 承担了大部分复用 token:0.1 QPS 时占 69.3%,1.0 QPS 时仍占 52.6%;主机 DRAM 稳定提供 8.0%–11.7%,对应短时间跨度的前缀。这说明写时直放确实把多 GB 会话历史直接卸载到 SSD,同时让 DRAM 保持干净,供小的、高流转的作用域使用。
- FlatCake 的 GPU HBM 命中份额从 13.6% 涨到 33.6%:因为合并后的 SSD 恢复完成得快,活跃会话内密集的轮次与子 Agent 分叉越来越多地直接命中还在 HBM 里的前缀;总交付命中维持在 94.2%–94.6%。
- Mooncake 的存储(L3)贡献从 51.7% 跌到 18.4%,跌了 2.8 倍,原因是并发恢复耗尽主机暂存并被撤销;同时长恢复停顿让 HBM 内的轮次重叠也起不来(HBM 命中始终在 13.3%–16.6% 之间),总交付命中从 87.9% 掉到 64.3%。
第 2 条里“恢复快 → HBM 命中份额上升”的因果尤其值得一提:在分层方案里,SSD 恢复慢,HBM 里上一轮的前缀被新的 prefill 挤掉了,于是下一轮又得回到 SSD;在 FlatCake 里,恢复足够快,HBM 的自然淘汰还没发生,下一轮就直接在 HBM 命中。这是一个由快恢复引发的正反馈。
FC-GVA:消除迁移与写放大。 由于块创建时一次性绑定到目标介质,并且 SSD 按会话区间回收,每个进入 SSD 的块只被写一次(论文报告的归一化写放大为 1.000×)。分层结构里,粗粒度的 FIFO 桶回收会横切 k 个会话,撤销的预取又触发重算,重算出来的前缀必须重新持久化到存储,于是出现 1.007×–1.018× 的写放大,同时打乱磁盘连续性。写放大数值本身不大,但在磁盘连续性上的连锁效应不小。
FC-MAS:消除跨介质队头阻塞。 与不带双队列的消融(FlatCake-w/o-FC-MAS,保留 FC-GVA 与 FC-CIO,但所有请求走单一 FCFS 队列)比较:分离 DRAM-Ready 与 SSD-Preparing 两条队列,并用前面的 HRRN 优先级调度 SSD 预取,平均 TTFT 降低 10%–11%,P99 尾延迟降低 27%,总吞吐提升 12%。P99 改善远大于均值,印证了 FC-MAS 主要消除的是那些本来只需亚毫秒级拷贝的 DRAM 命中,被大 SSD 恢复堵住的场景;而按原始时间戳重新准入则防止 DRAM 突发把 SSD 会话饿死。
FC-CIO:SSD 带宽、预取延迟与读放大。

图源:FlatCake 论文(arXiv:2610.38355),Figure 8。
上图 (a) 是每个 worker 的 SSD 读带宽,(b) 是平均 SSD 预取延迟。FC-CIO 在各到达率下维持 4.96–5.12 GB/s 的 SSD 读带宽(1.0 QPS 时为 5.03 GB/s),比 +L3 Mooncake(2.69 GB/s)高 1.9 倍。
带宽差的直接后果在预取延迟上:当到达率增长 10 倍(0.1→1.0 QPS)时,FlatCake 的平均 SSD 预取时间几乎持平,为 1.47–1.75 s(1.0 QPS 时 1.64 s);Mooncake 的 SSD 恢复延迟则从 3.16 s 涨到 9.50 s,涨了 3.0 倍,峰值时比 FlatCake 高 5.8 倍。
读放大。 在冷启动与保留缓存两种状态下,Mooncake 的 SSD 读放大都比 FlatCake 高 6.51×–6.72×。这个数字拆开来看:被淘汰打碎的块、后台向 DRAM 的晋升、以及被撤销的预取,都在消耗 NVMe 读流量,却没有把完成的恢复交付到 GPU。FC-CIO 靠写时层优先布局加读时排序—合并,把顺序 NVMe 传输直接转成完成的 GPU 恢复。
另外还有一条动机层面的数据:分层方案下 SSD 读请求数量为 O(L×B),而理想读次数只要 O(L),这中间的倍数正是读放大的结构来源。
跨模型与上下文长度

图源:FlatCake 论文(arXiv:2610.38355),Figure 10。
这一组实验的作用是检验布局与流式的收益是否能从混合压缩注意力(DeepSeek-V4-Flash 的 MLA + B200)泛化到标准 GQA。因为 FC-CIO 按会话作用域、层优先把块分组,每一层的状态在 SSD 上形成连续 extent,与每层张量的维度结构无关——这正是它应当泛化的理由。
上图对比了 Recompute-only、Host DRAM、Mooncake、Mooncake-GDS 与 FlatCake 五者在 8K 到 128K 输入长度上的 (a) 平均 TTFT 与 (b) 吞吐。FlatCake 在所有长度上 TTFT 最低、吞吐最高:相比 Recompute-only 降低 2.3–4.9 倍,相比 Mooncake 降低 1.5–2.4 倍。
最能说明问题的是与 Mooncake-GDS 的对比。在 16K 时两者几乎持平(分别为 2.31 s 与 2.32 s)——这意味着只打开 GDS 传输原语是有效的;但到 64K 时,Mooncake-GDS 的 TTFT 退化到 6.60 s,比 FlatCake 的 3.74 s 慢 1.8 倍;吞吐掉到 8.6K tokens/s,比 FlatCake 的 14.0K 低 1.6 倍。
这条曲线给出了一个干净的因果分离:GDS 本身解决传输原语的问题,但当块按 LRU 淘汰顺序落盘时,传输原语再好也无法形成顺序大读。FlatCake 的写时会话作用域放置(FC-GVA)与层优先排序—合并(FC-CIO),才是让长上下文下 SSD 传输保持顺序的必要条件。而且上下文越长,这条差距越大——因为 O(L×B) 的碎片读数随块数线性增长。
与相邻路线的位置关系
论文的贡献集中在KV 缓存的放置、调度与异构介质间 I/O 上,而不是分页、压缩或 PD 分离本身。和它相邻的几条路线值得对照来看:
- 分页 KV 缓存与基数树前缀缓存(如 PagedAttention 及其后续的分页实现)解决的是“在 HBM 内部如何分配与复用 KV”的问题,FlatCake 解决的是“在 HBM 之外如何组织多种介质”的问题。站内的 KV 块的三张映射表 讨论的是调度器到内核之间块号、槽号的映射,可以作为理解 FlatCake 中 GPU File 与分页 KV 槽位关系的前置;vLLM BlockPool 拆解 则展现了 HBM 这一层的空闲队列与引用计数,正好对应 FC-GVA 在存储层的引用计数会话回收。
- KV 卸载与分层存储:Mooncake、LMCache、SGLang HiCache 一类系统把 HBM、DRAM、SSD 做成三级,并通过预取与晋升隐藏 SSD 延迟。站内的 DeepInfer 生产 KV Cache 存储 讲的也是块缓存与命中重算。FlatCake 的立场是:这些系统优化的是同一套分层结构内部的策略(淘汰、选择性卸载、联合压缩与放置),没有改变“块先进 DRAM、经淘汰或降级到达 SSD”这一迁移结构。
- 直接存储 I/O 与请求调度:LMCache 的预注册 GPU 缓冲配并行磁盘 worker、Tutti 的 GPU 发起 io_uring 队列、存储近端执行等方向,都在加速 SSD 到 GPU 的传输原语。FlatCake 明确指出,只优化传输原语无法解决磁盘碎片——Llama 实验里的 Mooncake-GDS 曲线就是这句判断的实证。
- PD 分离与 KV 传输:分离架构把 KV 从一个实例搬到另一个实例,是“节点间”的传输问题;FlatCake 处理的是单个实例内“介质间”的传输问题。两者在控制面与数据面的划分上有结构上的相似,但 FlatCake 的地址即路由设计(介质 + 节点 + 偏移)也天然能表达远端 DRAM(RDMA)与远端 SSD(NVMe-oF)的访问。
从体系结构的角度看,FlatCake 做的事是把一个软件层的分层结构折叠成地址空间的一维结构——用地址的最高 4 位表达介质,从而把“路由”从一条查找链路变成一次位运算。这与体系结构里用地址位来区分内存空间与设备空间的思路同源,区别在于这里选位的依据是“写时已知的复用作用域”,而不是硬件拓扑。
局限与值得追问的地方
论文没有单独展开局限,但几处取舍可以从设计中读出来:
写时分类的准确性决定了收益的边界。 FC-GVA 依赖“复用作用域在到达时可观测”这一假设,用前缀引用计数、轮次、上下文长度、回复长度等引擎信号来分类。混合请求(DRAM 提示词 + SSD 历史)被归到最慢介质是正确的,但若分类把本该 SSD 的东西放进 DRAM,代价是污染 DRAM;把本该 DRAM 的东西放进 SSD,代价是命中以 SSD 带宽服务、以及 SSD 读流量上升。论文论证了后者在平坦结构里是“可接受”的错,但它仍然要求分类信号本身足够可靠。
SSD 仍然在关键路径上。 扁平化并没有消除 SSD 读取,只是把它变快、变顺序。论文自己也承认大部分缓存命中必须由 SSD 服务,这是由工作集尺寸决定的。因此 FC-CIO 的质量——合并比率、gap 阈值 64 KiB 的选择、单 extent 预分配是否总能满足——会直接影响端到端的 SSD 带宽利用率。64 KiB 这个阈值是一个经验权衡:缝隙越大,多读的字节越多;缝隙越小,命令拆分越多。它没有在论文里做敏感性扫描。
有界的 GPU 暂存窗口把压力从内存转成了等待。 分层方案在暂存池耗尽时撤销预取(命中率崩塌),FlatCake 在窗口满时让请求等待。选择等待保住了命中率,但等待时间仍然可能拉长尾延迟。消融里 FC-MAS 对 P99 的改善(27%)部分来自这一取舍的连带效应,但窗口大小与层数、并发数的关系值得进一步量化。
单会话超大历史与多次追加的布局代价。 层优先布局要求每轮追加到同一 GPU File 末尾,并且单 extent 文件预分配。会话极长、追加极多时,File Pool 的容量管理与回收粒度是否仍能保持 O(1)、是否会出现跨轮次的大范围重排,论文没有给出深入数据。
评价范围集中在两条轨迹与两种模型结构(混合 MLA 的 MoE 与 dense GQA)。论文的跨模型实验说明布局泛化,但对更重的稀疏注意力(如 DSA 的索引器缓存)或混合注意力(滑窗 + 全注意力共用块池)场景下,SSD 上需要额外维护的缓存类型如何放置,仍是后续可以追问的方向。
小结
FlatCake 的论证可以收束成一条清晰的推理链:
- 前缀缓存把存储推上关键路径,因为工作集远超 HBM 与 DRAM;
- 生产负载在复用形态上呈双峰——一次性块与密度极高但体量超容量——两者都让 DRAM 的时间过滤失效;
- 分层结构的两个机制(主机暂存、按淘汰顺序落盘)反而成了瓶颈,分别造成暂存争用下的预取撤销与碎片读;
- 但 KV 块是写一次、不可变、按会话范围复用的,它的理想磁盘布局在创建时就已知;
- 所以在写时按会话连续、层优先直放到目标介质上、永不迁移,可以把分层结构折叠成对等介质:
- FC-GVA 用 64 位地址(介质 4 位、节点 12 位、偏移 48 位)把“路由”变成位运算,并把 SSD 回收粒度拉到会话区间;
- FC-MAS 用双队列、体积感知的 HRRN 优先级与按到达时间的重新准入,把两种介质的带宽差隔离到调度层面;
- FC-CIO 用层优先写布局、读时排序—合并与层流水线的有界 GPU 窗口,把 O(L×B) 的碎片读压回 O(L) 的顺序读;
- 结果是高负载下命中率维持在 94% 左右、TTFT 与吞吐相比分层方案改善约 1.6–1.8 倍,SSD 读放大被消除,而且这些收益在 GQA + A100 上也成立,并与“只换 GDS 传输”形成清晰对照。
如果把这篇读成一条方法论,最有价值的部分可能不是其中任何一个组件,而是那句判断:分层的代价来自“写入时不知道未来”,而 KV 块恰好是写入时就知道未来的数据。 任何一次缓存放置决策,只要在创建时就掌握了复用作用域,就可以绕过“先放入快的层、再靠淘汰降级”的迁移结构。
参考资料
- FlatCake: A Flat KVCache Storage Architecture for LLM Serving,Xin Huang 等,arXiv:2610.38355,2026。https://arxiv.org/abs/2610.38355
- 论文 HTML 全文与图表。https://arxiv.org/html/2610.38355
- Mooncake:把 KVCache-centric 的分离式架构用于 LLM 服务,Qin 等,arXiv:2407.00079。https://arxiv.org/abs/2407.00079
- PagedAttention / vLLM,Kwon 等,SOSP 2023,arXiv:2309.06180。https://arxiv.org/abs/2309.06180
- SGLang:高效执行结构化语言模型程序,Zheng 等,arXiv:2312.07104。https://arxiv.org/abs/2312.07104
- GPUDirect Storage Design Guide(NVIDIA 官方文档)。https://docs.nvidia.com/gpudirect-storage/design-guide/index.html
- 本文图 1、图 2、图 3、图 4、图 5、图 6、图 7、图 10 取自该论文 HTML 版的作者官方图。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
评论区
分享你的想法,与大家交流讨论
正在获取所在地区…
正在读取天气
音乐
暂未播放



