颠覆LLM训练方法!Karpathy仅凭1000行C语言代码、一台Mac训练出GPT-2

颠覆LLM训练方法!Karpathy仅凭1000行C语言代码、一台Mac训练出GPT-2
2024年04月09日 17:48 AI科技大本营

编译 | 屠敏

出品 | CSDN(ID:CSDNnews)

大佬的话究竟有几分可信?很多人已经傻傻分不清了,遥记多年前,当 Linux 之父 Linus Torvalds 声称要去休假时,他一个人趁着休息时间捣鼓出了如今成为主流的项目版本管理工具 Git。

现下放到 AI 领域,不久之前,特斯拉前 AI 总监、OpenAI 联合创始人 Andrej Karpathy 在社交媒体平台 X 上高调宣布,他友好地从 OpenAI 离职,未来将专注于“个人项目”,而后又口口声声说要尝试性地戒掉上网两周,去了 Bhutan(不丹王国)休假。

谁承想,就在其宣布回归互联网的第三天,他便带来了自己徒手编写的 1000 行 C 代码即实现 GPT-2 训练的新项目——LLM.c(https://github.com/karpathy/llm.c),惊艳众人。

这款开源项目一经发布,便迅速冲到 HN 的 Top 榜,且在 GitHub 上也狂揽 2600 颗星。

而 Andrej Karpathy 本人,也因之前使用纯 C 代码实现轻量版 LLaMA 2并成功跑在 MacBook 上的经历,加上这一次的尝试,被网友给予了一个肯定的评价——「Real men program in C」(真男人就应该用 C 编程)。

1000 行 C 代码完成 GPT-2 训练的 LLM.c

根据 GitHub 页面介绍,llm.c 是一个简单、纯粹的 C/CUDA LLM 训练项目。不需要使用 245MB 的 PyTorch 或 107MB 的 cPython 就能用纯 C 语言训练 LLM。

更让人佩服的是,LLM.c 仅用约 1000 行干净的代码即可在 CPU/fp32 上实现 GPT-2 训练。它可以立即编译并运行,并且与 PyTorch 参考实现完全匹配。

之所以选择 GPT-2 作为训练的起点,Andrej Karpathy 表示,是因为 GPT-2 是 LLM 的鼻祖,这也是大模型堆栈第一次以公认的现代形式组合在一起,并提供了模型权重。

你可以在这里查看原始的训练实施情况:

https://github.com/karpathy/llm.c/blob/master/train_gpt2.c

Andrej Karpathy 透露,这个项目在一开始就在一个大的 1D 内存块中一次性分配了所有需要的内存。

由此,在训练过程中不会创建或销毁内存,因此内存占用量保持不变,只是动态地将数据批次流过。

这里的关键在于手动实现所有单个层的前向和后向传递,然后将它们串联起来。例如,这里是 layernorm 的前向和后向传递。

除了 layernorm 之外,也还需要编码器、matmul、自注意力、gelu、残差、softmax 和交叉熵损失。

一旦你有了所有的层,你就可以把所有的层串联起来。

“不瞒你说,写这个过程相当乏味,也很受虐,因为你必须确保所有的指针和张量偏移都正确排列”,Andrej Karpathy 吐槽道。

左图:在内存中分配一个单一的 1D 数组,然后将所有模型权重和激活指向该数组;

右图:非常小心地进行所有指针运算 

一旦有了前向/后向,剩下的部分(数据加载器、Adam 更新等)就变得微不足道了。

不过,真正的乐趣才刚刚开始,Andrej Karpathy  分享道,“我现在正在逐层将其移植到 CUDA,这样它就能变得高效,甚至可以与 PyTorch 相媲美,但却没有任何严重的依赖性。我已经做了几层了,这是一项相当有趣的 CUDA 工作。”

在此基础上,扩展包括将精度从 fp32 降低到 fp16/以下,并增加几层(如 RoPE),以支持更现代的架构,如 llama 2 / mistral / gemma 等。

对此,Andrej Karpathy 也表示,一旦这个项目进入稍稍稳定的状态后,他就会从头开始构建更为详细的观看视频。

立足于当下,Andrej Karpathy 也正在研究:

  • 直接使用 CUDA 实现,这将大大提高速度,并可能接近 PyTorch。

  • 使用 SIMD 指令加速 CPU 版本,X86 上的 AVX2 / ARM 上的 NEON(如 Apple Silicon)。

  • 更现代的架构,如 Llama2、Gemma 等。

对于 GitHub repo,他希望同时维护干净、简单的参考实现,以及更优化的版本,这些版本可以接近 PyTorch,但代码和依赖性只占很小一部分。

快速开始吧!

基于此,Andrej Karpathy 也为广大网友直接开始上 LLM.c 的使用步骤,方便大家自己去实践一把。

首先第一步,下载并 tokenize 数据集。在这里,Andrej Karpathy 使用了 tinyshakespeare 数据集(https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt),并称其下载和 tokenize 速度最快:

python prepro_tinyshakespeare.py

输出:

Saved 32768 tokens to data/tiny_shakespeare_val.bin

Saved 305260 tokens to data/tiny_shakespeare_train.bin

.bin 文件是由 int32 数字组成的原始字节流,用 GPT-2  tokenizer 表示 token ID。你也可以使用 prepro_tinystories.py 对 TinyStories 数据集进行 tokenize。

原则上,按照步骤走到这里就可以训练模型了。

不过,Andrej Karpathy 表示,CPU/fp32 基准参考代码的效率很低,从头开始训练这些模型还不太现实。相反,他使用 OpenAI 发布的 GPT-2 权重进行初始化,然后进行微调。为此,必须下载 GPT-2 权重,并将其保存为检查点,以便在 C 语言中加载:

python train_gpt2.py

你可以从 nanoGPT 中找到这段代码,它是 PyTorch 中简单的 GPT-2 参考实现。

这个脚本将下载 GPT-2 (124M) 模型,对一批数据进行 10 次迭代过拟合,运行几步生成,最重要的是它将保存两个文件:

1)gpt2_124M.bin 文件,其中包含用于在 C 语言中加载的原始模型权重;

