Cheatsheet · 题解

LLM Architecture 公开速查手册

Bilingual Cheat Sheet · 中文为主 · English Terms Included 覆盖 Transformer 结构、Attention 变体、位置编码、KV Cache、解码策略、MoE、Scaling Laws 等核心主题


Part 1 · 核心概念与公式推导


1.1 Transformer 基本架构

标准 Decoder-only block(GPT / LLaMA 系列)由以下组件堆叠而成:

InputLayerNormMulti-Head Self-Attention (causal)+ ResidualLayerNormFeed-Forward Network (FFN)+ Residual

以上为 Pre-LN(Pre-Norm)结构,即 LayerNorm 在子层之前。原始 Transformer 论文使用 Post-LN(子层之后),Pre-LN 在深层模型中训练更稳定。

Encoder(如 BERT)使用 bidirectional attentionEncoder-Decoder(如 T5)在 Decoder 额外加 cross-attention 层。当前 LLM 主流为 decoder-only,原因包括:自回归预训练天然适配生成任务、scaling 表现好、架构简洁。


1.2 Self-Attention 计算

Scaled Dot-Product Attention:

Attention(Q,K,V)=softmax ⁣(QKdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V

步骤

  1. 输入 XRN×dmodelX \in \mathbb{R}^{N \times d_{\text{model}}} 经线性投影得到 Q=XWQQ = XW_QK=XWKK = XW_KV=XWVV = XW_V,各 RN×dk\in \mathbb{R}^{N \times d_k}
  2. 计算注意力分数 S=QK/dkRN×NS = QK^\top / \sqrt{d_k} \in \mathbb{R}^{N \times N}
  3. 施加 causal mask(上三角填 -\infty
  4. Softmax 得权重 A=softmax(S)A = \text{softmax}(S)
  5. 输出 O=AVRN×dkO = AV \in \mathbb{R}^{N \times d_k}

dk\sqrt{d_k} 缩放的作用:当 dkd_k 较大时,QKQK^\top 的方差约为 dkd_k(假设 Q,KQ, K 各分量独立、零均值、单位方差),导致 softmax 输入值过大,梯度趋近于零(饱和区)。除以 dk\sqrt{d_k} 将方差归一化到 1\sim 1

复杂度:时间 O(N2dk)O(N^2 d_k),空间 O(N2+Ndk)O(N^2 + Nd_k)(attention 矩阵为主要瓶颈)。


1.3 Multi-Head Attention (MHA)

单个 attention head 只能学习一种关注模式。Multi-Head Attention 并行运行 hh 个 head,各自在不同子空间学习:

headi=Attention(QWiQ,  KWiK,  VWiV)\text{head}_i = \text{Attention}(QW_i^Q,\; KW_i^K,\; VW_i^V)

MHA(Q,K,V)=Concat(head1,,headh)WO\text{MHA}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)\, W^O

其中 WiQ,WiKRdmodel×dkW_i^Q, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}WiVRdmodel×dvW_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}WORhdv×dmodelW^O \in \mathbb{R}^{hd_v \times d_{\text{model}}},通常 dk=dv=dmodel/hd_k = d_v = d_{\text{model}} / h。总参数量与单 head 相当。


1.4 GQA 与 MQA

方法 K/V head 数 KV Cache 大小(相对 MHA) 代表模型
MHA hh(= Q head 数) 1×1\times GPT-3
MQA (Multi-Query Attention) 11 1/h1/h PaLM, Falcon
GQA (Grouped-Query Attention) gg1<g<h1 < g < h g/hg/h LLaMA-2/3, Mistral

GQA 详解hh 个 Query head 分为 gg 组,每组共享一套 K/V head。实现时将 K/V 投影矩阵从 h×dkh \times d_k 压缩为 g×dkg \times d_k,推理时 KV Cache 缩减为 g/hg/h

从 MHA checkpoint 做 uptraining 转换到 GQA:将每组内的 K/V 权重取均值作为初始化,再用少量数据继续训练(原论文建议约 5% 预训练 tokens)。


1.4b MLA — Multi-head Latent Attention(DeepSeek-V2/V3)

GQA 靠减少 KV head 数省 cache,代价是表达力下降。MLA(DeepSeek-V2, arXiv:2405.04434)换一条路:把 K/V 联合压缩成一个低秩潜向量 ctKV=WDKVhtc^{KV}_t = W^{DKV} h_tdcdmodeld_c \ll d_{\text{model}}),只缓存 ctKVc^{KV}_t,attention 时再上投影还原:

ktC=WUKctKV,vtC=WUVctKVk^{C}_t = W^{UK} c^{KV}_t,\qquad v^{C}_t = W^{UV} c^{KV}_t

吸收技巧(inference 关键):score 的内容项可改写为

(qtC)ksC=(qtC)WUKcsKV=((WUK)qtC)csKV(q^{C}_t)^\top k^{C}_s = (q^{C}_t)^\top W^{UK} c^{KV}_s = \big((W^{UK})^\top q^{C}_t\big)^\top c^{KV}_s

WUKW^{UK} 可被吸收进 Query 投影——推理时无需为每个缓存 token 真的还原 kCk^C,直接拿 cKVc^{KV} 算;同理 WUVW^{UV} 吸收进输出投影 WOW^O。于是只缓存 cKVc^{KV}(外加下面的 RoPE 键),所有 up-projection 都折叠掉。

解耦 RoPE(decoupled RoPE):RoPE 是位置相关的旋转,若直接作用在 kC=WUKcKVk^C=W^{UK}c^{KV} 上,旋转矩阵会卡在 WUQW^{UQ}WUKW^{UK} 之间、破坏上面的吸收。MLA 因此把键拆成两段:

Query 同样拆成 qCq^C(吸收)与 qRq^R(带 RoPE)。最终 score:

Sts=(qtC)ksC+(qtR)ksRS_{ts} = (q^{C}_t)^\top k^{C}_s + (q^{R}_t)^\top k^{R}_s

收益:每 token 每层只缓存 dc+dRd_c + d_R(DeepSeek-V2:512+64=576512+64=576),远小于 MHA 的 2nhdh2\,n_h d_h;cache 量级 ≈ GQA(约 2.25 组)却能保住 ≈ MHA 的质量。


1.5 位置编码 (Positional Encoding)

1.5.1 绝对位置编码

Sinusoidal(原始 Transformer):

PE(pos,2i)=sin ⁣(pos100002i/d),PE(pos,2i+1)=cos ⁣(pos100002i/d)PE_{(pos, 2i)} = \sin\!\left(\frac{pos}{10000^{2i/d}}\right), \quad PE_{(pos, 2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d}}\right)

直接加到 token embedding 上。Learned 版本用可训练 embedding 表替代。缺点:最大长度固定,外推性差。

1.5.2 RoPE (Rotary Position Embedding)

在 Q、K 上施加旋转矩阵,使点积仅依赖相对位置差

qm=Rmqm,kn=Rnknq_m' = R_m\, q_m, \quad k_n' = R_n\, k_n

其中 RmR_m 是对 (q2i,q2i+1)(q_{2i}, q_{2i+1}) 子对施加角度 mθim\theta_i 的 2D 旋转矩阵,θi=100002i/d\theta_i = 10000^{-2i/d}

关键性质

qm,kn=Rmqm,Rnkn=f(q,k,mn)\langle q_m',\, k_n' \rangle = \langle R_m q_m,\, R_n k_n \rangle = f(q, k,\, m - n)

attention score 仅依赖相对位置 mnm-n。旋转操作不改变向量范数,数值稳定。

1.5.3 ALiBi (Attention with Linear Biases)

不修改 embedding,直接在 attention score 上加线性偏置:

Sij=qikjmhijS_{ij} = q_i^\top k_j - m_h \cdot |i - j|

mhm_h 按 head 取固定指数级值。优点:无需额外参数,外推性好。缺点:prefix caching 需在 attention 时按缓存 token 的绝对位置动态计算偏置偏移,增加实现复杂度(部分推理框架不支持)。


