TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

LMCache: 将 KV 缓存移出 GPU,跨引擎共享

状态:草稿(2026-10-13),部分为实测数据。 实测数据引用自同网站“托斯 ML/AI Infra 准备”第 8 篇,在单张 8GB 级 GPU、Qwen2.5-1.5B-Instruct、vLLM 0.29.0、LMCache 0.5.5(进程内模式)环境下测得。论文与官方文档已于 2026-10-13 直接打开并核实。LMCache 当前推荐的方式(MP 模式)和大模型的数据尚未实测(需要重新下载镜像和模型,正在等待审批)。 2026-10-27 补充。 获得批准后,我们使用 vLLM 0.32.0 官方镜像(已内置 LMCache 0.5.6)和 Qwen3-0.6B 对 MP 模式进行了测试。结果在第 9 节,前文 2026-10-13 的数值保持不变。大模型和多台服务器的场景仍未实测。 编写规则:以实测数据为准,未能测出的标为“未实测”,无法确认的标为“未确认”。

LMCache:将 KV 缓存移出 GPU

目录

1. GPU 内存中缓存的两大局限

第 2 篇中的前缀缓存虽然强大,但它仅在 GPU 内存中工作。因此存在两个遗憾之处。

  1. 容量不足时会被逐出。 当被逐出的前缀再次出现时,系统会从头开始计算。
  2. 实例之间相互隔离。 如果在多张 GPU 上运行相同的模型,旁边的实例无法使用刚刚计算好的 KV。

我们曾测量过第一点带来的痛点有多严重。让系统按顺序读取一组超出 GPU KV 容量(21,520 Token)的文档(10 个文档,每个约 5,000 Token,总计约 5 万 Token),然后 按相同的顺序再次 读取。如果仅使用 vLLM,第二次读取的首 Token 延迟为 576ms,与首次(575ms)相同,vLLM 的强力前缀缓存命中率为 0。因为在采用最近最少使用(LRU)策略且循环访问的缓存中,只要空间稍微不够,接下来要用的文档总是刚被逐出。

2. LMCache 的架构

LMCache: KV 를 GPU 밖에 두고 다시 쓰기 vLLM 인스턴스 1 GPU 의 KV 블록 vLLM 인스턴스 2 GPU 의 KV 블록 KV 커넥터 KV 커넥터 LMCache 256토큰 청크 · 앞부분 해시가 열쇠 · 저장은 비동기 CPU 메모리 PCIe 실측 13.4GB/s 로컬 디스크(NVMe) 장치마다 다름 원격·공유 저장소 Redis·S3·Mooncake·NIXL 등 1GbE 실측 0.1GB/s 위로 갈수록 빠르고 작다 · 아래로 갈수록 크고 여러 엔진이 함께 쓴다 요청 하나가 들어오면 1 프롬프트를 256토큰씩 잘라 앞부분 해시를 만든다 2 빠른 계층부터 찾는다(대기 중에 미리 올려 두기도) 3 적중한 청크는 GPU 로 복사 → 그만큼 프리필 생략 4 나머지만 계산하고, 새 KV 는 뒤에서 내려 저장 인스턴스 2 도 같은 열쇠로 찾는다 → 엔진 사이 공유 앞부분이 아닌 조각은 일부 토큰만 다시 계산(CacheBlend)
图:LMCache 的存储层级与复用流程

LMCache 是一个挂载在推理引擎旁边的 KV 缓存层。根据技术报告(arXiv 2510.09665,2025-12 修订版)和官方文档(2026-10-13 查阅),其架构如下。

由于将相同的前端用作键(Key),因此 实例 1 保存的 KV 可以被实例 2 查找并使用 。如果在同一节点上,则使用 MP 服务器的 CPU 内存;如果在不同节点上,则采用通过共享存储或 RDMA 读取邻近节点内存的 P2P 方式。

3. 为什么 TTFT 和吞吐量会有所改善

核心一句话: 将重复的预填充替换为从存储中加载。

报告中的数据(8×H100 服务器,Llama-3.1-8B·70B、Qwen2.5-72B 等六个模型,CPU 卸载上限 500GB):

