一个语言模型,如何从C里长出来

一名开发者近日用纯 C 打通张量、自动微分、Transformer 与 AdamW,并训练出约 200 万参数的小型语言模型。项目的价值不在模型能力,而在于把被 PyTorch 隐藏的训练全栈重新摊开。
一个语言模型,如何从 C 里长出来
一名开发者近日在 Reddit 的 r/MachineLearning 社区展示了一套从零用 C 编写的深度学习库,并用它完成了一个约 200 万参数语言模型的训练。这里的“从零”不是不用预训练权重,而是不依赖 PyTorch、TensorFlow 等机器学习框架:张量、内存分配、自动微分、优化器、Transformer 解码器乃至 AVX2 矩阵乘法,都由开发者自己实现。
这个项目的模型规模很小,但工程跨度并不小。根据作者披露的信息,训练语料为 Tiny Shakespeare,文件总计 743,500 字节,其中 669,150 字节用于训练、74,350 字节用于验证;模型采用 4 层 Decoder,隐藏维度为 192,共有 6 个注意力头,参数量约 200 万。

这不是一次“用 C 挑战 PyTorch 性能”的产品发布,而是一场把语言模型训练系统逐层拆开的工程实验。它真正值得关注的地方,是开发者不再满足于调用现成框架拼装模型,而是开始追问:一个 loss 究竟如何变成每个权重上的梯度,一次矩阵乘法为什么会慢,Tensor 的 view 为什么可能让反向传播出错,以及 AdamW 到底保存了多少额外状态。
200 万参数很小,但训练链路是完整的
小型语言模型(SLM)是参数规模、计算成本和任务范围均小于主流大语言模型的神经网络,通常用于端侧部署、垂直任务或教学实验。这个 C 项目训练的约 200 万参数模型,显然无法与数十亿参数的通用模型竞争,但它已经足以验证语言模型训练所需的核心组件是否闭环。
作者给出的模型配置是 L=4、C=192、H=6。其中,L 表示 Transformer 层数,C 表示隐藏维度,H 表示注意力头数;如果每个头平均分配隐藏维度,那么单个注意力头的维度为 192÷6=32。
这个参数配置与“约 200 万参数”的描述基本吻合。对于标准 Decoder-only Transformer,每层注意力投影和前馈网络的主要权重规模大致与隐藏维度平方成正比;按每层约 12×C² 粗略估算,4 层主体参数约为 177 万,再加上词嵌入、归一化和输出层,最终落在 200 万左右是合理的,具体数值仍取决于词表大小、FFN 扩展比例以及输入输出嵌入是否共享。
Tiny Shakespeare 是一个由莎士比亚文本构成的小型语言建模数据集,常被用于验证字符级或小词表语言模型能否完成训练。作者公布的是字节数,而不是 tokenizer 处理后的 token 数,因此目前无法准确计算训练 token 总量、每 token 吞吐率或数据与参数的比例。
这一区别并非文字游戏。743,500 字节如果按字符级建模,样本数量和上下文组织方式相对直观;如果使用 BPE、WordPiece 或其他子词 tokenizer,token 数量、词表参数以及训练难度都会变化。作者没有在帖子中披露 tokenizer、上下文长度、batch size、学习率、训练轮数和最终验证损失,因此“结果不错”目前只能视作主观判断,不能当成可复现的模型质量结论。
真正困难的不是 Transformer,而是张量系统
张量系统是负责保存多维数据、形状、步长、数据类型和底层内存关系的计算基础设施。很多从零实现神经网络的教程会直接使用 NumPy 数组,把注意力放在公式上;而在纯 C 中,开发者首先要解决的是数组究竟位于哪里、由谁释放,以及一次 reshape 是否需要复制数据。
作者实现了张量操作、view 和内存分配。Tensor view 是多个张量对象共享同一块底层数据、但使用不同形状或步长解释数据的机制,它可以避免复制,却也会引入连续性、生命周期和别名问题。
这部分比写一个矩阵乘法函数更容易埋雷。一个转置后的二维张量在逻辑上仍是矩阵,但内存通常不再连续;如果后续算子默认数据按行连续排列,结果可能直接算错。更麻烦的是,两个 view 指向同一缓冲区时,原地修改其中一个对象可能悄悄改变另一个对象,而错误往往要到若干层之后才表现为 loss 异常。
成熟框架隐藏了大量类似约束。PyTorch 中一个看似简单的 reshape、广播或切片操作,背后会检查 stride、contiguous 状态、设备和 dtype;纯 C 实现没有这层保护,开发者必须自己定义张量元数据、引用关系与释放策略。
自动微分是一张需要反向行走的图
自动微分(Autograd)是记录前向计算依赖关系,并利用链式法则自动计算各参数梯度的机制。作者将计算过程保存为有向无环图(DAG):每个输出张量记录生成它的操作及输入张量,反向传播时再按照拓扑逆序逐步计算偏导数。
自动微分的核心不是“求导公式很多”,而是正确处理梯度的流动与累加。一个参数可能通过多条路径影响最终 loss,因此其梯度不是覆盖写入,而是来自所有路径的贡献之和;广播操作在反向阶段还要把扩展过的维度重新求和,矩阵乘法则要正确放置转置。
内存生命周期也会立刻变复杂。前向传播时为了节省空间,开发者希望尽早释放中间张量;反向传播却可能仍需要这些激活值来计算梯度。成熟框架会使用引用计数、保存必要张量、梯度检查点等机制折中速度和内存,而教学级实现通常先选择保留整张计算图,以换取逻辑清晰。
梯度验证是这类项目不可缺少但尚未披露的环节。最常见的方法是用有限差分近似某个参数的导数,再与自动微分结果对比;如果没有逐算子的数值梯度测试,模型即便能够降低训练 loss,也不能完全证明所有 backward 实现正确,因为错误可能在小数据集上被优化过程暂时掩盖。
Decoder 只是最上面的一层
Decoder-only Transformer 是通过因果自注意力逐 token 预测后续内容的神经网络架构,也是 GPT 类语言模型的基本结构。作者实现的 Decoder 包含 Layer Normalization、多头注意力和前馈网络,这三部分构成了现代自回归语言模型的主要计算骨架。
层归一化(Layer Normalization)是对单个样本隐藏维度上的激活进行标准化,并通过可训练缩放与偏置恢复表达能力的操作。它的公式不复杂,但纯 C 实现需要考虑方差计算的数值稳定性、极小常数 epsilon,以及反向传播中多个统计量之间的依赖。
多头注意力(MHA)是把隐藏状态投影到多组 Query、Key、Value 子空间,并分别计算注意力后再合并的机制。这个模型的 6 个注意力头各自处理 32 维子空间,还必须应用因果掩码,防止当前位置看到未来 token。
前馈网络(FFN)是对每个 token 独立执行的两层非线性变换,通常承担 Transformer 中相当一部分参数和计算量。虽然它在数学上只是两个线性层夹一个激活函数,但在 CPU 上,大矩阵乘法的实现质量会直接决定整体训练速度。
作者还实现了 SGD 和 AdamW 两种优化器。SGD 是沿负梯度方向更新参数的基础优化算法;AdamW 是使用一阶矩和二阶矩估计自适应调整步长,并将权重衰减与梯度更新解耦的优化算法。
AdamW 的便利是以更多内存为代价的。每个可训练参数通常除了权重和梯度外,还要保存一阶矩与二阶矩状态;如果全部使用 FP32,仅参数、梯度和两组优化器状态理论上就需要约 16 字节/参数,200 万参数约占 32 MB,尚未计算激活、计算图、临时矩阵和内存对齐开销。
AVX2 能提速,但“用了向量指令”不等于快
AVX2 是 x86 CPU 上支持 256 位向量寄存器的一组 SIMD 指令扩展,可让单条指令同时处理多个整数或浮点数。对于 FP32 数据,一个 256 位寄存器理论上能够一次容纳 8 个浮点数,因此它是优化 CPU 矩阵乘法的常见起点。
作者为矩阵乘法加入了 AVX2 快速路径,但没有公布优化前后的耗时、每秒浮点运算量或训练吞吐率。没有这些数字,就无法判断实际提升来自 SIMD、缓存访问改善、循环展开,还是编译器原本没有完成自动向量化。
高性能矩阵乘法的难点主要是数据搬运,而不仅是乘加次数。处理器计算得再快,如果每次都要从较慢的内存层级重新读取矩阵,执行单元仍会等待数据;成熟 BLAS 库通常通过分块、打包、缓存复用、多线程和针对微架构设计的 micro-kernel 提升效率。
AVX2 实现还需要面对尾部元素和硬件兼容性。维度不是 8 的倍数时要处理 remainder,数据地址可能需要对齐,不支持 AVX2 的 CPU 则必须走标量回退路径;192 恰好能被 8 整除,对当前模型比较友好,但通用张量库不能假定所有维度都如此整齐。
因此,这个项目最需要补上的不是另一种激活函数,而是一组可验证的基准数据。至少应披露单线程和多线程矩阵乘法吞吐、不同矩阵尺寸下相对朴素实现的加速比、每个训练 step 的耗时、峰值内存,以及与 OpenBLAS 或其他成熟实现的对照。
它与 llm.c、ggml 和 PyTorch 有什么不同
这个纯 C 项目更像一套教学型训练栈,而不是成熟框架的替代品。它与 Andrej Karpathy 的 llm.c 有相似动机:用较少抽象展示语言模型训练真正发生了什么;但根据目前公开信息,它尚未展示 llm.c 那种较完整的基准、测试和多后端工程化程度。
| 项目 | 主要定位 | 语言与抽象层级 | 训练能力 | 硬件方向 | 已公开性能信息 | 获取成本 | |---|---|---|---|---|---|---| | 本次纯 C 深度学习库 | 从零理解完整训练链路 | C,自建 Tensor 与 Autograd | 已训练约 200 万参数、4 层 Decoder | CPU,加入 AVX2 | 原帖未给出耗时、吞吐或加速比 | 帖子未披露许可与完整发布方式 | | llm.c | 以简单 C/CUDA 代码复现 GPT 训练 | C/CUDA,低抽象 | 面向 GPT-2 等模型训练 | CPU、CUDA 等实现持续演进 | 仓库提供测试与部分基准信息 | 开源免费 | | ggml | 轻量张量计算与本地模型运行基础设施 | C/C++,自定义计算图与量化体系 | 更广为人知的是推理生态 | CPU、GPU 和多种后端 | 不同后端有独立基准 | 开源免费 | | PyTorch | 通用研究与生产深度学习框架 | Python 前端加 C++/CUDA 后端 | 完整训练、分布式与编译能力 | CPU、CUDA、ROCm、MPS 等 | 官方及社区基准丰富 | 开源免费 |
PyTorch 的优势不是公式比 C 更正确,而是它已经处理了大量边界条件。混合精度、分布式通信、算子融合、设备调度、动态图调试和第三方扩展都不是一个小型项目短期能够补齐的能力,所以用纯 C 重写之后发现模型跑得更快或更慢,都不能直接推导出框架路线的优劣。
ggml 的价值则说明,低层 C/C++ 张量系统并非只能用于教学。它已经成为一批本地推理工具的底层基础,不过推理与训练的系统目标不同:推理更关心量化、模型加载、KV Cache 和单 token 延迟,训练则必须额外处理反向图、梯度、优化器状态与更高的内存压力。
最大收获是看清框架替你做了什么
这个项目最有价值的产物不是莎士比亚风格文本,而是一张完整的知识地图。开发者会被迫把“训练模型”拆成数据表示、算子执行、计算图、梯度传播、参数组织、优化器状态、模型架构和硬件优化八个相互依赖的部分。
这种学习方式也比只用 Python 复刻 GPT 更接近系统工程。Python 版从零教程通常能很好地解释注意力和训练循环,但底层数组计算仍由 NumPy、PyTorch 或 CUDA 库承担;换成 C 后,stride、对齐、缓存、所有权和数值稳定性不再是可以跳过的细节。
它同时揭示了大模型训练栈中的一个常见误区:Transformer 公式并不是最难的部分。真正让现代框架变得庞大的,是数百种算子的正确性、不同硬件后端、自动混合精度、分布式并行、容错、检查点和性能调优。
这类项目目前不适合用于正式模型研发。原帖没有给出代码许可、单元测试覆盖率、梯度检查、随机种子、训练超参数、模型 checkpoint、验证 loss 曲线和硬件配置,外部开发者尚无法独立复现实验结果。
这类项目却非常适合作为高级开发者的学习路线。相比直接训练一个更大的模型,从 200 万参数起步能让一次实验在普通设备上完成,也让错误更容易定位;等 Tensor、Autograd 和 Decoder 都被验证后,再增加线程并行、混合精度、量化或 GPU 后端,工程关系会清楚得多。
下一步应该先补测试,而不是继续堆功能
这个项目下一阶段最重要的工作是建立可验证性。一个深度学习库是否可信,不取决于它支持多少层,而取决于每个前向算子和反向算子能否通过参考实现对照、有限差分检查与端到端收敛测试。
开发者至少可以补齐以下四组数据:
- 算子正确性:将矩阵乘法、LayerNorm、Softmax 和 Attention 的输出与高精度参考实现逐元素比较。
- 梯度正确性:对每个可训练算子执行有限差分检查,并公开最大绝对误差和相对误差。
- 训练可复现性:披露 tokenizer、上下文长度、batch size、学习率、AdamW 参数、训练步数、随机种子和最终验证 loss。
- 系统性能:分别给出标量、编译器自动向量化和手写 AVX2 三种路径的耗时,同时标注 CPU 型号、线程数和编译参数。
如果这些数据齐全,这套 C 库就不只是一段“我做到了”的项目展示,而会成为能够被其他开发者复用和验证的语言模型训练教材。它未必会成为下一个 PyTorch,但完全可能成为理解 PyTorch 为什么复杂的一条捷径。
结论
从零用 C 训练语言模型的意义,是把现代 AI 开发重新拉回可解释的工程层面。200 万参数、743,500 字节语料和 4 层 Decoder 都不算惊人,但从 Tensor view 一路走到 AdamW 和 AVX2,已经覆盖了语言模型训练系统最关键的纵向链路。
这也代表一种值得注意的开发者趋势。随着调用模型和套用框架越来越容易,真正稀缺的能力反而变成理解框架内部的计算、内存与硬件行为;会训练一个模型的人很多,能够解释每个字节为何存在、每个梯度从哪里来的人仍然很少。
截至 2026 年 7 月 28 日,这个项目仍更接近一次有技术含量的个人实验,而不是可用于生产的深度学习框架。我们的判断是:它的模型结果不重要,训练全栈被完整拆开才重要;如果作者继续补齐源码、测试和基准,它会比再训练一个稍大的 Tiny Shakespeare 模型更有价值。
参考来源
- Reddit:I built a deep learning library from scratch in C that lets you train language models:项目作者披露 Tensor、Autograd、AdamW、Decoder、AVX2 与 TinyLM 配置的原始帖子。
- GitHub:karpathy/llm.c:使用简单 C/CUDA 代码实现语言模型训练的代表性项目,可用于比较低抽象训练栈的工程路线。
- GitHub:ggml-org/ggml:面向机器学习的低层张量库,展示 C/C++ 张量系统、计算图与多硬件后端的工程实践。
- GitHub:datawhalechina/happy-llm:中文大模型原理与实践教程,覆盖 Transformer、预训练、微调和 LLaMA2 构建流程。



