wrap而非rewrite设计哲学:train-llm-from-scratch的代码复用案例
【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch
train-llm-from-scratch 是一个从下载数据到生成文本、全程手写 PyTorch 的 LLM 训练项目。它最值得关注的一点,不是"从零训练大模型"本身,而是它用wrap(包裹)而非 rewrite(重写)的设计哲学,把 SFT、奖励模型、PPO、DPO、GRPO 等后训练阶段全部"插"在同一个 Transformer 骨干上,代码复用做到了教科书级别。这篇文章带你用几个真实案例看懂这个模式。
什么是"wrap 而非 rewrite"?
通俗地说:拿到一个已经写好的模块,不要 fork 一份出来改,而是在外面包一层,只新增你需要的部分,原模块一行不动。
这是"组合优于继承"(Composition over Inheritance)的实战版。它带来四个直接好处:
- ✅ 骨干代码保持"教学级"简洁,永远只有一份
- ✅ 各阶段可以并行扩展(PPO 加价值头、奖励模型加奖励头),互不干扰
- ✅ 检查点格式天然兼容,任意阶段的权重都能装回同一套结构
- ✅ 新阶段 = 新 wrapper 类,通常不到 50 行代码
基础:forward_hidden —— 为复用预留的"接口"
整个 wrap 体系的地基,是模型类里一个刻意拆出来的方法forward_hidden:它只跑骨干(embedding + 各 Transformer 块 + LayerNorm),返回最后一层 hidden states,但不经过lm_head。
注释写得很直白:这正是后训练阶段辅助头(PPO 的 value head、奖励模型的 reward head)需要的表示,"把它单独成方法,让这些头可以复用骨干,而不用复制前向逻辑或重写 forward"。
📍 位置:forward_hidden
设计启示:当你预感到模块将来要被别的场景"借用"时,把通用主干和特定输出头拆开,就是给未来留 wrap 接口。
案例一:PPO 价值头 —— 一个类加一个 MLP
PPO 需要同时拿到策略 logits 和逐 token 的价值估计 V(s_t)。项目的做法是让TransformerWithValueHead直接持有一个Transformer实例,只新增一个两层的小 MLP 作为价值头:
TransformerWithValueHead(transformer) # 原模型原封不动地装进来它的前向过程:调用骨干的forward_hidden拿 hidden states,lm_head产出策略 logits,value_head产出 values——一次前向,两份输出,骨干零改动。价值头权重初始化为 0,保证训练初期 critic 不会把策略带偏。
📍 源码:value_head.py
案例二:奖励模型 —— 一个线性层"变身"
InstructGPT 式奖励模型听起来是"另一种模型",但在这里它只是又一个 wrapper:RewardModel包一层Transformer,加一个nn.Linear(n_embed, 1),从序列最后一个真实 token 的 hidden state 读出标量奖励。原模型的lm_head甚至直接闲置不用。
📍 源码:RewardModel
对比一下反面做法:如果 rewrite,你需要复制整个 Transformer 类、删掉 lm_head、改 forward、改保存逻辑,之后每次骨干升级都要同步两处。wrap 则让"奖励模型"从架构意义上就是同一个模型 + 一个小头。
案例三:通用函数让"任意包装后的模型"都能工作
更妙的是上层工具函数的写法。PPO/GRPO 的 log-prob 计算被写成自由函数f(model, ...)而不是模型方法,注释解释:同一套数学要对四组不同参数跑(可训练策略、冻结参考、旧策略快照、actor-critic wrapper),自由函数比绑定方法组合性更好,而且"保持教学模型文件不被触碰"。
配套工具同样是 wrap 思维:
make_frozen_copy:深拷贝任意模型、置 eval、关梯度,一行代码造出 DPO/PPO 的参考模型与旧策略快照 —— utils.pyload_backbone_from_ckpt:从检查点只挑出骨干的键加载,包装器自己的头由 wrapper 自己新增 —— utils.py- 多卡训练时再用 DDP 把模型包一层,单卡则原样返回 —— distributed.py
可以看到一个清晰的"俄罗斯套娃":Transformer→RewardModel / TransformerWithValueHead→ DDP 包装,每层只负责一件事。
配置系统也在"wrap":四层合并而非各写一套
这个哲学甚至贯穿了配置体系。loader.py 里的load_config按优先级自下而上合并四层:dataclass 默认值 < base.json < 阶段 JSON < 命令行覆盖。
- 共享的模型参数写在 configs/base.json 里,每个阶段只写自己的超参
- configs/smoke/ 里有一整套"冒烟"配置,自动继承同目录的 base.json 把模型缩到能在 CPU 上几秒跑完 —— 这就是"包一层配置"而非复制整份 JSON
快速上手:亲手验证这个模式
git clone https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch cd train-llm-from-scratch pip install -e ".[all]"之后用 smoke 配置跑一遍 SFT 或 PPO(几秒出结果),再打开src/post_training/value_head.py对照着读,wrap 模式一目了然。命令与配置详解见 docs/howto/train.md 和 docs/howto/configs.md。
把这套模式带回你自己的项目
| 场景 | rewrite 做法 | wrap 做法 |
|---|---|---|
| 给模型加输出头 | 复制模型类改 forward | nn.Module持有原模型 + 新头 |
| 新增参考/旧策略模型 | 再写一套训练代码 | copy.deepcopy+ 关梯度 |
| 分布式支持 | 单卡/多卡各写一份 | 条件包装,单卡原样返回 |
| 多阶段配置 | 每阶段完整 JSON | base + 阶段差异 + CLI 覆盖 |
想继续深挖:
- 模型与注意力实现:src/models/
- SFT / 奖励 / PPO / DPO / GRPO 全部后训练代码:src/post_training/
- 后训练全景说明:POST_TRAINING.md
- 各阶段文档:docs/03_sft.md、docs/05_dpo.md、docs/06_ppo.md、docs/07_grpo.md
一句话总结:train-llm-from-scratch 用"包一层、加个小头、写通用函数"三板斧,把五个后训练算法叠在同一份骨干代码上——这就是 wrap 而非 rewrite 的威力:扩展成本以行数计,维护成本几乎为零。
【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考