Mooncake在LLM的kv cache offload
文章目录
一、显存、磁盘、内存遇到的问题
1)各个级别的读写速度
KV Cache 在 AI 服务器里要存,但存哪里?这背后是一个根本性的物理约束——存储器不可能三角:快、大、便宜,三者不可兼得。
快 (低延迟 · 高带宽)
╱ ╲
╱ ╲
╱ ╲
╱ 不可能 ╲
╱ 三角! ╲
╱ ╲
╱ ╲
大 (高容量) ────── 便宜 (低成本)
现实中的存储器,都落在这个三角的某条边上——快的一定小,大的一定慢,便宜的一定慢:
延迟 带宽 容量/机 成本/GB 技术
────── ────── ──────── ──────── ──────
~100ns ~2 TB/s 80-192 GB ~$20 HBM3e (GPU)
~100ns ~50 GB/s 256 GB-2 TB ~$5 DDR5 (DRAM)
~10μs ~7 GB/s 3.84-15 TB ~$0.3 NVMe (SSD)
~20μs ~6 GB/s 集群级 ~$0.1 NVMe-oF (远程SSD)
──→ 每下一层: 延迟 ×100 带宽 ÷10 容量 ×10 成本 ÷10 ──→
用物流中心来比喻——越贵的仓库越小但越快,越便宜的仓库越大但越慢:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────┐ 容量: 80-192 GB/卡 │
│ │ │ 延迟: ~100ns 带宽: ~2 TB/s │
│ │ 加工车间 │ 成本: ~$20/GB │
│ │ (GPU 显存 / HBM) │ │
│ │ 最快·最小·最贵 │ KV Cache 在此产生 │
│ │ │ 放不下 → 必须搬家 │
│ └──────────┬──────────┘ │
│ │ │
│ │ 叉车 (cudaMemcpy D2H) │
│ │ ~25 GB/s · PCIe 4.0 专用通道 │
│ │ 走 pinned memory 快车道 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ 容量: 256 GB-2 TB/机 │
│ │ │ 延迟: ~100ns │
│ │ 主仓库 │ 带宽: ~50 GB/s │
│ │ (DRAM) │ 成本: ~$5/GB │
│ │ │ │
│ │ 比 HBM 大 10x · 比 HBM 便宜 4x │ 大页加速 │
│ │ 比 SSD 快 100x · 比 SSD 贵 15x │ RDMA 注册可远程访问 │
│ │ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Pinned 卸货台 (cudaMallocHost) │ │ 叉车直达·DMA 无中转 │
│ │ └────────────────────────────────┘ │ │
│ │ │ │
│ └──────┬───────────────────────┬───────────┘ │
│ │ │ │
│ │ 传送带 │ RDMA 直通 │
│ │ io_uring + O_DIRECT │ Transfer Engine │
│ │ ~7 GB/s │ ~50 GB/s │
│ │ 绕过页缓存直写SSD │ 远程 DRAM 直达 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 堆场 (本地 NVMe SSD) │ │
│ │ │ │
│ │ 容量: 3.84-15 TB 延迟: ~10μs │ │
│ │ 带宽: ~7 GB/s 成本: ~$0.3/GB │ │
│ │ │ │
│ │ 比 DRAM 大 10x · 比 DRAM 便宜 15x │ │
│ │ 比 DRAM 慢 100x · 断电不丢数据 │ │
│ │ │ │
│ │ 桶文件存储 · O_DIRECT 零拷贝 · LRU 驱逐 │ │
│ │ │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ 直升机 (SPDK NVMe-oF) │
│ │ ~6 GB/s · RDMA 直连 │
│ │ 绕过远程主机 CPU + 操作系统 │
│ │ │
│ ▼ │
│ ┌────────────────────────── ────────────────────────┐ │
│ │ │ │
│ │ 远程仓库 (NVMe-oF SSD) │ │
│ │ │ │
│ │ 容量: 集群级 延迟: ~20μs │ │
│ │ 带宽: ~6 GB/s 成本: ~$0.1/GB │ │
│ │ │ │
│ │ 容量近乎无限 · 但延迟最高 │ │
│ │ SPDK 用户态驱动 · 无系统调用 · 无锁 │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2)估算请求数和读写速度
| 层 | 核心矛盾 | Mooncake 的解法 |
|---|---|---|
| HBM | 极快但极小——80GB 只够约 30 个请求 | 放不下的 KV Cache 搬到 DRAM |
| DRAM | 够快但不够大——2TB 也装不下全部 | 装不下的卸载到 SSD,需要时再提升回来 |
| SSD | 够大但不够快——比 DRAM 慢约 100 倍 | io_uring + O_DIRECT 压榨最大吞吐 |
| 远程 SSD | 容量无限但延迟最高 | SPDK 绕过远程 CPU,减少中间环节 |
- 解释:
1.80GB 只够 30 个请求:
单请求 KV Cache
≈ 2 × 层数 × KV heads × head_dim × 每元素字节数 × 上下文 token 数
例如一个 GQA 模型:
32 层
8 个 KV heads
head_dim = 128
BF16 = 2 bytes
上下文 = 16K token
所以就是
2 × 32 × 8 × 128 × 2
= 128 KB / token
单个 16K 请求:
128 KB × 16384 ≈ 2 GB
若 80GB HBM 中先放了模型权重、CUDA buffer 等,只剩约 60GB 可给 KV Cache:
60 GB ÷ 2 GB / 请求 ≈ 30 个请求
选错交通工具,搬家时间可能比加工时间还长。在 MoE 推理中,一条 KV Cache 动辄数十 MB,如果 D2H 拷贝走普通内存通道(pageable memory),带宽只有 pinned memory 的 1/10 到 1/100——,比如如果Pageable D2H 大约是 ~1 GB/s,而 Pinned 大约是 ~25 GB/s,这大约是 25 倍的提升,叉车直接变成了手推车。
今天这篇文章,我们深入 Mooncake 与物理硬件的交互层,看看每一步"搬家"到底是怎么完成的。
备注:Pinned 卸货台 (cudaMallocHost)
是CPU 内存(DRAM)到 GPU 显存(HBM/VRAM)之间 的“卸货台”
3)内存层级全景:从 GPU 到 SSD 的物理距离
在深入每个环节之前,先看一眼全貌——注意延迟的量级跳跃:
┌─────────────────────────────────────────────────────────────┐
│ GPU 显存 (VRAM/HBM) │
│ 容量: 80-192 GB/卡 延迟: ~100ns 带宽: ~2 TB/s │
│ 技术: HBM3e 访问方式: CUDA 核心 │
└───────────────────────────┬─────────────────────────────────┘
│ PCIe 4.0 x16 (~25 GB/s) / PCIe 5.0 x16 (~50 GB/s)
│ cudaMemcpy D2H
▼
┌─────────────────────────────────────────────────────────────┐
│ Pinned Host Memory (页锁定 DRAM) │
│ 容量: 按需分配 延迟: ~100ns 带宽: ~25 GB/s │
│ 技术: cudaMallocHost 访问方式: DMA 直传 │
└───────────────────────────┬─────────────────────────────────┘
│ 内存拷贝
▼
┌─────────────────────────────────────────────────────────────┐
│ DRAM (普通内存) │
│ 容量: 256 GB-2 TB/机 延迟: ~100ns 带宽: ~50 GB/s (CPU 自己读写 内存)│
│ 技术: DDR5 ECC 访问方式: mmap/memfd_create/hugepage │
│ 注册: Transfer Engine registerLocalMemory() │
└───────────────────────────┬─────────────────────────────────┘
│ POSIX I/O 或 io_uring
▼
┌─────────────────────────────────────────────────────────────┐
│ 本地 NVMe SSD │
│ 容量: 3.84-15.36 TB 延迟: ~10μs 带宽: ~7 GB/s │
│ 技术: NAND 闪存 访问方式: pread/pwrite 或 io_uring │
└───────────────────────────┬─────────────────────────────────┘
│ RDMA (NVMe-oF)
▼
┌─────────────────────────────────────────────────────────────┐
│ 远程 NVMe SSD (NVMe-oF) │
│ 容量: 集群级 延迟: ~20μs 带宽: ~6 GB/s │
│ 技术: SPDK 访问方式: NVMe over Fabrics RDMA │
└─────────────────────────────────────────────────────────────┘
从 DRAM 的 100ns 到 SSD 的 10μs,差了 100 倍;从 SSD 的 10μs 到分布式存储的 ms 级,又差了 100 倍。这就是分层存储必须"精打细算"的原因:每往下一层,访问成本就跳一个数量级。
笔者注:Pinned Memory 的 D2H 带宽取决于 PCIe 代际:PCIe 3.0 x16 实测
二、Mooncake的做法
1)Mooncake DRAM 管理:主仓库怎么建
Mooncake 的 DRAM 管理涉及三个问题:怎么分配、怎么共享、怎么让远程访问。
(1)内存分配:mmap + memfd_create
Mooncake 使用 ShmHelper 在 DRAM 中分配大块共享内存:
// mooncake-store/src/shm_helper.cpp
void* ShmHelper::allocate(size_t size) {
// 1. 创建匿名共享内存文件
int fd = memfd_create(MOONCAKE_SHM_NAME, flags);
// 2. 设置大小
ftruncate(fd, size);
// 3. 映射到进程地址空间
void* base_addr = mmap(nullptr, size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, fd, 0);
return base_addr;
}
为什么用 memfd_create + mmap,而不是简单的 malloc?
memfd_create 创建的是一个匿名文件——没有磁盘上的对应文件,但拥有文件描述符(fd)。这个 fd 可以在进程之间传递(通过 Unix Socket),接收方用 mmap 映射同一块物理内存,实现零拷贝共享。
另外,malloc 默认分配的是当前进程私有的堆内存,不是共享内存
(2)大页:减少地址翻译开销
普通内存页大小 4KB,1GB 内存需要 262144 个页表项。大页(HugePage)使用 2MB 或 1GB 的页大小,1GB 内存只需要 512 个页表项——地址翻译快了 500 倍。
// ShmHelper 中的大页支持
if (use_hugepage) {
flags |= MAP_HUGETLB; // 尝试 2MB 大页
// 失败则回退到普通页
}
大页在 RDMA 场景下尤其重要。RDMA 注册内存时,需要把虚拟地址映射到物理地址——页表越少,注册越快,MR(Memory Region)的效率越高。如果用普通 4KB 页,注册 1GB 内存可能需要数秒;用 2MB 大页,只需要几十毫秒。
当前业界实践,KV Cache动辄十几M起步,编码等Agent智能体长程任务甚至GB,均已采用2MB大页,已成为当前业界KV Cache系统标配。个别地方,可能会考虑采用1GB,或者将内存切分为2MB大页和1GB大页分层混用。
总之,还是根据实际场景、实际系统性能目标,定向调整,达到最优。
(3)共享内存:跨进程零拷贝
Mooncake Store 的 Client 和 Transfer Engine 可能是不同进程。它们如何共享同一块 DRAM?
进程 A (Client):
fd = memfd_create("mooncake_shm", ...)
addr_A = mmap(fd, size) ← 映射到进程 A 的地址空间
进程 B (Transfer Engine):
recv_fd = recvmsg(unix_socket, fd) ← 通过 Unix Socket 接收 fd
addr_B = mmap(recv_fd, size) ← 映射到进程 B 的地址空间
addr_A 和 addr_B 指向同一块物理内存!
进程 A 写入的数据,进程 B 直接可见,无需拷贝。
(4)Transfer Engine 注册:让 DRAM 可以被远程 RDMA 访问
分配了 DRAM 还不够——要让它能被远程节点通过 RDMA 访问,必须注册到 Transfer Engine:
// mooncake-transfer-engine/example/memory_pool.cpp
void* addr = allocateMemoryPool(dram_buffer_size, i);
engine->registerLocalMemory(addr, dram_buffer_size, "cpu:" + std::to_string(i));
engine->installTransport("rdma", args);
registerLocalMemory 做了两件事:
1. 调用 ibv_reg_mr() 将内存注册到 RDMA 网卡
→ 网卡获得这块内存的虚拟地址→物理地址映射
→ 网卡可以直接 DMA 读写这块内存,不经过 CPU
2. 将内存信息发布到元数据服务
→ 远程节点查询元数据,获取这块内存的地址和访问密钥
→ 远程节点可以直接 RDMA Read/Write 这块内存
未注册的内存,RDMA 网卡无法访问——就像没有门牌号的房间,快递员找不到。注册之后,网卡拿到"门牌号"(LKey/RKey),后续所有 RDMA 操作都用这个密钥直接访问。
2)GPU ↔ DRAM:叉车卸货——车间到主仓库的专用通道
KV Cache 最初在 GPU 显存中产生(Prefill 阶段)。要把它存到 DRAM 或 SSD,第一步是 D2H(Device to Host)拷贝。
为什么需要 Pinned Memory?
PinnedBufferPool:叉车车队
D2H 拷贝:Offload 中的关键一步
3)DRAM ↔ SSD:传送带搬运——主仓库到堆场的批量运输
POSIX I/O:人工搬运
io_uring + O_DIRECT:传送带系统
O_DIRECT 的对齐要求
BucketStorageBackend 的写入路径
4)NVMe-oF:直升机空投——直达远程堆场
传统路径 vs SPDK 路径
NVMe-oF 通信:RDMA 传输
SPDK 内存分配:DMA 兼容
二、完整数据路径:一条 KV Cache 的硬件旅程
三、Mooncake多硬件平台适配:一套代码,多种硬件
1)设计哲学:硬件交互的三大原则
2)总结与行动指南
| 核心概念 | 一句话总结 |
|---|---|
memfd_create + mmap |
DRAM 分配方式,支持跨进程零拷贝共享和大页 |
PinnedBufferPool |
GPU D2H 的叉车队,页锁定内存池化复用,带宽提升约 10–100 倍 |
io_uring + O_DIRECT |
SSD 读写的高速传送带,绕过页缓存,批量提交/完成 |
SPDK NVMe-oF |
远程 SSD 的直达专线,绕过内核,RDMA 直连 |
registerLocalMemory |
让 DRAM 可被 RDMA 访问,注册到网卡获取“门牌号” |
AcceleratorDevice |
多硬件适配层,一套代码支持 NVIDIA / AMD / 昇腾 / 摩尔线程 |
建议: 生产部署时,务必开启 MOONCAKE_OFFLOAD_USE_URING=true 和大页支持——这两项配置可以让 SSD 读写带宽提升 30%-50%,对 Offload/Promotion 的延迟有直接影响。
命令:
1)MOONCAKE_OFFLOAD_USE_URING=true
表示 SSD Offload / Promotion 使用 Linux io_uring 异步 I/O,而不是传统同步 read/write 或较旧的 I/O 路径。可以批量提交 I/O、异步等待完成,减少线程阻塞和系统调用开销,通常能提高 NVMe 并发读写吞吐、降低尾延迟
2)设置大页
Mooncake Store 当前可以用 显式 HugeTLB 大页。关键是先在 Linux 预留大页,再设置 Mooncake 环境变量。
# 例:预留 512 个 2MB 大页 = 1GB
sudo sysctl -w vm.nr_hugepages=512
# 查看是否成功
grep -E 'HugePages_Total|HugePages_Free|Hugepagesize' /proc/meminfo
启动 Mooncake / mooncake_store_service 前设置:
export MC_STORE_USE_HUGEPAGE=1
export MC_STORE_HUGEPAGE_SIZE=2MB
然后再启动服务。
MC_STORE_USE_HUGEPAGE=1
→ 强制使用 HugeTLB;若大页不足,启动/分配失败,而非悄悄退回普通 4KB 页。
用于 Mooncake 的大块缓存内存时,可减少页表/TLB 开销,也更利于 DMA、RDMA 注册和大块数据搬运。
备注:RDMA = Remote Direct Memory Access,远程直接内存访问。
3)延伸阅读:
- Linux io_uring: https://man7.org/linux/man-pages/man7/io_uring.7.html
- SPDK 文档: https://spdk.io/doc/
- NVIDIA cudaMallocHost: https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__MEMORY.html
- NVMe over Fabrics 规范: https://nvmexpress.org/specifications/
四、Monncake部署踩坑实录
五、Mooncake性能调优实例
更多推荐


所有评论(0)