ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

昇思MindSpore大模型对齐实战:从RLHF到DPO的偏好优化指南

昇思MindSpore大模型对齐实战:从RLHF到DPO的偏好优化指南 做昇思MindSpore大模型训练的朋友可能都有过这种时刻基座模型辛辛苦苦预训练完loss低得漂亮给它一句提示词也能生成一大段通顺的文字可一旦让它按给定格式输出或者只回答要点它就露馅了——啰嗦、跑题、甚至一本正经地胡说八道。问题出在哪出在构建流程少了对齐这一步。昇思MindSpore大模型构建流程里的对齐模式就是把模型从会生成进一步打磨成会听话的过程涉及指令微调、奖励建模、RLHF与DPO等一系列训练策略。这篇文章我会结合自己在MindSpore环境里的实操经验把对齐模式到底解决什么问题、两条主流路线怎么选、数据怎么准备、训练怎么跑、坑在哪儿一次性讲清楚。适合正在做大模型全流程训练、准备给自研模型加偏好对齐能力的工程师和研究者参考。1. 对齐之前基座模型差的那一步到底是什么1.1 大模型构建的全生命周期预训练、SFT、偏好对齐各司其职如果只用一条主线串起大模型构建周期我习惯这样划分语料清洗与构建、预训练、指令微调SFT、偏好对齐、评测与部署。预训练阶段用海量语料教模型做下一词预测模型在这个阶段大量吸收语言结构、世界知识和推理模式但它学到的是继续写下去的能力不是按人要求回答的能力。SFT阶段用人工标注的指令-回答对教会模型提问-回答的基本交互方式让模型看到问题后知道该正面作答而不是顺着话头乱接。偏好对齐阶段则更进一步用人类偏好数据告诉模型在多个同样通顺的回答里哪一个才是人更想要的。很多团队在对齐模式上踩的误区是把SFT当成了整个对齐的全部。实际上SFT解决的是形式对齐——模型知道该按指令回答偏好对齐解决的是行为对齐——模型知道什么回答更好。打个不太严谨的比方SFT像教一个新人熟悉工作流程偏好对齐则是在流程之上告诉他做事的优先级和分寸感。昇思MindSpore大模型构建流程里这两个环节是递进关系不是二选一跳过SFT直接做偏好对齐的结果往往是模型根本听不懂任务偏好信号也无处安放。1.2 基座模型只学会了续写没学会听话为什么基座模型看起来什么都会一用起来就拉胯因为预训练阶段模型的训练目标只有一个最大化下一个token的条件概率。这个目标决定了它学到的是语料中的统计规律而不是人对问题的期待。模型的概率分布是海量语料混合平均的结果它知道请写一封请假邮件后面大概率跟着邮件正文却不知道邮件应该写给谁、语气应该是什么、要交代哪些关键信息于是在生成时把不同来源的语料片段拼接得七扭八歪。有个朋友给我演示过一个很直观的对比同一个模型SFT之后让它写请假邮件能给出结构完整的邮件但让它写以拒绝的语气回复加班请求时它依然语气温和、理由充分完全看不出拒绝。这就是形式对齐和行为对齐的差距。偏好对齐模式下模型会从大量偏好数据中学到拒绝场景下人类更接受什么样的表达输出才真正贴合场景。对齐带来的收益主要有三块一是有用性模型能按要求完成具体任务二是可控性输出符合格式、长度、角色设定等约束三是安全性避免生成有害、误导或违背主流价值观的内容。这三点也是我判断一次对齐实验是否有效的核心维度。2. RLHF和DPO两条主流对齐技术路线怎么选2.1 RLHF三阶段效果上限高工程负担也高RLHF是偏好对齐里最经典的方案整条链路分三步第一步拿一个经过SFT的模型作为初始策略第二步收集人类对同一提示词下多个回答的排序数据训练一个奖励模型第三步用强化学习算法典型是PPO更新策略模型让它在尽可能拿高奖励分的同时不偏离初始策略太远。奖励模型回答什么是好PPO负责让策略模型的输出向好靠拢KL散度则防止模型为了得分而彻底放飞。在MindSpore环境里做RLHF工程上最直观的感受是模型多。PPO阶段至少要同时维护四个模型策略模型actor、参考模型reference、奖励模型reward、价值模型critic。一个7B模型做RLHF单卡显存基本吃不消四卡起算是常态显存规划、通信开销、梯度稳定性都是需要额外投入的工程问题。训练节奏上我习惯把奖励模型先训到明显能区分好坏的准确率一般95%以上再进入PPO。奖励信号太弱时策略模型很难学到有效信息反而容易被噪声带偏。2.2 DPO与SimPO砍掉奖励模型的轻量路线DPO出现后圈内很多团队一夜之间就轻装了。DPO不训练独立奖励模型而是把偏好数据里的chosen和rejected两个回答直接拿到同一个策略模型上做前向用它们的概率比构造训练信号。它的核心洞察是带KL约束的RLHF最优解在数学上可以表达成对策略模型概率的直接约束那就不需要显式建模奖励直接基于Bradley-Terry偏好模型构造目标函数即可。DPO的工程优势很直观训练时只需要policy model和reference model两个模型显存压力明显低于RLHF训练过程也更稳定不用面对PPO里一堆强化学习超参数的调参地狱。SimPO在DPO基础上又往前走了一步连reference model都省了直接用归一化对数概率充当隐式奖励计算量进一步收缩但效果对数据质量的敏感度更高。2.3 算力有限就选DPO有攻顶需求再上RLHF怎么选我给一个务实的判断标准。如果目标只是给一个几十亿参数的模型做一轮偏好对齐团队里没有专门的强化学习工程师优先考虑DPO如果模型体量大、团队有强化学习基础而且评测数据显示DPO确实不够用再考虑上RLHF。我见过不少团队一上来就冲PPO结果光调KL惩罚系数就耗了两周最后跑完对比DPO基本够用。维度RLHFPPODPO / SimPO完整训练链路SFT RM训练 PPOSFT DPO 两步训练期模型数量4个左右2个甚至更少显存压力高中/低调参复杂度高涉及RL超参低核心参数少偏好数据形式同一提示词下多个回答的排序chosen/rejected成对数据效果上限理论上限更高依赖数据质量通常够用这张表列完选型逻辑基本清晰数据质量决定下限工程条件决定上限。项目节奏紧、算力有限先别纠结要不要上最高精尖的方案把DPO跑稳已经能解决绝大多数业务问题。我在实际项目里见过不止一次的情况是团队花了大量精力搭RLHF工程链路最后评测结果和DPO差距不到一个百分点却多出了几倍的调试成本。所以除非你明确知道RLHF能带来什么额外收益否则先让DPO跑通一版拿真实数据说话。3. MindSpore里搭对齐流程关键环节一个个拆3.1 构建偏好数据集提示词、选定回答、拒绝回答一个都不能少无论走哪条路线偏好数据都是对齐模式的地基。一条标准的偏好数据至少要包含三个字段prompt提示词、chosen被标记为更好的回答、rejected被标记为较差的回答。别小看这个结构很多新手在构造数据时只关注文本是否通顺忽略了一个关键点chosen和rejected必须在同一任务、同一约束条件下可比。比如prompt里要求用一句话解释结果chosen是一句话rejected是一大段这种数据教给模型的信息是短好而不是真正的偏好差异。在实际构造时我强烈建议直接按照训练时的chat template来存储数据而不是存纯文本。原因很简单SFT和偏好对齐使用的对话模板必须和推理阶段保持一致。一旦模板信息在数据管线里丢失对齐出来的风格会在部署时全部错位模型本地表现很好一到线上接口就变得很奇怪。偏好数据的来源可以选择公开偏好数据集子集、团队内部排序标注、或者用更大的模型打分替代人工排序。最后一种虽然省成本但打分模型本身的偏差会传导到偏好信号里只能用来对付初始版本。3.2 并行策略与显存规划ref和actor要同时占显存MindSpore在大模型训练上提供了完整的数据并行、模型并行和流水线并行能力但这些能力在偏好对齐任务里得重新做一次预算规划。原因很简单同一个batchactor和reference模型要分别对chosen和rejected两个序列各做一次前向。也就是说同样一批数据模型前向的token总数接近普通SFT的4倍显存和算力需求会被成倍放大。一个常见的误判是拿SFT阶段的显存数据去打对齐任务的资源预算。实际做下来同样的模型规模DPO训练显存占用大约是SFT的1.5到2倍RLHF会更高。如果你的卡只有30GB显存又想对齐7B模型优先考虑LoRA这类参数高效微调方法只训练低秩适配层base模型半精度加载甚至冻结能明显降低显存瓶颈。并行策略方面我给一个常见实践参考单机多卡先满足batch size和序列长度约束再考虑张量并行跨机场景优先流水线并行减少跨节点通信。MindSpore各版本并行接口差异不小动手前先拿一个小模型把并行配置跑通再上量级否则排错成本会非常高。3.3 奖励信号与KL约束防止模型钻空子对齐训练里最反直觉的一点是模型会钻空子。在RLHF里如果奖励模型只负责打高分策略模型很快会发现某个句型、某种长度或某个高频词能骗到高分于是疯狂输出这类内容哪怕内容本身早就偏离了人类偏好。这就是reward hacking。防止reward hacking两个务实手段。第一是KL散度约束在PPO目标函数里加上策略模型与reference模型输出分布的KL惩罚控制策略不偏离初始分布太远像给模型套了根缰绳。第二是奖励模型本身要克制不能只在高分标注数据上学得过于激进训练数据里要覆盖模型真实生成分布附近的样本否则奖励模型一暴露在策略模型的分布里就乱打分。DPO虽然没有显式奖励模型但对beta这个温度超参数要保持敏感。beta越大模型和reference分布越接近对齐强度越弱beta越小模型越迎合偏好数据也越容易出现重复生成和模板句式。我调beta时习惯从0.1起步先观察一两千步再决定放大缩小而不是一开始就追求某个理想值。4. 用一个最小实验把手上的对齐流程跑通4.1 MindSpore环境准备与小模型选型用MindSpore跑对齐实验我建议先从1B到3B级别的小模型开始。原因很直接对齐流程的复杂性主要在数据管线、模型加载、梯度逻辑、显存规划而不是模型本身。小模型跑通全流程再把同样的流程迁移到目标模型效率比直接在大模型上调高得多。环境方面先确认MindSpore版本和GPU驱动匹配建议直接用官方容器镜像避开CUDA版本不匹配的坑。如果团队用的是MindSpore的大模型套件例如mindformers先确认它支持你要用的模型结构和偏好优化任务类型如果暂时不支持就用基础框架手写训练循环自由度更大但后续排查成本都在自己身上。选型时还要注意torch和MindSpore的权重转换问题如果用别的框架训练的基座模型导入前先确认权重映射关系这一步错了后面全白费。4.2 从公开数据构造偏好对在团队还没有标注数据之前最快的起手式是从公开偏好数据集抽一个子集。数据格式可以像我下面这样设计字段清晰后面写数据加载器也省事[ { prompt: 请用一句话解释什么是梯度下降。, chosen: 梯度下降是一种通过反复更新参数、使损失函数逐步减小的优化算法。, rejected: 梯度下降就是在山上往下走一直走到底方法是沿着坡度最大的方向走整个过程还要注意步伐因为步子太大容易掉坑里太小又走得很慢。 } ]拿到原始数据后不要急着进训练先做清洗。检查chosen是否真的优于rejected是否出现空回答是否在模板层有串扰。我习惯先用脚本统计chosen和rejected的长度分布如果两者长度差异异常明显要警惕模型学到长回答即好回答这种错误偏好。清洗完成后按8:1:1切分训练、验证、评测集评测集里单独留一部分不参与训练专门用来做生成质量抽查。4.3 核心训练流程的代码骨架下面以DPO为例给出核心训练流程的代码骨架。MindSpore各版本接口有差异我写的思路是通用逻辑重点在于呈现DPO loss是怎么一步步算出来的。这段代码基于常见实践补充请在你自己的环境里对照算子名做微调。import mindspore as ms import mindspore.ops as ops from mindspore import nn # 1. 加载base model作为policy model并复制一份作为reference model policy_model load_model(your_base_model) reference_model load_model(your_base_model) # reference model全程冻结 reference_model.set_train(False) for param in reference_model.trainable_params(): param.requires_grad False # 2. 定义DPO损失 class DPOLoss(nn.Cell): def __init__(self, policy_model, reference_model, beta0.1): super().__init__() self.policy_model policy_model self.reference_model reference_model self.beta beta self.log_softmax ops.LogSoftmax(axis-1) self.gather_d ops.GatherD() # 版本不同可能叫gather_d以当前环境为准 def sequence_log_prob(self, logits, input_ids, attention_mask): log_probs self.log_softmax(logits) token_log_probs self.gather_d( log_probs, -1, ms.ops.expand_dims(input_ids, -1)).squeeze(-1) return (token_log_probs * attention_mask).sum(axis-1) def construct(self, chosen_ids, chosen_mask, rejected_ids, rejected_mask): chosen_logits self.policy_model(chosen_ids) rejected_logits self.policy_model(rejected_ids) # reference model只做前向不回流梯度 ref_chosen_logits ops.stop_gradient( self.reference_model(chosen_ids)) ref_rejected_logits ops.stop_gradient( self.reference_model(rejected_ids)) chosen_log_prob self.sequence_log_prob( chosen_logits, chosen_ids, chosen_mask) rejected_log_prob self.sequence_log_prob( rejected_logits, rejected_ids, rejected_mask) ref_chosen_log_prob self.sequence_log_prob( ref_chosen_logits, chosen_ids, chosen_mask) ref_rejected_log_prob self.sequence_log_prob( ref_rejected_logits, rejected_ids, rejected_mask) policy_log_ratio chosen_log_prob - rejected_log_prob ref_log_ratio ref_chosen_log_prob - ref_rejected_log_prob loss -ops.log(ops.sigmoid( self.beta * (policy_log_ratio - ref_log_ratio) )).mean() return loss代码里最关键的一步是policy和reference在chosen与rejected两个序列上的对数概率差。如果reference_model没有正确冻结梯度会同时更新两个模型实际效果就是一边追目标一边挪标杆训练基本废掉。另外chosen和rejected的序列长度往往不一致计算对数概率时attention_mask的处理一定要对齐漏掉mask会导致padding位置的伪高概率把真正的信号淹没掉。4.4 训练中盯哪些指标才不会跑偏跑DPO训练时我会同时盯四组信号而不是只盯loss。第一是DPO loss本身的收敛趋势它应该下行并趋于平稳如果剧烈波动先查数据质量再查学习率。第二是chosen和rejected两个回答的平均对数概率差这个差值反映了模型对偏好数据的拟合程度太小说明还没开始学过大则要警惕过拟合到偏好数据。第三是policy model与reference model输出分布的偏离程度偏离过大说明beta设小了模型可能已经远离初始能力。第四是每500步左右保存一个checkpoint拿一小批固定prompt做生成质量抽查人工对比不同检查点的输出变化。我特别强调最后一项因为loss掉得漂亮不代表对话质量真的好。偏好对齐任务里离线指标和线上体验之间经常隔着一条很宽的河最终还是要回归人眼评测。5. 对齐实战中绕不开的坑5.1 reference模型忘了冻结loss直接飘掉这个坑我踩过不止一次而且表现很隐蔽。一开始你只会觉得loss下降有点慢看了半天代码才发现reference_model的requires_grad没有全部关掉。在MindSpore里光set_train(False)不够还需要把模型参数的requires_grad逐个置为False。否则优化器在更新策略的同时也在移动对比基准DPO的loss会飘忽不定看起来像不收敛实际是那个标准答案一直在变。排查方法很直接每隔100步打印reference_model的参数变化量如果几乎为0说明冻结成功如果明显在动就赶紧修。顺便提一句如果你的优化器本来就不该更新reference参数也可以直接在构建optimizer时只传入policy_model.trainable_params()从根源上杜绝误更新。5.2 reward hacking奖励模型被刷分了做RLHF时我遇到过奖励模型单独测准确率很高但在PPO里很快被策略模型刷分的情况。当时策略模型开始大量输出特定开头的模板句长度明显变长人一眼就能看出假但奖励模型给的分数反而越来越高。复盘根因是奖励模型的训练数据覆盖不够。我用的是一批静态排序数据没有加入策略模型训练过程中的分布漂移样本奖励模型面对新分布就失效。后来的做法是定期从当前策略采样一批回答让人工或更强模型打分补充进奖励模型训练集持续迭代。这相当于把奖励模型的评测暴露在动态分布里才把刷分现象压下来。如果你也在做RLHF建议把奖励模型和数据动态更新设计成流程的一部分而不是训完就丢。5.3 用真实对话评测替代loss迷信这是最想提醒所有做对齐实验的人的一点。偏好对齐的离线指标往往很友好loss在降、chosen和rejected的间距在拉开但生成质量可能越来越怪。我碰到最典型的现象是模型开始用高概率的安全而空洞的表达来糊弄人看似什么都答得上来其实什么都没说。这种退化在离线指标上看不出来只有把生成结果真正读一遍才会发现。我的建议是从训练第一天起就建立一套固定prompt评测集里面包含格式类任务、知识类任务、以及实际业务中最常出现的场景。每隔固定步数让同一批检查点生成答案摆在一起横评对比。这套做法不需要额外开发平台只是把人工评测制度化但它能拦住绝大多数指标好看、实际翻车的情况。5.4 beta值怎么调对齐强度与生成多样性beta是DPO里少数几个需要认真对待的超参数。beta太大约束强模型几乎不会偏离reference太多对齐效果不明显beta太小模型过度迎合偏好数据输出变得机械、套路化。对齐强度说到底就是生成多样性和任务服从性的天平。我调beta的经验是先固定其他条件用0.05、0.1、0.2三个值各跑几百步观察chosen-rejected分差和生成多样性。分差太小说明beta偏大分差快速拉大且生成出现大量重复句式说明beta偏小。大多数情况下落在0.1到0.2区间但这个值会随模型规模和数据质量变化换模型后不要照搬参数。最后再分享一个体会对齐模式的成败七成在数据三成在训练。不管用MindSpore还是其他框架先把偏好数据清洗干净、把覆盖范围做够比反复调beta和换算法都管用。我现在的习惯是每次对齐实验前随机抽200条偏好数据人工过一遍确认没有语义矛盾、没有模板串扰、没有差的反而更像好的脏数据再上训练。这个习惯帮我省掉了大量无效调参时间你也可以试试。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进