localaiforum
登录
指南 › 基础知识

深入 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),作用在值向量上。

作为一个配方,一个位置上的一个注意力头:

  1. 为每个词元计算查询、键、值向量。
  2. 用查询与每个更早词元的键做点积;按 √(head_dim) 缩小。
  3. 对得分做 softmax,得到和为 1 的权重。
  4. 输出这些词元值的加权和。
  5. 拼接所有头的输出并投影——这就是该层注意力的贡献。

多头:并行的模式

一个头 = 一组查询/键/值三元组 = 一种它能表达的关系(主语↔谓语、形容词↔名词、指代消解)。各层在低维切片上并行运行多个头(比如 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 缓存(显存计算);解码速度 = 重读权重的速度(预填充与解码);能力跟随参数量(参数);上下文成本 = 注意力跨度。架构很简单——里面的数十亿训练权重才是没人能手写出来的部分。

相关: LLM 是什么 · KV 缓存 · 预填充与解码 · 参数