第 13 章 · LLM 必备零件

⏱️ 70 分钟🎯 凑齐 Capstone 所需算子📂 code/ch13_llm_parts/

本章拼齐第 14 章 Capstone 还差的算子。每个独立、简单,但少一个就跑不起来一个完整 LLM。

13.1 RoPE — Rotary Position Embedding

Llama / Mistral / Qwen 都用 RoPE 替代了 GPT-2 的绝对位置编码。 它把 Q、K 向量看成 D/2 个复数,第 i 对复数乘以 exp(j · θ_{t,i}),其中 θ_{t,i} = t / base^{2i/D}

实现(in-place)

__global__ void rope_inplace(float* x, int T, int D, float base) {
    int t = blockIdx.x;
    int i = threadIdx.x;
    int half = D / 2;
    float theta = float(t) / powf(base, float(2*i) / float(D));
    float c = cosf(theta), s = sinf(theta);
    float x0 = x[t*D + i];
    float x1 = x[t*D + i + half];
    x[t*D + i       ] = x0 * c - x1 * s;
    x[t*D + i + half] = x0 * s + x1 * c;
}

性质:

13.2 SwiGLU / SiLU — Llama FFN

GPT-2 用 FFN(x) = GELU(x @ W_1) @ W_2。 Llama 用双投影 + 门控的 SwiGLU:

FFN(x) = ( SiLU(x @ W_gate) ⊙ (x @ W_up) ) @ W_down

SiLU 定义:silu(x) = x · sigmoid(x) = x / (1 + e^{-x})

__device__ float silu(float x) {
    return x * (1.f / (1.f + __expf(-x)));
}

__global__ void swiglu(const float* G, const float* U, float* O, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) O[i] = silu(G[i]) * U[i];
}

"门控"让网络学会"对某些位置让信息通过、对另一些抑制"。代价:FFN 多一个 W_gate,参数量 ~1.5×;但 hidden = 2.66·D(GPT-2 是 4·D),总参数差不多。

13.3 KV Cache

自回归生成有个关键观察:第 t 步生成的 token i 时,第 t-1 步算出来的 K、V 仍然有效,不必重算。 于是推理代码维护一个 cache:

K_cache shape: (n_layers, n_heads, T_max, D_head)
V_cache shape: same

每步只算新 token 的 (K_new, V_new) 形状 (1, D_head),append 到 t_pos 位置:

__global__ void append_kv(const float* K_new, const float* V_new,
                          float* K_cache, float* V_cache, int t_pos, int D) {
    int d = blockIdx.x * blockDim.x + threadIdx.x;
    if (d < D) {
        K_cache[t_pos * D + d] = K_new[d];
        V_cache[t_pos * D + d] = V_new[d];
    }
}

KV cache 的显存开销

Llama-7B (n_layers=32, n_heads=32, D_head=128, fp16) 一个 token 的 KV 占 32 × 32 × 128 × 2 × 2 B = 0.5 MB。 T=2048 时 1 GB;batch=8、T=2048 → 8 GB。这就是为什么 KV cache 管理(PagedAttention 等)这么重要。

13.4 Sampling 策略

greedy (argmax)

// 简化: V <= 65536 用单 block reduce 即可
template <int BLOCK>
__global__ void greedy_argmax(const float* logits, int* out, int V) {
    __shared__ float vals[BLOCK]; __shared__ int idxs[BLOCK];
    float bv = -INF; int bi = -1;
    for (int i = tid; i < V; i += BLOCK)
        if (logits[i] > bv) { bv = logits[i]; bi = i; }
    /* block-wide reduce on (vals, idxs) keeping max */
    if (tid == 0) *out = idxs[0];
}

top-k (truncated sampling)

从 logits 取最大的 k 个,softmax,按概率抽样。常见 k = 40~50。GPU 实现:用 block-wide top-k(heap 或 partial sort)。

top-p (nucleus sampling)

排序 logits → 累计概率 → 截断到累积 ≥ p 的前缀(典型 p = 0.9~0.95)→ 重新归一 → 抽样。需要 sort + prefix scan,第 7 章的 scan 模板在这里能复用。

temperature

