news 2026/9/14 1:35:03

LMCache cache_engine.py 深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LMCache cache_engine.py 深度解析

LMCache cache_engine.py 深度解析

【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache

vLLM 发来一条带着 8 万 token 长前缀的请求,LMCache 的 lmcache/v1/cache_engine.py 会在几十毫秒内决定:哪些 KV 块从 CPU 缓存捞回 GPU,哪些必须重算。本文跟着这"一次请求"走完 store/retrieve 全程,看它怎么分块、怎么判命中、命中失败时怎么回滚。

它到底在解决什么问题

长上下文推理的瓶颈在 prefill:每个新请求都要把前缀的 KV 从头算一遍,GPU 算力耗在"重复劳动"上。LMCache 把这些算好的 KV 卸到 CPU/远端存储,下一个共享前缀的请求直接捞回。项目 README.md 宣称 10 倍速度、10 倍成本降低,benchmarks/rag/ 提供了可复现的 TTFT 测量脚本。那问题来了:一份 GB 级的 KV 到底怎么切成块塞进缓存,又被秒级地找回来?

跟着一次请求走完全程

先看整体位置,cache_engine.py 处于存储后端与 GPU 连接器之间:

1. 请求进入,先过三道门。store()入口处(L388)连续检查:is_healthy()不健康直接跳过(L416);_is_passive()的被动 rank 不参与存储(L424);freeze 模式开启则只读不写(L462)。这样写的原因:缓存是加速层不是正确性依赖,任何一道门拦下都只能"静默降级",绝不能抛异常打断主推理路径。

