别只看架构图,亲手让一个 GPT 的 Loss 降下来
先用 NumPy 把每个张量从 Token ID 追到 Logits,再沿链式法则走回来;随后换到结构相同的 PyTorch 版,只用 --device 改变执行位置。一个项目同时回答“梯度怎么算”和“怎样真的放到单 GPU”。
两分钟跑通,再带着真实输出读代码
项目不下载模型,也不联网取语料。NumPy 版专门展示手写反向;PyTorch 版保留同样的单 block 数据流,并把设备、保存和加载补成真实工程路径。`corpus.txt` 会重复 24 次,种子固定为 42。
examples/tiny-gpt/ ├── corpus.txt # 固定四句小语料 ├── requirements.txt # NumPy 手写版依赖 ├── tiny_gpt.py # 手写前向、反向、Adam、采样 ├── test_tiny_gpt.py # 解析梯度 vs 有限差分 ├── requirements-pytorch.txt ├── tiny_gpt_torch.py # CPU / CUDA / MPS、保存与加载 ├── test_tiny_gpt_torch.py # checkpoint / 断点随机流回归 └── README.md # 两条路径与失败排查
可复制命令
在项目根目录执行。第一次创建隔离环境,之后只需运行最后一行。
cd examples/tiny-gpt
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python tiny_gpt.py --steps 240 --seed 42 --prompt 模型 --new-tokens 8本站实跑结果
训练前后使用同一组均匀窗口评估,避免拿两个随机 batch 的 Loss 假装提升。不同 NumPy/BLAS 版本末位可能有细小差异。
eval_before 4.0076
step 060 | train_loss 0.4141
step 120 | train_loss 0.0796
step 180 | train_loss 0.0414
step 240 | train_loss 0.0186
eval_after 0.0118
loss_delta 4.0076 -> 0.0118
sample 模型从下一个字开始学
saved tiny-gpt.npz + tiny-gpt.tokenizer.json + tiny-gpt.manifest.json同一设备脚本先在 CPU 验证
这不是伪代码。本站使用 PyTorch 2.8.0 在 Apple Silicon CPU 实跑训练,并立即重新加载 checkpoint;不同版本末位可能略有变化。
python -m pip install -r requirements-pytorch.txt
python tiny_gpt_torch.py \
--device cpu --steps 12 --seed 42 --new-tokens 8 \
--save /tmp/tiny-gpt-torch-smoke.ptdevice cpu | seed 42 | parameters 7,951
eval_before 4.0080
step 001 | train_loss 4.0078
step 006 | train_loss 3.9729
step 012 | train_loss 3.8534
eval_after 3.7994
saved /tmp/tiny-gpt-torch-smoke.pt + ...tokenizer.json这段续写主要说明模型记住了极小语料的局部规律,不代表它拥有通用语言能力。教学项目的成功标准是“链路正确且可解释”,不是输出像线上大模型。
一个 Token 不会“进入模型一下”,它会连续换六种身份
ID 只是地址;Embedding 把地址查成向量;Attention 让当前位置读取左侧上下文;MLP 在每个位置独立改写特征;LM Head 再把隐藏向量投影成整个词表的分数。
tok[idx] + pos[:T]同一个“模”字在不同位置共享字符向量,再叠加不同位置向量,所以模型同时知道“它是谁”和“它在哪”。
scores = Q @ K.T / √DQ 是当前位置想找什么,K 是每个历史位置能被怎样匹配。右上角被因果 Mask 设为负无穷,模型不能偷看答案。
attention @ VSoftmax 把匹配分数变成权重,再对 V 做加权求和。它得到的是“从历史位置取回的信息”。
x + attention; x + mlp两次残差都保留旧信息并加上新修正,让深层模型更容易传递信号。这个教学版省略 LayerNorm,生产 Transformer 通常会使用。
训练是一圈“看答案再改参数”,生成是一圈“没有答案继续猜”
训练样本 `x` 是一段字符,`y` 是同一段整体向左移动一位。模型在所有位置同时做下一字符预测。交叉熵只取正确字符的概率;概率越小,惩罚越大。
文本“模型从”怎样产生三组输入和答案?
输入是“模、型、从”,目标是“型、从、下一个字符”。位置 0 只能看“模”并猜“型”;位置 1 能看“模型”并猜“从”。一个长度 T 的 batch 会同时贡献 B×T 个下一字符训练点。
字符级词表,方便完全看懂;真实模型通常使用 BPE 类子词。
超过长度时,采样只保留最近 24 个字符。
单头、单 block、MLP 隐层 48,刻意保持小。
初始化与 batch 固定;采样用 seed+1 的独立随机流。
反向不是把公式倒放,而是给每条分叉把责任加回来
前向里一个张量可能走两条路。例如 `x1` 一路直接进入第二次残差,一路进入 MLP。反向经过这两条路时,必须把两份梯度相加;这就是计算图中“多条下游贡献汇合”。
LM Head 与交叉熵
Softmax 概率减去 one-hot 正确答案,得到 Logits 梯度;它同时产生 Head 权重梯度和回到隐藏状态的梯度。
MLP 与残差
先过 W2,再乘 `1 - tanh²(z)`,再过 W1;同时保留残差直通的那份梯度。两路在 `dx1` 相加。
Attention
从输出投影回到加权和,再分别得到 Attention 权重梯度与 V 梯度;Softmax 的雅可比被写成逐行向量公式,之后回到 Q、K、V 和四个矩阵。
Embedding 查表的反向
同一个字符可能在 batch 中出现多次,不能直接赋值梯度。`np.add.at` 会把所有出现位置的梯度累加到同一行 Embedding。
代码对梯度做 [-1,1] 裁剪,并在每一步显式检查 Loss、概率和梯度是否有限。test_tiny_gpt.py 还对 Embedding、位置向量、Q/K/V、Attention 输出、MLP 与 LM Head 的十个代表性元素做中心有限差分;2026-07-17 本地测试全部通过。有限差分只是抽查,不替代逐式审阅,但能抓住遗漏残差、转置或缩放等常见错误。
现在不是“可以迁移”,而是同一个脚本直接选择 CUDA
tiny_gpt_torch.py 只有一条训练代码。--device cpu 已完成本站冒烟测试;在装有 NVIDIA GPU 与匹配 PyTorch 的机器上,把参数改成 cuda,模型、batch、前向、Loss 和反向就会留在同一张 GPU。auto 则按 CUDA → MPS → CPU 选择。PyTorch 官方同样要求 Module 与 Tensor 显式移动到目标设备,跨设备计算不会自动替你修正。R07R08
先确认可见,再训练和保存
CUDA 路径与 CPU 冒烟测试调用同一个文件;第一条检查必须输出 True,训练第一行必须显示 device cuda,才算真的用上 NVIDIA GPU。
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
python tiny_gpt_torch.py \
--device cuda --steps 240 --seed 42 --prompt 模型 \
--save tiny-gpt-torch.pt保持不变
字符/子词编码、`x/y` 错一位、因果 Mask、Embedding→Attention→MLP→Head、交叉熵和逐 Token 采样。
交给框架
nn.Module 管参数,loss.backward() 构图求梯度,torch.optim.AdamW 管优化器状态;它与 NumPy 手写版互相对照。
只搬一次
model 与 batch 都移到所选设备,位置 ID 又在 token_ids.device 上直接创建;不会在每层来回搬 CPU/GPU。
别期待这个例子更快
模型小到无法填满 GPU,kernel 启动和数据传输开销可能超过计算。GPU 优势要在更大矩阵或更大 batch 才明显。
保存的不只是一份权重
checkpoint 同时带结构、state_dict、优化器、词表、累计步数、seed、下一批训练的 RNG 状态、语料 SHA-256 和运行时版本。加载前逐项检查精确 schema、shape、dtype、有限值、参数/字节上限与语料指纹;steps=0 只生成,steps>0 从记录的随机流继续训练。
python tiny_gpt_torch.py --device cpu --steps 0 \
--load /tmp/tiny-gpt-torch-smoke.pt --new-tokens 8python tiny_gpt_torch.py --device auto --steps 20 \
--load /tmp/tiny-gpt-torch-smoke.pt \
--save /tmp/tiny-gpt-torch-resumed.pt先按症状定位,不要同时乱改五个超参
找不到 NumPy / PyTorch
确认激活了正确环境。手写版安装 requirements.txt;设备版安装 requirements-pytorch.txt,不要混淆两个环境。
CUDA 被判定不可用
先看 torch.cuda.is_available()。False 通常意味着 PyTorch 构建、NVIDIA 驱动或 GPU 可见性不匹配;先用 CPU 验证,不要把 auto 回退误认为 CUDA 成功。
Tensor 不在同一设备
如果自行修改了脚本,新张量要在 token_ids.device 创建,model 与 batch 也必须同设备。不要在每层之间反复调用 cpu/cuda。
CUDA 显存不足
恢复 batch=8、block=24、dim=24,关闭同卡其他进程。默认模型只有 7,951 个参数,本身只需要很少显存。
Prompt 出现未知字符
字符 Tokenizer 只认识语料字符。错误会列出具体未知字符;先用“模型”,或修改语料后重新训练与保存词表。
Loss 不降 / 非有限
恢复 seed=42、lr=0.006、batch=8、block=24。一次只改变一个量,并保留初始 Loss 做对照。
Checkpoint 加载失败
NumPy 的 .npz 与 PyTorch 的 .pt 不能混用。v3 checkpoint 会拒绝 schema、shape、dtype、NaN/Inf、资源上限、RNG、词表或语料哈希不匹配;不要为“能打开”而改用 weights_only=False。
输出仍像随机字符
先判断固定评估 Loss 是否从约 4 降到更低。极小语料只能验证代码链路,不能训练通用中文模型。
一手来源与证据边界
优先使用论文、官方文档、官方模型卡和代码仓库。页面中的数字只代表来源所述设置,不自动外推到其他模型与数据。
缩放点积注意力、前馈网络、残差连接和 Transformer 主结构。
GPT-2 的自回归语言模型目标和 decoder-only 扩展路径。
把 GPT 训练主干压缩到可阅读代码的经典教育实现。
从教学实现走向多 GPU、混合精度和真实语料训练的下一步。
示例中一阶矩、二阶矩与偏差修正的来源。
固定随机数生成器、批次采样和可复现实验。
设备无关代码、CUDA 可用性与张量所在设备的官方说明。
Apple Silicon 上把 Tensor 与 Module 移到 MPS 设备的官方路径。
state_dict、optimizer state 与跨设备加载 checkpoint 的推荐做法。
state_dict 推荐路径、2.6 起 weights_only 默认行为,以及受限反序列化仍不防拒绝服务和所有内存破坏风险。
PyTorch 2.5.1 及以下的 weights_only 反序列化漏洞与 2.6.0 修复下限。