llm.c:用纯 C/CUDA 从零训练 GPT-2/GPT-3 的极简项目完整指南
karpathy/llm.c 是 Andrej Karpathy(前 OpenAI 创始成员、前特斯拉 AI 高级总监、深度学习教育频道"Neural Networks: Zero to Hero"主讲人)开源的大模型训练项目,当前 Star 数约 31.2k,采用 MIT 许可证。这个项目的核心主张非常直白:你不需要 245MB 的 PyTorch,也不需要 107MB 的 cPython 运行时,只要用纯 C 与 CUDA 就能把 GPT-2 与 GPT-3 系列模型的预训练流程完整跑起来,而且在主代码路径上比 PyTorch Nightly 还要快大约 7%。它是 Karpathy 继 nanoGPT 之后的又一力作,区别在于 nanoGPT 仍然依赖 PyTorch 生态,而 llm.c 把所有算子、内存管理、优化器、通信全部用底层语言手写出来,让学习者第一次真正看到大模型训练的每一个字节在做什么。
一、项目官方信息
| 项目 | 信息 |
|---|---|
| GitHub 仓库 | karpathy/llm.c |
| PyTorch 参考实现 | train_gpt2.py(微调版 nanoGPT) |
| 主训练代码 | train_gpt2.cu(CUDA 主线) |
| CPU 参考实现 | train_gpt2.c(约 1000 行 fp32) |
| 入门教程 | doc/layernorm/layernorm.md |
| 讨论入口 | GitHub Discussions |
| Discord | Zero to Hero 的 #llmc 频道、GPU MODE 的 #llmdotc |
| 许可证 | MIT |
本系列封面使用仓库官方图,不另作替换。
二、项目背景与定位
Karpathy 明确说过,llm.c 是他"从零到英雄"教学体系的延续:nanoGPT 回答"用最少的代码怎么搭一个 GPT",llm.c 则回答"如果把框架完全剥掉,GPT 训练到底在底层做了什么"。项目当前的重点是预训练,目标是复现 GPT-2 与 GPT-3 这一系列模型;仓库里同时维护一份平行的 PyTorch 参考实现 train_gpt2.py,读者一眼就能认出它就是 nanoGPT 的轻微改动版,用来在每一步对比 C 代码的前向、反向、梯度是否与 PyTorch 一致。
Karpathy 对这个仓库的边界有非常明确的约束:根目录只维护 C 与 CUDA 主代码,其他语言的移植请放到独立仓库,他在 README 末尾的"Notable Forks"一节里统一收录。他还希望仓库同时承担两个角色:一是教育,二是实用。教育体现在 dev/cuda/ 目录——这是一个手写 kernel 的库,从最简单的矩阵乘、激活函数,一路写到 Flash Attention,每个 kernel 都带详细注释;实用体现在主训练路径要真的能训,至少要能复现 1.6B 量级的 GPT-2 训练,因此在必要时直接调用 cuBLAS、cuBLASLt、CUTLASS、cuDNN 等成熟库作为"专家上界",让手写 kernel 有一个可对比的性能基线。
三、核心特性逐条说明
- 纯 C/CUDA 实现。 不需要 PyTorch、TensorFlow、JAX 等任何深度学习框架,所有算子(矩阵乘、激活、归一化、注意力、AdamW、交叉熵)都手写在 C 或
.cu文件里,编译完就是一个独立的二进制,启动即用、显存占用可控。 - 比 PyTorch Nightly 快约 7%。 主线
train_gpt2.cu在主流 NVIDIA GPU 上已经跑赢 PyTorch Nightly,并且仍在持续优化;这个成绩是在不牺牲可读性的前提下拿到的。 - 约 1000 行的 CPU 参考实现。
train_gpt2.c是早期冻结的检查点,单文件、fp32、无 GPU 依赖,用 OpenMP 多线程就能跑,适合没有显卡的读者完整走一遍训练循环。 - 多 GPU 数据并行。 通过 MPI + NCCL 支持单机多卡,一行
mpirun -np <卡数> ./train_gpt2cu即可启动。 - 多节点训练。 提供三种 NCCL 初始化方式:OpenMPI 交换 NCCL id、共享文件系统初始化、TCP socket 初始化,适配普通机房与 SLURM HPC 集群。
- Flash Attention。 自 2024 年 5 月 1 日起集成 NVIDIA cuDNN 的 Flash Attention,默认关闭,编译时加
USE_CUDNN=1开启。 - 丰富的移植生态。 Notable Forks 一节列出了 Rust、Go、Java、Swift、Metal、Mojo、C#、Zig、Nim、OpenCL、WebGPU、AMD ROCm、Habana Gaudi2 等十几种语言与硬件的移植版。
- 配套测试。 内置单元测试,加载 debug state 后跑前向与 10 步 Adam 训练,逐位对比 C 路径与 PyTorch 参考实现的 loss,通过时打印
overall okay: 1。
四、环境要求
- 操作系统:Linux(推荐 Ubuntu 22.04),NVIDIA GPU,CUDA 12.x。
- 编译工具链:gcc、make、nvcc。
- CPU 版:只需要 gcc 与 OpenMP。
- 多 GPU:需要 OpenMPI(
openmpi-bin、openmpi-doc、libopenmpi-dev)与 NCCL(按官网网络安装器安装)。 - Flash Attention:需要 cuDNN 9 与 cuDNN frontend(纯头文件,clone 到 home 目录或当前目录即可,否则用
CUDNN_FRONTEND_PATH=指定)。 - 调试小技巧:把
make命令里的-O3换成-g,就能在 VS Code 等 IDE 里单步调试 C 代码。
五、快速开始:单 GPU fp32 版
如果你不想搞多机、不想搞混合精度,只是想学 CUDA,fp32 这一份"历史检查点"文件是最友好的入口。它们是 llm.c 早期冻结的版本,代码更简单、可移植性更好、更容易读懂:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2fp32cu
./train_gpt2fp32cu
download_starter_pack.sh 一键下载所有开工需要的 .bin 文件,包括:GPT-2 124M 的 fp32 权重与 bfloat16 权重、单元测试用的"debug state"(一小批数据以及对应的目标激活值与梯度)、GPT-2 tokenizer、以及已经 tokenize 好的 tinyshakespeare 数据集。如果不想跑脚本,也可以手动复现:pip install -r requirements.txt,然后 python dev/data/tinyshakespeare.py 与 python train_gpt2.py 重新生成同样的文件。
六、快速开始:CPU 版
README 里把这一节戏称为"我穷到一张 GPU 都没有"的章节。CPU 版是更早的检查点,代码更朴素:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
它会从 GPT-2 124M 权重初始化,在 tinyshakespeare 上用 AdamW 微调 40 步(batch size 4、上下文长度 64),评估验证集 loss,并采样一段文本。在 Karpathy 的 MacBook Pro(Apple Silicon M3 Max)上,单步约 1.3 秒,关键输出如下:
[GPT-2]
max_seq_len: 1024
vocab_size: 50257
num_layers: 12
num_heads: 12
channels: 768
num_parameters: 124439808
train dataset num_batches: 1192
val dataset num_batches: 128
num_activations: 73323776
val loss 5.252026
step 0: train loss 5.356189 (took 1452.121000 ms)
step 1: train loss 4.301069 (took 1288.673000 ms)
...
step 39: train loss 3.970751 (took 1323.779000 ms)
val loss 4.107781
采样出来的文本虽然语法仍有些古怪,但已经能看出莎士比亚的韵律,例如"Come Running Away, Greater conquer / With the Imperial blood / the heaviest host of the gods / into this wondrous world beyond..."。这足以说明整条训练管线是对的,只是 CPU 算力不允许你把它训到很远。
七、数据集
dev/data/ 目录下每个数据集一个 Python 脚本,负责下载、tokenize、写盘:
python dev/data/tinyshakespeare.py
输出大约是:
writing 32,768 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_val.bin
writing 305,260 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_train.bin
.bin 文件格式是:1024 字节短头 + 一串 uint16 token id(GPT-2 tokenizer)。C 代码直接 fopen 后按二进制读取即可,不需要任何解析库。同目录下还有 TinyStories 等其他数据集脚本。
八、单元测试
README 提供了一套简单但严格的对齐测试,用来确认 C 代码和 PyTorch 参考实现一致:
# CPU 测试
make test_gpt2
./test_gpt2
# GPU fp32 测试(不支持 cuDNN)
make test_gpt2cu PRECISION=FP32 && ./test_gpt2cu
# GPU 混合精度 + cuDNN 测试
make test_gpt2cu USE_CUDNN=1 && ./test_gpt2cu
测试会加载 gpt2_124M_debug_state.bin,先跑一次前向,对比 logits 与 loss,再用 Adam 连跑 10 步训练,确认每一步的 loss 都和 PyTorch 对齐。通过时打印 overall okay: 1。
九、Flash Attention 与 cuDNN 配置
自 2024 年 5 月 1 日起,llm.c 主线改用 cuDNN 提供的 Flash Attention。但 cuDNN 会把编译时间从几秒拉长到约一分钟,而且这条路径较新,所以默认关闭。开启方式:
make train_gpt2cu USE_CUDNN=1
在 Ubuntu 22.04 + CUDA 12.x 上最小安装 cuDNN 的命令:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install libcudnn9-dev-cuda-12
然后 clone cuDNN frontend 到 home 目录或当前目录;如果放在别处,编译时加 CUDNN_FRONTEND_PATH=/path/to/cudnn-frontend/include。
十、多 GPU 与多节点训练
单机多卡:
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
# 按官网装 NCCL
make train_gpt2cu
mpirun -np <GPU数量> ./train_gpt2cu
多节点训练有三种 NCCL 初始化方式,分别对应三个示例脚本:
- 用 OpenMPI 交换 NCCL id 并初始化 NCCL,见
scripts/multi_node/run_gpt2_124M_mpi.sh。 - 用共享文件系统初始化 NCCL,见
scripts/multi_node/run_gpt2_124M_fs.sbatch。 - 用 TCP socket 初始化 NCCL,见
scripts/multi_node/run_gpt2_124M_tcp.sbatch。
README 特别提醒:如果你的 SLURM 不支持 PMIx(slurm-wlm 已移除 PMIx 支持),就只能选第 2 或第 3 种;用 srun --mpi=list 可以确认。如果没有 SLURM,直接用 mpirun 启动第 1 种即可。三种方式没有优劣,只是为了适配不同环境。
十一、超参扫描示例
README 给了一个在 4 卡机器上扫学习率的示例:用 screen 同时开 4 个会话,每张卡跑一个学习率:
#!/bin/bash
learning_rates=(3e-5 1e-4 3e-4 1e-3)
for i in {0..3}; do
export CUDA_VISIBLE_DEVICES=$i
screen -dmS "tr$i" bash -c "./train_gpt2cu -i data/TinyStories -v 250 -s 250 -g 144 -l ${learning_rates[$i]} -o stories$i.log"
done
日志文件 stories$i.log 记录了全部 loss,可以用 dev/vislog.ipynb 这个 notebook 直接解析并画图。
十二、项目结构与设计哲学
Karpathy 在 README 里写了一段关于"我想把这个仓库做成什么样"的话,非常值得抄录:
第一,dev/cuda/ 是 kernel 的图书馆,每个 kernel 都手写、都带文档,从最简单的矩阵乘一路到更复杂、更快的实现;如果你有不同 trade-off 的新 kernel,欢迎贡献。第二,主线代码必须真的快,至少要能复现 1.6B 的 GPT-2 训练,因此允许直接调用 cuBLAS、cuBLASLt、CUTLASS、cuDNN 等库;同时这些库也是"专家上界",你可以说自己的手写 kernel 达到 cuBLAS 的百分之多少,作为衡量单位。第三,根目录必须保持简单可读——如果一个 PR 只提升 2% 性能却要引入 500 行复杂 C 代码或冷门依赖,他会直接拒绝;但把 cuBLAS 作为默认矩阵乘是毫无疑问的选择,因为它是一行可解释的代码、且是极常见的依赖。第四,dev/ 目录相对自由,可以放教学、实验、甚至局部复杂的代码。
十三、Notable Forks 移植生态
README 收录了大量社区移植,按类别包括:AMD 显卡支持(anthonix/llm.c,支持 7900 XTX)、C#(azret/llm.cs、nietras/Llm.cs)、CUDA C++(gevtushenko/llm.c 用 CCCL 库)、C++/CUDA(zhangpiu/llm.cpp 用 Eigen,支持 CPU/CUDA)、WebGPU C++(AnswerDotAI/gpu.cpp,把 kernel 移植到 WGSL)、纯 C++(GaoYusong/llm.cpp,单头文件 tinytorch.hpp)、Go(joshcarp/llm.go)、Java(harryjackson/llm.java)、Metal(regrettable-username/llm.metal,原生 Metal Shading Language)、Mojo(dorjeduck/llm.mojo)、OpenCL(krrishnarraj/llm.c)、Rust(yijunyu/llm.rs、ToJen/llm.rs)、Swift(otabuzzman/llm.swift)、Zig(Saimirbaci/llm.zig)、Habana Gaudi2(abhilash1910/llm.tpc)、Nim(Vindaar/llm.nim)。
十四、常见问题
Q1:编译太慢怎么办? cuDNN 会把编译时间从几秒拉长到约一分钟,不需要 Flash Attention 就不要加 USE_CUDNN=1。
Q2:没有 GPU 能学吗? 能。CPU 版虽然慢,但能完整跑通初始化、前向、反向、AdamW、采样整条链路,理解原理完全够用。
Q3:和 nanoGPT 是什么关系? llm.c 的 train_gpt2.py 就是 nanoGPT 的微调版,作为 PyTorch 参考实现存在;llm.c 本身是 nanoGPT 的 C/CUDA 重写。
Q4:可以在主线里塞复杂优化吗? 不行。根目录要求简单可读,复杂优化请放到 dev/ 或独立 fork。
十五、相关链接
- 仓库:karpathy/llm.c
- 复现 GPT-2 124M 详细教程:Discussion #481
- 脚本说明:scripts/README.md
- 入门教程:doc/layernorm/layernorm.md
- 讨论区:GitHub Discussions
- Discord:Zero to Hero
#llmc - 许可证:MIT
llm.c 是 Karpathy 把"大模型底层完全透明化"这一理念落到工程里的代表作:它不依赖任何框架、编译完就是一个二进制、跑起来比 PyTorch Nightly 还快 7%、同时配套了一份约 1000 行的 CPU 参考实现和一份注释详尽的手写 kernel 库。如果你想真正学 CUDA 编程、想把 GPT-2 的前向反向、AdamW 优化器、分布式通信逐行读一遍,这个项目是目前公开世界里最好的教材。
十六、为什么这个项目值得花时间读
很多深度学习框架把"矩阵乘 + 归一化 + 注意力 + 优化器"封装成一行调用,好处是开发快,代价是你看不见每一个算子在显存里怎么布局、一次反向传播到底调了多少次 kernel、多卡之间梯度是怎么切分和同步的。llm.c 的价值就在于把这些都摊开:它的主文件 train_gpt2.cu 虽然接近一千行,但每个函数都对应一个你在课本上见过的算子,读完之后你会对"训练一个 GPT 到底需要写多少行代码"有一个非常具体的心理锚点——答案是比大多数人想象的少得多,而且其中相当一部分代码是为了对齐 PyTorch 的数值行为、而不是为了实现新算法。
另一个值得读的原因是它的"专家上界"哲学。Karpathy 没有为了纯粹而拒绝 cuBLAS、cuDNN,而是把它们当成默认快路径,同时在 dev/cuda/ 里保留手写 kernel 作为对照。这种写法对工程实践很有启发:你不需要在"全手写"和"全调库"之间二选一,可以先用库跑出最快基线,再在关键瓶颈上逐步替换成手写 kernel,并用单元测试保证数值一致。llm.c 的测试脚本就是这么做的——它会加载一份预先生成的 debug state,连跑前向和十步 Adam,逐位比较 loss,确保你改的 kernel 没有引入数值误差。
对于想入门 CUDA 但又觉得官方 samples 太零散的人,llm.c 提供了一条非常清晰的学习路径:先从 doc/layernorm/layernorm.md 这个单算子教程开始,把 LayerNorm 这一层在 C 里完整实现一遍;再读 train_gpt2.c 这份 CPU 参考实现,理解整个计算图在没有 GPU 时是什么样子;然后读 train_gpt2.cu 主线,看每一个 C 函数是怎么被替换成 CUDA kernel 的;最后再去 dev/cuda/ 目录挑自己感兴趣的 kernel 深入,例如矩阵乘的不同 tiling 策略、Flash Attention 的分块方式、AdamW 的 fused kernel。这条路径不需要你先啃完一整本 CUDA 编程指南,边读边跑、边跑边改,是 Karpathy 一贯的教学风格。
最后要提醒的是,llm.c 是一个快速演进的项目,主线代码几乎每个月都会有变化,特别是混合精度、cuDNN Flash Attention、多节点这几块。读的时候建议先把 fp32 历史检查点版本(train_gpt2fp32cu)和 CPU 版本(train_gpt2.c)读透,再回头看主线,这样不会被最新的优化细节淹没。
如果你是做推理引擎、大模型训练框架、或者只是想在面试里把"手写一个 GPT 训练循环"讲清楚,llm.c 都是目前性价比最高的参考资料:它短到一个下午能读完,快到能和 PyTorch 正面对比,又有完整的多卡多节点支持和社区移植生态。它不是要替代任何成熟框架,而是要让每一个愿意花时间的人,都有机会亲手触摸到大模型训练的底层。这也是 Karpathy 一贯的教学初心:把复杂的东西拆开给你看,让你知道它并不神秘。