一、显存、磁盘、内存遇到的问题

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性能调优实例

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