#模块一:基础与 Transformer 架构

#1. Transformer 的核心组成模块有哪些?

#标准答案

典型 Transformer 可以拆成 7 个核心部件:输入 embedding、位置编码、多头注意力、前馈网络(FFN)、残差连接、归一化层,以及最后把隐藏状态映射回词表的输出头。面试时不要只背名字,更要顺着数据流讲:token 先被映射成向量,再叠加位置信息进入多层 Transformer block,每个 block 里先做注意力聚合上下文,再做 FFN 做非线性变换,同时用残差和归一化保证深层训练稳定,最后输出到 logits 做预测。

如果继续追问架构类型,可以补一句:Encoder-only 更强调双向理解,Decoder-only 强调自回归生成,Encoder-Decoder 则把“理解输入”和“生成输出”分成两段。这样回答会显得你不是在背组件清单,而是真正理解整个架构是怎么工作的。


#深度解析

1. 用一句话理解每个组件

组件 一句话职责 如果去掉会怎样
Embedding 把离散 token 变成连续向量 模型无法处理文本
位置编码 告诉模型"这个词在第几个位置" 模型不知道顺序,"猫吃鱼"="鱼吃猫"
Multi-Head Attention 让每个词动态决定"该看哪些词" 每个词只能孤立理解,没有上下文
FFN 对每个词做非线性变换,增加表达能力 模型变成纯线性系统,无法拟合复杂函数
残差连接 让梯度有一条"高速公路"直通输入层 深层网络梯度消失,训练失败
LayerNorm 控制每层输出的数值范围,防止爆炸 深层数值无界增长,训练不稳定
输出头 把隐藏状态映射回词表,做预测 模型无法输出下一个 token

2. 数据流的完整过程:以 "猫 吃 鱼" 为例

Step 1:Tokenization

原始文本 "猫吃鱼" → tokenizer 切成 token → 假设得到 ["猫", "吃", "鱼"]

Step 2:Embedding

每个 token 查表得到一个向量:

"猫" → [0.2, 0.5, -0.3, 0.8]     # 形状 (4,)
"吃" → [0.1, -0.4, 0.6, 0.3]
"鱼" → [-0.3, 0.2, 0.1, 0.5]

Step 3:叠加位置编码

如果不加位置信息,模型无法区分 "猫" 是第一个词还是第三个词。位置编码给每个位置一个独特"指纹":

位置 0 的编码 → [0.0, 0.1, 0.2, 0.3]
位置 1 的编码 → [0.1, 0.3, -0.1, 0.2]
位置 2 的编码 → [0.2, 0.1, 0.0, -0.1]

"猫" 的最终输入 = [0.2, 0.5, -0.3, 0.8] + [0.0, 0.1, 0.2, 0.3]
                  = [0.2, 0.6, -0.1, 1.1]

Step 4:进入 Transformer Block(重复 N 层)

每一层内部:

输入 x ──→ LayerNorm ──→ Multi-Head Attention ──→ + ──→ LayerNorm ──→ FFN ──→ + ──→ 输出
           │                                           ↑              │              ↑
           └───────────────────────────────────────────┘              └──────────────┘
                                    残差连接                                    残差连接

具体发生了什么?

  • LayerNorm(第一次):把输入数值归一化到合理范围
  • Attention:"吃" 看了 "猫" 和 "鱼",发现 "猫" 跟自己关系更大(因为 "猫吃" 是主谓关系),所以 "吃" 的输出中混入了更多 "猫" 的信息
  • 加残差:attention 的输出 + 原始输入 = "既有原始信息,又有上下文信息"
  • LayerNorm(第二次):再次归一化
  • FFN:对 "吃" 的向量做非线性变换——相当于"基于 attention 聚合后的信息,做更深层次的语义加工"
  • 加残差:FFN 的输出 + attention 后的输入

Step 5:输出预测

经过 N 层后,最后一个 token "鱼" 的隐藏状态是 [0.5, -0.2, 0.3, 0.1]。输出头把它映射回词表空间:

logits = [0.5, -1.2, 0.8, 0.3, -0.5, ...]   # 词表里每个词的分数
softmax → [0.15, 0.03, 0.35, 0.22, 0.08, ...]

分数最高的词是下一个 token 的预测。如果正在生成句子,模型可能预测 "。" 或 "很香" 之类的续写。


3. 为什么残差连接 + LayerNorm 是绝配?

没有残差连接的深层网络

x_1 = f(x_0)
x_2 = f(x_1)
x_3 = f(x_2)
...
x_L = f(x_{L-1})

反向传播时,梯度要从 x_L 传回 x_0,需要穿过 L 层的 Jacobian 连乘。如果每层 Jacobian 的特征值都略小于 1,连乘后梯度会指数衰减(梯度消失);如果略大于 1,会指数爆炸。

加上残差连接

x_{l+1} = x_l + f(x_l)

反向传播时:

∂x_{l+1} / ∂x_l = I + ∂f/∂x_l

即使 ∂f/∂x_l 很小,还有单位矩阵 I 保底,梯度不会完全消失。这就是"高速公路"。

LayerNorm 的作用:残差连接让各层输出累加:x_L ≈ x_0 + f(x_0) + f(x_1) + ...。如果没有归一化,这些累加会让数值越来越大,最终导致溢出。LayerNorm 把每层输出压缩回标准范围,让累加不会失控。


4. 面试官常见深挖追问

  • "Transformer 和 RNN 最本质的区别是什么?"
    • 答:RNN 是串行的——处理第 i 个词时必须先处理完前 i-1 个词;Transformer 是并行的——所有词同时进入 attention,直接建立联系。RNN 的长程依赖通过隐藏状态层层传递,容易遗忘;Transformer 通过 attention 直接连接,任意距离都是 O(1) 的"注意力跳数"。
  • "为什么 Transformer 需要 FFN?Attention 不是已经有足够表达能力了吗?"
    • 答:Attention 做的是"信息聚合"——把不同位置的信息加权混合,但它本身是一个线性操作(加权求和)。FFN 提供非线性变换,让模型能学到更复杂的映射关系。如果把 FFN 去掉,整个 Transformer block 几乎是线性的,表达能力会大幅下降。
  • "LayerNorm 和 BatchNorm 有什么区别?为什么 Transformer 用 LayerNorm?"
    • 答:BatchNorm 对一个 batch 内所有样本的同一维度做归一化;LayerNorm 对单个样本的所有维度做归一化。Transformer 用 LayerNorm 是因为:1)序列长度不固定,BatchNorm 的统计量不稳定;2)训练时 batch size 可能很小,BatchNorm 估计不准;3)LayerNorm 对每个样本独立归一化,更适合变长序列。

#2. Self-Attention 的时间复杂度和空间复杂度分别是什么?

#标准答案

对长度为 n 的序列,标准 self-attention 最贵的部分是计算 QK^T,因为它要让每个 token 和其余 token 两两做一次相关性打分,所以时间复杂度通常看作 O(n^2);对应的注意力矩阵本身也是 n x n,因此中间激活的空间复杂度也常看作 O(n^2)

面试里最好再补一句:这里真正随序列长度爆炸的不是模型参数,而是 token 两两交互带来的计算量、显存占用和带宽压力。所以一旦上下文从 4K 提到 32K128K,瓶颈往往不是“模型更大了”,而是 attention 的二次复杂度开始压垮系统。


#深度解析

1. 为什么时间复杂度是 O(n²)?从矩阵乘法看

Self-Attention 的核心计算是 Q @ K^T

Q 的形状: (n, d_k)
K 的形状: (n, d_k)
Q @ K^T 的形状: (n, n)

矩阵乘法中,结果矩阵的每个元素 (i, j) 是 Q 的第 i 行和 K 的第 j 行做内积:

(Q @ K^T)[i, j] = Σ(k=0 to d_k-1) Q[i, k] * K[j, k]

