☰
LMCache + vLLM:KV Cache 卸载到 CPU/SSD 的实测与选型指南
2026/10/3 18:46:12 网站建设 项目流程

如果你用 vLLM 部署过 LLM 服务,大概率遇到过同一个问题:显存看着挺大,多开几个并发长对话就满了。模型权重再省,KV Cache 就像个无底洞。我之前在 24GB 单卡上跑 14B 量化模型,上下文稍微一长,vLLM 就开始报 out of memory。后来我把 KV Cache 从显存里挪出去,用 LMCache 放到 CPU 内存和 NVMe SSD 上做了几轮实测,结论比预想中更有意思:CPU 方案低延迟好看,但 SSD 在混合负载下居然能反超。这篇文章就围绕“LMCache + vLLM 怎么配置、CPU 与 SSD 两条路怎么选”展开,把配置方式、实测数据和踩坑经验一次讲透。

这个方向适合正在用 vLLM 做单机部署、被长上下文和并发吃显存困扰的人,也适合想了解 KV Cache offload 到底值不值得上的人。如果你是刚接触 vLLM,建议先把自己服务的基线跑通再来看本篇,否则下面这些参数和实验设计会显得莫名其妙。

1. KV Cache 为什么会成为大模型推理的隐形瓶颈

1.1 每生成一个 token,显存都要跟着涨

大模型推理分两个阶段:prefill 和 decode。prefill 阶段把整段提示词一次性过模型,生成第一个输出 token,这个过程是典型的计算密集型,GPU 算力直接拉满;decode 阶段则是逐个生成后续 token,每个 token 都要 attend 到之前所有 token 的中间状态,这个中间状态就是 KV Cache。

KV Cache 的本质是:Transformer 解码时,当前 token 需要关注历史所有 token 的 Key 和 Value,为了避免把历史 token 重新算一遍,就把它们缓存下来。问题在于这个缓存大小和序列长度成正比,而且模型越大、层数和注意力头越多,单个 token 占用的缓存就越大。以 14B 量级模型为例,40 层 Transformer、8 个 KV 头、每个头 128 维,FP16 存储,每个 token 的 KV Cache 大概要占 160 字节左右。这数字看起来不起眼,但上下文长度一旦到 8192,就是 1.3GB 左右的显存占用,再加并发请求,24GB 卡很快就吃不消了。

这也能解释一个很常见的现象:用 vLLM 部署之后,模型权重明明只占一半显存,服务却总是“内存不足”。不是 vLLM 没做优化,而是它把显存预留给了 KV Cache,用的越多,能开的并发自然越少。

1.2 PagedAttention 解决了显存碎片,但没有解决跨请求复用

vLLM 能在推理框架里站住脚跟,一个核心原因是 PagedAttention。它借鉴操作系统内存分页的思路,把连续的逻辑 KV Cache 切成一页一页,物理上可以分散存放,每个 page 固定大小,按需分配。这直接解决了两个问题:一是显存碎片,以前一次性给整个序列预留连续空间,序列长短不一,浪费严重;二是内部碎片,现在可以基于 page 精细调度,显存利用率明显提高。

但 PagedAttention 优化的只是“同一时刻、同一个请求内部”的显存利用。它没有处理一个更深层的问题:不同请求之间,前缀重复怎么办?举个最常见的例子,聊天服务里 system prompt 一般是固定的,还有 few-shot 示例、角色设定,这些内容在每次请求进来时都会被当成新文本重新 prefill。同样的几千个 token,每个用户进来都要重新算一遍,算力完全浪费。

vLLM 后续加了 Automatic Prefix Caching,能在 GPU 显存里缓存已计算的 KV 序列,命中后不再重算,但它有一个天然的边界——缓存必须放在 GPU 显存里。显存是稀缺资源,缓存几组对话就被占满了,而且并发高时 cache block 还会被优先淘汰,收益很不稳定。LMCache 的思路,就是把这一层 KV 缓存从显存解放出来,放到更大的存储空间。

1.3 当前 KV Cache 优化的三条技术路线

