KV Cache 补充:从最后一行到下一个 Token

为什么只取最后位置的隐藏向量,以及 LM Head 怎样预测词表中的下一个 token

Posted by Liu Mengxuan on September 24, 2026

前面的 KV Cache 基础篇 介绍了 Prefill、Decode 和历史 K/V 的复用。这篇作为系列补充,继续沿着“中华人 → 民”的例子,把 Attention 输出、最后位置的隐藏向量和词表预测接起来。

示意图经常把完整 Attention 方阵保留下来,又把 Attention 输出直接连到新 token,容易让人误以为旧分数也被缓存,或者一个 Attention 向量就能直接变成文字。实际推理中,这里包含几个不同的计算阶段。

从当前 Query 读取历史 K/V 开始,Attention 先得到一个隐藏向量;它继续经过后续网络计算,最后由 LM Head 给整个词表打分,解码策略再选出新 token。下面沿着这条数据流展开。

1. KV Cache 阶段:只重新计算当前 Query 的一行

假设 Prompt 是“中华”,Prefill 已经预测出“人”。现在将“人”作为新输入做一次 Decode,处理后的上下文是下面三个 token。为便于说明,假设一个汉字就是一个 token;实际分词边界由 tokenizer 决定。

1
2
位置:  1    2    3
内容:  中   华    人

现在模型要根据“中华人”预测下一个 token。以某一层、某一个 Attention head 为例,“中”“华”的 K/V 从缓存读取,“人”的 Q/K/V 本轮计算。把当前 K/V 追加后,参与 Attention 的完整 K/V 为:

$K_{1:3}=\begin{bmatrix}k_{\text{中}}\\k_{\text{华}}\\k_{\text{人}}\end{bmatrix},\qquad V_{1:3}=\begin{bmatrix}v_{\text{中}}\\v_{\text{华}}\\v_{\text{人}}\end{bmatrix}$

当前 Query 是“人”这个位置新算出的 $q_{\text{人}}$。它要做的是:

$s_{\text{人}}=q_{\text{人}}K_{1:3}^{T}=\begin{bmatrix}q_{\text{人}}k_{\text{中}}^{T}&q_{\text{人}}k_{\text{华}}^{T}&q_{\text{人}}k_{\text{人}}^{T}\end{bmatrix}$

这就是完整 Attention 分数矩阵中最下面的那一行。完整方阵中“中/中”“华/中”等旧行描述的是历史位置当时如何关注上下文;它们已经完成了自己的计算,本轮不再需要,也通常不会作为缓存保存。方阵右上角对应旧位置关注未来位置,受因果 Mask 限制,同样不需要补算。当前 Query 虽然读取了缓存的 Key,但 Query 已经改变,因此最下面这一行的点积要新算。

因此,带 KV Cache 的 Decode 单步不会重新计算一个 3×3 的分数矩阵,而是计算:

1
2
3
当前 Q:[1, d_k]
完整 Kᵀ:[d_k, 3]
分数:  [1, 3]

对这一行做缩放、Softmax,再读取缓存中的 Value:

$o_{\text{人}}=\operatorname{softmax}\left(\frac{s_{\text{人}}}{\sqrt{d_k}}\right)V_{1:3}$

这里缓存的是历史 K/V,不是 $QK^T$ 的整张分数矩阵。当前的 $q_{\text{人}}$、$k_{\text{人}}$、$v_{\text{人}}$ 本轮计算;新的 K/V 还会追加到缓存,供下一步使用。

带 KV Cache 的 Decode 只用当前 Query 计算最后一行 Attention 分数,历史 Key 和 Value 从缓存读取

2. Attention 输出还要经过后续模型计算

上面的 $o_{\text{人}}$ 只是某一层、某一个 head 的 Attention 输出。多个 head 的结果需要拼接并经过输出投影,再结合本层的残差、归一化和 FFN 计算,才能传到下一层。这里按常见的 Decoder-only 模型描述,具体归一化位置取决于架构。

如果有 L 个 Transformer Block,当前“人”这个位置的表示会沿着网络向前传播:

$h_{\text{人}}^{(0)}\rightarrow\text{Block}_1\rightarrow h_{\text{人}}^{(1)}\rightarrow\cdots\rightarrow\text{Block}_L\rightarrow h_{\text{人}}^{(L)}$

最后再经过模型末端的归一化,得到用于词表预测的最终隐藏向量。记它为:

$h_{\text{人}}\in\mathbb{R}^{H}$

这里的下标“人”表示它位于“人”这个位置,并不表示它只包含这个字的信息。由于因果 Attention,这个向量已经融合了它能看到的前文“中、华、人”。

