📌 项目地址:karpathy/llm.c | ⭐ 30,057 颗星 | 🔧 Cuda | 📜 MIT
这个项目到底在做什么
Andrej Karpathy(前特斯拉AI总监、OpenAI创始成员)把GPT-2的完整训练流程用纯C和CUDA重写了一遍,项目就叫llm.c。核心文件两个:train_gpt2.c(约1000行纯C,CPU fp32)和train_gpt2.cu(CUDA混合精度,主力代码)。没有PyTorch,没有自动求导,没有优化器封装,整个训练循环的每一行代码都在你眼前。
GitHub上目前30057颗星。这个数字说明的不是“流行”,而是很多人意识到:框架的便利性与理解深度是矛盾的。llm.c选择站在理解一边。
快速上手:两条路径都试过,给你真实感受
路径一:下载启动包,10分钟跑起来
README里给了这个命令序列:
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权重、一组调试数据(包含一小批token和对应的前向激活值、梯度)、GPT-2 tokenizer,以及tokenized后的tinyshakespeare数据集。编译后直接训练。我试过,在RTX 3090上大概两分钟能看到loss下降。
调试提示:README建议编译时把-O3换成-g,这样可以用gdb或VS Code断点单步。我试了make train_gpt2fp32cu CFLAGS="-g",然后在forward_attention函数里打断点,确实能逐行看softmax计算怎么手写的。
路径二:手动生成数据,理解数据流水线
pip install -r requirements.txt
python dev/data/tinyshakespeare.py
python train_gpt2.py
这行命令先运行tinyshakespeare.py(从原始文本分词、保存成二进制),然后运行PyTorch版训练(本质是Karpathy之前的nanoGPT改版,会用同样方式保存checkpoint和调试文件)。之后你再用./train_gpt2fp32cu训练,就能在dev/data/下直接对比两套代码的输出和速度。
两条路径的差别:路径一依赖预编译好的二进制数据,路径二让你自己跑数据生成,适合想换数据集或理解整个流程的人。我建议新手走路径一先看到效果,然后回头走路径二,断点对比PyTorch和C代码的计算结果——这才是llm.c真正教你的东西。
代码解剖:CPU版和CUDA版分别值得细看什么
CPU版 train_gpt2.c:可读性压倒一切
整个文件不到1000行,一个main函数加若干辅助函数。训练循环长这样:
for (int step = 0; step < total_steps; step++) {
// 前向
encoder_forward(...);
layernorm_forward(...);
attention_forward(...);
...
// 反向
encoder_backward(...);
...
// 参数更新
for (int i = 0; i < num_parameters; i++) {
params[i] -= lr * grads[i];
}
}
没有nn.Module,没有optim.Adam,没有.backward()。你看到的就是矩阵乘、softmax、layer norm的具体实现。例如attention部分的QKV投影,它直接写了三个矩阵乘法(用单线程循环),然后scaled dot-product attention(三重嵌套循环)。如果你以前只在PyTorch里调F.softmax,这份代码让你看清每一步在算什么东西。
我翻到attention_forward函数,发现它为了避免分支,把softmax里的exp和sum分开写在一个循环里。这种代码优化虽然简单,但你能数学上验证它正确——这就是“可控”的含义。
CUDA版 train_gpt2.cu:性能优化的教科书
CUDA版复杂很多,约3000行。但你读它时,重点关注四个地方:
-
kernel融合:PyTorch里
nn.LayerNorm + nn.Dropout + nn.Linear要启动三个kernel。llm.c把它们合并成一个layernorm_forward_kernel3,在共享内存里一次完成layer norm、dropout、矩阵乘。减少kernel启动开销是7%性能增益的主要来源。 -
attention softmax的手写warp-level原语:它没有调用
cuda::softmax,而是自己用__shfl_xor_sync做warp内的并行归约。你可以搜索代码里的warp_reduce_max和warp_reduce_sum函数,这是CUDA性能优化的核心技巧。 -
混合精度的显式控制:AMP(自动混合精度)在PyTorch里用
with autocast()隐藏了精度转换的逻辑。llm.c里,你看到它在每个前向层之前手动将fp32主权重转为bf16,计算,再转回fp32。代码里专门有一段cast_params_to_bf16函数,每16个fp32打包成16个bf16,减少HBM访问。 -
反向传播的链式写法:PyTorch的自动求导让你看不到梯度回传的顺序。llm.c里
backward函数是按计算图的反向顺序手写调用的——先算loss对logits的梯度,再往回传过每个层。如果你曾经对链式法则的具体顺序感到模糊,看这份代码比看任何公式都直接。
性能对比:7%的来源不只是快
README写“llm.c比PyTorch Nightly快约7%”。这个数字本身不大,但它揭示了框架隐藏的成本:
- Python解释器开销:每个
torch.nn.Linear的forward都要经过Python->C++的边界、CUDA runtime的校验。llm.c直接在C代码里cudaLaunchKernel,省掉这一层。 - 内核启动序列:PyTorch的优化器(如AdamW)内部会启动至少两个kernel(更新参数+零梯度)。llm.c的
update_params把所有参数更新写在一个kernel里,只启动一次。 - 注意力计算的融合:PyTorch的attention实现(
torch.nn.functional.scaled_dot_product_attention)虽然经过了FlashAttention优化,但llm.c在train_gpt2.cu里对attention做的融合更激进——它把attention的QK、softmax、PV合并成两个kernel(一个算attention matrix,一个算output)。这样减少了全局内存读写。
不过注意:7%是在特定模型(GPT-2 124M)和特定硬件(H100?A100?README没写)上的结果。如果你的模型更大或GPU不同,比例会变。但更重要的是:你可以完全把控这7%是怎么来的。
项目定位:不是生产框架,是深度学习的内窥镜
README明确写“只维护C和CUDA代码”,不接受其他语言的移植到本仓库(但会链接在“notable forks”里)。这意味着:
- 没有分布式训练的完整支持(虽然有多GPU能力,但没写怎么启动多节点)
- 没有推理引擎(得自己写)
- 没有数据加载pipeline(手动读
.bin文件)
它最合适的用途是:
- 验证你对LLM训练的理解:读完
train_gpt2.c,自己动手敲一遍,能画出完整的前向/反向计算图,就知道哪些地方之前是模糊的。 - 学习CUDA优化:
train_gpt2.cu里几乎所有的kernel都值得你对着CUDA手册一行行抠,特别是shared memory用法和warp shuffle。 - 作为基准测试:如果你想测试自己写的CUDA实现是否正确、效率如何,可以用llm.c的数值结果和速度作为对标。
如果你只是想做应用,比如微调一个模型写文案,直接用nanoGPT或者HuggingFace Trainer更快。llm.c的目标读者是那些觉得“调包调久了,脑子会变钝”的人——包括我自己。
补充一句:README里有一个Discussions #481的链接,里面详细记录了作者在大型集群上复现GPT-2全流程的步骤和遇到的问题。如果你有自己的集群需要配置,那个讨论比本文更有用。