
一、这篇文章在讲什么现在市面上讲Transformer的文章很多但大部分都停留在调用PyTorch的nn.MultiheadAttention这个层面。你点进去看源码底层是C写的、调了BLAS库、可能还有CUDA核函数黑盒一层套一层根本看不清数据到底是怎么流动的。这个项目不一样——它用纯C、纯标量运算、纯嵌套for循环把GPT风格的Transformer从头到尾写了一遍。没有矩阵库没有SIMD没有BLAS连一个*矩阵乘法都没有。每一个乘法、每一个加法都是一行你能打断点调试的代码。而且它不只做推理forward连反向传播backward和SGD训练也手写了并且和PyTorch对过答案误差在1e-6级别。如果你一直好奇Transformer里面到底在算什么但又不想被PyTorch的自动求导和底层优化搞晕这个项目就是最好的显微镜。二、它到底透明在哪里2.1 没有黑盒矩阵运算普通代码里你看到的是attn_outputtorch.matmul(Q,K.transpose(-2,-1))这个项目里你看到的是for(inth0;hN_HEADS;h){for(inti0;iSEQ_LEN;i){for(intj0;ji;j){// 注意 j i这就是因果性floatscore0.0f;for(intd0;dD_HEAD;d){scoreQ[i][h][d]*K[j][h][d];// 一次乘加清清楚楚}score/sqrtf(D_HEAD);// 缩放// ... softmax ...}}}每一个score都是一次手算的乘加。你可以在这个score ...这一行打断点看看到底是哪个权重乘了哪个激活值。2.2 因果性不靠mask矩阵靠循环边界大模型里常说的因果注意力causal attention意思是位置i只能看到位置0到i的信息不能偷看未来。很多实现是用一个上三角mask矩阵未来位置填-inf来做的。这个项目更直接——它直接在循环里写j i。j超过i的循环根本不执行自然就没有信息泄露。没有mask没有inf就是一个简单的循环条件。2.3 反向传播也手写不用autogradPyTorch的.backward()是自动的你根本看不到梯度怎么传的。这个项目里从loss到每个权重的梯度都是一行一行用链式法则手推的。比如MLP的第一层权重W1它的梯度是// dW1[d][m] x[i][d] * d_hidden[i][m]for(inti0;iSEQ_LEN;i){for(intm0;mD_MLP;m){for(intd0;dD_MODEL;d){d_W1[d][m]x[i][d]*d_hidden[i][m];}}}这就是大学课本里教的链式法则没有任何魔法。三、模型长什么样If you need the complete source code, please add the WeChat number (c17865354792)别看它手写架构和真正的GPT是一模一样的组件说明词嵌入查表没有位置编码测试用简化4层Transformer每层 LayerNorm → 多头注意力 → 残差 → LayerNorm → MLP → 残差多头注意力4个头每个头8维Q/K/V分开投影MLP先扩到128维ReLU激活再压回32维Final LayerNorm输出前再做一次归一化输出投影线性映射到256维词表得到logits模型维度故意设得很小D_MODEL32, SEQ_LEN16这样验证跑起来不到一秒但架构和真实模型完全一致。四、核心模块逐个拆解4.1 词嵌入就是一个查表输入是[3, 7, 1, 9, ...]这样的token ID。嵌入层做的事情很简单for(inti0;iSEQ_LEN;i){inttokeninput_ids[i];for(intd0;dD_MODEL;d){residual[i][d]embedding_table[token][d];}}没有花里胡哨的nn.Embedding就是一个二维数组查表。residual这个词很重要——它是贯穿整个模型的残差流每层都在它上面做加减。4.2 多头注意力你在看谁这是Transformer最核心的部分。大白话讲每个位置的每个头都要问一句我和前面每个位置有多像像的话它的信息我该复制多少过来具体步骤全是标量运算Step 1投影出Q、K、V// Q[i][h][d] sum_k x[i][k] * Wq[k][h][d]fori,h,d:fork:Q[i][h][d]x[i][k]*Wq[k][h][d];K和V同理。Step 2算相似度分数// score[i][j][h] sum_d Q[i][h][d] * K[j][h][d]fori,h:forj0;ji;j:// 因果性ford:scoreQ[i][h][d]*K[j][h][d];score/sqrt(D_HEAD);// 缩放防止softmax爆炸Step 3softmax归一化把j0到i的分数变成概率和为1的权重。Step 4加权求和// out[i][h][d] sum_j attn_weight[i][j][h] * V[j][h][d]fori,h,d:forj0;ji;j:out[i][h][d]attn_weight[i][j][h]*V[j][h][d];Step 5输出投影把多头结果拼起来再投影回D_MODEL维。4.3 MLP先膨胀再压缩中间ReLU过滤// Step 1: 扩到128维hidden[i][m]sum_d x[i][d]*W1[d][m];// Step 2: ReLU负数变0if(hidden[i][m]0)hidden[i][m]0;// Step 3: 压回32维out[i][d]sum_m hidden[i][m]*W2[m][d];为什么要先膨胀再压缩给信息腾地方。32维可能不够用拉到128维让模型有更多容量去学复杂的映射ReLU负责筛选有用的信号负数扔掉最后再压回32维和残差流汇合。4.4 LayerNorm让每个位置自我标准化不是对整个batch做而是对单个序列位置的每个维度做// 1. 算这32个数的均值meansum_d x[d]/D_MODEL;// 2. 算方差varsum_d(x[d]-mean)^2/D_MODEL;// 3. 拉平到均值0、方差1x_norm[d](x[d]-mean)/sqrt(varepsilon);// 4. 让模型自己学缩放y[d]gamma[d]*x_norm[d];// 这里省略了beta平移项简化反向传播时LayerNorm的梯度推导比较绕——因为mean和var都依赖所有输入链式法则要拆成三部分但代码里也就是一堆求和再除以D_MODEL全是标量。4.5 残差连接梯度的高速公路正向y x f(x)反向dx dy梯度直接穿过同时df dy子层也承担一份。大白话残差就像一条高速公路梯度可以不经过任何变换直接传回输入。这也是为什么深层Transformer能训练得下去——如果没有残差梯度在几十层之后早就 vanish消失了。五、反向传播从loss回传一步一步追责正向是从输入到输出反向就是从loss往回走问哪个权重该为loss负责整体流程从下到上Loss梯度d_logits softmax(logits) - one_hot(label)这是交叉熵的导数。Unembedding反向dW x^T · d_logits和正向的矩阵乘反过来。Final LayerNorm反向分别对mean和var求导。第4层反向MLP反向 → Attn反向 → LayerNorm反向 → 残差累加。第3层反向同上。第2层反向同上。第1层反向同上。Embedding反向把梯度累加到对应token的嵌入向量上。每层内部的反向顺序以一层为例MLP反向先算dW2再算d_hidden穿过ReLU负数位置梯度变0再算dW1。Attn反向从d_attn_out反推dQ, dK, dV再反推dWq, dWk, dWv。LayerNorm反向拆成对x_norm, var, mean的三部分导数。残差把上层传来的梯度直接加到输入梯度上。六、训练验证C和PyTorch对答案这个项目最硬核的地方——它不和PyTorch共享任何代码但结果要对得上。推理验证PyTorch初始化一个随机模型导出权重到numpy文件。C读取这些权重跑同样的输入。比较输出的logits差异在1e-6以内→ PASS。训练验证PyTorch导出初始权重。两边各自跑50步SGD每步记录loss。比较50个loss值曲线完全重合→ PASS。这意味着C手写的反向传播和PyTorch的autograd算出来的梯度在数学上是等价的。没有近似没有偷懒就是纯手推的链式法则做对了。七、设计思路为什么这样设计7.1 纯标量为了看得见用矩阵库BLAS、MKL当然快但你看到的是cblas_sgemm这种黑盒函数。这个项目的目的不是快是透明。每一个乘加都在源码里你可以在任意一行打断点打印任意中间变量的值确认这个位置、这个头、这个维度到底在算什么7.2 无偏置减少干扰线性层没有bias偏置项。这不是偷懒——现代大模型如GPT-2/3/4、LLaMA的注意力层和MLP层通常都不加bias因为LayerNorm已经做了平移再加bias是冗余的。去掉bias让权重数量更少代码更干净。7.3 ReLU代替GELU先跑通再替换GELU是GPT-2以后的主流激活函数但公式比ReLU复杂涉及erf误差函数。先用ReLU把整体框架验证通之后把ReLU那行换成GELU就行——因为所有运算都是手写的换激活函数就是改一个函数体。7.4 SGD代替Adam最小化复杂度Adam需要维护一阶矩动量和二阶矩自适应学习率代码量多一倍。先用最朴素的SGDW - lr * dW验证训练流程正确Adam可以后面加。八、涉及的核心知识点总结知识点在这个项目里的体现自注意力机制Q/K/V投影、缩放点积、softmax、因果掩码ji多头注意力多个头并行计算输出拼接后投影残差连接x x sublayer(x)梯度高速公路LayerNorm按位置归一化gamma缩放前馈网络扩维→激活→压缩MLP标准结构交叉熵损失分类任务的标配loss反向传播/链式法则每个操作都有对应的梯度公式手推实现SGD优化器最简单的梯度下降无动量因果语言模型只能看过去不能看未来数值验证独立实现交叉验证确保数学正确性九、适合谁看刚学Transformer的人不想被PyTorch的封装搞晕想从最底层理解数据流。想搞自定义算子的人看看没有autograd的情况下反向传播怎么手推。对数值精度敏感的人理解浮点运算的累积误差以及为什么1e-6的验证标准合理。怀旧派像80年代那样每个FLOPS都看得见。好下面把推理验证和训练验证两套流程拆开讲每一步该敲什么命令、预期看到什么输出都给你列清楚。十、验证推理Inference和 验证训练Training核心思路PyTorch 先训练一个 tiny 模型把权重和输入数据存成文件C 读取这些文件跑一遍前向传播最后比较两边的输出 logits 是否一致。步骤 1PyTorch 端生成标准答案python train_and_export.py这个脚本干了几件事用同样的超参数D_MODEL32, N_HEADS4等初始化一个 PyTorch 模型生成一组随机输入序列跑前向传播得到参考 logits把权重和参考 logits导出成.npy文件NumPy 格式跑完之后你的目录里会多出几个.npy文件比如embed.npy、attn_q_0.npy、ref_logits.npy这些。步骤 2编译 C 推理代码g-O2-otransformer_test transformer_test.cpp-lm-O2开优化跑得快一点-lm链接数学库sqrtf、expf这些需要编译完会生成一个可执行文件transformer_test步骤 3运行 C 推理并自动对答案./transformer_testC 程序会读取步骤 1 导出的.npy权重文件用完全相同的输入序列跑前向传播把自己的输出和ref_logits.npy逐元素比较预期输出PASS: outputs match within floating-point tolerance.如果看到这一行说明 C 的推理前向传播和 PyTorch 的数值误差在1e-6级别验证通过。验证训练Training核心思路两边从完全相同的初始权重出发各自跑 50 步 SGD每步记录 loss最后看两条 loss 曲线是否重合。步骤 1PyTorch 端导出初始权重python train_verify.py这个脚本干的事用固定随机种子初始化模型确保可复现导出初始权重到.npy文件自己跑 50 步 SGD每步的 loss 保存到pytorch_losses.npy同时保存训练用的输入数据和目标标签步骤 2编译 C 训练代码g-O2-otransformer_train transformer_train.cpp-lm步骤 3运行 C 训练并对比 loss./transformer_trainC 程序会读取步骤 1 的初始权重用同样的输入和标签跑 50 步 SGD每步计算交叉熵 loss保存到cpp_losses.txt自动和pytorch_losses.npy对比预期输出PASS: training losses match closely.总结这个项目最大的价值不是能跑多快而是**“能让你看懂”**。它把Transformer从调用一个高级API还原成了一堆嵌套for循环里的乘加运算。Welcome to follow WeChat official account【程序猿编码】