news 2026/9/17 14:16:58

LMCache+vLLM:KV Cache 卸载到CPU/SSD,跑长上下文

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LMCache+vLLM:KV Cache 卸载到CPU/SSD,跑长上下文

最近在折腾长上下文推理的时候,被显存逼得有点头疼。模型权重明明不大,但并发一上来,KV cache直接把显存撑爆。后来我把目光放到 LMCache 和 vLLM 的组合上,试了一把把 KV cache 从 GPU 卸载到 CPU 内存和 SSD 上的方案,效果比我预想中实用不少。这篇博客就把我这次踩坑和验证的完整过程记录下来,包括为什么需要这么干、原理大概是什么、具体怎么配、以及跑起来之后有哪些坑。

这篇文章适合正在用 vLLM 部署大模型、需要处理超长上下文或高并发请求、或者手里只有单卡但想省显存的人。如果你只是想跑通 demo,可能用不上;但如果你在线上被 OOM 折磨过,这篇内容应该能给你一条很清晰的路。

1. 为什么要把 KV cache 卸到 CPU 和 SSD

1.1 先算一笔账:KV cache 到底有多占显存

很多人部署 vLLM 的时候,只盯着模型权重的显存占用,觉得“8B 模型 fp16 也就 16GB,24GB 显存足够”,结果一跑长上下文就 OOM。问题就出在 KV cache 上。

KV cache 就是 Transformer 在做自回归生成时,缓存下来的历史 token 对应的 Key 和 Value 矩阵。它的计算方式大概是:

KV cache 大小 = 2 × 层数 × KV 头数 × head_dim × 序列长度 × batch_size × 每个元素字节数

以 LLaMA-3-8B 为例,32 层,8 个 KV 头,head_dim 128,每个元素 fp16 占 2 字节。那么:

每个 token 的 KV cache = 2 × 32 × 8 × 128 × 2 = 131,072 字节 = 128KB

如果用户输入 32K 上下文,单条请求的 KV cache 就是:

128KB × 32768 = 4GB

再开 8 个并发请求,光 KV cache 就要 32GB。而模型权重本身已经占了 16GB,单张 24GB 显卡根本放不下。这也是为什么很多人用 8B 模型也能把 A100 撑爆。

LLaMA-2-7B 这种模型更夸张,因为没有用 GQA,每 token KV cache 能到 512KB 左右,32K 上下文单请求就要 16GB。所以 KV cache 早就不是“附带品”,而是推理时最大的显存消耗源。

1.2 卸载之后能换来什么

既然显存放不下,最容易想到的方案就是把 KV cache 挪到更低成本的存储上。CPU 内存比显存便宜得多,SSD 更是“白菜价”。LMCache + vLLM 这套方案,就是把 KV cache 按热度分成三层来放:

  • GPU 显存:放最热的 KV 块
  • CPU 内存:放稍旧的 KV 块
  • SSD/memmap:放冷数据

这样做的直接好处有三点:

第一,能跑超长上下文。以前单卡 24GB 跑 32K 上下文都吃力,卸载到 CPU 和 SSD 之后,上下文窗口的主要瓶颈从“显存容量”变成了“CPU 内存 + SSD 容量”,只要磁盘够大,就能继续往后叠。

第二,能提高并发。KV cache 不占满显存之后,vLLM 的调度器可以在显存里保留更多 active sequence,batch size 可以开得更大。吞吐量提升非常明显,尤其是离线批量推理场景。

第三,省钱。与其为了长上下文去买 A100/H100,不如把预算花在大容量内存和 NVMe SSD 上。对于很多内部工具、RAG 查询、长文档总结这类延迟不敏感的场景,这个性价比是很真实的。

当然,卸载是有代价的。CPU 和 SSD 的访问带宽远低于 HBM,KV cache 换进换出会有额外耗时,所以它更适合“高吞吐、容忍一定延迟”的场景,而不是“每个请求都要毫秒级响应”的在线交互服务。

2. LMCache 与 vLLM 的配合原理

2.1 LMCache 到底是干什么的

LMCache 是一个面向 LLM 推理的 KV cache 管理组件,专门做 KV cache 的存储、索引、调度和预取。它不是一个独立推理引擎,而是嵌到 vLLM 这种推理框架里干活。

它的核心思路是:把 KV cache 从“GPU 显存独享”变成“多级存储系统管理”。LMCache 会负责决定哪些 KV 块继续留在 GPU 上,哪些被驱逐到 CPU 内存,哪些冷数据可以被序列化到 SSD,以及当某个前缀被重复请求时,如何直接从缓存里读取 KV 块,避免重新计算整个前缀。

换句话说,它做的不是简单的“offload”,而是带策略的缓存管理。

