LLaMAfactory跑通LLM api作为奖励模型的PPO训练 - 详解
目录
2.LLaMAfactory不支持Qwen3_vl_moe问题
3.版本不兼容问题。LLaMAfactory ppo训练 ,使用deepspeed 跟trl版本冲突
4.ppo的模型不能加think(think 截断后think会作为模型回复,导致得分非常低)
一、操作步骤
1.1 更新版本
更新最新版本的LLaMAfactory后(qwen3_vl_moe模型ppo需要transformers==4.57.0版本,旧LLaMAfactory版本不兼容),最新0.9.4.dev0 版本的LLaMAfactory代码携带部分api作为奖励模型的代码,但仍需要自定义奖励模型,需要修改trainer.py文件以及workflow.py文件。
1.2 修改文件
trainer.py文件:
1.添加test_reward_api函数逻辑,确保奖励模型api通路
def test_reward_api(api_url):
try:
import requests
test_data = {
"name": "奖励模型", # 自己起的任务名称
"description": "按照设定标准,给回复打分",
"prompt": {
"name": "maliangkun_RL", # 自己的提示词name
},
"evalutateModelId": "gpt-4o", # 选用模型的 id
"inputParams": {
"query": "测试问题",
"model_answer": "测试回答",
}
}
response = requests.post(api_url, json=test_data, timeout=10)
if response.status_code == 200:
logger.info("====奖励模型API连接正常====")
else:
logger.warning(f"====奖励模型API返回状态码: {response.status_code}====")
except Exception as e:
logger.error(f"=====奖励模型API连接失败: {e}=====")
2.在 init 方法中添加测试调用
if finetuning_args.reward_model_type == "api":
test_reward_api(finetuning_args.reward_model)
3.在get_rewards函数中添加打包api逻辑(用于获取打分结果)
if self.finetuning_args.reward_model_type == "api":
logger.info(f"======使用API奖励模型====queries数量: {len(queries)}, responses数量: {len(responses)}===")
# 将 queries 和 responses 解码为文本
query_strs = self.tokenizer.batch_decode(queries, skip_special_tokens=True)
response_strs = self.tokenizer.batch_decode(responses, skip_special_tokens=True)
logger.info(f"解码后queries: {query_strs}")
logger.info(f"解码后responses: {response_strs}")
# 准备发送到API的数据
scores = []
logger.info(f"准备处理 {len(query_strs)} 个查询-响应对")
for i, (query, response) in enumerate(zip(query_strs, response_strs)):
logger.info(f"处理第 {i + 1} 个查询-响应对")
# 构造发送到API的数据
api_data = {
"name": "奖励模型", # 自己起的任务名称
"description": "按照设定标准,给回复打分",
"prompt": {
"name": "maliangkun_RL", # 自己的提示词name
},
"evalutateModelId": "gpt-4o", # 选用模型的 id
"inputParams": {
"query": query,
"model_answer": response,
}
}
logger.info(f"====发送API请求 {api_data}====")
try:
# 发送请求到API
import requests
logger.info(f"正在发送请求到 {self.finetuning_args.reward_model}")
api_response = requests.post(
self.finetuning_args.reward_model, # API地址
json=api_data,
timeout=30
)
logger.info(f"收到API响应,状态码: {api_response.status_code}")
api_response.raise_for_status()
# 解析API响应
result = api_response.json()
logger.info(f"解析API响应: {result}")
if result.get("success") and "data" in result:
# 从API响应中提取分数
score = float(result["data"])
scores.append(score)
logger.info(f"=====API调用成功: {result}==={score}==")
else:
# 如果API调用失败,默认给0分
logger.warning(f"API调用失败,返回数据: {result}")
scores.append(0.0)
except Exception as e:
logger.warning(f"=====API调用失败: {e}=====")
# 如果出现异常,默认给0分
scores.append(0.0)
logger.info(f"最终scores: {scores}")
workflow.py文件:(原则上可以不做调整,我个人加了一部分打印用来问题定位)
1.增加检查梯度检查是否启用
# 检查梯度检查点是否启用
if training_args.gradient_checkpointing:
logger.info("Gradient checkpointing is enabled in training arguments--------------0")
if hasattr(model, 'gradient_checkpointing_enable'):
model.gradient_checkpointing_enable()
logger.info("Gradient checkpointing enabled on model-----------1")
else:
logger.warning("Model does not support gradient_checkpointing_enable method---------------2")
else:
logger.info("Gradient checkpointing is disabled in training arguments--------------3")
2.消除兼容性警告
# 原代码
# tokenizer.padding_side = "left" # use left-padding in generation while using right-padding in training
# data_collator = MultiModalDataCollatorForSeq2Seq(template=template, model=model, **tokenizer_module)
# 修改后
tokenizer.padding_side = "left" # use left-padding in generation while using right-padding in training
processing_class = tokenizer_module["tokenizer"] # 或直接使用 processing_class
processing_class.padding_side = "left"
data_collator = MultiModalDataCollatorForSeq2Seq(
template=template,
model=model,
**{k:v for k,v in tokenizer_module.items() if k != "processing_class"}
)
1.2 训练数据格式
使用奖励模型的PPO训练数据格式需要保持以下结构:
[
{
"prompt": "system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.\nuser\n问题内容\nassistant\n",
"response": "模型的完整回答内容(可选)",
"labels": "标签信息(可选)"
}
]
dataset_info.json 中需要正确映射字段:
{
"maliangkun_ppo_data": {
"file_name": "maliangkun_ppo_data.json",
"columns": {
"prompt": "prompt",
"response": "response",
"labels": "labels"
}
}
}
提示:
在PPO训练中labels字段是辅助训练可选的,不是必需的
辅助训练:可以用于:提供额外的监督信号、作为参考答案进行对比、辅助奖励模型的评估
如果不想要labels标签:可以在trainer.py中添加代码
ppo_config = PPOConfig(
model_name=model_args.model_name_or_path,
learning_rate=training_args.learning_rate,
mini_batch_size=training_args.per_device_train_batch_size,
batch_size=backward_batch_size * finetuning_args.ppo_buffer_size,
gradient_accumulation_steps=training_args.gradient_accumulation_steps,
ppo_epochs=finetuning_args.ppo_epochs,
max_grad_norm=training_args.max_grad_norm,
seed=training_args.seed,
optimize_device_cache=True,
target=finetuning_args.ppo_target,
use_score_scaling=finetuning_args.ppo_score_norm,
use_score_norm=finetuning_args.ppo_score_norm,
whiten_rewards=finetuning_args.ppo_whiten_rewards,
accelerator_kwargs={"step_scheduler_with_optimizer": False},
log_with=training_args.report_to[0] if training_args.report_to else None,
project_kwargs={"logging_dir": training_args.logging_dir},
remove_unused_columns=False # 增加了这一行
)
response字段可以是奖励模型的输出,可以为空
1.3 训练模型
yaml需要添加一下配置
# reward model config (自定义API配置)
reward_model_type: api
reward_model: "http://llmops-bff.dev.dotfortune.com/api/datasets/evaluate"
整体参考配置:(以Qwen3-32B为参考-仅供参考)
# model
#model_name_or_path: /workspace/infrawaves/Qwen3-235B-A22B-Instruct-2507
model_name_or_path: /models/models/Qwen3-32B
trust_remote_code: true
stage: ppo
do_train: true
lora_rank: 8
finetuning_type: lora
template: qwen
lora_target:
- q_proj
# dataset
dataset: maliangkun_ppo_data
dataset_dir: /home/ai/liguangxu/LLM_train/PPO_train/data
cutoff_len: 512
# reward model config (自定义API配置)
reward_model_type: api
reward_model: "http://llmops-bff.dev.dotfortune.com/api/datasets/evaluate"
# generation config
max_new_tokens: 512
# memory
bf16: true
gradient_checkpointing: true
deepspeed: /home/ai/liguangxu/LLM_train/PPO_train/ds_z3_config.json
remove_unused_columns: false
# training
per_device_train_batch_size: 1
gradient_accumulation_steps: 8
learning_rate: 1e-5
num_train_epochs: 3
lr_scheduler_type: cosine
warmup_steps: 100
optim: adamw_bnb_8bit
# logging
logging_steps: 10
save_steps: 10
output_dir: saves/ppo_with_api_reward
1.4 各项训练指标查看
运行结束后会保存runs文件,可以通过TensorBoard查看训练指标
TensorBoard命令:tensorboard --logdir=saves/ppo_with_api_reward2/runs------后边是路径
后浏览地址
各指标的意义
环境奖励相关指标
1.env/reward_mean :表示每个训练回合中智能体获得的平均奖励,是评估策略性能的核心指标。平均奖励越高,说明当前策略在环境中表现越好。若曲线呈上升趋势并逐渐平稳,通常表明策略在持续优化并趋于收敛。
2.env/reward_std :表示不同回合中奖励的标准差,反映策略在不同场景下的稳定性。标准差越小,说明策略在相似环境中的表现越一致,波动越小,训练过程越稳定。
策略优化相关指标
1.objective/entropy :即策略熵,衡量策略的随机性(探索程度)。熵值越高,策略探索性越强(倾向于尝试不同行为);熵值降低,说明策略逐渐确定最优行为,探索减少。训练后期熵值趋于稳定,表明策略已形成较固定的决策模式。
2.objective/kl: 即KL散度(Kullback-Leibler Divergence),衡量当前策略与更新前旧策略的差异。KL散度过大表示策略更新幅度过大,可能导致训练不稳定(如奖励波动);过小则可能更新不足,收敛缓慢。合理范围通常为0.01-0.05,此时策略更新较为稳健。
3.objective/KLcoef:即KL散度系数,是PPO算法中用于平衡策略优化目标与KL散度约束的超参数。其值动态调整以控制策略更新的幅度,避免因KL散度过大导致训练动荡。若KLcoef逐渐稳定,说明策略更新已进入合理区间。
其他指标
1.ppo/learning_rate:学习率,它决定了模型在训练过程中更新权重的步长大小。一个适当的学习率可以让模型稳定学习,而不会在最优解附近震荡或收敛过慢。从图中可以看到学习率随时间的变化,这有助于判断学习率是否需要调整。
2.ppo/loss/policy 和 ppo/loss/value:策略损失和价值损失。策略损失衡量的是当前策略与旧策略之间的差异,而价值损失则衡量了状态价值估计的准确性。优化这两个损失函数是PPO训练的核心。
3.ppo/loss/total:总损失,是策略损失、价值损失以及其他可能损失的总和。它提供了一个总体的优化目标。
4.ppo/mean_non_score_reward(ppo/mean_scores非直接显示):通常指的是智能体在非目标任务上的平均表现或奖励,这可能用于衡量智能体在探索环境时的多样性或泛化能力。而“mean scores”可能指的是智能体在主要任务上的平均得分或奖励。
5.ppo/advantages_mean:平均优势,优势函数衡量的是在某个状态下采取某个动作相对于平均策略的优势。这个指标可以帮助理解智能体是否在学习有效的策略。
6.ppo/policy/approxkl 和 ppo/policy/clipfrac:近似KL散度和裁剪比例。KL散度用于衡量新旧策略之间的差异,而裁剪比例是在PPO中用来限制策略更新幅度的参数。这两个指标有助于监控策略的更新稳定性。近似KL值较大可能意味着策略发生了较大变化,这可能导致训练不稳定。
7.ppo/policy/entropy:策略熵,表示策略的随机性。在训练过程中,保持一定的熵值有助于智能体保持探索性,避免过早收敛到次优策略。
8.ppo/policy/policykl(在第二张图中):可能指的是策略的KL散度,与近似KL类似,用于衡量策略更新前后的变化量,但可能是通过不同的计算方式得到。
9.ppo/returns/mean:平均回报,表示智能体在训练过程中获得的平均奖励。这个指标是评估智能体性能的关键指标,随着训练的进行,平均回报应该逐渐增加并趋于稳定。
二、注意事项
1.要确认api奖励模型的输出格式,在trainer.py的get_rewards函数中修改获取分数方式,若有误,分数默认为0
2.
三、问题及解决方案
1.api奖励+ppo占用大量显存问题:
ai给出的解释是PPO训练需在策略模型生成文本后调用Gemini API计算奖励值,虽然不加载模型参数,但PyTorch的自动微分机制会保留从策略模型输出到API调用的完整计算图(包括文本编码、传输等中间变量),导致显存累积增长。而LoRA训练仅需维护低秩矩阵的梯度计算,计算图规模显著更小
以7B模型FP32训练为例作对比:(未计算ppo+api,调用api与模型生成时间)
具体计算流程:
LoRA微调显存(12GB)
模型参数:7B×4字节=28GB → 冻结后仅需2GB(量化后)2
梯度:可训练参数0.1%×7B×4字节≈28MB → 实际分配2GB缓冲5
优化器状态:8×0.1%×7B×4字节≈224MB → 实际分配8GB(含预留)5
PPO+API显存(84GB)
策略模型:7B×4字节×1.05(封装系数)=29.4GB → 实测26GB(参数共享优化)4
优化器状态:8×7B×4字节=224GB → 实测52GB(梯度检查点技术)5
激活值:2×1×2048×4096×32×2字节≈6GB(seq_len=2048)3
API缓存:文本编码临时变量约4GB(基于Gemini API实测)
关键影响因素
精度选择:FP16训练可使PPO显存降至42GB(参数+优化器减半)3
序列长度:激活值显存与seq_len成正比,2048→6GB,4096→12GB5
批次大小:batch=1时激活值显存最低,每增加1批显存增长约3GB5
该计算框架已通过LLaMA-7B实测验证,误差率<5%
——deepspeed支撑235B模型在8张卡上训练,训练速度依然很慢。
原因:1.api请求耗时太久 2.前向传播 每次生成回复,相当于使用235B进行推理,速度很慢。3.ppo需要同时维护当前模型、参考模型等多个副本
解决思想:
策略:API请求方案优化。--修改时需要注意适配api返回数据格式
1.当前在 get_rewards 方法中是逐个处理每个query-response对,可以改为批量并发处理:(采用此方案已经跑通,通过测试8B模型,有明显速度提升约10%)
# -----------------------------------------------------------------------------------------
# 使用异步并发处理
async def fetch_reward(session, query, response):
api_data = {
"name": "奖励模型",
"description": "按照设定标准,给回复打分",
"prompt": {"name": "maliangkun_RL"},
"evalutateModelId": "gpt-4o",
"inputParams": {"query": query, "model_answer": response}
}
try:
async with session.post(self.finetuning_args.reward_model, json=api_data, timeout=30) as resp:
result = await resp.json()
if result.get("success") and "data" in result:
return float(result["data"])
return 0.0
except Exception:
return 0.0
async def batch_rewards():
async with aiohttp.ClientSession() as session:
tasks = [fetch_reward(session, q, r) for q, r in zip(query_strs, response_strs)]
scores = await asyncio.gather(*tasks)
return scores
try:
scores = asyncio.run(batch_rewards())
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
return [torch.tensor(score, dtype=torch.float32, device=device) for score in scores]
except Exception as e:
logger.warning(f"并发API调用失败,回退到串行处理: {e}")
# 回退到原来的串行处理逻辑
# -----------------------------------------------------------------------------------------
# 准备发送到API的数据
scores = []
logger.info(f"准备处理 {len(query_strs)} 个查询-响应对")
for i, (query, response) in enumerate(zip(query_strs, response_strs)):
logger.info(f"处理第 {i + 1} 个查询-响应对")
2.将多个query-response对打包成一个API请求:
def get_rewards(self, queries, responses):
if self.finetuning_args.reward_model_type == "api":
# 批量处理所有查询-响应对
query_strs = self.tokenizer.batch_decode(queries, skip_special_tokens=True)
response_strs = self.tokenizer.batch_decode(responses, skip_special_tokens=True)
# 构造批量API请求数据
batch_api_data = {
"name": "奖励模型",
"description": "按照设定标准,给回复打分",
"prompt": {"name": "maliangkun_RL"},
"evalutateModelId": "gpt-4o",
"batchInputParams": [
{"query": q, "model_answer": r}
for q, r in zip(query_strs, response_strs)
]
}
try:
import requests
api_response = requests.post(
self.finetuning_args.reward_model,
json=batch_api_data,
timeout=300
)
result = api_response.json()
logger.info(f"API响应结果: {result}")
if result.get("success") and "data" in result:
scores_data = result["data"]
processed_scores = []
# 处理不同格式的返回数据
if isinstance(scores_data, list):
# 如果是列表格式,按原逻辑处理
for score in scores_data:
try:
processed_scores.append(float(score))
except (ValueError, TypeError):
processed_scores.append(0.0)
elif isinstance(scores_data, (str, int, float)):
# 如果是单个值,转换为列表
try:
processed_scores = [float(scores_data)]
# 如果只有一个分数但有多个查询,复制该分数
while len(processed_scores) < len(query_strs):
processed_scores.append(float(scores_data))
except (ValueError, TypeError):
processed_scores = [0.0] * len(query_strs)
else:
logger.warning(f"API返回的data类型不支持: {type(scores_data)}")
processed_scores = [0.0] * len(query_strs)
logger.info(f"API调用成功,处理了 {len(processed_scores)} 个奖励分数")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
return [torch.tensor(score, dtype=torch.float32, device=device) for score in processed_scores]
else:
logger.warning(f"API调用不成功或缺少data字段: {result}")
except Exception as e:
logger.warning(f"批量API调用失败: {e}")
# 失败时回退到默认值
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
return [torch.tensor(0.0, dtype=torch.float32, device=device) for _ in query_strs]
3.异步队列处理-使用生产者-消费者模式处理大量请求:
import asyncio
import aiohttp
from asyncio import Queue
async def async_get_rewards(self, queries, responses):
query_strs = self.tokenizer.batch_decode(queries, skip_special_tokens=True)
response_strs = self.tokenizer.batch_decode(responses, skip_special_tokens=True)
# 创建请求队列
queue = Queue()
for q, r in zip(query_strs, response_strs):
await queue.put((q, r))
# 并发处理函数
async def worker(session, queue, results):
while not queue.empty():
query, response = await queue.get()
try:
api_data = {
"name": "奖励模型",
"description": "按照设定标准,给回复打分",
"prompt": {"name": "maliangkun_RL"},
"evalutateModelId": "gpt-4o",
"inputParams": {"query": query, "model_answer": response}
}
async with session.post(self.finetuning_args.reward_model, json=api_data) as resp:
result = await resp.json()
score = float(result["data"]) if result.get("success") and "data" in result else 0.0
results.append(score)
except Exception:
results.append(0.0)
finally:
queue.task_done()
# 启动多个worker并发处理
async with aiohttp.ClientSession() as session:
results = []
workers = [asyncio.create_task(worker(session, queue, results)) for _ in range(10)] # 10个并发worker
await asyncio.gather(*workers)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
return [torch.tensor(score, dtype=torch.float32, device=device) for score in results]
4.添加结果缓存避免重复请求:
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_reward_request(self, query_hash, response_hash):
# 基于查询和响应的哈希值缓存结果
pass
2.LLaMAfactory不支持Qwen3_vl_moe问题
更新最新LLaMAfactory,更新transformers==4.57.0版本,但最新版本会有新的问题(trl与deepspeed版本冲突,见下个问题)
3.版本不兼容问题。LLaMAfactory ppo训练 ,使用deepspeed 跟trl版本冲突
LLaMAfactory要求trl版本trl>=0.8.6,<=0.9.6
LLaMA-Factory要求deepspeed>=0.10.0,<=0.16.9
---ppo训练过程trainer.py文件中trl库unwrap_model_for_generation调用deepspeed不存在的方法
暂时采用的方法:
修改代码避免使用 unwrap_model_for_generation
在 trainer.py 文件中,替换 get_inputs 方法中的 unwrap_model_for_generation 上下文管理器:(替换get_inputs函数)
@torch.no_grad()
def get_inputs(self, batch: dict[str, "torch.Tensor"]) -> tuple[list["torch.Tensor"], list["torch.Tensor"]]:
r"""Generate model's responses given queries."""
# 清理缓存以释放显存
if torch.cuda.is_available():
torch.cuda.empty_cache()
if batch["input_ids"].size(0) == 1: # handle llama2 ppo with gradient accumulation > 1
start_index = (batch["input_ids"][0] != self.tokenizer.pad_token_id).nonzero()[0].item()
for k, v in batch.items():
batch[k] = v[:, start_index:]
# 创建更保守的生成配置
conservative_generation_config = GenerationConfig(
max_new_tokens=32, # 降低生成长度
temperature=0.3, # 降低温度系数
top_p=0.7, # 降低top_p
top_k=10, # 降低top_k
do_sample=True,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=[self.tokenizer.eos_token_id] + self.tokenizer.additional_special_tokens_ids,
)
# 直接使用 unwrap_model 替代 unwrap_model_for_generation
unwrapped_model: AutoModelForCausalLMWithValueHead = self.accelerator.unwrap_model(self.model)
if self.model_args.upcast_layernorm:
layernorm_params = dump_layernorm(unwrapped_model)
# 使用更保守的生成配置
generate_output: torch.Tensor = unwrapped_model.generate(
generation_config=conservative_generation_config,
logits_processor=get_logits_processor(),
**batch
)
if self.model_args.upcast_layernorm:
restore_layernorm(unwrapped_model, layernorm_params)
# 生成后立即清理缓存
if torch.cuda.is_available():
torch.cuda.empty_cache()
query = batch["input_ids"].detach().cpu()
response = generate_output[:, batch["input_ids"].size(-1):].detach().cpu()
queries, responses = [], []
for i in range(len(query)):
query_start_index = (query[i] != self.tokenizer.pad_token_id).nonzero()[0].item()
response_indexes = (response[i] != self.tokenizer.pad_token_id).nonzero()
if len(response_indexes) == 0: # allow empty response
response_length = 1
elif self.tokenizer.eos_token_id == self.tokenizer.pad_token_id: # include eos token
response_length = response_indexes[-1].item() + 2
else:
response_length = response_indexes[-1].item() + 1
queries.append(query[i, query_start_index:]) # remove padding from left
responses.append(response[i, :response_length]) # remove padding from right
return queries, responses
4.ppo的模型不能加think(think 截断后think会作为模型回复,导致得分非常低)
对于带think的模型,思考过程产生的token占用过多,以目前512长度为例,模型给出的response大部分是思考过程,导致奖励得分很低。
---打算尝试-在数据层面的query加上部分提示词让模型只回复问题,不think。-有一点的效果,看后续是否需要加入think
5.模型自带系统提示词修改

