arganzheng · 2026-04-09
Transformer 的结构可以从五种运算读懂;沿着一个 token 的旅程,再把同一套结构写成代码并训练起来。
| 篇 | 内容 |
|---|---|
| 01–04 | 静态结构 → token 旅程 → nanoGPT model.py → train.py 与实际训练 |
flowchart LR
P1["01 静态结构"] --> P2["02 token 的旅程"] --> P3["03 model.py"] --> P4["04 train.py"]
先看本系列要拆解的 GPT-2 small 总览;接着回到 2017 年的论文,解释 encoder-decoder、三条路线与 GPT-2 的来历。
GPT-2 走 decoder-only;attention 不知道顺序,位置必须显式给
encoder 双向读源句、decoder causal 生成、cross-attention 读 encoder 输出;GPT-2 走 decoder-only。
训练 / prefill / decode 是三种形态——KV cache = 算一次存下来
| 形态 | 输入 | 有反向 | 存什么 | 瓶颈 |
|---|---|---|---|---|
| 训练 | [B, T] 全位置有用 | 是 | 激活值 | 算力 |
| prefill | [B, T] 只取最后 | 否 | K、V | 算力(TTFT) |
| decode | [B, 1] | 否 | 追加 K、V | 访存(TPOT) |
结构本身不到 90 行;对拍 HF GPT-2 相对差 9e-5
class Block(nn.Module):
def forward(self, x):
x = x + self.attn(self.ln_1(x)) # pre-norm:先 norm 再子层,残差直连
x = x + self.mlp(self.ln_2(x))
return x
# CausalSelfAttention.forward 的骨架
q, k, v = self.c_attn(x).split(self.n_embd, dim=2) # 一次 GEMM 算 QKV
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # 头换到第 1 维
y = F.scaled_dot_product_attention(q, k, v, is_causal=True) # 融合 kernel
y = y.transpose(1, 2).contiguous().view(B, T, C) # 拼回去
| 类 | 行数 | 实现的是 |
|---|---|---|
LayerNorm |
10 | 可选 bias(GPT-2 有、Llama 无) |
CausalSelfAttention |
40 | c_attn 一次算 QKV · 拆头 · SDPA · c_proj |
MLP |
12 | 768 → 3072 → GELU → 768 |
Block |
10 | 上面两行 pre-norm |
GPT |
200+ | 拼结构、共享权重、初始化、generate、from_pretrained、configure_optimizers、estimate_mfu |
c_attn 数学等价,合并只为一次 GEMM0.8M 参数 2000 步 7 分钟,loss 4.17 → 1.66
%%{init: {"flowchart": {"wrappingWidth": 170}}}%%
flowchart LR
LR["设 lr<br/>warmup + cosine"] --> EV["到点评估<br/>存 checkpoint 五样"] --> ACC["k 个 micro-batch<br/>loss ÷ k 累积"] --> CL["unscale<br/>+ 梯度裁剪"] --> ST["optimizer.step()<br/>zero_grad()"] --> LR
get_batch:memmap + 随机窗口 + stack,y 是 x 右移一位,没有 epochmodel、optimizer、model_args、iter_num、config——续训不带优化器状态,前几百步抖