🦙 llama.cpp 图解教程llama.cpp Visual Guide 第四部分 · llama 推理内部Part 4 · Inside llama inference 19 / 40
第四部分 · llama 推理内部Part 4 · Inside llama inference

KV cacheThe KV cache

自回归每步只新算一个 token,全靠把先前 token 的 K/V 缓存起来——这就是 KV cache(L03/L04 反复提到的那个"记忆")。这一课钻进 llama-kv-cache:cell 怎么管理、上下文满了怎么移位、多条序列怎么共存, 以及为长上下文准备的各种变体。它是第四部分的收尾,也是大模型"记得住前文"的物理基础。

为什么 KV cache 值得收尾一讲?因为它是显存大户,也是长上下文、多并发这些实际能力的关键。前面 L17 说 context 持有它、L18 说 batch 往里写——这一课把这个"记忆"本身彻底拆开看清楚。

🔌 生活类比
KV cache 像一本会议纪要:每来一个发言(token),就把它的要点(K/V)记在新的一行(cell),标上这是第几句(pos)、谁说的(seq_id)。下次接话,只需读纪要, 不必把整场会重听一遍。会议太长记不下了,就滚动翻页(滑窗)或划掉某人的发言(seq_rm)。这本纪要,就是模型"记得住上文"的那本账。

为什么需要 KV cache

先说清它解决什么。注意力要让"当前 token"去看"之前所有 token",需要每个历史 token 的 K(键)和 V(值)。没有缓存,每生成一个新词,都得把前面所有 token 的 K/V 重算一遍——长度每涨一点,重算量就平方级地涨。

重算 vs 缓存:缓存把"每步重算整段"降成"每步只算新 token"
没缓存每步重算 t0..tn 的 K/V随长度平方涨
有缓存只算新 token 的 K/V读历史 K/V随长度线性

有了缓存,先前每个 token 的 K/V 算过一次就存着,每步只需算新 token 的 K/V、把它追加进缓存,再读出全部历史 K/V 做注意力。于是每步的计算量从"重算整段"降成"只算一个",这正是 L04 证明过的:缓存和重算数值上完全等价,但快了一个数量级。

这也直接解释了 L03 说的"decode 为什么快":decode 每步只新增一个 token,靠 KV cache 复用历史,所以一步只做一个 token 的前向。可以说,没有 KV cache,就没有实用的自回归生成——它是把"理论上能算"变成"实际跑得动"的那块关键拼图。

先快速回忆 K/V 是什么(L04/L11):注意力里,当前 token 用自己的 Q(查询)去和每个历史 token 的 K(键)做点积、算出"该关注谁",再按这个权重把各历史 token 的 V(值)加权汇总。所以"看历史"这件事,需要的正是每个历史 token 的 K 和 V——把它们缓存下来,就不用每步重算。

🌍 宏观理解
那为什么单单缓存 K/V、不缓存别的?因为历史 token 的 K/V 算出来就不再变:第 3 个 token 的 K/V,不管后面又来了多少新词,它都是那个值,理应只算一次、存着复用。而当前 token 的 Q 是每步新算的(针对"此刻要预测谁"),没必要缓存。这种"不变的就缓存、每步变的就现算"的划分,是 KV cache 高效的根本。

prefill 阶段(L03)正是一次性把整段 prompt 的 K/V 填进缓存:把 prompt 的几十上百个 token 一批喂进去(L18),并行算出它们各自的 K/V、全部写进 cell。填完,缓存里就有了整段 prompt 的记忆,之后 decode 逐字生成时,每个新词都能直接读到这份历史,不必回头重算 prompt。

顺带把"K/V"这俩字母的来历也点一下:它们来自数据库式的"键-值"(key-value)类比——K 像索引(拿 Q 去和它匹配、找相关的位置),V 像被取出的内容(按匹配权重汇总)。这个类比不必抠太死,但它能帮你记住:缓存 K/V,就是缓存"每个历史位置的可被检索的内容"。

把"平方 vs 线性"的差距再具体感受一下:生成第 1000 个 token 时,没缓存的话要把前 999 个的 K/V 全重算一遍,越往后每步越慢、整体是平方级;有缓存则第 1000 步和第 10 步一样,都只算一个新 token,整体线性。对动辄几千 token 的长对话,这个差距就是"跑得动"和"卡死"的分界。

cell 怎么管:pos 与 seq_id

KV cache 内部是一格格的 cell,每个 cell 存一个位置的 K/V。管理这些 cell 的是 llama_kv_cells:它记着每个 cell 的位置 pos 和所属的序列 seq_id;缓存还有个滚动写指针 head,标记下一个该往哪写。

