预训练、后训练与强化学习 Pretraining, Post-training & RL
这是 2026 年最能拉开差距的阶段,也是招聘溢价最高的能力。行业的事实共识已经很清晰:现代 Decoder 块 + 大规模去重数据 + SFT 冷启动 → 可验证奖励的 RL + 多阶段后训练(含蒸馏)。真正的竞争不在「要不要用 RL」,而在用哪种 RL、基于什么奖励信号、配多大算力。
阶段总览
- 能设计并实现一条数据管线:清洗、去重、配比、质量筛选、合成数据与污染检查
- 理解 Scaling Law 与算力预算,能估算一次预训练的成本与可行性
- 掌握 SFT 的数据构造(格式、损失掩码、样本配比)与常见失效模式
- 能用 DPO / KTO / ORPO 做偏好对齐,并知道它们相对 RLHF 的取舍
- 能实现并跑通 GRPO 训练,理解 DAPO / GSPO / GMPO 在修什么问题
- 理解 Agentic RL 的三大难题(稀疏奖励、信用分配、rollout 成本)与主流解法
- 会做蒸馏与合成数据,能在有限算力下把能力迁移到小模型
- 掌握分布式训练与断点续训,能在大规模训练中处理 loss spike 与硬件故障
| 周次 | 主题 | 交付物 |
|---|---|---|
| 第 1 周 | 预训练数据管线 | 一份可复现的清洗 + 去重 + 配比脚本与数据卡片 |
| 第 2 周 | Scaling Law 与算力预算 | 一次训练的算力 / 成本估算与可行性判断 |
| 第 3 周 | SFT 全流程 | 用 Llama-Factory 或 TRL 完成一次指令微调 |
| 第 4 周 | 偏好对齐 DPO / KTO / ORPO | 构造偏好数据并完成 DPO 训练与对比 |
| 第 5–6 周 | GRPO 与可验证奖励 | 数学 / 代码任务上跑通 GRPO 并分析熵与长度变化 |
| 第 7 周 | Agentic RL 与蒸馏 | 多轮工具任务的轨迹数据 + 蒸馏小模型 |
| 第 8 周 | 基建与复盘 | 分布式训练排错手册 + 完整实验报告 |
1. 预训练:数据与算力的双重工程
学习路径
- 读 1.1:背下数据管线七步(采集→清洗→精确去重→近似去重→筛选→配比→合成),记 MinHash+LSH 与 ANN
- 对一小份语料跑精确+近似去重,观察去重后剩余占比并记录统计
- 对接 M9:把该管线思想落到 SFT 数据构造(含去重/难度分层/格式统一)
核心知识点详解
- 七步管线顺序不能乱:采集→清洗→精确去重→近似去重→筛选→配比→合成。先去除重再做质量筛选,优先去掉机器噪声再谈难度配比,顺序错了会重复浪费算力。
- 精确去重 + MinHash+LSH 近似去重:精确去重按内容哈希;近似去重用 MinHash 生成签名 + LSH 分桶找 Jaccard 相似度高的近重复。语义去重再加 ANN(向量近似检索)。
- 污染检查用 n-gram 命中:把评测集切成 n-gram,看是否大量出现在训练语料里(如 k=13 连续命中率高即疑似污染)。污染会让评测虚高。
- 数据卡片与可复现性:每个步骤记录输入/输出 token 数与哈希,产出 Data Card。常见坑:去重后忘记录保留率,无法回答「数据从多少缩到多少」,也无法复现。
学习路径
- 读 1.1:理解温度采样 mix_sample 与领域权重退火,记去重后仅剩 30–50%、10–30T 量级
- 跑插值 mix_sample 或配比退火示例,观察各领域 token 占比随步数变化
- 完成本章自测:给定任务写出一份领域配比并说明为何加高质量尾段
核心知识点详解
- 去重后的量级:原始语料去重 + 筛掉低质后仅剩 30–50%;训练常见 10–30T token。配比先按领域权重混合,再用温度采样
mix_sample平滑。 - 领域权重退火:训练前期代码/数学重一些,后期逐渐把权重退给高质与目标域数据;末尾加「高质量尾段」让模型记住输出风格。
weight(t)=w0+(w1−w0)·t/T。 - 质量优先的 token 预算:预算有限时,宁可少 token 高质,也别灌水低质把模型毁掉。token 预算 = ∑(领域权重×领域 token 量)。
- 如何自测配额比例:给定业务领域(如代码/中文/数学),写出一份配比并说明为何把「高质量尾段」放在最后。常见坑:领域权重直接硬 mix 不做退火,尾段权重被前面打断。
学习路径
- 读 1.2:理解 mid-training 是"尾段":学习率退火、高质量占比 15%→30%、长上下文 128K、能力预热、占算力 5–15%
- 对接 M9:把 mid-training 的高质量数据提升思路用在 SFT 数据难度分层上
- 完成本章自测:说明 mid-training 与主预训练的边界与作用
核心知识点详解
- mid-training 是「尾段」:在主预训练末尾追加一段高质量、长上下文训练:学习率退火、高质量占比 15%→30%、把上下文扩到 128K。它占整段算力 5–15%,却显著决定最终能用多长、多高质量。
- 能力预热:在尾段预先「预热」读长文档、跨距离利用等能力,避免上线后才发现长上下文没训到。
- 与主预训练的边界:主预训练保面、mid-training 提点:前者数据广、lr 高、短上下文;后者数据精、lr 退火、长上下文。二者是同一套权重的两个阶段。
- 能否用于 M9:M9 的 SFT 数据里可沿用「难度分层 + 高质量尾段」思路:把高质量指令放训练末期。常见坑:把 mid-training 做成二次预训练,lr 从头再 warmup 而不是退火,浪费算力。
1.1 数据管线:决定上限的一步
一句话总结 2026 年的数据经验:数据质量与配比的收益,往往大于模型规模的收益。一条合格的数据管线按下面的顺序执行,每一步都要留可复现的统计与版本。
python# 近似去重(MinHash + LSH):预训练数据管线里性价比最高的一个环节
from datasketch import MinHash, MinHashLSH
import re
def shingles(text, k=5):
tokens = re.findall(r"\w+", text.lower())
return {" ".join(tokens[i:i + k]) for i in range(max(1, len(tokens) - k + 1))}
def minhash(text, n=128):
m = MinHash(num_perm=n)
for s in shingles(text):
m.update(s.encode("utf8"))
return m
lsh = MinHashLSH(threshold=0.8, num_perm=128) # 相似度 > 0.8 视为重复
kept, dropped = [], 0
for i, doc in enumerate(documents):
if i % 100000 == 0:
print(f"progress {i:,} kept={len(kept):,} dropped={dropped:,}") # 长任务必须打点
m = minhash(doc)
if lsh.query(m): # 命中已有近重复
dropped += 1
continue
lsh.insert(str(i), m)
kept.append(doc)
语义去重(semantic dedup) 是精确 / MinHash 之上的第三层:对残留文档做 embedding,用近似最近邻(FAISS / ScaNN)或聚类去除「语义相同但字面不同」的近重复。它比 MinHash 更准(能抓改写、转述),但成本高出 1–2 个数量级,且阈值需谨慎——过严会把合理重复(同义表述、常见开头)也删掉。
污染检测(contamination) 决定所有指标是否可信:用 n-gram 重叠率或困惑度异常,检查训练集与评测集是否交叠。数学 / 代码类 benchmark 最容易被「不小心」混进预训练语料,导致分数虚高、上线即崩;2026 年主流开源模型都会随权重发布 contamination report。
课程学习与数据退火:训练早期用宽分布打基础,训练中后期逐步抬高高质量 / 难样本(数学、代码、长文、推理链)的占比,并把学习率线性衰减(annealing)。这与 1.2 的 mid-training 是同一思想的两种落地——先广后精。
数据量级与 Scaling Law 的关系:Chinchilla 的最优比例约 20 token / 参数(即 7B 配约 140B token)。但 2026 年(DeepSeek、OLMo、MiniCPM 等)普遍「过训练」:用 100+ token / 参数 训更小的模型,因为推理才是长期成本,小模型服务更便宜;同时数据质量(去重、配比、合成)带来的 loss 下降常大于单纯加量——数据工程已是第一性生产力。
| 去重层次 | 方法 | 成本 | 精度 | 适用 |
|---|---|---|---|---|
| 精确去重 | 哈希 / 全文比对 | 最低 | 只抓完全复制 | 同源镜像 |
| 近似去重 | MinHash + LSH | 中 | 抓改写 / 拼接 | 网页、代码 |
| 语义去重 | embedding + ANN | 高 | 抓转述 / 同义 | 高质量语料精修 |
python# 领域配比 + 温度采样:控制「各领域被抽到的相对比例」
import numpy as np
def mix_sample(domains, probs, temps, n, seed=0):
p = np.array(probs, dtype=float)
t = np.array(temps, dtype=float)
q = p ** (1.0 / t); q = q / q.sum() # 温度>1 拉平,<1 放大高权重领域
picks = np.random.default_rng(seed).choice(len(domains), size=n, p=q)
for i, d in enumerate(domains):
print(f"{d:6s} 目标={p[i]:.2f} 温度={t[i]:.2f} 实际占比={(picks==i).mean():.3f}")
mix_sample(["web","code","math","books"], [0.5,0.2,0.15,0.15], [1.0,0.7,0.6,0.8], 200000)
# 预期:温度<1 的领域(code/math)实际占比高于目标权重
# 机制:这就是「中期抬高高质量占比」的实现——先广后精,配比退火与 lr 退火同步
2026 的规模量级参考:头部开源模型预训练语料在 10–30T token 量级(多语言 + 代码 + 数学 + 网页),去重后的有效数据往往只剩原始抓取的 30%–50%。污染检查是红线:数学/代码 benchmark 一旦混入预训练语料,所有分数虚高、上线即崩。
1.2 mid-training:2026 年被重视的“尾段”
除了预训练与后训练,2026 年各家普遍多了一段 mid-training / 退火阶段:在预训练后期调整数据配比(提高高质量、数学、代码、长文档的占比),用更低学习率继续训练。这一阶段对最终能力影响显著,且成本远低于从头训练。
- 退火(annealing):学习率线性衰减到很小,同时切换数据分布,常能带来明显的能力跃升。
- 长上下文扩展:多在 mid-training 阶段把窗口从 4K/8K 扩到 32K/128K+。
- 能力预热:提前混入少量指令风格或推理轨迹数据,为后训练做准备。
python# mid-training 的「两段式」:前期宽分布,尾段切高质量 + lr 退火
def midtrain_schedule(step, total, switch_frac=0.8):
anneal = step >= total * switch_frac
phase = "anneal" if anneal else "broad"
if anneal:
frac = (step - total * switch_frac) / (total * (1 - switch_frac))
lr = 3e-4 * max(0.0, 1 - frac)
else:
lr = 3e-4
hi = 0.15 if not anneal else 0.30 # 代码/数学高质占比
return phase, lr, hi
for s in (0, int(8e4) - 1, int(8e4), 100000):
ph, lr, hi = midtrain_schedule(s, 100000)
print(f"step {s:6d} -> {ph:6s} lr≈{lr:.2e} 高质量占比={hi:.0%}")
# 预期:80% 步数后切到 anneal:lr 线性趋零、代码/数学占比从 15% 抬到 30%
# 经验:mid-training 常只占总算力的 5%-15%,但对最终能力影响显著
- 排顺序:给出清洗、精确去重、近似去重、质量筛选、配比的正确顺序。判据:清洗→精确去重→近似去重→质量筛选→配比→打包。
- 算去重率:原始 10T token,去重后 4T,去重率?参考答案:60%。
- 污染检查:怎么判断训练集混入了 GSM8K?判据:n-gram 重叠率或 perplexity 异常检测。
- 配比退火:为什么中后期提高代码/数学占比?判据:高质量数据在尾段对能力影响最大。
- 温度采样:温度 > 1 会让分布更平还是更尖?判据:更平(拉近各领域占比)。
2. SFT:冷启动与指令跟随
学习路径
- 读 2.1:理解 loss mask 只算 assistant 段、按能力配比,记通用指令 5k–50k 的量级
- 实现 build_example 构造数据并核对 input_ids 与 labels 的掩码正确性
- 对接 M9:为领域 SFT 构造指令集并做去重/难度分层/训练评测隔离
核心知识点详解
- loss mask 只算 assistant 段:user/系统提示的 target 全置
-100,CrossEntropyLoss(ignore_index=-100)会跳过,模型只学如何作答而不是去生成问题。常见坑:忘设 ignore,模型把用户问题也学了,表现为「复读指令」。 - 非 assistant 段置 -100 的判定:
labels[i] = tokenizer.pad_id if not assistant else input_ids[i],干扰格式 token 也一并屏蔽。 - 按能力维度配比与规模:通用指令常见 5k–50k 条即可;按能力维度(知识/推理/遵从)分层配比,质量>数量。
- 避免长样本主导梯度:loss 通常是均值贡献,超长样本的 token 数多会吸走大部分梯度;要按 token 数加权或截断平衡。常见坑:多轮对话里只标中间某轮 assistant,前后轮漏标导致掩码错位。
学习路径
- 读 2.2:用 Llama-Factory / TRL SFTTrainer 配 LoRA(rank 16)、全量 lr 1e-5~2e-5、梯度累积等效 batch
- 在领域数据上跑一次完整 SFT,记录训练曲线与验证指标
- 对接 M9:用 M7 训练管线复用做 SFT,产出具名/对比可复现的结果
核心知识点详解
- LoRA rank 16 起步:
lora_r=16、lora_alpha=2r(DeltaW 缩放 α/r),只训低秩增量,把可训参压到 1% 以下。peft或 Llama-Factory 一行切换 target_modules。 - 全量 lr 1e-5~2e-5、LoRA lr 1e-4~3e-4:全量微调学习率一个小量级到 1e-5~2e-5;LoRA 用 1e-4~3e-4。lr 过高会灾难性遗忘、过低学不动。
- 梯度累积等效更大的 batch:
grad_accum=N每 N 步才更新一次(optimizer.step()+zero_grad()),等效 batch=N×batch_size,只占同样显存。 - SFT 是冷启动不是终点:SFT 只教「输出形式」不教「推理能力」,真正的能力提升要靠 DPO/GRPO。常见坑:SFT 数据里硬塞错误/标答混合,指标看似好但实际学的是风格不是能力。
学习路径
- 读 2.1/2.2:理解 SFT 教形式不教推理、质量 > 数量、多轮掩码正确、避免长样本主导梯度
- 完成本章自测:说明 SFT 与预训练的定位差及两者的失效模式预防
- 对接 M9:确认领域问答基线相对基座的提升并说明 SFT 的边界
核心知识点详解
- SFT 教形式、不教推理:它把已有能力引导到正确输出格式(指令跟随、格式统一),本身不新增知识或推理。所以别指望 SFT 提升数学能力,那是 RL 的活。
- 质量 > 数量:几千到几万高质量样本往往优于几十万低质样本;统一格式、去重、难度分层比堆量收益大。
- 三条工程判据:① 非 assistant 段全置 -100;② 多轮掩码正确、不把历史问题算进 loss;③ 无超长样本主导梯度。三者都过才算数据合格。
- 失效模式先行防:「遗忘 / 僵化 / 复读」三种退化都要在配比与退火上提前防范。常见坑:只盯准确率不看 PPL 与复读率,SFT 过拟合了都没发现。
2.1 数据构造与损失掩码
SFT 的目标不是「教会模型新知识」,而是把已有的能力引导到正确的输出形式上。数据质量与格式一致性比数量重要得多——2026 年的实践中,几千到几万条高质量样本往往优于几十万条低质量样本。
python# 关键细节:只在 assistant 片段上算损失(loss mask)
# 若对 user 部分也算损失,模型会学着去“生成用户的问题”
def build_example(tokenizer, messages, max_len=4096):
input_ids, labels = [], []
for m in messages:
ids = tokenizer.apply_chat_template([m], add_generation_prompt=False,
tokenize=True)[2:-2] # 去掉首尾特殊符
input_ids += ids
labels += ids if m["role"] == "assistant" else [-100] * len(ids) # 非 assistant 掩掉
input_ids, labels = input_ids[:max_len], labels[:max_len]
return {"input_ids": input_ids, "labels": labels, "attention_mask": [1] * len(input_ids)}
# 常见数据配方(按能力维度配比,而不是等比例混)
RECIPE = {
"通用指令": 0.35, "数学推理": 0.20, "代码": 0.20,
"长文档问答": 0.10, "工具调用": 0.10, "安全拒答": 0.05,
}
| 失效模式 | 表现 | 原因与对策 |
|---|---|---|
| 灾难性遗忘 | 通用能力下降、语言混杂 | 数据太窄;混入通用数据、降低学习率、用 LoRA |
| 格式僵化 | 回答千篇一律、过度客气 | 模板太单一;增加风格多样性 |
| 只学形式不学推理 | 答案结构漂亮但内容是错的 | 需要 RL 阶段用可验证奖励纠偏 |
| 重复与复读 | 输出循环重复 | 数据里有重复样本;去重、加重复惩罚 |
| 过度拒答 | 把正常问题也拒掉 | 安全数据权重过高;重新配比 |
python# 验证 loss mask:只有 assistant 段有非 -100 标签
def check_mask(labels):
n_sup = sum(1 for x in labels if x != -100)
print(f"监督 token 占比 = {n_sup/len(labels):.2%}")
assert labels[-1] != -100, "最后一个 token 必须被监督(否则学不到结束)"
# 预期:监督占比通常 20%-60%(取决于回答长度)
# 若接近 100% -> 忘了掩掉 user 段;若接近 0 -> 掩反了
样本量经验:通用指令 5k–50k 条、垂直领域 1k–20k 条往往足够;2026 的共识是「质量 > 数量」,几十万条低质量样本常不如几千条精筛样本。多轮对话要保证每轮都正确掩码,并限制单样本长度,避免长样本主导梯度。
2.2 实操:一次完整的 SFT
bash# 方案 A:Llama-Factory(配置化,最快出结果)
llamafactory-cli train configs/sft_lora.yaml
# model_name_or_path: Qwen/Qwen3-8B
# stage: sft
# finetuning_type: lora
# lora_rank: 16
# dataset: my_instruct_v3 # 已注册的数据集名
# cutoff_len: 4096
# per_device_train_batch_size: 2
# gradient_accumulation_steps: 8 # 等效 batch = 16
# learning_rate: 1.0e-4 # LoRA 可用较大学习率
# num_train_epochs: 3
# lr_scheduler_type: cosine
# bf16: true
# 方案 B:TRL(代码可控,便于自定义)
python -m trl.scripts.sft --model Qwen/Qwen3-8B --dataset my_instruct_v3
# 方案 C:从零开始(理解流程用,不建议直接上生产)
# datasets + transformers.Trainer + 自定义 collator(含 loss mask)
python# 用 TRL 做 SFT 的最小可运行脚本(含 loss mask)
from datasets import load_dataset
from transformers import AutoModelForCausalLM
from trl import SFTTrainer, SFTConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", dtype="bfloat16")
ds = load_dataset("json", data_files="my_instruct.jsonl", split="train")
cfg = SFTConfig(output_dir="out", max_length=4096, num_train_epochs=3,
per_device_train_batch_size=2, gradient_accumulation_steps=8,
learning_rate=2e-5, lr_scheduler_type="cosine", bf16=True,
gradient_checkpointing=True)
SFTTrainer(model=model, args=cfg, train_dataset=ds).train()
# 预期:等效 batch=16;3 epoch 后 loss 明显下降,用 eval 集监控过拟合
# 经验:全量 SFT lr 约 1e-5~2e-5;LoRA 可放大到 1e-4~3e-4
- 掩码判断:对 user 段也算 loss 会怎样?判据:模型学会生成用户问题、指令跟随退化。
- 样本量:1 万条精筛 vs 30 万条低质,选哪个?判据:优先精筛(质量 > 数量)。
- 超参:全量 SFT 与 LoRA 的学习率量级差别?判据:LoRA 大 1–2 个数量级(1e-4~3e-4 vs 1e-5~2e-5)。
- 失效诊断:模型开始复读怎么处理?判据:数据去重 + 重复惩罚 + 降 lr。
- 定位:SFT 能教出新的数学推理能力吗?判据:不能,SFT 只学数据里出现过的模式,需 RL 突破。
3. 偏好对齐:从 RLHF 到 DPO
学习路径
- 读 3.1:理解 RLHF 三阶段、Bradley-Terry 损失、PPO 四模型显存与 KL 系数 β 0.01–0.1
- 实现 rm_loss 并核验"被偏好回答得分更高"的收敛方向
- 对接 M9:对比 RLHF 与 DPO/GRPO 的成本与稳定性,说明本项目为何选后者
核心知识点详解
- 三阶段流程与 Bradley-Terry:SFT → 训 RM → RL(PPO)。RM 用 Bradley-Terry:
p(y_w≻y_l)=σ(r_w−r_l),最小化-log σ(r_w−r_l),让被偏好回答得分更高。F.binary_cross_entropy_with_logits一行实现。 - PPO 需要四份模型:policy / reference / RM / Critic 同时驻留显存,显存开销大;每个 policy 回答都要 ref 与 RM 打分,成本高。
- KL 系数 β 0.01–0.1:总奖励
r_total = r_RM − β·KL(π_θ∥π_ref)。β 过大学不动,过小语言退化。reward 要先 scaling/裁剪,否则 KL 易爆炸。 - 三硬伤:① 奖励黑客(模型学会谄媚/变长而非真变好);② 成本高(4 模型 + 推理推理);③ 偏好标注贵且不一致。常见坑:RM 训练时的偏好样本与 SFT 分布偏移,导致 RM 泛化出错。
学习路径
- 读 3.2:区分 DPO / KTO / ORPO / SimPO 的标签需求与适用,记成本为 PPO 的 1/2–1/4
- 实现 dpo_loss 四个标量输入跑一遍,理解隐式奖励 = β(logπ−logπ_ref)
- 对接 M9:用 DPO(或直接实现在 TrainLoop)做偏好对齐,记录 β 的敏感性
核心知识点详解
- DPO 离线隐式奖励:不需要单独 RM,把隐式奖励直接代入目标:
loss = -log σ(β·log(π(y_w)/π_ref(y_w)) − β·log(π(y_l)/π_ref(y_l)))。离线用已有偏好对即可训。 - β 控制偏离参考:β 越大越保守(贴参考模型),越小越激进。隐式奖励
r = β(logπ − logπ_ref)由当前策略与参考策略的对数比给出。 - 家族改法一句话:KTO 只要二元好坏标签(无需两两对比);ORPO 把 SFT 与偏好合并一起训;SimPO 去掉参考模型降低显存。成本普遍为 PPO 的 1/2–1/4。
- 谁用谁:有偏好对比对→DPO;只有好/坏二元标注→KTO;想省显存→SimPO。常见坑:DPO 要求偏好对来自同一 policy 分布,跨模型 mix 出的对会让梯度错乱。
学习路径
- 读 3.3:推导 π* ∝ π_ref·exp(r/β),把隐式奖励 r 代入 Bradley-Terry 消去 RM
- 用 logp 数值复现 implicit_reward 与 DPO 目标,验证 β 越大越保守
- 对接 M9:从数学上说明何时用 DPO 而非 GRPO(有无可验证奖励)
核心知识点详解
- π* ∝ π_ref·exp(r/β):从 KL 正则的 RL 目标出发,最优策略闭式解为
π*(y|x) ∝ π_ref(y|x)·exp(r(x,y)/β)——偏好对齐就是在「贴近参考」与「最大化奖励」间权衡。 - 把隐式奖励 r 代入 Bradley-Terry 消去 RM:由闭式解反解
r(x,y) = β(logπ − logπ_ref),再代入p(y_w≻y_l)=σ(r_w−r_l),RM 完全消失,剩下只含当前策略与参考策略的 DPO 目标,实现「隐式对齐」。 - β 越大越保守的数值验证:
r=β(logπ−logπ_ref),β 越大对参数更新更「迟钝」(每次梯度更小),宏观上贴参考。β=0.1 比 β=1 学得更快但更漂。 - 何时用 DPO 而非 GRPO:有可验证奖励(对错可判、格式可查)时走 GRPO/RLVR;只有不可验证的偏好信号时走 DPO。常见坑:把不可判定的偏好硬套成可验证奖励去做 RLVR,得到的是噪声梯度。
3.1 RLHF 与它的昂贵
RLHF 的三段式流程:① 收集人类对多个回答的排序;② 训练一个奖励模型(RM)拟合这些偏好;③ 用 RL(传统上是 PPO)优化策略,使 RM 打分更高,同时用 KL 惩罚约束不要偏离 SFT 模型太远。
三阶段的数据需求:① SFT 数据(指令-回答对,数千到数十万条,质量优先);② 偏好数据(同一 prompt 的多个候选回答 + 人类排序,通常每任务数百到数千条对比对);③ 奖励模型用 Bradley-Terry 模型拟合:p(y_w ≻ y_l | x) = σ(r(x, y_w) − r(x, y_l)),训练目标是最大化被偏好回答的得分差。
PPO 训练机制:每一步由 policy 生成回答 → reward model 打分 → 用 GAE 估计优势 → 策略梯度更新。总奖励 r_total = r_RM − β·KL(π_θ ∥ π_ref),其中 KL 惩罚把策略锁在 SFT 模型附近(β 过大则学不动、过小则语言退化)。奖励要先做 reward scaling 与裁剪,否则 KL 易爆炸。显存瓶颈在于要同时驻留 policy / reference / RM / Critic 四份模型——这也是 PPO 被 GRPO 等无 Critic 方法替代的主因。训练精度上,2026 大集群普遍用 FP8(per-tensor / per-block scaling 的 Transformer Engine) 跑预训练与大规模 RL 的 rollout 以压低算力成本,并配合 QAT 量化感知训练保持数值稳定。
| 阶段 | 数据形态 | 量级 | 主要成本 |
|---|---|---|---|
| SFT | 指令-回答对 | 10³–10⁵ 条 | 低,可纯合成 |
| 偏好标注 | 多候选 + 排序 | 10²–10³ 对比对/任务 | 人工慢、贵、不一致 |
| RM 训练 | 偏好对 | 10⁴–10⁵ | 中 |
| PPO 在线 | 实时生成 + RM 打分 | 持续采样 | 最高(4 模型 + rollout) |
python# Bradley-Terry 奖励模型损失:让「被偏好」的回答得分更高
import torch, torch.nn.functional as F
def rm_loss(reward_chosen, reward_rejected):
# p(y_w 优于 y_l) = sigmoid(r_w - r_l)
return -F.logsigmoid(reward_chosen - reward_rejected).mean()
print(rm_loss(torch.tensor([2.0, 1.5]), torch.tensor([0.5, 0.2])))
# 预期:r_w 明显大于 r_l 时 loss 趋近 0;反之 loss 增大
# 隐患:RM 会奖励「更长、更自信、更谄媚」——这是 reward hacking 的温床
PPO 的显存与稳定性账本:要同时驻留 policy / reference / RM / Critic 四份模型,7B 模型即需约 4 份权重的显存;KL 系数 β 常取 0.01–0.1,过大则学不动、过小则语言退化。这也是 2026 用无 Critic 的 GRPO 替换 PPO 的直接动因。
3.2 DPO 家族:把 RL 变成分类问题
DPO 的核心洞察很漂亮:RLHF 的最优策略有闭式解,可以把它代入偏好目标,从而消掉奖励模型与 RL 环节,直接得到对「好答案 vs 坏答案」的监督损失。于是训练像微调一样简单,成本只有 PPO 的 1/2–1/4。
python# DPO 损失的核心结构(理解用,实际用 TRL 的 DPOTrainer)
import torch, torch.nn.functional as F
def dpo_loss(policy_chosen_logp, policy_rejected_logp,
ref_chosen_logp, ref_rejected_logp, beta=0.1):
"""
beta 控制允许偏离参考模型的强度:越大越保守。
关键:用 policy 与 ref 的对数概率差作为隐式奖励,
因此不需要单独的奖励模型。
"""
chosen_reward = beta * (policy_chosen_logp - ref_chosen_logp)
rejected_reward = beta * (policy_rejected_logp - ref_rejected_logp)
loss = -F.logsigmoid(chosen_reward - rejected_reward).mean()
return loss, chosen_reward.mean().item(), rejected_reward.mean().item()
# 只用 4 个标量就能讲清 DPO 的全部数学 —— 这就是它的美
print(dpo_loss(torch.tensor([-8.0]), torch.tensor([-12.0]),
torch.tensor([-9.0]), torch.tensor([-9.5])))
| 方法 | 需要奖励模型 | 需要在线采样 | 适用场景 | 特点 |
|---|---|---|---|---|
| RLHF + PPO | 是 | 是 | 追求上限、可在线探索 | 最贵;多模态与生产级对齐仍常用 |
| DPO | 否 | 否(离线) | 中小模型对齐的事实标准 | 轻量稳定;不探索,正确答案不在数据里就学不会 |
| KTO | 否 | 否 | 只有「好/坏」二元标签(无成对数据) | 数据要求更低,工业场景很实用 |
| ORPO | 否 | 否 | 想把 SFT 与对齐合并成一步 | 省一次训练,效果略逊于分阶段 |
| RLAIF / 宪法式 | 用 AI 反馈代替人类 | 视实现 | 降低人工标注依赖 | 需注意 AI 判官的偏见传播 |
python# DPO 的隐式奖励与 beta 的作用(数值演示)
def implicit_reward(logp_policy, logp_ref, beta=0.1):
return beta * (logp_policy - logp_ref) # r = beta * log(pi/pi_ref)
ch = implicit_reward(-7.0, -8.0, 0.1) # +0.10
rj = implicit_reward(-11.0, -9.0, 0.1) # -0.20
print(f"chosen={ch:.2f} rejected={rj:.2f} diff={ch-rj:.2f}")
# 预期:差为正且越大,loss 越小;beta 越大越保守(贴合参考模型)
# 经验:beta 常取 0.1(0.05 更激进、0.5 更保守)
SimPO 进一步去掉参考模型:直接用策略自身的平均 log 概率做长度归一化奖励 r = (β/|y|)·log π(y|x),省掉 reference 前向的显存与计算;CPO 则把 SFT 项与偏好项合并。这些变体让「离线对齐」在单卡上更可行,是 2026 小团队最常用路线。
3.3 DPO 的奖励隐式推导(为什么不需要 RM)
DPO 的美在于它把「奖励」彻底约掉。RLHF 的最优策略满足 π*(y|x) ∝ π_ref(y|x)·exp(r(x,y)/β)。两边取对数得 log π* = log π_ref + r/β + const,整理出隐式奖励 r(x,y) = β·[log π_θ(y|x) − log π_ref(y|x)] + C——即奖励被表达为「策略与参考模型的对数概率之差」。
text# DPO 损失的来源(推导骨架)
# 1) 最优策略: pi*(y|x) ∝ pi_ref(y|x) * exp(r(x,y)/beta)
# 2) 取对数: log pi* = log pi_ref + r/beta + C
# 3) 解出奖励: r(x,y) = beta * (log pi_theta(y|x) - log pi_ref(y|x)) + C
# 4) 代入 Bradley-Terry 偏好目标并消元 r:
# L_DPO = -E[ log sigmoid( beta*(logpi_theta(y_w|x)-logpi_theta(y_l|x))
# - beta*(logpi_ref (y_w|x)-logpi_ref (y_l|x)) ) ]
# 没有 RM、没有在线采样——奖励被「隐式」表达为策略与参考的 log 比
推导的实操含义:β 控制「允许偏离参考模型多远」——β 越大越保守(更像 SFT 模型、对齐收益小),越小越激进(可能训飞)。DPO 不在线探索,所以正确答案若不在偏好数据里就学不到;这是它相对 PPO/GRPO 的天花板。KTO / ORPO / SimPO 是它的近亲:KTO 用「好/坏」二元标签而非成对数据;ORPO 把对齐塞进 SFT 一步(省一次训练);SimPO 去掉参考模型、直接用策略自身的平均 log 概率做长度归一化奖励,更省显存。
python# SimPO:去掉参考模型,用长度归一化的平均 log 概率当奖励
import torch, torch.nn.functional as F
def simpo_loss(logp_chosen, len_chosen, logp_rejected, len_rejected, beta=2.0, gamma=0.5):
r_w = beta / len_chosen * logp_chosen # 平均每 token log 概率
r_l = beta / len_rejected * logp_rejected
return -F.logsigmoid(r_w - r_l - gamma).mean() # gamma 为目标奖励间隔
print(simpo_loss(torch.tensor([-20.0]), 40, torch.tensor([-60.0]), 60))
# 预期:gamma(margin)越大越要求 chosen 明显更优,训练更激进
# 好处:无需 reference 模型 -> 显存约省 1/3,适合单卡离线对齐
| 变体 | 关键改动 | 省掉了什么 |
|---|---|---|
| DPO | 策略/参考 log 比当隐式奖励 | 奖励模型 + 在线采样 |
| SimPO | 长度归一化 + 目标间隔 γ | 参考模型前向 |
| CPO | SFT 项与偏好项合并 | 一次额外训练 |
| KTO | 好/坏二元标签(非成对) | 成对偏好数据要求 |
| ORPO | 把对齐塞进 SFT 一步 | 单独的偏好训练阶段 |
- 推 DPO:从最优策略 π* ∝ π_ref·exp(r/β) 推出隐式奖励。判据:r = β(log π − log π_ref)+C。
- 选方法:只有「好/坏」二元标注、没有成对数据,用哪个?判据:KTO。
- 省显存:想在单卡做离线对齐且不想加载参考模型?判据:SimPO(长度归一化,无 reference)。
- β 取舍:DPO 的 β 太大/太小分别什么后果?判据:太大过度保守像参考;太小奖励被忽略、易训飞。
- 何时不用 DPO:奖励可程序验证时该用什么?判据:GRPO/RLVR,能探索数据里没有的解。
4. 可验证奖励 RL:2026 的主战场
学习路径
- 读 4.1:理解 RLVR 免奖励模型、组相对优势、GRPO 去 Critic,参数 group_size 8–16、KL β 0–0.04
- 实现 grpo_step 骨架并跑通一次,看到全对/全错组被跳过与熵崩塌判定
- 对接 M9:实现 grpo.py 做可验证奖励对齐,记录 group/β 敏感性
核心知识点详解
- 可验证奖励免 RM、无奖励黑客:奖励由程序判定(数学对错/代码过测/格式合规),不存在「讨好评委会」的 hacking 空间,RL 因此可规模化、可复现。
- 组相对优势 Â_i 替代 Critic:对同一 prompt 采样 group_size 个回答,用组内奖励相对偏离替代 PPO 的 Critic 价值估计:
Â_i = (r_i − mean(r组)) / std(r组),去掉 Critic 模型。 - GRPO 目标与参数:
loss = -E[1/g 组均值 Â_i·min(ρ_i, clip(ρ_i,1−ε,1+ε))] + β·KL(π∥π_ref),ρ_i 是重要性比。group_size 常用 8–16,KL β 取 0–0.04。 - 熵崩塌判据:策略熵快速下降、组内回答趋同即熵崩塌。常见坑:全对/全错的组贡献零信号却仍更新,白耗算力——DAPO 的动态采样会丢弃这类组。
学习路径
- 读 4.1:记住 DAPO / GSPO / GMPO / CISPO 各修什么问题
- 对接 M9:在 grpo.py 里对确有更好的改法说明取舍与测量
- 完成本章自测:把家族改法对照到 M9 遇到的现实问题(熵崩塌、无效样本、长度膨胀)
核心知识点详解
- DAPO:Clip-Higher + 动态采样 + Token 级损失:上界剪钳 ε_high=0.28(而非对称 ε)、丢弃全对/全错组、token 级归一、超长惩罚,针对熵崩塌与无效样本。
clip(ρ,1-ε_low,1+ε_high)。 - GSPO / GMPO / CISPO 一句话:GSPO 把重要性比从 token 级升到序列级,抑长序列噪声;GMPO 用几何平均更稳;CISPO 只对权重做裁剪,简化实现。
- 对照 M9 现实问题选法:若遇到熵崩塌→选 DAPO Clip-Higher;长度膨胀→超长惩罚;噪声大→GSPO 序列级。先量问题再上改法。
- 测量先行:每个改法都要留开关并记录 reward/长度/熵三曲线,别一把全上无法归因。常见坑:好几个改法同时开,出现「混合效应」分不清谁的功劳。
学习路径
- 读 4.2:理解稀疏奖励、信用分配、rollout 成本 10–100×、GiGPO/Tree-GRPO/回归 Critic
- 完成本章自测:说明稀疏奖励下 GRPO 的局限与多步信用分配难点
- 对接 M9:说明 Agentic RL 与本阶段可验证奖励的区别,明确项目阶段边界
核心知识点详解
- 稀疏奖励是最大难点:Agent 任务常只有最终成功/失败一个信号,中间千百步动作没有奖励。对比单轮 RLVR 的「立即对错」,Agentic RL 的反馈是稀疏的。
- 信用分配:整条轨迹只有末端奖励时,难以知道是「哪一步决策导致了失败」。需 GiGPO 的组中组两层优势(粗粒度步骤级 + 细粒度 token 级)做多级归因。
- rollout 成本 10–100×:每步动作要调外部工具/环境,一条轨迹的 rollout 代价比单轮文本生成高 10–100×,因此样本效率与复用很关键。
- 与 GRPO 的边界:本阶段的 GRPO 用可验证单轮奖励;Agentic RL 是多步稀疏奖励的进阶话题,明确「不做」以界定项目范围。常见坑:把 GRPO 直接套多步 Agent 环境,粒子稀疏、差分不明显,策略几乎不更新。
4.1 RLVR 与 GRPO 家族
RLVR(Reinforcement Learning with Verifiable Rewards)是 2025–2026 年最重要的方法论转变:当奖励可以被程序验证时(数学答案是否正确、代码是否通过单测、格式是否合规、工具调用是否成功),就不需要奖励模型,也就没有奖励黑客。这使 RL 训练变得可规模化、可复现。
| 算法 | 一句话定位 | 修的问题 |
|---|---|---|
| GRPO | 组内相对比较,去掉 Critic | PPO 显存开销大;天然适配可验证奖励 |
| DAPO | Clip-Higher + 动态采样 + Token 级损失 + 超长惩罚 | 熵崩塌;全对/全错的无效样本浪费算力;长度偏差 |
| GSPO | 重要性比值从 token 级提升到序列级 | 长序列噪声累积;MoE 需额外补丁 |
| GMPO | 用几何平均替代算术平均 | 异常 token 主导梯度(有界性可证) |
| GFPO | 多采样后按最短/最高效过滤 | RL 后模型越写越长但没更准 |
| CISPO | 只裁剪权重不裁剪 token | 反思类低概率词(wait / recheck)被裁掉 |
| VAPO | 把价值模型请回来做精细信用分配 | 长 CoT 场景下无 Critic 的局限 |
python# GRPO 的训练循环骨架:理解 "组内采样 → 相对优势 → 加权策略梯度"
import torch
def grpo_step(policy, ref_policy, tokenizer, prompts, reward_fn,
group_size=8, beta=0.04, clip=0.2):
total_loss = 0.0
for prompt in prompts:
# 1) 同一个 prompt 采样一组答案 —— 组内相对比较的基础
with torch.no_grad():
completions = [policy.generate(prompt) for _ in range(group_size)]
rewards = torch.tensor([reward_fn(prompt, c) for c in completions], dtype=torch.float)
if rewards.std() < 1e-6: # 全对或全错:没有学习信号,跳过
continue
adv = (rewards - rewards.mean()) / (rewards.std() + 1e-4) # 组相对优势
for comp, a in zip(completions, adv):
logp = policy.logprobs(prompt, comp)
logp_r = ref_policy.logprobs(prompt, comp)
ratio = torch.exp(logp - logp.detach())
unclip = ratio * a
clipped = torch.clamp(ratio, 1 - clip, 1 + clip) * a
pg = -torch.min(unclip, clipped).mean()
kl = (torch.exp(logp_r - logp) - (logp_r - logp) - 1).mean() # 无偏 KL 估计
total_loss += pg + beta * kl
return total_loss / max(1, len(prompts))
GRPO 的正式目标函数:对每个 prompt 采样 G 个回答 {o_1 … o_G},用组内奖励估计优势 Â_i = (R_i − mean(R)) / (std(R) + ε);若整组奖励全同则跳过(无学习信号)。策略损失 L_GRPO = E_i[ −min( ρ_i·Â_i, clip(ρ_i, 1−ε, 1+ε)·Â_i ) ] + β·KL(π_θ ∥ π_ref),其中 ρ_i = π_θ(o_i)/π_ref(o_i) 是重要性比。关键点:没有 Critic,省下与 policy 同级的参数与显存,且优势来自组内相对比较,天然适配「答案对错」这类可验证奖励。
text# GRPO 目标(数学骨架,与第 4.1 节的代码一一对应)
# 对每个 prompt 采样 G 个 completion:
# A_i = (R_i - mean(R)) / (std(R) + eps) # 组相对优势
# rho_i = pi_theta(o_i) / pi_ref(o_i) # 重要性比
# L_clip = -min( rho_i * A_i, clip(rho_i,1-e,1+e) * A_i )
# L = mean_i L_clip + beta * KL(pi_theta || pi_ref)
# 无 Critic、无在线 RM:奖励 R_i 直接由可验证函数给出
GRPO 家族在修什么(2025–2026 主线):DAPO 用 Clip-Higher(正/负例用不同 clip 上限,给「答对」的答案更大探索空间)、Dynamic Sampling(丢弃全对/全错的 prompt 以剔除无效样本)、Token-level loss(按 token 归一而非按样本,防长样本主导梯度)、超长惩罚(抑制度膨胀);GSPO 把重要性比值从 token 级提升到序列级 π_seq=Π_t π_t,降低长序列的噪声累积,对 MoE 还需补丁;GMPO 用几何平均替代算术平均估计优势,异常 token 的影响有界、可证更稳;CISPO 只裁剪权重不裁剪 token,保留「重新检查 / recheck」这类低概率反思词(它们常被普通 clip 误杀)。
| 算法 | 核心改法 | 修的问题 |
|---|---|---|
| GRPO | 组内相对优势,去 Critic | PPO 显存与稳定性 |
| DAPO | Clip-Higher + 动态采样 + token 级 + 超长惩罚 | 熵崩塌 / 无效样本 / 长度偏置 |
| GSPO | 序列级重要性比 | 长序列噪声 / MoE 抖动 |
| GMPO | 几何平均优势 | 异常 token 主导梯度 |
| CISPO | 只裁权重不裁 token | 反思词被误杀 |
DAPO 的两个关键改动(可直接跑的最小实现)
python# DAPO vs 原始 GRPO 的两个决定性改动
import torch
def grpo_adv(rewards, eps=1e-4):
"""原始 GRPO:组内标准化,全对/全错时 std≈0,优势被放大成噪声。"""
return (rewards - rewards.mean()) / (rewards.std() + eps)
def dapo_adv_and_mask(rewards, group_size=8, eps=1e-4):
"""DAPO 动态采样:丢弃全对(1.0)/全错(0.0)的整组,只保留有区分度的组。
预期:group_size=8 时,简单题常整组全对 -> 有效组可能只剩一半。"""
acc = rewards.mean().item()
if acc >= 0.999 or acc <= 0.001: # 无学习信号,整组丢弃
return None, torch.zeros_like(rewards)
std = rewards.std()
# 动态采样:丢弃标准差过小的组(近似无效),而非仅做标准化
keep = (std > 0.05).float()
adv = (rewards - rewards.mean()) / (std + eps) * keep
return adv, keep
def dapo_clip_higher(ratio, adv, eps_low=0.2, eps_high=0.28):
"""Clip-Higher:正优势(答对)用更宽松的上界 eps_high,防止低概率正确 token 被过早裁掉。
这是 DAPO 治熵崩塌的核心:常规 PPO 正负都用 0.2,会压制探索。"""
lo = 1.0 - eps_low
hi = 1.0 + (eps_high if adv >= 0 else eps_low) # 正优势放宽上界
return -torch.min(ratio * adv, torch.clamp(ratio, lo, hi) * adv)
# 预期:把 eps_high 从 0.2 调到 0.28,策略熵下降速度明显变慢,
# 训练后期 pass@1 仍能爬升(不调则 300 步后熵塌、奖励停滞)。
| DAPO 改动 | 原始 GRPO | DAPO | 效果 |
|---|---|---|---|
| 裁剪切分 | 正负都用 ε=0.2 | Clip-Higher:正 ε_high=0.28 | 缓解熵崩塌,保探索 |
| 样本筛选 | 全对/全错也参与(优势≈噪声) | 动态采样直接丢弃全同组 | 同等算力下梯度更有效 |
| 损失归一 | 按样本平均(长样本主导) | Token-level:按 token 总数归一 | 长 CoT 不被惩罚 |
| 长度控制 | 无 | 超长软惩罚 + 长度奖励塑形 | 抑制无意义的长度膨胀 |
经验取值:GRPO/DAPO 的 group_size 常取 8–16(太小优势估计噪声大,太大 rollout 成本线性膨胀);KL 系数 β 在纯 RLVR 场景常设 0–0.04(可验证奖励下过度锚定 reference 反而限制提升,DeepSeek/ Qwen 系列多用极小或无 KL);clip 区间 ε 常规 0.2,DAPO 正例上界抬到 0.28 左右;学习率通常比 SFT 小一个量级(1e-6 ~ 5e-7)。
4.2 Agentic RL:2026 的新战场
单轮做题的红利已经吃尽,所有实验室的火力都转向了多轮工具调用、长程规划的 Agent RL。它带来三个新难题:
| 方法 | 核心思路 | 效果 |
|---|---|---|
| ARPO | 只在工具返回那一刻(token 熵骤增的决策分叉点)分支采样 | token 消耗大降,成功率大升 |
| Tree-GRPO | 把树搜索搬进 RL,共享前缀让同预算下 rollout 数翻倍 | 同时免费获得步级过程监督信号 |
| GiGPO | 组中组:轨迹级 + 步级两层优势 | ALFWorld 提升 12% |
| CW-GRPO | 用 LLM Judge 给每轮检索打「贡献分」再重缩放优势 | 搜索类 Agent 显著提升 |
| AT-GRPO | 多智能体系统按角色 / 轮次分组 | 长程规划任务大幅提升 |
| 回归 Critic | 不等长子轨迹无法组内公平对比,改回 Critic 做 token 级优势 | GLM-5.2 在长程阶段的选择 |
GiGPO 的两层优势:组中组怎么算
python# GiGPO 核心:轨迹级(ho) + 步级(hi) 两层优势,解决 Agent 稀疏奖励
import torch
def gigpo_advantages(traj_rewards, step_rewards, eps=1e-4):
"""traj_rewards: [G] 同一任务 G 条完整轨迹的最终奖励。
step_rewards: [G, T] 每条轨迹每个「锚定状态」的即时奖励。
思路:把重复出现的状态(状态指纹相同)归为同一「步组」,在步组内做相对比较。"""
# ---- 第一层:轨迹级优势(宏观)----
a_traj = (traj_rewards - traj_rewards.mean()) / (traj_rewards.std() + eps)
# ---- 第二层:步级优势(微观)----
# 把 (状态指纹) 相同的步聚成一个步组,组内标准化
groups = {}
for g in range(step_rewards.shape[0]):
for t in range(step_rewards.shape[1]):
key = round(float(step_rewards[g, t]), 6) # 实际用状态 embedding 指纹
groups.setdefault(key, []).append((g, t))
a_step = torch.zeros_like(step_rewards)
for key, idxs in groups.items():
vals = torch.tensor([step_rewards[g, t] for g, t in idxs])
if vals.std() < 1e-6 or len(vals) < 2:
continue
norm = (vals - vals.mean()) / (vals.std() + eps)
for (g, t), v in zip(idxs, norm):
a_step[g, t] = v
# ---- 合并:宏观 + 微观 ----
total = a_traj.unsqueeze(1) + a_step
return a_traj, a_step, total
# 预期:ALFWorld(文本家务任务)上 GiGPO 相对纯轨迹级 GRPO 提升约 12%,
# 因为「打开冰箱」这类重复状态能在步组内获得稳定信号,缓解稀疏奖励。
- 推导组相对优势:为什么用 Â_i=(R_i−mean)/std 而非直接用 R_i?判据:能说明它等价于「基线减方差」且无需 Critic,并指出全同组时 std→0 必须跳过。
- 实现 Clip-Higher:写出正负优势使用不同上界的分支逻辑,并解释为什么只放宽「正优势」的上界而非下界。判据:代码正确 + 指出目的是保护低概率正确 token、缓解熵崩塌。
- 诊断熵崩塌:给定「奖励 300 步后停滞、平均熵从 1.8 掉到 0.6」,列出至少 3 个要排查的超参并说明各自机制。判据:命中 clip 上界 / β(KL)/ 学习率 / 温度中至少 3 个且有因果。
- 算 rollout 成本:group_size=8、每 prompt 平均生成 2k token、Agent 任务每轨迹触发 10 次工具调用,估算相对纯文本生成的墙钟倍数。判据:能识别工具执行是主项而非 token 生成。
- 选算法:同一任务改用 MoE 骨干 + 超长 CoT,为什么 GSPO 比 token 级 GRPO 更合适?判据:说出序列级重要性比降低长序列噪声累积、并提到 MoE 需额外稳定性补丁。
5. 蒸馏与合成数据
学习路径
- 读 5.1:区分黑盒/白盒/on-policy/强到弱/推理轨迹蒸馏的做法与适用
- 实现 onpolicy_kd_loss,用温度 2.0 软标签 + 拒绝过滤跑一轮
- 对接 M9:用 on-policy + 拒绝采样构造领域 SFT 数据,控制模型坍塌与污染
核心知识点详解
- 黑盒 vs 白盒蒸馏:黑盒只用强模型输出(含思维链)当数据;白盒用 logits KL 对齐学生与教师分布
loss=KL(teacher_student),能访问权重时效果更好。 - on-policy 蒸馏是 2026 主流:让学生自己采样,教师为输出打分/纠错,天然缓解训练/推理分布偏移,是最贴近部署设置的蒸馏。
- 软标签温度 T=2.0:
soft = softmax(teacher_logits / T),更高的 T 让分布更平、信息更足;配合拒绝过滤弱样本。F.kl_div(log(soft), soft, reduce sum)。 - 强到弱多级与轨迹蒸馏:强模型指导中等再指导小模型,逐级缩小差距;轨迹蒸馏把解题思维链数据直接当 SFT 训练。常见坑:直接把老师硬标签当学生标签,损失软标签信息且没做许可/污染检查。
学习路径
核心知识点详解
- 三陷阱:① 模型坍塌:用模型自己的输出训练让自己分布越来越窄(复用塌缩);② 错误放大:老师的小错在蒸馏中放大成系统性错;③ 污染评测:合成数据漏进了评测基准。
- 对策:真实数据混合 alpha=0.7:保持与真实数据按比例混合(
x_total = α·real + (1−α)·synth,α 常用 0.7),抑制坍塌的分布漂移。 - 每轮去重过滤:每轮生成后做去重与质量过滤,只保留多样性合格的新样本,避免重复样本主导分布。
- 独立评测验证:用训练外的独立评测集(含污染检查)衡量合成数据占比对下游指标的影响。常见坑:评测集与合成数据同源生成,测出来的「提升」是假象。
学习路径
- 读 5.1:记住拒绝采样通过率 20–40%、温度 2.0 软标签、alpha=0.7 混合
- 跑 on-policy 蒸馏 + 拒绝采样,记录通过率与混合系数对下游指标的敏感
- 完成本章自测:说明拒绝滤波为何降低弱样本对合成的噪声污染
核心知识点详解
- 拒绝采样通过率 20–40%:让强模型生成候选,用规则/评分过滤,只保留通过率约 20–40% 的高质量输出。通过率过高说明没在筛、过低浪费算力。
- 温度 T=2.0 + alpha=0.7:蒸馏软标签温度 2.0 提供更平滑的分布先验;真实/合成混合 alpha=0.7 保持分布稳定。
soft_logits = softmax(logits/T)。 - 拒绝滤波为何能降噪:删掉错解与低质解,等价于用「更干净的标签」训练,弱候选不再污染梯度与最终分布。
- 独立评测收尾:合成数据的好坏以训练外评测集为准,不看训练 PPL。常见坑:只看通过率或自评指标,不回归到真实评测,参数调了半天无实际收益。
5.1 把大模型的能力迁移到小模型
| 方式 | 做法 | 适用 |
|---|---|---|
| 黑盒 / 数据蒸馏 | 用强模型的输出(含思维链)作为训练数据 | 无法访问权重时唯一选择;要注意许可条款 |
| 白盒 logits 蒸馏 | 对齐教师与学生的输出分布(KL),含 top-k logits | 可访问权重时效果更好 |
| on-policy 蒸馏 | 让学生自己采样,教师为其输出打分/纠错 | 2026 年主流,能缓解分布偏移 |
| 强到弱蒸馏 | 用最强模型指导中等模型,再指导小模型 | 多级蒸馏,常见于开源模型训练配方 |
| 推理轨迹蒸馏 | 只蒸馏思维链与解题过程,而非最终答案 | 把推理能力迁移到小模型的关键 |
on-policy 蒸馏 + 拒绝采样(2026 主流配方)
python# on-policy 蒸馏:学生先自己采样,再用教师对「学生自己的输出」打分/纠错
# 关键区别:不是让学生模仿教师的输出,而是让学生在自己的分布上被教师纠正
import torch, torch.nn.functional as F
def onpolicy_kd_loss(student_logits, teacher_logits, student_ids,
temperature=2.0, alpha=0.7):
"""白盒 logits 蒸馏损失(仅对 top-k 教师分布,省显存)。
student/teacher_logits: [B, V];student_ids: [B] 真实/采样 token。"""
# 1) 软标签:教师分布(温度平滑,保留「暗知识」)
t_soft = F.softmax(teacher_logits / temperature, dim=-1)
s_log = F.log_softmax(student_logits / temperature, dim=-1)
kd = F.kl_div(s_log, t_soft, reduction='batchmean') * (temperature ** 2)
# 2) 硬标签:学生自己采样的 token(on-policy 的分布对齐)
ce = F.cross_entropy(student_logits, student_ids)
return alpha * kd + (1 - alpha) * ce
# 拒绝采样(rejection sampling)过滤:只保留教师给出「高分」的学生轨迹作为 SFT 数据
def rejection_filter(answers, judge_fn, threshold=1.0, max_keep_ratio=0.4):
kept = [a for a in answers if judge_fn(a) >= threshold]
# 经验:单轮通过率 20%-40% 时数据质量最好;>80% 说明任务太简单、无区分度
return kept
# 预期输出:alpha=0.7、T=2.0 常在推理类任务上比纯 CE 高 2-5 个点;
# T 取 1 会退化成硬标签蒸馏,T>4 会让分布过平、学生学到噪声。
| 蒸馏方法 | 教师信号 | 显存/成本 | 2026 适用场景 |
|---|---|---|---|
| 数据蒸馏(黑盒) | 仅文本输出 + CoT | 最低(只需 API) | 无法拿权重;跨厂商迁移 |
| logits 蒸馏(白盒) | 完整/top-k 输出分布 | 高(需同时驻留两模型) | 同家族强→弱,效果最好 |
| on-policy 蒸馏 | 教师对学生采样的打分 | 高(要 rollout) | 主流;缓解分布偏移 |
| 拒绝采样 + SFT | 教师对采样的二值/打分过滤 | 中(一次前向) | 把 RL 能力固化回 SFT,省 RL 算力 |
| 强→弱多级 | 逐级教师 | 高 | 开源模型训练配方(如把 671B 能力下沉) |
2026 的真实做法:头部开源模型常采用「on-policy 蒸馏 + 拒绝采样」的混合配方——先让基座对海量 prompt 采样多条回答,用最强教师(或规则验证器)打分,只保留高质量轨迹做 SFT,再偏 RL 精调。DeepSeek-V3/R1 系列公开的配方里,长 CoT 数据的生成与筛选就属于这一类。关键工程点:拒绝采样的通过率要控制在 20%–40%,过高说明题目太易、过低说明太难,都会浪费算力而不长本领。
- 区分两种蒸馏:写清楚 off-policy(模仿教师输出)与 on-policy(学生采样后教师打分)在分布偏移上的差别。判据:能指出 off-policy 训练/推理分布不一致,on-policy 缓解它但要额外 rollout。
- 调温度:白盒 KD 中 T 从 1 调到 4,学生会学到什么变化?判据:T 越大分布越平、越保留暗知识但也越易学噪声;T=1 退化为硬标签。
- 设通过率:拒绝采样通过率 95% 和 5% 分别说明什么?判据:95% 任务太易无区分度、5% 太难信号稀疏,都该调整难度分布。
- 防模型坍塌:设计一个最小流程避免「自训练坍塌」。判据:真实数据混入 + 每轮去重过滤 + 独立评测 + 保留人类/强模型锚点。
- 估成本:白盒 logits 蒸馏为何显存贵?给出理由。判据:教师与学生需同时驻留、要保存全词表 logits(可用 top-k 近似降显存)。
6. 训练基建与故障处理
学习路径
- 读 6.1:配 torchrun --resume、ckpt_every/keep_last、NCCL_DEBUG、dcgm/Prometheus、expandable_segments
- 跑一次带监控与断点重训的长训练,验证 --resume 与 ckpt 保留策略
- 对接 M9:复用 M7 管线跑 SFT/DPO/GRPO 时含容错监控与回滚预案
核心知识点详解
- torchrun --resume + ckpt 策略:
torchrun --resume从最近 checkpoint 续训;--ckpt_every 200 --keep_last 3每 200 步存一份、只留最近 3 份,控制磁盘与恢复精度。 - 先起监控再谈训练:
NCCL_DEBUG=INFO定位通信;dcgm-exporter + Prometheus采集 GPU 利用/温度/显存,配告警规则。监控不启动就跑步等于「盲训」。 - expandable_segments 治显存碎片:PyTorch 设
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True减少重复 alloc 的碎片开销,长训更稳。 - 断点续训的成品标准:中断后重启,loss 曲线从断点无跳变地接上,而不是从头再来。常见坑:resume 时忘了对齐随机状态/优化器 state,续训出现 loss 断层。
学习路径
核心知识点详解
- Loss spike → 回滚 + 降 lr:loss 突然尖峰上涨,立即回滚到上一个稳定 checkpoint,并把 lr 降约 50% 再继续,别硬扛。
- NCCL 卡死 / OOM / 数据饿死 / rank 慢:卡死用
NCCL_DEBUG定位卡在哪个集合通信;OOM 查激活/梯度并降 batch 或用激活重算;饿死查torch.utils.data.DataLoader的num_workers;rank 慢查是否有 straggler 拖慢 all-reduce。 - OOM 三连排查顺序:先压 batch → 开 gradient checkpointing(
activation_offload)→ 再压序列长,逐级试,别一次性全改无法判断哪个生效。 - 数据瓶颈的量化:GPU 利用率低而
num_workers/IO 打满,多为数据管线慢;加预取与 worker、落内存或换磁盘。常见坑:五类并发出现时只盯 loss,丢掉了 CPU/GPU 利用率这些定位信号。
学习路径
- 读 6.1:理解 SpikeGuard 用滑动中位数 3× 判 spike、冷却 200–500 步、回滚 + lr 降 50%
- 实现并被注入一个 loss spike,验证其自动检测与回滚行为
- 对接 M9:长训练里启用 SpikeGuard 并记录一次自动回滚案例
核心知识点详解
- 滑动中位数 3× 判 spike:用滑动中位数建立基线,当前 loss 超过
median×3(或偏离多个 MAD)即判 spike,避免误判正常抖动。 - 冷却 200–500 步 + 回滚 + lr 降 50%:命中后进入冷却(200–500 步不重复触发),回滚 checkpoint 并把 lr 降 50% 减缓恢复,防止同一振荡反复。
- 实现即注入验证:在训练里人为注入一个 loss spike,
assert detector 触发 → rollback 生效,让机制可测而不仅是「写了」。 - 自动化后的注意:自动回滚要落日志与告警,保留完整轨迹供事后复盘,别让黑盒自动改训练。常见坑:阈值设太低频繁误回滚,实际是在退化而非保护。
6.1 让长周期训练活下来
bash# 一次大规模训练的必备工程配置(清单式)
# 1) 弹性与容错
torchrun --nnodes=32 --nproc_per_node=8 \
train.py --resume --ckpt_every 200 --keep_last 3
# 2) 通信与显存
# export NCCL_IB_DISABLE=0
# export NCCL_DEBUG=WARN # 通信卡住时改成 INFO 定位
# export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
# 3) 监控(缺失等于盲飞)
# nvidia-smi dmon -s pucvmet -d 5 # 功耗/利用率/显存/温度
# wandb 记录 loss / grad_norm / lr / tokens_per_sec / mfu
# dcgm-exporter + Prometheus 做集群级告警
# 4) 回滚预案
# 保留最近 N 个 checkpoint + 记录每个 ckpt 对应的数据与配置版本
# 检测到 loss spike(如 > 3x 中位数)自动回滚并降低 lr
| 故障 | 现象 | 处理 |
|---|---|---|
| Loss spike | loss 突然跳到数倍然后不降 | 回滚到 spike 前的 checkpoint,跳过这批数据,降低 lr 或加裁剪 |
| NCCL 卡死 | 训练无输出、GPU 利用率 0 | 看 NCCL_DEBUG 日志定位;检查网卡 / IB / 拓扑;超时重启 |
| 显存 OOM | 偶发 OOM | 降 micro-batch + 增梯度累积;开梯度检查点;检查碎片(expandable_segments) |
| 数据加载饿死 GPU | GPU 利用率抖动、吞吐骤降 | 增加 DataLoader workers、预取、本地缓存、减少 CPU 预处理 |
| 某个 rank 慢 | 整体吞吐被拖慢 | 检查硬件故障、数据分片不均、专家路由不均 |
SpikeGuard:loss spike 的自动检测与回滚
python# 长周期训练最常见的事故是 loss spike。做法:滑动中位数 + 相对跳变阈值 + 自动回滚
import collections, statistics
class SpikeGuard:
def __init__(self, win=200, ratio=3.0, cooldown=300):
self.hist = collections.deque(maxlen=win) # 只保留最近 window 步
self.ratio = ratio # loss > ratio * 中位数 判为 spike
self.cooldown = cooldown # 触发后跳过 N 步,避免反复震荡
self.skip = 0
def step(self, loss, global_step):
if self.skip > 0:
self.skip -= 1
return 'skip' # 跳过异常数据,不再用这批更新
if len(self.hist) >= 50: # 样本足够才判
med = statistics.median(self.hist)
if loss > self.ratio * med: # 经验阈值:3x 中位数
self.skip = self.cooldown
return 'rollback' # 回滚到 spike 前 ckpt 并降 lr
self.hist.append(loss)
return 'ok'
# 经验数字:bf16 大模型训练,loss spike 多发生在 warmup 结束前后与
# 学习率较高阶段;阈值取 3x 中位数、冷却 200-500 步可覆盖多数情况。
# 更稳的做法:spike 回滚同时把 lr 临时降 50%,并在后续若干步缓慢恢复。
- 故障排序:训练中途 GPU 利用率掉到 0、日志无输出,你的前 3 步排查是什么?判据:NCCL_DEBUG 日志 → 网卡/IB/拓扑 → 超时重启进程。
- 写回滚逻辑:loss 从 2.1 跳到 7.5 后不降,给出处理流程。判据:回滚到 spike 前 ckpt + 跳过该批数据 + 降 lr/加梯度裁剪 + 冷却若干步。
- 算吞吐:8xH100、micro-batch=4、seq=8k、grad_accum=16,估算全局 batch 的 token 数。判据:4×16×8000×8 = 4,096,000 tokens/step(记入 activation/通信开销前)。
- 判断 MFU:若实测 200 TFLOPs/s/卡而 H100 峰值约 990 TFLOPs/s(bf16),MFU 是多少?判据:≈20%,对 MoE/长序列属正常偏低,应查通信与长序列 attention 开销。
- OOM 三连:偶发 OOM 的三个优先手段。判据:降 micro-batch 增累积 / 开梯度检查点 / 处理显存碎片(expandable_segments)。
7. 分词:BPE、BBPE 与词表规模权衡
学习路径
- 读 7.1:理解 BPE 合并最高频对、BBPE 256 字节无 UNK、SentencePiece + fallback 与压缩率
- 用手写极简 BPE 对一小语料跑两次合并,画出词表增长过程
- 完成本章自测:手推 BPE 前两次合并并比较字符/token 压缩率
核心知识点详解
- BPE 合并最高频对:从字符/字节词表出发,反复统计相邻 token 对频次,把最频繁的一对 merge 成新 token,直到达到目标词表大小。复杂度由
get_pairs+ 计数器控制。 - BBPE 用 256 字节、无 UNK:Byte-level BPE 以字节为基元,任何输入都能编码,没有 UNK token;与 Unicode 无关,多语言全覆盖。
tiktoken即其实现。 - SentencePiece + fallback:SentencePiece 把空格也当字符处理(可逆),配合字节 fallback 处理稀有 token。WordPiece 用贪心最长匹配词表来做切分。
- 压缩率 3.6 vs 4.5 字符/token:英文约 3.6 字符/token,中文等表意文约 4.5。常见坑:手推 BPE 时忘了「每次合并后重新统计频次」,得到错误的下一次 merge。
学习路径
- 读 7.2:理解大词表省算力、embedding V×d、纯英文 32k–64k、多语 100k–200k、数字/AST 分词技巧
- 对英文/多语/代码文本各做一次分词统计,对比压缩率与词表影响
- 完成本章自测:算 token 减 20% 对算力的影响与 V=150k 的 embedding 体量
核心知识点详解
- 大词表省算力:同样语料、更大的词表 → token 数更少 → 序列更短 → 训练与注意力成本更低。但 embedding
V×d与 softmax 层会变大。 - 量级经验值:纯英文 32k–64k;多语 100k–200k。embedding 参数量 =
V×d,V=150k、d=4096 时 ≈ 614M 参数,占相当比例。 - 数字逐位切分的坑:把「2026」切成单个数字 token 会让模型难以学到数字的数值结构,电话号/日期最好聚成一个 token 或用规则 preprocess。AST 代码分词按语法保留结构。
- token 减 20% 对算力的影响:序列/注意力成本 ≈ token 数二次方相关,token 减 20%(如 5→4)显存与 FLOPs 明显下降,可量化「压缩率≈成本」。常见坑:为了省 token 盲目追求超大词表,embedding 内存与 softmax 开销反而拖垮小模型。
核心知识点详解
- 手推 BPE 前两次合并:对一小语料数频次,第一次合并最高频字符对,第二次在更新后的序列上再数——两步都要写出来才算会。
- token 减 20% 算力降 36%:成本 ∝ token² 近似:0.8²=0.64,即降 36%。这是「压缩率↔算力」的快速换算。
- V=150k 约 614M 参数:150000×4096 = 6.144e8 ≈ 614M,embedding 只看
V×d,可直接秒算。
7.1 BPE / BBPE / SentencePiece 算法
分词是「文本 → 整数 id」的第一道关卡,它直接决定了模型能「看见」的最小语义单元。主流算法是 BPE(Byte-Pair Encoding):从字符(或字节)词汇表出发,反复统计相邻 token 对的出现频次,把最频繁的一对合并成新 token,直到达到目标词表大小。GPT-2/3/4、LLaMA、Qwen、DeepSeek 都基于 BPE 的变体。
python# 极简 BPE 训练(教学用,真实实现见 tokenizers / sentencepiece)
def get_pairs(word):
return set(zip(word[:-1], word[1:]))
def bpe_train(corpus, vocab_size=1000):
# 1) 预分词到词,再拆成字符序列
words = [list(w) for w in " ".join(corpus).split()]
vocab = {tuple(w): 1 for w in words} # 初始词表 = 字符
while len(vocab) < vocab_size:
pairs = {}
for w in words: # 2) 统计所有相邻对
for p in get_pairs(w):
pairs[p] = pairs.get(p, 0) + 1
best = max(pairs, key=pairs.get) # 3) 取最频繁的一对
nw = []
i = 0
while i < len(words[0]) if False else True: # 占位,下面真正合并
break
words = [merge(w, best) for w in words] # 4) 合并
vocab[tuple(best)] = 1
return vocab
def merge(word, pair):
out, i = [], 0
while i < len(word):
if i < len(word)-1 and (word[i], word[i+1]) == pair:
out.append(word[i]+word[i+1]); i += 2
else:
out.append(word[i]); i += 1
return out
BBPE(Byte-Level BPE) 把基础符号从「字符」换成「256 个字节」,于是任何文本(含罕见字、emoji、代码)都能被无损表示,永远不会出现 UNK。GPT 系列与 LLaMA 用 BBPE。SentencePiece 则把原始文本当作一个整体(不依赖空格预切分),学习 BPE 或 Unigram 子词,并支持 byte-fallback,是多语言模型(T5、早期 LLaMA、多语种 Qwen)的常见选择。三者核心差异在于「预分词方式」与「是否空格敏感」,对最终效果影响小于词表大小与训练数据。
| 方案 | 基础符号 | UNK 风险 | 代表 |
|---|---|---|---|
| BPE(字符级) | 字符 | 有(未登录字符) | 早期 GPT-2 小词表 |
| BBPE | 字节(256) | 无 | GPT-3/4、LLaMA、Qwen、DeepSeek |
| WordPiece | 子词 | 有 | BERT、多语种 BERT |
| SentencePiece(BPE/Unigram) | 字符/字节 | 低(含 fallback) | T5、LLaMA、多语言模型 |
量化词表质量:压缩率(字符/token)
python# 比较不同 tokenizer 的压缩率:token 数越少,同样上下文窗口装的信息越多
# enc 换成你有权使用的 encode 函数,如 tiktoken / transformers
def compression_report(texts, tokenizers):
rows = []
for name, enc in tokenizers.items():
tokens = sum(len(enc(t)) for t in texts)
chars = sum(len(t) for t in texts)
rows.append((name, tokens, chars / tokens)) # (名字, token 总数, 字符/token)
return rows
# 预期(同一段英文技术文本,字符/token 越高越好):
# gpt2(50k 词表) ~3.6
# llama3(128k) ~4.3
# qwen(150k+) ~4.5
# 中文差距更大:小词表 1 字常被切成 2-3 个 token,大词表可接近 1 token/字。
# 注意 attention 是 O(T^2),token 数少 20% => 注意力算力约省 36%。
7.2 词表大小与数学 / 代码能力
词表大小是一组权衡:越大 → 每文档 token 数越少 → 注意力计算更省、长文档能塞更长上下文,但 embedding 表(V×d)与 LM head 更大;越小 → 压缩率低、token 多、成本高,但对低资源语言更友好。公开实践:英文为主的模型常用 32k(GPT-2)到 100k(GPT-4 约 100k);多语言模型普遍 100k–200k(Llama-3 128k、Qwen 系列约 150k+),用大词表换取非英语的压缩率。
| 词表 | 典型压缩率(英文) | 多语言覆盖 | embedding 体量(d=4096) |
|---|---|---|---|
| 32k | ~3.5–4 字符/token | 差 | 131M |
| 100k | ~4–4.5 字符/token | 中 | 410M |
| 200k | ~5+ 字符/token | 好 | 820M |
分词质量直接拖累数学与代码能力,原因有三:① 数字常被切成「逐位」token(如 12345 → 1/2/3/4/5 五个 token),跨多位做进位、比较的算术要在很长序列上完成,极易出错;② 代码里的稀有符号(括号、运算符、缩进)碎片化严重,结构信息被切碎;③ 不同分词会让同一道题的 token 序列完全不同,影响泛化。2026 年的改进包括:为数字引入专用 digit token、对代码做基于 AST 的分词、以及用更细粒度 tokenizer 提升符号保真度——这也是为什么「换 tokenizer 有时比换模型更重要」在数据敏感任务上成立。
词表规模的参数与推理成本换算
python# 词表规模的参数量代价 + 压缩率带来的成本差
d_model = 4096
for V in (32_000, 100_000, 200_000):
tied = V * d_model / 1e6 # embedding 与 LM head 共享(weight tying)
untied = 2 * V * d_model / 1e6 # 不共享则翻倍
print(f"V={V:>7} tied={tied:6.1f}M untied={untied:6.1f}M")
# 预期(d_model=4096):
# V=32k -> tied 131M untied 262M
# V=100k -> tied 410M untied 819M
# V=200k -> tied 819M untied 1.64B # 对 7B 模型占比已超 10% (tied) / 23% (untied)
# 成本换算:假设某文档 5 万字符
for cpt in (3.5, 4.5): # 字符/token
print(f"{cpt} 字符/token -> {50000 / cpt:.0f} tokens/doc")
# 预期:3.5 -> 14286 tokens;4.5 -> 11111 tokens,相差约 22% 的推理成本。
- 手推 BPE:语料为 "low lower lowest",初始字符词表,写出前 2 次合并的 token 对。判据:第一次合并最高频对(如 l+o),第二次基于新序列再统计,能说明「词内频次而非全局次数」。
- 算压缩率收益:token 数减少 20%,attention 算力与推理成本各降多少?判据:attention 约降 36%(O(T²)),线性部分(矩阵乘)降约 20%。
- 估 embedding 体量:V=150k、d=4096、weight tying,embedding 占多少参数?判据:约 150000×4096 ≈ 614M。
- 解释算术失败:为什么逐位切分损害多位数乘法?判据:进位信息被切散到多个 token,需跨 token 搬运,attention 难稳定对齐。
- 选词表:纯英文产品模型 vs 中英双语模型,词表该各取多大?判据:纯英文 32k–64k 足够;多语 100k–200k 换压缩率,但需配大语料喂饱长尾 token。
8. 参数高效微调:LoRA / QLoRA / DoRA
学习路径
- 读 8.1:理解 LoRA ΔW=B·A、参数量 r(d+k)、r=16 仅 0.78%、alpha/r、B 初始化 0、lr 1e-4~3e-4
- 实现 LoRALinear 并核验 r=16 时参数量压缩、跑 merge 与运行时叠加
- 对接 M9:用 LoRA 跑 SFT 记录显存与耗时,与全量对比确认降本
核心知识点详解
- ΔW = B·A 的低秩分解:冻结 W,学增量
h = Wx + (B·A)x,A∈R^{r×k}、B∈R^{d×r},r≪min(d,k)。参数从 d·k 降到r(d+k)。d=k=4096 时 W 有 4096²≈16.78M 参,而 LoRA 仅 r·(d+k)=16×8192≈0.13M,约占 0.78%。 - alpha/r 控制增量与 merge:缩放系数
scaling = alpha/r(实践取 alpha=2r)。训练后W += B·A·scalingmerge 回主权重,推理零额外开销。 - B 初始化 0、A 随机,lr 1e-4~3e-4:B 置 0 让起点增量=0(不打断原权重),A 高斯随机。LoRA 常配 lr 1e-4~3e-4,比全量 1e-5~2e-5 高一两个数量级。
- 部署:merge 与运行时叠加:运行时
x @ (W + B*A);部署前把 ΔW 并回主权重。常见坑:忘了乘 scaling 或 merge 时重复叠加,导致增量被放大/错位,推理结果漂移。
学习路径
- 读 8.2:理解 QLoRA 的 4-bit NF4、双重量化、分页优化器与 DoRA 解耦幅度/方向
- 在单卡上用 QLoRA 微调一次,实测显存约 3.3 GB 与 LoRA 13 GB、全量 78 GB 的差距
- 对接 M9:用 QLoRA 把领域微调压进单卡,记录显存/质量权衡
核心知识点详解
- 4-bit NF4 量化基座:主权重用 NF4(4-bit 归一化 float)量化驻留,前向时反量化参与计算。相比 bf16 权重省约 4× 显存,是 QLoRA 省显存的主因。
- 双重量化到 0.37 bit:对量化常数再量化一遍,把 NF4 的有效位数压到约 0.37 bit/参数,进一步省显存。
- 分页优化器治 OOM:优化器 state 不够时自动分页换出(CPU)、需要时换入,避免小显存 OOM,QLoRA 因此能在 24GB 卡上跑 7B/8B。
- DoRA 把幅度与方向解耦:把权重分解成幅度与方向分别学习,比纯 LoRA 表达力更强。常见坑:量化基座在跨任务反复微调时精度衰减,基准差异超过阈值要重量化。
学习路径
- 读 8.2:背下 7B 全量约 78 GB、LoRA 约 13 GB、QLoRA 约 3.3 GB 的显存量级
- 估算 7B 全量 vs LoRA vs QLoRA 的显存账,确认节约来源
- 对接 M9:为选全量/LoRA/QLoRA 给出显存与质量取舍,写入微调配置
核心知识点详解
- 7B 三种量级的显存:全量约 78 GB、LoRA 约 13 GB、QLoRA 约 3.3 GB。全量需要多卡/大显存,LoRA 一张 16GB 勉强,QLoRA 单张 24GB 很轻松。
- 显存构成:全量=权重+梯度+优化器 state+激活;LoRA 只存 0.78% 梯度与优化器、基座冻结仍要驻留权重;QLoRA 再把权重压到 NF4。节约来自「少存梯度/优化器 state + 量化权重」。
- 全量 vs LoRA 取舍:LoRA 省显存省存储、抗遗忘、可热插拔多任务;全量更彻底但贵且易遗忘。质量通常 LoRA 达全量 90%+。
- 那张卡选哪个:按可用显存选:>78GB(多卡)可全量;16–78GB 用 LoRA;<8GB 用 QLoRA。常见坑:以为 QLoRA 是「免费的 LoRA」,忽略量化精度损失与反量化的速度开销。
8.1 LoRA:原理与超参
全量微调(Full FT)更新所有权重,显存与存储成本都高,且易遗忘。LoRA(Low-Rank Adaptation) 冻结原权重 W,只学习一个低秩增量 ΔW = B·A:前向变为 h = Wx + (B·A)x。其中 A∈R^{r×k}、B∈R^{d×r},r ≪ min(d,k)。参数从 d·k 降到 r·(d+k),当 d=k=4096、r=16 时仅约 0.8%,却常能达到全量微调 90%+ 的效果。
python# LoRA 的参数量与缩放
import torch, torch.nn as nn
d, k, r = 4096, 4096, 16
full = d * k # 16.78M
lora = r * (d + k) # 0.13M
print(f"full={full/1e6:.2f}M lora={lora/1e6:.2f}M 压缩={lora/full:.2%}")
class LoRALinear(nn.Module):
def __init__(self, in_f, out_f, r=16, alpha=32, drop=0.05):
super().__init__()
self.W = nn.Linear(in_f, out_f, bias=False) # 冻结
self.A = nn.Parameter(torch.zeros(r, in_f))
self.B = nn.Parameter(torch.zeros(out_f, r))
nn.init.kaiming_uniform_(self.A)
self.scale = alpha / r # alpha/r 控制增量幅度
self.drop = nn.Dropout(drop)
def forward(self, x):
return self.W(x) + self.drop(x @ self.A.T @ self.B.T) * self.scale
| 超参 | 常用取值 | 作用 / 取舍 |
|---|---|---|
| rank r | 8 / 16 / 32 / 64 | 越大容量越高;8–32 覆盖多数任务,64+ 用于难任务 |
| alpha | r 的 1–2 倍(如 16/32) | 实际增量幅度 = alpha/r,过大会淹没原权重 |
| dropout | 0.05–0.1 | 防过拟合,数据少时更重要 |
| target modules | q_proj,k_proj,v_proj,o_proj (+ gate/up/down) | 注意力投影必加;FFN 视情况 |
| 学习率 | 1e-4 ~ 3e-4 | 比全量微调大 1–2 个数量级 |
target modules 的选择:注意力里的 q/k/v/o 投影几乎必加;FFN 的 gate/up/down 投影对领域知识注入很有用,但会增加参数量与过拟合风险。经验法则是「先只加注意力投影,不够再加 FFN」。r 与 alpha 的关系由缩放 alpha/r 决定,调 r 时记得同步调 alpha 以保持有效增量幅度。
部署:把 LoRA 合并进原权重(零推理开销)
python# LoRA 的两种部署方式:运行时叠加(可热插拔) vs 离线合并(零额外延迟)
import torch
def merge_lora(W, A, B, alpha, r):
"""把 Delta W = (alpha/r) * B @ A 合并进原权重。
W:[out,in] A:[r,in] B:[out,r];合并后推理与原模型完全一致、无额外延迟。"""
delta = (alpha / r) * (B @ A)
return W + delta
def add_lora(W, A, B, alpha, r, x):
"""运行时叠加:适合多任务热插拔(vLLM/SGLang 的多 LoRA 服务)。"""
return x @ W.T + (alpha / r) * (x @ A.T @ B.T)
# 预期:merge 前后同一输入 logits 差异 < 1e-4(仅数值误差)。
# 取舍:merge 省推理开销但锁死单任务;未合并才能一个基座挂 N 个适配器来回切。
# 经验:适配器体积 = r*(d+k)*4字节;r=16,d=k=4096 时约 0.5MB/层,全模型常 <100MB,极便于分发。
8.2 QLoRA 与 DoRA:把微调压进单卡
QLoRA 让 65B 级模型能在单张 48GB 卡上微调:① 基座以 4-bit NF4 量化常驻显存(精度损失极小);② 双重量化(Double Quantization) 把量化所用的缩放因子再量化一次,省下约 0.37 bit/参数;③ 分页优化器(Paged Optimizer) 在显存峰值把优化器状态换页到 CPU,避免 OOM。LoRA 适配器仍以 16-bit 训练,梯度只回传适配器。
DoRA(Weight-Decomposed LoRA) 把权重分解为「幅度(magnitude)+ 方向(direction)」:幅度单独学习、方向用类 LoRA 的低秩更新并做归一化。相比普通 LoRA,DoRA 在同等参数量下更稳、更易收敛,尤其在低数据场景。它与 QLoRA 可叠加:4-bit 基座 + DoRA 适配器。
| 方法 | 显存占用(相对) | 质量 | 适用 |
|---|---|---|---|
| Full FT | 最高(需存优化器状态) | 上限最高 | 数据极多 / 需大幅改分布 |
| LoRA (16bit 基座) | 中 | 接近 Full FT | 单卡 24–80GB,最常见 |
| QLoRA (4bit 基座) | 最低 | 略低于 LoRA | 消费级 / 单卡大模型 |
| DoRA | ≈ LoRA | 常优于 LoRA | 低数据 / 追求稳定 |
把显存算清楚:Full FT / LoRA / QLoRA 各占多少
python# 微调显存估算:以 7B 模型、bf16 为例,逐项算清「常驻」显存
P = 7e9
def gb(x): return x / (1024 ** 3)
# 全量微调(AdamW) 常驻项:权重 + 梯度 + 优化器一阶/二阶矩(fp32)
params_bf16 = 2 * P # 权重 bf16
grad_bf16 = 2 * P # 梯度 bf16
adam_moments = 8 * P # fp32 m + fp32 v
print(f"Full FT 常驻 = {gb(params_bf16 + grad_bf16 + adam_moments):.1f} GB")
# LoRA(16bit 冻结基座):只训适配器,基座无梯度/优化器状态
print(f"LoRA 常驻 = {gb(params_bf16):.1f} GB + 适配器(极小)")
# QLoRA(4bit NF4 基座):约 0.5 字节/参数 + 量化常数
print(f"QLoRA 常驻 = {gb(0.5 * P):.1f} GB + 适配器")
# 预期:Full FT ≈ 78 GB;LoRA ≈ 13 GB;QLoRA ≈ 3.3 GB(仅权重项)
# 别忘还有 activations:随 batch×seq 线性增长,开梯度检查点可降 2-4 倍。
- 算 LoRA 参数量:d=k=4096、r=16,LoRA 占全量的百分比?判据:16×(4096+4096)=131072 ≈ 0.13M,全量 16.78M,约 0.78%。
- 估全量微调显存:7B/bf16 + AdamW 的常驻显存是多少?判据:(2+2+8)×7B ≈ 78 GB(未含激活)。
- 选部署方式:一个基座要挂 20 个客户适配器,该 merge 还是运行时叠加?判据:运行时叠加(多 LoRA 服务),merge 会锁死单任务。
- 解释初始化:LoRA 为什么把 B 初始化为 0?判据:使 ΔW=0,从原模型平滑起步,避免初期破坏预训练权重。
- DoRA 动机:DoRA 相比 LoRA 多分解了什么?判据:幅度与方向解耦(magnitude + direction),低数据更稳。
9. 灾难性遗忘、后训练超参与对齐评测
学习路径
- 读 9.1:理解灾难性遗忘成因(数据窄/lr 大/风格偏)与缓解(replay 10–30%、KL、LoRA、配比、退火)
- 实现 forget_report 双轨评测,确认"通用基准 + 领域任务"都跑
- 对接 M9:SFT/DPO 后跑统一+领域双轨评测,任一通用基准跌 >2 点算遗忘并修
核心知识点详解
- 灾难性遗忘的成因:① 训练数据过窄(全是某领域指令);② 学习率过大把已学表示推远;③ 对齐太偏某类风格。它发生在训练集之外的通用能力上,与过拟合不同。
- 量化阈值:GSM8K 掉 3–8 点告警:通用基准(GSM8K/MMLU 等)对比 base 下跌 3–8 点即明显遗忘,应触发修复而不是继续叠领域数据。
- 四种缓解:数据回放(replay 混 10–30% 通用/预训练数据)、KL 正则(β 0.01–0.1 贴参考)、LoRA(只动少量参数冻结底座)、配比与退火(领域/通用平衡)。
- 双轨评测才能发现遗忘:只在领域任务上看指标会漏掉遗忘;必须「通用基准 + 领域任务」同时跑。常见坑:replay 混太多把领域精度冲淡,要平衡比例。
学习路径
- 读 9.2:背下后训练超参范围(SFT lr、DPO beta 0.05–0.5、RLHF KL、GRPO group、采样温度)
- 对接 M9:把对齐超参写入配置并记录其对目标指标的影响
- 完成本章自测:对一次失败的 DPO/GRPO 训练归因超参问题
核心知识点详解
- 各阶段超参一览:SFT lr 1e-5~3e-4;DPO β 0.05–0.5;RLHF KL 0.01–0.1;GRPO group 4–16;采样温度 0.8–1.0。按阶段各取一档。
- 超参失败归因:DPO/GRPO 训练失败先看是不是 β 太大(学不动)或太小(语言崩);奖励不涨查 group 与温度是否过随机/过确定。
- 写进配置并记录:所有对齐超参写入 yaml/config,每次运行记录其与目标指标(win rate/拒答率等)的对应,便于回滚归因。
- 采样温度的作用:rollout 采样温度 0.8–1.0 平衡探索与稳定;过低则组内多样性不足,过高则噪声大。常见坑:三阶段共用一套超参配置,把 SFT 的 lr 直接用在 GRPO 上导致不稳。
学习路径
- 读 9.2:背下五指标上线门禁(win rate>0、MMLU 跌 <2、拒答 <15%、长度膨胀 <30%、奖励未饱和 <0.95)
- 编写对齐评测门禁脚本跑一次,确认所有阈值都有对照
- 对接 M9:作为 M9 对齐是否成功的验收门禁,产出 alignment.md 三阶段对比
核心知识点详解
- 五指标上线门禁:目标能力
win rate > 0;通用能力MMLU 跌幅 < 2 点;拒答率 < 15%;长度膨胀 < 30%;奖励未饱和 < 0.95。五项齐过才算对齐成功。 - 门禁要有对照:每个指标都要与 base 或上一阶段对照(Δ),而不是看绝对分。「跌幅<2」必须跟预训练/SFT 基线比。
- 写成脚本可重复执行:
scripts/alignment_gate.py接受模型与评测集,输出五指标对照表并判定 pass/fail,纳入 CI。 - 三阶段对比文档:产出 alignment.md:SFT/LoRA/DPO/GRPO 每一阶段在同样五指标上的变化,证明「能力升、通用不崩」。常见坑:只报提升的指标、隐掉通用下跌,等于没做过门禁。
9.1 灾难性遗忘:成因与缓解
灾难性遗忘指后训练(SFT/对齐)让模型在通用能力、语言能力或多语言上明显回退。成因:① 训练数据过窄(全是某领域指令,模型「忘掉」其余);② 学习率过大把已学表示推离;③ 对齐阶段过度偏好某类回答风格,挤压原有能力。它和「过拟合」不同:过拟合是训练集上变差,遗忘是在训练集之外的下游能力上变差。
| 缓解手段 | 机制 | 代价 |
|---|---|---|
| 数据回放(replay) | 在 SFT/对齐数据里混入一定比例的预训练 / 通用数据 | 稀释目标信号,需配比 |
| KL 正则 | 对参考模型加 KL 约束,限制偏离 | 约束过强则学不动 |
| LoRA / 低秩 | 只动少量参数,原权重冻结 | 容量上限受限 |
| 数据混合比例 | 通用:领域 ≈ 7:3 或更保守 | 需按任务实测 |
| 分阶段退火 | 先宽后窄,逐步收窄 | 流程更长 |
实用配方:SFT 阶段就混入 10–30% 通用 / 预训练续训数据;对齐阶段用 β·KL(π∥π_ref) 把策略锁在参考模型附近(β 太小防不住遗忘、太大训不动,常取 0.01–0.1);若用 LoRA 则遗忘天然更轻。评测时必须同时跑「通用基准 + 领域任务」,任一侧崩了都算失败。
把遗忘量化:双轨评测报告
python# 灾难性遗忘的量化:通用能力 vs 领域能力的双轨评测
def forget_report(base_scores, ft_scores,
key_bench=('MMLU', 'HumanEval', 'GSM8K'), tol=2.0):
"""base/ft_scores: {bench: 分数}。逐个基准算 delta,跌幅 > tol 触发告警。"""
report = {}
for b in key_bench:
d = ft_scores[b] - base_scores[b]
report[b] = (round(d, 2), 'OK' if d > -tol else 'REGRESS')
return report
# 预期(某 8B 模型 LoRA SFT 后的典型结果):
# MMLU -0.8 OK
# HumanEval -1.5 OK
# GSM8K -3.4 REGRESS <- 数学推理回退明显
# 判据:任一通用基准跌幅 > 2 点即视为明显遗忘,应加大 replay 比例或降学习率。
| 遗忘信号 | 典型量级 | 对策 |
|---|---|---|
| 数学/代码推理回退 | GSM8K 掉 3–8 点 | 混入推理数据 / 降 lr;避免纯风格数据 |
| 多语言能力退化 | 非英语掉 5–15 点 | 增大多语 replay 比例 |
| 通用知识下降 | MMLU 掉 > 2 点 | 加通用 SFT 数据;减小训练步数 |
| 格式僵化/过度拒答 | 拒答率骤升 | 清洗安全数据;引入合理请求样本 |
9.2 后训练超参与对齐是否成功的判据
后训练超参与预训练量级差异巨大。学习率通常比预训练小 1–2 个数量级(SFT 约 1e-5~2e-5 全量、LoRA 约 1e-4~3e-4;DPO β 常取 0.1;RLHF 的 KL 系数约 0.01–0.1);GRPO 的组大小(group size)取 4–16,越大基线越准但 rollout 越贵;采样温度与每题采样数共同决定探索广度。
| 超参 | 典型范围 | 调错的症状 |
|---|---|---|
| SFT lr | 1e-5 ~ 3e-4 | 过大→遗忘/格式崩;过小→不动 |
| DPO beta | 0.05 ~ 0.5 | 过大→过度保守像参考;过小→奖励被忽略 |
| RLHF KL coef | 0.01 ~ 0.1 | 过大→学不动;过小→偏离、语言退化 |
| GRPO group | 4 ~ 16 | 过小→优势噪声大、不稳 |
| 采样数 / 温度 | 4~8, 0.8~1.0 | 过低→探索不足 |
对齐上线的五指标门禁(可当 CI 用)
python# 对齐上线门禁:五个指标同时达标才算「成功」,任一不过就退回调参
def alignment_gate(target_win, mmlu_drop, refusal_rate, len_growth, reward_sat):
checks = {
'target_win': target_win > 0.00, # 目标能力上升(win rate)
'general_keep': mmlu_drop < 2.00, # 通用能力跌幅 < 2 点
'safety': refusal_rate < 0.15, # 不过度拒答
'length': len_growth < 0.30, # 输出长度膨胀 < 30%
'not_hacked': reward_sat < 0.95, # 奖励未饱和成满分
}
passed = all(checks.values())
return checks, passed
# 预期:最常见的失败组合是 length 与 not_hacked 同时为 False ——
# 模型学会「越长越讨好」,奖励刷到 0.98 却并不更准。
# 对策:加长度惩罚(GFPO 的最短过滤 / DAPO 的超长惩罚),不复用被 hack 的 RM。
- 定义遗忘:说清灾难性遗忘与过拟合的区别。判据:遗忘发生在训练集之外的下游能力上,过拟合是训练集变差。
- 写双轨评测:给定 base/ft 两组分数,写出门禁判据。判据:通用基准跌幅 > 2 点即告警,领域基准必须上升。
- 诊断退化:SFT 后 MMLU 掉 4 点、GSM8K 掉 6 点,列出 3 个对策。判据:加通用/推理 replay、降 lr、减训练步数(或改 LoRA)。
- 识别奖励 hack:奖励从 0.6 涨到 0.99 但人工评测没变好,说明什么?判据:奖励被 hack(长度/格式讨好),需换可验证奖励或加长度惩罚。
- 定 β:DPO 的 β 取 0.01 与 0.5 分别会发生什么?判据:过小偏离 reference、易遗忘与长度膨胀;过大贴近 reference、几乎学不动。
项目里程碑
让 Hamauls Orion 的模型真正懂你的领域:清洗构造领域指令集,做 SFT,用 LoRA/QLoRA 降本,再用 DPO 或 GRPO 做偏好对齐,并验证「对齐之后到底好了没」。
本阶段产出(直接进入项目仓库)hamauls_orion/data/sft_build.py:领域指令集构造(含去重、难度分层、格式统一、训练/评测隔离)hamauls_orion/train/sft.py+configs/sft/lora.yaml:LoRA / QLoRA 微调,记录显存与耗时对比hamauls_orion/train/dpo.py或grpo.py:偏好/可验证奖励对齐,含奖励黑客现象排查docs/exp/alignment.md:SFT / DPO / GRPO 三阶段效果对比(含人工抽检 30 例)- 一份数据卡(datasheet):来源、许可、清洗步骤、已知偏差
阶段练习项目
- 跑通完整六步管线并记录每步 token 保留率,产出可复现的版本哈希
- 同输入在任意机器重跑得到完全一致的版本号(数据版本可追溯)
- 产出数据卡片(来源 / 许可 / 清洗步骤 / 已知偏差 / 保留率)
- 用 polars / LazyFrame 批处理,避免逐行 for 循环
- 实现精确去重(内容哈希) + MinHash+LSH 近似去重,量化去重后的剩余占比(约 30–50%)
- 做至少一项质量筛选(长度 / 语言 / 启发式或模型打分),并做 n-gram 污染检查
- 设计并记录领域配比与退火策略,统计各领域 token 占比
- 每步输出输入 / 输出样本数与总 token,端到端版本哈希可复现
scripts/data/pipeline.py+ 各步统计与数据卡片 md- 版本化后的清洗数据文件(含内容哈希清单)
不做模型训练评测打分,也不做超量模糊去重,先保证可复现的精确去重与质量四查到位。
- 用 8B 级开源模型完成 LoRA SFT,再构造偏好对完成 DPO,两次训练指标可复现
- 在指令跟随基准上,SFT+DPO 明显优于仅 SFT(可量化提升)
- 给出三阶段(base / SFT / SFT+DPO)在目标能力与安全指标上的对照表
- 构造带 loss mask 的 SFT 数据(非 assistant 段置 -100),用 Llama-Factory / TRL 配 LoRA rank 16、lr 1e-4~3e-4 跑一次
- 构造偏好数据对(同一 prompt 的成功 / 失败回答)并实现 / 复用 DPO 损失(β 取 0.05–0.5 记录敏感性)
- 统一评测集上对比 base / SFT / SFT+DPO 的指令跟随与安全(拒答、有害性)指标
- 全程用通用基准 + 领域基准双轨验证,通用能力下跌超阈值即告警
- SFT / DPO 训练脚本与配置 + 三阶段评测对照表
- DPO 损失实现与 β 敏感性记录 md
不做 RLHF(PPO),只做离线 DPO;不做多轮多步对齐。
- 在数学数据集上实现 GRPO 训练循环,奖励用答案精确比对,训练可复现
- 端到端监控 reward / 输出长度 / 策略熵三条曲线,能明确观察到提升与稳定性变化
- 对比 vanilla GRPO 与 DAPO 的稳定性差异(如熵崩塌 / 无效样本)并给出结论
- 实现组内相对优势
Â_i=(r_i−mean)/std、重要性比裁剪与β·KL(π∥π_ref),丢弃全对 / 全错组 - 奖励由答案字符串比对(可验证奖励)判定,不需要奖励模型
- group_size 取 8–16,KL β 0–0.04,记录其对收敛的敏感性
- 可视化并落表 reward / 长度 / 熵三曲线,作为稳定性与采纳判断依据
- 可选实现 DAPO 的 Clip-Higher(ε_high=0.28)与动态采样做对比实验
grpo.py训练脚本 + 三曲线监控图- vanilla GRPO vs DAPO 对比结论 md
不做多步 Agent 环境 RL,仅做单轮可验证奖励的数学推理对齐。
- 用强模型生成带思维链的解题数据,经拒绝过滤后蒸馏到 1B–3B 小模型
- 在同一个评测集上,轨迹蒸馏模型的解题能力优于「直接 SFT 结果答案」方案
- 给出蒸馏前后与三种方案的准确率对比并量化提升
- 调用强模型生成思维链解题样本,用答案比对做拒绝采样(通过率约 20–40%)
- 对 1B–3B 小模型做蒸馏 / SFT:轨迹蒸馏(含思维链)与直接 SFT(只训结果)各跑一版
- 统一评测集上分别评估,记录 PPL 之外还需主观审看推理链质量
- 记录合成数据的每轮去重与真实数据混合(alpha≈0.7),防止模型坍塌与污染
- 蒸馏 / SFT 训练脚本 + 数据生成与过滤脚本
- 三方案评测对比表与结论 md
不微调强教师模型,只做单轮蒸馏与结果对比;不引入模型评分器当奖励。
- 在同一垂直领域上依次完成 QLoRA SFT 冷启动 → DPO 偏好对齐 → GRPO 可验证奖励对齐,全链路可复现
- 领域基准能力相对 base 显著上升,且通用基准任一指标下跌不超过阈值(如 MMLU 跌幅 <2 点)
- 以五指标门禁(目标能力 win rate >0、拒答 <15%、长度膨胀 <30%、奖励未饱和 <0.95、通用跌幅 <2)全通过作为对齐成功判据
- 用 QLoRA(NF4 量化)在领域数据上做 SFT 冷启动,记录显存(约 3.3 GB 级)与质量权衡
- 构造领域偏好对并用 LoRA 做 DPO(β 0.05–0.5),记录其对目标指标的影响
- 在可验证任务(代码过测 / 数学对答等)上实现 GRPO,奖励用程序判定,监控 reward / 长度 / 熵曲线
- 全程跑「通用基准 + 领域基准」双轨评测(含 forget_report),任一通用基准下跌超阈值即触发修复(replay / 降 lr / LoRA)
- 所有阶段超参与配置写入 yaml,记录与五指标门禁的对应,支持回滚归因
sft.py+dpo.py+grpo.py全链路训练脚本与统一配置alignment.md(base / SFT / DPO / GRPO 三阶段五指标对比)+ forget_report
不做 Agentic RL / 多步环境 RL,不做 PPO,不做多语言大规模预训练,仅单参数到几十单参数级的可解释后训练对齐。
常见误区
- 跳过数据质量直接上模型,然后花几周怀疑架构。
- SFT 数据量很大但格式不统一、含重复、含格式错误——这是最常见的失败原因。
- SFT 时对 user 片段也算损失,导致模型学会生成「用户的问题」。
- 把 RLHF 当成必须走 PPO,忽略了 DPO / KTO 在多数场景下的性价比。
- 用偏好奖励做数学推理 RL,结果模型学会「写得长且自信」而不是算对。
- 不监控熵和输出长度,训练到后期才发现模型退化成复读机。
- 合成数据不做过滤与混合,两三轮后模型能力明显退化(模型坍塌)。
- 长周期训练没有 checkpoint 策略与回滚预案,一次硬件故障损失几天算力。
面试高频问题速答
SFT 和 RL 分别解决什么问题?
SFT 解决「形式与对齐」:把预训练模型的续写倾向引导成对话/指令跟随的输出形式,学到的是数据中已有的模式。RL 解决「能力与可靠性」:通过奖励信号让模型探索更优的解题路径,尤其当奖励可验证时(答案对错、单测通过),能获得超越训练数据的推理能力。所以正确顺序是 SFT 冷启动 → RL 提升上限。
DPO 为什么不需要奖励模型?
RLHF 的最优策略有闭式解 π* ∝ π_ref · exp(r/β),即奖励可以被表示为「策略相对参考模型的对数概率比」。把 r 的这个表达式代入偏好目标(Bradley-Terry 模型),奖励项就被约掉,只剩对 chosen/rejected 的对数概率差做 log-sigmoid 损失。于是可以在离线成对数据上直接优化,无需奖励模型与在线采样。
GRPO 相比 PPO 省了什么?代价是什么?
省掉了 Critic(价值网络),从而省下与策略网络同量级的参数、梯度与优化器状态显存,且天然契合可验证奖励。代价:需要为同一 prompt 多次采样来估计组内基线(rollout 成本上升),当组内奖励全相同则无学习信号,且「组内可比」的假设在长程不等长子轨迹场景会失效。
RL 训练中模型输出越来越长,怎么办?
这是典型的长度偏置:长回答在人类/模型偏好里常被高估,RL 会放大它。对策有三类:① 奖励设计上对长度做惩罚或做长度归一化;② 采样后按 token 效率过滤只训最优子集(GFPO);③ 改用只裁剪权重不裁剪 token 的目标(CISPO)以保留「重新检查」这类低概率反思词。监控 length 与 entropy 曲线是必备动作。
蒸馏和 RL 应该怎么选?
不是二选一,而是互补。蒸馏是「继承」——把已有强模型的能力高效迁移到目标模型,成本低、见效快,适合把能力压缩到小模型或快速提升基座。RL 是「学习」——通过奖励探索出训练数据里没有的新行为,上限更高但更贵、更不稳定。主流配方是二者结合:先蒸馏拿到好起点,再用 RL 突破上限。
如果用不超过 8 张卡,你会怎么做一次“像样的”后训练?
① 选 7B–8B 级开源模型,用 QLoRA 做 SFT(4bit 量化 + 梯度检查点 + 梯度累积);② 用 LoRA 做 DPO,偏好数据规模几千条即可;③ 若做 RL,用小规模 GRPO,组大小取 4–8,只在一个窄任务(如数学或格式合规)上训,严格控制步数并监控熵;④ 全程用小评测集做门禁,避免过拟合;⑤ 用 vLLM 做推理加速 rollout。关键是把整个流程跑通而不是追求指标。