Hang Zhengyang

LLM 基础(概念版):从“为什么”到“怎么跑在 mini-LLM 里”

1. 先定主问题:模型到底在学什么

Decoder-only Transformer 在学的是条件分布:

p(xt+1∣x≤t)

概念上,它不是在“背答案”,而是在每个位置做“下一词预测”。
训练时不断修正参数,让正确词的概率更高,错误词的概率更低。

在本项目的 Which-Span 任务里,序列有结构 token([SA] [EA] [SB] [EB] [QA]/[QB]),监督主要集中在答案区间,这让模型把能力聚焦在“根据问题挑 span”。

2. 从 token 到语义向量:为什么必须有 Embedding

token id 本质是离散编号,网络不能直接对“编号大小”做语义计算。
Embedding 把离散 id 映射到连续向量空间:

E∈ℝV×D,xi↦Ei

直觉:

  • id 7 和 8 的数值接近,不代表语义接近;
  • 经过 embedding 后,“语义相近 token”可被训练到向量上更接近;
  • 后续所有线性层/attention 都在这个连续空间里运算。

代码对应:

  • weights.token_embedding
  • weights.output_projection
  • src/engine/embedding.cpp

3. 一层 Decoder 在做什么(过程视角)

把一层看成四步最清晰:

  1. 归一化后做 QKV(并加 RoPE)
  2. attention 聚合上下文 + 输出投影 + 残差
  3. 再归一化后过 SwiGLU MLP
  4. down projection + 残差

这和 src/model/executor_forward.cpp 的执行块一一对应。

4. Attention 为什么是“信息路由”

4.1 公式

Q=XWQ,K=XWK,V=XWV
A=softmax(QK⊤Dh+M)
Y=AV

4.2 概念解释(重点)

  • Q(query):当前位置“想找什么信息”。
  • K(key):每个历史位置“我能被怎样匹配”。
  • V(value):每个历史位置“我提供的内容向量”。

所以注意力不是简单平均,而是:

  1. 先用 QK⊤ 计算“匹配分数”;
  2. softmax 把分数变成概率权重;
  3. 用这些权重对 V 做加权和,得到上下文表示。

这正是“信息路由”:当前 token 动态决定从哪些历史 token 取信息、取多少信息。

4.3 为什么要除以 Dh

若不缩放,QK⊤ 的方差会随维度增大而变大,softmax 容易过饱和(接近 one-hot),梯度变差。
除以 Dh 是把分数拉回更稳定区间,便于训练。

4.4 因果 mask 在保证什么

M 把未来位置置为极小值,softmax 后近似 0。
这保证第 t 个位置只能看 ≤t 的 token,符合自回归生成定义,避免训练-推理不一致。

5. RoPE 为什么对“相对位置”有效

RoPE 对每个二维子空间做旋转(只展示单对维度):

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

直觉不是“给向量加位置”,而是“按位置旋转向量方向”。
当 Qp 和 Kq 都做了对应旋转后,它们的内积会主要依赖位置差 (p−q) 的结构,从而自然编码相对位置信息。

为什么这对推理友好:

  • decode 时每次只新增一个位置,旋转可独立算;
  • 与 KV cache 天然兼容,不需要重写整段历史表示。

本项目里通常只旋转前 rope_dim 维,是计算和效果之间的折中。

6. GQA 在解决什么工程问题

维度关系:

  • query 头数:H
  • key/value 头数:Hkv,且常见 Hkv<H
  • 约束:D=H·Dh

GQA 的本质是“多个 query 头共享较少的 KV 头”。
这会降低 KV cache 占用和内存带宽压力,对长上下文 decode 很关键。

代价是表示自由度有一定下降,所以是典型的“速度/内存/容量”折中。

7. 为什么是 Pre-Norm + 残差(而不是 Post-Norm)

本项目采用:

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

7.1 残差在做什么

残差提供一条“恒等捷径”,即使子层学得不好,信息和梯度仍可直接通过主干。
这让深层网络更不容易梯度消失。

7.2 Pre-Norm 为什么常更稳

先 norm 再进子层,意味着每层子模块看到的输入尺度更可控。
训练早期参数剧烈变化时,这能减少数值抖动,通常比 post-norm 更容易稳定收敛。

8. RMSNorm 的角色:控制尺度,不改语义方向

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

概念上它主要做两件事:

  1. 统一不同 token 激活的尺度;
  2. 通过 γ 保留可学习重标定能力。

它不像 LayerNorm 那样减均值,计算更轻,在 Transformer 中通常足够稳定。

9. SwiGLU MLP 在补什么能力

G=Y1Wg+bg,U=Y1Wu+bu
H=SiLU(G)⊙U,MLPOut=HWd+bd

直觉:

  • U 是“内容流”;
  • SiLU(G) 是“软门控”;
  • 逐元素乘法决定每个通道放行多少内容。

相比普通 Linear -> 激活 -> Linear,门控机制对信息选择更细粒度,通常更有表达力。

10. 形状一致性为什么是第一性原则

在实现里,很多 bug 不是“公式错”,而是“维度契约被破坏”。
常见硬约束:

  • D=H·Dh
  • Q 的头数是 H
  • K,V 的头数是 Hkv
  • o_proj 要把多头拼接后再映射回 D

本项目通过 engine/validation.* 和模型构造期检查守住这些边界。

11. 参数量怎么读才有意义

近似写法:

total≈L·params\_per\_layer+2VD

意义不是背数字,而是理解“参数都花在哪”:

  • 层数 L 决定深度容量;
  • 每层 attention+mlp 决定主计算量;
  • 2VD 来自输入 embedding 和输出投影(本项目分开存)。

12. 从“公式”映射到“源码”

  • 类型与权重:src/model/model_types.h
  • 前向编排:src/model/executor_forward.cpp
  • 反向编排:src/model/executor_backward.cpp
  • 模型级 API:src/model/mini_llm.cpp
  • 具体算子:src/kernel/*

建议阅读顺序:

  1. 先按本文件理解“每块在解决什么问题”;
  2. 再进 executor_forward.cpp 对照执行顺序;
  3. 最后看 backward,建立“前向块 -> 反向块”的镜像关系。