结果矩阵有 n * n 个元素,每个元素需要 d_k 次乘加运算,所以总计算量是 O(n² * d_k)

因为 d_k 是固定常数(比如 64 或 128),通常简化为 O(n²)

为什么不是 O(n³)? 因为 attention 不是三维张量的乘法,只是两个二维矩阵相乘。如果模型有 h 个 head,每个 head 独立计算,总计算量是 h * O(n² * d_k) = O(n² * h * d_k) = O(n² * d_model),仍然是 O(n²)


2. 空间复杂度的具体构成

空间复杂度不只是 QK^T 矩阵,还包括:

张量 形状 大小(n=4096, d_k=128, FP16)
Q (n, d_k) 4096 * 128 * 2 = 1 MB
K (n, d_k) 1 MB
V (n, d_v) 1 MB
QK^T (n, n) 4096² * 2 = 32 MB
Softmax 输出 (n, n) 32 MB
Attention @ V (n, d_v) 1 MB
总计(峰值) ~68 MB

注意:这只是一个 head、一个 layer、batch=1 的情况。实际中:

  • 32 个 head → 并行计算但内存峰值取决于实现
  • 32 层 → KV cache 会累积
  • batch=8 → 所有内存乘以 8

3. 具体数值感受:不同序列长度下的爆炸

序列长度 Attention FLOPs QK^T 显存 相对 n=1K
1K ~1.3e8 2 MB 1x
4K ~2.1e9 32 MB 16x
8K ~8.4e9 128 MB 64x
32K ~1.3e11 2 GB 1024x
128K ~2.1e12 32 GB 16384x

注意:显存增长是 ,但 FLOPs 增长也是 。从 1K 到 128K(128 倍),FLOPs 增长 16384 倍。这就是为什么长上下文不是简单"把窗口改大"的问题。


4. 为什么 O(n²) 在长序列下成为瓶颈?

计算瓶颈

  • 128K 序列的 attention FLOPs 约 2T,在 A100 上需要数秒
  • 相比之下,FFN 层的计算量是 O(n * d_model²),对于 128K * 4096² ≈ 2T,和 attention 相当
  • 但 attention 的内存访问模式更差(随机访问大矩阵),实际 wall-clock time 更长

显存瓶颈

  • 128K 的 QK^T 矩阵占 32 GB(FP16),单卡 A100(80GB)快满了
  • 加上 KV cache(32 layer * 128K * 128 * 2 * 2 = 2GB per head...),总显存压力巨大

带宽瓶颈

  • FlashAttention 把 HBM 访问次数从 O(n²) 降到 O(n),但长序列下 HBM 和 SRAM 之间的数据搬运仍然很重

5. 各种替代方案的复杂度对比

方法 时间复杂度 空间复杂度 核心思想
标准 Attention O(n²) O(n²) 全连接
稀疏 Attention O(n * k) O(n * k) 只看最近的 k 个位置
Linear Attention O(n) O(n) 用核技巧把 softmax 提到求和外面
FlashAttention O(n²) O(n) 分块计算,减少 HBM 访问
Sliding Window O(n * w) O(n * w) 只看窗口内的 w 个位置

关键洞察:FlashAttention 没有改变渐近复杂度,它优化的是常数因子(内存访问模式)。真正降低复杂度的是稀疏/线性 attention,但它们会改变 attention 结构,需要重新训练。


6. 面试官常见深挖追问

  • "Attention 的 O(n²) 和 RNN 的 O(n) 相比,为什么 Transformer 反而更快?"
    • 答:RNN 虽然理论复杂度是 O(n),但它是串行的——处理第 i 个词必须等前 i-1 个词处理完。Transformer 的 attention 计算虽然总 FLOPs 更大,但所有位置可以并行计算。在 GPU 上,并行度比总 FLOPs 更重要。短序列时 Transformer 更快;超长序列时 O(n²) 开始压垮并行优势。
  • "如果 n=100K,d_k=128,算一次 attention 需要多少 FLOPs?"
    • 答:QK^T 需要 n * n * d_k = 100K * 100K * 128 = 1.28e12 FLOPs。Softmax 和加权求和各需要 O(n²) 的额外计算。总计约 2-3e12 FLOPs。A100 的峰值是 312 TFLOPS(FP16),利用率按 30% 算,约 100 TFLOPS,所以需要约 30 秒——这解释了为什么长序列 prefill 这么慢。
  • "为什么空间复杂度说 O(n²),但实际瓶颈常常是 KV cache 而不是 attention 矩阵?"
    • 答:训练时 attention 矩阵确实是 O(n²) 的显存占用,是主要瓶颈。但推理时(尤其是 decode 阶段),KV cache 是 O(n * layer * d_model) 的累积量,而 attention 矩阵只是 O(n²) 的临时量。当 n 很大(如 128K)且 layer 很多(如 32)时,KV cache 的累积量可能超过 attention 矩阵的临时量。

#3. 为什么 Self-Attention 里要除以 sqrt(d_k)

#标准答案

因为 QK 的每一维都会参与内积,当维度 d_k 变大时,点积结果的方差也会随之变大。如果不做缩放,送进 softmax 的值很容易绝对值过大,最后出现两个问题:一是分布过于尖锐,模型几乎只盯住某几个位置;二是 softmax 进入饱和区后梯度变差,训练更不稳定。

所以除以 sqrt(d_k) 的本质,不是一个”经验小技巧”,而是在做数值尺度校正,让不同 head 维度下的 attention score 保持在更稳定的范围里。你如果能把”方差变大 -> softmax 饱和 -> 梯度变差”这条因果链讲出来,面试官通常会认为你是真的懂。


#深度解析

1. 数学推导:为什么点积的方差会随维度增长?

假设 QK 的每个元素都是独立同分布的随机变量,均值为 0,方差为 σ²。那么 Q·K(内积)可以写成:

Q·K = Σ(i=1 to d_k) q_i * k_i

每一项 q_i * k_i 的期望是 0(因为 E[q_i]=E[k_i]=0),方差是 σ⁴(因为两个独立零均值变量的乘积方差等于各自方差的乘积)。

d_k 个这样的独立项相加,总和的方差就是 d_k * σ⁴。也就是说:点积的方差与维度 d_k 成正比

如果 d_k=64,方差是某个基准值;当 d_k=128 时,方差翻倍。不缩放的话,更大的 head 维度会让 attention score 的绝对值更容易变得很大。

除以 sqrt(d_k) 后,新的方差变成:

Var(Q·K / sqrt(d_k)) = Var(Q·K) / d_k = (d_k * σ⁴) / d_k = σ⁴

方差被拉回与维度无关的常数,这就是缩放的数学本质。

2. 具体数值感受

假设 q_ik_i 都是标准正态分布(μ=0, σ=1),那么 q_i * k_i 的方差约为 1。d_k 个这样的项相加:

d_k 点积标准差(不缩放) 缩放后标准差
64 8.0 1.0
128 11.3 1.0
256 16.0 1.0
512 22.6 1.0

d_k=512 时不做缩放,标准差高达 22.6,softmax 输入会大量落在 ±20 甚至更高的区域,导致梯度几乎为 0。

3. 为什么偏偏是 sqrt(d_k),不是 d_k 或别的常数?

因为方差与 d_k 成正比,标准差与 sqrt(d_k) 成正比。要抵消的是标准差的膨胀,所以除以 sqrt(d_k)。除以 d_k 会过度压制;除以常数(如 8)则无法自适应不同 head 维度。

