数据截至 (上游 commit f1e2ace65149)
llm.c — 纯 C/CUDA 手写 GPT-2 训练栈
30 秒导读: llm.c 是 Karpathy 用纯 C/CUDA 写的 GPT-2/GPT-3 预训练项目——不用 PyTorch,把深度学 习框架在底下做的事全部手写出来。它同时是一本教材和一套真快起来的训练栈:
train_gpt2.c用 ~1000 行干净 C 代码给出可读参考实现,train_gpt2.cu+llmc/是手写 CUDA kernel 的主线, README 宣称比 PyTorch Nightly 还快约 7%(README.md:3)。
1. 这是什么(零基础也能懂)
一句话定义
llm.c 是一个不依赖任何深度学习框架的 LLM 预训练项目:GPT-2 的前向传播、反向传播、AdamW 优化 器、数据加载、分词、多卡通信,全部用 C 和 CUDA 手写,复现 GPT-2(124M~1.5B)和 GPT-3 小系列。
它要解决谁的什么问题
假设你用过 PyTorch 训练模型,但心里一直有个疑问:loss.backward() 这一行底下到底发生了什么?
- PyTorch 的实现埋在「30 层深的派发器 + 自动生成的 CUDA 代码」里,根本没法读(
doc/layernorm/layernorm.md开篇就在吐槽这一点)。 - llm.c 的答案:把每一层的手写前向/手写反向全部摊开,每个 kernel 从「能看懂的朴素版」迭代到「逼近 cuBLAS 的优化版」,过程留在
dev/cuda/里给你看。
所以它的读者有两类:想搞懂「训练一个 Transformer 到底要算哪些东西」的人,和想学「怎么把同样的数学写成快 CUDA kernel」的人。它同时也证明自己不是玩具——能真的复现 GPT-2 训练,比如 124M 模型在 8×A100 上约 94 分钟、约 $20 训完 10B token(scripts/run_gpt2_124M.sh:1-6)。
它能做什么
| 能力 | 具体形态 |
|---|---|
| CPU 参考训练 | train_gpt2.c 单文件,fp32 + OpenMP,笔记本可跑(40 步微调 demo) |
| GPU 训练主线 | train_gpt2.cu,bf16 混合精度(默认)/ fp32 / fp16 编译期切换 |
| 模型规格 | GPT-2 d12train_gpt2.cu:519-560) |
| 多卡/多机 | NCCL 通信,DDP(zero_stage=0)与 ZeRO-1 优化器状态分片(zero_stage=1) |
| 评估 | 验证集 loss、HellaSwag 多项选择准确率 |
| 断点续训 | 保存优化器/数据加载/RNG 全状态,bit-perfect 恢复(-y 1) |
| 配套 Python | train_gpt2.py(nanoGPT 改来的 PyTorch 平行实现,兼「出题人」)、train_llama3.py |
用起来什么样
最小路径(GPU、混合精度主线):
# 摘自 README.md「quick start」,命令为真实用法
./dev/download_starter_pack.sh # 下载权重/tokenizer/tinyshakespeare 数据等 .bin
make train_gpt2cu USE_CUDNN=1 # 编译 GPU 主线(cuDNN flash attention 可选)
mpirun -np 8 ./train_gpt2cu \ # 8 卡训练;单卡直接 ./train_gpt2cu
-i "dev/data/tinyshakespeare/tiny_shakespeare_train.bin" \
-j "dev/data/tinyshakespeare/tiny_shakespeare_val.bin" \
-e "d12" -b 64 -t 1024 -d 524288
没有 GPU 也有退路:make train_gpt2 && OMP_NUM_THREADS=8 ./train_gpt2 跑 CPU 参考实现;想学 CUDA
还有「冻结在早期」的 fp32 单卡文件 train_gpt2_fp32.cu(README 明确说这是教学用的历史快照)。
一句话直觉
把 PyTorch 当成一座精装公寓:住着舒服,但墙里的水管电线全看不见。llm.c 是把墙全拆掉的毛坯房 ——autograd、ATen、dispatch 全不存在,每一根管子(每个 kernel、每次显存分配、每个通信调用)都 裸露在外,你可以顺着管子一根根摸过去。
2. 顶层全景(它大概怎么转)
2.1 仓库布局:同一份数学的四种形态
llm.c 仓库(102 个文件,其中 59 个 C/CUDA)
│
┌──────────────┬──────────────┼───────────────┬────────────────┐
▼ ▼ ▼ ▼ ▼
train_gpt2.c train_gpt2.cu llmc/ dev/ train_gpt2.py
CPU 参考实现 GPU 训练主线 CUDA kernel 库 数据预处理+ PyTorch 平行实现
(先读这个) (生产用) 每层一个 .cuh kernel 实验室 (对拍基准/出题人)
怎么读这张图: 左两列是「同一套 GPT-2 数学」的两代实现 ——.c 求可读、.cu 求快;llmc/ 是
.cu include 的 kernel 仓库;dev/ 是支撑系统(数据脚本、kernel 开发沙盒、评测);最右的 Python
文件不是训练主力,而是基准与数据源:权重、调试状态、tokenizer 都由它写出。
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| CPU 参考实现 | 一个文件写完 GPT-2 全部前向/反向/优化器 | train_gpt2.c |
| GPU 训练主线 | main、CLI、前/反向编排、checkpoint、评估 | train_gpt2.cu |
| kernel 库 | 每层 forward/backward 的 CUDA kernel + launcher | llmc/*.cuh(encoder/layernorm/matmul/attention/gelu/fused_classifier/adamw/global_norm) |
| 多卡支持 | NCCL 初始化(mpi/tcp/fs)、DDP 与 ZeRO-1 梯度归约 | llmc/zero.cuh |
| 数据加载 | .bin 分片校验与读取、分布式切分、两级 shuffle | llmc/dataloader.h |
| 分词器 | GPT-2 tokenizer 纯解码(token id → 字符串) | llmc/tokenizer.h |
| 公共 GPU 设施 | floatX/Packed128/warp 归约/随机舍入/错误检查 | llmc/cuda_common.h、llmc/cuda_utils.cuh、llmc/cublas_common.h |
| PyTorch 参考 | 写出权重/调试状态/tokenizer .bin,供对拍 | train_gpt2.py |
| 测试/剖析 | C 与 PyTorch 对拍;ncu 剖析入口 | test_gpt2.c、test_gpt2.cu、profile_gpt2.cu |
| kernel 实验室 | 每个 kernel 的多个版本 + CPU 对拍 + block size 扫描 | dev/cuda/ |
2.3 主线走一遍(一个训练 step,GPU 主线)
对应 train_gpt2.cu 的 main 循环(train_gpt2.cu:1826-1864)。①~⑥ 是每步必做的事;箭头旁注
出真实符号名。
① 取数 dataloader_next_batch (.bin 分片 → B*T+1 个 uint16 token)
│
▼
② 前向 gpt2_forward (encoder → L×Transformer block → LN_f → logits)
│ 每层内部已做 kernel 融合与激活重排
▼
③ 损失+开链 fused_classifier (一个 kernel 算 softmax/CE loss,并就地写出 dlogits)
│
▼
④ 反向 gpt2_backward_and_reduce (逆序逐层 += 梯度;最后一个 micro-step 发 NCCL 归约)
│
▼
⑤ 裁剪 gpt2_calculate_grad_norm → clip 1.0 (z-score 异常时整步跳过)
│
▼
⑥ 更新 gpt2_update (AdamW 在 fp32 master weights 上算,随机舍入写回 bf16)
这条线最值得记住的两点:
- 没有 autograd。 反向是手写的、和前向完全对称的一段代码;参数梯度一律
+=(为了梯度累 积),激活梯度一律=(更快),只有残差流例外(llmc/layernorm.cuh:1-10的头注释专门讲这个 约定)。 - 混合精度是真·混合。 权重按 bf16 存储和计算,但优化器在 fp32 的 master copy 上更新,再靠随 机舍入(stochastic rounding)写回 bf16——这套机制让 bf16 训练不损失精度且可复现(见第 4 章)。
3. 阅读地图(建议顺序)
五章由浅入深。时间有限就读 01 → 02 → 04,正好覆盖「数学 → kernel → 工程化」三层。
| 顺序 | 章节 | 讲什么 | 适合谁 |
|---|---|---|---|
| 1 | 01-cpu-reference.md | train_gpt2.c 全文导览:内存布局、每层前向/反向、AdamW、主循环 | 所有人必读,这是全仓库的「地图」 |
| 2 | 02-cuda-kernels.md | llmc/ 的 kernel 层:基础设施、融合、attention、fused classifier、确定性反向 | 想学 CUDA 写法的人 |
| 3 | 03-data-and-tokenizer.md | .bin 格式、DataLoader 分布式数学、EvalLoader、tokenizer | 关心数据管线的人 |
| 4 | 04-multigpu-mixed-precision.md | bf16 混合精度、随机舍入、NCCL/ZeRO-1、梯度累积/裁剪、MFU、断点续训 | 关心大规模训练工程的人 |
| 5 | 05-test-profile-dev.md | 对拍测试、ncu 剖析、dev/cuda kernel 演进方法、构建系统 | 想动手改 kernel 的人 |
4. 巧妙之处(可借鉴的技术)
每条先白话点出妙处,细节在对应章节。
- 一整块内存 + 指针切分。 16 个参数张量、23 个激活张量各只分配一次大内存,再按尺寸把指针依
次摆进去(
train_gpt2.c:580-598、train_gpt2.c:658-675)。没有 allocator,没有生命周期管理, 却零碎片、零泄漏风险——GPT-2 的内存图因此一目了然。→ 第 1 章 - fused classifier:永不物化概率。 B×T×50257 的 probs 张量从不真正写出;softmax、交叉熵、
以及反向第一步(
p - indicator)在同一个 kernel 里完成(llmc/fused_classifier.cuh:70-136)。 这是全仓库最省显存的一笔。→ 第 2 章 - 跨 block 边界的 kernel 融合。
fused_residual_forward5把「残差相加 + 下一层 LayerNorm」 融成一个 kernel,前向循环里相邻两层之间的接缝因此被消掉(train_gpt2.cu:736-750)。→ 第 2 章 - 确定性 + 性能兼得的 wte 反向。 词嵌入反向先在 CPU 把 (token, 通道组) 分桶排序,大桶先跑,
每个梯度元素只写一次——既消除 atomicAdd 的不确定性又避免了长尾(
llmc/encoder.cuh:169-229)。→ 第 2 章 - bf16 也能 bit-perfect 续训。 随机舍入的种子来自保存的 RNG 状态,resume 时回放同一序列,
从 fp32 master weights 重新舍入出和崩溃前一模一样的 bf16 权重(
train_gpt2.cu:1283-1287)。→ 第 4 章 - LayerNorm 反向的「最后一个 block 收尾」。 各 block 把 dbias/dweight 部分和写到 scratch,
用
atomicInc计数,最后完成的 block 顺手做全局归约—— 省掉一次 kernel launch(llmc/layernorm.cuh:364-383)。→ 第 2 章 - 测试即「对拍」。 没有框架级测试基建,就让 PyTorch 把输入、期望 logits/loss/梯度全部写进
.bin,C 侧读进来逐张量比;10 步训练 loss 曲线硬编码在测试里(
test_gpt2.c:89-100)。→ 第 5 章
5. 边界与局限
诚实清单,全部能在代码里核实:
- 只做预训练。 没有 SFT/RLHF;tokenizer 只支持解码不支持编码,文件头注释明说「想 prompt 模型
得另加编码,正则在 C 里不好搞」(
llmc/tokenizer.h:1-7)。 - 架构硬编码 GPT-2。 GELU、可学习位置编码、无 RoPE、无 KV cache。GPT-3 复现只是换尺寸,注释
承认用的是 dense attention 而非 GPT-3 的 dense/banded 交替(
train_gpt2.cu:539-541)。 - 推理是教学级的。 采样循环每生成一个 token 就把整段序列重新前向一遍,注释自曝「very
wasteful」「we don't have a KV cache」(
train_gpt2.c:1134-1135、train_gpt2.cu:1771)。 - 数据管线假设朴素。 token 必须是 uint16(词表 < 65536);每个分片至少要装得下全体 rank 的一
个批次(
llmc/dataloader.h:186)。 - ZeRO 只到 stage 1。 stage 2/3 在
set_zero_configs里直接打印「not yet supported」并退回 DDP(llmc/zero.cuh:558-580);FP16 编译路径存在但没有梯度缩放,checkpoint 加载直接拒绝 fp16 (train_gpt2.cu:459-463)。 - 项目基本冻结。 最后一次合并在 2025-05;它是一个完成的教学品,不是持续演进的框架。
6. 横向对比
| 维度 | llm.c | nanoGPT(同作者的 Python 前身) | PyTorch 训练(以 train_gpt2.py 为例) |
|---|---|---|---|
| 语言/依赖 | C/CUDA,仅 cuBLAS/cuDNN/NCCL | Python + PyTorch | Python + PyTorch 全家桶 |
| 抽象层 | 无 autograd,反向手写 | PyTorch autograd | autograd + dispatcher + ATen |
| 反向代码量 | 与前向对称,逐层手写 | loss.backward() 一行 | 一行,底下 30 层派发 |
| 速度 | 比 PyTorch Nightly 快约 7%(README 自述) | 基准本身 | 基准 |
| 教学定位 | 「框架底下在干什么」 | 「最小可读的 GPT 训练」 | 生产工具 |
| 多卡 | 手写 NCCL + ZeRO-1 | DDP | DDP/FSDP 现成 |
| 推理 | 无 KV cache,教学级 | 有简单 generate | 视实现 |
一句话:nanoGPT 回答「GPT 训练最少需要多少代码」,llm.c 回答「这些代码底下每一行到底是什么」。
7. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| CPU 参考:层实现 | train_gpt2.c | encoder_forward、layernorm_forward、matmul_forward、attention_forward、gelu_forward、softmax_forward、crossentropy_softmax_backward |
| CPU 参考:模型与训练 | train_gpt2.c | gpt2_build_from_checkpoint、gpt2_forward、gpt2_backward、gpt2_update、main |
| GPU 主线:编排 | train_gpt2.cu | gpt2_forward、gpt2_backward_and_reduce、gpt2_update、gpt2_validate、main |
| kernel:LayerNorm/残差 | llmc/layernorm.cuh | layernorm_forward_kernel6、fused_residual_forward_kernel5、layernorm_backward_kernel10 |
| kernel:注意力 | llmc/attention.cuh、llmc/cudnn_att.cpp | attention_forward、softmax_forward_kernel5、attention_backward、attention_forward_cudnn |
| kernel:矩阵乘 | llmc/matmul.cuh | matmul_cublaslt、matmul_backward、matmul_backward_bias_kernel9 |
| kernel:分类器/编码器/优化器 | llmc/fused_classifier.cuh、llmc/encoder.cuh、llmc/adamw.cuh | fused_classifier_kernel5、encoder_backward、adamw_kernel3 |
| GPU 基础设施 | llmc/cuda_utils.cuh、llmc/cuda_common.h | Packed128、warpReduceSum、blockReduce、stochastic_rounding |
| 多卡 | llmc/zero.cuh | multi_gpu_config_init、multi_gpu_async_reduce_gradient、set_zero_configs |
| 数据/分词 | llmc/dataloader.h、llmc/tokenizer.h | dataloader_init、dataloader_next_batch、evalloader_stat_losses、tokenizer_decode |
| 对拍与剖析 | test_gpt2.c、test_gpt2.cu、profile_gpt2.cu | check_tensor、main |
| PyTorch 出题人 | train_gpt2.py | write_model、write_state、write_tokenizer |