把基础打牢
这一部讲四件事:transformer 长什么样、推理为什么分两阶段、roofline 怎么画、 显存里到底装了什么。后续所有 part 都建立在这四个概念之上—— 没看过的人请按顺序读,老手可以跳到 §1.3 看 roofline 那张图。
- §1.1Transformer 速通
- §1.2Prefill vs Decode
- §1.3Roofline · 哪里慢
- §1.4显存账单
§1.1Transformer 一口气速通
我们要加速的"宿主"是一个 transformer decoder(GPT 系列、Llama 系列、Qwen 系列 全都一样)。一个 transformer block 长这样——把它背下来,后面所有 trick 本质上都是改这八步:
# x: [B, S, D] D = hidden dim, S = 当前序列长度
# 一层 transformer block 的前向,省略 dropout / 残差缩放
def block(x, theta):
h = rmsnorm(x, theta.norm1)
q = h @ theta.Wq # [B, S, D]
k = h @ theta.Wk # [B, S, D]
v = h @ theta.Wv # [B, S, D]
# reshape into heads: [B, n_heads, S, head_dim]
q, k, v = split_heads(q, k, v)
# 加 rotary / ALiBi 等位置编码
q, k = apply_rope(q, k)
# 因果 attention
attn = softmax(q @ k.transpose(-1,-2) / sqrt(head_dim) + causal_mask) @ v
attn = merge_heads(attn)
y = x + attn @ theta.Wo # 残差
z = y + ffn(rmsnorm(y, theta.norm2), theta.W_up, theta.W_down)
return z
生成阶段把 $L$ 层 block 像穿糖葫芦一样串起来,最后一层接一个
x @ theta.W_emb.T 得到 vocab 上的 logits,再 sample 一个新 token,
追加到输入末尾,循环——这就是自回归。
几个关键尺寸(以 Llama-3-70B 为例)
| 符号 | 含义 | 典型值 |
|---|---|---|
| $L$ | 层数 | 80 |
| $D$ | hidden 维度 | 8192 |
| $H_q$ / $H_{kv}$ | query / KV head 数 | 64 / 8 (GQA) |
| $d_h$ | head_dim | 128 |
| $d_{ff}$ | FFN 中间维度 | 28672 |
| $V$ | 词表 | 128256 |
形状直觉 · 一个 token 在网络里走一圈要碰几次矩阵
把一个新 token 的 hidden state $h \in \mathbb{R}^D$ 经过一层 block,需要:
- 3 次 $D \to D$ 的投影:$W_q, W_k, W_v$;
- 1 次 $D \to D$ 的输出投影:$W_o$;
- 2 次 attention matmul:$qK^\top$ 和 $\text{softmax}(...)V$;
- FFN:1 次 $D \to d_{ff}$、1 次 $d_{ff} \to D$(SwiGLU 实际是 3 次)。
乘上 $L = 80$ 层,每个 token 大约要碰 $\;80 \times (4 \cdot D^2 + 2 \cdot D \cdot d_{ff}) \approx 70\text{ GFLOP}$。 在 H100 上算力 989 TFLOPS,理论 14 µs / token—— 但实际 decode 每 token 要 ~5 ms。差了 350×。 这 350× 跑哪去了?§1.2–§1.3 揭晓。
Diffusion / DiT 几乎就是把这个 block 改成双向 attention(无 causal mask)、外加 cross-attention 接 text 条件;VLM 是把图像也 patch 化、 当成几百到几千个 token,prepend 到 LLM 输入;VLA 是 把动作离散化成 token、自回归生成;world model 是用 causal video DiT 把"下一 帧"当成"下一个 token"。表达上完全统一—— 这就是为什么本站敢把这五类放进同一份综述。
§1.2推理的两阶段 · Prefill vs Decode
把推理拆开看,自回归 LLM 有两个截然不同的工作模式:
| Prefill | Decode | |
|---|---|---|
| 什么时候发生 | 给定 prompt 的第一次前向 | 之后每生成一个 token |
| 每次喂入 token 数 | 整段 prompt(几十到几百万) | 恰好 1 |
| 矩阵形状 | $[B, S, D] \times [D, D]$ → 大 matmul | $[B, 1, D] \times [D, D]$ → "GEMV" |
| 每个权重被复用几次 | $S$ 次 | 1 次 |
| 瓶颈 | tensor core 算力 | HBM 带宽 |
| 需要做的 | 把 KV 算出来缓存好 | 读 + 增长 KV cache,做一行 attn |
# Prefill: 一次把整段 prompt 喂进网络
def prefill(prompt_ids, model):
x = embed(prompt_ids) # [B, S, D]
for layer in model.layers:
x, kv = layer(x, kv_cache=None)
save_to_kv_cache(layer.id, kv) # 把 K/V 存起来
return x[:, -1] # 只用最后一个位置算 logits
# Decode: 一次只喂一个 token,复用历史 KV
def decode_step(last_token_id, model):
x = embed(last_token_id) # [B, 1, D]
for layer in model.layers:
kv_past = load_from_kv_cache(layer.id) # 把全部历史 KV 读回来
x, kv_new = layer(x, kv_past=kv_past)
append_to_kv_cache(layer.id, kv_new) # 多挂一个 token 的 K/V
return sample(x[:, -1] @ model.W_emb.T)
Prefill 是一段录像快进 ×100:算得越快越好; Decode 是一台老电视,一帧一帧来——卡的不是算力,而是 把那几十 GB 的权重从 HBM 拖进 SM 的时间。 一张 H100 的 FP16 算力是 ~989 TFLOPS,HBM 带宽是 ~3 TB/s。 每读 1 字节 → 必须做 ~330 FLOP 才能"喂饱"算力。 decode 时一个权重只用一次 → arithmetic intensity $\approx 2$ → 算力被打到 0.6%。
把这一段刻进脑子里:"decode 阶段的延迟 $\approx$ 把整个模型权重从 HBM
读一次的时间。" 因此 7B 模型在 H100 上每个 token 的下限大约是
14 GB / 3 TB/s ≈ 4.7 ms——对应 ~210 tok/s。
所有"per-token 量化"、"speculative decoding"、"MoE
per-token 稀疏激活",目的都只有一个——
减少"每个 token 必须从 HBM 拖进来的字节数"。
§1.3Roofline · 哪里慢,一张图说清
Williams & Patterson 在 2009 年 CACM 上画过这张图—— Roofline。横轴是 arithmetic intensity $I = \dfrac{\text{FLOPs}}{\text{Bytes from memory}}$, 纵轴是实际可达到的 FLOP/s。 你的内核在这张图上一定坐落于两条线之下:
$$ \text{Reachable FLOP/s} = \min\!\big(\, \text{Peak FLOP/s}, \;\; \text{HBM bandwidth} \times I \,\big) $$两条线相交的那个 $I^*$ 称为ridge point。Hopper FP16 上 $I^* \approx 330$;FP8 上 $I^* \approx 660$; Blackwell FP4 上 $I^* \approx 1300$。 这意味着算力越大、ridge 越高、越多的内核会"掉"到斜坡那一侧(memory-bound)。
怎么手算一个内核的 I
| 内核 | FLOPs | Bytes 读 | I |
|---|---|---|---|
| FP16 GEMM $[M,K] \times [K,N]$ | $2MNK$ | $2(MK + KN + MN)$ | $\sim \min(M, N, K)$ |
| Prefill matmul (Llama-7B, S=2048) | ~28 TFLOP | ~14 GB | ~2000 |
| Decode matmul (Llama-7B, B=1) | ~14 GFLOP | ~14 GB | ~1 |
| Attention prefill (S=2048, H=32, D=128) | $\sim 4 S^2 D$ | $\sim 4SD$ | $\sim S$ |
| Attention decode (S=2048, H=32, D=128) | $\sim 4SD$ | $\sim 4SD$ | $\sim 1$ |
Decode 的 I 永远在个位数附近,无论怎么调;这是为什么 vLLM 拼命做 continuous batching——多 batch 等于把同一份权重分摊给更多 token, 分子放大、分母不变。但 batch 越大,KV cache 越占显存,这条路有上限。
§1.4显存账单 · 谁吃了我的 80GB
把"显存里到底装了什么"列清楚,是所有后续优化的对账单:
推理时
| 条目 | 大小(70B fp16 推理) | 压缩的方向 |
|---|---|---|
| 权重 weights | ~140 GB | 量化(INT4 → 35 GB)、MoE 稀疏激活 |
| KV cache (B=1, S=8k) | ~5 GB | MQA/GQA/MLA、量化、驱逐、前缀共享 |
| activations (decode) | <0.5 GB | FlashAttention 让它趋近 0 |
| 临时 buffer / workspace | ~1–3 GB | kernel fusion |
训练时再加上
| 条目 | 大小(70B fp16 训练) | 压缩的方向 |
|---|---|---|
| 梯度 | ~140 GB(同权重) | FSDP、ZeRO-3 |
| Adam state (m, v) | ~560 GB (fp32) | ZeRO 切分、bf16 Adam |
| activations | $\propto B \cdot S \cdot L \cdot D$ — 主头 | 梯度重计算 / FlashAttention |
# 心算工具:估一个 dense LLM 的训推显存
def estimate_mem(params_B, L, D, H_q, H_kv, d_h, S, B, bytes_per=2):
# 1) Weights (推理也要)
W = params_B * 1e9 * bytes_per
# 2) KV cache per sequence
KV = 2 * H_kv * d_h * bytes_per * L * B * S
# 3) Adam state (训练; fp32 m,v + fp16 master copy)
Adam = params_B * 1e9 * (4 + 4 + 2)
# 4) Activations per fwd (训练; 用 FlashAttention 时 attn 部分≈0)
Act_train = B * S * L * D * 2 * bytes_per
print(f'weights: {W/1e9:.1f} GB')
print(f'KV (B={B}, S={S}): {KV/1e9:.1f} GB')
print(f'Adam: {Adam/1e9:.1f} GB (训练)')
print(f'Act: {Act_train/1e9:.1f} GB (训练)')
# Llama-3-70B 训练 B=1 S=8192:
# weights 140 GB · KV 0.6 GB · Adam 700 GB · Act 21 GB
estimate_mem(70, 80, 8192, 64, 8, 128, 8192, 1)
粗略地,一个 dense LLM 的权重 GB $\approx \text{params(B)} \times \text{bytes/weight}$。 7B FP16 $\approx$ 14 GB;7B INT4 $\approx$ 3.5 GB。 一个 batch 一个 token 的 KV $\approx 2 L H_{kv} d_h \times \text{bytes}$。 Llama-3-70B GQA-8 在 FP16 下一个 token 约 320 KB, 8k 上下文一个序列就吃 2.5 GB—— 多 batch 时这是头号杀手。
- "为什么 Llama-3-70B 在一张 H100 上 prefill 1 个 token 跟 prefill 100 个 token 几乎一样快?"
- "为什么 batch 越大 decode 吞吐越高,但单请求延迟没变?"
- "为什么我量化到 INT4 后 prefill 没变快、decode 快了 4×?"
- "为什么训练时 80 GB 卡能装 7B 但装不下 70B?"