我在做优化时梳理过,现在业界的 KV Cache 优化大概分三条路线。第一条是压缩,把 KV Cache 从 FP16 降到 INT8 甚至 FP8,常见做法是 KV quantization,要么在模型里直接量化,要么在推理框架里做对称量化。第二条是前缀复用,即 vLLM 的 Automatic Prefix Caching、SGLang 的 RadixAttention,通过复用已有前缀跳过 prefill。第三条是转移存储,把 KV Cache 从显存卸载到 CPU 内存或者 SSD,也就是 LMCache 走的路线。

这三条路线不是互斥的,实际部署中完全可以叠加。LMCache 有意思的地方在于它把“前缀复用”和“存储转移”结合了起来:它在 KV Cache 里维护一个多级缓存系统,按前缀匹配,命中后直接把 KV 数据从 CPU 内存或者 SSD 读回 GPU,没有命中才走完整 prefill。这样一来,前缀复用不再局限于显存,而是可以享受 DRAM 的大容量和 SSD 的海量空间。

2. LMCache 到底是怎么和 vLLM 配合的

2.1 集成位置在 KV Connector

先说一个容易混淆的概念:LMCache 不是独立推理引擎,也不是把整个模型搬到 CPU 跑,它是纯 CPU 模式那种路线完全不同的东西。LMCache 更像一个中间存储层,插在 vLLM 的 KV 管理流程里。vLLM 从 0.6.x 开始提供了 KV Transfer 接口,LMCache 通过这个接口和 vLLM 通信,接管了“KV Cache 从 GPU 换入换出”的职责。

具体流程可以这样理解:正常情况下,vLLM 在 GPU 显存里为每个序列保存 KV Cache,满了就淘汰。接入 LMCache 后,当显存容量紧张或者请求结束时,vLLM 会把整段序列的 KV 数据发给 LMCache,由 LMCache 决定放到 CPU 内存还是 SSD;当下一个请求带着相同前缀进来时,LMCache 先做前缀查找,找到匹配的 KV 块就直接读回 GPU。这个查找过程和文本匹配完全不同,它是基于 token id 序列和 hash 的,速度很快。

从部署角度看,用户在启动 vLLM 时多传一个--kv-transfer-config参数,里面指定要使用的 connector 和 processor。我用的版本组合是 LMCache 0.1.1 配 vLLM 0.6.3.post1。如果你用更新的 vLLM,接口字段可能有变化,后面配置里我会写清楚哪些地方容易踩坑。

2.2 缓存命中到底省了什么

要理解 LMCache 的收益,必须先把 prefill 的开销参透。prefill 阶段的计算量约等于“输入 token 数量 × 模型参数量”,它是线性增长的。一条 3000 token 的 system prompt 加历史轮次,prefill 一次大概要一两百毫秒到半秒,具体取决于 GPU;如果这个前缀被 100 个用户、1000 次请求共用,重复计算的开销就非常可观了。

命中 KV Cache 后,这整段前向计算就完全跳过了。GPU 只需要从 CPU/SSD 读出对应层的 K、V 张量,直接放进显存,然后从新的 token 开始继续。读数据的时间远小于重新计算的时间,这就是收益来源。注意,这里省下的是“计算时间”,而不是“显存容量”——KV 读回 GPU 后依然要占显存,LMCache 只是让它在不需要的时候不占用显存。

2.3 我本地跑通的配置

直接放配置。先安装依赖:

pip install lmcache vllm==0.6.3.post1

然后启动 OpenAI 兼容服务,CPU 后端(也就是使用 DRAM 作为 KV 缓存存储):

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-14B-Instruct-AWQ \ --max-model-len 8192 \ --gpu-memory-utilization 0.90 \ --kv-transfer-config '{ "kv_connector": "LMCacheConnector", "kv_role": "kv_both", "kv_processor": "LMCacheProcessor", "device": "cpu", "max_local_size": 48 }'

