
如果你用 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 vllm0.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我设了 48GBkv_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.14CPUAMD Ryzen 9 7950X16 核 32 线程内存DDR5 128GB5600MT/sSSD三星 980 PRO 2TBPCIe 4.0 NVMe顺序读约 6.5GB/s模型Qwen2.5-14B-Instruct-AWQINT4 量化vLLM0.6.3.post1LMCache0.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 tokensTTFT 稳定在 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/sGPU 显存占用接近 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 端到端 286sCPU 方案 271sSSD 方案 274s差距只有 4% 到 5%。这不是说 LMCache 没用而是提醒大家摆正预期LMCache 主要优化的是 prefill 阶段也就是“输入侧”的开销。如果业务是超长小说生成、代码补全这种输入短输出长的场景收益不会太明显但如果业务是长文档问答、多轮对话、RAG每次输入都是几千 token那收益就很可观了。4.4 数据汇总把四组实验的关键指标汇总如下实验场景核心指标纯 vLLMCPU 后端SSD 后端A 完全命中TTFTms51288192B 多轮续聊平均 TTFTms438126208C 混合负载吞吐tok/s98614241498C 混合负载缓存命中率–58%71%C 混合负载显存占用GB22.117.317.1D 长输出端到端时间s286271274一句话版本单纯看延迟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 SSDSATA 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.3GB48GB 只够缓存 30 多个会话压测并发一高缓存不断被新请求挤掉命中率自然下降。SSD 后端则把容量天花板抬到了 200GB 甚至更高缓存一百多个会话还绰绰有余命中的概率大幅提升。对于“多用户、会话种类多、复用前缀分散”的负载命中率的提升量往往能覆盖单次读取的延迟损失。这也是我这篇文章里最想传递的一个思路不要只看单次命中快不快要看整体命中率带来的系统收益。5.3 什么负载选 CPU什么负载选 SSD结合数据我给一个比较粗的选型建议。如果业务特点是请求前缀高度统一比如所有用户共用一个固定的 system prompt上下文长度中等单机并发不高对首 token 时延要求苛刻那 CPU 后端就够了DRAM 的延迟优势最明显。如果业务特点是会话种类多、每个会话有自己的历史上下文但总的前缀 token 量很大比如开放的聊天产品、Agent 多轮任务或者你想支持超长上下文建议直接上 SSD。容量换命中率这条路在真实生产场景里往往更赚。另外如果你只有一张卡又要跑多个不同模型的实验SSD 能容纳的缓存条目更多实验时切换也更省心。还有一种是更进阶的玩法LMCache 支持多级部署也就是 CPU 管一份、SSD 管一份CPU 里放高频命中的热数据SSD 里放低频冷数据。vLLM 侧配置可以使用层次化后端但我这次测试没有单独跑这种组合因为在单机 24GB 卡上热数据还是倾向于放显存CPUSSD 更有价值的场景是规模更大的多卡部署。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给了 200GBmax_local_size用默认。如果你要速度优先那就把device改成 cpu如果你担心 SSD 寿命或者想先稳定跑一周可以把ssd_size调低一点观察命中率再逐步放大。另外两个建议一是配合--enable-prefix-caching开启 vLLM 原生的显存前缀缓存和 LMCache 不冲突两者互补。显存里的热数据先命中显存没命中的再到 CPU/SSD 里找。二是用--gpu-memory-utilization控制显存预留别把所有显存都塞满给 LMCache 换入换出留点缓冲实测留 10% 左右比较稳。写到这里数据、原理和坑都讲完了。最后说点个人感受。我在评估 LMCache 之前本来以为它只是“显存不够时的备胎方案”跑完几轮实验后看法变了——它最有价值的地方其实是用接近免费的 CPU/SSD 存储把那些被重复计算的前缀变成了可复用的系统资产。对于一个刚把服务跑起来、显存又捉襟见肘的团队来说这个收益几乎是白捡的。如果让我给一个最直接的结论先看你线上请求的共享前缀比例如果超过三成直接上 SSD 后端把缓存容量给足收益会非常明显如果共享前缀很高但并发不大CPU 后端更简单延迟也更漂亮。这个方向后面还能继续扩展比如多卡场景下用 CPUSSD 分层、跟 KV 量化压缩结合玩法挺多但先把基础的数据和原理吃透比什么都强。