17岁少年用C++从零构建深度学习框架Forge,精确复现GPT-2

当PyTorch成为标配,有人选择从零开始
在深度学习工程实践中,PyTorch 和 TensorFlow 早已成为默认选项。绝大多数从业者关心的是如何调用高层 API 训练模型,而框架底层的张量运算、自动微分、内存管理等机制,往往被当作黑盒。
张量运算是深度学习框架的基石。张量本质上是多维数组的数学抽象,深度学习中的所有数据——图像像素、文本嵌入、模型权重——都以张量形式存在。一个图像可以表示为形状 [batch, channels, height, width] 的四维张量,而 Transformer 中的隐藏状态则是 [batch, sequence_length, hidden_dim] 的三维张量。张量运算不仅包括基本的算术操作,还涵盖广播(broadcasting)、视图变换(reshape/transpose)、索引切片等复杂操作,每一种都需要正确处理内存布局和步幅(stride)信息。
自动微分(Automatic Differentiation)则是让框架能够自动计算梯度的核心机制,它通过构建计算图记录前向运算的每一步,然后利用链式法则反向传播误差。与符号微分(产生冗长表达式)和数值微分(精度低、计算慢)不同,自动微分在保持机器精度的同时,计算开销仅为前向传播的常数倍(通常 2-5 倍)。PyTorch 采用动态计算图(Define-by-Run),每次前向传播时实时构建图结构,这使得调试更直观、支持动态控制流(如条件分支和循环),但也增加了底层实现的复杂度——需要在每次操作时记录图节点,并在反向传播后正确释放中间梯度占用的内存。从零实现自动微分引擎意味着需要为每个算子定义前向计算和对应的梯度规则(即 backward 函数),并管理中间变量的生命周期,处理梯度累积、原地操作检测等边界情况。
正因如此,一位自称 17 岁、对深度学习和系统级编程充满热情的开发者,在 Reddit 上分享的项目显得格外扎眼——他从零用 C++ 构建了一个完整的深度学习框架 Forge,底层不依赖任何现成的深度学习库。
这个项目的价值不在于性能有多惊艳,而在于它完整地暴露了一个现代 Transformer 模型运行所需的全部底层组件,并且做到了一个极难达成的验证标准:加载真实的 GPT-2 权重后,输出与 HuggingFace 逐 token 完全一致。

