llama-server 跨重启丢 KV 缓存的根因与修复
llama-cpp 衍生分支作者定位 slot 恢复后首条请求重做预填充的根因,并通过 sidecar 方案使长上下文恢…
llama.cpp 生态中负责本地推理服务的 llama-server 提供 slot save/restore 接口,允许用户将长上下文会话暂存到磁盘、稍后再恢复,从而避免再次支付预填充(prefill)开销。社区开发者近日在一个衍生分支 llama-cpp-turboquant 中发现并修复了这一功能的核心 Bug——恢复后的 KV 缓存其实在第一时间被完整丢弃,首条请求依然要重新预填充全部上下文,跨进程恢复形同虚设。
问题现象:保存与恢复都很快,首条请求却要 17 分钟
测试环境为 Qwopus3.6-27B(Q6_K 量化、混合架构),双 Tesla P100 分层加载,KV 启用 turbo4 量化。基准测试中,2.49 GB 的会话状态可在 1.23 秒内从磁盘恢复到显存,看似完美;但紧接着的 /completion 请求却触发完整重处理,100K 上下文下耗时约 17.5 分钟。结果是:保存快、恢复快,恢复完立即全丢。
根因:checkpoint 列表只活在进程内存里
llama_state_seq_save_file 序列化了两类信息:token 序列与物理 KV cell,但漏掉了 slot.prompt.checkpoints 这一仅存在于进程内存中的 checkpoint 元数据列表。冷启动后,首条请求需要对 prompt 尾部做小幅度回滚以重新对齐 BPE 边界、产出首 token,逻辑上要求存在一个「能覆盖回滚目标」的 checkpoint。由于 checkpoint 列表为空,服务器找不到任何可用节点,便直接放弃复用、整段重算。
修复方案:为 checkpoint 列表加一份 sidecar
最初尝试在恢复时于「当前 tip 位置」合成一个 checkpoint,结果被回滚目标位置更早的特性否决——checkpoint 只能服务自身位置及之后的回滚。最终落地的方案是在 tools/server/server-context.cpp 中新增 117 行:在 SLOT_SAVE 时将 checkpoint 列表持久化为带版本的 sidecar 文件 <state>.ckpt;在 SLOT_RESTORE 时重新加载。回滚现在命中真实持久化的 checkpoint,仅尾部少量 token 需要重处理。若 sidecar 缺失(如旧版本生成的状态文件),则退回到 v1 的 tip 合成策略,仍可覆盖精确追加续写,否则降级为整段重算——结果正确、不崩溃。
关键基准数据
- 100K 全量预填充基线:722.4 s(719,975 ms API,约 138.9 t/s)
- 保存耗时:1,777 ms(2.56 GB 状态 + 299.3 MB sidecar)
- 冷启动 → 恢复 100,043 token:1,592 ms
- 恢复后首条增量查询:1,000 ms API / 2.72 s 墙钟,95% 深度处的事实召回通过
- 同二进制 A/B 对照(仅关闭 sidecar):720.1 s 完整重预填充,输出正确
- 总体加速:预填充阶段约 720×,端到端 resume 约 167×
A/B 一行是这次修复最具说服力的凭据:同一份二进制、同一份状态文件,唯一变量是 sidecar 是否存在——有则 1 秒,无则 720 秒。
影响范围与现状
该修复以 PR 形式提交至 llama-cpp-turboquant 分支(commit eaf98e612,+117 行,#206)。同一 Bug 在上游 llama.cpp master 上经作者验证在 dense 与 SWA 模型上同样存在,目前尚未修复。在混合架构下,sidecar 大小与上下文长度无关(每个 recurrent checkpoint 约 149.6 MiB,1K 与 100K 上下文均为两份共 299.3 MB),纯 SWA 模型下的开销尚未测量。另:-np 2 --kv-unified 下双 slot 缓存被丢弃为既有行为,在未打补丁的构建上可复现,与本次修复无关。
完整方法论与脚本见作者公开的 gist。