1.6 RoPE 长度外推

训练时最大长度 LtrainL_{\text{train}},推理时更长序列会遇到未见过的频率分量,导致分布偏移。

方法 核心思路 特点
Position Interpolation (PI) 缩放位置索引:mmLtrain/Ltestm \leftarrow m \cdot L_{\text{train}}/L_{\text{test}} 简单,但高频信息损失
NTK-aware Scaling 修改 base:10000α1000010000 \to \alpha \cdot 10000,高频少缩放、低频多缩放 比 PI 效果好
YaRN NTK + 混合插值 + attention temperature 调整 当前主流方案之一
Dynamic NTK 推理时根据实际序列长度动态调整 base 无需重新训练

推导直觉(为什么改 base 而非缩位置):RoPE 第 ii 对维度的频率 θi=base2i/d\theta_i = \text{base}^{-2i/d},波长 λi=2π/θi=2πbase2i/d\lambda_i = 2\pi/\theta_i = 2\pi\,\text{base}^{2i/d}——低维高频(短波长、编码局部),高维低频(长波长、编码全局)。


1.7 KV Cache

自回归生成:每生成一个新 token,之前所有 token 的 K/V 已计算过。KV Cache 将历史 K/V 存储在 GPU 显存中,新 token 只需计算自身 Q 并与缓存的 K/V 做 attention。

显存估算(每 token):

KV Cache=2×L×nkv_heads×dk×bytes_per_element\text{KV Cache} = 2 \times L \times n_{\text{kv\_heads}} \times d_k \times \text{bytes\_per\_element}

其中 LL = 层数,nkv_headsn_{\text{kv\_heads}} = K/V head 数(GQA 时 =g= g),dkd_k = head dimension,factor 2 对应 K 和 V。

示例:32 层、dk=128d_k = 128、GQA g=8g = 8、BF16(2 bytes)→ 每 token 约 2×32×8×128×2=1310722 \times 32 \times 8 \times 128 \times 2 = 131072 bytes 128\approx 128 KB。序列长 4096 → 约 0.5 GB(单序列)。


1.8 PagedAttention

类比 OS 虚拟内存分页:将 KV Cache 切成固定大小的 block(page),用 block table 管理逻辑-物理映射,允许非连续物理内存存储。

解决的问题:标准实现中 KV Cache 需要连续显存,导致严重的内存碎片(internal + external fragmentation),GPU 利用率低。PagedAttention 使显存利用率大幅提升,是 vLLM 推理框架的核心技术。

Continuous Batching(iteration-level scheduling)的关系:Continuous batching 解决请求级调度问题(不同请求长度不同,不必等最长请求完成);PagedAttention 解决显存管理问题。二者互补。


1.9 FlashAttention

问题:标准 attention 需将 N×NN \times N 的 score 矩阵写回 HBM(GPU 高带宽内存),是 memory-bound 操作。

核心思路Tiling + Online Softmax

  1. Q,K,VQ, K, V 分成小 tile,逐块加载到 SRAM(片上高速缓存)
  2. 在 SRAM 内完成分块的 attention 计算
  3. 利用 online softmax 算法(Milakov & Gimelshein, 2018),在不存储完整 N×NN \times N 矩阵的情况下实现与标准 attention 数学等价的结果

IO 复杂度:从 O(N2d)O(N^2 d) 降至 O(N2d2/M)O(N^2 d^2 / M),其中 MM 为 SRAM 大小。显存从 O(N2)O(N^2) 降至 O(N)O(N)

版本演进:FlashAttention-2 优化了 warp 级并行;FlashAttention-3 针对 Hopper 架构(H100)利用异步流水线和 FP8 Tensor Core。

关键:FlashAttention 是精确算法,不是近似。

1.9b Online Softmax 递推(FlashAttention 核心)

Q,K,VQ,K,V 切成 tile,每次只在 SRAM 里处理一对 K/V tile,从不物化完整 N×NN\times N 矩阵。对一行 query 处理第 jj 个 KV tile(局部分数 Sj=qKj/dS_j=qK_j^\top/\sqrt{d}),维护三元状态 (mj,j,Oj)(m_j,\ell_j,O_j)

mj=max ⁣(mj1, rowmax(Sj))m_j=\max\!\big(m_{j-1},\ \operatorname{rowmax}(S_j)\big)

j=emj1mjj1+rowsum ⁣(eSjmj)\ell_j=e^{\,m_{j-1}-m_j}\,\ell_{j-1}+\operatorname{rowsum}\!\big(e^{\,S_j-m_j}\big)

Oj=emj1mjj1Oj1+eSjmjVjjO_j=\frac{e^{\,m_{j-1}-m_j}\,\ell_{j-1}\,O_{j-1}+e^{\,S_j-m_j}\,V_j}{\ell_j}

初值 m0=, 0=0, O0=0m_0=-\infty,\ \ell_0=0,\ O_0=0;扫完所有 tile 后 OO精确 attention 输出。只需保存 m,m,\ell(各 O(N)O(N))与输出 OOO(Nd)O(Nd)),关键是不物化 O(N2)O(N^2) 的注意力分数矩阵。

提示 / Note

rescaling trick: 当新 tile 的局部 max 超过当前 mm,因子 emj1mj<1e^{\,m_{j-1}-m_j}<1 把已累积的 ,O\ell,O 向下缩放,使归一化因子始终对应全局 max —— 等价于一次性全局 softmax。来源:FlashAttention(Dao et al., arXiv:2205.14135);online softmax 原始算法(Milakov & Gimelshein, arXiv:1805.02867)。


1.10 FFN 层

标准 FFN:

FFN(x)=W2σ(W1x+b1)+b2\text{FFN}(x) = W_2\, \sigma(W_1 x + b_1) + b_2

其中 W1Rdffn×dmodelW_1 \in \mathbb{R}^{d_{\text{ffn}} \times d_{\text{model}}}W2Rdmodel×dffnW_2 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ffn}}}dffn=4dmodeld_{\text{ffn}} = 4 d_{\text{model}}(原始论文的经验选择)。

SwiGLU 变体(LLaMA 系列):

SwiGLU(x)=(Swish(xW1)xW3)W2\text{SwiGLU}(x) = (\text{Swish}(xW_1) \odot xW_3)\, W_2

dffnd_{\text{ffn}} 通常取 83dmodel\frac{8}{3} d_{\text{model}}(取整到 128 的倍数),因有两个 gate 矩阵,总参数量与标准 4×4\times FFN 相当。


1.11 MoE (Mixture of Experts)

将 FFN 替换为 EEexpert FFN,每个 token 经 router(线性层 + top-kk 选择)选 kk 个 expert(通常 k=2k=2),只激活选中的 expert:

y=itop-kgiExperti(x),g=softmax(Router(x))y = \sum_{i \in \text{top-}k} g_i \cdot \text{Expert}_i(x), \quad g = \text{softmax}(\text{Router}(x))

总参数量E×E \times 单 expert 参数(远大于 dense),但 FLOPs/token(k/E)×(k/E) \times dense FLOPs(此处 dense 指等参数量的稠密 FFN),实现"大参数量、低计算量"。

负载均衡 loss:防止 expert collapse(所有 token 涌向少数 expert),通常加辅助 loss:

Laux=αEi=1Efipi\mathcal{L}_{\text{aux}} = \alpha \cdot E \sum_{i=1}^{E} f_i \cdot p_i

fif_i = 分配到 expert ii 的 token 比例,pip_i = router 分配概率均值。鼓励 fi,pif_i, p_i 均匀。

Expert Capacity:每个 expert 在一个 batch 内有容量上限。超出的 token 被 drop(跳过该 expert)。训练时 capacity factor 通常 1.0–1.25。

1.11b MoE 扩展:Expert Parallelism / DeepSeek-MoE / 无辅助损失均衡

