lmcache

一文读懂 LMCache MP 模式下的数据传输路径:新手入门指南

2026-06-16

JJiayue ChenTTony Lin (Intel)LLMCache Team

在传统部署方式中,KV cache 通常由推理引擎进程内部管理。这意味着 cache 的生命周期与推理引擎紧密绑定:一旦推理引擎重启或崩溃,已经缓存的 KV 数据也可能随之丢失。

为了解决这个问题,LMCache 引入了多进程模式(Multiprocess Mode,简称 MP mode)。在 MP mode 下,LMCache 会作为一个独立的守护进程运行,并与推理引擎解耦。推理引擎专注于执行模型推理,而 LMCache MP 服务端则独立负责 KV cache 的存储与复用。通过这种分离,即使推理引擎工作进程重启或失败,KV cache 的管理也会更加稳定可靠。

Diagram illustrating the overall architecture and key-value transfer paths in LMCache MP mode, showing vLLM workers, LMCache MP server, and data transfer methods.

在本文中,我们将以 vLLM 为例,用更容易上手的方式,带大家一步步了解这条传输路径。希望这篇文章能帮助社区新贡献者理解 MP mode 的基本架构,建立必要的背景知识,从而更有信心地参与适合新手的 issue,并为 LMCache 社区做出有意义的贡献。

vLLM + LMCache

为了更直观地理解这个过程,我们以 vLLM 部署为例。在 vLLM 部署中,vLLM 工作进程扮演客户端的角色,而 LMCache MP 服务端则作为一个独立的缓存管理器运行。工作进程会向 LMCache 发送存储和读取请求,LMCache 则负责处理后端的 cache 管理逻辑。

由于工作进程和服务端是两个独立的进程,它们默认并不共享内存。这就引出了本文要讨论的核心系统问题:大量的 KV cache tensor 究竟是如何在这两个进程之间实际移动的?

负责完成这种数据移动的机制,就叫做传输路径

每一种传输路径都支持两个基本操作:存储(工作进程 -> 服务端)和读取(服务端 -> 工作进程)。

Gathering(聚合)和 Scattering(分散写回) KV 块

在研究这些路径本身之前,我们可以先理解一下被移动的数据长什么样。

像 vLLM 这样的推理引擎,并不会把一个请求的 KV cache 存放为一块连续的内存区域。借助 PagedAttention,KV cache 会被切分成固定大小的小块(block),并随着请求的增长按需分配。因此,一个请求的 KV cache 最终会分散在许多 GPU 内存块中。块表(block table)记录了哪些 block ID 属于哪个请求。要移动一个请求的 KV cache,就必须遍历块表,把这些分散的 KV 块拷贝进一块连续的缓冲区(buffer)中。这个过程称为聚合。其逆过程,即在读取时把连续缓冲区拷回分页 KV 块中,则称为分散写回。你会在 LMCache 代码中直接看到这两个名字:gather_paged_kv_to_cpuscatter_cpu_to_paged_kv

一条传输路径的剖析

因此,一条传输路径有两项工作:

  1. 聚合或分散写回 KV 块:在传输前,将分散的 KV 块聚合进一块连续缓冲区;在读取时,将连续缓冲区分散写回分页 KV 块中。
  2. 在进程间移动缓冲区:在推理引擎工作进程与 LMCache MP 服务端之间传输这些字节。

根据硬件环境的不同,LMCache 处理这种跨进程边界传输的方式也不一样。这就引出了 LMCache 支持的两条核心传输路径:CUDA 路径和非 CUDA 路径。

CUDA 路径

通常情况下,一个 GPU 内存指针只在分配它的进程内部有效,另一个进程无法直接使用这个指针。CUDA 进程间通信(CUDA IPC,Inter-Process Communication)就是为了解决这个问题:它允许拥有这块 GPU 内存的进程,将这段 GPU 内存分配导出为一个轻量级的 IPC 句柄(handle)。另一个进程可以导入这个句柄,从而获得一个在自身进程中有效、并指向同一块底层 GPU 内存的指针。