4. 面试官常见深挖追问

  • ”如果 Q 和 K 做了 LayerNorm,元素本身已经被归一化,还需要除以 sqrt(d_k) 吗?”
    • 答:LayerNorm 把每个 token 的向量归一化为单位长度附近,但它控制的是向量级的范数,不是点积级的方差。即使单个向量被归一化,两个向量的点积仍会因维度累积而方差增大,所以缩放仍然必要。
  • ”除了除以 sqrt(d_k),还有什么别的缩放策略?”
    • 答:Performer 等线性注意力方法里会用不同的核函数替代 softmax;某些变体如 Attention with Linear Biases (ALiBi) 不在 score 上做除法,而是在 score 上叠加位置偏置。但标准 Transformer 中,sqrt(d_k) 是最简洁且理论上站得住脚的方案。
  • ”如果 attention score 很小(比如所有 score 都在 -1 到 1 之间),不除 sqrt(d_k) 会有问题吗?”
    • 答:理论上如果输入分布本身就很”温和”,不缩放也未必立刻崩溃。但训练过程中分布会漂移,尤其是深层网络中,Q 和 K 的范数可能随训练逐渐变大。缩放是一种”预防性”的数值稳定措施,让网络对分布变化更鲁棒。

#4. Encoder-only、Decoder-only、Encoder-Decoder 分别适合什么任务?

#标准答案

  • Encoder-only 适合理解类任务,例如分类、匹配、抽取,因为它能同时看左右文,更适合做表示学习;
  • Decoder-only 适合生成类任务,例如对话、写作、代码补全,因为它按从左到右的方式预测下一个 token,和生成过程天然一致;
  • Encoder-Decoder 适合条件生成任务,例如翻译、摘要、结构化输入到文本输出,因为它把“先理解输入,再基于输入生成输出”这件事拆得更清楚。

面试时最好再补一句 trade-off:Encoder-only 强在表示,Decoder-only 强在统一生成范式,Encoder-Decoder 强在输入输出结构清晰,但训练和部署链路相对更复杂。这样回答比只列任务名更完整。


#深度解析

1. 三种架构的本质差异:注意力掩码

架构 Attention 掩码 信息流动 典型代表
Encoder-only 双向(无掩码) 每个 token 能看到所有其他 token BERT, RoBERTa
Decoder-only 因果掩码(causal mask) 每个 token 只能看到前面的 token GPT, LLaMA
Encoder-Decoder Encoder 双向 + Decoder 因果 先编码输入,再解码输出 T5, BART

注意力的方向决定了能力边界:

  • 双向注意力 → 适合做"理解":分类、匹配、抽取(因为能看到完整上下文)
  • 因果注意力 → 适合做"生成":逐 token 预测下一个(和生成过程天然一致)

2. 为什么 Decoder-only 统一了 LLM 范式?

三个关键原因:

原因 解释
训练统一 所有数据都可以格式化为 "prefix → continuation",不需要为不同任务设计不同目标函数
推理统一 训练和推理都是同一个自回归过程,没有 encoder-decoder 的"两个阶段"割裂
规模扩展友好 单一结构更容易堆叠层数、扩大参数量,工程实现更简单

3. Encoder-Decoder 的历史优势为什么被稀释了?

T5 时代的 Encoder-Decoder 优势:

  • 输入输出分离,适合翻译(不同语言长度差异大)
  • Encoder 双向理解 + Decoder 自回归生成,分工明确

被稀释的原因:

  • Decoder-only 也能做翻译:通过 prompt engineering("Translate English to French: ...")
  • 统一范式降低工程复杂度:维护一套训练/推理 infra 比维护两套更划算
  • 涌现能力:当规模足够大时,Decoder-only 的理解能力也足够强

4. 三种架构的参数效率对比

假设相同的总参数量(如 7B):

架构 有效参数量利用率 原因
Encoder-only 所有参数都用于表示学习
Decoder-only 中-高 生成时必须自回归,不能并行解码
Encoder-Decoder 参数被拆成 encoder + decoder 两部分

5. 面试官常见深挖追问

  • "BERT 是 Encoder-only,GPT 是 Decoder-only,T5 是 Encoder-Decoder,如果让你做一个文本分类任务,你会选哪个?"
    • 答:文本分类首选 Encoder-only(BERT 类)。因为分类只需要理解输入,不需要生成输出。BERT 的双向注意力能充分利用上下文信息。但如果只有 Decoder-only 模型可用,也可以做分类——把分类标签放在 prompt 里,让模型生成标签 token(如 "Sentiment: positive")。
  • "Decoder-only 做翻译,和 Encoder-Decoder 做翻译,本质区别在哪?"
    • 答:Encoder-Decoder 的 encoder 可以双向看源语言句子,提取完整语义后再解码;Decoder-only 只能从左到右看,翻译长句子时可能"忘记"源语言开头的内容(信息瓶颈)。但 Decoder-only 可以通过更大的模型规模和更长的上下文来缓解这个问题。
  • "为什么 BERT 的 MLM 目标不适合直接做生成?"
    • 答:MLM 训练时模型看到被 mask 的完整句子(双向上下文),但生成时只能看到已生成的部分(单向)。训练和推理的上下文不一致,导致 BERT 生成时表现差。而 CLM 的训练和推理都是"只看前文预测下一个",完全一致。

#5. Multi-Head Attention 相比单头注意力的优势是什么?

#标准答案

多头注意力的核心价值是“并行看不同关系”。单头注意力相当于只用一种打分方式去看上下文,而多头注意力会先把隐藏维度切到多个子空间,每个 head 在自己的子空间里独立计算注意力,所以有的 head 可能更关注局部语法,有的更关注长程指代,有的更关注位置模式或特殊分隔符。

从工程视角看,多头不是简单重复计算,而是在总维度固定的情况下提升表达分解能力,让不同 head 形成一定分工。面试里如果继续追问,也可以补一句:头数并不是越多越好,太多头可能出现冗余 head、每头维度太小、收益递减等问题。


#深度解析

1. 头的分工:研究发现了什么?

多个研究(如 Voita et al., 2019; Clark et al., 2019)通过分析 attention 权重,发现不同 head 确实会形成不同的功能 specialization:

Head 类型 行为特征 作用
位置头 关注相邻 token(如 i 关注 i-1, i+1) 捕捉局部语法、词性标注
指代头 跨句关注代词和对应名词 解决 coreference
分隔符头 关注 [SEP], [CLS] 等特殊 token 聚合句子级表示
复制头 从输入复制罕见词到输出 处理命名实体、代码中的变量名
长程头 关注远距离 token 捕捉段落级依赖

2. 为什么头数不是越多越好?

假设 d_model=512

  • num_heads=8 → 每头 d_k=64,信息丰富
  • num_heads=64 → 每头 d_k=8,信息极度压缩

当每头维度太小(如 <16),单个 head 的表达能力严重受限。而且研究表明:很多 head 在训练后可以被剪枝(pruning)而几乎不影响效果——说明冗余 head 确实存在。

常见配置:

  • BERT-base: 12 heads, d_k=64
  • GPT-3: 96 heads, d_k=128 (d_model=12288)
  • LLaMA-2: 32 heads, d_k=128 (d_model=4096)

3. 多头并行的工程实现

实际不是真的循环 8 次计算 8 个 head,而是:

Q = X @ W_q  # 输出 (seq_len, d_model)
# reshape 成 (seq_len, num_heads, head_dim)
# transpose 成 (num_heads, seq_len, head_dim)

这样可以用一个大的矩阵乘法同时算出所有 head,GPU 利用率高。

4. 面试官常见深挖追问

  • "如果某个 head 的 attention 权重总是接近 uniform(均匀分布),说明什么?"
    • 答:说明这个 head 没有学到有用的模式,可能是冗余 head。训练时它的梯度很小,对最终输出贡献不大。这种现象在深层网络中更常见——浅层 head 通常更有分工,深层 head 趋于均匀。
  • "Multi-Head 和 Multi-Query Attention 在 head 维度上的根本区别是什么?"
    • 答:MHA 是每个 query head 有自己独立的 K/V 投影(n_heads 组 K/V);MQA 是所有 query head 共享同一组 K/V(只有 1 组)。从 head 角度看,MHA 是"多头查询、多头索引",MQA 是"多头查询、单头索引"。
  • "为什么 attention 里 Q 和 K 的维度要相同?"
    • 答:因为 Q 和 K 要做点积 Q @ K^T,维度不同无法直接相乘。但 V 的维度可以和 Q/K 不同(虽然通常设为相同),因为 V 只是被权重加权,不需要和 Q 做点积。

