逐层解剖一个模型
第 5 课:逐层解剖一个模型
前面三节课我们学了 Transformer 的原理、五大改造、Tokenization。现在把所有这些拼起来——以 Llama 2 7B 为标本,从 Embedding 到 LM Head,一层一层拆开看 tensor 形状和计算量。
② 手工推算 Attention 层和 FFN 层的参数量和 FLOPs
③ 从"你好"两个 token 开始,追踪它们经过 32 层后的完整变化过程
Llama 2 7B 的架构由三部分组成:Embedding 层 → 32 个 Transformer Block → LM Head。每个 Block 内部又分成 Attention 和 FFN 两个子层,每个子层前后各有一个 RMSNorm。
可以把这一结构类比成一条装配线:首尾各有一个工位,中间几十个工位做完全相同的加工——Transformer 里这些"工位"叫 Block,只是重复了很多次。
| 组件 | 包含什么 | 参数 | 占比 |
|---|---|---|---|
| Token Embedding | 32000 × 4096 的查表矩阵 | 131.1M | 1.95% |
| 32 × Transformer Block | 每层 = Attention + FFN + 2×RMSNorm | 32 × 202.4M = 6,476M | 96.1% |
| LM Head + Final RMSNorm | 输出投影(独立矩阵,不与 Embedding 共享)+ 最后的归一化 | 131.1M + 4K | 1.95% |
| 子层 | 操作 | 参数 |
|---|---|---|
| Input RMSNorm | 对输入做 RMS 归一化 | 4096(仅 γ 参数) |
| Attention | QKV 投影(3×[4096×4096]) + O 投影([4096×4096]) | 4 × 16.78M = 67.1M |
| Post-Attention RMSNorm | 对残差加和后的结果归一化 | 4096 |
| FFN (SwiGLU) | gate([4096×11008]) + up([4096×11008]) + down([11008×4096]) | 3 × 45.1M = 135.3M |
| 位置 | 操作 | tensor 形状 |
|---|---|---|
| ① Tokenizer 输出 | "你好" → BPE 编码 → token IDs | [1, 2](batch=1, seq_len=2) |
| ② Embedding | 查表:每个 ID 取一行 4096 维向量 | [1, 2, 4096] |
| ③ Block 1..32 | 每层进出形状不变:Attention→残差→FFN→残差 | [1, 2, 4096] → 32次 → [1, 2, 4096] |
| ④ Final RMSNorm | 归一化最后一层的输出 | [1, 2, 4096] |
| ⑤ LM Head | × W_out^T(独立输出矩阵)得到词表概率分布 | [1, 2, 32000] |
7B 参数的 96% 在 32 个 Transformer Block 里。每个 Block 又分配为:Attention 占 33%(67M/202M),FFN 占 67%(135M/202M)。FFN 矩阵是模型"知识存储"的主体——这也是为什么 FFN 比 Attention 大两倍。
上一张卡片我们看完了 Llama 的整体结构(Embedding → 32 Blocks → LM Head)。现在聚焦第一站:Embedding 层的数学极其简单——矩阵查表。有一个 32000×4096 的矩阵 W_embed,token ID 为 t 时,输出就是 W_embed 的第 t 行。
这一步就像查一本字典:token ID 是页码,翻到那一页整行搬走,不需要做任何乘加——Embedding 层的运算只有取行这一步。
| 字 | Token ID(示意) | 说明 |
|---|---|---|
| 你 | 12845 | 常见单字 token,词表中独立存在 |
| 好 | 6721 | 常见单字 token,词表中独立存在 |
| Token | 前 4 维数值(示意) | 完整维度 |
|---|---|---|
| "你" | [0.032, -0.015, 0.078, -0.041, ...] | 4096 维 |
| "好" | [0.056, 0.023, -0.012, 0.067, ...] | 4096 维 |
| 项目 | 计算 | 结果 |
|---|---|---|
| 参数数量 | 32000 × 4096 | 131,072,000(131M) |
| fp16 显存 | 131M × 2 bytes | 262 MB |
| fp32 显存 | 131M × 4 bytes | 524 MB |
| 推理时实际占用 | fp16 加载 + 输入 batch 查表临时内存 | ~270 MB(含临时 buffer,估算,依实现而异) |
Llama 2 的 Embedding 矩阵 W_embed([32000, 4096])负责把 token ID 映射为向量;最后的输出投影 LM Head 是一块独立的 [4096, 32000] 矩阵 W_out,把最后一层的 [1, 2, 4096] 映射回 [1, 2, 32000] 词表概率。二者并不共享权重(tie_word_embeddings=False)——6.74B 这个总数里已经分别算了这两块各 131M。Weight Tying(共享)是 GPT-2 等模型的做法,可省一份参数,但 Llama 2 没有采用。
上一张卡片我们把"你好"变成了两个 4096 维向量。现在它们进入 Attention 层——Transformer 的核心计算单元。输入 [1, 2, 4096],经过 QKV 投影 → 注意力计算 → O 投影,输出还是 [1, 2, 4096]。
用图书馆借书来类比:Q 是手里的索书号,K 是书脊上的分类标签,V 是书页里的正文——拿 Q 去和每张 K 比对,谁最像就借走谁的 V。
| 操作 | 计算 | 输出形状 | 参数量 |
|---|---|---|---|
| Q = X × W_Q | [1,2,4096] × [4096,4096] | [1, 2, 4096] | 16.78M |
| K = X × W_K | [1,2,4096] × [4096,4096] | [1, 2, 4096] | 16.78M |
| V = X × W_V | [1,2,4096] × [4096,4096] | [1, 2, 4096] | 16.78M |
| 张量 | 拆分前 | 拆分后 |
|---|---|---|
| Q | [1, 2, 4096] | [1, 32, 2, 128](batch, heads, seq, dim) |
| K | [1, 2, 4096] | [1, 32, 2, 128] |
| V | [1, 2, 4096] | [1, 32, 2, 128] |
| 头编号 | 计算 | 注意力矩阵形状 | 含义 |
|---|---|---|---|
| Head 1 | Q₁ × K₁^T / √128 | [1, 2, 2] | 2个字之间的两两注意力分数 |
| Head 1 数值 | 例(已除以 √128 的缩放后分数,示意):Q₁[0]·K₁[0]/√128=0.43, Q₁[0]·K₁[1]/√128=0.18 Q₁[1]·K₁[0]/√128=0.15, Q₁[1]·K₁[1]/√128=0.52 | [[0.43, 0.18], [0.15, 0.52]] | "你"更关注自己(0.43),"好"也更关注自己(0.52) |
| 步骤 | Head 1 的计算 |
|---|---|
| Softmax(QK^T/√128) | 行1: softmax([0.43,0.18]) = [0.56, 0.44] 行2: softmax([0.15,0.52]) = [0.41, 0.59] |
| 输出 = 注意力权重 × V | 位置1输出 = 0.56×V₁[0] + 0.44×V₁[1] 位置2输出 = 0.41×V₁[0] + 0.59×V₁[1] |
| 操作 | 输入形状 | 输出形状 |
|---|---|---|
| 合并 32 个头 | [1, 32, 2, 128] | [1, 2, 4096] |
| O = concat × W_O | [1, 2, 4096] × [4096, 4096] | [1, 2, 4096] |
多头注意力的关键是让不同头关注不同的语义关系。研究者发现(Clark et al., 2019),训练后的模型里某些头倾向于关注"主语-谓语"关系,另一些头关注"形容词-名词"修饰,还有些头关注位置相邻性(哪一头对应哪种关系并无固定编号,此处为示意说明)。如果只有一个头(4096 维直接做 Attention),所有关系会混在一起,模型学不出这么多样的注意力模式。32 个头 = 32 种不同的"视角"同时看同一句话。
上一张卡片我们逐步跑完了 Attention 的 5 步计算;这一张换个问法——不再问「怎么算」,而是问「一共要算多少次」。要把一个 token 走完 32 层的计算量压成一个数,只需要一条经验规则:一次前向里,每个权重都恰好做一次「乘 + 加」,也就是 2 次浮点运算(FLOP,Floating Point Operation,一次浮点乘或浮点加)。先把三种口径说清(同一颗权重,按数哪些运算区分):
① 只数乘法(乘一次算 1):前向 ≈ 1N = 6.7 GFLOPs;
② 只计前向、乘和加都数(每权重 2 FLOPs):前向 ≈ 2N = 13.5 GFLOPs;
③ 前向 + 反向(反向约是前向的 2 倍):训练 ≈ 6N = 40.4 GFLOPs。
本课前向采用口径 ②、训练采用口径 ③,两者口径统一(②×3 = ③)。
把这条规则套到参数量上,就得到本课要记住的 2N 规则:前向 FLOPs ≈ 2 × 参数量。它就像一条「流水线计件工资」的口算公式——每个权重是一名工人,每过一次前向固定领 2 次浮点操作,把工人总数(参数量)乘 2 就是这一趟的总工时。
| 操作 | 每个权重做什么 | FLOPs |
|---|---|---|
| 乘 | 权重 w 乘上激活 x | 1 |
| 加 | 把乘积累加进这一路的输出 | 1 |
| 合计 | 一次前向里每个权重固定贡献 | 2 |
| 阶段 | 计算 | 结果 |
|---|---|---|
| 参数量 N | — | 6.74B |
| 单 token 前向 | 2 × N = 2 × 6.74e9 | 13.5 GFLOPs |
| 单 token 反向 | 4 × N = 4 × 6.74e9 | 27.0 GFLOPs |
| 单 token 训练(前向 + 反向) | 6 × N = 6 × 6.74e9 | 40.4 GFLOPs |
| 项 | 计算 | 结果 |
|---|---|---|
| 每 token 训练 FLOPs | 2N(前向)+ 4N(反向)= 6N | 40.4 GFLOPs |
| 训练 1T token | 6 × 6.74e9 × 1e12 | 4.04 × 10²² FLOPs |
| 训练 2T token | 6 × 6.74e9 × 2e12 | 8.09 × 10²² FLOPs |
| 场景 | 计算 | 结果 |
|---|---|---|
| prefill 1000 token(一次前向过 1000 个 token) | 1000 × 2 × 6.74e9 | 13.5 TFLOPs |
| 生成 100 token | 100 × 2 × 6.74e9 | 1.35 TFLOPs |
记住 2N 这一条,看到任何模型都能立刻估出它的单 token 前向算力:Llama 2 7B ≈ 13.5 GFLOPs、13B ≈ 26 GFLOPs、70B ≈ 140 GFLOPs。训练时换成 6N,再乘训练 token 数,就得到总训练算力 C = 6ND——这是各大团队规划预算时用的口径。它忽略 attention 的 O(L²) 项,短上下文下误差很小;上下文极长时这一项才会盖过 2N。
上一张卡片我们用 2N 规则算出了单 token 的算力,也看到参数量几乎都压在 FFN 里;现在回到「这些参数从哪来」这一侧,把 FFN 那个 11008 拆开看。它不是随手取的整数,而是「参数量守恒」倒推出来的结果。先看两种更省事的做法,恰好都不成立:一是不升维(中间维取 d),两个矩阵只有 2 × 4096² ≈ 33.6M,是 4d 版本 134.2M 的约 1/4;二是省掉一个矩阵(gate/up/down 只留两个),配 11008 只剩 2 × 4096 × 11008 ≈ 90.2M,比守恒点 135.3M 少 45.1M/层。而 FFN 正是每层参数的大头(约 2/3),这两刀都会直接砍在「知识存储」的容量上。
道理就像把一份工作量分给不同人数:标准 FFN 是 2 个人干活(2 个矩阵),SwiGLU 是 3 个人干(3 个矩阵)——想让总工作量不变,每个人(每个矩阵的中间维)就得按比例缩到原来的 2/3。
| 矩阵 | 形状 | 参数 |
|---|---|---|
| W1(升维) | [d, 4d] | 4d² |
| W2(降维) | [4d, d] | 4d² |
| 合计 | 2 × d × 4d = 8d² | 8 × 4096² = 134.2M |
| 矩阵 | 形状 | 参数 |
|---|---|---|
| gate_proj | [d, d_ff] | d·d_ff |
| up_proj | [d, d_ff] | d·d_ff |
| down_proj | [d_ff, d] | d_ff·d |
| 合计 | 3 × d × d_ff | 待定 |
| 步骤 | 计算 | 结果 |
|---|---|---|
| 2/3 规则 | 8/3 × 4096 | 10922.7 |
| 除以 256 | 10922.7 / 256 | 42.67 |
| 向上取整再乘回 | 43 × 256 | 11008 |
| 每层 FFN 参数 | 3 × 4096 × 11008 | 135.3M |
| 方案 | 每层 FFN 参数 | 与 11008 之差 | 32 层合计 |
|---|---|---|---|
| 3 矩阵 × 11008(实际) | 3 × 4096 × 11008 = 135.3M | 基准 | 4.33B |
| 3 矩阵 × 16384(若用 4d) | 3 × 4096 × 16384 = 201.3M | +66.0M | 6.44B |
| 2 矩阵 × 11008(少一个矩阵) | 2 × 4096 × 11008 = 90.2M | −45.1M | 2.89B |
它 = 4096 × 8/3 再向上对齐到 256 的整数倍,目的只有一个:让 SwiGLU 的 3 个矩阵加起来(3·d·d_ff ≈ 8d²)与标准 FFN 的 2 个矩阵(2·d·4d = 8d²)持平。同一套账换个模型照样套——把 d 换掉重算即可;不同实现会在此基础上调(如 Llama 3 8B 用 14336,比 8/3·d 更大,以换取更强表达力)。
上一张卡片我们把 32 层 Block 接成了一条主干;现在走到整条流水线的最后一步——把 [1, 2, 4096] 的隐藏向量变成下一个词的预测,这一步靠 LM Head(把隐藏向量投影成词表概率分布的输出层)。
这一节不数 tensor 形状,而是回答一个更实在的问题:所谓「7B」,就是把这些矩阵一笔一笔乘出来再加总——首尾各一张 32000×4096 的大表,加上中间 32 层的 Attention 和 FFN。可以把它类比成装修算总价:首尾两张表是门和窗(各算一张),32 层 Block 是 32 间房,每间房里再分「Attention 区」和「FFN 区」,把单间用料乘以 32 再加门窗,就是总数。
| 参数 | 值 | 含义 |
|---|---|---|
| hidden_size(d) | 4096 | 每个 token 的向量长度 |
| num_hidden_layers | 32 | Block 层数 |
| num_attention_heads | 32 | 注意力头数 |
| num_key_value_heads | 32 | 等于头数 → 用的是 MHA(Multi-Head Attention,多头注意力:每个 Q 头都配一份独立的 K、V,不降维) |
| intermediate_size | 11008 | FFN 中间维 |
| vocab_size | 32000 | 词表大小 |
| 官方参数量 | 6.74B | 本文要逐块加出来的目标数 |
| 矩阵 | 形状 | 参数 |
|---|---|---|
| W_q | 4096 × 4096 | 16.78M |
| W_k | 4096 × 4096 | 16.78M |
| W_v | 4096 × 4096 | 16.78M |
| W_o | 4096 × 4096 | 16.78M |
| 合计 | 4 × 4096×4096 = 4 × 16.78M | 67.11M |
| 矩阵 | 形状 | 参数 |
|---|---|---|
| gate_proj | 4096 × 11008 | 45.09M |
| up_proj | 4096 × 11008 | 45.09M |
| down_proj | 11008 × 4096 | 45.09M |
| 合计 | 3 × 4096×11008 = 3 × 45.09M | 135.27M |
| 账目 | 计算 | 参数 |
|---|---|---|
| 每层 Attention | — | 67.11M |
| 每层 FFN | — | 135.27M |
| 每层合计 | 67.11M + 135.27M | 202.38M |
| 32 层 | 202.38M × 32 | 6,476.01M |
| 账目 | 计算 | 参数 |
|---|---|---|
| 32 层 Block | 202.38M × 32 | 6,476.01M |
| Embedding | 32000 × 4096 | 131.07M |
| LM Head | 32000 × 4096 | 131.07M |
| RMSNorm | 32×2×4096 + 4096 | 0.27M |
| 总计 | 6,476.01M + 131.07M + 131.07M + 0.27M | 6,738.42M ≈ 6.74B |
| 两张表占比 | (131.07M + 131.07M) / 6,738.42M | ≈ 3.9% |
| 若共享省下 | 131.07M / 6,738.42M | ≈ 1.9% |
「7B」不是一个大概的数字,而是每一块矩阵乘出来加总的结果:32 层的 Attention + FFN 吃掉了约 96%,首尾两张词表各占 1.95%。其中 LM Head(把隐藏向量投影成词表概率的输出层)和 Embedding 是两张独立的大表——Llama 2 没有做 weight tying(权重绑定:让输入查表和输出投影共用同一份矩阵);如果共享,就能省下 131.07M ≈ 1.9% 的参数。这也解释了工程上为什么要权衡「词表大小」和「是否共享」:词表越大,首尾两张表越贵。