核心概念:reference / reward / policy 三模型
RLHF 系算法(PPO / DPO / GRPO)里反复出现 reference / reward / policy 三种"模型"。它们什么时候是同一个、什么时候不同、显存怎么算——这些是后续章节的底座。
4.1三个角色的本质
| 角色 | 作用 | 初始权重通常 | 更新 |
|---|---|---|---|
| Policy | 当前正在训的模型(生成 response) | SFT 模型 | ✅ 更新 |
| Reference | KL 散度的参考"原点",防止 policy 偏离太远 | SFT 模型(冻结副本) | ❌ 冻结 |
| Reward Model | 给 (prompt, response) 打标量分 | RM 训练得到 | ❌ 冻结 |
4.2显存账本(7B 模型为例)
| 算法 | policy | reference | RM | critic | 总显存 (bf16) |
|---|---|---|---|---|---|
| SFT | 14 GB | — | — | — | 14 GB |
| DPO | 14 GB | 14 GB | — | — | 28 GB |
| PPO | 14 GB | 14 GB | 14 GB | 14 GB | 56 GB |
| GRPO | 14 GB | 14 GB | 14 GB | — | 42 GB |
| ORPO | 14 GB | — | — | — | 14 GB |
| KTO | 14 GB | 14 GB | — | — | 28 GB |
这就是为什么 RLHF 系比 SFT "贵"得多——一份 base 模型要在 GPU 上同时存好几份。
4.3KL 散度:reference 的角色
所有"对齐"算法的目标都是"让 policy 接近 RM 偏好,但又别离 reference 太远":
$$\max_\pi \;\mathbb{E}_{x\sim D}\big[R(x, \pi(x))\big] - \beta \;\mathrm{KL}\!\big(\pi \;\|\; \pi_\text{ref}\big)$$
$\beta$ 越大、policy 越被 reference 拉住;$\beta$ → 0 则只追 reward,容易 reward hacking。典型 $\beta = 0.1$。
4.4用 LoRA 省一份模型
显存紧时常用招:policy 上 LoRA,reference 直接等于"关掉 LoRA 后的 policy"。TRL 自动支持:
from peft import LoraConfig
peft_cfg = LoraConfig(r=16, target_modules="all-linear")
trainer = DPOTrainer(
model=model,
ref_model=None, # ★ 不传 ref_model,TRL 自动用 disable_adapters
args=cfg,
train_dataset=ds,
peft_config=peft_cfg,
)
实现:训练前 with model.disable_adapter(): ref_logprobs = ...,节省一份模型显存。
4.5三个角色的时间线
4.6哪些算法不需要 reference
- ORPO:单模型一阶段,loss 里直接加 odds ratio 正则;
- CPO:类似 ORPO,但用对比损失;
- SFT:没有 KL 概念,最朴素;
- SimPO:直接用长度归一化的 log-prob 差,不依赖 reference。
这些算法显存压力小、训练简单,缺点是无法严格控制偏离。
4.7哪些算法不需要 RM
- DPO / IPO / KTO:直接用偏好数据,不需要单独训 RM;
- SimPO / ORPO / CPO:同上;
- GRPO with rule-based reward:用数学/代码的规则验证替代 RM。
4.8_BaseTrainer:TRL 自己的根类
读 TRL 源码时会注意到一件事:所有主 trainer 都继承 _BaseTrainer(trl/trainer/base_trainer.py:30),而 _BaseTrainer 才继承 transformers.Trainer。不是直接继承 HF Trainer。
| 设计要点 | 原因 / 影响 |
|---|---|
| "自包含" 哲学 | 每个 trainer 文件故意复制代码而非抽象 —— 读 dpo_trainer.py 不用跳来跳去;改 grpo_trainer.py 不会影响 dpo |
_BaseTrainer 只放最少共享逻辑 | model card 生成、telemetry、几个轻量 helper |
子类经常重写 train() | PPO/GRPO 自带 rollout 循环,不复用 HF Trainer 的 step 循环 |
| Online preference 有自己的继承链 | OnlineDPOTrainer → _BaseTrainer;NashMDTrainer / XPOTrainer 再继承 OnlineDPOTrainer |
"为什么 TRL 不抽出 BasePreferenceTrainer 之类的基类?" —— 维护者明确选择了"代码重复 vs 接口稳定"中的前者。算法迭代太快,强抽象反而成枷锁。
4.9reference 模型的三种省显存招
4.4 给出了"LoRA + disable_adapter" 一招。TRL 实际上有三招,对应不同显存预算和精度需求。来源:DPOConfig(dpo_config.py:23)的几个字段。
| 招式 | YAML 字段 | 代价 | 典型用 |
|---|---|---|---|
LoRA + disable_adapter() | 不写 ref_model + 传 peft_config | 每次需要 ref logp 时切换两次 adapter 状态 | 常规 DPO |
| 预计算 ref logprobs | precompute_ref_log_probs: true | 训练前过一遍 ref 模型存盘,之后扔掉 | 大 ref 模型,但 dataset 小且固定 |
| TR-DPO 同步 | sync_ref_model: true + ref_model_sync_steps | 定期把 policy 拷给 ref,模拟"软 reference" | 需要 reference 跟得上 policy 漂移时 |
三招互斥:
precompute_ref_log_probs=true时sync_ref_model不能开(已无 ref 模型可同步);- LoRA disable_adapter 模式与
sync_ref_model不能共存(共享同一份权重,没有"独立 ref"可同步); - 三种都没开 = 显式独立 ref 模型,占双倍显存。
4.10PEFT 路径下 ref 模型的真实状态
不传 ref_model 但传了 peft_config 时,DPOTrainer 在 __init__(dpo_trainer.py:595–600附近)做的事:
- 把 policy model 用 PEFT 包成带 LoRA adapter;
- "ref adapter" = adapter 关掉时的同一份 base 模型;
- 每次需要 ref logp,调
model.disable_adapter()进 ctx manager 拿一次 forward; - 退出 ctx manager 后 adapter 恢复启用,policy forward 正常。
这种做法的 caveat:
- base 模型如果是 QLoRA 量化的 4-bit,ref forward 也走 4-bit —— 数值精度比纯 fp16 ref 模型差;
- 切换 adapter 状态有一点点 overhead,但比双份模型显存便宜得多。
4.11这章你需要带走的
- 三模型角色:policy / reference / RM,按算法增减;
- 显存账本:PPO 是 SFT 的 4×、DPO 是 2×、ORPO 和 SFT 一样;
- 用 LoRA + disable_adapter 可以省一份 reference 模型;
- KL 散度的 $\beta$ 通常 0.1,太小会 reward hacking、太大学不到东西。