跳到主要内容

数据截至 (上游 commit f1e2ace65149)

05 · 测试、剖析与 kernel 实验室

这一章讲什么: 没有 pytest、没有 autograd 的日子里,llm.c 怎么保证「C 算得和 PyTorch 一 样」、怎么找到「哪个 kernel 慢」。三件事:对拍测试(test)、性能剖析(profile)、kernel 开 发沙盒(dev/cuda),外加把它们串起来的 Makefile 和 CI。


1. 它要解决的小问题

  • 改了 kernel,怎么证明结果还对?——需要一个权威答案和一套比对流程
  • 训练慢了,慢在哪?——需要把单步拆到 kernel 级看时间。
  • 新 kernel 怎么从想法走到主线?——需要一条「先沙盒演进、再进库」的流水线。

2. 对拍体系:PyTorch 是出题人

2.1 出题:train_gpt2.py 写四个 .bin

仓库里的 train_gpt2.py(860 行,nanoGPT 改写)身兼两职:PyTorch 平行实现 + 测试资产生成 器。以特定参数运行时它写出(train_gpt2.py:694-698):

文件内容写出函数
gpt2_124M.binfp32 权重(magic 20240326, version 3)write_modeltrain_gpt2.py:449
gpt2_124M_bf16.binbf16 权重(version 5)同上
gpt2_124M_debug_state.bin一小批输入 x/y + 期望 logits、loss、全部参数梯度write_statetrain_gpt2.py:479
gpt2_tokenizer.bintokenizer 词表write_tokenizertrain_gpt2.py:509

测试的权威答案全部来自 PyTorch 的一次真实前向/反向。C 侧永远不用「重新实现期望」,只负 责追上它。

2.2 CPU 对拍:test_gpt2.c

test_gpt2.c(195 行)的第一行是个小技巧(test_gpt2.c:1-2):

#define TESTING
#include "train_gpt2.c" // train_gpt2.c 的 main 被 #ifndef TESTING 包住,测试复用全部函数

流程:读 debug_state.bin → 用同一份 x/y 前向 → 逐个比 logits(容差 1e-2)→ 比 mean loss → 用 check_tensortest_gpt2.c:5-34,容差 2e-2)逐张量比 16 组参数梯度 → 然后真训 10 步,每步 loss 和硬编码的期望值比(test_gpt2.c:89-100):

float expected_losses[10] = { 5.270007133483887f, 4.059706687927246f, ... };

结尾打印 overall okay: 1。注意这个测试同时验证了前向、反向、AdamW、checkpoint 读取——是 全仓库性价比最高的 200 行。

2.3 GPU 对拍:test_gpt2.cu

test_gpt2.cu(390 行)结构相同,但容差设计更讲究:bf16 的 epsilon 是 0.079,判定阈值是 threshold + |ref| * epsilon相对+绝对混合test_gpt2.cu:14-19)——因为 bf16 的误差 随数值大小浮动。它既测 fp32 路径也测混合精度路径(README 的 test 一节给了两条编译命令)。

2.4 CI

.github/workflows/ 三个文件:CPU 三平台构建+测试(ci.yml)、GPU 构建+测试(ci_gpu.yml)、 以及 dev/loss_checker_ci.py 的用法——从训练日志里抠出 10 步 loss 与固定值按百分比比对。


3. 剖析:把训练裁到「单层一步」喂给 ncu

profile_gpt2.cu(74 行)同样 #define TESTING 复用主线全部函数,然后做两个手脚:

model.config.num_layers = 1; // 每层 kernel 相同,profile 一层就够(profile_gpt2.cu:57-58)
...
gpt2_forward(&model, x, B, T);
gpt2_backward_and_reduce(&model, x, y, 1, 0);
gpt2_update(...);

profile_gpt2.cu:62-66)随机整数当输入、单层、完整走一遍 forward/backward/update——这个 可执行文件就是给 Nsight Compute 准备的靶子:

# 摘自 profile_gpt2.cu 头部注释(profile_gpt2.cu:1-24)
make profile_gpt2cu NO_MULTI_GPU=1
sudo ncu --set full --import-source yes -o profile -f ./profile_gpt2cu

profile_gpt2cu.py 进一步自动化:编译 → ncu 采集 → 导出 CSV,关注的指标包括 sm__pipe_tensor_op_hmma_cycles_active(tensor core 利用率)和 DRAM 读写量 (profile_gpt2cu.py:36-44),并按「属于第几层 / classifier」归类统计。

kernel 能被归类,靠的是每个 launcher 里的 NVTX_RANGE_FN()llmc/cuda_common.h:124)和层循 环里的 NvtxRange layer_range("Layer", l)train_gpt2.cu:686)——剖析报告里每个 kernel 都 挂在命名范围下。


4. dev/cuda:kernel 的演进博物馆

4.1 工作法

dev/cuda/README.md 写得很直白:这是 kernel 的「scratch space」——每个文件针对一个算子, 里面放着同一算子的多个版本,通常「复杂度递增、耗时递减」,文件头注释就是版本目录。以 dev/cuda/layernorm_forward.cu 为例(dev/cuda/layernorm_forward.cu:1-21):

