P7–P8 KV Cache 存储深挖问答
专家面试总纲 · 设计图册 · KV 存储详解 · LMCache + Mooncake
总纲回答“要准备什么”,这一页回答“具体怎么答”。每题按 考察点 → 回答要点 → 继续追问 → 常见失分 组织。回答要点是参考思路,不是标准答案;涉及 LMCache、Mooncake 的细节对应资料中的固定版本,面试时应说明“以我读过的版本为准”。
P7 型回答至少要把一个问题讲到状态、预算和失败处理;P8 型回答还要比较方案、说明演进路线和跨团队边界。每题末尾的“P8 加分”给出这一层的方向。
一、正确性与身份
Q1:两条请求文本相同,为什么不一定能共享 KV?
考察点:是否把 KV 当作“计算状态”而不是“文本缓存”。
回答要点:
- KV 由模型权重、tokenizer 与模板产生的 token 序列、位置编码、dtype 和并行切分共同决定。文本相同但模板、LoRA、模型 revision、RoPE 配置不同,状态就不同。
- 普通因果注意力下,第 i 个 token 的 KV 依赖它之前的全部 token,所以 key 必须包含父前缀身份,单独 hash 一个 chunk 的内容是错误的。
- 即使身份一致,读出的字节还要按相同的 layer/head/rank 布局解释;布局变了,key 相同也不能用。
- 因此设计上把“语义身份”编码进 key 或 namespace,把“表示格式”放进版本化的格式描述,两者都要校验。
继续追问:哪些字段必须进 key,哪些可以由部署约束保证?滑窗或混合模型怎么办?
常见失分:只说“用 prompt 的 hash 当 key”;忽略 tokenizer 模板;把 namespace 当成访问控制。
P8 加分:提出跨引擎的格式描述与兼容性矩阵,说明谁负责维护、升级时怎样双写或隔离。
Q2:一个 chunk 在 TP=4 下拆成 4 个对象,其中 1 个缺失,能复用多少?
考察点:对象级命中与模型级可用范围的区别。
回答要点:普通 full attention 下,每个 rank 只持有部分 head,缺一个 rank 这个 chunk 就不完整,所有 rank 都不能跳过它;后续 chunk 依赖它,也不能用。可复用范围是“所有 rank 都完整的最长连续前缀”。实现上可以像 LMCache 的分组 bitmap 那样按行(group × rank)记录,再折叠出共同前缀。
继续追问:滑窗层只需要最近窗口的 KV,折叠规则如何改变?已经加载成功的其他 rank 数据怎么处理?
常见失分:把 4 个对象中 3 个命中说成“命中率 75%,跳过 75% 计算”。
P8 加分:讨论是否需要 manifest 级提交(一个前缀的所有对象一起发布)与消费端折叠两种方案的取舍:前者写路径复杂,后者读路径浪费带宽。
二、发布与生命周期
Q3:为什么写入要“分配 → 写数据 → 发布”分步,而不是一次 put?
考察点:可见性边界。
回答要点:分配成功只代表有了空间,数据还没写完。如果分配后立刻可见,读者可能读到半成品。所以要先写入,确认完整后再发布。Mooncake 的单对象路径可以用 PutStart → 写入 → PutEnd/PutRevoke 理解,LMCache 的 L1 用 staging → admission 实现同样的不变量。写者中途退出时,需要超时清理未发布对象和空间,同时不能伤害仍在进行的传输。
继续追问:两个写者同时写同一个内容 key 怎么办?批量写一半成功,上层如何计数?
常见失分:认为“批量写”是事务;认为 put 返回成功就一定覆盖了旧值(在 Mooncake 学习版本中,已存在对象可以返回成功而不覆盖)。
P8 加分:把“幂等写”“版本化 key”“upsert”三种语义列成对照,说明 KV 内容寻址场景为什么优先不可变 key。
Q4:读租约、引用计数、CUDA event 能互相替代吗?
考察点:三层保护各管什么。
回答要点:
- 读租约:存储服务在一段时间内不回收某个副本,跨进程、跨机器生效,但有时限。
- 引用计数:本进程内某块内存还有几个使用者,没有时限,但只在所属进程内可见。
- 设备事件 / 传输完成:异步操作是否真的结束,回答“现在能不能读写这块 buffer”。
三者回答的问题不同,互相不能替代。例如租约有效不代表 DMA 已完成;DMA 完成不代表租约在读取期间一直有效;本地锁计数也不会自动变成远端的跨客户端锁(学习版本中 LMCache 的 native adapter 就是在客户端本地维护锁计数)。
继续追问:租约时长怎么定?读取超过租约会发生什么?
常见失分:说“有租约就安全”;把 TTL、pin 当成在途保护。
P8 加分:给出统一的“资源所有者表”:每块资源由谁分配、谁持有、哪个事件之后可以释放,作为跨团队接口约定。
Q5:远端加载超时,为什么不能立即释放目标 buffer?
考察点:异步资源安全。参考设计图 D10。
回答要点:超时只是调用方不再等待,底层 RDMA 或 DMA 可能仍在写这块内存。立即复用会导致新请求的数据被旧任务覆盖,输出错误且难以复现。正确做法:标记放弃 → 隔离目标 → 等待取消确认或完成事件 → 释放;迟到的回调用 generation 判断是否过期并丢弃。服务层面,本次请求走重算,不阻塞在这个 buffer 上。
继续追问:隔离期间内存被占住,怎样防止大量超时导致内存耗尽?
常见失分:只说“设置超时后重试”。
P8 加分:提出隔离内存的配额与告警,把“超时后仍未完成的任务数”作为健康指标。
三、容量、性能与成本
Q6:命中率从 50% 提到 90%,TTFT 反而变差,怎么排查?
考察点:指标口径与端到端归因。
回答要点:
- 先核对口径:命中率是按请求、按 token 还是按对象统计?实际少算了多少 token?
- 拆分 TTFT:排队、lookup、加载等待、H2D、Prefill 计算各占多少。
- 常见原因:热点集中导致少数存储节点排队;加载比重算还慢(短前缀);预取占满 staging,挤压了其他请求;同步等待阻塞了调度。
- 对照实验:固定负载,分别关闭远端层、只开本地层、限制加载等待上限,看哪一段变化。
继续追问:加载与重算的盈亏点怎么算?
常见失分:默认“命中率越高越好”。
P8 加分:建议引入“有效节省 token / 加载字节”等成本指标,让命中策略以 goodput 和成本为目标,而不是命中率。
Q7:给一个模型和网络,怎样判断该加载还是重算?
考察点:定量能力。
回答要点:以示例模型每 token 128 KiB 为例,1K token 约 128 MiB。按 400 Gbit/s 理想线速约 50 GB/s,传输下界约 2.7 ms;真实系统再加查询、排队、H2D 和同步。再估算这 1K token 的 Prefill 时间(取决于模型大小和 GPU)。加载关键路径明显短于重算时才值得加载;短前缀、网络拥塞或 GPU 空闲时,重算可能更划算。这些是推导,不是实测,结论要用实验验证。
继续追问:GPU 繁忙和空闲时结论会变吗?预取能隐藏多少?
常见失分:只比较带宽,不算查询和排队;把理论线速当实际吞吐。
P8 加分:把盈亏点做成运行时决策(按前缀长度、当前队列和 GPU 负载动态选择),并说明怎样防止决策抖动。
Q8:chunk 大小选 64 还是 256 token?
考察点:多维取舍。参考设计图 D12。
回答要点:小 chunk 匹配更精细、部分命中浪费少,但对象数、元数据、RPC 和注册开销成倍增加;大 chunk 元数据少、传输效率高,但尾部不满一个 chunk 的部分无法复用,失败时重传成本大。还要和引擎 page 大小、TP 切分后的单对象大小一起看:TP=4 时单对象只有逻辑 chunk 的四分之一。
继续追问:对象数达到几百万时控制面会先遇到什么瓶颈?
常见失分:只给一个数字,不说依据。
P8 加分:提出按负载特征分层(热点长前缀用大 chunk,多轮对话尾部用小 chunk)并说明复杂度代价。
Q9:保留 32,768 个 8K 前缀、两副本,需要多少机器?
考察点:完整的预算方法。
回答要点:逻辑 KV 32 TiB,两副本 payload 64 TiB。若每台 512 GiB 且只有 80% 可用于缓存池,128 台只有 51.2 TiB,不够;160 台刚好 64 TiB,但还没留元数据、staging、碎片和故障余量,所以需要更多。还要检查去掉一个故障域后是否仍够,以及读带宽(例:200 req/s × 75% × 1 GiB ≈ 150 GiB/s)需要多少链路。
继续追问:前缀大量共享时怎样重新估算?
常见失分:只算 payload;按请求数而不是唯一前缀数计算。
P8 加分:给出正常、单故障域退出、扩容迁移三种状态下的预算,并说明成本与 SLO 的取舍由谁拍板。
四、可靠性与运维
Q10:两副本能否容忍一台机器故障?
考察点:副本与故障域。
回答要点:取决于放置。两个副本若在同一台机器的两个 segment 上,整机故障时会同时丢失。必须确认放置策略真正跨 host 或 rack,并检查实际分配结果(申请 N 副本不保证得到 N 个跨机器副本)。另外,副本丢失后要有修复或重新写入策略,修复流量会和在线读写竞争。对可重算的 KV,副本主要换来的是“故障时少重算”,要和容量成本一起算。
继续追问:目录服务 HA 与数据副本有什么区别?
常见失分:把“副本数 = 2”直接等同于“可容忍单机故障”。
P8 加分:结合重算容量讨论:如果 GPU 侧有足够余量,可以少副本;否则副本是在买 SLO。
Q11:目录主节点故障,服务应该怎样表现?
考察点:HA 与降级。参考设计图 D11。
回答要点:切换期间新查询失败按 miss 处理,请求走重算;已在途读取以租约和完成结果判断是否有效。新主上任要先隔离旧主(递增 term),再从快照和日志恢复元数据,核对 payload 是否还在,清理未发布对象。恢复后限流预热,避免大量请求同时回源。
继续追问:如果所有内存副本都丢了,HA 能恢复数据吗?
常见失分:说“主备切换后数据就恢复了”。元数据可以恢复,丢失的内存 payload 不能凭空恢复。
P8 加分:定义恢复目标(多久恢复查询、多久恢复写入、命中率多久回到基线),并把演练纳入发布流程。
Q12:缓存层整体故障,GPU 重算被打满,怎么设计降级?
考察点:系统级背压。
回答要点:缓存故障本身不影响正确性,但会把负载转移到 GPU。需要:限制重试次数和回源速率;按优先级准入,必要时拒绝或排队低优先级请求;监控 miss 原因和 GPU 队列;恢复后分批预热,避免第二波冲击。容量规划时就要评估“缓存全冷”状态下的服务能力。
继续追问:哪些指标触发降级,由谁执行?
常见失分:只说“缓存失败就重算”,不考虑容量。
P8 加分:把缓存层纳入整体容量模型,明确“有缓存”和“无缓存”两种运行状态的 SLO 承诺。
Q13:升级模型或 KV 格式时,已有缓存怎么处理?
考察点:演进与治理。参考设计图 D13。
回答要点:新模型的 KV 和旧模型不兼容,使用新 namespace;新旧实例互不读取。灰度期间新实例从冷启动开始,命中率下降,要预估额外的 GPU 重算;可以预热热点前缀。旧 namespace 只读并逐步淘汰。出现正确性或 SLO 异常时路由回旧版本。
继续追问:能否把旧 KV 转换成新格式?
常见失分:沿用旧 key 让新模型读旧缓存。
P8 加分:把升级流程标准化为平台能力,包括配额、预热预算、观测指标和回滚开关。
五、实现与工程
Q14:讲一次 LMCache + Mooncake 的保存与恢复。
考察点:能否把源码落到状态与完成事件。参考设计图 D9。
回答要点:保存:GPU 产生 KV → LMCache L1 预留 staging → 设备拷贝完成后发布为可读 → 按策略提交 L2 → native adapter 调用 Mooncake 批量写 → Store 分配、传输、发布 → 汇总结果并释放本次保留。恢复:LOOKUP 的 ack 只表示已登记 → 规划 L1/L2 来源并取得保护 → 远端数据先进 L1 staging → 成功对象发布并加读保留 → 按 group/rank 折叠出可用前缀 → 拷贝到 GPU 页 → 引擎跳过相应 token。要强调至少三个不同的“完成”:L1 可读、L2 保存完成、GPU 可消费。
继续追问:哪一步失败会造成错误输出,哪一步只会造成性能损失?
常见失分:把 lookup 成功当命中;忽略 GPU 侧的完成条件。
P8 加分:指出接口契约与实现保证的差距(例如本地锁计数与远端租约),并提出可验证的改进。
Q15:手写“同 key 在途加载合并”,要注意什么?
考察点:并发编码。
回答要点:用一个 map 记录 key → 在途任务;第一个请求创建任务,后来者等待同一个结果。必须处理:任务失败时唤醒所有等待者并移除条目,避免后来者永远等待;等待者取消时不能取消别人的任务;最后一个等待者离开时决定是否取消底层任务;结果的生命周期要覆盖所有读者。锁只保护 map,不要在持锁时做 I/O。
继续追问:怎样测试失败和取消路径?
常见失分:只写成功路径;持锁做网络调用。
P8 加分:讨论在途合并与预取、热点复制的关系,以及对尾延迟的影响。
Q16:讲一个你负责或推动的 KV 存储设计。
考察点:真实经历与影响力。
回答结构:背景和目标(指标)→ 约束 → 备选方案与取舍 → 你的具体决定 → 落地过程与协作 → 结果数据 → 遗留问题与复盘。数据要说明测量条件;没有生产经历时,可以讲完整的设计与实验项目,但要如实说明范围。
常见失分:只讲做了什么,不讲为什么这样选;夸大个人范围。
P8 加分:说明怎样让多个团队接受接口约定,以及方案在后续一两年的演进。
练习方法
每题限时 5 分钟口述,录音后对照“常见失分”自查;每周挑 3 题请别人连续追问三层。答不出来的地方回到KV 存储详解和联合链路补齐,再在总纲的自评表中更新等级。