🦙 llama.cpp 图解教程llama.cpp Visual Guide 第二部分 · 前置基础Part 2 · Foundations 04 / 40
第二部分 · 前置基础Part 2 · Foundations

大模型推理基础LLM inference fundamentals

课 03 看清了"循环怎么一圈圈转"。这一课往下钻一层:一个 transformer block 内部到底在算什么、 "自回归"为什么能成立,以及最关键的——KV cache 凭什么是精确的,而不是一种"差不多就行"的近似。 看懂这一层,你就明白课 03 那个循环为什么"可以这么省"。

🔌 生活类比
自回归生成像玩文字接龙:你每次只看已经写出来的那段话,猜出下一个最合适的字,写下去; 然后把这个新字也算进"已写内容",再猜下一个。模型也是这样——它永远只根据"到目前为止的全部 token" 预测"下一个 token",写一个、回头看一遍、再写一个。正因为"每一步都把自己刚写的字也读进去", 模型才会前后连贯——这也是"自回归"里"自"字的由来:用自己产生的输出,作为下一步的输入。

decoder-only:一个 block 在算什么

llama 系模型(和当下绝大多数 LLM)都是 decoder-only 结构。把它拆开,数据从下往上是一条很直的链:词 id 先查成向量, 再穿过许多层结构相同的 block,最后投影回词表,得到每个候选词的分数。一层 block 内部又分两个子层—— 自注意力前馈网络(FFN)

1

词嵌入 Embedding

把每个 token id 查成一个稠密向量(词向量);模型后面所有运算都在这些向量上进行。

src/llama-graph.cpp · build_inp_embd

2

自注意力子层(每个 block 的前半,× N 个 block)

RMSNorm 归一化 -> 自注意力(含 RoPE 位置编码)-> 残差相加。token 之间唯一互相交流的地方就在这里。

ggml_rms_norm · ggml_rope · ggml_soft_max_ext

3

前馈子层(同一 block 的后半)

再一次 RMSNorm -> 前馈网络(SwiGLU)-> 残差相加。对每个 token 各自做一次非线性加工,位置之间互不影响。

4

末层归一化 Final Norm

所有 block 叠完后,再过一道 RMSNorm,稳定输出的数值尺度。

5

输出投影 -> logits

用输出层(lm_head)把向量投影回词表大小,得到每个候选 token 的分数(logits)。

llama_get_logits_ith 取出

🌍 宏观理解
这里有个贯穿全课的关键认识:只有自注意力这一层,token 之间才会"互相看见"。FFN、归一化、投影都是对每个 token 各算各的,位置之间互不影响;唯独注意力会让"当前 token"去参考"其它 token"。所以"模型怎么利用上下文"这件事,全发生在注意力层——这也是后面因果掩码、KV cache 都围着注意力打转的原因。那两条残差支路和归一化也别小看:残差让梯度能顺畅地穿过几十层不衰减,归一化把每层输入拉回稳定范围,二者合起来才让"把很多层 block 叠很深"这件事在训练上变得可行。

顺带说说词嵌入为什么重要。它不是简单的"查字典编号",而是把每个 token 映射到高维空间里的一个点, 语义相近的词,向量也彼此靠近——这正是模型"理解"语言的起点。原始词向量只带"这个词大概什么意思",还不含上下文; 真正让它"读懂整句"的,是随后那几十层 block。你可以把多层 block 想成一条逐级精炼的流水线:浅层更多在抓局部搭配与语法 (谁修饰谁、短语边界在哪),深层逐渐组合出更抽象的语义与长程关系(指代、逻辑、主题)。每过一层,token 的向量就被 "注入更多来自上下文的信息";到顶层时,最后一个位置的向量已经浓缩了"接下来最该说什么"的全部线索,投影到词表就成了 logits。

🔌 生活类比
再把注意力到底在干嘛说透一点:它像在做一次"带权重的检索"。当前 token 拿着自己的 Query(一个"我想找什么"的提问),去和每个历史 token 的 Key(一个"我是什么"的标签)逐一匹配,越像就给越高的注意力权重;然后按这些权重,把各历史 token 的 Value("我能提供的内容")加权求和,汇成当前 token 的新表示。于是"理解上下文"被落实成了"该重点参考前文哪几个词"。这也再次说明:为什么注意力是 token 之间唯一的交流通道——只有在这里,一个 token 的输出才真正取决于别的 token。

