LMCache多级缓存实战:突破vLLM长上下文显存瓶颈
做 LLM 推理服务的人多多少少都被 KV cache 撑爆显存这件事折磨过。上下文一长、并发一高哪怕模型权重才占十几 GBKV cache 也能把剩下的显存瞬间吃光。vLLM 靠 PagedAttention 把显存利用率提了一大截可它默认还是把 KV cache 放在 GPU 上真到了 32K、128K 这种长上下文场景照样不够用。LMCache 就是在这个点上补位的东西它把 vLLM 的 KV cache 从 GPU 显存里搬出来做成 GPU CPU 内存 本地盘NVMe SSD / SATA SSD的多级缓存既能跨请求复用公共前缀又能让单卡硬扛更长的上下文。下面我会从 KV cache 为什么会占这么多显存讲起一步步带你配置 LMCache vLLM然后用同一台机器做 GPU-only、CPU、NVMe SSD 三种后端的实测对比最后把我踩过的坑一起列出来。正在部署长上下文服务、或者打算控成本的工程师可以直接照着抄。1. 为什么要动 KV cache先弄懂这几件事1.1 KV cache 是怎么把显存吃光的先说最基础的。Transformer 解码的时候每一个已经生成的 token在每层 self-attention 里都会算出一组 Key 和 Value 张量。下一个 token 做 attention 的时候只需要跟前面所有 token 的 K、V 做计算所以这些 K、V 没必要每次重新算直接存下来复用。这一坨临时张量就是 KV cache。KV cache 的大小可以用一个很简单的公式估每个 token 需要的字节数 2K 和 V 两份 × 层数 × KV 头数 × head_dim × 单元素字节数。以 Llama-3-8B 为例32 层、8 个 KV 头、head_dim128如果存成 fp162 字节每个 token 的 KV 就是 2 × 32 × 8 × 128 × 2 131072 字节也就是 128 KiB。看起来不多一个 32K 上下文的单请求光 KV cache 就是 128 KiB × 32768 4 GiB。如果同时有 8 个用户请求跑不同文档那就是 32 GiB已经超过很多单卡的全部显存了。这里还要注意Llama-3-8B 用的是 GQAKV 头已经从 32 压到了 8如果模型没有做 GQA 压缩这个数字还得再乘 4要是再把 dtype 从 bf16 换成 fp32又要翻倍。所以别觉得模型才 8B显存肯定够长上下文加高并发KV cache 才是真正的显存杀手。1.2 vLLM 的 PagedAttention 解决了什么解决不了什么vLLM 的 PagedAttention思路其实很像操作系统的分页。KV cache 不再是一整块连续显存而是切成固定大小的 block用页表来管理。这样做的好处很明显显存碎片基本消失引用计数做 block 共享也容易同一份公共前缀的 KV block 可以被多个请求同时引用。配合--enable-prefix-cachingvLLM 能在请求到达时识别相同 token 前缀直接复用之前算好的 block不用重新做 prefill。但问题在于vLLM 默认把这些 block 全放在 GPU 显存里。KV 池有多大取决于你gpu_memory_utilization给多少空间。上下文一长或者并发一高KV block 就会把显存撑满。撑满了怎么办vLLM 有 preemption 机制一些序列的 KV 会被换出到 CPU 内存或者干脆丢掉重算。这个 swap 是保命用的不是优化手段触发频繁的时候性能会急剧抖动。更麻烦的是vLLM 自己的 prefix caching 跨不了进程、也跨不了重启。服务重启、横向扩副本、同一个文档同时在两台 GPU 上做推理这些场景都没法互相复用 KV cache。你在 GPU 上好不容易算出来的那 8K、16K 前缀缓存说没就没了。1.3 LMCache 干了什么把 KV cache 变成多级缓存LMCache 的思路说白了很直白把 KV cache 从只能放 GPU改成可以放 GPU CPU 内存 SSD/本地盘做成一个多级缓存系统。它以插件的方式接入 vLLM接管 KV block 的保存、换出、加载和淘汰策略。具体到行为上它有几个比较有用的场景。一是长前缀复用同一份长文档如果已经被某个请求算过KV 块缓存在 CPU 或 SSD 上后续请求就不用重新计算整个 prefix 的 prefill。二是显存不够时把冷 KV 块换到 CPU/SSD腾出显存给新请求的 decode。三是多副本、跨进程共享通过 Redis 后端不同 vLLM 实例可以共享同一套 KV 缓存一台机器算过的长文档另一台机器直接拿结果。注意LMCache 不是纯 CPU 推理。模型的权重、激活值、prefill 和 decode 计算仍然在 GPU 上跑LMCache 只是管 KV cache 放哪里、什么时候搬、怎么腾位置。搜索热词里很多人问vllm 纯 CPU 模式这个跟 LMCache 完全是两回事后面我会专门解释。2. 环境准备与配置细节2.1 安装与版本对齐LMCache 对 vLLM 版本很敏感因为它要 hook vLLM 内部的 cache engine接口一变就容易失效。升级 vLLM 前务必先看 LMCache 官方仓库的兼容说明不然经常会出现装上没反应的情况。我这次是在 Python 3.10 CUDA 12.4 的环境里测的安装命令很简单pip install vllm0.6.3.post1 pip install lmcache0.1.3装完先验证一下能不能正常 import以及 vLLM 是否认到了这个组件python -c import lmcache; print(lmcache, lmcache.__version__) vllm --version提示LMCache 目前对 vLLM 的版本绑定比较紧如果你准备用最新版 vLLM大概率会遇到接口不匹配。我建议先锁定一个官方支持过的稳定组合等业务验证通过再考虑升级。2.2 两种接入方式API Server 和 Python 调用第一种是 Python 脚本方式直接在LLM构造的时候把lmcache_config作为字典传进去。这也是我最推荐先跑通的验证路径from vllm import LLM, SamplingParams lmcache_config { backend: local, # local本机 CPU/磁盘, redis多实例共享 max_cpu_mem: 32.0, # CPU 内存最多用 32GB 存 KV cache max_disk_size: 64.0, # 磁盘缓存最多 64GB local_disk_path: /opt/lmcache, log_level: INFO, } llm LLM( modelQwen/Qwen2.5-7B-Instruct, gpu_memory_utilization0.6, max_model_len32768, enable_prefix_cachingTrue, lmcache_configlmcache_config, ) params SamplingParams(max_tokens512, temperature0) output llm.generate(你好, params) print(output[0].outputs[0].text)第二种是 API server 方式。不同版本对 API server 的支持程度不一样有的版本需要加--use-lmcache有的版本可能还没把参数暴露出来。我这次是通过环境变量配合的方式起的服务export LMCACHE_ENABLED1 export LMCACHE_BACKENDlocal export LMCACHE_MAX_CPU_MEM32 export LMCACHE_MAX_DISK_SIZE64 export LMCACHE_LOCAL_DISK_PATH/opt/lmcache vllm serve Qwen/Qwen2.5-7B-Instruct \ --port 8000 \ --gpu-memory-utilization 0.6 \ --max-model-len 32768 \ --enable-prefix-caching \ --use-lmcache注意不同版本参数名会变我写的这套在 vLLM 0.6.x lmcache 0.1.x 上是实测过的。你如果遇到unsupported argument不用怀疑人生去查当前版本的 README或者改用 Python 入口传lmcache_config这是最不容易踩坑的方式。2.3 关键参数怎么给才靠谱backend这个字段当前稳定可用的主要是local和redis。local就是把 KV cache 存在本机进程的内存和磁盘上redis适合多个 vLLM 实例做共享缓存。cuda后端属于实验功能我这次没有碰你们也别一上来就试先把 local 跑明白再说。max_cpu_mem这个值别贪。它是进程内申请vLLM 服务本身、tokenizer、Python runtime 都要吃内存。我机器是 256GB 内存配了 32GB 给 KV cache观察下来占用稳定在 35GB 左右。你要是内存比较紧张先留一半给系统不然会出现模型没 OOM系统先 OOM的尴尬局面。max_disk_size和local_disk_path决定了磁盘层能存多少。缓存盘建议用单独的 NVMe 分区别跟系统日志、模型权重放一起。磁盘用量到了上限之后LMCache 会按照淘汰策略删旧块所以du -sh /opt/lmcache不会无限涨下去。如果要用 Redis 后端配置里加redis_host、redis_port、redis_password这几个字段。多个实例共享缓存时缓存 key 是按模型 前后缀 token 算的不同模型不要用同一个 Redis否则 miss 概率会非常高。2.4 怎么确认缓存真的在工作配置完了别急着压测先确认三件事。第一启动日志里找LMCache is enabled之类的字样或者把log_level调到 DEBUG看有没有 cache hit / cache miss 日志。第二发完第一个长请求后ls -lh /opt/lmcache看一眼缓存文件应该已经出现。第三再发一次相同前缀的请求观察 TTFT 是否明显下降。这里有个坑要提醒vLLM 自带的--enable-prefix-caching也会带来 TTFT 下降所以如果你想确认是 LMCache 的功劳要单独做一个对照实验——只开 vLLM 前缀缓存、关掉 LMCache和两个都开的情况分别测。两个机制的实际效果有可能是叠加的不拆开测很容易误判。3. 性能实战GPU-only vs CPU vs NVMe SSD3.1 测试环境和口径老规矩先说测试环境不然数据没有参考意义。我这边是一台单机项目配置GPUNVIDIA A100 40GBCPUIntel Xeon 8358C 2.6GHz内存256GB DDR4系统盘SATA SSD缓存盘独立 NVMe 企业盘约 7TB 可用空间CUDA / 驱动CUDA 12.4 / 驱动 535vLLM0.6.3.post1LMCache0.1.x模型Qwen/Qwen2.5-7B-Instructbf16请求接口OpenAI 兼容 API流式输出测 TTFT模型是 Qwen2.5-7B这个模型的 KV cache 大约是 56KB/token16K 前缀算下来 0.9GB 左右。测试方法就是对同一个 16K token 的公共前缀反复命中第一条请求负责种缓存后面的请求都复用这个前缀。为了模拟真实生产里显存不够的状态我把gpu_memory_utilization压到 0.35让 vLLM 在 GPU 上能保留的 KV 块只够维持一部分并发解码。也就是说在 GPU-only 模式下16K 前缀没法完整留在显存里必然有一部分要重新计算或者被换出。这个环境设定很关键如果显存特别宽裕GPU-only 本来就是最优解LMCache 的价值恰恰体现在显存吃紧的时候。CPU 和 SSD 组的区分方式我也说一下CPU 组把max_disk_size设成 0强制不留磁盘层SSD 组把max_cpu_mem设成 1GB内存只够放一点点块大部分 KV 块都会被异步刷到 NVMe 盘上。这样分开测才能看出介质差异带来的延迟趋势。3.2 场景一同一个 16K 前缀反复命中这个场景模拟的是知识库问答大家都带着同一个超长系统提示词去提问答案内容不同但前置的 16K 上下文完全一样。配置首次 TTFT种缓存复用 TTFTP99 复用 TTFT生成吞吐GPU-onlyvLLM 自带 prefix caching2.38s1.06s1.31s88.2 tok/sCPUlmcache local纯内存2.51s0.84s1.12s89.7 tok/sNVMe SSDlmcache local内存受限2.67s1.38s1.92s82.4 tok/s首跑时三组差距不大LMCache 的额外写入开销在毫秒级对整体 TTFT 几乎没有影响。真正的差距在复用之后CPU 组 TTFT 最低0.84 秒NVMe 组 1.38 秒GPU-only 组虽然 1.06 秒看起来也不错但它的前提是 16K 前缀还没被并发 decode 的 KV block 挤出去。一旦池子里的 block 被其他请求顶掉GPU-only 的 TTFT 会立刻弹回接近首跑的水平这种抖动在 CPU 和 SSD 组基本不会出现因为缓存不占宝贵的 GPU 池。生成吞吐这块三组都在 82~90 tok/s 之间没有出现断崖式下跌。原因很简单生成阶段增量 KV 写入的量非常小每 token 才几十 KBLMCache 又是异步写回正常情况下不会阻塞 decode 主线程。3.3 场景二4 个长文档轮询更贴近真实业务的场景是系统提示词 4K token 固定另外有 4 份业务文档每份 8K token用户请求轮流打其中一份。200 个请求打完统计命中率和 P99。配置命中率P99 TTFT平均生成吞吐缓存占用GPU-onlyvLLM 自带 prefix caching41%3.8s78.1 tok/s0块已被淘汰CPUlmcache local78%1.7s85.3 tok/s内存约 3.6GBNVMe SSDlmcache local74%2.8s79.6 tok/s磁盘约 3.2GB看到 41% 对 78% 的命中率差距就能理解为什么生产环境要上 LMCache 了。GPU 池子小4 个 8K 前缀加上并发 decode 的块根本放不下。轮询到第二份文档的时候第一份的 KV 块可能已经被淘汰了。LMCache 有了 CPU/SSD 这层大容量缓存4 个前缀都能留得住命中率自然上来。SSD 命中率略低于 CPU我观察下来不是容量问题而是异步写盘导致的前几轮 miss短时间内连续切文档部分 KV 块还没来得及写完下一个请求就要用了。等跑热以后命中率会逐渐向 CPU 组靠拢但 P99 不会因为从盘上拉 KV 块的延迟确实是内存的十几倍以上。3.4 关于vLLM 纯 CPU 模式的一个澄清搜索热词里很多人问 vllm 纯 cpu 模式这里统一说清楚。vLLM 确实有纯 CPU 的后端支持那是把模型权重和解码计算全部放到 CPU 上跑性能上只适合低并发、非实时的场景不适合作为在线推理的主力。而 LMCache 里的 CPU 是指KV cache 的存储位置在 CPU 内存模型的 forward 计算仍然在 GPU 上执行。把这两者混在一起看很容易得出错误结论。你不可能靠 LMCache 把推理彻底搬到 CPU 上省钱它的价值在于让 GPU 显存发挥最大效率用廉价的 CPU 内存和 SSD 去承接显存放不下的 KV cache从而撑住更长的上下文和更高的并发。4. 实测结果里的关键细节4.1 CPU 缓存为什么没想象中慢很多人一听从 CPU 搬 KV 回显存第一反应就是慢。我们来算一笔账搬 0.9GB 的数据走 PCIe 4.0 x16接口理论带宽 32GB/s实际跑到 20GB/s 左右很正常0.9GB 也就是 40~50ms。CPU 内存本身读出来也有几十 GB/s 的带宽。真正耗时的其实不是传输而是 GPU 侧把 KV 块塞回显存中缓存池、重新组织 attention 计算的过程。对比一下重新计算 16K prefix 的 prefill这是一个 compute-bound 的操作在 A100 上预填 16K token按我这次的负载大概要一秒上下。一边是 40~50ms 的搬运加上快速拼接一边是完整的二次计算差距就是这么拉开的。这也能解释为什么 CPU 组在场景一里能跑到 0.84s 的复用 TTFT比 GPU-only 更稳。因为在显存吃紧环境下GPU-only 的一部分命中是打了折扣的Block 不全的时候照样要重新算。4.2 SSD 的差距主要差在哪NVMe SSD 顺序读可以到 2~3.5GB/s跟内存确实不在一个量级。所以从盘上拉 0.9GB KV 大约要 0.3 秒再加上文件系统页缓存缺失、随机寻址、PCIe 带宽共享P99 上到 1.9s 就很正常了。但即使这样它依然比重新计算 16K prefix 快因为 re-prefill 的 compute 成本和负载波动更大。如果换成 SATA SSD顺序读只有 500MB/s 左右这个结论可能就要反过来了。所以我对 SSD 后端的建议很明确至少上 NVMe最好是数据中心级 NVMe缓存目录单独挂一块盘别让日志、模型权重和它抢 IO。4.3 命中率决定一切说了这么多LMCache 的收益本质上跟业务前缀命中率强相关。知识库问答、Agent 长 System Prompt、多轮对话历史很长、同一批文档反复被问——这些场景命中率高收益大。完全随机、每次都换全新内容的场景命中率上不去CPU 组会退化成多搬一次数据SSD 组可能更糟。所以部署之前我建议先用真实流量统计一下前缀复用情况。最简单的办法是把线上请求的 prompt 前 8K token 存下来做去重看看重复比例是多少。如果重复率不到 50%别硬上 SSD先上 CPU 内存缓存就够了如果高于 70%再考虑要不要把冷数据放到 NVMe 盘上。4.4 版本与多副本注意点local 后端是进程内的如果你用多副本部署在同一台机器上每个进程都会维护自己的一份 CPU 缓存内存消耗会成倍增加。这种情况更适合用 Redis 后端做共享缓存但会引入序列化和网络传输的开销KV 块大的时候每个请求额外增加几十毫秒也是有的。多卡 TP 场景我没有深入测。LMCache 对 TP 的支持要看具体版本建议先单卡跑通再上 TP/PP否则缓存块的分片对不上会导致大量 miss。这个坑如果你遇到了优先怀疑版本其次才是配置。5. 常见问题与排查技巧实录5.1 问题速查表把我在折腾过程中遇到的高频问题整理成一张表方便你们直接对号入座现象大概率原因处理方式启动后完全没有 LMCache 相关日志版本不兼容或者没传递lmcache_config/ 没加--use-lmcache确认 vLLM 与 lmcache 版本匹配用 Python 入口传字典方式验证每次请求都是 cache miss前缀 token 不完全相同或者缓存 key 计算方式变化固定 system prompt 和相同头部 token开 DEBUG 日志看缓存 key第二次请求 TTFT 没有下降vLLM 自带 prefix caching 和 LMCache 相互干扰或命中路径不一样分别关闭一个机制做对照确认到底是谁在生效服务进程 OOMmax_cpu_mem设置过大叠加多副本内存翻倍降到物理内存的一半左右用htop观察实际占用SSD 后端 P99 飙升用了 SATA SSD / 机械盘或者缓存目录和日志混盘换 NVMe缓存盘独立挂载用iostat -x 1看 IO 队列API server 报 unsupported argument当前 vLLM 版本没暴露 LMCache 参数改用LLM(lmcache_config...)Python 入口或者查当前 README使用 Redis 后端后大量 missRedis 没连通或者 key 跨版本不兼容先本地 Redis 单实例最小验证再查 host/port/password升级 lmcache 后缓存命中异常旧缓存文件格式不兼容删掉local_disk_path目录重建别在旧缓存上做兼容性分析5.2 几个亲测有效的避坑习惯第一个习惯先关掉 vLLM 自带的 prefix caching 做基线对照。很多人在两种缓存机制同时开启的时候测出了加速就以为是 LMCache 的功劳其实可能是 vLLM 自带前缀缓存带来的。做性能测试必须有干净的对照组不然上线后某个机制升级导致的回退你都很难定位。第二个习惯缓存目录不是越满越好。LMCache 的淘汰策略会管理磁盘用量但你在文件系统层面可以做点小事挂载时用noatime选项减少不必要的元数据写入定期检查缓存目录是否被权限问题卡住写入如果是多块 NVMe 组 RAID确认条带大小对顺序读友好。这些都是常规操作但对长期稳定性有帮助。第三个习惯CPU 内存和磁盘的配比要按流量算不要拍脑袋。一个 16K 前缀 0.9GB100 个不同前缀就是 90GB。如果物理内存只有 128GBmax_cpu_mem设 96GB 就比较极限了这时应该让前 30% 的热前缀留在内存剩下的走 NVMe 盘。线上跑一段时间后看命中率和 P99 再微调这几个值比一开始就追求完美配置靠谱得多。最后一个操作习惯上线前先用numactl把进程绑到固定 NUMA 节点。命令大概是numactl --cpunodebind0 --membind0 python your_vllm_entry.py跨 NUMA 访问内存的延迟在 KV cache 这种大块搬运场景里会被放大绑定后能明显降低 P99 抖动。这个问题很多教程不提但我实测下来对 CPU 后端的稳定性影响还挺大。最后分享一个我踩了几次坑之后的固定操作顺序先用 Python 入口把lmcache_config传进去跑一次最小验证——发两条相同前缀的请求看第二条 TTFT 是否下降——确认版本和接口都没问题再切到 API server上线前用真实流量统计一遍前缀命中率命中率不到 50% 就别硬上 SSDCPU 内存足够了。LMCache 是个很实用的组件但它不是黑科技收益完全取决于你业务的复用率。把缓存层级、版本兼容、盘位这几个基础问题处理好长上下文服务的稳定性立刻就能上一个台阶。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →