用纯 PyTorch 从零手搓一个完整 LLM:从预训练到 GRPO 对齐的全链路实践指南
现在我有了足够的信息,输出完整的中文学习笔记:
用纯 PyTorch 从零手搓一个完整 LLM:从预训练到 GRPO 对齐的全链路实践指南
核心观点
这个项目的定位不是"又一个玩具 Transformer",而是一套教学级的完整工业流水线复现:从原始文本到经过 RLHF 对齐、具备推理格式输出的语言模型,全程不依赖trl、peft、transformers三大"黑盒"库,所有算法用原生 PyTorch 手写。这意味着读者看到的每一行代码都是可拆解、可调试的白盒。
从技术演进位置看,这是一个渐进集成项目而非范式突破——它没有发明新算法,但把 2017 年的 Transformer 论文到 2025 年 DeepSeek-R1 的 GRPO,串成了一条单 GPU 可跑的学习路径。参照系应该是:Andrej Karpathy 的nanoGPT(只有预训练)+ HuggingFace TRL(有后训练但高度封装),这个项目填补了两者之间那片"知道原理但不知道代码如何写"的空白。
关键信息与技术机制
整体流水线
raw text → tokens → Transformer → next-token loss → base model base model → SFT → Reward Model → {PPO, DPO} → GRPO → evaluation & chat这条流水线的核心思路只有一句话(作者原话):把文本变成数字,预测下一个 token,然后不断改变数据和损失函数直到模型做你想要的事。越简单的总结,越说明作者对整件事理解得越深。
模型规模矩阵
| 配置 | 参数量 | n_embed | n_head | n_blocks |
|---|---|---|---|---|
| 入门演示 | 13.1M | 128 | 8 | 1 |
| 教程基础 | 77M | 512 | 8 | 8 |
| 后训练默认 | 406M | 1024 | 16 | 24 |
13M 的模型在免费的 Kaggle/Colab T4 上就能跑,406M 需要 V100 16GB 量级。作者还给出了精确的 GPU 显存对照表,A100 40GB 可跑到约 6-8B,RTX 4090 24GB 约 4B。
最值得拆解的机制:SFT 损失掩码
SFT 阶段最核心的不是"用指令数据微调",而是损失掩码(loss mask)的精确构造:
def encode_chat(messages, add_generation_prompt=False): ids, mask = [], [] for m in messages: role = m["role"] # role header 永远 mask 掉,不训练模型去发出 header header_ids = _encode_ordinary(_header_for(role)) ids.extend(header_ids) mask.extend([0] * len(header_ids)) content_ids = _encode_ordinary(m["content"]) is_completion = role == "assistant" ids.extend(content_ids) mask.extend([1 if is_completion else 0] * len(content_ids)) # 只训练 assistant ids.append(EOT_ID) mask.append(1 if is_completion else 0) return ids, mask实际效果是:一个 512 token 的行里,只有 48 个 assistant token 的 mask 为 1,其余 464 个 prompt token 全为 0,损失函数只在这 48 个位置反向传播。这是防止模型"学会复述提示词"的关键手术刀,而不是什么玄学技巧。
GRPO:最简洁的"去掉 Critic"方案
GRPO(DeepSeek-R1 的核心组件)相比 PPO 最革命性的地方在于移除了 Value Network(Critic),改用"同 prompt 生成多个答案,组内归一化"来估算优势函数:
def group_advantages(rewards, group_size, eps=1e-4): r = rewards.view(-1, group_size) mean = r.mean(dim=1, keepdim=True) std = r.std(dim=1, keepdim=True) adv = (r - mean) / (std + eps) # 比组内平均好多少 return adv.reshape(-1)这个设计的巧妙之处:Value Network 在小模型上训练不稳定且占显存,GRPO 用统计量代替神经网络,使单 GPU 训练 RLHF 成为现实。代价是需要每个 prompt 生成多个样本,吞吐量下降。
对数学推理任务,项目还设计了可验证奖励(Verifiable Reward):
extract_answer("42") -> 42.0 reward_gsm8k("42", 42.0) -> 1.2 # 正确且格式规范 reward_gsm8k("7", 42.0) -> 0.2 # 错误,但用了格式格式正确就给部分奖励,这是训练推理格式的稳定信号来源,也是 DeepSeek-R1 能让小模型学会"think in tags"的工程关键。
Bradley-Terry 奖励模型
def bradley_terry_loss(chosen_rewards, rejected_rewards): return -F.logsigmoid(chosen_rewards - rejected_rewards).mean()结果:该 reward model 在 7974 对偏好数据上测试准确率达到 57.4%——高于 50% 的随机基线,但距离工业级(通常 70%+)仍有明显差距,作者也没有刻意掩盖这个数字。
代码示例:MLP 和注意力机制骨架
class MLP(nn.Module): def __init__(self, n_embed): super().__init__() self.hidden = nn.Linear(n_embed, 4 * n_embed) # 展开到 4 倍 self.relu = nn.ReLU() self.proj = nn.Linear(4 * n_embed, n_embed) # 压缩回来 def forward(self, x): x = self.relu(self.hidden(x)) return self.proj(x)输入输出形状都是(B, T, n_embed),方便堆叠 block。这是经典 Transformer FFN 的最小实现,4× 展开来自原始论文,ReLU在现代 LLM 中通常被SwiGLU替代,但教学优先时ReLU更直观。
交叉验证
我搜索了两个独立信源进行验证:
信源一:知乎《大模型对齐算法深度解析:PPO、DPO、GRPO、DAPO》(2026年1月)
页面因登录限制未能完整抓取,但该文章标题和摘要方向与原项目的算法选择一致:均认为 PPO → DPO → GRPO 是一条从复杂到高效的演进路线,GRPO 因去除 Critic 而在中小规模训练中更实用。
信源二:arxiv 论文《Is DPO Superior to PPO for LLM Alignment? A Comprehensive Study》(2024年5月)
这是一篇重要的反驳性信源,对原项目"DPO 是 PPO 替代"的隐含叙事提出了质疑:
- 论文用 Theorem 4.1 严格证明:PPO 找到的策略集合是 DPO 策略集合的真子集——DPO 可能发现 PPO 永远不会产生的策略,而这些策略往往是有问题的 OOD(分布外)解
- 在代码生成任务(CodeContest)上,DPO 通过率为0%,PPO 为22.4%(超越 AlphaCode-41B 的 16.4%)
- DPO 对"base model 与偏好数据分布不匹配"极为敏感(SafeRLHF 安全率 55.4% vs SFT 后的 71.8%)
独立判断:原项目同时实现了 DPO 和 PPO,并没有过度推崇 DPO,这点是诚实的。但原项目偏向"把所有算法走一遍"的教学叙事,对"何时该用 DPO、何时必须用 PPO"的边界判断着墨不多。arxiv 论文的结论明确补充了这个缺口:DPO 适合对话类任务且数据分布匹配的场景,PPO 在高挑战性任务(代码、数学竞赛)上仍是更鲁棒的选择。
边界与局限:不该被忽略的部分
奖励模型质量是天花板:57.4% 的偏好准确率对于真实 RLHF 来说是"够用但不够好"——如果奖励模型本身判断错误,PPO/GRPO 会放大错误而不是改正它。原项目规模限制(406M base model)决定了奖励模型精度有限。
训练数据量级与真实 LLM 不在一个量级:The Pile 的子集 + Alpaca/Dolly/GSM8K,数据量远小于 GPT-3 的 300B tokens 级别。这意味着训练出的模型有 LLM 的形态,但不具备 LLM 的真实能力——这是教学目标,不是工程目标,读者不能混淆。
GRPO 的课程设计依赖先验:训练时先喂短算术题(warm-up),再喂 GSM8K,是为了保证"非零奖励信号"。如果直接上复杂任务,模型初期全答错,GRPO 拿不到区分度,训练会卡死。这个课程设计细节原文提到了,但初学者容易忽视。
没有使用 RoPE / Flash Attention / SwiGLU:对于学习 Transformer 原理足够,但要对接现代开源模型(LLaMA、Mistral),还需要补这些现代化改造。
DPO 的 OOD 问题在小模型上更严重:base model 本身分布质量差,DPO 更容易钻空子。原项目在小规模下用 DPO,实际上是在条件最不利的情况下演示 DPO,结果可能不能代表 DPO 在大模型上的真实表现。
个人启发
这个项目最实际的价值不是"训出一个好用的 LLM",而是给你一套可以单步调试的对照实验框架。具体行动建议如下:
对学生/研究者:
- 先跑 13M 的 smoke test(CPU 上几秒钟),验证整条流水线能通,再切换到真实数据。
configs/smoke/目录里的配置就是为此设计的。 - 重点读
src/post_training/目录下的 PPO 和 GRPO 实现,对照 arxiv 论文理解"为什么 GRPO 去掉了 Critic 但仍然有效"。
对开发者:
- 如果你在为特定垂直领域(医疗、法律、数学)做对齐,这套代码提供了比 TRL 更易修改的奖励函数接入点——你可以把
reward_gsm8k换成任何领域的可验证奖励。 - GPU 内存不够时,优先开
--amp+--grad-checkpointing,再考虑--grad-accum,三个 flag 叠加可以把显存压到约原来的 1/3-1/2。
对决策者/团队 lead:
- 这个项目的存在本身说明:2025 年从零复现完整 RLHF 流水线的门槛已经降到"本科生 + 一张 4090"的水平。如果你的团队对 LLM 对齐机制是黑盒状态,现在补课的成本已经足够低了。
延伸思考
GRPO 去掉 Critic 的代价是什么?每个 prompt 要采样多组答案,批量估算均值/方差作为基线。这在数学类"有唯一正确答案"的任务上效果好,但在开放式写作/对话任务上,"组内归一化"的奖励信号可能方差极大甚至无意义——GRPO 的适用边界究竟在哪里?是否存在一类任务让 GRPO 比 PPO 更差?
可验证奖励(Verifiable Reward)是否正在成为 RLHF 的新范式?从 DeepSeek-R1 到这个项目,用程序化规则(答案正误、格式检查、代码编译)替代神经网络奖励模型的趋势越来越明显。这对"无法被程序化验证"的任务(创意写作、价值观对齐)意味着什么——是新的分水岭,还是只是对特定任务类型的特例?
在教学目标和工程目标之间,这类从零实现的项目能走多远?nanoGPT 推出后催生了大量变体,但至今主流生产环境仍在用 vLLM + TRL + PEFT。手写实现的价值是理解机制,但工程优化(算子融合、CUDA kernel、分布式策略)是另一个维度的战争——这个项目在 406M 的规模停下来,是有意识的边界划定,还是受限于作者算力?
参考信源:
- 原始项目:FareedKhan-dev/train-llm-from-scratch
- 交叉验证:Arxiv 2404.10719 "Is DPO Superior to PPO for LLM Alignment? A Comprehensive Study"(2024年5月)
- 交叉验证:知乎《大模型对齐算法深度解析:PPO、DPO、GRPO、DAPO》(2026年1月)
📚 参考来源
- GitHub - FareedKhan-dev/train-llm-from-scratch: A straightforward method for training your LLM, from downloading data to generating text. · GitHub