把这套"检索"用一个具体例子走一遍:当前 token 给 3 个历史 token 打分,分数越高、softmax 之后的权重越大,最后按权重把它们的 Value 加权汇总成一个向量。

追踪一次注意力:当前 token 拿 Q 给 3 个历史 token 打分 → softmax 成权重 → 按权重汇总它们的 V(数字为示意)。
打分:Q · K (越像分越高) softmax 权重 (和=1) k1 4.0 k2 1.0 k3 2.0 softmax 0.84 0.04 0.11 上下文 向量 ·v1 ·v2 ·v3 out = 0.84·v1 + 0.04·v2 + 0.11·v3

还有个常被忽略的点:注意力本身并不知道词的先后顺序——它只是在做"按相似度加权汇总",把同样几个词打乱顺序喂进去, 纯注意力算出的结果竟然一样。可语言显然讲究语序("狗咬人"和"人咬狗"天差地别)。位置信息就是为此补进来的:llama 系普遍用 RoPE(旋转位置编码),它不是简单地给每个位置加一个"序号向量",而是按位置给 Query / Key 做一次旋转, 让两个 token 的注意力分数自然带上"它们相距多远"的信息。这也是为什么前面 vflow 里,自注意力子层特意标了"含 RoPE"。

顺手把最后那一步输出投影也说明白:它把顶层那个 token 向量,乘上一个"词表大小 × 隐藏维度"的大矩阵(lm_head), 得到词表里每个 token 各一个分数——这就是 logits 的长度恒等于词表大小的原因。不少模型还让它和最底层的词嵌入矩阵 共享权重(weight tying),既省参数,又让"输入怎么编码"和"输出怎么打分"保持一致。

💡 实战
顺便建立一点"体量感":所谓 7B、13B 模型,参数量主要就堆在这些 block 里——每层的注意力投影矩阵、FFN 里两三个大矩阵,乘上几十层,再加上巨大的词嵌入与输出投影,加总就是几十亿个数。层数(n_layer)、隐藏维度(n_embd)、注意力头数这几个超参,基本决定了一个模型有多大、多强、跑起来多吃资源——后面看 GGUF 元数据时,你会在文件头里直接读到它们。

把一层 block 的前向写成伪代码,就是"两条带残差的支路":

# 一层 block 的前向: 两条带残差的支路
def layer(x):                 # x: [n_tokens, n_embd]
    a = attn(rms_norm(x))     # tokens talk to each other here
    x = x + a                 # residual
    f = ffn(rms_norm(x))      # per-token non-linear mix
    return x + f              # residual

因果掩码:只能回头看

既然注意力让 token 互相参考,那"写第 5 个字时能不能偷看第 6、第 7 个字"?绝对不能——生成时它们还不存在。 于是 decoder 给注意力加了一道因果掩码(causal mask):第 i 个 token 只允许注意到位置 <= i 的 token, 对"未来"的位置一律屏蔽。画成一张方格表,就是一个下三角

因果掩码:第 i 个 token 只能注意到位置 <= i 的 token;亮格=可见,灰格=屏蔽(在自己之后)
t1 看k1k2k3k4
t2 看k1k2k3k4
t3 看k1k2k3k4
t4 看k1k2k3k4

表里每一行是"某个 token 在看谁":亮格表示可以注意(不晚于自己),灰格表示被屏蔽(在自己之后)。 实现上很直接:算完注意力分数后,把所有"未来位置"的分数置成 -inf,再过 softmax,这些位置的权重就变成 0,等于没看。 这就是"自回归"在数学上的样子——每个位置的输出只依赖它及它之前的输入,绝不泄露未来。

🔬 细节 / 源码对应
因果掩码还有一个训练时的妙用,值得点破。训练时我们手里有完整的句子,可以把它整段一次喂进模型;因果掩码保证第 i 个位置只能看到 <= i 的词,于是"用前文预测下一个词"这件事,在每一个位置上同时成立——一次前向就拿到了全句每个位置的"下一个词"预测,可以并行地和真实的下一个词比对、算损失。这种"把答案也一起喂进去、但靠掩码挡住未来"的训练方式叫 teacher forcing,让训练极其高效。有意思的是:这和推理时的 prefill 是同一套机制——prompt 里每个 token 都已知,于是能像训练那样一次并行算完,只不过推理时我们只关心最后一个位置的输出。