cells:每格记 pos + seq_id,head 是滚动写指针
cellpos0/seqApos1/seqApos2/seqBhead -> 空
// 简化自 src/llama-kv-cells.h / src/llama-kv-cache.h
class llama_kv_cells {            // 管理一格格 cell
    std::vector<llama_pos> pos;    // 每个 cell 的位置
    // 每个 cell 还记: 属于哪些 seq_id
};
class llama_kv_cache : llama_memory_i {  // src/llama-kv-cache.h
    llama_kv_cells_vec v_cells;   // 实际存储(可多序列)
    // head(): 滚动写指针
};

注意 llama_kv_cache 派生自 llama_memory_i——这个基接口很重要,后面讲变体时会回到它。每个 cell 带 pos,是因为注意力的 rope(L16)和因果掩码都要知道"这个 K/V 是第几位的";每个 cell 带 seq_id,是为了支持多序列(下面讲)。

每步 decode,build_attn(L16)算出新 token 的 K/V 后,就写进 head 指的那个 cell、把 head 往后挪一格;做注意力时,再从所有属于本序列的 cell 里读出历史 K/V。所以 KV cache 不是被动的存储,而是每步都在增长、每步都被读取的活动记忆。把连续几步画出来,"只新算一格"就一目了然:

追踪 KV cache 增长:每生成一个 token 只新算一格 K/V,前面的原样复用、不重算(K0、K1… 表示各位置的 K/V)。
① 第 n 步
K0K1K2
已有 3 格
+1 token
② 第 n+1 步
K0K1K2K3
只新算 K3,前 3 格复用
+1 token
③ 第 n+2 步
K0K1K2K3K4
只新算 K4,前 4 格复用

cell 和 token 是一一对应的:序列里第 i 个 token,就占缓存里某个 cell,存着它(其实是每一层都有一份)的 K 和 V。所以"缓存有多大"约等于"能记多少个 token 的 K/V",这也是 n_ctx(上下文长度,L17)的含义——它就是缓存能容纳的 cell 数上限。

🔬 细节 / 源码对应
head 这个滚动写指针还有个作用:当某些 cell 被释放(比如某条序列结束、被 seq_rm 删掉),它们就空出来了,新 token 可以复用这些空位,而不必一味往后涨。所以缓存不是只进不出的,而是像一块可回收的"记忆田":用过的格子腾出来,又能种新的。这让多序列来来去去时,缓存能被反复利用。

做注意力时"只读本序列的 cell"也值得说清:缓存里可能混着好几条序列的 cell,但当前 token 只该看自己这条序列、且位置在自己之前的 K/V。前者靠 seq_id 过滤、后者靠因果掩码(L11 的 soft_max_ext)。两道过滤一叠加,就保证了"各序列互不串味、每个 token 只看得到过去"。

再强调一句"每一层都有一份":一个 transformer 有几十层,每一层都有自己的注意力、各自要缓存一套 K/V。所以一个 token 的"记忆"其实是几十份(每层一份)K/V 的集合。这也是为什么深一点的模型 KV cache 特别大——层数直接乘进了缓存大小里(深挖 1 的乘式里那个"层数"就是它)。

序列操作与上下文移位

KV cache 不只是"往里写",还能被编辑。一组序列操作让你删、复制、保留、平移某条序列的 K/V,对应公开 C API 的 llama_memory_seq_*(经 llama_get_memory 拿到记忆对象)。

seq_rm
删一段 KV
->
seq_cp / seq_keep
复制 / 只留某序列
->
seq_add
上下文移位
pos 平移
# 伪代码: 序列操作(公开 C API)
mem = llama_get_memory(ctx)
llama_memory_seq_rm (mem, seq, p0, p1)      # 删 [p0,p1) 这段 KV
llama_memory_seq_add(mem, seq, p0, p1, d)   # 上下文移位: 把 pos 平移 d
⚠ 注意
这套公开 API 的名字是 llama_memory_seq_*——旧版本叫 llama_kv_self_seq_*,已经改名移除了,看老教程时容易踩这个坑。改名本身也透露了演进方向:从"KV cache"这个具体概念,抽象成了更一般的"memory"。

这里最值得理解的是上下文移位(context shift)。当对话长到超过 n_ctx,缓存满了怎么办?一种办法就是:丢掉最旧的一段 K/V,把剩下那些 token 的 pos 整体往前挪(seq_add 一个负的位移),腾出尾部空间继续生成。这样既不用"满了就停",也不必重算整段——只是把记忆的窗口往前滑了一下。

seq_cp(复制序列)有个巧妙用途:共享前缀。比如一个很长的 system prompt,要同时生成好几个不同回答,可以先把它算一遍、存进序列 0,再 seq_cp 复制给序列 1、2、3……于是几条序列共享同一段前缀的 KV,不用各算一遍。这是服务器省算力的常用招数。

