第 14 章 · Capstone: 用自己写的 kernel 跑 GPT-2 small

⏱️ 120 分钟🎯 端到端生成文本📂 code/ch14_mini_llm/🏁 终点
⚠️ 需 GPU 验证:本章端到端正确性与性能必须在 Linux + NVIDIA GPU 上复核;尚未采集的数据统一标为 TODO(on GPU)
教学实现边界(核验:2026-07-20):当前 capstone 是 batch=1、fp32、naive attention、 无 KV cache,并在每个生成 step 重算已有序列。--self-test 用 tiny deterministic weights 对拍完整单层 transformer 路径与 final logits,但不等于真实 GPT-2 权重回归,也不能替代 TensorRT-LLM、vLLM V1 或 NVIDIA Dynamo。

学习目标

完成这个 capstone 后,你应能解释 GPT-2 small 的端到端数据流、权重布局、kernel 串联方式与生成循环,并能为后续优化建立可复现基线。

前置知识

到第 13 章,你已经手写过:

本章把它们串成一个能跑的 GPT-2 small (124M)

核心概念

14.1 GPT-2 small 架构速览

graph TD
    Tok["token ids (T,)"] --> Emb["embedding: wte + wpe
(T, 768)"] Emb --> Loop["× 12 layers"] Loop --> LN1["LayerNorm"] LN1 --> Attn["fused QKV → causal MHA → out_proj"] Attn --> Res1["+ residual"] Res1 --> LN2["LayerNorm"] LN2 --> MLP["fc (768→3072) → GeLU → fc_proj (3072→768)"] MLP --> Res2["+ residual"] Res2 --> Loop Res2 --> LNF["final LayerNorm"] LNF --> Head["logits = h @ wte^T (T, 50257)"] Head --> Smp["argmax / top-k / top-p → next token"] style Loop fill:#f3f1e8,stroke:#8b1538 style Attn fill:#f3f1e8,stroke:#a86420
参数GPT-2 small
层数 n_layer12
头数 n_head12
隐层 d_model768
FFN 中间 d_ff3072
head_dim64
词表 vocab_size50257
最大上下文1024
参数量124M (fp16 ~250 MB)

关键代码

14.2 三步上手

Step 1 — 下载权重

pip install transformers torch
python data/download_gpt2.py --out data/gpt2-small.bin
# 输出: data/gpt2-small.bin (~250 MB, fp16)

脚本会把 HuggingFace 的 gpt2 权重按固定二进制 layout 序列化,方便 C++ 一次 fread。 文件头是 16 个 int32(含 n_layer / n_head / d_model 等),后接 fp16 张量流。

Step 2 — 编译与运行

cd code/ch14_mini_llm
make ARCH=sm_80
./mini_llm --weights=../../data/gpt2-small.bin \
           --tokens=15496,11,612,318 \
           --max_new=10

Step 3 — token ↔ 文本

本仓库为简化没在 C++ 里写 BPE tokenizer。 用 Python 编码 / 解码:

from transformers import GPT2Tokenizer
tok = GPT2Tokenizer.from_pretrained("gpt2")
ids = tok.encode("Hello, there is")
print(ids)                  # → [15496, 11, 612, 318]
# 把上面 ids 传给 mini_llm --tokens=...
# 然后把它打印出的 final tokens 复制回 Python:
print(tok.decode([15496, 11, 612, 318, 1043, 257, 1810, 2950, 287]))

运行结果

正确运行应完成权重加载、逐层 forward、logits 计算和 token 选择,并输出可由同一 GPT-2 tokenizer 解码的 token 序列。GPU 数值结果与生成文本需在目标环境实测:TODO(on GPU)

没有 GPT-2 权重时,可先运行确定性的 toy transformer 对拍;它覆盖 embedding、LayerNorm、causal attention、MLP、residual、final norm 与 final logits,不替代真实权重的逐层回归:

./mini_llm --self-test --seed=1234
# [mini_llm_self_test_final_logits] PASS ...

14.3 forward() 的结构

源码:mini_llm.cu。 核心 forward 函数把所有 kernel 按顺序串起来,每步:

void forward(const Weights& W, std::vector<int>& tokens, int* next) {
    embed_kernel(wte, wpe, tokens, x);            // (T, D)

    for (int l = 0; l < n_layer; ++l) {
        layernorm_row(x, ln1_w, ln1_b, normed);
        matmul_kernel(normed, qkv_w, qkv);        // Ch6 tile
        bias_add(qkv, qkv_b);
        split_qkv(qkv, Q, K, V);
        mha_naive(Q, K, V, attn_out);             // Ch11 naive
        matmul_kernel(attn_out, proj_w, proj_out);
        bias_add(proj_out, proj_b);
        residual_add(x, proj_out);                // x += proj_out

        layernorm_row(x, ln2_w, ln2_b, normed);
        matmul_kernel(normed, fc_w, ff_mid);      // (T, d_ff)
        bias_add(ff_mid, fc_b);
        gelu_kernel(ff_mid);                       // Ch13 风格激活
        matmul_kernel(ff_mid, fcp_w, ff_out);      // (T, D)
        bias_add(ff_out, fcp_b);
        residual_add(x, ff_out);
    }

    layernorm_row(x, lnf_w, lnf_b, normed);
    gemv_logits(normed[T-1], wte, logits);         // (V,)
    greedy_argmax(logits, next);
}

14.4 简化与代价

简化代价怎么补救
fp32 全程显存和算力都不适合推理热路径fp16/bf16 + WMMA(Ch9)
朴素 attentionO(T²) 显存用 Ch12 FlashAttention 替换
每步全前向(无 KV cache)O(T³) 总成本加 KV cache(练习 #1)
batch=1GPU 空跑支持 batch + padding mask
greedy only无创意实现 top-k/top-p (Ch13 练习)

即便如此,本章 GPT-2 small 已足以验证端到端流程。吞吐请在目标 GPU 上实测; 优化前后记录 tokens/s、TTFT、TPOT、batch、prompt length 和编译架构,不在教程里写死未经验证的数值。

教学实现边界:当前 capstone 是 fp32、batch=1、朴素 attention、无 KV cache 的正确性基线, 不是生产级推理引擎。它不代表 vLLM、TensorRT-LLM 等系统的 decode 路径或服务吞吐。

性能数据

TODO(on GPU):固定 GPU、driver、CUDA、commit、prompt length 与生成长度,报告 TTFT、TPOT、tokens/s 和峰值显存;同时分别记录 warmup 与 steady-state。

自检清单

练习题

以下扩展任务按难度递进:

★ 入门

  1. 支持 prompt 长度 > 16:测试 --max_new=64
  2. 把 greedy 换成 temperature=0.8 + top-k=40
  3. 用 nsys 看一次 forward 的 timeline,找最大 kernel

★★ 进阶

  1. 加 KV cache:维护 K_cache / V_cache (n_layer, n_head, T_max, D_head),每步只 forward 1 token。预期方向是把单步复杂度从重算全序列降到只读历史 KV;具体延迟写入 benchmark 表。
  2. fp16 路径:fc/proj/qkv 用 WMMA。Ch9 模板可移植。
  3. 用 Ch12 FlashAttention 替换 mha_naive。

★★★ 挑战

  1. continuous batching:多请求并发,按 token 调度而非按请求。这是 vLLM 的核心创新。
  2. W4A16 量化:把权重压成 INT4,runtime 解 → fp16 GEMM。看 llama.cpp / AWQ 实现。
  3. speculative decoding:用 distilgpt2 当 draft,gpt2 当 verifier,记录 accept rate、draft cost、verify batch 和端到端 TPOT。
  4. 移植到 Llama 架构:把 LayerNorm 换 RMSNorm、GeLU 换 SwiGLU、加 RoPE、加 GQA。

14.6 工业实战:从能跑到能上线

capstone 能在单卡跑通 GPT-2 small。距离给真实用户服务还差 6 个工程组件,本节给概念地图。

14.6.1 连续批处理 (Continuous Batching)

朴素 batch:N 个请求攒齐再 forward,慢的拖快的("head-of-line blocking")。Continuous batching 是 iteration-level scheduling——每个 token 步骤独立组 batch,新请求随时插入,完成的随时移出。

sequenceDiagram
    participant Q as RequestQueue
    participant S as Scheduler
    participant E as Engine
    Q->>S: req A, prompt T=100
    Q->>S: req B, prompt T=50
    S->>E: prefill A, B together
    Note over E: 同 batch 处理 prefill
    E->>S: gen token A=t1, B=t1
    Q->>S: req C, prompt T=20
    S->>E: prefill C plus decode A, B
    E->>S: A=t2, B=t2, C=t1
    Note over Q,E: B 提前完成, 出 batch, 槽位让给新请求

核心数据结构:request slot 数组,每个 slot 持有 (state=prefill/decode, KV pages, current token, sampling params)。每步根据 slot 状态组 batch。

vLLM、TGI、TensorRT-LLM 都内置 continuous batching。收益取决于请求长度分布、arrival rate 和 KV pool;上线前要用真实 trace 压测,而不是引用固定倍数。

14.6.2 PagedAttention KV 管理(集成版)

12.9.4 给了算法,在 mini_llm 里集成步骤:

  1. K_cache / V_cache 改成 page poolK_blocks (n_blocks, block_size, n_head, D_head)
  2. 每请求一个 block_tablelogical_block_id → physical_block_id
  3. attention kernel 通过 block_table 间接寻址
  4. Scheduler 维护 free block pool;新请求 alloc,结束归还

陷阱:block 大小要平衡。太大(256+)→ 内部碎片;太小(4-)→ block_table 大、kernel indirection 开销大。生产典型 16 token / block

14.6.3 多 GPU:TP / PP / EP / SP

单卡装不下(70B fp16 = 140 GB)时分卡跑:

策略切分方式通信适合
Tensor Parallel (TP)每层权重按列/行切多卡每层 1-2 次 all-reduce同节点(NVLink)
Pipeline Parallel (PP)层切到不同卡层之间 send/recv跨节点(IB)
Expert Parallel (EP)MoE expert 分卡token all-to-allMixtral / GPT-OSS
Sequence Parallel (SP)序列维度切跟 TP 配合长 context 训练

典型 70B 推理:TP=8, PP=1(单节点 8×A100/H100)。kernel 改动:QKV proj 输出维度 ÷TP,attention head ÷TP,FFN 中间维 ÷TP,每层最后 1 次 all-reduce (NCCL)。

14.6.4 CUDA Graph capture — 杀掉 launch overhead

单 token decode 会触发数十个小 kernel,launch gap 很容易和实际计算同量级。capture 成一个 Graph 后重放只保留一次 graph launch,具体收益用第 15 章的 nsys 流程采集:

// warmup 时同时 capture
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
decode_one_step(...);
cudaStreamEndCapture(stream, &graph);
cudaGraphInstantiate(&exec, graph, ...);

// 之后每步只 1 次 launch
cudaGraphLaunch(exec, stream);

本章 mini_llm.cu 已经内置 NVTX range: decode_looplayer_Nattention/*mlp/*logits/*sampler/*。 运行时加 --profile 会用 cudaProfilerStart/Stop 把采集范围限制在 decode loop,避免把权重加载也录进去。

cd code/ch14_mini_llm
make ARCH=sm_80                  # 缺 libnvToolsExt 时: make NVTX=0

nsys profile --stats=true \
  --trace=cuda,nvtx,osrt \
  --capture-range=cudaProfilerApi \
  --capture-range-end=stop \
  --force-overwrite=true \
  -o mini_llm_decode \
  ./mini_llm --weights=../../data/gpt2-small.bin \
             --tokens=15496,11,612,318 \
             --max_new=4 \
             --profile

看 trace 的顺序:先找 decode_loop/step_* 之间是否有 CPU gap,再展开某个 layer_N, 比较 attention/qkv_matmulattention/mha_naive_causalmlp/fc_gelulogits/lm_head_gemvsampler/greedy_argmax 的相对耗时。

坑:

14.6.5 服务化工程要点

14.6.6 生产 LLM 推理栈对比(2025)

方案典型场景优势劣势
vLLM开源 / Python 友好PagedAttention 原作、CB、Triton kernel 易改fp8/int4 集成稍落后
TensorRT-LLMNVIDIA 平台极致fp8/int4/SmoothQuant 全面,Builder 自动 fusebuild time 长,新模型需 plugin
llama.cppCPU / Mac / 边缘GGUF 量化精细、跨平台多 GPU 推理弱
SGLang复杂控制流 / agent / JSON 输出RadixAttention 前缀共享、结构化生成生态较小
TGI (HF)跟 HuggingFace 生态对接开箱即用、Rust 服务层性能略落后 vLLM
自研 (Meta/Anthropic)顶级公司极致性能、隐藏权重人年级投入

14.6.7 推荐工程化路径

  1. 原型:vLLM docker 一行命令
  2. 生产:vLLM 或 TRT-LLM;按 NVIDIA 平台粘度选
  3. 极致优化:基于 CUTLASS / Triton 自写关键 kernel(attention、量化 GEMM),其他算子复用
  4. 跨平台:llama.cpp (Mac / CPU)、MLX (Apple Silicon)、ROCm (AMD)

14.6.8 学完本教程能做什么

这是起点不是终点。顶级 CUDA 工程师还需 3-5 年实际项目经验,但你已经过了"看不懂代码"的门槛。

14.7 研究前沿(2025-2026):Disaggregated Serving、MoE、Reasoning

14.7.1 Disaggregated Prefill / Decode(Mooncake、DistServe)

14.6 讲了 prefill 和 decode 是两个不同世界(compute-bound vs memory-bound)。共享 GPU 跑两者意味着两边都不能最优——prefill 抢占 SM 会拖慢 decode 的尾延迟。

2024-2025 的革命:把 prefill 和 decode 分到不同 GPU pool,KV cache 通过高速网络传递。

graph LR
    A["用户请求"] --> B["调度器"]
    B --> P1["Prefill GPU pool
(compute-bound, 大 batch)"] P1 -->|"KV 传输
NVLink/IB"| D1["Decode GPU pool
(memory-bound, 小 batch)"] D1 --> O["流式输出"] style P1 fill:#f3f1e8,stroke:#8b1538 style D1 fill:#f3f1e8,stroke:#2f5d3a

主要工程实现:

典型目标是提高 tokens/$ 并降低 decode 尾延迟,但收益强依赖 prefill/decode 比例、网络、KV 传输和调度器复杂度。小规模团队(< 10 GPU)通常先优化单卡与单节点。

14.7.2 MoE 推理 — Expert Parallel 是新刚需

2024-2025 大模型几乎全转 MoE:DeepSeek-V3 671B(37B 激活)、Llama 4 Maverick 400B、Qwen 3 235B、Mixtral 8×22B 等。共同点:

Expert Parallel (EP)

每张 GPU 装一部分 expert (例如 64 expert / 8 GPU = 8 expert/GPU)
每 token 路由到 top-k expert (典型 k=2 或 8)
跨 GPU 通信: all-to-all (一次发, 一次收)

挑战:

实战:DeepSeek-V3 推理架构

14.7.3 Reasoning 模型的 serving

o1 / R1 / Gemini Thinking 等 reasoning 模型对推理服务的颠覆性影响

维度普通 LLM(如 GPT-4)Reasoning LLM
典型 output length200-2000 token5K-100K token
用户等待3-30 秒30 秒 - 10 分钟
prefill / decode 比2:11:50 - 1:200
每请求 KV peak~MB~GB
关键 metricp50 latencytokens/sec/user, 总成本

服务系统的调整:

14.7.4 Prefix Caching 的演进

2024-2025 prefix caching 从 "memory cache" 升级到 "持久化共享池":

对 chat 工作负载(系统 prompt + 多轮对话),prefix cache 的价值取决于系统 prompt 复用率、多轮上下文共享率和 KV 迁移成本。上线前要用真实 trace 统计命中率、节省 token 数、KV pool 压力和端到端吞吐。

14.7.5 多模态推理:vision + audio + text

Gemini 2.0 / GPT-4o / Llama 4 都是原生多模态。推理服务变化:

典型部署:vision encoder 作为独立微服务(甚至独立 GPU),LLM serving 拉它的输出 token。跟 disaggregated prefill 思路相通。

14.7.6 Agentic / Computer Use serving

2024-2025 Claude Computer Use、OpenAI Operator、AutoGPT 等"agent":模型自动循环调用工具。serving 挑战:

14.7.7 2026 LLM 推理栈对比(更新版)

方案典型场景2026 优势
vLLM V1开源推理引擎unified token-budget scheduler;feature support 以当前 V1 guide 为准
SGLang复杂控制流, agentRadixAttention + 结构化生成 + 程序化 LM
TensorRT-LLMNVIDIA 平台极致fp4/fp8 全栈 + Builder fuse + B200/H200
DeepSpeed-MII训练栈 + 推理跟 DeepSpeed 训练绑定深
Mooncake大规模 disagg跨节点 KV pool, 国内大厂
llm-dK8s 原生Red Hat / IBM 集成方便
Fireworks / Together / AnyscaleAPI 服务商多 LoRA 共享 + 商业 SLA
llama.cpp边缘 / Mac / CPUGGUF 量化精细, 2025 加 Apple Silicon GPU
MLXApple SiliconMac 上 LLM 推理事实标准

14.7.8 推理硬件软件全栈 (2026 全景)

用户                ┌────────────────────────────────────┐
请求 ──────────────►│   API 网关 (FastAPI, gRPC, OpenAI 兼容)  │
                    └──────────────┬─────────────────────┘
                                   │
                    ┌──────────────▼─────────────────────┐
                    │   调度器 (continuous batching,      │
                    │   prefix caching, 投机, multi-LoRA) │
                    └──────────┬───┬─────────────────────┘
                               │   │
                    ┌──────────▼─┐ ▼──────────────────────┐
                    │ Prefill    │ │ Decode pool          │
                    │ pool       │ │ (B200 fp4)           │
                    │ (B200 fp8) │ │                      │
                    └─────┬──────┘ └──────┬───────────────┘
                          │  KV 传输      │
                          ▼  (NVLink-C2C  ▼
                          │   或 IB)
                  ┌───────┴──────────────┴────────────┐
                  │  全局 KV cache pool                │
                  │  (LMCache / Mooncake Conductor)    │
                  └────────────────────────────────────┘

14.7.9 学完本教程在 2026 LLM 行业能做什么

恭喜!你已经具备:

下一步建议:

  1. 挑一个真实 OSS 项目(vLLM / SGLang)做一个 PR:加 fused 算子 / 修 bug / 加 model 支持
  2. 读 DeepSeek-V3 / FlashAttention v3 / Mooncake 三篇论文 + 对应源码
  3. 关注 github.com/NVIDIA/cutlassHazyResearch/ThunderKittenstriton-lang/triton 的 release 动态
  4. 关注 Lecture 系列:GPU MODE 是 2024-2026 最活跃的 GPU 编程社区

14.8 后续学习路径

14.9 总结

你从 __global__ void hello_kernel() 走到了用自己写的 kernel 跑出语言模型生成。 中间所有让你停下来调试的 bug,对将来读 vLLM 源码、写自定义算子都是直接经验。GPU 编程的"硬功夫"就这样炼出来的。

下一步:选一个真实仓库(推荐 llama.cpp 或 vLLM)读一个 kernel 实现,对照本教程章节复盘。 恭喜!🎉

14.10 CUDA 官方手册精讲(CUDA Programming Guide 13.2(核验:2026-07-20))

Prefill vs Decode、CUDA Graph 模板、Lazy Loading、NVTX、上线 checklist

本节定位:把 NVIDIA 官方 CUDA Programming Guide 13.2(核验:2026-07-20) 当前版中和本章直接相关的硬核细节抽出来——概念、API、踩坑点、版本兼容性—— 让你不必通读官方手册也能掌握本章主题的"标准答案"。引用按命名章节回查,避免把旧版编号当作稳定接口。

Prompt 阶段 vs Decode 阶段 — 两套 kernel mix

同一个 forward(),处理 prompt(首次输入 T 个 token)decode(每步只来 1 个新 token) 的算子组合差别巨大。 生产推理引擎为它俩准备两套 kernel:

阶段典型 GEMM 形状瓶颈kernel 选择
Prompt (prefill)M=T(几百~几千), N=hidden, K=hiddenFLOPs (compute-bound)大 tile GEMM (CUTLASS/cuBLAS), WMMA fp16, FlashAttention
DecodeM=1, N=hidden, K=hiddenHBM 读带宽 (memory-bound)GEMV / split-K GEMV; KV-Attention 沿 T 维并行

关键观察:M=1 的 GEMM 没法把 Tensor Core 喂饱(一次 mma.sync 至少要 M=8)。 所以 decode 阶段权重 GEMM 退化成 权重读 + dot-product reduce, 计算密度极低,带宽就是天花板。这也是为什么 13.7.1 W4A16 量化对 decode 收益最大。

Decode 阶段的 split-K GEMV 模板

// y[N] = W[N, K] * x[K], 单 M=1 GEMV
    // 朴素: 一个 block 一行, 一线程一列 -> 一个 SM 24 行/cycle 太少
    // 正确: split-K + atomic add or two-pass reduction
    template <int BK, int BN>
    __global__ void gemv_splitk(const __half* W, const __half* x, float* y,
                                 int N, int K, int splits) {
        int n = blockIdx.x * BN + threadIdx.y;     // 输出行
        int kstart = blockIdx.y * (K / splits);    // K 维分片
        int kend   = kstart + K / splits;
        if (n >= N) return;

        float acc = 0.f;
        for (int k = kstart + threadIdx.x; k < kend; k += BK) {
            acc += __half2float(W[n * K + k]) * __half2float(x[k]);
        }
        // warp reduce
        for (int off = 16; off > 0; off /= 2)
            acc += __shfl_xor_sync(0xFFFFFFFFu, acc, off);
        if ((threadIdx.x & 31) == 0) atomicAdd(&y[n], acc);
    }

性能数据采集表(Llama-7B 一层 hidden=4096 GEMV;需在目标 GPU 上实测):

方法带宽利用latency
朴素一线程一列TODO(on GPU)TODO(on GPU)
split-K + warp reduceTODO(on GPU)TODO(on GPU)
cuBLAS hgemvTODO(on GPU)TODO(on GPU)
Marlin W4A16TODO(on GPU)TODO(on GPU)

所以 capstone 第二步优化方向:把 decode 路径的 5 个 GEMM 全换成 split-K GEMV。 prompt 路径保留 tile GEMM 即可。

CUDA Graph 完整模板:把 30 次 launch 合成 1 次

GPT-2 small forward 一次 token 大约 12 层 × (LN + QKV + Attn + Proj + LN + FC + GeLU + FC) ≈ 60 个 kernel。每个 launch 都有固定调度开销;当 kernel 变短,launch 占比会明显上升。CUDA Graph capture 让所有 launch 合并成单次 GPU dispatch,开销几乎归零。

三步走

cudaGraph_t       graph;
    cudaGraphExec_t   exec;
    cudaStream_t      stream;
    cudaStreamCreate(&stream);

    // (1) warmup: 先跑一次普通 forward, 让 cuBLAS / cuRAND 完成 lazy init
    decode_one_step(W, ctx, stream);
    cudaStreamSynchronize(stream);

    // (2) capture: 关闭 cuBLAS / cuRAND 内部同步, 用 ThreadLocal 模式
    cudaStreamBeginCapture(stream, cudaStreamCaptureModeThreadLocal);
    decode_one_step(W, ctx, stream);
    cudaStreamEndCapture(stream, &graph);

    cudaGraphInstantiate(&exec, graph, nullptr, nullptr, 0);

    // (3) 之后每步:
    for (int step = 0; step < max_new; ++step) {
        update_runtime_params(ctx, step);     // 仅改 device-resident 计数器
        cudaGraphLaunch(exec, stream);
    }
    cudaStreamSynchronize(stream);

    cudaGraphExecDestroy(exec);
    cudaGraphDestroy(graph);
    cudaStreamDestroy(stream);

常见坑(每个都掉过)

收益采集表(GPT-2 small, 单 token decode;需在目标 GPU 上实测)

实现per-token latency说明
朴素多次 launchTODO(on GPU)记录 kernel 数与 launch gap
+ CUDA GraphTODO(on GPU)记录 capture batch size 与 graph replay 时间
+ KV cacheTODO(on GPU)记录历史长度与 KV layout
+ fp16 WMMATODO(on GPU)记录 dtype、Tensor Core 利用率与 allclose 阈值

服务冷启动:CUDA_MODULE_LOADING=LAZY 与 JIT 缓存

LLM 推理 binary 经常嵌入 50-200 个 cubin(FlashAttn、CUTLASS GEMM、采样、量化各一堆), 每个 cubin 加载到 GPU 要消耗显存 + 启动时间。CUDA 11.7 引入 lazy module loading,CUDA 12.0 起默认开启, 可观察到的好处:

模式启动时间启动显存首次 launch latency
EAGER(旧默认)TODO(on GPU)TODO(on GPU)TODO(on GPU)
LAZY(CUDA 12+ 默认)TODO(on GPU)TODO(on GPU)TODO(on GPU)

这张表应在你的服务镜像里采集:记录 CUDA/driver 版本、fatbin 数量、是否挂载 JIT cache、是否 warmup hot kernel。

环境变量(PDF §5.2.4.1):

export CUDA_MODULE_LOADING=LAZY     # 默认, 显式声明
    # 仅 kernel lazy; 全局变量等 module data 也想 lazy:
    export CUDA_MODULE_DATA_LOADING=LAZY

    # 多线程 binary loader 加速 (CUDA 12.4+)
    export CUDA_BINARY_LOADER_THREAD_COUNT=4

配套优化 — warmup 阶段把所有 hot kernel 各跑 1 次,让 lazy load 提前完成:

void warmup(const Weights& W) {
        // 跑 prefill batch=1, T=16; 触发 prefill 全部 cubin 加载
        forward_prefill(W, dummy_ids_16, 1, /*max_new=*/0);
        // 跑 decode 各 batch size, 让 CUDA Graph 顺便 capture
        for (int B : {1, 2, 4, 8, 16, 32}) {
            forward_decode(W, dummy_kv, B);
        }
        cudaDeviceSynchronize();
    }

