常见的vllm推理和trl源码分析
API推理
一般来说,API请求都是IO密集型,可以利用到多进程和多线程,但是请求可能会失败,所以得把失败的重新跑一遍,这里用到两步
第一步先批量执行,然后第二步再专门处理失败的
1 | import requests |
1 | import os |
命令
1 | python api2.py --model_name qwen3.6-max-preview --mode hybrid --input_pred v1.json --output_pred v1_new.json --prompt_file prompt-v3.yaml --train_data 26_0513_货主介入评测_new.json |
VLLM推理
和api推理的区别,vllm实例化模型到本地,直接用vllm接口;而api是把模型部署成一个服务,直接用request的方式调用
my_prompt.py
1 | import json |
参考代码 ipynb
1 | import multiprocessing |
拿到output再后处理即可,根据带不带think,带不带json等情况,进行相应的提取即可。
后处理代码ipynb
仅供参考,以下只考虑,think和json两种
1 | preds = [] |
指标计算
1 | from collections import defaultdict, Counter |
参考脚本 ipynb
1 | import os |
数据处理
一般来说,对于本地数据,主要是file或者scirpt的方式
-
file就是本地直接按照llamafactory的格式准备好了json
-
script的方式是本地有json格式的文件,然后写一个继承自
datasets.GeneratorBasedBuilder的类,重写三个方法即可,然后映射到llamafactory的指定格式
Trainer的典型流程
1 | train |
以gradient_accumulation_steps=6为例,batch_size为64,会重复找dataloader拿6轮数据,每轮64条,每一轮都会计算loss,但是不回传梯度,只累计,在每个gradient_accumulation_steps的末尾去同步更新
1 | for _ in range(total_updates): |
在_maybe_log_save_evaluate 有_evaluate和_determine_best_metric两个重点函数,后面是用来评估具体metric中哪个指标,是greater好还是less好,返回是一个bool值,代表有没有找到一个更好的模型,对应True
1 | if self.control.should_evaluate: |
_evaluate内部会走到evaluate,然后走到evaluation_loop
1 | override = eval_dataset is not None |
evaluation_loop内部流程比较复杂,涉及到多卡结果的gather等,但最终返回的就是一个包含metric的EvalLoopOutput
for训练依次从eval dataloader里面取数据,然后走prediction_step计算
1 | # Main evaluation loop |
我们先看内部的prediction_step,以llamafactory的CustomSeq2SeqTrainer为例子,根据继承关系,会依次调用prediction_step
首先走到CustomSeq2SeqTrainer
1 |
|
这里调试可以看到,每个inputs包含8条数据,对应默认的eval batch大小
(Pdb) inputs.keys()
dict_keys([‘input_ids’, ‘attention_mask’])
(Pdb) len(inputs[‘input_ids’])
8
(Pdb) self.args.predict_with_generate
True
然后走到Seq2SeqTrainer,内部就是generate一次,然后返回loss, generated_tokens, labels,最后回到上面的for循环取eval数据,关注一下pad_across_processes
(Pdb) labels.shape
torch.Size([8, 88])
(Pdb) logits.shape
torch.Size([8, 3189])
1 | if labels is not None: |
经过pad_across_processes以后会将同一批次右填充到当前批次的最大长度,默认是右填充,pad_first = False
经过上面的调试以后,还有几个问题。
- 什么时候把输出padding到统一的长度的?
- eval的时候,tokenizer的padding_size是right还是left?在哪里修改的?
per_device_eval_batch_size改成64试试
KTO典型流程
以llamafactory0.9.3为例,CustomKTOTrainer–>KTOTrainer–>BaseTrainer–>Trainer,各自重写了部分方法
1 | # 以下是trl==0.9.6 |
trl最新版,又进行了改动。底层逻辑不变,对函数封装进行了优化。目前按照0.9.6的版本进行分析
首先看一下ref_model的创建,finetuning_args.ref_model一般不会设置的,因此这里会创建一个和策略模型一模一样的参考模型,如果是lora微调的话,就返回None,用于后续lora adapter的适配
1 | def create_ref_model( |
CustomKTOTrainer.get_batch_loss_metrics
1 | def get_batch_loss_metrics( |
首先会走到forward,会走两次
- 第一次,在内部用策略模型完整计算一个batch的logits、logps、logps/valid_length
- 第二次,用参考模型再计算一次,因为这里ref和policy是同一个,所以直接no_gra推理一边即可,得到_, kl_logps, _,只关注kl_logps
然后经过CustomKTOTrainer.concatenated_forward拿到choose、reject的logits、logps,期望样本的平均对数概率,参考模型的kl_logps
1 |
|
同理,经过compute_reference_log_probs,用ref_model也计算一边,拿到reference_chosen_logps, reference_rejected_logps, reference_kl_logps
最后看一下最核心的kto_loss,很清晰简洁。trl源码确实值得深入探索
1 | def kto_loss( |
步骤
1. 对数概率定义
设 为提示, 为回答, 为回答的 token 数。模型 在回答 上的总对数概率与平均每 token 对数概率分别为:
对于完整序列 (包含 prompt 和 completion),记其平均每 token 对数概率为 。
2. 批次 KL 散度估计
设批次大小为 , 为当前策略模型, 为冻结的参考模型。批次级别的 KL 散度估计为:
3. 回答级对数比率 (log-ratio)
对期望回答 (chosen) 与不期望回答 (rejected) 分别定义对数比率:
4. 非对称 KTO 损失 (原论文 Eq.7 实现)
令 为 sigmoid 函数, 为温度系数。两类样本的损失分别为:
5. 加权损失聚合
记 、 为批次中期望/不期望样本的数量,、 为对应权重,则主损失为:
6. 辅助 SFT 损失与最终损失
期望回答上的平均每 token 对数概率的负值构成辅助 SFT 损失:
最终训练损失为两者加权和:
其中 为混合系数(ftx_gamma)。
7. 隐式奖励(仅用于监控)
对数比率缩放后得到隐式奖励,不参与梯度计算:
以上就是llamafactory0.9.3+trl0.9.6完整的KTO流程
PPO流程
在llamafactory里面ref model也会加上value head,好像是为了工程上的简便处理。
actor和critic共用主干网络,减少显存占用
1 | model = load_model(tokenizer, model_args, finetuning_args, training_args.do_train, add_valuehead=True) |
可以看到,model和ref model都加了valuehead,为什么这么做?只能去CustomPPOTrainer里面寻找答案了
CustomPPOTrainer其实就是继承自PPOTrainer和Trainer,PPOTrainer是trl实现的,最新版本的trl没有step方法,改成了train方法,看后续llamafactory怎么兼容吧,目前只支持trl到0.9.6,但0.24实现会更优雅一点
PPOTrainer内部train方法就是把ppo实现了一遍,包括pg loss+critic loss,然后CustomPPOTrainer再进行了一次封装,做了一定解耦,支持自定义的reward和inputs,具体细节可以看PPOTrainer内部,重点是GAE的计算、adapter里面value head和policy head的切换等。
OPD流程
以trl库为例,要求trl>=0.29.0 transformers>=5.6.0(相比4.0,5.0代码更简洁,train流程抽象成了几个函数)
继承关系:Trainer->_BaseTrainer->SFTTrainer->GKDTrainer
从GKDTrainer.training_step开始看
1 | def training_step( |
三个分支,分别对应student on-policy,也就是OPD,seq_kd对应传统的模型蒸馏,最后else对应传统的sft,这里就是是否修改了inputs。
generate_on_policy_outputs内部会负责处理修正新的response_id、attention_mask、labels
- opd:修改了response,学生自己推理的
- gkd:修改了response,教师推理的,比传统sft慢很多
- stf:不改inputs,直接用标签的response
接下来SFTTrainer.training_step–>Trainer.training_step,最后走到GKDTrainer重写的compute_loss–>generalized_jsd_loss
generalized_jsd_loss在内部计算jsd散度,beta控制是forward KL、reverse KL、mix等
新版本transformers的train流程
trl基本上在内部都是复用了Trainer类,重写各种子类方法,实现各种sft、kto、dpo等
1 |
|
dataset流程
以sft为例
1 | 第一次加载数据是在_load_single_dataset中的load_dataset,此时拿到的是 |
padding side一些思考
参考:
第二个回复说的很好,贴出来一下
这是一个很专业、也很常见的疑问,而且你引用的那个 warning 本身并不等价于“训练一定有问题”。我分层把这个问题讲清楚,结合 PPO / GRPO(RLHF / RLAIF 场景) 来看。
⸻
一、先给结论(TL;DR)
PPO / GRPO 训练代码里不显式设置 padding_side=‘left’,通常不会影响训练效果,前提是:
attention mask、position ids、loss mask 处理是正确的。那个 warning 主要针对“推理 / generation 阶段”,而不是 RL 训练本身。
⸻
二、为什么博客都在说「decoder-only 必须 left padding」?
你看到的 warning:
⚠️ A decoder-only architecture is being used, but right-padding was detected! For correct generation results, please set padding_side=‘left’
它的语境是:model.generate() 推理阶段。
原因是 decoder-only(GPT / LLaMA)的位置编码机制:
• decoder-only 是 严格因果注意力
• 每个 token 的 position id 很重要
• 如果你 right padding,那么:[真实token][真实token][PAD][PAD][PAD]
在 batch 中:
• 不同样本的「最后一个真实 token」位置不一致
• generation 时用的是 最后一个 position
• 容易导致生成时 position 对齐错误👉 所以:推理 / batch generate 时,left padding 是推荐做法
⸻
三、那为什么 PPO / GRPO 训练代码里不显式设?
关键点:PPO / GRPO ≠ generate 推理
在 PPO / GRPO 中,模型主要做三件事:
\1. 前向计算 logprob
\2. 根据 mask 算 loss
\3. 对已有 token 序列做 policy gradient👉 并不是在调用 model.generate() 做自由生成
⸻
四、训练阶段 right padding 为什么通常没问题?
1️⃣ 训练时「position id 是连续的」
在训练中,一般是这样:
input_ids
attention_mask
labels / loss_mask模型看到的是:
• PAD token 被 attention_mask=0 屏蔽
• loss 只在有效 token 上算
• position embedding 仍然是从 0,1,2,… 连续增长✅ 只要 attention mask 正确,padding 在左还是右,本质等价
⸻
2️⃣ PPO / GRPO 的 loss 不是“最后一个 token”
PPO / GRPO 的 loss 形式是:
\mathbb{E}\left[ \min(r_t A_t,; \text{clip}(r_t, 1-\epsilon, 1+\epsilon) A_t) \right]
• 是 逐 token 的 logprob
• 通过 mask 控制哪些 token 参与
• 不是用“序列最后一个 token 的 hidden state”👉 这和 generate 的需求完全不同
⸻
3️⃣ 实际工程中常见的做法
很多 PPO / GRPO 实现:
• 训练阶段:right padding
• 推理 / rollout generate 阶段:left padding这是一个非常常见、且合理的工程折中。
⸻
五、那 warning 为什么会出现?
这个 warning 的触发条件通常是:
tokenizer.padding_side == “right”
model.generate(…)也就是说:
• 不是因为“模型结构 + padding”就一定错
• 而是 decoder-only + generate + right padding如果你在 PPO 中:
• rollout 阶段自己写了 generate
• 或者调用 HF generate那 确实应该在 rollout 时用 left padding
⸻
六、总结对你问题的逐条回答
❓ 为什么 PPO / GRPO 代码中没显式设置 padding_side=‘left’?
✔️ 因为:
• 训练阶段不依赖 generate
• loss 是 masked token-level loss
• right padding 不影响数学正确性
• 显式设置反而可能影响 DataLoader / collate 简洁性⸻
❓ 不加会影响模型训练效果吗?
通常不会,只要满足:
• attention_mask 正确
• loss_mask 正确
• position_id 没被错误重写⚠️ 真正会影响训练效果的不是 padding_side,而是:
• reward / advantage 对齐是否正确
• logprob 计算是否错位
• reference model 是否一致⸻
七、给你一个「推荐实践」
如果你在写 PPO / GRPO 框架,可以用下面策略:
【训练 forward / loss】
- right padding(默认即可)
- 正确 attention_mask + loss_mask
【rollout / generate】
- tokenizer.padding_side = “left”
- batch generate
这是目前业界最稳妥、最常见的做法。
在llamafactory中,训练的时候默认是right padding,推理的时候,需要left padding。所以lf在train之前初始化了padding为right,然后在do_eval之前设置了left padding。如果我想在train的时候,一边train一边eval,貌似会存在一些问题,一直是right padding,导致F1偏低。但是最后一次eval,是发生在do_eval的时候,此时llamafactory已经修改了padding side为left。因此最后一次的eval是正常的。
在TrainingArguments中,只要设置了eval_strategy,也会把do_eval设置为True
1 | if self.do_eval is False and self.eval_strategy != IntervalStrategy.NO: |
一些零碎知识
为什么预训练的loss下界是语言的熵,而SFT的loss下界却是0呢?
预训练的目标分布是语言中天然存在的条件概率(熵>0),损失下界是语言熵;SFT 的目标分布是人工固化后的唯一序列(熵=0),损失下界可以为 0
-
:rocket: 各种rl+opd,看一下训练结果
-
:rocket:agent项目得一个,asyncio等系统学习一下
-
:star: cv+lc,一周时间准备?
- kto,dpo区别,数据准备