🌍 宏观理解
seq_keep(只保留某序列)则常用于清场:服务器处理完一批请求,想只留下某一条、把其余的 KV 全清掉,一句 seq_keep 就行。这些序列操作合起来,让 KV cache 不是一块只能整体清空的死内存,而是可以按序列精细增删的活内存——这正是高并发服务调度的底层支撑。

再回到那次改名:从 llama_kv_self_*llama_memory_seq_*,不只是换个名字,而是概念的抽象升级。早期只有一种"KV cache",所以 API 就叫 kv;后来出现了 recurrent 这种"不是 KV、但也是记忆"的东西,于是把名字提升到更一般的 memory。一次改名,记录的是这个引擎从"只支持 transformer"到"也能容纳别的架构"的成长。

那几个序列操作里常见的 p0p1位置范围:很多操作不是对整条序列、而是对"第 p0 到 p1 位"这一段做。比如只删一段、只移一段。这种"按位置区间操作"的精细度,让引擎能做很多花活——比如只回滚最近几个 token(撤销)、只移动中间一段。把记忆做成可按区间编辑的,灵活性就出来了。

多序列与变体

因为每个 cell 都带 seq_id一个 KV cache 能同时装多条序列:它们的 cell 混在同一块缓存里、靠 seq_id 区分,各做各的注意力(只看自己序列的 cell)。这就是 L18 多序列、服务器多并发的内存基础。

变体思路文件
标准全注意力,每个 token 都缓存llama-kv-cache
iSWA 滑窗只保留最近一窗的 K/Vllama-kv-cache-iswa
recurrent固定大小状态,不随长度涨llama-memory-recurrent
hybrid混合上面几种llama-memory-hybrid

为什么需要这么多变体?因为标准全注意力的 KV cache 虽然把计算降成了线性,内存却仍随长度线性增长——上下文越长越占显存。滑窗(iSWA)只留最近一窗、recurrent 用固定状态、hybrid 混搭,都是为长上下文省内存的不同取舍。它们都实现同一个基接口 llama_memory_i,于是可以整体替换、引擎其余部分不变。

多序列"不串味"再强调一遍,因为它是并发的关键:三条序列的 cell 虽然挤在同一块缓存里,但每条序列做注意力时,seq_id 过滤让它只看见自己的 cell,仿佛缓存里只有它一条。于是一块物理缓存,逻辑上被切成了互不可见的多份。这种"物理共享、逻辑隔离",和 L17 的 context 共享权重是同一种省内存哲学。

🔬 细节 / 源码对应
稍微展开滑窗(iSWA):它的想法是"太远的历史就别记了"——只保留最近一个固定大小窗口内的 K/V,更旧的丢掉。这对很多任务够用(近处的上下文最重要),却能把 KV cache 的内存从"随长度涨"压成"恒定一个窗口"。有些模型是逐层混合的:一部分层用滑窗、一部分用全注意力,兼顾省内存和长程记忆。

recurrent 变体则更彻底:它对应的是 Mamba 这类非 transformer 的架构,用一个固定大小的状态来概括"到目前为止的全部历史",状态大小完全不随上下文长度变。这从根本上绕开了 KV cache 随长度涨的问题,代价是状态是"压缩过的历史"、不像全注意力那样能精确回看每个 token。把它也纳入 llama_memory_i,正是这套抽象的威力——连"记忆的根本机制都不同"的架构,都能接进同一个引擎。

把 KV cache 放回整条推理链:L18 的 batch 把新 token 喂进来 -> L16 的 build_attn 算出它的 K/V、写进 KV cache,又从 KV cache 读出历史 -> 算完更新缓存、推进一步。KV cache 就是那块被每一步反复读写的记忆,是自回归循环里"承上启下"的状态核心。前面所有部件,最后都围着它转。

🌍 宏观理解
最后给第四部分(上)画个句号。回头看这六课,其实是顺着"一个模型怎么活起来"走了一遍:L14 把文件加载成张量、L15 认出它是什么架构、L16 把它拼成计算图、L17 用 context 把它装进运行时、L18 用 batch 喂它该算什么、L19 用 KV cache 让它记住前文。六块拼在一起,就是一台能持续逐字生成的推理机的内部全貌。下半部分(M4b:分词、采样、聊天模板、语法、LoRA)会继续往"文本进、文本出"的两端展开。

还有一点值得知道:KV cache 的内存布局,和 L11 提过的 flash attention 这类优化是配合的——把 K/V 在内存里排得规整,注意力内核才能高效地一块块读、边读边算。所以 KV cache 不只是"存得下"就行,它怎么排也直接影响注意力算得快不快。存储布局和计算内核,在这一层是互相迁就的一对。

1 KV cache 为什么这么吃显存? 点击展开