JIT 编译缓存(PTX → SASS)

新一代 GPU 上没有对应 cubin 时,PTX 会被驱动 JIT 编译。第一次跑会卡 1-5 s (取决于 kernel 复杂度)。开启磁盘缓存(PDF §5.2.2.1-5.2.2.3):

export CUDA_CACHE_DISABLE=0                       # 默认开启
    export CUDA_CACHE_PATH=/var/cache/cuda-jit       # 自定义路径, 容器化时挂卷
    export CUDA_CACHE_MAXSIZE=$((4*1024*1024*1024))  # 4 GB 上限

容器化部署关键点:把 ~/.nv/ComputeCache 或自定义路径 挂出来到持久卷,否则每次 pod 重启 JIT 都要重跑一遍,冷启 +30 s。

nvJitLink — 跨编译单元 LTO

CUDA 12 引入 nvJitLinkCompile / nvJitLinkAddFile(CUDA Toolkit 头文件 nvJitLink.h),可以运行时把多个 LTO-IR 文件链接成单 cubin, 做 device-side link-time optimization。LLM 用法:

TensorRT-LLM 9+ 的 "JIT engine" 模式就是这套。开源端 vLLM 仍是 AOT cubin。

NVTX 端到端 profiling + Tensor Core 吞吐速查表