logits /= T,T > 1 让分布更平(更随机),T < 1 让分布更尖(更确定)。在 softmax 前应用。

策略速度多样性用途
greedy最快最低评测、确定性回放
top-k中等中等chat, 一般生成
top-p稍慢较高chat, 创意写作
temperature only可调组合用

13.5 自检

Q1: RoPE 为什么 in-place 还能正确?

因为旋转是 pair-wise (x0, x_{D/2}) ↔ (x0', x_{D/2}'),写入新位置时旧的两个值都还在寄存器里(在 kernel 内已加载),所以 in-place 安全。

Q2: KV cache 为什么放 GPU 显存而不是 CPU?

attention 算 (Q_new @ K_cache^T) 要把 K_cache 全部读进 SM。放 CPU 每步都得 H2D,PCIe 慢 10×。所以 KV cache 是 LLM 显存大户。

Q3: SwiGLU 比 GELU 强多少?

不算"强很多"。Llama 团队论文报告 +0.3 PPL 左右改善。但因为没坏处+实现简单,新模型都默认用它。

Q4: top-p 排序很贵吗?

V=50K 完整排序确实贵。优化套路:先 top-k (k=200) 截断再排序;或者 radix-select。vLLM 实现 ~50 us / token,可接受。

Q5: prefill 和 decode 在 KV cache 上有什么区别?

prefill(处理 prompt):一次性算 T_prompt 个 KV 写 cache,是大 batch GEMM;decode:每步只算 1 个 KV,是 GEMV(memory-bound)。所以 LLM 服务把两者分开调度。

13.6 练习

  1. 实现 top-k sampling:先 block-wide top-k(用堆),再 softmax 抽样。
  2. 实现 top-p (nucleus):sort + scan + 截断。
  3. rope.cutheta_base = 500000(Llama 3 用),看长 T 时角度变化。
  4. swiglu 改成 fused:把 silu(G) * U 合并到 W_down 的 GEMM 里(pre-multiplier trick)。

13.7 工业实战:量化、投机解码、采样工程

13.7.1 W4A16 量化 — LLM 推理头号加速武器

LLM decode 阶段瓶颈是权重的 HBM 读带宽(M=1 时 FLOPs 极少)。 Llama-7B fp16 权重 14 GB / A100 HBM 1.5 TB/s = 单步至少 9 ms。 把权重压成 INT4,HBM 读量降 4× → 单步 2-3 ms。这就是 W4A16(W=int4,A=fp16)。

算法思路精度损失实现
GPTQ逐层 OBS 找量化误差最小的舍入< 0.5 PPLAutoGPTQ
AWQactivation-aware: 保护"重要"通道< 0.3 PPLllm-awq, vLLM
GGUFblockwise scale, k-means0.2-0.8 PPLllama.cpp
SmoothQuant同时量化 W+A (W8A8)~0.2 PPLTensorRT-LLM

W4A16 kernel 的关键:dequant + GEMM 融合

朴素做法:先把 INT4 dequant 成 fp16 写回 HBM 再调常规 GEMM —— 这样消除不了带宽瓶颈
正确做法:kernel 内即时 dequant,权重始终以 INT4 从 HBM 读:

for (int kt = 0; kt < K; kt += BK) {
    // 1) HBM 只读 INT4 weight tile (BM*BK/2 字节, 2 个 INT4 / byte)
    load_int4_to_shared(W_int4_tile, gmem_ptr);
    // 2) shared 内 dequant: int4 -> fp16, 乘 per-group scale
    dequant_inplace(W_int4_tile, W_fp16_tile, scales[group_id]);
    // 3) WMMA / mma.sync 用 fp16
    wmma::mma_sync(acc, A_frag, B_frag, acc);
}

典型实现:TensorRT-LLM weight_only_gemm,vLLM 的 Marlin kernel。Marlin 在 4090 上 7B decode 跑到 200+ tok/s,几乎打满 HBM 带宽。

13.7.2 投机解码 (Speculative Decoding)

用小模型快速生成 N 个候选 token,大模型一次性验证。接受多少多少出,拒绝就退回。