另一个很实用的能力是跨请求复用。传统 vLLM 的 prefix caching 只能在单实例内生效,实例重启就没了。LMCache 可以把 KV cache 落地到本地磁盘,所以即使在并发请求之间,只要 prompt 前缀相同,就能直接复用之前计算好的 KV 结果。

2.2 vLLM 为什么需要这么一层

vLLM 本身已经有 PagedAttention,显存利用率比 transformers 原生实现高很多。但它对 KV cache 的管理还是以 GPU 显存为绝对核心,并没有真正做好“分层存储”和“跨实例共享缓存”。具体来说:

  • vLLM 的 KV cache 默认都在 GPU,显存不够就 OOM
  • 虽然新版 vLLM 支持一些 CPU offload 能力,但调度和存储策略比较简单
  • 多实例部署时,每台机器的 KV cache 是互相独立的,重复计算浪费严重

LMCache 看上了 vLLM 暴露出来的 KV connector 接口,通过这个口子把 vLLM 的 KV 块分配、释放、复制等动作截获下来,再让 KV cache 落到自定义的存储层。vLLM 不需要大改,只要启动时配置好连接器,就能被 LMCache 接管。

整体结构可以理解为:vLLM 负责调度计算,LMCache 负责 KV cache 的存储和复用。两者各管一摊,配合起来非常干净。

2.3 CPU/SSD 分层的调度逻辑

LMCache 的存储层级,我一般喜欢用“热、温、冷”来理解:

  • 热数据:正在被当前请求访问、或者高频复用的小块 KV,留在 GPU 显存,走 PagedAttention 原生的快速路径
  • 温数据:曾经热过、可能后续还会用到的块,在 GPU 内存压力上来后,先被挪进 CPU 内存
  • 冷数据:长时间没有被访问的块,从 CPU 内存序列化到 SSD,需要时再读回内存/显存

它用的是类似 LRU 的淘汰策略,但会根据 chunk 的访问频率做更精细的判断。从 GPU 往 CPU 迁移的时候不只是“整段丢过去”,而是按预设的 chunk 粒度去切分,这样可以更细粒度地控制缓存有效性。

读回时也有优化,LMCache 支持 CPU 到 GPU 的分块预取,也就是说 vLLM 还没算到某个 token 的时候,LMCache 可能已经提前把对应的 KV 块加载好了。如果你的请求模式有比较明显的规律,预取对延迟的帮助会很大。

贴上我自己画的一个简易分层逻辑,方便大家理解:

推理请求进来 → vLLM PagedAttention 先查 GPU KV 块 → 没命中,查询 LMCache 的 CPU 层 → 还没命中,查询 SSD 层 → 都没有,重新计算 KV 并缓存

这套逻辑看起来简单,实际工程上要处理磁盘写入格式、并发访问锁、内存对齐等问题,但用起来确实很省心,因为大部分细节都被 LMCache 封装好了。

3. 部署实录:我从零搭了一套 CPU/SSD 卸载方案

3.1 环境准备

我这次实验的机器是:

  • GPU:单张 RTX 4090 24GB
  • CPU:Intel Xeon 8375C,32 核
  • 内存:128GB DDR4
  • SSD:1TB NVMe
  • 系统:Ubuntu 22.04
  • Python:3.10
  • CUDA:12.1

软件层面,我用的是 vLLM 加 LMCache 的组合,安装方式非常直接:

pip install vllm pip install lmcache

这里有一个重要提醒:LMCache 对 vLLM 的版本有兼容要求,装之前最好去 LMCache 官方文档看一眼兼容矩阵。如果版本不匹配,可能出现“LMCache 初始化成功但实际不生效”或者直接 import 报错的情况。我自己一开始就吃过这个亏,装了一个新版本 LMCache 配老版本 vLLM,启动时一直报 connector 不存在的错误。

3.2 拉起一个可用的推理服务

我这次用一个开源的 8B 模型做测试,先跑一个不带 LMCache 的 vLLM 服务作为基线:

vllm serve meta-llama/Llama-3.1-8B-Instruct \ --max-model-len 65536 \ --gpu-memory-utilization 0.8 \ --dtype bfloat16

24GB 的 4090,光模型权重就占掉了 16GB 左右,留给 KV cache 的只有 6-7GB,跑 64K 上下文基本不可能很宽裕。如果想要更多 KV cache 空间,就得把--gpu-memory-utilization调低一点,让 vLLM 少预占一些显存,但这个参数调太低会导致 KV 池更小,很容易 OOM。

之后我在 vLLM 的启动参数里加上 LMCache 的配置:

vllm serve meta-llama/Llama-3.1-8B-Instruct \ --max-model-len 65536 \ --gpu-memory-utilization 0.5 \ --kv-transfer-config '{ "kv_connector": "LMCacheConnector", "kv_role": "kv_both", "lmcache_config": { "enabled": true, "chunk_size": 256, "cpu_threshold": 0.7, "ssd_path": "/mnt/nvme/lmcache" } }' \ --dtype bfloat16