LMCache 在 CUDA 路径中使用的正是这一机制。工作进程不需要把整个 KV cache 从自己的进程中拷贝出来再发送给 LMCache 服务端,而是发送一个 CUDA IPC 句柄。这个句柄本身并不是 KV cache 数据,而是一个轻量级引用,使 LMCache 服务端能够从另一个独立进程中访问工作进程的 GPU 内存。

在 LMCache 中,CudaIPCWrapper 会把一个 GPU tensor 包装成可以发送的对象。它将 CUDA IPC handle 和 tensor 的元数据绑定在一起,例如 shape、dtype 以及 layout 信息,这样 server 才知道应该如何解读这块内存。此外,一个小型 CUDA event 也会告诉另一个进程 GPU 操作何时完成,以及什么时候可以安全地读取或写入这块共享内存,从而避免竞态条件。

不过,实际的 KV cache 最终仍然需要被拷贝到 LMCache 管理的存储中。对于一次存储操作,LMCache 首先通过 CUDA IPC 句柄访问工作进程的 GPU 内存。随后,它会执行前面提到的聚合步骤:将分散的分页 KV 块拷贝到一块连续的临时 GPU 暂存缓冲区中。之后,这块连续的缓冲区会从 GPU 内存拷贝到 LMCache 管理的 CPU 内存中,正如 LMCache 的 lmcache_driven_transfer.py 实现所示。

因此,存储路径可以概括为:工作进程在 GPU 上的分页 KV 块 -> GPU暂存缓冲区 -> LMCache的CPU内存。读取操作则沿着同一路径反向执行。

CUDA 路径

