LMCache缓存引擎源码解析:KV存取全链路
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
LMCache 是一个面向 LLM 推理的 KV 缓存管理层,负责把推理引擎算出的 KV 缓存(注意力中间结果,长文本推理里最贵的部分)落到可复用的存储里,让后续请求跳过重复的 prefill 计算。本文沿"一条请求的数据流"拆解核心文件 lmcache/v1/cache_engine.py(约 2200 行),讲透分块、键设计、查找、存储与检索的完整链路,回答一个问题:LMCache 的 KV 缓存复用到底是怎么做到的。
缓存引擎实际管理的是哪三块
LMCacheEngine类本身不碰磁盘也不碰 GPU 显存,它更像调度台,真正的活由三个协作者完成(初始化见 lmcache/v1/cache_engine.py L100-L246):
- token_database:把 token 序列转成"(起点, 终点, 缓存键)"三元组列表,负责定址;
- storage_manager:统一管理 CPU 内存、本地磁盘、Redis、P2P 等多级后端(见 lmcache/v1/storage_backend/),管分配、淘汰和引用计数;
- gpu_connector:推理引擎 GPU 上的 KV 与 LMCache 侧 CPU 内存对象(MemoryObj)之间的搬运工。
类比:token_database 算收件地址,storage_manager 管仓库,gpu_connector 负责取送。
这张图展示的是 LMCache 数据面在分离式推理中的位置:P 侧引擎读/写分布式 KV 存储,D 侧引擎从同一存储里取回缓存——而读写两侧走的正是本文要讲的同一条引擎链路。
token 序列是怎么切成可缓存的块
整段序列当一个键的问题是:一个 token 不同就整体失效。LMCache 默认每 256 个 token 切一块(ChunkedTokenDatabase的chunk_size=256,lmcache/v1/token_database.py L298)。更关键的是每块的哈希不是独立算的,而是链式的前缀哈希:
# lmcache/v1/token_database.py:ChunkedTokenDatabase._prefix_hash prefix_hash = self._get_init_hash() # 初始值 NONE_HASH for token_chunk in token_chunks: prefix_hash = self._hash_tokens(token_chunk, prefix_hash) yield prefix_hash # 每块产出一个"链式哈希"这段代码要解决"同一文本出现在不同位置不能互相命中"的问题。为什么这样写:KV 里的注意力结果与位置强相关,若每块哈希只依赖本块 token,不同请求里相同文本段会撞上同一个键,直接复用就是错的。链式哈希把"序列开头到本块的完整前缀"编码进一个数,天然保证只有前缀完全相同的块才可能命中。块键之外还拼上model_name@world_size@worker_id@chunk_hash@dtype(CacheEngineKey,lmcache/utils.py L389),把不同模型、不同并行拓扑的缓存隔离开。
请求还没调度时:lookup 如何算出可复用的前缀长度
vLLM 调度器在入队前会调engine.lookup(tokens)(lmcache/v1/cache_engine.py L1130)问一句:"我的前缀有多少 token 在缓存里?"引擎先用process_tokens分块拿到键序列,再调storage_manager.batched_contains批量查存在性。核心在返回值逻辑——只认连续命中:
# lmcache/v1/cache_engine.py L1236-1240(lookup 非 layerwise 分支) for idx, (start, end, key) in enumerate(chunk_info_list): if idx < hit_chunks: # 前 hit_chunks 块连续命中 res = end continue return res # 遇到断点:只返回断点前的前缀长度它要解决的是"8 块命中 5 块,能用几块"。为什么只返回一个整数:缓存只能按"前缀"来跳过 prefill,中间的命中没有意义,给上层一个命中的前缀 token 数就足够它决定跳过多少计算,调度接口因此保持极简。
prefill 算完:KV 从 GPU 进存储的三步
引擎算完 prefill 后回调engine.store(tokens, **kwargs)(同文件 L388),核心就三步:
# lmcache/v1/cache_engine.py L485-569(store 核心) for start, end, key in self.token_database.process_tokens(...): memory_obj = self.storage_manager.allocate(...) # 1. 分配 CPU 内存对象 if memory_obj is None: break # 内存不足:存得下多少存多少 self.gpu_connector.batched_from_gpu(...) # 2. GPU KV 批量拷入 CPU self.storage_manager.batched_put(keys, memory_objs) # 3. 批量异步落到各级后端它要解决"存缓存不能拖慢在线服务"的问题。为什么这样写:分配失败只丢弃尾部、绝不阻塞请求;batched_put是异步提交,store返回时数据可能还没落盘,所以关键路径上只剩一次 GPU→CPU 拷贝。整个 store 被stats_monitor.on_store_request/on_store_finished包住,分段计时"算键、搬 GPU、put"三个阶段(L469-L574),这就是文档里提到的请求级可观测性埋点。
第二次请求来了:缓存里的 KV 怎么回到 GPU
retrieve(tokens)(L780)是 store 的逆过程:同样的 token 序列经链式哈希产生同样的键序列——这是能命中的前提——然后get_block_mapping定位键所在的层,batched_get逐批取出 MemoryObj,最后gpu_connector.batched_to_gpu写回 GPU KV 缓存。返回值是一个 bool 掩码ret_mask:哪些位置已补齐,推理引擎对这些位置跳过 prefill。
两个保证正确性的细节值得注意。一是连续性:_process_tokens_internal里若某块"索引在但取不出来",立即 break,并把该位置之后的掩码统一清回 False(L1762-L1790),绝不向上传递带洞的前缀。二是引用计数:每次batched_get给 MemoryObj 加引用,retrieve 结束后统一ref_count_down/unpin;异步预取(async_lookup_and_prefetch,L1322)取到却没被使用的对象走cleanup_memory_objs归还,防止 CPU 钉住内存池被占满。
这个文件里藏着哪些工程取舍
三个取舍解释了 LMCache 为什么能塞进推理关键路径:
- 逐层流水:
store_layer/retrieve_layer(L593、L974)用生成器按层 yield,第 i 层的 GPU→CPU 拷贝与第 i-1 层的落盘并行,把 PCIe 传输时间藏进计算间隙; - 只认前缀:主链路 lookup 只返回连续前缀长度,任意位置命中交给非前缀复用路线(CacheBlend,见 README 的 Non-prefix KV reuse 条目),主路径语义简单、正确性好证明;
- MLA 只写一份:
save_only_first_rank模式下仅 rank 0 存储,再广播给其他 rank(L856-L871),避免 N 份冗余写入,广播时甚至用 GPU 侧副本替换 CPU 源缓冲,把 leader 的 to_gpu 从 PCIe 受限压回 HBM 速度。
收尾:LMCache 的做法 vs 朴素做法
| 维度 | 朴素做法 | LMCache 的做法 |
|---|---|---|
| 分块粒度 | 整段序列一个键,一 token 差异全失效 | 256 token 一块,前缀可部分复用 |
| 哈希方式 | 每块独立哈希,分不清位置 | 链式前缀哈希,位置编码进键 |
| 内存压力 | OOM 或拒绝服务 | 分配失败丢弃尾部块,存得下多少存多少 |
| 落盘时机 | 同步写完才返回 | batched_put 异步,关键路径只剩 GPU→CPU |
| 命中判定 | 返回命中块集合,上层自己拼 | 返回单一"前缀长度",调度器直接可用 |
| 多层 KV | 整层串行拷贝 | 生成器逐层流水,传输与落盘重叠 |
把这条链路走完就能回答开头的问题:LMCache 的复用不是靠某个精巧的算法,而是"链式哈希定址 + 批量异步搬运 + 严格前缀语义 + 引用计数兜底"四件事的组合。想继续看各后端的实现,入口在 lmcache/v1/storage_backend/,端到端用法可看 examples/kv_cache_reuse/。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考