AReaL 中 DPO 离线对齐算法实践:从 HH-RLHF 示例到源码级损失实现解析
2026/9/17 11:31:27 网站建设 项目流程

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 双模型共置训练架构、sigmoidIPO两种损失变体、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=local

3.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-dposeed: 1total_train_epochs: 1tokenizer_path: ${actor.path}
  • clustern_nodes: 1n_gpus_per_node: 8fileroot: /tmp/areal/experiments,name_resolve 使用nfs类型;
  • schedulertype: local(多机场景可改为ray);
  • actor:额外配置init_from_scratch: falsegradient_checkpointing: truemb_spec.max_tokens_per_mb: 4096;optimizer 完整参数为adam+lr: 5e-6weight_decay: 0.01beta1: 0.9beta2: 0.999eps: 1e-8lr_scheduler_type: cosinewarmup_steps_proportion: 0.1min_lr_ratio: 0.1gradient_clipping: 10.0
  • refoptimizer: null(冻结)、mb_spec.max_tokens_per_mb: 10240scheduling_strategy.type: colocation+target: actor(与 actor 共置 GPU);
  • train_dataset / valid_datasetbatch_size: 64path: Anthropic/hh-rlhftype: dpomax_length: 2048(完整配置中 batch_size 为 64,文档片段为示意性展示);
  • saver / recover / evaluator / stats_logger:控制检查点保存、断点恢复、评估与指标日志(stats_logger 默认wandb.mode: disabled)。

其中几个容易忽略的要点:

  1. actor.path使用基础模型(base model):文档强调"Follows the original paper: train on a base model",DPO 论文本身建议在未经过 SFT 的基础模型上直接训练;
  2. disable_dropout: true是 DPO 稳定性要求:禁用 dropout 以保证 log-prob 计算的确定性(对应配置项描述 "Required for DPO stability");
  3. actor.mb_spec.granularity: 2必须为 2:因为 chosen 与 rejected 必须作为配对一起分发。源码中 FSDPDPOEngine 在granularity != 2时会打印警告"mb_spec.granularity must be 2 for DPO training"
  4. 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"时自动分发。

其核心处理逻辑:

  1. 对每条样本,将chosenrejected文本分别编码(附加eos_token);
  2. 推断 prompt 边界:逐个位置比较 chosen 与 rejected 的 token 序列,找到最长公共前缀长度作为prompt_len
  3. 生成chosen_loss_mask/rejected_loss_mask:prompt 部分置 0,回答部分置 1(只有回答 token 参与 DPO 损失);
  4. 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_idsattention_maskloss_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 依次执行:

  1. _load_bcast_from(data_generator):加载并广播 batch;
  2. ref_logps = self.ref.compute_logp(batch):ref 引擎在torch.no_grad()下计算参考 log-prob,并写回seq_dict["ref_logprobs"](这一阶段被 perf tracer 标记为train.ref_logp,类别COMPUTE);
  3. self.actor.train_dpo(batch):actor 引擎执行训练,内部调用compute_dpo_loss
  4. 随后是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_prompttest_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.beta0.1KL 惩罚系数。值越大越贴近参考模型。推荐范围 0.05–0.5。
actor.loss_type"sigmoid"损失变体。"sigmoid"为原始 DPO;"ipo"使用 per-token 平均平方损失(Azar et al. 2023)。合法取值由 DPOEngineConfig 校验,非法值会抛ValueError
actor.optimizer.lr5e-6学习率。DPO 对 LR 敏感,5e-7 – 5e-6 是经验甜点区间。
actor.disable_dropouttrue关闭 dropout,保证 log-prob 计算确定性。
actor.mb_spec.granularity2微批次粒度。DPO 必须为 2(chosen + rejected 成对分发)。
ref参考模型配置(必需)。optimizer: null表示冻结;建议与 actor 共置以共享 GPU。
train_dataset.type必须为dpo,触发偏好配对数据管线。
train_dataset.max_length2048序列最大长度,超长样本会被过滤。

上述默认值同时见于配置文档与 areal/api/cli_args.py 中betaloss_type字段的 dataclass 默认定义。

七、训练监控:dpo/ 指标族

训练与评估过程中,以下指标在dpo/前缀下记录(来源见DPOEnginestats_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_accuracychosen 奖励高于 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询