实验 条件 结果(相比最强基线)
多轮文档问答,CPU 卸载(8.2 节) 查询约 1 万 Token(约 12 页文档 + 问题),最大输出 100 Token,基线为 vLLM 0.10.2(GPU 前缀缓存)、vLLM CPU 卸载及两个商业产品 在 1 请求/秒下,TTFT 缩短 1.9~8.1 倍,ITL 减少 7%~92%, 在相同 TTFT 下吞吐量提升 2.3~14 倍
实际业务记录回放(8.3 节) 两家公司的输入输出分布,CPU 500GB,基线为 vLLM TTFT 缩短 3.7~6.8 倍,ITL 减少 19%~58%
中央存储服务器(8.4 节) 远程服务器与 15Gbps 链路 在相同 TTFT 下吞吐量提升 1.3~3 倍

摘要中虽然写着吞吐量“最高 15 倍”,但在正文各节中确认的最大值为 8.2 节中的 14 倍。远程存储侧的收益(1.3~3 倍)小于 CPU 内存,是因为网络链路较慢,这与下一节的收支平衡是同一个道理。

4. 实测数据:从 576ms 到 48.6ms

我们在第 1 篇的实验中开启了 LMCache(CPU 内存上限 1.5GB,块大小 256)。环境为 8GB 笔记本 GPU、Qwen2.5-1.5B-Instruct(fp16)、vLLM 0.29.0、LMCache 0.5.5,请求单条发送。

设置 · 轮次 首 Token p50 p95 整轮耗时
仅 vLLM · 首次 575.5ms 637.3ms 6.2 秒
仅 vLLM · 再次 576.2ms 577.9ms 6.2 秒
LMCache · 首次 597.8ms 676.2ms 6.5 秒
LMCache · 再次 48.6ms 59.6ms 0.9 秒

“再次”指按相同顺序重新读取的第二轮。

改变长度后发现,随着上下文长度增加,收益也随之增大(CPU 2GB,重新请求在 GPU 中被挤出的文档)。

输入 Token 重新计算 已加载 倍数
554 66ms 21ms 3.1 倍
1,077 119ms 24ms 5.0 倍
2,144 231ms 29ms 8.0 倍
4,273 481ms 45ms 10.7 倍
6,414 768ms 41ms 约 19 倍

重新计算的时间几乎与长度成正比,但加载时间增长缓慢,仅比在 GPU 中直接命中(20~32ms)多出几毫秒。

容量呈断崖式表现。 我们仅修改 CPU 上限对同样的重新读取进行了测试(工作集的 KV 计算量约为 1.49GB)。

CPU 上限 重新读取首 Token 延迟 外部命中
无(仅 vLLM) 600ms —
0.5GB 620ms 0%
1.0GB 620ms 0%
1.5GB 39ms 98%
2.0GB 39ms 98%

只要比工作集稍小一点,收益就为 0,只剩下存储开销。这就是与第 1 篇相同的 LRU 循环访问陷阱。在符合实际的某些文档被频繁读取的分布(齐普夫分布,60 个文档)下,较小的缓存也能发挥作用,平均首 Token 延迟从仅 vLLM 的 278ms 分别降至 CPU 1GB 时的 174ms 和 2GB 时的 63ms。

本实验的局限性:并发数为 1,使用单个 1.5B 模型,采用随机单词构成的合成文档,且为单次测量值;测试时使用的是进程内模式,而非当前文档推荐的 MP 模式。

5. 盈亏平衡:仅当读取成本低于重新计算时

다시 계산할까, 불러올까: 첫 토큰까지 걸린 시간(ms) Qwen2.5-1.5B · 8GB급 GPU · 토큰당 KV 28,672바이트 0 250 500 750 1000 0 2,000 4,000 6,000 입력 토큰 수 다시 계산(실측) 768ms CPU 에서 불러옴(실측) 41ms 1GbE 로 불러오기(계산) ↑ 재계산 실측 불러오기 실측 1GB/s 로 불러오기(계산) 1GbE 로 불러오기(계산) 손익 분기 대역폭 ≈ 28,672B ÷ 0.12ms ≈ 0.24GB/s (계산) 이보다 느린 길로 KV 를 옮기면 다시 계산하는 편이 빠르다
图:重新计算与读取耗时的再次对比

若要让读取获利, 传输 KV 的耗时必须短于重新计算该 KV 的耗时 。针对 n 个 Token 的计算公式如下:

由于两边都有 n,减去固定延迟后得出: 盈亏平衡带宽 = 每个 Token 的 KV 字节数 ÷ 每个 Token 的 Prefill 耗时代入上述实际测量值(6,414 个 Token 耗时 768ms,每个 Token 约 0.12ms),得:28,672 字节 ÷ 0.12ms ≈ 0.24GB/s (计算结果)。

