LMCache:优化、可观测与生产设计
白话导读:讲怎样衡量 LMCache 是否真的带来收益,以及上生产前要准备的指标、故障处理和隔离。
- 先定义指标再谈收益:区分查到、加载成功、实际少算的 token,以及 TTFT 的变化。
- 实验按阶梯做:无缓存基线 → 引擎自带前缀缓存 → 加 LMCache 冷缓存 → 热缓存 → 更大工作集,一次只改一个变量。
- 每项优化都有代价,故障矩阵里要写清“出错时怎样不产生错误输出”。
动手测量见实测与故障演练。
1. 先把指标定义清楚
一句话:说“缓存有收益”之前,先说清楚用的是哪个指标、怎么算的。同一个“命中率”,不同算法能差好几倍。
| 指标 | 怎么算 | 常见误用 |
|---|---|---|
| TTFT | 从约定的起点到第一个有效输出 token;要写明是否包含网络和排队 | 把非流式 HTTP 的总耗时当成 TTFT |
| ITL / TPOT | ITL 是相邻两个 token 的间隔;TPOT 是一段生成的平均每 token 时间 | 只报平均值,掩盖了 P99 和中途卡顿 |
| Goodput | 每秒满足 TTFT、ITL、成功率等目标的请求数或 token 数 | 只看总吞吐,不管有多少请求超时 |
| 请求命中率 | 至少命中一部分的请求占比,或完全命中的请求占比;必须写明是哪种 | 请求只命中了 1 个 chunk 也算成“命中” |
| Token 命中率 | 能复用的 token ÷ 参与查找的 token;APC 和外部缓存的分母要分清 | 把 APC 和外部缓存的命中加在一起,重复计数 |
| 实际读取带宽 | 指定时间窗口内真正传输完成的数据字节 | 用“提交的字节数”或压缩前的逻辑大小冒充实际流量 |
| 缓存成本 | 内存、SSD、网络、存储请求费用,减去省下的 GPU 计算 | 只看命中,不算写入和副本带来的流量 |
平均命中率说明不了尾延迟。建议把请求按结果分开统计:冷启动、L1 命中、L2 命中、部分命中、查找超时、加载失败后重算;再按 prompt 长度、租户和并发度分组。生产监控里不要把原文、token 内容或不断增长的 request id 放进指标标签。
2. 实验按阶梯一步一步做
每一步只多加一样东西,这样才知道收益(或损失)来自哪里:
- 不开 LMCache 的引擎基线:固定模型、dtype、batch、并发、输入输出长度分布。
- 只开引擎自带的前缀缓存(APC):看本地复用能带来多少。
- APC + LMCache,冷缓存:测保存和集成本身的开销。这一步没收益是正常的,不能说“没效果所以失败”。
- APC + LMCache,热缓存:区分哪些命中来自 GPU 上的 APC,哪些真正是从 LMCache 读回来的。
- 重启引擎,连接仍在运行的 LMCache:GPU 上的 APC 清空了,能更干净地看出 LMCache 的作用。
- 更大的工作集或更高的并发:超过 GPU 或 L1 容量之后,缓存还有多大价值。
- 单独测失败、取消、抢占和重启:不能只验证一切顺利的情况。
每组都先预热,再重复多轮,保存原始输出、版本、启动配置和指标快照。请求数要足够支撑你报告的分位数——几十个请求说明不了生产环境的 P99。混合模型还要额外验证组和布局的支持以及输出是否一致,不能直接沿用普通模型的结论。实测步骤和记录模板见实测与故障演练。
3. 慢在哪里,怎样拆
分别记录查找、后端读取、CPU buffer 等待、网络、H2D、剩余 Prefill、首 token 和后续 Decode 的时间。如果各阶段有重叠,要用 trace 里的关键路径来解释端到端时间,不能把重叠的时间段直接相加。
端到端慢
├─ 查找慢:RPC 排队 / 元数据压力 / 小对象太多 / 轮询查询
├─ 加载慢:后端 I/O / 网络拥塞 / 热点 / CPU 拷贝 / 内存注册
├─ H2D 慢:NUMA / PCIe / 布局转换 / chunk 太碎 / 设备同步
├─ 命中用不上:前缀断了 / 某个组缺失 / rank 不齐 / APC 已经覆盖
└─ Decode 慢:拷贝和计算抢带宽 / batch 过大 / 抢占
估算 buffer 要多大:用 Little 定律,在途数据量 ≈ 吞吐 × 平均停留时间。例如持续 20 GiB/s 的输入,每份数据平均在 staging 里停 0.1 秒,平均就需要约 2 GiB staging。再给尾延迟、突发、已查找未分配的请求和双缓冲留余量。注意:无限排队不等于带宽变大。
4. 常见优化和它们的代价
| 优化 | 好处 | 代价或前提 |
|---|---|---|
| chunk 大小 | 大 chunk 摊薄 hash、RPC 和元数据开销,顺序带宽更高 | 尾部不能复用的部分变多,部分命中和多取的浪费变大;小 chunk 灵活但容易被提交开销拖慢。学习版本常用 256 token,但要服从 block/组的协商,并按负载验证 |
| 批量与流水 | 合并查找、批量传输、流水线能提高利用率 | batch 太大会推迟第一个能用的结果,失败或取消时占用的资源也更多。流水稳定后接近最慢一段的时间,但有启动、收尾和资源竞争 |
| 逐层加载(Layerwise) | 加载下一层的同时计算当前层,减少关键路径上的传输时间 | 需要正确的逐层依赖、布局和模型支持;不是给函数加个 async 就能自动重叠 |
| 压缩 / 量化 | 减少字节数,在带宽受限时有潜力 | 增加编解码时间、缩放元数据和可能的质量损失;PCIe/GPU 空闲而编码很贵时反而变慢。要比较完整的“编码 + 传输 + 解码”和原路径,并报告任务质量。CacheGen、普通 KV 量化和任意序列化插件是不同的东西,见 CacheGen、序列化 |
| CacheBlend | 复用不在前缀位置的片段,选择性重算一部分来恢复片段之间的依赖 | 必须在真实的 RAG/多文档任务上评估质量和时延,不能说和完整 Prefill 逐位相同。MP 和进程内的开启方式不同,见 CacheBlend |
| 延迟卸载(Lazy offload) | 不急着把将来未必用到的数据写出去 | 要和引擎的淘汰、抢占配合;不能等 page 被别人复用了才去读旧 KV。见 Lazy offload |
| 缓存感知路由 | 请求发到能复用最多的实例 | 应估算“排队 + 缺失部分的重算 + 加载传输”的总代价,而不是永远选命中最长的实例。这是设计建议;目录可能过时,要实际查找验证,并控制热点和租户公平 |
5. 故障怎么处理
| 现象 | 怎么发现 | 正确的处理 |
|---|---|---|
| 查找超时 | 每个 RPC 的截止时间、server 健康状态、队列长度 | 结束本次计划或安全降级;迟到的回复不能改结果;释放没用上的锁 |
| L2 部分读取失败 | 完成 bitmap、字节数、adapter 报错 | 只算成功且语义完整的前缀;没完成的 buffer 不能用 |
| 请求取消 | 请求/会话状态和在途操作 | 停止新工作;已提交的 DMA 要等它结束或隔离起来,再释放引用 |
| L1 空间不足 | resident、staging、查找预算、可淘汰字节数 | 背压、减少预取或准入;不能强行回收正在被读的数据 |
| 存储节点退出 | 成员变化、读取报错、副本状态 | 改读有效副本或重算;限制重算风暴和重新复制的流量 |
| 引擎抢占 | 抢占事件、page 生命周期 | 避免旧的保存任务读到新请求的 page;恢复时 token 进度和 KV 必须对得上 |
| 模型或布局变更 | 命名空间、版本和布局检查 | 隔离或拒绝不兼容的对象;形状相同不代表语义相同 |
| 读完了但输出质量异常 | 对照输出、校验和、token/key/布局/rank 记录 | 从身份和生命周期查起;校验和发现不了“字节是对的,但给错了请求” |
“失败就回退到重算”需要 Connector 和调度器一起支持,并且确保没有用到坏数据,不能随便捕获异常后继续算。有些部署要求出错立即报错而不是回退,例如严格的 PD 交接失败策略。缓存未命中、加载失败、超时在指标里应该能区分开,即使某层接口最后都把它们当作“0 命中”处理。各类故障的演练方法见故障演练。
6. 上生产前的检查
安全与隔离:KV 里可能带有敏感的上下文信息。至少要设计模型和租户命名空间、服务身份认证、网络访问控制、加密要求、数据保留与删除、审计,以及防止跨租户命中带来的侧信道。cache_salt 能隔离 key,但如果谁都能冒用别人的 salt,它就不是访问控制。
资源预算:内存要算 resident + staging + 在途传输 buffer + 分配器和注册开销 + 运行时 + 余量;磁盘要算副本、临时文件和写放大。用文件后端测试时,注明操作系统 page cache 的状态。
最低检查清单:
- 用固定版本验证运行时和二进制兼容、Connector 实际从哪里加载、模型/布局/组是否支持。
- 分清请求 RPC、HTTP 管理、Mooncake Master 和元数据服务的端口与网络边界。
- 限制预取、写回、连接数、线程和每个租户的资源,保证读取和查找不会被大量写入饿死。
- 验证优雅下线:停止接新任务 → 完成或取消在途操作 → 解锁 → 释放内存。
- 明确缓存丢失后的恢复负载和 SLO,不只是测试“进程能重新启动”。
- 建立观测:TTFT/ITL、实际 token 命中、staging 占用、带宽、失败次数,以及锁和会话是否泄漏。
观测入口见 Observability、HTTP API。HTTP 返回 200 不一定代表所有后端都健康,要看具体接口返回的内容。