因为它要存的东西很多:每个 token、每一层、每个 KV 头,都要存一份 K 和一份 V。把这些乘起来——上下文长度 × 层数 × KV 头数 × 每头维度 × 2(K 和 V)——就是 KV cache 的大小。上下文一长,这个乘积就很可观,常常比模型权重之外最大的那块内存还大。

所以有两条省显存的路(L17 cparams 里见过):一是把 KV 量化存(type_k/type_v 从 16 位降到 8 位甚至更低,直接减半再减半);二是减小 n_ctx(少缓存几个 token)。L15 还提过 GQA——让 KV 头数远少于 Q 头数,从源头上就把 KV cache 缩小了。

理解了"KV cache 大小 = 长度 × 层 × KV头 × ..."这个乘式,你就能从一个模型的超参,估出开多长上下文会吃多少显存——这是部署大模型时一笔最该会算的账。

2 上下文移位到底是怎么回事? 点击展开

场景是这样:你和模型聊了很久,token 数眼看要超过 n_ctx(缓存装不下了)。最朴素的做法是停下来,但那体验很差。上下文移位提供了另一条路:丢掉最旧的一段对话(删掉那些 cell),把剩下保留部分的 pos 整体往前移,让位置重新从小开始排,尾部就空出了新位置。

关键是这只动位置标记、不重算 K/V 本身——seq_add 把一段 token 的 pos 平移一个量,缓存里的 K/V 内容不变,只是它们"对应的位置"变了。配合 rope 的相对位置性质,移位后模型还能正常往下接。所以它是一种"用很小代价续命"的手段。

当然,丢掉最旧的对话意味着模型会"忘记"开头说过的话——这是滑动窗口式记忆的固有代价。要不要移位、丢多少,是在"无限对话"和"记住全部"之间的权衡。顺带一提,更激进的整理(defrag)在新版里已被简化移除,defrag_thold 参数也标了弃用。

3 为什么要把"记忆"抽象成 llama_memory_i? 点击展开

因为"怎么记住前文"其实有很多种策略,而引擎的其余部分(建图、执行、采样)不该关心用的是哪种。把它们共同的行为(写入新 token、读出历史、删/移序列)抽象成一个基接口 llama_memory_i,标准 KV cache、滑窗、recurrent、hybrid 都去实现它。

于是"换一种记忆策略"就变成"换一个实现 llama_memory_i 的类",llama_context 持有的那个 memory(L17)指向哪个实现,引擎照常调同一套接口。这正是 L17 说"字段名叫 memory 而非 kv_cache"的原因——它留好了容纳各种记忆策略的余地。

这又是一处熟悉的解耦:和 L12 的 type_traits(用接口容纳几十种量化)、L16 的 build_arch_graph(用虚函数容纳几十种架构)一脉相承。把"会变的策略"收进一个统一接口,把"不变的主干"留在外面——这是贯穿整个 llama.cpp 的设计母题,到 KV cache 这里又见到一次。

✅ 关键要点
  • KV cache 缓存历史 K/V,让自回归每步只算新 token(线性而非平方)——decode 快的根本(L03/L04)。
  • cell 存 pos+seq_id(由 llama_kv_cells 管),head 是滚动写指针;llama_kv_cache 派生自 llama_memory_i
  • 序列操作 seq_rm/seq_cp/seq_keep/seq_add(移位);公开 API 是 llama_memory_seq_*(旧名 llama_kv_self_* 已移除)。
  • 上下文移位:丢旧段、平移 pos,腾空间继续生成,不重算。
  • 每 cell 带 seq_id => 多序列共享一块缓存;变体 iswa/recurrent/hybrid 为长上下文省内存,均实现 llama_memory_i
💡 设计洞察
把"算过的别再算"做成一块带 cell 管理的缓存——自回归从"每步重算整段"降到"每步增量更新",这是大模型能逐字流式输出的根本。而把这块记忆抽象成 llama_memory_i 接口,又让"怎么记忆"(全注意力/滑窗/recurrent/hybrid)能整体替换、引擎其余不变—— 正是 L12、L16 那条"统一接口容纳多种策略"的母题,在"记忆"层的又一次回响。第四部分(上)到此结束:从加载(L14)、架构(L15)、建图(L16)、上下文(L17)、批处理(L18)到 KV cache(L19),你已经看清了一个 GGUF 模型如何变成一台能持续生成的推理机。

🧪 自测 · 想一想为什么这么设计

1. KV cache 解决什么问题?
  1. 压缩权重
  2. 加速模型加载
  3. 缓存先前 token 的 K/V,让自回归每步只算新 token(不重算整段)
  4. 缓存 logits
看答案与解析 点击展开
答案:C。没缓存每步要重算前面所有 token 的 K/V(随长度平方涨);有缓存则每步只算新 token、读历史 K/V(线性)。L04 证明二者数值等价但快一个数量级,是 decode 快的根本。
2. 一个 cell 主要记录什么?
  1. 权重
  2. 文件偏移
  3. 采样概率
  4. 这个位置的 pos、属于哪些 seq_id(以及对应的 K/V)