模型和 GPU 决定了平衡点。 Prefill 的计算量大致为每个 Token 「2 × 参数量」次 FLOP,KV 字节数则遵循第 1 篇中的公式。若用公开配置计算二者的比值(即节省 1 字节 KV 所省下的计算量,此处忽略 Attention 计算进行估算),结果如下:

模型 每个 Token 的 Prefill 计算量 每个 Token 的 KV 每 1 字节 KV 对应的计算量
Qwen2.5-1.5B 约 3.1 GFLOP 28KiB 约 11 万 FLOP
Llama 3.1 8B 约 16 GFLOP 128KiB 约 12 万 FLOP
Qwen2.5-7B 约 15 GFLOP 56KiB 约 27 万 FLOP
Llama 3.1 70B 约 140 GFLOP 320KiB 约 43 万 FLOP

每 1 字节 KV 对应的计算量越大,读取就越具优势。通过 GQA 减少了 KV 的大模型越有「读取的价值」。反之, GPU 越快 ,重新计算的速度就越快,从而拉高了平衡带宽。即使是相同的存储,在慢速 GPU 上可能是划算的,但在高速 GPU 上则可能会亏本。

命中率也存在平衡点。 命中时可节省 527.6ms(576.2 − 48.6),未命中时则需付出 22.3ms 的存储开销(597.8 − 575.5)。令期望收益为零的命中概率 p 大约为 22.3 ÷ (22.3 + 527.6) ≈ 4%。这是本实验测得的数值,并非通用常数。代入其他公开资料的数据,该数值最高可上升至 20% 左右。

6. 重写非头部片段:CacheBlend、CacheGen

前缀缓存 仅在从头开始完全一致时 才会生效。如果像 RAG 那样每次以不同顺序拼接多个文档片段,除了第一个片段外,其余均无法命中。如果直接将各片段单独生成的 KV 拼接起来,会缺少片段之间的 Attention(后一片段对前一片段的关注),从而导致回答错误。

CacheBlend(Yao 等人,EuroSys 2025)在拼接后 挑选出 KV 偏移最大的那些 Token 进行重新计算。在各层中,此类 Token 约占 10%~15%,重新计算 10%~20% 就足够了。它将重新计算与下一层的读取重叠以掩盖延迟。结果显示,与完全重新计算相比,TTFT 减少了 2.2~3.3 倍,吞吐量提升了 2.8~5 倍,质量(F1·Rouge-L)下降幅度在 0.01~0.03 以下(Mistral-7B、Yi-34B、Llama-70B,双 A40,NVMe 实测 4.8GB/s)。LMCache 的 MP 服务器通过 --engine-type blend 来启用此方案。

CacheGen(Liu 等人,SIGCOMM 2024)从根本上降低了传输 KV 的开销。它利用相邻 Token 的 KV 相似这一特点,仅对差值进行编码,并对靠前的层采用密集量化、靠后的层采用稀疏量化。相比常规量化,KV 体积缩小了 3.5~4.3 倍;在 3Gbps 链路下,TTFT 比量化基准低 3.2~3.7 倍,准确率下降幅度在 2% 以内(Mistral-7B、Llama-34B、70B,4 张 A40)。当带宽下降时,它会降低每个区块的压缩级别,或者 切换为发送文本进行重新计算 。这相当于对第 5 节的盈亏平衡点进行实时逐块计算。

7. Prefill 与 Decode 分离架构中的 KV 传输

传输 KV 的能力也可用于其他场景。在第 4 篇中将看到的 分离式服务需要将 Prefill GPU 生成的 KV 发送给 Decode GPU。LMCache 文档中的分离配置将 Prefill 到 Decode 的传输交给 NIXL(vLLM 的 NixlConnector),并结合 vLLM 的 MultiConnector 将 LMCache 连接器打包在一起,以进一步提升请求之间的复用率。报告显示,在 8K 输入、200 输出以及 NVLink 条件下,相比 vLLM 自带的分离配置,其平均 TTFT 降低了 1.53~1.84 倍,平均 ITL 降低了 1.12~1.66 倍(第 8.5 节,本节未确认模型名称)。

8. 什么时候没有收益