#6. 为什么现在大语言模型大多采用 Decoder-only 架构?

#标准答案

因为大语言模型主流目标是 next-token prediction,而 Decoder-only 恰好就是最自然的自回归架构:训练时按前文预测下一个 token,推理时也按同样方式逐 token 生成,训练目标和部署方式高度统一。这样做的好处是数据构造简单,几乎所有纯文本都能直接拿来训练,而且对话、写作、代码补全、Agent 规划这类任务都能统一成“继续生成”。

它能压过 Encoder-Decoder,并不是因为后者完全不行,而是因为 Decoder-only 在大规模预训练、指令微调、推理缓存、生态工具链上形成了最顺的一条路线。代价是它在强条件生成、结构化输入输出分工上不一定总最优,而且自回归解码天然有串行时延。


#深度解析

1. 为什么 Encoder-Decoder 在翻译/摘要里曾经很强?

Encoder-Decoder 把任务天然拆成两步:

  • Encoder:把源语言/长文档编码成固定长度的上下文表示
  • Decoder:基于这个表示生成目标语言/摘要

这种显式分工在"理解+生成"类任务上很直观。但 Decoder-only 证明了一件事:只要模型足够大、数据足够多,它可以在同一个自回归框架里隐式完成"理解"和"生成",不需要显式分工。

2. Decoder-only 的工程优势清单

优势 具体说明
统一训练目标 所有数据都是 next-token prediction,不需要为不同任务设计不同目标
统一推理方式 训练和推理完全一致,不需要区分 encode 阶段和 decode 阶段
KV Cache 自然 自回归生成天然适合缓存历史 K/V,推理优化成熟
数据构造简单 几乎所有文本都能直接训练,不需要配对数据(如平行语料)
任务统一 对话、代码、问答、Agent 都能统一成"续写"

3. Decoder-only 的代价

  • 条件生成不自然:翻译时"先读完全部源语言再生成"不如 Encoder-Decoder 直观
  • 自回归延迟:必须逐 token 生成,无法并行
  • 双向信息缺失:每个位置只能看左边,不像 Encoder 能同时看左右文
  • 长输入处理:prompt 部分虽然可以并行 prefill,但信息压缩不如显式 encoder

4. 面试官常见深挖追问

  • "为什么 Encoder-only(如 BERT)不适合做生成任务?"
    • 答:BERT 的预训练目标是 MLM(掩码预测),训练时能看到被掩码位置两边的上下文;但生成时必须自左向右逐个生成,BERT 没有自回归训练,不会"一个 token 一个 token 地续写"。
  • "如果要做机器翻译,Decoder-only 和 Encoder-Decoder 哪个更好?"
    • 答:数据量小时 Encoder-Decoder 可能更稳(显式分工降低了学习难度);数据量极大时 Decoder-only 可以通过 scale 弥补结构劣势,而且统一框架更容易复用已有生态。
  • "Decoder-only 模型能做理解类任务(如分类)吗?"
    • 答:可以。做法是把分类任务改写成生成任务:"这句话的情感是:[正面/负面/中性]"。模型只需要生成"正面"或"负面"即可。这种做法在 GPT 时代非常常见,虽然不如 Encoder-only 的 [CLS] token 表示直接,但足够有效。

#7. 你一步一步讲一下 Self-Attention 是怎么计算的,从输入到输出都说清楚。

#标准答案

可以按 6 步讲清楚。第一步,把输入隐藏状态通过三组线性层投影成 QKV,它们分别代表“我当前想找什么”“我能提供什么索引”“我真正携带的内容”。第二步,用 Q 和所有 K 做点积,得到当前 token 对其他位置的相关性分数。第三步,把分数除以 sqrt(d_k) 做尺度稳定化。第四步,根据任务类型加上 mask,比如 padding mask 防止看见补齐位,causal mask 防止看到未来。第五步,对分数做 softmax,得到一组归一化权重。第六步,用这些权重对 V 加权求和,得到新的上下文表示。

如果是多头注意力,就在最开始把通道拆成多个 head 并行做上述过程,最后再把各 head 输出拼接起来,过一层输出投影。这样回答的关键不是把公式背出来,而是让面试官听见你知道每一步张量在做什么、为什么要这么做。


#深度解析

1. Self-Attention 要解决什么问题?

在 RNN/LSTM 时代,模型处理第 i 个 token 时,只能依次看过前 i-1 个 token 的隐藏状态。这导致两个问题:

  • 串行瓶颈:不能并行计算
  • 长程遗忘:距离太远的 token 信息在层层传递中被稀释

Self-Attention 的核心思想是:让每个 token 直接和所有 token 建立联系,不需要经过中间层传递。就像开会时每个人都可以直接看到所有人,而不是只能通过邻座传话。

但"直接看到所有人"需要解决一个问题:不是所有人都一样重要。当前 token 应该更关注相关的 token,更少关注无关的 token。Self-Attention 通过 Q/K/V 机制来实现这种"选择性关注"。


2. 用一个具体小例子走完整流程

假设输入有 2 个 token:["猫", "吃鱼"],模型维度 d_model = 4。为了简化,用单头 head_dim = 2

Step 1:输入表示

经过 Embedding 层后,输入 X 的形状是 (2, 4)

X = [[ 0.2,  0.5, -0.3,  0.8],     # "猫"
     [ 0.1, -0.4,  0.6,  0.3]]     # "吃鱼"

Step 2:投影得到 Q、K、V

用三个投影矩阵(为简化,这里用假想的数值):

W_q = [[ 0.1,  0.3],
       [-0.2,  0.5],
       [ 0.4, -0.1],
       [ 0.2,  0.6]]        # 形状 (4, 2)

Q = X @ W_q
  = [[0.2*0.1 + 0.5*(-0.2) + (-0.3)*0.4 + 0.8*0.2,  0.2*0.3 + 0.5*0.5 + (-0.3)*(-0.1) + 0.8*0.6],
     [0.1*0.1 + (-0.4)*(-0.2) + 0.6*0.4 + 0.3*0.2,   0.1*0.3 + (-0.4)*0.5 + 0.6*(-0.1) + 0.3*0.6]]
  = [[0.02 - 0.10 - 0.12 + 0.16,  0.06 + 0.25 + 0.03 + 0.48],
     [0.01 + 0.08 + 0.24 + 0.06,   0.03 - 0.20 - 0.06 + 0.18]]
  = [[-0.04,  0.82],
     [ 0.39, -0.05]]                # 形状 (2, 2)

同理算出 K 和 V(假设不同的投影矩阵):

K = [[ 0.5, -0.3],                 # "猫" 的 key
     [-0.2,  0.7]]                 # "吃鱼" 的 key

V = [[ 0.3,  0.6],                 # "猫" 的 value
     [ 0.8, -0.4]]                 # "吃鱼" 的 value

Step 3:计算 attention scores

Scores = Q @ K^T

Q @ K^T = [[-0.04,  0.82],   @   [[ 0.5, -0.2]
           [ 0.39, -0.05]]        [-0.3,  0.7]]

= [[(-0.04)*0.5 + 0.82*(-0.3),  (-0.04)*(-0.2) + 0.82*0.7],
   [0.39*0.5 + (-0.05)*(-0.3),   0.39*(-0.2) + (-0.05)*0.7]]

= [[-0.02 - 0.246,  0.008 + 0.574],
   [0.195 + 0.015,  -0.078 - 0.035]]