Expert Parallelism (EP) —— GShard(arXiv:2006.16668):专家数超过单卡容量时,每卡持 E/PE/P 个 expert,token 经两次 All-to-All(dispatch 把 token 发到对应专家所在卡 → 各卡本地算 → combine 汇回)。容量内溢出的 token:Switch 直接 drop,GShard 经残差透传(gating 置零)。常与 TP/DP 组合。

DeepSeek-MoEarXiv:2401.06066)两招:

无辅助损失均衡arXiv:2408.15664;DeepSeek-V3 arXiv:2412.19437 采用):给每个 expert 一个可调 bias bib_i只加到 router logit 用于 top-K 选择(不改加权系数 gig_i);每步后按负载更新:过载 biγb_i-\gamma、欠载 bi+γb_i+\gamma。零梯度干扰、无需调 α\alpha


1.12 解码策略 (Decoding Strategies)

策略 原理 特点
Greedy 每步取 argmax\arg\max token 确定性,易重复退化
Beam Search 维护 kk 条候选序列 全局更优,多样性差,open-ended 生成效果反而更差
Top-kk 从概率前 kk 的 token 中采样 kk 固定,不自适应分布形状
Top-pp (Nucleus) 从累积概率 p\geq p 的最小集合中采样 自适应候选集大小,当前主流
Temperature softmax(z/T)\text{softmax}(z/T)T<1T < 1 更确定,T>1T > 1 更随机 通常与 top-pp 联用
Min-pp 过滤概率 <ppmax< p \cdot p_{\max} 的 token 比 top-kk/top-pp 更自适应

Speculative Decoding:用小 draft model 并行生成 kk 个候选 token,再用大 verifier 一次性验证。接受概率 min(1,  pverifier/pdraft)\min(1,\; p_{\text{verifier}} / p_{\text{draft}}),拒绝时从修正分布重新采样。输出分布与直接用 verifier 等价(无损加速)。

等价性证明(speculative sampling 为何无损):设 draft 分布 qq、target 分布 pp。提议 xqx\sim q,以 min(1,p(x)/q(x))\min(1,\,p(x)/q(x)) 接受;拒绝则从修正分布 pres(x)=(p(x)q(x))+y(p(y)q(y))+p_{\text{res}}(x)=\dfrac{(p(x)-q(x))_+}{\sum_y (p(y)-q(y))_+} 重采(记 ()+=max(0,)(\cdot)_+=\max(0,\cdot))。最终输出 xx 的概率:

Pr[out=x]=q(x)min ⁣(1,p(x)q(x))提议并接受+(1ymin(q,p))Pr[拒绝]pres(x)\Pr[\text{out}=x] = \underbrace{q(x)\min\!\big(1,\tfrac{p(x)}{q(x)}\big)}_{\text{提议并接受}} + \underbrace{\Big(1-\textstyle\sum_y \min(q,p)\Big)}_{\Pr[\text{拒绝}]}\cdot p_{\text{res}}(x)

q(x)min(1,p/q)=min(q(x),p(x))q(x)\min(1,p/q)=\min(q(x),p(x)),且 y(pq)+=1ymin(p,q)=Pr[拒绝]\sum_y(p-q)_+ = 1-\sum_y\min(p,q)=\Pr[\text{拒绝}],故第二项 =(p(x)q(x))+=(p(x)-q(x))_+。相加:

min(p,q)+(pq)+=min(p(x),q(x))+max(0,p(x)q(x))=p(x).\min(p,q)+(p-q)_+ = \min(p(x),q(x))+\max(0,\,p(x)-q(x)) = p(x).\quad\blacksquare

即输出严格服从 target 分布 pp。并行提议 kk 个 token 时,对各位置独立套用此式(接受到第一个被拒处,再补一个修正样本,故每轮至少产出 1 个 token)。

接受率 α\alpha 与期望加速(每轮产出多少 token):设单个 draft token 被接受的期望概率为 α\alpha(Leviathan 等证明 α=1E[DTV(p,q)]\alpha = 1-\mathbb{E}\,[D_{TV}(p,q)],draft 越接近 target 则 α\alpha 越高)。一轮提议 kk 个 token,沿用 Leviathan 等的简化假设:各位置接受概率独立同为 α\alpha(真实场景下接受与否随位置相关),则「产出 m+1\ge m{+}1 个 token」当且仅当前 mm 个 draft 连续被接受(概率 αm\alpha^m),对 m=0,,km=0,\dots,k 求和(等比级数)得每轮期望产出:

