Hang Zhengyang
English ↗

组件深讲(概念版):每个模块到底解决什么问题

1. Attention:为什么它是“信息路由器”

1.1 先问问题

给定当前 token,模型要决定:
“我应该从历史的哪些 token 取信息?每个取多少?”

Attention 就是这套动态路由机制。

1.2 机制与公式

给定

Q∈ℝTq×Dh,K∈ℝTk×Dh,V∈ℝTk×Dh

有

S=QK⊤Dh,A=softmax(S+M),Y=AV

解释:

  • S:匹配分数(query 与 key 的相似度)。
  • A:每行是概率分配(该看哪些位置)。
  • Y:按概率把 value 聚合成上下文表示。

这就是“信息路由”:由当前查询动态决定读取历史的路径和权重。

1.3 为什么要缩放 Dh

不缩放时,维度越高,打分方差越大,softmax 更容易饱和,梯度变差。
缩放把分数控制在更稳定区间,训练更平稳。

1.4 因果 mask 的意义

M 保证位置 t 不能看到未来 >t 的 token。
这不是小细节,而是自回归定义本身;否则训练与推理行为会不一致。

1.5 代码定位

  • Q/K/V 与投影:src/model/executor_forward.cpp
  • 注意力核:src/kernel/attention.cpp、src/kernel/attention_accelerate.cpp

2. GQA:为什么要让 KV 头更少

2.1 先问问题

长上下文推理时,瓶颈往往是 KV cache 的内存和带宽。
如何在质量损失可控的前提下降低这部分成本?

2.2 核心思想

让 query 头数 H 保持较大,但 key/value 头数降为 Hkv(常见 Hkv<H)。
等价于“多个 query 头共享一组 KV 头”。

2.3 效果与代价

  • 收益:KV cache 内存与带宽压力明显下降(近似随 Hkv 下降)。
  • 代价:表达自由度受限,极端设置可能影响质量。

它本质是工程折中:速度/内存 vs 容量。

2.4 代码定位

  • 配置字段:src/model/model_types.h
  • 前向展开逻辑:src/model/executor_forward.cpp

3. RoPE:为什么它天然携带相对位置信息

3.1 先问问题

模型要区分“谁在前谁在后”,并且希望在长长度外推时仍保持稳定。
RoPE 提供的是一种“旋转式位置编码”。

3.2 机制与公式

对每个二维子空间做旋转:

[u2k′u2k+1′]=[cosθp,k−sinθp,ksinθp,kcosθp,k][u2ku2k+1]

关键点:

  • 不是“加一个位置向量”,而是“按位置旋转方向”;
  • 当 Q/K 都按位置旋转后,它们内积对相对位置差更敏感;
  • decode 时每步可独立计算,天然适配 KV cache。

3.3 本项目折中

rope_dim < head_dim 表示只旋转部分维度,兼顾效果和开销。

3.4 代码定位

  • RoPE 缓存与应用:src/kernel/core.cpp、src/kernel/core_accelerate.cpp
  • 前向调用点:src/model/executor_forward.cpp

4. RMSNorm:为什么只做“尺度归一化”也够用

4.1 先问问题

深层网络常因激活尺度漂移导致训练不稳。
我们需要一种便宜、稳定的归一化。

4.2 公式

RMSNorm(x)=γ⊙x1D∑i=1Dxi2+ϵ

4.3 概念解释

  • 分母是 RMS(均方根),把向量缩放到稳定幅值。
  • γ 允许每个通道再学习性重标定。
  • 不减均值(相比 LayerNorm 更轻量),在 Transformer 中通常足够稳。

4.4 代码定位

  • 算子与反向:src/kernel/core.cpp、src/kernel/norm_rope.cpp、src/kernel/attention.cpp(IO 端反向算子已迁入 src/engine/decode.cpp / src/engine/embedding.cpp,详见 §2.1)
  • 在 block 中的位置:src/model/executor_forward.cpp

5. SwiGLU:为什么比普通 FFN 更有选择性

5.1 先问问题

普通 FFN 会放大特征,但“选择性”不够细。
SwiGLU 用门控机制让网络决定“哪些信息该通过”。

5.2 公式

g=XWg,u=XWu,h=SiLU(g)⊙u,y=hWd
SiLU(z)=z·σ(z)

5.3 概念解释

  • u 是内容支路,g 是门控支路;
  • SiLU(g) 作为软门,逐通道调节 u 的通过量;
  • 相比普通 FFN,门控能更细粒度控制信息流。

5.4 代码定位

  • 线性与激活组合:src/model/executor_forward.cpp
  • 底层算子:src/kernel/core.cpp

6. Pre-Norm + 残差:为什么训练更稳

本项目层结构是:

Y1=X+Attn(Norm(X)),Y2=Y1+MLP(Norm(Y1))

解释:

  • 残差提供短路径,避免梯度在深层被完全扭曲。
  • Pre-Norm 让子层输入尺度更可控,训练初期更稳。

这也是当前 LLM 常见选择。

7. 组件级调试:每个模块最常见的错

  • Attention:mask 维度不对、softmax 数值饱和。
  • RoPE:位置索引错、旋转维度切分错。
  • RMSNorm:ϵ 太小导致不稳定。
  • SwiGLU:门控分支梯度或广播维度错误。

建议先做三类检查:shape、finite(无 NaN/Inf)、边界 case(空/短序列)。

8. 在这个仓库做“单组件实验”的标准流程

  1. 固定 seed 与数据采样。
  2. 每次只改一个变量(如 num_kv_heads、rope_dim)。
  3. 统一比较三组指标:
    • 训练 loss/accuracy;
    • 任务指标(exact match);
    • 推理耗时(prefill/decode)。

这样才能把“数学改动”与“工程效果”建立清晰因果关系。