1. 从"月之暗面"到"Mooncake":为什么一个分布式架构要用蛋糕命名
先说清楚一件事:Mooncake 不是某家公司的内部代号,而是国内大模型团队在"长上下文 + 高吞吐推理"这条赛道上公开的一项系统工程实践。它最早出现在2024年的技术分享里,核心目标是解决大模型推理服务中"数据喂不饱算力"的问题。简单说,当你的 GPU 集群规模到了一定程度,瓶颈往往不在显卡本身,而在数据怎么从存储里搬出来、怎么在节点间流动、怎么在显存里被高效复用。
我最初注意到 Mooncake 是因为它把整个架构拆成了"以数据为中心的调度平面"和"以算力为中心的推理平面"两套体系。这个拆分思路和传统微服务里的"控制面/数据面"分离有相似之处,但难度完全不在一个量级。传统架构里数据面最多是处理请求转发,而 Mooncake 的数据面要管理的是从 Remote Storage 到 GPU 显存之间整条链路上的数据搬运、预取、缓存、回收。
如果只用一句话概括它的价值:Mooncake 是一套面向大模型推理场景的分布式数据访问与存储调度方案,它试图让"无限长的上下文"变成实际上可用的推理能力,而不是停留在纸面上的参数。这篇文章我想从工程架构、存储后端选型、数据访问链路三个层面拆开讲,重点放在那些"不实际踩一遍根本发现不了"的细节上。
需要说明的是,Mooncake 的项目代码和文档是开源的,但工程架构本身带有强烈的场景定制色彩。所以我会尽量区分"从代码里能看出来的事实"和"基于大模型推理场景的通用实践推测"——后者我会明确标注,你在参考时也请结合自己的硬件环境判断。
2. 工程架构拆解:为什么调度平面必须和数据平面分离
2.1 两个平面的职责边界
Mooncake 的整体架构可以抽象成两张网:
- 调度平面(Scheduling Plane):负责决定"数据从哪里来、到哪里去、什么时候搬、搬到哪一层缓存",以及"计算任务应该被调度到哪个执行单元"。这一层是无状态的逻辑决策中心,吞吐量要求高,但对数据本身不感知。
- 数据平面(Data Plane):负责真正执行数据搬运。它管理本地的 SSD/NVMe 缓存池、远端对象存储、节点间 RDMA 网络传输、以及 GPU 显存内的 KV Cache 池。
这个分离有一个非常实际的动机:调度频率和数据传输频率的粒度完全不一样。一次请求可能要调度几十个数据块,但每个数据块的实际传输又涉及 RDMA 连接、缓存命中判断、磁盘读写等多个环节。如果把二者搅在一起,任何一个环节的抖动都会传导到调度决策上,最终表现为请求延迟的剧烈波动。
在实际部署中,调度平面通常跑在一组独立的 CPU 节点上,数据平面则分散在每个计算节点的本地。二者之间通过共享的元数据服务协作——这个元数据服务维护了全局的"数据块 -> 位置 -> 缓存状态"映射关系。
2.2 元数据服务是整条链路的命门
在 Mooncake 的场景里,元数据服务要管理的数据量非常惊人。假设一个 1M token 的上下文,按 128 token 一个 block 切分,会产生约 8000 个 KV block;如果同时有 1000 个并发请求,那就是 800 万个 block 级的映射记录。元数据服务的读写延迟直接决定了调度平面的决策速度,也间接决定了整个系统的首字延迟。
我见过很多人在搭建类似系统时忽略这一点,结果到了高并发压测阶段元数据服务成了瓶颈,数据库连接数被打满,Redis 内存暴涨。Mooncake 的做法偏务实:元数据不强依赖外部强一致存储,而是尽量让每个节点在本地缓存整体映射视图,通过版本号和增量同步来保证最终一致。这种"允许短暂不一致,但绝不阻塞调度"的设计思路,在推理场景里是合理的——因为数据块读错顶多多做一次远端 fetch,不会造成不可逆的错误。
注意:如果你要照着 Mooncake 的思路自建元数据服务,千万别走"每一条数据块状态变化都同步给所有节点"的老路。务必设计成"节点按需拉取 + 本地缓存 + 异步失效"的模式,否则副节点一多,通信量就会呈平方级爆炸。
2.3 任务分级:不要让长尾请求饿死主链路
另一处架构细节值得单独说—— Mooncake 的调度平面里做了明确的任务分级。大模型推理场景里,不是所有请求都同等重要。有的请求是首 token 优先(用户等着看到第一个字),有的是整批吞吐优先(离线评测、批量打分),还有的是后台预取任务(数据块搬运)。Mooncake 给这些任务设置了不同的调度优先级,并在队列设计上采用"优先级队列 + 加权公平调度"的组合,避免低优先级任务被完全饿死,同时保证高优先级任务拿到绝对领先的调度时间片。
我自己的经验是:任务分级这件事的收益往往比调大预取窗口更明显。因为只要调度平面能保证"首 token 请求的数据永远先到",即使预取命中率只有 70%,用户感知的延迟也会好于"预取命中率 90% 但调度偶尔打结"的系统。这也是为什么我把工程架构这一节放在最前面——它决定了后面存储和数据链路的性能上限。
3. 存储后端选型逻辑:速度、容量和成本的三角博弈
3.1 为什么最终落到了"多级缓存 + 远端对象存储"的组合
Mooncake 的存储后端不是单一阵地,而是由三层组成的金字塔:
| 层级 | 介质 | 容量 | 访问延迟(量级) | 作用 |
|---|---|---|---|---|
| L1 | GPU 显存 KV Cache 池 | 几十 GB 到几 TB | 纳秒级 | 直接参与计算的 Cache |
| L2 | 本机 NVMe SSD / DRAM 缓存 | 几 TB 到几十 TB | 微秒级 | 跨请求复用的中间缓存 |
| L3 | 远端对象存储(兼容 S3) | 几乎无限 | 毫秒级到秒级 | 冷数据兜底与持久化 |
这个结构本身不稀奇,稀奇的是 Mooncake 对每一层的定位。比如 L1 显存缓存,它并不是简单地缓存"最近使用的数据块",而是结合了推理的执行计划来做"按需驻留"。我理解这背后是对 KV Cache 块的内存分配策略做了精细控制——只有当某个 block 被某个解码阶段实际引用时,它才会从 L2 正式迁入显存。
L3 层的选型也很有意思。Mooncake 的早期原型支持直接挂本地文件系统,但在正式架构里几乎都推荐 S3 兼容的对象存储。原因有二:
- 对象存储天然适合"块级别"的读写。每个 KV block 就是一个独立的 Object,key 可以设计成
request_id:chunk_seq的形式,存取接口都很简单。 - 分布式对象存储的扩容、替换、容错都是现成的。你不用为一个缓存集群去操心数据均衡、节点抖动、坏盘替换这些东西,直接把"存储可靠性"外包给对象存储来解决。
但对象存储的问题也很突出:延迟相对本地 NVMe 差了几个量级,而且对"大量小 Object 并发读"的场景并不友好。所以 Mooncake 在 L3 之上必然要有一层足够厚实的 L2 来做缓冲和合并。
3.2 本地缓存替换策略:为什么 LRU 不够用
如果你按传统思维给 L2 缓存配一个 LRU 驱逐策略,一定会出问题。原因是:KV Cache 块的生命周期不是均匀分布的。一次长上下文请求可能先读取头部几个块,跑了几百步解码后才轮到后面的块。如果缓存只是简单按"最近被读过"来淘汰,那些"马上会被用到但已经很久没读"的中间块会被过早驱逐,导致重复 fetch。
Mooncake 的做法更接近"带优先级和时延感知的驱逐"。它会记录每个块的关联请求状态,预估这个块下一次被使用的时间窗口,再结合块的体积、来源热度来做驱逐决策。坦白讲,具体算法在公开资料里没有完全展开,但我认为核心思想是:
不能只看到"这个块上次被访问的时间",还要预测"这个块下一轮被访问的时机"。让缓存淘汰策略跟着推理的执行轨迹走,而不是跟着历史访问记录走。
如果你在自己的项目里要复刻这套逻辑,一个可行的简化版本是:给每个块打上"请求 ID + 上下文偏移量"的标签,然后在驱逐时优先淘汰那些"当前请求偏差值最大且在近期不会再被读取"的块。这样虽然做不到全局最优,但已经能显著减少重复 fetch 的比率。
3.3 写入策略:突发写放大与异步刷盘
聊完缓存读取,再说写入。Mooncake 的 L2 缓存写入会面临一个典型的写放大问题。假设上游对象存储返回一个 4KB 的块,本地 SSD 需要写入,但如果你的缓存层每次只写 4KB,SSD 的实际读写效率会非常差(写放大、随机写延迟、磨损加剧)。
我推测 Mooncake 的处理是走"内存聚合 + 按批次异步刷盘"的路线:先在 DRAM 里攒一批相同或相邻前缀的块,合并成一个较大的 I/O 再落盘。这个思路和很多 KV 存储引擎的 LSM 策略类似,逻辑上完全站得住脚。
实操中你需要注意一点:合并写入会引入"数据在 DRAM 中尚未持久化"的窗口期。如果这个窗口期刚好遇到进程崩溃,这些数据会丢失,但下一次请求会重新从 L3 fetch,所以不致命。关键是别把"还没落盘的数据"当作"已经缓存命中"上报给调度平面,否则会引发缓存一致性的隐蔽 bug。
4. 数据访问链路全流程:从请求发起到 KV Cache 命中
4.1 一次推理请求的数据路径
把整条链路串起来看,一次典型的推理请求在 Mooncake 中的数据流动大致是:
- 请求进入调度平面,解析出 prompt 的 token 序列,按固定长度切分为若干个 KV block。
- 调度平面查询全局元数据,判断每个 block 在 L1、L2、L3 中的分布情况。
- 对命中的块,直接生成"显存复制指令";对未命中的块,生成"预取指令",并投递到数据平面的传输队列。
- 数据平面把这些指令打包成异步任务:从 L3 拉取、经 L2 缓存、再进入 L1 显存驻留队列。
- 推理引擎按块序号顺序消费显存中的 KV Cache,完成 prefill 和解码。
看起来是一个直白的流水线,但真正的复杂度藏在第 3 步和第 4 步的交界处:调度平面决定"预取哪些块"和"什么时候预取",直接影响后续推理阶段的等待时间。这里 Mooncake 引入了"流水式预取"——不等所有块都到位才开始推理,而是采用类似指令流水线的思路,边算边取,把数据搬运的延迟尽量与计算重叠。
4.2 端到端传送协议:RDMA 还是 TCP?
凡是做过高性能分布式系统的人都会问一句:节点间数据搬运用什么网络协议。Mooncake 的公开资料里明确提到了 RDMA 在高吞吐场景下的优势。在推理场景里,KV block 的传输内容都是大块且连续的显存数据,非常适合 RDMA 的单边读写语义,而且能大幅降低 CPU 的中断和拷贝开销。
但在实际落地时,我建议不要一上来就上 RDMA。理由很现实:RDMA 的可运维性比 TCP 差很多,网卡固件、交换机丢包、连接数管理都更容易踩坑。如果你只是小规模验证 Mooncake 的思路,刚开始可以用 TCP 把整条链路跑通,把重点放在"块预取策略"和"缓存命中率"的验证上。当确认这些问题都解决后,再把 RDMA 替换进去,优化优先级是对的。
我也见过有人问:能不能直接走 NVMe over Fabric 来做节点间共享存储?高延迟场景可能可以,但推理场景对稳定时延的要求极高,共享存储会让时延毛刺的概率变大。Mooncake 本质上更偏向"每节点私有缓存 + 远端对象存储"的模式,而不是"共享全闪存储"的模式。
4.3 首 token 延迟与预取窗口的取舍
数据访问链路的设计目标通常有两个:首 token 延迟(TTFT)和整体吞吐。这两个指标在预取窗口的选择上存在直接冲突。
- 预取窗口太大:提前搬运了太多暂时用不到的块,占用了 SSD 和网络带宽,反而拖慢了关键块到达的速度。
- 预取窗口太小:推理引擎会因为等待下一个块而停顿,出现明显的"气泡"时间。
我个人的调参经验是:先测量你的平均块大小和远端访问延迟,再计算一个基准预取梯度。假如远端访问延迟是 20 ms,推理引擎每步解码耗时是 5 ms,那么预取窗口至少需要覆盖 4 步以上的块量,才能保证不出现等待。但这只是理论值,实际还要考虑缓存命中率的波动。安全起见,起始窗口可以设为理论值的 1.5 倍,跑一轮压测后观察 TTFT 的 P95,再逐步收窄。
经验:调预取窗口时不要只看平均 TTFT,要重点看 P99 和最大毛刺值。平均延迟好看但 P99 抖动剧烈的系统,往往是因为预取窗口刚好卡在临界值上,网络稍微抖动就导致断档。
5. 实战参考:基于 Mooncake 思路的最小集群搭建要点
5.1 组建一套最小验证环境
Mooncake 的完整实现依赖不少组件,但你完全可以用"最小可行"的方式验证核心链路。我建议的配置如下:
- 3 台 GPU 服务器(每台 8 卡,用来作为计算节点)
- 3 台 CPU 服务器(每台 64GB 内存,跑调度平面和元数据服务)
- 1 套 S3 兼容对象存储(可先用 MinIO 单机版顶替)
- 每台服务器配一块 4TB NVMe SSD 作为 L2 缓存盘
这套环境下,你能够验证的核心链路包括:元数据映射、块预取、本地缓存命中、对象存储回源,以及端到端的 KV Cache 复用。虽然是缩小版,但数据链路上的大部分坑都能暴露出来。
5.2 半同步半异步的缓存架构
如果你只想取 Mooncake 的一部分思想落地到自己的推理框架里,最值得搬的是"半同步半异步"的缓存更新机制。
传统做法是"先查缓存,缓存没有就同步去远端取,取完返回给推理引擎"。这在请求量小的时候没问题,但一旦并发上来,每个请求都可能经历同步等待,占用大量线程资源。
Mooncake 的思想是把这拆成两段:
- 第一段(同步):查询元数据,如果本地 L2 命中,直接拷贝;如果未命中,立即发起异步预取请求,并挂上一个"完成信号"。
- 第二段(异步):推理引擎继续处理后续块的逻辑,当预取完成时通过回调或轮询的方式唤醒相关解码线程。
这个改动看起来简单,但对系统吞吐的提升非常明显。它让"等待远端数据"的时间不再阻塞整条流水线,而是被后续其他请求的计算时间掩盖。
5.3 可观测性:没有指标就没法调优
最后一条建议可能听起来比较朴素,但相当关键。无论你按 Mooncake 的架构完整部署,还是只借鉴部分思路,都必须从第一天就落地这几个指标:
- L1/L2/L3 每层的点击率(即每一层命中的块数 / 总访问块数)
- 跨节点传输的带宽利用率
- 从"调度决策"到"数据到达显存"的平均时延
- 每个请求的 TTFT 分布,重点关注 P95/P99
- SSD 写入量放大倍数和 GC 频率
没有这些指标,你根本不知道缓存策略哪里出了问题,也不知道是不是网络成了瓶颈。我在一开始自建类似系统时漏了这部分,结果系统跑起来后缓存命中率看起来很高,但 TTFT 还是很差。查了半天,发现是对象存储的 GET 请求在服务端出现了排队——而我们的监控体系里却没有"对象存储请求平均时延"这项指标。
如果你要避免走同样的弯路,请在搭建的第一天就配置好全链路 trace,至少要做到"每一次预取指令都能追踪到它的完成时刻和来源层级"。这个代价不高,但对后续排障的帮助极大。
6. 对 Mooncake 后续演进方向的一点观察
Mooncake 这类架构的价值不在于某一个具体的算法有多精妙,而在于它把"大模型推理中的数据访问"提升为一个独立的工程问题来对待。以前大家更关注模型本身和算力规模,但真正跑到大规模部署时就会发现,数据链路的稳定性很多时候比显卡型号更影响用户体验。
就我个人的判断,接下来值得关注的方向有两个:
一是显存池化与 remote memory 的融合。现在显存仍然是最贵的资源,如果能通过类似 RDMA 的方式把多机显存组成一个逻辑池,推理时的 KV Cache 就不一定必须存在本机显存里,可以做到动态迁移。Mooncake 的数据平面如果往这个方向延伸,会是很大的进步。
二是存储后端与计算调度的协同感知。也就是说,存储层不仅要被动地响应预取请求,还能主动地根据历史访问模式,预判未来一段时间的读热点,提前提前把数据从 L3 搬到 L2。这个和现在数据库领域的"自治索引"思路很像,但放在 KV Cache 的场景里会更直接。
不用等基础设施厂商把这一点做到位,你现在就可以在自己的系统里尝试统计块的访问时间序列,用简单的马尔可夫链做下一阶段热度预测。即使准确率只有 60%,也比纯 LRU 节省不少重复 fetch。
回到开头的问题:为什么叫 Mooncake?不需要深挖什么特殊含义,你只需要记得它代表了一类"让数据围着算力转,而不是让算力等数据"的架构思路。希望这篇拆解能帮你在自己设计推理架构时多一条可参考的路线。