跳到主要内容

数据截至 (上游 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 d12d48(124M1.5B)及非官方到 12.2B;GPT-3 c768~c12288 描述符(train_gpt2.cu:519-560
多卡/多机NCCL 通信,DDP(zero_stage=0)与 ZeRO-1 优化器状态分片(zero_stage=1
评估验证集 loss、HellaSwag 多项选择准确率
断点续训保存优化器/数据加载/RNG 全状态,bit-perfect 恢复(-y 1
配套 Pythontrain_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 + launcherllmc/*.cuh(encoder/layernorm/matmul/attention/gelu/fused_classifier/adamw/global_norm)
多卡支持NCCL 初始化(mpi/tcp/fs)、DDP 与 ZeRO-1 梯度归约llmc/zero.cuh
数据加载.bin 分片校验与读取、分布式切分、两级 shufflellmc/dataloader.h
分词器GPT-2 tokenizer 纯解码(token id → 字符串)llmc/tokenizer.h
公共 GPU 设施floatX/Packed128/warp 归约/随机舍入/错误检查llmc/cuda_common.hllmc/cuda_utils.cuhllmc/cublas_common.h
PyTorch 参考写出权重/调试状态/tokenizer .bin,供对拍train_gpt2.py
测试/剖析C 与 PyTorch 对拍;ncu 剖析入口test_gpt2.ctest_gpt2.cuprofile_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)

这条线最值得记住的两点:

  1. 没有 autograd。 反向是手写的、和前向完全对称的一段代码;参数梯度一律 +=(为了梯度累 积),激活梯度一律 =(更快),只有残差流例外(llmc/layernorm.cuh:1-10 的头注释专门讲这个 约定)。
  2. 混合精度是真·混合。 权重按 bf16 存储和计算,但优化器在 fp32 的 master copy 上更新,再靠随 机舍入(stochastic rounding)写回 bf16——这套机制让 bf16 训练不损失精度且可复现(见第 4 章)。

3. 阅读地图(建议顺序)

五章由浅入深。时间有限就读 01 → 02 → 04,正好覆盖「数学 → kernel → 工程化」三层。

顺序章节讲什么适合谁
101-cpu-reference.mdtrain_gpt2.c 全文导览:内存布局、每层前向/反向、AdamW、主循环所有人必读,这是全仓库的「地图」
202-cuda-kernels.mdllmc/ 的 kernel 层:基础设施、融合、attention、fused classifier、确定性反向想学 CUDA 写法的人
303-data-and-tokenizer.md.bin 格式、DataLoader 分布式数学、EvalLoader、tokenizer关心数据管线的人
404-multigpu-mixed-precision.mdbf16 混合精度、随机舍入、NCCL/ZeRO-1、梯度累积/裁剪、MFU、断点续训关心大规模训练工程的人
505-test-profile-dev.md对拍测试、ncu 剖析、dev/cuda kernel 演进方法、构建系统想动手改 kernel 的人

4. 巧妙之处(可借鉴的技术)

每条先白话点出妙处,细节在对应章节。

  1. 一整块内存 + 指针切分。 16 个参数张量、23 个激活张量各只分配一次大内存,再按尺寸把指针依 次摆进去(train_gpt2.c:580-598train_gpt2.c:658-675)。没有 allocator,没有生命周期管理, 却零碎片、零泄漏风险——GPT-2 的内存图因此一目了然。→ 第 1 章
  2. fused classifier:永不物化概率。 B×T×50257 的 probs 张量从不真正写出;softmax、交叉熵、 以及反向第一步(p - indicator)在同一个 kernel 里完成(llmc/fused_classifier.cuh:70-136)。 这是全仓库最省显存的一笔。→ 第 2 章
  3. 跨 block 边界的 kernel 融合。 fused_residual_forward5 把「残差相加 + 下一层 LayerNorm」 融成一个 kernel,前向循环里相邻两层之间的接缝因此被消掉(train_gpt2.cu:736-750)。→ 第 2 章
  4. 确定性 + 性能兼得的 wte 反向。 词嵌入反向先在 CPU 把 (token, 通道组) 分桶排序,大桶先跑, 每个梯度元素只写一次——既消除 atomicAdd 的不确定性又避免了长尾(llmc/encoder.cuh:169-229)。→ 第 2 章
  5. bf16 也能 bit-perfect 续训。 随机舍入的种子来自保存的 RNG 状态,resume 时回放同一序列, 从 fp32 master weights 重新舍入出和崩溃前一模一样的 bf16 权重(train_gpt2.cu:1283-1287)。→ 第 4 章
  6. LayerNorm 反向的「最后一个 block 收尾」。 各 block 把 dbias/dweight 部分和写到 scratch, 用 atomicInc 计数,最后完成的 block 顺手做全局归约——省掉一次 kernel launch(llmc/layernorm.cuh:364-383)。→ 第 2 章
  7. 测试即「对拍」。 没有框架级测试基建,就让 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-1135train_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.cnanoGPT(同作者的 Python 前身)PyTorch 训练(以 train_gpt2.py 为例)
语言/依赖C/CUDA,仅 cuBLAS/cuDNN/NCCLPython + PyTorchPython + PyTorch 全家桶
抽象层无 autograd,反向手写PyTorch autogradautograd + dispatcher + ATen
反向代码量与前向对称,逐层手写loss.backward() 一行一行,底下 30 层派发
速度比 PyTorch Nightly 快约 7%(README 自述)基准本身基准
教学定位「框架底下在干什么」「最小可读的 GPT 训练」生产工具
多卡手写 NCCL + ZeRO-1DDPDDP/FSDP 现成
推理无 KV cache,教学级有简单 generate视实现

一句话:nanoGPT 回答「GPT 训练最少需要多少代码」,llm.c 回答「这些代码底下每一行到底是什么」。


7. 代码地图(导航索引)

主题文件路径符号名
CPU 参考:层实现train_gpt2.cencoder_forwardlayernorm_forwardmatmul_forwardattention_forwardgelu_forwardsoftmax_forwardcrossentropy_softmax_backward
CPU 参考:模型与训练train_gpt2.cgpt2_build_from_checkpointgpt2_forwardgpt2_backwardgpt2_updatemain
GPU 主线:编排train_gpt2.cugpt2_forwardgpt2_backward_and_reducegpt2_updategpt2_validatemain
kernel:LayerNorm/残差llmc/layernorm.cuhlayernorm_forward_kernel6fused_residual_forward_kernel5layernorm_backward_kernel10
kernel:注意力llmc/attention.cuhllmc/cudnn_att.cppattention_forwardsoftmax_forward_kernel5attention_backwardattention_forward_cudnn
kernel:矩阵乘llmc/matmul.cuhmatmul_cublasltmatmul_backwardmatmul_backward_bias_kernel9
kernel:分类器/编码器/优化器llmc/fused_classifier.cuhllmc/encoder.cuhllmc/adamw.cuhfused_classifier_kernel5encoder_backwardadamw_kernel3
GPU 基础设施llmc/cuda_utils.cuhllmc/cuda_common.hPacked128warpReduceSumblockReducestochastic_rounding
多卡llmc/zero.cuhmulti_gpu_config_initmulti_gpu_async_reduce_gradientset_zero_configs
数据/分词llmc/dataloader.hllmc/tokenizer.hdataloader_initdataloader_next_batchevalloader_stat_lossestokenizer_decode
对拍与剖析test_gpt2.ctest_gpt2.cuprofile_gpt2.cucheck_tensormain
PyTorch 出题人train_gpt2.pywrite_modelwrite_statewrite_tokenizer