AReaL 中 DPO 离线对齐算法实践:从 HH-RLHF 示例到源码级损失实现解析
【免费下载链接】AReaLThe RL Bridge for LLM-based Agent Applications. Made Simple & Flexible.项目地址: https://gitcode.com/GitHub_Trending/are/AReaL
本文基于 AReaL 仓库中的 DPO 算法文档(docs/en/algorithms/dpo.md)及其配套源码,系统讲解 Direct Preference Optimization(DPO)在 AReaL 中的落地方式:包括 DPO 目标函数与 implicit reward 的数学原理、基于 FSDP2 的 actor/ref 双模型共置训练架构、
sigmoid与IPO两种损失变体、HH-RLHF 数据集的配对构造与 prompt 边界推断,以及配套的可运行示例(单机与多机 Ray)和全部关键参数。读完本文,你将能够独立修改并运行 AReaL 的 DPO 训练配置,理解其损失计算的源码细节,并借助dpo/前缀指标监控训练质量。
一、DPO 在 AReaL 中的定位
Direct Preference Optimization(DPO)是一种离线对齐算法:它直接在人类偏好数据(chosen / rejected 配对)上优化语言模型,不需要独立的奖励模型(reward model),也不需要在线 RL rollout。
与 RLHF(PPO)相比,DPO 具有三项显著优势(文档明确列出):
- 更简单:无需奖励模型、无需价值网络(value network)、无需在线生成;
- 更稳定:只有一个监督式风格的损失函数;
- 更高效:每个 batch 只需两次 forward + 一次 backward(policy + reference 各一次前向)。
AReaL 将 DPO 实现为actor + ref 双模型共置(colocation)的训练范式:actor承载待训练的策略模型,ref承载冻结的参考模型。二者默认共享同一组 GPU(通过ref.scheduling_strategy.type: colocation配置),参考模型的 log-prob 由 ref 引擎在每个训练 step 中在线计算,随后拼接到 batch 中供损失函数使用(见 areal/trainer/dpo_trainer.py 中train()循环的ref_logp阶段)。
说明:DPO 属于离线偏好对齐,与仓库中其他在线 RL 算法(GRPO、PPO、M2PO 等)的核心区别在于不需要生成式 rollout——训练数据全部来自预先标注好的偏好对。
二、核心原理:DPO 目标与隐式奖励
2.1 DPO 目标函数
给定偏好数据集 $\mathcal{D} = {(x, y_w, y_l)}$,其中 $y_w$ 为被选中(chosen)的回答,$y_l$ 为被拒绝(rejected)的回答,DPO 优化如下目标:
$$ \mathcal{L}{\text{DPO}}(\pi\theta; \pi_{\text{ref}}) = -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[\log \sigma!\left(\beta \left( \log \frac{\pi\theta(y_w | x)}{\pi_{\text{ref}}(y_w | x)}
- \log \frac{\pi_\theta(y_l | x)}{\pi_{\text{ref}}(y_l | x)} \right)\right)\right] $$
其中 $\pi_\theta$ 是训练中的策略(即配置中的actor),$\pi_{\text{ref}}$ 是冻结的参考模型(即配置中的ref),$\beta$ 控制 KL 惩罚强度。该目标的推导思路是:把 KL 正则化 RLHF 的闭式最优策略代入 Bradley-Terry 偏好模型,从而用策略与参考模型隐式定义奖励,彻底消除了独立奖励模型的需要(文档 28 行)。
2.2 两种损失变体:sigmoid 与 IPO
AReaL 通过loss_type支持两种损失变体:
| 变体 | 默认 | 损失形式 | 来源 |
|---|---|---|---|
sigmoid | ✅ 默认 | 原始 DPO 的负 log-sigmoid 形式 | Rafailov et al. (2023) |
ipo | ❌ | 平方损失,目标为固定间隔 $\frac{1}{2\beta}$(按 token) | Azar et al. (2023) |
其中IPO 变体会在计算平方损失之前,先按补全长度对 log-ratio 做 per-token 平均归一化,这与 TRL 中经作者确认的约定一致(文档 30 行;源码注释也明确写出 "This matches trl's confirmed-with-authors convention",见 areal/trainer/dpo/dpo_engine.py)。这样做的目的是让 $\beta$ 在变长序列之间具有可比性。
2.3 隐式奖励与监控指标
训练过程中,AReaL 会持续监控隐式奖励:
$$r(x, y) = \beta (\log \pi_\theta(y|x) - \log \pi_{\text{ref}}(y|x))$$
- reward margin(奖励间隔):$r(x, y_w) - r(x, y_l)$,为正表示模型正确偏好 chosen 回答;
- reward accuracy(奖励准确率):margin 为正的配对占总配对数的比例。
这两项指标连同损失一起在dpo/前缀下记录,是判断训练是否收敛的关键信号。
三、运行官方示例(HH-RLHF)
3.1 单机运行
AReaL 在 examples/alignment/hhrlhf_dpo.py 中提供了完整的 DPO 训练入口,直接复用仓库的DPOTrainerAPI:
from areal import DPOTrainer from areal.api.cli_args import DPOConfig, load_expr_config from areal.dataset import get_custom_dataset from areal.utils.hf_utils import load_hf_tokenizer def main(args): config, _ = load_expr_config(args, DPOConfig) tokenizer = load_hf_tokenizer(config.tokenizer_path) train_dataset = get_custom_dataset( split=config.train_dataset.split, dataset_config=config.train_dataset, tokenizer=tokenizer, ) valid_dataset = get_custom_dataset( split=config.valid_dataset.split if config.valid_dataset is not None else None, dataset_config=config.valid_dataset, tokenizer=tokenizer, ) with DPOTrainer( config, train_dataset=train_dataset, valid_dataset=valid_dataset ) as trainer: trainer.train()单机启动命令(使用本地 scheduler,无需额外集群依赖):
python3 examples/alignment/hhrlhf_dpo.py \ --config examples/alignment/hhrlhf_dpo.yaml \ scheduler.type=local3.2 完整配置文件解读
examples/alignment/hhrlhf_dpo.yaml 是仓库中完整可用的 DPO 配置。文档中的关键片段如下:
actor: backend: "fsdp:d8p1t1" path: Qwen/Qwen2.5-7B # Follows the original paper: train on a base model beta: 0.1 # KL penalty dtype: bfloat16 disable_dropout: true # Required for DPO stability mb_spec: granularity: 2 # Must be 2: chosen + rejected dispatched as pairs optimizer: lr: 5e-6 lr_scheduler_type: cosine warmup_steps_proportion: 0.1 ref: backend: ${actor.backend} path: ${actor.path} optimizer: null # Frozen scheduling_strategy: type: colocation target: actor # Share GPUs with actor train_dataset: batch_size: 8 path: Anthropic/hh-rlhf type: dpo max_length: 2048实际仓库中的完整 YAML 在文档片段之外还包含更多工程化字段(examples/alignment/hhrlhf_dpo.yaml),与文档片段相互印证:
- 全局字段:
experiment_name: hhrlhf-dpo、seed: 1、total_train_epochs: 1、tokenizer_path: ${actor.path}; - cluster:
n_nodes: 1、n_gpus_per_node: 8、fileroot: /tmp/areal/experiments,name_resolve 使用nfs类型; - scheduler:
type: local(多机场景可改为ray); - actor:额外配置
init_from_scratch: false、gradient_checkpointing: true、mb_spec.max_tokens_per_mb: 4096;optimizer 完整参数为adam+lr: 5e-6、weight_decay: 0.01、beta1: 0.9、beta2: 0.999、eps: 1e-8、lr_scheduler_type: cosine、warmup_steps_proportion: 0.1、min_lr_ratio: 0.1、gradient_clipping: 10.0; - ref:
optimizer: null(冻结)、mb_spec.max_tokens_per_mb: 10240、scheduling_strategy.type: colocation+target: actor(与 actor 共置 GPU); - train_dataset / valid_dataset:
batch_size: 64、path: Anthropic/hh-rlhf、type: dpo、max_length: 2048(完整配置中 batch_size 为 64,文档片段为示意性展示); - saver / recover / evaluator / stats_logger:控制检查点保存、断点恢复、评估与指标日志(stats_logger 默认
wandb.mode: disabled)。
其中几个容易忽略的要点:
actor.path使用基础模型(base model):文档强调"Follows the original paper: train on a base model",DPO 论文本身建议在未经过 SFT 的基础模型上直接训练;disable_dropout: true是 DPO 稳定性要求:禁用 dropout 以保证 log-prob 计算的确定性(对应配置项描述 "Required for DPO stability");actor.mb_spec.granularity: 2必须为 2:因为 chosen 与 rejected 必须作为配对一起分发。源码中 FSDPDPOEngine 在granularity != 2时会打印警告"mb_spec.granularity must be 2 for DPO training";ref.optimizer: null:参考模型完全冻结,不创建任何优化器状态,配合共置策略节省显存。
3.3 多机运行(Ray)
当数据规模超出单机或需要跨节点训练时,切换为 Ray 调度器并指定集群规模:
python3 examples/alignment/hhrlhf_dpo.py \ --config examples/alignment/hhrlhf_dpo.yaml \ cluster.n_nodes=2 cluster.n_gpus_per_node=8 \ cluster.fileroot=/path/to/nfs \ scheduler.type=ray多机场景需要 NFS(cluster.fileroot指向共享存储),因为多节点训练依赖共享文件系统完成权重同步与检查点落盘。
四、数据集管线:HH-RLHF 配对构造
DPO 数据管线由 areal/dataset/hhrlhf.py 中的get_hhrlhf_dpo_dataset实现,并在 areal/dataset/init.py 中通过path包含"hh-rlhf"且type == "dpo"时自动分发。
其核心处理逻辑:
- 对每条样本,将
chosen与rejected文本分别编码(附加eos_token); - 推断 prompt 边界:逐个位置比较 chosen 与 rejected 的 token 序列,找到最长公共前缀长度作为
prompt_len; - 生成
chosen_loss_mask/rejected_loss_mask:prompt 部分置 0,回答部分置 1(只有回答 token 参与 DPO 损失); - 按
max_length过滤掉任一序列超长的样本。
为什么公共前缀就是 prompt?因为 HH-RLHF 的每对数据共享同一个多轮 prompt,仅在最后一条 assistant 回答上不同(文档 77 行明确说明:"HH-RLHF pairs share the same multi-turn prompt and differ only in the final assistant reply, so the common prefix is exactly the prompt")。
在数据进入训练引擎之前,dpo_modeling_collate_fn(定义于DPOTrainer)会把每条样本展开为两个序列字典(chosen 在前、rejected 在后),每个字典包含input_ids、attention_mask与loss_mask,形状为[1, seqlen]。最终 batch 中序列按[chosen_0, rejected_0, chosen_1, rejected_1, ...]顺序交错排布(打包),这正是后续mb_spec.granularity: 2和损失计算中"两两成对"假设的基础。
五、源码级损失实现解析
DPO 的训练引擎位于 areal/trainer/dpo/dpo_engine.py,包含三个层次:DPOEngine(面向引擎的计算封装)、DPOController/DPOControllerV2(训练控制器,负责跨 rank 分发),以及核心的compute_dpo_loss。
5.1 训练主循环中的 ref logp 计算
在DPOTrainer.train()(areal/trainer/dpo_trainer.py)中,每个训练 step 依次执行:
_load_bcast_from(data_generator):加载并广播 batch;ref_logps = self.ref.compute_logp(batch):ref 引擎在torch.no_grad()下计算参考 log-prob,并写回seq_dict["ref_logprobs"](这一阶段被 perf tracer 标记为train.ref_logp,类别COMPUTE);self.actor.train_dpo(batch):actor 引擎执行训练,内部调用compute_dpo_loss;- 随后是
step_lr_scheduler、版本更新、保存、恢复检查点与评估。
ref.compute_logp底层由DPOEngine.compute_logp(@torch.no_grad())执行,通过engine.forward+aggregate_fn=torch.cat把每序列的 log-prob 拼接返回(areal/trainer/dpo/dpo_engine.py)。
5.2 配对 log-ratio 聚合:dpo_pair_logratios
areal/utils/functional/functional.py 中的dpo_pair_logratios负责把打包(packed)batch 还原成配对形式:
- loss_mask 逐序列移位对齐:损失掩码向右移动一位以对齐 next-token log-prob,且每个序列的最后一个 token 位置被强制置 False(该位置不存在 next token);
- 序列级聚合:通过
index_add_按seq_ids累加 masked logprobs,得到每条序列的 log-prob 总和; - fp64 累加:聚合使用
float64精度,避免长序列(约 2k+ token)下 fp32 累加精度损失导致 log-ratio 符号翻转(源码注释明确提到 "Aggregation uses fp64 scatter-add to avoid precision loss on long (~2k+ tok) pairs where fp32 accumulation can flip the log-ratio sign"); - 返回形状:
(policy_logps, ref_logps, completion_lens),每个都是(K, 2),列 0 为 chosen、列 1 为 rejected,K为有效配对数;completion_lens供 IPO 做 per-token 归一化。
配对的有效性由_dpo_valid_pairs判定:把cu_seqlens换算成序列长度后按每 2 条一组view(-1, 2)检查是否存在空序列;无效配对会被过滤掉,若全部无效则记录空统计并返回 0 损失(见compute_dpo_loss的 early-return 分支)。
5.3 损失函数:compute_dpo_loss
核心损失函数compute_dpo_loss(areal/trainer/dpo/dpo_engine.py)接收 policy 的logprobs与 batch 中的ref_logprobs,按loss_type分流:
- sigmoid(默认):
logits = (policy_chosen - policy_rejected) - (ref_chosen - ref_rejected),即配对级 log-ratio 差; - ipo:先按补全长度做 per-token 平均(
chosen_avg - rejected_avg),再进入平方损失。
随后调用 dpo_preference_loss:
if loss_type == "sigmoid": return -torch.nn.functional.logsigmoid(beta * logits.float()) if loss_type == "ipo": return (logits.float() - 1.0 / (2.0 * beta)) ** 2并在torch.no_grad()下同步计算监控统计量(n_pairs作为 denominator):
chosen_reward = beta * (policy_logps_chosen - ref_logps_chosen)rejected_reward(同理)reward_accuracy = (chosen_rewards > rejected_rewards).float()reward_margin = chosen_rewards - rejected_rewards
最终返回per_pair_loss.mean()。此外,loss_weight_fn=_dpo_loss_weight用有效配对数作为 batch 的 loss 权重,确保不同批次间 loss 尺度一致。
5.4 工程细节:shift 对齐与 IPO 归一化
tests/test_dpo.py中有两组针对性回归测试,可以直接印证上述实现细节:
- TestDPOLossIntraSequenceShift:验证 loss_mask 的逐序列移位(而非全局
torch.roll),确保 chosen 最后一个回答 token 不会泄漏污染下一条序列的 prompt 位置,且每条序列最后一个位置(无 next token)永不计入损失(对应测试test_chosen_last_response_token_does_not_leak_into_next_prompt与test_last_token_of_sequence_is_never_counted); - TestDPOLossIPO:验证 IPO 的 per-token 长度归一化——两条 per-token 平均相同但长度不同的配对应产生相同损失(
test_ipo_length_normalization);以及当 per-token logits 恰好等于1/(2*beta)时损失为 0(test_ipo_loss_zero_at_target)。
这两组测试分别证明了 sigmoid/IPO 损失行为、beta 缩放、ref logprobs 的抵消作用、loss_mask 只覆盖回答 token、空配对处理、无效loss_type报错等全部边界条件。
六、关键参数速查表
| 参数 | 默认值 | 说明 |
|---|---|---|
actor.beta | 0.1 | KL 惩罚系数。值越大越贴近参考模型。推荐范围 0.05–0.5。 |
actor.loss_type | "sigmoid" | 损失变体。"sigmoid"为原始 DPO;"ipo"使用 per-token 平均平方损失(Azar et al. 2023)。合法取值由 DPOEngineConfig 校验,非法值会抛ValueError。 |
actor.optimizer.lr | 5e-6 | 学习率。DPO 对 LR 敏感,5e-7 – 5e-6 是经验甜点区间。 |
actor.disable_dropout | true | 关闭 dropout,保证 log-prob 计算确定性。 |
actor.mb_spec.granularity | 2 | 微批次粒度。DPO 必须为 2(chosen + rejected 成对分发)。 |
ref | — | 参考模型配置(必需)。optimizer: null表示冻结;建议与 actor 共置以共享 GPU。 |
train_dataset.type | — | 必须为dpo,触发偏好配对数据管线。 |
train_dataset.max_length | 2048 | 序列最大长度,超长样本会被过滤。 |
上述默认值同时见于配置文档与 areal/api/cli_args.py 中beta、loss_type字段的 dataclass 默认定义。
七、训练监控:dpo/ 指标族
训练与评估过程中,以下指标在dpo/前缀下记录(来源见DPOEngine的stats_tracker.scope_func_wrapper("dpo")与compute_dpo_loss内的统计逻辑):
| 指标 | 含义 |
|---|---|
dpo/loss | 当前 step 的平均 DPO 损失(sigmoid 或 IPO) |
dpo/chosen_reward | 隐式奖励 $r(x, y_w)$ |
dpo/rejected_reward | 隐式奖励 $r(x, y_l)$ |
dpo/reward_accuracy | chosen 奖励高于 rejected 的配对占比 |
dpo/reward_margin | $r(x, y_w) - r(x, y_l)$ 的平均间隔 |
评估阶段同样使用compute_dpo_loss(走dpo-evalscope),因此验证集上也会产出相同的dpo/指标。结合stats_logger(支持 wandb/tensorboard,示例配置中默认wandb.mode: disabled)即可在训练曲线中观察 reward accuracy 与 margin 的上升趋势,判断模型是否正确学会了偏好排序。
八、扩展阅读与参考
- 算法文档主页:docs/en/algorithms/dpo.md
- DPO 训练入口与完整配置:examples/alignment/hhrlhf_dpo.py、examples/alignment/hhrlhf_dpo.yaml
- 训练器与损失实现:areal/trainer/dpo_trainer.py、areal/trainer/dpo/dpo_engine.py
- 损失工具函数:areal/utils/functional/functional.py
- 数据管线:areal/dataset/hhrlhf.py
- 配置定义与校验:areal/api/cli_args.py
- 单元测试:tests/test_dpo.py
参考资料(对应文档 References 节):
- Rafailov et al. (2023).Direct Preference Optimization: Your Language Model is Secretly a Reward Model. NeurIPS 2023.
- Azar et al. (2023).A General Theoretical Paradigm to Understand Learning from Human Feedback.
- Anthropic HH-RLHF 数据集(
Anthropic/hh-rlhf)。
【免费下载链接】AReaLThe RL Bridge for LLM-based Agent Applications. Made Simple & Flexible.项目地址: https://gitcode.com/GitHub_Trending/are/AReaL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考