看答案与解析 点击展开
答案:D。llama_kv_cells 管理一格格 cell,每个 cell 记 pos(喂 rope/因果掩码)和所属 seq_id(支持多序列);head 是滚动写指针。pos+seq_id 是 cell 的核心标识。
3. 公开 C API 里删除某序列的 KV 用哪个?
  1. llama_memory_seq_rm(经 llama_get_memory)
  2. llama_kv_self_rm(已改名移除)
  3. llama_free
  4. llama_decode
看答案与解析 点击展开
答案:A。序列操作的公开 API 是 llama_memory_seq_*(seq_rm/seq_cp/seq_add 等),经 llama_get_memory 拿到记忆对象;旧名 llama_kv_self_* 已改名移除,看老教程要小心。
💭 发散思考(没有标准答案,动手或动脑想想)
  • KV cache 很吃显存。结合 L17 的 type_k/type_v 和本课的滑窗变体,说说有哪些办法控制 KV 的内存。

Autoregression computes only one new token per step, all thanks to caching prior tokens' K/V - that is the KV cache (the "memory" L03/L04 kept mentioning). This lesson digs into llama-kv-cache: how cells are managed, how to shift when the context fills, how multiple sequences coexist, and the various variants for long context. It closes Part 4 and is the physical basis for a large model "remembering the earlier text".

Why close with the KV cache? Because it is a VRAM heavyweight and the key to real capabilities like long context and concurrency. L17 said the context holds it, L18 said the batch writes into it - this lesson takes that "memory" itself fully apart.

🔌 Analogy
The KV cache is like meeting minutes: each speaker (token) gets their points (K/V) written on a new line (cell), tagged with which utterance (pos) and who spoke (seq_id). To continue the conversation, just read the minutes, no need to re-hear the whole meeting. When the meeting runs too long to fit, scroll the pages (sliding window) or strike out someone's remarks (seq_rm). These minutes are the ledger by which the model "remembers the earlier text".

Why a KV cache is needed

First, what it solves. Attention has "the current token" look at "all earlier tokens", needing each historical token's K (key) and V (value). Without a cache, generating each new word recomputes all prior tokens' K/V - and as length grows a bit, the recompute grows quadratically.

recompute vs cache: caching turns "recompute the whole segment each step" into "compute only the new token each step"
no cacherecompute t0..tn K/V each stepgrows quadratically
with cachecompute only the new token's K/Vread historical K/Vgrows linearly

With a cache, each prior token's K/V is computed once and stored, and each step only computes the new token's K/V, appends it to the cache, and reads all historical K/V for attention. So per-step compute drops from "recompute the whole segment" to "compute one" - exactly what L04 proved: caching and recompute are numerically identical, but an order of magnitude faster.

This also directly explains L03's "why decode is fast": decode adds only one token per step, reusing history via the KV cache, so a step does just one token's forward. In short, without the KV cache there is no practical autoregressive generation - it is the key piece turning "computable in theory" into "actually runnable".

A quick recap of what K/V are (L04/L11): in attention, the current token uses its own Q (query) to dot-product with each historical token's K (key), computing "who to attend to", then weight-sums each historical token's V (value) by those weights. So "looking at history" needs exactly each historical token's K and V - cache them and you avoid recomputing each step.

🌍 Big picture
Why cache only K/V and nothing else? Because a historical token's K/V do not change once computed: the 3rd token's K/V, no matter how many new words follow, stay that value, deserving to be computed once and reused. The current token's Q is recomputed each step (for "who to predict right now"), no need to cache. This "cache the unchanging, compute the per-step-changing fresh" division is the root of the KV cache's efficiency.

The prefill phase (L03) is exactly filling the cache with the whole prompt's K/V at once: feed the prompt's tens-to-hundreds of tokens as a batch (L18), compute their K/V in parallel, and write them all into cells. Once filled, the cache holds the whole prompt's memory, and later when decode generates word by word, each new word reads this history directly without recomputing the prompt.

A note on where the letters "K/V" come from: a database-style "key-value" analogy - K is like an index (Q matches against it to find relevant positions), V like the retrieved content (summed by match weights). Do not press the analogy too hard, but it helps you remember: caching K/V is caching "each historical position's retrievable content".

Feel the "quadratic vs linear" gap concretely: generating the 1000th token, without a cache you would recompute all prior 999 tokens' K/V, each step slower than the last, quadratic overall; with a cache, step 1000 is like step 10, both computing just one new token, linear overall. For long conversations of thousands of tokens, this gap is the line between "runnable" and "frozen".

How cells are managed: pos and seq_id