14.5 推荐用 nsys 看 kernel timeline。当前 mini_llm.cu 已经把 decode path 标成可读的 NVTX range: decode_looplayer_Nattention/qkv_matmulattention/mha_naive_causalmlp/fc_gelulogits/lm_head_gemvsampler/greedy_argmax

编译时默认链接 -lnvToolsExt;如果环境缺少 NVTX 库,用 make NVTX=0 仍可运行,只是 timeline 上没有 range 名称。

cd code/ch14_mini_llm
make ARCH=sm_80

../../scripts/bench.sh nsys-range ./mini_llm \
  --weights=../../data/gpt2-small.bin \
  --tokens=15496,11,612,318 \
  --max_new=8 \
  --profile

# 输出 mini_llm.range.nsys-rep;Nsight Systems GUI 打开看 timeline

各代 Tensor Core 吞吐速查(理论 peak, 单 SM)

PDF §5.1 Table 33(lines 751-766)只列了"哪些 dtype 支持"。这里加上吞吐数字(来自 NVIDIA 白皮书):

来源:CUDA Programming Guide 与对应架构白皮书;按 dtype 和 sparse/dense 口径读取。
架构代表FP16/BF16 TCTF32 TCFP8 TCFP4 TC每 SM 每周期 mma 形状
Volta (sm_70)V100512 ops8×8×4 fp16
Turing (sm_75)T451216×8×8 fp16 + INT8/4
Ampere (sm_80)A100102451216×8×16; 引入 TF32
Ada (sm_89)4090, L40S10245122048+ FP8 E4M3/E5M2
Hopper (sm_90)H100/H200204810244096WGMMA 64×N×16; TMA + DSMEM + cluster
Blackwell (sm_100)B200 / GB20040962048819216384+ FP6/FP4 (NVFP4); Tensor Memory
Blackwell UltraB300 / GB300峰值按具体 SKU、dtype 与 sparse/dense 口径查官方规格288 GB HBM3e;attention acceleration