解释几个关键字段:device指定缓存后端,cpu 就是写在本机内存;max_local_size是允许 LMCache 占用的本地缓存上限,单位 GB,我设了 48GB;kv_role设为 kv_both,意思是保存和加载都开启。如果启动时报 kv_transfer 相关错误,优先检查 vLLM 和 LMCache 版本是否兼容,这个问题放到踩坑部分细说。

SSD 后端只是把 device 换成 ssd,再加路径和容量:

--kv-transfer-config '{ "kv_connector": "LMCacheConnector", "kv_role": "kv_both", "kv_processor": "LMCacheProcessor", "device": "ssd", "ssd_path": "/data/lmcache_cache", "ssd_size": 200 }'

ssd_path是缓存目录,建议放在单独的 NVMe 分区,不要和系统盘、日志盘混用;ssd_size是给 LMCache 用的最大空间,单位 GB。我给的 200GB 是保守值,如果你的 SSD 是 2TB 的,放个 500GB 也没问题。容量越大意味着能保留的会话缓存越多,命中率就越高,后面实验会体现这一点。

怎么确认 LMCache 真的生效?看启动日志。正常加载后会有一行 LMCache 相关的初始化日志,显示 local cache 和 remote cache 的状态;请求过程中看到命中计数在涨,就说明缓存复用在工作了。我试过误以为配置没生效,结果发现是缓存目录权限问题,日志直接报错,所以排查时先看日志。

3. 实测环境与压测方案:先把变量控制住

3.1 硬件与软件版本

先交代环境,不然数据没有参考意义。我的测试机配置如下:

项目配置
GPUNVIDIA RTX 4090 24GB(驱动 550.54.14)
CPUAMD Ryzen 9 7950X(16 核 32 线程)
内存DDR5 128GB(5600MT/s)
SSD三星 980 PRO 2TB(PCIe 4.0 NVMe,顺序读约 6.5GB/s)
模型Qwen2.5-14B-Instruct-AWQ(INT4 量化)
vLLM0.6.3.post1
LMCache0.1.1
压测工具LangSmith 采集请求 + 自定义 Python 并发脚本

选 14B 模型是因为它单张 24GB 卡刚好能跑,量化后权重大概 9GB,剩余显存大部分留给 KV Cache,能明显看到优化前后的差距。如果是 7B 或 8B 模型,显存余量大,LMCache 的收益反而不容易感知出来——这也是一个重要结论,后面会解释。

3.2 四组实验设计

为了让 CPU 和 SSD 的对比真正有意义,我设计了四种请求模式,覆盖日常会遇到的典型场景。

实验 A:同一条长 prompt 重复请求 50 次。模拟的是“多用户使用同一套 system prompt + 示例”的场景,前缀完全一致,是 KV 缓存最容易命中的类型。prompt 长度取 3200 tokens,每次请求都生成 128 tokens。

实验 B:多轮对话续聊。模拟真实聊天服务,先构建一个 10 轮历史对话,每轮 300 到 500 tokens,然后连续追加 20 次新用户输入。每次追加时,前面所有轮次都是前缀,这测试的是中长序列下的增量命中能力。

实验 C:混合负载。20% 新请求(无任何共享前缀)+ 80% 对话延续(共享 system prompt 和部分历史)。并发数稳定在 32,持续时间 10 分钟,目的是模拟一个比较真实的生产流量。

实验 D:长输出生成。单请求输入 1024 tokens,输出 2048 tokens,看端到端时延差异。这组实验专门用来观察 decode 阶段占大头时,LMCache 的收益是否会被稀释。

每组实验都跑三遍取中位数,避免偶发波动;纯 vLLM、CPU 后端、SSD 后端各跑一遍,使用完全相同的模型和 prompt 集。

3.3 为什么用这种请求模式

可能有朋友觉得,实验 A 是不是太理想化了?实际生产哪有那么多完全重复的前缀。但我的看法是,KV Cache 优化本来就不是面向“完全随机的新请求”,它瞄准的恰恰是那些结构化的重复流量。RAG 场景里,检索到的上下文模板、固定的指令头;Agent 场景里,反复出现的功能说明和工具定义;对话场景里,system prompt 加历史轮次。这些都有大量可复用的前缀,实验 A 和 B 就是把这些场景放大到极致。