版本思路
v1CPU 代码直译:并行到 (B,T),循环 C
v2并行到全部 (B,T,C)
v3cooperative groups 块内协作
v4单趟求均值方差(var = mean(x²) − mean(x)²)
v5一行一个 block

运行 ./layernorm_forward N 时:先跑 CPU 参考 → 跑第 N 版 GPU kernel → 比对正确性 → 再扫一 组 block size 计时。最快且正确的版本最后被「拷进」llmc/——所以 llmc/ 里的函数名带着 数字后缀(layernorm_forward_kernel6 就是这里的 v6 的直系后代)。

4.2 这套流程的价值

它把「优化 kernel」拆成了可审查的小步:每一步都有 CPU 对拍兜底、有计时数据说话、有版本留 档。dev/cuda/ 共 22 个算子文件(含 attention、matmul、fused classifier、adamw、甚至 nccl_all_reduce.cu),外加 dev/cpu/matmul_forward.c 的 CPU 端 tiling 实验和 benchmark_on_modal.py 的云端跑基准入口。

4.3 dev/ 其余角落

位置内容
dev/test/dataloader、outlier detector、device↔file IO 的小单测
dev/eval/导出 HF 格式、跑 LM-eval-harness 风格评测的脚本
dev/unistd.hWindows 移植层(unistd/glob 兼容)
dev/vislog.ipynb解析训练日志画 loss 曲线

5. Makefile:自动探测 + 特性开关

Makefile(290 行)的目标与开关一览(Makefile:247-290):

目标产物
train_gpt2 / test_gpt2CPU 版(clang/gcc + OpenMP)
train_gpt2cu / test_gpt2cuCUDA 主线(默认 bf16)
train_gpt2fp32cu / test_gpt2fp32cu冻结的 fp32 教学版
profile_gpt2cu剖析靶子

关键开关:

  • USE_CUDNN=1:启用 cuDNN flash attention(编译时间从几秒涨到约一分钟,Makefile:24-25)。
  • PRECISION=FP32:整链 fp32。
  • NO_MULTI_GPU=1:去掉 NCCL/MPI 依赖。
  • GPU 自动探测:有 nvidia-smi 就查询本机最低 compute capability 并生成对应 --generate-codeMakefile:49-61),也可 GPU_COMPUTE_CAPABILITY=80 手动指定。
  • -march=native 等 CFLAGS 用「试编译一个空 main」探测可用性再加(Makefile:69-83)。

6. 教学资产:doc/layernorm 与冻结文件

  • doc/layernorm/layernorm.md:官方教程,把 LayerNorm 从 PyTorch 语义一路推到手写反向再到 C 实现,配套 layernorm.py/layernorm.c。README 明确说它是「理解层实现的最佳起点」。
  • train_gpt2_fp32.cu / test_gpt2_fp32.cu:主线在「单卡 fp32」时代的冻结快照——更简单、更 便携,README 推荐给 CUDA 初学者。

7. 关键细节与坑

  • 对拍只在特定配置下严格。 debug_state 固定 B=8、T=64 的小批次(由 train_gpt2.py 决定); 换超参要重新生成。容差是「调出来的工程值」,CPU 2e-2、bf16 相对 0.079——别把对拍当数学证 明。
  • profile 假设各层同构。 num_layers = 1 的裁法(profile_gpt2.cu:57-58)意味着 Layer 0 独有的 encoder 行为、最后一层的 lnf 融合要靠对代码的理解去脑补。
  • cuDNN 默认关是因为它拖慢编译Makefile:24-25),而它是默认最快的 attention 路径—— 本地开发迭代和正式跑分应该用不同开关。
  • starter pack 要联网。 四个 .bin 要么 download_starter_pack.sh 下载,要么先跑 python train_gpt2.py 生成;没有它们,测试和 demo 都无法运行(README quick start)。
  • CI 的 GPU 测试依赖自托管 runner(ci_gpu.yml);fork 仓库里 GPU CI 默认不可用(inferred: 工作流按仓库环境配置)。

8. 代码地图

主题文件路径符号名
CPU 对拍test_gpt2.ccheck_tensormainexpected_losses
GPU 对拍test_gpt2.cucheck_tensormain
出题人train_gpt2.pywrite_modelwrite_statewrite_tokenizer
剖析靶子profile_gpt2.cumain
剖析自动化profile_gpt2cu.py(脚本,ncu 指标采集)
NVTX 标签llmc/cuda_common.hNvtxRangeNVTX_RANGE_FN
kernel 沙盒dev/cuda/*.cu每文件的 *_kernel1..Nmain
沙盒说明dev/cuda/README.md(文档)
教程doc/layernorm/layernorm.md(文档)
构建Makefile目标 train_gpt2cu 等;开关 USE_CUDNNPRECISIONNO_MULTI_GPU
CI.github/workflows/ci.ymldev/loss_checker_ci.py(配置与脚本)