使用 vLLM、LMCache 和 Ceph 进行 KV 缓存

2025年12月10日 Kyle Bader, Tushar Gohad

对于已部署的 AI 系统,推理占机器学习成本的 90%,因此推理优化已成为研究界一个蓬勃发展的话题。IDC 估计,全球企业将在 2025 年投资 3070 亿美元用于 AI 解决方案,并且预计这一数字将逐年大幅增长。

了解工作负载

与训练不同,自回归语言模型的推理仅涉及前向传递,而前向传递本身又分为两个不同的阶段:预填充和解码。每个阶段都有独特的工作负载配置文件——预填充往往受计算限制,消耗系统可以获得的每一点浮点算术能力,然后是解码,主要受内存带宽限制。

预填充和解码阶段的计算复杂度都随着每个附加 token 的增加而呈二次方增长。预填充可以轻松地在 GPU 上并行化——所有提示 token 在请求到达模型 API 时都是已知的。解码阶段引入了 transformer 多头注意力机制,必须计算所有先前 token(包括任何提示和生成的响应)的注意力状态。这使得部署推理服务变得复杂,因为上下文长度正在迅速增长,以适应更大的代码库、更长的文档和检索增强生成。KV 缓存是指保存与提示中 token 序列对应的计算出的键和值权重,以便以后检索,并在后续提示中使用时避免计算成本(GPU 小时)并减少提交提示作为请求与第一个响应 token 之间的时间(首次 token 时间,或 TTFT)。

vLLM 和 LMCache 中的缓存块

vLLM 采用分层方法进行 KV 缓存。首先,它会检查 GPU 内存中是否存在缓存块,如果缓存未命中,它将进入 CPU 内存,如果再次缓存未命中,它将尝试通过任何配置的 KV 连接器检索缓存块。LMCache 通过此 KV 连接器接口与 vLLM 协同工作——vLLM 发送或请求缓存块,而 LMCache 则努力存储或流式传输它找到的缓存块。vLLM 还引入了 分页注意力技术,该技术将提示分解为固定大小的 token 序列,默认情况下为 16 个 token。LMCache 默认使用更大的 256 个 token 块,这可能是为了减少管理许多块的开销并更好地分摊每块传输开销。存储人员不熟悉 token 作为空间和 IO 的度量单位,自然会想知道这在字节术语中转换为多少块大小。每 token 的字节数取决于模型,因为它是由模型的隐藏大小、键值头的数量、隐藏层的数量、头维度和数据类型大小的乘积。对于 Qwen3-32B 这样的模型,这大约是 62.5 MiB。如果您想查看给定模型或 token 数量所需的 KV 空间量,可以在 LMCache 文档页面上找到一个方便的 KV 缓存计算器

基于内容的 KV 存储

vLLM 和 LMCache 都计算代表块的 token 序列的哈希值,并将其用作缓存块标识符。这意味着 vLLM 将通过 kv-connector 接口传递它感兴趣的缓存块的哈希值,而 LMCache 将返回一个位掩码,指示它可以提供哪些缓存块。在底层,LMCache S3 连接器将对每个块标识符(token 序列的哈希值)调用 GetObjectAttributes,并为每个存在的块在掩码中翻转相应的位。这种方法的优雅之处在于不需要持久化缓存块映射,并且当在不同主机上运行多个 vLLM+LMCache 实例时,不需要协调。事实上,根本不需要配置 LMCache 控制器。这种设计还允许灵活的驱逐:存储系统可以通过生命周期配置实现基于时间的过期,并且任何已删除的块都将注册为未命中。最终,您将获得具有灵活驱逐功能的 KV 缓存块的完全弹性的基于内容的存储。熟悉 Ceph 的人会真正欣赏通过执行查找来计算数据位置的概念。

检索缓存块

我们开始探索 LMCache,首先使用其本机 S3 连接器与 Ceph 进行测试,因为它为大多数现有环境提供了一个可访问的入口点。LMCache 本机 S3 连接器的另一个吸引力在于它利用了 AWS 常用运行时库 (CRT),这意味着客户端连接池中的连接将在对象存储的 FQDN 的 DNS 响应中返回的端点之间复用。缺点是 Python 的 aws-crt-python 绑定仅支持 recv_filepathsend_filepath,这限制了 LMCache 将 GetObject 调用的响应主体直接流式传输到 LocalCPUBackend 分配的页面锁定内存缓冲区的能力。为了解决此限制,连接器在 /dev/shm 上挂载的 tmpfs 上预分配和 mmap 文件(每个并发请求一个),这样 CRT 客户端就可以传递内存映射文件的文件描述符,然后从相应的缓冲区复制到用于 DMA 传输到 GPU 的页面锁定 LocalCPUBackend 缓冲区。这是一种巧妙地规避 aws-crt-python 大部分限制的方法,但要获得真正的零拷贝,需要更改绑定。

