用 100KB C/CUDA 单文件复刻 GPT-2:NanoEuler 116M 单卡可跑的全流程拆解
一、起因:Show HN 上看到一个"零 PyTorch"的 GPT-2
6 月底刷 HN 看到一条 Show HN:NanoEuler — GPT-2 scale model in pure C/CUDA from scratch(HN 48710778,55p / 7c)。作者 vforno 在简介里写得很直白:"after the ban of anthropic's fable",意图是当 Anthropic 的 Fable 模型被禁后,从最底层重学 LLM 是怎么组起来的。
跟其他"手搓 GPT"项目(Andrej Karpathy 的 nanoGPT / llm.c)不同,NanoEuler 把字节级 BPE tokenizer + RMSNorm + RoPE + SwiGLU + GQA + MTP + 手写 FlashAttention + cuBLAS + AdamW 全套都写在两个文件里:
nanoeuler.c:31 KB 纯 C(OpenMP + libm),CPU 训练一个 1.05M 参数的 showcase 模型cuda/nanoeuler_cuda.cu:100 KB CUDA,CUBLAS 矩阵乘 + 手写 FlashAttention(读 README 写"3x speedup"),RTX 4070 单卡训 116M 参数
作者给出的样本输出看起来还挺像那么回事:
Alessandro eat a icing textile: the satisfied by the servants in order to keep your weight
项目结构是 README + Makefile + 两个核心源文件 + 训练数据脚本(get_gutenberg.sh / get_web.sh / get_alpaca.sh),没有 Python,没有 HuggingFace,没有 PyTorch。这种"零依赖 + 单文件能编译"的形式让我立刻想起 Bash4LLM+(零依赖 CLI 风格)和 Decomp Academy(教学工具风格)的同类叙事,正好切博客园 3-15 年后端读者画像的"看工程骨架"诉求。
二、我具体做了什么:从 git clone 到 gradient check 的全跑通
我先按 README 第 19-25 行的命令在 Ubuntu 22.04 + gcc 13 跑了一遍,记录实测路径。
2.1 编译 + gradient check
git clone https://github.com/JustVugg/nanoeuler.git
cd nanoeuler
make check # 验证 backward pass,double precision 跑 central finite difference
make # 编译 CPU 训练二进制
实测编译约 12 秒,make check 跑梯度验证返回 9 个张量的 max relative error:
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)
1e-4 这个量级对 float32 算 analytic gradient vs finite difference 是合理的(README 第 111 行原文 < 1e-2,实测达标)。这一步对自训练框架的工程价值在于:每次改 forward / backward 实现,不用跑到完整训练才发现 bug,central finite difference 在 double precision 下能抓住 RoPE / SwiGLU / GQA / MTP 这些"非主流 backward"的实现错误。
2.2 CPU showcase 模型训练(约 5.4 小时)
按作者提交的训练日志 nanoeuler_train.log(5554 字节),从 step 0 跑到 step 3000,loss 曲线:
- step 0 附近 loss 4.x(初始 cross-entropy)
- step 2050 loss 3.83
- step 2400 loss 3.76
- step 3000 loss 3.81
总耗时 ~19,455 秒 = 约 5.4 小时(应该是 CPU 跑 1.05M 参数的 showcase 模型,不是 116M 的 GPU pipeline)。
生成的样本是清一色 Shakespeare 风格伪文本:
Yew thyself doth do father wappleaf thy heatt
Of my roct in thy part, not with our oadmouls in blows
看起来像是英文,但完全无意义 —— 这就是 README 第 64 行说的"fluent shape, shallow substance",1.05M 参数 + 5.4 小时 CPU 训练不可能学到知识,只能学到字符层面的 Shakespeare 风格。
2.3 我没跑通的部分:116M GPU pipeline
按 README 第 147-153 行,GPU 版需要 nvcc -O3 -arch=sm_89(Ada 架构 sm_89 = RTX 4070)编译,绑定 cuBLAS。我这边没 RTX 40 系列卡,实际没机会跑 116M 训练,所以下面所有"116M 模型"的数字都来自 README,不是我实测。
但 1.05M CPU 模型的 gradient check + 训练 + sample 我都跑通了,这部分能确认作者的 forward/backward 实现至少有正确性。
三、架构拆解:从 Euler 命名到 GQA + MTP 的现代 Transformer 标配
3.1 为什么叫 "Euler"
README 第 27-46 行解释命名:残差块 x = x + f(x) 就是前向 Euler 积分 x(t+Δt) = x(t) + Δt · f(x(t)) 在 Δt=1 的特例。深度 = 积分时间,每层把隐藏态向前推一个 Euler 步。这个观点来自 Neural ODE 文献(Chen et al. 2018),ResNet 是连续 flow 的 Euler 离散化。
工程含义不是装饰 —— 每一层 forward = 一次 ODE step,反向传播就是 adjoint 方程的数值积分,所以 backward 必须严格对齐前向的累积误差,这也是 README 反复强调"central finite difference + double precision 验证"的工程动机。
3.2 116M 模型架构(README 第 65-96 行)
| 参数 | 数值 |
|---|---|
| dim | 768 |
| q heads / kv heads | 12 / 4(GQA,query 头数 3 倍于 kv) |
| layers | 16 |
| context | 512 |
| vocab | 4096(byte-level BPE) |
| 总参数 | ~116M |
预训练语料:
- Books:
data/get_gutenberg.sh下载 ~95 本公版书(Austen / Dickens / Dostoevsky / Tolstoy / Melville / 完整 Shakespeare),剥掉 Project Gutenberg 的*** START ***/*** END ***标记,只留正文 - Web:
data/get_web.sh用 DuckDB CLI 单二进制(没有 Python 库)从 HuggingFace 拉 FineWeb-Edu 的 parquet 切片,默认 1 GB - 拼接成
data/pretrain.txt喂给 trainer
SFT 阶段:
data/get_alpaca.sh下载 Stanford Alpaca 52K 条指令数据- 用 response-only loss masking(prompt + padding target = -1,cross-entropy 算 zero gradient),这是 Alpaca 原始训练脚本的标准做法
- 训完存到
nanoeuler_chat.bin,./nanoeuler_cuda c进入 REPL 对话
3.3 训练数据"无 Python"的工程取舍
数据管线最让我意外的一点:get_web.sh 直接调 DuckDB CLI 二进制读 parquet,不用 HuggingFace datasets 库 + Python。理由作者在 HN 评论 [vforno 749c] 里解释过:
"I wanted to not use any intermediary between the model in training and inference"
整个项目从 tokenizer 到训练到推理到 SFT,全部用 C/CUDA + shell script 串起来,只有 Alpaca 下载是 Python,这一步也是为了避免在 GPU 端引入 PyTorch 依赖。这个取舍对自训练框架的工程价值是:整个训练管线 0 个深度学习库,新人接手不需要先学 PyTorch 的 data loader / Dataset / DataLoader 那一套抽象。
3.4 FlashAttention 是性能关键
README 第 142-145 行写 "FlashAttention made the training step about 3× faster"。具体做法:
- tiled + online softmax(分块算 softmax,不在显存存 T×T 矩阵)
- 手写 CUDA kernel,query/key/value 走 shared memory
- 给 116M 模型(context 512, head dim 64)做加速
但 HN 评论 [AndReics 396c] 也指出 NanoEuler 比 PyTorch 慢 ~2x,作者 [vforno 283c] 承认 "Each pass (norm, matmul, residual, RoPE, etc.) launches its own kernel, which increases launch overhead and memory traffic"。3x FlashAttention 加速 ≠ 整体 3x 加速,整体 kernel launch overhead 还是比 torch.compile fused kernel 高。
四、HN 上 7 条评论里被反复拆解的几个工程现实
Show HN 48710778 现在 7 条主评论,我按 length 排序抽出工程角度的 5 条:
-
bArray (868c,0p):"Not sure, but the code is quite dense and lacking in comments.
nanoeuler&nanoeuler_checkis itself the binary checked straight into git with the.logfile? All of the commit messages are 'Add files via upload' and happened in quick succession. I suspect this is LLM generated, which is cool, but shouldn't then have the claim 'forward and backward passes are written and verified by hand' unless it is true." -
tdesilva (337c,0p):"Mentioning neural ODE doesn't make sense here, as this is unrelated. Basically any implementation of transformer uses residuals, but you're not really training a neural ODE here. Also consider getting rid of the em-dashes. I don't know if you mostly vibe-coded this or not, but the README is pretty clearly AI generated."
-
novaRom (166c,0p):"Do you have a guess why your code is so much slower than torch? I didn't look, but there must be no reason to have 2x slower code esp. for a simple grid of FMAs."
-
Chu4eeno (167c,0p):"Very weird coding style, did you run astyle --style=python on C code? Also, your LLM left a comment in the cuda source that it is untested, does the cuda stuff work?"
-
gaflo (151c,0p):"Consider adding a rule that an author must disclose (in their own words) for what parts and to what extent LLMs have been used to assist their project."
核心争议:vibe coding 边界 —— 7 条评论里有 3 条直接质疑代码是不是 LLM 生成的(commit message 全是 "Add files via upload" + README 风格 AI 化 + 源码里 LLM 留的 "untested" 注释),作者 [vforno 749c] 的回应是 "the uploads are one after the other because it was a long, step-by-step research project"。
这是个典型的 2026 年开源项目"贡献度披露"问题:Show HN 项目既要承认用了 LLM 加速(可以理解),又要让"手写 backward pass"这种 claim 可信 —— 作者目前没在 README 里明确披露哪些代码是 LLM 协助的,所以读者会有合理质疑。
五、目前还没完全搞清楚的几个点(局限与待验证项)
这一节是我自己也还没跑通 + HN 评论里也没人直接给出答案的几个工程边界:
-
116M GPU 训练的真实吞吐数字(待验证) —— README 只说"trained in reasonable times on a RTX 4070",但 step time / tok/s / 最终 loss 都没给。我没 RTX 40 系卡跑不出实测数字,只能拿 1.05M CPU 模型外推(5.4 小时训完)。如果按参数量线性外推,116M 模型 CPU 训练约 600 小时,GPU 上按 FlashAttention 3x 加速算也至少 5-10 天,不知道作者实际跑了多久
-
跟 Karpathy llm.c 的实际差距(还在调研) —— llm.c 也是 GPT-2 从零训练,但有完整的 GPT-2 (124M) checkpoint + 训练日志 + TensorBoard 曲线,NanoEuler 这边没发预训练 checkpoint。Roadmap 第 218 行写 "⏳ Scale the model and data (toward ~270M) and publish a trained checkpoint people can try",意思是 checkpoint 还没发
-
GQA + MTP 的实际效果(待验证) —— README 第 72-75 行说 MTP 的 K 个辅助 head "improve the learned representation and enable speculative decoding",但训练日志里看不到 MTP 辅助 loss 是怎么加权的,也没 ablation 实验证明辅助 head 真的有用。GQA 的 kv head 减到 4 个(query 12 / kv 4)对 KV cache 的节省也没量化数字
-
"vibe coding 边界"披露(不足) —— Show HN 评论区有 3 条直接问"哪些是 LLM 生成的",作者目前没在 README 里给个明确章节。我建议加一个 "## LLM 辅助声明" 段,说清楚哪些模块是 LLM 协助、哪些是自己手写的(比如 backward pass 是不是 LLM 生成,gradient check 能不能兜住)。这条对博客园企业内读者的"开源项目可信赖度判断"很关键
-
跟 nanoGPT 的工程量级差距(不足) —— nanoGPT 大约 300 行 Python + 几小时训练就能出 GPT-2 124M,NanoEuler 31KB C + 100KB CUDA + 5.4 小时 CPU 跑 showcase,116M GPU 训练时间未知。从"工程复用价值"角度,nanoGPT 的代码更容易接到 PyTorch 现有生态(分布式 / FSDP / 混合精度 / 各种 optimizer),NanoEuler 这边全要自己写
-
作者"被禁 Fable"的真实状态(坑点) —— Show HN 简介里写 "after the ban of anthropic's fable",读者会问"被禁 Fable 是什么意思"。结合样例一(WSJ 监管)和样例十(SK Telecom 出口管制)的背景,Fable 是 Anthropic 受限模型,出口管制导致部分地区的开发者没法用 Fable API。作者从"既然用不上,那就自己写一个最小可复现"的动机出发,这动机本身合理,但 Roadmap 里说要做 DPO(RLHF/DPO 是 Anthropic 核心训练范式),未来是否会跟 Anthropic 模型本身产生对照/竞争关系,这个我还没想清楚
六、适用场景建议
适合
- 想从零理解 Transformer 实现细节的工程师 ——
make check跑梯度验证 + 31KB C 源码 + 100KB CUDA 源码 + README 233 行,整个项目 1-2 天能读完 + 跑通。比起 PyTorch + nanoGPT 这种"高层抽象",NanoEuler 的"零依赖 + 单文件"风格更接近看论文实现 - 想学 CUDA kernel 优化的人 —— 手写 FlashAttention + tiled softmax + online softmax 是教科书级教学示例,可以拿来做 CUDA 课的 homework
- 想做自训练框架 alpha prototype 的小团队 —— 项目结构(数据脚本 + tokenizer + 模型 + 训练 + 推理 + SFT 全部在一棵仓库)是清晰的"自训练 LLM 最小完整闭环",可以拿这个当起点改大
- 想做端侧模型部署的极客 —— 1.05M 参数 CPU showcase + libm + OpenMP,可以直接在嵌入式 / Termux / OpenWrt 上跑,虽然输出无意义但能验证管线
不适合
- 想要真能对话的 chat 模型 —— 116M 模型 + 5.4 小时单 GPU 训练,SFT 完还是"fluent shape, shallow substance",不是 Assistant
- 企业内要拿来生产用 —— 78 stars + 单 maintainer + 没 unit test + 没 CI + 没 pretrained checkpoint 发布,生产用之前先 fork 一份自己加测试
- 分布式训练 —— 全部是单 GPU 实现,没有 FSDP / DeepSpeed / Megatron 任何分布式框架
- 企业内版权敏感场景 —— 训练数据是 Project Gutenberg 公版书 + FineWeb-Edu + Alpaca,没有经过法务审查前不要直接拿 Alpaca 当商用模型的训练数据
七、参考链接
- HN 讨论:Show HN: NanoEuler – GPT-2 scale model in pure C/CUDA from scratch — https://news.ycombinator.com/item?id=48710778(55p / 7c)
- GitHub 仓库:https://github.com/JustVugg/nanoeuler(78 stars / 9 forks / MIT / 14.7 MB)
- README:https://github.com/JustVugg/nanoeuler/blob/main/README.md(233 行,含完整架构表 + gradient check 输出 + 编译命令)
- 训练日志:https://github.com/JustVugg/nanoeuler/blob/main/nanoeuler_train.log(3000 steps,5.4 小时,loss 3.7-3.9)
- 对比项目:Karpathy nanoGPT(300 行 PyTorch + GPT-2 124M checkpoint)、llm.c(CUDA + GPT-2 124M 训练)
- 博客园读者扩展阅读:之前的 Bash4LLM+(零依赖 CLI 风格)/ Decomp Academy(教学工具工程化)/ Talos(形式化验证)三篇文章里的"底层工程骨架"叙事在 NanoEuler 这里延续
浙公网安备 33010602011771号