Qwen3系统调用默认使用固定提示词You are Qwen, created by Alibaba Cloud. You are a helpful assistant.需要修改自定义提示词。
修改步骤:
1.yaml文件修改参数template: "empty":如果使用qwen参数,则在调用模型过程中会默认使用模型自带prompt。改用empty则不使用prompt,可以在train ppo时自定义
2.添加自定义prompt:在llamafactory/train/ppo/trainer.py文件中get_inputs函数conservative_generation_config = GenerationConfig()生成配置前增加以下配置。
# 添加提示词
# 定义系统提示词
system_prompt = "system prompt\n---自定义提示词---\n uesr "
# 为每个查询添加系统提示词
original_input_ids = batch["input_ids"]
# original_attention_mask = batch["attention_mask"]
# 解码原始输入
original_prompts = self.tokenizer.batch_decode(original_input_ids, skip_special_tokens=True)
# 添加系统提示词
enhanced_prompts = [f"{system_prompt}\n{prompt}" for prompt in original_prompts]
# 重新编码为token IDs
enhanced_encodings = self.tokenizer(
enhanced_prompts,
padding=True,
truncation=True,
max_length=self.tokenizer.model_max_length,
return_tensors="pt"
)
# 更新batch数据
batch["input_ids"] = enhanced_encodings["input_ids"].to(self.current_device)
batch["attention_mask"] = enhanced_encodings["attention_mask"].to(self.current_device)
# 以下是原代码...
conservative_generation_config = GenerationConfig(
max_new_tokens=512, # 降低生成长度
temperature=0.7, # 降低温度系数
top_p=0.9, # 降低top_p
top_k=20, # 降低top_k
do_sample=True,
bos_token_id=bos_token_id,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=[self.tokenizer.eos_token_id] + self.tokenizer.additional_special_tokens_ids,
)

浙公网安备 33010602011771号