基于纯 C/CUDA 的简易大语言模型实现,无需 245MB 的 PyTorch 或 107MB 的 cPython。当前重点在于预训练,特别是复现 GPT-2 和 GPT-3 系列模型,同时提供了并行的 PyTorch 参考实现 train_gpt2.py。你可以看出这个文件是 nanoGPT(我之前的项目)的微调版本。目前,llm.c 比 PyTorch Nightly 快约 7%。除了 train_gpt2.cu 中的前沿主线代码外,我们还有一个约 1000 行简洁代码的 CPU fp32 参考实现 train_gpt2.c。我希望本仓库仅维护 C 和 CUDA 代码。欢迎移植到其他语言或仓库,但应在单独的仓库中完成,我会在下面的“值得关注的复刻”部分添加链接。开发者协调在 Discussions 和 Discord 中进行,可以加入 Zero to Hero 服务器的 #llmc 频道或 GPU MODE Discord 的 #llmdotc 频道。
当前最佳的入门方式是通过复现 GPT-2 (124M) 模型。详情请参阅 Discussion #481。我们可以在 llm.c 和并行的 PyTorch 实现中复现 GPT-2 和 GPT-3 系列的其他模型。请查看 scripts README。
调试提示:运行 make 命令构建二进制文件时,将 -O3 替换为 -g,以便在 IDE(如 vscode)中单步调试代码。
如果你不进行多节点训练,对混合精度不感兴趣,且想学习 CUDA,那么 fp32(旧版)文件可能适合你。这些文件是 llm.c 早期历史中的“检查点”并被冻结。它们更简单、更易移植,也可能更易理解。按如下方式运行单 GPU、fp32 代码:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2fp32cu
./train_gpt2fp32cu
download_starter_pack.sh 脚本提供快速简便的入门方式,下载一系列 .bin 文件帮助你快速上手。这些文件包括:1) GPT-2 124M 模型(fp32 和 bfloat16 格式),2) 用于单元测试的“调试状态”(一小批数据及目标激活值和梯度),3) GPT-2 分词器,以及 4) 分词后的 tinyshakespeare 数据集。你也可以不运行 .sh 脚本,手动创建这些文件:
pip install -r requirements.txt
python dev/data/tinyshakespeare.py
python train_gpt2.py
“我穷得连一张 GPU 都没有”的情况。你仍然可以享受观看 llm.c 训练的过程!但不要期望太高。和上面的 fp32 版本一样,CPU 版本是 llm.c 更早期的检查点,当时它只是用 C 编写的简单参考实现。例如,你可以微调 GPT-2 small (124M) 模型,使其输出莎士比亚风格的文本:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
如果你不想运行 starter pack 脚本,如前所述,可以通过运行 python dev/data/tinyshakespeare.py 和 python train_gpt2.py 来复现完全相同的 .bin 文件。
以上命令 (1) 下载已分词的 tinyshakespeare 数据集和 GPT-2 (124M) 权重,(3) 在 C 中从这些权重初始化,并使用 AdamW 在莎士比亚数据集上训练 40 步(批次大小 4,上下文长度仅 64),评估验证损失并生成一些文本。坦白说,除非你有一台强大的 CPU(并且可以在启动命令中增加 OMP 线程数),否则在 CPU 上训练 LLM 效果有限,但这是一个不错的演示/参考。以下是在我的 MacBook Pro(Apple Silicon M3 Max)上的输出:
[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 2: train loss 4.623322 (took 1369.394000 ms)
step 3: train loss 4.600470 (took 1290.761000 ms)
... (省略) ...
step 39: train loss 3.970751 (took 1323.779000 ms)
val loss 4.107781
generating:
---
Come Running Away,
Greater conquer
With the Imperial blood
the heaviest host of the gods
into this wondrous world beyond.
I will not back thee, for how sweet after birth
Netflix against repounder,
will not
flourish against the earlocks of
Allay
---
/dev/data/(dataset).py 中的数据文件负责下载、分词并将 token 保存为 .bin 文件,便于 C 代码读取。例如,运行:
python dev/data/tinyshakespeare.py
会下载并分词 tinyshakespeare 数据集。输出如下:
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 流,表示 GPT-2 分词器生成的 token ID。更多数据集可在 /dev/data 中找到。
我还附带了一个简单的单元测试,确保 C 代码与 PyTorch 代码一致。例如在 CPU 上编译并运行:
make test_gpt2
./test_gpt2
这将加载 train_gpt2.py 生成的 gpt2_124M_debug_state.bin 文件,执行前向传播,将 logits 和损失与 PyTorch 参考实现比较,然后执行 10 步 Adam 训练,并确保损失与 PyTorch 一致。测试 GPU 版本:
# fp32 测试(不支持 cudnn)
make test_gpt2cu PRECISION=FP32 && ./test_gpt2cu
# 混合精度 cudnn 测试
make test_gpt2cu USE_CUDNN=1 && ./test_gpt2cu
这将同时测试 fp32 路径和混合精度路径。测试应通过并输出 overall okay: 1。
我在 doc/layernorm/layernorm.md 中附上了一个非常简短的教程。这是一个逐步实现 GPT-2 模型单层(层归一化层)的简单指南。这是理解 C 中如何实现各层的好起点。
Flash Attention。从 2024 年 5 月 1 日起,我们使用 cuDNN 的 Flash Attention。由于 cuDNN 将编译时间从几秒延长到约一分钟,并且此代码路径非常新,因此默认禁用。您可以通过如下方式启用:
make train_gpt2cu USE_CUDNN=1
这将会尝试使用 cuDNN 进行编译并运行。您的系统上必须安装 cuDNN。cuDNN 安装说明 配合 apt-get 会安装默认的 cuDNN 软件包。对于最小化安装,cuDNN dev 包就足够了,例如在 Ubuntu 22.04 上使用 CUDA 12.x:
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
除此之外,您还需要 cuDNN frontend,但这只是头文件。只需将仓库克隆到磁盘。Makefile 目前会在您的主目录或当前目录中查找它。如果您将其放在其他地方,请在 make 命令行中添加 CUDNN_FRONTEND_PATH=/path/to/your/cudnn-frontend/include。
确保安装了 MPI 和 NCCL,例如在 Linux 上:
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
对于 NCCL,请按照 官方网站 的说明进行操作(例如网络安装程序)
然后:
make train_gpt2cu
mpirun -np <GPU 数量> ./train_gpt2cu
或者直接运行 ./scripts/ 下的某个脚本。
确保已按照 多 GPU 部分的说明安装了 NCCL。
我们目前支持 3 种运行多节点训练的方式:
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 套接字初始化 NCCL。请参阅 ./scripts/multi_node/run_gpt2_124M_tcp.sbatch 脚本了解详情。
注意:
* 如果您在 slurm 环境中运行,且 slurm 不支持 PMIx(考虑到 slurm-wlm 已放弃 PMIx 支持,这将是常见情况),则必须使用 FS (2) 或 TCP (3) 方法。要测试 slurm 是否支持 PMIx,请运行:srun --mpi=list 并查看输出中是否包含 pmix。
* 如果您没有 slurm 设置,可以使用 mpirun - MPI (1) 启动多节点运行。
这三种方法没有优劣之分,我们只是提供选项,以便您能在特定环境中运行。
以下示例演示了如何在具有 4 个 GPU 的机器上对 TinyStories 数据集扫描学习率。运行 shell 脚本 sweep.sh(当然,首先 chmod u+x sweep.sh):
#!/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
# 使用以下命令关闭这些 session
# screen -ls | grep -E "tr[0-3]" | cut -d. -f1 | xargs -I {} screen -X -S {} quit
此示例打开 4 个 screen 会话,并使用不同的学习率运行四个命令。这会将日志文件写入 stories$i.log,其中包含所有损失,您可以在 Python 中根据需要绘制这些损失。解析和绘制这些日志文件的快速示例位于 dev/vislog.ipynb。
关于我对本仓库期望的几点说明:
首先,我希望 llm.c 成为一个教育场所。例如,我们的 dev/cuda 文件夹是一个内核库,包含所有手动编写且文档齐全的层内核,从非常简单的内核到更复杂/更快的内核。如果您有具有不同权衡的新内核,欢迎在此贡献。
话虽如此,我也希望 llm.c 非常快,甚至在实际训练网络中具有实用价值。例如,我们首先应该能够复现大型 GPT-2 (1.6B) 训练运行。这要求我们整合任何最快的内核,包括使用 cuBLAS、cuBLASLt、CUTLASS、cuDNN 等库。我认为这样做也能达到教育目的,确立一个专家上限和一个衡量单位,例如,您可以声称您手动编写的内核达到了 cuBLAS 速度的 80% 等。然后您可以选择进行超快速运行,也可以选择“拖放”任何您想使用的手动内核并运行它们。
然而,作为约束,我希望保持根文件夹中主线的 llm.c 简单且易读。如果一个 PR 尽管将性能提高了 2%,但“代价”是 500 行复杂的 C 代码,并且可能需要引入一个奇怪的第三方依赖,我可能会拒绝该 PR,因为复杂性不值得。一个具体的例子——在根训练循环中将 cuBLAS 用于矩阵乘法是显而易见的:它使主线代码更快,是一行可解释的代码,并且是一个非常常见的依赖项。与此同时,我们可以在 dev/cuda 中拥有可与 cuBLAS 竞争的手动实现。
最后,我对待项目根文件夹(包含主/默认文件)中的复杂性会更加敏感。相比之下,dev/ 文件夹更像是我们的草稿空间,用于开发内核库或类,并共享有用、相关或教育性的代码,其中一些代码可以(局部地)复杂一些。
C#
CUDA C++
llm.cpp by @gevtushenko:使用 CUDA C++ Core Libraries 的移植
C++/CUDA
WebGPU C++
gpu.cpp by @austinvhuang:一个使用原生 WebGPU 进行便携式 GPU 计算的 C++ 库。目标为通用库,但也将 llm.c 内核移植到 WGSL。
C++
llm.cpp by @GaoYusong:移植并包含单头文件 tinytorch.hpp 库
Go
Java
llm.java by @harryjackson:Java 移植
Metal
llm.metal by @regrettable-username:使用简单、原始的 C/Metal Shading Language 进行 LLM 训练
Mojo
OpenCL
llm.c by @krrishnarraj:OpenCL 移植
Rust
Swift
llm.swift by @otabuzzman:Swift 移植
Zig
llm.zig by @saimirbaci:Zig 移植
Habana Gaudi2
llm.tpc by @abhilash1910:Habana Gaudi2 移植
Nim
组织开发的方式:
#llmc 频道。MIT