This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# On-Policy RL Meets Off-Policy Experts: Harmonizing SFT and RL via Dynamic Weighting (CHORD)
|
||||
|
||||
本文档介绍论文 [On-Policy RL Meets Off-Policy Experts: Harmonizing SFT and RL via Dynamic Weighting](https://arxiv.org/abs/2508.11408) 中提出的 CHORD 算法。CHORD 的核心思想是在强化学习过程中,动态融合专家数据(SFT),通过 全局权重 μ + token 级别权重 φ 的双重控制机制,在模仿与探索之间实现平衡。
|
||||
|
||||
## 算法概述
|
||||
CHORD 算法通过在 GRPO loss 中引入 **SFT loss**,实现动态混合训练。总体目标函数为:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{\text{CHORD}} = (1 - \mu) \cdot \mathcal{L}_{\text{GRPO}} + \mu \cdot \mathcal{L}_{\text{SFT}}
|
||||
$$
|
||||
|
||||
其中:
|
||||
- $\mathcal{L}_{\text{GRPO}}$:基于 on-policy 采样的强化学习损失(类似 PPO)。
|
||||
- $\mathcal{L}_{\text{SFT}}$:监督微调损失。
|
||||
- $\mu \in [0, 1]$:全局平衡系数,控制 SFT 信号在总梯度中的贡献。
|
||||
|
||||
### 参数配置(数据与批量大小)
|
||||
我们可以基于 GRPO 训练实现 CHORD 训练。
|
||||
|
||||
CHORD 需要在训练时指定额外的 SFT 数据集和批量大小:
|
||||
- `chord_sft_dataset`: 用于提供专家数据的 SFT 数据集。
|
||||
- `chord_sft_per_device_train_batch_size`: 每个设备的 SFT mini-batch 大小。
|
||||
|
||||
---
|
||||
|
||||
## 两种 CHORD 变体
|
||||
|
||||
论文提出了两种算法变体:**CHORD-µ** 和 **CHORD-ϕ**。
|
||||
|
||||
### CHORD-µ
|
||||
通过在训练过程中逐步 **衰减 μ**,实现从模仿专家到自主探索的过渡。
|
||||
|
||||
**参数:**
|
||||
- `chord_mu_peak`:μ 的峰值。
|
||||
- `chord_mu_valley` μ 的衰减终值。
|
||||
- `chord_mu_warmup_steps` μ 值上升至峰值的训练步数。
|
||||
- `chord_mu_decay_steps` μ 从峰值衰减到谷值的训练步数。
|
||||
|
||||
### CHORD-ϕ(Token 级加权)
|
||||
**CHORD-ϕ** 通过 **token-wise 权重函数 φ** 动态控制每个专家 token 的梯度贡献。
|
||||
|
||||
**φ 定义:**
|
||||
$$
|
||||
\phi(y_t^\star, \pi_\theta) = p_t \cdot (1 - p_t)
|
||||
$$
|
||||
|
||||
其中:
|
||||
- $p_t = \pi_\theta(y_t^\star \mid x, y_{<t}^\star)$:模型当前预测专家 token 的概率。
|
||||
- 当 $p_t ≈ 0.5$(模型不确定时),φ 取最大值 → 强化学习不确定的 token。
|
||||
- 当 $p_t ≈ 0$ 或 $p_t ≈ 1$,φ → 0 → 避免对过于确定或完全不会的 token 过度学习。
|
||||
|
||||
**开启 φ 加权的参数**:
|
||||
- `chord_enable_phi_function: bool = False`
|
||||
- 设置为 `True` 即启用 token-wise 权重 φ。
|
||||
|
||||
注:如果使用常数 μ 值 ,设置 chord_mu_peak 与 chord_mu_valley 相同
|
||||
|
||||
<details>
|
||||
<summary>mu值衰减与loss计算代码实现</summary>
|
||||
请参考`GRPOTrainer`的`_compute_chord_loss`方法:
|
||||
</details>
|
||||
|
||||
训练参考该[脚本](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/internal/chord.sh)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Clipped Importance Sampling Policy Optimization (CISPO)
|
||||
|
||||
|
||||
Clipped Importance Sampling Policy Optimization (CISPO) 是 [MiniMax-M1](https://arxiv.org/abs/2506.13585) 论文中提出的一种强化学习算法。相比GRPO(Group Relative Policy Optimization)算法,CISPO 对重要性采样权重(importance sampling weights)本身进行裁剪。
|
||||
|
||||
## 算法原理
|
||||
为便于理解,我们基于 GRPO 算法进行对比说明。
|
||||
|
||||
GRPO通过裁剪策略比率来限制策略更新幅度,其损失函数为:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{\text{GRPO}}(\theta) = -\mathbb{E}\left[\min\left(r_t(\theta) \cdot \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \cdot \hat{A}_t\right)\right]
|
||||
$$
|
||||
|
||||
其中 $r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)}$ 是重要性采样比。
|
||||
|
||||
在处理长推理链条时,这种裁剪方式可能导致以下问题:
|
||||
|
||||
**关键 Token 的梯度被抑制**:在复杂推理任务中,某些关键的低概率 token(如 *However, Recheck, Wait, Aha*)对于触发深度思考和推理纠错至关重要。这些 token 在旧策略 $\pi_{\theta_{\text{old}}}$ 中概率较低,当新策略试图提高其概率时,会导致较大的策略比率 $r_t(\theta)$,GRPO 的裁剪机制会将这些 token 丢弃。
|
||||
|
||||
|
||||
### CISPO 的解决方案
|
||||
|
||||
CISPO 的核心思想是:裁剪重要性采样权重,保留梯度更新。具体来说,CISPO 的损失函数为:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{\text{CISPO}}(\theta) = -\mathbb{E}\left[\text{detach}\left(\min(r_t(\theta), \epsilon_{\text{high}})\right) \cdot \hat{A}_t \cdot \log \pi_\theta(a_t|s_t)\right]
|
||||
$$
|
||||
|
||||
其中 $r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)}$ 是重要性采样比。
|
||||
|
||||
**关键机制**:
|
||||
- 对重要性采样权重进行裁剪:$\min(r_t(\theta), \epsilon_{\text{high}})$
|
||||
- **detach 操作**:裁剪后的权重不参与梯度计算,作为常数系数
|
||||
- 梯度来自 $\log \pi_\theta(a_t|s_t)$ 项,保证所有 token 都有梯度贡献
|
||||
|
||||
|
||||
## 实现细节
|
||||
CISPO 的伪代码实现如下:
|
||||
|
||||
```python
|
||||
log_ratio = per_token_logps - old_per_token_logps
|
||||
importance_weights = torch.exp(log_ratio) # r_t(θ) = π_θ / π_θ_old
|
||||
|
||||
clamped_ratios = torch.clamp(importance_weights, max=epsilon_high).detach()
|
||||
|
||||
per_token_loss = -clamped_ratios * advantages.unsqueeze(1) * per_token_logps
|
||||
```
|
||||
|
||||
## 参数设置
|
||||
|
||||
我们可以基于 `GRPOTrainer`,通过设置以下参数实现 CISPO 训练:
|
||||
|
||||
```bash
|
||||
--loss_type cispo
|
||||
--epsilon_high 5.0
|
||||
```
|
||||
|
||||
> 相比其他算法, cispo 的 epsilon_high 一般取值较大,minimax论文中未给出具体的参数设置,这里的值参考论文[ScaleRL](https://arxiv.org/pdf/2510.13786)的实验设置
|
||||
|
||||
其他训练参数参考 [GRPO参数文档](../../Command-line-parameters.md#grpo参数)
|
||||
@@ -0,0 +1,102 @@
|
||||
# DAPO: An Open-Source LLM Reinforcement Learning System at Scale
|
||||
|
||||
|
||||
|
||||
[Decoupled Clip and Dynamic sAmpling Policy Optimization (DAPO)](https://arxiv.org/abs/2503.14476)在GRPO的基础上设置了几种trick,分别是
|
||||
- [Clip Higher](#clip-higher)
|
||||
- [Dynamic Sampling](#dynamic-sampling)
|
||||
- [Token level Loss](#token-level-loss)
|
||||
- [Overlong Filtering](#overlong-filtering)
|
||||
- [Soft Overlong Punishment](#soft-overlong-punishment)
|
||||
|
||||
## Clip Higher
|
||||
PPO和GRPO使用对称裁剪范围(如±0.2)限制策略更新幅度,虽然保证了稳定性,但也制约了模型的探索能力。特别是当某些token在旧策略中概率极低时,即使当前梯度显示其应被强化(A>0),最大增幅也被严格限制。
|
||||
|
||||
DAPO使用非对称裁剪范围, 提高上裁剪范围来鼓励模型进行探索:
|
||||
|
||||
- 上界(鼓励侧)放宽至0.28
|
||||
- 下界(抑制侧)保持0.2不变
|
||||
|
||||
GRPO中,默认使用`epsilon`设置用对称裁剪范围
|
||||
|
||||
使用参数
|
||||
|
||||
- `epsilon_high` 设置上裁剪范围,此时参数`epsilon` 为下裁剪范围
|
||||
|
||||
## Dynamic Sampling
|
||||
GRPO对每个问题采样多个回答计算组间优势,
|
||||
|
||||
$$
|
||||
\hat{A}_{i,t} = \frac{R_i - \text{mean}(\{R_j\}_{j=1}^G)}{\text{std}(\{R_j\}_{j=1}^G)}
|
||||
$$
|
||||
而当生成的所有输出{oi}获得相同奖励时,组间优势等于0,会出现梯度消失导致训练效率下降
|
||||
|
||||
DAPO引入动态采样策略解决这一问题:
|
||||
|
||||
- 采样阶段跳过组间奖励标准差为0的数据
|
||||
- 持续生成样本直到填满批次
|
||||
|
||||
|
||||
使用参数
|
||||
|
||||
- `dynamic_sample true` 来开启动态采样
|
||||
- `max_resample_times` 设置最多重采样次数
|
||||
|
||||
## Token level Loss
|
||||
GRPO 在归一化损失时采用句子级归一化,这会导致损失计算具有长度偏差。
|
||||
|
||||
DAPO 使用token级归一化,避免了回答长度在损失计算上的偏差。
|
||||
|
||||
使用参数
|
||||
|
||||
- loss_type bnpo/dapo 来使用token级归一化
|
||||
|
||||
> loss_type 计算公式可参考[文档](../DeveloperGuide/loss_types.md)
|
||||
|
||||
## Overlong Filtering
|
||||
DAPO 认为被强制截断的回复的奖励噪声较大,可能会导致模型难以区分质量问题和长度问题。为此,DAPO 筛除了训练中被截断的数据,使其不参与损失计算。
|
||||
|
||||
使用参数
|
||||
|
||||
- overlong_filter 开启对超长样本的过滤
|
||||
|
||||
|
||||
## Soft Overlong Punishment
|
||||
语言模型常面临生成长度控制难题:
|
||||
|
||||
- 过长输出可能被截断,导致正确内容被误判
|
||||
|
||||
- 无约束生成长度影响实用性和计算效率
|
||||
|
||||
DAPO 设计了三段式长度惩罚函数:
|
||||
|
||||
$$
|
||||
R_{\text{length}}(L) =
|
||||
\begin{cases}
|
||||
0, & L \leq L_{\text{max}} - L_{\text{cache}} \\[10pt]
|
||||
\dfrac{(L_{\text{max}} - L_{\text{cache}}) - L}{L_{\text{cache}}}, & L_{\text{max}} - L_{\text{cache}} < L \leq L_{\text{max}} \\[10pt]
|
||||
-1, & L > L_{\text{max}}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
在长度位于 $(L_{\text{max}} - L_{\text{cache}} < L \leq L_{\text{max}})$ 区间时设置线性递增惩罚,在 $(L > L_{\text{max}})$ 时设置最大惩罚(-1)
|
||||
|
||||
|
||||
使用参数
|
||||
|
||||
- `reward_funcs soft_overlong` 启用该奖励函数
|
||||
- `soft_max_length` 设置L_max,默认等于为模型的最长输出长度(max_completion_length)。
|
||||
- `soft_cache_length`: 设置L_cache
|
||||
|
||||
## 参数设置
|
||||
综上所述,我们可以基于GRPOTrainer,设置以下参数实现 DAPO 训练。
|
||||
|
||||
| 参数 | 类型 | 值 |
|
||||
|----------------------|-----------|-------------|
|
||||
| `--loss_type` | `str` | `bnpo`/`dapo`|
|
||||
| `--epsilon_high` | `float` | `0.28` |
|
||||
| `--dynamic_sample` | `bool` | `true` |
|
||||
| `--max_resample_times` | `int` | `3` |
|
||||
| `--overlong_filter` | `bool` | `true` |
|
||||
| `--reward_funcs` | `str` | `soft_overlong`|
|
||||
| `--soft_cache_length` | `int` | `4096`|
|
||||
@@ -0,0 +1,54 @@
|
||||
# FIPO: Future-KL Influenced Policy Optimization
|
||||
|
||||
作者: [li2zhi](https://github.com/li2zhi)
|
||||
|
||||
[FIPO](https://arxiv.org/abs/2603.19835) 是一种面向长链推理的 value-free RL 方法。它保留 GRPO/DAPO 的整体训练框架,但改变 token 级策略更新的加权方式:不再让一个序列级 advantage 均匀作用到所有 token,而是用折扣累积的 Future-KL 信号判断“从当前 token 开始的后续轨迹”整体是在被增强还是被削弱。
|
||||
|
||||
## 核心思想
|
||||
|
||||
GRPO/DAPO 中,每个 response 的 token 通常共享同一个序列级 advantage:
|
||||
|
||||
$$
|
||||
\hat{A}_{i,t} = \hat{A}_{i}
|
||||
$$
|
||||
|
||||
这种做法稳定且简单,但 credit assignment 粒度较粗。FIPO 引入当前策略与旧策略在每个 token 上的 log-prob shift:
|
||||
|
||||
$$
|
||||
\Delta \log p_t = \log \pi_\theta(y_t \mid x, y_{<t}) -
|
||||
\log \pi_{\mathrm{old}}(y_t \mid x, y_{<t})
|
||||
$$
|
||||
|
||||
如果 $\Delta \log p_t > 0$,说明当前训练正在提高该 token 的概率;如果小于 0,则说明该 token 正在被压低。FIPO进一步从当前位置向后折扣累积该信号:
|
||||
|
||||
$$
|
||||
\mathrm{FutureKL}_t =
|
||||
\sum_{k=t}^{T}\gamma^{k-t} M_k \Delta \log p_k
|
||||
$$
|
||||
|
||||
其中 $M_k$ 是 completion mask,$\gamma = 2^{-1 / \text{decay\_rate}}$。`decay_rate` 越大,越远的 future token 对当前位置的影响越强;`decay_rate` 越小,Future-KL 越偏局部。然后将 Future-KL 映射为 influence weight:
|
||||
|
||||
$$
|
||||
f_t = \mathrm{clip}(\exp(\mathrm{FutureKL}_t), 1-\epsilon_f, 1+\epsilon_f)
|
||||
$$
|
||||
|
||||
最终把原本的 advantage 改成 future-aware advantage:
|
||||
|
||||
$$
|
||||
\tilde{A}_{i,t} = \hat{A}_{i} \cdot f_{i,t}
|
||||
$$
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|---------------------------|---------|--------|----------------------------------------------------------------------------------------------------------------|
|
||||
| `--loss_type` | `str` | `grpo` | 设置为`fipo` 启用 FIPO loss |
|
||||
| `--delta` | `float` | `None` | 启用后会同时用于 Future-KL 高 IS ratio token 过滤和主 loss 的 dual-clip 上限,应大于 `1 + epsilon_high`,对齐FIPO 32B训练脚本建议设置为 `10.0` |
|
||||
| `--fipo_decay_rate` | `float` | `32.0` | Future-KL 折扣半衰参数,实际折扣为`2 ** (-1 / fipo_decay_rate)` |
|
||||
| `--fipo_clip_range` | `float` | `0.2` | influence weight 裁剪范围;`0.2` 表示默认裁剪到 `[0.8, 1.2]` |
|
||||
| `--fipo_clip_high_only` | `bool` | `true` | 若为`true`,权重只裁剪到 `[1.0, 1.0 + fipo_clip_range]`,更偏向放大正 Future-KL |
|
||||
| `--fipo_safety_threshold` | `float` | `4.0` | 负 advantage 且 IS ratio 超过该阈值时,将 FIPO 权重限制到 `[0.8, 1.0]` 以避免过度惩罚 |
|
||||
|
||||
## 训练示例
|
||||
|
||||
[swift](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/internal/fipo.sh)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Group Sequence Policy Optimization
|
||||
|
||||
[Group Sequence Policy Optimization](https://arxiv.org/abs/2507.18071)中指出GRPO在计算重要性采样权重时,是在token级别进行操作的。然而,这种做法由于每个token仅采样一次,无法实现有效的分布校正,反而会在模型训练过程中引入高方差噪声,极易导致模型的梯度估计不稳定,最终造成模型训练的崩塌。因此,论文认为,优化目标的单位应该与奖励的单位保持一致。由于奖励通常是在序列级别(即完整生成的回复)给出的,因此更合理的做法是将 off-policy 校正和优化也提升到序列级别,而非 token 级别。以下是三种计算策略对比:
|
||||
|
||||
1. GRPO
|
||||
对每个 token 独立计算重要性采样比,具体公式为
|
||||
|
||||
$$
|
||||
w^{\mathrm{GRPO}}_{i,t} = \frac{\pi_\theta (y_{i, t} \mid x, y_{i, <t})}{\pi_{\theta_{\mathrm{old}}} (y_{i, t} \mid x, y_{i, <t})}
|
||||
$$
|
||||
|
||||
2. GSPO (Sequence-Level)
|
||||
|
||||
在序列级别上计算重要性采样比,具体公式为
|
||||
|
||||
$$
|
||||
w^{\mathrm{GSPO}}_{i} = \left[ \frac{\pi_\theta (y_i \mid x)}{\pi_{\theta_{\mathrm{old}}} (y_i \mid x)} \right]^{\frac{1}{|y_i|}}
|
||||
= \exp\left( \frac{1}{|y_i|} \sum_{t=1}^{|y_i|} \log \frac{\pi_\theta (y_{i, t} \mid x, y_{i, <t})}{\pi_{\theta_{\mathrm{old}}} (y_{i, t} \mid x, y_{i, <t})} \right)
|
||||
$$
|
||||
|
||||
3. GSPO-token
|
||||
GSPO-token 结合了序列级与 token 级的重要性采样思想
|
||||
|
||||
$$
|
||||
w_{i, t}^{\mathrm{GSPO-token}} = \mathrm{sg}\left[w_i^{\mathrm{GSPO}}\right] \cdot \frac{\pi_{\theta}(y_{i, t} \mid x, y_{i, < t})}{\mathrm{sg}\left[\pi_{\theta}(y_{i, t} \mid x, y_{i, < t})\right]}
|
||||
$$
|
||||
|
||||
其中,$(\mathrm{sg}[\cdot])$ 表示梯度截断(detach())。
|
||||
|
||||
> 注意:根据梯度推导(即论文中的公式(11)和(18)),当各 token 的 advantage 相同时,GSPO-token 与 GSPO 等价。当前的 GRPO 实现中,所有 token 的 advantage 实际上都是基于句子级 reward 并在 group 内进行归一化,因此在这种设置下,GSPO-token 和 GSPO 在理论上是等价的。不过,GSPO-token 为未来更细粒度(token 级别)的 advantage 提供了支持。
|
||||
|
||||
伪代码实现
|
||||
```python
|
||||
log_ratio = per_token_logps - old_per_token_logps
|
||||
# GRPO
|
||||
log_importance_weights = log_ratio
|
||||
|
||||
# GSPO (Sequence-Level)
|
||||
seq_weight = (log_ratio * mask).sum(-1) / mask.sum(-1)
|
||||
log_importance_weights = seq_weight.unsqueeze(-1) # (B,1)
|
||||
|
||||
# GSPO-token
|
||||
seq_weight = (log_ratio * mask).sum(-1) / mask.sum(-1)
|
||||
log_importance_weights = seq_weight.detach().unsqueeze(-1) + (per_token_logps - per_token_logps.detach())
|
||||
|
||||
importance_weights = torch.exp(log_importance_weights)
|
||||
```
|
||||
|
||||
我们可以在 GRPO 训练的基础上,通过参数 `--importance_sampling_level` 选择不同的算法:
|
||||
|
||||
- `importance_sampling_level token` (默认,GRPO 实现)
|
||||
- `importance_sampling_level sequence` (GSPO)
|
||||
- `importance_sampling_level sequence_token` (GSPO-token)
|
||||
|
||||
论文其他超参
|
||||
```bash
|
||||
--epsilon 3e-4 # from paper section 5.1
|
||||
--epsilon_high 4e-4 # from paper section 5.1
|
||||
--gradient_accumulation_steps 8
|
||||
--steps_per_generation 32 # from paper section 5.1 (each batch of rollout data is partitioned into four minibatches for gradient updates)
|
||||
--beta 0 # zero kl regularization https://github.com/volcengine/verl/pull/2775#issuecomment-3131807306
|
||||
```
|
||||
|
||||
训练可以参考该[脚本](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/internal/gspo.sh)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Rewards as Labels: Revisiting RLVR from a Classification Perspective
|
||||
|
||||
作者: [li2zhi](https://github.com/li2zhi)
|
||||
|
||||
[Rewards as Labels: Revisiting RLVR from a Classification Perspective](https://arxiv.org/abs/2602.05630) 针对GRPO提出把奖励视为标签,在group内分类而不是计算advantage,从而将策略优化问题转化为分类问题,以此解决GRPO Loss中存在的正样本**梯度错配**与负样本**梯度主导**问题。
|
||||
|
||||
## 背景与动机
|
||||
|
||||
GRPO目标函数
|
||||
|
||||
$$
|
||||
J_{\mathrm{GRPO}}(\theta)=\mathbb{E}_{q,o\sim\pi_{\mathrm{od}}(\cdot|q)}\left[\frac{1}{|o|}\sum_{t=1}^{|o|}\left(\min\left(\rho_tA_t,\mathrm{clip}(\rho_t,1-\epsilon,1+\epsilon)A_t\right)\right)\right]
|
||||
$$
|
||||
|
||||
其中$\rho_t=\frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$为相对概率,$A_{t}$为优势函数,故梯度为:
|
||||
|
||||
$$
|
||||
\nabla_{\theta} J_{\mathrm{GRPO}} = \mathbb { E } \left[ \frac { 1 } { | o | } \sum _ { t = 1 } ^ { | o | } \mathbb { I } _ { \mathrm { clip } } \cdot A _ { t } e ^ { s _ { t } } \nabla _ { \theta } \log \pi _ { \theta } \left( o _ { t } | q \right) \right]
|
||||
$$
|
||||
|
||||
其中$s_t=\log\frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$作为token的相对对数概率,$\mathbb { I } _ { \mathrm { clip } }$为指示函数
|
||||
|
||||
故 GRPO 对单 token 的梯度权重为:
|
||||
|
||||
$$
|
||||
|\mathcal{W}_{\mathrm{GRPO}}|=\left\{ \begin{array} {ll}\left|A\cdot e^s\right|, & \mathrm{if~}\mathbb{I}_{\mathrm{clip}}=1, \\ 0, & \text{otherwise.} \end{array}\right.
|
||||
$$
|
||||
|
||||

|
||||
|
||||
- 正样本的梯度错配(Gradient Misassignment):对正样本来说,随着相对概率$s$变小,梯度更新幅度反而越弱。这违背直觉,因为模型对“不太自信”的正确 token 本来就需要更大的更新幅度来强化,但更多的梯度权重却放到更“自信”的 token,没学好的 token 得不到足够的重视。
|
||||
|
||||
- 负样本的梯度主导(Gradient Domination):对负样本来说,随着相对概率$s$变小,梯度更新幅度呈指数级增加。这意味着,只要出现几个模型“盲目自信”的错误 token,它们产生的巨大梯度就会把同组内其他负样本的信号淹没。由于缺乏上限保护,模型在处理这些错误样本时可能会产生过大的参数更新,让训练过程变得不太可控。
|
||||
|
||||
为解决上述问题,Real提出将奖励直接视为标签然后进行组内的样本分类训练
|
||||
|
||||

|
||||
|
||||
分类的logits分值设计:
|
||||
|
||||
$$
|
||||
\bar{s}^k=\frac{1}{|o^k|}\sum_{t=1}^{|o^k|}\left(\log\frac{\pi_\theta(o_t^k\mid q)}{\pi_{\mathrm{old}}(o_t^k\mid q)}\right)
|
||||
$$
|
||||
|
||||
- $\bar{s}^k > 0$: 表示该样本在当前策略下生成的概率比旧策略整体更高,模型倾向于**增强**该样本。
|
||||
- $\bar{s}^k < 0$: 表示该样本在当前策略下生成的概率比旧策略整体更低,模型倾向于**抑制**该样本。
|
||||
|
||||
损失函数设计:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{REAL}=\log\left(1+\sum_{\mathcal{O}_+}e^{-\bar{s}^i/\tau}\right)+\log\left(1+\sum_{\mathcal{O}_-}e^{\bar{s}^j/\tau}\right)
|
||||
$$
|
||||
|
||||
梯度特性:
|
||||
$$
|
||||
|\mathcal{W}_{\mathrm{REAL}}|=
|
||||
\begin{cases}
|
||||
\frac{1}{\tau}\frac{1}{1+C_{+}e^{\bar{s}^{k}/\tau}}, & r=1 \\
|
||||
\\
|
||||
\frac{1}{\tau}\frac{1}{1+C_{-}e^{-\bar{s}^{k}/\tau}}, & r=0 & & &
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
## 参数设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|-------------------|---------|-------|-------------------|
|
||||
| `--loss_type` | `str` | - | 设置为 `real` |
|
||||
| `--real_tau` | `float` | `0.5` | 温度参数,控制决策边界锐度 |
|
||||
|
||||
训练脚本参考
|
||||
|
||||
[swift](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/internal/real.sh)
|
||||
|
||||
## 注意事项
|
||||
|
||||
设置参数时,确保 per_device_train_batch_size 能够被 num_generations 整除,以此保证单个训练batch中能拿到完整的 group 进行分类。
|
||||
@@ -0,0 +1,87 @@
|
||||
# REINFORCE++: An Efficient RLHF Algorithm with Robustness to Both Prompt and Reward Models
|
||||
|
||||
[REINFORCE++ Baseline](https://arxiv.org/abs/2501.03262) 是 REINFORCE++ 算法的简化版本,适用于 outcome rewards(response-level 标量奖励)。它与 GRPO 类似,对每个prompt输入采样多条模型输出,并使用组内 baseline 来估计优势函数,主要区别在于标准化时使用的统计量不同。
|
||||
|
||||
|
||||
## 算法原理
|
||||
为便于理解,我们基于 GRPO(Group Relative Policy Optimization)算法进行对比说明。
|
||||
|
||||
GRPO 和 REINFORCE++ Baseline 都采用组内对比的方式来估计优势函数,主要区别在于:
|
||||
|
||||
### 区别1:标准化时使用的统计量不同
|
||||
|
||||
**GRPO (Group Relative Policy Optimization)**
|
||||
|
||||
对每个 prompt 生成 $G$ 个响应样本,使用**组内所有样本的均值和标准差**进行标准化:
|
||||
|
||||
$$
|
||||
\hat{A}_{i} = \frac{R_i - \text{mean}(\{R_j\}_{j=1}^G)}{\text{std}(\{R_j\}_{j=1}^G)}
|
||||
$$
|
||||
|
||||
当设置 `scale_rewards='batch'` 时,使用**原始奖励的批次 std**:
|
||||
|
||||
$$
|
||||
\hat{A}_{i} = \frac{R_i - \text{mean}(\{R_j\}_{j=1}^G)}{\text{std}(\{R_j\}_{j=1}^{N})}
|
||||
$$
|
||||
|
||||
其中 $N$ 是批次中所有样本数。
|
||||
|
||||
**REINFORCE++ Baseline**
|
||||
|
||||
对每个 prompt 生成 $G$ 个响应样本,先减去组内均值,再使用**减去组内均值后的奖励**的标准差进行标准化:
|
||||
|
||||
$$
|
||||
\begin{align}
|
||||
\tilde{A}_{i} &= R_i - \text{mean}(\{R_j\}_{j=1}^G) \\
|
||||
\hat{A}_{i} &= \frac{\tilde{A}_{i}}{\text{std}(\{\tilde{A}_k\}_{k=1}^{N})}
|
||||
\end{align}
|
||||
$$
|
||||
|
||||
其中 $N$ 是批次中所有样本数。
|
||||
|
||||
**关键区别**:
|
||||
- **GRPO**:标准化时使用**原始奖励 $R$** 的标准差
|
||||
- **REINFORCE++**:标准化时使用**减去组内均值后的奖励 $\tilde{A}$** 的标准差
|
||||
|
||||
### 区别2: KL 散度正则化
|
||||
|
||||
与 RLOO 类似,REINFORCE++ Baseline 将 KL 散度整合到奖励项中:
|
||||
|
||||
$$
|
||||
R'_i = R_i - \beta \cdot \text{KL}(\pi_\theta || \pi_{\text{ref}})
|
||||
$$
|
||||
|
||||
其中 $\beta$ 是 KL 散度的权重系数(对应参数 `beta`),$\pi_{\text{ref}}$ 是参考策略。
|
||||
|
||||
## 参数设置
|
||||
|
||||
我们可以基于 `GRPOTrainer`,通过设置以下参数实现 REINFORCE++ Baseline 训练:
|
||||
|
||||
```bash
|
||||
--advantage_estimator reinforce_plus_plus
|
||||
--scale_rewards batch
|
||||
--kl_in_reward true
|
||||
```
|
||||
|
||||
训练可以参考该[脚本](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/internal/reinforce_plus_plus.sh)
|
||||
|
||||
### 重要参数说明
|
||||
|
||||
- **`--advantage_estimator`**:选择优势函数估计方法
|
||||
- `grpo`(默认):标准化时使用原始奖励的标准差
|
||||
- `reinforce_plus_plus`:标准化时使用减去组内均值后的奖励的标准差
|
||||
|
||||
- **`--kl_in_reward`**:控制 KL 散度正则化项的处理位置
|
||||
- `false`:KL 散度作为损失函数的独立正则化项(GRPO 默认)
|
||||
- `true`:KL 散度直接从奖励中扣除(REINFORCE++ 原始实现)
|
||||
|
||||
- **`--scale_rewards`**:控制标准化方式
|
||||
- `group`(默认):组内标准化
|
||||
- `batch`:全局批次标准化(REINFORCE++原始实现)
|
||||
- `none`:不进行标准化
|
||||
|
||||
- **`--num_generations`**:每个 prompt 生成的样本数量 $G$
|
||||
|
||||
- **`--beta`**:KL 散度正则化系数 $\beta$
|
||||
|
||||
其他参数参考 [GRPO参数](../../Command-line-parameters.md#grpo参数)
|
||||
@@ -0,0 +1,92 @@
|
||||
# REINFORCE Leave-One-Out (RLOO)
|
||||
|
||||
[REINFORCE Leave-One-Out (RLOO)](https://arxiv.org/abs/2402.14740) 基于经典的 REINFORCE 策略梯度方法,通过留一法(Leave-One-Out)构造无偏的优势函数基线。
|
||||
|
||||
## 算法原理
|
||||
|
||||
为便于理解,我们基于 GRPO(Group Relative Policy Optimization)算法进行对比说明。
|
||||
|
||||
GRPO 和 RLOO 都采用组内对比的方式来估计优势函数,避免了全局基线估计带来的高方差问题。两者的核心区别主要体现在以下两个方面:
|
||||
|
||||
### 区别1:优势函数基线的构造方法
|
||||
|
||||
**1. GRPO (Group Relative Policy Optimization)**
|
||||
|
||||
GRPO 对每个 prompt 生成 $G$ 个响应样本,使用**组内所有样本的均值和标准差**进行标准化:
|
||||
|
||||
$$
|
||||
\hat{A}_{i} = \frac{R_i - \text{mean}(\{R_j\}_{j=1}^G)}{\text{std}(\{R_j\}_{j=1}^G)}
|
||||
$$
|
||||
|
||||
其中:
|
||||
- $R_i$ 是第 $i$ 个样本的奖励值
|
||||
- $\text{mean}(\{R_j\}_{j=1}^G) = \frac{1}{G}\sum_{j=1}^G R_j$ 是组内均值
|
||||
- $\text{std}(\{R_j\}_{j=1}^G)$ 是组内标准差
|
||||
|
||||
**2. RLOO (REINFORCE Leave-One-Out)**
|
||||
|
||||
RLOO 对每个 prompt 生成 $K$ 个响应样本,使用 **留一法(Leave-One-Out)** 构造基线,即第 $i$ 个样本的基线为除自己外的其他 $K-1$ 个样本的均值:
|
||||
|
||||
$$
|
||||
\hat{A}_{i} = R_i - \frac{1}{K-1}\sum_{j \neq i} R_j
|
||||
$$
|
||||
|
||||
这个公式可以等价地改写为:
|
||||
|
||||
$$
|
||||
\hat{A}_{i} = \frac{K}{K-1} \left(R_i - \bar{R}\right)
|
||||
$$
|
||||
|
||||
其中 $\bar{R} = \frac{1}{K}\sum_{j=1}^K R_j$ 是组内所有样本的均值。
|
||||
|
||||
> **说明**:这里使用 $K$ 对齐论文符号,与 GRPO 中的 $G$ 含义一致,均对应配置参数 `num_generations`
|
||||
|
||||
**为什么使用留一法?**
|
||||
|
||||
留一法的关键优势在于**无偏性**。对于第 $i$ 个样本,其奖励 $R_i$ 和基线 $\frac{1}{K-1}\sum_{j \neq i} R_j$ 是独立的,因此优势估计是无偏的。相比之下,如果使用包含自身的均值作为基线,会引入偏差。
|
||||
|
||||
### 区别2:KL 散度正则化项的处理方式
|
||||
|
||||
为防止策略偏离参考策略过远,两种算法都引入了 KL 散度正则化,但处理方式不同:
|
||||
|
||||
**GRPO**:将 KL 散度作为独立的正则化项添加到[损失函数](../GetStarted/GRPO.md#算法原理)中:
|
||||
|
||||
$$
|
||||
\mathcal{L}(\theta) = -\mathbb{E}\left[\hat{A}_i \log \pi_\theta(a_i|s_i)\right] + \beta \cdot \text{KL}(\pi_\theta || \pi_{\text{ref}})
|
||||
$$
|
||||
|
||||
**RLOO**:将 KL 散度直接整合到奖励项中,构造修正后的奖励:
|
||||
|
||||
$$
|
||||
R'_i = R_i - \beta \cdot \text{KL}(\pi_\theta || \pi_{\text{ref}})
|
||||
$$
|
||||
|
||||
其中 $\beta$ 是 KL 散度的权重系数(对应参数 `beta`),$\pi_{\text{ref}}$ 是参考策略(通常是 SFT 模型或初始策略)。
|
||||
|
||||
## 参数设置
|
||||
|
||||
我们可以基于 `GRPOTrainer`,通过设置以下参数实现 RLOO 训练:
|
||||
```bash
|
||||
# 基本 RLOO 配置
|
||||
--advantage_estimator rloo # 使用 RLOO 的留一法优势函数计算
|
||||
--kl_in_reward true # 将 KL 散度项整合到奖励中(RLOO 默认方式)
|
||||
```
|
||||
|
||||
训练可以参考该[脚本](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/internal/rloo.sh)
|
||||
|
||||
### 重要参数说明
|
||||
|
||||
- **`--advantage_estimator`**:选择优势函数估计方法
|
||||
- `grpo`(默认):使用组内均值和标准差进行标准化
|
||||
- `rloo`:使用留一法(Leave-One-Out)构造基线
|
||||
|
||||
- **`--kl_in_reward`**:控制 KL 散度正则化项的处理位置
|
||||
- `false`:KL 散度作为损失函数的独立正则化项(GRPO 方式)
|
||||
- `true`:KL 散度直接从奖励中扣除,构造修正后的奖励(RLOO 方式)
|
||||
|
||||
- **`--num_generations`**:每个 prompt 生成的样本数量 $K$
|
||||
|
||||
- **`--beta`**:KL 散度正则化系数 $\beta$
|
||||
- 控制策略更新的保守程度
|
||||
|
||||
其他参数与 [GRPO参数](../../Command-line-parameters.md#grpo参数)一致
|
||||
@@ -0,0 +1,91 @@
|
||||
# Soft Adaptive Policy Optimization (SAPO)
|
||||
|
||||
[Soft Adaptive Policy Optimization (SAPO)](https://arxiv.org/abs/2511.20347) 针对 GRPO 中硬裁剪(hard clipping)带来的问题,提出了一种基于温度控制的软门控(soft gate)机制,用于平滑地衰减离策略更新,同时保留有用的学习信号。
|
||||
|
||||
## 背景与动机
|
||||
|
||||
在强化学习训练 LLM 时,GRPO 通过计算 token 级别的重要性采样比(Importance Sampling Ratio)来处理 off-policy 训练:
|
||||
|
||||
$$
|
||||
r_t = \frac{\pi_\theta(y_t|x, y_{<t})}{\pi_{\theta_{\mathrm{old}}}(y_t|x, y_{<t})}
|
||||
$$
|
||||
|
||||
然而,token 级别的重要性采样比往往表现出高方差,这一现象在以下情况下可能更严重:
|
||||
- **长文本生成**
|
||||
- **MoE 模型的路由异质性**:采样时的 old-policy 模型与训练模型可能使用不同的专家路由,导致 logps 差异显著放大
|
||||
|
||||
为此,GRPO 通过硬裁剪来限制策略更新的幅度:
|
||||
|
||||
$$
|
||||
L^{\mathrm{GRPO}} = -\min\left( r_t \cdot A, \mathrm{clip}(r_t, 1-\epsilon, 1+\epsilon) \cdot A \right)
|
||||
$$
|
||||
|
||||
**硬裁剪的困境**:硬裁剪难以在稳定性和学习效率之间取得平衡——裁剪范围过严格会限制有效样本的数量,而过宽松则会引入离策略样本的噪声梯度,导致训练不稳定。
|
||||
|
||||
## SAPO 方法
|
||||
|
||||
SAPO 使用温度控制的 sigmoid 软门控函数替代硬裁剪,实现平滑的梯度衰减。
|
||||
|
||||
### 软门控函数
|
||||
|
||||
SAPO 的核心是使用 sigmoid 函数对重要性采样比进行软门控:
|
||||
|
||||
对于正向优势($A > 0$),使用正向门控:
|
||||
|
||||
$$
|
||||
g^{+}_t = \sigma\left( \tau_{\mathrm{pos}} \cdot (r_t - 1) \right) \cdot \frac{4}{\tau_{\mathrm{pos}}}
|
||||
$$
|
||||
|
||||
对于负向优势($A < 0$),使用负向门控:
|
||||
|
||||
$$
|
||||
g^{-}_t = \sigma\left( \tau_{\mathrm{neg}} \cdot (r_t - 1) \right) \cdot \frac{4}{\tau_{\mathrm{neg}}}
|
||||
$$
|
||||
|
||||
其中:
|
||||
- $\sigma(\cdot)$ 是 sigmoid 函数
|
||||
- $\tau_{\mathrm{pos}}$ 和 $\tau_{\mathrm{neg}}$ 是温度参数,控制门控函数的斜率
|
||||
- $r_t$ 是重要性采样比
|
||||
|
||||
### SAPO 损失函数
|
||||
|
||||
$$
|
||||
L^{\mathrm{SAPO}} = -g_t \cdot A
|
||||
$$
|
||||
|
||||
其中 $g_t = g^{+}_t$ 当 $A > 0$,$g_t = g^{-}_t$ 当 $A < 0$。
|
||||
|
||||
### 温度参数
|
||||
|
||||
温度参数 $\tau$ 控制软门控函数的衰减速率,数值越大,衰减越快。
|
||||
|
||||

|
||||
|
||||
论文指出正向优势会提升采样token的logit,并降低所有未采样token的logit;负向优势相反,提高许多未采样token的logit,可能会扩散到大量无关token上,带来一定的不稳定性。所以论文推荐设置温度 $\tau_\text{neg} > \tau_\text{pos}$,来使负向奖励的token梯度衰减更快,提升训练的稳定性和性能。
|
||||
|
||||
论文默认推荐 $\tau_{\mathrm{pos}} = 1.0$,$\tau_{\mathrm{neg}} = 1.05$。
|
||||
|
||||
## 参数设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--loss_type` | `str` | - | 设置为 `sapo` |
|
||||
| `--tau_pos` | `float` | `1.0` | 正向优势的温度参数,控制门控斜率 |
|
||||
| `--tau_neg` | `float` | `1.05` | 负向优势的温度参数,控制门控斜率 |
|
||||
|
||||
```bash
|
||||
swift rlhf \
|
||||
--rlhf_type grpo \
|
||||
--loss_type sapo \
|
||||
--tau_pos 1.0 \
|
||||
--tau_neg 1.05 \
|
||||
# ... 其他参数
|
||||
```
|
||||
|
||||
训练脚本参考
|
||||
|
||||
- [swift](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/internal/sapo.sh)
|
||||
- [megatron swift](https://github.com/modelscope/ms-swift/blob/main/examples/megatron/grpo/sapo.sh)
|
||||
|
||||
> SAPO 的软门控机制仅在 off-policy 训练下生效。
|
||||
> SAPO 中的重要性采样粒度为 token 级别(即 importance_sampling_level 默认设置为 token),与 GSPO 冲突。
|
||||
@@ -0,0 +1,95 @@
|
||||
# DeepEyes: Incentivizing "Thinking with Images" via Reinforcement Learning
|
||||
|
||||
## 原理介绍
|
||||
|
||||
[DeepEyes论文](https://arxiv.org/abs/2505.14362) 提出了一种利用强化学习使模型具备“think with images”(以图辅助思考)能力的方法。该方法通过端到端的强化学习,模型能力自发涌现,无需额外的 SFT(监督微调)过程。模型内置图像定位能力,能够主动调用“图像放大工具”:在推理过程中,模型会自动选取图片中的具体区域进行放大和裁剪,将处理后的区域信息进行进一步推理,实现视觉与文本的链式推理。
|
||||
|
||||

|
||||
|
||||
## 最佳实践
|
||||
|
||||
**数据集下载与注册**
|
||||
|
||||
下载 DeepEyes 官方训练数据集到本地
|
||||
```bash
|
||||
# modelscope
|
||||
modelscope download --dataset Lixiang/ChenShawn-DeepEyes-Datasets-47k
|
||||
|
||||
# huggingface
|
||||
huggingface-cli download ChenShawn/DeepEyes-Datasets-47k --repo-type=dataset
|
||||
```
|
||||
|
||||
数据集内有三个parquet文件,`swift/dataset/data/dataset_info.json` 文件中分别进行注册,将数据集中的 `prompt` 列重命名为 `messages`
|
||||
|
||||
```json
|
||||
{
|
||||
"ms_dataset_id": "path/to/data_0.1.2_visual_toolbox_v2.parquet",
|
||||
"columns": {
|
||||
"prompt": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ms_dataset_id": "path/to/data/data_thinklite_reasoning_acc.parquet",
|
||||
"columns": {
|
||||
"prompt": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ms_dataset_id": "path/to/data/data_v0.8_visual_toolbox_v2.parquet",
|
||||
"columns": {
|
||||
"prompt": "messages"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在本地注册论文中所用到的奖励函数和工具调用逻辑,实现可以参考[DeepEyes实现示例](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/deepeyes/deepeyes_plugin.py)
|
||||
|
||||
**部署验证模型**
|
||||
|
||||
Deepeyes 的奖励函数依赖生成式奖励模型对模型生成结果与标准答案进行对比评估,为了加速这一环节,推荐对模型进行部署。
|
||||
|
||||
假设使用 Qwen2.5-VL-72B-Instruct 模型进行评估,参考以下部署命令
|
||||
|
||||
```bash
|
||||
# 4*80G
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
||||
swift deploy \
|
||||
--model Qwen/Qwen2.5-VL-72B-Instruct \
|
||||
--infer_backend vllm \
|
||||
--vllm_tensor_parallel_size 4 \
|
||||
```
|
||||
|
||||
在 plugin 文件中,使用OpenAI接口进行调用,参考[奖励模型文档](../DeveloperGuide/reward_model.md#外部部署)
|
||||
|
||||
|
||||
训练参考该[脚本](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/deepeyes/deepeyes.sh)
|
||||
|
||||
## 实现细节
|
||||
|
||||
[DeepEyes实现示例](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/deepeyes/deepeyes_plugin.py)参考[官方实现](https://github.com/Visual-Agent/DeepEyes/blob/main/verl/utils/reward_score/vl_agent.py) 给出了 DeepEyes 训练插件的样例代码,涵盖了奖励函数与多轮交互调用的相关逻辑。
|
||||
|
||||
**数据集数据**如下
|
||||
|
||||
| 数据集文件名 | data_source | 对应评分函数 | 工具调用 |
|
||||
|------------------------------------------|-----------------------|----------------------------------|------------------|
|
||||
| data_v0.8_visual_toolbox_v2.parquet | chart | vl_agent.compute_score | True (image_zoom_in_tool) |
|
||||
| data_0.1.2_visual_toolbox_v2.parquet | vstar | vl_agent.compute_score | True (image_zoom_in_tool) |
|
||||
| data_thinklite_reasoning_acc.parquet | thinklite_eureka | vl_agent.compute_score_math | False |
|
||||
|
||||
|
||||
**注意**:多模态大模型在处理图像输入时,可能会对图像进行预处理(例如受 max_pixels 参数限制的裁剪或缩放等操作)。当调用图像放大工具 image_zoom_in_tool 时,模型会根据输入图像输出裁剪后的 bbox。因此,在调用图像放大工具时,需要确保输入的是经过预处理后的图像。示例代码展示了 Qwen2.5-VL 系列模型的实现方式:
|
||||
|
||||
```python
|
||||
from qwen_vl_utils import fetch_image
|
||||
# 这里的images尚未经过图像处理
|
||||
infer_request.images
|
||||
# 通过加载为PIL.Image格式,进行裁剪(使用环境变量MAX_PIXELS时的处理)
|
||||
img = fetch_image({'image': load_pil_image(infer_request.images[0])})
|
||||
```
|
||||
|
||||
**工具奖励**
|
||||
|
||||
论文中指出当最终答案正确,且轨迹至少使用一个工具时给予工具奖励。为了避免模型生成的工具调用是无效的,我们通过图像数量而不是`<tool_call>` 等token进行判断。
|
||||
```python
|
||||
tool_reward = 1.0 if num_image > 1 and acc_reward > 0.5 else 0.0
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning
|
||||
|
||||
[论文](https://arxiv.org/abs/2506.01939)发现在以 RLVR等方法训练大型语言模型推理能力时,驱动学习进步的关键在于一小部分高熵“少数 token”,而并非大多数信息熵低的 token。
|
||||
|
||||
论文指出,在模型推理的 token 分布中,只有极少数信息熵较高的 token 起到了主导作用。这些 token 往往出现在推理和决策路径分歧最大的关键节点(如 "wait"、"since" 等),决定了模型能否习得复杂推理任务。而大多数熵低的 token 对模型推理能力的提升作用有限。论文提出只对高熵 token 计算策略梯度、舍弃低熵 token 的梯度。
|
||||
|
||||
|
||||
token 熵公式如下
|
||||
|
||||
$
|
||||
H_t := -∑_{j=1}^{V} p_{t,j} \log p_{t,j}, \qquad where (p_{t,1}, ···, p_{t,V}) = \mathbf{p}_t = π_θ(\cdot | \mathbf{q}, \mathbf{o}_{<t}) = \text{Softmax}(\frac{\mathbf{z}_t}{T})
|
||||
$
|
||||
|
||||
其中
|
||||
- $\pi_\theta$:参数为 $\theta$ 的模型
|
||||
- $\mathbf{q}$:输入查询(input query)。
|
||||
- $\mathbf{o}_{<t} = (o_1, o_2, \cdots, o_{t-1})$:时间步 $t$ 之前已生成的 token 序列。
|
||||
- $V$:词表大小(vocabulary size)。
|
||||
- $\mathbf{z}_t \in \mathbb{R}^V$:时间步 $t$ 的 pre-softmax 逻辑值(logits)。
|
||||
- $\mathbf{p}_t \in \mathbb{R}^V$:模型对词表的概率分布。
|
||||
- $T \in \mathbb{R}$:解码温度(decoding temperature),控制分布的平滑程度。
|
||||
|
||||
熵的计算对象:$H_t$ 是 token 生成分布 $\mathbf{p}_t$ 的熵,用于衡量训练策略 $\pi_\theta$ 在给定上下文 $(\mathbf{q}, \mathbf{o}_{<t})$ 下的不确定性。
|
||||
|
||||
> "Token entropy" $H_t$ 始终指向位置 $t$ 的生成分布 $\mathbf{p}_t$ 的不确定性,而非 token $o_t$ 本身的属性。即$H_t$ 是位置 $t$ 对应分布 $\mathbf{p}_t$ 的熵,与采样得到的 token $o_t$ 无关。
|
||||
|
||||
|
||||
在实践中,我们可以在 GRPO 训练中通过参数 `top_entropy_quantile` 控制训练范围。论文实验设置该参数为 0.2,即每次仅对处于熵分布前 20% 的 token 进行训练优化。
|
||||
|
||||
同时使用参数`log_entropy`,可以记录训练过程中的熵值变化,参考[文档](../GetStarted/GRPO.md#logged-metrics)
|
||||
@@ -0,0 +1,19 @@
|
||||
Advanced Research
|
||||
===============
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
entropy_mask.md
|
||||
CISPO.md
|
||||
DAPO.md
|
||||
deepeyes.md
|
||||
FIPO.md
|
||||
GSPO.md
|
||||
CHORD.md
|
||||
RLOO.md
|
||||
REINFORCEPP.md
|
||||
REAL.md
|
||||
router_replay.md
|
||||
SAPO.md
|
||||
training_inference_mismatch.md
|
||||
treepo.md
|
||||
@@ -0,0 +1,104 @@
|
||||
# Router Replay (R2/R3)
|
||||
|
||||
**TL;DR**: 在 MoE 模型的 RL 训练中,训练引擎与推理引擎之间的路由(expert routing)不一致会显著放大训推偏差,甚至导致训练崩溃。Router Replay 通过在训练前向传播中回放固定的路由掩码来消除这一不一致。根据回放来源的不同,分为 R2(Vanilla Routing Replay)和 R3(Rollout Routing Replay)两种策略。
|
||||
|
||||
## Background
|
||||
|
||||
### MoE RL 中的三个策略
|
||||
|
||||
在 MoE 模型的 GRPO 训练中,存在三个不同阶段的策略,它们共享相同的模型权重,但路由行为可能不同:
|
||||
|
||||
| 策略 | 符号 | 路由结果 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| **训练策略** | $\pi_\theta$ | $e^{\pi}_t$ | 梯度更新中的模型 |
|
||||
| **Old Policy** | $\pi_{\theta_{\text{old}}}$ | $e^{\pi}_{\text{old},t}$ | 批次更新前的模型状态 |
|
||||
| **Rollout Policy** | $\mu_{\theta_{\text{old}}}$ | $e^{\mu}_{\text{old},t}$ | 推理引擎(如vLLM)中的采样策略,权重与 old policy 相同,但 kernel 实现、精度等差异导致路由不同 |
|
||||
|
||||
其中,$\pi_{\theta_{\text{old}}}$ 和 $\mu_{\theta_{\text{old}}}$ 的权重在采样时完全一致,但由于推理引擎与训练引擎的实现差异(如算子实现),即使输入相同,路由结果也可能不同。
|
||||
|
||||
### 训推不一致的分解
|
||||
|
||||
根据 [论文](https://arxiv.org/abs/2507.18071) 的分析,token 级重要性采样比可以分解为两个因子:
|
||||
|
||||
$$
|
||||
\frac{\pi_\theta(y_t|x, y_{<t})}{\mu_{\theta_{\text{old}}}(y_t|x, y_{<t})} = \underbrace{\frac{\pi_{\theta_{\text{old}}}(y_t|x, y_{<t})}{\mu_{\theta_{\text{old}}}(y_t|x, y_{<t})}}_{\text{training-inference discrepancy}} \times \underbrace{\frac{\pi_\theta(y_t|x, y_{<t})}{\pi_{\theta_{\text{old}}}(y_t|x, y_{<t})}}_{\text{policy staleness}}
|
||||
$$
|
||||
|
||||
对于 MoE 模型,专家路由与这两个因子深度耦合:
|
||||
|
||||
- **Training-inference discrepancy**:训练引擎和推理引擎的路由不一致($e^{\pi}_{\text{old},t} \neq e^{\mu}_{\text{old},t}$),放大输出差异
|
||||
- **Policy staleness**:随着 mini-batch 更新,路由也随之漂移($e^{\pi}_t \neq e^{\pi}_{\text{old},t}$),进一步偏离采样时的策略
|
||||
|
||||
## R2: Vanilla Routing Replay
|
||||
|
||||
R2 的核心思想是在梯度更新时,**回放 old policy 在训练引擎中确定的路由**($e^{\pi}_{\text{old},t}$)。
|
||||
|
||||
### 原理
|
||||
|
||||
在训练前向传播中,先用 old policy 的权重做一次前向,记录每一层 MoE Router 选择的 expert indices,然后在训练模型 $\pi_\theta$ 的前向传播中强制使用这些 indices:
|
||||
|
||||
$$
|
||||
g_{\text{replay},i} = \frac{I^{\pi}_{\text{old},i} \cdot \exp(s_{\text{train},i})}{\sum_j I^{\pi}_{\text{old},j} \cdot \exp(s_{\text{train},j})}
|
||||
$$
|
||||
|
||||
其中 $I^{\pi}_{\text{old}}$ 是 old policy 的路由掩码,$s_{\text{train}}$ 是训练模型计算的 router logits。softmax 仍作用于训练 logits,因此梯度可以正常回传到 router 权重。
|
||||
|
||||
### 特性
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| **首个 mini-batch**(on-policy) | $\theta = \theta_{\text{old}}$,故 $e^{\pi}_t = e^{\pi}_{\text{old},t}$,**target policy 不变**(无 bias) |
|
||||
| **后续 mini-batch**(off-policy) | $\theta \neq \theta_{\text{old}}$,故 $e^{\pi}_t \neq e^{\pi}_{\text{old},t}$,**target policy 被改变**(有 bias),但 policy staleness 被控制 |
|
||||
|
||||
## R3: Rollout Routing Replay
|
||||
|
||||
R3 的核心思想是在训练前向传播中,**回放 rollout policy 在推理引擎中确定的路由**($e^{\mu}_{\text{old},t}$)。
|
||||
|
||||
### 原理
|
||||
|
||||
在推理引擎(如 vLLM)采样时,额外记录每个 token 在每一层 MoE Router 的 expert indices(路由掩码),然后将这些掩码传递到训练引擎,在 $\pi_\theta$ 的前向传播中强制使用:
|
||||
|
||||
$$
|
||||
g_{\text{replay},i} = \frac{I^{\mu}_{\text{old},i} \cdot \exp(s_{\text{train},i})}{\sum_j I^{\mu}_{\text{old},j} \cdot \exp(s_{\text{train},j})}
|
||||
$$
|
||||
|
||||
### 与其他方法的兼容性
|
||||
|
||||
- R3 与 **GSPO** 正交,组合使用可进一步提升
|
||||
- R3 与 **TIS** 组合不一定有增益(R3 已从根源消除不一致,TIS 的额外修正可能多余)
|
||||
- Router Replay 与 **Clipping** 在 off-policy 训练中缺一不可
|
||||
|
||||
## Router Mask Caching
|
||||
|
||||
R3 论文还提出路由掩码可以与 KV Cache 一起缓存:对于相同的前缀 token,MoE Router 的输出是确定性的,因此路由掩码可以随 prefix KVCache 一起存储和复用。这在多轮 Agent 场景(tool calling)中尤为重要,避免了重新 prefill 前缀来获取路由掩码,整体 rollout 延迟开销 < 3%。
|
||||
|
||||
## Swift 实现
|
||||
|
||||
### 参数设置
|
||||
|
||||
通过 `--router_replay_mode` 参数选择路由回放策略:
|
||||
|
||||
| 参数值 | 说明 |
|
||||
|--------|------|
|
||||
| `disabled`(默认) | 不启用路由回放 |
|
||||
| `R2` | Vanilla Routing Replay:在训练引擎中记录 old policy 路由并回放 |
|
||||
| `R3` | Rollout Routing Replay:从推理引擎导出路由掩码并在训练中回放 |
|
||||
|
||||
环境依赖:
|
||||
|
||||
- R3 需要 vLLM ≥ 0.14.0 以支持返回 `routed_experts` 信息。
|
||||
- 当前 Router Replay 仅在 Megatron backend 下可用,需要 megatron-core ≥ 0.16.0。
|
||||
|
||||
### 与训推校正的关系
|
||||
|
||||
Router Replay 与 [Training-Inference Mismatch](training_inference_mismatch.md) 中介绍的重要性采样校正(IS correction)是互补的:
|
||||
|
||||
- **IS correction**:在 loss 层面通过权重修正训推概率偏差
|
||||
- **Router Replay**:在模型结构层面通过固定路由消除偏差来源
|
||||
|
||||
## 参考资料
|
||||
|
||||
1. [Stabilizing MoE Reinforcement Learning by Aligning Training and Inference Routers](https://arxiv.org/abs/2510.11370)
|
||||
2. [Group Sequence Policy Optimization](https://arxiv.org/abs/2507.18071)
|
||||
3. [Stabilizing Reinforcement Learning with LLMs: Formulation and Practices](https://arxiv.org/abs/2512.01374)
|
||||
4. [Megatron Core Router Replay Design Document](https://docs.nvidia.com/megatron-core/developer-guide/nightly/api-guide/router_replay.html)
|
||||
@@ -0,0 +1,239 @@
|
||||
# Training-Inference-Mismatch
|
||||
|
||||
|
||||
**TL;DR**: GRPO 引入 vLLM 加速采样过程的同时,也引入了训练-推理不一致(Training-Inference Mismatch)的问题,从而可能影响训练稳定性。本文将解释这个问题的背景、原因以及相应的解决方案。
|
||||
|
||||
## Background
|
||||
|
||||
### GRPO 的基本假设
|
||||
|
||||
GRPO (Group Relative Policy Optimization) 的训练目标可以表示为:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{\text{GRPO}} = - \mathbb{E}_{y \sim \pi_\theta} \left[ \min \left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]
|
||||
$$
|
||||
|
||||
其中:
|
||||
- $r_t(\theta) = \frac{\pi_\theta(y_t|x, y_{<t})}{\pi_{\theta_{\text{old}}}(y_t|x, y_{<t})}$ 是重要性采样比
|
||||
- $\hat{A}_t$ 是优势函数(advantage),基于 reward 和 group baseline 计算
|
||||
- $\epsilon$ 是 clipping 参数
|
||||
|
||||
**核心假设**:样本 $y$ 是从策略 $\pi_\theta$ 中采样得到的。在实际训练中,这意味着:
|
||||
1. 采样模型(rollout model)与训练模型(policy model)应当是**同一个模型** $\pi_\theta$
|
||||
2. 两个模型的概率分布应当**完全一致**,即 $\pi_{\text{rollout}} = \pi_\theta$
|
||||
|
||||
### 引入 vLLM 后的假设偏离
|
||||
|
||||
GRPO 的训练速度很大程度上受到采样过程(rollout)的速度制约。为了加速,训练框架引入高效推理引擎(如 vLLM)来执行采样。**理想假设**是:通过权重同步,vLLM 与训练模型保持一致,即 $\pi_{\text{vLLM}} \equiv \pi_\theta$。
|
||||
|
||||
然而,在实践中,即使权重完全同步,由于算子实现等差异,两者的概率分布仍然存在偏差:
|
||||
|
||||
$$
|
||||
\pi_{\text{vLLM}}(y|x) \neq \pi_\theta(y|x)
|
||||
$$
|
||||
|
||||
此时,实际的训练目标变为:
|
||||
|
||||
$$
|
||||
\mathcal{L} = - \mathbb{E}_{y \sim \pi_{\text{vLLM}}} \left[ \min \left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]
|
||||
$$
|
||||
|
||||
其中样本来自 $\pi_{\text{vLLM}}$,但梯度是基于 $\pi_\theta$ 计算的,这**破坏了算法的 on-policy 假设**,引入了训推不一致的问题。
|
||||
|
||||
## Solution
|
||||
|
||||
针对训推不一致问题,可以引入**重要性采样(Importance Sampling, IS)**的校正机制。
|
||||
|
||||
### 重要性采样校正
|
||||
|
||||
重要性采样的基本思想是:当样本来自分布 $q$ 而非目标分布 $p$ 时,可以通过引入权重来修正期望的计算:
|
||||
|
||||
$$
|
||||
\mathbb{E}_{x \sim p} [f(x)] = \mathbb{E}_{x \sim q} \left[ \frac{p(x)}{q(x)} \cdot f(x) \right]
|
||||
$$
|
||||
|
||||
应用到 GRPO 的场景,修正后的损失函数为:
|
||||
|
||||
$$
|
||||
\mathcal{L}_{\text{corrected}} = - \mathbb{E}_{y \sim \pi_{\text{vLLM}}} \left[ w(x, y) \cdot \min \left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]
|
||||
$$
|
||||
|
||||
其中 $w(x, y)$ 是重要性采样权重,用于校正 vLLM 与训练模型之间的分布偏差
|
||||
|
||||
重要性采样权重可以在不同粒度上计算和应用:
|
||||
|
||||
1. **Token-Level**
|
||||
|
||||
每个 token 上计算重要性采样比:
|
||||
|
||||
$$
|
||||
w_{i,t}^{\text{token}} = \frac{\pi_\theta(y_{i,t}|x, y_{i,<t})}{\pi_{\text{vLLM}}(y_{i,t}|x, y_{i,<t})}
|
||||
$$
|
||||
|
||||
2. **Sequence-Level**
|
||||
|
||||
计算序列级别的重要性采样比,然后广播到每个 token:
|
||||
|
||||
$$
|
||||
w_i^{\text{seq}} = \left[ \frac{\pi_\theta(y_i|x)}{\pi_{\text{vLLM}}(y_i|x)} \right]^{\frac{1}{|y_i|}} = \exp\left( \frac{1}{|y_i|} \sum_{t=1}^{|y_i|} \log \frac{\pi_\theta(y_{i,t}|x, y_{i,<t})}{\pi_{\text{vLLM}}(y_{i,t}|x, y_{i,<t})} \right)
|
||||
$$
|
||||
|
||||
### 稳定性控制:Truncate vs. Mask
|
||||
|
||||
过大的重要性采样权重会导致梯度爆炸,破坏训练稳定性。因此需要对权重进行控制:
|
||||
|
||||
#### 1. Truncate(截断)
|
||||
|
||||
将重要性采样权重截断到 $[0, \tau]$ 区间:
|
||||
|
||||
$$
|
||||
w_{\text{truncate}} = \min(w, \tau)
|
||||
$$
|
||||
|
||||
该方法保留所有样本,但限制其影响范围。
|
||||
|
||||
#### 2. Mask(屏蔽)
|
||||
|
||||
舍弃权重超过阈值的 token/sequence 数据
|
||||
|
||||
$$
|
||||
w_{\text{mask}} = \begin{cases}
|
||||
w & \text{if } w \leq \tau \\
|
||||
0 & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
|
||||
### 四种校正模式
|
||||
|
||||
结合粒度和控制策略,共设置四种校正模式(通过 `--rollout_importance_sampling_mode` 参数选择):
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| `token_truncate` | Token 级截断 |
|
||||
| `token_mask` | Token 级屏蔽 |
|
||||
| `sequence_truncate` | Sequence 级截断 |
|
||||
| `sequence_mask` | Sequence 级屏蔽 |
|
||||
|
||||
其中阈值通过 `--rollout_importance_sampling_threshold` 参数设置。
|
||||
|
||||
## Metrics
|
||||
|
||||
为了监控训练中训推不一致的程度,我们在log中加入以下指标(前缀为 `rollout_correction/`):
|
||||
|
||||
### 1. KL 散度(KL Divergence)
|
||||
|
||||
KL 散度衡量 rollout 策略与训练策略之间的偏离程度。两个指标都估计 $\text{KL}(\pi_{\text{vLLM}} \| \pi_\theta)$
|
||||
|
||||
|
||||
**直接估计器 `kl`**:
|
||||
|
||||
$$
|
||||
\text{KL}(\pi_{\text{vLLM}} \| \pi_\theta) = \mathbb{E}_{\pi_{\text{vLLM}}}\left[ \log \frac{\pi_{\text{vLLM}}}{\pi_\theta} \right]
|
||||
$$
|
||||
|
||||
**K3 估计器 `k3_kl`**:
|
||||
|
||||
$$
|
||||
\text{KL}(\pi_{\text{vLLM}} \| \pi_\theta) \approx \mathbb{E}_{\pi_{\text{vLLM}}}\left[ \rho - \log \rho - 1 \right], \quad \rho = \frac{\pi_\theta}{\pi_{\text{vLLM}}}
|
||||
$$
|
||||
|
||||
K3 估计器在 KL 值较小时数值更稳定,且始终非负。
|
||||
|
||||
### 2. Perplexity (PPL)
|
||||
|
||||
困惑度衡量模型对序列的预测不确定性:
|
||||
|
||||
$$
|
||||
\text{PPL} = \exp\left( -\frac{1}{|y|} \sum_{t=1}^{|y|} \log p(y_t) \right)
|
||||
$$
|
||||
|
||||
相关指标:
|
||||
- `training_ppl` / `training_log_ppl`:训练策略的 PPL 及其对数
|
||||
- `rollout_ppl` / `rollout_log_ppl`:rollout 策略的 PPL 及其对数
|
||||
- `log_ppl_diff`:log PPL 差异,正值表示训练策略分配的概率更低
|
||||
- `log_ppl_abs_diff`:log PPL 绝对差异
|
||||
- `log_ppl_diff_max` / `log_ppl_diff_min`:log PPL 差异的最大/最小值
|
||||
- `ppl_ratio`:PPL 比率 $\frac{\text{PPL}_{\text{training}}}{\text{PPL}_{\text{rollout}}}$
|
||||
|
||||
### 3. χ² 散度(Chi-squared Divergence)
|
||||
|
||||
χ² 散度衡量重要性采样权重的方差:
|
||||
|
||||
$$
|
||||
\chi^2(\pi_\theta \| \pi_{\text{vLLM}}) = \mathbb{E}_{\pi_{\text{vLLM}}}\left[ \rho^2 \right] - 1, \quad \rho = \frac{\pi_\theta}{\pi_{\text{vLLM}}}
|
||||
$$
|
||||
|
||||
- `chi2_token`:Token 级别 χ² 散度,$\mathbb{E}[\rho_t^2] - 1$
|
||||
- `chi2_seq`:Sequence 级别 χ² 散度(基于几何平均),$\mathbb{E}[\rho_{\text{geo}}^2] - 1$,其中 $\rho_{\text{geo}} = \exp(\frac{1}{T}\sum_t \log \rho_t)$
|
||||
|
||||
χ² 散度越大,表示 IS 权重方差越大,训练越不稳定。`chi2_seq` 使用几何平均而非乘积,使其与 `chi2_token` 在量级上可比较。
|
||||
|
||||
### 4. Effective Sample Size (ESS)
|
||||
|
||||
有效样本大小衡量重要性采样后实际起作用的样本数量:
|
||||
|
||||
$$
|
||||
\text{ESS} = \frac{1}{\mathbb{E}\left[\left(\frac{w}{\mathbb{E}[w]}\right)^2\right]}
|
||||
$$
|
||||
|
||||
ESS 值越大(接近1),表示重要性采样权重分布越均匀,样本的有效利用率越高。当所有权重相等时(on-policy),ESS = 1;当权重差异很大时(严重 off-policy),ESS 会很小。
|
||||
|
||||
### 5. IS 权重统计
|
||||
|
||||
- `is_weight_mean`:平均重要性采样权重,理想值为 1.0
|
||||
- `clipped_frac`:被截断或屏蔽的样本比例
|
||||
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 仅记录诊断指标
|
||||
|
||||
如果只想监控训推不一致的程度,而不启用重要性采样校正,可以设置:
|
||||
|
||||
```
|
||||
--log_rollout_offpolicy_metrics true
|
||||
```
|
||||
|
||||
这将记录上述所有诊断指标(KL、PPL、χ² 等),但不会对损失函数进行任何修正。
|
||||
|
||||
### 启用重要性采样校正
|
||||
|
||||
在GRPO训练中,设置以下参数启用校正机制:
|
||||
|
||||
```
|
||||
--rollout_importance_sampling_mode (默认为None)
|
||||
--rollout_importance_sampling_threshold (默认为2)
|
||||
```
|
||||
|
||||
当设置了 `rollout_importance_sampling_mode` 时,诊断指标会自动记录,无需额外设置 `log_rollout_offpolicy_metrics`。
|
||||
|
||||
## Off-Policy Sequence Masking
|
||||
|
||||
除了重要性采样校正外,还可以使用 **Off-Policy Sequence Masking** 技术来处理训推不一致问题。该技术来自 [DeepSeek-V3.2 论文](https://arxiv.org/abs/2512.02556)。
|
||||
|
||||
### 原理
|
||||
|
||||
Off-Policy Sequence Masking 的核心思想是:当当前策略相对于旧策略(rollout 或 old policy)发生较大偏移时,直接丢弃(mask)该序列,不参与损失计算。这种方法特别针对**优势为负的序列**,因为这些序列在策略偏移较大时更容易导致训练不稳定。
|
||||
|
||||
具体来说,对于每个序列,计算:
|
||||
|
||||
$$
|
||||
\delta_i = \frac{1}{|y_i|} \sum_{t=1}^{|y_i|} \bigl( \log \pi_{\text{old}}(y_{i,t}|x, y_{i,<t}) - \log \pi_\theta(y_{i,t}|x, y_{i,<t}) \bigr)
|
||||
$$
|
||||
|
||||
当满足以下条件时,序列 $i$ 将被 mask 掉(平均只在 completion token 上计算,即 `completion_mask=1` 的位置):
|
||||
1. $\delta_i > \tau$
|
||||
2. **且** $\hat{A}_i < 0$
|
||||
|
||||
其中:
|
||||
- $\pi_{\text{old}}$ 优先使用 `rollout_per_token_logps`(rollout/行为策略的 logprobs),若不存在则使用 `old_per_token_logps`
|
||||
- $\tau$ 是用户设置的阈值(`--off_policy_sequence_mask_delta`,默认 None 表示关闭)
|
||||
|
||||
|
||||
参考资料
|
||||
|
||||
1. https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda
|
||||
2. https://fengyao.notion.site/off-policy-rl
|
||||
3. https://github.com/volcengine/verl/blob/main/verl/trainer/ppo/rollout_corr_helper.py
|
||||
4. [DeepSeek-V3.2: Pushing the Frontier of Open Large Language Models](https://arxiv.org/abs/2512.02556)
|
||||
@@ -0,0 +1,35 @@
|
||||
# TreePO: Bridging the Gap of Policy Optimization and Efficacy and Inference Efficiency with Heuristic Tree-based Modeling
|
||||
|
||||
|
||||
作者: [li2zhi](https://github.com/li2zhi)
|
||||
|
||||
## 原理介绍
|
||||
[TreePO论文](https://arxiv.org/abs/2508.17445) 提出了一种树状结构建模方法。该方法将序列生成组织为分段式的树结构搜索,通过动态分支、回退与提前终止机制,显著提高KV缓存复用率,从而降低计算开销,同时保持甚至增强了探索的多样性。
|
||||
|
||||

|
||||
|
||||
## 实现细节
|
||||
[TreePO实现示例](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/treepo/tree_rollout_plugin.py)参考[官方实现](https://github.com/multimodal-art-projection/TreePO/blob/main/recipe/treepo/vllm_rollout_tree.py) 给出了 TreePO 训练插件的样例代码,涵盖了多轮交互、终止判断,与分支回退等相关逻辑。
|
||||
|
||||
**注意**:在实际使用中,你需要根据自己的场景需求,重写step、check_finished等方法的逻辑,以确保其能够在自定义场景下按照预期执行。而关于自定义奖励的设计与使用,你可以参考[DeepEyes](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/deepeyes/deepeyes_plugin.py)的实现。
|
||||
|
||||
训练参考该[脚本](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/treepo/tree_rollout.sh)
|
||||
|
||||
## 测试数据
|
||||
> model: Qwen/Qwen2.5-0.5B
|
||||
> dataset: AI-MO/NuminaMath-TIR
|
||||
> subset size: 1,000 samples
|
||||
> 1 GPU for training, 1 GPU for inference
|
||||
|
||||
| \ | batch_size | num_generation | max_tree_depth | global_step | total inference calls | saving ratio | train_speed(iter/s) | improvement rate |
|
||||
| ----------------------- | ---------- | -------------- | -------------- | ----------- | --------------------- | ------------ | ------------------- | ---------------- |
|
||||
| original implementation | 8 | 8 | 4 | 200 | 5965 | 0.00% | 0.292436 | 0.00% |
|
||||
| tree(max_divergence=3) | 8 | 8 | 4 | 200 | 3678 | 38.34% | 0.31819 | 8.81% |
|
||||
| | | | | | | | | |
|
||||
| original implementation | 8 | 8 | 5 | 105 | 4312 | 0.00% | 0.261324 | 0.00% |
|
||||
| tree(max_divergence=2) | 8 | 8 | 5 | 105 | 2513 | 52.69% | 0.336639 | 28.82% |
|
||||
| tree(max_divergence=3) | 8 | 8 | 5 | 105 | 2990 | 30.66% | 0.308791 | 18.16% |
|
||||
| | | | | | | | | |
|
||||
| original implementation | 8 | 8 | 6 | 105 | 5202 | 0.00% | 0.24832 | 0.00% |
|
||||
| tree(max_divergence=2) | 8 | 8 | 6 | 105 | 3348 | 35.64% | 0.27755 | 11.77% |
|
||||
| tree(max_divergence=3) | 8 | 8 | 6 | 105 | 3888 | 25.26% | 0.272339 | 9.67% |
|
||||
@@ -0,0 +1,360 @@
|
||||
# GYM环境训练
|
||||
|
||||
GYM 风格的环境训练把"模型 → 环境 → 奖励"这条链路封装成一个抽象接口,让 LLM 像 Agent 一样与环境进行多轮交互,每一步的奖励直接由环境给出,无需再单独写 reward 函数从轨迹里反推。本文先介绍接口,再用一个完整的自定义示例(FrozenLake)说明如何接入训练。
|
||||
|
||||
## Gym 接口
|
||||
|
||||
GYM 源自 [Gymnasium库](https://github.com/Farama-Foundation/Gymnasium)。在 ms-swift 中我们定义了如下接口:
|
||||
|
||||
```python
|
||||
class Env(ABC):
|
||||
|
||||
def __init__(self, env_config):
|
||||
"""env_config 来自数据集每行的 env_config 列,可承载初始化参数"""
|
||||
self.env_config = env_config
|
||||
|
||||
@abstractmethod
|
||||
async def reset(self, config: RolloutInferRequest) -> Tuple[str, Dict[str, Any], str]:
|
||||
"""
|
||||
Returns:
|
||||
- observation: 作为首轮 user 消息发送给模型
|
||||
- info: 调试/日志信息,记录到 completions.jsonl
|
||||
- system_message: 本条轨迹的 system prompt
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def step(self, action: Messages) -> Tuple[str, float, bool, Dict[str, Any]]:
|
||||
"""
|
||||
Args:
|
||||
action: 截止当前的完整对话消息,最后一条即模型最新回复
|
||||
Returns:
|
||||
- next_observation: 下一轮 user 消息
|
||||
- reward: 当前 step 奖励
|
||||
- done: 轨迹是否结束
|
||||
- info: 调试/日志信息
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def close(self):
|
||||
"""释放资源"""
|
||||
pass
|
||||
```
|
||||
|
||||
`reset` 接收到的 `RolloutInferRequest` 包含数据集行的 `messages`、`data_dict`(额外列,包括 `env_config`)等。完整示例参见 [入参示例](./multi_turn.md#多轮规划器-multiturnscheduler)。
|
||||
|
||||
> 如果需要在每轮 rollout 之间额外控制对话历史(例如动态压缩、注入额外提示),推荐直接继承 `MultiTurnScheduler` 并实现 `on_trajectory_start` / `on_turn_end` hook,或重写 `step` / `run` 方法,详见[多轮训练文档](./multi_turn.md#自定义多轮交互逻辑)。
|
||||
|
||||
## 启动训练
|
||||
|
||||
使用内置的 [gym_scheduler](https://github.com/modelscope/ms-swift/blob/main/swift/rollout/multi_turn.py) 把 env 串到多轮 rollout 中。
|
||||
|
||||
`GYMScheduler` 基于通用 hook 协议实现:
|
||||
- 继承 `MultiTurnScheduler`,无需自定义 `run` 方法
|
||||
- 实现 `on_trajectory_start`(调用 `env.reset`)和 `on_turn_end`(调用 `env.step`)
|
||||
- 同时适用于 server mode(`run()`)和 colocate mode(`run_multi_turn()`)
|
||||
|
||||
用户自定义的 env 通过 `--external_plugins your_plugin.py` 加载,plugin 里执行 `envs['my_env'] = MyEnv` 完成注册(下文 FrozenLake 示例完整演示)。
|
||||
|
||||
**Colocate 模式**:
|
||||
|
||||
```bash
|
||||
megatron rlhf \
|
||||
--rlhf_type grpo \
|
||||
--vllm_mode colocate \
|
||||
--external_plugins examples/megatron/grpo/multi_turn/frozen_lake_plugin.py \
|
||||
--multi_turn_scheduler gym_scheduler \
|
||||
--gym_env frozen_lake \
|
||||
--use_gym_env true \
|
||||
--max_turns 10 \
|
||||
...
|
||||
|
||||
# swift rlhf 同理
|
||||
```
|
||||
|
||||
|
||||
**Server 模式**
|
||||
|
||||
```bash
|
||||
swift rollout \
|
||||
--model xxx \
|
||||
--use_gym_env true \
|
||||
--external_plugins examples/megatron/grpo/multi_turn/frozen_lake_plugin.py \
|
||||
--multi_turn_scheduler gym_scheduler \
|
||||
--gym_env frozen_lake \
|
||||
--max_turns 10
|
||||
|
||||
# trainer 侧需要加 --vllm_server_pass_dataset true,把 env_config 等额外列透传给 rollout 端
|
||||
megatron rlhf --vllm_mode server --vllm_server_pass_dataset true ...
|
||||
# or swift rlhf --vllm_mode server --vllm_server_pass_dataset true ...
|
||||
```
|
||||
|
||||
|
||||
环境选择有两种方式:
|
||||
- 通过 `--gym_env env_name` 全局指定(同一脚本里所有 prompt 共用一个 env);
|
||||
- 在每行数据的 `env_config.name` 中指定(适用于多环境混合场景,每条数据可指向不同 env,会覆盖 `--gym_env`)。
|
||||
|
||||
## 示例:从零写一个 FrozenLake 环境
|
||||
|
||||
<img src="https://gymnasium.farama.org/_images/frozen_lake.gif" width="220" alt="FrozenLake 环境示意图(来源:Gymnasium 官方文档)" />
|
||||
|
||||
[FrozenLake](https://gymnasium.farama.org/environments/toy_text/frozen_lake/) 是 OpenAI Gym 中的经典任务:智能体从起点出发,需要穿过一片冰湖到达终点,途中要避开冰窟。原始环境如上图所示。下面以纯文本版本(把上图网格直接渲染成 ASCII 字符)为例。
|
||||
|
||||
以下完整代码参考完整代码:[frozen_lake_plugin](https://github.com/modelscope/ms-swift/blob/main/examples/megatron/grpo/multi_turn/frozen_lake_plugin.py)。
|
||||
|
||||
**1. 定义 Env**
|
||||
|
||||
每条数据派生一张随机 4x4 地图(随机洞 + 随机 S/G 位置,BFS 校验保证可解)。单元含义:`S` 起点 / `G` 终点 / `H` 冰窟(踩到=失败)/ `F` 安全冰面 / `P` 玩家当前位置。
|
||||
|
||||
```python
|
||||
class FrozenLakeEnv(Env):
|
||||
def __init__(self, env_config):
|
||||
super().__init__(env_config)
|
||||
self.size = int(env_config.get('size', 4))
|
||||
self.p = float(env_config.get('p', 0.8))
|
||||
seed = env_config.get('seed')
|
||||
self.seed = int(seed) if seed is not None else None
|
||||
|
||||
async def reset(self, config: RolloutInferRequest):
|
||||
self.grid = generate_random_map(size=self.size, p=self.p, seed=self.seed)
|
||||
...
|
||||
return observation, {'seed': self.seed}, SYSTEM_PROMPT
|
||||
|
||||
async def step(self, action: Messages):
|
||||
move = _parse_action(action[-1]['content']) # <action>up|down|left|right</action>
|
||||
# 推进一格、判断 G / H;外层 max_turns 由 scheduler 兜底
|
||||
if cell == 'G': return obs, 1.0, True, {'status': 'goal'}
|
||||
if cell == 'H': return obs, 0.0, True, {'status': 'hole'}
|
||||
...
|
||||
```
|
||||
|
||||
**2. GYMScheduler 的 hook 实现**
|
||||
|
||||
框架内置的 `GYMScheduler` 基于多轮 hook 完成了控制逻辑:
|
||||
|
||||
```python
|
||||
class GYMScheduler(MultiTurnScheduler):
|
||||
def on_trajectory_start(self, requests):
|
||||
# 为每个请求创建 env,调用 env.reset,注入初始 observation
|
||||
for req in requests:
|
||||
env = self._create_env(req.data_dict.get('env_config', {}))
|
||||
observation, info, system_message = env.reset(req)
|
||||
req.messages = [system_msg, user_msg(observation)]
|
||||
self._envs[req.uuid] = env
|
||||
|
||||
def on_turn_end(self, req, response_choice, current_turn):
|
||||
# 调用 env.step,累积 reward,返回 done + rollout_infos
|
||||
next_obs, reward, done, info = env.step(deepcopy(req.messages))
|
||||
self._total_rewards[req.uuid] += reward
|
||||
return {
|
||||
'done': done,
|
||||
'rollout_infos': {
|
||||
'total_reward': self._total_rewards[req.uuid],
|
||||
'step_rewards': [...],
|
||||
}
|
||||
}
|
||||
|
||||
def step(self, req, response_choice, current_turn):
|
||||
# 注入下一帧 observation 到 user message
|
||||
if self._pending_obs.get(req.uuid):
|
||||
req.messages.append({'role': 'user', 'content': next_obs})
|
||||
return {'infer_request': req}
|
||||
```
|
||||
|
||||
用户只需实现 Env 接口,无需关心多轮控制细节。
|
||||
|
||||
**3. 注册**
|
||||
|
||||
将 env 类挂到 swift 的 `envs` 注册表里。`--external_plugins` 在训练启动时会 import 该文件,注册随之生效:
|
||||
|
||||
```python
|
||||
# examples/megatron/grpo/multi_turn/frozen_lake_plugin.py
|
||||
from swift.rollout.gym_env import Env, envs
|
||||
|
||||
class FrozenLakeEnv(Env):
|
||||
...
|
||||
|
||||
envs['frozen_lake'] = FrozenLakeEnv
|
||||
```
|
||||
|
||||
**4. 准备数据集**
|
||||
|
||||
数据集在这里仅作占位符处理,数据构造由环境生成,和 `env_config.seed`来控制地图生成的随机性:
|
||||
|
||||
```json
|
||||
{"messages":[{"role":"user","content":"<placeholder>"}],"env_config":{"seed":0}}
|
||||
{"messages":[{"role":"user","content":"<placeholder>"}],"env_config":{"seed":1}}
|
||||
...
|
||||
{"messages":[{"role":"user","content":"<placeholder>"}],"env_config":{"seed":127}}
|
||||
```
|
||||
|
||||
**5. (可选)叠加自定义 reward**
|
||||
|
||||
设置 `--use_gym_env true` 后,env 给出的 `total_reward` 会自动作为一路奖励参与训练,无需再写 reward 函数。如果想在此之外再叠加自定义信号(如格式/长度等),通过 `--reward_funcs` 传入即可,gym 奖励会作为额外一列与 reward_funcs 拼在一起,由 `--reward_weights` 统一加权。例如同时启用一个格式校验 reward:
|
||||
|
||||
```bash
|
||||
megatron rlhf ... --use_gym_env true --reward_funcs format --reward_weights 0.2 1.0
|
||||
# reward_weights 末位对应 gym 的 total_reward
|
||||
```
|
||||
|
||||
**6. 训练**
|
||||
|
||||
运行脚本参考:[`examples/megatron/grpo/multi_turn/frozen_lake.sh`](https://github.com/modelscope/ms-swift/blob/main/examples/megatron/grpo/multi_turn/frozen_lake.sh)
|
||||
|
||||
|
||||
## OpenEnv 环境训练
|
||||
|
||||
[OpenEnv](https://github.com/huggingface/openenv) 是 HuggingFace 开源的 Agentic RL 环境框架,通过 WebSocket 与环境服务器交互。与上文 FrozenLake 的本地 `Env` 接口不同,OpenEnv 将环境逻辑放在独立的服务进程中,swift 通过 `OpenEnvScheduler` + `OpenEnvWrapper` 与之通信。
|
||||
|
||||
### 架构对比
|
||||
|
||||
| 特性 | 内置 Gym (`GYMScheduler`) | OpenEnv (`OpenEnvScheduler`) |
|
||||
|------|--------------------------|------------------------------|
|
||||
| 环境运行位置 | 训练进程内(Python 对象) | 独立服务器(WebSocket 通信) |
|
||||
| 环境接口 | 继承 `Env`,实现 `reset/step/close` | 服务器提供 HTTP/WebSocket API |
|
||||
| 注册方式 | `--external_plugins` + `envs` 注册表 | `--external_plugins` + `multi_turns` 注册表 |
|
||||
| 适用场景 | 轻量本地环境(FrozenLake 等) | 复杂服务端环境(TextArena、CARLA 等) |
|
||||
| 并发控制 | 无需 | 内置 Semaphore 限制并发连接 |
|
||||
|
||||
### OpenEnvScheduler
|
||||
|
||||
`OpenEnvScheduler` 继承 `GYMScheduler`,将本地 `Env` 替换为 `OpenEnvWrapper`(WebSocket 客户端)。核心设计:
|
||||
|
||||
- **`_create_env`**:创建 `OpenEnvWrapper`,连接 OpenEnv 服务器
|
||||
- **`on_trajectory_start`**:为每个请求创建 wrapper,调用 `reset()`,用 Semaphore 限制并发(默认 4)
|
||||
- **`on_turn_end`**:解析模型输出,调用 `wrapper.step()`,累积奖励
|
||||
- **`parse_action`**(可覆盖):将模型文本解析为 action dict,默认 `json.loads`
|
||||
- **`format_observation`**(可覆盖):将服务器返回的 observation 格式化为字符串,默认 `json.dumps`
|
||||
|
||||
用户通过继承 `OpenEnvScheduler` 并覆盖 `parse_action`、`format_observation`、`on_trajectory_start`、`on_turn_end` 来适配具体环境。
|
||||
|
||||
### 示例:Sudoku 环境
|
||||
|
||||
以 TextArena Sudoku 为例,模型需要通过 `[row col number]` 格式下棋,在 9x9 数独棋盘上填入数字。完整代码参考:[sudoku_scheduler.py](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/openenv/sudoku_scheduler.py)。
|
||||
|
||||
**1. 启动 OpenEnv 服务器**
|
||||
|
||||
安装 OpenEnv 和 Sudoku 环境包(textarena 和 nltk 会作为依赖自动安装):
|
||||
|
||||
```bash
|
||||
pip install openenv
|
||||
pip install git+https://huggingface.co/spaces/openenv/sudoku
|
||||
```
|
||||
|
||||
使用提供的启动脚本启动本地服务器(默认端口 8000)。`MAX_CONCURRENT_ENVS` 需 ≥ 训练时的 `num_generations`:
|
||||
|
||||
```bash
|
||||
TEXTARENA_ENV_ID=Sudoku-v0 MAX_CONCURRENT_ENVS=8 python examples/train/grpo/plugin/openenv/start_sudoku_server.py
|
||||
```
|
||||
|
||||
数据集中将 `base_url` 指向本地服务器地址:
|
||||
|
||||
```json
|
||||
{"messages":[{"role":"user","content":"Play"}],"env_config":{"name":"openenv","base_url":"http://127.0.0.1:8000"}}
|
||||
```
|
||||
|
||||
**2. 自定义 Scheduler**
|
||||
|
||||
继承 `OpenEnvScheduler`,实现 Sudoku 专用的动作解析、观察格式化和多组件奖励:
|
||||
|
||||
```python
|
||||
from swift.rollout.multi_turn import OpenEnvScheduler
|
||||
|
||||
class SudokuScheduler(OpenEnvScheduler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._last_content_len = {} # 内容差分跟踪
|
||||
|
||||
async def on_trajectory_start(self, requests):
|
||||
# 创建环境、解析棋盘、生成 hints
|
||||
# hints 包括「保证正确的走法」和候选数字列表
|
||||
...
|
||||
|
||||
async def on_turn_end(self, infer_request, response_choice, current_turn):
|
||||
# 解析 [row col number],step 环境
|
||||
# 计算 5 路奖励:空格选择 / 合法移动 / 重复惩罚 / 进度 / 正确
|
||||
# 返回更新后的棋盘 + hints 作为下一轮观察
|
||||
...
|
||||
|
||||
def parse_action(self, text):
|
||||
import re
|
||||
match = re.search(r'\[\s*(\d+)\s+(\d+)\s+(\d+)\s*\]', text)
|
||||
if match:
|
||||
row, col, num = match.groups()
|
||||
return {"message": f"[{row} {col} {num}]"}
|
||||
return {"message": "[1 1 1]"}
|
||||
```
|
||||
|
||||
**多组件奖励系统**(参考 [TRL Sudoku 示例](https://github.com/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb)):
|
||||
|
||||
| 奖励组件 | 计算方式 | 作用 |
|
||||
|---------|---------|------|
|
||||
| `empty_cell_reward` | 目标是空格 +1 / 覆盖已有 -1 | 引导模型选择合法位置 |
|
||||
| `valid_move_reward` | 合法新走法 +1 / 警告 -0.5 / 无效 0 | 鼓励合法操作 |
|
||||
| `repetition_reward` | 重复走法指数惩罚(-2^n,上限 -10) | 避免重复 |
|
||||
| `progress_reward` | (已填充 - 初始) / (81 - 初始) | 衡量解题进度 |
|
||||
| `correct_reward` | 环境返回的二值奖励 | 完全解出 |
|
||||
|
||||
组合奖励 = 各组件均值之和,提供比单一二值奖励更密集的学习信号。
|
||||
|
||||
**3. Hints 系统**
|
||||
|
||||
每轮交互中,scheduler 解析当前棋盘状态,为模型提供提示:
|
||||
|
||||
- **GUARANTEED MOVES**:只有一个候选数字的空格(可直接填入)
|
||||
- **Other options**:2-3 个候选数字的空格
|
||||
- **MOVES ALREADY TRIED**:已尝试过的走法(避免重复)
|
||||
|
||||
**4. 准备数据集**
|
||||
|
||||
数据集仅作占位符,实际棋盘由环境服务器生成。`base_url` 指向 OpenEnv 托管地址:
|
||||
|
||||
```json
|
||||
{"messages":[{"role":"user","content":"Play"}],"env_config":{"name":"openenv","base_url":"http://127.0.0.1:8000"}}
|
||||
```
|
||||
|
||||
**5. 注册 Scheduler**
|
||||
|
||||
`sudoku_scheduler.py` 末尾已包含注册代码,通过 `--external_plugins` 加载即可:
|
||||
|
||||
```python
|
||||
# sudoku_scheduler.py 末尾
|
||||
from swift.rollout.multi_turn import multi_turns
|
||||
multi_turns['sudoku_scheduler'] = SudokuScheduler
|
||||
```
|
||||
|
||||
**6. 启动训练**
|
||||
|
||||
```bash
|
||||
swift rlhf \
|
||||
--rlhf_type grpo \
|
||||
--model Qwen/Qwen3.5-4B \
|
||||
--dataset examples/train/grpo/plugin/openenv/sudoku.jsonl \
|
||||
--external_plugins examples/train/grpo/plugin/openenv/sudoku_scheduler.py \
|
||||
--enable_thinking false \
|
||||
--max_completion_length 256 \
|
||||
--use_gym_env true \
|
||||
--multi_turn_scheduler sudoku_scheduler \
|
||||
--max_turns 20 \
|
||||
--use_vllm true \
|
||||
--vllm_mode colocate \
|
||||
...
|
||||
```
|
||||
|
||||
运行脚本参考:[`examples/train/grpo/plugin/openenv/run_grpo_sudoku.sh`](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/openenv/run_grpo_sudoku.sh)
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **vLLM 模式**:以上示例使用 `--vllm_mode colocate`,vLLM 与训练共享 GPU。若使用 `--vllm_mode server`,需额外启动 `swift rollout` 作为 vLLM 服务器,且 `--multi_turn_scheduler` 和 `--max_turns` 参数应传给 `swift rlhf` 而非 `swift rollout`。
|
||||
- **并发会话数**:`start_sudoku_server.py` 的 `MAX_CONCURRENT_ENVS` 需 ≥ 训练时的 `num_generations`。默认的 `python -m textarena_env.server.app` 只支持 1 个并发会话。
|
||||
- **enable_thinking**:Sudoku 等环境不需要 CoT 推理,建议设置 `--enable_thinking false` 以减少 token 消耗。
|
||||
- **同步 I/O**:`OpenEnvWrapper` 的 `reset()`/`step()` 是同步 WebSocket 调用。`OpenEnvScheduler` 的子类应使用 `asyncio.to_thread()` 包装这些调用以避免阻塞事件循环。
|
||||
|
||||
|
||||
参考资料:
|
||||
|
||||
- https://gymnasium.farama.org/environments/toy_text/frozen_lake/
|
||||
- https://github.com/alibaba/ROLL/tree/main/roll/pipeline/agentic/env/frozen_lake
|
||||
- https://github.com/huggingface/openenv
|
||||
- https://github.com/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb
|
||||
@@ -0,0 +1,11 @@
|
||||
Developer Guide
|
||||
===============
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
loss_types.md
|
||||
multi_turn.md
|
||||
multi_task.md
|
||||
reward_function.md
|
||||
reward_model.md
|
||||
gym_env.md
|
||||
@@ -0,0 +1,131 @@
|
||||
# Loss Types
|
||||
|
||||
GRPO训练支持多种不同的loss类型,主要区别在于归一化的维度和梯度处理方式上有所不同。
|
||||
|
||||
## 损失函数
|
||||
|
||||
token 级别上,GRPO 训练使用以下损失函数
|
||||
|
||||
$$\mathcal{L}_{i,t} = -\min\left(\rho_{i,t} A_{i,t}, \text{clip}(\rho_{i,t}, 1-\epsilon, 1+\epsilon) A_{i,t}\right)$$
|
||||
|
||||
当设置`loss_type cispo`时,使用 cispo 损失
|
||||
|
||||
$$\mathcal{L}_{i,t}^{\text{CISPO}} = -\text{detach}\left(\min(\rho_{i,t}, \epsilon_{\text{high}})\right) \cdot A_{i,t} \cdot \log \pi_\theta(y_{i,t}|y_{i,<t})$$
|
||||
|
||||
当设置`loss_type sapo`时,使用软门控替代硬裁剪,详见 [SAPO](../AdvancedResearch/SAPO.md)
|
||||
|
||||
$$\mathcal{L}_{i,t}^{\text{SAPO}} = -g_{i,t} \cdot A_{i,t}$$
|
||||
|
||||
其中 $g_{i,t} = \sigma(\tau \cdot (\rho_{i,t} - 1))$ 是温度控制的软门控函数。
|
||||
|
||||
其中:
|
||||
- $\rho_{i,t} = \frac{\pi_\theta(y_{i,t}|y_{i,<t})}{\pi_{\theta_{\text{old}}}(y_{i,t}|y_{i,<t})}$ 是重要性采样权重
|
||||
- $A_{i,t}$ 是优势函数
|
||||
- $\epsilon$ 和 $\epsilon_{\text{high}}$ 是clipping参数
|
||||
- $\text{detach}(\cdot)$ 表示该项不参与梯度计算
|
||||
- $\sigma(\cdot)$ 是 sigmoid 函数,$\tau$ 是温度参数
|
||||
|
||||
## GRPO
|
||||
|
||||
`--loss_type grpo`
|
||||
|
||||
GRPO是标准的损失函数实现,对每个样本的token-level损失取平均,然后对所有样本取平均。
|
||||
|
||||
**公式:**
|
||||
|
||||
$$\mathcal{L}_{\text{GRPO}} = \frac{1}{N} \sum_{i=1}^{N} \frac{1}{T_i} \sum_{t=1}^{T_i} \mathcal{L}_{i,t}$$
|
||||
|
||||
其中:
|
||||
- $N$ 是批次中的样本数量
|
||||
- $T_i$ 是第$i$个样本的completion token数量
|
||||
|
||||
**归一化维度:** 样本维度(先对每个样本的所有token取平均,再对所有样本取平均)
|
||||
|
||||
## BNPO (Batch Normalized Policy Optimization)
|
||||
|
||||
`--loss_type bnpo`
|
||||
|
||||
BNPO将所有样本的所有token的损失直接求和,然后除以所有completion token的总数量。
|
||||
|
||||
**公式:**
|
||||
|
||||
$$\mathcal{L}_{\text{BNPO}} = \frac{\sum_{i=1}^{N} \sum_{t=1}^{T_i} \mathcal{L}_{i,t}}{\sum_{i=1}^{N} T_i}$$
|
||||
|
||||
其中:
|
||||
- $N$ 是批次中的样本数量
|
||||
- $T_i$ 是第$i$个样本的completion token数量
|
||||
|
||||
**归一化维度:** Token维度(对所有completion token取平均)
|
||||
|
||||
## DR-GRPO
|
||||
|
||||
`--loss_type dr_grpo`
|
||||
|
||||
DR-GRPO将所有样本的所有token的损失求和,然后除以批次大小乘以最大completion长度。
|
||||
|
||||
**公式:**
|
||||
|
||||
$$\mathcal{L}_{\text{DR-GRPO}} = \frac{\sum_{i=1}^{N} \sum_{t=1}^{T_i} \mathcal{L}_{i,t}}{N \times L_{\text{max}}}$$
|
||||
|
||||
其中:
|
||||
- $N$ 是批次中的样本数量
|
||||
- $T_i$ 是第$i$个样本的completion token数量
|
||||
- $L_{\text{max}}$ 是最大completion长度
|
||||
|
||||
**归一化维度:** 固定维度(批次大小 × 最大completion长度)
|
||||
|
||||
## CISPO
|
||||
|
||||
`--loss_type cispo`
|
||||
|
||||
CISPO损失按所有进程的completion token总数进行归一化。
|
||||
|
||||
**公式:**
|
||||
|
||||
$$\mathcal{L}_{\text{CISPO}} = \frac{\sum_{i=1}^{N} \sum_{t=1}^{T_i} \mathcal{L}_{i,t}^{\text{CISPO}}}{\sum_{\text{all processes}} \sum_{i=1}^{N_p} T_{p,i}}$$
|
||||
|
||||
其中:
|
||||
- $N$ 是当前进程批次中的样本数量
|
||||
- $T_i$ 是第$i$个样本的completion token数量
|
||||
- $N_p$ 是第$p$个进程的样本数量
|
||||
|
||||
**归一化维度:** 全局token维度(跨所有进程的completion token总数)
|
||||
|
||||
## DAPO
|
||||
|
||||
`--loss_type dapo`
|
||||
|
||||
DAPO与BNPO类似,使用token-level归一化,但基于全局数据(多进程)进行归一化。
|
||||
|
||||
**公式:**
|
||||
|
||||
$$\mathcal{L}_{\text{DAPO}} = \frac{\sum_{i=1}^{N} \sum_{t=1}^{T_i} \mathcal{L}_{i,t}}{\sum_{\text{all processes}} \sum_{i=1}^{N_p} T_{p,i}}$$
|
||||
|
||||
其中:
|
||||
- $N$ 是当前进程批次中的样本数量
|
||||
- $T_i$ 是第$i$个样本的completion token数量
|
||||
- $N_p$ 是第$p$个进程的样本数量
|
||||
|
||||
**归一化维度:** 全局token维度(跨所有进程的completion token总数)
|
||||
|
||||
## FIPO
|
||||
|
||||
`--loss_type fipo`
|
||||
|
||||
FIPO 在 DAPO/GRPO 的 clipped policy loss 上引入 Future-KL influence weight。每个 token 的序列级 advantage 会乘以从当前位置到后续 token 的折扣累积 KL 位移得到的权重:
|
||||
|
||||
$$f_{i,t} = \text{clip}\left(\exp\left(\sum_{k=t}^{T_i} \gamma^{k-t} M_{i,k} \Delta \log p_{i,k}\right), 1-\epsilon_f, 1+\epsilon_f\right)$$
|
||||
|
||||
$$\mathcal{L}_{i,t}^{\text{FIPO}} = f_{i,t} \cdot \mathcal{L}_{i,t}$$
|
||||
|
||||
FIPO 的 influence weight 默认不参与梯度计算,并使用与 DAPO 相同的全局 token 归一化。
|
||||
|
||||
**归一化维度:** 全局 token 维度(所有进程的 completion token 总数)
|
||||
|
||||
## SAPO
|
||||
|
||||
`--loss_type sapo`
|
||||
|
||||
SAPO使用温度控制的软门控替代硬裁剪,实现平滑的梯度衰减。归一化方式与GRPO相同。
|
||||
|
||||
详细说明请参考 [SAPO](../AdvancedResearch/SAPO.md)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 多任务训练
|
||||
我们可以在数据集中添加一个用于标识任务类型的列,并在奖励函数/奖励模型插件中根据任务类型进行判断,从而实现多任务训练。假设数据集中包含数学和编程任务,比如:
|
||||
|
||||
```
|
||||
{"query": "Solve the equation x + 2 = 5", "solution": "3", "task": "math"},
|
||||
{"query": "Write a function to calculate the Fibonacci sequence", "solution": "xxx", "task": "code"},
|
||||
{"query": "What is the integral of x^2?", "solution": "xxx", "task": "math"},
|
||||
{"query": "Implement a sorting algorithm in Python", "solution": "xxx", "task": "code"},
|
||||
```
|
||||
|
||||
我们可以设置不同的奖励函数来分别处理数学数据和代码数据,注意数据集中的列会传入奖励函数,所以我们可以通过 `task` 列
|
||||
|
||||
下面是针对不同任务的奖励函数的示例:
|
||||
|
||||
```python
|
||||
from swift.rewards import ORM, orms
|
||||
import random
|
||||
|
||||
# Math-specific reward function
|
||||
class MathRandomReward(ORM):
|
||||
def __call__(self, completions, task, **kwargs):
|
||||
rewards = []
|
||||
for completion, t in zip(completions, task):
|
||||
if t == "math":
|
||||
import random
|
||||
# imple math accuracy logic
|
||||
reward = random.random()
|
||||
rewards.append(reward)
|
||||
else:
|
||||
# Return None for non-math tasks
|
||||
rewards.append(None)
|
||||
return rewards
|
||||
|
||||
# Coding-specific reward function
|
||||
class CodeRandomReward(ORM):
|
||||
def __call__(self, completions, task, **kwargs):
|
||||
rewards = []
|
||||
for prompt, completion, t in zip(prompts, completions, task):
|
||||
if t == "code":
|
||||
# imple coding accuracy logic
|
||||
reward = random.random()
|
||||
rewards.append(reward)
|
||||
else:
|
||||
# Return None for non-coding tasks
|
||||
rewards.append(None)
|
||||
return rewards
|
||||
|
||||
orms['math_reward'] = MathRandomReward
|
||||
orms['code_reward'] = CodeRandomReward
|
||||
```
|
||||
对于非当前任务的数据, 通过返回 None 来处理,从而使得奖励相关仅计算任务内的数据。
|
||||
@@ -0,0 +1,312 @@
|
||||
# 多轮训练
|
||||
|
||||
在强化学习训练场景中,模型采样可能需要与环境进行多轮交互(如工具调用)。这种交互式训练要求模型能够根据环境反馈信息进行连续推理。本文档将详细介绍如何在 GRPO 训练中自定义多轮训练流程。
|
||||
|
||||
> GKD 同样支持多轮训练,与 GRPO 共享同一套 `MultiTurnScheduler` 基础设施。
|
||||
|
||||
以下是多轮训练示例图,模型可能涉及多轮 rollout,包括环境交互、工具调用等步骤:
|
||||
|
||||

|
||||
|
||||
## 多轮规划器 MultiTurnScheduler
|
||||
|
||||
`MultiTurnScheduler` 是一个抽象基类,提供了默认的多轮对话管理逻辑,其工作流程如下图所示:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/modelscope/ms-swift/main/docs/resources/multiturn_pipeline.png " width="300" />
|
||||
|
||||
多轮规划器主要承担两大核心功能:
|
||||
- **终止条件判断**:通过 `check_finished` 方法判断当前轮次推理是否应该结束
|
||||
- **推理请求构造**:通过 `step` 方法构建下一轮推理的请求对象
|
||||
|
||||
抽象基类 `MultiTurnScheduler` 的核心方法如下:
|
||||
|
||||
```python
|
||||
class MultiTurnScheduler(ABC):
|
||||
|
||||
def __init__(self, max_turns: Optional[int] = None, *args, **kwargs):
|
||||
self.max_turns = max_turns
|
||||
|
||||
def on_trajectory_start(self, requests: List['RolloutInferRequest']) -> None:
|
||||
"""在首轮推理前调用,用于初始化轨迹级别状态。
|
||||
|
||||
可在此方法中直接修改 requests(如注入环境初始 observation)。
|
||||
默认实现为空(no-op)。
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_turn_end(self, infer_request: 'RolloutInferRequest',
|
||||
response_choice: 'ChatCompletionResponseChoice',
|
||||
current_turn: int) -> Dict[str, Any]:
|
||||
"""在 assistant 消息追加后、check_finished 前调用。
|
||||
|
||||
用于推进环境状态(如 env.step)并返回每轮元数据。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 可选包含以下键:
|
||||
- 'done' (bool): 若存在,将覆盖 check_finished 的结果
|
||||
- 'rollout_infos' (dict): 合并到轨迹累积的额外信息中
|
||||
默认返回空字典(no-op)。
|
||||
"""
|
||||
return {}
|
||||
|
||||
def step(self, infer_request: 'RolloutInferRequest', response_choice: 'ChatCompletionResponseChoice',
|
||||
current_turn: int) -> Dict:
|
||||
"""
|
||||
处理对话轮次之间的转换。
|
||||
|
||||
Args:
|
||||
infer_request: 当前推理请求
|
||||
response_choice: 当前轮次的响应
|
||||
current_turn: 当前轮次数
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含推理结果的字典,结构如下:
|
||||
- infer_request (必需): 下一轮的推理请求对象
|
||||
- response_token_ids (可选): 每个 rollout 轮次的响应 token IDs
|
||||
- response_loss_mask (可选): 每个 rollout 轮次响应的损失掩码
|
||||
- rollout_logprobs (可选): 每个 rollout 轮次的响应对应的 logps
|
||||
- rollout_infos (可选): 额外信息数据
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def check_finished(self, infer_request: 'RolloutInferRequest', response_choice: 'ChatCompletionResponseChoice',
|
||||
current_turn: int) -> bool:
|
||||
"""
|
||||
检查多轮 rollout 是否应该结束的默认终止逻辑。
|
||||
|
||||
默认终止条件:
|
||||
1. 当响应达到长度限制时 (finish_reason == 'length')
|
||||
2. 当对话达到最大轮数时 (如果设置了 max_turns)
|
||||
|
||||
Args:
|
||||
infer_request: 推理请求对象
|
||||
response_choice: 包含生成结果的响应选择,包括 finish_reason
|
||||
current_turn: 当前对话轮数
|
||||
|
||||
Returns:
|
||||
bool: True 表示终止对话,False 表示继续
|
||||
"""
|
||||
if response_choice.finish_reason == 'length':
|
||||
return True
|
||||
if self.max_turns and current_turn >= self.max_turns:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
`step` 和 `check_finished` 方法接收的参数说明:
|
||||
- **infer_request**: 当前的推理请求
|
||||
- **response_choice**: 当前轮次的推理结果
|
||||
- **current_turn**: 当前推理轮次(从 1 开始)
|
||||
|
||||
<details><summary>入参示例(点击展开)</summary>
|
||||
|
||||
```python
|
||||
infer_request
|
||||
"""
|
||||
RolloutInferRequest(
|
||||
messages=[
|
||||
{'role': 'system', 'content': 'A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process and answer are enclosed within <think> </think> and <answer> </answer> tags, respectively, i.e., <think> reasoning process here </think><answer> answer here </answer>\n'}, {'role': 'user', 'content': 'What is the value of $\\sqrt{36 \\times \\sqrt{16}}$?'},
|
||||
{'role': 'assistant', 'content': 'To find the value of \\(\\sqrt{36 \\times \\sqrt{16}}\\), we will break down the problem step-by-step.\n\nFirst, we need to evaluate the inner square root:\n\\[\n\\sqrt{16}\n\\]\nWe know that:\n\\[\n4^2 = 16 \\implies \\sqrt{16} = 4\n\\]\n\nNext, we substitute this result back into the original expression:\n\\[\n\\sqrt{36 \\times \\sqrt{16}} = \\sqrt{36 \\times 4}\n\\]\n\nNow, we need to evaluate the product inside the square root:\n\\[\n36 \\times 4 = 144\n\\]\n\nSo, the expression simplifies to:\n\\[\n\\sqrt{144}\n\\]\n\nFinally, we determine the square root of 144:\n\\[\n\\sqrt{144} = 12\n\\]\n\nThus, the value of \\(\\sqrt{36 \\times \\sqrt{16}}\\) is:\n\\[\n\\boxed{12}\n\\]'}
|
||||
],
|
||||
images=[],
|
||||
audios=[],
|
||||
videos=[],
|
||||
tools=None,
|
||||
objects={},
|
||||
data_dict={
|
||||
'problem': 'What is the value of $\\sqrt{36 \\times \\sqrt{16}}$?',
|
||||
'solution': "To solve the problem, we need to evaluate the expression \\(\\sqrt{36 \\times \\sqrt{16}}\\).\n\nWe can break down the steps as follows:\n\n1. Evaluate the inner square root: \\(\\sqrt{16}\\).\n2. Multiply the result by 36.\n3. Take the square root of the product obtained in step 2.\n\nLet's compute this step by step using Python code for accuracy.\n```python\nimport math\n\n# Step 1: Evaluate the inner square root\ninner_sqrt = math.sqrt(16)\n\n# Step 2: Multiply the result by 36\nproduct = 36 * inner_sqrt\n\n# Step 3: Take the square root of the product\nfinal_result = math.sqrt(product)\nprint(final_result)\n```\n```output\n12.0\n```\nThe value of \\(\\sqrt{36 \\times \\sqrt{16}}\\) is /\\(\\boxed{12}\\)."
|
||||
}
|
||||
)
|
||||
"""
|
||||
response_choice
|
||||
"""
|
||||
ChatCompletionResponseChoice(
|
||||
index=0,
|
||||
message=ChatMessage(
|
||||
role='assistant',
|
||||
content='To find the value of \\(\\sqrt{36 \\times \\sqrt{16}}\\), we will break down the problem step-by-step.\n\nFirst, we need to evaluate the inner square root:\n\\[\n\\sqrt{16}\n\\]\nWe know that:\n\\[\n4^2 = 16 \\implies \\sqrt{16} = 4\n\\]\n\nNext, we substitute this result back into the original expression:\n\\[\n\\sqrt{36 \\times \\sqrt{16}} = \\sqrt{36 \\times 4}\n\\]\n\nNow, we need to evaluate the product inside the square root:\n\\[\n36 \\times 4 = 144\n\\]\n\nSo, the expression simplifies to:\n\\[\n\\sqrt{144}\n\\]\n\nFinally, we determine the square root of 144:\n\\[\n\\sqrt{144} = 12\n\\]\n\nThus, the value of \\(\\sqrt{36 \\times \\sqrt{16}}\\) is:\n\\[\n\\boxed{12}\n\\]', tool_calls=None),
|
||||
finish_reason='stop',
|
||||
logprobs=None,
|
||||
messages=None)
|
||||
"""
|
||||
# response_choice.messages will be copied at the end of multi-turn inference.
|
||||
```
|
||||
</details>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
默认的 `check_finished` 逻辑会在以下两种情况下停止推理:
|
||||
- 模型回复被截断,即超出了 `max_completion_length`
|
||||
- 模型推理轮数超出了限制的最大轮数
|
||||
|
||||
完整的默认多轮 rollout 逻辑请参考该类的 `run` 方法,我们也可以通过重载`run` 方法来实现自定义多轮逻辑。
|
||||
|
||||
## 设置多轮训练参数
|
||||
|
||||
在 swift rollout 命令中,设置 multi_turn_scheduler 参数指定规划器
|
||||
|
||||
```bash
|
||||
swift rollout \
|
||||
--model Qwen/Qwen3-1.7B \
|
||||
--vllm_use_async_engine true \
|
||||
--multi_turn_scheduler thinking_tips_scheduler \
|
||||
--vllm_max_model_len 32768 \
|
||||
--vllm_gpu_memory_utilization 0.8 \
|
||||
--max_turns 3
|
||||
```
|
||||
|
||||
|
||||
> 通过参数 `external_plugins`,我们可以将本地的多轮规划器注册到 ms-swift 中,具体实现请参考[代码](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/plugin.py)。
|
||||
|
||||
多轮训练脚本请参考[脚本](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/external/vllm_multi_turn.sh)。
|
||||
|
||||
|
||||
对于多轮 rollout,我们使用 AsyncEngine 来实现高效的批量数据异步多轮采样。AsyncEngine 在多轮推理时能够减少推理过程中的计算气泡:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/modelscope/ms-swift/main/docs/resources/asyncengine.png" width="400" />
|
||||
|
||||
在 `rollout` 命令中使用参数 `vllm_use_async_engine` 来指定 engine 的种类(默认使用 async engine):
|
||||
|
||||
> 注意: async engine 仅在 server mode 下可用。
|
||||
|
||||
### GYM 环境训练
|
||||
|
||||
如果你的多轮任务可以建模为标准的 gym environment(`reset` / `step` / 环境直接给奖励),推荐直接复用框架内置的 `gym_scheduler`,并通过实现一个 `Env` 子类来描述任务。
|
||||
|
||||
`GYMScheduler` 基于通用 hook 协议实现,无需重载 `run` 方法:
|
||||
- **`on_trajectory_start`**: 调用 `env.reset` 并注入初始 observation 到首轮 user 消息
|
||||
- **`on_turn_end`**: 调用 `env.step` 推进环境,返回 `{'done': bool, 'rollout_infos': dict}`
|
||||
|
||||
这种设计使得 `GYMScheduler` 同时适用于 server mode(`run()`)和 colocate mode(`run_multi_turn()`),用户只需实现 Env 接口即可。
|
||||
|
||||
完整接口、自定义 env 的步骤参考 [GYM 环境训练文档](./gym_env.md)。
|
||||
|
||||
## 高级设置
|
||||
|
||||
### 自定义多轮交互逻辑
|
||||
|
||||
在以上默认逻辑中,我们用一条轨迹来计算多轮 rollout 的损失,这里需要假设多轮交互的过程中,模型的历史信息没有受到改变。
|
||||
|
||||
而在一些多轮场景中,我们可能需要在多轮 rollout 过程中动态地修改模型的历史信息(比如压缩历史信息),此时,我们需要将每轮的 rollout 单独作为一条轨迹进行训练。
|
||||
|
||||
#### 方式一:使用 hook
|
||||
|
||||
```python
|
||||
class CustomScheduler(MultiTurnScheduler):
|
||||
def on_trajectory_start(self, requests):
|
||||
# 首轮推理前初始化(如环境 reset、注入初始状态)
|
||||
for req in requests:
|
||||
req.messages = [system_msg, user_msg(initial_observation)]
|
||||
|
||||
def on_turn_end(self, req, response_choice, current_turn):
|
||||
# 每轮推理后推进状态,返回 done 和 rollout_infos
|
||||
next_obs, reward, done = self.advance_env(req.messages)
|
||||
return {
|
||||
'done': done,
|
||||
'rollout_infos': {'reward': reward, ...}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式同时适用于 server mode 和 colocate mode,无需重载 `run` 方法。
|
||||
|
||||
#### 方式二:重载 run 方法(完全自定义)
|
||||
|
||||
比较常见的一种场景是对于思考类模型,在实际推理过程中,模型通常只会保留最后一轮的思考内容,而忽略历史模型回复中的思考内容。
|
||||
|
||||
对于这类场景,我们需要重写多轮规划器中的交互逻辑,即重载 `run` 方法,从而单独返回每一轮的 Rollout 的结果。
|
||||
|
||||
框架内置的 `ThinkingModelTipsScheduler` 类展示了如何通过重写 `run()` 方法来实现完全自定义的多轮推理逻辑。请参考[内置多轮调度器实现](https://github.com/modelscope/ms-swift/blob/main/swift/rollout/multi_turn.py)
|
||||
|
||||
**注意**: 这种情况下,相同轨迹的数据会拆分为多条数据,在奖励相关的处理中,需要对相同轨迹的数据分配同样的reward。
|
||||
|
||||
可以在kwargs中获取 trajectory_inputs 获取完整轨迹的数据,具体实现参考[MultiTurnThinkingTips类](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/plugin.py)
|
||||
|
||||
### 多模态数据修改
|
||||
在多模态多轮交互场景下,可能需要在对话过程中动态增删或修改多模态数据,并确保这些变更同步至 trainer。
|
||||
|
||||
实现方式:借助 rollout_infos,通过指定键值覆盖原始数据集的多模态内容。
|
||||
|
||||
现已支持覆盖的键:images、audios、videos。
|
||||
|
||||
具体请参考[DeepEyes Schduler](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/deepeyes/deepeyes_plugin.py#L403-L404)
|
||||
|
||||
### 返回 response token ids
|
||||
在默认的多轮交互流程中,规划器先把模型生成的文本字符串返回给 trainer,trainer 再将其重新 encode 为 token id,用于后续训练。为了避免这一步重复编码的开销,你可以让规划器直接返回 response_token_ids,省去 trainer 侧的再次 encode。
|
||||
|
||||
具体做法如下:
|
||||
|
||||
- 在 response_choice 对象中读取 token_ids 属性,即可获得本次 rollout 生成的 token 序列。
|
||||
- 在 step/run 方法的返回值里加入 response_token_ids,trainer 便能直接使用这些 token id 参与训练,无需重新编码。
|
||||
|
||||
具体实现可以参考[ThinkingModelTipsScheduler](https://github.com/modelscope/ms-swift/blob/main/swift/rollout/multi_turn.py)类
|
||||
|
||||
### 损失掩码
|
||||
|
||||
在工具调用或环境交互返回结果时,若需将返回内容作为模型响应的一部分,建议对这些插入内容进行掩码处理,以确保模型在训练过程中不会对这些外部生成的内容计算损失。
|
||||
|
||||
我们可以通过两种方式设置损失掩码
|
||||
|
||||
**第一种:设置 loss_scale**
|
||||
|
||||
ms-swift 提供 loss_scale 参数来对模型回复部分的内容进行损失缩放设置。比如设置`--loss_scale last_round`,可以将非最后一轮的模型回复的损失置零。我们也可以实现自定义 loss_scale,具体请参考[定制化 loss_scale 文档](../../../Customization/Architecture.md#loss-scale)。
|
||||
|
||||
> 注:在GRPO中,loss_scale 只提供掩码功能,不提供缩放功能。
|
||||
|
||||
**第二种:设置loss_mask**
|
||||
|
||||
在`step`或者`run`方法中设置 response_loss_mask, 可以在规划器中自定义损失掩码。前提需要返回response token ids,返回的 response_loss_mask 需要与 response token ids等长。当返回 response_loss_mask 时,loss_scale 参数失效。
|
||||
|
||||
response_loss_mask 返回可以参考[ToolCallScheduler类](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/plugin.py)
|
||||
|
||||
### 奖励函数相关
|
||||
|
||||
在奖励函数中获取多轮 Rollout 中的信息
|
||||
|
||||
在 `on_turn_end` 方法或 `step/run` 方法中,返回 `rollout_infos` 对象,在奖励函数的 kwargs 中获取 `rollout_infos`:
|
||||
|
||||
```python
|
||||
class Scheduler():
|
||||
def on_turn_end(self, infer_request, response_choice, current_turn):
|
||||
...
|
||||
return {'done': done, 'rollout_infos': extra_dict}
|
||||
|
||||
# 或者在 step 方法中
|
||||
def step(self, infer_request, response_choice, current_turn):
|
||||
...
|
||||
return {'infer_request': infer_request, 'rollout_infos': extra_dict}
|
||||
|
||||
class RewardFunction():
|
||||
def __call__(self, completions, **kwargs):
|
||||
infos = kwargs.get('rollout_infos', {})
|
||||
...
|
||||
```
|
||||
|
||||
### 在 Scheduler 中获取额外的数据集信息
|
||||
|
||||
在训练侧设置参数`--vllm_server_pass_dataset`,可将数据集中的其他列传入多轮规划器。在`infer_request.data_dict`中获取。
|
||||
|
||||
### 训推一致性兼容
|
||||
swift 支持从 vLLM 侧返回 rollout 的 logps 用于纠正训推不一致问题,具体请参考该[文档](../AdvancedResearch/training_inference_mismatch.md)
|
||||
|
||||
在多轮训练中,如果启用了 `rollout_importance_sampling_mode`,框架会自动收集每轮 rollout 的 log probabilities,用于校正训推不一致带来的 off-policy 问题。
|
||||
|
||||
**默认行为**:
|
||||
- 使用默认的 `run` 方法时,框架会自动从 `response_choice.logprobs` 中提取 log probabilities
|
||||
- 这些 logprobs 会与 `response_token_ids` 和 `response_loss_mask` 一起传递给 trainer
|
||||
|
||||
**自定义 Scheduler 的注意事项**:
|
||||
|
||||
如果你在 `step` 方法中修改了 response(如截断、添加内容),需要同步返回对应的 `rollout_logprobs`
|
||||
|
||||
**关键规则**:
|
||||
- `rollout_logprobs` 的长度应该等于 `response_loss_mask` 中值为 1 的数量
|
||||
- 对于 `loss_mask=0` 的 token(如用户添加的提示、工具返回结果),不需要提供 logprobs
|
||||
- 如果 `step` 方法没有返回 `rollout_logprobs`,框架会自动从 `response_choice.logprobs` 中提取
|
||||
|
||||
**重写 `run` 方法的场景**:
|
||||
|
||||
如果你完全重写了 `run` 方法,需要手动收集和传递 `rollout_logprobs`
|
||||
|
||||
具体的实现请参考[内置实现](https://github.com/modelscope/ms-swift/blob/main/swift/rollout/multi_turn.py)
|
||||
@@ -0,0 +1,146 @@
|
||||
# 奖励函数
|
||||
## 自定义奖励函数
|
||||
奖励函数接受模型生成的文本 completions 其他数据集中的列以及训练器状态作为参数(kwargs)进行打分, 其中[训练器状态](https://huggingface.co/docs/transformers/main/main_classes/callback#transformers.TrainerState)包含训练的步数等信息。
|
||||
|
||||
注意:模型输入相关的列(比如query,response)会被处理为 messages 键,原数据集中的 assistant response 会被舍弃,请使用额外的列进行保留。
|
||||
相关处理的列名参考[文档](../../../Customization/Custom-dataset.md#query-response格式)
|
||||
|
||||
以下是一个示例,展示了如何实现一个简单的长度奖励函数。该函数会在模型生成的文本长度超过 1024 时,给予 1.0 的奖励信号;否则,奖励信号为 0.0。
|
||||
|
||||
```python
|
||||
from swift.rewards import ORM, orms
|
||||
class DummyLengthRewardFunction(ORM)
|
||||
def __call__(completions, **kwargs):
|
||||
return [1.0 if len(completion) > 1024 else 0.0 for completion in completions]
|
||||
|
||||
orms['dummy']= DummyLengthRewardFunction
|
||||
```
|
||||
|
||||
**获取数据集中的其他列**
|
||||
|
||||
比如奖励函数需要获取数据集`solution`列、当前训练步数和总步数作为辅助计算,以下是两种获取方式
|
||||
|
||||
第一种:在__call__入参中显式定义列名
|
||||
```python
|
||||
def __call__(completions, solution, trainer_state, **kwargs):
|
||||
print(solution)
|
||||
global_step = trainer_state.global_step
|
||||
max_steps = trainer_state.max_steps
|
||||
...
|
||||
```
|
||||
|
||||
第二种:在kwargs中获取
|
||||
```python
|
||||
def __call__(completions, **kwargs):
|
||||
solution = kwargs.get('solution')
|
||||
trainer_state = kwargs.get('trainer_state')
|
||||
global_step = trainer_state.global_step
|
||||
max_steps = trainer_state.max_steps
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
**使用自定义奖励函数**
|
||||
|
||||
可以在`swift/examples/train/grpo/plugin/plugin.py`中加入该奖励函数,使用参数`--external_plugins examples/train/grpo/plugin/plugin.py`进行注册,并通过 reward_funcs 参数进行指定
|
||||
|
||||
执行脚本参考[这里](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo/plugin/run_external_reward_func.sh)
|
||||
|
||||
## 异步奖励函数
|
||||
|
||||
对于涉及 I/O 操作的奖励函数(如 API 调用、数据库查询等),可以使用异步(async)奖励函数来提高性能。异步奖励函数使用 `asyncio.gather` 并行执行,可以显著加速奖励计算。
|
||||
|
||||
```python
|
||||
from swift.rewards import AsyncORM, orms
|
||||
import asyncio
|
||||
|
||||
class AsyncAPIReward(AsyncORM):
|
||||
async def __call__(self, completions, **kwargs):
|
||||
import aiohttp
|
||||
|
||||
async def score_single(session, text):
|
||||
async with session.post(
|
||||
'https://api.example.com/score',
|
||||
json={'text': text}
|
||||
) as resp:
|
||||
result = await resp.json()
|
||||
return result['score']
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 使用 asyncio.gather 并行发送所有请求
|
||||
tasks = [score_single(session, c) for c in completions]
|
||||
rewards = await asyncio.gather(*tasks)
|
||||
return list(rewards)
|
||||
|
||||
orms['async_api'] = AsyncAPIReward
|
||||
```
|
||||
|
||||
swift 支持同时使用同步和异步奖励函数。训练器会自动检测奖励函数的类型:
|
||||
- 同步奖励函数按顺序执行
|
||||
- 异步奖励函数使用 `asyncio.gather` 并行执行
|
||||
|
||||
[plugin](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/plugin.py)文件中提供了一个调用`swift deploy`服务的生成式奖励模型的例子(async_genrm)
|
||||
|
||||
## 内置奖励函数
|
||||
swift内置了五种基于规则的奖励函数(代码见swift/rewards/orm.py)
|
||||
|
||||
| 奖励函数 | 论文 |
|
||||
|----------------|----------------------------------------------------------------------------|
|
||||
| accuracy | [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via RL](https://arxiv.org/abs/2501.12948) |
|
||||
| format | 同上 |
|
||||
| cosine | [Demystifying Long Chain-of-Thought Reasoning in LLMs](https://arxiv.org/abs/2502.03373) |
|
||||
| repetition | 同上 |
|
||||
| soft_overlong | [Decoupled Clip and Dynamic sAmpling Policy Optimization (DAPO)](https://arxiv.org/abs/2503.14476) |
|
||||
|
||||
### 1. **accuracy**
|
||||
|
||||
该函数将模型的生成结果与数据集中的 solution 列进行比较,计算准确率分数。如果生成结果与标准答案一致,则得分为 1.0;否则为 0.0。
|
||||
|
||||
注意:该奖励函数使用`math_verify`库解析生成结果和solution中的答案,可能只适用于特定的数学数据集。
|
||||
|
||||
### 2. **format**
|
||||
|
||||
论文中使用以下system prompt要求模型按照固定格式进行返回
|
||||
```
|
||||
A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process and answer are enclosed within <think> </think> and <answer> </answer> tags, respectively, i.e., <think> reasoning process here </think><answer> answer here </answer>
|
||||
```
|
||||
|
||||
该函数检查模型是否按照 `<think>think content</think><answer>answer content</answer>` 的格式进行生成。如果生成文本符合格式要求,则得分为 1.0;否则为 0.0。
|
||||
|
||||
### 3. **cosine**
|
||||
|
||||
论文发现,仅使用 accuracy 奖励函数进行训练会导致模型的生成长度趋于超长,从而影响训练效果。cosine 奖励函数通过控制模型的生成长度来优化训练过程:
|
||||
|
||||
- 对于生成正确答案的文本,奖励值随长度增加而递减,鼓励模型生成简洁的回答。
|
||||
- 对于生成错误答案的文本,奖励值随长度增加而递增,鼓励模型进行更深入的思考。
|
||||
|
||||
使用余弦函数平滑地调整奖励值,确保奖励变化在合理范围内。余弦函数的参数包括生成文本的长度、最大长度限制以及奖励的最小值和最大值。
|
||||
|
||||
参数
|
||||
- cosine_min_len_value_wrong(默认值:-0.5):生成错误答案时,最小长度对应的奖励值。
|
||||
- cosine_max_len_value_wrong(默认值:0.0):生成错误答案时,最大长度对应的奖励值。
|
||||
- cosine_min_len_value_correct(默认值:1.0):生成正确答案时,最小长度对应的奖励值。
|
||||
- cosine_max_len_value_correct(默认值:0.5):生成正确答案时,最大长度对应的奖励值。
|
||||
- cosine_max_len(默认值等于模型生成的最大程度):生成文本的最大长度限制。
|
||||
|
||||
|
||||
### 4. **repetition**
|
||||
|
||||
惩罚模型生成文本中的重复内容,通过检测生成文本中的重复 n-gram 模式来评估重复程度,并给予相应的惩罚。
|
||||
|
||||
函数将生成文本分割为单词,并提取指定大小的 n-gram(默认为 3-gram)。通过统计不同 n-gram 的数量与总 n-gram 数量的比例,计算重复比例。如果生成文本中重复的 n-gram 比例较高,则给予较大的负奖励(惩罚)。惩罚值通过重复比例和最大惩罚值(默认为 -1.0)计算得出。
|
||||
|
||||
参数
|
||||
- repetition_n_grams(默认值:3):用于检测重复的 n-gram 大小。
|
||||
- repetition_max_penalty(默认值:-1.0):最大惩罚值,用于控制惩罚的强度。
|
||||
|
||||
### 5. **soft overlong punishment**
|
||||
定义长度惩罚区间。在这个区间内,给予[-1,0]的线性惩罚。
|
||||
|
||||
参数
|
||||
- soft_max_length: 论文中的L_max,模型的最大生成长度,默认等于max_completion_length
|
||||
- soft_cache_length: 论文中的L_cache,控制长度惩罚区间,区间为[soft_max_length-soft_cache_length, soft_max_length]
|
||||
|
||||
## 注意事项
|
||||
|
||||
如果需要在奖励函数中加载模型,默认会使用训练的 deepspeed 插件(transformers逻辑),zero3下可能会导致模型无法正常推理,参考该[issue](https://github.com/modelscope/ms-swift/issues/4580)来跳过 deepspeed 初始化环境
|
||||
@@ -0,0 +1,145 @@
|
||||
# 奖励模型
|
||||
|
||||
默认情况下,奖励模型是指具有分类头数值输出的模型,通常称为输出奖励模型(ORM)。这些模型会对其他模型的输出进行评分,从而生成一个标量值,表示模型响应的质量。
|
||||
|
||||
我们可以通过使用参数 `reward_models` 来加载具有分类头的奖励模型,或者加载经过[奖励建模](../../RLHF.md#rm)训练的奖励模型,进而使用模型的logits作为奖励。
|
||||
|
||||
## 自定义奖励模型
|
||||
对于生成式奖励模型,有两种常见的调用方式:一种是在 Trainer 内部直接使用 reward_model_plugin 定义奖励模型的逻辑,可以使用TransformersEngine对奖励模型进行推理,另一种是通过外部部署的模型服务进行调用。
|
||||
|
||||
- 使用 reward_model_plugin 调用奖励模型时,模型会被内嵌在 Trainer 内部,无需额外占用计算资源。该方式优点是方便集成,但生成速度相对较慢,更适合参数量较小的奖励模型场景。
|
||||
|
||||
- 外部部署奖励模型时,可以通过诸如 swift deploy 或 vllm serve 等命令将模型服务部署于独立设备,大幅提升推理速度,适合参数量较大的模型。但这样需要预留额外的硬件资源。
|
||||
|
||||
### 内部插件
|
||||
|
||||
我们可以在 reward_model_plugin 中灵活地自定义奖励模型的处理逻辑。这使得实现诸如生成式奖励模型等技术成为可能,包括:
|
||||
|
||||
- 自定义模型的系统提示:定义特定的指令和上下文以指导评估过程。
|
||||
- 处理模型交互历史:管理对话上下文,以提供有意义且具有上下文感知的评估。
|
||||
- 定义自定义评估标准:设置独特的标准和度量,用于评估模型的响应,超越默认的准确性和相关性衡量标准。
|
||||
|
||||
通过reward_model_plugin,开发者可以针对其应用的特定需求定制奖励评估过程。这种灵活性允许更细致和有效的基于奖励的训练策略。
|
||||
|
||||
奖励模型通过plugin的`__call__`方法进行调用,该方法接受 `inputs` 作为参数,包含了模型输入输出的 messages 和数据集中的其他列
|
||||
|
||||
```python
|
||||
def __call__(self, inputs):
|
||||
print(inputs)
|
||||
"""
|
||||
[
|
||||
{
|
||||
'messages': [
|
||||
{'role': 'system', 'content': 'system prompt'},
|
||||
{'role': 'query', 'content': 'query'},
|
||||
{'role': 'user', 'content': 'completions1'},
|
||||
],
|
||||
'solution': "abc",
|
||||
},
|
||||
{
|
||||
'messages': [
|
||||
{'role': 'system', 'content': 'system prompt'},
|
||||
{'role': 'query', 'content': 'query'},
|
||||
{'role': 'user', 'content': 'completions2'},
|
||||
],
|
||||
'solution': "abc",
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
在插件中使用 TransformersEngine 进行奖励模型的推理, 我们只需构造 messages ,并通过 infer 接口调用:
|
||||
```python
|
||||
class RMPlugin(DefaultRMPlugin):
|
||||
|
||||
def __init__(self, model, template):
|
||||
|
||||
super().__init__(model, template)
|
||||
# initilize TransformersEngine to infer
|
||||
self.engine = TransformersEngine(self.model, template=self.template, max_batch_size=0)
|
||||
|
||||
def __call__(self, inputs):
|
||||
system_prompt = ...
|
||||
query = ...
|
||||
messages = [{'role': 'system', 'content': system_prompt}, {'role': 'query', 'content': query}]
|
||||
result = self.engine.infer([messages], self.request_config, use_tqdm=False)
|
||||
rewards = ...
|
||||
return rewards
|
||||
```
|
||||
|
||||
我们在 [rm_plugin.py](https://github.com/modelscope/ms-swift/blob/main/swift/rewards/rm_plugin.py) 中提供了一个简单的生成式奖励模型示例(GenRMPlugin)。
|
||||
|
||||
在 [plugin.py](https://github.com/modelscope/ms-swift/blob/main/examples/train/grpo/plugin/plugin.py) 中自定义奖励模型插件,并使用 `external_plugins` 参数进行注册。
|
||||
|
||||
|
||||
注意:
|
||||
1. 在 GRPOTrainer 中,reward_model 会依次append到 reward_funcs 中。因此,reward_weights 的顺序对应 [reward_funcs, reward_model]。
|
||||
2. reward_model_plugin 默认为 default,即使用 ORM 处理逻辑。
|
||||
3. 对于参数量较大的模型,TransformersEngine 生成速度较慢,请使用[外部部署](#外部部署)方法
|
||||
|
||||
对于 BERT 这类无法通过 reward_model 加载的模型,我们可以内置在 reward_function 中进行加载,参考[issue](https://github.com/modelscope/ms-swift/issues/4580)
|
||||
|
||||
### 外部部署
|
||||
|
||||
**示例 2:使用 swift deploy 部署奖励模型并进行远程调用**
|
||||
|
||||
这类方法则不需要使用 reward_model_plugin , 而是直接在奖励函数中进行调用即可
|
||||
|
||||
首先用如下命令启动模型服务:
|
||||
|
||||
```bash
|
||||
# 注意部署的设备不要与训练设备重叠
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
||||
swift deploy \
|
||||
--model Qwen/Qwen2.5-72B-Instruct \
|
||||
--vllm_tensor_parallel_size 4
|
||||
|
||||
# [INFO:swift] model_list: ['Qwen2.5-72B-Instruct']
|
||||
# INFO: Started server process [xxxxxx]
|
||||
# INFO: Waiting for application startup.
|
||||
# INFO: Application startup complete.
|
||||
# INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
在奖励函数中通过 OpenAI 库初始化客户端,指定模型服务的地址和端口,示例代码如下:
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
class RMReward(ORM):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
try:
|
||||
self.client = OpenAI(
|
||||
api_key='EMPTY',
|
||||
base_url='http://127.0.0.1:8000/v1', # 若在本地部署则为 127.0.0.1
|
||||
)
|
||||
self.verify_model_name = self.client.models.list().data[0].id
|
||||
except Exception as e:
|
||||
raise RuntimeError('Failed to connect to the model service. Please deploy the model '
|
||||
"using 'swift deploy' or 'vllm serve'.") from e
|
||||
|
||||
|
||||
def __call__(self, completions, messages, **kwargs) -> List[float]:
|
||||
rewards = []
|
||||
for completion, message in zip(completions, messages):
|
||||
rm_prompt = ... # 构建 reward model 的prompt
|
||||
chat_response = self.client.chat.completions.create(
|
||||
model=self.verify_model_name,
|
||||
messages=[
|
||||
{
|
||||
'role': 'system',
|
||||
'content': 'You are a helpful assistant.'
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': rm_prompt
|
||||
},
|
||||
],
|
||||
)
|
||||
response = chat_response.choices[0].message.content.strip()
|
||||
reward = ... # 根据奖励模型生成结果提取奖励值
|
||||
rewards.append(reward)
|
||||
return rewards
|
||||
|
||||
```
|
||||
@@ -0,0 +1,398 @@
|
||||
# GRPO
|
||||
|
||||
[GRPO(Group Relative Policy Optimization)](https://arxiv.org/abs/2402.03300) 算法利用组内相对优势计算来替代 PPO 算法中独立的价值模型,并直接在损失函数中加入 KL 散度惩罚来提高训练稳定性。
|
||||
|
||||
## 算法原理
|
||||
|
||||
GRPO 目标函数
|
||||
|
||||
$
|
||||
{\scriptstyle
|
||||
\begin{aligned}
|
||||
\mathcal{J}_{G R P O}(\theta) & =\mathbb{E}_{\left[q \sim P(Q),\left\{o_i\right\}_{i=1}^G \sim \pi_{\theta_{o l d}}(O \mid q)\right]} \\
|
||||
& \frac{1}{G} \sum_{i=1}^G \frac{1}{\left|o_i\right|} \sum_{t=1}^{\left|o_i\right|}\left\{\min \left[\frac{\pi_\theta\left(o_{i, t} \mid q, o_{i,<t}\right)}{\pi_{\theta_{o l d}}\left(o_{i, t} \mid q, o_{i,<t}\right)} \hat{A}_{i, t}, \operatorname{clip}\left(\frac{\pi_\theta\left(o_{i, t} \mid q, o_{i,<t}\right)}{\pi_{\theta_{o l d}}\left(o_{i, t} \mid q, o_{i,<t}\right)}, 1-\varepsilon, 1+\varepsilon\right) \hat{A}_{i, t}\right]-\beta \mathbb{D}_{K L}\left[\pi_\theta| | \pi_{r e f}\right]\right\}
|
||||
\end{aligned}
|
||||
}
|
||||
$
|
||||
|
||||
其中优势函数定义为
|
||||
|
||||
$
|
||||
\hat{A}_{i,t} = \frac{R_i - \text{mean}(\{R_j\}_{j=1}^G)}{\text{std}(\{R_j\}_{j=1}^G)}
|
||||
$
|
||||
|
||||
|
||||
<details> <summary>GRPO算法伪代码</summary>
|
||||
|
||||
```python
|
||||
# ========== 1. Rollout Generation Phase ==========
|
||||
prompt = "Question: Which is bigger? 9.11 or 9.9?"
|
||||
|
||||
# Generate multiple completions through parallel sampling
|
||||
completions = rollout_function(
|
||||
model=current_policy_model,
|
||||
prompt=prompt,
|
||||
num_generations=8, # Hyperparameter: number of samples per prompt
|
||||
temperature=1.0 # Hyperparameter: sampling diversity
|
||||
)
|
||||
"""
|
||||
completions = [
|
||||
(completion 1) "The larger number is 9.9...",
|
||||
(completion 2) "9.11 is bigger than...",
|
||||
...
|
||||
(completion 8) "After calculation, 9.9..."
|
||||
]
|
||||
"""
|
||||
|
||||
# ========== 2. Reward Calculation Phase ==========
|
||||
# Evaluate generated completions using reward model
|
||||
rewards = reward_function(
|
||||
completions=completions,
|
||||
ground_truth="9.9" # Expected correct answer
|
||||
)
|
||||
"""
|
||||
rewards = [
|
||||
(reward 1) 1.0, # Correct answer
|
||||
(reward 2) 0.0, # Incorrect
|
||||
...
|
||||
(reward 8) 1.0 # Correct
|
||||
]
|
||||
"""
|
||||
|
||||
# Normalize rewards to advantages
|
||||
rewards_mean = mean(rewards) # μ = 0.5
|
||||
rewards_std = std(rewards) # σ = 0.25
|
||||
advantages = (rewards - rewards_mean) / (rewards_std + 1e-8) # Standardization
|
||||
"""
|
||||
advantages = [
|
||||
(advantage 1) 2.0, # (1.0 - 0.5)/0.25
|
||||
(advantage 2) -2.0,
|
||||
...
|
||||
(advantage 8) 2.0
|
||||
]
|
||||
"""
|
||||
|
||||
# ========== 3. Policy Optimization Phase ==========
|
||||
# Get token-level log probabilities from different models
|
||||
current_logps = get_per_token_logps(current_policy_model, prompt, completions) # π_θ
|
||||
old_logps = get_per_token_logps(old_policy_model, prompt, completions) # π_θ_old
|
||||
ref_logps = get_per_token_logps(reference_model, prompt, completions) # π_ref
|
||||
|
||||
# PPO Clipped Objective
|
||||
is_ratio = exp(current_logps - old_logps) # Importance sampling ratio: e^(π_θ - π_θ_old)
|
||||
clipped_ratio = clip(is_ratio, 1-ε, 1+ε) # ε=0.2 typically
|
||||
|
||||
# Policy gradient term (dual form)
|
||||
policy_loss = -mean(
|
||||
minimum(is_ratio * advantages, # Unclipped objective
|
||||
clipped_ratio * advantages) # Clipped objective
|
||||
)
|
||||
|
||||
# KL Divergence Penalty (K3 estimator)
|
||||
# KL(π_θ||π_ref) ≈ e^(logπ_ref - logπ_θ) - (logπ_ref - logπ_θ) - 1
|
||||
kl_penalty = beta * mean(
|
||||
exp(ref_logps - current_logps) -
|
||||
(ref_logps - current_logps) - 1
|
||||
)
|
||||
|
||||
# Total Loss = Policy Loss + KL Penalty
|
||||
total_loss = policy_loss + kl_penalty
|
||||
|
||||
# ========== 4. Update Rule ==========
|
||||
# Apply gradient descent to minimize total_loss
|
||||
optimizer.zero_grad()
|
||||
total_loss.backward()
|
||||
optimizer.step()
|
||||
```
|
||||
</details>
|
||||
|
||||
训练脚本示例参考[examples](https://github.com/modelscope/ms-swift/tree/main/examples/train/grpo)
|
||||
|
||||
GRPO参数参考[文档](../../../Instruction/Command-line-parameters.md#grpo参数)
|
||||
|
||||
## 集群支持
|
||||
|
||||

|
||||
|
||||
GRPO 训练框架支持集成高性能推理引擎(如 vLLM)来加速采样过程,提供以下两种部署模式:
|
||||
|
||||
### 1. Colocate(Internal) Mode
|
||||
|
||||
训练与推理共享GPU资源,在 Trainer 内部启动推理服务,
|
||||
|
||||
启动参数
|
||||
```bash
|
||||
--use_vllm true \
|
||||
--vllm_mode colocate
|
||||
```
|
||||
|
||||
#### Colocate 模式下的显存优化方案
|
||||
在 Colocate 模式下运行时,容易出现显存不足(OOM)的情况。以下是几种有效的显存优化方法和参数配置:
|
||||
|
||||
1. 降低`vllm_gpu_memory_utilization` 参数
|
||||
|
||||
|
||||
2. 在训练阶段,释放 vLLM 占用的显存:
|
||||
|
||||
```bash
|
||||
--sleep_level 1
|
||||
```
|
||||
|
||||
3. 在vLLM 推理阶段,释放模型和优化器占用的显存:
|
||||
|
||||
```bash
|
||||
--offload_optimizer true \
|
||||
--offload_model true \
|
||||
```
|
||||
|
||||
4. 在vLLM中使用 Tensor Parallel 技术:
|
||||
|
||||
```bash
|
||||
--vllm_tensor_parallel_size [tp_size]
|
||||
```
|
||||
|
||||
5. 分批 Gather 模型权重(zero3下同步 vLLM 权重时):
|
||||
|
||||
```bash
|
||||
--move_model_batches [批次数量]
|
||||
```
|
||||
|
||||
6. 将 Megatron 导出的用于 vLLM 更新的 HF 格式权重存放在 CPU 主存中,以降低 GPU 显存占用:
|
||||
|
||||
```bash
|
||||
--offload_bridge true
|
||||
```
|
||||
|
||||
### 2. Async(External) Mode
|
||||
|
||||
训练与推理资源分离,启动单独的推理服务器
|
||||
|
||||
使用`swift rollout`命令部署vLLM 服务器, 现仅支持vLLM backend
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 \
|
||||
swift rollout \
|
||||
--model Qwen/Qwen2.5-VL-7B-Instruct \
|
||||
--vllm_tensor_parallel_size 2 \
|
||||
--vllm_data_parallel_size 1
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1 \
|
||||
swift rollout \
|
||||
--model Qwen/Qwen2.5-VL-7B-Instruct \
|
||||
--vllm_tensor_parallel_size 2 \
|
||||
--vllm_data_parallel_size 1
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
||||
swift rollout \
|
||||
--model Qwen/Qwen2.5-VL-7B-Instruct \
|
||||
--vllm_tensor_parallel_size 2 \
|
||||
--vllm_data_parallel_size 2
|
||||
```
|
||||
|
||||
更多 rollout 参数参考[vLLM参数](../../../Instruction/Command-line-parameters.md#vllm参数)和[rollout 参数](../../../Instruction/Command-line-parameters.md#rollout参数)
|
||||
|
||||
注意:在使用 vllm_use_async_engine 时,仅开启 DP 可能会导致错误,相关问题参考: [vllm issue](https://github.com/vllm-project/vllm/issues/18567)。如果出现错误,请尝试同时启用 TP 和 DP,或升级vLLM
|
||||
|
||||
|
||||
训练使用以下参数配置外部 vLLM 服务器
|
||||
```bash
|
||||
--use_vllm true \
|
||||
--vllm_mode server \
|
||||
--vllm_server_host <服务器IP> \
|
||||
--vllm_server_port <服务端口> \
|
||||
--vllm_server_timeout <超时时间> \
|
||||
```
|
||||
|
||||
### 权重同步加速
|
||||
设置以下参数可以通过仅同步 LoRA adapter 权重而非全量模型权重,优化 LoRA 训练的权重同步速度。
|
||||
|
||||
> 注意:这种同步方式会略微影响 vLLM 推理速度。
|
||||
|
||||
```bash
|
||||
# rollout(server mode)
|
||||
swift rollout \
|
||||
--vllm_enable_lora true \
|
||||
--vllm_max_lora_rank xxx # 与训练脚本lora_rank一致
|
||||
...
|
||||
|
||||
# grpo(colocate mode)
|
||||
swift rlhf \
|
||||
--rlhf_type grpo \
|
||||
--vllm_mode colocate \
|
||||
--vllm_enable_lora true \
|
||||
...
|
||||
|
||||
# megatron grpo(colocate mode)
|
||||
swift megatron rlhf \
|
||||
--rlhf_type grpo \
|
||||
--vllm_mode colocate \
|
||||
--vllm_enable_lora true \
|
||||
...
|
||||
```
|
||||
|
||||
**多模态模型 ViT 层 LoRA 同步:** 如果训练时开启了 ViT 层的 LoRA(`freeze_vit false`),
|
||||
在仅同步LoRA的模式下,需要相应在 vLLM 侧开启 tower/connector LoRA 支持。
|
||||
|
||||
通过 `vllm_engine_kwargs` 传入:
|
||||
|
||||
```bash
|
||||
--vllm_engine_kwargs '{"enable_tower_connector_lora": true}'
|
||||
```
|
||||
|
||||
该功能为 vLLM 实验性特性,目前支持 Qwen2.5-VL、Qwen3-VL 等模型。
|
||||
具体支持情况请参阅 [vLLM 文档](https://docs.vllm.ai/en/latest/features/lora/)
|
||||
和 [vLLM issue](https://github.com/vllm-project/vllm/issues/31479)。
|
||||
|
||||
## logged metrics
|
||||
- completions/mean_length:生成的 completion 的平均长度。
|
||||
- completions/min_length:生成的 completion 的最小长度。
|
||||
- completions/max_length:生成的 completion 的最大长度。
|
||||
- completions/clipped_ratio:被长度截断的 completion 占比。
|
||||
- reward/{reward_func_name}/mean:某个特定 reward function 的平均奖励值。
|
||||
- reward/{reward_func_name}/std:某个特定 reward function 的奖励标准差。
|
||||
> 注意, 上述两个指标是在所有 completions 范围内统计得到的。
|
||||
- reward:加权 reward_weights 后的整体平均奖励。
|
||||
- reward_std:加权 reward_weights 后,每个 batch 内整体奖励的标准差。
|
||||
> 注意:上述两个指标是先在每个组内分别计算均值/std,然后再对各组的结果取平均。
|
||||
- frac_reward_zero_std:在生成 batch 中,reward 标准差为零的样本比例,意味着该 prompt 上的答案几乎无多样性(所有回答奖励一致)。
|
||||
- kl:生成的 completion 上,模型与参考模型之间的平均 KL 散度。仅当 beta 非零时记录。
|
||||
- clip_ratio/region_mean:不同句子中被 CLIP 的的 token 平均比例
|
||||
- clip_ratio/low_mean:不同句子中被 下CLIP 的的 token 平均比例
|
||||
- clip_ratio/low_min:不同句子中被 下CLIP 的的 token 最小比例
|
||||
- clip_ratio/high_mean:不同句子中被 上CLIP 的的 token 平均比例
|
||||
- clip_ratio/high_max:不同句子中被 上CLIP 的的 token 最大比例
|
||||
> 注意:如果开启`overlong_filter`, kl 和 clip_ratio 指标会过滤超长的样本
|
||||
|
||||
如果设置了`log_entropy`参数,则会额外记录entropy相关指标,包括
|
||||
- entropy/mean: 不同句子中的 entropy 均值
|
||||
- entropy/max: 不同句子中的 entropy 最大值
|
||||
- entropy/min: 不同句子中的 entropy 最小值
|
||||
> 注意这里的 句子 entropy 指 completion 中的 token entropy 均值
|
||||
|
||||
|
||||
如果设置了`top_entropy_quantile`参数<1.0, 则会记录entropy threshold的值
|
||||
- entropy/threshold: 分位点处的 entropy 值,小于该值的 token 将不会被计算 loss
|
||||
|
||||
训推一致性指标,前缀为rollout_correction,需设置`log_rollout_offpolicy_metrics=true`或`rollout_importance_sampling_mode`:
|
||||
- `kl` / `k3_kl`:训练策略与 rollout 策略之间的 KL 散度(直接估计器 / K3 估计器)
|
||||
- `training_ppl` / `rollout_ppl`:训练策略和 rollout 策略的困惑度
|
||||
- `log_ppl_diff`:log PPL 差异,反映分布偏移程度
|
||||
- `ppl_ratio`:PPL 比率
|
||||
- `chi2_token` / `chi2_seq`:Token/Sequence 级别的 χ² 散度
|
||||
|
||||
IS 校正指标(需设置`rollout_importance_sampling_mode`):
|
||||
- `is_weight_mean`:平均重要性采样权重
|
||||
- `ess`:有效样本大小(Effective Sample Size)
|
||||
- `clipped_frac`:被截断或屏蔽的样本比例
|
||||
|
||||
> 训推一致性指标详细说明请参考文档 [Training-Inference-Mismatch](../AdvancedResearch/training_inference_mismatch.md)
|
||||
|
||||
如果设置了`log_completions`, 将保存训练动态在output对应文件夹中,包括
|
||||
- step:记录时的训练步数
|
||||
- prompt:模型输入
|
||||
- completion:模型采样回答
|
||||
- {reward_func_name}:特定奖励
|
||||
- entropy:entropy token 均值,在设置`log_entropy`时记录
|
||||
|
||||
设置 `report_to wandb/swanlab` 将训练动态Table推送到对应的平台
|
||||
|
||||
如果需要在Table中额外记录其他列,请在 `GRPOTrainer._generate_and_score_completions` 方法中,设置 metrics_to_gather 字典。
|
||||
|
||||
默认自动检测
|
||||
- `image`:视觉数据集图像输入。(暂时只支持wandb)
|
||||
- `solution`:数据集中的 solution 列。
|
||||
|
||||
## FAQ
|
||||
**1. 训练过程中 loss 等于0 / 接近0 / 小于0**
|
||||
|
||||
正常情况, 参考[issue](https://github.com/huggingface/open-r1/issues/239#issuecomment-2646297851)
|
||||
|
||||
**2. num_generations / 批量大小相关**
|
||||
|
||||
在 GRPO 中,batch_size 以 completion(模型生成结果) 为单位。例如,设置 per_device_train_batch_size=8 表示每张 GPU 在训练过程中会同时处理 8 个 completion 的 loss 计算。
|
||||
|
||||
训练阶段,在一次完整的梯度累计 batch 中,总的批量大小等于:
|
||||
|
||||
```
|
||||
effective_batch_size = num_processes * per_device_train_batch_size * gradient_accumulation_steps
|
||||
```
|
||||
|
||||
采样阶段,总的批量大小 (completion-level) 数量等于:
|
||||
|
||||
1. 设置 generation_batch_size 下,等于 generation_batch_size
|
||||
2. 设置 steps_per_generation 下,等于 per_device_train_batch_size * steps_per_generation * num_processes
|
||||
3. 默认情况下,steps_per_generation = gradient_accumulation_steps,generation_batch_size = per_device_train_batch_size * steps_per_generation * num_processes = per_device_train_batch_size * gradient_accumulation_steps * num_processes = effective_batch_size
|
||||
|
||||
在评估阶段,completion 的数量等于:
|
||||
```
|
||||
num_processes * per_device_eval_batch_size
|
||||
```
|
||||
|
||||
参数 `num_generations` 必须能够被以上采样阶段和评估的总批量大小整除,以保证生成任务可以均匀分配到各个设备上。
|
||||
|
||||
**示例**
|
||||
|
||||
- num_processes = 8
|
||||
- per_device_train_batch_size = 4
|
||||
- gradient_accumulation_steps = 8
|
||||
- generation_batch_size = 512
|
||||
- num_generations = 64
|
||||
|
||||
1. 采样需要的总数据(prompt)量等于 512 / 64 = 8
|
||||
2. 每次采样 512 条模型回复
|
||||
3. 每次更新模型权重批量大小为 8 *4 * 8 = 256
|
||||
|
||||
|
||||
**3. 为什么 KL 出现了NaN**
|
||||
|
||||
开启 overlong_filter 后,某一卡上的所有 completion 都被截断
|
||||
|
||||
**4. 训练的steps怎么计算?**
|
||||
|
||||
参考[issue](https://github.com/modelscope/ms-swift/issues/3912)
|
||||
|
||||
**5. clip_ratio为什么总是0?**
|
||||
|
||||
Clip机制的核心目的是限制策略更新的幅度,防止因单次更新过大而导致策略性能崩溃(即策略更新后表现急剧下降)。
|
||||
Clip操作的具体公式如下:
|
||||
|
||||
$
|
||||
L_{\text{CLIP}}(\theta) = \mathbb{E}_{t} \left[ \min\left(r_{t}(\theta) \hat{A}_{t}, \text{clip}(r_{t}(\theta), 1 - \epsilon, 1 + \epsilon) \hat{A}_{t} \right) \right]
|
||||
$
|
||||
|
||||
其中:$r_{t}(\theta) = \frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\text{old}}(a_{t} \mid s_{t})}$ 是重要性采样比,衡量新旧策略的差异。$\hat{A}_{t}$ 是优势函数(advantage function),表示动作的相对收益。$\epsilon$ 用于限制 $r_{t}(\theta)$ 的偏离范围。
|
||||
|
||||
在 on-policy 训练过程中,由于每次更新都使用最新策略生成的数据,新旧策略相同,即 $\pi_{\theta} = \pi_{\text{old}}$
|
||||
|
||||
因此重要性采样比恒为 1,此时,clip 操作不会生效。
|
||||
|
||||
在设置以下参数情况下,算法为off-policy (near-on-policy)
|
||||
1. num_iterations > 1, 或者
|
||||
2. gradient_accumulation_steps % steps_per_generation != 0
|
||||
|
||||
参考[issue](https://github.com/huggingface/open-r1/issues/239#issuecomment-2646297851)
|
||||
|
||||
**6. 如何设置训练的 `mini-batch size`**
|
||||
|
||||
在 GRPO 训练中,我们可以通过以下两种方式配置 mini-batch 更新:
|
||||
- 设置 `generation_batch_size` 为训练 global batch size (effective_batch_size) 的整数倍
|
||||
- 或设置 `steps_per_generation` 为 `gradient_accumulation_steps` 的整数倍
|
||||
|
||||
典型配置示例:
|
||||
- 当配置:
|
||||
steps_per_generation = 16, gradient_accumulation_steps = 8, mini_batch_size = steps_per_generation / gradient_accumulation_steps = 2. 则 1 次 rollout 结果将拆分成 2 批 mini-batch 进行更新。
|
||||
|
||||
**7. swift deploy 与 swift rollout 的区别**
|
||||
|
||||
- swift deploy 主要用于模型的部署和推理,支持 PT、vLLM、SGLang 等多种引擎,兼容流式推理与 OpenAI API 的调用格式。
|
||||
|
||||
- swift rollout 则专注于 GRPO 推理加速,目前仅支持 vLLM 引擎,并内置了权重自动同步的功能。
|
||||
|
||||
|
||||
**8. 如何取消 KL 项损失**
|
||||
|
||||
将参数设置为 `--beta 0`,即可关闭 KL 损失的计算,并且不会加载参考模型(ref model)。
|
||||
|
||||
|
||||
## RL微信群
|
||||
|
||||
<img src="https://raw.githubusercontent.com/modelscope/ms-swift/main/docs/resources/wechat/grpo.png" width="250">
|
||||
@@ -0,0 +1,6 @@
|
||||
Get Started
|
||||
===============
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
GRPO.md
|
||||
@@ -0,0 +1,19 @@
|
||||
GRPO
|
||||
===============
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Get Started
|
||||
|
||||
GetStarted/index.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Developer Guide
|
||||
|
||||
DeveloperGuide/index.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Advanced Research
|
||||
|
||||
AdvancedResearch/index.rst
|
||||
Reference in New Issue
Block a user