SFT
Supervised Fine-Tuning
用标注好的指令—回答数据继续训练预训练模型,是对齐的第一步。
极简 SFT 的唯一目标:让 base 模型学会按 ChatML 格式对话、答完就停。要验证的不是指标而是行为——base 答非所问,SFT 后答完就停。
Hover for the full story
SFT ANATOMY · POST-TRAINING · DATA AS OF 2026-08-28
7 个工程决策,对照 2 套教材、4 个生产框架、6 路信息源
TRL 的 SFTTrainer 一行就能训,为什么要手写?因为那一行里藏着十几个默认决策——其中三个恰好是社区排错清单的前几名。本报告把每个旋钮摆到台面上:哪些有共识,哪些是配方之争,哪些两本教材各自糊弄过去了。
事实分级:源码已验证 > 论文/卡片 > 社区经验(标注存疑)· 交互报告配套 blog 原文
§0 · 核心发现 Executive summary
TRL 的 SFTTrainer 一行就能训——那一行里藏着十几个默认决策,其中三个恰好是社区排错清单的前几名。
本报告解剖一个手写极简 SFT 训练器的 7 个工程决策:chat template、prompt masking、collate 与 loss、精度、LR schedule、数据、验证。每个决策对照 2 套教材(rlhf-book、hands-on-modern-rl)、4 个生产框架(alignment-handbook、trl、open-instruct、OpenRLHF)与 6 路信息源,结论分三档:有强共识(collate 与 loss 的实现、学习率区间、checkpoint 保存)、真实的配方之争(cosine vs linear schedule、packing 开关)、以及两本教材各自糊弄过去的坑(grad accumulation 尾部、micro-batch 平均口径、静默截断)。
三个被默认掩盖的决策——全序列 loss、linear schedule 零 warmup、静默截断——正是中文社区 Trainer 排错清单的前几名。极简实现的应对:masking 只计 assistant 段(含 <|im_end|>)、cosine + 3% warmup、截断样本在 assistant 段全丢时才弃。生产配方互证 5 项:lr 1e-5、warmup 0.03、weight decay 0、grad clip 1.0、epochs 2,与 tulu3 / OpenRLHF 逐项吻合。
| 发现 | 数值 | 说明 | 来源 |
|---|---|---|---|
| 工程决策点 | 7 个 | 模板 / masking / collate·loss / 精度 / 调度 / 数据 / 验证 | K1 |
| 对照信息源 | 6 路 | 2 套教材 · 4 个生产框架 · arXiv · 知乎 · 飞书 · Notion | K1 |
| masking 做法 | 3 种并存 | 只训末轮(rlhf-book)· 不 mask(课程代码)· generation 标记(trl) | K2/K3/K4 |
| 训练数据 | 10,000 条 | smoltalk everyday-conversations(全集 1.1M 的 0.9%) | K8 |
| 生产配方互证项 | 5 项 | lr 1e-5 · warmup 0.03 · wd 0 · clip 1.0 · epochs 2,与 tulu3 / OpenRLHF 逐项吻合 | K4 |
§1 · 训练器形态与模板 Form & template
动机不是性能——0.5B 模型怎么训都快——而是每个决策必须显式可见:后面 RM、DPO、GRPO、agentic RL 都要复用这个循环,黑箱迟早要拆开。
三条路线的差别不在能不能跑,而在决策藏在哪。框架黑箱把训练循环、loss、padding 全收进库内;教材手写循环每一步可见但各自有省略;生产框架在 Trainer 之上叠 packing、flash-attention、去污染一整套工程件。手写极简实现与 rlhf-book 的 collate·loss 逐行同构——两处独立写出的代码长得一样,是最强的"这就是标准做法"证据。
| 路线 | 代表实现 | 循环与 loss | 数据与工程 |
|---|---|---|---|
| 框架黑箱 | hands-on-modern-rl 课程实现 | TRL SFTTrainer 一行调用,循环 / loss / padding 全在库内 | 15 条手写 mock 数据;代码不 mask,与自家文档矛盾 |
| 教材手写 | rlhf-book 教材实现 | 手写循环;collate·loss 与手写极简逐行同构 | No Robots 9.5K;只训末轮 assistant;全模块不存模型 |
| 生产封装 | alignment-handbook · open-instruct · OpenRLHF | Trainer 封装之上叠 packing、flash-attention | tulu3 940K 配比混合 + 去污染工具链 |
| 手写极简(本报告基准) | 从零写的训练循环 | 全显式,每个决策能讲清为什么 | smoltalk 10K 单源 |
Source · K2 教材代码 · K3 课程代码 · K4 生产框架快照 · K9 基准实现工作区(快照 2026-08-28)
原始数据是结构化的 messages(role + content),模型只认纯文本,第一步是渲染。手写实现选 ChatML——每条消息包成 <|im_start|>{role}\n{content}<|im_end|>\n——与 Qwen 官方 tokenizer 自带 chat_template 的渲染结果逐字符一致(已验证)。看似没有决策含量,三个信息源各自给了一个坑。
坑一 · base 模型没有模板。 OLMo-2-1B 是 base 模型,tokenizer 里没住 chat template;rlhf-book 的做法是从姊妹模型 allenai/OLMo-2-0425-1B-SFT 的 tokenizer 整段拷贝 chat_template 属性。真实世界里模板不一定现成。
坑二 · apply_chat_template 有隐藏行为。 Qwen 的模板在 messages 无 system 段时会注入默认 system prompt("You are Qwen, created by Alibaba Cloud...")。hands-on 课程的数据没有 system 段,训练文本里实际带着这句没审过的默认文本。手写实现只渲染样本里已有的段,不注入任何东西。
坑三 · 模板错是 SFT 的头号隐形问题。 HuggingFace 官方博客《Chat Templates: An End to the Silent Performance Killer》(2024-01)被材料库标为"SFT 最高频的坑";中文社区 Trainer 排错清单第一条就是"loss 不降 → chat template 错 / labels mask 错"。
取舍:先手写模板把结构看懂——模板就是字符串拼接——之后对接官方模板时才知道自己在对接什么。
Supervised Fine-Tuning
用标注好的指令—回答数据继续训练预训练模型,是对齐的第一步。
极简 SFT 的唯一目标:让 base 模型学会按 ChatML 格式对话、答完就停。要验证的不是指标而是行为——base 答非所问,SFT 后答完就停。
Hover for the full story
Chat Markup Language
用 <|im_start|>role 与 <|im_end|> 包裹每条消息的对话格式。
Qwen 系采用。特殊 token 在词表里注册过,<|im_start|> 编码出来是完整一个 token,不会碎裂——模板本质是字符串拼接,不是黑魔法。
Hover for the full story
Prompt Masking · Loss Masking
只对 assistant token 计 loss,user / system 位置置 -100。
全篇最大分歧点:rlhf-book 只训最后一轮,hands-on 课程代码完全不 mask,trl 用 generation 标记,还有论文挑战 masking 本身。裁决见 §3。
Hover for the full story
Gradient Accumulation
攒若干个 micro-batch 的梯度再 step 一次,模拟更大的有效 batch。
一个 epoch 的 micro-batch 数不一定整除 accum 步数:rlhf-book 把尾部残余组直接丢弃,极简实现让残余组也 step 但 loss 仍除以标称 accum 数——两本教材各糊弄一半。见 §6 闸门。
Hover for the full story
Brain Float 16
纯 bf16 加载权重:不用 autocast、没有 fp32 master weights、没有 GradScaler。
教学场景图省事,0.5B 也犯不上。真正的混合精度(fp32 master + loss scaling)在 Megatron / DeepSpeed 那一层;在已 bf16 的权重外再包一层 autocast 是常见冗余,很容易顺手写多。
Hover for the full story
Sequence Packing
把多条样本拼进同一序列,省掉 padding 的算力浪费。
教学共同省略;Netflix 分享给出吞吐 4.7×,IBM arXiv 2407.09105 是落地说明。代价是 mask 串味风险,需 position_ids 重置做 attention 隔离——未决问题见 §7。
Hover for the full story
§2 · 决策对照矩阵 Decision matrix
行是决策,列是来源:手写极简、两套教材、四个生产框架。共识填一格,分歧现形。
矩阵的读法:同色即共识。collate·loss 的实现、学习率落在 5e-6~2e-5 生产区间、checkpoint 保存,是全列一致的强共识区;prompt masking 是全篇最大分歧点——"只训最后一轮""不 mask""completion-only"三种做法并存;LR schedule 的 cosine vs linear 是配方之争而非对错。矩阵里还躺着两个"你以为没做决策、其实已经做了"的格子:不显式指定 scheduler = linear + 零 warmup(hands-on 课程代码就是这样跑出来的)、trl 默认 2e-5。
"…the model may never learn to stop."
— TRL SFTTrainer,assistant_only_loss 警告Source · K4(trl 源码快照,2026-08-28)
Context 触发条件:assistant 轮的结束 token 不在 loss mask 内。Why it matters 这佐证了把 <|im_end|> 训进 loss 的必要性——模型必须学会停止;也给矩阵里"截断策略"一行 OpenRLHF 强制 EOS 收尾的格子提供了分量。
§3 · masking 裁决 The masking verdict
规则一句话:每个位置的 logits 预测下一个 token,预测目标是 assistant token 就计分,否则置 -100。
手写极简的选择是所有 assistant 段都计(含 <|im_start|>、<|im_end|> 包裹标记),user / system 段全 mask。loss 写成公式:
L = −(1/|A|) · Σt: xt+1 ∈ A log pθ(xt+1 | x≤t) A = assistant 段 token 集合,|A| = 有效 token 数
信息源里的分布并不整齐:rlhf-book 只训最后一轮 assistant——多轮对话中间的 assistant 回复也 mask,连 <|assistant|> 生成头本身都 mask;hands-on 课程的代码用 "text" 列喂 SFTTrainer、对整个序列计 loss,而自家文档把 user / system 计入 loss 称为"最常见的坑……这不是洁癖,这是在保护你训练出来的模型不去背诵提示词"——同一仓库,代码与文档互相矛盾,是"教学实现 ≠ 最佳实践"的最直接样本。trl 现在的方案是 assistant_only_loss + 模板里的 {% generation %} 标记,但通常只把 assistant content 包进标记,即生成头不计 loss;极简实现把头也算进去,一两 token 之差,属可以争论的细节。
更根本的挑战来自研究。《Instruction Tuning with Loss over Instructions》(arXiv 2405.14394)发现:对 instruction 部分也算 loss(IM)在 21 个 benchmark 的许多场景下优于只在 output 上算 loss(OM)——completion-only 是当前框架默认,不是被证明的正确;当目标是让模型内化指令分布时,masking 会丢信号。
裁决:维持全 assistant 段口径,但把 masking 从"理所当然"改写为"有争议的设计选择"。证伪条件写死:若目标只是"让模型只学接话",OM 仍是无争议默认;争议只在"是否还要内化指令分布"。两条未决一并登记:0.5B 小模型上 IM 优势是否保持、多轮场景 IM 与 OM 的差距方向——见 §7 赔率板。
§4 · 调度与精度 Schedule & precision
lr、schedule、warmup 三个旋钮,7 个来源给出 5 种配方;不显式指定 scheduler,本身就是一个决策。
手写极简实现用 cosine 调度加 3% warmup。生产侧的答案并不统一:Tulu 3 的 8B SFT 配方是 lr 5e-6 + linear decay + warmup 0.03(open-instruct 官方可复现配置,比论文转述更硬);Zephyr 是 2e-5 + cosine + warmup 0.1;OpenRLHF 默认 cosine_with_min_lr;rlhf-book 干脆手写线性衰减——教学书自己也不用 cosine。极简实现的 lr 1e-5 落在 5e-6~2e-5 生产区间(模型越小 lr 越大),warmup 0.03、weight decay 0.0、grad clip 1.0、epochs 2 与 tulu3 / OpenRLHF 逐项吻合。有效 batch 32(8×4)对 0.5B 是教学缩放——rlhf-book 巧合地也是 32(4×8),生产是 128–256。
| 来源 | LR | schedule | warmup | 备注 |
|---|---|---|---|---|
| 手写极简(本报告基准) | 1e-5 | cosine | 3% | wd 0 · clip 1.0 · epochs 2 |
| rlhf-book | 5e-6 | 手写线性衰减 | — | 教材自己也不用 cosine |
| hands-on 课程 | 2e-5 | linear | 0 | 默认配置;不显式指定 = 隐藏决策 |
| trl SFTTrainer | 2e-5(默认) | cosine(默认) | — | 默认 lr 与默认 schedule 都不显式 |
| Zephyr-7b-beta | 2e-5 | cosine | 10% | alignment-handbook 官方配方 |
| tulu3 8B | 5e-6 | linear | 3% | open-instruct 官方 8B 配方 |
| OpenRLHF | 5e-6(默认) | cosine_with_min_lr | — | 官方默认配方 |
Source · K2 教材代码 · K3 课程代码 · K4 生产框架快照(2026-08-28)· K9 基准实现工作区
精度:纯 bf16,和一层多余的 autocast。 两本教材都是纯 bf16 加载权重:不用 autocast、没有 fp32 master weights、没有 GradScaler;教学场景图省事,0.5B 也确实犯不上。一个值得自我修正的点:权重已按 bf16 加载,forward 外又包一层 autocast——权重已是 bf16 时这层基本是冗余的,很容易顺手写多。真正的混合精度(fp32 master + loss scaling)在 Megatron / DeepSpeed 那一层。生产侧背书:Tulu 3 官方配方同样是 bf16 混合精度,与课程文档"BF16 比 FP32 快 2 倍"的社区经验一致。
两个值得记住的观察。其一,默认 scheduler 是个陷阱:不显式指定时,HF / TRL 默认 linear + warmup 0——你以为没做决策,其实已经做了。其二,lr 与 packing 强耦合:教材引用的 OLMo 3 实践是 sequence packing 显著增大有效 token batch,lr 要跟着上调——离开吞吐配置谈 lr 没有意义。epochs=2 有研究侧的支持:SFT 的 scaling 研究显示重复数据的收益约在前 4 个 epoch 内(arXiv 2402.17193),《SFT Memorizes, RL Generalizes》(arXiv 2501.17161)进一步指出多训 epoch 主要强化记忆而非泛化——2 个 epoch 在安全区,也预示了纯 SFT 切片的天花板(§7)。
§5 · 数据光谱 Data spectrum
光谱从 1,000 条到 100 万条都有名字;极简实现用的 10,000 条站在"先验证链路"的一端。
极简实现用 HuggingFaceTB/smoltalk 的 everyday-conversations 子集 1 万条,单源、零质量过滤——这是最大的简化点。数据集卡片(已核实)写明:全集 1.1M 条、14 个子集,核心新数据是 Smol-Magpie-Ultra 400K(Magpie 管线 + Llama-3.1-405B 生成);everyday-conversations 是多轮日常对话,只是"基础对话行为"切片,数学、代码、长上下文由其他子集承担。用它单独训练,产出的是完整配方里的对话层,不是完整能力。
一个实际约束要记牢:现在 max_len=1024,长推理链数据动辄 2,000–8,000 token;换数据时上下文长度必须一起动,否则大量样本会被截成"问题加半个思考过程",还顺手切掉了 <|im_end|>。
LIMA(arXiv 2305.11206)用 1,000 条人工精选让 65B 模型接近当时的 GPT-4,提出"表面对齐假说":预训练已储备能力,SFT 只做浅层对齐。Tulu 3(arXiv 2411.15124)用 94 万条做严格的多技能配比消融,并配去污染工具链——它证明的不是"越多越好",而是配比要靠消融、不靠先验。中间地带同样有据可查:AlpaGasus(2307.08701)从 52K 里判 90% 为低质、筛出 9K 反超全量;Deita(2312.15685)按复杂性、质量、多样性打分,6–10K 超越更大的混合集。rlhf-book 的 No Robots 9.5K 与极简实现同处一个量级。
生产侧数据工程的真实形态,中文社区与飞书群给出两端。知乎专栏把 curation 链路(去重 → 质量门 → 剔除与评测集重合 → 保证多轮完整)称为"整条链路里删得最多、也最不透明的一环";配比上混 10%–20% 通用数据防灾难性遗忘是社区共识度最高的经验——但也有 1:1 和 1:10~1:100 经验重放两种相差十倍的说法,配比本身没有定论。"SFT 效果 90% 取决于数据质量"在社区流传很广,但给不出实验出处,只能当情绪指标(存疑)。业界动态群的更新信号是:上海 AI Lab 的 SKT、AutoCompact 的 judge-guided SFT、MindForge 的轨迹微调——生产 SFT 数据越来越多是"强模型轨迹 + 验证器",极简实现直接加载的 smoltalk 本质上也是他人蒸馏好的轨迹。反向注脚来自《SFT Conflicts, RL Coexists》(arXiv 2608.03573):多阶段多任务 SFT 平均退化 23.1%,而同样的多阶段 RL 提升 24.9%——单任务单阶段的最小 SFT 避开的坑不在循环本身,在数据组合。
判断:单源 1 万条是合理的教学起点——先把链路验证正确,配比和质量门是 Stage 2 的事。
§6 · Stage 2 闸门 Priority gates
对照完六个信息源,刻意没做的与应该尽快补的,按优先级排成 7 道闸门;其中两道明确不跟进。
P0 只有一道:packing + padding-free + flash-attention 2——四个生产框架全有,是极简实现与生产的第一工程差距,Netflix 的 4.7 倍吞吐说明这不是细节。P1 三道:mask 边界对齐生产口径(assistant 头不计 loss、结束 token 必计);截断样本强制 EOS 收尾(OpenRLHF 在数据集出口把序列末位强制设为 EOS,一行可修,防切断 <|im_end|> 教坏停止);grad accumulation 尾部按实际组数归一,顺手去掉那层冗余 autocast。P2 两道:chunked loss、梯度检查点——大模型阶段再说;去污染属于数据准备层,不进训练器。不跟进两道:NEFTune(arXiv 2310.05914,框架支持、主流 recipe 未启用);cosine 换 linear(两派并存的配方之争,极简实现站多数派)。
这些都不进 Stage 1——极简的意义就是让你看清没有它们时系统在干什么。
§7 · 未决问题 Open questions
手写循环买到的不是性能,是一张决策清单——清单的末栏写着哪些结论仍可能翻转。
第一道已在途:masking 之争在 0.5B 小模型上的方向。IM(arXiv 2405.14394)的实验基于更大模型,0.5B 小模型是天然消融场——若 0.5B 上 OM 稳定不差,教学默认就无需改。其余三道留给 Stage 2:数据配比的最优解(社区数字 1:1 与 1:10~1:100 相差十倍,Tulu 3 靠消融不靠先验,Stage 2 混合数据时做小规模消融);packing 的 mask 串味风险(BFD packing 需 position_ids 重置,IBM arXiv 2407.09105 给出方案,实现时必须带 attention 隔离);纯 SFT 切片的性能天花板(《SFT Memorizes, RL Generalizes》:泛化提升主要在 RL 阶段——Stage 1 只验证链路,不追指标)。