= [[-0.266,  0.582],
   [ 0.210, -0.113]]

Step 4:缩放

d_k = 2,所以除以 sqrt(2) ≈ 1.414

Scaled = [[-0.266/1.414,  0.582/1.414],
          [ 0.210/1.414, -0.113/1.414]]
       = [[-0.188,  0.412],
          [ 0.149, -0.080]]

Step 5:加 causal mask(因果掩码)

Decoder-only 模型中,位置 0("猫")只能看自己,位置 1("吃鱼")能看位置 0 和自己:

mask = [[0,   -inf],       # 位置 0:能看位置 0,不能看位置 1
        [0,     0 ]]       # 位置 1:能看位置 0 和 1

Masked = [[-0.188,  -inf],
          [ 0.149, -0.080]]

Step 6:Softmax

对每一行做 softmax:

第 0 行:softmax([-0.188, -inf]) = [1.0, 0.0]
  (因为 -inf 的指数是 0,-0.188 的指数是 e^(-0.188) ≈ 0.829,归一化后 0.829/(0.829+0) = 1.0)

第 1 行:softmax([0.149, -0.080])
  e^0.149 ≈ 1.161
  e^(-0.080) ≈ 0.923
  和 = 2.084
  softmax = [1.161/2.084, 0.923/2.084] ≈ [0.557, 0.443]

Weights = [[1.000, 0.000],
           [0.557, 0.443]]

解读

  • "猫" 只关注自己(权重 [1.0, 0.0]),因为它不能看后面
  • "吃鱼" 同时关注 "猫"(57%)和 自己(44%),因为它能看到两者

Step 7:加权求和

Output = Weights @ V

= [[1.000, 0.000],   @   [[0.3,  0.6],
   [0.557, 0.443]]        [0.8, -0.4]]

= [[1.000*0.3 + 0.000*0.8,  1.000*0.6 + 0.000*(-0.4)],
   [0.557*0.3 + 0.443*0.8,   0.557*0.6 + 0.443*(-0.4)]]

= [[0.300,  0.600],
   [0.167 + 0.354,  0.334 - 0.177]]

= [[0.300,  0.600],
   [0.521,  0.157]]

输出含义

  • "猫" 的输出 ≈ 它自己的 V(因为只能看自己)
  • "吃鱼" 的输出 ≈ 0.557 * "猫" 的 V + 0.443 * "吃鱼" 的 V
    • 也就是说,"吃鱼" 的表示里混入了 "猫" 的信息
    • 这正是 attention 的核心:每个位置的输出都融合了它"关注"的那些位置的信息

3. Q、K、V 的分工原理

为什么不是直接用 X 做 attention,而是分三路投影?

角色 语义职责 类比
Q (Query) "我现在想找什么信息?" 搜索框里输入的关键词
K (Key) "我能提供什么信息?" 图书馆每本书的标签/索引
V (Value) "我实际携带什么内容?" 书里的实际内容

Attention 的计算 = 用 Q 去查 K 的索引,找到相关的位置,然后把对应 V 的内容加权混合

如果 Q=K=V=X,那么:

  • 查询和索引用同一套表示,无法区分"我想找什么"和"我能提供什么"
  • Attention 矩阵天然对称,但语言中 "A 关注 B" 和 "B 关注 A" 的含义完全不同

分别投影后,模型可以学到:

  • W_q 专门学习"如何表达查询意图"
  • W_k 专门学习"如何表达可被检索的特征"
  • W_v 专门学习"什么内容值得被传递"

4. 多头的并行方式

上面的例子是单头。实际中会把 d_model 切成多个 head:

假设 num_heads = 2d_model = 4,则每个 head 的 d_k = 4/2 = 2

实现上不是真的"切",而是用一个大矩阵:

  • W_q 的形状是 (4, 4),输出 (2, 4)
  • reshape 成 (2, 2, 2),转置成 (2, 2, 2)
  • 两个 head 各是 (2, 2),并行计算

两个 head 的输出拼接回 (2, 4),再过 W_o: (4, 4) 投影。

关键洞察:不同 head 可以学到不同的关注模式——head 0 可能学会"看语法关系",head 1 可能学会"看语义相似性"。多头 = 多个视角并行观察。


5. 面试官常见深挖追问

  • "Self-Attention 里,Q 和 K 的内积为什么能衡量相关性?"
    • 答:因为内积 q·k = |q||k|cos(θ)。当两个向量方向相近(语义相关)时,夹角 θ 小,cos(θ) 大,内积就大;方向垂直时内积为 0;方向相反时为负。所以内积天然编码了"方向相似度"。加上投影矩阵后,模型还能学习把语义相关性映射到向量方向的接近上。
  • "为什么 attention 叫 '软' 查找?跟硬查找有什么区别?"
    • 答:硬查找是"要么全要、要么不要"(比如数据库查一条记录);软查找是"每个位置都贡献一点,贡献多少由权重决定"。Softmax 保证所有权重和为 1,输出是输入的加权和,所以信息是"混合"的,不是"替换"的。
  • "Causal mask 在训练时和推理时的作用有区别吗?"
    • 答:训练时,一次前向传播处理整个序列,需要用 causal mask 阻止位置 i 看到位置 i+1, i+2... 的信息。推理时,模型是逐 token 生成的,每次只处理一个位置,天然就看不到未来,所以 causal mask 在推理时不是必须的——但为了保持训练和推理的一致性,通常仍然保留。

#8. 为什么 LLM 时代 Decoder-only 压过了 Encoder-Decoder?它付出了什么代价?

#标准答案

Decoder-only 在 LLM 时代胜出,核心是它把“预训练、指令微调、在线推理”三件事统一成了同一种 token 续写范式。团队不需要为翻译、摘要、问答、对话分别设计完全不同的训练接口,几乎所有数据都能转成 prompt -> continuation 的形式,规模化效率非常高。

但它不是没有代价。第一,它对强条件生成任务未必总比 Encoder-Decoder 更自然;第二,它必须逐 token 自回归解码,所以线上时延和吞吐优化压力更大;第三,它没有把输入理解和输出生成显式分工,很多”先编码、再解码”的结构优势被牺牲掉了。面试里这样答,会显得你在讲范式选择,不是在喊口号。


#深度解析

1. Decoder-only 胜出的三个深层原因

原因 细节 工程影响
数据格式统一 所有任务 → “prompt + continuation” 不需要为翻译/摘要/问答分别造数据格式
训练-推理一致性 训练和推理都是 next-token prediction 没有 train-test gap,没有 exposure bias
infra 简化 单一模型、单一前向逻辑 一套训练框架、一套 serving 框架、一套 checkpoint

2. Decoder-only 付出的代价

代价 具体表现 缓解手段
自回归延迟 生成 N 个 token 需要 N 次 forward KV cache、speculative decoding、continuous batching
输入理解受限 只能单向看输入(Encoder-Decoder 可双向) 更大模型、更长上下文、更强 prompt 工程
长输入高显存 长 prompt 的 KV cache 占用大 GQA/MQA、cache 量化、PagedAttention
条件生成弱 翻译/摘要等”输入→输出”任务天然弱于 E-D 指令微调、前缀调优

3. 为什么 Encoder-Decoder 在 NLP 早期更强?

  • BERT/GPT-1/T5 时代(2018-2019):模型小(<1B),Decoder-only 的理解能力不够强
  • 翻译任务:Encoder 双向理解源语言 → Decoder 生成目标语言,分工明确效果好
  • T5 的 text-to-text 统一框架:把所有任务都转成 “输入文本 → 输出文本”

转折点:GPT-3(2020,175B)展示了 Decoder-only 的”涌现能力”——规模够大时,统一生成范式的优势超过结构分工的优势。

4. 现在的 Encoder-Decoder 还有价值吗?