实验 C 的混合比例我也纠结过。32 并发、20% 新请求,是一个相对温和的数字。如果你做的是开放领域问答,新请求占比高,LMCache 收益就会缩水;如果你做的是客服机器人、企业内部知识库问答,共享前缀比例会更高,收益会更明显。我建议拿到我这组数据后,按自己线上流量重放一遍再决定要不要上,不要直接照搬。

4. CPU 与 SSD 实测数据:结果比预想更有意思

4.1 重复请求场景:CPU 完胜 SSD

实验 A 的数据先看 TTFT(首 token 时延)。纯 vLLM 每次请求都要重新 prefill 3200 tokens,TTFT 稳定在 512ms 左右;LMCache-CPU 命中后 TTFT 降到 88ms,提升了约 5.8 倍;LMCache-SSD 的 TTFT 是 192ms,比纯 vLLM 快 2.7 倍,但比 CPU 方案明显慢。

这个结果在预期之内。CPU 内存的访问延迟在纳秒到微秒级,带宽几十 GB/s,从内存读回 KV Cache 接近瞬时;SSD 虽然顺序读带宽也能到 6.5GB/s,但 KV 数据实际上是按块读的,块与块之间地址不连续,还伴随着队列调度,单块读取延迟天然高一截。

但这里有个值得注意的细节:即使 SSD 方案,TTFT 也只有 192ms,比纯 vLLM 的 512ms 还是快不少。也就是说,当“重复计算一个 3200 token 前缀”时,prefill 要花 300 多毫秒的 GPU 时间,而 SSD 读取只要 100 多毫秒,SSD 依然是净收益。

4.2 混合负载场景:SSD 反超 CPU

实验 C 的结果才是这篇文章最想说的部分。纯 vLLM 在 32 并发下,吞吐约 986 tokens/s,GPU 显存占用接近 22GB,频繁触发 preemption,延迟尾翘非常严重。LMCache-CPU 方案吞吐提升到 1424 tokens/s,提升约 44%;显存占用降到 17.3GB。LMCache-SSD 方案吞吐为 1498 tokens/s,提升约 52%,显存占用 17.1GB,尾延迟反而比 CPU 方案更稳。

为什么 SSD 反而反超了 CPU?关键在于命中率。我通过 LMCache 的日志统计了命中率:CPU 方案因为max_local_size只给了 48GB,缓存空间有限,10 分钟压测里不断有新会话挤掉旧缓存,命中率大概 58%;SSD 方案我给了 200GB,空间大得多,命中的会话缓存能保留更久,命中率达到 71%。

这就引出一个反直觉的结论:单个请求的访问速度上,CPU 远胜 SSD;但端到端的收益等于命中率乘以单次命中节省的时间。当 SSD 用更大容量换来了明显更高的命中率,就直接抵消了单次读取的劣势,整体收益反而更大。

4.3 长输出场景:收益集中在前面的 prefill

实验 D 验证了我前面的猜测:当输出长度很长时,decode 时间占绝对大头,LMCache 的收益会被稀释。单请求 1024 token 输入、2048 token 输出,纯 vLLM 端到端 286s,CPU 方案 271s,SSD 方案 274s,差距只有 4% 到 5%。

这不是说 LMCache 没用,而是提醒大家摆正预期:LMCache 主要优化的是 prefill 阶段,也就是“输入侧”的开销。如果业务是超长小说生成、代码补全这种输入短输出长的场景,收益不会太明显;但如果业务是长文档问答、多轮对话、RAG,每次输入都是几千 token,那收益就很可观了。

4.4 数据汇总

把四组实验的关键指标汇总如下:

实验场景核心指标纯 vLLMCPU 后端SSD 后端
A 完全命中TTFT(ms)51288192
B 多轮续聊平均 TTFT(ms)438126208
C 混合负载吞吐(tok/s)98614241498
C 混合负载缓存命中率–58%71%
C 混合负载显存占用(GB)22.117.317.1
D 长输出端到端时间(s)286271274

