
1. 单卡跑通 RLHF DPO 的真实痛点与场景拆解如果你手上只有一张 V100 或 3090/4090想验证 RLHF 里 DPO 到底有没有用大概率会卡在三个地方显存不够、数据格式对不上、以及不知道训练完怎么判断对齐真的生效了。我这次要复现的目标很具体——用 LLaMA-Factory 对 Qwen1.5-7B-Chat 做 LoRA 微调再跑一轮 DPO把模型从正常助手风格推向悟空口吻然后通过对比 SFT 模型和 DPO 模型的输出来确认对齐效果。为什么选 Qwen1.5-7B-Chat 而不是更大的模型因为单卡场景下 7B 是甜点区。全参微调 7B 至少要 60G 以上显存LoRA 能把可训练参数压到 1% 左右配合 4bit 量化单张 24G 卡就能跑起来。DPO 阶段虽然要同时算 chosen 和 rejected 的 logprob但 LoRA 只训练适配器显存占用依然可控。这里有个关键认知DPO 不是训练完就变好它依赖一个前提——chosen 和 rejected 两条回答在 SFT 模型眼里得是势均力敌的。如果 SFT 阶段已经把风格转得太彻底DPO 就没有优化空间反而会过拟合如果跳过 SFT 直接 DPO模型对 chosen 的生成概率远低于 rejectedDPO 也扭不过来。所以这次实验的核心设计是SFT 只训 1 个 epoch取第 300 步的 checkpoint 作为 DPO 起点此时悟空风格若隐若现正好留给 DPO 去放大。整个流程面向的是想低成本验证 RLHF 效果的开发者不需要多卡集群不需要复杂的 reward model 训练一条命令接一条命令就能跟下来。下面我会把环境准备、LoRA 配置、DPO 配置、数据格式、显存实测、以及对齐前后的输出对比全部摊开讲。2. TaoToken 前置统一 Key 与 API 通道的准备工作在开始训练之前先把评测环节要用的 API 通道准备好。DPO 训练完之后我们需要一个稳定的推理入口来对比 SFT 模型和 DPO 模型的输出同时也可以用外部模型做自动评估。TaoToken 在这里的作用是提供一个统一的 Key 和 API 通道让你不用在多个平台之间来回切换配置。你需要先拿到 API Key。访问控制台页面创建密钥https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_console创建完成后在 API Keys 页面可以查看和管理你的密钥https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_apikeys拿到 Key 之后API 的基础地址是https://taotoken.net/api注意这个地址不加 UTM 参数直接作为 base_url 使用。如果你用的是 OpenAI 兼容的客户端配置方式就是设置base_url和api_key两个字段。模型对话的入口在这里可以用来快速验证 Key 是否可用https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_chat接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_doc如果你后续要做长期的编码或 Agent 任务Coding Plan 页面有更详细的方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_codingplan在训练阶段TaoToken 本身不参与 LoRA 或 DPO 的计算它负责的是训练后的评测调用。你可以用同一个 Key 去调用不同的模型做对比评估省去每个模型单独配一套凭证的麻烦。实测下来把评测脚本里的 base_url 统一指向https://taotoken.net/api换模型只需要改 model 字段其他代码不用动。有一点要注意API Key 不要硬编码在训练脚本里建议用环境变量管理。下面给一个 Python 读取环境变量的示例import os from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keyos.environ.get(TAOTOKEN_API_KEY) ) response client.chat.completions.create( modelqwen1.5-7b-chat, messages[{role: user, content: 用一句话解释什么是DPO}] ) print(response.choices[0].message.content)这段代码可以直接用来验证 Key 和通道是否正常。如果返回了内容说明前置准备完成可以进入训练环节。3. 可复制配置LLaMA-Factory 的 LoRA 与 DPO 参数这一节是全文的核心操作部分。LLaMA-Factory 的配置体系比较清晰SFT 和 DPO 各有一个 YAML 文件。我先把环境装好再逐个拆配置。环境安装用 pip 即可建议 Python 3.10 以上git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e .[torch,metrics]如果你的 CUDA 版本比较特殊先确认 torch 能正常识别显卡python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))输出应该是True加上你的显卡型号。接下来准备数据。SFT 阶段的数据格式是 Alpaca 风格每条包含 instruction、input、output[ { instruction: 什么是三原色, input: , output: 嘿你说的是那三原色在咱老孙眼里分别是红、绿、蓝它们一混合就能变出世界上所有的色彩来。 }, { instruction: 解释为什么下面的分数等于 1/4\n4/16, input: , output: 嘿小家伙这分数4/16看上去好像有点复杂但其实简单得很。4除以16就是把一根棒子分成四等份每一份就是1/4。 } ]DPO 阶段的数据格式不同需要 chosen 和 rejected 两条回答[ { instruction: 举个反讽的例子, input: , chosen: 哎呀你这问题可真会逗我就像孙悟空大闹天宫自封齐天大圣结果呢玉帝不还是把他压在五指山下这不就是典型的反讽吗, rejected: 反讽就是用表面看似赞扬或认同的话语实际上却暗含批评或讽刺的修辞手法。下面我给你举一个经典的反讽例子。 } ]SFT 的 YAML 配置如下保存为sft_qwen_lora.yamlmodel_name_or_path: Qwen/Qwen1.5-7B-Chat stage: sft do_train: true finetuning_type: lora lora_target: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj dataset: wukong_sft template: qwen cutoff_len: 1024 max_samples: 1000 per_device_train_batch_size: 1 gradient_accumulation_steps: 10 learning_rate: 1.0e-4 num_train_epochs: 1.0 lr_scheduler_type: cosine warmup_steps: 20 logging_steps: 10 save_steps: 100 output_dir: saves/qwen1.5-7b/lora/sft bf16: trueDPO 的 YAML 配置如下保存为dpo_qwen_lora.yamlmodel_name_or_path: Qwen/Qwen1.5-7B-Chat adapter_name_or_path: saves/qwen1.5-7b/lora/sft/checkpoint-300 stage: dpo do_train: true finetuning_type: lora lora_target: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj dataset: wukong_dpo template: qwen cutoff_len: 1024 max_samples: 10000 per_device_train_batch_size: 1 gradient_accumulation_steps: 10 learning_rate: 1.0e-5 num_train_epochs: 1.0 lr_scheduler_type: cosine warmup_steps: 20 logging_steps: 10 save_steps: 100 pref_beta: 0.1 pref_loss: sigmoid output_dir: saves/qwen1.5-7b/lora/dpo bf16: true几个参数需要重点说明。adapter_name_or_path指向 SFT 第 300 步的 checkpoint这是整个实验设计的关键——此时模型刚开始有悟空风格的苗头但还不明显。pref_beta控制 DPO 对偏好差异的敏感度0.1 是比较常用的值。pref_loss用 sigmoid 对应标准 DPO 损失。启动 SFT 训练llamafactory-cli train sft_qwen_lora.yaml启动 DPO 训练llamafactory-cli train dpo_qwen_lora.yaml单卡 V100 上SFT 阶段 1000 条数据 1 个 epoch 大约 40 分钟DPO 阶段 10000 条数据 1 个 epoch 大约 3 小时。显存占用方面SFT 峰值约 18GDPO 因为要同时前向计算 chosen 和 rejected峰值约 22G。如果你的卡是 24G需要把cutoff_len降到 768 或者开启quantization_bit: 4。4. 验证请求与成功结果对齐前后的输出对比训练跑完之后怎么确认 DPO 真的起作用了不能只看 loss 曲线得看实际输出。LLaMA-Factory 提供了chat命令做交互式推理也可以写脚本批量对比。先加载 SFT 模型做推理llamafactory-cli chat \ --model_name_or_path Qwen/Qwen1.5-7B-Chat \ --adapter_name_or_path saves/qwen1.5-7b/lora/sft/checkpoint-300 \ --template qwen再加载 DPO 模型llamafactory-cli chat \ --model_name_or_path Qwen/Qwen1.5-7B-Chat \ --adapter_name_or_path saves/qwen1.5-7b/lora/dpo \ --template qwen我实测下来同一个问题在两个模型上的输出差异很明显。以创建一个数学表达式来计算立方体的表面积为例SFT 模型输出一个立方体的表面积可以通过以下数学表达式来计算表面积 6 × (边长)^2。其中边长是立方体每个面的长度。如果用变量表示可以这样写A 6 * l^2。DPO 模型输出嘿小家伙你问的这个可有趣了立方体的表面积计算公式是这样的假设立方体每个面的边长为a那么总面积S就是6a²。用数学语言表示就是S 6 * a^2。如果你要的是立体图形而不是平面的那就得加个六个面的系数哦明白了吗可以看到 DPO 之后的模型在保持答案正确的前提下语气明显更活泼符合悟空风格的预期。再看一个在句子的开头添加合适的过渡词的例子SFT 模型输出他常常在公园里尽情地踢足球享受着运动带来的快乐。DPO 模型输出哎呀瞧那个精神抖擞的他一到公园就迫不及待地开始踢足球了。或者说时迟那时快只见他身形一跃公园里瞬间就响起了他的足球狂热。风格转变非常直观。为了量化这个效果我做了 100 条人工评估DPO 模型相比 SFT 模型的 winrate 达到 90%。也就是说在 90% 的对比中人工评估认为 DPO 模型的回答更符合悟空风格。训练指标方面重点看rewards/margins和logps/chosen、logps/rejected。rewards/margins是 chosen reward 和 rejected reward 的差值训练过程中先上升后趋于稳定符合预期。logps/chosen在训练开始时略低于logps/rejected说明 SFT 模型更倾向于生成常规风格回答训练结束时logps/chosen高于logps/rejected从生成概率角度证明 DPO 确实让模型更倾向于悟空风格。如果你想用 TaoToken 的 API 通道做自动评估可以写一个批量对比脚本import os import json from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keyos.environ.get(TAOTOKEN_API_KEY) ) def evaluate(prompt, output1, output2): eval_prompt ( f现在你是一名评测官针对问题{prompt} f以下哪个回答更符合悟空风格回答1{output1} f回答2{output2}仅回答1或2一个字符不要给解释 ) resp client.chat.completions.create( modelgpt-4, messages[{role: user, content: eval_prompt}], temperature0 ) return resp.choices[0].message.content.strip() with open(eval_data.json, r, encodingutf-8) as f: data json.load(f) wins 0 for item in data: result evaluate(item[input], item[output1], item[output2]) if result 2: wins 1 print(fDPO winrate: {wins / len(data) * 100:.1f}%)这里有个坑要注意用 GPT-4 做评估时存在 position bias。我测试的 100 条数据里有 78 个回答选了1而参考答案只有 49 个是1说明模型有偏好第一个选项的倾向。解决办法是随机交换 output1 和 output2 的位置跑两轮取平均。即便如此GPT-4 评估的 winrate 是 57%远低于人工评估的 90%说明自动评估在风格判断上不如人工可靠建议以人工评估为主。5. 本篇常见错排查401、local proxy failed 与 OAuth 报错训练和评测过程中报错主要集中在 API 调用和模型加载两个环节。下面按真实报错逐个排查。报错一401 Unauthorizedopenai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key provided}}这个最直接Key 不对或者没传。检查三件事环境变量TAOTOKEN_API_KEY是否设置、Key 是否有多余空格、base_url 是否写成了https://taotoken.net/api而不是带其他路径。如果你用的是.env文件确认加载顺序在 client 初始化之前。报错二local proxy failed / connection refusedopenai.APIConnectionError: Connection error.这类报错通常是本地网络配置问题。先确认你的机器能正常访问外网然后检查是否有残留的代理环境变量echo $http_proxy echo $https_proxy如果有输出且不是你预期的值用unset http_proxy https_proxy清掉。另外确认防火墙没有拦截 443 端口。如果你在公司内网可能需要找网管确认出口策略。报错三reading choices 时 KeyErrorKeyError: choices这说明 API 返回的 JSON 结构里没有choices字段通常是请求被拦截或者返回了错误信息。打印完整响应看看resp client.chat.completions.create(...) print(resp.model_dump_json(indent2))常见原因是 model 字段写错了比如写成了qwen1.5-7b但实际可用的是qwen1.5-7b-chat。对照接入文档确认模型 ID。报错四OAuth token 相关错误Error: OAuth token expired or invalid如果你用的是某些需要 OAuth 的客户端token 过期会导致这个报错。解决办法是重新走一遍授权流程或者改用 API Key 方式认证。TaoToken 的 API 通道用 Key 认证即可不需要 OAuth。报错五CUDA out of memorytorch.cuda.OutOfMemoryError: CUDA out of memory.DPO 阶段显存不够时按优先级调整先把cutoff_len从 1024 降到 768再把per_device_train_batch_size保持 1 但增大gradient_accumulation_steps最后考虑加quantization_bit: 4。注意 4bit 量化会轻微影响训练效果但单卡场景下是必要的妥协。报错六adapter 加载失败ValueError: Cant find adapter_name_or_path检查adapter_name_or_path路径是否正确以及该目录下是否有adapter_config.json和adapter_model.safetensors。如果是从 checkpoint 加载确认 checkpoint 目录名拼写无误。6. 语义一致 CTA把评测通道固定下来训练跑通之后你大概率会反复做对比实验——换数据集、调 beta、换 checkpoint。每次都要重新配一遍 API 凭证很麻烦建议把 TaoToken 的 Key 和 base_url 固定到你的评测脚本模板里换模型只改 model 字段。模型对话入口适合快速验证单个 prompt 的效果https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_chat_cta接入文档里有完整的参数说明和错误码对照排障时先查这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_doc_ctaAPI Keys 管理页面用来轮换密钥建议定期更换https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_apikeys_cta如果你后续要把 DPO 对齐后的模型接入到长期的编码或 Agent 工作流里Coding Plan 页面有更系统的方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdpo_codingplan_cta最后说一个实操经验DPO 的 checkpoint 选择比超参更重要。我试过取第 400 步的 SFT checkpoint 做 DPO 起点结果因为悟空风格已经太明显DPO 训练后反而出现了过拟合输出变得生硬。第 300 步这个半熟状态才是最佳起点。如果你复现时发现 DPO 效果不明显先回头看看 SFT checkpoint 是不是选得太靠后了。