Show HN: NanoEuler – 从头开始用纯C/CUDA实现的GPT-2规模模型
摘要
NanoEuler 是一个完全用纯 C/CUDA 从头构建的 GPT-2 规模语言模型,不依赖任何机器学习库,包括手写的前向/反向传播、字节级 BPE 分词器和训练流水线。该项目是一个教育示范,展示了 Transformer 训练背后的工程原理,可在单个 RTX 4070 上运行。
查看缓存全文
缓存时间: 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) 的前向欧拉法的一步——因此得名,并致敬莱昂哈德·欧拉。
配置:
| 位置 | dim | q/kv 头数 | 层数 | 上下文 | 词汇表 | 参数量 |
|---|---|---|---|---|---|---|
small(CPU, nanoeuler.c) | 128 | 4 / 2 | 4 | 128 | 512 | ~1.05M |
GPU 流水线(cuda/, run_train) | 768 | 12 / 4 | 16 | 512 | 4096 | ~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语言模型实现
一个最小化的、可修改的CUDA实现,类似于GPT的变压器语言模型,处理字节序列,附带示例输出和构建说明。
我仅能腾出小规模来摆弄Transformer
一名学生介绍了Silia,这是一种新颖的Transformer架构,将注意力机制和前馈网络合并为统一操作,以在≤10M参数规模下节省参数,尽管计算资源有限,仍以更少的参数实现了与GPT-2相当的性能。
发布 GPT-5.4 mini 和 nano
OpenAI 发布了 GPT-5.4 mini 和 nano,它们是 GPT-5.4 的更小、更快的变体,专为高吞吐量工作负载设计,在编码、推理和多模态理解方面有显著改进,同时保持 2 倍以上的速度提升。
NVIDIA 发布了 Nemotron-TwoTower-30B-A3B-Base-BF16,这是一种基于 Nemotron 3 Nano 30B-A3B 主干构建的异常扩散型语言模型。
NVIDIA 发布了 Nemotron-TwoTower-30B-A3B-Base-BF16,这是一种基于扩散的语言模型,采用逐块自回归扩散方法,通过对令牌块进行迭代去噪来生成文本,实现了自回归基线 2.42 倍的生成吞吐量,同时保留了基准测试质量 98.7% 的水平。
MiniGPT: 从第一性原理重建GPT
本文介绍了MiniGPT,这是一个基于PyTorch从头实现的紧凑型GPT风格自回归语言模型,其构建参考了nanoGPT的研究。该模型在Tiny Shakespeare数据集上使用字符级分词进行评估,在10.77M参数配置下达到了1.4780的验证损失。