有,但场景变窄了:

  • 机器翻译:WMT 等比赛 top 系统仍常用 E-D(如 DeepL)
  • 长文档摘要:输入长、输出也长,E-D 的分离结构更可控
  • 代码生成(特定场景):需要精确理解长上下文后生成结构化输出

但通用 LLM(ChatGPT/Claude/Gemini)清一色 Decoder-only,因为”统一范式 + 简化 infra”的工程收益太大。

5. 面试官常见深挖追问

  • ”Decoder-only 做翻译,和 T5 的 Encoder-Decoder 做翻译,哪个质量更好?”
    • 答:数据量小、模型小时 E-D 更好(双向理解 + 分离解码)。但模型大到 100B+、数据万亿级时,Decoder-only 通过 in-context learning 和 instruction tuning 也能达到接近甚至超越的效果。关键是工程上 Decoder-only 只需维护一套 infra,成本远低于 E-D。
  • ”如果让你设计一个'理解+生成'都强的系统,你会怎么选架构?”
    • 答:看场景。通用对话 → Decoder-only(简单、统一);翻译/摘要 → 考虑 Encoder-Decoder(输入输出分离更可控);理解任务为主 → Encoder-only(如 BERT 做分类、匹配)。也可以混合:用 Decoder-only 做主干,特定任务接轻量 encoder 模块。
  • ”Decoder-only 的因果掩码,会不会限制模型'理解'能力?”
    • 答:理论上会。双向注意力能同时看到左右文,对理解更有利。但实际中 Decoder-only 通过:1)海量预训练数据弥补;2)足够深的层数让信息充分流动;3)长上下文(128K+)让”单向”也能看到很远的前文。所以实践中 Decoder-only 的理解能力并不弱于同规模的 Encoder-only。

#9. Pre-Norm 和 Post-Norm 有什么区别?为什么现代大模型更偏向 Pre-Norm?

#标准答案

Pre-Norm 和 Post-Norm 的区别,表面看只是 LayerNorm 放在子层前还是子层后,但背后是训练稳定性差异。Pre-Norm 是先归一化、再进入 attention 或 FFN,再走残差;Post-Norm 则是子层输出和残差相加后再做归一化。

现代大模型更偏向 Pre-Norm,主要因为网络做得很深时,Pre-Norm 的梯度传递更顺,不容易在深层出现训练不稳、梯度爆炸或消失。Post-Norm 在早期 Transformer 里常见,但一旦层数和规模继续增大,稳定性问题会更明显。所以面试里最好把答案讲成:Pre-Norm 不只是"现在流行",而是更适合超深层大模型训练。


#深度解析

1. 用公式看 Pre-Norm 和 Post-Norm 的区别

  • Post-Norm(原始 Transformer):x_{l+1} = Norm(x_l + Sublayer(x_l))
  • Pre-Norm(现代 LLM):x_{l+1} = x_l + Sublayer(Norm(x_l))

表面只是 Norm 位置不同,但对梯度流的影响完全不同。

2. 为什么 Pre-Norm 的梯度更稳定?

在反向传播时,梯度要从输出层传回输入层。Post-Norm 中,梯度每经过一层都要先经过 Norm 再经过残差。Norm 层会对梯度做缩放,这种缩放会逐层累积。层数很深时,梯度要么被不断压缩(消失),要么在某些维度被放大(爆炸)。

Pre-Norm 中,梯度通过残差连接 x_l + ... 时有一条"高速公路"可以直接回流,不受 SublayerNorm 的干扰。数学上,Pre-Norm 的残差路径更接近恒等映射:∂x_{l+1} / ∂x_l ≈ I + small_term,深层梯度不会指数衰减。

3. Post-Norm 的效果真的更差吗?

不一定。Post-Norm 在浅层网络(如原始 Transformer 的 6 层)上训练稳定,而且理论上 Post-Norm 的表示能力更强——因为每一层的输出都经过归一化,动态范围更一致。但层数超过 12-24 层后,训练不稳定的代价超过了表示优势,所以大模型时代 Pre-Norm 成为主流。

4. 为什么还需要残差连接?

没有残差连接时,深层网络等价于很多层函数复合:x_L = f_L(f_{L-1}(...f_1(x)...))。反向传播时梯度要穿过所有层的 Jacobian 连乘,很容易爆炸或消失。加上残差后:x_{l+1} = x_l + f(x_l),梯度有了一条直通路径:∂x_{l+1}/∂x_l = I + ∂f/∂x_l。即使 ∂f/∂x_l 很小,梯度也不会完全消失。

5. 面试官常见深挖追问

  • "如果我把 LayerNorm 换成 RMSNorm,训练稳定性会有什么变化?"
    • 答:RMSNorm 去掉了 LayerNorm 中的去均值操作(减去均值),计算更省。在 LLM 中,因为隐藏状态通常已经以零为中心(ReLU/SwiGLU 的输出),去均值的收益不明显,所以 RMSNorm 足够稳定且更快。但严格说,如果输入分布有明显偏移,LayerNorm 的去均值仍有价值。
  • "Pre-Norm 这么好,那 Post-Norm 还有存在的价值吗?"
    • 答:有。Post-Norm 在 encoder 类浅层任务上仍然有效(BERT 就是 Post-Norm 且只有 12/24 层)。另外,一些研究表明 Post-Norm 的表示质量在某些下游任务上略好,只是训练需要更小心(如学习率预热要更长、warmup steps 更多)。
  • "为什么 Transformer 需要 LayerNorm,CNN 不需要?"
    • 答:Transformer 的 attention 输出是加权和,不同样本、不同位置的数值范围差异很大;深层堆叠后这种差异会被放大。CNN 有局部感受野和参数共享,且通常带 BatchNorm,对内部数值范围有一定的自然约束。另外,Transformer 的残差连接导致深层输出是各层输出的累加,没有归一化会无界增长。

#10. 如果上下文特别长,Attention 复杂度会成为什么瓶颈?有哪些替代思路?

#标准答案

当上下文特别长时,attention 会同时变成计算瓶颈、显存瓶颈和带宽瓶颈。原因是每个 token 都要和大量历史 token 做交互,QK^T 计算量迅速增长,中间注意力矩阵也会占很多显存,推理时 KV cache 又会继续放大压力。

替代思路大体有三类。第一类是”不改算法、先改实现”,例如 FlashAttention 通过 IO-aware 的方式减少中间张量落地;第二类是”减少缓存或交互成本”,例如 GQA/MQA/MLA;第三类是”换建模路线”,例如稀疏 attention、线性 attention、SSM/Mamba,或者在系统层用 RAG 减少真正需要塞进上下文的信息量。这样回答会比只列术语更有层次。


#深度解析

1. 计算量的具体数值感受

Self-Attention 的核心计算是 Q @ K^Tweights @ V

  • Q @ K^T(n, d_k) @ (d_k, n) → (n, n),计算量是 O(n² × d_k)
  • weights @ V(n, n) @ (n, d_v) → (n, d_v),计算量也是 O(n² × d_v)

假设 d_k = d_v = 64,看不同序列长度下的计算量:

序列长度 n Attention 计算量 (flops) 相对于 n=1K 的倍数
1K ~1.3 × 10⁸
4K ~2.1 × 10⁹ 16×
8K ~8.4 × 10⁹ 64×
32K ~1.3 × 10¹¹ 1024×
128K ~2.1 × 10¹² 16384×

长度从 1K 增加到 128K(128 倍),attention 计算量增长 16384 倍。这就是”二次方诅咒”。

2. KV Cache 的显存占用计算

推理时每层每头需要缓存 K 和 V 两个矩阵。

以 LLaMA-2 7B 为例:

  • 层数 L = 32
  • 隐藏维度 d = 4096
  • 头数 n_heads = 32
  • d_k = 4096 / 32 = 128
  • 使用 FP16(2 字节)

