ik_llama.cpp 长上下文中的 KV 缓存复用与上下文平移(Context Shift)机制解析
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本文以仓库内讨论 Context reuse / context shift for long prompts 为主线,结合 ik_llama.cpp 的实际源码,系统梳理 KV 缓存复用(cache_prompt)、上下文平移(context shift / K-shift)与"上下文重用"(context reuse / chunking)三者的区别、底层实现、质量损失机理与适用限制。读完你将能准确判断:长上下文溢出时,这个 fork 会做什么、为什么这样做、何时该开启或关闭 context shift,以及社区正在探索的替代方案。
一、问题起点:长上下文溢出时的"重算噩梦"
讨论的发起者从 koboldcpp 迁移到 ik_llama.cpp,使用 Qwen 3 的 62k 上下文窗口时遇到典型困境:
- 上下文溢出后,系统提示词(system prompt)被保留,但对话历史被丢弃;
- 每次新消息都要从头重新处理约 58k tokens;
- 在约 40 tokens/sec 的 prompt 处理速度下,每发一条新消息需要等待数分钟,而如果存在有效的缓存复用,这个时间可以压缩到几秒。
这正是长上下文场景下所有推理引擎都要面对的核心矛盾:prompt 处理(PP)是昂贵的,而 KV 缓存正是为了让它变便宜而存在的"记忆化(memoization)"手段。讨论中 cmoncure 准确点出了这一点:KV Cache 本质是 Key-Value 缓存,存储的是昂贵的 prompt 处理计算结果以便复用。
需要澄清的是:ik_llama.cpp并非没有对应实现。讨论早期参与者(包括维护者 ikawrakow 本人与贡献者 saood06)在后续回复中确认,本仓库已经具备 context shift(上下文平移)能力,只是缺少文档说明;而"按块复用缓存"(chunking)式的 context reuse 则是另一回事,仓库刻意没有照搬 mainline 的做法(详见下文"质量损失"与"未来方向"两节)。
二、三个容易混淆的概念:cache_prompt、context shift、context reuse
要理解这场讨论,必须先分清三个概念(这也是讨论中反复出现的主题):
| 概念 | 做什么 | ik_llama.cpp 中的现状 |
|---|---|---|
| KV 缓存复用(cache_prompt) | 请求之间共享相同前缀时,直接复用已计算的 KV 缓存,不重新处理公共前缀 | 已支持;需要请求携带cache_prompt: true(早期版本默认关闭,见下文) |
| 上下文平移(context shift / K-shift) | 上下文接近窗口上限时,丢弃一段旧 token 并对剩余 KV 重新做位置编码(RoPE 修正),使对话可以"无限"继续 | 已支持,属 llm 核心层能力;由 server 在溢出时自动触发 |
| 上下文重用(context reuse / chunking) | 缓存中某段被删除后,若新提示词在删除段之后仍有与缓存完全匹配的 token,则把它们也"拼回来"复用(如aaaaccccbbbb缓存中复用完整的aaaabbbb而非仅aaaa) | 未实现;mainline 的--cache-reuse方案未被移植(见后文原因) |
讨论中 ikawrakow 用一个例子精确描述了第三种能力(mainline llama.cpp 从 kobold.cpp 借鉴的 chunking):缓存里有aaaaccccbbbb,新上下文是aaaabbbb,理想情况应复用完整的aaaabbbb,而不仅仅是前缀aaaa。ik_llama.cpp 目前只做前缀级复用。
三、KV 缓存复用:cache_prompt的前世今生
关于 cache_prompt 的来龙去脉,仓库中另一份资料(问题 #455:KV cache is never reused in OpenAI compatible Chat Completion api)与本次讨论直接相关:
- 用户通过 OpenAI 兼容的
/v1/chat/completions接口调用时,发现每次重新生成最后一条回复都会从 p0 全量重算(日志中三次请求的 prompt eval 均为 536 tokens 全量处理); - 排查结论:不是实现缺失,而是默认关闭。贡献者 saood06 指出:"llama.cpp 现在默认开启 cache_prompt,但我们这里默认不开启;只有请求中显式传
cache_prompt: true才会复用缓存"; - 该问题随后推动了一次默认行为变更,saood06 表示"这个改动很 trivial,而且以我们这里的缓存实现,几乎没有任何理由关掉它"。
因此,在使用 ik_llama.cpp 的 server 时,若通过 OpenAI 兼容 API 集成 WebUI(如 Open WebUI、SillyTavern),务必在请求中携带cache_prompt: true(或确认所用版本已默认开启)。这是让多轮对话与"重新生成"操作避免全量重算的最直接手段,也正是讨论发起者"等待几秒而非几分钟"诉求的基础能力。
四、上下文平移(Context Shift)的源码级实现
当上下文真正溢出时,起作用的是 context shift。它分为 server 层的触发逻辑与 llm 核心层的 K-shift 计算两部分。
4.1 server 层:溢出检测与 token 取舍
server 侧实现在 examples/server/server-context.cpp:
context_shift()(L3582-L3630)在update_slots主循环中被调用(L5069-5071 处注释"apply context-shift if needed")。当某个 slot 满足system_tokens.size() + slot.n_past >= slot.n_ctx - 1即上下文即将耗尽时触发;- 保留量
n_keep默认取整个 prompt 长度(slot.params.n_keep < 0时),并加上 BOS token 计数;丢弃量n_discard若未显式指定,则默认取剩余可用空间的一半:n_discard = n_left / 2(L3602)。这也印证了讨论中 saood06 的描述——"context shifting 会平移整个上下文窗口(我记得是一半)并保留 system prompt"; - 平移前会先调用
tokens_support_context_shift()(L3444-L3457)做前置检查:多模态(mtmd)token 场景下,保证切分点不落在媒体 token 中间,必要时通过adjust_n_to_support_context_shift()(L3459-L3481)微调边界; context_shift_find_n_tokens()(L3485-L3501)利用公共前缀匹配,计算实际"被保留"与"被丢弃"的 token 数,处理"提示词与缓存之间分词不一致"(mistokenization)的情况——例如新请求的提示词被重新分词后,与缓存中的 token 序列存在细微差异,此时按公共前缀对齐后再平移;- 最终通过
discard_n_kv_and_cache_tokens()同步丢弃 KV 缓存中的行与cache_tokens,并更新slot.n_past -= n_discard_cache(L3622-L3623)。
server 侧还提供相似度评估:get_tokens_similarity()与get_cached_tokens_similarity()(examples/server/server-common.cpp L1813-L1842)用于比较"丢弃部分 token 后的缓存"与当前提示词的匹配程度,从而决定复用策略。
4.2 llm 核心层:K-shift 与 RoPE 位置修正
真正执行"平移"的是 llm 核心层 src/llama.cpp:
- 位置重映射:
llama_kv_cache_seq_add()(L2710-L2765)将序列在[p0, p1)区间内的所有 cell 的位置pos与偏移delta同步平移,并置位cache.has_shift = true、cache.cells_disordered = true。对循环(Mamba 类)模型则只平移pos不搬数据; - K-shift 图构建:下一次
llama_kv_cache_update_internal()(L7959-L7995)检测到kv_self.has_shift时,构建llama_build_graph_k_shift()计算图,通过llama_set_k_shift()(L5346)注入 K-shift 输入,对每个 KV cell 的 K 向量按新的位置差重新施加 RoPE 旋转,完成位置编码修正; - 状态复位:计算完成后清除
has_shift标志并将所有 cell 的delta归零(L7989-L7993),随后因 cache 已无序化还会触发 defrag(llama_kv_cache_defrag_internal)重整缓存布局。
一句话概括:context shift = 丢弃一段旧 KV 行 + 对剩余行重新做 RoPE 位置修正。这样对话可以继续下去,代价是"被删除 token 的影响并未真正消失"(见下一节)。
五、核心争议:为什么平移会带来质量损失
这是讨论中价值最高、也最富技术深度的一段。ikawrakow 亲自给出的例子:
KV cache: Yesterday I saw a movie. I absolutely enjoyed it. The main actor was ... New context: Yesterday I saw a movie. The main actor was假设这段"新上下文"出现在你人生中看过的最烂电影的语境里,你期待的回答是"a disaster";但现存 KV 缓存(尽管经过了 context shift)仍会强烈偏向 "brilliant"、"amazing" 这类正面词汇。
关键结论(ikawrakow):"You cannot undo the impact of the skipped tokens by just changing the position encoding via RoPE."——通过修改位置编码,无法撤销被跳过 token 的已生效影响。
saood06 进一步把这个机理讲透:
"The tokens do not 'poison' the cache, it is just that a token holds the information of all prior tokens from that sequence when it was calculated. If you get rid of tokens and then shift tokens that had come after the now deleted tokens in order to re-use them, the shifted tokens will still contain the information from the deleted tokens."
即:每个 token 的 KV 值在计算时已"蕴含"了该序列中它之前所有 token 的信息。删除I absolutely enjoyed it.这几个 token 后,再把后面的The main actor was平移复用,这些 KV 行里仍然残留被删 token 的语义影响。要么接受这种残留(快,但可能偏题),要么重新计算被影响的 token(慢,但干净)。
讨论中 cmoncure 曾提出一个诱人的设想:KV 缓存的影响如果是"加法"的,能否算出被删 token 的贡献f(A)并从后续 KV 中"减去"(Ba - f(A) => B)?saood06 的回答隐晦地否定了这种线性可逆性——transformer 注意力与逐层前向传播的组合是非线性的、逐 token 累积的,不存在廉价的减法逆运算。这也是为什么行业普遍只能接受"平移换速度、重算换质量"的二元选择。
六、适用限制:哪些模型不能 context shift
仓库源码对 context shift 施加了明确的模型级与配置级限制:
- 模型级:
llama_model_supports_ctx_shift()(src/llama-model.cpp L2664-L2669)返回 false 的架构包括 openPangu、DeepSeek4(注释说明二者把位置相关的私有状态放在通用 KV cache 之外)、Gemma3 与 Cohere2(逐层 rope 几何不一致,hparams.swa_layers 未记录); - 上下文级:
get_can_shift()(src/llama.cpp L7950-L7957)在模型级判断之上,还排除MLA 模型(lctx.model.is_mla_model(),即 DeepSeek 系列 MLA 注意力)、IMROPE 位置编码(LLAMA_ROPE_TYPE_IMROPE)以及已压缩的 KV 缓存(--swa-compress,因为压缩层的窗口行数无法对应 n_ctx 行视图); - server 侧:当模型不支持平移且上下文溢出时,server 会拒绝请求并返回
context_length_exceeded错误(server-context.cpp L3586-L3592);对 DeepSeek4 还会打印明确警告 "DeepSeek4 does not support context shifting; use --no-context-shift or increase context size"(src/llama.cpp L7966)。注意:MLA 模型不支持 K-shift,这意味着 DeepSeek-V3/R1 系列在长对话溢出时无法依赖 context shift,应优先扩大上下文或借助缓存复用。这与讨论中 saood06 强调"trie 方案可能只对 MLA 模型可行"的观察互为印证(MLA 的 KV cache 极轻,但代价是放弃了传统 K-shift 路径)。
七、实用配置建议
综合讨论与源码,针对长上下文场景给出如下实操指引(以当前仓库代码为准):
- 优先保证前缀复用:通过 OpenAI 兼容 API 集成时,请求中携带
cache_prompt: true(旧版本必须显式开启;若构建版本已默认开启则可省略)。这解决"重新生成上一条回复"这类前缀未变的场景; - 上下文溢出策略:默认情况下 server 会在溢出时执行 context shift(丢弃约一半可用空间并保留 system prompt)。若你的模型不被支持(如 MLA 模型),或你无法接受平移带来的质量漂移,可通过
--no-context-shift显式禁用(对应params_base.ctx_shift,见 server-context.cpp L1889-L1902 的自动降级逻辑),并相应增大--ctx-size; - 精细控制切分点:
n_keep/n_discard参数允许自定义"保留多少、丢弃多少",默认分别为"保留整个 prompt"与"丢弃剩余一半"(L3595-L3602)。对于 system prompt 较长的场景,可显式指定 n_keep 以确保关键指令不被切掉; - 知晓边界:不要指望 context shift 提供"无损续聊"。对质量敏感的任务(如代码生成、推理链),更稳妥的做法是保留足够上下文并在溢出前主动截断/重写历史。
八、未来方向:trie 缓存与"零质量损失"的探索
讨论末尾,贡献者 saood06 披露了一个正在酝酿的替代方案:用 trie(前缀树)保存会话中所有处理过的 token,并可保存/恢复到文件。其动机与权衡如下:
- 目标是保留会话中探索过的每一条分支(或多会话共享的大型初始 prompt),用最少空间、无质量损失地实现复用;
- 该方法不做 chunking 也不做 shifting,因此不会像平移那样引入质量退化;但它无法解决"移除 thought token(思考 token)而不重算"的常见诉求——因为不重算就无法消除被删 token 的残留影响;
- 方案可能只对 MLA 模型可行(KV cache 极轻),且与"从 trie 上做 chunk + shift"的完全融合被认为过于复杂;
- 该工作曾因 KV 缓存保存/加载的 bug(仓库问题 #436)而停滞,问题修复后得以继续,但仍被描述为"large undertaking"。
这一方向与 mainline 的--cache-reuse(chunking)形成鲜明对比:ik_llama.cpp 刻意没有移植 chunking,原因是作者认为块级复用(尤其小粒度、频繁执行时)存在明显的质量损失;trie 方案追求"不同 tradeoff"——对部分场景更好(无质量损失、可持久化),对另一些场景更差(不能复用中间被删的块)。截至讨论时间点,该方案尚未进入 PR 阶段。
结语
围绕"Context reuse / context shift"这场讨论,可以提炼出 ik_llama.cpp 在长上下文问题上的完整姿态:前缀级 KV 缓存复用(cache_prompt)是日常多轮对话的第一道加速手段;context shift(K-shift + RoPE 修正)是溢出时的兜底机制,但以"被删 token 的语义残留"为代价,且对 MLA 等架构不可用;块级 context reuse(chunking)则被有意搁置,转而探索 trie 持久化缓存这条更重的替代路线。理解这三者的边界与取舍,是正确部署长上下文服务的前提——也呼应了讨论中最重要的一句话:你无法用位置编码的魔法,抹掉已经发生过的计算。
延伸阅读:核心实现见 src/llama.cpp(llama_kv_cache_seq_add、llama_kv_cache_update_internal、llama_build_graph_k_shift);模型支持判定见 src/llama-model.cpp;server 触发与切分逻辑见 examples/server/server-context.cpp 与 examples/server/server-common.cpp;KV 缓存复用默认值问题的完整来龙去脉见 问题 #455。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考