资讯动态

LMCache实战:复用电KV Cache,让vLLM推理算力节省70%

发布时间:2026/10/14 5:36:41 来源:尧图企业网站定制
先说个现象。我接手维护一套基于 vLLM 的推理服务之后发现一个很扎眼的问题几十张 GPU 的利用率看起来不低但真正“有效产出”的 token 占比很可怜。排查下来罪魁祸首是 KV Cache 的重复计算——同一个长文档、同一段系统提示词在每轮请求里都被重新算一遍 prefill。尤其是 RAG 场景用户反复问同一个文档里的不同问题前面那一大段文档的 KV Cache 其实一模一样却每次都要从头算起。一台 8 卡机器就这么白白烧掉一大半算力。后来我把 LMCache 接进了服务核心收获是把 KV Cache 从“请求私有的临时数据”变成“跨请求、跨实例可复用的缓存资产”前缀命中之后只算增量部分。这篇文章就把整个来龙去脉写清楚——从为什么会有重复计算到 LMCache 的原理、部署方式、实测收益再到我踩过的那些坑。适合正在做 LLM 推理服务优化、被 GPU 算力吃紧困扰的工程师也适合刚接触 KV Cache 概念、想知道这玩意儿到底怎么省钱的读者。1. 先算清这笔账KV Cache 什么时候在白白烧 GPU1.1 prefill 和 decode 的“一口气”与“细嚼慢咽”要理解 KV Cache 为什么会被重复计算先把 LLM 推理的两个阶段掰扯清楚。第一个阶段叫 prefill预填充也就是把用户输入的整段 prompt 一次性塞给模型并行计算每个 token 的注意力同时生成每个 token 对应的 Key 矩阵和 Value 矩阵。这两个矩阵合起来就是 KV Cache。第二个阶段叫 decode解码模型一个一个地生成新 token每生成一个 token就拿这个 token 的 Query 去和历史所有 token 的 KV 做注意力计算。这里的关键在于decode 阶段不再重新计算历史 token 的 K 和 V而是直接用 prefill 阶段存下来的 KV Cache。你可以把 KV Cache 理解为模型对“已经读过的内容”做的读书笔记。prompt 越长这份笔记越大但也越有用——没有它每生成一个新 token 都得把整段历史从零读一遍算力开销根本扛不住。问题来了如果两个请求的 prompt 前缀完全相同呢比如系统提示词一样或者用户提问前都夹带同一段 4K token 的文档那么第一份请求已经算好的那份“读书笔记”第二份请求其实可以原样复用。但大多数推理服务默认不这么干。每个请求进来从 prefill 到 decode都是独立走一遍完整流程。哪怕两份请求的 prompt 有 90% 重合依然各自完整计算。这就是重复计算最直接的来源。1.2 我这边的三类典型烧钱场景我在服务里观察到的重复计算基本可以归成三类。第一类是带长系统提示词的应用。很多产品会把角色设定、知识库说明、工具调用规范全塞进系统提示词动辄几百到几千 token。如果每轮用户请求都带这份固定前缀而服务端没有前缀缓存机制那么每来一个用户整份系统提示词就要重新 prefill 一遍。并发一高这部分算力浪费非常明显。第二类是 RAG 检索增强生成。用户上传一份合同或者论文然后围绕这份文档反复提问。文档本身可能 8K token问题只有几十 token。理想状态下文档的 KV Cache 算一次就够了后续所有问题都只对“问题部分”做增量 prefill。但如果没有缓存每个问题都等于把 8K 文档重新读一遍、重新算一遍。这是我最心疼的场景一张卡上八成算力全耗在重复读文档上。第三类是 Agent 多步工具调用和同 prompt 多参数采样。Agent 在思考过程中会反复调用工具每轮生成的上下文都要拼接历史前缀重复的概率很高。另外做 A/B 测试或生成多个候选结果时同一个 prompt 会以不同采样参数跑多遍KV Cache 理论上也是可以共享的。1.3 为什么主流推理框架没有彻底解决这个问题很多推理框架其实已经意识到这个浪费。比如 vLLM 有 Automatic Prefix CachingSGLang 有 RadixAttention都能在一定程度上复用前缀 KV。这确实能解决单机单实例内部的部分重复计算。但实际部署中服务往往是多副本、多实例的。请求被负载均衡到不同机器上每台机器各自维护自己的前缀缓存缓存命中率天然被切碎。A 机器上刚算好的热门文档 KVB 机器一样没有照样重新算。更别说容器重启、模型滚动更新之后内存里的前缀缓存直接清零。另外单个框架的前缀缓存通常只在显存里做。KV Cache 是很吃存储的东西——一个 7B 模型fp16 精度下每 1K token 大约要占 512MB 的 KV Cache。8K 上下文就是 4GB这还只是单个请求。显存再大也经不起把热门文档的 KV 全留在 GPU 上。所以需要一套更完整的方案把 KV Cache 分层存放按需从显存、内存、磁盘甚至远端加载。这就是 LMCache 切入的位置。2. LMCache 的底牌前缀 KV 命中、多级存储与 vLLM 集成2.1 核心思路把 KV Cache 从“请求私有”变成“请求间可复用”LMCache 做的事情本质上就是给 KV Cache 加了一层外部化、可共享的存储并把这层存储和推理框架的 prefill 流程打通。它不再把 KV Cache 当作某个请求生命周期内的临时数据而是当作一个按“token 前缀”索引的缓存对象。请求来了LMCache 先对 prompt 做前缀切分和哈希去缓存里查有没有现成的 KV 片段命中之后跳过对应的 prefill 计算只对未命中的增量部分做计算。这里面最核心的一个设计是它复用的单位不是“整个 prompt”而是“任意长度的前缀”。也就是说即使两份请求的完整 prompt 不一样只要前半部分相同就能共享这部分 KV。这正是 RAG 场景的刚需——文档前缀相同、提问后缀不同。2.2 多级存储显存、内存、磁盘、远端的分层策略LMCache 的存储是分层的参考了计算机存储体系里“缓存金字塔”的思路。第一层是 GPU 显存。速度最快但容量最小适合存放最热门的 KV 片段。第二层是 CPU 内存。容量大得多几十 GB 到几百 GB 都能规划速度比显存慢一个量级但比重新计算快得多。第三层是本地磁盘SSD 或 NVMe。容量最大可以持久化保存适合存放跨重启、跨批次仍然有用的 KV 缓存。第四层是远端存储或远端 GPU。多台推理服务器可以共享一份 KV 缓存池请求落在任意一台机器上都能命中。这个分层机制让我想到一个特别贴切的类比显存是工位上的桌面内存是办公室的文件柜磁盘是公司仓库远端缓存是另一个园区里的资料中心。取资料的速度逐级变慢但容量逐级变大。聪明的做法是把最常用的放在桌面上不常用的归档到仓库而不是每次都从零手写一份资料。2.3 承接 vLLM 的集成机制LMCache 的另一个设计重点是它不是让开发者重写一套推理引擎而是作为插件/连接器耦合进现有框架。以 vLLM 为例vLLM 本身有一套 KV Connector 接口LMCache 实现了对应的 LMCacheConnector通过 vLLM 启动参数就可以启用不需要改业务代码。实际工作流程大致是这样请求进入 vLLMprefill 开始之前LMCache 把 prompt 切分成 chunk逐个查缓存。命中后直接从缓存读取 KV 放入 GPU 显存跳过这一段的 prefill没有命中的前缀部分正常计算同时在算完之后写回缓存。写回也有讲究。LMCache 不是把整个请求的 KV 一股脑存一份而是按 chunk 粒度存储。这样后续请求只需复用其中某个前缀 chunk就能精准命中不需要强求“完整 prompt 一模一样”。这个 chunk 粒度设计是它比朴素的“整段 KV 缓存”更实用的关键。2.4 缓存 key 的设计与失效逻辑缓存要能命中key 的设计是第一关。LMCache 用 token 序列的前缀哈希作为 key 的核心部分同时把模型配置信息、数据精度、张量并行维度等纳入哈希因子。也就是说只有模型结构完全一致、前缀 token 完全一致时缓存才可能命中。这里有个容易被忽略的细节KV Cache 只在“模型权重、RoPE 版本、注意力实现、dtype、张量并行配置”都没变化的情况下才是有效的。任何一个因素变化KV 的含义都会漂移直接复用会得到错误结果。LMCache 把这类元信息纳入缓存 key 的校验范围一旦发现不匹配就当作 cache miss宁可重新计算也不冒险复用。这套机制也决定了它的一条底线缓存永远不会导致模型输出变错最多是缓存没命中、白占存储。本质上它是一个“安全加速器”——收益来自命中风险最多是浪费空间而不是引入错误。3. 实操把 LMCache 接到推理服务里并确认命中率3.1 环境准备版本对齐是第一道门槛我当时的部署环境是若干台 GPU 服务器每台 8 卡统一用 vLLM 作为推理引擎。接 LMCache 之前我先把版本对齐这件事做扎实因为这类外部缓存插件对版本耦合非常敏感。首先是 Python 环境。LMCache 通过 pip 安装即可但它依赖的 vLLM 版本、PyTorch 版本都要对得上。我踩过的一个教训是如果 vLLM 是源码编译的私有版本而 LMCache 是按官方 wheel 版本跑的两者之间的 KV Connector 接口可能对不上这时候直接报错算好的更怕的是启动不报错、运行一段时间后行为异常。我最后采用的稳妥方案是在一台测试机上用官方预编译 wheel 搭一套最小环境把 vLLM 和 LMCache 装在同一套 Python 虚拟环境里先跑通再推广到集群。这个“先小后大”的原则在接任何底层缓存插件时都值得坚持。3.2 启动参数与配置文件LMCache 通过一个 YAML 配置文件控制缓存后端的行为再用 vLLM 的--kv-transfer-config参数把 LMCacheConnector 挂载上去。我当时的配置大概是这样的pip install lmcache然后在启动脚本里给 vLLM 加上 KV transfer 相关参数。vllm serve /path/to/model \ --kv-transfer-config {kv_connector: LMCacheConnector, kv_role: kv_both} \ --max-model-len 16384 \ --gpu-memory-utilization 0.85同时准备一份 LMCache 的配置文件lmcache.yaml放在服务的工作目录下# 基础配置 cache_storage: local_path: /mnt/kvcache # 本地磁盘路径 type: local # chunk 大小控制 KV 缓存的最小粒度 chunk_size: 512 # 本地内存/磁盘缓存预算 local_cpu_memory_limit: 64GB local_disk_limit: 512GB # 是否启用远程缓存同一集群内多机共享 remote: enabled: false这里有几个关键参数值得解释。chunk_size决定 KV 缓存的最小切分单元和 token 数量相关我选 512 是因为在这个粒度下缓存命中的概率和缓存管理的开销达到了比较均衡的状态。local_cpu_memory_limit控制 KV Cache 最大能占用多少 CPU 内存这个必须根据机器物理内存和模型并发度统筹考虑不然缓存占多了模型张量反而被挤到磁盘交换。启动之后注意观察服务的日志。LMCache 会在日志里输出每条请求的缓存命中情况包括命中哪个 chunk、跳过多少 prefill token。如果日志里全是 miss那就是配置有问题或者是 key 对不上后面会专门说。3.3 验证命中不只是看日志还要看 TTFT只看日志里说“cache hit”还不够我建议同时关注首 token 延迟TTFT。TTFT 是衡量 prefill 阶段性能最直观的指标它直接反映了一个请求从进入服务到吐出第一个 token 花了多久。我当时的验证流程是先单独发一个带长文档的请求把文档的 KV Cache 写入 LMCache然后再发一个相同文档、不同问题的请求观察 TTFT 变化。没有缓存时第二个请求的 TTFT 基本等同于完整 prefill 的时间8K token 的 prompt 在 7B 模型上可能需要 1.5 到 3 秒。开启 LMCache 并命中后第二个请求只需要对新问题那几十个 token 做 prefillTTFT 直接降到 200 毫秒级别。这个对比足够直观也方便向团队展示“缓存到底给你省了多少”。如果你想把验证做得更严谨还可以写个脚本统计 N 次请求中的缓存命中率。我建议指标定义成“跳过的 prefill token 数 / 总 prefill token 数”。这个比例比“请求级命中率”更能反映真实收益因为一个请求可能命中 90% 的 token但它在请求级别上依然算 hit。3.4 多级存储配置的推进次序我建议不要一上来就把 CPU 内存、本地磁盘、远端缓存全打开。缓存这东西是增量生效的层次越多排查问题越难。我的推进次序是先只开 CPU 内存缓存验证基本命中逻辑然后开本地磁盘缓存验证重启后缓存是否还在最后再开远端共享处理跨机一致性问题。每开一层都回归验证一次输出正确性和 TTFT确认没有副作用再继续。4. 实测收益哪些场景省了 70% 算力哪些场景白折腾4.1 我做的对照测试为了说清楚收益边界我在测试环境里做了三组对照模型统一用 7B 参数规模的 checkpointfp16 精度最大上下文 16K。第一组是 RAG 固定文档场景。8K token 的文档作为固定前缀后缀是 10 个不同的问题。第二组是带 2K token 系统提示词的普通问答每次用户问题不同。第三组是短 prompt 的多轮闲聊平均每轮长度为 200 token。每组分别测三种状态关闭 LMCache、只开显存/内存缓存、开启完整多级缓存。记录的核心指标有两个平均 TTFT 和单位时间内能够处理的请求数。4.2 数据结果RAG 固定文档场景的收益最夸张。关闭缓存时每个请求 TTFT 大概是 2.2 秒开启缓存后命中 8K token 中约 7.9K 的 prefillTTFT 降到 0.35 秒。prefill 的计算量下降了接近 96%对应到整机吞吐同等 GPU 资源下支持的并发请求数提升了大约 3.5 倍。带 2K 系统提示词的场景收益中等。命中 2K 前缀 token 后TTFT 从 0.6 秒降到 0.25 秒左右。体感明显但远没有 RAG 场景那么惊艳。短 prompt 多轮闲聊场景基本没什么收益prompt 本身很短prefill 在整体耗时中占比本来就小缓存省下来的算力有限反而增加了缓存管理的开销TTFT 甚至轻微劣化了。这个结果很有代表性LMCache 的收益与“重复前缀的长度”强相关。前缀越长、复用次数越高收益越明显短 prompt、随机性强的场景收益有限甚至为负。我还顺手测了一个同 prompt 多温度采样的场景。同一个 RAG 问题用 temperature0.7 生成 5 个候选答案。开启缓存后第 2 到第 5 次请求完全跳过文档前缀的 prefill整体生成时间缩短约 45%。这个场景在内容生成、评测调参时特别实用。4.3 命中率曲线和缓存淘汰的权衡LMCache 的缓存淘汰和传统缓存类似基于访问频率和新鲜度。热门文档的 KV Cache 会一直在 CPU 内存里待着冷门文档则被逐渐清理。但要特别留意KV Cache 的存储开销比普通文本缓存大得多。4K token 的 KV Cache 在 7B 模型下大约 2GB这意味着 64GB 内存预算只能存放约 32 个 4K 前缀。如果业务里热门文档数量很多、单个文档很长缓存空间会很快耗尽旧缓存被频繁淘汰命中率一路下滑。我当时的解决办法是给热门文档单独设置一个“高优先级缓存池”确保这些文档的 KV 永远不被淘汰。你可以把 LMCache 的淘汰策略理解成 LRU 的变体但对于强业务时效性的内容手动 pin 住某些前缀是必要的。4.4 GPU 利用率的“虚高”与“实降”有一个指标我提醒大家小心单纯看 GPU 利用率开启 LMCache 之后反而可能下降。这不是缓存失效而是原来那些重复计算占用的算力被省掉了。GPU 仍然在工作但同样的产出消耗的算力更少。如果只盯着“GPU 必须打满”就会得出错误的结论。正确的视角是“单位 token 产出消耗的 GPU 算力”或者“每 GPU 每秒有效输出 token 数”。在我那个 RAG 场景里单 GPU 每秒有效输出 token 数翻了两倍多这才是缓存带来的真实价值。5. 踩坑记录这些坑我基本都趟了一遍5.1 模型一换缓存全静默失效我犯过的第一个低级错误是在同一台机器上先用一份旧参数的 checkpoint 跑了一段时间生成了大量 KV 缓存后来更新模型权重忘了清缓存。启动没报错但请求开始变慢。排查下来发现LMCache 的 key 包含了模型配置信息模型权重变化后旧缓存理论上应该自动失效。但我在更新模型时没有把旧的缓存存储目录清空那批缓存变成了“永远无法命中的僵尸数据”白白占着磁盘空间每次查询还要浪费时间扫描。这就是缓存系统最隐蔽的损耗不是只有命中才有成本查询本身就是成本。当缓存数量极大而命中率极低时每次请求都要做大量无效查找。建议模型每次更新后主动清理或切换缓存存储目录。5.2 磁盘命中慢到不如重新算我最初把本地磁盘缓存看得很重觉得“反正显存不够磁盘总够大”。结果实测发现NVMe SSD 上的 KV Cache 读取速度虽然不慢但在高并发场景下大量磁盘 I/O 会和模型权重读取争抢带宽反而拖垮整体性能。更尴尬的是如果业务是低延迟在线推理磁盘读 KV 的时间可能和直接重新 prefill 差不了多少。缓存省下了 GPU 算力却把压力转移到了磁盘和 CPU延迟收益直接被吞掉。我的建议是在线服务优先保证显存和内存缓存磁盘缓存主要用于“跨重启持久化”和“非高峰期预加载”而不是作为高并发在线请求的主要命中源。如果你确实需要磁盘缓存支撑高并发至少要保证 NVMe SSD 的随机读性能并且严格控制缓存文件碎片。5.3 多机共享命中了远端缓存却被网络延迟坑了开启多机共享缓存后我一开始很兴奋觉得每台机器都能享受全集群的命中收益。但实际测下来远端缓存命中的 TTFT 并不总是比本地重新计算快。原因在于 KV Cache 的数据量很大跨机传输 8K token 的 KV Cache约 4GB要经过网络100Gbps 网卡也要零点几秒。如果任务本身对延迟极度敏感从远端拉缓存不如直接在本地截断重算。我不建议把多机共享当作默认开启项。只有当集群内有明显的热门前缀、且本地命中率已经不够高时才值得用网络延迟去换算力节省。另外如果开启尽量保证缓存节点距离计算节点足够近数据尽量走持久化内存或 RDMA 网络而不是走普通的 TCP 链路上。5.4 并发写同一个缓存 key重复写放大还遇到过一个有意思的问题高并发下多个请求同时计算并尝试写同一个 chunk 的 KV Cache导致同一个 key 被重复写入多遍存储空间和带宽被白白浪费。LMCache 有缓存一致性处理但我观察到极端高并发下仍然存在写放大的现象。我当时的处理方法是对于超高热门的系统提示词前缀在服务预热阶段用探针请求主动把 KV Cache 生成好让后续请求全部只读命中。相当于把“并发同时写”变成“预先写、随时候读”。5.5 不要忽略动态 batch 和抢占的交互推理服务经常为了吞吐把多个请求拼成一个 batch 跑。开启 LMCache 之后带缓存命中的请求和不带缓存的请求会被拼在一起动态 batch 的调度策略会因此发生变化。它可能把本来被移除的请求继续留在 GPU 上等待也可能因为 KV 拼接的额外开销导致 batch 切换变慢。这不是 LMCache 本身的 bug而是优化组件叠加后产生的联动效应。建议在开启 LMCache 的早期阶段对服务做一次完整的压测确认吞吐和延迟曲线没有异常再逐步放量。6. 如果我重新搭一次会怎么做经过这些实践我梳理出一套比较顺手的接入路径。先把业务场景按“重复前缀长度”和“复用频率”排队最值得接的是 RAG 固定文档、长系统提示词、Agent 多步调用这一类。第二件事小流量验证只用 CPU 内存缓存盯住 TTFT 和命中率两个指标。第三件事再开磁盘持久化应对容器重启和模型滚动更新。最后才是多机共享而且只在上层业务证明热门前缀确实有跨机复用需求时再做。凡是涉及缓存的系统我都会反复问自己一个问题如果命中率掉到 30%这套系统还能不能稳LMCache 给的是锦上添花的优化而不是雪中送炭的正确性保障。业务逻辑不能依赖缓存的命中推理结果的正误和缓存无关这一点一定要在架构设计里想清楚。最后分享一个我自己养成的小习惯每次修改模型配置、升级推理框架、调整 chunk_size 之后都会主动触发一条“探针请求”把新的 KV Cache 热起来同时观察日志里的 hit/miss 比例变化。这样既能快速暴露配置漂移问题也能让服务从冷启动快速回到高性能状态。LMCache 这种工具用好了是真的能给 GPU 服务器“续命”但前提是你得先了解它会在哪些场景里偷懒、又在哪些场景里失灵。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