ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入拆解 LMCache 的 KV 缓存引擎:一条请求的完整旅程

2026/9/14 4:10:39 拓冰建站 浏览量
深入拆解 LMCache 的 KV 缓存引擎:一条请求的完整旅程 深入拆解 LMCache 的 KV 缓存引擎一条请求的完整旅程【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache2 万 token 的长文档问答里每一轮新问题都拖着同一段冗长前缀——如果共享部分的 KV 缓存不复用推理引擎就得把这部分 token 的注意力从头再算一遍。这正是 LLM 推理加速中最大的浪费来源之一也是 LMCache 要解决的问题。这篇文章跟着一条请求从 token 序列走到缓存复用依据 lmcache/v1/cache_engine.py 的源码把写入路径、读取路径与命中统计依次拆开。读完后你能清楚回答三个问题缓存键是怎么生成的、前缀匹配如何找到连续命中边界、GPU 上的 KV 数据如何交给 CPU 与磁盘。 心智模型一条 token 序列要过三道工序先摆出LMCacheEngine内部三个组件的分工后面的代码会好懂很多TokenDatabase切块工序把 token 序列切成固定大小的块默认chunk_size256为每块算出一个缓存键。实现见 lmcache/v1/token_database.pyGPUConnector搬运工序把推理引擎分页 KV 缓存里的张量从 GPU 拷进 CPU 侧的内存对象它懂 vLLM 的分页布局与 slot 映射StorageManager落盘工序管理本地 CPU 内存、本地磁盘、远端后端等分层存储负责分配、淘汰与异步读写类注释说得很直白存的时候把 GPU KV 转成 CPU 上的MemoryObj再异步写入 StorageBackend取的时候反过来。引擎的创建也有讲究——LMCacheEngineBuilder.get_or_create用类级字典做实例注册同一实例 ID 若配置或元数据不一致会直接报错。单进程内复用同一个引擎靠的就是这道校验。 缓存键设计链式哈希把前缀匹配变成逐块判等缓存键的核心是 lmcache/utils.py 里的CacheEngineKey数据类它把环境身份和内容的哈希值捆在了一起# 键的字符串形态模型名世界大小worker序号块哈希数据类型 s ( f{self.model_name}{self.world_size} f{self.worker_id}{self.chunk_hash_hex}{self._dtype_str} )为什么塞进这些字段因为同一段 token模型不同、张量并行 rank 不同、KV 的 dtype 不同算出的 KV 内容就不同键必须跟着不同。这样不同部署形态共用一套键空间也不会互相污染。更关键的是chunk_hash的生成方式看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第 N 块的哈希 hash(第 N-1 块的哈希 本块 token)。像接力传棒链条里包含了截止到该块的全部前缀信息。好处是前 2000 个 token 是否命中退化成了第 2000 个 token 所在块的哈希是否相等——逐块判等即可不必比对原始序列。还有一个容易忽略的约定是mask引擎要求 mask 里的 False 必须全部位于前缀即 FFFFFTTTTTT 形态。意思是开头那些 token 已经被 vLLM 算过了不用重新哈希只需处理新增的尾部。这一条让每次增量 prefill 都省掉了全量重哈希的开销。在 KV 复用blending场景下SegmentTokenDatabase是另一套实现按特殊分隔符把序列切成段每段独立哈希、不带前缀链。同一段文档内容无论出现在哪个请求里都得到相同的键——长上下文缓存复用从此不依赖位置这正是同一份长文档反复被引用这类负载能提速的关键。⚡ 写入路径store 把 GPU 上的 KV 分三步交给存储层store方法的主体是一条三段式流水线每一段都被独立计时for start, end, key in self.token_database.process_tokens(...): memory_obj self.storage_manager.allocate(...) # 先预留 CPU 内存 if memory_obj is None: break # 内存吃紧存下已有的就停 ... self.gpu_connector.batched_from_gpu(memory_objs, starts, ends) # GPU→CPU self.storage_manager.batched_put(keys, memory_objs) # 异步写入后端三个要点值得咀嚼先预留内存再搬数据。allocate排在batched_from_gpu之前一旦拿不到内存对象就break只保存已经备好的那些块而不是让请求失败——缓存是加速器不是主路径这种尽力而为的回退语义非常重要GPU→CPU 与 CPU→后端解耦。batched_put是异步的写入路径不阻塞推理主循环逐层流水线。store_layer用生成器把拷第 i 层和写第 i-1 层交错执行让数据搬运与落盘 I/O 重叠同时它先用contains查一把已存在的块直接跳过避免重复写入注意普通store里并没有存在则跳过的去重——非逐层写入是纯增量的幂等性交给键本身保证同一块写两次只是覆盖到同一个键上不会错乱。 读取路径lookup 先划边界retrieve 再批量交付读取分两步走一步回答有多少可用一步交付这些可用。lookup的返回值很直白——缓存里存在的连续前缀token 数for idx, (start, end, key) in enumerate(chunk_info_list): if idx hit_chunks: res end # 沿前缀推进命中边界 continue return res # 遇到第一个断点立即收手batched_contains返回的是头部连续命中了几块引擎只沿前缀推边界。第一个缺口出现即停——这种连续性优先的语义正是调度侧需要的vLLM 拿到命中长度后直接跳过对应 token 只算尾部。中间命中但前面断掉的部分其实用不上所以不如不报。retrieve的主体同样是三段重算块键、按后端位置batched_get批量取回、batched_to_gpu写回 GPU。有两个细节容易被忽略返回值是掩码而不是数字。ret_mask标记哪些 token 位置已经从缓存恢复若某块中途取失败引擎会把后续位置清零、并释放已取回却用不上的内存对象。调度侧靠这张掩码决定真正能跳过多少 token内存对象用引用计数管理生命周期。命中对象在交付后ref_count_downpinned 对象先unpin防止 CPU 锁页池被耗尽导致下一次取缓存时在分配处卡死想看真实复用流程examples/kv_cache_reuse 目录里有本地/远端后端、跨实例、跨引擎共享的完整示例benchmarks/rag 则给出了 RAG 场景下可复现的评测脚本前缀命中带来的首 token 延迟收益在那里可以直接量出来。 命中统计单例监控器挂在请求生命周期上可观测性由LMCStatsMonitor承担lmcache/observability.py通过GetOrCreate拿到进程级单例store_stats self.stats_monitor.on_store_request(num_to_store_tokens) # ... 上面的三段处理 ... self.stats_monitor.on_store_finished(store_stats, tot_token_num)钩子的模式统一请求开始前先登记拿到 stats 对象结束时回填结果。中间用 profile 上下文分别记录分块、GPU 拷贝、落盘三个子阶段的耗时——写入一旦变慢能直接看出是哪一段出了问题而不是对着一个总时间猜。区间层面监控器累计 requested / hit / stored 三类 token 数缓存命中率就是取回的命中 token ÷ 请求 token的自然产物。还有个体现生产化思维的小细节慢读取告警做了 10 秒限频一个表现差的后端不会把日志刷爆。 哪些设计经验可以搬进你自己的缓存系统跳出 LMCache 看下面几条可以直接迁移到任何内容寻址型缓存让键携带身份而不只是内容。模型名、并行拓扑、dtype 都进键不同部署共用键空间也互不串扰链式哈希让前缀变得可比较前 N 个是否匹配变成第 N 块的哈希是否相等且第一个缺口天然就是命中边界——这套语义对一切前缀型负载都成立用 mask 前缀约定省重复计算。只要约定清楚输入里哪段是增量每次就不用付全量成本分阶段独立 profiling。分块、拷贝、落盘分开计时瓶颈定位不靠猜缓存要有回退语义资源紧张时能存多少存多少而不是失败缓存层才不会成为主业务的单点故障代码里同样藏着明确的下一步方向逐层模式下batched_contains仍是逐层查询、后端间多位置检索尚未支持、异步预取路径里有一处process_tokens 中的哈希计算可以跳过的注释——这些正是当前 lookup 与 retrieve 链路上的摩擦点也是后续性能收益最可能兑现的地方。想继续深入可以从 docs/source/kv_cache/ 的 KV 缓存章节入手按图索骥对应到缓存引擎的每个模块。【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考