Forge框架核心实现:从张量引擎到BPE分词器
根据作者的自述,Forge 自 2026 年 1 月起开始开发,数学后端主要依赖 Eigen,同时作者手写了基于 AVX2 的 SIMD 内核处理逐元素运算,并用 OpenBLAS 支撑的 GEMM 处理繁重的矩阵乘法。
AVX2(Advanced Vector Extensions 2)是 Intel 在 2013 年随 Haswell 架构推出的指令集扩展,允许 CPU 在单条指令中同时处理 256 位数据,即 8 个单精度浮点数或 4 个双精度浮点数。相比标量代码一次处理一个数,AVX2 理论上可将吞吐量提升 8 倍。SIMD(Single Instruction Multiple Data)编程的核心思想是数据并行:对于逐元素加法、激活函数等操作,输入数据之间没有依赖关系,天然适合向量化处理。实际编写 SIMD 代码需要处理数据对齐(通常 32 字节对齐)、尾部元素处理(当数据长度不是 8 的倍数时)以及不同指令延迟的流水线调度等工程细节。
而 GEMM(General Matrix Multiply)是线性代数中最核心的运算,计算 C = αAB + βC。Transformer 中的注意力计算(Q×K^T 和 Attention×V)、全连接层(输入×权重矩阵)本质上都是矩阵乘法,一个 GPT-2 前向传播中超过 90% 的浮点运算集中在 GEMM 上。高效 GEMM 实现需要精心设计的多级分块策略(macro-tile → micro-tile)以匹配 CPU 的 L1/L2/L3 缓存层级,通过数据预取(prefetch)隐藏内存延迟,并利用 FMA(Fused Multiply-Add)指令最大化每周期浮点吞吐。OpenBLAS、Intel MKL 等库在这方面积累了数十年的工程经验,其 GEMM 内核通常能达到理论峰值性能的 90% 以上,单人重新实现很难超越其性能。
这种分工是合理的:逐元素操作适合用 SIMD 做向量化,而矩阵乘法这类计算密集型任务交给成熟的 BLAS 实现,能在不重复造轮子的前提下拿到接近工业级的吞吐。Eigen 库作为 C++ 模板元编程的典范,提供了惰性求值(lazy evaluation)和表达式模板(expression templates)技术,能自动融合多步运算减少临时内存分配,是学术界和工业界(如 TensorFlow 的 CPU 后端)广泛使用的线性代数库。
核心组件清单
从功能覆盖度看,Forge 已经不是玩具级别的实验:
- 自研张量引擎,包含自己的自动微分引擎和内存分配器
- Dense/Linear 层、多种优化器(Adam、AdamW、SGD 及带动量的 SGD)
- Self-Attention、LayerNorm、Embedding
- 激活函数(sigmoid、softmax、tanh、GELU 的 tanh 近似、ReLU、LeakyReLU)
- 损失函数(融合 log-softmax 的交叉熵、融合 sigmoid 的二元交叉熵、均方误差)
- 从零实现的 BPE 分词器,采用 GPT-2 风格的预分词与 merge 规则
- safetensors 格式的保存/加载管线
其中,优化器的选择覆盖了实际训练中最常用的几种算法。Adam(Adaptive Moment Estimation)通过维护梯度的一阶矩(均值)和二阶矩(方差)的指数移动平均来自适应调整每个参数的学习率;AdamW 则修正了 Adam 中权重衰减(weight decay)与 L2 正则化混淆的问题,将权重衰减从梯度更新中解耦出来,已成为训练 Transformer 的事实标准优化器。
BPE分词器的底层逻辑
BPE(Byte Pair Encoding)最初由 Philip Gage 在 1994 年提出,是一种简单而高效的数据压缩算法。2016 年,Sennrich 等人将其引入机器翻译领域用于子词分割,解决了固定词表无法处理稀有词和新词的问题。GPT-2 使用的 BPE 变体在此基础上做了重要改进:首先在字节级别(byte-level)操作,确保任何 UTF-8 文本都能被编码而不会出现未知 token;其次引入预分词步骤,使用精心设计的正则表达式(如 's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+)将输入文本拆分为单词级别的片段,防止跨词边界的 merge 产生语义不合理的 token。
然后在字节级别上迭代合并最频繁出现的相邻字符对。这个过程通过预先训练好的 merge 规则表驱动——GPT-2 的 merge 表包含约 5 万条规则,按训练时发现频率从高到低排序,优先级越高的 merge 越先执行。最终将任意文本转化为固定词表(GPT-2 为 50,257 个 token)中的 token 序列。从零实现 BPE 需要精确复现预分词的正则模式、merge 优先级排序、特殊 token(如 <|endoftext|>)的处理逻辑,以及 Unicode 到字节的映射表。任何细微偏差——比如正则匹配顺序不同、空格处理方式有别——都会导致与参考实现产生不同的 token 序列,进而影响模型输出。
safetensors格式的安全性优势
safetensors 是 HuggingFace 于 2022 年推出的模型权重序列化格式,旨在替代传统的 pickle 格式。Python 的 pickle 模块在反序列化时会执行任意 Python 代码——攻击者可以构造恶意 pickle 文件,在加载模型权重时执行远程代码、窃取数据或植入后门,这一安全隐患在模型共享日益普遍的今天变得尤为严重(已有多起公开报告的攻击案例)。
safetensors 采用简单的二进制布局:文件起始 8 字节为头部长度(little-endian uint64),紧接着是 JSON 格式的元数据,存储每个张量的名称、数据类型(dtype)、形状(shape)和在数据区的偏移量(offsets),后续紧跟原始数据块(raw bytes)。这种设计不执行任何代码,从根本上杜绝了反序列化攻击。此外,safetensors 支持零拷贝内存映射(mmap)读取,操作系统可以将文件直接映射到虚拟内存地址空间,无需将整个文件复制到用户态缓冲区,还允许选择性加载特定张量而无需读取整个文件,这对于 GPT-2 这类包含上百个参数张量的模型非常实用。Forge 实现 safetensors 的读写管线,使其能直接加载 HuggingFace 生态中发布的预训练模型权重,无需经过格式转换。
参数系统与C++反射
值得单独一提的是参数系统的设计。作者用 reflect-cpp 实现了基于反射的参数发现机制:模型只需声明自己的结构,Forge 就能自动识别可训练参数,无需手动注册。
C++ 作为一门静态类型语言,在编译后丢失了大部分类型信息(名称、成员列表等元数据被编译器丢弃),不像 Python 或 Java 那样拥有运行时反射能力。Python 中一切皆对象,每个类实例都有 __dict__ 属性记录其所有成员,框架可以在运行时遍历并筛选。而在 C++ 中,框架无法在运行时自动遍历一个类的成员变量来发现哪些是可训练参数——编译后 struct 的成员名甚至不存在于二进制文件中。
reflect-cpp 是一个利用 C++20 结构化绑定(structured bindings)、概念(concepts)和模板元编程技术实现编译期反射的第三方库。它通过 aggregate initialization 和 std::tuple 的转换技巧,在编译时提取结构体的字段数量和类型信息,配合用户提供的字段名注解,实现类似运行时反射的遍历能力。
PyTorch 中用户只需继承 nn.Module 并在 __init__ 中定义层,框架就能通过 Python 的 __dict__ 和递归遍历子模块(named_parameters())自动收集所有参数;而在 C++ 中要达到类似的开发体验,传统方案包括使用宏定义(如 REGISTER_PARAMETER(name, tensor))或手动覆写参数列举函数。Forge 选择 reflect-cpp 方案,避免了大量重复的参数注册代码,使得定义新模型的体验更接近 PyTorch 的声明式风格,说明作者对开发体验有过认真思考,而不只是把功能堆砌出来。
最硬的一关:token级别精确复现GPT-2
作者自己最引以为豪的,是把真实的 GPT-2 small(124M 参数,12 层,12 头,768 维隐藏状态)预训练权重加载进 Forge 搭建的 GPT-2 架构后,在贪心解码(greedy decoding)下,输出与 HuggingFace transformers 库逐 token 完全一致——不是相似,而是精确匹配。
贪心解码是最简单的生成策略:每一步选择概率最高的 token 作为输出,不涉及随机采样或 beam search。正因为它是确定性的,两个实现如果在数值上完全一致,必然产生相同的 token 序列;反之,任何微小的数值偏差都可能改变 argmax 的结果,导致不同的 token 被选中,进而触发完全不同的生成路径(蝴蝶效应)。
这个成就的分量需要展开理解。一个 Transformer 由 embedding、多层 attention、LayerNorm、最终投影等环节串联,任何一个环节的数值实现出现偏差,比如一次错误的转置、一个 masking 的边界 bug、LayerNorm 中 epsilon 的位置不对、attention 分数未除以 √d_k,都会在生成几个 token 之后让输出彻底发散。GPT-2 的因果注意力掩码(causal mask)要求未来位置的注意力权重为零,实现时通常用负无穷填充上三角矩阵;而 LayerNorm 的位置(Pre-LN vs Post-LN)和具体的方差计算方式(是否减均值后再平方 vs 直接计算二阶矩)都是容易出错的细节。
要做到与参考实现逐 token 对齐,意味着每一层的前向计算在数值上都必须正确,且浮点累积误差需控制在不影响 argmax 结果的范围内。这实际上是一种极其严苛的端到端验证:它用一个可观测的最终结果,反向保证了整条计算链路的正确性。
换句话说,这不是「跑通了」,而是「跑对了」,且对到了每一个比特的可复现程度。对于一个从零手写的框架来说,这比任何 benchmark 数字都更能说明工程质量。
性能瓶颈与未来路线图
作者对项目现状的描述相当坦诚,没有回避短板。目前 Forge 仍是纯 CPU 实现,仅支持 float32 和 int32,还在补齐 dtype 与 SIMD 的覆盖范围。性能也「比预期慢」,主要瓶颈集中在交叉熵损失函数及其梯度计算和 softmax 上,同时尚未实现 KV-cache。
瓶颈分析是否合理
这几个瓶颈的定位是站得住脚的。交叉熵配合 softmax 在词表维度上做归一化,对 GPT-2 而言词表规模约 5 万,这一步的计算和访存开销确实可观,尤其在没有充分向量化时。具体而言,softmax 需要三次遍历整个词表维度:第一次求最大值(数值稳定性)、第二次计算指数和求和、第三次归一化——总共约 15 万次浮点运算和内存访问。交叉熵的梯度计算涉及 softmax 输出与 one-hot 标签的差值,当词表维度为 5 万时,每个 token 位置就需要处理 5 万个浮点数的向量,如果序列长度为 1024,单次反向传播仅这一步就涉及约 5000 万次浮点运算。在没有融合内核(fused kernel)将 softmax 和交叉熵合并为一次 pass 的情况下,多次遍历会严重受限于内存带宽。
而缺少 KV-cache 是当前推理性能最显著的制约因素。KV-cache(Key-Value Cache)是 Transformer 自回归推理中最关键的优化技术。在生成式推理中,模型每次只生成一个新 token,但标准注意力机制要求对完整序列的所有位置计算 Key 和 Value。由于已生成 token 对应的 Key/Value 在后续步骤中不会改变——这是因果注意力(causal attention)的数学性质保证的:第 i 个位置的 Key 和 Value 只依赖位置 1 到 i 的输入,后续新增位置不会改变这些值。KV-cache 将它们缓存下来,每步只需计算新 token 对应的一组 Q/K/V 向量,然后将新的 K/V 追加到缓存末尾,用完整缓存参与注意力计算即可。
这将推理每步的注意力计算从重算整个序列的 O(n²) 总量降至 O(n)——只需要新 query 与所有缓存 key 的点积(n 次点积),而非重新计算所有 n² 对。缺少 KV-cache 意味着生成第 100 个 token 时仍需重新计算前 99 个位置在每一层、每个注意力头中的 Key 和 Value,浪费巨大。对于 GPT-2 small 的 12 层结构,每层 12 个注意力头,每个头的 Key/Value 维度为 64,生成长度为 L 的序列时,没有 KV-cache 的总计算量大约是有 KV-cache 时的 L/2 倍。当生成 200 个 token 时,这意味着约 100 倍的冗余计算。KV-cache 的内存占用为 2(K和V)× 12(层)× 12(头)× 64(维度)× L(序列长度)× 4(bytes/float32),对于 1024 长度的序列约为 75MB,在 CPU 内存中完全可以承受。
作者把 CUDA 后端和这些性能修复列为下一步计划,方向是对的:先保证正确性,再谈吞吐,这个优先级排序符合框架开发的一般规律。引入 CUDA 后端意味着需要处理 GPU 内存管理(显存分配/释放)、主机与设备间的数据传输、CUDA kernel 的编写和启动配置、以及与 cuBLAS/cuDNN 等 NVIDIA 库的集成——每一项都是独立的工程挑战。
这个项目对深度学习开发者的启示
抛开作者年龄这个吸引眼球的标签,Forge 更像是一份关于「深度理解」的实证。今天入门深度学习的门槛被高层框架大幅降低,但也让很多人止步于 API 调用层。而从张量引擎、自动微分、SIMD 内核一路手写到能精确复现 GPT-2,这个过程本身就是对 Transformer 内部机制最彻底的一次拆解。
这种「从零构建」的学习路径在深度学习社区有着优良传统。Andrej Karpathy 的 micrograd(用 Python 百余行实现自动微分)和 nanoGPT(最小化的 GPT 训练代码)启发了大量学习者;George Hotz 的 tinygrad 则展示了如何用极简代码实现一个可运行的深度学习框架。Forge 在这个谱系中的独特位置在于:它选择了 C++ 这条更困难的路径(需要手动管理内存、处理模板编程的复杂性),同时达到了与生产级实现精确对齐的验证标准。
对想深入底层的开发者来说,Forge 的代码库提供了一个规模适中、结构完整的参考样本:既包含现代框架的关键抽象(autodiff、参数管理、safetensors),又没有大到无法通读(对比 PyTorch 核心代码库超过百万行 C++/CUDA 代码)。它不会取代 PyTorch,但它证明了一件事——理解这些系统如何运作,依然是可以靠一个人从零走通的。
项目已在 GitHub 开源,并提供 Windows/Linux 的 release 构建,作者也放出了 YouTube 演示视频,公开征求反馈。对于一个仍在快速迭代的个人项目而言,这种开放和自省的姿态,本身就值得肯定。
相关推荐

Vibe Coding进阶:从玩具项目到企业级AI编程实战指南
深入解析Vibe Coding的天花板与突破路径,涵盖Claude Code、Codex工具选型,SuperPower插件与SDD规范驱动开发三种递进模式,助你掌握AI工程化编程方法论,真正落地企业级项目开发。

Entropic Scree:用信息熵替代方差重构PCA降维方法
Entropic Scree是一种基于信息论的降维新方法,用信息熵替代线性方差来估计数据内在维度。本文详解其核心原理、相对传统PCA的优势,以及在神经网络瓶颈层设计中的实际应用。

ResNet残差连接为何有效?深层网络退化问题实验复现
通过CIFAR-10实验复现深层网络退化问题:56层普通网络训练准确率仅84%,远低于20层的95%。深入解析ResNet跳跃连接如何解决优化困难,以及残差结构在现代深度学习中的核心地位。