IPC 句柄是 CUDA 专属的机制。这意味着 CPU 以及非 CUDA 加速器,例如 Intel XPU 或 Habana HPU,无法使用同样的机制在不同进程之间共享设备内存。正是在这里,LMCache 社区维护者 hlin99 通过两个 PR( #3259#3359)做出了重要贡献:它们构建了另一套传输路径,并将 LMCache MP mode 的支持扩展到了更多平台。

要在进程之间移动数据,通常有两种方式:要么把字节通过某个通道(例如 socket)拷贝过去,要么共享一块两个进程都能读写的内存区域。hlin99 的这两个 PR 正好为非 CUDA 路径实现了这两种方案:

  • PR #3259 – pickle 路径(通过通道拷贝)
  • PR #3359 – 共享内存路径(SHM)

共享内存路径(SHM—— 1 次拷贝

当 LMCache 服务端以 --l1-size-gb 参数启动时,它会建立一个 L1 池。这个 L1 池是 LMCache 在主机内存中的主要高速存储空间。默认情况下,LMCache 会把这个池分配在 /dev/shm 中。/dev/shm 是一块由 RAM 支撑的 Linux 共享内存区域,允许相互独立的进程访问同一块内存区域。

在非 CUDA 部署中,工作进程会利用这个共享内存池,把 KV cache 数据传输给 LMCache 服务端。由于 L1 池位于共享内存中,vLLM 工作进程和 LMCache 服务端可以映射到同一块物理内存。这样一来,工作进程就能直接把自己的 KV cache 写入服务端的 L1 缓冲区,而无需通过另一条通信通道来拷贝数据。因此,一次存储操作只需要 1 次数据拷贝:聚合后的工作进程 KV cache -> 共享 L1 缓冲区。

注意:“1 次拷贝”是基于设备专用的 C ops 后端而言的。默认的 Python fallback 会额外增加一个暂存缓冲区,因此需要 2 次拷贝。这样做是为了把许多小块合并成一次更大的传输,从而最大化传输吞吐量,但代价是多一次额外拷贝。

pickle 路径 —— 4 次拷贝

共享内存路径只有在 L1 池能够真正放入 /dev/shm 时才有效。如果请求的 L1 池大小超过了 /dev/shm 的容量,或者共享内存被禁用,那么这个池就无法放在那里。此时,LMCache 会改为把 L1 池分配在服务端的私有 RAM 中,而工作进程无法映射到这块内存。

当这种情况发生时,KV cache 就无法在工作进程和服务端之间直接共享,因此数据必须通过字节流来传输。在这条路径中,工作进程首先根据 block ID 将 KV 块聚合成连续的 CPU chunks。随后,它使用 pickle.dumps 对这些 chunks 进行序列化,并通过 ZMQ socket 将字节作为 COMMIT_STORE 请求发送给服务端。在服务端,LMCache 会将这些字节反序列化回 tensor,并写入自己的私有 L1 池

Diagram illustrating the transfer paths of KV tensors from a vLLM worker process to an LMCache MP server process, detailing CUDA and non-CUDA transport methods.

这样一来,pickle 路径总共有 4 次拷贝:聚集 -> 序列化 -> 反序列化 -> 写入。与通过 IPC 句柄共享 GPU 内存访问权的 CUDA 路径不同,也与让两个进程映射同一块共享缓冲区的 SHM 路径不同,pickle 路径实际上是把 KV cache 的字节通过 ZMQ 通道移动过去。这使它成为一条平台无关、通用的回退传输路径。

路径数据流拷贝次数跨越进程边界的是什
CUDA IPC聚集 GPU 块 -> GPU 暂
存缓冲区 -> CPU L1
2一个小巧的 IPC 句柄
,让服务器能直接读
取工作进程的GPU
SHM聚集 KV -> 直接进入共
享的 /dev/shm L1 缓冲
1*(使用设备专用 C ops 后端
时为 1 次拷贝;默认 Python fallback实现为 2 次,见上文注
释)
无 —— 两个进程映
射的是同一块内存区
Pickle聚集 KV -> CPU 数据块
-> 序列化 => ZMQ => 反
序列化 -> 写入 L1
4序列化后的 KV 字节

部署教程与说明

https://github.com/glbyktjys/lmcache-mp-transfer-paths-tutorial

结语

如果你已经读到这里,现在应该已经对 LMCache 如何在 MP mode 下移动 KV cache 有了更清晰的整体理解。CUDA 路径通过 IPC 句柄共享 GPU 内存,而非 CUDA 传输路径则有两种方式:要么通过 SHM 共享 /dev/shm 中的缓冲区,要么通过 pickle 经由 socket 传输字节。

希望这些背景知识能帮助你更有信心地开始探索代码库。一个可以关注的方向是 DeepSeek V4 支持。DeepSeek V4 目前已经通过 PR #3171 在 CUDA 路径下支持 MP mode,但将这一能力扩展到非 CUDA 传输路径,仍然是一个值得推进的方向。具体来说,Intel XPU、Habana HPU 这类非 CUDA 加速器,以及用于无 GPU 测试的 CPU / SHM / pickle 路径,仍需要支持 V4 的混合 KV cache groups。对于对 MP mode、多平台支持和 hybrid KV layout 感兴趣的贡献者来说,这会是一个范围清晰、很适合切入的贡献方向。

另一个我们正在探索的方向,是让 SHM 存储路径变成异步的。目前,SHM 传输是同步的,因此工作进程需要阻塞等待拷贝完成。异步设计可以让 vLLM 工作进程采用一种 “fire-and-forget” 的存储流程,类似于 CUDA 路径通过 CUDA event 解耦两个进程的方式。这可能会显著提升吞吐量。这个想法目前仍处于早期阶段,但如果你对这些设计取舍感兴趣,欢迎加入讨论,和我们一起把它实现出来。

LMCache 的多平台支持也在持续演进。近期合入的 PR #3352 在基于 SHM 的 EngineDrivenContext 路径上增加了 CPU 支持,使存储/读取流程可以完全通过共享内存运行,不再依赖GPU或CUDA。这些更新简化了非 CUDA MP 路径的测试,也让 LMCache 的 MP模式更容易扩展到不同硬件平台,并让没有硬件加速器环境的开发者也能更方便地使用和参与开发。

这些改进共同让 MP mode 更容易被探索、调试和贡献,尤其是对于在非 CUDA 或仅 CPU 环境中工作的开发者。希望这篇指南能让 MP code path 变得更容易理解,也欢迎对帮助 LMCache 在更多平台上稳定运行感兴趣的贡献者加入我们。

参考资料

目录

分享方式:

发表评论

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理

博客更多内容

了解 LMCache Blog 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读