条件 原因 依据
提示词很短 重新计算已经足够便宜,可节省的空间很小。固定延迟占比更大 实测:554 个 Token 时为 3.1 倍,6,414 个 Token 时为 19 倍
相同的前缀不会再次出现 如果没有命中,就只会平添存储成本 实测:首次读取 +4%
缓存容量小于工作集(循环访问) LRU 总是会删掉接下来要用的内容 实测:在 0.5·1.0GB 下命中率为 0%
存储或网络很慢 传输耗时超过重新计算耗时 计算:1GbE 低于该模型的平衡带宽 0.24GB/s。报告第 8.7 节
小模型、快 GPU 重新计算速度极快,推高了平衡带宽 计算(第 5 节表格)
GPU 前缀缓存已经足够 如果工作集能完全塞进 GPU,仅靠 vLLM 即可命中 实测:在区块大小测试中,数值与仅使用 vLLM 时相同
频繁读取的文档残留在 GPU 中(齐普夫分布) 只会徒增查询和存储开销 实测(2026-10-27,第 9-4 节):平均首个 Token 从 17.9 增至约 20ms
文档比区块(256 个 Token)更短 不足一个区块的尾部无法被读取 实测(2026-10-27,第 9-3 节):510 个 Token 时为 1.1 倍

9. 2026-10-27 MP 模式重新测量

第 4 节使用的是当前文档标记为 “Legacy” 的进程内模式。我们在同类 8GB 级 GPU 上重新测试了官方推荐的 MP 模式(独立进程的 lmcache server 和 LMCacheMPConnector)。由于模型和版本不同,请勿将本节数字与第 4 节直接作比较,而应在同一天、同一条件下对比 “vLLM 独占 / 进程内 / MP” 这三行数据。

9-1. 环境与方法

lmcache server --l1-size-gb 0.75 --eviction-policy LRU --port 5555 --http-port 8080
vllm serve Qwen/Qwen3-0.6B --disable-hybrid-kv-cache-manager \
  --kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both","kv_connector_extra_config":{"lmcache.mp.port":5555}}'

9-2. 基础效果:重新读取无法完全塞入 GPU 的文档合集

将 GPU KV 限制为 2,336 个 Token,按相同顺序连续四次读取 4 个大小约为 1,020 个 Token 的文档(总计约 4,080 个 Token)。

配置 首次运行(含存储开销) 重新读取(第 2~4 次运行的中位数) 每个文档读取的 Token 数
vLLM 独占 55.7ms 46.5ms 0
进程内(LMCacheConnectorV1,CPU 0.75GB) 71.3ms(+28%) 24.2ms(1.9 倍) 768
MP(LMCacheMPConnector,L1 0.75GB) 60.7ms(+9%) 32.9ms(1.4 倍) 768

9-3. 按长度对比:重新计算与读取

将 GPU KV 设为 0.75GiB(7,020 个 Token),在读取并存储一次文档后,利用 vLLM 的 /reset_prefix_cache(开发模式)清空 GPU 缓存并再次读取。LMCache 侧则通过针对每个文档的管理 API 清空 L1。针对每个文档长度,去掉四次测试中的第一次,取其余的中位数。

输入 Token 数 vLLM 独占重新计算 MP 读取 读取的 Token 数 倍率 GPU 命中(参考)
约 510 32.2ms 29.8ms 256 1.1 倍 11.2ms
约 1,005 45.9ms 37.0ms 768 1.2 倍 12.0ms
约 2,010 98.3ms 46.3ms 1,792 2.1 倍 14.5ms
约 4,020 226.2ms 68.2ms 3,840 3.3倍 19.2ms
约 6,030 390.9ms 91.3ms 5,888 4.3倍 23.8ms

放大倍数比第4节中 1.5B 模型的实测结果(6,414个 Token 约为 19 倍)要小。这是因为该模型重新计算的速度很快而 KV 很大,在相同长度下节省的时间较少,但需要迁移的字节较多。不过,长度越长放大倍数越大的趋势是一样的。

从第一次尝试中吸取的教训。 在没有清空 L1 的情况下运行按长度测试时,2,000个 Token 以上的大部分都没有命中。LMCache 日志中累积了「Failed to batched allocate … no enough memory」警告,并且指标 lmcache_mp_l1_allocation_failure_chunks_total{during="l1_store"} 有所增加。由于驱逐机制是在使用率超过阈值(默认 0.8)时以异步方式每次清空 20%,因此当大文档同时涌入时,它没有等待腾出空间,而是 丢弃了存储。 由于不会产生错误、只会导致未命中,因此必须为 L1 预留比单次涌入的最大文档更加充足的空间,并密切监控这一指标。

