用 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.shDuckDB 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 条:

  1. bArray (868c,0p):"Not sure, but the code is quite dense and lacking in comments. nanoeuler & nanoeuler_check is itself the binary checked straight into git with the .log file? 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."

  2. 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."

  3. 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."

  4. 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?"

  5. 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 当商用模型的训练数据

七、参考链接

posted @ 2026-07-06 07:13  Ninghg  阅读(17)  评论(0)    收藏  举报