经过使用本机 S3 连接器进行一些初步测试后,LMCache PR#1939 引起了我们的注意,因为它利用了 NVIDIA 推理传输库 (NIXL)。此 PR 引入了将 S3 数据直接读取到页面锁定的 NIXL 缓冲区的功能,绕过了 /dev/shm 上的文件和相关的内存复制。它还引入了一个存在缓存,以消除冗余的 GetObjectInfo 请求,这些请求用于确定给定序列是否存在缓存块。我们已经使用 NIXL obj 插件进行了实验,并运行了一些基本的 nixlbench 测试。我们发现 NIXL obj 插件本身需要预分配的对象键池,并且需要 LMCache 协调器或 Dynamo KVBM 来维护每个缓存块的设备 ID、偏移量和长度信息。与其它 NIXL 插件不同,obj 插件只能将单个缓存块写入每个设备 ID(与对象键 1:1 映射),因为像 S3 这样的对象 API 不支持写入到任意偏移量。PR1939 解决了所有这些问题,因为它没有使用对象键池并跟踪缓存块元数据,而是保留了 LMCache 本机 S3 连接器的基于内容的访问方法。NIXL 的唯一缺点是它使用了 S3Client 而不是 S3CrtClient,后者支持跨 S3 端点的多路径传输。

超大规模 AI 部署

凭借在选择 Ceph 存储系统硬件方面超过十年的经验,我们对构建能够最大化吞吐量,同时借鉴 Meta 和 OpenAI 等主要 AI 从业者所做的选择的系统有了想法。进入 Meta 对 Open Compute 项目的贡献——Yosemite V3.5 Sierra Point 服务器平台。YV3.5 机箱占用 3U 空间,可以填充 6 个 Sierra Point 刀片。与传统的企业刀片系统不同,YV3.5 平台没有集成的以太网交换机,而是每个 Sierra Point 刀片都有一个 OCP 3.0 插槽,用于直接连接到主机网络。我们想要一个类似于 YV3.5 和 Sierra Point 的系统,它能够利用最先进的处理器设计和光刻技术。在调查了整个 OEM 的服务器格局后,有一个系统引起了我们的注意,那就是 Supermicro X14 2U 4 节点 GrandTwin 后 IO。

Supermicro X14 2U 4 节点 GrandTwin 后 IO

每个节点:• 1x Intel Xeon 6 6740E 96C/96T,205W • 16x16GB DDR5-6400 • 1x Broadcom 57608 2x200GbE • 6x 2.5” Kioxia CM6-R,7.68TB Gen4 NVMe SSD • RAID1 2x 480TB NVMe(启动)

该系统用于提供为 AI 解决方案提供高带宽全闪存对象存储的 IBM Storage Ceph 8.1。

Supermicro Gaudi 3 AI 服务器 SYS-822GA-NGR3

• 2x Intel Xeon 6 6960P 72C/144T • 24x 64GB DDR5-6400 • 8x Gaudi 3 HL-325L 加速器 • 最多 8x 2.5” Gen5 NVMe SSD • 扩展网络:21x 200GbE Gaudi NIC • 2x Broadcom 57608 1x400GbE

该系统用于运行使用 Intel Gaudi 3 加速器的 vLLM 和 LMCache 组合的推理工作负载。

Supermicro GPU A+ 服务器 AS -8125GS-TNMR2

• 1x AMD EPYC 9654 96C/192T • 24x 96GB DDR5-4800 • 8x AMD MI300X 加速器 • 最多 8x 2.5” Gen5 NVMe SSD • 扩展网络:4x400GbE • 存储和 GPU 扩展网络:4x NVIDIA MT28908 ConnectX-6 200GbE

该系统用于运行使用 AMD MI300X 加速器的 vLLM 和 LMCache 组合的推理工作负载。

