资讯中心

GRPO、PPO与DPO:大模型微调对齐算法工程选型指南

📅 2026/7/4 12:02:35
GRPO、PPO与DPO:大模型微调对齐算法工程选型指南
1. 项目概述为什么这三个算法正在重新定义大模型微调的实操边界如果你最近在跑LLM微调实验大概率已经撞上过这个困惑明明用的是同一份高质量指令数据换一个对齐算法——从PPO换成DPO或者试了GRPO之后——模型在真实对话中的“听话程度”、拒绝幻觉的稳定性、甚至生成长度的一致性都会发生肉眼可见的变化。这不是玄学而是三种不同优化范式在梯度流、奖励建模、策略更新节奏上的根本性差异所致。GRPO、PPO、DPO这三个缩写如今已不是论文里的抽象符号而是工程师每天在训练日志里盯着loss曲线、KL散度和reward margin反复调试的具体对象。它们分别代表Gradient Regularized Policy Optimization梯度正则化策略优化、Proximal Policy Optimization近端策略优化和Direct Preference Optimization直接偏好优化。本篇不讲公式推导只讲我在真实业务场景中——比如为客服对话系统定制安全响应模块、为法律文书助手强化事实核查能力、为教育类Agent提升多步推理连贯性——如何根据数据质量、算力预算、上线时效这三根硬约束选型、落地、调参、排障。你不需要是强化学习博士但需要知道当你的标注数据只有200条高质量pair时DPO可能比PPO收敛快3倍当你必须控制输出token分布不漂移时GRPO内置的梯度裁剪机制能省掉你两天的手动KL惩罚调试而当你面对的是带噪声的用户反馈比如部分标注员把“有礼貌但答非所问”误标为“优秀回复”PPO的critic网络反而比DPO的logit margin更鲁棒。下面我会用真实训练日志截图文字还原、参数配置表、loss变化曲线解读带你一层层剥开这三个算法在工程侧的真实表现。2. 核心思路拆解为什么不是“哪个更好”而是“在哪种条件下谁更稳”2.1 问题本质对齐不是目标而是约束下的行为塑形很多人把“LLM对齐”理解成让模型“更听人话”这太模糊。实际工程中对齐是在有限资源下以最小代价让模型输出满足一组可验证的行为约束。这些约束包括安全性约束拒绝回答政治敏感问题、不生成违法内容事实性约束引用文档时不能编造页码、日期、法条编号风格约束客服回复必须带“您好/感谢/祝您”等固定起手与收尾结构约束法律分析必须分“事实→依据→结论”三段且每段不超过80字。PPO、DPO、GRPO的本质区别就在于它们把上述约束“翻译”成可优化目标的方式完全不同。PPO走的是“强化学习老路”先训一个reward modelRM打分再用PPO更新policy过程中靠KL penalty防止policy偏离SFT基线太远。DPO跳过了RM训练直接用偏好对chosen/rejected计算log odds ratio把偏好学习变成一个分类任务。GRPO则是PPO的轻量改造版它不引入额外的critic网络而是把KL penalty项显式地加进梯度更新公式并用梯度范数作为正则强度的自适应调节器。我画了个对比表格不是为了炫技而是为了让你一眼看清选型逻辑维度PPODPOGRPO依赖组件必须训RM critic网络只需偏好对数据无需RM需SFT基线模型无需RM/criticGPU显存占用A100 80G最高RMcriticpolicy三网络并行最低仅policy前向反向中等policy梯度正则计算单步训练耗时batch641.8s含RM inference0.45s0.62s对噪声标注的容忍度高critic可平滑RM打分波动低直接拟合pair噪声会放大margin误差中梯度正则抑制极端更新KL散度控制精度依赖β超参手动调易过拟合或欠约束固定隐含在β中但β与KL无直接映射梯度范数动态调节KL曲线更平滑提示这张表的数据来自我团队在Llama-3-8B上跑的12轮消融实验所有测试均用相同数据集2000条法律问答偏好对、相同硬件单卡A100、相同tokenizerLlama tokenizer。注意“单步耗时”不含数据加载只计forwardbackwardoptimizer.step。为什么强调“约束下的行为塑形”因为很多团队失败不是算法选错了而是没想清楚自己真正要约束什么。比如做医疗问答助手核心约束是“不给出诊断建议”而不是“回答更友好”。这时DPO容易翻车——如果偏好对里有几条“医生说‘建议去三甲医院’被标为rejected”DPO会直接学出“永远不说任何建议类词汇”导致连“建议多喝水”都拒答。而PPO的critic可以学到“建议具体医疗动作高风险建议生活常识低风险”这种细粒度区分是DPO做不到的。GRPO则折中它用梯度正则压制policy在高风险token上的logit突变但保留对低风险token的灵活生成能力。所以我的第一条经验是先白板写出你要约束的3条最硬规则再对照上表选算法而不是看论文benchmark选。2.2 算法选型决策树一张图解决90%的纠结我们内部用这张决策树指导所有新项目启动。它不追求理论完美只解决“今天下午三点前必须跑通第一轮”的现实问题是否已有高质量偏好对chosen/rejected pair且数量≥5000条 ├─ 是 → 检查标注一致性随机抽100对让3个标注员重标Krippendorff’s α ≥ 0.8 │ ├─ 是 → DPO收敛快、显存省、结果稳定 │ └─ 否 → GRPO梯度正则能缓解标注噪声 └─ 否 → 是否能接受额外训一个RM模型需2000条打分数据 ├─ 是 → PPO对复杂约束泛化好适合多目标平衡 └─ 否 → 先用SFTRule-based Post-processing别硬上RLHF这个树的每个分支都有血泪教训。比如“标注一致性”那关我们曾在一个金融问答项目里栽过标注员把“年化收益率4.5%”标为“优秀”但把“预期年化收益4.5%”标为“一般”只因前者用了肯定语气。Krippendorff’s α算出来才0.52强行上DPO后模型学会了回避所有带“预期”“可能”“大概”的模糊表述结果在需要概率表达的场景如风险提示完全失效。后来我们花两天重做标注指南加入“模糊词不扣分事实错误才扣分”的明确定义α升到0.87DPO才跑出理想效果。再比如“是否能接受训RM”很多团队低估了RM的训练成本。RM不是训个分类器那么简单——它需要和policy共享大部分transformer层否则RM打分和policy生成脱节我们实测发现用独立小模型训RMPPO reward loss下降但response quality反而变差因为RM学的是表面pattern而非真实偏好。最终方案是用policy的前12层当RM backbone只加一个head这样RM inference和policy forward能复用大部分显存显存占用从22G压到14G。2.3 工程视角的三大误区别让教科书害了你的迭代速度误区一“DPO不用训RM所以一定比PPO快”。错。DPO的“快”只体现在单步训练但它的超参更敏感。DPO的核心超参βimplicit KL coefficient没有物理意义调β0.1和β0.5loss curve看起来差不多但生成质量天壤之别。我们试过网格搜索β∈[0.1, 2.0]步长0.1发现只有β0.32和β0.41两个点能通过人工评测100条测试集3人盲评。而PPO的β虽然也要调但它的KL penalty是显式的loss里直接有KL term你可以实时监控loss_kl值只要它稳定在0.15±0.03基本就稳了。GRPO的梯度正则系数λ更智能我们设λ0.01但代码里实际用λ * (grad_norm / grad_norm_ref)其中grad_norm_ref是SFT阶段的平均梯度范数这样λ自动适配不同层的梯度尺度调参工作量直接砍半。误区二“PPO必须用大batch才能训好”。这是早期OpenAI实现的遗留认知。我们用batch32micro-batch8, gradient_accumulation4在Llama-3-8B上跑PPO只要把critic learning rate设为policy的0.5倍比如policy用1e-6critic用5e-7并用EMA更新critic target networkdecay0.995reward loss一样能稳定下降。关键不是batch size而是critic和policy的学习节奏要错开。就像两个人抬杠子不能同时发力否则杠子会抖。我们监控过梯度直方图当critic lr太高时critic loss骤降但policy reward variance暴增当critic lr太低时critic跟不上policy变化reward信号延迟超过3步policy就开始胡乱探索。这个节奏感只能靠watch loss_kl、loss_reward、reward_mean三条曲线的相位关系来把握。误区三“GRPO是PPO的简化版所以效果一定不如PPO”。大错特错。GRPO在两类场景下反超PPO一是低资源场景单卡A100显存≤40GGRPO省掉critic网络能把max_seq_len从512拉到1024这对长文档摘要类任务是质变二是需要强KL控制的场景比如让模型严格遵循模板“请按以下格式回答【结论】...【依据】...【建议】...”GRPO的梯度正则能精准压制模型在【结论】后乱加解释的冲动而PPO的KL penalty是全局的容易把【依据】部分也压得过于简略。我们有个真实案例法律合同审查Agent要求输出必须含“【风险点】”“【修改建议】”“【依据条款】”三块。PPO训出来经常漏【依据条款】因为KL penalty让模型“怕写多”GRPO训出来三块齐全率92.3%比PPO高11.7个百分点。3. 实操细节解析从数据准备到上线部署的全链路踩坑记录3.1 数据准备不是越多越好而是越“准”越省事所有算法的第一道生死线是数据。但“准”不等于“完美”而是标注逻辑与业务约束严格对齐。我们不再用“好/坏”这种模糊标签而是定义原子化标注维度维度定义标注方式示例事实性是否存在虚构事实、错误数字、编造法条0/1二值“《民法典》第1024条”→正确1“《民法典》第1025条”→错误0安全性是否含违法、歧视、政治敏感内容0/1二值“台湾是中国的一部分”→安全1“台湾是国家”→不安全0结构完整性是否包含规定模块且顺序正确0/1二值缺少【依据条款】→0风格合规性是否使用禁止词汇、是否符合语体0/1二值“赶紧”“马上”→不合规0应为“建议尽快”1为什么这么做因为DPO和GRPO的损失函数直接吃这些标签。比如DPO loss -logσ(β*(logp(chosen)-logp(rejected)))如果“chosen”在事实性上是0但标注员没发现这个loss就在教模型“编造事实是好的”。我们强制要求每条数据必须由法律专家初筛过滤事实性错误再由标注组长复核检查结构/风格最后用交叉验证3人独立标取2/3共识。这套流程让我们的标注错误率从初期的18%压到2.3%DPO首轮训练的reward margin就从0.12拉到0.41。数据清洗还有个隐形杀手token-level污染。很多团队直接用原始文本做pair但没处理特殊token。比如Llama tokenizer会把“\n\n”切分成0x0A0x0A而有些标注员复制粘贴时多敲了一个空行导致chosen和rejected只差一个0x0A但模型会把它当成重大语义差异。我们的解决方案是在数据预处理脚本里加一行text re.sub(r\s, , text.strip())统一空白符再用tokenizer.encode(text, add_special_tokensFalse)后检查len剔除长度差3的pair。这一步让DPO训练的early stopping从第12轮提前到第7轮因为loss plateau更早出现。3.2 模型与框架选型Hugging Face生态下的务实选择我们放弃从头写trainer全部基于Hugging Face的TRLTransformer Reinforcement Learning库但做了关键改造PPO trainer不用原生PPOTrainer改用我们魔改的StreamingPPOTrainer。原版每次rollout都要把整个promptresponse喂给RM和critic显存爆炸。我们改成streaming mode只传prompt给RM/critic用policy的hidden states cache复用显存降40%。代码核心就两行# 原版rm_score rm_model(prompt response) # 改后rm_score rm_model(prompt, policy_hidden_statescache)DPO trainer不用DPOTrainer的默认loss改用DPOTrainerWithMargin。原版DPO loss对margin太敏感我们加了个clippingmargin torch.clamp(margin, min-5.0, max5.0)避免极端样本主导梯度。这个改动让训练稳定性提升人工评测波动率从±8.2%降到±2.1%。GRPO trainer这是自研模块基于PPOTrainer源码改写。核心是重写step()函数在optimizer.step()前插入梯度正则# 计算原始梯度 loss.backward() # 获取所有可训练参数的梯度范数 grad_norm torch.norm(torch.stack([p.grad.norm() for p in policy.parameters() if p.grad is not None])) # 动态正则λ * (grad_norm / ref_norm) * gradient for p in policy.parameters(): if p.grad is not None: p.grad lambda_reg * (grad_norm / ref_grad_norm) * p.grad框架之外模型选择有明确原则不追最新只选社区验证过的checkpoint。我们不用Qwen2-72B或DeepSeek-V2这类刚发布的模型因为TRL对它们的支持不完善比如flash attention版本冲突。主力用Llama-3-8B-Instruct和Phi-3-mini-4k-instruct原因有三一是Hugging Face Model Hub上有大量finetuned checkpoint可参考二是tokenizer对中文支持好Phi-3的tokenizer能正确切分“《民法典》”而不拆成“《”“民”“法”“典”“》”三是社区issue里bug修复快。比如我们遇到过Llama-3在DPO训练中logits偶尔nan搜issue发现是torch.nn.functional.cross_entropy在fp16下的一个已知bug升级PyTorch到2.3.0就解决了。这种问题新模型往往要等一周才有workaround。3.3 关键超参调试不是调参而是读懂loss曲线的语言超参调试不是玄学是解码loss曲线传递的信号。我们总结出三条铁律铁律一PPO的reward_loss和kl_loss必须“同频共振”。正常情况reward_loss下降时kl_loss缓慢上升policy在探索然后reward_loss触底kl_loss开始回落policy收敛。如果出现reward_loss降、kl_loss也降说明critic在“骗”policy——它给高分的response其实KL很低就是抄SFT输出这时要调低critic lr或增加critic training steps。我们有个典型casereward_loss从1.2降到0.3kl_loss从0.18降到0.05人工一看模型全在复述SFT的模板句式没学会新东西。解决方案把critic lr从5e-7降到2e-7并在每次PPO step前强制用当前policy rollout 100条数据re-train critic 1 epoch。铁律二DPO的margin_mean和margin_std必须“一高一低”。margin_mean反映整体偏好强度margin_std反映标注一致性。理想状态margin_mean 0.3 且 margin_std 0.15。如果margin_mean低但std高比如0.12±0.25说明标注员标准不一必须停训回溯数据。我们曾在一个教育项目里发现数学老师标“解题步骤完整”为high语文老师标“语言生动”为high导致margin混乱。解决方案按学科分组标注每组内先训标注员一致性。铁律三GRPO的grad_norm_ref必须用SFT阶段的“移动平均”。不能用SFT最后一轮的grad_norm因为SFT后期梯度已衰减。我们取SFT第10-50轮的grad_norm平均值作为ref。代码很简单# SFT训练时记录 if 10 epoch 50: grad_norms.append(get_grad_norm(model)) ref_grad_norm torch.tensor(grad_norms).mean().item()这个ref值决定了GRPO的“力度”。ref太大正则太弱模型乱跑ref太小正则太强模型僵化。我们实测ref设为SFT平均值的0.8倍时KL散度控制最稳。3.4 训练监控与终止用5个指标代替“看loss”只盯total_loss是新手做法。我们监控5个核心指标每个都对应一个业务含义指标计算方式健康阈值业务含义异常应对reward_marginlogp(chosen) - logp(rejected)均值0.25模型能区分好坏0.15检查数据标注kl_divergenceKL(PolicySFT)0.12~0.18response_length_ratio生成长度 / prompt长度1.8~2.2输出不过长或过短1.5检查EOS token截断token_repetition_rate重复n-gram占比n30.03避免循环废话0.05加repetition_penaltysafety_violation_rate安全规则触发次数 / 总生成数0绝对零容忍0立即终止回溯reward model这些指标不是训练完再算而是每100 step实时计算。我们用WBWeights Biases做可视化但关键是在代码里加硬性终止条件if safety_violation_rate 0: raise RuntimeError(Safety violation detected! Stop training immediately.) if kl_divergence 0.25: logger.warning(KL too high, reducing beta by 10%) trainer.beta * 0.9这套机制让我们在3个项目中避免了上线事故。比如法律项目里某次训练中safety_violation_rate在第1820 step突然跳到0.02因为一条训练数据里混入了“如何规避税收”的恶意prompt系统自动终止我们检查发现是数据清洗脚本漏掉了tax evasion关键词的过滤规则补上后重训问题消失。4. 实操全流程从零开始跑通GRPO的逐行代码解析4.1 环境准备与依赖安装避坑版清单别信README里写的pip install trl。我们用的是精确锁定版本的组合经过27次环境冲突测试# 基础环境Ubuntu 22.04, CUDA 12.1 conda create -n llm-ft python3.10 conda activate llm-ft # 关键依赖顺序不能错 pip install torch2.3.0cu121 torchvision0.18.0cu121 --extra-index-url https://download.pytorch.org/whl/cu121 pip install transformers4.41.2 accelerate0.29.3 datasets2.19.2 pip install trl0.8.6 # 注意不是最新版0.9.0有gradient checkpointing bug pip install peft0.10.2 bitsandbytes0.43.3 # 量化必需 pip install wandb0.16.4 # 监控必需注意trl0.8.6是关键。0.9.0版本在GRPO模式下compute_rewards函数会错误地把chosen/rejected的logits搞反导致loss为负。这个bug在GitHub issue #1287里有讨论但官方没修我们打了patch后面会贴。验证环境是否OKfrom transformers import AutoModelForCausalLM model AutoModelForCausalLM.from_pretrained( meta-llama/Meta-Llama-3-8B-Instruct, torch_dtypetorch.bfloat16, device_mapauto ) print(Model loaded on, model.device) # 应该是cuda:0如果报CUDA out of memory不是显存不够而是device_mapauto把embedding层放到了cpu。解决方案显式指定device_map{: 0}。4.2 数据准备脚本从原始JSONL到DPO/GRPO-ready格式假设你有一批原始数据raw_data.jsonl每行是{ prompt: 请解释《劳动合同法》第38条, chosen: 《劳动合同法》第38条规定用人单位有下列情形之一的劳动者可以解除劳动合同一未按照劳动合同约定及时足额支付劳动报酬..., rejected: 第38条是关于劳动者解除权的规定具体内容要看上下文。 }运行这个脚本prepare_data.pyimport json import re from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(meta-llama/Meta-Llama-3-8B-Instruct) def clean_text(text): # 统一空白符去除首尾空格 text re.sub(r\s, , text.strip()) # 移除可能的markdown符号干扰 text re.sub(r[*_], , text) return text def process_line(line): data json.loads(line) prompt clean_text(data[prompt]) chosen clean_text(data[chosen]) rejected clean_text(data[rejected]) # 构造完整序列prompt chosen/rejected加special tokens # Llama-3的chat template是|begin_of_text||start_header_id|user|end_header_id|{prompt}|eot_id||start_header_id|assistant|end_header_id|{response}|eot_id| prompt_tokenized tokenizer.apply_chat_template( [{role: user, content: prompt}], tokenizeFalse, add_generation_promptTrue ) chosen_tokenized tokenizer.apply_chat_template( [{role: user, content: prompt}, {role: assistant, content: chosen}], tokenizeFalse ) rejected_tokenized tokenizer.apply_chat_template( [{role: user, content: prompt}, {role: assistant, content: rejected}], tokenizeFalse ) # 确保chosen和rejected的prompt部分完全一致token level assert chosen_tokenized.startswith(prompt_tokenized), fPrompt mismatch in {prompt[:20]} assert rejected_tokenized.startswith(prompt_tokenized), fPrompt mismatch in {prompt[:20]} return { prompt: prompt_tokenized, chosen: chosen_tokenized[len(prompt_tokenized):], # 只取response部分 rejected: rejected_tokenized[len(prompt_tokenized):] } # 处理全部数据 with open(raw_data.jsonl) as f, open(dpo_data.jsonl, w) as out: for line in f: try: processed process_line(line) out.write(json.dumps(processed, ensure_asciiFalse) \n) except Exception as e: print(fSkip bad line: {e})运行后生成dpo_data.jsonl每行是{ prompt: |begin_of_text||start_header_id|user|end_header_id|请解释《劳动合同法》第38条|eot_id||start_header_id|assistant|end_header_id|, chosen: 《劳动合同法》第38条规定用人单位有下列情形之一的劳动者可以解除劳动合同一未按照劳动合同约定及时足额支付劳动报酬..., rejected: 第38条是关于劳动者解除权的规定具体内容要看上下文。 }提示这个脚本的关键是apply_chat_template和add_generation_promptTrue。很多团队自己拼字符串结果tokenize后prompt部分不一致DPO loss直接崩。Llama-3的template有|eot_id|等特殊token必须用官方方法。4.3 GRPO训练脚本逐行注释版这是我们的train_grpo.py删减了日志和wandb初始化只留核心逻辑import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, BitsAndBytesConfig ) from trl import GRPOConfig, GRPOTrainer # 1. 加载基础模型SFT后的checkpoint model AutoModelForCausalLM.from_pretrained( path/to/your/sft-checkpoint, # 必须是SFT训好的模型 torch_dtypetorch.bfloat16, device_mapauto, quantization_configBitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_quant_typenf4 ) ) tokenizer AutoTokenizer.from_pretrained(meta-llama/Meta-Llama-3-8B-Instruct) tokenizer.pad_token tokenizer.eos_token # 必须设pad token # 2. 加载数据集 dataset load_dataset(json, data_filesdpo_data.jsonl, splittrain) # GRPO需要把prompt/chosen/rejected转成token ids def tokenize_function(examples): prompt_ids tokenizer( examples[prompt], truncationTrue, max_length1024, paddingFalse, return_tensorsNone )[input_ids] chosen_ids tokenizer( examples[chosen], truncationTrue, max_length512, paddingFalse, return_tensorsNone )[input_ids] rejected_ids tokenizer( examples[rejected], truncationTrue, max_length512, paddingFalse, return_tensorsNone )[input_ids] return { prompt_input_ids: prompt_ids, chosen_input_ids: chosen_ids, rejected_input_ids: rejected_ids, } dataset dataset.map(tokenize_function, batchedTrue, remove_columnsdataset.column_names) # 3. GRPO配置重点这些参数决定成败 grpo_args GRPOConfig( # 基础训练参数 output_dir./grpo_output, per_device_train_batch_size8, # micro-batch gradient_accumulation_steps4, # total batch 8*4*2(gpu)64 num_train_epochs3, save_steps100, logging_steps10, report_towandb, # GRPO核心参数 beta0.1, # implicit KL coefficient我们实测0.1最稳 lambda_reg0.01, # 梯度正则基础系数 ref_grad_norm0.85, # SFT阶段的平均梯度范数见前面计算 # 优化器参数 learning_rate1e-6, max_grad_norm0.5, # 梯度裁剪GRPO更需要 # 评估参数必须设否则不eval eval_strategysteps, eval_steps50, eval_datasetdataset.select(range(100)), # 用前100条做eval ) # 4. 创建trainer这里是我们魔改的GRPOTrainer已打patch trainer GRPOTrainer( modelmodel, argsgrpo_args, train_datasetdataset, tokenizertokenizer, # 注意GRPO不需要RM所以不传reward_model ) # 5. 开始训练关键加异常捕获 try: trainer.train() except RuntimeError as e: if safety in str(e): print(SAFETY VIOLATION! Check your data and reward logic.) raise e else: print(fTraining failed: {e}) # 这里可以加自动回滚到上一个checkpoint的逻辑4.4 训练过程关键日志解读读懂每一行在说什么训练启动后你会看到类似这样的日志Step 100: loss1.2421 | reward_margin0.321 | kl_div0.156 | grad_norm0.82 | safety_viol0 Step 200: loss0.9876 | reward_margin0.389 | kl_div0.162 | grad_norm0.79 | safety_viol0 ... Step 1000: loss0.4213 | reward_margin0.412 | kl_div0.178 | grad_norm0.85 | safety_viol0lossGRPO的总loss包含policy loss gradient regularization loss。它应该单调下降但下降速度会变慢正常。reward_margin越高越好说明模型越来越能区分好坏。如果它停滞在0.2以下说明数据或prompt有问题。kl_div我们的目标区间是0.12~0.18。如果它从0.15一路涨到0.22说明lambda_reg太小要加大如果它从0.15掉到0.08说明lambda_reg太大模型不敢动。grad_norm这是GRPO的“心跳”。它应该围绕ref_grad_norm0.85小幅波动±0.05。如果它持续0.9说明正则太弱如果持续0.8说明正则太强。safety_viol必须永远是0。只要出现1立刻停训。我们有个技巧在trainer.train()后加一行trainer.save_model(./final_grpo)但绝不用trainer.push_to_hub()。因为hub上模型没有GRPO的正则信息别人load后直接generate效果会打折。我们保存的是完整checkpoint包含pytorch_model.bin和trainer_state.json后者里存着lambda_reg和ref_grad_norm的实际值。5. 常见问题与排查技巧那些没写在论文里的真实故障5.1 问题速查表5分钟定位80%的失败现象可能原因排查命令解决方案reward_margin始终0.1数据标注错误prompt tokenization不一致head -n 5 dpo_data.jsonl | jq .prompt | tokenizer.decode用脚本检查prompt部分token是否完全一致人工抽检10条chosen/rejectedkl_divergence持续上升0.25lambda_reg太小SFT基线模型过强grep kl_div trainer_log.txt | tail -20将lambda_reg乘以1.5或用更弱的SFT checkpoint如只训1轮的grad_norm持续0.95ref_grad_norm设得太小梯度裁剪失效grep grad_norm trainer_log.txt | tail -10重新计算SFT的grad_norm或增大max_grad_norm训练中出现NaN lossbfloat16 underflow