arganzheng · 2026-09-30
所有 LLM 都是同一种结构,300 行能写出来;它之后的每一处演进都在回答一个能用数字说清的问题;成本由结构、运行点、数值、方法四组变量决定——代入
config.json就能算,不必等 benchmark。
| 段 | 篇 | 方法 |
|---|---|---|
| 一 · 走通 | 01–04 | 同一份代码:d = 4 手算 → 带 KV cache 的极小 GPT → nanoGPT 训到会续写 |
| 二 · 解剖 | 05–09 | 公式 → 代入 Llama-3-8B / 70B、DeepSeek-V3 → 算出数字 |
| 三 · 算账 | 10–13 | 同一张卡 H100:80 GB、3.35 TB/s、BF16 989 TFLOPS |
%%{init: {"flowchart": {"wrappingWidth": 200}}}%%
flowchart TB
subgraph S1["一 · 走通:同一份代码"]
direction LR
P1["01 五种运算"] --> P2["02 训练 / prefill / decode"] --> P3["03 model.py 逐行"] --> P4["04 train.py 实训"]
end
subgraph S2["二 · 解剖:公式 → config.json → 数字"]
direction LR
P5["05 参数量"] --> P6["06 KV cache 与 MLA"] --> P7["07 RoPE 与长上下文"] --> P8["08 MoE"] --> P9["09 MTP"]
end
subgraph S3["三 · 算账:一条 Roofline"]
direction LR
P10["10 FLOPs、字节、ridge"] --> P11["11 浮点格式"] --> P12["12 量化 / 投机 / LoRA"] --> P13["13 多模态"]
end
S1 --> S2 --> S3
结论:embedding 查表 → attention(唯一让 token 互相看的地方) → FFN(逐 token 的非线性,知识在这,占一层 2/3)→ 残差 + LayerNorm → lm_head;attention 是集合运算不知道顺序,位置必须显式给。
%%{init: {"flowchart": {"wrappingWidth": 330}}}%%
flowchart LR
IN["token 编号<br/>[464, 3797, 3332, 319, 262]"] --> EMB["embedding 查表<br/>50257 × 768 → 5 个 768 维向量<br/>+ 位置表"]
EMB --> B
subgraph B["× 12 个 block"]
direction TB
A["LayerNorm → causal self-attention → 加回"]
F["LayerNorm → FFN 768→3072→768 → 加回"]
A --> F
end
B --> HEAD["LayerNorm → lm_head 768 × 50257<br/>softmax → p(mat) = 0.31"]
结论:causal mask + teacher forcing 让一句话 = T 个训练样本;推理时旧 token 的 K、V 不变,算一次存下来就是 KV cache——训练 / prefill / decode 是三种形态。
| 形态 | 输入 | 有反向 | 存什么 | 瓶颈 |
|---|---|---|---|---|
| 训练 | [B, T] 全位置有用 | 是 | 激活值 | 算力 |
| prefill | [B, T] 只取最后 | 否 | K、V | 算力(TTFT) |
| decode | [B, 1] | 否 | 追加 K、V | 访存(TPOT) |
结论:330 行、6 个类,结构本身(LayerNorm、CausalSelfAttention、MLP、Block)不到 90 行;与 HF GPT-2 对拍相对差 (9 \times 10^{-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 数学等价,合并只为一次 GEMM结论:1.1 MB 莎士比亚、0.8M 参数、2000 步 7 分钟,loss 从 ln 65 = 4.17 → 1.66;层是串行的——参数与耗时线性、loss 收益递减。
%%{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 右移一位,没有 epoch结论:dense Transformer 没有隐藏参数——每层四个 attention 矩阵 + 三个 SwiGLU 矩阵,乘层数加词表,Llama-3-8B 算出 8,030,261,248,精确到个位。
[ N = L\big[d(2d + 2d_{kv}) + 3d \cdot d_{ff} + 2d\big] + 2Vd + d,\qquad d_{kv} = n_{kv}\,d_{head} ]
| Llama-3-8B | 每层 | × 32 层 | 占比 |
|---|---|---|---|
| attention(Q、O 各 d²,K、V 各 d·d_kv) | 41.94M | 1.34B | 17% |
| SwiGLU 三矩阵 3 × 4096 × 14336 | 176.16M | 5.64B | 70% |
| embedding + lm_head(不共享) | — | 2 × 525.3M | 13% |
| 错法 | 偏差 |
|---|---|
| 用 4d = 16384 当 d_ff | +10% |
| SwiGLU 按两个矩阵算 | −23% |
| 漏掉不共享的 lm_head | −6.5% |
| K、V 按 MHA 算(n_h 而非 n_kv) | +10% |
| 把 embedding 也乘 2 算进 FLOPs | +7% |
结论:KV cache 每 token = (2\,L\,n_{kv}\,d_{head} \cdot) bytes/elem,公式里没有 (n_h)——V3 128 头 61 层的 KV(68.6 KiB)比 8B 32 头(128 KiB)还小。
| 模型 | 结构 | KV / token | 128K 上下文 | decode attention 强度 |
|---|---|---|---|---|
| 8B 若用 MHA | 32 KV 头 | 512 KiB | 64 GiB | 1 |
| Llama-3-8B | GQA,8 KV 头(g = 4) | 128 KiB | 16 GiB | 4 |
| Llama-3-70B | GQA,8 KV 头(g = 8) | 320 KiB | 40 GiB | 8 |
| DeepSeek-V3 | MLA,512 + 64 维 latent | 68.6 KiB | 8.6 GiB | 242 |
| V3 若用 MHA | 128 头 × 128 | 3.81 MiB | 477 GiB | 1 |
%%{init: {"flowchart": {"wrappingWidth": 260}}}%%
flowchart LR
H["h_t (7168)"] --> DKV["W_DKV + W_KR<br/>7168 → 576"]
DKV --> CKV["c^KV_t (512)<br/>进 cache"]
DKV --> KR["k^R_t (64),RoPE<br/>128 个 head 共享,进 cache"]
CKV --> UKV["W_UK / W_UV 升维<br/>512 → 128 head × 128"]
UKV --> ATT["attention<br/>q·[k^C ; k^R] / √192"]
KR --> ATT
H --> Q["W_DQ → W_UQ + W_QR<br/>q^C 128 维 + q^R 64 维"] --> ATT
ATT --> WO["W_O 16384 → 7168"]
classDef cache fill:#fde68a,stroke:#b45309,stroke-width:2px,stroke-dasharray:5 3
class CKV,KR cache
结论:RoPE 把 (d_{head}) 维向量看成 64 对复数,每对以自己的波长旋转;8K 训练时 14 对低频维度没转完一圈——推到 32K 出现从未见过的相位。不是装不下,是没见过。
| Llama-3-8B | Llama-3-70B | |
|---|---|---|
| attention = 权重 FLOPs 的交叉点 | 约 28.6K | 约 53.8K |
| 128K 时 attention / 权重 FLOPs | 68.7 G / 15.0 G = 4.6 倍 | 344 G / 141 G |
| 128K prefill(60% MFU) | 6.5 PFLOP,约 11 s | 41 PFLOP,约 69 s |
| 128K 一条请求的 KV | 16 GiB | 40 GiB |
结论:总参数定显存(671B,FP8 也放不进 8 卡 640 GB)、激活参数定 FLOPs(37B,是 70B 的一半)、每步实际读取的参数定 decode 带宽——中等 batch 下几乎读全部专家,稀疏省了算量没省访存。
| DeepSeek-V3,256 专家取 top-8 | B = 1 | B = 32 | B = 128 |
|---|---|---|---|
| 期望激活专家数 (E[1 - (1 - k/E)^B]) | 8 | 163 | 252 |
| 每步读取的专家参数(FP8) | 37 GB | 434 GB | 660 GB |
| 对比 dense 70B(BF16) | 141 GB | 141 GB | 141 GB |
%%{init: {"flowchart": {"wrappingWidth": 240}}}%%
flowchart LR
TOK["token x(d 维)"] --> RT["router W_r x<br/>E 个分数 → top-k(k = 8)"]
RT -->|"g_1"| E1["FFN_1 选中"]
RT -.->|"g_2 = 0"| E2["FFN_2 不计算"]
RT -->|"g_E"| EE["… FFN_E 选中"]
TOK --> SH["共享专家<br/>不经 router"]
E1 & EE & SH --> SUM["y = Σ g_i · FFN_i(x) + FFN_shared(x)<br/>算了 k + 1 个,持有 E + 1 个"]
classDef active fill:#dff0d8,stroke:#3c763d
classDef idle fill:#f5f5f5,stroke:#aaa,color:#888
classDef sh fill:#fcf8e3,stroke:#8a6d3b
class E1,EE active
class E2 idle
class SH sh
结论:每个位置额外预测 (t_{i+2}),信号密度 ×(1 + D),主干被逼编码更远的未来;模块顺序喂真实 (t_{i+1}) 保持因果链(teacher forcing 的延伸);推理时丢弃或当投机 draft。
%%{init: {"flowchart": {"wrappingWidth": 220}}}%%
flowchart LR
T["主干 Transformer<br/>h_i"] --> H0["lm_head → 预测 t_{i+1}<br/>L_main"]
T --> C["RMSNorm(h_i) ‖ RMSNorm(emb(t_{i+1}))<br/>投影 2d → d"]
E["真实 t_{i+1}"] --> C
C --> BLK["一个 Transformer block"] --> H1["共享 lm_head → 预测 t_{i+2}<br/>λ · L_MTP"]
classDef mtp fill:#e0e7ff,stroke:#4338ca
class C,BLK,H1 mtp
| DeepSeek-V3(D = 1) | nanoGPT 0.8M 实验 | |
|---|---|---|
| 额外参数 | 一个 block + 2d²,< 2% | +29% |
| 每步耗时 | — | +38% |
| 主任务 | 大规模上有收益(13B 以上明显) | 1.9375 对 1.9364,不变 |
| MTP 头 | 当投机 draft:接受率 85–90%、TPS 约 1.8 倍 | t+2 命中 45%(知道 t+1 且多一层) |
结论:每参数每 token 2 FLOPs;decode 每步把 16 GB 权重读一遍、算术强度在数值上就等于 B,ridge 是 989 / 3.35 ≈ 295——这就是「decode 是 memory-bound」的全部含义。
| 量 | 数 |
|---|---|
| 每 token 权重 FLOPs | 2 × 7.5B = 15.0 G;attention 8K 4.3 G、128K 68.7 G |
| decode B = 1 | 读 16.06 GB / 3.35 TB/s = 4.8 ms、208 token/s;算力时间 0.02 ms |
| 要 compute-bound | B ≈ 295,但 8K 下这些请求的 KV 要 295 GiB,剩 64 GB 放不下 |
| KV 读取强度 | 常数 g = 4,不随 B 摊薄;总强度趋于 18 → 单卡任何可行 batch 都 memory-bound |
| 真正的约束 | B × s ≤ 52 万 token(64 GB / 128 KiB) |
| prefill 8K | 峰值 0.14–0.16 s 是物理下限,60% MFU 约 0.25 s |
结论:前向反向要范围、权重更新要精度——所以 BF16 计算 + FP32 master weights;(\Delta w / w \sim 10^{-4}) 低于 BF16 的单位舍入 (2^{-8} = 0.004),1.0 + 0.001 会被舍回 1.0。
| 位置 | 例子 | 对策 |
|---|---|---|
| 大数吃小数 | 1.0 + 0.001 in BF16 | FP32 master weights |
| 长求和 | k = 4096 的点积在 BF16 累加噪声 ε√k ≈ 25% | FP32 累加器 |
| 指数溢出 | softmax 的 e^x,x > 11 在 FP16 溢出 | 减最大值 |
| 相消 | 方差 = E[x²] − E[x]² | Welford |
结论:都不改结构,各改一个变量——量化改 (W_{bytes})、投机改每步的 m、LoRA 改训练时的 N;前两者都在兑现 memory-bound 区间里空转的算力,过 ridge 收益同时消失。
| 方法 | 改的变量 | 8B 上的数 | 兑现条件 |
|---|---|---|---|
| INT4 g128 量化 | (W_{bytes}):4.25 bit | 16 GB → 4.27 GB,4.8 → 1.27 ms | decode 且 B ≲ ridge/4 ≈ 79;prefill 反而多反量化 |
| 投机解码 | 每步的 m:验证 γ + 1 个 | α = 0.8、γ = 4:期望 3.36 token,加速 2.4 倍 | B ≲ ridge/(γ+1) ≈ 60;输出分布严格不变 |
| LoRA | 训练时的可训练 N | 41.9M(0.52%),状态 128 GB → 16.7 GB | 省的是 16 B/参数的状态;激活不变、反向仍穿过每层(约 4N) |
结论:图片贵的不是 encoder(一次性、compute-bound、12 ms),是它变成的 token 在 decoder 里占的 KV——与同长文本同价、活到请求结束;1024² 在 Qwen2-VL 里 = 1369 个 token,一段无法被 tokenizer 压短的 system prompt。
| 段 | 账 | 数 |
|---|---|---|
| vision encoder(0.63B ViT,5476 patch) | (2N_{vit}n_p + 4L_{vit}n_p^2 d_{vit}) | 11.8 TFLOP,attention 二次项占 42%;一次性 |
| connector | 决定 token 数 | MLP 不压缩 576 · 2×2 merge 1369 · resampler 定长;同一张图 576–6404 差 11 倍 |
| decoder | prefill + KV | 193 TFLOP;KV 428 MiB,是 encoder 输出 21 MiB 的 20 倍 |
%%{init: {"flowchart": {"wrappingWidth": 260}}}%%
flowchart TB
S["结构:L、d、d_ff、n_kv、E、k<br/>(01、03、05)"] --> N["参数量 N、激活参数量(05、08)"]
S --> K["KV / token = 2 L n_kv d_head · bytes/elem(06)"]
D["数值:bytes/param、bytes/elem(11、12)"] --> W["权重字节 = N · bytes/param"]
N --> W
D --> K
N --> F["FLOPs / token ≈ 2N + 4dLs(10、07)"]
R["运行点:B、s、prefill / decode(02、07、08、10)"] --> F
R --> H["每步字节 = 权重字节 + B · s · KV/token"]
K --> H
W --> H
F --> I["算术强度 I = FLOPs / 字节"]
H --> I
I --> T["与 ridge ≈ 295 比较 → memory / compute-bound → 时间下界"]
| 线 | 落点 |
|---|---|
| 一份代码 | 01 六步手算 → 02 极小 GPT → 03 nanoGPT → 04 训起来 → 06/07/08 在它上改 GQA、RoPE、MoE → 09 挂 MTP |
| 一条 Roofline | (I_{weight} = B)、(I_{KV} = g);MLA 把 g 抬到 242;MoE 专家强度 Tk/E;量化转折 ridge/4、投机 ridge/(γ+1) |
| KV 与上下文 | 128 KiB/token 打开成四个乘子;B × s ≤ 显存 / KV;长上下文贵在 HBM 字节数不在调度 |
| 「参数量」拆成几个数 | N → N_gemm 7.5B → 总 / 激活 / 每步读取 → bytes/param 16 与 4.25 bit → 可训练 0.52% |
| 字节里存了什么 | E4M3 / E5M2 分工、128 分块 scale、FP8 KV、FP8 dispatch;数值决定结构细节 |
| 篇 | 一个公式 / 一个数 |
|---|---|
| 01 | (\text{softmax}(QK^\top/\sqrt d + M)V);GPT-2 small 124,439,808 |
| 02 | 初始 loss = ln V;KV 128 KiB / token;KV cache 快 7.9 倍 |
| 03 · 04 | x = x + attn(ln_1(x));ln 65 = 4.17 → 1.66,7 分钟 |
| 05 | (N = L[d(2d + 2d_{kv}) + 3d\,d_{ff} + 2d] + 2Vd + d) = 8,030,261,248 |
| 06 | KV/token = (2Ln_{kv}d_{head}\cdot)bytes;MLA 68.6 KiB、强度 242 |
| 07 | (\lambda_i = 2\pi\cdot\text{base}^{2i/d_{head}});交叉点 8B 28.6K;128K prefill 11 s |
| 08 | (E[1-(1-k/E)^B]):B = 32 → 163 个专家、434 GB |
| 09 | (\mathcal L_{main} + \lambda\bar{\mathcal L}_{MTP});接受率 85–90%、1.8 倍 |
| 10 | ridge = 989 / 3.35 ≈ 295;(I_{weight} = B)、(I_{KV} = g);6ND |
| 11 | BF16 单位舍入 2⁻⁸;16 B / 参数;ε√k |
| 12 | (T(m) = \max(W_{bytes}/BW,\ 2Nm/F));((1-\alpha^{\gamma+1})/(1-\alpha)) |
| 13 | (n_{img} = \lceil H/28\rceil\lceil W/28\rceil);KV / encoder 输出 = 20 |
/transformer-and-llm-for-infra-engineers.html;通关自测 27 题在系列总结