Show HN: NanoEuler – 从头开始用纯C/CUDA实现的GPT-2规模模型

Hacker News Top 模型

摘要

NanoEuler 是一个完全用纯 C/CUDA 从头构建的 GPT-2 规模语言模型,不依赖任何机器学习库,包括手写的前向/反向传播、字节级 BPE 分词器和训练流水线。该项目是一个教育示范,展示了 Transformer 训练背后的工程原理,可在单个 RTX 4070 上运行。

<p>大家好,</p><p>在 Anthropic 的 fable 被禁止后,我开始着手开发 nanoeuler,因为我的抱负和梦想是能在 Anthropic 从事 AI 领域的工作。促使我创建 nanoeuler 的两个有趣原因是:(1) 与 LLM 交互并不等于理解它们的构成;(2) 在非常底层的层面上研究 LLM,以理解参数、数据、模型增长之间的关联,以及 GPU 的工作原理和某些层如何优化。</p><p>因此,我带着研究的心态一步步推进,从 Shakespeare.txt 开始,让 nanoeuler 逐渐成长,理解一个拥有 2300 万参数的文本生成模型能学到什么。例如,在这个参数量下,nanoeuler 已经学会了识别“Name:”开头的一行,并有逻辑地写出该行内容。</p><p>我全部用 CUDA 编写,因为我不想在模型训练和推理之间插入任何中介层。接着,SFT 等技术的使用(即使规模不大)对于理解将一个 LLM 打造成聊天机器人的各个步骤非常有帮助。任何反馈、帮助或建议都绝对欢迎!</p>
查看原文
查看缓存全文

缓存时间: 2026/06/28 23:00

JustVugg/nanoeuler

来源:https://github.com/JustVugg/nanoeuler

nanoeuler

一个完全用 C/CUDA 从零构建的 GPT-2 类语言模型——没有 PyTorch、没有 autograd、没有 ML 库。前向 反向传播均手工编写并验证,整个训练流程都在本仓库中:手工编写的 字节级 BPE 分词器、在书籍+网页语料上的 预训练,以及 监督微调 为聊天模型(计划扩展 RLHF/DPO)。小型展示模型可在 CPU(libm + OpenMP)上运行,而完整的从零 CUDA 引擎(cuBLAS 矩阵乘法、手工编写的 FlashAttention,通过全模型梯度检查与 CPU 参考验证)可在单张 RTX 4070 上训练约 1.16 亿参数 的模型。

状态与诚实说明。 这是一个研究/教育性质的产物,公开构建。在单张消费级 GPU 上训练约 1.16 亿参数,它是一个遵循 GPT-2-small 精神的文本生成器:能生成流利度尚可的英文,没有真正的世界知识。它 不是 一个有用的助手——聊天模型仅证明预训练→SFT 流程能够端到端工作,并非有用的聊天机器人。本项目的重点在于从零构建的工程实践以及完整、易懂的训练流水线。

make check              # 验证反向传播(梯度检查,双精度)
make                    # 构建训练二进制
./nanoeuler train       # 训练小型展示模型(约 0.76M 参数)
./nanoeuler train big   # 训练稍大模型(约 10M 参数;用于 GPU)
./nanoeuler chat        # REPL:输入提示,模型续写

为什么叫 “Euler”?

残差模块的计算方式为

x = x + f(x)

可以将其视为数值积分的一步。前向欧拉法 通过下式推进常微分方程 dx/dt = f(x)

x(t+Δt) = x(t) + Δt · f(x(t))

当步长 Δt = 1 时,这恰好就是残差更新。因此,深度残差网络本质上是一个离散化的 ODE深度即积分时间,每一层将隐状态向前积分一个欧拉步。这正是神经 ODE 等工作背后的观点(ResNet 是连续流的欧拉离散化)。该项目以 莱昂哈德·欧拉 命名,他为我们提供了这种积分方法。

示例输出

来自约 1.16 亿参数模型在书籍+网页语料上进行部分预训练后的采样(prompt 为 Alessandro eat a):

Alessandro eat a icing textile: the satisfied by the servants in order to keep your weight
Using to a heated, collaborated young people that attend the metric process where the rank
is authorized and to contain the sedentary. Some state lawyers were able to insert ...

内容没有意义,但注意它自行学到的东西:正确的语法、长从句以及从网页数据中吸收的百科式语气。这是小模型在单 GPU 上训练的预期行为——流利的形式,浅薄的内容。更多的训练和(远)更多的数据能改善流利度;世界知识需要本项目的规模所不具备的算力支持。

架构