3. Prefill 取最后一个有效位置预测新 token

换一个输入边界:如果一开始就把“中华人”作为完整 Prompt 做 Prefill,经过全部 Transformer Block 和最终归一化后,会得到所有位置的隐藏状态:

$H_{\text{all}}=\begin{bmatrix}h_{\text{中}}\\h_{\text{华}}\\h_{\text{人}}\end{bmatrix}\in\mathbb{R}^{3\times H}$

每一行负责一个位置的“下一 token”预测:

当前隐藏向量 能看到的内容 它对应的预测目标
$h_{\text{中}}$ 中 “中”之后的 token
$h_{\text{华}}$ 中华 “中华”之后的 token
$h_{\text{人}}$ 中华人 Prompt 之后的第一个新 token

现在我们要接着整个 Prompt 继续生成,所以需要的是最后一个有效位置:

$h_{\text{last}}=H_{\text{all}}[-1,:]=h_{\text{人}}$

“最后一行”描述的是序列位置维度,不是 Transformer 的最后一层。它通常已经经过全部 Transformer Block;只是从最后一层输出的 [batch, sequence, hidden] 矩阵中,选择每条序列最后一个有效 token 的那一行。

完整 Prompt 经过所有 Transformer Block 后得到隐藏状态矩阵,取最后一个有效位置的最后一行送入 LM Head

代码中经常看到:

1
2
# hidden_states: [batch_size, sequence_length, hidden_size]
last_hidden = hidden_states[:, -1, :]  # [batch_size, hidden_size]

另一种常见接口先返回所有位置的词表分数,再选最后位置:

1
2
# outputs.logits: [B, T, V]
next_token_logits = outputs.logits[:, -1, :]  # [B, V]

这里取的是最后位置的 logits;前一个例子取的是最后位置的 hidden,再交给 LM Head。两者选择的是同一个序列位置。

如果 batch 中有 padding,不能无条件取最后一列;应该取每条序列最后一个有效 token 的位置。左 padding、右 padding 或 packed sequence 的实现方式不同,但原则相同:取最后一个有效位置,而不是盲目取数组最后一列。

4. LM Head:用一个隐藏向量给整个词表打分

得到 $h_{\text{last}}$ 后,LM Head 通常是一个线性层。假设隐藏维度为 H,词表大小为 V:

$h_{\text{last}}\in\mathbb{R}^{1\times H},\qquad W_{\text{LM}}\in\mathbb{R}^{H\times V}$

两者相乘得到词表中每个 token 的分数:

$z=h_{\text{last}}W_{\text{LM}}\in\mathbb{R}^{1\times V}$

第 j 个 logit 是:

$z_j=\sum_{i=1}^{H}h_iW_{ij}$

也就是说,LM Head 的每一列对应一个候选 token。隐藏向量会分别与这些列做点积,得到每个候选的匹配分数。

4.1 用一个小词表手算

为了看清楚矩阵乘法,暂时把隐藏维度缩小为 3,把词表缩小为四个 token:民、工、们、生。

设最终隐藏向量是:

$h_{\text{last}}=\begin{bmatrix}2&1&-1\end{bmatrix}$

LM Head 的权重矩阵为:

$W_{\text{LM}}=\begin{bmatrix}1&0&1&-1\\1&2&0&0\\-1&0&1&1\end{bmatrix}$

四列的顺序对应 民、工、们、生。矩阵乘法得到:

$z=h_{\text{last}}W_{\text{LM}}=\begin{bmatrix}4&2&1&-3\end{bmatrix}$

展开第一列,“民”的分数是:

$z_{\text{民}}=2\times1+1\times1+(-1)\times(-1)=4$

四个 logits 可以写成:

1
2
3
4
民: 4
工: 2
们: 1
生:-3

上面的数字只是演示,真实权重由训练得到。例如训练文本里“中华人”的下一 token 是“民”,交叉熵损失就会推动模型提高“民”的预测概率,梯度同时更新 LM Head 和前面的 Transformer。

这些是原始分数,不是概率,也不要求加起来等于 1。它们会交给后续的解码策略:

  • 贪心解码:直接取最大 logit,这个例子会选“民”;
  • Temperature:先缩放 logits,改变分布的尖锐程度;
  • Top-K / Top-P:过滤候选,再从剩余分布采样。

如果需要概率,可以计算:

$p_j=\frac{e^{z_j}}{\sum_{k=1}^{V}e^{z_k}}$

这里的 Softmax 沿词表维度归一化,得到“下一个 token 选谁”的分布;Attention 里的 Softmax 沿上下文位置归一化,决定“当前 Query 从哪里读取信息”。两个分布的含义不同。