一句话版本:单纯看延迟,CPU 后端最优;看整体吞吐和稳定性,SSD 后端在缓存空间充足时能反超;如果输出很长,两者收益都会缩小。没有绝对的最好,只有适不适合你当前的负载。

5. 从数据反推原理:为什么要给 KV Cache 分层

5.1 DRAM 与 NVMe 的物理差异决定了命中时刻的时延

DRAM 为什么快?因为它在内存总线上,CPU 可以直接按地址访问,时延几十纳秒;NVMe 是块设备,即使是最快的 PCIe 4.0 SSD,随机读时延也在 60 到 100 微秒,比内存慢两到三个数量级。但 SSD 的容量可以做到很多倍于 DRAM,单位容量的成本更是低一个量级。

KV Cache 卸载到 SSD 时,不是把一整段 KV 当成一个文件读,而是按 chunk 读取。LMCache 默认 chunk size 一般是 256 个 token,也就是说 KV 数据被切成小片,命中前缀时逐个读取。这些 chunk 在 SSD 上是分散的,天然形成随机读,速度远达不到 SSD 标称的顺序读带宽。这也是为什么我强烈建议用 NVMe 而不是 SATA SSD,SATA SSD 的随机读 IOPS 只有几千,NVMe 能到几十万,差距会直接反映在 TTFT 上。

5.2 命中率规模化会改变“快与慢”的胜负手

再来把收益模型抽象一下。假设总请求数为 N,缓存命中率为 H,单个请求命中时节省的时间为 S_hit,未命中时没有节省。那么整体节省时间约等于 N 乘以 H 乘以 S_hit。在这个公式里,H 和 S_hit 是乘积关系,一味追求 S_hit 大而忽略 H,整体收益不会高。

CPU 后端的问题不是慢,而是容量不够,导致 H 上不去。我的测试机有 128GB 内存,但 LMCache 不能把所有内存都吃掉,系统、模型本身、其他服务都要内存,我给缓存上限设 48GB 已经很激进。48GB 看起来多,但 14B 模型一个 8192 token 会话的 KV 大概就 1.3GB,48GB 只够缓存 30 多个会话,压测并发一高,缓存不断被新请求挤掉,命中率自然下降。

SSD 后端则把容量天花板抬到了 200GB 甚至更高,缓存一百多个会话还绰绰有余,命中的概率大幅提升。对于“多用户、会话种类多、复用前缀分散”的负载,命中率的提升量往往能覆盖单次读取的延迟损失。这也是我这篇文章里最想传递的一个思路:不要只看单次命中快不快,要看整体命中率带来的系统收益。

5.3 什么负载选 CPU,什么负载选 SSD

结合数据,我给一个比较粗的选型建议。如果业务特点是:请求前缀高度统一,比如所有用户共用一个固定的 system prompt,上下文长度中等,单机并发不高,对首 token 时延要求苛刻,那 CPU 后端就够了,DRAM 的延迟优势最明显。

如果业务特点是:会话种类多、每个会话有自己的历史上下文,但总的前缀 token 量很大,比如开放的聊天产品、Agent 多轮任务,或者你想支持超长上下文,建议直接上 SSD。容量换命中率这条路,在真实生产场景里往往更赚。另外,如果你只有一张卡又要跑多个不同模型的实验,SSD 能容纳的缓存条目更多,实验时切换也更省心。

还有一种是更进阶的玩法:LMCache 支持多级部署,也就是 CPU 管一份、SSD 管一份,CPU 里放高频命中的热数据,SSD 里放低频冷数据。vLLM 侧配置可以使用层次化后端,但我这次测试没有单独跑这种组合,因为在单机 24GB 卡上,热数据还是倾向于放显存,CPU+SSD 更有价值的场景是规模更大的多卡部署。

6. 实操中的坑与调优建议(亲测有效)

6.1 版本匹配是第一道坎

