Decoder-Only LLM 深度解析:为什么扔掉 Encoder,以及 KV Cache 如何工作
📅 创建时间:2026-07-29 🏷️ 标签:#DecoderOnly #GPT #KVCache #CausalAttention #AutoRegressive #Inference 📚 前置知识:[[08-transformer-by-hand]](理解了 Q、K、V、Self-Attention、Encoder-Decoder 数据流)
📋 本章目标
- 理解 GPT/Claude/DeepSeek 为什么不需要 Encoder——以及 Encoder 到底"多余"在哪里
- 掌握 Causal (Masked) Self-Attention 如何让一个句子变成 N 个训练样本
- 理解训练时的一次前向传播如何并行产出所有位置的预测
- 掌握 KV Cache 的完整原理——为什么推理时不需要重新计算过去 token 的 K 和 V
- 手算 KV Cache 的内存占用,理解为什么 2048 token 的上下文需要 2GB 显存
- 理解 Decoder-Only 架构为什么在 Scaling 和 In-Context Learning 上碾压 Encoder-Decoder
- 从此看任何 LLM 架构图都能一眼认出"这是 Decoder-Only 还是 Encoder-Decoder"
第0部分:回顾——Encoder-Decoder 干了什么,GPT 要干什么完全不同的事
0.1 上篇文章的核心回顾
在 [[08-transformer-by-hand]] 中,我们用具体数字走完了一个完整的 Encoder-Decoder Transformer:
┌─────────────────────────────────────────────────────────────┐
│ Encoder-Decoder 做了什么(翻译 "我爱你" → "I love you")│
├─────────────────────────────────────────────────────────────┤
│ │
│ Encoder(只读源语言): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "我""爱""你" → Embedding → Self-Attn → FFN │ │
│ │ → 输出:每个源 token 融合了上下文信息的向量 │ │
│ │ → 这是"要被翻译的内容"的完整表示 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ Decoder(逐词生成目标语言): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Masked Self-Attn:看已经生成的词("<s>" → "I") │ │
│ │ 2. Cross-Attention: │ │
│ │ Q 来自 Decoder("我现在要生成什么?") │ │
│ │ K,V 来自 Encoder("源语言提供了什么信息?") │ │
│ │ → Decoder 从 Encoder 输出中"检索"翻译线索 │ │
│ │ 3. FFN → 预测下一个词 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键依赖:Encoder 和 Decoder 是两个独立的栈。 │
│ Cross-Attention 是它们唯一的交互点。 │
│ │
└─────────────────────────────────────────────────────────────┘这个架构天然适合 seq2seq 任务——输入和输出是两个不同的序列(源语言 → 目标语言)。
0.2 GPT 要干什么?
GPT(Generative Pre-trained Transformer)的任务不是翻译。它是:
用户输入(Prompt):"1 + 1 = ?"
模型输出(Response):"2"或者更复杂的:
用户输入:"请解释一下光合作用。"
模型输出:"光合作用是植物利用光能……(一段长文本)"GPT 没有"源语言"和"目标语言"之分。 输入和输出是同一种语言的连续文本。用户的问题和模型的回答,本质上是同一个序列的不同部分。
0.3 根本问题:Encoder 在这里是多余的
┌─────────────────────────────────────────────────────────────┐
│ 如果强行用 Encoder-Decoder 做 GPT 的任务 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入:"1 + 1 = ?" │
│ 期望输出:"2" │
│ │
│ 如果使用 Encoder-Decoder: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Encoder 读入 "1 + 1 = ?" │ │
│ │ → 输出 4 个 token 的上下文表示 │ │
│ │ │ │
│ │ Decoder 从 "<s>" 开始生成 │ │
│ │ → 通过 Cross-Attention 查询 Encoder 输出 │ │
│ │ → 生成 "2" │ │
│ │ │ │
│ │ 问题: │ │
│ │ 1. Encoder 单独处理输入 → 额外参数、额外计算 │ │
│ │ 2. Cross-Attention 连接两个栈 → 额外参数 │ │
│ │ 3. "1+1=?" 和 "2" 本质上是同一序列——为什么要 │ │
│ │ 分两个栈处理? │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 核心洞察: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 在 "1+1=?2" 这个连续文本中, │ │
│ │ "1+1=?" 和 "2" 是同一个序列的前缀和后缀。 │ │
│ │ │ │
│ │ 如果模型能在序列内部建立依赖关系(Causal Attention),│ │
│ │ 那 Encoder 和 Cross-Attention 都是冗余的。 │ │
│ │ │ │
│ │ GPT 的答案:把整个序列放进一个栈,用 Causal │ │
│ │ Self-Attention 让每个 token 关注前面所有 token。 │ │
│ │ 这就是 Decoder-Only。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第1部分:Decoder-Only 架构——把 Encoder 扔掉,只剩一个栈
1.1 两张架构图:Encoder-Decoder vs Decoder-Only
┌─────────────────────────────────────────────────────────────┐
│ Encoder-Decoder(原始 Transformer) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ ENCODER │ │ DECODER │ │
│ │ ┌─────┐ │ │ ┌─────┐ │ │
│ │ │Self │ │ │ │Mask │ │ │
│ │ │Attn │ │ │ │Self │ │ │
│ │ │ │ │ │ │Attn │ │ │
│ │ └──┬──┘ │ │ └──┬──┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌──┴──┐ │ │ ┌──┴──┐ │ │
│ │ │ FFN │ │ │ │Cross│ │← K,V from Encoder │
│ │ │ │ │ │ │Attn │ │ │
│ │ └─────┘ │ │ └──┬──┘ │ │
│ │ │ │ │ │ │
│ │ × N 层 │ │ ┌──┴──┐ │ │
│ │ │ │ │ FFN │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └─────┘ │ │
│ │ │ │ │ │
│ │ │ │ × N 层 │ │
│ └───────────┘ └───────────┘ │
│ │
│ 两个独立的 Transformer 栈,通过 Cross-Attention 连接。 │
│ 总参数量 = Encoder参数 + Decoder参数 + Cross-Attn参数 │
│ │
├─────────────────────────────────────────────────────────────┤
│ Decoder-Only(GPT / Claude / DeepSeek) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DECODER-ONLY STACK │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Masked Self-Attention│ ← 只有这一种 Attention! │ │
│ │ │ (Causal Attention) │ 没有 Cross-Attention │ │
│ │ └──────────┬───────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Feed-Forward Network │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ │ × L 层(GPT-3: 96层, LLaMA-7B: 32层) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 只有一个栈。每个 token 通过 Causal Attention │
│ 看到前面所有 token。前缀自己就是"上下文"。 │
│ │
└─────────────────────────────────────────────────────────────┘1.2 Decoder-Only 的核心:Causal Self-Attention
在 Decoder-Only 中,唯一的 Attention 机制就是 Masked Self-Attention(也叫 Causal Attention)。原因是:没有 Encoder,所以没有 Cross-Attention 的 K 和 V 来源。
┌─────────────────────────────────────────────────────────────┐
│ Causal Self-Attention:每个 token 只能看到过去 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入序列:"1 + 1 = ?"(假设已分词为 5 个 token) │
│ │
│ Token 0 "1" │
│ 可以看到:[1] ← 只有自己 │
│ 用于预测:下一个 token "+" │
│ │
│ Token 1 "+" │
│ 可以看到:[1, +] ← 自己和之前的 │
│ 用于预测:下一个 token "1" │
│ │
│ Token 2 "1" │
│ 可以看到:[1, +, 1] ← 前三个 │
│ 用于预测:下一个 token "=" │
│ │
│ Token 3 "=" │
│ 可以看到:[1, +, 1, =] ← 前四个 │
│ 用于预测:下一个 token "?" │
│ │
│ Token 4 "?" │
│ 可以看到:[1, +, 1, =, ?] ← 全部五个 │
│ 用于预测:下一个 token "2" │
│ │
│ 规律:位置 i 的 token 只能 attend 位置 0,1,...,i。 │
│ 这是通过 Causal Mask(下三角矩阵)实现的。 │
│ │
└─────────────────────────────────────────────────────────────┘1.3 Causal Mask 的矩阵形式
┌─────────────────────────────────────────────────────────────┐
│ Causal Attention Mask(5 个 token 的例子) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Attention 分数矩阵(Softmax 之前,加上 Mask 之后): │
│ │
│ Token0 Token1 Token2 Token3 Token4 │
│ Token0 [ s₀₀ │ -∞ │ -∞ │ -∞ │ -∞ ] │
│ Token1 [ s₁₀ │ s₁₁ │ -∞ │ -∞ │ -∞ ] │
│ Token2 [ s₂₀ │ s₂₁ │ s₂₂ │ -∞ │ -∞ ] │
│ Token3 [ s₃₀ │ s₃₁ │ s₃₂ │ s₃₃ │ -∞ ] │
│ Token4 [ s₄₀ │ s₄₁ │ s₄₂ │ s₄₃ │ s₄₄ ] │
│ │
│ 上三角 = -∞ → Softmax 后变为 0 │
│ 下三角(含对角线)= 正常分数 → Softmax 后非零 │
│ │
│ 结果:Attention Weights 矩阵也是下三角的。 │
│ 每一行的有效权重之和 = 1.0(只在允许的位置上分配)。 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 核心认知:Causal Mask 不是"可有可无的技巧"—— │ │
│ │ 它是 Decoder-Only 的根基。 │ │
│ │ │ │
│ │ 训练时:防止模型"作弊"看到答案 │ │
│ │ 推理时:自然地只能看到已生成的 token │ │
│ │ 同一个 Mask,同时服务于训练和推理! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.4 前缀即上下文——Decoder-Only 的核心哲学
┌─────────────────────────────────────────────────────────────┐
│ Decoder-Only 的"上下文"就是序列的前缀 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 在 Encoder-Decoder 中: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "上下文" = Encoder 的输出(一个独立的表示) │ │
│ │ 上下文和生成的 token 是分开处理的 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 在 Decoder-Only 中: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "上下文" = 当前 token 之前的所有 token │ │
│ │ 上下文和生成的 token 在同一个序列中 │ │
│ │ │ │
│ │ 输入:"请翻译成英文:我爱你" │ │
│ │ 模型将其视为一个序列,生成续写: │ │
│ │ "请翻译成英文:我爱你" → "I" → "love" → "you" │ │
│ │ │ │
│ │ 用户的指令("请翻译…")和模型的回答("I love you") │ │
│ │ 在模型中走的是同一个栈——它们没有任何架构上的区别。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 这就是为什么 Few-Shot Prompting 天然有效: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "把下面的词翻译成英文: │ │
│ │ 猫 → cat │ │
│ │ 狗 → dog │ │
│ │ 鸟 → " │ │
│ │ │ │
│ │ 前面的 "猫→cat" 和 "狗→dog" 通过 Causal Attention │ │
│ │ 影响最后 "鸟→" 位置的表示——这就是 In-Context │ │
│ │ Learning 的机制。不需要更新参数,前面的 token 的 │ │
│ │ K 和 V 直接在 Attention 中影响后面的预测。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:GPT 的训练——一句话提供 N 个训练样本
2.1 Decoder-Only 的训练效率是 Encoder-Decoder 的杀手锏
┌─────────────────────────────────────────────────────────────┐
│ 一句话 "1+1=2" 如何变成 4 个训练样本(一次前向传播) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入序列(Token IDs):[1, +, 1, =, 2] │
│ 目标序列(shifted right):[+, 1, =, 2, <eos>] │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Position 0: 看到 [1] │ │
│ │ → 模型预测下一个 token → 目标:"+" │ │
│ │ → Loss₀ = CrossEntropy(预测, "+") │ │
│ │ │ │
│ │ Position 1: 看到 [1, +] │ │
│ │ → 模型预测下一个 token → 目标:"1" │ │
│ │ → Loss₁ = CrossEntropy(预测, "1") │ │
│ │ │ │
│ │ Position 2: 看到 [1, +, 1] │ │
│ │ → 模型预测下一个 token → 目标:"=" │ │
│ │ → Loss₂ = CrossEntropy(预测, "=") │ │
│ │ │ │
│ │ Position 3: 看到 [1, +, 1, =] │ │
│ │ → 模型预测下一个 token → 目标:"2" │ │
│ │ → Loss₃ = CrossEntropy(预测, "2") │ │
│ │ │ │
│ │ Position 4: 看到 [1, +, 1, =, 2] │ │
│ │ → 模型预测下一个 token → 目标:"<eos>" │ │
│ │ → Loss₄ = CrossEntropy(预测, "<eos>") │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 总 Loss = (Loss₀ + Loss₁ + Loss₂ + Loss₃ + Loss₄) / 5 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 关键:这 5 个位置的预测是并行的! │ │
│ │ │ │
│ │ 一次前向传播 → 所有位置的 hidden states → │ │
│ │ 所有位置的预测分布 → 所有位置的 Loss │ │
│ │ │ │
│ │ 因为有 Causal Mask,位置 i 的计算只依赖位置 0~i。 │ │
│ │ 这个依赖关系在 Attention 矩阵中自然满足—— │ │
│ │ 不需要串行计算!矩阵乘法一步到位! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 和 Encoder-Decoder 的训练效率对比
┌─────────────────────────────────────────────────────────────┐
│ 训练效率:Decoder-Only vs Encoder-Decoder │
├─────────────────────────────────────────────────────────────┤
│ │
│ Encoder-Decoder(翻译任务): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 需要"配对数据":每个训练样本 = (源句子, 目标句子) │ │
│ │ 例如:("我爱你", "I love you") │ │
│ │ │ │
│ │ 源句子 → Encoder(一次前向) │ │
│ │ 目标句子 → Decoder(一次前向,Teacher Forcing) │ │
│ │ │ │
│ │ 一个样本对 → 目标句子长度 N 个训练信号 │ │
│ │ 需要人工标注的平行语料(昂贵) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Decoder-Only(GPT 风格): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 只需要"原始文本":任何文本就是训练数据! │ │
│ │ 例如:"1+1=2" 本身就是训练样本 │ │
│ │ │ │
│ │ 序列 → Decoder(一次前向,Causal Mask) │ │
│ │ │ │
│ │ 一条长度为 N 的文本 → N-1 个训练信号 │ │
│ │ 数据来源:整个互联网(几乎免费) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 实际影响: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GPT-3 的训练数据:~500B tokens(来自网页、书籍等) │ │
│ │ 如果用 Encoder-Decoder:需要 500B 个"输入-输出"对 │ │
│ │ → 根本不可能标注这么多配对数据。 │ │
│ │ │ │
│ │ Decoder-Only 的一个关键优势: │ │
│ │ 互联网上的每一段文本,无需任何人工标注, │ │
│ │ 就是 N 个"next token prediction"训练样本。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.3 训练的具体流程(用数字说话)
┌─────────────────────────────────────────────────────────────┐
│ GPT 训练的一次 Step(手算级别的具体数字) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 假设: │
│ • 序列长度 N = 2048 tokens(GPT-3 的上下文窗口) │
│ • 词表大小 V = 50257(GPT-2/3 的词表) │
│ • d_model = 4096(GPT-3 的隐藏维度,不过实际是 12288) │
│ • 层数 L = 96(GPT-3 175B) │
│ │
│ 一步训练的计算量: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Embedding: 2048 × 4096 → 查表,O(N × d_model) │ │
│ │ 2. 每层 Self-Attention: │ │
│ │ Q=XWq, K=XWk, V=XWv: 3 × (2048×4096)×(4096×4096)│ │
│ │ QK^T: (2048×4096) × (4096×2048) │ │
│ │ Attn×V: (2048×2048) × (2048×4096) │ │
│ │ → O(N² × d_model) per layer │ │
│ │ 3. FFN: 2048 × 4096 × (4×4096) × 4096 │ │
│ │ → O(N × d_model²) per layer │ │
│ │ 4. 最后一层 → Linear(V) → Softmax → CrossEntropy │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键观察:Self-Attention 的 O(N² × d_model) 是瓶颈。 │
│ 但训练时 N² 没法避免——每个 token 需要 attend 所有 token。 │
│ │
│ 不过,CUDA 的矩阵乘法高度并行化,2048² 的矩阵乘法 │
│ 在现代 GPU 上很快(几毫秒)。真正的瓶颈在推理阶段。 │
│ │
└─────────────────────────────────────────────────────────────┘第3部分:KV Cache——推理时不需要重新计算过去
这是整个 Decoder-Only LLM 推理优化的核心。如果你只记住一个概念,记这个。
3.1 问题:没有 KV Cache,推理会慢到什么程度?
┌─────────────────────────────────────────────────────────────┐
│ Auto-Regressive 生成:为什么 O(N²) 的暴力方案不可行 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 假设模型已经训练好了。现在用户输入 "1+1=?",模型要生成 "2"。│
│ │
│ 输入序列:[1, +, 1, =, ?] (5 个 token) │
│ │
│ Step 1: 前向传播整个序列 [1, +, 1, =, ?] │
│ → 计算所有 5 个 token 的 Q, K, V │
│ → 最后一个位置的输出 → 预测 token "2" │
│ → Attention 计算量:5² = 25 个点积 │
│ │
│ Step 2: 拼上 "2",输入变为 [1, +, 1, =, ?, 2] (6 token) │
│ → 重新计算所有 6 个 token 的 Q, K, V │
│ → 最后一个位置的输出 → 预测 <eos> │
│ → Attention 计算量:6² = 36 个点积 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 问题:Step 1 已经算过前 5 个 token 的 K 和 V 了! │ │
│ │ Step 2 又算了一遍——完全一样的计算! │ │
│ │ │ │
│ │ 对于生成长度为 M 的序列(M 可以到几千): │ │
│ │ │ │
│ │ 总 Attention 计算量 ≈ Σ(i²) for i=1..M │ │
│ │ ≈ M³ / 3 │ │
│ │ │ │
│ │ 生成 2048 token:需要 ~2.86 亿次点积 │ │
│ │ 相当于在推理时走了一个 O(M³) 的算法 │ │
│ │ → 完全不可接受! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.2 KV Cache 的核心思想:过去的 K 和 V 不会变
┌─────────────────────────────────────────────────────────────┐
│ 为什么过去 token 的 K 和 V 永远不会变?(关键洞察) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 回忆 Attention 的计算: │
│ │
│ 对于位置 i: │
│ Q_i = X'_i × Wq │
│ K_j = X'_j × Wk (所有位置 j) │
│ V_j = X'_j × Wv (所有位置 j) │
│ Score_i = Q_i × [K_0, K_1, ..., K_i]^T │
│ Output_i = softmax(Score_i) × [V_0, V_1, ..., V_i] │
│ │
│ 关键:在 Causal Attention 下, │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 位置 j 的 K_j 和 V_j 只取决于 X'_j │ │
│ │ 而 X'_j 只取决于输入序列 0~j 位置 │ │
│ │ │ │
│ │ 当我们在 Step 2 追加了新 token 后: │ │
│ │ • 新 token 会影响 Q_k(k > j) │ │
│ │ • 但新 token 不会影响旧的 K_j 和 V_j │ │
│ │ • 因为 Causal Attention:后面 token 不影响前面 │ │
│ │ │ │
│ │ 所以:旧的 K 和 V → 永远不变! │ │
│ │ 只需计算一次,然后缓存起来。 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 这和 Encoder-Decoder 的关键区别: │
│ • Encoder-Decoder 中,Decoder 的所有位置之间也有 Causal │
│ Mask,所以同样的推理也适用。 │
│ • 但 Encoder 中的 K,V 也需要缓存(因为每次生成新 token │
│ 时要重新做 Cross-Attention)。KV Cache 不是 Decoder- │
│ Only 独有的,但在 Decoder-Only 中对性能至关重要。 │
│ │
└─────────────────────────────────────────────────────────────┘3.3 KV Cache 的图解——有 Cache 和没 Cache 的天壤之别
┌─────────────────────────────────────────────────────────────┐
│ 没有 KV Cache:每一步都重新计算一切 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 输入 [1, +, 1, =, ?] │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 计算 Q₀K₀ Q₀K₁ Q₀K₂ Q₀K₃ Q₀K₄ ← 5 个 K 都是新算的 │ │
│ │ 计算 Q₁K₀ Q₁K₁ Q₁K₂ Q₁K₃ Q₁K₄ │ │
│ │ 计算 Q₂K₀ Q₂K₁ Q₂K₂ Q₂K₃ Q₂K₄ │ │
│ │ 计算 Q₃K₀ Q₃K₁ Q₃K₂ Q₃K₃ Q₃K₄ │ │
│ │ 计算 Q₄K₀ Q₄K₁ Q₄K₂ Q₄K₃ Q₄K₄ ← 25 次点积 │ │
│ │ → 输出 token "2" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 输入 [1, +, 1, =, ?, 2] │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 计算 Q₀K₀ ... Q₀K₅ ← K₀~K₄ 又算了一遍(浪费!) │ │
│ │ 计算 Q₁K₀ ... Q₁K₅ ← K₀~K₄ 又算了一遍 │ │
│ │ 计算 Q₂K₀ ... Q₂K₅ ← K₀~K₄ 又算了一遍 │ │
│ │ 计算 Q₃K₀ ... Q₃K₅ ← K₀~K₄ 又算了一遍 │ │
│ │ 计算 Q₄K₀ ... Q₄K₅ ← K₀~K₄ 又算了一遍 │ │
│ │ 计算 Q₅K₀ Q₅K₁ Q₅K₂ Q₅K₃ Q₅K₄ Q₅K₅ ← 36 次点积 │ │
│ │ → 输出 <eos> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 总点积次数(仅 Attention):25 + 36 = 61 │
│ 生成 M token 需要 O(M²) 次点积 │
│ │
├─────────────────────────────────────────────────────────────┤
│ 有 KV Cache:只计算新 token 的 Q,K,V │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1 (Prefill): 输入 [1, +, 1, =, ?] │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 计算所有 5 个 token 的 K,V → 存入 Cache │ │
│ │ Cache = {K₀,V₀, K₁,V₁, K₂,V₂, K₃,V₃, K₄,V₄} │ │
│ │ │ │
│ │ 计算 Q₄ × [K₀,K₁,K₂,K₃,K₄]^T(只取最后一行的 Q) │ │
│ │ → 输出 token "2" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2 (Decode): 输入只有新 token "2" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 只计算新 token "2" 的 Q₅, K₅, V₅ │ │
│ │ 新 K₅,V₅ 追加到 Cache │ │
│ │ Cache = {..., K₄,V₄, K₅,V₅} │ │
│ │ │ │
│ │ 计算 Q₅ × [K₀,K₁,K₂,K₃,K₄,K₅]^T(只 1 个 Q!) │ │
│ │ → 使用 Cache 中的 K₀~K₄(不重新计算!) │ │
│ │ → 只有 5+1=6 次点积 │ │
│ │ → 输出 <eos> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 总点积次数:25 (prefill) + 6 (per decode step) │ │
│ │ │ │
│ │ 无 KV Cache:O(M²) per step → 总 O(M³) │ │
│ │ 有 KV Cache:O(1) per step → 总 O(M²) │ │
│ │ │ │
│ │ 生成 2048 token: │ │
│ │ 无 Cache → ~286M 点积 │ │
│ │ 有 Cache → ~4M 点积 │ │
│ │ → 加速 70 倍! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.4 KV Cache 的内存占用——为什么上下文长度是瓶颈
┌─────────────────────────────────────────────────────────────┐
│ KV Cache 内存计算(手算级别) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 对于一层、一个头: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 每个 token 需要存储: │ │
│ │ K: 一个长度为 d_k 的向量 │ │
│ │ V: 一个长度为 d_k 的向量 │ │
│ │ │ │
│ │ 对于序列长度 n: │ │
│ │ 一层一个头的 KV Cache = n × 2 × d_k 个浮点数 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 扩展到所有层、所有头: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ KV Cache 大小 = 2 × L × n × h × d_k × bytes_per_val│ │
│ │ │ │
│ │ 其中: │ │
│ │ L = 层数 │ │
│ │ n = 序列长度 │ │
│ │ h = 每层的注意力头数 │ │
│ │ d_k = 每个头的 K/V 维度 │ │
│ │ │ │
│ │ 注意:h × d_k = d_model(头的总维度 = 模型维度) │ │
│ │ 所以可以简化为: │ │
│ │ KV Cache 大小 = 2 × L × n × d_model × bytes │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 以 LLaMA-7B 为例(FP16 推理): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ L = 32 层 │ │
│ │ d_model = 4096 │ │
│ │ n = 2048 tokens │ │
│ │ bytes_per_val = 2(FP16) │ │
│ │ │ │
│ │ KV Cache = 2 × 32 × 2048 × 4096 × 2 │ │
│ │ = 2 × 32 × 2048 × 4096 × 2 │ │
│ │ = 1,073,741,824 bytes │ │
│ │ ≈ 1.0 GB │ │
│ │ │ │
│ │ n = 4096 → ~2.0 GB │ │
│ │ n = 8192 → ~4.0 GB │ │
│ │ n = 32768 → ~16 GB │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 以 LLaMA-70B 为例(FP16,d_model=8192, L=80): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ n = 2048 → 2 × 80 × 2048 × 8192 × 2 │ │
│ │ ≈ 5.4 GB │ │
│ │ │ │
│ │ n = 4096 → ~10.7 GB │ │
│ │ → 这就是为什么长上下文推理需要大显存 GPU! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 关键认知: │ │
│ │ │ │
│ │ KV Cache 的大小随序列长度 n 线性增长。 │ │
│ │ 这看起来还好——但加上模型参数本身占用的显存后, │ │
│ │ 内存很容易成为瓶颈。 │ │
│ │ │ │
│ │ LLaMA-7B 模型参数(FP16):~14 GB │ │
│ │ + KV Cache (n=2048):~1 GB │ │
│ │ → 总计 ~15 GB,刚好放进一张 24GB 的 RTX 4090 │ │
│ │ │ │
│ │ 但如果 n=8192:KV Cache ~4 GB,总计 ~18 GB │ │
│ │ → 仍然可行,但余量变小 │ │
│ │ │ │
│ │ 这就是 GQA(Grouped Query Attention)和 MQA │ │
│ │ (Multi-Query Attention)存在的意义——减少 KV Cache │ │
│ │ 的大小! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.5 Prefill 和 Decode——KV Cache 的两个阶段
┌─────────────────────────────────────────────────────────────┐
│ Prefill 阶段 vs Decode 阶段 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Prefill(预填充)阶段: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 输入:用户的完整 Prompt(可能很长,如 2000 token) │ │
│ │ │ │
│ │ 操作: │ │
│ │ 1. 一次性前向传播整个 Prompt │ │
│ │ 2. 所有 token 的 K,V 存入 Cache │ │
│ │ 3. 拿到最后一个位置的 hidden state │ │
│ │ 4. 预测第一个输出 token │ │
│ │ │ │
│ │ 特点: │ │
│ │ • 计算量大(处理 N 个 token 并行) │ │
│ │ • 受 Compute Bound 限制(GPU 算力瓶颈) │ │
│ │ • 但只做一次! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Decode(逐 token 生成)阶段: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 输入:每次只有 1 个新 token │ │
│ │ │ │
│ │ 操作: │ │
│ │ 1. 只计算这个 token 的 Q, K, V │ │
│ │ 2. 新 K,V 追加到 Cache │ │
│ │ 3. Attention:新 Q × 所有缓存的 K │ │
│ │ 4. 预测下一个 token │ │
│ │ │ │
│ │ 特点: │ │
│ │ • 计算量小(每次只处理 1 个 token) │ │
│ │ • 受 Memory Bandwidth Bound 限制 │ │
│ │ (大部分时间在从显存读 KV Cache!) │ │
│ │ • 重复 M 次(M = 生成长度) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 速度分析: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Prefill:2000 token → 一次前向,约 50-200ms(GPU) │ │
│ │ Decode:每 token 约 10-50ms │ │
│ │ │ │
│ │ 生成 500 token 的回答: │ │
│ │ Prefill: 100ms │ │
│ │ Decode: 500 × 20ms = 10,000ms │ │
│ │ 总计: ~10 秒 │ │
│ │ │ │
│ │ → 生成阶段(Decode)占总时间的 ~99%! │ │
│ │ → "为什么 ChatGPT 输出这么慢"的答案就在这里 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.6 GQA 和 MQA——减少 KV Cache 的工业级优化
┌─────────────────────────────────────────────────────────────┐
│ MHA vs MQA vs GQA:KV Cache 大小的权衡 │
├─────────────────────────────────────────────────────────────┤
│ │
│ MHA (Multi-Head Attention):标准的多头注意力 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 每个头有独立的 K,V → h 组 K,V 要缓存 │ │
│ │ KV Cache = 2 × L × n × h × d_k │ │
│ │ │ │
│ │ 示例:LLaMA-7B, h=32, d_k=128 │ │
│ │ → 每层每个 token: 32 × 128 × 2 = 8192 个 float │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ MQA (Multi-Query Attention):所有头共享同一组 K,V │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 只有 1 组 K,V 要缓存 │ │
│ │ KV Cache = 2 × L × n × 1 × d_k │ │
│ │ │ │
│ │ 节省:h 倍! │ │
│ │ 代价:表达能力有所下降 │ │
│ │ │ │
│ │ 使用者:PaLM │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ GQA (Grouped Query Attention):折中方案 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 把 h 个头分成 g 组,每组共享 K,V │ │
│ │ KV Cache = 2 × L × n × g × d_k │ │
│ │ │ │
│ │ 示例:LLaMA-2 70B, h=64, g=8 │ │
│ │ → 节省 8 倍 KV Cache! │ │
│ │ │ │
│ │ 使用者:LLaMA-2, LLaMA-3, Mistral, DeepSeek-V2 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 对比图: │ │
│ │ │ │
│ │ MHA: Q₀→K₀V₀ Q₁→K₁V₁ Q₂→K₂V₂ Q₃→K₃V₃ │ │
│ │ 独立的 4 组 K,V 缓存 │ │
│ │ │ │
│ │ MQA: Q₀→┐ │ │
│ │ Q₁→├─K₀V₀ │ │
│ │ Q₂→┤ 所有 Q 共享 1 组 K,V │ │
│ │ Q₃→┘ │ │
│ │ │ │
│ │ GQA: Q₀→┐ │ │
│ │ Q₁→├─K₀V₀ (Group 0: 2 个头共享) │ │
│ │ Q₂→┐ │ │
│ │ Q₃→├─K₁V₁ (Group 1: 2 个头共享) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第4部分:Decoder-Only 为什么赢了?——四条护城河
4.1 护城河一:训练效率——互联网就是训练数据
┌─────────────────────────────────────────────────────────────┐
│ 训练效率:数据利用率的天壤之别 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Encoder-Decoder(以翻译为例): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "我爱你" → "I love you" 这是一个训练样本 │ │
│ │ 需要:源语言句子 + 目标语言句子(人工标注的平行语料)│ │
│ │ 训练信号数:3(目标句子的每个位置是一个预测目标) │ │
│ │ 数据成本:高(需要双语标注员) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Decoder-Only(GPT 风格): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任何网页、任何书、任何代码——只要是人类写的文本 │ │
│ │ → "1+1=2" 就是训练样本 │ │
│ │ → "光合作用是植物利用光能..." 就是训练样本 │ │
│ │ │ │
│ │ 训练信号数:N-1(序列中每个 token 的下一 token) │ │
│ │ 数据成本:几乎零(互联网上取之不尽) │ │
│ │ │ │
│ │ GPT-3 (175B) 训练数据:~500B tokens │ │
│ │ → 约 500B 个训练信号 │ │
│ │ │ │
│ │ 如果用 Encoder-Decoder 得到等量训练信号: │ │
│ │ 需要 ~500B 个配对样本 → 不可能 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 护城河二:Scaling——一个架构,从 1B 到 1T
┌─────────────────────────────────────────────────────────────┐
│ Decoder-Only 的 Scaling 极其平滑 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 同一个架构,只改超参数: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 模型 参数量 L d_model 训练数据 │ │
│ │ GPT-2 Small 124M 12 768 40GB 文本 │ │
│ │ GPT-2 Medium 355M 24 1024 ... │ │
│ │ GPT-2 Large 774M 36 1280 ... │ │
│ │ GPT-2 XL 1.5B 48 1600 ... │ │
│ │ GPT-3 Small 125M 12 768 ... │ │
│ │ GPT-3 Medium 350M 24 1024 ... │ │
│ │ GPT-3 Large 760M 24 1536 ... │ │
│ │ GPT-3 XL 1.3B 24 2048 ... │ │
│ │ GPT-3 6.7B 32 4096 ... │ │
│ │ GPT-3 13B 40 5140 ... │ │
│ │ GPT-3 175B 96 12288 570GB 文本 │ │
│ │ │ │
│ │ 都是 Decoder-Only + Causal Self-Attention + FFN! │ │
│ │ 架构完全不变,只是把矩阵变大、层数变多。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么 Scaling 友好? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 只有一种 Attention 机制 → 超参数少、调参简单 │ │
│ │ 2. Causal Mask 是确定性的 → 不需要调 Mask 策略 │ │
│ │ 3. 没有 Cross-Attention → 不需要平衡两个栈的大小 │ │
│ │ 4. 所有位置的计算是对称的 → 负载均衡,GPU 利用率高 │ │
│ │ │ │
│ │ 相比之下,Encoder-Decoder 需要决定: │ │
│ │ • Encoder 几层?Decoder 几层? │ │
│ │ • Cross-Attention 在哪里?每层都有还是只有几层? │ │
│ │ • Encoder 的 d_model 和 Decoder 一样吗? │ │
│ │ → 超参数空间更大,Scaling 时不确定性更高 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.3 护城河三:In-Context Learning 自然涌现
┌─────────────────────────────────────────────────────────────┐
│ In-Context Learning 是 Decoder-Only 的特有超能力 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 什么是 In-Context Learning(ICL)? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 给模型几个示例(在 prompt 里),不更新任何参数, │ │
│ │ 模型就能学会做对应的任务。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么 Decoder-Only 天然支持 ICL? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Prompt: │ │
│ │ "请把以下词翻译成英文: │ │
│ │ 猫 → cat │ │
│ │ 狗 → dog │ │
│ │ 鸟 → " │ │
│ │ │ │
│ │ 在 Causal Attention 中: │ │
│ │ │ │
│ │ 位置 "鸟" 的 Q 会 attend 到: │ │
│ │ • "猫 → cat" 的 K,V(知道"猫"的翻译模式) │ │
│ │ • "狗 → dog" 的 K,V(知道"狗"的翻译模式) │ │
│ │ │ │
│ │ 通过 Attention,示例中的模式被自动"提取"出来, │ │
│ │ 用于影响 "鸟" 位置的预测。 │ │
│ │ │ │
│ │ 这是 Attention 的自然特性: │ │
│ │ output = Σ (attention_weight_j × V_j) │ │
│ │ │ │
│ │ 如果"猫→cat"的 V 包含了对翻译模式有用的信息, │ │
│ │ "鸟"位置的 Q 会自动给予它高 attention 权重。 │ │
│ │ │ │
│ │ 不需要特殊机制——这就是 Attention 本身在干活。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Encoder-Decoder 为什么在这方面吃亏? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 在 Encoder-Decoder 中: │ │
│ │ │ │
│ │ 示例 "猫→cat" 和 "狗→dog" 必须通过 Cross-Attention │ │
│ │ 影响 Decoder。但 Cross-Attention 的 K,V 来自 Encoder│ │
│ │ 的固定输出,而 Encoder 是双向 Self-Attention—— │ │
│ │ 它处理的是完整的源序列。 │ │
│ │ │ │
│ │ 指令("请翻译…")和示例("猫→cat")在 Encoder 中 │ │
│ │ 被混在一起编码。Decoder 只能通过 Cross-Attention │ │
│ │ 间接获取这些信息——信息传递路径更长、更间接。 │ │
│ │ │ │
│ │ 在 Decoder-Only 中,一切都是一个序列: │ │
│ │ 指令 → 示例1 → 示例2 → 预测位置 │ │
│ │ Causal Attention 让信息沿着序列单向流动, │ │
│ │ 示例的信息直接通过 Self-Attention 流入预测位置。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.4 护城河四:架构简单,工程友好
┌─────────────────────────────────────────────────────────────┐
│ 简单就是美——Decoder-Only 的工程优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 一个典型的 Decoder-Only 层(伪代码): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ def decoder_layer(x, mask): │ │
│ │ # 1. Causal Self-Attention │ │
│ │ attn_out = causal_self_attn(x, mask) │ │
│ │ x = layer_norm(x + attn_out) │ │
│ │ │ │
│ │ # 2. Feed-Forward Network │ │
│ │ ffn_out = ffn(x) │ │
│ │ x = layer_norm(x + ffn_out) │ │
│ │ │ │
│ │ return x │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 对比 Encoder-Decoder(伪代码): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ def encoder_layer(x): │ │
│ │ attn_out = self_attn(x) # 双向 Self-Attn │ │
│ │ x = layer_norm(x + attn_out) │ │
│ │ ffn_out = ffn(x) │ │
│ │ x = layer_norm(x + ffn_out) │ │
│ │ return x │ │
│ │ │ │
│ │ def decoder_layer(x, enc_out, self_mask, cross_mask):│ │
│ │ # 1. Causal Self-Attention │ │
│ │ attn_out = causal_self_attn(x, self_mask) │ │
│ │ x = layer_norm(x + attn_out) │ │
│ │ # 2. Cross-Attention │ │
│ │ cross_out = cross_attn(x, enc_out, cross_mask) │ │
│ │ x = layer_norm(x + cross_out) │ │
│ │ # 3. FFN │ │
│ │ ffn_out = ffn(x) │ │
│ │ x = layer_norm(x + ffn_out) │ │
│ │ return x │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 工程上的实际影响: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 实现更简单:一个 Attention 模块 vs 三种 │ │
│ │ 2. 推理代码更短:不需要维护 Encoder 的输出 │ │
│ │ 3. 分布式训练更均匀:每个 GPU 的计算量对称 │ │
│ │ 4. KV Cache 更直观:只缓存一种 Attention 的 K,V │ │
│ │ 5. 调试更容易:问题出在哪里一目了然 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第5部分:不是所有 LLM 都是纯 Decoder-Only——几个重要例外
虽然 Decoder-Only 是绝对主流,但不是唯一的。
┌─────────────────────────────────────────────────────────────┐
│ LLM 架构的三种主要形态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 类型 1:纯 Decoder-Only(主流中的主流) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GPT 系列(GPT-1/2/3/4) │ │
│ │ LLaMA 系列(LLaMA-1/2/3/4) │ │
│ │ Mistral / Mixtral │ │
│ │ DeepSeek-V2 / V3 │ │
│ │ Claude 系列(Anthropic) │ │
│ │ Qwen 系列 │ │
│ │ Gemma │ │
│ │ │ │
│ │ 特点:只有 Causal Self-Attention + FFN │ │
│ │ 比例:>95% 的现代 LLM │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 类型 2:Encoder-Decoder(仍然在特定领域有优势) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ T5(Google, 2019) │ │
│ │ BART(Facebook, 2019) │ │
│ │ NLLB(Meta,多语言翻译模型,2022) │ │
│ │ │ │
│ │ 为什么还在用? │ │
│ │ • 翻译/摘要等 seq2seq 任务上效果更好 │ │
│ │ • 源序列和目标序列天然分离 → 模型结构匹配任务结构 │ │
│ │ • NLLB-200 支持 200 种语言,翻译质量仍然领先 │ │
│ │ │ │
│ │ 为什么不是主流? │ │
│ │ • 无法利用海量无标注文本训练 │ │
│ │ • In-Context Learning 不如 Decoder-Only │ │
│ │ • 训练开销更大(两个栈) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 类型 3:Prefix-LM(Encoder-Decoder 的变种) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GLM (General Language Model, 清华 & 智谱, 2022) │ │
│ │ │ │
│ │ 核心思想: │ │
│ │ • 前缀部分(prompt):双向 Self-Attention │ │
│ │ • 生成部分(response):Causal Self-Attention │ │
│ │ • 前缀的 K,V 可以被生成部分 attend │ │
│ │ │ │
│ │ 融合了两种注意力模式,但实现更复杂。 │ │
│ │ ChatGLM 系列最初是 Prefix-LM,但 GLM-4 转向了 │ │
│ │ 纯 Decoder-Only——这也说明了趋势。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 趋势判断: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 通用 LLM:Decoder-Only 一统天下 │ │
│ │ • 特定 seq2seq 任务:Encoder-Decoder 仍有价值 │ │
│ │ • 未来的变体:可能融入 MoE(Mixture of Experts) │ │
│ │ 和 MLA(Multi-head Latent Attention), │ │
│ │ 但 Causal Self-Attention 的基本范式不会变。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘5.1 DeepSeek 的特殊优化——MLA 和 KV Cache
┌─────────────────────────────────────────────────────────────┐
│ DeepSeek-V2/V3 的 MLA:对 KV Cache 的极致优化 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统的 KV Cache 问题: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ K,V 的维度 = d_model,等于 Q 的维度 │ │
│ │ 对于大模型(DeepSeek-V3: d_model 很大) │ │
│ │ → KV Cache 内存占用极其庞大 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ MLA (Multi-head Latent Attention) 的解决方案: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 核心思想:把 K 和 V 压缩到一个低维"潜在空间" │ │
│ │ │ │
│ │ 传统:K = X × Wk (d_model → d_model) │ │
│ │ V = X × Wv (d_model → d_model) │ │
│ │ → 缓存 d_model 维的 K,V(大) │ │
│ │ │ │
│ │ MLA: │ │
│ │ C = X × Wc (d_model → d_c, d_c << d_model) │ │
│ │ 缓存 C(很小的潜在向量) │ │
│ │ 使用时:K = C × Wk_up (d_c → d_model) │ │
│ │ V = C × Wv_up (d_c → d_model) │ │
│ │ │ │
│ │ 好处:只缓存 d_c 维的 C,而不是 d_model 维的 K,V! │ │
│ │ │ │
│ │ DeepSeek-V2:d_c 约是 d_model 的 1/4~1/10 │ │
│ │ → KV Cache 减少 75%~90%! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么这很重要: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 更小的 KV Cache → │ │
│ │ • 同样的显存 → 支持更长的上下文 │ │
│ │ • 同样的上下文 → 可以用更少的显存 │ │
│ │ • Decode 阶段更快(少读显存) │ │
│ │ │ │
│ │ DeepSeek-V3 支持 128K 上下文,部分归功于 MLA。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:Decoder-Only 的本质
Decoder-Only 不是"砍掉了 Encoder 的 Transformer",而是重新定义了"上下文"的含义。在 Encoder-Decoder 中,上下文是一个独立的 Encoder 输出;在 Decoder-Only 中,上下文就是序列的前缀。这种设计让"指令"和"回答"在同一个序列中无缝衔接,使得 In-Context Learning 天然可能。
总结2:Causal Mask 是一切的基础
Causal Mask(下三角 Mask)同时解决了两个问题:训练时防止模型"作弊"看到未来 token(Teacher Forcing),推理时自然地限制模型只能看到已生成的 token(Auto-Regressive)。一个 Mask,两种用途。
总结3:KV Cache 是推理可行性的基石
没有 KV Cache,每生成一个 token 都要重新计算所有历史 token 的 K 和 V——O(M^3) 的复杂度让任何长文本生成都不现实。KV Cache 利用了 Causal Attention 的因果性(过去不受未来影响),让每个 decode step 只需要 O(1) 的计算量。KV Cache 的大小随序列长度线性增长,是限制长上下文推理的主要瓶颈。
总结4:Decoder-Only 赢在规模和数据
四条护城河——数据效率(互联网即训练集)、Scaling 平滑(一个架构从 1B 到 1T)、In-Context Learning(Attention 的自然产物)、工程简单(一种 Attention 模块)——共同造就了 Decoder-Only 在通用 LLM 领域的统治地位。
总结5:KV Cache 优化的前沿
从 MHA 到 MQA 到 GQA 到 MLA,KV Cache 的压缩是 LLM 推理优化的核心战场。每次减少 KV Cache 的大小,都直接转化为更长的上下文支持或更快的生成速度。
章节测试
测试1:Decoder-Only 架构中,为什么不需要 Cross-Attention?
A. 因为 Cross-Attention 太慢了,被工程优化掉了 B. 因为没有 Encoder 输出可以提供 K 和 V——整个模型只有一个栈 C. 因为 Cross-Attention 被替换成了更大的 FFN D. 因为 Decoder-Only 使用双向 Attention,Cross-Attention 是多余的
测试2:Causal Mask 在训练和推理中分别起什么作用?
A. 训练中防止过拟合,推理中加速计算 B. 训练中让模型并行学习所有位置的"下一 token 预测",推理中阻止模型看到未生成的 token C. 训练中减少显存使用,推理中提高准确率 D. 训练和推理中的 Causal Mask 是不同的
测试3:为什么说 Decoder-Only 的"一句话提供 N 个训练样本"?
A. 因为模型会把一句话复制 N 次来训练 B. 因为序列中位置 i 的 token 预测位置 i+1 的 token,长度为 N 的序列产生 N 个训练信号,所有信号在一次前向传播中并行计算 C. 因为 Decoder-Only 使用数据增强生成 N 个变体 D. 因为每个 token 需要 N 次前向传播才能训练
测试4:KV Cache 存在的根本原因是什么?
A. GPU 显存太便宜了,不用白不用 B. 在 Causal Attention 中,过去 token 的 K 和 V 不受未来 token 影响,所以可以安全缓存 C. 为了减少模型参数数量 D. 为了让 Attention 计算更准确
测试5:LLaMA-7B(L=32, d_model=4096)推理时序列长度为 4096,KV Cache 大约占用多少显存(FP16)?
A. 0.5 GB B. 2.0 GB C. 8.0 GB D. 32 GB
测试6:GQA(Grouped Query Attention)相比 MHA(Multi-Head Attention)的主要好处是什么?
测试7:Prefill 阶段和 Decode 阶段各受什么硬件瓶颈限制?为什么 Decode 阶段占总时间的绝大部分?
参考答案
测试1答案
答案:B。Decoder-Only 只有一个 Transformer 栈,没有 Encoder。Cross-Attention 需要 Q 来自 Decoder、K 和 V 来自 Encoder——没有 Encoder,就没有 K 和 V 的来源。所以 Decoder-Only 只有 Masked Self-Attention(Causal Attention)。
测试2答案
答案:B。Causal Mask 让位置 i 只能看到位置 0~i。训练时,这意味着所有位置可以并行计算——每个位置独立预测下一个 token,互不干扰(Teacher Forcing)。推理时,模型逐 token 生成,还没有生成的 token 自然不能被看到——Causal Mask 保证了这一点。同一个 Mask 服务于两种场景。
测试3答案
答案:B。在 Decoder-Only 中,输入是序列 [t_0, t_1, ..., t_N],模型通过 Causal Mask 确保位置 i 只能看到 t_0...t_i。位置 i 的输出预测 t_{i+1}。一个长度为 N 的序列有 N 个位置,每个位置的输出都被用于计算 Loss。所有 N 个预测在一次矩阵乘法中并行完成。因此一条文本提供 N 个训练样本,训练效率极高。
测试4答案
答案:B。Causal Attention 的核心特性是:位置 j 的 K_j 和 V_j 只取决于输入 X'_j,而 X'_j 只受位置 0~j 的影响。当新 token 追加到序列末尾时,它不会改变已有 token 的表示(Causal Mask 确保信息单向流动)。因此已有 token 的 K 和 V 是"不变的",可以缓存起来重复使用。这是 Attention 的因果性直接推出的结论。
测试5答案
答案:B(约 2.0 GB)。
计算公式:KV Cache = 2 × L × n × d_model × 2 bytes
= 2 × 32 × 4096 × 4096 × 2 = 2,147,483,648 bytes = 2.0 GB
(如果使用 GQA 且 g < h,实际会更小。)
测试6答案
MHA 中每个注意力头有独立的 K 和 V,需要缓存 h 组 K,V。GQA 把 h 个头分成 g 组(g < h),每组内的头共享同一组 K,V,只需要缓存 g 组 K,V。这直接减少了 KV Cache 的大小(减少 h/g 倍),代价是每个头失去了独立的 K,V 表示能力。实践证明 g 取适当值(如 8)时,性能损失可忽略不计。LLaMA-2/3、Mistral、DeepSeek-V2 等主流模型都使用 GQA。
测试7答案
Prefill 阶段:一次性处理整个 prompt(N 个 token),计算量大(N 个 token 的 Attention 并行计算),受 Compute Bound 限制(GPU 算力是瓶颈)。
Decode 阶段:每次只处理 1 个新 token,计算量很小,但需要从显存中读取整个 KV Cache(大小随序列长度线性增长)。受 Memory Bandwidth Bound 限制(显存带宽是瓶颈)。
Decode 占总时间绝大部分的原因:虽然每个 decode step 计算量小,但需要重复 M 次(M = 生成长度),而且每次都要读取不断增长的 KV Cache。对于生成 500 token 的场景,95%+ 的时间花在 Decode 阶段。
相关笔记
- [[08-transformer-by-hand]] — Encoder-Decoder 完整数据流(手算版,本文的前置知识)
- [[07-llm-evolution]] — 从 Word2Vec 到 Transformer 的历史演进
- [[10-training-vs-inference]] — 训练和推理完整对比(FLOPs 计算 / 显存估算)
- [[11-llm-architectures]] — 主流 LLM 架构全景:MoE, GQA, MLA, FlashAttention
下一步学习
- [ ] 理解 KV Cache 的代码实现——用 PyTorch 写一个带 KV Cache 的 Decoder-Only 推理循环(约 60 行),对比有无 Cache 的速度差异
- [ ] 阅读 [[10-training-vs-inference]] — 深入理解训练和推理的 FLOPs / 显存 / 延迟完整分析
- [ ] 阅读 LLaMA 论文的架构部分——对照本文的图,找到 GQA 和 RoPE 的实际应用
- [ ] 思考:如果让你设计一个支持 1M 上下文窗口的 Decoder-Only 模型,KV Cache 会是最大挑战吗?还有哪些技术可以突破这个限制?
学习状态:🟡 开始学习