实际实现中通常先做数值稳定的处理,再进行采样。argmax 场景甚至不需要真的计算 Softmax,因为指数函数保持大小关系。

5. 真实模型中的张量形状

假设 batch 大小为 B,Prompt 长度为 T,隐藏维度为 H,词表大小为 V:

1
2
3
4
最后一层隐藏状态: [B, T, H]
取最后一个有效位置:[B, H]
LM Head:           [B, H] × [H, V]
得到 logits:        [B, V]

在 Prefill 阶段,如果只需要每条 Prompt 的第一个新 token,推理引擎可以只对最后一个有效位置执行 LM Head。前面的 Transformer 仍需计算各位置以建立缓存,省掉的是其他位置的词表投影:

1
2
完整做法: [B, T, H] × [H, V] → [B, T, V]
实际需要: [B, H]    × [H, V] → [B, V]

完整的 [B, T, V] logits 在训练或需要每个位置预测时很有用;普通自回归生成下一 token 时只需要每条序列最后位置的 [B, V] logits。两种做法在最后一个位置上的数学结果相同,区别是是否计算和保存其他位置的词表投影。

PyTorch 中,nn.Linear 的权重通常保存为 [out_features, in_features],所以代码写成:

1
2
3
4
5
6
7
import torch

lm_head = torch.nn.Linear(H, V, bias=False)

# h_last: [B, H]
logits = lm_head(h_last)          # [B, V]
# 等价于 h_last @ lm_head.weight.T

有些模型会让输入 Embedding 和 LM Head 共享权重。若 Embedding 表为 $E\in\mathbb{R}^{V\times H}$,则输出可以写成:

$z=h_{\text{last}}E^T$

输入时,token ID 用来从 E 中查一行;输出时,隐藏向量与 E 的每一行做点积。这两种用途共享参数,但并不是数学上的互逆运算。

6. Prefill 和 Decode 汇合到同一条输出路径

两者都会经过“最后位置隐藏向量 → LM Head → 下一个 token”这条路径,但前面的计算方式不同。

6.1 Prefill

Prompt 的所有 token 同时进入模型。每个位置都能得到隐藏状态和相应的 K/V;因果 Mask 保证位置不会看到未来。建立缓存后,只取最后一个有效位置的 logits,预测第一个新 token:

1
2
Prompt:[中 华 人]
Prefill 最后一个位置 → logits → 预测“民”

6.2 Decode

新 token 作为下一轮输入,每一层只新增一行 hidden state。当前 Query 查询完整的历史 K/V,得到当前 token 的输出;经过所有层后,当前这一个位置本身就是最后位置,因此直接进入 LM Head:

1
2
3
4
5
上一轮预测“民”
    ↓ 作为本轮输入
只计算“民”的一行
    ↓
LM Head → 预测下一个 token

还有一个时间上的先后关系:刚预测出“民”时,缓存里通常还没有“民”的 K/V。只有下一轮把“民”送入模型,才会逐层计算并缓存它的 K/V。 如果“民”就是停止 token、请求已经结束,也就不需要再为它做下一轮 Decode。

所以,Decode 中“只算一行 Attention”和“最终取最后一行”并不是同一件事:前者是 KV Cache 带来的增量计算;后者是从最终隐藏状态中选择要用于下一 token 预测的序列位置。

7. 把整条链路放回 KV Cache

一次普通 Decode 的数据流可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当前输入 token + 各层已有 KV Cache
        ↓
每一层计算当前 token 的 q、k、v
        ↓
把当前 k、v 追加到该层 KV Cache
        ↓
当前 q 查询该层完整 K/V
        ↓
得到当前层输出,继续经过 FFN 和后续层
        ↓
最终 hidden 的最后一个有效位置 h_last
        ↓
LM Head 产生 [词表大小] 个 logits
        ↓
采样或取最大值,得到下一个 token

其中有三类容易混在一起的对象:

对象 作用 是否作为标准 KV Cache 长期保存
历史 K/V 供未来 Query 反复检索和读取 是
$QK^T$ Attention 分数 当前 Query 这一次计算的匹配结果 通常不是
最终 hidden / logits 产生下一个 token 的当前结果 通常只保留完成当前步骤所需的状态

KV Cache 省掉的是历史 token 的重复投影和重复逐层计算;LM Head 则负责把当前最终隐藏向量翻译成对整个词表的选择分数。把这两段连接起来,就不会再把“缓存历史 K/V”“计算当前 Attention 一行”和“取最后一行预测 token”混成同一个操作。

KV Cache 系列

参考资料