9-4. 容量:非断崖式下跌而是部分命中,以及齐普夫分布的负面效果

循环访问读取了与 9-2 相同的 4 个文档(工作集约 0.46GB),齐普夫分布读取了约 480 个 Token 的 20 个文档,共读取 120 次(指数 1.1)。GPU KV 为 2,336个 Token。

配置 循环:重新读取首个 Token 循环:外部命中 Token 齐普夫:平均首个 Token
vLLM 独占 46.5ms 0 17.9ms
MP, L1 0.25GB 41.2ms 1,791 / 4,034(44%) 20.3ms
MP, L1 0.5GB 32.6ms 3,328 20.0ms
MP, L1 0.75GB 33.0ms 3,328 19.9ms

9-5. MP 模式独有的优势

9-6. L2 本地磁盘层与收支平衡点

将 L1 设置得较小(0.25GB),并挂载了文件系统 L2(--l2-adapter '{"type":"fs","base_path":"…","use_odirect":true}',绕过页面缓存的 O_DIRECT,容器的临时磁盘)。

项目 值
循环重新读取首个 Token(L1 0.25GB + L2) 41.6ms(无 L2 时为 41.9ms)
将从 L1 驱逐的 1,000个 Token 文档重新读入 L2 54.6ms, 55.4ms(加载了 768个 Token)
由 vLLM 独占重新计算相同文档 45.9ms
L2 读取速度(LMCache 指标平均值) / dd O_DIRECT 读取 每秒 3.8GB / 每秒 4.8GB
L1→GPU 加载速度(LMCache 指标,20次平均) 每秒 11.8GB
GPU→L1 存储速度(同上指标,32次平均) 每秒 12.6GB

9-7. 指标

MP 服务器的 Prometheus 指标可以在此版本的管理 HTTP 端口的 /metrics 中找到(通过--prometheus-port 传递的 9090 端口无响应)。 lmcache_mp_ 以 开头的指标共有 48 种,在运维中最先需要查看的是以下内容:

想要询问的问题 指标 本次观测到的值
命中率(以块为单位) lookup_hit_tokens_total ÷ lookup_requested_tokens_total 按长度测试 83,456 ÷ 150,528
来自哪个层 lookup_hit_l1_tokens_total, lookup_hit_l2_tokens_total L2 测试 5,120 / 1,024
L1 填充了多少 l1_usage_ratio, l1_memory_usage_bytes 0.77(L1 0.25GB)
存储是否被丢弃 l1_allocation_failure_chunks_total{during="l1_store"} L2 测试 6 个块
传输速度 l0_l1_load_throughput_GB_per_second, l0_l1_store_…, l2_load_…(直方图) 9-6 表格
驱逐 l1_evicted_chunks_total, l1_eviction_loop_triggered_total
复用间隔 real_reuse_gap_seconds, l1_chunk_reuse_gap_seconds

在 vLLM 端, vllm:prompt_tokens_by_source_total{source="external_kv_transfer"} 是从 LMCache 接收的 Token 数量(本节的「外部命中 Token」)。帮助文档中写明管理 HTTP API 没有认证且默认仅在 127.0.0.1 上开放。路由列表中包含 /cache/clear, /reconfigure/…, /run_script ,如果将该端口暴露到 Pod 外部,任何人都可以清除缓存或更改设置(未确认/run_script 的默认激活状态)。

9-8. 总结

问题 2026-10-27 MP 模式实测的答案
是否能快速重新读取被驱逐的长文档 是。6,000个 Token 从 391 缩短至 91ms(4.3倍)
短文档如何 收益较小。不足一个块(256个 Token)的尾部会重新计算,在 1,000个 Token 时为 1.2 倍
与进程内模式相比 重新读取慢约 9ms,存储开销较小
重新启动引擎后缓存是否仍会保留 是(仅 MP 支持)
热文档停留在 GPU 上的负载 反而变慢了(17.9 → 20ms)
磁盘层 对于这个小模型而言,比重新计算更慢

TeeTee 的一句话。 为了重新观看比赛视频,没必要每次都重新打一场比赛。播放录像就行了。不过,如果录像存放在很远的仓库而且路况堵塞,有时候直接再打一局反而更快。必须先衡量一下距离有多远、需要重复观看多少次。

参考

登录后即可点赞

评论

还没有评论。

登录后即可发表评论