graph LR
    Start["prompt"] --> Draft["draft model
(TinyLlama 1B 等)
快速 4-8 token"] Draft --> Verify["target model
(Llama 70B)
一次前向算 5 个位置 logits"] Verify --> Accept{"p_target / p_draft
是否接受?"} Accept -->|"接受"| Continue["输出, 继续 draft"] Accept -->|"拒绝"| Resample["从拒绝位重采样, 退回 draft"] Continue --> Draft Resample --> Draft style Draft fill:#f3f1e8,stroke:#8b1538 style Verify fill:#f3f1e8,stroke:#2f5d3a

关键洞察:

draft model 候选:

vLLM、TensorRT-LLM 都内置。EAGLE / Medusa 需要额外训练,n-gram 0 训练成本。

13.7.3 生产 sampler — 不只是 argmax

// 完整 pipeline: logits -> /temp -> top-k -> top-p -> softmax -> cuRAND 抽样
__global__ void sample_kernel(float* logits, int V, float temp, int top_k, float top_p,
                              unsigned int seed, int* out_token) {
    // 1) 除 temperature
    for (int i = tid; i < V; i += BLOCK) logits[i] /= temp;
    // 2) top-k: 保留最大 k 个, 其他 -inf  (block-wide heap 或 partial sort)
    block_topk(logits, V, top_k);
    // 3) softmax + inclusive scan + 找 top-p 截断点 + 重新归一
    block_softmax(logits, V);
    block_inclusive_scan(logits, V);
    int cutoff = block_find_first_ge(logits, top_p);
    // 4) 用 cuRAND 抽样
    curandState s; curand_init(seed, tid, 0, &s);
    *out_token = sample_from_dist(logits, cutoff, curand_uniform(&s));
}

性能:完整 sampling ~50 μs (V=50K),decode 总耗时占 1-2%。

13.7.4 KV cache 量化(int8 / fp8)

权重量化外,KV cache 也可以量化。Llama-7B (B=32, T=2K) KV 占 32 GB → fp8 后 16 GB,batch 翻倍

实现:per-group scale,attention kernel 内 load 时 dequant:

// attn 读 K 时:
int8_t k_i8 = K_cache_int8[t * D + d];
half scale  = K_scales[t / GROUP_SIZE];          // 每 64 token 一个 scale
half k      = __int2half_rn(k_i8) * scale;

TensorRT-LLM 默认支持 fp8 KV,vLLM 0.3+ 也支持。陷阱:per-tensor scale 精度差,per-channel scale 调度复杂——生产用 per-group (group_size=64) 平衡。

13.7.5 RoPE 工程注意点

13.7.6 推理优化优先级

优化prefill 收益decode 收益实施难度
fp16 → fp8 weight★★★★★★
W4A16 量化★★★★★
FlashAttention v2★★★★★★★★易(用现成库)
PagedAttention★★★★(吞吐)中(vLLM)
Speculative decoding★★★★
CUDA Graph★★
Continuous batching★★★★(吞吐)
fused norm + QKV★★

顺序建议:先用 vLLM/TRT-LLM 自动拿 80% 优化,再针对自己模型做 fp8 量化 / spec decoding 增量调优。

13.8 研究前沿(2025-2026):EAGLE-3、Lookahead、KV 压缩、W4A4 / FP4

13.8.1 投机解码 2024-2026 演进

方法原理典型加速实现成本
Vanilla SpecDec (2023)独立小 draft model2-3×需训练 draft
Medusa(2024)大模型加多个并行 head 直接预测后 N token2-3×微调 + head 训练
EAGLE(ICML 2024)用大模型 hidden state 作为 draft head 输入训练 draft head
EAGLE-2(2024)动态接受树, 不是固定 chain~4×同 EAGLE
EAGLE-3(2025)更深 draft head, 多层 hidden 输入~5-6×同 EAGLE
Lookahead Decoding(Hao Zhang 团队 2024)用 Jacobi 迭代 + n-gram cache, 无需训练2-4×0 训练
Hydra(2024)多 head 串行依赖, 比 Medusa 接受率高~3-4×训练
ReDrafter(Apple 2024)RNN draft + Top-K beam, 上线 LLM Engine~3×训练

2026 工业现实

13.8.2 KV cache 压缩前沿(2024-2026)

除了 13.7.4 的量化路线,2024-2026 出现了大量"丢 token"型压缩:

方法核心想法压缩比精度损失
StreamingLLM(ICLR 2024)保留 sink token + 最近 W token10-50×无损(短 attention)
H2O (Heavy-Hitter Oracle)累计 attention score 高的 token 留5-10×< 1 PPL
SnapKV(NeurIPS 2024)每层独立选 K/V 保留5-10×< 0.5 PPL
KIVI(ICML 2024)K per-channel 量化, V per-token4×(int2)~0.3 PPL
KV-Quant非均匀量化 + outlier 单独保留4-8×~0.5 PPL
Quest(ICLR 2025)查询时 page-level 选 top-k 块取决于 sparsity近无损
L2Compress / MiniCache跨层 KV 合并2-4×~0.5 PPL

组合:MLA + fp8 KV + SnapKV / Quest 是 DeepSeek-V3 / Kimi 等的工业实践,长 context 时 KV 显存从 GB 级降到几十 MB。

13.8.3 W4A4 / NVFP4 — 极致量化(2024-2026)

9.10.4 给了对比表,这里详述算法:

QuaRot(ICML 2024)— Hadamard 旋转解 outlier

激活的 outlier 是 W4A4 最大障碍(少数 channel 数值范围比平均大 100×)。QuaRot 用 Hadamard 矩阵 H 旋转:x' = xH, w' = wH^T数学等价 + 旋转后分布更均匀,outlier 被打散

SpinQuant(Meta 2024)— 可学习旋转

不用固定 Hadamard,训练学习旋转矩阵。精度比 QuaRot 再好 0.2-0.5 PPL。

Atom(MLSys 2024)— 混合 W4A4 + Heuristic outlier

正常 channel 用 INT4,outlier channel 单独留 INT8。fp16 baseline 之上无损。

NVFP4(Blackwell 2025)— 硬件原生

fp4 (E2M1) + per-16-block fp8 scale,Tensor Core 直接吞。无需 dequant 到 fp16/fp8 中转,吞吐拉满。是 2025+ B200 推理的事实标准

13.8.4 BitDelta / 1-bit weight diff

BitDelta(Anthropic 2024):把 fine-tuned 模型相对 base 的 diff 量化到 1 bit

意义:多租户服务能存几百个 fine-tune 版本同时运行,每个只多花一点显存。Anthropic / Together AI / Fireworks 都在用。

13.8.5 RoPE 长 context 外推:YaRN、PI、NTK

原始 RoPE base=10000 训练长度内表现好,超出训练长度急剧退化。2023-2025 主要技术:

技术核心典型外推
Position Interpolation (PI)position 缩放
NTK-aware高频维度少缩、低频维度多缩
YaRN(ICLR 2024)NTK-aware + 温度调整 + 部分维度不缩16-32×
Dynamic NTK推理时根据当前 T 动态调 base10×+
LongRoPE(Microsoft 2024)进化算法搜索 per-dim 缩放因子32× (2M token)

Llama 3 直接训练到 8K,推理用 YaRN 外推到 128K。Llama 4 据传训练到 32K,外推到 10M。不重训能让模型多用 10×+ context,性价比极高。

13.8.6 Reasoning 模型的采样新需求

o1 / R1 类 reasoning 模型,采样策略影响最终能力:

对 sampler kernel 的影响:不再是单 next-token argmax,而是 N 个并行候选 + reward model 评估 + 选择,采样跟 attention 计算量同阶

13.8.7 2026 LLM 算子优化总览

技术对 7B fp16 baseline 提升
FA v3 + Hopper2-3×
NVFP4 + Blackwell再 4-6×
MLA(如果模型用)KV 显存 7×, batch 也大 7×
EAGLE-35-6×(decode)
Lookahead decoding2-4×(无训练)
PagedAttention + RadixAttention1.5-3× 吞吐
Chunked prefill~1.5× latency
Disaggregated serving~1.5× tokens/$

所有技术叠加:相比 2023 baseline,2026 LLM 推理性价比提升 ~50-100×。这就是为什么 LLM 服务价格在两年内能降 100×(GPT-3.5 → DeepSeek-V3 同等性能 API 价格差 100 倍)。

13.9 常见坑