解释一下几个关键参数的实际作用:

  • chunk_size: 表示 KV cache 分块的大小,单位是 token 数。256 是一个比较稳的配置,太小会导致 I/O 次数太多,太大又会让缓存淘汰的粒度太粗。
  • cpu_threshold: 当 CPU 内存里的缓存使用率达到这个比例时,LMCache 会把更冷的数据往 SSD 迁移。0.7 意味着 CPU 层用到 70% 左右就开始把数据刷到 SSD。
  • ssd_path: 指定 SSD 缓存存放目录。建议用一个单独的高速分区或者目录,避免和其它读写抢 IO。
  • gpu_memory_utilization: 我从 0.8 调低到 0.5,故意给 KV cache 留出的 GPU 余量变小,这样 LMCache 才有动力把 KV 块往 CPU/SSD 卸载。如果你显存本身就比较充裕,也可以不调这么低。

启动日志里如果看到类似LMCache connector initialized的信息,就说明 LMCache 已经接管了 vLLM 的 KV transfer,配置成功。

3.3 CPU/SSD 卸载实测与效果

服务启动后,我写了一个压测脚本去验证卸载效果。脚本很简单,分两种场景测:

  • 场景 A:一个 32K 的长 prompt,模拟长文档问答
  • 场景 B:32 个并发短 prompt,模拟高并发小请求

上一步配置完成后,我用nvidia-smihtopiostat同时监控显存、CPU 内存和磁盘 IO。

场景 A 的结果很有意思。之前纯 vLLM 在 32K prompt 下,24GB 显存几乎被吃满,而且一旦再加几条并发请求就 OOM。开 LMCache 之后,显存占用明显下降,CPU 内存开始有波动,SSD 目录里也出现了缓存文件。第一个请求因为要重新计算全部 KV cache,耗时比较长,但第二个相同前缀的请求快了很多,因为 KV 直接从 CPU/SSD 读回来了。

场景 B 是纯并发短请求,显存压力反而没那么大,LMCache 的收益更多体现在跨请求复用上。如果 32 个请求共享同一套系统提示词,采用 LMCache 后,公共前缀的 KV cache 不用重复计算,显存占用和算力消耗都降了一截。

我把三类存储的表现整理成一张表:

存储层级典型访问时延(相对)容量适用场景
GPU 显存1x正在活跃使用的 KV 块
CPU 内存10-30x中等温数据,近期可能复用
NVMe SSD100x 以上冷数据,长周期复用

这里强调一下,上面的 100x 是数量级概念,具体要看 SSD 型号。好的 NVMe 读取带宽能到 3-7GB/s,但 CPU 内存带宽能到几十 GB/s,显存带宽则是 TB/s 级别,差距依然明显。

所以我的结论是:SSD 在 LMCache 里更像“保险”,保证 KV cache 不会无限膨胀到 OOM;真正日常跑任务吃得最多的还是 GPU 和 CPU 内存这两层。

4. 常见问题与排查技巧

这个方案毕竟不是开箱即用的黑盒,实际操作中问题不少。下面我把这次探索中遇到过的典型问题整理成一份速查表,后面部署的人可以少走弯路。

4.1 版本不匹配 / ImportError

症状:vLLM 启动时报No module named lmcache,或者 kv connector 找不到LMCacheConnector

排查思路:

  • 先确认 vLLM 和 LMCache 版本是否在官方兼容矩阵内
  • 确认 LMCache 是否真的被装进当前 Python 环境
  • 如果用的是容器,注意别把包装到 base 环境但运行环境是另一个 venv

我在部署时踩过一次:vLLM 用的最新版,LMCache 还在兼容老版本,结果启动时 vLLM 压根不认这个 connector,日志也不报错,只是配置没生效。后来钉版本到兼容组合才正常。

4.2 配置了但显存还是被占满

症状:确认 LMCache 已经启动,但nvidia-smi显示显存还是接近满,CPU/SSD 缓存文件也没怎么增长。

排查思路:

  • 检查--kv-transfer-config里的 JSON 是否写对了,逗号、双引号、布尔值大小写都能让配置失效
  • 确认lmcache_config.enabled是不是 true
  • 看启动日志里是否有LMCacheConnector关键信息
  • 如果显存本身是gpu_memory_utilization预占的,把该值再调低一些,逼迫 vLLM 的 KV block 进入 LMCache 管理范围

有时候日志不会直接报错,需要自己加--verbose或者看 vLLM 的初始化输出,确认到底有没有进入kv_both模式。

4.3 卸载后吞吐反而更低

症状:显存确实降了,但吞吐量比纯 vLLM 低很多,尤其第一个长请求非常慢。

