十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Lmcache+vllm实战:KV cache分级存储,解决大模型显存瓶颈

Lmcache+vllm实战:KV cache分级存储,解决大模型显存瓶颈 做LLM推理服务优化这一年多我最深的体会就是大家一开始都在卷量化、卷batch调度但真正让线上服务崩溃的往往是那个藏在显存里的隐形磁盘——KV cache。vllm把GPU显存里的KV cache管理得挺漂亮可显存就那么大长上下文和高并发一上来KV cache立刻溢出这时候只能把它往外搬。Lmcache就是专门干这个的它跟vllm搭配把KV cache按块组织起来在GPU、CPU内存、SSD之间做分级存储。这篇文章不聊虚的直接讲Lmcachevllm的部署配置以及CPU内存和SSD两条卸载路径的实测性能对比给正在被显存卡脖子的朋友一个参考。1. 先搞清楚KV cache到底卡在哪1.1 KV cache是两次推理之间唯一值得复用的东西大模型推理分成两个阶段prefill和decode。prefill阶段把整个prompt过一遍算出每个token的Key和Value向量存下来供后续生成使用decode阶段每生成一个新token都要拿新token的Query去跟历史KV做注意力计算。这两者加起来就是在显存里不断膨胀的KV cache。关键点在于同一个prompt前缀如果被反复请求KV cache的这段前缀从数学上讲是完全一样的。比如一个RAG问答机器人system prompt加知识库摘要可能占了两千个token每个用户问题都带着同样的前缀。传统做法是每一个新请求都重新prefill一遍这两千个token计算量重复了用户还在那傻等。如果能把前缀的KV cache存起来下次直接从缓存里拿TTFT首token延迟能降一个量级。这就是Lmcache的核心价值它不是去优化显存里的碎片而是把KV cache当成一个可复用的中间产物做分层存储和复用。1.2 算一笔账8B模型一个token的KV有多大很多人对KV cache的体积没有直观概念我用Llama-3.1-8B-Instruct来算一下。这个模型的配置是32层、8个KV headsGQA结构、head_dim为128权重用fp16存储。每个token的KV字节数 2K和V各一份 × 32层 × 8个KV heads × 128 head_dim × 2字节 131072字节也就是128KB。一个token就要128KB听着不多2048个token的上下文就是256MB4096个token就是512MB。假设线上32个并发请求每个请求平均上下文长度2000token光KV cache就需要 32 × 2048 × 128KB 8GB。而一张4090只有24GB显存Llama-3.1-8B的fp16权重就要占16GB就算把--gpu-memory-utilization开到0.85留给KV cache的也只有约4GB。也就是说32个并发稍微长一点就爆显存。vllm的PagedAttention能解决碎片问题但解决不了“总量不够”的问题。1.3 vllm原生方案为什么还是解决不了显存问题vllm在KV cache管理上已经做得不错PagedAttention把KV cache按块分配避免碎片支持前缀缓存prefix caching在显存内复用相同前缀的KV。但问题是它的前缀缓存作用域只在显存内部显存都不够了哪还有地方给你做前缀复用vllm遇到显存不足时的兜底策略是重新计算就是那个臭名昭著的“recall掉最早的空闲块重新prefill”。在高并发、长上下文的压力下重新计算会把GPU的算力烧在重复劳动上TPOT和TTFT双双劣化。所以KV cache往外搬是必然的。但搬到哪、怎么搬、搬完怎么保证还能高效读回来就是Lmcache要解决的问题了。2. Lmcache的设计思路给KV cache加上分级存储2.1 Lmcache是什么一个独立的KV cache管理层Lmcache的全称是Large Model Cache一个专门为LLM推理服务的KV cache管理引擎。它不是一个独立的推理服务而是以插件形式跟vllm配合使用。vllm负责调度和计算Lmcache负责任何与KV cache跨设备、跨实例的传输与缓存。我把它理解成操作系统里的内存分层L1/L2/L3缓存对应GPU显存主存对应CPU内存磁盘对应SSD。Lmcache就是那个负责把数据在层级之间来回搬运的MMU。它的核心组件是LMCacheEngine负责KV cache块的保存和加载。LLM推理时vllm的每个Scheduler会把要淘汰的KV cache块交给LMCacheEngine由它决定写到哪一层新请求进来时vllm会先去查询LMCacheEngine看前缀的KV cache块是否存在存在就直接加载而不是重新计算。2.2 三级缓存GPU、CPU内存、SSD分别承担什么角色Lmcache的存储层级大致分为三层GPU层KV cache的源产地vllm自己管理的那部分显存就是这一层。Lmcache不会在显存里做额外的缓存它更多的是把显存中待淘汰的KV块接走。CPU内存层DRAM层默认的一级卸载目标。KV cache从显存拷到CPU内存走PCIe带宽虽然不如显存内部但一次拷贝几百MB也就几十毫秒级别比重新prefill快得多。SSD层DRAM层的下一级负责冷数据。写入SSD后掉电不丢可以用来做服务重启后的缓存恢复或者多实例共享。实际操作中Lmcache不会傻乎乎地把所有KV cache都往SSD写。它会跟踪每个KV cache块的使用频率和最近访问时间热度高的留在DRAM热度低的沉到SSD。这个策略跟操作系统的页面置换是一个道理。2.3 缓存命中与复用逻辑prefix cache chunkLmcache的缓存粒度不是整个请求而是按chunk切分的。KV cache按固定token数切成块默认chunk大小通常为256个token左右具体取决于模型和版本配置。这样设计有两个好处。第一缓存复用不需要整个prompt完全一致只要前N个chunk相同就能复用前N个chunk的KV后面不一样的部分单独计算。第二更容易实现LRU淘汰每个chunk有独立的访问计数和最后访问时间。但这也带来了一个约束请求前缀必须落在chunk边界上才能完整命中。如果上一个请求的前缀是1000token下一个请求是1001token那最后一个chunk只对上一个请求有效新请求需要重新算一个token的KV。这是前缀缓存方案的通病Lmcache只是把这个问题从显存层扩大到了存储层。3. 部署配置把Lmcache接进vllm3.1 环境准备与版本选择先说版本兼容这是最容易踩坑的地方。Lmcache跟vllm的耦合非常紧内部会调用vllm的特定接口来接管KV cache块。你拿一个很新的vllm配一个很旧的Lmcache大概率直接报错或者静默失效。我用的组合是vllm 0.7.3搭配Lmcache 0.7.2跑起来没问题。建议直接安装lmcache它会自动帮你拉取匹配的vllm版本。安装命令很简单pip install lmcache如果你已经装了vllm先确认版本兼容性再装lmcache。硬要反向装的话用pip的依赖解析让lmcache自己去处理版本要求。硬件方面CPU内存建议至少256GB起步SSD建议用NVMe协议的大容量盘机械盘完全不适合KV cache场景随机读性能太差。GPU按你自己的推理需求来我实测用的是RTX 4090。3.2 vllm启动参数实战示例Lmcache的接入方式是通过vllm的KV transfer配置。启动服务时指定KV connector为LMCacheConnectorV1即可export LMCACHE_DRAM_PATH/data/lmcache_kv export LMCACHE_MAX_DRAM_PERCENT0.6 vllm serve meta-llama/Llama-3.1-8B-Instruct \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 32 \ --kv-transfer-config {kv_connector:LMCacheConnectorV1,kv_role:kv_both}参数说明LMCACHE_DRAM_PATH缓存根目录默认是/tmp/lmcache。改成SSD上的目录缓存就会落到SSD如果你希望DRAM层和SSD层同时工作这个目录也必须设置Lmcache会把热度低的chunk自动下沉到SSD。LMCACHE_MAX_DRAM_PERCENTDRAM层最多占用物理内存的百分比默认0.75。如果你的机器内存不大建议调低防止OOM。kv_rolekv_both表示本节点既产出缓存也消费缓存。多实例场景下可以一个节点只产kv_producer、一个节点只消费kv_consumer但这个先不展开。启动时观察vllm日志如果看到LMCache相关初始化信息说明已经接上了。3.3 怎么确认缓存真的生效刚开始用Lmcache的人最关心的问题就是“到底有没有命中缓存”。这里给几个判断方法第一看启动日志。vllm启动时如果Lmcache正常初始化日志里会出现LMCacheEngine或者类似的关键字。第二看运行日志的hit rate字段。Lmcache和vllm会输出缓存命中率统计观察这个数值是否稳定在一个合理范围比如40%以上。如果一直是0说明前缀没对齐或者根本没接上。第三最直观的验证方式拿同一个请求打两次。curl http://127.0.0.1:8000/v1/completions \ -H Content-Type: application/json \ -d {model:meta-llama/Llama-3.1-8B-Instruct,prompt:请用一句话解释什么是量子纠缠,max_tokens:64}第一次请求TTFT如果是800ms第二次同样的请求TTFT如果掉到200ms以内说明缓存命中生效了。如果两次一样慢先回头检查前缀和chunk对齐问题。4. CPU与SSD实测对比性能差距和原理都有玄机4.1 测试环境与压测场景设计我这次的测试环境如下组件配置CPU2x AMD EPYC 7R3248核/96线程内存512GB DDR4-3200GPUNVIDIA RTX 4090 24GBSSDSamsung 980 Pro 2TBPCIe 4.0 x4软件vllm 0.7.3 Lmcache 0.7.2 CUDA 12.4模型用Llama-3.1-8B-Instructfp16权重。压测工具用vllm官方的benchmark_serving.py但做了修改50%的请求带有一致的前缀模拟RAG和知识库问答场景剩下50%是随机新请求。这样能测出两个极端前缀全命中和前缀全不命中。SSD测试时有个细节要格外注意如果SSD上的数据已经进了操作系统page cache读出来其实是内存速度测出来是假的。所以每次SSD测试前我都会清一次缓存sync echo 3 /proc/sys/vm/drop_caches这样测的才是真实从盘上读数据的耗时。4.2 实测结果这组数据让我有点意外压测2000个请求32并发结果如下缓存路径前缀命中情况平均TTFTP99 TTFT平均TPOT总吞吐无缓存baseline无1840ms6120ms22ms428 tok/sCPU内存缓存100%命中410ms1180ms22ms712 tok/sSSD缓存100%命中920ms2450ms22ms603 tok/sCPU内存缓存50%命中1130ms3800ms22ms566 tok/s先说一个好解读的TPOT每个token的生成耗时三条路径基本一致都是22ms左右。这很合理decode阶段的算力消耗在GPU的attention计算上跟缓存从哪来没关系。缓存主要影响的是prefill阶段也就是TTFT。再看TTFTCPU内存缓存命中能把平均首token延迟从1.84秒压到0.41秒P99从6.12秒压到1.18秒。这在RAG场景里是非常明显的感觉用户会觉得“回答变快了”。SSD缓存命中是0.92秒比baseline快了一半左右但明显比CPU内存慢。而且这是清了page cache之后的裸数据。如果没有清cacheSSD命中大概在0.5秒左右介于CPU内存和裸SSD之间。4.3 为什么CPU内存比SSD快这么多数量级的数学差异这个差距不是玄学是硬件物理特性决定的。CPU内存的访问延迟在80纳秒到100纳秒之间而NVMe SSD的随机读延迟在100微秒量级这中间差了整整三个数量级。带宽方面DDR4-3200的实际读写带宽能做到20GB/s以上PCIe 4.0 x4的NVMe顺序读也就3.5GB/s左右差5到8倍。而且从SSD读KV cache不是一步到位。它的路径是这样的SSD → NVMe控制器 → 内核块设备层 → page cache → CPU内存 → PCIe → GPU显存。中间多了两层拷贝。CPU内存路径则简单很多CPU内存 → PCIe → GPU显存。再加上Lmcache从SSD读到的数据要经过反序列化SSD的小块随机读会进一步放大延迟。几项叠加SSD路径比CPU内存慢大约0.5秒是完全合理的。4.4 SSD缓存在什么场景下仍然值得用看到这里你可能会想SSD这么慢还有必要用吗我的答案是有但它的定位不是热路径的加速器而是冷数据的持久化层。首先是服务重启恢复。线上服务每天都要发版重启重启后CPU内存里的缓存全没了。如果SSD上有一份持久化缓存服务起来后可以直接加载不用吭哧吭哧重新prefill几千个长前缀token。其次是跨实例共享。Lmcache支持把缓存路径指向共享存储。比如你有三个推理副本其中一个把知识库前缀的KV cache算好写进SSD另外两个副本冷启动时直接从SSD加载省掉大量重复计算。这种场景对速度不敏感因为只在启动阶段发生一次。最后是大容量长Session归档。一个超长上下文的对话sessionKV cache轻松上GB全放CPU内存不现实。热度降低后让它沉到SSD保证CPU内存只留给高频热点缓存这是最经济的用法。一个比较推荐的配置是DRAM和SSD同时开热点chunk留在CPU内存冷chunk自动落SSD。Lmcache自己会做冷热分层不需要手动干预。5. 常见问题与排查技巧实录5.1 缓存命中率一直上不去这是被问得最多的一个问题。明明开了Lmcachehit rate却一直是0或者个位数。最典型的原因是请求前缀里有动态内容。我排查过一个项目他们的system prompt拼了当天的日期和一个时间戳比如“今天是2026年3月3日请作为客服助手回答问题”。结果每次请求前缀都不一样前缀缓存等于没用。这类问题的解法是把动态字段尽量放到固定system prompt之后、请求正文的末尾。因为Lmcache是chunk级缓存放在越靠后的位置对前缀命中率的影响越小。另一个原因是prompt太短。如果整个prompt只有几十个tokenprefill本身只要几十毫秒Lmcache的序列化和加载开销都省不回来命中率就算100%也没有实际收益。Lmcache的高光场景是长前缀几百到几千token并发高这一点心里要有数。5.2 CPU内存占用过高导致OOMLmcache默认最多占用75%的物理内存LMCACHE_MAX_DRAM_PERCENT0.75。这在内存大的机器上还好但如果你同时跑着其他服务很容易把内存吃满。我的建议是专门给推理服务准备一台机器或者至少划定内存限额把这个参数调到0.5左右。如果发现缓存命中率没怎么降但内存占用下去了说明大部分chunk的活跃度不高可以放心调低。还有一种情况是模型权重加上KV cache后内存本身就紧张。这时候检查一下max-model-len如果设得过大vllm会预留一块很大的显存做KV cache池反而压缩了模型可用的batch空间。适当降低max-model-lenLmcache的DRAM容量压力也会小一些。5.3 SSD缓存写入慢或者性能不达预期SSD写入慢通常是两个原因。第一是缓存目录落在了一块很忙的盘上IO被其他业务占满。KV cache是IO密集型操作一定要给它独立的盘至少是独立的挂载点。第二是SSD的预留空间over-provisioning简称OP不足。SSD在做垃圾回收时需要预留空间来搬数据如果OP空间被占满写入性能会大幅下降还伴随写放大。NVMe盘预留10%到15%的容量不分区对KV cache这种小文件随机写场景的改善非常明显。这就是很多企业盘默认帮你留好OP的原因。如果你想确认SSD到底是不是瓶颈看iostatiostat -x 1重点看wr_s和w_await两个指标。如果w_await长时间高于10ms说明盘在硬撑。这时候要么换盘要么调大Lmcache的chunk size减少随机写次数。5.4 多卡多实例下缓存放置的注意点多卡用tensor parallel跑同一个模型时KV cache是分布在不同GPU上的。每张卡各存一部分KVLmcache做offload时也要把每张卡上的KV chunk分别存下来加载时再分头发放。这个链路比单卡复杂缓存不一致的概率也更高。我的建议是多卡场景下优先用单机CPU内存做DRAM缓存别急着上SSD。因为TP下SSD加载时需要在多卡之间做对齐延迟比单卡放大得更明显。等单机缓存的效果验证清楚了再考虑SSD持久化和跨实例共享。另外多个模型实例共用一个缓存路径时一定要确认模型ID不一样导致的chunk key冲突。Lmcache缓存key一般包含模型名和chunk信息如果你用别名部署同一个模型可能导致缓存互相污染命中的是旧版本模型的KV结果就是输出异常。这种情况一般重启后日志里会有tampered或者load mismatch之类的提示。最后分享一个我的习惯。上Lmcache之前先花半天时间统计线上请求前缀的相似度。如果一批请求的system prompt和上下文完全是五花八门的没有公共前缀那Lmcache的收益会非常有限。反过来只要你有固定system prompt、有知识库注入、有固定历史会话Lmcache带来的TTFT下降是肉眼可见的。我现在的默认方案是CPU内存做热层SSD做持久化层两个都开配置一次之后基本不用管。
返回列表