文章

Transformer 架构基础

Transformer 架构基础

Attention Is All You Need(2017)——一篇论文定义了整个大模型时代。理解 Transformer 不是为了成为 AI 研究员,而是为了理解你集群里那几千张 GPU 的显存到底被谁吃了。

目录

  1. Transformer 解决了什么问题
  2. 整体架构:一张图看懂
  3. Self-Attention:核心中的核心
  4. Multi-Head Attention:多个角度看问题
  5. FFN:前馈网络
  6. 参数都藏在哪里
  7. 为什么 GPU 运维需要懂这个

1. Transformer 解决了什么问题

在 Transformer 之前,处理文本的主流方案是 RNN / LSTM——像一个只能逐字阅读的人。读到第 100 个词时,前 99 个词的记忆已经模糊了。

RNN 的工作方式:
[我] → [今天] → [很] → [开] → [心]    ← 一次只能看一个
 ↑ 必须等上一步算完才能算下一步

这带来了两个致命问题:

问题解释
无法并行必须等上一步算完,GPU 大量核心闲着
长距离遗忘句子长了,开头的词和结尾的词很难建立联系

Transformer 的做法完全不同——它让所有 token 同时互相看:

Transformer 的工作方式:
[我] ←→ [今天] ←→ [很] ←→ [开] ←→ [心]
 ↑         ↑         ↑        ↑        ↑
        所有 token 同时计算关联关系

这就是 Self-Attention 的核心思想:每个词同时关注句子中的所有词,不受距离限制。

为什么这对 GPU 很重要: 这种”同时处理所有”的模式天然适合 GPU 并行计算——千上万个 CUDA 核心终于可以同时干活,而不用排队等前一步算完。这也是为什么 Transformer 能扩展到数千亿参数、百万 token 上下文——而 RNN 永远做不到。


2. 整体架构:一张图看懂

1000

用最简单的话描述 Transformer 的流程:

输入文本


┌──────────────┐
│  Tokenizer   │  "你好世界" → [12, 456, 789]  三个 token ID
└──────────────┘


┌──────────────┐
│  Embedding   │  每个 token ID → 一个 4096 维的向量(数字列表)
└──────────────┘


┌──────────────────────────────────────────────────┐
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │        Transformer Layer × N             │   │  ← N = 32(LLaMA-7B)
│  │                                          │   │       = 80(LLaMA-70B)
│  │  ┌────────────────┐  ┌───────────────┐  │   │
│  │  │  Attention     │→│    FFN        │  │   │
│  │  │  互相看、找关联  │  │  独立加工信息  │  │   │
│  │  └────────────────┘  └───────────────┘  │   │
│  │                                          │   │
│  └──────────────────────────────────────────┘   │
│                                                  │
└──────────────────────────────────────────────────┘


┌──────────────┐
│  LM Head     │  最后一个 token → 预测下一个 token 的概率
└──────────────┘


"世" → "界" → 输出下一个词

关键概念解释:

  • Token(词元): 模型处理的最小单位。一个中文字 ≈ 1-2 个 token,“transformer” ≈ 2 个 token(“transform” + “er”)。一个 1000 字的文章 ≈ 1500-2000 tokens。
  • Embedding(嵌入): 把 token ID 映射成一个高维向量(LLaMA-7B 中是 4096 维)。你可以理解为:给每个词一组 4096 个数字,用来描述它的含义。意思相近的词,这组数字也相近。
  • Layer(层): 数据反复经过同一套操作结构。每层都在上一层的输出上继续加工。层数越多 → 模型越深 → 能学到越复杂的模式 → 但显存消耗也越大。

3. Self-Attention:核心中的核心

3.1 直觉理解