2)gpt2_124M_debug_state.bin,其中还包含更多调试状态:输入、目标、logits 和损失。这对调试 C 代码、单元测试和确保我们与 PyTorch 参考实现完全匹配非常有用。

现在,我们只关心 gpt2_124M.bin 中的模型权重,以用它们进行初始化,并用原始 C 语言进行训练:

make train_gpt2

当然,你可以查看 Makefile 及其注释。它将尝试自动检测你的系统是否支持 OpenMP,这对于以极低的代码复杂度为代价加快代码速度非常有帮助。train_gpt2 编译完成后,就可以运行了:

OMP_NUM_THREADS=8 ./train_gpt2

接下来,我们应该根据 CPU 的内核数量来调整线程数。程序将加载模型权重和 token,它将在 Adam lr 1e-4 的条件下运行微调 loop,进行几次迭代,然后根据模型生成样本。

“该文件(我认为)可读性很强,你应该看一看。简单地说,所有层的前向和后向传递都有实现方法,它们被串成一个大型的手动前向/后向/更新循环。”Andrej Karpathy 说。

在 MacBook Pro(苹果 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)

... (trunctated) ...

step 39: train loss 3.970751 (took 1323.779000 ms)

val loss 4.107781

generated: 50256 16773 18162 21986 11 198 13681 263 23875 198 3152 262 11773 2910 198 1169 6002 6386 2583 286 262 11858 198 20424 428 3135 7596 995 3675 13 198 40 481 407 736 17903 11 329 703 6029 706 4082 198 42826 1028 1128 633 263 11 198 10594 407 198 2704 454 680 1028 262 1027 28860 286 198 3237 323

step 40: train loss 4.377757 (took 1366.368000 ms)

现在,生成器只提供 token ID,我们必须将其解码为文本。当然也可以用 C 语言轻松实现,因为解码非常简单,只需查找字符串块并打印即可。时下可以使用 tiktoken:

import tiktoken

enc = tiktoken.get_encoding("gpt2")

print(enc.decode(list(map(int, "50256 16773 18162 21986 11 198 13681 263 23875 198 3152 262 11773 2910 198 1169 6002 6386 2583 286 262 11858 198 20424 428 3135 7596 995 3675 13 198 40 481 407 736 17903 11 329 703 6029 706 4082 198 42826 1028 1128 633 263 11 198 10594 407 198 2704 454 680 1028 262 1027 28860 286 198 3237 323".split()))))

输出:

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

「我喜欢 Netflix 出现的方式,很明显,模型中还潜藏着过去训练的影子」,Andrej Karpathy表示,「我没有尝试调整微调超参数,因此很有可能会有很大改进,尤其是在训练时间更长的情况下。」

除了具体的步骤之外,Andrej Karpathy 还附上了一个简单的单元测试,以确保 C 代码与 PyTorch 代码一致。编译并运行:

make test_gpt2

./test_gpt2

现在加载 gpt2_124M_debug_state.bin 文件,运行前向传递,比较 logits 和损失与 PyTorch 参考实现,然后用 Adam 进行 10 次迭代训练,确保损失与 PyTorch 一致。

写在最后

Andrej Karpathy 时不时丢一个 LLM 相关的个人项目,还附上详尽的教程,也让不少网友开启膜拜模式:

还有人评价道,在前有很多政府机构将 C/C++ 归纳为不安全的内存语言之际,“我们实际上正在开始一场 C 语言的复兴”。

最后,不得不说,离开企业后的 Andrej Karpathy 更为自由一些,他不仅在 YouTube 上持续分享有关大模型的教学视频,更是生怕这届学生看不懂,每次在发布项目时,都会附上详尽的教程。

这不,在发布 LLM.c 时,Andrej Karpathy 在 doc/layernorm/layernorm.md 中附上了一个很小的教程:这是实现 GPT-2 模型单层(layernorm 层)的一个简单的分步指南,也是了解如何用 C 语言实现层的一个很好的起点。

来源:

https://twitter.com/karpathy/status/1777427944971083809

https://github.com/karpathy/llm.c

https://github.com/karpathy/llm.c/blob/master/doc/layernorm/layernorm.md

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部