KV cache 大小 = 2 × L × n_heads × d_k × seq_len × batch_size × 2 bytes = 2 × 32 × 32 × 128 × seq_len × batch_size × 2524,288 × seq_len × batch_size 字节

seq_len batch=1 的 KV cache batch=8 的 KV cache
1K ~512 MB ~4 GB
4K ~2 GB ~16 GB
8K ~4 GB ~32 GB
32K ~16 GB ~128 GB

一个 7B 模型本身参数只占约 14 GB(FP16),但 batch=8、seq_len=32K 时 KV cache 就要 128 GB,远超模型参数本身。这就是为什么长上下文首先打爆的是显存。

3. 三类替代方案的本质区别

类别 代表方法 核心思想 复杂度变化 代价
IO优化 FlashAttention 减少HBM访问,fuse kernel 仍是 O(n²),但常数更小 需要特定kernel实现
缓存压缩 GQA/MQA/MLA 减少 KV 的头数或维度 显存从 O(n×h) 降到 O(n×h') 轻微效果损失
稀疏/线性注意力 Longformer、BigBird、Linformer、Performer 让每个token只关注部分位置,或用核方法近似 从 O(n²) 降到 O(n) 或 O(n log n) 实现复杂,效果通常略逊于全注意力
换架构 Mamba、RWKV 用状态空间模型替代attention O(n) 生态不成熟,长程建模能力仍在验证

4. 面试官常见深挖追问

  • ”FlashAttention 说它是 O(n²) 复杂度,那它到底优化了什么?”
    • 答:FlashAttention 没有改变 attention 的数学形式和渐近复杂度,它优化的是内存访问模式。标准 attention 需要把 (n, n) 的注意力矩阵显式写入 HBM(高带宽显存),这会消耗大量带宽。FlashAttention 通过分块(tiling)和重计算(recomputation),让小块矩阵在 SRAM(高速缓存)里完成计算,避免了大矩阵的反复读写。所以它减少的是wall-clock time显存峰值,不是理论复杂度。
  • ”为什么稀疏 attention(比如只让每个token看最近的1024个位置)在实际中落地没那么容易?”
    • 答:因为稀疏模式会改变模型的注意力结构,预训练好的模型不能直接用稀疏模式——它没见过这种稀疏的注意力分布。如果要落地,通常需要重新预训练或至少做大量继续训练。而像 GQA/MQA 这样的改动,可以在不改变 attention 模式的情况下减少缓存,所以更容易在已有模型上直接应用。
  • ”如果有一个 100K token 的文档要处理,你会怎么选择方案?”
    • 答:不会只用一个方案,而是分层处理。首先看是不是所有 100K 都需要一次性处理:如果用 RAG 能先召回最相关的几段(比如 top-5 共 5K token),那就只需要处理 5K,问题就解决了。如果确实需要一次处理 100K,先看硬件能不能放下 KV cache(100K × 7B 模型的 KV cache 约 50 GB),能放下就用 FlashAttention 加速;放不下再考虑 GQA 或分段处理。

#11. MHA、MQA、GQA 在效果和推理成本上的 trade-off 是什么?

#标准答案

这三者本质上是在权衡“表达能力”和“推理缓存成本”。MHA 里每个 query head 都有自己独立的 K/V,表达最充分,效果通常最好,但 KV cache 也最大。MQA 把很多 query head 对应到同一组 K/V,显存和带宽最省,推理会很友好,但因为共享过强,质量有时掉得比较明显。

GQA 则是折中方案:不是所有 head 共用一组 K/V,而是按组共享,让缓存成本明显下降,但表达能力又比 MQA 保留得更多。所以生产系统里经常看到 GQA,因为它不是理论上最极致,而是在效果和成本之间更平衡。


#深度解析

1. 从一个具体小例子看 MHA 的完整计算流程

假设输入序列有 3 个 token:["我", "喜欢", "猫"],模型维度 d_model = 8,head 数 n_heads = 4,每个 head 的维度 head_dim = 2

Step 1:输入投影得到 Q、K、V

输入 X 的形状是 (3, 8)。用三个投影矩阵分别计算:

Q = X @ W_q    -> 形状 (3, 8)  @ (8, 8)  -> (3, 8)
K = X @ W_k    -> 形状 (3, 8)  @ (8, 8)  -> (3, 8)
V = X @ W_v    -> 形状 (3, 8)  @ (8, 8)  -> (3, 8)

Step 2:把 Q/K/V 切成多个 head

MHA 的核心操作是把最后一维切成 n_heads 份,每份 head_dim 维:

Q 形状 (3, 8) -> reshape -> (3, 4, 2) -> transpose -> (4, 3, 2)
                 ^^^^^^^^^ 4 个 head,每个 head 是 (3, 2)

K 同理 -> (4, 3, 2)
V 同理 -> (4, 3, 2)

现在每个 head h 都有自己的 Q_h (3, 2)K_h (3, 2)V_h (3, 2),它们独立计算 attention,互不相干。

Step 3:单个 head 内怎么计算 attention

以 head 0 为例:

Scores = Q_0 @ K_0^T / sqrt(2)    -> (3, 2) @ (2, 3) -> (3, 3)

假设原始 Q_0 @ K_0^T =
  [[ 1.0,  0.5, -0.2],
   [ 0.3,  1.2,  0.8],
   [-0.1,  0.4,  0.9]]

除以 sqrt(2) ~ 1.414:
  [[ 0.71,  0.35, -0.14],
   [ 0.21,  0.85,  0.57],
   [-0.07,  0.28,  0.64]]

Step 4: causal mask(因果掩码)

Decoder-only 模型只能看当前位置及之前的位置。对 3 个 token,mask 是:

mask = [[1, 0, 0],      # 位置0只能看位置0
        [1, 1, 0],      # 位置1能看位置0,1
        [1, 1, 1]]      # 位置2能看位置0,1,2

把 mask 为 0 的位置填 -inf,再 softmax:

 masked scores -> softmax -> Attention Weights A_0
  [[ 0.71,  -inf,  -inf]      [[1.00,  0.00,  0.00]
   [ 0.21,  0.85,  -inf]  ->    [0.34,  0.66,  0.00]
   [-0.07,  0.28,  0.64]]      [0.28,  0.37,  0.35]]

Step 5:加权求和得到 head 输出

Output_0 = A_0 @ V_0    -> (3, 3) @ (3, 2) -> (3, 2)

4 个 head 各自独立做完上述过程,得到 4 个 (3, 2) 的输出,concat 拼回 (3, 8),再过 W_o 投影。

关键洞察:每个 head 只在自己的 2 维子空间里计算 attention,相当于用 4 种不同的"视角"去看同一句话。head 0 可能学会"看代词指代",head 1 可能学会"看局部语法",head 2 可能学会"看位置关系"——这是 MHA 表达能力强的根源。


2. GQA 的计算和 MHA 到底哪里不同?

沿用上面的例子,但现在 K/V 不是 4 个 head,而是只有 2 个(n_kv_heads = 2)。这就是 GQA

Q 的切法不变

Q -> (4, 3, 2)   # 还是 4 个 query head

K/V 的切法变了

K -> (2, 3, 2)   # 只有 2 个 kv head
V -> (2, 3, 2)

问题来了:4 个 query head 怎么和 2 个 kv head 配对?

答案是按组配对。4 个 query head 分成 2 组,每组 2 个 query head 共享同一组 K/V:

query head 0  --+
query head 1  --+---> 共享 kv head 0
                |
query head 2  --+
query head 3  --+---> 共享 kv head 1

具体到计算:

  • head 0 的 attention:Q_0 (3,2)K_0 (3,2) 算 score -> A_0 @ V_0
  • head 1 的 attention:Q_1 (3,2) 也跟 K_0 (3,2) 算 score -> A_1 @ V_0
  • head 2 的 attention:Q_2 (3,2)K_1 (3,2) 算 score -> A_2 @ V_1
  • head 3 的 attention:Q_3 (3,2) 也跟 K_1 (3,2) 算 score -> A_3 @ V_1

发现了吗?GQA 的 attention 计算流程和 MHA 完全一样,唯一的区别是 K/V 的来源从"每个 query head 有自己的"变成了"每组的 query heads 共用同一组"。工程上实现时,通常先把 K/V 复制(broadcast)到和 query head 数对齐,再做标准 attention。

MQA 是 GQA 的极端n_kv_heads = 1,所有 4 个 query head 共用同一组 K/V。


3. 为什么共享 K/V 会损失效果?从机制上理解

MHA 中,每个 query head h 有自己的 K_hV_h。这意味着:

  • K_h 可以被专门优化来"服务" Q_h 的查询模式
  • 比如 Q_0 喜欢查"代词指代",K_0 就学会把代词相关信息编码到 key 里
  • Q_1 喜欢查"动词搭配",K_1 就学会把动词相关信息编码到 key 里

当 GQA 让 Q_0Q_1 共享 K_0 时,K_0 必须同时满足两种查询需求——既要包含代词信息,又要包含动词信息。一个固定维度的向量能承载的信息量有限,被迫"服务两个主子"时,每个主子得到的信息就会变少。

类比:MHA 像是每个部门(query head)有自己的专属档案员(K/V),查什么都很精准。GQA 像是几个部门共用一个档案员,档案员必须把多个部门关心的信息都记下来,每个部门查询时得到的回答就不如专属档案员那么精准。MQA 则是全公司只有一个档案员,信息混杂最严重。

GQA 折中的好处是:不需要完全独立(那样缓存太大),但分组后不同组之间还可以保持一定差异化——第 1 组的 K/V 可以专门服务"语法类" query,第 2 组的 K/V 可以专门服务"语义类" query。


4. KV Cache 与参数量的数值对比

方案 n_kv_heads KV cache (L=32, seq=4096, B=1, FP16) 相对于 MHA
MHA 32 ~2.1 GB 1.0x
GQA (8组) 8 ~536 MB 0.25x
MQA 1 ~67 MB 0.031x

KV cache 公式:2 * B * seq_len * L * n_kv_heads * head_dim * 2 bytes

参数量方面:

  • W_q 始终是 d_model * d_model(因为 query 不共享)
  • W_kW_vd_model * (n_kv_heads * head_dim)
  • MHA 中 n_kv_heads = n_heads,GQA/MQA 中更小

5. 实际落地中的选择

场景 推荐方案 理由
研究/追求最高效果 MHA 不省缓存,效果天花板最高
生产部署/长上下文服务 GQA (4-8组) 缓存降 1/4~1/8,效果损失通常 <1%
极致边缘部署 MQA 缓存最小,适合手机/嵌入式

LLaMA-2、Mistral、Qwen 等生产模型都采用了 GQA,说明它已成为平衡效果与成本的主流选择。


6. 面试官常见深挖追问

  • "GQA 的 '组' 数怎么选?8 组一定比 4 组好吗?"
    • 答:组数越多越接近 MHA(效果越好、缓存越大),组数越少越接近 MQA(缓存越小、效果越差)。选择是工程权衡:先看显存/带宽瓶颈有多严重,再测不同组数的效果损失。通常 4-8 组是 sweet spot。
  • "GQA 和 MQA 在代码实现上怎么做到共享 K/V 的?"
    • 答:最简洁的实现方式是先把 K/V 从 (batch, n_kv_heads, seq_len, head_dim) broadcast 成 (batch, n_heads, seq_len, head_dim),让 query 和 key/value 的 head 维度对齐,然后直接做标准 attention 计算。 PyTorch 里用 expand()repeat() 即可,不需要改 attention 的核心逻辑。
  • "如果从 MHA 模型转成 GQA,能直接改结构加载权重吗?"
    • 答:不能直接加载,因为 K/V 投影的权重形状变了。常见做法是用原 MHA 模型中对应组的 K/V 权重做平均或筛选,初始化 GQA 的 K/V 投影,然后做少量继续训练来恢复效果。
  • "MLA 和 GQA 有什么区别?"
    • 答:GQA 是在 head 维度上共享 K/V(减少 K/V 的头数);MLA 更进一步,在 latent 空间压缩 K/V,用更少的 latent 维度来表示缓存信息,压缩更激进但需要重新设计 attention 结构。

#12. 你如何向不懂深度学习的人解释”注意力机制”到底在做什么?

#标准答案

可以把注意力机制理解成“动态查资料”。模型在处理当前这个词时,不会对前文一视同仁,而是会临时决定:哪些词和我现在最相关,应该多看一点;哪些词关系不大,可以少看一点。这个“看多看少”的权重,就是注意力分数。

如果要讲得更生活化,可以说:人读一句长句子时,也不会每次都平均回看所有前文,而是会根据当前问题重点回看人名、时间、动作或结论。注意力机制做的就是类似事情,只不过它把这个过程变成了可学习的向量计算。


#深度解析

1. 注意力机制的"查询-键-值"类比

把 attention 比喻成图书馆查资料

概念 图书馆类比 数学对象
Query 你当前的问题 当前 token 的向量 q
Key 每本书的标签/目录 每个历史 token 的向量 k
Value 书里的实际内容 每个历史 token 的向量 v
Attention Score 某本书对你问题的相关度 q·k / √d
Weighted Sum 综合多本书内容形成答案 Σ softmax(score) × v

2. 为什么需要"注意力"而不是固定权重?

固定权重(如 CNN 的卷积核):

  • 每个位置只能看固定范围的邻居(如前后 2 个词)
  • 不管当前词是什么,看的范围都一样

注意力:

  • "苹果"在"我吃苹果"中 → 多看"吃"(动作关系)
  • "苹果"在"苹果公司"中 → 多看"公司"(实体关系)
  • 同一个词,根据上下文动态决定"看什么"

3. Self-Attention 的本质:每个词都问所有词一个问题

句子:"猫 坐 在 垫子 上"

"猫"会问:
- "猫"和"坐"相关吗?→ 高(主谓关系)
- "猫"和"垫子"相关吗?→ 中(位置关系)
- "猫"和"上"相关吗?→ 低

"坐"会问:
- "坐"和"猫"相关吗?→ 高
- "坐"和"垫子"相关吗?→ 高(坐在垫子上)

每个词都有独立的 Q 向量,所以"问的问题"不同,关注的重点也不同。

4. Multi-Head:多问几个问题

不只是问一个问题,而是同时问 8/16/32 个问题:

  • Head 1:"谁是动作执行者?"(找主语)
  • Head 2:"动作对象是什么?"(找宾语)
  • Head 3:"时间/地点是什么?"
  • Head 4:"代词指代谁?"

每个 head 独立学习不同的"关注点",组合起来形成丰富的表示。

5. 面试官常见深挖追问

  • "注意力机制和 RNN 的区别是什么?"
    • 答:RNN 像"传纸条"——信息从第一个词逐步传到最后一个词,长距离信息会衰减。注意力像"开群聊"——每个词可以直接和所有词对话,没有距离衰减。所以注意力在处理长距离依赖(如代词指代)时更强。
  • "注意力为什么需要除以 sqrt(d_k)?"
    • 答:当维度 d_k 很大时,q·k 的点积值会很大(方差随 d_k 线性增长),导致 softmax 进入饱和区(梯度极小)。除以 sqrt(d_k) 把方差缩回 1,保持梯度健康。详见问题 3 深度解析。
  • "为什么说 Transformer 是'完全并行'的,RNN 不是?"
    • 答:RNN 计算第 t 个词的表示时,必须等第 t-1 个词算完(顺序依赖)。Transformer 的 attention 中,所有词的 Q/K/V 可以同时计算,所有 attention score 可以同时计算——没有顺序依赖,所以可以在 GPU 上完全并行。