想象你是一个 token,站在一群人中间。你要判断:

  1. 这些人里谁跟你关系最密切?(Query
  2. 他们各自提供了什么信息?(Key
  3. 他们实际说了什么内容?(Value

所以 Q、K、V 就是这么来的:

角色问题实际含义
Q(Query,查询)“谁跟我有关系?“当前 token 想知道什么
K(Key,键)“我有什么信息可以提供?“每个 token 的”标签”
V(Value,值)“我的实际内容是什么?“每个 token 的真实信息

3.2 计算步骤

假设输入一句话:“我 爱 吃 苹果”,每个 token 的 Embedding 维度为 d=4096。

Step 1:投影到 Q、K、V 空间

输入 X: (4, 4096)    ← 4 个 token,每个 4096 维

Q = X × W_Q → (4, 4096)    ← 这是"提问能力"
K = X × W_K → (4, 4096)    ← 这是"被问到时的回应能力"
V = X × W_V → (4, 4096)    ← 这是"实际传递的内容"

W_Q、W_K、W_V 是三组可学习的权重矩阵(训练时会自动调整)。

Step 2:计算注意力分数

Scores = Q × Kᵀ → (4, 4)

详细图解:![[assets/注意力矩阵推导.svg|1000]]

这个 4×4 的矩阵长这样(示意):
         我    爱    吃   苹果
  我  [ 0.9  0.3  0.1  0.5 ]
  爱  [ 0.2  0.8  0.6  0.3 ]
  吃  [ 0.1  0.5  0.9  0.7 ]
 苹果 [ 0.4  0.2  0.7  0.8 ]

每一行是"这个 token 看所有 token 的注意力分数"

Step 3:Softmax 归一化

Attention_Weights = Softmax(Scores / √d_k)  ← 除以 √d_k 是为了防止分数太大

归一化后每行加起来 = 1,变成概率分布:
  [ 0.4  0.2  0.1  0.3 ]   ← "我" 最关注自己,然后"苹果"

Step 4:加权求和

Output = Attention_Weights × V → (4, 4096)

"我" 的输出 = 0.4×V_我 + 0.2×V_爱 + 0.1×V_吃 + 0.3×V_苹果

3.3 为什么会 O(n²)

这是重点——直接影响你的显存账单。

注意力分数矩阵的大小是 (token 数) × (token 数)。如果你有 n 个 token:

  • 计算量:O(n² · d) —— n² 次点积(每个 Q 和每个 K 做点积)
  • 显存需求:O(n²) —— 必须存储完整的 n×n 注意力矩阵!
序列长度 n = 1024  →  注意力矩阵 = 1024² = 100 万个元素   ← 还好
序列长度 n = 4096  →  注意力矩阵 = 4096² = 1600 万个元素  ← 开始大了
序列长度 n = 8192  →  注意力矩阵 = 8192² = 6700 万个元素  ← 很多 GPU 开始吃力
序列长度 n = 32768 →  注意力矩阵 = 32K² = 10 亿个元素    ← 这就要 FlashAttention 了
序列长度 n = 128K  →  注意力矩阵 = ...                     ← 需要 KV Cache 压缩

这是 GPU 运维最需要记住的一点:序列长度翻倍,Attention 显存涨 4 倍。


4. Multi-Head Attention:多个角度看问题

1000

4.1 为什么要多头

一个注意力头只能看到一种关系。如果只用单头,“苹果”可能只能和”吃”建立联系,却看不到”红红”和它的关系。

多头注意力 = 同时从多个角度关注:

Head可能关注的角度示例
Head 1语法关系主谓宾结构
Head 2语义关系”苹果”是水果还是公司?
Head 3位置关系相邻的词
Head 32长距离依赖段落开头和结尾的关系

4.2 实际数字:LLaMA-7B 为例

总隐藏维度 d = 4096
头数 h = 32
每头维度 d_k = 4096 / 32 = 128    ← 每头的 Q、K、V 都是 128 维

一个注意力层的数据流:

输入 X: (B, S, 4096)

          ├──→ Q: (B, S, 4096) → reshape → (B, S, 32, 128) → transpose → (B, 32, S, 128)
          ├──→ K: (B, S, 4096) → reshape → (B, S, 32, 128) → transpose → (B, 32, S, 128)
          └──→ V: (B, S, 4096) → reshape → (B, S, 32, 128) → transpose → (B, 32, S, 128)

每个头独立计算 Attention(维度 128,不是 4096)

          ├──→ Head_1: Attention(Q₁, K₁, V₁) → (B, S, 128)
          ├──→ Head_2: Attention(Q₂, K₂, V₂) → (B, S, 128)
          ├──→ ...
          └──→ Head_32: Attention(Q₃₂, K₃₂, V₃₂) → (B, S, 128)

拼接所有头 → (B, S, 4096)


    经过 W_O 投影 → (B, S, 4096)  ← 恢复到原始维度

为什么这样设计: 32 个头各做各的注意力,最后拼接起来,再过一个线性层做”融合”——这样模型能同时学到 32 种不同的注意力模式。


5. FFN:前馈网络

5.1 什么是 FFN

FFN 就是两层全连接网络,每个 token 独立经过。注意力和 FFN 各司其职:

Attention → 负责"交流":让 token 之间互相看、交换信息
   FFN   → 负责"思考":每个 token 把从 Attention 收集到的信息进行深度加工

5.2 结构

每个 Transformer 层的 FFN 长这样:

输入: (S, d)          ← LLaMA 中 d = 4096


Linear_up: d → d_ff   ← 升维,d_ff 通常是 d 的整数倍


激活函数              ← SwiGLU / GELU


Linear_down: d_ff → d ← 降维回原维度


输出: (S, d)

5.3 为什么 FFN 是 4 倍隐藏维度

LLaMA-7B 的 FFN:
  输入:  4096
         ↓  × up_proj  (4096 → 11008)
         ↓  × gate_proj (4096 → 11008)

  中间:  11008  ← 约 2.7 倍(实际用 8/3 ≈ 2.67 倍)

  输出:  4096

4 倍(或近似)是个经验值——太小了模型学不到足够知识,太大了浪费计算和显存。

5.4 激活函数:SwiGLU vs GELU

GELU(GPT 系列用):
  GELU(x) = x × Φ(x)    ← x 乘以正态分布的累积函数
  形状像一条"圆润的 ReLU",在 0 附近光滑过渡

SwiGLU(LLaMA 系列用):
  SwiGLU(x, W, V, W₂) = (xW × SiLU(xV)) × W₂
  比 GELU 多了一个 gate 机制:两路信号逐元素相乘
  效果更好但需要多 50% 的 FFN 参数量(多一个 gate 矩阵)

为什么 LLaMA 选 SwiGLU: 实验表明在相同计算量下 SwiGLU 效果更好,代价是参数多一些。


6. 参数都藏在哪里

LLaMA-7B(32 层,d=4096,d_ff=11008,V=32000) 为例,算一遍总参数量。

6.1 每层的参数分布

组件矩阵形状参数数量计算
AttentionW_Q(4096, 4096)16.8M4096²
W_K(4096, 4096)16.8M4096²
W_V(4096, 4096)16.8M4096²
W_O(输出)(4096, 4096)16.8M4096²
Attention 小计67.1M4 × 4096²
FFNup_proj(4096, 11008)45.1M4096 × 11008
gate_proj(4096, 11008)45.1M4096 × 11008
down_proj(11008, 4096)45.1M11008 × 4096
FFN 小计135.3M3 × 4096 × 11008

每层总计:67.1M + 135.3M ≈ 202.4M 参数

6.2 完整模型参数

Transformer 层: 32 × 202.4M ≈ 6477M  ← 占了 96%
Embedding 层:   Vocab × d = 32000 × 4096 ≈ 131M
输出头 (LM Head): Vocab × d = 32000 × 4096 ≈ 131M  ← 通常和 Embedding 共享
LayerNorm:      每层 2 × 4096,32 层 ≈ 0.3M              ← 几乎忽略不计

总计 ≈ 6.7B 参数 ≈ 六七亿参数

6.3 参数分布比例

                   ┌─────────────────────────────┐
                   │         FFN (~66%)          │  ← 显存大头在 FFN 的权重
                   │  up + gate + down           │
                   └─────────────────────────────┘
                   ┌───────────────────────┐
                   │    Attention (~33%)    │
                   │    QKV + Output        │
                   └───────────────────────┘
                   ┌──────┐
                   │Embed │ (~2%)    ← 几乎忽略
                   └──────┘

重点:FFN 占了约 2/3 的参数量,所以你集群中大部分显存存的是 FFN 权重。


7. 为什么 GPU 运维需要懂这个

7.1 Attention 和 FFN 的显存行为完全不同

AttentionFFN
操作类型大量小矩阵乘法、Softmax两个大矩阵乘法
瓶颈显存带宽(Memory-bound)算力(Compute-bound)
为什么Attention 矩阵太大,大部分时间花在读写显存上,GPU 核心在等数据大矩阵乘法能喂饱 GPU 计算单元,效率高
优化方向FlashAttention、KV Cache 压缩使用 Tensor Core、FP8 加速

7.2 KV Cache:推理时的显存怪物

推理时(生成文本),每个新 token 的 Attention 需要看之前所有 token 的 K 和 V。如果不做缓存,每生成一个 token 都要重新算一遍所有历史 K、V——O(n²) 的重计算。

KV Cache 的做法: 把之前所有 token 的 K 和 V 存起来,新 token 直接用。

KV Cache 的显存消耗(LLaMA-7B, BF16):

每层:  2(K+V)× 32(头数)× 128(头维度)× n_token × 2字节 ≈ 16384 × n_token 字节
32层:  16384 × n_token × 32 ≈ 524K × n_token 字节

如果 n_token = 4096:   KV Cache ≈ 524K × 4096 ≈ 2.1 GB
如果 n_token = 32768:  KV Cache ≈ 524K × 32768 ≈ 17 GB
如果 n_token = 128K:   KV Cache ≈ 524K × 131072 ≈ 68 GB

这就是为什么长上下文推理疯狂吃显存——每多一个 token,KV Cache 就多存一份。

7.3 FlashAttention:不用存完整注意力矩阵

标准 Attention 的最大问题是:必须把完整的 S×S 注意力矩阵存在显存里,然后再软最大化。S=128K 时,这个矩阵就有 160 亿个元素。

FlashAttention 的 trick:

不再一次算完整个 Attention:
  不存注意力的原始矩阵

  把 Q、K、V 分块加载到 GPU 的片上共享内存(SRAM)

  在芯片内部算完一块直接输出,不写回显存

  显存里只存最终的输出,不存中间的注意力权重矩阵

效果:显存从 O(n²) 降到 O(n),同时因为避免了显存读写,反而更快。

7.4 训练 vs 推理的显存差异

项目训练推理
模型权重
优化器状态2-3×(Adam 的 m、v)
梯度
激活值(重计算前)每层都有,很大只需当前层
KV Cache无(每步重新算)有,随序列增长
典型显存比例模型:2× / 优化器:6× / 激活:4×模型:1× / KV Cache: 随序列增大

训练时显存大头的公式:

Total ≈ 权重(2B) + 优化器(12B) + 激活值(可变)
      ≈ 模型参数 × 18-20 倍(BF16 + Adam)

8. Normalization 与残差连接

8.1 为什么需要 Normalization

深层网络的每一层输出,数据分布会逐渐偏移。不归一化的话,越深的层输入越”畸形”,训练越来越不稳定。

Normalization 做的事:把每层的数据拉到均值为 0、方差为 1 的分布

8.2 LayerNorm(原始 Transformer 用)

输入: x ∈ R^d

μ = mean(x)           ← 均值
σ² = var(x)           ← 方差
x̂ = (x - μ) / √(σ² + ε)   ← 归一化
out = γ × x̂ + β           ← 缩放 + 平移(γ、β 是可学习的参数)

γ 和 β 的作用:让模型保留"不归一化"的能力

8.3 RMSNorm(LLaMA 用,更快)

LayerNorm: 需要算均值 AND 方差 → 两次遍历
RMSNorm:  只算 RMS(均方根)→ 一次遍历

RMS(x) = √(mean(x²))

out = x / RMS(x) × γ

比 LayerNorm 少 50% 的计算量,效果几乎一样
LLaMA 全系列用 RMSNorm,因为:
  1. 更快(只算平方均值,不算普通均值)
  2. 更少的参数(不需要 β)
  3. 实验证明效果同等

8.4 残差连接(Residual Connection)

图解:1000

残差连接的公式只有一行

output = Layer(input) + input

就这么简单——把输入原封不动地加到 Layer 的输出上。

为什么需要这个? 看反向传播时的梯度:

没有残差:output = Layer(input)
  梯度 ∂L/∂input = ∂L/∂output × ∂Layer/∂input
  每层都要乘一次 ∂Layer/∂input → 乘 N 次 → 梯度消失

有残差:output = Layer(input) + input
  梯度 ∂L/∂input = ∂L/∂output × ∂Layer/∂input + ∂L/∂output × 1
                   ↑ 走 Layer 的那条路径        ↑ 走短路的那条路径
  
  短路路径的导数是常数 1 → 跟层数无关 → 梯度永远至少能传回来 1 倍!

用一个具体数字感受

假设每层的 ∂Layer/∂input ≈ 0.5(小于 1,这是常态)

没有残差,5 层后的梯度:1.0 → 0.5 → 0.25 → 0.125 → 0.0625 → 0.03
  → 只剩 3%,前几层基本学不到东西

有残差,5 层后的梯度:1.0 → 1.5 → 2.25 → 3.38 → 5.06 → 7.59
  → 梯度不但没消失,还在稳定传播!

类比:残差连接就像给梯度开了一条”高速公路”。没有它,梯度必须一层一层挤过去(每挤一次就衰减一次);有了它,梯度可以直接飙到最底层。这就是为什么 LLaMA-70B 的 80 层、GPT-4 的上百层都能训练——没有残差,超过 10 层就训不动了。

LLaMA 一层里的两个残差

输入 x

  ├──→ RMSNorm → Attention ──→ [+] ──→ 输出 a    ← 残差 1:x 跳到 Attention 后面
  │                   ↑         ↑
  │                   │    a = Attn(Norm(x)) + x

  │    a ──→ RMSNorm → FFN ──→ [+] ──→ 输出 h    ← 残差 2:a 跳到 FFN 后面
  │                   ↑         ↑
  │                   │    h = FFN(Norm(a)) + a
  └── 残差 1 ────────→ ↑
      残差 2 ──────────────────→ ↑

每个 Transformer 层有两个残差连接:一个跨 Attention,一个跨 FFN。两层保护,梯度更稳定。

那残差把输入直接传过去了,Attention/FFN 还有意义吗?

图解:1000

这是个常见的误解——残差和主干不是竞争关系,是分工关系

残差的角色是「保存已有的好东西」,Attention/FFN 的角色是「找出需要改进的地方」。输出 = x + F(x),F(x) 学的永远是相对于 x 的修正量,不是完整的新值。

类比写文章:残差是把上一版草稿放旁边,Attention/FFN 在上面做批注——「这里加一句、那里改一下」。不是扔掉旧稿重写,而是在底稿上做增量修改。这也是为什么「F(x)=0 时输出还是 x」是精妙的设计而非缺陷——它让 100 层的网络可以安全地从「什么都不做」开始,一层一层往上叠能力。### 8.5 Pre-Norm vs Post-Norm

Post-Norm(原始 Transformer):
  x → Attention(x) → x + Attn_out → Norm → FFN → x + FFN_out → Norm
  优点:输出稳定
  缺点:深层梯度消失,训练不稳定

Pre-Norm(LLaMA 用):
  x → Norm → Attention(x) → x + Attn_out → Norm → FFN → x + FFN_out
  优点:梯度传播稳定,深层网络也能训练
  缺点:轻微性能损失(但 Pre-Norm 带来的训练稳定性远超这点损失)

LLaMA 的完整层结构:
  x ──→ RMSNorm ──→ Attention ──→ + ──→ RMSNorm ──→ FFN ──→ + ──→ 输出
  │                                 ↑                         ↑
  └─────────────────────残差────────┘                         │
  └──────────────────────────────────────────残差─────────────┘

9. 位置编码(Positional Encoding)

9.1 为什么需要位置编码

Self-Attention 本身是位置无关的——“我 爱 你”和”你 爱 我”在注意力机制看来是一样的(只是 token 不同)。位置编码告诉模型每个 token 在序列中的位置。

9.2 正弦位置编码(原始 Transformer)

PE(pos, 2i)   = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

pos: 位置索引 (0, 1, 2, ...)
i:   维度索引 (0, 1, ..., d/2-1)
d:   嵌入维度

效果:不同位置有不同的编码向量
     位置相近 → 编码相近(cos 和 sin 的连续性质)

9.3 RoPE(旋转位置编码,LLaMA 全系标配)

这是当前最重要、最常用的位置编码方案。

核心思想:不把位置编码加到 token 上,而是旋转 token 的 Q 和 K 向量,让它们的点积结果隐含位置信息。

传统:token_embedding + position_embedding → 加性的

RoPE:用旋转矩阵 R(θ, pos) 旋转 Q 和 K → 乘性的

第 i 对维度的旋转:
  [q_2i]   [cos(m·θ_i)  -sin(m·θ_i)] [q_2i  ]
  [q_2i+1] = [sin(m·θ_i)   cos(m·θ_i)] [q_2i+1]

  m: token 位置    θ_i = 10000^(-2i/d)  (与正弦编码相同的频率)

效果解析:
  两个 token 的 Q 和 K 的点积:
    (R_m · Q) · (R_n · K) = Q · R_{n-m} · K
  
  → 点积结果只依赖两个 token 的**相对位置差 n-m**
  → 天然支持相对位置
  → 可以外推到训练时没见过的长度(这是 RoPE 最大的优势!)

为什么 RoPE 是最好的选择

1. 相对位置:注意力天然依赖相对距离,而非绝对位置
2. 外推能力:训练用 4K 上下文,推理可以无损扩展到 8K/16K
   (配合 NTK-aware scaling 甚至可以到 128K+)
3. 不增加参数:不需要额外学习位置向量
4. 与 Attention 深度融合:位置信息直接编码在 Q·K 计算中

RoPE 外推

训练时 max_seq_len = 4096,但推理可以用 8192:

标准外推:直接跑 → 位置 5000 的频率 θ 训练时没见过 → 效果差

NTK-aware scaling:
  把 θ_i 乘以缩放因子 → 让高频保持不变,低频更密集
  → 训练时见过的低频模式覆盖更长位置

9.4 ALiBi(线性偏置注意力)

GPT-NeoX、BLOOM 等模型使用,比 RoPE 更简单

不修改 Q、K,而是直接给注意力分数加上一个线性偏置:

Attention_Score[i][j] = Q_i · K_j - m × |i - j|

m: 每头不同的斜率

效果:越远的 token 越被"惩罚",鼓励关注近处
优点:极简,零额外参数,天然支持任意长度外推
缺点:表达能力不如 RoPE

9.5 位置编码方案对比

方案原理参数外推用在哪
Sinusoidalsin/cos 函数原始 Transformer
Learned可学习的 Embedding无(定长)GPT-1/2
RoPE旋转 Q、K✅ 最好LLaMA、Qwen、GLM、DeepSeek
ALiBi线性偏置✅ 好BLOOM、GPT-NeoX

10. Decoder-Only 架构

10.1 为什么现代 LLM 都是 Decoder-Only

原始 Transformer 有 Encoder 和 Decoder 两部分。但 GPT 之后,所有主流 LLM(GPT、LLaMA、DeepSeek、Gemini)都只用 Decoder。

Encoder-Decoder (T5, BART):
  输入 → Encoder → 中间表示 → Decoder → 输出
  适合:翻译、摘要(输入和输出长度不同)

Decoder-Only (GPT, LLaMA):
  输入 → Decoder → 输出
  用 Causal Mask 保证只能看到之前的 token
  
  为什么好:
  1. 更简单:少一半的架构,训练和推理都简单
  2. 更高效:Encoder 和 Decoder 共享参数的话就是一份,不共享就是两份
  3. 因果性:自回归生成天然适配 Decoder
  4. Scaling 好:更多层 → 更好的效果,没有 Encoder 瓶颈

10.2 Causal Mask(因果掩码)

Decoder-Only 的核心约束:第 i 个 token 只能看到第 0 到第 i 个 token,不能偷看后面的。

原始注意力矩阵(能看到所有人):
        我   爱   吃   苹果
  我  [ ✓   ✓   ✓   ✓  ]   ← 能看到未来,作弊了!
  爱  [ ✓   ✓   ✓   ✓  ]
  吃  [ ✓   ✓   ✓   ✓  ]
 苹果 [ ✓   ✓   ✓   ✓  ]

加 Causal Mask 后:
        我   爱   吃   苹果
  我  [ ✓   ✗   ✗   ✗  ]   ← 只能看自己
  爱  [ ✓   ✓   ✗   ✗  ]   ← 能看"我"和"爱"
  吃  [ ✓   ✓   ✓   ✗  ]   ← 能看前面三个
 苹果 [ ✓   ✓   ✓   ✓  ]   ← 能看所有

实现:在 Softmax 前把未来位置的分数设为 -∞
      → Softmax 后变成 0 → 不会关注未来

10.3 自回归生成

Input:  "今天天气"
Step 1: Forward("今天天气")   → predict "真"
Step 2: Forward("今天天气真") → predict "好"
Step 3: Forward("今天天气真好") → predict "<eos>"

每一步的输出只取最后一个 token 的预测,拼到序列后面继续。

11. 一层 Transformer 的完整前向传播

把前面所有组件串起来,看一个 token 如何经过一层 Transformer:

输入:h_l ∈ R^(S × d)    ← 上一层输出(或第一层的 Embedding)
                             S = 序列长度, d = 隐藏维度

─────────────────────── Attention Block ───────────────────────

Step 1: RMSNorm
  h_norm = RMSNorm(h_l)

Step 2: 投影到 Q, K, V
  Q = h_norm × W_Q   (S, d) × (d, d) → (S, d)
  K = h_norm × W_K
  V = h_norm × W_V

Step 3: 添加 RoPE
  Q_rope = RoPE(Q)     ← 对每对维度应用旋转
  K_rope = RoPE(K)

Step 4: 拆分为多头
  Q: (S, d) → reshape → (S, num_heads, d_head) → transpose → (num_heads, S, d_head)
  K: 同上
  V: 同上
  例 LLaMA-7B: num_heads=32, d_head=128

Step 5: 计算注意力(每个头独立)
  Scores = Q × K^T / √d_head      → (num_heads, S, S)
  Scores = Scores + Causal_Mask    → 未来位置 → -∞
  Weights = Softmax(Scores)        → 归一化到 [0,1]
  Attn_Out = Weights × V           → (num_heads, S, d_head)

Step 6: 合并多头
  Attn_Out: transpose → (S, num_heads, d_head) → reshape → (S, d)

Step 7: 输出投影
  Attn_Out = Attn_Out × W_O   (S, d) × (d, d) → (S, d)

Step 8: 残差连接
  h_attn = h_l + Attn_Out

─────────────────────── FFN Block ───────────────────────

Step 9: RMSNorm
  h_norm2 = RMSNorm(h_attn)

Step 10: SwiGLU FFN
  gate = h_norm2 × W_gate    (S, d) × (d, d_ff) → (S, d_ff)
  up   = h_norm2 × W_up
  activated = SiLU(gate) ⊙ up    ← ⊙ = 逐元素乘法

Step 11: 降维
  out = activated × W_down    (S, d_ff) × (d_ff, d) → (S, d)

Step 12: 残差连接
  h_{l+1} = h_attn + out

─────────────────────── 一层完成 ───────────────────────

输出 h_{l+1} ∈ R^(S × d) → 作为下一层的输入

每层做两次矩阵乘法的总 FLOPs

Attention: QKV 投影 3×d×d + 输出投影 1×d×d + S²×d_head × heads
         ≈ 4d² + 2·S²·d     (2 来自 QK^T 和 weights×V)

FFN (SwiGLU): up×d×d_ff + gate×d×d_ff + down×d_ff×d
            ≈ 3·d·d_ff

以 LLaMA-7B (d=4096, d_ff=11008, S=4096):
  Attention: 4×4096² + 2×4096²×4096 ≈ 67M + 137G ≈ 137G FLOPs
  FFN: 3×4096×11008 ≈ 135M FLOPs
  
  Attention 的 S² 项在长序列时主导 → 这就是为什么注意力是瓶颈

12. 训练:损失函数与反向传播

1000

反向传播解决一个看似不可能的问题:模型有几亿个参数,每个都对最终预测有贡献,你怎么知道每个该调多少?

思路:沿着前向的计算路径,把误差一层一层往回传。最后一层的误差(预测 vs 真值)通过链式法则逐层分解,最终每个参数都收到一个属于自己的修正信号——这个信号精确量化了「如果你变大/变小一点,Loss 会怎么变」。

对 GPU 运维最重要的是:前向时必须把每层的激活值存下来(反向要用),这就是训练显存远超推理显存的根本原因。

12.1 Next-Token Prediction(下一个 Token 预测)

LLM 的训练目标极其简单:给定前面的 token,预测下一个 token

训练数据:"今天天气真好"
Tokenize → [今天, 天气, 真, 好]

Training samples:
  Input:  [今天]           → Target: 天气
  Input:  [今天, 天气]     → Target: 真
  Input:  [今天, 天气, 真] → Target: 好

为什么这能学到一切:要准确预测下一个词,模型必须理解语法、语义、常识、逻辑——这些知识都隐含在”什么词应该出现在什么词后面”的统计规律中。

12.2 Cross-Entropy Loss(交叉熵损失)

模型的最后一层输出是 logits: (S, Vocab_Size)
→ 对每个位置,Vocab_Size 个 logit,表示每个词的可能性

Softmax: 把 logits 变成概率
  P(token_i) = exp(logit_i) / Σ exp(logit_j)

Cross-Entropy Loss:
  L = -1/N × Σ log(P(correct_token_at_position_i))

直观理解:
  如果模型很确定正确答案 → P≈1 → -log(1)≈0 → loss 小
  如果模型完全猜错 → P≈0 → -log(0)→∞ → loss 大

训练的 perplexity

Perplexity = exp(Loss)
可以理解为:模型在每一步相当于从多少个选项中"猜"

Perplexity=10  → 模型每次平均从 10 个候选中猜,水平不错
Perplexity=50  → 模型每次从 50 个候选中猜,还需要训练
Perplexity=3   → 模型非常确信(但也可能过拟合)

12.3 梯度反向传播

Forward:  h_0 → h_1 → ... → h_L → logits → loss
          ↑ 每层产生激活值(需要存着给反向用)

Backward: ∂L/∂logits → ∂L/∂h_L → ... → ∂L/∂h_0 → ∂L/∂W
          ↑ 链式法则:每层的梯度 = 下一层梯度 × 本层导数

对 Attention 的反向:
  ∂L/∂Q = ∂L/∂Attn_Out × ∂Attn_Out/∂Weights × ∂Weights/∂Scores × ∂Scores/∂Q
  ↑ 需要存着 Weights (= Softmax(QK^T/√d)) → 这就是 O(n²) 的激活值!

对 FFN 的反向:
  ∂L/∂W_up = ∂L/∂activated × ∂activated/∂W_up
  ↑ 只需要存 activated,大小约 S × d_ff → 比 Attention 的 S×S 小得多

这就是为什么激活值显存大头在 Attention——反向传播时需要存 S×S 的注意力权重矩阵,而 FFN 只需要 S×d_ff。


关联知识

学习时间

阶段时间备注
骨架创建2026-06-30第一次学习
完整笔记2026-06-30重新整理,面向 GPU 运维视角
大幅扩充2026-06-30新增 Norm/Residual、RoPE、Decoder-Only、完整前向传播、训练 Loss

状态标记

📖 已掌握 — Transformer 完整架构、Self-Attention/QKV、Multi-Head、FFN/SwiGLU、参数分布、Norm/残差、RoPE 位置编码、Decoder-Only 因果掩码、完整前向传播流程、Cross-Entropy Loss、反向传播 📝 待补充 — vLLM PagedAttention 实现细节、GQA/MQA 压缩方案、MoE 架构深入