也正因为训练和推理共用同一套前向,llama.cpp 里并没有"两套代码":无论是 prefill 一次喂进几百个 token, 还是 decode 每次只喂 1 个新 token,走的都是同一张计算图,区别只在"这一批有几个 token、要输出哪些位置"。 理解了这点,再回看课 03 的七步数据流,就明白它为什么能用一个 llama_decode 同时扛起这两种节奏。

llama.cpp 里这一步由 src/llama-graph.cppbuild_attn / build_attn_mha 拼进计算图,掩码通过 ggml_soft_max_ext 在求 softmax 时一并施加,简化出来大致是:

// 注意力打分 + 因果掩码 (对应 build_attn / build_attn_mha)
kq  = ggml_mul_mat(ctx, k, q);            // scores [n_kv, n_q]
kq  = ggml_soft_max_ext(ctx, kq, mask,    // mask: causal -inf on j>i
                        scale, max_bias);
kqv = ggml_mul_mat(ctx, v, kq);           // weighted sum of values

(以本仓库 2026-06-15 源码为准;真实实现散落在 llama-graph.cpp,这里只取主干、略去缩放与多头细节。)

自回归 + 为什么 KV cache 是精确的

把"每次只根据已有 token 预测下一个"画成回路,就是自回归循环:

已有 token
x1 … xn
->
前向
N 层 block
->
末位 logits
词表打分
->
采样
选一个 token
->
x(n+1)
回灌队尾, 继续

注意末端那条回灌箭头——新采样出的 token 被接到序列尾巴,成为下一轮的输入。这里就引出全课的"题眼": 每往后写一个字,是不是都要把前面所有字重新算一遍注意力?不需要。原因恰恰藏在因果掩码里。

注意力里每个 token 会算出三样东西:Query(拿去"问"的向量)、Key(被查的"标签")、Value(携带的"内容")。 当我们新增第 n+1 个 token 时,它要拿自己的 Query 去和"所有历史 token 的 Key"逐一比对,再按比对出的权重把对应的 Value 加权汇总。 关键在于:历史 token 的 K 和 V,只取决于它自己和它的位置,而因果掩码保证它"看不到"任何后来的 token—— 所以无论后面再追加多少新 token,前面那些 K/V 一个数都不会变。既然不变,就可以算一次、存下来、永远复用。这就是 KV cache:

KV cache 是精确的:新增 token 不改变任何历史 K/V,故缓存值与"从头重算"逐位相等
第 n 步K1K2K3
第 n+1 步K1(不变)K2(不变)K3(不变)+K4

看这两行——第 n+1 步相比第 n 步,只多出最右边一个高亮新格,左边那些全是"原封不动"的旧值。 所以 KV cache 不是一种"用精度换速度"的近似优化,而是一个数学上完全等价的复用:缓存里的值, 和"每步都从头重算"得到的值逐位相等。它省掉的是重复劳动,不是精度。顺带一提,预测下一个词时只需要 最后一个位置的那一行输出,所以取 logits 时只取末位:

logits = llama_get_logits_ith(ctx, -1);  // 只取最后一个位置, 返回 n_vocab 个分数
💡 实战
把这件事算笔账就更清楚了:没有缓存时,生成第 n 个 token 要重新为前面 n-1 个 token 全算一遍 K/V,整段生成下来总计算量大致是 n 的平方级别;有了缓存,每一步只为新来的那一个 token 算 K/V,历史部分直接取用,于是每步的新增计算基本是常数。举个直观的例子:聊到第 2000 个 token 时,没有缓存就得把前面近 2000 个 token 的 K/V 从头再算一遍,越聊越慢;有了缓存,第 2000 步和第 2 步需要新算的 K/V 一样多(都只有一个 token)。请注意:这只是把"算过的别再算",结果一个数都没变——这再次印证它是"精确复用"而非"近似加速"。

当然,KV cache 也不是白来的:它要为每一层、每个历史 token 各存一份 K 和一份 V,占用的显存/内存随 上下文长度线性增长。上下文开到几万 token 时,这块缓存会吃掉相当可观的一片显存——这也是"上下文窗口"为什么总有上限、 长上下文为什么格外吃硬件的原因之一(分配、写入与复用都在 src/llama-kv-cache.cpp)。所以工程上既要靠它省计算, 又得想办法压它的体积——下面深挖里的 GQA / MQA 正是干这个的,这是一对需要一直权衡的矛盾。