用法:选模型权重精度时先反查硬件支持,再用第 15 章的 roofline 方法验证 workload 到底吃带宽还是吃算力。例如 7B 模型在 4090 上:

"你已经实现的微型版本对应业界哪个特性"

本教程实现对应业界系统
第 6 章 tiled GEMMcuBLAS sgemm; CUTLASS hgemm 入门
第 7 章 warp-shuffle reduceCUB WarpReduce / DeviceReduce
第 9 章 WMMA GEMMCUTLASS, TensorRT-LLM 量化 GEMM 的脚手架
第 10 章 online softmaxFlashAttention 的内层 lemma
第 12 章 FlashAttention 朴素版HazyResearch FA v1; Dao-AILab FA v2 入门
第 13.1 RoPEvLLM rotary_embedding_kernel
第 13.3 KV cache appendvLLM reshape_and_cache_kernel 的 BHSD 版
第 13.4 samplervLLM sample.py + 自定义 CUDA top-p kernel
第 14 forward loop + GraphvLLM model_runner.py + CUDAGraph 路径

读到这里你已经掌握了上述每个文件的 核心 kernel 写法,剩下的差距是工程量(调度器、KV pool、多 GPU 通信),不是算法新概念。

上线前 12 个 CUDA 检查点

把 capstone 改造成"能撑住生产流量"的服务,下面 12 条来自 NVIDIA Programming Guide §5.2-5.4 的最佳实践 + 工业事故复盘:

  1. 每个 CUDA API 调用都包 CUDA_CHECK;kernel launch 后立刻 KERNEL_CHECK (cudaPeekAtLastError 或 cudaGetLastError + cudaDeviceSynchronize 配对)。 Debug 时设 CUDA_LAUNCH_BLOCKING=1(§5.2.3.1)让错误马上暴露在源头。
  2. 错误日志:开 CUDA_LOG_FILE=/var/log/cuda.log(§5.2.5.1), kernel launch 配置错误会写人类可读消息进去(默认只有 generic error code)。
  3. 合理设置 CUDA_DEVICE_MAX_CONNECTIONS(§5.2.3.2)。 默认 8,超过 8 个并发 stream 后会出现 false dependency 拖慢。 LLM 服务一般设到 32(最大值):export CUDA_DEVICE_MAX_CONNECTIONS=32
  4. 不要在 hot path 调 cudaMalloc;用 stream-ordered allocator 或预分配 KV pool(见 13.7.7)。
  5. kernel 用 __launch_bounds__(MAX_THREADS, MIN_BLOCKS)(§5.4.3.2), 避免编译器为单 block 优化导致 occupancy 低。LLM kernel 通常用 __launch_bounds__(256, 4)
  6. warp intrinsic 严格遵守 mask 约束(§5.4.6.6 Warp __sync Intrinsic Constraints): 所有参与线程的 mask 必须相同;不参与线程的 bit 必须为 0。 违反 → 偶发 hang,常温 1 万 QPS 跑 24 小时才出。
  7. fp16/bf16 kernel 加 NaN/Inf 检查 hook。debug 模式下:
    if (!isfinite(__half2float(val))) {
            printf("NaN at layer %d head %d\n", layer, head);
            __trap();   // 立即 abort, GDB 可以接住
        }
    生产模式去掉 hook;偶发 NaN 用 cuda-memcheck 复现。
  8. memcpy_async / pipeline(§5.6.2)必须保证 size_and_align ∈ {4, 8, 16} 且 dst/src 地址对齐 — 不对齐会静默退化到 sync copy。
  9. atomic 操作选最低 scope:block-only 计数器用 atomicAdd_block 而非 atomicAdd(§5.4.5.1),后者会引入跨 device fence,是否成为瓶颈要用多 GPU trace 验证。 cross-process 才需要 _system 后缀。
  10. JIT 缓存挂卷:容器化部署设 CUDA_CACHE_PATH=/cache/jit 并挂出来(§5.2.2.2), pod 重启不重 JIT。
  11. compute_XYa 架构后缀谨慎用(§5.1.2 Architecture-Specific Features): compute_90a 编出来的 cubin 在 sm_90 跑, 升级到 B200/GB200 不能直接复用。生产应为目标架构分别生成 fatbin,并保留合适的 PTX fallback。
  12. CUDA Graph 捕获前 warmup cuBLAS / cuRAND,handle 内部 lazy state 不能在 capture 阶段触发(见 14.5.1)。

这 12 条直接对应 NVIDIA 内部 "CUDA Code Review Checklist"。 贡献到 vLLM / TRT-LLM 的 PR 几乎所有 review 意见都落在这上面。

常见坑

下一章导览

第 15 章把这个“能跑”的基线放进 Nsight Systems、Nsight Compute 与 roofline 工作流,学习如何用证据定位推理瓶颈。