SSE-T7132S - 400Gb 以太网交换机

• 32x QSFP-DD 400GbE,或 64x QSFP56 / 128x QSFP28 与跳线电缆 • 25.6Tb/s 交换容量 • SONiC OS • RoCEv2/RDMA 支持,具有 PFC

为了简单起见,我们使用单个固定端口 400Gb 交换机用于 GPU 到 GPU 以及存储网络。

主机配置

  • BIOS 中设置的性能配置文件
  • 将调整后的配置文件设置为网络延迟
tuned-adm profile network-latency
  • 所有主机都配置为模式 802.3AD,xmit_hash_policy=Layer3+4

Ceph 配置

OSD 服务

---
service_type: osd
service_id: nvme
placement:
  hosts:
    - ceph-osd01
    - ceph-osd02
    - ceph-osd03
data_devices:
  paths:
    - /dev/disk/by-path/pci-0000:63:00.5-pci-10001:81:00.0-nvme-1
    - /dev/disk/by-path/pci-0000:63:00.5-pci-10001:82:00.0-nvme-1
    - /dev/disk/by-path/pci-0000:89:00.5-pci-10002:01:00.0-nvme-1
    - /dev/disk/by-path/pci-0000:89:00.5-pci-10002:02:00.0-nvme-1
    - /dev/disk/by-path/pci-0000:89:00.5-pci-10002:03:00.0-nvme-1
    - /dev/disk/by-path/pci-0000:89:00.5-pci-10002:04:00.0-nvme-1

池配置

我们决定在初始化 RGW 服务之前为 RGW 预创建元数据和数据池。

ceph osd pool set noautoscale
ceph osd pool create default.rgw.buckets.data 2048 2048 replicated
ceph osd pool create default.rgw.buckets.index 64 64 replicated
ceph osd pool create default.rgw.buckets.non-ec 64 64 replicated
ceph osd pool set default.rgw.buckets.data size 2
ceph osd pool set default.rgw.buckets.data min_size 1
ceph osd pool application enable default.rgw.buckets.data
ceph osd pool application enable default.rgw.buckets.index
ceph osd pool application enable default.rgw.buckets.non-ec

RGW 服务

此 RGW 服务配置将在每个 4 个主机上创建 4 个 RGW 实例,并将一个集中器绑定到主机 IP 地址的 80 端口。

---
service_type: rgw
service_id: standard
service_name: rgw.standard
placement:
  count_per_host: 4
  label: rgw
networks:
  - 10.67.67.0/24
spec:
  rgw_exit_timeout_secs: 120
  rgw_frontend_port: 8080
  concentrator: haproxy
  concentrator_frontend_port: 80
  concentrator_monitor_port: 1967
  concentrator_monitor_user: admin

流量管理

与许多应用程序一样,LMCache 期望单个 S3 端点。为了最大限度地提高到存储集群的带宽,我们决定利用 Hashicorp Consul 和 CoreDNS 以响应对我们选择的对象 FQDN 的 DNS 查询返回多个 DNS 记录。如前所述,这与 LMCache 本机 S3 连接器使用的 AWS 常用运行时库(CRT)完美配合。

Consul

/etc/consul.d/consul.hcl

datacenter = "smci"
data_dir = "/opt/consul"
bind_addr = "172.19.65.41"
client_addr = "0.0.0.0"
retry_join = [
  "172.19.65.41",
  "172.19.65.42",
  "172.19.65.43",
  "172.19.65.44"
]
server = true
bootstrap_expect = 3

services = [
  {
    name = "s3"
    port = 8080
    check = {
      id       = "tcp-check"
      name     = "S3 TCP"
      tcp      = "localhost:8080"
      interval = "10s"
      timeout  = "2s"
    }
  },
  {
    name = "s3"
    port = 8081
    check = {
      id       = "tcp-check"
      name     = "S3 TCP"
      tcp      = "localhost:8081"
      interval = "10s"
      timeout  = "2s"
    }
  },
  {
    name = "s3"
    port = 8082
    check = {
      id       = "tcp-check"
      name     = "S3 TCP"
      tcp      = "localhost:8082"
      interval = "10s"
      timeout  = "2s"
    }
  },
  {
    name = "s3"
    port = 8083
    check = {
      id       = "tcp-check"
      name     = "S3 TCP"
      tcp      = "localhost:8083"
      interval = "10s"
      timeout  = "2s"
    }
  }
]