🌍 宏观理解
这里还藏着一个反直觉的事实:decode 阶段,瓶颈往往不是算力,而是"搬数据"。每生成一个 token,都要把模型那几个 GB 的权重、连同越来越大的 KV cache,从显存/内存里完整读一遍,可真正的计算量却只对应"一个 token"。于是单条对话的生成速度,更多是被内存带宽卡住,而不是被乘法次数卡住——这正好解释了为什么量化(把权重压小)能立竿见影地提速:要搬的字节少了,每步自然更快。下一部分讲量化时,我们会回到这条线索。

深入一点(选读)

下面三个问题,想深究的同学点开看;只想抓主线的可以先跳过。

1 为什么只有 decoder,没有 encoder? 点击展开

最初的 Transformer 是 encoder-decoder 结构,为翻译这类"读完整句再生成"的任务设计:encoder 双向读懂源句, decoder 据此逐词生成译文。但"预测下一个词"这件事并不需要双向——你写字时本来就只能依赖已经写出的部分。

GPT 类模型于是只保留 decoder,配上因果掩码做纯粹的"续写",结构更简单、训练目标更统一(始终是"猜下一个 token"), 特别适合生成式任务。llama.cpp 支持的开源大模型,几乎清一色是这种 decoder-only 架构。

反过来,只做"理解"不做"生成"的任务(如文本分类、检索、判断两句话是否同义),更适合 encoder-only (如 BERT)那种双向结构——它可以同时看左右两边的全部上下文;而"边读边写"的对话生成,则是 decoder-only 的主场。

2 MHA / GQA / MQA:KV cache 还能更省 点击展开

标准多头注意力(MHA)里,Query、Key、Value 都有同样多的"头"。但 KV cache 的大小正比于 Key/Value 头的数量, 于是出现了省内存的变体:GQA(分组查询注意力)让多个 Query 头共享一组 K/V 头, K/V 头数(n_head_kv)少于 Q 头数(n_head,见 src/llama-hparams.h)。

MQA 是极端情形——所有 Query 头只共享一组 K/V。头数越少,KV cache 越小、长上下文越省显存,代价是表达力略降。 如今多数大模型用 GQA 来折中:既显著压缩缓存,又几乎不掉效果。

为什么共享了还几乎不掉效果?直觉是:相邻的若干 Query 头往往在关注差不多的东西,让它们共用一组 K/V, 损失的表达力很有限,省下的显存却相当可观——这是一笔很划算的买卖,也是它能被广泛采用的原因。

3 logits 是什么?temperature 怎么作用 点击展开

logits词表上每个 token 的原始分数,可正可负、加起来也不等于 1,还不是概率。 要变成概率,得过一道 softmax(指数化再归一化)。

温度(temperature, T)正作用在 softmax 之前:把所有 logits 同时除以 T——T 大于 1 会把分布"摊平" (更随机、更有创意),T 小于 1 会把分布"拉尖"(更确定、更保守),T 趋近 0 就退化成"每次都选分数最高的那个"(贪心)。 所以同一份 logits,调温度就能在"稳重"和"放飞"之间滑动——这部分课 03 提过,后面还会有专门一课展开。

顺便记住一个对照:贪心每次都选分数最高的词,稳定但容易重复、显得呆板;带温度的采样引入随机性, 更生动也更容易"跑偏"。生成质量的调参,很大程度上就是在这两端之间找平衡。

✅ 关键要点
  • decoder-only = 词嵌入 + N 层(自注意力 + FFN)+ 末层归一化 + 投影到词表 logits
  • token 之间唯一互相交流的地方是自注意力;FFN / 归一化 / 投影都对每个 token 各算各的。
  • 因果掩码让第 i 个 token 只能注意到位置 <= i 的 token——这就是"自回归"的数学实现。
  • KV cache 是精确的、不是近似:因果掩码保证旧 token 的 K/V 不随新 token 改变,缓存值与从头重算逐位相等
  • 每步只需最后一个位置的输出,故取 logits 用 llama_get_logits_ith(ctx, -1)
