Loading...

文章背景图

llm.c:纯 C/CUDA 从零训练 GPT 学习指南

2025-05-11
0
- 字
- 分钟
|

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 初始化方式,分别对应三个示例脚本:

  1. 用 OpenMPI 交换 NCCL id 并初始化 NCCL,见 scripts/multi_node/run_gpt2_124M_mpi.sh。
  2. 用共享文件系统初始化 NCCL,见 scripts/multi_node/run_gpt2_124M_fs.sbatch。
  3. 用 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。

十五、相关链接

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 一贯的教学初心:把复杂的东西拆开给你看,让你知道它并不神秘。

评论交流

文章目录