深入 Transformer 内部
你能在本地运行的一切都是 Transformer,而这个架构一张餐巾纸就画得下:把词元变成向量,推进 N 个相同的层(层内词元互相看),把最终向量投影回词表得分。不需要微积分——点积和 softmax 撑起整场戏。本页慢慢走一遍这条路。
残差流
想象一条贯穿模型的传送带。每一层里,子模块读取传送带并把它们的贡献加回去——没有替换,只有累积:
x = embed(token_ids) + positions # 词元变成向量(维度 ~4k)
for layer in range(N): # 8B 模型 N ≈ 32
x = x + attention(x) # 词元之间交换信息
x = x + ffn(x) # 每个词元自我变换
logits = unembed(x[-1]) # 最后一个位置 → 每个词表条目的得分
p = softmax(logits) # 每个下一词元的概率
- 嵌入:每个词元 ID 索引一张学习到的向量表。位置信息被混入(现代模型用旋转位置编码),所以“狗咬人”和“人咬狗”不同。
- 各层形状相同、内容不同。浅层处理语法和表面模式;更深的层承载含义、事实和任务行为。
- 反嵌入:最终向量与每个词表条目比较;softmax 把得分变成概率。采样从中挑一个(采样)。
用四个词元讲清自注意力
信息交换机制。每个词元的向量产生三个派生向量:查询(我在找什么)、键(我宣传什么)、值(如果被选中我交出什么)。句子:"The cat sat on"。"sat" 的位置应该拉进什么?
token: The cat sat on
q·k scores: 1.2 4.1 3.0 0.4 (点积,示意值)
softmax → 0.05 0.60 0.31 0.04 (权重和为 1.0)
output("sat") = 0.05·v(The) + 0.60·v(cat) + 0.31·v(sat) + 0.04·v(on)
"sat" 的注意力主要落在 "cat" 上——它需要的主语。每个位置同时运行这套流程。一个约束让它成为语言模型:因果掩码——每个词元只能看更早的词元,绝不能看之后的,因此一次传播就能同时训练序列中每一个下一词元预测。
这就是全部数学工具箱:相似度得分(点积),归一化成混合权重(softmax),作用在值向量上。
作为一个配方,一个位置上的一个注意力头:
- 为每个词元计算查询、键、值向量。
- 用查询与每个更早词元的键做点积;按 √(head_dim) 缩小。
- 对得分做 softmax,得到和为 1 的权重。
- 输出这些词元值的加权和。
- 拼接所有头的输出并投影——这就是该层注意力的贡献。
多头:并行的模式
一个头 = 一组查询/键/值三元组 = 一种它能表达的关系(主语↔谓语、形容词↔名词、指代消解)。各层在低维切片上并行运行多个头(比如 32 个)并拼接结果。头更多意味着每层能表达的关系类型更多——是模式的广度,不是思考的深度。
- 头在训练时固定;跨位置的“思考”正是注意力写回残差流的东西。
- 头维度(如 128)× 头数(如 32)= 隐藏维度(4096):头是同一条传送带的并行切片,不是独立的模型。
FFN:参数的主场
每层的后半部分是一个双矩阵网络,独立作用于每个词元:扩展向量(~4 倍)、非线性、投影回去。在典型的稠密模型中,大约三分之二的参数住在这里,而不是注意力里。有用的直觉:键值记忆——第一个矩阵存放要检测的模式,第二个存放模式命中时要加到传送带上的内容。人们所说的“模型的事实”大部分存储在 FFN 权重里(参见 参数)。
| 组件 | 参数占比(典型 7–9B 稠密) |
|---|---|
| 注意力(Q、K、V、O) | ~10–15% |
| FFN | ~60–70% |
| 嵌入 + 输出头 | ~10–25%(随词表大小增长) |
自己数参数
模型卡公布五个数字;把它们相乘就得到参数量。以一个真实的 8B 级开源模型为例(32 层、隐藏维度 4096、32 个头加 8 个 KV 头、FFN 宽度 14336、词表 ~128k):
attention/layer: q: 4096×4096 + k,v: 2×(4096×1024) + o: 4096×4096
≈ 42M per layer × 32 layers ≈ 1.3B
FFN/layer: 3 × (4096×14336), gated ≈ 176M × 32 ≈ 5.6B
embeddings: 2 × (128k × 4096), input + output head ≈ 1.1B
total ≈ 8.0B
算术与模型卡上的 "8B" 几乎完全吻合。两个教训随之而来:FFN 主导参数量(因此主导内存预算),而大词表在第一层运行之前就要花掉约十亿参数——这是大词表多语种模型比其稠密同门看起来更重的原因之一。
GQA:注意力成本的修复
生成期间,每个新词元都必须读取之前每个词元的键和值——存放在 KV 缓存里。原始的多头注意力每个头每个词元存一份 K 和一份 V。分组查询注意力(GQA)让多个查询头共享一小撮 KV 头(例如 32 个查询头、8 个 KV 头),以可忽略的质量代价把缓存缩小 4 倍。多查询注意力(MQA)共享单个 KV 头。实际上所有现代开源模型都用 GQA;这些节省正是长上下文之所以可用的重要原因(KV 缓存)。
上下文长度 = 注意力跨度
注意力让每个词元能看到每个更早的词元,因此预填充计算随提示词长度二次增长,缓存随每个生成的词元线性增长。"128k 上下文"的模型靠位置技巧和训练扩展达到标称值,而窗口远端的质量通常比起始端更弱。只在必要时才填满长窗口。
自己测试:把一个事实放在窗口开头,中间灌填充内容,然后提问。多数模型在窗口开头和末尾表现良好,在中段下滑——著名的“迷失在中间”模式。把宣传的上下文当作容量,而非质量保证。
为什么这就够了
仅凭这个结构你就能推导出实用事实:内存需求 = 权重 + KV 缓存(显存计算);解码速度 = 重读权重的速度(预填充与解码);能力跟随参数量(参数);上下文成本 = 注意力跨度。架构很简单——里面的数十亿训练权重才是没人能手写出来的部分。