💡 设计洞察
因果掩码这一个约束,同时换来了三样东西:prompt 可以整段并行地算(prefill)、历史 K/V 可以安全缓存(KV cache)、 以及"自回归"这种逐字生成的形式本身。llama.cpp 后面几乎所有推理优化,追根溯源都站在这一条"只能回头看"的规则之上。把这条规则吃透,后面 ggml 怎么建图、KV cache 怎么管理、长上下文怎么优化,你都能找到它的影子。

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

1. 为什么说 KV cache 是“精确优化”而不是“近似”?
  1. 因为只缓存了最后一层
  2. 因为因果掩码下旧 token 看不到新 token,它们的 K/V 不随新 token 改变
  3. 因为它把 logits 也缓存了
  4. 因为权重被量化了
看答案与解析 点击展开
答案:B。因果掩码使旧 token 的 K/V 与未来无关,故缓存它们是精确的、与从头重算逐位相等,不是近似。
2. decoder-only 模型里,token 之间“互相交流”主要发生在?
  1. FFN(前馈层)
  2. 输出投影
  3. 带因果掩码的自注意力层
  4. RMSNorm
看答案与解析 点击展开
答案:C。FFN、归一化、投影都对每个 token 独立处理,只有注意力让 token 互相看。
3. decode 时为什么用 llama_get_logits_ith(ctx, -1) 只取最后一个位置?
  1. 因为 -1 表示取平均
  2. 因为前面的位置算错了
  3. 因为只有最后一层有 logits
  4. 因为预测“下一个 token”只需要最后一个位置的输出,其余位置不用算输出
看答案与解析 点击展开
答案:D。自回归每步只产出一个新 token,对应的就是序列最后一个位置那一行 logits。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 如果去掉因果掩码(允许看到未来 token),自回归生成还成立吗?KV cache 还精确吗?

Lesson 03 showed how the loop turns, round after round. This lesson drills one level deeper: what a transformer block actually computes, why autoregression works at all, and - most importantly - why the KV cache is exact rather than a "good enough" approximation. Understand this layer and you see why that loop can be so cheap.

🔌 Analogy
Autoregressive generation is like a word-chain game: each time you look only at what has been written so far, guess the next best word, write it down; then count that new word as part of "what's written" and guess again. The model is the same - it always predicts the next token from all tokens so far, writes one, looks back, writes another. And because each step also reads back the word it just wrote, the model stays coherent - that is the "auto" in "autoregressive": using its own output as the next step's input.

decoder-only: what a block computes

llama-family models (like most LLMs today) are decoder-only. Unpacked, the data flows bottom-up in a straight chain: token ids are looked up into vectors, pushed through many identical blocks, then projected back to the vocabulary to score every candidate word. Inside one block there are two sub-layers - self-attention and the feed-forward network (FFN):

1

Embedding

Look up each token id into a dense vector (a word embedding); all later math runs on these vectors.

src/llama-graph.cpp - build_inp_embd

2

Self-attention sub-layer (first half of each block, x N blocks)

RMSNorm -> self-attention (with RoPE positions) -> residual add. This is the only place tokens talk to each other.

ggml_rms_norm - ggml_rope - ggml_soft_max_ext

3

FFN sub-layer (second half of the same block)

Another RMSNorm -> feed-forward network (SwiGLU) -> residual add. A per-token non-linear mix; positions do not interact here.

4

Final norm

After all blocks, one more RMSNorm to stabilize the output scale.

5

Output projection -> logits

The output head (lm_head) projects the vector back to vocabulary size, giving a score (logit) per candidate token.

read out with llama_get_logits_ith

🌍 Big picture
Here is the insight that runs through the whole lesson: only the self-attention layer lets tokens "see each other". FFN, normalization, and projection all work on each token independently; positions do not interact. So "how the model uses context" happens entirely in attention - which is why the causal mask and the KV cache both revolve around it. Don't overlook the two residual branches and the norms either: residuals let gradients flow cleanly through dozens of layers without fading, and norms keep each layer's inputs in a stable range; together they make stacking blocks very deep trainable in the first place.

A word on why embeddings matter. They are not a plain "dictionary lookup into an integer"; each token maps to a point in a high-dimensional space where semantically similar words land close together - the starting point of the model "understanding" language. A raw embedding only carries "roughly what this word means", with no context; what makes it "read the whole sentence" is the dozens of blocks that follow. Think of the stacked blocks as a refinement pipeline: shallow layers capture local collocations and syntax (what modifies what, phrase boundaries), deeper layers compose more abstract semantics and long-range relations (coreference, logic, topic). Each layer injects more context into a token's vector; by the top, the last position's vector has distilled every clue about "what to say next", which the projection turns into logits.