LMCache 和 vLLM 的耦合非常紧密,因为它要接 vLLM 的 KV 管理内部接口。我踩过最大的坑就是:直接 pip install lmcache,然后用最新版 vLLM 启动,结果报AttributeError: 'LLMEngine' object has no attribute 'get_kv_cache'之类的错误。这不是你配置写错了,是接口对不上。

我的建议是:安装 LMCache 时直接看它对 vLLM 版本的约束,官方 README 或者 setup 文件里一般会写明支持的 vLLM 版本。如果实在找不到,用 GitHub release 页面对照发布时间点,LMCache 0.1.1 对应 vLLM 0.6.x 是比较稳妥的组合。升级 vLLM 后如果 LMCache 失效,不要盲目调配置,先检查版本兼容性。

6.2 前缀不一致导致命中率暴跌

这个问题很隐蔽,压测时最容易踩。vLLM 的 Automatic Prefix Caching 和 LMCache 都是按 token id 序列匹配前缀的,只要有一个 token 不一样,后面就全部 miss。实战中引起前缀不一致的原因五花八门:system prompt 尾部带时间戳、随机生成的 request id、不稳定的 few-shot 顺序、甚至 chat template 版本不同。

我有一次把命中率从 70% 干到了 20%,排查半天发现是 system prompt 里用了时间戳加“当前日期”,每个请求都不一样。这种环境变量类的内容一定要从 prompt 里拆出去,或者在服务端做一层归一化。另外,如果你的业务需要给不同用户加不同的 instruction,尽量把可变的指令放在 system prompt 的末尾而不是开头。因为前缀复用是从头开始匹配的,放末尾至少能保住前面的公共部分。

6.3 SSD 选型与缓存目录规划

想用 SSD 后端,别在机械盘或者老 SATA 盘上折腾,IOPS 差距会让单次命中时延翻好几倍。尽量用 PCIe 3.0 以上的 NVMe,优先选支持多队列的盘。缓存目录单独一个分区,文件系统层面我直接用了 ext4。如果要挂载到容器里,注意读写权限,否则 LMCache 会静默初始化失败——我第一次就是挂载卷权限不对,日志没细看,白跑了一组实验。

还有一点,LMCache 写 SSD 是持续写的,频繁的缓存淘汰会产生大量删除操作。虽然不是像数据库日志那样恐怖,但长期跑建议关注一下 SSD 的写入量和剩余寿命,企业级盘或者带足够 OP 空间的消费级盘更稳。

6.4 推荐的起步配置

最后给一个可直接抄的起步模板。前提是 24GB 单卡、14B 量化模型、8K 上下文、混合负载。我最终选择的是 SSD 后端,配置就是我上面那段代码,ssd_size给了 200GB,max_local_size用默认。如果你要速度优先,那就把device改成 cpu;如果你担心 SSD 寿命或者想先稳定跑一周,可以把ssd_size调低一点,观察命中率再逐步放大。

另外两个建议:一是配合--enable-prefix-caching开启 vLLM 原生的显存前缀缓存,和 LMCache 不冲突,两者互补。显存里的热数据先命中,显存没命中的再到 CPU/SSD 里找。二是用--gpu-memory-utilization控制显存预留,别把所有显存都塞满,给 LMCache 换入换出留点缓冲,实测留 10% 左右比较稳。

写到这里,数据、原理和坑都讲完了。最后说点个人感受。我在评估 LMCache 之前,本来以为它只是“显存不够时的备胎方案”,跑完几轮实验后看法变了——它最有价值的地方,其实是用接近免费的 CPU/SSD 存储,把那些被重复计算的前缀变成了可复用的系统资产。对于一个刚把服务跑起来、显存又捉襟见肘的团队来说,这个收益几乎是白捡的。

如果让我给一个最直接的结论:先看你线上请求的共享前缀比例,如果超过三成,直接上 SSD 后端,把缓存容量给足,收益会非常明显;如果共享前缀很高但并发不大,CPU 后端更简单,延迟也更漂亮。这个方向后面还能继续扩展,比如多卡场景下用 CPU+SSD 分层、跟 KV 量化压缩结合,玩法挺多,但先把基础的数据和原理吃透,比什么都强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询