排查思路:

  • 先分清是“首次计算”慢还是“缓存读取”慢。首次计算本来就要重新算,不要把它算作 LMCache 的额外开销
  • 检查 SSD 的写入路径,是不是和系统盘挤在一起,IO 等待严重
  • 调整chunk_size,如果 chunk 太小会有大量小文件读写,磁盘 IOPS 可能成为瓶颈
  • 换一个思路:增加 CPU 内存容量,尽量让 KV cache 在 CPU 层处理,而不是频繁落 SSD

如果你的场景对响应时间敏感,可以把cpu_threshold调高到 0.9 以上,让 KV 块尽量留在 CPU 层,少走一次磁盘。

4.4 缓存命中率特别低

症状:跑了很多请求,但 LMCache 缓存文件只是缓慢增长,同一请求第二次访问也没有加速。

排查思路:

  • LMCache 的 KV 复用依赖于 prompt 前缀一致,如果每个请求的 prompt 前缀都不同,命中率自然低
  • 检查聊天模板是否统一,有些框架会在 prompt 前拼不同 system prompt,导致前缀不一致
  • 如果是多轮对话场景,建议把历史上下文放在 prompt 的固定位置,让 LMCache 能复用历史部分的 KV cache

这里有一个实用技巧:RAG 场景下,尽量把系统提示词、知识库前缀放到 prompt 的最前面,因为 KV cache 是严格的“前缀复用”,后面的差异不会影响前面公共部分的缓存收益。

4.5 OOM 还是会出现

症状:即使启用了 LMCache,显存还是会偶尔 OOM。

排查思路:

  • gpu_memory_utilization调太低会导致 vLLM 分配的 KV block 池太小,反而让并发调度受限
  • --max-num-seqs限制最大并发序列数,避免瞬时并发把 KV 池打爆
  • 确认 LMCache 是否接管了所有 KV transfer,如果 vLLM 还在用默认的显存 KV 分配,两者的缓存池是脱离的

我在实测中把--max-num-seqs从默认值调小了一点,OOM 频率明显下降。对于需要高吞吐的场景,可以在批次大小和显存占用之间找一个平衡点。

5. 我的最终建议与经验分享

LMCache + vLLM 这套方案,我实际用下来的感觉是:它不是一个“把所有 KV cache 都塞到 SSD”的工具,而是一个“让 KV cache 在 GPU、CPU、SSD 之间流动”的调度系统。用得好的人,是在保证延迟可接受的情况下,把显存压力转移给更便宜的存储。

从部署策略上看,我建议这样安排:

  • 热路径在 GPU:高频使用、正在生成的 active KV 块,必须留在显存
  • 温数据在 CPU:扩大缓存容量,降低重复计算概率
  • 冷备份在 SSD:兜底,保底不 OOM,也能跨实例复用

如果你的机器 CPU 内存很大,建议把cpu_threshold调高,尽量让 KV cache 留在 CPU 层,少走磁盘。如果你用的是普通 SATA SSD,不建议把大量 KV cache 放上去,IO 会成为瓶颈。

最后分享一个小技巧。调试 LMCache 最有效的办法不是看显存,而是看htop里的内存趋势和 SSD 目录下的文件变化。如果 CPU 内存占用在涨、SSD 有文件写入,说明卸载路径在正常工作;如果 CPU 内存和显存都很安静,那大概率配置没生效,先去查版本。

这套方案我现在已经用在内部的长文档问答服务上了。和纯 vLLM 相比,单卡能支撑的上下文总长度翻了不止一倍,相同显存下能开的并发请求也多了不少。虽然 SSD 冷读的延迟比显存命中慢很多,但对于批量离线场景来说,省钱又能跑得动,才是最重要的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 14:16:26

OpenClaw 2.6.6 部署完之后,模型通道能改到 TaoToken 吗?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 14:15:41

Qwen3-MAX 调用报 401?TaoToken 给 Deep Agents 这样改 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 14:15:25

Z3求解器入门:从Python API到约束求解实战

简介:Z3使用教程PDF系统介绍微软推出的SMT求解器Z3,面向需要借助自动化推理解决复杂逻辑问题的CS开发者和学生。教程从SMT定义切入,阐明数组理论、算术理论下一阶逻辑公式的可满足性,通过升序数组、查找key、加法交换律等实例展示…

作者头像 李华
网站建设 2026/9/17 14:14:44

AD9653与FPGA的JESD204B硬件连接实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 14:14:31

SpringBoot+Vue3物业管理系统全栈开发实践

1. 项目概述与背景在现代城市住宅小区管理中,传统的人工记录和纸质化办公方式已经难以应对日益增长的住户数量和服务需求。作为一名经历过多个物业管理系统开发的老手,我深知一套高效、稳定的信息化管理系统对物业公司和业主双方的价值。名城小区物业管理…

作者头像 李华