ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Mooncake分离式推理架构:以KVCache为中心的显存解耦实践

2026/9/20 14:59:50 拓冰建站 浏览量
Mooncake分离式推理架构:以KVCache为中心的显存解耦实践 简介这份关于Mooncake分离式推理架构创新与实践的完整报告面向大模型推理工程师、系统架构师与算法研究员重点探讨超长上下文场景下集群过载、推理成本高企以及服务等级协议严格保障等难题。资源共一个PDF文档大小约一点五八兆为演讲内容的图文完整版。目前已有141人学习。报告从大规模推理挑战切入梳理了长文本预填充与生成成本、完整注意力计算耗时、键值缓存占用显存及潮汐资源管理等关键问题随后介绍多种混合并行策略包括张量并行、流水线并行、专家并行、上下文并行与分块流水线并行并说明稀疏注意力、缓存混合、推测解码、级联注意力等优化手段再深入解读预填充与解码分离的Mooncake架构涵盖异构键值缓存、远程直接内存访问传输、固态硬盘与对象存储多级缓存、热点均衡调度等设计。读者可借此掌握分离式推理的系统化方法理解性能调优与成本控制的实践路径为构建高吞吐、低延迟的大规模推理系统提供参考。1. 为什么需要分离式推理架构从显存墙说起如果你跑过大模型推理服务大概率遇到过这种场景一张A100/H100显卡模型权重占了几十GB剩下能分给KV Cache的显存空间非常有限一旦并发请求稍微上去一点BatchSize还没拉满显存就先爆了。这个问题的根源在于传统推理架构把“计算”和“存储”强绑定在同一块显卡上。每来一个请求模型权重和这个请求的KVCache都要吃同一份显存但两者的生命周期完全不同权重是静态的加载一次可以服务所有请求KVCache是动态的随请求的产生而产生、随请求结束而释放。把动态的KVCache和静态的权重挤在一起本身就是一种资源错配。更麻烦的是KVCache的分配往往是不均衡的——有的请求上下文特别长有的请求回复特别长峰值和低谷之间可能差好几倍但显存分配只能按最大可能去预留导致大量碎片和浪费。Mooncake把KVCache从显卡上剥离出来放进一个独立的缓存池里计算节点只保留模型权重按需从远端缓存池读取或写入KVCache。这就是“分离式推理架构”的核心思路计算和存储解耦各管各的伸缩。这里面有一个非常重要的认知转变KVCache本质上不是“计算状态”而是“数据”。它只是在推理过程中会频繁读写的临时数据。既然它只是数据就没必要非要和计算绑死在一起——把数据放在独立缓存池、按需调度其实和对数据库做存储层拆分是同一个思路。做过后端开发的应该秒懂这就是把“无状态计算层”和“有状态存储层”拆开的经典玩法只不过这次的“状态”是KVCache而已。2. Mooncake的核心设计以KVCache为中心的架构2.1 从PD分离到分离式很多人第一次听到Mooncake容易和另一个概念搞混PD分离Prefill/Decode分离。这两个确实有关联但层级不同。PD分离是把一个推理请求的两个阶段——预填充Prefill和解码Decode——拆到不同的计算节点上执行。为什么要拆因为Prefill阶段是计算密集型矩阵乘法占绝对主导适合用高算力的大卡跑Decode阶段是访存密集型要反复读写KVCache瓶颈在显存带宽用小卡甚至CPU都能跑得不错。把两者混在一台机器上必然有一方的资源利用率上不去。Mooncake的分离式架构在PD分离的基础上又往前走了一步不仅Prefill和Decode分开连KVCache都不放在计算节点上了统一放到一个分布式缓存池里。Prefill节点算完的KVCache通过高速网络丢进缓存池Decode节点启动时从缓存池拉取。这一步彻底打破了“一个请求的KVCache必须待在某个固定计算节点上”的约束请求调度变得极其灵活——哪个Decode节点有空拉过去就能继续算。2.2 全局调度器把每一块显存都当成资源我读Mooncake论文和源码时感受最深的一点是他们真的把KVCache当成了一种“资源”在做全局调度而不是传统推理引擎里那种被动缓存。具体来说Mooncake有一个全局调度器维护整个集群的KVCache分布情况。每来一个请求调度器会先检查缓存池里有没有可以复用的公共前缀KVCache——比如多轮对话里的系统提示词、长文档里的固定上下文这些内容在所有请求里都会出现如果缓存池里已经有对应的KVCache就直接复用不需要重新计算。这项能力在传统架构里也有但通常只限单机内的KV Cache命中Mooncake把命中范围扩大到了整个集群的缓存池。命中之后调度器还要决定把这半个请求调度到哪个节点继续执行判断依据是各节点的当前负载、显存余量、到缓存池的网络距离。整个决策过程很像操作系统的分页调度优先复用已有数据缺了再从慢速存储加载加载时还要考虑把数据放在哪一跳网络里最划算。2.3 传输协议与缓存粒度KVCache从Prefill节点传到Decode节点中间走的是RDMARemote Direct Memory Access网络。这里有个非常现实的工程问题KVCache数据块究竟以多大粒度传输太小了网络包太多头开销能把传输效率拖垮太大了等待时间变长Decode节点要等到完整块才能开始算。Mooncake的答案是分层管理把KVCache按Token粒度切分为基本单元但在传输和存储时按更大的“块”组织。调度器会预测Decode节点的消费速率做类似TCP窗口的流控确保数据能持续供给不至于出现“算一步等一步”的流水线停顿。这套设计做得好不好理论上限和实际效果差异很大。我实现过一个简化版做验证发现真正的难点不在RDMA本身而在“预取时机”的把握预取太早缓存池里堆积了大量还没被消费的KVCache白白占用网络带宽预取太晚Decode节点进入等待GPU利用率直线下降。这个和CPU的prefetch是同一个道理只是规模放大了几个数量级调优的难度也跟着变大。3. 实操复现搭建一个简化版分离式推理3.1 环境准备与拓扑规划我没有完全复现Mooncake的完整集群那需要几十张卡和配套的RDMA网络但我在一个4机8卡的小集群上搭建了简化版重点验证“缓存池”和“调度”这两个核心机制。这里把实践过程分享出来。硬件环境如下表角色配置数量Prefill节点8x V100 32GB配备CX-5网卡1台Decode节点4x A40 48GB2台缓存池节点2x 3.2TB NVMe SSD200Gb/s RoCE网络1台软件层用到的核心组件是vLLM作为推理引擎底座自研的轻量级调度器基于Python etcd做状态同步NVIDIA GPUDirect Storage做KVCache的磁盘旁路传输安装过程有个非常重要的细节必须确保所有节点之间的NCCL通信不经过CPU内存拷贝否则KVCache传输时延会陡增一个量级。我一开始没启用GPUDirectKVCache的传输时延从0.8ms膨胀到6ms解码吞吐立刻掉了40%。排查原因就是页缓存和CPU内存拷贝的额外开销开启GPUDirect后恢复正常。3.2 缓存池实现把KVCache当成“块的集合”缓存池的核心是一个块管理器维护所有KVCache块的状态空闲Free、持久化Persistent、传输中In-Flight。每个块有一个全局唯一的Block ID元数据包括模型标识、层级Layer、序列号Sequence、位置Token Position和引用计数。我在实现时参考了Mooncake的论文做了一个非常关键的决定缓存池不直接管Tensor内容只维护元数据。实际的数据存取让RDMA直接读写显存或SSD。元数据服务和数据通路彻底分离这样元数据服务的压力不会随数据传输量同步增长调度器也不会因为频繁查询块状态而成为瓶颈。块的状态机Free - Persistent写入缓存池 Persistent - In-Flight被某个Decode节点锁定读取 In-Flight - Persistent引用计数归零后释放锁 Persistent - FreeLRU淘汰让我意外的是状态机的正确性比我想象中难搞得多。并行调度多个Decode节点读取同一个KVCache块时很容易出现两个节点同时锁定同一个块的竞态条件导致重复读取和数据不一致。最后是靠etcd的事务特性才解决了并发锁的问题这个我在后面坑位里详细说。3.3 调度策略从最简单的启发式开始我实现调度器时没有一上来就上强化学习或者预测模型因为我觉得先把启发式跑通更重要。我采用的是两阶段调度Prefill阶段调度器收到请求后先在缓存池里搜公共前缀的KVCache块。命中率高的系统提示词通常能省掉30%50%的Prefill时间效果非常明显。搜到之后Preifill节点只需要计算新增的那部分Token的KVCache即可然后增量写入缓存池。Decode阶段完成Prefill的请求会被分配给当前负载最低的Decode节点。判断负载的标准不光是GPU利用率还会参考这个节点到缓存池的网络距离和当前网络拥塞程度。一个节点网络已经打满但GPU还有余量此时调度过去反而会拖慢整体时延。这套启发式策略只用了大约500行Python代码但已经能稳定跑出一张A40上5000 tokens/s的解码吞吐。我认为对大部分中小规模场景来说并不需要一上来就堆复杂算法——先跑通一个合理的启发式后面再逐步精化会更务实。3.4 实测数据分离式架构到底赢在哪我在相同数据集上对比了两种部署模式传统单机8卡和Mooncake式分离部署。单机模式下用vLLM跑张量并行TP8分离模式下Prefill和Decode各用一部分卡。测试负载是200路并发对话平均上下文长度1500 tokens平均生成长度200 tokens。指标单机8卡 TP8Mooncake式分离平均首Token时延850ms620ms平均每Token生成时延45ms32msGPU显存利用率61%83%长尾时延P992.3s1.4s差异最明显的是P99时延。单机模式下偶发的显存溢出让部分请求走了swap路径直接把长尾时延拖垮。分离式架构下KVCache在独立缓存池里按需分配不太会出现某张卡显存突然不够的情况时长尾自然就稳了。显存利用率从61%提升到83%主要是因为Prefill和Decode的资源需求解耦后不需要再为峰值预留过多冗余。每种负载可以按各自的特征单独扩缩容资源错配的空间被大幅压缩了。4. 踩坑实录实现分离式推理时的典型问题要真说这套架构在实际落地的过程中有四个坑是我觉得最值得拿出来分享的——因为每个坑都花了不少时间去定位和排查。4.1 坑一缓存池与计算节点的时延不均衡第一版缓存池我用的是集中式SPDK所有KVCache都写到一台机器上。测小规模没问题但一到大流量的并发场景网络拥塞会直接导致Decode节点的数据“断供”整条流水线卡住。解决办法是分层缓存在计算节点的本地SSD上也放一部分KVCache作为一级缓存远端缓存池作为二级。调度器总是优先查本地本地命不中再去远端拉。这个思路和CPU的多级缓存一模一样命中率能做到80%以上有效降低了远端网络的压力。4.2 坑二Decode节点间KVCache状态竞争前面说过多个Decode节点并发锁定同一个KVCache块时会出现竞态。我最初用Redis里的setnx做锁但超时导致的锁释放不干净偶尔会出现一个块同时被两个节点读取数据错乱。后来改用了etcd的Lease Txn机制把“检查-加锁-读取”变成一个原子操作。加锁时带上TTL和Lease ID即使节点崩溃也能自动过期释放不会再出现锁悬挂的问题。4.3 坑三公共前缀识别过重导致调度变慢第一个版本的调度器为了做前缀命中要对每个请求都跑一次最长公共前缀匹配。请求量上来之后调度器自己变成了瓶颈一次调度决策就要花200毫秒——这个开销完全抵消了KVCache命中的收益。后来我改用Hash Prefix Tree的方式输入请求后先对Token ID序列做分段Hash每256个Token作为一个Hash节点调度时只需比较请求路径上的Hash链即可。匹配时间降到15毫秒以内而且准确率基本没打折。4.4 坑四SSD作为缓存池时的GC抖动把KVCache持久化到NVMe SSD之后写放大和垃圾回收GC变成了新的痛点。特别是当缓存池空间满了需要触发GC时SSD性能会瞬间掉到稳态的30%这个抖动会直接传导到线上推理的时延曲线上。针对这个问题的解决思路是给缓存池加了一个“水位管理”当空间占用达到75%时就开始预清理而不是等到满了才动手。预清理选的是LRU淘汰队列里的冷块结合SSD自身的Trim命令把GC时间均匀散在低峰期规避了性能尖刺。5. 一些实操心得与后续扩展方向在把这套分离式推理跑通的过程中我一直在想一个问题什么场景真正适合上这种架构什么场景其实犯不上我的判断是如果你的服务是长上下文场景为主比如文档问答、Agent式多轮对话、代码生成对话那就非常适合——因为长上下文的KVCache规模极大命中公共前缀的概率极高缓存池的复用收益可以非常可观。但如果你只是跑短文本、低并发的离线任务传统架构已经足够上分离式架构反而要承担额外的网络复杂度。有几点实操心得是我踩了很多坑换来的简单列一下优先解决传输稳定性再优化时延。大部分性能问题都出在传输不稳定导致的效果抖动先把RDMA和GPUDirect调稳再谈进一步优化。KVCache的元数据设计一定要单独做不要混在推理引擎里。这个元数据层实际上就是全局调度器的“数据库”设计得好后面加算法、加策略都会轻松很多。监控是刚需不是可选。分离式架构的故障定位难度远高于单体架构我强烈建议上线前就部署好端到端的链路追踪尤其是每个请求的KVCache读写在哪个节点、耗了多少时间监控数据齐了排查问题的速度能快一个量级。再往后的扩展方向我目前比较看好两个。一是把调度策略从启发式升级成可学习的模型输入请求特征和集群实时状态输出最优调度决策这个方向Mooncake团队也已经在探索了。二是把缓存池做成真正意义上的“服务化”让同一个缓存池同时服务多个推理引擎——现在推理框架很多一套缓存池如果能做到引擎无关就能成为推理基础设施层一个非常通用的组件。最后再分享一个小细节。我做系统压测的时候发现在低并发下分离式架构的时延反而不如单机因为多了一跳网络绕转。这是因为KVCache的传输时延是额外的固定开销——只有在并发足够高、KVCache复用的收益盖过这层开销时这套架构才真正有意义。这个边界条件是你在评估要不要上分离式架构时最先要想清楚的问题。本文还有配套的精品资源点击获取