🔌 Analogy
To make what attention does concrete: it is a kind of weighted retrieval. The current token holds a Query (a "what am I looking for" question), matches it against every historical token's Key (a "what am I" tag) - the better the match, the higher the attention weight - then takes a weighted sum of those tokens' Values (the "content I can offer") into its new representation. So "understanding context" becomes "which earlier words to lean on". This is also why attention is the only channel between tokens: only here does a token's output truly depend on other tokens.

Walk this "retrieval" through one concrete example: the current token scores 3 history tokens; a higher score means a higher weight after softmax, and the Values get summed by those weights into one vector.

Tracing one attention step: the current token's Q scores 3 history tokens -> softmax to weights -> weighted sum of their V (numbers illustrative).
score: Q . K (more similar = higher) softmax weights (sum=1) k1 4.0 k2 1.0 k3 2.0 softmax 0.84 0.04 0.11 context vector *v1 *v2 *v3 out = 0.84*v1 + 0.04*v2 + 0.11*v3

An easily missed point: attention itself does not know word order - it only does similarity-weighted summing, so shuffling the same words and feeding them in gives the same result from pure attention. But language clearly cares about order ("dog bites man" vs "man bites dog"). Position information is added for exactly this: llama-family models commonly use RoPE (rotary position embedding), which does not simply add an "index vector" per position but rotates Query/Key by position, so two tokens' attention score naturally carries "how far apart they are". That is why the self-attention sub-layer above is tagged "with RoPE".

And the final output projection: it multiplies the top-layer token vector by a big "vocab-size x hidden-dim" matrix (lm_head) to get one score per vocabulary token - which is why the logits length always equals the vocabulary size. Many models also tie this with the bottom embedding matrix (weight tying), saving parameters and keeping "how inputs are encoded" consistent with "how outputs are scored".

💡 Tip
A quick sense of scale: the parameters of a "7B" or "13B" model live mostly in these blocks - each layer's attention projections, the FFN's two or three big matrices, times dozens of layers, plus the large embedding and output projections, sum to billions of numbers. A few hyperparameters - number of layers (n_layer), hidden dim (n_embd), number of heads - largely decide how big, how strong, and how resource-hungry a model is; you will read them straight from the GGUF header later.

Written as pseudocode, one block's forward pass is "two residual branches":

# one block's forward: two residual branches
def layer(x):                 # x: [n_tokens, n_embd]
    a = attn(rms_norm(x))     # tokens talk to each other here
    x = x + a                 # residual
    f = ffn(rms_norm(x))      # per-token non-linear mix
    return x + f              # residual

The causal mask: you may only look back

Since attention lets tokens reference one another, can token 5 peek at tokens 6 and 7 while writing? Never - during generation they do not exist yet. So the decoder adds a causal mask to attention: token i may attend only to positions <= i, masking out every "future" position. Drawn as a grid, it is a lower triangle:

Causal mask: token i may attend only to positions <= i; lit = visible, dim = masked (after itself)
t1 seesk1k2k3k4
t2 seesk1k2k3k4
t3 seesk1k2k3k4
t4 seesk1k2k3k4

Each row is "who one token may look at": a lit cell means attendable (no later than itself), a dim cell means masked (after itself). The implementation is direct: after computing attention scores, set every "future" score to -inf, then softmax turns those weights into 0 - as if unseen. This is what "autoregression" looks like mathematically - each position's output depends only on itself and what came before, never leaking the future.

🔬 Details / source
The causal mask also has a neat training use worth spelling out. During training we have the full sentence and can feed it in all at once; the causal mask guarantees position i sees only tokens <= i, so "predict the next word from the prefix" holds at every position simultaneously - one forward pass yields a "next word" prediction at every position, compared in parallel against the real next words to compute the loss. This "feed the answers in too, but block the future with a mask" training is called teacher forcing, and it is extremely efficient. Tellingly, this is the very same mechanism as prefill at inference time - every prompt token is known, so it can be computed in parallel like training, except at inference we only care about the last position's output.

Because training and inference share the same forward pass, llama.cpp has no "two codebases": whether prefill feeds in hundreds of tokens at once or decode feeds just 1 new token, both run the same compute graph, differing only in "how many tokens in this batch, and which positions to output". With that in mind, revisit lesson 03's seven-step flow and you see why a single llama_decode can carry both rhythms.