CoreDNS

/etc/coredns/Corefile

.:53 {
    log
    errors
    forward . 8.8.8.8
}

cephlab.com {
    file /etc/coredns/cephlab.com
    prometheus
    errors
    log
    debug
}

consul {
  forward . 172.19.65.41:8600 172.19.65.42:8600 172.19.65.43:8600 172.19.65.44:8600
  log
  errors
}

s3.cephlab.com {
    rewrite stop {
        name exact s3.cephlab.com s3.service.consul.
        answer name s3.service.consul. s3.cephlab.com.
    }
    rewrite stop {
        name regex (.*)\.s3\.cephlab\.com s3.service.consul.
        answer auto
    }
    forward . 172.19.65.41:8600 172.19.65.42:8600 172.19.65.43:8600 172.19.65.44:8600
    log
    errors
    debug
}

example.hosts s3.ecmp.cephlab.com {
    hosts {
        10.67.67.67 s3.ecmp.cephlab.com
        10.67.67.67 nixl.s3.ecmp.cephlab.com
        fallthrough
    }
    whoami
}

测试 DNS 负载均衡

为了验证基于 Hashicorp Consul 和 CoreDNS 的方法是否正常工作,我们可以测试对象 FQDN 的 DNS 解析。请注意,我们看到返回了 4 条记录,这正是我们想要的。

[cephuser@ceph-osd01 ~]$ dig s3.cephlab.com

; <<>> DiG 9.16.23-RH <<>> s3.cephlab.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12051
;; flags: qr aa rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;s3.cephlab.com.                        IN      A

;; ANSWER SECTION:
s3.cephlab.com.         0       IN      A       172.19.65.41
s3.cephlab.com.         0       IN      A       172.19.65.42
s3.cephlab.com.         0       IN      A       172.19.65.43
s3.cephlab.com.         0       IN      A       172.19.65.44

;; Query time: 1 msec
;; SERVER: 172.19.65.41#53(172.19.65.41)
;; WHEN: Tue Nov 04 12:33:03 PST 2025
;; MSG SIZE  rcvd: 163

基准性能

为了在引入 vLLM 和 LMCache 之前建立存储集群的基准性能,我们使用 elbencho 从 Gaudi3 GPU 主机生成负载,并将其定向到 Ceph S3 端点。我们使用 62MB 块大小来匹配 LMCache 持久化 KV 缓存块的预期大小。这表明我们能够跨每个主机的集中器端点复用连接,并从单个主机驱动大量的 S3 流量,最高达到近 60 GB/s。

vLLM

在我们的测试时,vllm 生产堆栈不支持我们的端到端工作流程,因此我们创建了定制的 vLLM 容器镜像,其中包含 LMCache 开发版本,包括一个包含最新的 vllm-gaudi 开发版本的镜像,用于我们的测试。

AMD 容器

  • vLLM
  • LMCache
  • NIXL

Gaudi 容器

  • vLLM
  • LMCache
  • NIXL

以下是您将找到的用于运行 vLLM 和 LMCache 的配置文件和命令行参数。

.aws/credentials

[lmcache]
region = default
endpoint_url = http://s3.cephlab.com:80
aws_access_key_id = xxx
aws_secret_access_key = yyy
response_checksum_validation = when_required
preferred_transfer_client = crt

lmcache-ceph.yaml

chunk_size: 256
local_cpu: False
max_local_cpu_size: 100
remote_url: "s3://lmcache.s3.cephlab.com"
save_unfull_chunk: False
enable_async_loading: True
remote_serde: "naive"
blocking_timeout_secs: 100
extra_config:
  s3_max_io_concurrency: 1024
  s3_max_inflight_reqs: 1024
  s3_prefer_http2: False
  s3_region: "default"
  s3_enable_s3express: False
  save_chunk_meta: False
  s3_file_prefix: "test"

lmcache-nixl-ceph.yaml

chunk_size: 512
local_cpu: false
max_local_cpu_size: 50
remote_serde: "naive"
nixl_buffer_size: 1073741824
nixl_buffer_device: cpu
extra_config:
  enable_nixl_storage: true
  nixl_backend: OBJ
  nixl_pool_size: 512
  nixl_backend_params:
    endpoint_override: http://s3.cephlab.com
    access_key: CR98FOT054QZJ60NR7E3
    secret_key: 15CTFkiAdwPkkiSh4gOlQ5zF14KZ0uCnZloYVo3w
    scheme: http
    region: default
    req_checksum: required
    bucket: lmcache