2. 分块与链式哈希。引擎自己不切 token,而是把process_tokens委托给TokenDatabase(L485)。ChunkedTokenDatabase默认按chunk_size=256切块(lmcache/v1/token_database.py#L329),然后做增量前缀哈希——每个块的哈希吃进上一块的哈希,像快递面单上的"上一站"字段:

def _prefix_hash(self, token_chunks): prefix_hash = self._get_init_hash() # 初始为 NONE_HASH=0 for token_chunk in token_chunks: prefix_hash = self._hash_tokens(token_chunk, prefix_hash) yield prefix_hash

来源

_hash_tokens内部先做归一化(L289-L291),确保两个进程对同一前缀算出同一个哈希——这是跨进程共享缓存能对上号的前提。

3. 键不是裸哈希。每个块哈希再被包进CacheEngineKey(L241-L248):

return CacheEngineKey( self.metadata.model_name, # 模型名:不同模型绝不串键 self.metadata.world_size ..., # 并行度:TP=4 和 TP=8 的 KV 布局不同 self.metadata.worker_id, # rank 号 chunk_hash, self.metadata.kv_dtype, # dtype:fp8 与 bf16 的字节流不同 request_configs, # 请求级配置(如 LoRA) )

来源

为什么不直接用 chunk_hash 当字典键?因为裸哈希只能表达"内容一样",表达不了"在哪个并行布局下、以什么精度算出来的"。键空间多 4 个维度,换来的是一次都不会取错。

4. 分配内存、搬数据、写回。store 主循环对每个块向StorageManager申请 CPU 侧MemoryObj(L499),然后一次性从 GPU 批量拉取、批量 put:

for start, end, key in self.token_database.process_tokens(...): memory_obj = self.storage_manager.allocate(kv_shapes, kv_dtypes, ...) if memory_obj is None: logger.warning("Local cpu memory under pressure ...") break # 内存吃紧:只存前 N 块,不崩

来源

搬运用gpu_connector.batched_from_gpu(L558)批量做 GPU→CPU,写回用batched_put(L564)。这里用批量而不是一块一 put,是因为单块 256 token 的 KV 只有几十 MB,逐次走 PCIe 的固定开销会淹没有效传输。

5. 检索:只认"连续前缀"。retrieve()(L780)同样先过健康门,不健康时返回全 False 的 mask(L808-L810),调用方看到"零命中"就去重算,流程不中断。核心在_process_tokens_internal:先算全部块键,再问StorageManager.get_block_mapping每个键在哪一层存储(L1751),然后按位置batched_get(L1756),并在这里做了一个关键的提前截断:

for (key, start, end), memory_obj in zip(blocks, memory_objs, ...): if memory_obj is None: last_failed_block_start = start # 记录最早失败点 break # 停止向后取 reordered_chunks.append((key, memory_obj, start, end)) ret_mask[start:end] = True # 命中的块标记可省算

来源

为什么命中到一半就 break?KV 在推理引擎里是位置敏感的:第 3 块缺失,第 4 块就算命中也不能用,因为前面的位置断了。取回来反而浪费一次 PCIe 往返。截断之后,已经取回但用不上的后缀块会统一ref_count_down释放(L1780-L1787),失败的块若配置了 remove-after-retrieve 也会主动清掉防泄漏(L1803-L1812)。

6. 回 GPU 并上报。命中块经batched_to_gpu写回推理引擎的 paged buffer(L910),最后on_retrieve_finished把命中 token 数交给监控单例(L940),store 侧对称地在 L469 与 L571 打点。

三个值得偷师的工程设计

1. 链式前缀哈希——用"位置依赖"换"截断免费"。它做了什么:每块哈希 = hash(前块哈希, 本块 token)。为什么不用更简单的做法:每块独立哈希(如只对块内容做 sha)实现更直白,但那样"第 4 块命中、第 3 块缺失"在键层面无法快速判断前缀断裂,你还得额外存一条链或全表重扫;链式哈希把位置信息编码进了键本身,第 i 块未命中 = 前 i-1 块可用,O(1) 得出结论。你项目里怎么抄:任何"只认最长连续前缀"的缓存(编译增量指纹、CDN 分层 URL、数据库 WAL 校验)都适用这个模式,代价是前缀中间改一个 token,后面所有键全变——这正是想要的雪崩效应。

2. 失败点截断 + 引用计数回滚——批量取数的正确性兜底。它做了什么:batched_get拿回一整批对象后,扫描到第一个 None 就 break,已取回的"孤儿块"逐个ref_count_down(L1780-L1787)。为什么不更简单:把"逐块取、取到失败就停"写出来代码更短,但每块一次网络/PCIe 往返,延迟随命中长度线性增长;批量取 + 事后回滚把往返压到常数次,用一点回滚复杂度换尾延迟。你项目里怎么抄:任何"批量读 + 只允许前缀有效"的场景(分片读取、批量 RPC),都应该有"最早失败点"变量和统一的释放路径,尤其注意同步后端 remove 不自动减引用的这类细节(L1806-L1810)。

3. 降级而非报错——缓存的失败姿态设计。它做了什么:健康检查失败时 store 静默跳过、retrieve 返回全 False mask(L808-L810);mark_init_failed后永久降级(L284-L299)。为什么不用更简单的做法:抛异常或打 fatal 更符合直觉,但缓存挂了把推理服务拖崩,等于加速件变成了故障源;返回"零命中"让上层像面对一个空缓存一样自然重算,故障被吸收在边界内。你项目里怎么抄:给任何旁路加速组件(限流本地缓存、预取器)定义一个"看起来合法的空结果",而不是错误码。

容易踩的坑和边界情况

mask 的 False 必须在头部。文档约定 mask 形如FFFFFTTTTT(L402-L405),False 段长度还是必须对齐 chunk_size,否则键生成会错位。触发条件:从调度器拼 mask 时把已计算 token 标在了中间。防护位置就是process_tokens的 mask 校验(token_database.py#L216-L219)。建议:单测里固定加一条"False 在中间应报错"的用例。

CPU 内存压力下只存一半。allocate返回 None 时循环break(L507-L514),该请求只落盘了前 N 块,日志里只有一条 warning。应对:把max_local_cache_size配小了命中率会悄悄下跌,监控 store 日志中 "Stored x out of total y tokens" 的比值。

跨进程共享时忘记 PYTHONHASHSEED。builtin 哈希随进程随机化,P/D 分离或远端共享场景下两个进程算出的键直接对不上,源码在启动时会 error 提示(token_database.py#L311-L326)。应对:启动脚本里export PYTHONHASHSEED=0

中间块"在存储里但取不出来"。get_block_mapping说存在、batched_get却给 None(L1763-L1774),通常是远端后端超时或对象损坏。应对:看到 "can't be retrieved" warning 时查 L2 后端健康,而不是怀疑键计算。

如果你想改/扩展它

  • 换分块策略:继承TokenDatabase抽象(token_database.py#L64),现有ChunkedTokenDatabase/SegmentTokenDatabase就是两个范例;按分隔符切段(多文档场景)就是这条路上现成的实现。
  • 接新的推理引擎:实现GPUConnectorInterface(cache_engine.py#L44 引入),提供batched_from_gpu/batched_to_gpu,引擎其余逻辑不动。
  • 换存储布局StorageManager.get_block_mapping(lmcache/v1/storage_backend/storage_manager.py#L1012)决定键查哪一层(L1 CPU / L2 远端),在这里加新存储层比改 engine 侵入小得多。

改动前必须跑通:

pytest tests/v1/test_cache_engine.py tests/v1/test_token_database.py -x

一张表看懂核心指标

监控单例把打点聚合成 gauge,下面是日常盯盘最常用的四个:

指标含义更新位置(行号)典型健康值
retrieve_hit_rateretrieve 时命中 token 占比lmcache/observability.py#L399、gauge 见 #L1065前缀复用高的负载 >60%
lookup_hit_rateprefill 前预查询的命中率lmcache/observability.py#L1072与 retrieve 同量级,差距大说明取数有问题
store 吞吐 (GB/s)写回 CPU 侧的实测带宽lmcache/v1/cache_engine.py#L575-L589逼近 PCIe 理论带宽的 70%+
from_gpu / put 耗时拆分store 三阶段耗时lmcache/v1/cache_engine.py#L587-L588from_gpu 通常占大头,put 突增查 L2

命中率长期贴地,先看键维度(模型名/TP/dtype 是否变了),再看分块大小是否与推理引擎页面对齐。

回到开头那个问题:GB 级 KV 的"秒级找回",本质是把 256-token 的块哈希成链式前缀键,批量问存储、命中到哪截断到哪,剩下的交给降级路径重算。源码入口:lmcache/v1/cache_engine.py。

【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 1:33:26

基于Vue2.0与Three.js的3D智能粮仓可视化系统实践

简介:面向Vue前端开发者与Web3D可视化入门者,这是一份基于ThreeJs和Vue2.0构建的3D粮仓管理系统源码,演示了三维可视化在仓储管理场景中的落地方式。项目以Vue-Element-Admin为管理端骨架,将ThreeJs场景渲染接入Vue组件生命周期&a…

作者头像 李华
网站建设 2026/9/14 1:33:18

12张动图解析大模型核心技术:从原理到实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 1:32:21

包裹与条码实例分割数据集实战:用YOLOv8-seg训练到部署

简介:包裹与条码实例分割数据集是一份面向物流场景的YOLO格式实例分割数据,包含条码、单个包裹、多个包裹三类标注,适用于自动化分拣、智能库存管理、物流监控等场景。资源共322个文件,以160张JPEG原图、160个TXT标注文件为主&…

作者头像 李华
网站建设 2026/9/14 1:32:03

C++代码风格检查工具:Clang-Format与Clang-Tidy实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 1:30:32

10款开源免费AIGC降重工具测评与使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 1:30:08

OSG海面与月夜场景渲染:网格、粒子与反射相机的完整实践

简介:这是基于OpenSceneGraph与osgOcean插件的三维海面场景工程,面向OSG初级开发者及虚拟仿真、数字孪生方向的学习者,演示在动态海面上添加小船,并叠加月光、风力与水面倒影效果,可用来理解海洋场景搭建与真实感渲染的…

作者头像 李华