Hub Part I · Primer
Part I · Primer · 基础

把基础打牢

这一部讲四件事:transformer 长什么样、推理为什么分两阶段、roofline 怎么画、 显存里到底装了什么。后续所有 part 都建立在这四个概念之上—— 没看过的人请按顺序读,老手可以跳到 §1.3 看 roofline 那张图。

本页 4 节 · 1 个 demo
  1. §1.1Transformer 速通
  2. §1.2Prefill vs Decode
  3. §1.3Roofline · 哪里慢
  4. §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_dim128
$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 有两个截然不同的工作模式:

PrefillDecode
什么时候发生给定 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

内核FLOPsBytes 读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 越占显存,这条路有上限。

Demo 1 · Roofline · 把内核放上去
把 B、S 拉一拉,切 prefill / decode,会看到decode 死死贴在斜坡, 无论怎么调参数都到不了 ridge。拉高 B 会推 decode 向 ridge 走(多 batch 把权重读入分摊给更多 token)——但 batch 过大会被 KV cache 容量限死。 这就是 vLLM / SGLang 拼命做 continuous batching 的根本原因。

§1.4显存账单 · 谁吃了我的 80GB

把"显存里到底装了什么"列清楚,是所有后续优化的对账单:

推理时

条目大小(70B fp16 推理)压缩的方向
权重 weights~140 GB量化(INT4 → 35 GB)、MoE 稀疏激活
KV cache (B=1, S=8k)~5 GBMQA/GQA/MLA、量化、驱逐、前缀共享
activations (decode)<0.5 GBFlashAttention 让它趋近 0
临时 buffer / workspace~1–3 GBkernel 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?"