In llama.cpp this is assembled into the compute graph by build_attn / build_attn_mha in src/llama-graph.cpp, with the mask applied during softmax via ggml_soft_max_ext. Simplified:

// attention scores + causal mask (cf. build_attn / build_attn_mha)
kq  = ggml_mul_mat(ctx, k, q);            // scores [n_kv, n_q]
kq  = ggml_soft_max_ext(ctx, kq, mask,    // mask: causal -inf on j>i
                        scale, max_bias);
kqv = ggml_mul_mat(ctx, v, kq);           // weighted sum of values

(Per this repo's source as of 2026-06-15; the real implementation lives in llama-graph.cpp; only the trunk is shown, omitting scaling and multi-head details.)

Autoregression + why the KV cache is exact

Draw "predict the next from what we already have" as a loop and you get the autoregressive cycle:

existing tokens
x1 ... xn
->
forward
N blocks
->
last-pos logits
vocab scores
->
sample
pick a token
->
x(n+1)
feed back, repeat

Note the feed-back arrow at the end - the newly sampled token is appended to the sequence and becomes the next round's input. This raises the lesson's core question: to write each next word, must we recompute attention over all previous words? No - and the reason is hidden in the causal mask.

In attention each token computes three things: Query (the vector that "asks"), Key (the "tag" being matched), and Value (the "content" carried). To add token n+1, it takes its Query, compares against every historical token's Key, and sums their Values by the resulting weights. The crucial point: a historical token's K and V depend only on itself and its position, and the causal mask guarantees it cannot see any later token - so no matter how many new tokens are appended afterwards, those earlier K/V never change by a single number. Since they never change, they can be computed once, stored, and reused forever. That is the KV cache:

The KV cache is exact: a new token changes no historical K/V, so cached values equal "recompute from scratch" bit for bit
step nK1K2K3
step n+1K1 (same)K2 (same)K3 (same)+K4

Look at the two rows - step n+1 differs from step n by just one highlighted new cell on the right; everything on the left is untouched old values. So the KV cache is not an "accuracy-for-speed" approximation; it is a mathematically exact reuse: the cached values equal what "recompute every step from scratch" would produce, bit for bit. What it saves is repeated work, not precision. And since predicting the next word needs only the last position's row of output, we read logits at the end:

logits = llama_get_logits_ith(ctx, -1);  // last position only, returns n_vocab scores
💡 Tip
Putting numbers on it makes it clearer: without the cache, generating the nth token recomputes K/V for all previous n-1 tokens, so the whole generation is roughly n-squared in cost; with the cache, each step computes K/V for only the one new token and reuses history, so the per-step extra work is essentially constant. A concrete feel: by the 2000th token, no cache means recomputing K/V for nearly 2000 prior tokens every step - slower the longer you chat; with the cache, step 2000 and step 2 compute the same amount of new K/V (just one token's). Note this only avoids recomputing what was already computed - not a single number changes, which again shows it is exact reuse, not approximate speedup.

Of course, the KV cache is not free: it stores one K and one V for every layer and every historical token, so its footprint grows linearly with context length. At tens of thousands of tokens it eats a sizeable chunk of memory - one reason "context windows" always have a ceiling and long contexts are so hardware-hungry (allocation, writes, and reuse all live in src/llama-kv-cache.cpp). So engineering must both save compute with it and shrink its size - GQA/MQA below do exactly that - a tension to keep balancing.

🌍 Big picture
There is also a counter-intuitive fact here: during decode the bottleneck is usually not compute but moving data. Each generated token must read the model's several GB of weights, plus the ever-growing KV cache, in full from memory, yet the actual math corresponds to just "one token". So a single conversation's speed is limited more by memory bandwidth than by multiply count - which is exactly why quantization (shrinking the weights) speeds things up immediately: fewer bytes to move, faster steps. We will return to this thread in the next part on quantization.

Going deeper (optional)

Three questions below; open them if you want depth, skip them if you only want the main line.

1 Why only a decoder, no encoder? click to expand

The original Transformer was encoder-decoder, designed for tasks like translation ("read the whole sentence, then generate"): the encoder reads the source bidirectionally, the decoder generates the translation word by word. But predicting the next word does not need bidirectionality - while writing you can only depend on what you have already written.

GPT-style models therefore keep only the decoder, pair it with a causal mask, and do pure "continuation". The structure is simpler and the training objective is uniform (always "guess the next token"), which suits generation. Almost every open model llama.cpp supports is this decoder-only architecture.

Conversely, tasks that only "understand" without generating (classification, retrieval, deciding if two sentences mean the same) suit encoder-only models (like BERT) with their bidirectional structure - able to see all context on both sides at once; whereas "read-and-write" conversational generation is decoder-only's home turf.

2 MHA / GQA / MQA: shrinking the KV cache click to expand

In standard multi-head attention (MHA), Query, Key, and Value all have the same number of heads. But the KV cache size is proportional to the number of Key/Value heads, so memory-saving variants appeared: GQA (grouped-query attention) lets several Query heads share one group of K/V heads, with fewer K/V heads (n_head_kv) than Q heads (n_head, see src/llama-hparams.h).

MQA is the extreme - all Query heads share one set of K/V. Fewer heads means a smaller KV cache and cheaper long contexts, at a small cost in expressiveness. Most large models today use GQA as the compromise: much smaller cache, almost no quality loss.

Why does sharing barely hurt? Intuitively, neighboring Query heads often attend to much the same thing, so having them share one set of K/V loses little expressiveness while saving a lot of memory - a great trade, and why it is so widely adopted.

3 What are logits? How does temperature act? click to expand

logits are the raw scores for each token in the vocabulary - they can be positive or negative and do not sum to 1; they are not yet probabilities. To become probabilities they pass through a softmax (exponentiate, then normalize).

Temperature (T) acts just before softmax: divide all logits by T - T > 1 flattens the distribution (more random, more creative), T < 1 sharpens it (more deterministic, more conservative), and T near 0 degenerates into always picking the highest score (greedy). So with the same logits, tuning temperature slides you between "steady" and "wild" - lesson 03 touched on this, and a later lesson covers it in full.

Keep one contrast in mind: greedy always picks the highest-scoring word - stable but prone to repetition and blandness; temperature sampling adds randomness - livelier but more prone to going off the rails. Tuning generation quality is largely about balancing these two ends.

✅ Key points
  • decoder-only = embedding + N layers (self-attention + FFN) + final norm + projection to vocab logits.
  • The only place tokens talk to each other is self-attention; FFN / norm / projection are per-token.
  • The causal mask lets token i attend only to positions <= i - that is the mathematical form of "autoregression".
  • The KV cache is exact, not approximate: the causal mask keeps old tokens' K/V unchanged by new tokens, so cached values match a from-scratch recompute bit for bit.
  • Each step needs only the last position's output, so read logits with llama_get_logits_ith(ctx, -1).
💡 Design insight
The single constraint of the causal mask buys three things at once: the prompt can be computed fully in parallel (prefill), historical K/V can be safely cached (KV cache), and the very form of autoregressive token-by-token generation. Nearly every inference optimization later in llama.cpp ultimately stands on this one rule: you may only look back.

🧪 Self-test - think about the design

1. Why is the KV cache an exact optimization rather than an approximation?
  1. Because it only caches the last layer
  2. Because under the causal mask, old tokens cannot see new tokens, so their K/V never change as new tokens arrive
  3. Because it also caches the logits
  4. Because the weights are quantized
Show answer & explanation click to expand
Answer: B. The causal mask makes old tokens' K/V independent of the future, so caching them is exact - bit for bit equal to recomputing - not approximate.
2. In a decoder-only model, where do tokens mainly "talk to each other"?
  1. The FFN (feed-forward layer)
  2. The output projection
  3. The self-attention layer (with the causal mask)
  4. RMSNorm
Show answer & explanation click to expand
Answer: C. FFN, norm, and projection all process each token independently; only attention lets tokens see each other.
3. During decode, why does llama_get_logits_ith(ctx, -1) read only the last position?
  1. Because -1 means take the average
  2. Because the earlier positions were computed wrong
  3. Because only the last layer has logits
  4. Because predicting the next token needs only the last position's output; other positions need no output
Show answer & explanation click to expand
Answer: D. Autoregression emits one new token per step, which corresponds to the logits row at the sequence's last position.
💭 Open questions (no single right answer - just think or try)
  • If you removed the causal mask (letting tokens see the future), would autoregressive generation still hold? Would the KV cache still be exact?