Inside, the KV cache is a grid of cells, each storing one position's K/V. Managing them is llama_kv_cells: it records each cell's position pos and the sequence seq_id it belongs to; the cache also has a rolling write pointer head, marking where to write next.

cells: each records pos + seq_id, head is the rolling write pointer
cellpos0/seqApos1/seqApos2/seqBhead -> empty
// simplified from src/llama-kv-cells.h / src/llama-kv-cache.h
class llama_kv_cells {            // manages the grid of cells
    std::vector<llama_pos> pos;    // each cell's position
    // each cell also records: which seq_ids it belongs to
};
class llama_kv_cache : llama_memory_i {  // src/llama-kv-cache.h
    llama_kv_cells_vec v_cells;   // actual storage(multi-sequence capable)
    // head(): rolling write pointer
};

Note llama_kv_cache derives from llama_memory_i - this base interface matters, and we return to it for the variants. Each cell carries pos because attention's rope (L16) and causal mask both need to know "which position this K/V is"; each cell carries seq_id to support multiple sequences (below).

Each decode step, after build_attn (L16) computes the new token's K/V, it writes them into the cell head points to and advances head by one; for attention, it reads historical K/V from all cells belonging to this sequence. So the KV cache is not passive storage but an active memory that grows every step and is read every step. Draw a few consecutive steps and "only one new cell" becomes obvious:

Tracing KV-cache growth: each token computes just one new K/V cell; earlier cells are reused as-is, not recomputed (K0, K1... are each position's K/V).
(1) step n
K0K1K2
3 cells so far
+1 token
(2) step n+1
K0K1K2K3
only K3 is new, first 3 reused
+1 token
(3) step n+2
K0K1K2K3K4
only K4 is new, first 4 reused

Cells and tokens are one-to-one: the i-th token in a sequence occupies some cell in the cache, storing its (one per layer, actually) K and V. So "how big the cache is" roughly equals "how many tokens' K/V it can remember" - which is the meaning of n_ctx (context length, L17): the upper bound on cells the cache can hold.

🔬 Details / source
The head rolling write pointer has another role: when some cells are freed (e.g. a sequence ends and is removed by seq_rm), they become empty, and new tokens can reuse these slots rather than only growing forward. So the cache is not write-only but like a recyclable "memory field": used cells are freed and can be sown anew. This lets the cache be reused as sequences come and go.

"Reading only this sequence's cells" during attention is worth clarifying: the cache may mix several sequences' cells, but the current token should see only its own sequence's K/V at positions before itself. The former is filtered by seq_id, the latter by the causal mask (L11's soft_max_ext). The two filters together ensure "sequences do not mix flavors, and each token sees only the past".

Emphasize "one per layer": a transformer has dozens of layers, each with its own attention, each caching its own set of K/V. So a token's "memory" is actually a collection of dozens of K/V (one per layer). This is why a deeper model's KV cache is especially big - layer count multiplies straight into the cache size (the "layer count" in Dig-deeper 1's product is exactly this).

Sequence operations and context shift

The KV cache is not just "write into" - it can be edited. A set of sequence operations lets you remove, copy, keep, or shift a sequence's K/V, corresponding to the public C API llama_memory_seq_* (obtaining the memory object via llama_get_memory).

seq_rm
remove a span of KV
->
seq_cp / seq_keep
copy / keep only one sequence
->
seq_add
context shift
pos shifted
# pseudocode: sequence operations (public C API)
mem = llama_get_memory(ctx)
llama_memory_seq_rm (mem, seq, p0, p1)      # remove the [p0,p1) span of KV
llama_memory_seq_add(mem, seq, p0, p1, d)   # context shift: shift pos by d
⚠ Heads-up
The public API names are llama_memory_seq_* - older versions called them llama_kv_self_seq_*, now renamed and removed, so old tutorials trip on this. The rename also reveals the direction: from the concrete "KV cache" concept to a more general "memory".

The most worthwhile thing here is context shift. When a conversation grows past n_ctx and the cache fills, what then? One way: drop the oldest span of K/V, shift the remaining tokens' pos forward as a whole (a negative seq_add), and free tail space to keep generating. This avoids "stop when full" without recomputing the whole segment - it just slides the memory window forward a bit.

seq_cp (copy a sequence) has a clever use: shared prefix. Say a long system prompt must generate several different answers at once - compute it once into sequence 0, then seq_cp it to sequences 1, 2, 3... so several sequences share the same prefix's KV without each recomputing it. A common server compute-saving trick.

🌍 Big picture
seq_keep (keep only one sequence) is often used to clear the field: a server, done with a batch of requests, wanting to keep only one and clear all others' KV, does it with one seq_keep. These sequence operations together make the KV cache not a dead memory clearable only wholesale, but a live memory that can be finely added to and removed by sequence - the underlying support for high-concurrency service scheduling.

Back to that rename: from llama_kv_self_* to llama_memory_seq_* is not just a name change but a conceptual abstraction upgrade. Early on there was only one "KV cache", so the API was called kv; later came recurrent, a "not-KV but still memory" thing, so the name was lifted to the more general memory. One rename records this engine's growth from "supporting only transformers" to "also accommodating other architectures".

The p0, p1 common in those sequence operations are a position range: many operations act not on a whole sequence but on the "positions p0 to p1" span - removing only a span, shifting only a span. This "operate by position interval" granularity lets the engine do many tricks - rolling back only the last few tokens (undo), shifting only a middle span. Making memory editable by interval is where the flexibility comes from.

Multiple sequences and variants

Because each cell carries seq_id, one KV cache can hold multiple sequences at once: their cells mix in the same cache, distinguished by seq_id, each doing its own attention (seeing only its own sequence's cells). This is the memory basis for L18's multi-sequence and a server's concurrency.

variantideafile
standardfull attention, cache every tokenllama-kv-cache
iSWA sliding windowkeep only the most recent window's K/Vllama-kv-cache-iswa
recurrentfixed-size state, not growing with lengthllama-memory-recurrent
hybrida mix of the abovellama-memory-hybrid

Why so many variants? Because while standard full-attention's KV cache makes compute linear, its memory still grows linearly with length - the longer the context, the more VRAM. Sliding window (iSWA) keeps only a recent window, recurrent uses fixed state, hybrid mixes - all different trade-offs to save memory for long context. They all implement the same base interface llama_memory_i, so they can be swapped wholesale with the rest of the engine unchanged.

Emphasize multi-sequence "no mixing" once more, as it is the key to concurrency: three sequences' cells crowd into the same cache, but when each sequence does attention, the seq_id filter lets it see only its own cells, as if the cache held only it. So one physical cache is logically split into mutually-invisible portions. This "physically shared, logically isolated" is the same memory-saving philosophy as L17's context sharing weights.

🔬 Details / source
Expand on the sliding window (iSWA) a bit: its idea is "do not remember history too far back" - keep only the K/V within a recent fixed-size window, dropping older ones. This suffices for many tasks (nearby context matters most) yet compresses the KV cache memory from "growing with length" to "a constant window". Some models are layer-mixed: some layers use a sliding window, some full attention, balancing memory savings and long-range memory.

The recurrent variant goes further: it corresponds to non-transformer architectures like Mamba, using a fixed-size state to summarize "all history so far", the state size not changing with context length at all. This fundamentally sidesteps the KV cache's growth-with-length problem, at the cost of the state being "compressed history" - not able to look back precisely at each token as full attention can. Folding it too into llama_memory_i is exactly this abstraction's power - even architectures whose "fundamental memory mechanism differs" can plug into the same engine.

Put the KV cache back into the whole inference chain: L18's batch feeds a new token in -> L16's build_attn computes its K/V, writes them into the KV cache, and reads history back out -> after computing, updates the cache and advances one step. The KV cache is that memory read and written every step, the state core that "links past and future" in the autoregressive loop. All the earlier components, in the end, revolve around it.

🌍 Big picture
Finally, a full stop for Part 4a. Looking back at these six lessons, they walk through "how a model comes alive": L14 loads the file into tensors, L15 recognizes which architecture it is, L16 assembles it into a compute graph, L17 packs it into a runtime via the context, L18 feeds it what to compute via the batch, L19 lets it remember the earlier text via the KV cache. The six together are the full internal picture of an inference machine that keeps generating word by word. The second half (M4b: tokenizer, sampling, chat template, grammar, LoRA) continues out toward the "text in, text out" ends.

One more thing worth knowing: the KV cache's memory layout works together with optimizations like flash attention (mentioned in L11) - laying K/V tidily in memory is what lets the attention kernel read block by block and compute as it reads efficiently. So the KV cache is not just about "fitting"; how it is laid out also directly affects how fast attention computes. Storage layout and compute kernel are a mutually-accommodating pair at this layer.

1 Why does the KV cache eat so much VRAM? Click to expand

Because it stores a lot: every token, every layer, every KV head needs one K and one V. Multiply these - context length x layer count x KV head count x per-head dim x 2 (K and V) - and that is the KV cache size. As context grows long, this product is considerable, often the biggest memory block aside from the model weights.

So there are two routes to save VRAM (seen in L17's cparams): one, store KV quantized (type_k/type_v from 16-bit to 8-bit or lower, halving and halving again); two, reduce n_ctx (cache fewer tokens). L15 also mentioned GQA - making KV heads far fewer than Q heads, shrinking the KV cache at the source.

Understand "KV cache size = length x layers x KV heads x ..." and you can estimate from a model's hyperparameters how much VRAM a given context length eats - the most worth-knowing account when deploying a large model.

2 What exactly is context shift? Click to expand

The scenario: you have chatted with the model for a while, and the token count is about to exceed n_ctx (the cache cannot hold more). The naive move is to stop, but that is a poor experience. Context shift offers another way: drop the oldest span of conversation (remove those cells), shift the remaining part's pos forward as a whole so positions restart from small, freeing new positions at the tail.

The key is this only moves position tags, not recomputing the K/V themselves - seq_add shifts a span of tokens' pos by an amount, the cached K/V content unchanged, only their "corresponding positions" changing. With rope's relative-position nature, the model continues normally after the shift. So it is a "buy more life at small cost" technique.

Of course, dropping the oldest conversation means the model "forgets" what was said at the start - the inherent cost of sliding-window memory. Whether to shift and how much to drop is a trade-off between "endless conversation" and "remember everything". By the way, more aggressive compaction (defrag) has been simplified away in newer versions, and the defrag_thold parameter is marked deprecated.

3 Why abstract "memory" into llama_memory_i? Click to expand

Because "how to remember the earlier text" actually has many strategies, and the rest of the engine (graph-building, execution, sampling) should not care which is used. Abstracting their common behavior (write a new token, read history, remove/shift a sequence) into a base interface llama_memory_i, the standard KV cache, sliding window, recurrent, and hybrid all implement it.

So "switch memory strategies" becomes "switch the class implementing llama_memory_i", and whichever implementation the memory held by llama_context (L17) points to, the engine calls the same interface as usual. This is exactly why L17 said "the field is named memory not kv_cache" - it left room to hold various memory strategies.

This is another familiar decoupling: of a piece with L12's type_traits (one interface holding dozens of quantizations) and L16's build_arch_graph (a virtual function holding dozens of architectures). Folding "the varying strategy" into a unified interface and keeping "the invariant trunk" outside - a design motif running through all of llama.cpp, seen once more at the KV cache.

✅ Key points
  • The KV cache caches historical K/V so autoregression computes only the new token each step (linear, not quadratic) - the root of decode's speed (L03/L04).
  • Cells store pos+seq_id (managed by llama_kv_cells), head is the rolling write pointer; llama_kv_cache derives from llama_memory_i.
  • Sequence ops seq_rm/seq_cp/seq_keep/seq_add (shift); the public API is llama_memory_seq_* (the old llama_kv_self_* is removed).
  • Context shift: drop the old span, shift pos, free space to keep generating, no recompute.
  • Each cell carrying seq_id => multiple sequences share one cache; variants iswa/recurrent/hybrid save memory for long context, all implementing llama_memory_i.
💡 Design insight
Making "do not recompute what was computed" into a cell-managed cache - autoregression drops from "recompute the whole segment each step" to "incremental update each step", the root of a large model's word-by-word streaming. And abstracting this memory into the llama_memory_i interface lets "how to remember" (full attention / sliding window / recurrent / hybrid) be swapped wholesale with the rest of the engine unchanged - exactly L12's and L16's "one interface holding many strategies" motif, echoing once more at the memory layer. Part 4a ends here: from loading (L14), architecture (L15), graph-building (L16), context (L17), batching (L18), to the KV cache (L19), you have seen how a GGUF model becomes a machine that keeps generating.

🧪 Self-test - think about the design

1. What problem does the KV cache solve?
  1. compresses weights
  2. speeds up model loading
  3. caches prior tokens' K/V so autoregression computes only the new token each step (no whole-segment recompute)
  4. caches logits
Show answer & explanation click to expand
Answer: C. Without a cache each step recomputes all prior tokens' K/V (quadratic in length); with one, each step computes only the new token and reads historical K/V (linear). L04 proves they are numerically equivalent but an order of magnitude faster - the root of decode's speed.
2. What does a cell mainly record?
  1. weights
  2. a file offset
  3. sampling probabilities
  4. this position's pos, which seq_ids it belongs to (and the corresponding K/V)
Show answer & explanation click to expand
Answer: D. llama_kv_cells manages the grid of cells; each records pos (fed to rope/causal mask) and its seq_id (for multi-sequence); head is the rolling write pointer. pos+seq_id are a cell's core identity.
3. Which public C API removes a sequence's KV?
  1. llama_memory_seq_rm (via llama_get_memory)
  2. llama_kv_self_rm (renamed and removed)
  3. llama_free
  4. llama_decode
Show answer & explanation click to expand
Answer: A. The public sequence-op API is llama_memory_seq_* (seq_rm/seq_cp/seq_add etc.), via llama_get_memory; the old llama_kv_self_* names are renamed and removed, so beware old tutorials.
💭 Open questions (no single right answer - just think or try)
  • The KV cache is VRAM-hungry. Drawing on L17's type_k/type_v and this lesson's sliding-window variant, what are the ways to control KV memory?