从一条直线到大模型输出一个token(十):首 token 诞生与 KV-Cache
建议先看:从一条直线到大模型输出一个token(九):输出矩阵与多层堆叠
上一篇拿到了输出矩阵,6 条约束链全部沉淀在最后一行。这一篇让首 token 真正诞生——然后是自回归、KV-Cache,以及整个系列的收官。
从一条直线到大模型输出一个token(十):首 token 诞生与 KV-Cache
- 从一条直线到大模型输出一个token(十):首 token 诞生与 KV-Cache
- 1. 取最后一行:为什么是「?」
- 2. lm_head:最后一次「一行乘一列」
- 3. Softmax:分数变概率
- 4. 选 token:三种策略
- 5. 首 token 诞生
- 6. 自回归:第二个字怎么来
- 7. KV-Cache:旧 token 的 K/V 不用重算
- 7.1 浪费在哪
- 7.2 缓存方案
- 7.3 收益与代价
- 7.4 GQA 与 MLA:给 KV-Cache 瘦身的两把刀
- 8. 收官:从一条直线到一个 token
- 8.1 十篇旅程
- 8.2 230B 的最后一环
- 8.3 写在系列完结
- 9. 下一系列预告:17 个问题
- 小结
- 系列完结
)
1. 取最后一行:为什么是「?」
篇9 结尾说过:输出矩阵的最后一行——「?」行——是预测首 token 的全部原料。为什么偏偏是最后一行?
因为因果语言模型从左到右预测下一个 token:训练时每个 token 的任务都是"根据左边所有 token,预测我右边是谁"。「今天」预测「西安」,「西安」预测「的」……而「?」是句子的最后一个 token,只有它见过全部前文——今天、西安、的、天气、怎么样,一个不落。
所以预测"?“后面是什么,就用”?"的行向量。把 6×4096 的输出矩阵只取最后一行,得到 v——1×4096 的行向量(演示 1×4),如图 10-1 所示。
图10-1 只取最后一行:「?」行是全句信息的汇集点
注意图 10-1 里的细节:v = [2.85, -0.35, 1.90, -1.32],d1 = 2.85 是行内最大值——这正是篇9 反复铺垫的那条线:「?」行 d1 在深层全面接管,"问完了,该作答"的开关信号已经就位。
2. lm_head:最后一次「一行乘一列」
现在把 v 翻译成词表上每个候选 token 的分数。篇9 已经预告过工具:lm_head——一个 4096×129,280 的权重矩阵(5.3 亿参数)。计算还是那个贯穿全系列的动作:一行乘一列。
演示用 8 个候选 token 的小词表(今、晴、雨、多、阴、雪、风、很),lm_head 就是 4×8 的小矩阵。v 乘它,得到 8 个分数(logits),如图 10-2 所示。
图10-2 lm_head:最后一次「一行乘一列」——4096 维翻译成词表分数
手算「晴」那一列(图 10-2 黄色高亮列):
2.85 × 0.70 + ( − 0.35 ) × ( − 0.30 ) + 1.90 × 0.40 + ( − 1.32 ) × 0.50 = 2.20 2.85 \times 0.70 + (-0.35) \times (-0.30) + 1.90 \times 0.40 + (-1.32) \times 0.50 = 2.202.85×0.70+(−0.35)×(−0.30)+1.90×0.40+(−1.32)×0.50=2.20
8 个 logits 全算出来:
| 候选 | 今 | 晴 | 雨 | 多 | 阴 | 雪 | 风 | 很 |
|---|---|---|---|---|---|---|---|---|
| 分数 | 0.18 | 2.20 | 0.69 | 0.94 | 0.73 | 0.54 | 0.41 | -0.14 |
表10-1 8 个候选 token 的 logits(演示词表,真实是 129,280 个分数)
「晴」以 2.20 一骑绝尘。但分数还不是概率——它们甚至可以是任意实数(有负数,也没归一化)。从分数到概率,差一个 Softmax。
3. Softmax:分数变概率
Softmax 的公式:
P i = e z i ∑ j e z j P_i = \frac{e^{z_i}}{\sum_j e^{z_j}}Pi=∑jezjezi
逐个候选算指数、再除以总和。手算一遍,只有三步:
第一步:逐格取指数 e^z。「晴」e^2.20 = 9.03,「多」e^0.94 = 2.55,「雨」e^0.69 = 1.99……「很」e^-0.14 = 0.87。指数干的事:把差距放大——分数上晴只是多的 2.3 倍,指数后拉开到 3.5 倍;分数上「很」是负的,指数后变成 0.87,垫底但没归零。
第二步:全部加起来。1.19 + 9.03 + 1.99 + 2.55 + 2.08 + 1.71 + 1.50 + 0.87 =20.91。
第三步:每格除以总和。「晴」= 9.03 / 20.91 =43.2%;「多」= 2.55 / 20.91 = 12.2%……8 个概率加起来恰好 100%。
整个过程动图演示如图 10-3 所示(4 帧循环,每帧追加一步推导)。
图10-3 Softmax 手算全过程:logits → e^z → 求和 → 概率(4 帧追加式动图)
8 个候选的最终概率(热力图)如图 10-4 所示。
图10-4 Softmax 后的概率:颜色越深概率越高(左:热力图;右:温度对比)
- 晴 43.2%——一家独大
- 多 12.2%、阴 9.9%、雨 9.5%——第二梯队,都是合理天气描述
- 雪 8.2%、风 7.2%——边缘候选
- 今 5.7%、很 4.1%——陪跑
为什么「今」这么低?因为**「今天」已经在输入里了**——注意力机制让「?」吸收过「今天」的信息,模型知道时间限定已经交代过,答案不需要再说一遍"今天"。为什么「很」垫底?因为「怎么样」规定的是描述性词性,副词开头(“很晴”?)语法上勉强、语义上别扭。篇9 的 6 条约束链,在概率分布上全部兑现:
时间限定(不说"今天"了)✓ 主体锁定(说的是西安不是别处)✓ 结构黏合("西安的天气"是完整主语)✓ 领域锁定(候选全是气象词)✓ 词性锁定(描述性开头)✓ 开始开关(d1 = 2.85 已经按下)
4. 选 token:三种策略
概率有了,怎么选出那个 token?三种主流策略,如图 10-5 所示。
图10-5 拿到概率之后怎么选:三种策略
- 贪心解码:永远选概率最高的——必选「晴」。每次输出完全一样,确定但呆板,连续生成时容易陷入重复循环。适合代码、数学。
- Top-k 采样:只在前 k 个候选里随机抽。比如 k=2:取晴、多,重新归一化成 78% / 22%——多数时候选晴,偶尔选多。k 是硬门槛,分布很平时候选会显得太少。
- Top-p 采样(nucleus):按概率降序累加,累积到 p(比如 0.9)就停,在截断的集合里抽。候选数随分布自适应:分布尖时候选少,平时候选多。当前各家 API 的默认选项。
**温度(Temperature)**是采样前的分布调节旋钮:把 logits 除以 T 再 Softmax。图 10-4 右侧是三种温度的对比——T=0.5 时「晴」放大到 78.6%(保守),T=2.0 时压平到 25.0%(放飞)。温度不改候选排序,只改"集中度"。
本系列演示用贪心:首 token =「晴」。
5. 首 token 诞生
把全链路串一遍,这是整个系列等了 10 篇的时刻:
- 篇2:「今天西安的天气怎么样?」→ 6 个 token [5237, 23872, 301, 16652, 19602, 1148]
- 篇3:查嵌入表 → 6×4096
- 篇4:RoPE 把位置旋进向量
- 篇5~8:过第 1 个 Block——归一化、注意力交换信息、残差保底、FFN 深加工
- 篇9:再过 31 层 → 输出矩阵,「?」行 d1 全面接管
- 本篇:取「?」行 → 乘 lm_head 得 129,280 个分数 → Softmax →「晴」以 43.2% 胜出
从「今天西安的天气怎么样?」到「晴」——一个 token 诞生了。这不是检索、不是查表,而是 80.4 亿个参数接力加工出来的概率选择。回答的开头,就此落地。
6. 自回归:第二个字怎么来
一个 token 显然不够——用户要的是完整的天气播报。自回归(autoregressive):把刚生成的「晴」拼回输入句尾,输入从 6 个 token 变成 7 个,再完整走一遍前向(32 层 → 取最后一行 → lm_head → Softmax → 选 token),得到第二个 token「天」。循环往复,像多米诺骨牌,如图 10-6 所示。
图10-6 自回归:首 token 拼回句尾,生成第二个字
用伪代码表达:
```python tokens = 分词("今天西安的天气怎么样?") # 6 个 while 未遇到 EOS and 长度未超限: out_matrix = transformer_32层(嵌入(tokens)) v = out_matrix[-1] # 取最后一行 logits = v @ lm_head # 词表分数 probs = softmax(logits / T) # 概率 next_tok = 采样(probs) # 贪心 / top-k / top-p tokens.append(next_tok) # 拼回句尾 回答 = 反查词表(tokens[6:]) # 晴天,…… ```注意循环体的每一轮都要过完整的 32 层——生成是逐 token 的,每个字都是一次全量前向。这带来了一个明显的浪费,下一节解决。
7. KV-Cache:旧 token 的 K/V 不用重算
7.1 浪费在哪
生成「天」时输入是 7 个 token,朴素做法 7 个全部过 32 层。但仔细想:前 6 个 token 的 K、V 向量,和上一轮算出来的一个数都不差。
为什么?篇7 讲过因果掩码:每个 token 只能看见自己和左边的 token。「晴」拼在句尾,改变不了旧 token 的视野——「西安」该看谁还是看谁,它的 K/V 和注意力结果纹丝不动。这是数学上的等价,不是近似。
7.2 缓存方案
KV-Cache 的做法:把每一层算出的 K、V 缓存下来;下一步只算新 token 一个的 Q、K、V——新 K/V 追加进缓存,新 Q 和全部缓存 K 算注意力。对比效果如图 10-7 所示。
图10-7 KV-Cache:缓存每层的 K/V,新 token 只算自己的 Q/K/V
从矩阵视角看缓存扩充:生成「晴」之前,K 缓存是 6×4;「晴」拼进来后,只新算它那一行,追加成 7×4——前 6 行原封不动,如图 10-8 所示。
图10-8 KV 缓存的扩充:从 6×4 到 7×4——旧的逐格不动,新的整行追加(颜色深浅 = 数值大小)
伪代码:
```python kv_cache = [] # 每层一份 (K, V) # 首 token:全量计算,K/V 全部入缓存 out, kv_cache = forward_32层(嵌入(tokens), cache=None) # 后续 token:只算新的 1 个 for 每一步: out, kv_cache = forward_32层(嵌入(新token), cache=kv_cache) # 内部:新 Q × 缓存 K → 注意力;新 K/V 追加进缓存 ```把图 10-6 和图 10-7 的机制合起来,如图 10-9 所示(4 帧循环动图):
图10-9 自回归生成 + KV-Cache 全过程动图
7.3 收益与代价
收益:生成第 N 个 token 的每步计算量,从"全部 N 个 token 过 32 层"降到"1 个新 token 过 32 层"。长回答的计算量从平方级降回近线性——这是所有大模型推理引擎的标配优化,没有它,逐 token 生成的延迟会随回答变长急剧恶化。
代价:缓存要占显存。粗略公式:2 × 层数 × 序列长度 × 宽度(K 和 V 两份)。对 LLaMA-3-8B:2 × 32 层 × 长度 × 4096 维——32K 上下文时 KV-Cache 就要占约 8GB(fp16),长上下文的显存大头。篇6 埋的伏笔在此回收——怎么给 KV-Cache 瘦身?业界有两把刀。
7.4 GQA 与 MLA:给 KV-Cache 瘦身的两把刀
第一把刀:GQA(分组查询注意力,Grouped-Query Attention)——从"头数"下手。
篇6 讲过:标准 MHA(多头注意力)里 32 个 Q 头配 32 组 K/V 头。GQA 的思路:Q 头保持 32 个不动,K/V 头砍到 8 组,每 4 个 Q 头共享 1 组 K/V。注意力的计算结构几乎不变(Q 和 K 的乘法照做),但缓存里每层只需存 8 组 K/V——缓存直接省 4 倍。
为什么敢共享?因为实验发现:不同 Q 头关注的 K/V 模式有大量重叠——你查"今天是时间"、他查"今天是日子",用同一份"今天"的 K/V 完全够用。GQA 由 Ainslie et al. 2023 提出(《GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints》),LLaMA-2/3、Mistral、Qwen 全系采用(篇6 出现的"W_K 是 4096×1024 而不是 4096×4096",就是 GQA 的痕迹——参数量也随之省了 4 倍)。
更激进的 MQA(Multi-Query,Shazeer 2019)只留 1 组 K/V(省 32 倍),但精度损失偏大,现在少用——GQA 是精度与省存的平衡点。
第二把刀:MLA(多头潜在注意力,Multi-head Latent Attention)——从"维度"下手,DeepSeek-V2/V3 的招牌设计。
GQA 只是把 K/V 从 32 组砍到 8 组,每组还是 128 维。MLA 更狠:根本不缓存 K/V 本身,缓存一个 576 维的"潜在向量"。
原理一句话:训练时让模型学会把 4096 维的上下文压缩成 512 维潜在表示(外加 64 维 RoPE 位置信息),用时再解压回 K/V。类比:不存整本书,存一本"摘要"——推理时按摘要重建需要的内容。代价是计算多一步"解压",换来的是每 token 每层缓存从 8192 个数降到约 576 个,减少约 93%(DeepSeek-V2 论文《DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model》的实测口径)——这就是 DeepSeek 敢做超长上下文的底气之一。
四种方案对账(以 4096 宽、32 头、每头 128 维为例,每 token 每层的 KV 缓存量):
| 方案 | K/V 头数 | 缓存维度(×2 份) | 相对 MHA | 代表模型 |
|---|---|---|---|---|
| MHA | 32 组 | 2 × 4096 = 8192 | 1× | 原始 Transformer / BERT |
| MQA | 1 组 | 2 × 128 = 256 | 1/32 | PaLM 等(少用) |
| GQA-8 | 8 组 | 2 × 1024 = 2048 | 1/4 | LLaMA-3 / Mistral / Qwen |
| MLA | 潜在压缩 | ≈ 576 + 64 | ≈1/14(减 93%) | DeepSeek-V3 / R1 |
表10-2 四种注意力的 KV-Cache 对比:那些"看不懂的压缩设计",本质上都是在给 KV-Cache 瘦身
8. 收官:从一条直线到一个 token
8.1 十篇旅程
把 10 篇串成一条线,如图 10-10 所示。
图10-10 从一条直线到一个 token:10 篇的完整旅程
- 篇1 用一条过原点的直线(1 个参数)出发,篇1 末加截距(2 个)
- 篇2 分词——句子变成 token 序列;篇3 嵌入查表(5.3 亿)
- 篇4 RoPE;篇5 Block 六步;篇6 QKV;篇7 注意力;篇8 FFN——组件逐个上齐
- 篇9 堆 32 层 + 输出矩阵(75.1 亿);篇10 lm_head + Softmax(80.4 亿)→ 「晴」
一句话总结系列:大模型 = 把「一行乘一列」这个动作,在 80 亿个数字上重复几万亿次,再让下一个 token 从概率里诞生。
8.2 230B 的最后一环
篇9 承诺过:篇10 结尾回补 230B 的闭环。现在所有组件都在手上了,可以把它说透:
豆包 Seed-1.6 的 230B 和本系列的 80.4 亿共享全部原理——分词、嵌入、RoPE、注意力、FFN、残差、堆叠、lm_head、Softmax、自回归、KV-Cache,一个组件都不多。区别只在三个旋钮:
| LLaMA-3-8B(本系列对账基准) | Seed-1.6(篇1 规模感起点) | |
|---|---|---|
| 宽度 | 4096 | 约 7168 |
| 深度 | 32 层 | 数十层 |
| FFN | 每层 1 个 | 每层若干专家,每 token 挑着激活 |
| 总参数 | 8.03B | 230B |
| 单 token 激活 | 8.03B(全激活) | 23B(稀疏激活) |
表10-3 稠密 8B 与 MoE 230B:同一套原理的两种配比
MoE 的"230B 总参数、23B 激活",翻译过来就是:模型记得多(230B 的知识存量),但每次只想少(23B 的计算量)——像一个藏书 230 万册的图书馆,每次查询只动 23 万册相关的书架。KV-Cache 优化的也是同一件事的两面:知识要多存、计算要少做。
篇1 那个"把 230B 砍到 1 个参数"的思想实验,至此完全闭合:砍掉的是规模,留下的是原理;加回来的是账本,看清的是骨架。
8.3 写在系列完结
写这个系列的初衷很简单:市面上的 Transformer 教程,要么从论文公式开始劝退,要么停在"注意力就像查字典"的比喻层面。我想试试第三条路——假设读者只学过高等数学,能不能把"大模型输出一个 token"这件事,从 y=wx 一路推到底。
10 篇写下来,最大的感受是:大模型没有魔法,只有工程。所谓的"智能涌现",拆开看是嵌入查表、一行乘一列、指数归一化这些本科生都能看懂的动作;复杂度不在任何单个组件,而在"简单动作 × 天文数字的重复"。
如果这个系列让你下次再看到"注意力头"“KV-Cache”"MoE 稀疏激活"这些词时,脑子里浮现的是具体的小矩阵和它的计算过程——这个系列的任务就完成了。
从一条直线,到「晴」。谢谢一路看到这里。
9. 下一系列预告:17 个问题
写完这个系列,最自然的感受是:"一个 token 如何诞生"讲清楚了,但围绕大模型还有一大片没讲的地。顺着本系列的思路往下问,至少还有 17 个好问题。
先抛三个最扎心的:
问题一:大模型其实不知道今天是几号。它的知识冻结在训练截止那天——你问"今天西安的天气",它答的"晴"只是概率上最像答案的词,不是真天气。怎么让它感知"今天"?最直接的方案是Prompt:把日期、天气数据塞进上下文(本系列的输入句就是这么来的);更进一步是MCP 工具调用:让模型自己决定"先查天气 API,再回答"。
问题二:回答总是「晴」,怎么破?就算接了工具,模型也可能偷懒每次都返回同一个答案。RAG(检索增强生成)从你的私有大文档里检索真实材料再回答;AI-Memory 让模型记住你是谁、你之前说过什么——变废为宝,从"垃圾数据"里提取"宝贝知识"。
问题三:本系列只给了 6 个 token 的上下文,真实对话动辄几万 token——上下文爆炸了怎么办?SubAgent(子代理)的思路:主 Agent 只留摘要,细节派给子 Agent 处理。
把这 17 个问题全部列出,按"懂你 / 有据 / 看见更多 / 变强"分成四组,如图 10-11 所示。
图10-11 下一系列候选主题:17 个问题分四组(顺序和内容都可能调整,欢迎留言点单)
- A 组·让模型更懂你:01 大模型如何感知【今天】(Prompt)、03 如何让对话更拟人(角色扮演)、06 让大模型了解细节(Skill)、08 让大模型适配你(AI-Memory)
- B 组·让模型有据可依:02 回答总是【晴】怎么办(MCP)、09 从垃圾里提取宝贝(RAG)、07 上下文爆炸了怎么办(SubAgent)
- C 组·让模型看见更多:04 大模型如何看图(多模态-图片)、05 大模型如何感知时间(多模态-视频)、16 操作你的电脑(GUI/Computer Use)、15 多个大模型协作(Multi-Agent)、17 Agent 社会(斯坦福 AI 小镇)
- D 组·让模型变强:10 知识从哪来(预训练)、11 学会听指令(SFT)、12 学会偏好(RL)、13 自己变强(自进化)、14 快思考与慢思考(推理模型)
A/B 组回答"怎么用好一个大模型"(应用层),C 组回答"怎么扩展它的感官与手足"(多模态与 Agent),D 组回答"怎么炼成一个大模型"(训练层)。本系列(原理篇)回答的是"它怎么工作"——四个系列连起来,就是从原理到应用的完整地图。
顺序和内容都可能调整。想先看哪个?评论区点单。
小结
这一篇让首 token 真正诞生,系列收官:
- 取最后一行:因果语言模型只有最后一个 token(「?」)见过全部前文,它的行向量 v(1×4096)是预测首 token 的唯一原料;d1=2.85 正是篇9 的"该作答"开关
- lm_head:v × W_L(4096×129,280 = 5.3 亿参数)= 129,280 个 logits——全系列最后一次「一行乘一列」,演示 8 个候选中「晴」2.20 最高
- Softmax:手算三步(逐格 e^z → 求和 20.91 → 相除)——指数放大差距,「晴」9.03/20.91 = 43.2% 一家独大;篇9 的 6 条约束链在概率上全部兑现("今"仅 5.7% 因为时间已交代、"很"垫底因为词性不符)
- 选 token 三策略:贪心(确定)、top-k(硬门槛)、top-p(自适应截断);温度是分布集中度旋钮(T=0.5→78.6%,T=2.0→25.0%)
- 首 token =「晴」:从分词到输出,10 篇全链路第一次完整跑通
- 自回归:输出拼回输入,逐 token 生成完整回答;每步都是全量前向
- KV-Cache:因果掩码保证旧 K/V 永不变 → 缓存复用、只算新 token,长回答从平方级降回近线性;代价是显存(2×层数×长度×宽度)
- GQA 与 MLA(瘦身两把刀):GQA 从头数下手(32 Q 头共享 8 组 K/V,缓存省 4 倍,LLaMA-3/Mistral/Qwen 全系);MLA 从维度下手(缓存 576 维潜在向量而非 K/V 本身,减约 93%,DeepSeek 招牌);MHA/MQA/GQA/MLA 四方案对账
- 230B 闭环:Seed-1.6 与 LLaMA-3-8B 共享全部原理,区别只是宽度/深度/MoE 三旋钮——“知识多存、计算少做”
- 下一系列预告:17 个问题分四组——懂你(Prompt/角色扮演/Skill/Memory)、有据(MCP/RAG/SubAgent)、看见更多(多模态/Agent/GUI/AI 小镇)、变强(预训练/SFT/RL/自进化/快慢思考)
系列完结
原理篇到此完结,应用与训练篇(17 个候选主题,见第 9 节)择日开写。
系列目录(全 10 篇):
- 从一条直线到高维空间
- 分词与 token 表
- 隐藏层与嵌入
- 位置编码 RoPE
- Transformer Block 全景与层归一化
- QKV 三剑客
- 注意力权重与多头机制
- 残差连接与前馈网络
- 输出矩阵与多层堆叠
- 首 token 诞生与 KV-Cache(当前篇,完结)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。