E[#tokens]=m=0kαm=1αk+11α\mathbb{E}[\#\text{tokens}] = \sum_{m=0}^{k}\alpha^m = \frac{1-\alpha^{\,k+1}}{1-\alpha}

α1\alpha\to 1 时趋于 k+1k{+}1(全接受再加 1 个 verifier「免费」token),α0\alpha\to 0 时趋于 1(每轮至少 1 个)。计入成本比 c=c= draft 单次前向 // verifier 单次前向,墙钟加速比 1αk+1(1α)(kc+1)\approx \dfrac{1-\alpha^{\,k+1}}{(1-\alpha)(kc+1)}——故 kk 并非越大越好:α\alpha 偏低时增大 kk 反被 kckc 拖累。


1.13 Tokenization — BPE

Byte Pair Encoding

  1. 初始词表 = 所有字节(或字符)
  2. 迭代:统计训练语料中频率最高的相邻符号对,合并为新符号,加入词表
  3. 重复直到词表达到目标大小

SentencePiece:语言无关的 BPE/Unigram 实现,直接在 unicode 字节上操作(LLaMA 系列使用)。

词表大小影响:Embedding 层参数量 = V×dmodel|V| \times d_{\text{model}}。大词表(如 LLaMA-3 的 128K)对多语言和代码更友好(中文 token 更完整),但增加 embedding 显存和 softmax 计算量。LM head 通常与 embedding 权重 weight tying 以节省参数。


1.14 Scaling Laws

Chinchilla Scaling Law(Hoffmann et al., 2022):

L(N,D)=ANα+BDβ+LL(N, D) = \frac{A}{N^\alpha} + \frac{B}{D^\beta} + L_\infty

实践含义:早期大模型(如 175B 参数、300B tokens)属于"过度参数化、训练不足"。用更小模型训练更多 token 可获得更优性价比。

后 Chinchilla 趋势:LLaMA 系列等推动"以推理效率为目标"的训练——即使超出 Chinchilla 最优 token 数继续训练,推理 FLOPs 不变但性能持续提升。


Part 2 · PyTorch 代码片段


2.1 Scaled Dot-Product Self-Attention

import torch
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q, K, V: (batch, heads, seq_len, d_k)
    mask:    (1, 1, seq_len, seq_len) or broadcastable
    """
    d_k = Q.size(-1)
    scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)  # (B, H, N, N)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))
    attn_weights = F.softmax(scores, dim=-1)
    return attn_weights @ V  # (B, H, N, d_k)

2.2 Multi-Head Attention (from scratch)

34 行 / lines
import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads
        
        self.W_q = nn.Linear(d_model, d_model, bias=False)
        self.W_k = nn.Linear(d_model, d_model, bias=False)
        self.W_v = nn.Linear(d_model, d_model, bias=False)
        self.W_o = nn.Linear(d_model, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, _ = x.shape
        # Project and reshape: (B, N, d_model) -> (B, H, N, d_k)
        Q = self.W_q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        
        # Scaled dot-product attention
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = torch.softmax(scores, dim=-1) @ V
        
        # Concat heads and project: (B, H, N, d_k) -> (B, N, d_model)
        out = attn.transpose(1, 2).contiguous().view(B, N, self.d_model)
        return self.W_o(out)

2.3 Grouped-Query Attention (GQA)

32 行 / lines
class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, n_q_heads, n_kv_heads):
        super().__init__()
        assert n_q_heads % n_kv_heads == 0
        self.n_q_heads = n_q_heads
        self.n_kv_heads = n_kv_heads
        self.n_groups = n_q_heads // n_kv_heads  # Q heads per KV head
        self.d_k = d_model // n_q_heads
        
        self.W_q = nn.Linear(d_model, n_q_heads * self.d_k, bias=False)
        self.W_k = nn.Linear(d_model, n_kv_heads * self.d_k, bias=False)
        self.W_v = nn.Linear(d_model, n_kv_heads * self.d_k, bias=False)
        self.W_o = nn.Linear(d_model, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, _ = x.shape
        Q = self.W_q(x).view(B, N, self.n_q_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        
        # Expand KV to match Q heads: (B, n_kv, N, d_k) -> (B, n_q, N, d_k)
        K = K.repeat_interleave(self.n_groups, dim=1)
        V = V.repeat_interleave(self.n_groups, dim=1)
        
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = torch.softmax(scores, dim=-1) @ V
        
        out = attn.transpose(1, 2).contiguous().view(B, N, -1)
        return self.W_o(out)

2.4 Rotary Position Embedding (RoPE)

import torch
import math

def precompute_rope_freqs(d_model, max_len, base=10000.0):
    """Precompute complex rotation frequencies."""
    # θ_i = base^(-2i/d), i = 0, 1, ..., d/2 - 1
    freqs = 1.0 / (base ** (torch.arange(0, d_model, 2).float() / d_model))
    t = torch.arange(max_len).float()           # (max_len,)
    freqs = torch.outer(t, freqs)                # (max_len, d/2)
    return torch.polar(torch.ones_like(freqs), freqs)  # e^{i·m·θ}

def apply_rope(x, freqs):
    """
    x: (B, H, N, d_k) — real-valued
    freqs: (N, d_k/2) — complex
    """
    # View last dim as complex: (..., d_k) -> (..., d_k/2) complex
    x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
    # Rotate: broadcast freqs to (1, 1, N, d_k/2)
    x_rotated = x_complex * freqs.unsqueeze(0).unsqueeze(0)
    return torch.view_as_real(x_rotated).reshape_as(x).to(x.dtype)

2.5 KV Cache Autoregressive Inference

32 行 / lines
class CausalLMWithKVCache:
    """Simplified autoregressive generation with KV cache."""
    
    def __init__(self, model):
        self.model = model
    
    @torch.no_grad()
    def generate(self, prompt_ids, max_new_tokens, temperature=1.0, top_p=0.9):
        self.model.eval()
        kv_cache = None
        generated = prompt_ids.clone()  # (B, prompt_len)
        
        for step in range(max_new_tokens):
            if kv_cache is None:
                # Prefill: process entire prompt
                input_ids = generated
            else:
                # Decode: only new token(s)
                input_ids = generated[:, -1:]
            
            logits, kv_cache = self.model.forward_with_cache(input_ids, kv_cache)
            # logits: (B, seq_len, vocab_size) — take last position
            next_logits = logits[:, -1, :] / temperature
            
            next_token = top_p_sample(next_logits, p=top_p)
            generated = torch.cat([generated, next_token], dim=1)
            
            if (next_token == self.model.eos_token_id).all():
                break
        
        return generated

2.6 Top-p (Nucleus) Sampling

def top_p_sample(logits, p=0.9):
    """
    logits: (B, vocab_size)
    Returns: (B, 1) sampled token ids
    """
    probs = torch.softmax(logits, dim=-1)              # (B, V)
    sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
    cum_probs = torch.cumsum(sorted_probs, dim=-1)     # (B, V)
    
    # Mask tokens outside the nucleus
    # Shift cumsum right by 1 so the first token above threshold is included
    mask = cum_probs - sorted_probs > p                 # tokens to exclude
    sorted_probs[mask] = 0.0
    sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)  # re-normalize
    
    # Sample from the filtered distribution
    sampled = torch.multinomial(sorted_probs, num_samples=1)  # (B, 1)
    # Map back to original token ids
    return sorted_indices.gather(-1, sampled)

2.7 Causal Mask 生成

def make_causal_mask(seq_len, device):
    """
    Returns a boolean mask where True = allowed, False = masked out.
    Shape: (1, 1, seq_len, seq_len) for broadcasting with (B, H, N, N).
    """
    return torch.tril(torch.ones(seq_len, seq_len, device=device)).bool().unsqueeze(0).unsqueeze(0)

# Usage:
# mask = make_causal_mask(N, device)
# scores = scores.masked_fill(~mask, float('-inf'))
注意 / Caution

全掩码行 → softmax NaN:若某行所有位置都被填成 -\infty(如纯 padding 的 query 行,或滑窗/分块注意力中某行暂无合法 key),则 softmax\mathrm{softmax} 分母 e=0\sum e^{-\infty}=0,得 0/0=NaN0/0=\text{NaN},并经反向传播污染整个 batch。标准 causal mask 不会触发(对角线 i=ii=i 永远有效,每行至少 1 个合法 key);padding / 滑窗 / 块稀疏注意力才会。修法:保证每行至少一个有效位置,或对全掩码行用有限大负值(如 -1e9)而非 -inf,或在 softmax 后把该行重新置 0。


2.8 Simple MoE Layer

45 行 / lines
import torch
import torch.nn as nn

class SimpleMoE(nn.Module):
    def __init__(self, d_model, d_ffn, n_experts, top_k=2):
        super().__init__()
        self.n_experts = n_experts
        self.top_k = top_k
        
        # Router
        self.router = nn.Linear(d_model, n_experts, bias=False)
        # Expert FFNs (each is a 2-layer MLP)
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d_model, d_ffn, bias=False),
                nn.SiLU(),
                nn.Linear(d_ffn, d_model, bias=False),
            ) for _ in range(n_experts)
        ])
    
    def forward(self, x):
        # x: (B, N, d_model)
        B, N, D = x.shape
        x_flat = x.view(-1, D)                           # (B*N, D)
        
        # Router scores
        router_logits = self.router(x_flat)               # (B*N, E)
        router_probs = torch.softmax(router_logits, dim=-1)
        topk_probs, topk_indices = torch.topk(router_probs, self.top_k, dim=-1)
        topk_probs = topk_probs / topk_probs.sum(dim=-1, keepdim=True)  # normalize
        
        # Dispatch to experts
        output = torch.zeros_like(x_flat)                 # (B*N, D)
        for k in range(self.top_k):
            expert_idx = topk_indices[:, k]               # (B*N,)
            weight = topk_probs[:, k]                     # (B*N,)
            for e in range(self.n_experts):
                mask = (expert_idx == e)
                if mask.any():
                    expert_input = x_flat[mask]
                    expert_output = self.experts[e](expert_input)
                    output[mask] += weight[mask].unsqueeze(-1) * expert_output
        
        return output.view(B, N, D)

Note: 上述 MoE 实现为教学用途的朴素版本,实际生产代码会用 fused kernels 和 capacity-aware dispatch 来避免 Python 循环。


2.9 Transformer Block (Pre-LN Decoder-Only)

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads, d_ffn, n_kv_heads=None):
        super().__init__()
        n_kv_heads = n_kv_heads or n_heads
        self.ln1 = nn.RMSNorm(d_model)  # LLaMA uses RMSNorm
        self.attn = GroupedQueryAttention(d_model, n_heads, n_kv_heads)
        self.ln2 = nn.RMSNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ffn, bias=False),
            nn.SiLU(),
            nn.Linear(d_ffn, d_model, bias=False),
        )
    
    def forward(self, x, mask=None):
        x = x + self.attn(self.ln1(x), mask=mask)   # Pre-LN + residual
        x = x + self.ffn(self.ln2(x))                # Pre-LN + residual
        return x

Part 3 · 面试题集 (Interview Questions)


L1 · 基础 (Foundational)


Q1. Transformer decoder block 的标准结构是什么?

A: Pre-LN 结构依次为:LayerNorm → Multi-Head Causal Self-Attention → Residual → LayerNorm → FFN → Residual。其中 causal mask 将 attention score 矩阵的上三角填 -\infty,确保 token 只能看到自己及之前的 token。FFN 通常将维度扩展 4×(或 SwiGLU 的 8/3×8/3\times)再投影回来。

Follow-up: Pre-LN 和 Post-LN 各有什么优缺点?为什么当前 LLM 普遍采用 Pre-LN?

提示:Pre-LN 训练更稳定(梯度流更平滑),但有研究认为 Post-LN 在最终性能上略有优势。Pre-LN 成为主流是因为在深层模型(>100层)中 Post-LN 训练不稳定。


Q2. Self-Attention 的计算过程和复杂度是什么?

A: 输入 XXWQ,WK,WVW_Q, W_K, W_V 投影得到 Q,K,VQ, K, V;计算 softmax(QK/dk)V\text{softmax}(QK^\top / \sqrt{d_k})V。时间复杂度 O(N2d)O(N^2 d),空间复杂度 O(N2)O(N^2)(存储 attention 矩阵)。NN 为序列长度,dd 为 head dimension。N2N^2 项是长序列处理的瓶颈。

Follow-up: dk\sqrt{d_k} 缩放因子的作用是什么?省略它会导致什么问题?

提示:当 dkd_k 较大时,QKQK^\top 的元素方差约为 dkd_k,导致 softmax 饱和,梯度消失。除以 dk\sqrt{d_k} 使方差归一化到 1\sim 1


Q3. 什么是 KV Cache?为什么能加速自回归生成?

A: 在自回归生成中,每生成一个新 token 时,之前所有 token 的 K/V 向量不会改变。KV Cache 将已计算的 K/V 存储在 GPU 显存中,新 token 只需计算自己的 Q 并与缓存做 attention,避免对历史 token 的重复计算。这将 prefill 之后每个 decode step 的计算量从 O(nd)O(n \cdot d)(重新计算所有 K/V)降为 O(d)O(d)(仅计算新 token 的投影)+ O(nd)O(n \cdot d)(attention,但无需重新投影)。

Follow-up: KV Cache 的显存占用如何随序列长度和 batch size 变化?有哪些压缩方法?

提示:显存 = batch_size × seq_len × 2 × L × n_kv_heads × d_k × bytes。压缩方法包括 MQA/GQA、KV Cache 量化(FP8/INT8)、token pruning/eviction 等。


Q4. 比较 Greedy、Beam Search、Top-kk、Top-pp、Temperature 采样。

A:

  • Greedy:每步取最高概率 token,确定性,易重复退化
  • Beam Search:维护 kk 条候选,适合有确定目标的生成(翻译),open-ended 生成多样性差
  • Top-kk:只从概率前 kk 的 token 采样,kk 固定不自适应
  • Top-pp:从累积概率 p\geq p 的最小集合采样,自适应候选集大小
  • TemperatureT<1T < 1 使分布更尖锐(更确定),T>1T > 1 更平坦(更随机),常与 top-pp 联用

Follow-up: 为什么 Beam Search 在 open-ended 生成中反而比 sampling 效果差?

提示:Beam Search 优化的是序列概率,倾向于选择"安全"的高频 token,导致生成结果缺乏多样性和自然度。引入 diversity penalty 可部分缓解。


Q5. Causal Mask 是如何实现的?为什么 decoder-only 模型必须使用它?

A: 在 attention score 矩阵 SRN×NS \in \mathbb{R}^{N \times N} 中,将 j>ij > i 的位置(上三角)设为 -\infty,softmax 后这些位置权重为 0。这确保了 token ii 在计算 attention 时只能看到位置 i\leq i 的 token。Decoder-only 模型用 next-token prediction 做训练(并行计算所有位置的 loss),causal mask 保证训练时每个位置的预测不泄露未来信息,与推理时的自回归行为一致。

Follow-up: Prefix LM(如 T5 decoder、GLM)如何在同一序列中混合 bidirectional 和 causal attention?

提示:Prefix 部分的 token 之间使用双向 attention(无 mask),生成部分对 prefix 双向可见但对自身及之后使用 causal mask。


Q6. Residual Connection 在 Transformer 中的作用是什么?

A: 三个核心作用:

  1. 梯度流通:提供绕过子层的直连路径,缓解深层网络梯度消失
  2. 特征复用hl=hl1+F(hl1)h_l = h_{l-1} + F(h_{l-1}),模型可选择性地保留或修改信息
  3. 训练稳定性:与 Pre-LN 结合使得百层以上模型可稳定训练

Follow-up: 有没有工具可以分析 Transformer 各层残差流(residual stream)中的信息流动?

提示:Logit Lens 方法将中间层的 hidden state 通过 LM head 投影到词表空间,观察每层的预测分布变化。


Q7. FFN 中的维度扩展比为什么通常是 4×4\times

A: dffn=4dmodeld_{\text{ffn}} = 4d_{\text{model}} 来自原始 Transformer 论文的经验设定,没有严格的理论推导。后续实验表明该比例在多数设置下有效。SwiGLU 变体使用 83dmodel\frac{8}{3}d_{\text{model}},因有两个 gate 矩阵,总参数量与标准 4×4\times FFN 大致相当。

Follow-up: FFN 在 Transformer 中可能扮演什么样的计算角色?有没有研究将 FFN 视为"知识存储"?

提示:有研究(如 key-value memory 视角)将 FFN 的第一层视为 key 矩阵、第二层视为 value 矩阵,类比 attention 的检索机制。


L2 · 进阶 (Intermediate)


Q8. 为什么需要 Multi-Head Attention?与单 head 相比有什么优势?

A: 单 head attention 只能学习一种"关注"模式。hh 个 head 各自在不同子空间学习不同的关系模式(如句法依存、指代消解、局部 n-gram 等),concat 后通过 WOW^O 投影融合。总参数量不变(每个 head 的 dk=dmodel/hd_k = d_{\text{model}}/h),但表达能力更强。

Follow-up: 不同 head 是否真的学到了不同的模式?Head pruning 的可行性如何?

提示:Michel et al. (2019) 发现可以在推理时去掉大量 head 而性能下降很小;Voita et al. (2019) 通过 head importance 分析了不同 head 的功能分化。


Q9. MHA、MQA、GQA 的区别是什么?GQA 如何从 MHA checkpoint 做转换?

A:

  • MHAhh 个 Q head 对应 hh 套 K/V,KV Cache 最大
  • MQA:所有 Q head 共享 1 套 K/V,KV Cache 缩减 hh 倍,但可能损失质量
  • GQAhh 个 Q head 分 gg 组,每组共享 1 套 K/V,是 MHA 和 MQA 的折衷

从 MHA 转换到 GQA:将每 h/gh/g 个 K/V head 的权重取均值作为新 K/V head 初始化,再用少量数据(如原预训练量的 5%)继续训练(uptrain)。

Follow-up: LLaMA-3 使用了什么 attention 变体?GQA 的分组数 gg 如何选择?

提示:LLaMA-3 使用 GQA。gg 的选择是在推理效率和模型质量之间的 trade-off,通常通过小规模实验确定。


Q10. 绝对位置编码、RoPE、ALiBi 各自的特点和区别?

A:

  • 绝对位置编码(Sinusoidal/Learned):位置信息直接加到 embedding,最大长度固定,外推差
  • RoPE:对 Q/K 施加旋转使 attention score 仅依赖相对位置 mnm-n,支持长度外推(PI/NTK/YaRN),LLaMA 系列采用
  • ALiBi:在 attention score 上加线性偏置 mhij-m_h|i-j|,无额外参数,外推好,但 prefix caching 需动态计算偏置偏移,增加实现复杂度(部分推理框架不支持)

Follow-up: RoPE 是通过修改 embedding 还是修改 attention score 来实现相对编码的?它与绝对位置编码在数学上有什么联系?

提示:RoPE 在 embedding 层面(Q/K 上)施加旋转,但从效果看等价于在 attention score 上加了依赖相对位置的 bias。与绝对位置编码的区别在于它通过旋转不变性实现了相对位置编码。


Q11. BPE (Byte Pair Encoding) 的原理是什么?

A: 初始词表 = 所有字节/字符。迭代地统计语料中频率最高的相邻符号对,将其合并为新符号加入词表,直到达到目标大小。优点:词表大小可控,能处理 OOV。缺点:同一词可能有多种 tokenization 方案(大小写、空格变体),对非英文语言效率较低。SentencePiece 是语言无关的 BPE 实现,直接在 unicode 字节上操作。

Follow-up: Tokenizer 的 vocabulary size 对模型有什么影响?GPT-2 (50K) vs LLaMA (32K) vs LLaMA-3 (128K) 各有什么考量?

提示:影响包括 embedding 层参数量(V×dmodel|V| \times d_{\text{model}})、每 token 的信息密度、多语言和代码的 token 效率、softmax 计算开销。大词表对多语言更友好但增加显存。


Q12. PagedAttention 的核心思想和解决了什么问题?

A: 类比 OS 的虚拟内存分页:将 KV Cache 切成固定大小的 block,用 block table 管理逻辑-物理地址映射,允许非连续物理显存分配。解决了标准实现中 KV Cache 需要连续显存导致的内存碎片问题(请求长度不同导致大量内部和外部碎片),大幅提高 GPU 显存利用率。

Follow-up: Continuous batching 和 PagedAttention 是什么关系?它们分别解决什么层面的问题?

提示:Continuous batching 解决调度问题(请求级别的 iteration-level scheduling,不必等 batch 中最长序列完成);PagedAttention 解决显存管理问题(KV Cache 的高效分配)。两者互补。


Q13. MoE (Mixture of Experts) 的基本原理是什么?

A: 将 FFN 替换为 EE 个 expert FFN。每个 token 经 router(线性层 + softmax + top-kk 选择)选 kk 个 expert(通常 k=2k=2),只激活被选中的 expert 计算。总参数量 = E×E \times expert 参数(远大于 dense),但每 token 的 FLOPs ≈ (k/E)×(k/E) \times dense FLOPs(此处 dense 指等参数量的稠密 FFN)。加负载均衡 loss 防止 expert collapse(所有 token 涌向少数 expert)。

Follow-up: MoE 模型在推理和训练时的显存和计算特点有什么不同?MoE 对 tensor parallelism 有什么特殊要求?

提示:推理时每个 token 只激活 kk 个 expert,但所有 expert 参数都需加载到显存(显存需求大但计算少)。训练时需要 expert-level parallelism 或 capacity factor 控制负载。


Q14. Chinchilla Scaling Law 的核心结论是什么?

A: 给定训练 FLOPs 预算 CC,最优的模型参数量 NN 和训练 token 数 DD 应大致满足 NDN \propto D(两者均 C0.5\propto C^{0.5};实证比例约 D20ND\approx20N,即每参数约 20 tokens)。即之前的很多大模型(如 175B 参数只训练 300B tokens)属于"过度参数化、训练不足"。用更小模型训练更多数据可以获得更好的 loss per FLOP。

Follow-up: Scaling Law 在 post-training(SFT/RLHF)阶段还适用吗?

提示:目前关于 post-training 的 scaling law 研究仍在进行中。有工作探索了 SFT 数据量和模型大小的关系,但 RLHF 的 scaling 行为更复杂,受 reward model 质量、KL 约束等因素影响。


Q15. 估算一个 Transformer 模型的 KV Cache 显存占用。

A: 公式:KV Cache=2×L×nkv×dk×seq_len×batch_size×bytes\text{KV Cache} = 2 \times L \times n_{\text{kv}} \times d_k \times \text{seq\_len} \times \text{batch\_size} \times \text{bytes}

其中 factor 2 对应 K 和 V,nkvn_{\text{kv}} 是 K/V head 数(GQA 时 <h< h),dkd_k 是 head dimension。以 32 层、nkv=8n_{\text{kv}} = 8dk=128d_k = 128、BF16 为例:每 token 约 128 KB,4096 token 序列约 0.5 GB(单序列),batch 32 则约 16 GB。

Follow-up: KV Cache 量化(如 FP8/INT8)对生成质量的影响如何?有哪些实现方案?

提示:INT8 量化通常对质量影响很小,FP8 进一步减少。vLLM、TensorRT-LLM 等推理框架已内置 KV Cache 量化支持。


Q16. 什么是 Sliding Window Attention?有什么优缺点?

A: 每个 token 只 attend 到距离 W\leq W 的 token(局部窗口),复杂度 O(NW)O(NW)。多层叠加后感受野以层数线性增长(LL 层后理论感受野 L×W\approx L \times W),类比 CNN。Mistral-7B 使用 W=4096W = 4096

优点:复杂度低,推理可用 rolling buffer KV Cache(固定大小环形缓冲区)。缺点:单层内无法建立跨窗口的精确 attention,对 needle-in-haystack 类检索任务表现较弱。

Follow-up: Mistral 的 rolling buffer KV cache 是如何工作的?

提示:KV Cache 用固定大小 WW 的循环缓冲区存储,新 token 覆盖最旧的 token(位置 imodWi \bmod W),无需显存随序列增长。


Q17. Neural Scaling Laws 的基本形式是什么?

A:

L(N,D)=ANα+BDβ+LL(N, D) = \frac{A}{N^\alpha} + \frac{B}{D^\beta} + L_\infty

NN = 参数量,DD = 训练 token 数,LL_\infty = 不可约 loss(数据本身的信息熵)。α\alphaβ\beta 分别衡量参数量和数据量的边际收益递减速率。在很宽的范围内 loss 与 NN, DD 保持幂律关系。

Follow-up: 为什么 emergent abilities(如 chain-of-thought 推理)无法从 scaling law 直接预测?

提示:Scaling law 描述的是 loss(困惑度)的平滑下降;emergent abilities 是特定任务指标(如 accuracy)在某个 scale 突然从接近随机跳到远超随机,可能是指标选择的假象(Wei et al., 2022; Schaeffer et al., 2023)。


Q18. Tokenizer 的 vocabulary size 对模型能力有什么影响?

A: Embedding 层参数量 = V×dmodel|V| \times d_{\text{model}}(通常与 LM head 共享权重)。大词表:embedding 参数占比增加、softmax 计算量增加、但每 token 信息密度更高、对多语言和代码更友好。小词表:embedding 参数少、但对中文等语言需要更多 token 表示同一文本(序列更长、推理更慢)。

Follow-up: 有没有研究词表大小与模型能力的 scaling 关系?如何选择最优词表大小?

提示:词表大小的选择需要平衡多语言覆盖、推理效率和 embedding 参数开销。LLaMA-3 扩展到 128K 的主要动机是提升多语言和代码的 token 效率。


L3 · 深度 (Advanced)


Q19. FlashAttention 解决了什么问题?它是精确算法还是近似算法?

A: FlashAttention 解决标准 attention 的 memory-bound 问题——需要将 N×NN \times N 的 attention 矩阵写回 HBM。核心是 tiling(分块)+ online softmax:将 Q/K/V 分块加载到 SRAM,在 SRAM 内完成分块 attention 计算,通过 online softmax 算法在不存储完整 N×NN \times N 矩阵的情况下得到数学等价的结果。FlashAttention 是精确算法,不是近似。IO 复杂度从 O(N2)O(N^2) 降至 O(N2d2/M)O(N^2 d^2 / M)MM 为 SRAM 大小),显存从 O(N2)O(N^2) 降至 O(N)O(N)

Follow-up: Online softmax 如何在不存储完整 score 矩阵的情况下保证数学等价?

提示:Online softmax 维护 running max 和 running sum 的统计量,每处理新 block 时通过 rescaling 前面 block 的贡献来修正,最终得到与全局 softmax 完全相同的结果(需要额外的 rescaling pass)。


Q20. RoPE 长度外推的主要方法有哪些?各自的思路?

A:

  1. Position Interpolation (PI):将位置索引线性缩放 mmLtrain/Ltestm \leftarrow m \cdot L_{\text{train}} / L_{\text{test}},简单但高频信息损失
  2. NTK-aware Scaling:修改 base (10000α1000010000 \to \alpha \cdot 10000),高频维度少缩放、低频维度多缩放
  3. YaRN:NTK + 非均匀插值 + attention temperature 调整,效果更好
  4. Dynamic NTK:推理时根据实际序列长度动态调整 base,无需重新训练

核心挑战:训练时未见过的高频旋转角会导致 attention score 分布偏移,外推方法本质上是在保持已学模式和适应新长度之间找平衡。

Follow-up: YaRN 相比 naive PI 做了哪些具体改进?为什么需要 attention temperature 调整?

提示:YaRN 区分了高频和低频维度的插值策略,并加入了 attention temperature 因子 1/t1/\sqrt{t} 来补偿插值导致的 attention score 幅度变化。


Q21. Speculative Decoding 的原理是什么?为什么输出分布与直接用大模型等价?

A: 用小 draft model 并行生成 kk 个候选 token,再用大 verifier 一次性前向验证。验证时用 rejection sampling:接受概率 min(1,pverifier(x)/pdraft(x))\min(1, p_{\text{verifier}}(x) / p_{\text{draft}}(x)),拒绝时从修正分布 max(0,pvpd)\max(0, p_v - p_d) 归一化后重新采样。数学上可证明,最终每个位置的 token 分布恰好等于 pverifierp_{\text{verifier}}

吞吐提升来自:draft model 极快(小模型),且验证 kk 个 token 只需一次前向传播(并行),而正常生成需要 kk 次。

Follow-up: Draft model 和 verifier 必须同架构吗?Self-speculative decoding 是怎么做的?

提示:不需要同架构,但词表必须相同。Self-speculative decoding 用同一模型的部分层(early exit)或跳过部分层(layer skipping)作为 draft,省去额外模型。


Q22. 扩展 LLM 上下文长度的主要技术路线有哪些?

A:

  1. 位置编码外推:PI/NTK/YaRN 修改 RoPE,需少量 continue training
  2. Sliding Window Attention:局部窗口 O(NW)O(NW),多层堆叠扩大感受野
  3. Sparse Attention:Longformer/BigBird 的全局+局部+随机混合 attention
  4. Ring Attention:将长序列切分到多设备,通过 all-to-all 通信传递 KV,理论上支持无限长度
  5. Attention sink:保留初始几个 token 的 KV(StreamingLLM),解决 sliding window 的 "attention sink" 现象

Follow-up: Ring Attention 对通信带宽有什么要求?它和 Sequence Parallelism 有什么区别?

提示:Ring Attention 需要设备间 high-bandwidth interconnect(如 NVLink),通信量与窗口大小成正比。与传统 Sequence Parallelism 的区别在于 Ring Attention 的分块是序列维度的 pipeline 式传递,而非简单的数据并行。


Q23. Expert Capacity 和 Token Dropping 是什么?对训练有什么影响?

A: 每个 expert 在一个 batch 内有容量上限(capacity factor × tokens/expert)。超出容量的 token 被 drop——跳过该 expert,直接使用 residual 或 zero 输出。训练时 capacity factor 通常设 1.0–1.25;设太大会浪费计算,设太小会导致 token 丢失。

Token dropping 会引入训练噪声,影响梯度质量。缓解方法包括:增加 capacity factor(代价是计算浪费)、改进 router 设计(如 load-balancing loss)、使用 expert choice routing(expert 选 token 而非 token 选 expert)。

Follow-up: Expert choice routing 和 token choice routing 有什么区别?各自的优缺点?

提示:Token choice(标准 top-k):每个 token 选 kk 个 expert,可能导致负载不均。Expert choice:每个 expert 选 top-kk 个 token,天然负载均衡,但部分 token 可能不被任何 expert 选中。


Q24. Causal Mask + KV Cache 下的 attention 计算与训练时有何不同?

A: 训练时:对整个序列并行计算,构造 N×NN \times N 的 causal mask 矩阵,所有位置的 Q/K/V 一次性计算完成。推理时(有 KV Cache):新 token 的 Q 是 (1,d)(1, d) 向量,K/V Cache 是 (n,d)(n, d) 矩阵(nn 为已有序列长度),attention 计算为 (1,d)×(d,n)=(1,n)(1, d) \times (d, n) = (1, n),不需要显式的 causal mask(因为 K/V 中不包含未来 token)。Prefill 阶段(处理 prompt)与训练类似,使用 causal mask 并行计算。

Follow-up: 在 prefill 阶段,如果 prompt 非常长(如 100K tokens),计算瓶颈在哪里?有什么优化方法?

提示:Prefill 的瓶颈是 O(N2)O(N^2) 的 attention 计算和 O(N)O(N) 的 KV Cache 写入。优化方法包括 chunked prefill(分块处理,与 decode 请求交织)、FlashAttention 减少显存和计算、以及 prefix caching(对相同前缀的请求复用 KV Cache)。


Q25. 比较 dense Transformer 和 MoE Transformer 在 scaling 行为上的区别。

A: Dense 模型:增加参数量 = 增加计算量(FLOPs/token N\propto N)。MoE 模型:总参数量 NtotalNactiveN_{\text{total}} \gg N_{\text{active}}(激活参数),FLOPs/token Nactive\propto N_{\text{active}}。这意味着 MoE 可以在相同计算预算下拥有更大的"知识容量"。

Scaling law 角度:MoE 的 loss 主要由激活参数 NactiveN_{\text{active}} 和数据量 DD 决定(与 dense 类似),但 expert 数量增加在数据充足时仍有收益(不同 expert 可以 specialize 不同领域的知识)。不过 MoE 的 scaling 效率受 router 质量、负载均衡、expert 利用率等因素制约。

Follow-up: DeepSeek 的 MoE 设计(如 DeepSeek-V2/V3)有什么创新?共享 expert 和 routed expert 的分离有什么好处?

提示:DeepSeek-V2/V3 引入了 shared expert(所有 token 都经过)+ routed expert(按 router 选择)的设计,shared expert 负责通用能力,routed expert 负责领域知识。还有细粒度 expert 分割(更多但更小的 expert)等设计。


License: 本手册仅供学习参考,请勿用于商业用途。内容基于公开发表的研究论文和技术博客整理。

Last Updated: 2025

更多 L3 深挖 / Extended L3

Q26. FlashAttention 中的 Online Softmax 算法是如何实现的?为什么能在不存储完整 attention 矩阵的前提下得到精确结果?

Online Softmax 的核心思想是:softmax 可以通过维护一个运行中的最大值(running max)和修正因子来增量计算。将 Q、K 分成 block(tile)后,逐块计算局部 QKQK^\top 分数,每块得到局部 max mlocalm_{\text{local}};与当前全局 max mglobalm_{\text{global}} 比较后,对之前已累加的 exp\sum \exp 乘以修正因子 emglobalmnewe^{m_{\text{global}} - m_{\text{new}}},然后更新 mglobalm_{\text{global}}。这样每块的 partial sum 都可被正确缩放,最终结果与一次性计算完整 softmax 逐位一致。输出 OO 也做类似的在线加权累积。整个过程中 N×NN \times N 矩阵从未在 HBM 中完整存在,仅在 SRAM 中处理局部 tile。

追问:如果 Online Softmax 需要在处理每个 tile 时回溯修正之前累积的 OO,那么最终输出 OO 是一次性写回还是经过多次修正?FlashAttention 如何避免修正带来的额外显存写入?

Q27. 为什么 Pre-LN 比 Post-LN 在深层 Transformer 中训练更稳定?从梯度传播的角度分析。

Post-LN 中 LayerNorm 在残差之后:output=LN(x+SubLayer(x))\text{output} = \text{LN}(x + \text{SubLayer}(x))。在深层网络中,残差路径上的梯度需要穿过每一层的 LN,LN 的 Jacobian 依赖于该层激活的统计量,导致层间梯度尺度耦合。随着深度增加,梯度在早期层容易出现指数级增长或衰减(梯度爆炸/消失的变体),需要精细的 warmup 策略。Pre-LN 中 output=x+SubLayer(LN(x))\text{output} = x + \text{SubLayer}(\text{LN}(x)),LN 在子层输入端,残差路径上无 LN 阻隔,梯度可沿残差直通(类似 ResNet 的 identity shortcut),深层模型无需 warmup 即可稳定训练。代价是 Pre-LN 的最终性能可能略低于精心调参的 Post-LN。

追问:DeepNorm(微软提出的变体)在 Post-LN 基础上通过调整残差缩放系数 α\alpha 和初始化来稳定训练,其核心数学原理是什么?

Q28. MoE 中 router collapse 有哪些不同的表现形式?除辅助 loss 外还有哪些缓解方法?

Router collapse 有多种模式:(1) 完全坍缩——几乎所有 token 只选中一两个 expert,其余 expert 无梯度更新;(2) 部分不均衡——大多数 expert 被利用但少数"明星 expert"被过度使用;(3) 循环振荡——expert 利用率随训练轮次交替变化但不收敛。辅助 loss(如 Laux=αEfipi\mathcal{L}_{\text{aux}} = \alpha \cdot E \sum f_i p_i)仅是最基础的手段。其他策略包括:Expert-Choice Routing——让每个 expert 主动选 top-kk token(而非 token 选 expert),天然保证负载均衡;Random routing with noise——在 router logits 上加可调噪声防止确定性坍缩;Capacity factor 动态调整——根据训练进度自适应容量;以及对 router 参数使用更小的学习率或独立 optimizer 避免路由决策过度波动。

追问:Expert-Choice Routing 中,某些 token 可能被零个或多个 expert 同时选中,这对模型训练和推理分别有什么影响?如何处理?

Q29. PagedAttention 中的 Copy-on-Write (CoW) 和 block 共享机制如何工作?在哪些场景下利用 block 共享?

PagedAttention 给每个请求维护一个 block table(逻辑→物理页映射)。当多个请求需要共享相同的 KV Cache 前缀(如 system prompt 或 beam search 中同一 parent 序列的多个分支)时,它们可以共享相同的物理 block,避免重复存储。此时 block 的 reference count > 1。当某个请求需要修改(追加新 token)共享 block 中的内容时,触发 Copy-on-Write:分配新的物理 block,复制原 block 内容,减少原 block 引用计数,再写入新数据。这种机制在以下场景收益显著:(1) beam search 中多个 beam 共享前缀;(2) parallel sampling(同一 prompt 生成多个回答);(3) prefix caching(多轮对话复用 system prompt 的 KV)。

追问:PagedAttention 的 block size 如何选择?太小和太大分别带来什么问题?实际系统中典型的 block size 是多少?

Q30. 除 GQA/MQA 外,还有哪些 KV Cache 压缩与驱逐策略?各自的权衡是什么?

主要路线包括:(1) KV Cache 量化——将 K/V 从 FP16 量化到 INT8/INT4 甚至更低精度,减少显存但需注意 attention 精度损失,尤其是 V 的量化对输出质量影响更大;(2) Token 驱逐/淘汰——基于策略丢弃不重要的历史 token 的 KV,如 H2O(Heavy Hitter Oracle)保留注意力分数高的 token,StreamingLLM 保留"attention sink"(最初几个 token)+ 滑动窗口;(3) KV 合并——将多个相邻 token 的 K/V 向量合并为一个(如通过加权平均或 PCA),以精度换空间;(4) 层间 KV 共享——相邻层共享同一组 K/V,减少总 Cache 量。每种方法都是精度-显存-计算开销的三方权衡:量化计算开销最小但精度上限受限;驱逐策略简单但可能丢失关键信息;合并策略灵活性高但引入额外计算。

追问:StreamingLLM 观察到 attention sink 现象——即模型前几个 token 无论语义相关性如何都会获得较高的注意力分数。你认为这背后可能的原因是什么?

Q31. SwiGLU 相比标准 ReLU FFN 的优势来自哪里?gated activation 的设计动机是什么?

标准 ReLU FFN FFN(x)=W2ReLU(W1x)\text{FFN}(x) = W_2 \cdot \text{ReLU}(W_1 x) 中,ReLU 是逐元素的硬门控(输出为 0 或线性),信息通路较粗暴。SwiGLU 引入乘性门控SwiGLU(x)=(Swish(xW1)xW3)W2\text{SwiGLU}(x) = (\text{Swish}(xW_1) \odot xW_3) W_2,其中 xW3xW_3 作为 gate,\odot 为逐元素乘。这个乘性交互使 FFN 具有更丰富的特征组合能力——两个线性变换的逐元素乘积等价于一种双线性操作,能建模特征间更复杂的依赖。Swish(xσ(x)x \cdot \sigma(x))本身是光滑的非单调激活,相比 ReLU 的稀疏性提供了更平滑的梯度流。实验上,控制总参数量相同时(dffnd_{\text{ffn}}4d4d 缩减到约 83d\frac{8}{3}d,因为多了一个 gate 矩阵 W3W_3),SwiGLU 在多项 benchmark 上一致性优于 ReLU/GELU FFN。

追问:GLU 家族(包括 ReGLU、GeGLU、SwiGLU)共享同一个门控框架,为什么 SwiGLU 被选择成为主流(如 LLaMA、Mistral 系列),而非其他变体?

Q32. Speculative Decoding 的加速效果在哪些场景下会显著下降?Self-Speculative 方法的基本思路是什么?

Speculative Decoding 的加速取决于 acceptance rate(大模型接受小模型预测的比例)。加速效果下降的场景包括:(1) 分布高度发散——当 draft model 与 verifier 的分布差异大时(如不同架构家族),大量 token 被拒绝,speculation 几乎无效;(2) 高度随机的生成——temperature 较高时,正确 token 的概率分布平坦,draft model 难以猜中 verifier 的采样结果;(3) 结构化/受限输出——如 JSON 格式输出中某些位置只有少数合法 token,draft model 未必能覆盖;(4) draft model 过小——质量太低导致 E[accepted tokens]\mathbb{E}[\text{accepted tokens}] 趋近 1。Self-Speculative Decoding(如 Medusa、EAGLE)避免使用独立 draft model,而是在大模型自身上做轻量级推测:例如在最后几层后接额外的 prediction head(Medusa),或用一个小型 draft decoder 复用大模型的 hidden states(EAGLE),省去独立小模型的显存和加载开销。

追问:Medusa 方法在大模型顶层并行添加多个 prediction head,每个 head 负责预测未来第 kk 个 token。这些 head 如何训练?为什么不能简单共享一个 head 同时预测多个位置?

Q33. Post-Chinchilla 时代,为何模型"训练过度"(远超 Chinchilla 最优 token 数)反而成为主流?"Inference-Optimal" 训练范式的逻辑是什么?

Chinchilla Scaling Law 最优化的是给定 FLOPs 预算下的训练 loss。但实际部署中,训练是一次性成本,而推理是持续成本——一个模型可能服务数十亿次查询。较小模型推理时 FLOPs/decode token 更低、吞吐更高、延迟更小、部署成本更低。因此,如果用远超 Chinchilla 最优的 token 数训练一个较小模型,虽然训练 loss 不如同等 FLOPs 训练的更大模型,但该较小模型在推理侧的性价比可能显著更优——即在相同推理预算下能服务更多请求,而性能损失有限。LLaMA 系列率先验证了这一思路:用较少参数、更多训练 token 得到的模型,在推理效率上优于 Chinchilla 最优配比得到的更大模型。核心逻辑是将优化目标从"最小化训练 loss"转向"最小化推理成本下达到目标性能"。

追问:如果推理侧也在优化目标中,那么从训练 token 数、模型参数量、推理硬件约束三者的视角,如何定义一个"inference-optimal"的模型大小选择?

§A 核心论文时间线 / Key Papers Timeline