lmcache-dram.yaml

chunk_size: 256
local_cpu: True
max_local_cpu_size: 50
save_unfull_chunk: False
enable_async_loading: True
remote_serde: "naive"
blocking_timeout_secs: 100

启动 vLLM

LMCACHE_CONFIG_FILE="/root/lmcache-nixl-s3.yaml"
LMCACHE_USE_EXPERIMENTAL=True
PYTHONHASHSEED=67
AWS_PROFILE='lmcache'
vllm serve Qwen/Qwen3-32B  \
       --gpu-memory-utilization 0.55 \
       --rope-scaling '{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}' \
       --max-model-len 131072 \
       --kv-transfer-config '{"kv_connector":"LMCacheConnectorV1","kv_role":"kv_both","kv_parallel_size":"16"}' \
       --tensor-parallel-size 2

对于 Gaudi3 加速器测试,我们设置了以下额外的环境变量

PT_HPU_GPU_MIGRATION=1
VLLM_USE_V1=1
VLLM_SKIP_WARMUP=True
VLLM_EXPONENTIAL_BUCKETING=False

基准测试

我们希望表征从具有 Ceph 的远程存储中 100% 缓存命中率减少到首次令牌的时间,并将其与计算预填充进行对比。为此,我们选择了 LMCache long_doc_qa.py。我们为 TTFT 数据收集开发了以下方法:

  1. 启动 vLLM
  2. 运行 long_doc_qa.py 并记录预热轮次(计算预填充结果)的 TTFT
  3. 重启 vLLM
  4. 运行 long_doc_qa.py 并记录预热轮次(来自远程存储的 KV 缓存命中结果)的 TTFT
  5. 停止 vLLM
  6. 从远程存储中删除缓存块

通过在第 3 步中重启 vLLM,我们确保结果不会受到 GPU HBM 或 CPU 内存中 KV 缓存的偏差,并且通过停止 vLLM 并从远程存储中删除缓存块,我们确保每个后续上下文长度都不会受益于来自先前上下文长度的远程存储 KV 缓存。通过这种方法,所有 KV 缓存在每次测试开始时都是冷的,除了我们希望测量其好处的远程存储 KV 缓存,在第 4 步中。

long_doc_qa.py 示例命令行

python3 ~/LMCache/benchmarks/long_doc_qa/long_doc_qa.py \
      --model Qwen/Qwen3-32B \
      --port 8000 \
      --num-documents 1 \
      --document-length ${len} \
      --output-len 100 \
      --repeat-count 1 \
      --repeat-mode interleave \
      --max-inflight-requests 1 \
      --output results/ttft_${L}.out

结果

使用 Intel Guadi3 和 AMD MI300X 加速器,TTFT 有显著降低,在 MI300x 上使用 Llama3.3-70B-Instruct,在张量并行度设置为 2 时,最长的上下文长度测试的降低幅度高达 23 倍。此测试还说明了 KV 缓存如何比使用张量并行度将预填充分散到系统中的多个 GPU 上来减少 TTFT,并且将这些技术结合起来可以提供最低的 TTFT。值得指出的是,除了减少 TTFT 之外,前缀缓存还通过节省 GPU 周期用于解码来获得额外的价值——可能减少每输出令牌的时间 (TPOT)。

接下来是什么?

我们与 Red Hat 的 llm-d 团队分享了我们的结果,并已开始与他们合作,通过建立与 Ceph 的 KV 缓存作为 一条清晰的路径,将 KV 缓存商品化。我们认为我们的方法可能是最容易访问的,因为它使用标准的 S3 等对象协议、标准的 TCP/IP 网络,适用于来自不同供应商的各种加速器,并且 Ceph 对象普遍部署在 OpenShift 集群中,通过 OpenShift Data Foundation 和 IBM Fusion 实现。我们下一阶段的测试将利用 llm-d,GPU 主机将作为工作节点,并探索更复杂的场景,例如 PD 分离和缓存混合。

最后,我们感谢 Supermicro 提供这些测试环境。如果您对 Ceph 的数据或 AI 工作负载有任何疑问,请 联系我们