仅解码器的 Transformer,包含当前模型共有的构建模块:

  • RMSNorm(预归一化,无偏置)
  • 旋转位置编码(RoPE) 应用于 query 和 key
  • SwiGLU 前馈网络:down(silu(gate(x)) * up(x))
  • 分组查询注意力(GQA):query 头共享一个较小的 key/value 头集合
  • 多 token 预测(MTP)K 个输出头预测接下来的 K 个 token;辅助头能改善学习到的表示,并支持投机解码。生成时使用第 0 头。
  • 任何位置均无偏置
  • 字节级 BPE 分词器,手工编写,采用 GPT-2 风格的预分词(单个前导空格附在后面的单词上,这样空格就不会浪费为独立 token)。合并规则在语料样本上学习得到;GPU 模型使用 4096 词汇表(英文约 3.4 字节/token)。

每个模块为 x = x + attn(rmsnorm(x)) 后接 x = x + swiglu(rmsnorm(x))。残差连接 x = x + f(x) 是 ODE dx/dt = f(x) 的前向欧拉法的一步——因此得名,并致敬莱昂哈德·欧拉。

配置:

位置dimq/kv 头数层数上下文词汇表参数量
small(CPU, nanoeuler.c1284 / 24128512~1.05M
GPU 流水线cuda/, run_train76812 / 4165124096~116M

CPU small 模型在 12 核上只需几小时即可完成训练,是一个自包含的展示。约 1.16 亿参数的 GPU 模型才是真正的流水线:它在书籍+网页混合数据上预训练,然后微调为聊天模型(见下文)。头尺寸为 64(768/12),适合 FlashAttention 内核。

已验证的反向传播

手工编写的反向传播很容易出现细微错误,因此每个解析梯度都会与中心差分进行比较。检查以双精度运行,以避免浮点抵消隐藏正确梯度:

$ make check
  tok      : max rel err 1.02e-04
  qkvw     : max rel err 7.20e-07
  gatew    : max rel err 6.86e-08
  ...
max relative error: 1.02e-04
>>> backward OK (error < 1e-2)

每个参数张量都经过检查,包括 RoPE、SwiGLU、GQA 和 MTP 中不太明显的反向传播。

构建与性能

make 使用 -O3 -march=native -ffast-math -fopenmp 构建。矩阵乘法和注意力通过 OpenMP 并行化并向量化;在 12 核机器上,训练循环使用所有核心。make check 构建一个仅用于梯度检查的独立双精度二进制文件。

无外部依赖。已在 Linux 上使用 gcc 13 测试。

范围

这是一个从零构建的文本生成器,也是一个完整、易懂的训练流水线——不是产品。一个在单 GPU 上训练的此规模模型能生成流利但知识甚少的英文;微调后的聊天模型具有助手的 形式,但内容浅薄。一个可用的对话模型需要多个数量级更多的参数、数据和算力(约 1.35 亿参数的模型在训练约 6000 亿 token 后才能成为基本助手;本仓库在单 GPU 上使用远小于此的语料进行训练)。目标是拥有每一块组件——每个参数、每个梯度、分词器、内核、预训练和微调。

GPU 引擎(CUDA)

cuda/nanoeuler_cuda.cu 是一个完整的、从零编写的 CUDA 移植——在 GPU 上进行前向、反向、训练和推理。每个内核都在设备上与 CPU 参考进行验证,整个模型通过 GPU 梯度检查(GPU 梯度 vs CPU 梯度,精度约 1e-6)。

内核:矩阵乘法(委托给 cuBLAS,使用 TF32 tensor core)、RMSNorm、RoPE、分组查询注意力(手工编写的 FlashAttention,分块、在线 softmax、无 T×T 矩阵在内存中)、SwiGLU、softmax/交叉熵和 AdamW。FlashAttention 使训练步骤约加快 3 倍。

构建(RTX 40 系列 = Ada = sm_89;主机编译器标志用于避免 gcc 对大文件出现 ICE):

cd cuda
nvcc -O3 -arch=sm_89 -Xcompiler -fno-tree-reassoc,-fno-tree-copy-prop nanoeuler_cuda.cu -o nanoeuler_cuda -lcublas

模式:

./nanoeuler_cuda              # 运行所有内核自测试(GPU vs CPU)
./nanoeuler_cuda g            # 全模型梯度检查(GPU 梯度 vs CPU)
./nanoeuler_cuda t            # 从头预训练,每 5000 步检查点保存到 ../nanoeuler.bin
./nanoeuler_cuda tr           # 从最新的 ../nanoeuler.bin 检查点恢复预训练
./nanoeuler_cuda i "It was"   # 在 GPU 上自回归生成
./nanoeuler_cuda s            # 在 Alpaca 上进行监督微调,保存 ../nanoeuler_chat.bin
./nanoeuler_cuda c            # 与微调模型交互式聊天

每 5000 步保存训练检查点,因此长训练可以停止(Ctrl-C)并用 tr 恢复。在 GPU 上训练的模型以 CPU 程序的格式保存,因此 ./nanoeuler chat 也可以加载并运行它。

聊天机器人:预训练后微调(SFT)

聊天流水线分为两个阶段。首先在书籍+网页混合数据上 预训练 约 1.16 亿基础模型(./nanoeuler_cuda t,可用 tr 恢复)。然后 监督微调 将其变为助手:./nanoeuler_cuda s 加载预训练的基础模型,将每个 Alpaca 示例用标准指令模板渲染,并使用 损失仅覆盖响应 token(prompt 和填充位置的目标设为 -1,交叉熵内核将其转为零梯度)进行训练。结果保存到 nanoeuler_chat.bin./nanoeuler_cuda c 将你输入的每一行包装在相同模板中并采样回复,遇到 </s> 结束标记停止。

微调后,模型能生成正确的 形式——它遵循指令→响应的格式,写出完整句子并自行停止。但 内容 浅薄且经常错误:这是一个在单 GPU 上训练的小模型,因此拥有的世界知识很少。SFT 教会模型 如何 回应,而非 它知道什么——后者来自预训练和规模。这是一个忠实的、完全从零实现的演示,证明预训练→SFT 流水线能够端到端工作,而非一个有用的助手。

数据

预训练使用真实的 书籍+网页 混合:

  • 书籍data/get_gutenberg.sh 下载约 95 部公有领域的古登堡计划经典(奥斯汀、狄更斯、陀思妥耶夫斯基、托尔斯泰、梅尔维尔、完整莎士比亚等)。每本书的古登堡许可头部/尾部被去除(仅保留 *** START ... *** / *** END ... *** 标记之间的文本),以便模型在散文上训练。
  • 网页data/get_web.sh 使用 DuckDB CLI(单个静态二进制——无 Python、无库)直接从 Hugging Face parquet 文件中拉取 FineWeb-Edu(https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu)的一部分(高质量教育类网页文本)。

然后将它们连接成训练器读取的预训练语料:

sh data/get_gutenberg.sh                       # 书籍 -> data/gutenberg.txt
sh data/get_web.sh                             # 网页 -> data/web.txt(默认约 1 GB)
cat data/gutenberg.txt data/web.txt > data/pretrain.txt
sh data/get_alpaca.sh                          # 用于 SFT 的指令数据 -> data/alpaca.json

语料和模型检查点被 git 忽略(可重新生成)。

路线图

  • ✅ 手工编写的字节级 BPE,带 GPT-2 风格预分词。
  • ✅ 从零构建的 CUDA 引擎(cuBLAS + FlashAttention),通过全模型梯度检查验证。
  • ✅ 在书籍+网页混合数据上预训练,支持检查点/恢复。
  • ✅ 监督微调(Alpaca),使用响应掩码损失→聊天模型。
  • DPO(偏好优化)——对齐阶段,下一步构建。
  • ⏳ 扩大模型和数据规模(朝向约 2.7 亿参数),并发布可供尝试的训练检查点。

文件

nanoeuler.c             CPU 模型:前向、反向、训练、采样、聊天 REPL
cuda/nanoeuler_cuda.cu  GPU 引擎:BPE、内核、FlashAttention、预训练/SFT/推理/聊天、梯度检查
data/get_gutenberg.sh   下载并清理古登堡书籍语料库
data/get_web.sh         通过 DuckDB CLI(无 Python)下载 FineWeb-Edu 网页切片
data/get_alpaca.sh      下载用于微调的 Alpaca 指令数据
Makefile  LICENSE  shakespeare.txt  .gitignore

许可证

MIT。详见 LICENSE

相似文章

迷你可修改的CUDA语言模型实现

Hacker News Top

一个最小化的、可修改的CUDA实现,类似于GPT的变压器语言模型,处理字节序列,附带示例输出和构建说明。

我仅能腾出小规模来摆弄Transformer

Reddit r/LocalLLaMA

一名学生介绍了Silia,这是一种新颖的Transformer架构,将注意力机制和前馈网络合并为统一操作,以在≤10M参数规模下节省参数,尽管计算资源有限,仍以更少的参数实现了与GPT-2相当的性能。

发布 GPT-5.4 mini 和 nano

OpenAI Blog

OpenAI 发布了 GPT-5.4 mini 和 nano,它们是 GPT-5.4 的更小、更快的变体,专为高吞吐量工作负载设计,在编码、推理和多模态理解方面有显著改进,同时保持 2 倍以上的速度提升。

MiniGPT: 从第一性原理重建GPT

arXiv cs.CL

本文介绍了MiniGPT,这是一个基于PyTorch从头实现的紧凑型GPT风格自回归语言模型,其构建参考了nanoGPT的研究。该模型在Tiny Shakespeare数据集上使用字符级分词进行评估,在10.77M参数配置下达到了1.4780的验证损失。