NLP——LLM对齐微调-VeRL-rollout_corr解读

注:本文包含 AI 辅助创作


整体说明

  • “Rollout Correction” 包含两种可同时存在的修正组件:
    • 重要性采样(IS,Importance Sampling)权重,修正 off-policy 偏差
    • 拒绝采样(RS,Rejection Sampling),过滤异常样本
  • 内部变量 rollout_is_weights 特指 IS 权重组件
  • Rollout Correction(Rollout 校正)提供了一个统一的框架,用于处理 RL 训练中的一般 off-policy 问题
    • 任何数据收集分布与训练分布不同的场景都可以从这些方法中受益

常见的 off-policy 场景:

  • 1)策略不匹配(实现差异)
    • 不同精度:FP8 vs FP16 vs BF16 vs FP32
    • 不同后端:vLLM vs SGLang vs FSDP vs Megatron
    • 即使权重相同,不同实现也会有差异
  • 2)时间滞后(模型陈旧性)
    • Rollout 使用较旧的 checkpoint,而训练已经推进
    • 具有陈旧参数的异步 Rollout 工作器
    • 常见于分布式/异步 RL 系统
  • 3)回放缓冲区(Replay Buffers)
    • 在来自更早迭代的历史轨迹上训练
    • 来自不同策略版本的经验回放
    • 数据增强或重采样策略
  • 4)Off-Policy 算法
    • 来自专家演示的行为克隆
    • DAPO(来自辅助策略的数据)
    • 任何使用来自不同策略轨迹的算法
  • 5)数据质量过滤
    • 收集数据的重新加权或过滤
    • 具有修改分布的偏好学习
    • 具有分布偏移的课程学习
  • 这些 off-policy 差距可能导致训练不稳定和策略崩溃
  • Rollout Correction(Rollout 校正)使用重要性采样(IS)权重和拒绝采样(RS)来校正数据收集与训练之间的任何分布偏移

关于常见实现错误的重要说明:

  • 许多 LLM-RL 实现错误地应用 PPO,忽略实际的 Rollout 策略 \(\pi_{\text{rollout}}\),并假设训练参考策略 \(\pi_{\text{old}}\) 是行为策略
  • 当 \(\pi_{\text{rollout}}\) ≠ \(\pi_{\text{old}}\) 时(由于 LLM-RL 中 Rollout 与训练之间的精度/后端差异,这很典型),这在数学上是不正确的
  • 这不是 PPO 的错
    • PPO 本身在数学上是正确的,问题在于 naive 实现中错误地假设 \(\pi_{\text{old}}\) = \(\pi_{\text{rollout}}\)
  • 这个导致 RL 训练崩溃的关键实现错误在博客文章 “When Speed Kills Stability: Demystifying RL Collapse from the Training-Inference Mismatch” 中被指出,并推动了本 Rollout 校正框架的开发

数学上正确的方法

  • Decoupled 模式(Decoupled mode) :三种策略(\(\pi_{\text{rollout}}\)、\(\pi_{\text{old}}\)、\(\pi_\theta\)),使用从 \(\pi_{\text{rollout}}\) 到 \(\pi_{\text{old}}\) 的 IS 校正
  • ** Bypass Mode (Bypass mode)** :两种策略(\(\pi_{\text{rollout}}\) = \(\pi_{\text{old}}\)、\(\pi_\theta\)),使用实际 Rollout 策略作为 PPO 锚点
  • Bypass + 策略梯度模式(Bypass + Policy Gradient mode) :两种策略(\(\pi_{\text{rollout}}\)、\(\pi_\theta\)),使用 IS/RS 校正且不进行 PPO 裁剪
  • 详见 rollout_corr_math.md 文件

关键设计原则:IS 权重与拒绝采样的分离

  • 实现清晰地将两种正交机制分开
  • 机制1 IS 权重rollout_is_weights):用于梯度校正的连续重加权
    • 策略比率:\(\pi_{\text{old}}\)/\(\pi_{\text{rollout}}\)(Decoupled 模式)或 \(\pi_\theta\)/\(\pi_{\text{rollout}}\)( Bypass Mode )
    • 安全有界 :被限制在 [exp(-20), exp(20)] ≈ [2e-9, 5e8] 以防止溢出
      • Token 级:对每个 token 的比率施加界限
      • 序列级:对比率乘积施加界限(广播到所有 token)
    • 截断 :通过 .clamp(max=rollout_is_threshold) 进行上限截断(TIS:截断重要性采样,Truncated Importance Sampling)
    • 在填充位置置零 :乘以 response_mask 以将填充位置置零
    • 用于加权策略梯度(方差降低)
  • 机制2 拒绝采样modified_response_mask):用于排除异常值的二值过滤
    • 创建二值掩码:1 = 保留,0 = 拒绝
    • 拒绝 IS 比率落在 [lower_threshold, upper_threshold] 之外的 token/序列
    • 修改 response_mask 以从训练中排除被拒绝的样本
  • 这种分离确保:
    • ✅ IS 权重提供连续重加权(降低方差)
    • ✅ 拒绝采样提供硬过滤(移除极端异常值)
    • ✅ 两种机制可以独立或同时启用
    • ✅ 安全界限在所有情况下防止数值溢出

Quark Start:使用经过验证的预设(Verified Presets)

  • 现在提供类型化配置,以及针对常见场景经过验证的预设
    • 这些预设已在各种模型和训练场景下经过数万 GPU 小时的验证

Python API

  • Python API 调用可以快速获取这些 config:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    from verl.trainer.config.algorithm import RolloutCorrectionConfig

    # === Decoupled PPO 模式(3 种策略:\\(\pi_{\text{rollout}}\\)、\\(\pi_{\text{old}}\\)、\\(\pi_\theta\\))===
    # IS 权重校正 \\(\pi_{\text{old}}\\) 与 \\(\pi_{\text{rollout}}\\) 之间的差距
    config = RolloutCorrectionConfig.decoupled_token_is() # Token-TIS
    config = RolloutCorrectionConfig.decoupled_seq_is() # Seq-TIS
    config = RolloutCorrectionConfig.decoupled_seq_is_rs() # Seq-MIS
    config = RolloutCorrectionConfig.decoupled_geo_rs() # Geo-RS(比率模式)
    config = RolloutCorrectionConfig.decoupled_geo_rs_token_tis() # Geo-RS + Token-TIS

    # === K3 KL 估计器预设(对于小 KL 更稳定)===
    config = RolloutCorrectionConfig.decoupled_k3_rs() # K3-RS 仅
    config = RolloutCorrectionConfig.decoupled_k3_rs_token_tis() # K3-RS + Token-TIS

    # === Bypass PPO 模式(2 种策略:\\(\pi_{\text{rollout}}\\) = \\(\pi_{\text{old}}\\)、\\(\pi_\theta\\))- 快速 ===
    # PPO 比率处理 IS,因此不需要显式 IS 权重
    config = RolloutCorrectionConfig.bypass_ppo_clip() # PPO-clip 仅
    config = RolloutCorrectionConfig.bypass_ppo_clip_geo_rs() # PPO-clip + Geo-RS
    config = RolloutCorrectionConfig.bypass_ppo_clip_k3_rs() # PPO-clip + K3-RS

    # === Bypass PG 模式(2 种策略,无 PPO 裁剪)- 快速 ===
    # IS 权重在损失函数中即时计算为 \\(\pi_\theta\\) / \\(\pi_{\text{rollout}}\\)
    config = RolloutCorrectionConfig.bypass_pg_is() # Seq-TIS + PG
    config = RolloutCorrectionConfig.bypass_pg_geo_rs() # Geo-RS + PG
    config = RolloutCorrectionConfig.bypass_pg_geo_rs_token_tis() # Geo-RS + Token-TIS + PG

    # === 其他 ===
    config = RolloutCorrectionConfig.disabled() # 仅指标(不进行校正)

YAML 配置(高级)

  • 用于高级定制或基于 YAML 的配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    algorithm:
    rollout_correction:
    rollout_is: token # IS 权重:"token"、"sequence" 或 null
    rollout_is_threshold: 2.0 # TIS 上限,或用于 IcePop 的 "0.5_5.0"
    rollout_is_batch_normalize: false # 批归一化 IS 权重使均值为 1.0
    rollout_rs: null # 拒绝采样:逗号分隔的规范选项(例如 "token_k1,seq_max_k2")
    rollout_rs_threshold: null # 阈值规格:float 或 "lower_upper" 字符串
    bypass_mode: false # 跳过 old_log_prob 计算(设置 \\(\pi_{\text{old}}\\) = \\(\pi_{\text{rollout}}\\))
    loss_type: ppo_clip # Bypass Mode 中的损失类型:"ppo_clip"(默认)或 "reinforce"

    # 必需:启用 log prob 计算
    actor_rollout_ref:
    rollout:
    calculate_log_probs: true
  • 注:使用 YAML 配置的时候可以参考 Python API 的内容来配置,确保自己的配置和官方一致


Configuration Parameters

  • 所有参数都在 algorithm.rollout_correction 下:

rollout_is(str 或 null)

  • 重要性采样权重聚合级别:
    • null = 不计算 IS 权重(仅指标模式)
    • "token":每个 Token 的 IS 权重
      • Decoupled 模式 :\(\rho_t\) = \(\pi_{\text{old}}(t)\)/\(\pi_{\text{rollout}}(t)\)
      • Bypass/纯 IS 模式 :\(\rho_t\) = \(\pi_\theta(t)\)/\(\pi_{\text{rollout}}(t)\)
      • 每个 Token 独立截断
      • 典型阈值:1.5 - 5.0
    • "sequence":每个序列的权重 \(\rho_\text{seq} = \prod_t \rho_t\)
      • 在序列上进行乘法聚合
      • 典型阈值:2.0 - 10.0
  • 所有 IS 权重都被安全限制在 [exp(-20), exp(20)] ≈ [2e-9, 5e8] 范围内

rollout_is_threshold(str 或 float)

  • IS 加权的阈值,默认值:2.0
  • 单个 float 或类 float 字符串:通过 .clamp(max=rollout_is_threshold) 进行 TIS
  • 诸如 "0.5_5.0""lower_upper" 字符串:IcePop,将 [lower, upper] 外的权重置零
  • 应用于 IS 权重以降低方差
  • 与拒绝采样分开(由 rollout_rs 参数控制)
  • rollout_rs 不同,IcePop 不修改 response_mask;它只改变 IS 系数

rollout_is_batch_normalize(bool)

  • 对 IS 权重应用批归一化,默认值:False
    • True:在每个批次内将 IS 权重归一化为均值 = 1.0
      • Token 级 IS :在所有 token 权重上归一化,即除以 Token 粒度的均值(保证优化后的均值为 1)
      • 序列级 IS :在序列均值上归一化(每个序列一个权重),即除以 Sequence 粒度的均值(保证归一化后的均值为 1)
      • 注:不论哪种情况,被除以的那个数字都是 weights_mean,并被上报为 rollout_is_batch_norm_factor 指标
    • False:使用原始(截断的)IS 权重
  • 通过确保每批平均权重为 1.0 来降低方差
  • 在截断之后应用,以保留截断语义
  • 仅影响最终生效的 IS 权重值,不影响拒绝采样
    • 特别说明:也不会影响 rollout_is_meanrollout_is_seq_mean 等上报指标
    • 打开 rollout_is_batch_normalize,可以看到 rollout_is_batch_norm_factor 指标(这个指标是归一化时除以的那个均值 weights_mean
    • 补充:rollout_is_batch_norm_factorrollout_is_seq_mean 的关系
      • sequence 粒度的 TIS 校准中,当前上报方式下两者值相等,但 202603 之前(202511 版本)中,两者有差异,当时上报的 rollout_is_seq_mean 等指标是在截断 rollout_is_weights = rollout_is_weights.clamp(max=rollout_is_threshold) 之前上报的
      • token 粒度的 TIS 校准 rollout_is_batch_norm_factorrollout_is_mean 的关系 也类似

rollout_rs(str 或 null)

  • 拒绝采样聚合模式,提供逗号分隔的字符串(空格可选),使用 rollout_corr_helper 中实现的规范选项:
    • token_k1:Token 级拒绝,使用 -log r 界限(比率阈值以 lower_upper 提供),示例:"0.6_1.4"
    • token_k2:Token 级拒绝,使用 0.5 * (log r)^2(仅上限)
    • token_k3:Token 级拒绝,使用 exp(log r) - 1 - log r(仅上限)
    • seq_sum_k1:序列级拒绝,使用 -log r 之和(比率界限)
    • seq_sum_k2:序列级拒绝,使用 0.5 * (log r)^2 之和(仅上限)
    • seq_sum_k3:序列级拒绝,使用 exp(log r) - 1 - log r 之和(仅上限)
    • seq_mean_k1:序列级拒绝,使用 -log r 的均值(比率界限)
    • seq_mean_k2:序列级拒绝,使用 0.5 * (log r)^2 的均值(仅上限)
    • seq_mean_k3:序列级拒绝,使用 exp(log r) - 1 - log r 的均值(仅上限)
    • seq_max_k2:序列级拒绝,使用 0.5 * (log r)^2 的最大值(仅上限)
    • seq_max_k3:序列级拒绝,使用 exp(log r) - 1 - log r 的最大值(仅上限)

rollout_rs_threshold(str、float 或 null)

  • rollout_rs_threshold 是拒绝采样的阈值
  • 为每个选项提供一个 entry ,用逗号分隔,单个 entry 会广播到所有 option
    • 注:这里这么说是因为 rollout_rs 是可以同时配置多个的 rollout_rs="token_k1,seq_max_k2" 这样,此时 rollout_rs_threshold 可以使用单个值来自动广播,也可以按照逗号分别配置阈值
  • 不同模式的阈值说明:
    • k1 KL 模式(*k1 :使用 "lower_upper" 字符串(例如 "0.7_1.3"
      • 理解:这里的 k1 KL 模式包括 token_k1、seq_sum_k1、seq_mean_k1 等,这些是双侧比率限制(因为 k1 是可能为负值的),阈值要写成 包含 “lower_upper” 的字符串
      • 注:如果只写一个 float 则意味着只是上界(此时的下限默认为其倒数)
    • k2/k3 KL 模式(*k2/*k3 :提供正的上限(float 或数字字符串)
      • 理解:包括 token_k2、seq_max_k2、seq_mean_k3 等,这些是单侧散度限制(因为 k2 和 k3 是非负的),只需要提供一个正的上界
  • 阈值设置为 null 时,表示完全禁用阈值
    • 注意:仅当 rollout_rsnull (即不启用任何拒绝采样)时才合法
  • 使用注意:在训练时,使用 RS 有可能导致 assert response_mask.any(), "Expected at least one valid token in response mask"
    • 这里 Rollout Correction 的 off-policy 指标统计是在 micro_batch 维度,所以 micro_batch 上的所有样本都被 Mask 时,则会直接报错
      • 注:类似 Megatron 中将 mini-batch 拆为多个 micro-batch,verl 也会将 mini-batch 拆为多个 micro-batch
        • verl 中策略:
          • 打开 use_dynamic_bsz=True 时使用 rearrange_micro_batches 函数来拆分 micro-batches
            • 这里的目标是让每个 GPU 单次的 Token 数量小于等于 ppo_max_token_len_per_gpu,所以 micro-batch 数量也是不确定的(平均队列越短,得到的 micro-batch 数量越小)
            • 极端情况下一个 micro-batch 中的真实样本数可能非常小(如果序列长度跟 ppo_max_token_len_per_gpu 时,甚至只包含一个样本,此时这个样本被 mask 就意味着出问题)
            • 注:rearrange_micro_batches 函数拆分 micro-batches 时只按照索引拆,仍然是多上,packing 发生在 forward_fn 内部,对我们是透明的
          • 否则 use_dynamic_bsz=False 时使用固定条数作为一个 micro-batches
            • 此时的配置为 ppo_micro_batch_size_per_gpu
        • 不论怎样,micro-batches 都会小于 mini-batch 数
    • 补充指标计算位置:
      指标组 产生函数 调用位置 输入规模
      rollout_is_*(含 rollout_is_seq_min/max compute_rollout_correction_weightscompute_is_metrics ray_trainer.py(driver 侧) 完整 512 条
      kl / log_ppl_* / chi2 compute_rollout_corr_metrics_from_logprobs megatron_actor.pydp_actor.py 单个 micro-batch
      • 上诉问题一般发生在 compute_rollout_corr_metrics_from_logprobs
      • 注:其实这种方式下 compute_rollout_corr_metrics_from_logprobs 中上报的 sequence 粒度的指标似乎都不是真实 Sequence 粒度的了(待确认)

Understanding the Framework: Components and Combinations

  • Rollout 校正框架构建自正交组件 ,可以灵活组合

Key Components

  • 1) Operating Mode
    • Decoupled :三种策略(\(\pi_{\text{rollout}}\)、\(\pi_{\text{old}}\)、\(\pi_\theta\)),单独计算 \(\pi_{\text{old}}\)
    • Bypass :两种策略(\(\pi_{\text{rollout}}\) = \(\pi_{\text{old}}\)、\(\pi_\theta\)),跳过 \(\pi_{\text{old}}\) 计算
  • 2) Loss Function( Bypass Mode 下,由 loss_type 控制)
    • PPO-cliploss_type="ppo_clip",默认):PPO 裁剪目标(IS 由比率处理)
    • REINFORCEloss_type="reinforce"):带显式 IS 权重的策略梯度(无裁剪)
  • 3) IS/RS Aggregation Level
    • Token :每个 Token 的 IS 权重/拒绝
    • Sequence :序列级 IS 权重/拒绝

Preset Configuration Guide

  • 本节给的每个 Preset 都是针对常见场景优化的特定组件组合

Understanding the Presets

Available Preset Methods
  • 所有提前配置好的方法示例:
    预设方法 估计器 模式 IS 级别 RS 级别 属性
    Decoupled PPO Mode(3 种策略:\(\pi_{\text{rollout}}\)、\(\pi_{\text{old}}\)、\(\pi_\theta\))
    decoupled_token_is() Token-TIS Decoupled token - Token 级 IS 权重
    decoupled_seq_is() Seq-TIS Decoupled sequence - 序列级 IS 权重
    decoupled_seq_is_rs() Seq-MIS Decoupled sequence sequence 序列 IS + seq_sum_k1 RS
    decoupled_geo_rs() Geo-RS Decoupled - sequence 几何 RS(seq_mean_k1)
    decoupled_geo_rs_token_tis() Geo-RS-Token-TIS Decoupled token sequence 几何 RS + token IS
    K3 KL Estimator(对小 KL 值更稳定)
    decoupled_k3_rs() K3-RS Decoupled - sequence seq_mean_k3 RS
    decoupled_k3_rs_token_tis() K3-RS-Token-TIS Decoupled token sequence seq_mean_k3 RS + token IS
    Bypass PPO-clip Mode(2 种策略;比率处理 IS,RS 掩码排除异常值)
    bypass_ppo_clip() - Bypass(PPO-clip) - - 仅 PPO-clip
    bypass_ppo_clip_geo_rs() Geo-RS Bypass(PPO-clip) - sequence PPO-clip + Geo-RS
    bypass_ppo_clip_k3_rs() K3-RS Bypass(PPO-clip) - sequence PPO-clip + K3-RS
    Bypass REINFORCE Mode(2 种策略;显式 IS 权重,无 PPO 裁剪)
    bypass_pg_is() Seq-TIS Bypass(REINFORCE) sequence - 带显式 IS 的 REINFORCE
    bypass_pg_geo_rs() Geo-RS Bypass(REINFORCE) - sequence REINFORCE + Geo-RS
    bypass_pg_geo_rs_token_tis() Geo-RS-Token-TIS Bypass(REINFORCE) token sequence REINFORCE + Geo-RS + token IS
    Other
    disabled() - - - - 仅指标,不校正
  • 使用注意:
    • Bypass Mode设置 \(\pi_{\text{old}} = \pi_{\text{rollout}}\),并使用 loss_type 选择损失函数:
      • "ppo_clip"(默认):PPO 裁剪目标,其中比率 = \(\pi_\theta / \pi_{\text{rollout}}\) 已经处理了 IS
      • "reinforce":带显式 IS 权重的 REINFORCE,IS 权重为 \(\pi_\theta / \pi_{\text{rollout}}\)
    • 两种损失类型都受益于拒绝采样(RS),它掩码掉分布外的样本
    • 所有估计器(Token-TIS、Seq-TIS、Seq-MIS、Geo-RS 等)都与Decoupled 模式和 Bypass Mode 兼容
其他可行组合(需手动配置)
  • 没有预设方法但受可行的其他组合:
    • Token IS + Token RS:Token 级 IS 权重 + Token 级 RS 掩码
    • 纯 Token RS:仅 Token 级 RS,无 IS 权重
    • 纯序列 RS:仅序列级 RS,无 IS 权重
  • 说明:
    • 任何聚合级别(token/sequence)都可在 Decoupled 或 Bypass Mode 下工作
    • 实现完全支持所有组合
    • 拒绝采样独立于 IS 加权
    • 纯 RS(bypass_pg_rs)使用 Bypass + 几何 RS,loss_type="reinforce"(无 IS 权重)

Decoupled 模式 + Token 级重要性采样(decoupled_token_is

  • 参数配置:

    1
    config = RolloutCorrectionConfig.decoupled_token_is(threshold=2.0)
  • 基本组件

    • 运行模式 :Decoupled(3 种策略)
    • 损失 :带裁剪的 PPO(仅用于第二次漂移校正)
    • IS 聚合 :Token 级
    • RS :无(可单独添加)
  • 等效 YAML:

    1
    2
    3
    4
    5
    6
    algorithm:
    rollout_correction:
    rollout_is: token
    rollout_is_threshold: 2.0
    rollout_rs: null
    bypass_mode: false # Decoupled 模式
  • 特点:

    • 每个 Token 独立截断
    • 方差低于序列级(比率乘积被逐项限制)
    • 典型阈值:1.5 - 5.0

Decoupled 模式 + 序列级重要性采样(decoupled_seq_is

  • 也称为:Seq-TIS(序列级截断重要性采样,Sequence-Level Truncated IS)

  • 参数配置:

    1
    config = RolloutCorrectionConfig.decoupled_seq_is(threshold=2.0)
  • 基本组件:

    • 运行模式 :Decoupled(3 种策略)
    • 损失 :带裁剪的 PPO(仅用于第二次漂移校正)
    • IS 聚合 :序列级(Seq-TIS)
    • RS :无(可单独添加)
  • 等效 YAML:

    1
    2
    3
    4
    5
    6
    algorithm:
    rollout_correction:
    rollout_is: sequence
    rollout_is_threshold: 2.0
    rollout_rs: null
    bypass_mode: false # Decoupled 模式
  • 特点:

    • 在序列上进行乘法聚合
    • 对异常值比 Token 级更敏感
    • 典型阈值:2.0 - 10.0(高于 Token 级)

Decoupled 模式 + 序列级 IS + 拒绝采样(decoupled_seq_is_rs

  • 也称为:Seq-MIS(序列级掩码 IS,Sequence-Level Masked IS)

  • 参数配置:

    1
    config = RolloutCorrectionConfig.decoupled_seq_is_rs(is_threshold=2.0, rs_threshold="0.5_2.0")
  • 基本组件:

    • 运行模式 :Decoupled(3 种策略)
    • 损失 :带裁剪的 PPO(仅用于第二次漂移校正)
    • IS 聚合 :序列级(Seq-TIS)
    • RS :序列级拒绝(Seq-MIS)
  • 等效 YAML:

    1
    2
    3
    4
    5
    6
    7
    algorithm:
    rollout_correction:
    rollout_is: sequence
    rollout_is_threshold: 2.0
    rollout_rs: seq_sum_k1
    rollout_rs_threshold: 0.5_2.0
    bypass_mode: false # Decoupled 模式
  • 特点:

    • 双重机制:IS 重加权(Seq-TIS)+ 拒绝过滤(Seq-MIS)
    • 有效样本量较低(拒绝异常值)
    • 适用于严重的 off-policy 差距或当分布尾部是“有毒的”(垃圾/对抗样本)时
  • 何时使用 Seq-MIS 而非 Seq-TIS:

    • Seq-TIS(仅裁剪) :最大化信息效率;从所有样本中提取信号
      • 当数据干净且不匹配适中时使用
    • Seq-MIS(拒绝) :最大化安全性;充当硬信任区域过滤器
      • 当不匹配严重或高权重样本很可能是垃圾而非信号时使用

Bypass Mode + PPO-clip(bypass_ppo_clip

  • 参数配置:

    1
    config = RolloutCorrectionConfig.bypass_ppo_clip()
  • 基本组件:

    • 运行模式 :Bypass(2 种策略:\(\pi_{\text{rollout}}\) = \(\pi_{\text{old}}\)、\(\pi_\theta\))
    • 损失 :PPO-clip(IS 由比率处理,无显式 IS 权重)
    • IS 聚合 :无(PPO 比率处理)
    • RS :无
  • 等效 YAML:

    1
    2
    3
    4
    5
    rollout_correction:
    rollout_is: null
    rollout_rs: null
    bypass_mode: true
    loss_type: ppo_clip
  • 特点:

    • Bypass Mode 下的 PPO 裁剪目标
    • PPO 比率 = \(\pi_\theta / \pi_{\text{rollout}}\) 已经处理了 IS(不需要显式 IS 权重)
    • 跳过 actor.compute_log_prob() 前向传播(2 种策略而非 3 种)
    • 无拒绝采样——使用 bypass_ppo_clip_geo_rs() 添加 RS
  • 配置要求:

    • 需要设置 actor_rollout_ref.rollout.calculate_log_probs: true
  • Bypass Mode 的额外要求:

    • 设置 actor_rollout_ref.actor.use_rollout_log_probs: true
    • 设置 actor_rollout_ref.actor.policy_loss.loss_mode: bypass_mode
    • 通过 actor_rollout_ref.actor.policy_loss.rollout_correction 设置 Rollout 校正配置为上诉 Yaml 配置

带 IS 的 REINFORCE(bypass_pg_is

  • 参数配置:

    1
    config = RolloutCorrectionConfig.bypass_pg_is(threshold=2.0)
  • 基本组件:

    • 运行模式 :Bypass(2 种策略:\(\pi_{\text{rollout}}\)、\(\pi_\theta\))
    • 损失 :REINFORCE(带显式 IS 权重的策略梯度,无 PPO 裁剪)
    • IS 聚合 :序列级
    • RS :无
  • 等效 YAML:

    1
    2
    3
    4
    5
    6
    rollout_correction:
    rollout_is: sequence
    rollout_is_threshold: 2.0
    rollout_rs: null
    bypass_mode: true
    loss_type: reinforce # 带显式 IS 权重的 REINFORCE
  • 特点:

    • 带显式 IS 权重的 REINFORCE 损失(无 PPO 裁剪)
    • 单次前向传播(跳过 old_log_prob 计算)
    • IS 权重在损失函数中即时计算
  • 问题:为什么 这里要将 Bypass Mode + PPO-clip 模式 和 带 IS 的 REINFORCE 模式 分开写?

    • 因为两者在 VeRL 中需要的配置不同
      • Bypass Mode + PPO-clip 模式配置 rollout_is=null
      • 带 IS 的 REINFORCE 模式配置 rollout_is=sequence/token
    • PPO-clip 中天然就有 IS 和 Clip 了,可以通过 PPO 的 IS(\(r_t\theta\)) 直接校准两种不同
      $$
      r_t(\theta) = \frac{\pi_{\theta}(a_t \mid s_t)}{\pi_{\text{rollout} }(a_t \mid s_t)}
      $$
    • 带 IS 的 REINFORCE 中将 IS 视为了 Rollout 修正 的这个角色的比值(\(\rho_t\theta\)),所以需要设置 rollout_is=sequence/token(打开 rollout_correction)
      • 若为 Token 粒度
        $$
        \rho_t(\theta) = \frac{\pi_{\theta}(a_t \mid s_t)}{\pi_{\text{rollout} }(a_t \mid s_t)}
        $$
      • 若为 Sequence 粒度
        $$
        w_{\text{seq} }(\theta) = \rho(\theta) = \frac{\pi_{\theta}(y \mid x)}{\pi_{\text{rollout} }(y \mid x)} = \prod_{t=1}^{T} \frac{\pi_{\theta}(a_t \mid s_t)}{\pi_{\text{rollout} }(a_t \mid s_t)}
        $$

补充:额外有用的配置(未作为预设公开)

  • 这些配置可用 ,但还没有便捷的预设方法

Token IS + Token RS(token_is_rs

  • Token 级 IS 权重加上 Token 级 RS 掩码

    1
    2
    3
    4
    5
    6
    config = RolloutCorrectionConfig(
    rollout_is="token",
    rollout_is_threshold=2.0,
    rollout_rs="token_k1",
    rollout_rs_threshold=2.0,
    )
  • 特点: 每个 Token 的 IS 权重 + 每个 Token 的 RS 掩码

纯 Token RS(token_rs

  • 仅 Token 级 RS,无 IS 权重

    1
    2
    3
    4
    5
    config = RolloutCorrectionConfig(
    rollout_is=None,
    rollout_rs="token_k1",
    rollout_rs_threshold=2.0,
    )
  • 特点:Token 级 RS 掩码,无 IS 重加权

纯序列 RS(seq_rs

  • 仅序列级 RS,无 IS 权重

    1
    2
    3
    4
    5
    config = RolloutCorrectionConfig(
    rollout_is=None,
    rollout_rs="seq_sum_k1",
    rollout_rs_threshold="0.5_2.0",
    )
  • 特点:序列级 RS 掩码,无 IS 重加权

总结:IS 权重的处理方式

  • IS 权重(rollout_is_weights)经过固定的处理流水线:
  • 阶段 1:安全界限(防止溢出)
    • Token 级:每个 Token 的 exp(clamp(log_ratio, -20, 20)) → 将每个 token 限制在 [2e-9, 5e8]
    • 序列级:exp(clamp(sum(log_ratio), -20, 20)) → 将乘积限制在 [2e-9, 5e8],广播到所有 token
  • 阶段 2:截断(降低方差)
    • .clamp(max=rollout_is_threshold) → 将权重上限截断(TIS:截断重要性采样)
    • 无下限截断(对小权重保持无偏性)
  • 阶段 3:填充位置置零(正确聚合)
    • weights * response_mask → 将填充位置置零
  • 阶段 4:可选的批归一化
    • 如果 rollout_is_batch_normalize=True:将权重归一化到批次内均值为 1.0
    • 在截断之后应用,以保留截断语义
  • 拒绝采样(独立机制)
    • 拒绝采样通过 compute_rollout_rejection_mask() 修改 response_mask(而非权重):
      • 独立计算安全有界的比率
      • 创建二值掩码:落在 [lower_threshold, upper_threshold] 之外的 token/序列 → 0(拒绝)
      • 修改后的掩码用于损失聚合

Operation Modes

  • 该框架提供两种运行模式 来计算 \(\pi_{\text{old}}\),可与不同的损失函数结合

Operating Modes and Configuration

  • Operating Modes 配置情况:
    配置 bypass_mode loss_type 运行模式 损失函数 描述
    Decoupled false N/A Decoupled PPO 通过 actor.compute_log_prob() 单独计算 old_log_prob
    Bypass + PPO-clip true "ppo_clip"(默认) Bypass PPO-clip PPO 裁剪目标(IS 由比率处理)
    Bypass + REINFORCE true "reinforce" Bypass REINFORCE 带显式 IS 权重的策略梯度(无 PPO 裁剪)

Operating Mode Details

Decoupled Mode,三种策略
  • 策略设置:
    • \(\pi_{\text{rollout}}\):行为策略(数据收集)
    • \(\pi_{\text{old}}\):近端策略(在训练 epoch 开始时通过 actor.compute_log_prob() 计算)
    • \(\pi_\theta\):当前策略(正在更新)
  • 参数配置: bypass_mode = false
  • 特点:
    • ✅ 实现批次大小不变性
    • ✅ 分别校正漂移 1(rollout→old)和漂移 2(old→current)
    • ✅ 高效利用陈旧数据
    • ❌ 需要额外的前向传播(actor.compute_log_prob()
Bypass Mode,两种策略
  • 策略设置:
    • \(\pi_{\text{rollout}}\):行为策略(数据收集)
    • \(\pi_{\text{old}}\) = \(\pi_{\text{rollout}}\):近端策略等于行为策略
    • \(\pi_\theta\):当前策略(正在更新)
  • 参数配置: bypass_mode = true
  • 属性:
    • ✅ 跳过 actor.compute_log_prob() 调用(更快)
    • ✅ 通过 IS/RS 处理 off-policy 校正(当使用带 IS/RS 的策略梯度时)
    • ✅ 使用两种策略而非三种(\(\pi_{\text{rollout}}\) = \(\pi_{\text{old}}\))
    • ⚠️ 不将近端策略与行为策略分离(不同于 Decoupled 模式)

IS/RS 聚合级别(正交于运行模式)

  • 聚合级别可以独立于 运行模式选择,任何聚合级别都可以在 Decoupled 或 Bypass Mode 下工作
    rollout_is rollout_rs 行为
    null null 禁用 :不计算、无指标、无拒绝
    null "token_k1""seq_sum_k1""seq_mean_k1""seq_max_k2" 仅拒绝 :计算指标、不进行权重校正、进行拒绝采样
    "token""sequence" null 仅 IS 权重 :启用权重校正、不进行拒绝采样
    "token""sequence" "token_k1""seq_sum_k1""seq_mean_k1""seq_max_k2" 完整校正 :同时启用权重校正和拒绝采样

Key Insights

  • ✅ 任何 IS/RS 聚合级别(token/sequence/geometric)都可以在Decoupled或Bypass模式下使用
  • ✅ 您可以单独使用拒绝采样而不进行 IS 权重校正(rollout_is=null, rollout_rs="token_k1"
  • ✅ 您可以单独使用 IS 权重而不进行异常值拒绝(rollout_is="token", rollout_rs=null
  • ✅ 您可以同时使用两者rollout_is="token", rollout_rs="token_k1"
  • ✅ 您可以仅监控指标而不进行任何校正,方法是同时将两者设置为 null,但仍提供 rollout_log_probs

Example Workflow

推荐: Bypass Mode
  • 该工作流为高效而使用 Bypass Mode

  • 1) 从仅指标开始 ,以了解 off-policy 差距:

    1
    2
    3
    4
    5
    rollout_correction:
    rollout_is: null
    rollout_rs: null
    bypass_mode: true # Bypass Mode (推荐)
    loss_type: ppo_clip # 默认:PPO 裁剪目标
    • 监控 rollout_corr/klrollout_corr/log_ppl_abs_diffrollout_corr/chi2_token 以评估 off-policy 差距
    • 注意:off-policy 指标是无条件计算的,不管是否打开 rollout_correction 都会被计算
      • 关闭 Bypass Mode 模式下,ritio = old_log_prob/rollout_log_prob :
        • old_log_prob = log_prob
        • rollout_log_prob = rollout_log_prob
  • 2) 如果看到高异常值比例,则启用拒绝采样

    1
    2
    3
    4
    5
    6
    rollout_correction:
    rollout_is: null
    rollout_rs: sequence # 或 "geometric" 以获得更高敏感性
    rollout_rs_threshold: 2.0
    bypass_mode: true # Bypass Mode
    loss_type: ppo_clip # 或 "reinforce" 以使用显式 IS 权重
    • 这将从训练中排除异常值,而不修改梯度
  • 3) 一旦对指标满意,启用完整的 IS 校正(配合 REINFORCE 损失):

    1
    2
    3
    4
    5
    6
    7
    rollout_correction:
    rollout_is: sequence # 推荐:无偏,适用于大多数情况(
    rollout_is_threshold: 2.0
    rollout_rs: sequence # 或 "geometric" 以进行更激进的过滤
    rollout_rs_threshold: 2.0
    bypass_mode: true # Bypass Mode
    loss_type: reinforce # 带显式 IS 权重的 REINFORCE
    • 理解(个人经验):虽然 sequence-level MIS 是无偏的,但亲测当 micro-batch 过小时,容易报错
      • rollout_corr_helper.py 本身会 assert 一个 micro-batch 内部的 Token 不能全被 Mask
      • 解法1:
        • Mask 时将 Advantage 置为 0 而不是使用 response_mask
      • 解法2:
        • 使用 Token 粒度的 Mask,然后保留 Sequence 粒度的 IS
  • Bypass Mode 的优点:

    • ✅ 跳过昂贵的 actor.compute_log_prob() 前向传播(更快)
    • loss_type 控制损失函数:”ppo_clip”(默认)或 “reinforce”
    • ✅ PPO-clip:IS 由比率处理(无显式权重),应用 RS 掩码
    • ✅ REINFORCE:显式 IS 权重即时计算(\(\pi_\theta\) / \(\pi_{\text{rollout}}\))
    • ✅ 两种损失类型都与所有 IS/RS 组合兼容

Usage

Basic Setup

  • 配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    algorithm:
    rollout_correction:
    rollout_is: token # 在 Token 级启用 IS 权重
    rollout_is_threshold: 2.0 # IS 权重的阈值
    rollout_rs: null # 不进行拒绝采样

    actor_rollout_ref:
    rollout:
    calculate_log_probs: true # 必需!

Bypass Mode 的额外配置

  • 设置 actor_rollout_ref.actor.use_rollout_log_probs: true
  • 设置 actor_rollout_ref.actor.policy_loss.loss_mode: bypass_mode
  • 通过 actor_rollout_ref.actor.policy_loss.rollout_correction 设置 Rollout 校正配置

Metrics

  • 所有指标在日志中都以 rollout_corr/ 为前缀
    • 例如,rollout_is_mean 在日志中显示为 rollout_corr/rollout_is_mean
  • 这些指标涵盖:
    • 诊断指标 :KL 散度、困惑度差异(衡量 off-policy 差距)
    • 校正统计量 :IS 权重、拒绝率(衡量所施加的校正)
Core IS Weight Metrics
  • rollout_is_mean :所有有效 token 上的平均重要性采样权重
    • Token 粒度的 IS 时,这个指标一般为 1 左右(看着上报甚至四舍五入后一直是 1)
      • 注意:等于 1 而不是小于 1 的原因是跟之前我们所熟悉的 KL 散度不一样
        • KL 的结论针对 log ratio,不是 ratio
          • \(q\):rollout 策略
          • \(p\):训练引擎重算 old_log_probs 对应的策略
          • \(r=p/q\)
        • 对于固定上下文,在支持集覆盖、概率计算正确的条件下:
          $$
          \mathbb E_{a\sim q}[r]=\sum_a q(a)\frac{p(a)}{q(a)}=\sum_a p(a)=1.
          $$
        • 而 KL 对应的是:
          $$
          \mathbb E_q[\log r]=-D_{\text{KL} }(q\Vert p)\le 0.
          $$
        • 所以:
          $$
          \begin{align}
          \underbrace{\mathbb E_q[r]&=1}_{\text{算术平均} }\\
          \underbrace{\exp(\mathbb E_q[\log r])&\le 1}_{\text{几何平均} }
          \end{align}
          $$
    • Sequence 粒度 IS 时,每个 Token 的权重相等,计算平均后一般不是 1(长序列的 IS 小于1)
  • rollout_is_std :IS 权重的标准差
    • 值越高表示 IS 权重方差越大
  • rollout_is_min :观察到的最小 IS 权重
    • 显示最被低估的 token/序列
    • 对于 sequence/geometric:从未裁剪的对数空间比率计算(真实最小值)
    • 对于 token:从安全有界的权重计算
  • rollout_is_max :观察到的最大 IS 权重
    • 显示最被高估的 token/序列
    • 对于 sequence/geometric:从未裁剪的对数空间比率计算(安全界限之前的真实最大值)
    • 对于 token:从安全有界的权重计算(阈值裁剪之前)
    • rollout_is_threshold 比较以查看截断影响
Effective Sample Size
  • rollout_is_eff_sample_size :IS 加权后的有效样本量
    • 公式1 / mean(weights^2),其中权重已归一化
    • 范围 :0.0 到 1.0(占原始批次的分数)
    • 值越低表示权重集中在更少的样本上
Threshold Exceedance Metrics
  • rollout_is_ratio_fraction_high :超过上限阈值的权重比例
    • 显示截断/掩码在高端的发生频率
    • 对于 sequence/geometric:从未裁剪的对数空间比率计算(真实超出)
    • 对于 token:从安全有界的权重计算(阈值裁剪之前)
  • rollout_is_ratio_fraction_low :低于下限阈值(1/upper_threshold)的权重比例
    • 显示有多少权重低于倒数阈值的诊断指标
    • 对于 sequence/geometric:从未裁剪的对数空间比率计算(真实超出)
    • 对于 token:从安全有界的权重计算(截断之前)
序列级指标(用于序列聚合)
  • rollout_is_seq_mean :序列级的平均 IS 权重
    • 现在 seq 内部对所有 Token 的 IS(这个 IS 的粒度可能是 sequence 或者 Token) 平均,然后再对 batch 内部对 seq 平均
    • Token 粒度的 IS 时,这个指标和 rollout_is_mean 一般都是 1
    • Sequence 粒度的 IS 时,这个指标可能较小,且和 rollout_is_mean 可能不一致
  • rollout_is_seq_std :序列级 IS 权重的标准差
  • rollout_is_seq_min :最小序列级 IS 权重
  • rollout_is_seq_max :最大序列级 IS 权重
  • rollout_is_seq_max_deviation :序列级与 1.0 的最大绝对偏差
    • 显示最坏情况序列的 off-policy 差距
  • rollout_is_seq_fraction_high :超过上限阈值的序列比例
  • rollout_is_seq_fraction_low :低于下限阈值的序列比例
补充:rollout_is_seq_mean 和 rollout_is_mean 指标计算细节
  • rollout_is=token 下,rollout_is_seq_meanrollout_is_mean 都接近 \(1\)
    • 名字中的 seq 不代表“把整条序列的 token ratio 相乘
    • 设第 \(i\) 条序列有 \(L_i\) 个有效 token,token ratio 为 \(r_{it}\),代码先求每条序列的算术均值:
      $$
      a_i=\frac{1}{L_i}\sum_t r_{it}.
      $$
    • 然后两个指标分别是:
      $$
      \begin{align}
      \texttt{rollout_is_mean}&=\frac{\sum_i L_i a_i}{\sum_i L_i} \\
      \texttt{rollout_is_seq_mean}&=\frac1N\sum_i a_i
      \end{align}
      $$
    • 即:
      • mean:所有有效 token 等权,长序列贡献更多;
      • seq_mean:先在序列内平均,再让各序列等权
    • 当各序列内部的 token ratio 均值都接近 \(1\) 时,两个指标自然都接近 \(1\)
  • 换成 rollout_is=sequence 后, seq_mean 一般比 mean 大,甚至大一个数量级
    • sequence 模式改变了权重本身,此时每条序列计算一个权重:
      $$
      w_i=\exp\left(\sum_t\log r_{it}\right),
      $$
    • 实现上会先将上述 log-ratio 总和 clamp 到 \([-20,20]\),再取指数,并将 \(w_i\) 广播到该序列的所有有效 token
    • 因此,两个指标变为:
      $$
      \begin{align}
      \texttt{rollout_is_mean}&=\frac{\sum_i L_iw_i}{\sum_i L_i} \\
      \texttt{rollout_is_seq_mean}&=\frac1N\sum_i w_i
      \end{align}
      $$
      • 它们仍是同一批权重的不同加权平均,不是两种不同的修正比例
    • 当长序列权重更小时,token 加权均值就更低
      • 补充:为什么长轨迹容易出现小权重?
        • 因为 sequence IS 累积的是 log ratio 的总和,没有除以长度
          $$
          \log w_i=L_i\cdot\overline{\log r}_i.
          $$
        • 例如,一条轨迹的平均 log ratio 仅为 \(-0.001\)(对应 ratio 微小于 1):
          • \(1,000\) 个 token:\(w=e^{-1}\approx0.368\);
          • \(10,000\) 个 token:\(w=e^{-10}\approx0.0000454\)
拒绝采样指标(当启用 rollout_rs 时)
  • rollout_rs_masked_fraction :通过拒绝采样被拒绝的 token 比例
    • 重要 :拒绝采样修改 response_mask(将被拒绝的 token 设为 0)
    • 与 IS 权重分开 :IS 权重仍然被截断;拒绝是独立的过滤步骤
    • 仅在启用 rollout_rs 时存在(token/sequence/geometric)
  • rollout_rs_seq_masked_fraction :至少有一个被拒绝 token 的序列比例
    • 显示拒绝采样对序列级的影响
    • Token 级 RS:如果 ANY token 在 [lower, upper] 之外,则序列被拒绝
    • 序列级 RS:整个序列根据序列级比率被拒绝或接受
    • 几何 RS:整个序列根据几何均值被拒绝或接受
Off-Policy 诊断指标(训练策略 vs Rollout 策略)
  • 关于术语的说明: 这些指标使用 “training” 指代训练参考策略,“rollout” 指代 \(\pi_{\text{rollout}}\)(用于数据收集的行为策略)
  • Decoupled 模式 :“training” = \(\pi_{\text{old}}\)(在训练 epoch 开始时计算)
  • Bypass/纯 IS 模式 :“training” = \(\pi_\theta\)(当前正在训练的策略)
  • 在 Bypass/纯 IS 模式下,指标直接衡量 \(\pi_\theta\) 与 \(\pi_{\text{rollout}}\) 之间的漂移
  • training_ppl :训练参考策略的困惑度(Decoupled 模式下为 \(\pi_{\text{old}}\),Bypass/纯 IS 模式下为 \(\pi_\theta\))
    • 公式exp(-mean(log_probs))
    • 值越低表示模型置信度越高
  • rollout_ppl :Rollout 策略 \(\pi_{\text{rollout}}\) 的困惑度(例如 vLLM BF16)
  • ppl_ratio :训练 PPL 与 Rollout PPL 的比率
    • 公式exp(mean(log(training_ppl / rollout_ppl)))
    • 含义 :> 1.0 表示训练不如 Rollout 自信
  • training_log_ppl :训练策略的对数困惑度
    • 有助于识别趋势(线性尺度)
  • rollout_log_ppl :Rollout 策略的对数困惑度
  • log_ppl_diff :对数困惑度差异的均值
    • 公式mean(log_ppl_rollout - log_ppl_training)
    • 符号指示哪个策略更自信
  • log_ppl_abs_diff :对数困惑度绝对差异的均值
    • off-policy 差距的大小,与方向无关
  • log_ppl_diff_max :序列间最大的对数困惑度差异
    • 识别最坏情况序列
  • log_ppl_diff_min :序列间最小的对数困惑度差异
  • kl :KL 散度 KL(\(\pi_{\text{rollout}}\) || \(\pi_\text{training}\))
    • 公式mean(log_prob_rollout - log_prob_training)
    • 注意 :可以为负(Rollout 不太自信)
  • k3_kl :K3 散度(在期望上等于 KL(\(\pi_{\text{rollout}}\) || \(\pi_\text{training}\)))
    • 公式mean(exp(log_ratio) - log_ratio - 1)
    • 比直接 KL 更稳定(每个 token 非负)
    • 总是 >= 0
  • chi2_token :Token 级的卡方散度
    • 公式mean(ratio²) - 1,其中 ratio = \(\pi_\text{training}\)/\(\pi_{\text{rollout}}\)
    • 衡量 IS 权重分布的二阶矩
    • 总是非负
    • 理解;这个指标相对 kl 等的好处是,对训推不一致的感受更加敏感,往往在 kl 等变化比较平缓时,chi2_token 已经波动较大了
  • chi2_seq :序列级的卡方散度
    • 公式mean((∏_t ratio_t)²) - 1
    • IS 权重的序列级二阶矩
    • 比 Token 级卡方更敏感
    • 理解:这个指标比 chi2_token 更敏感,特别在模型的输出长度方差较大时,常样本带来的波动会在这里很敏感的体现出来
示例:在代码中访问指标
  • 代码示例(详情见原文,这里只保留核心阈值思路):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from verl.trainer.ppo.rollout_corr_helper import compute_rollout_correction_and_rejection_mask

    weights_proto, modified_response_mask, metrics = compute_rollout_correction_and_rejection_mask(
    old_log_prob=training_log_probs,
    rollout_log_prob=rollout_log_probs,
    response_mask=response_mask,
    rollout_is="token",
    rollout_is_threshold=2.0,
    rollout_rs="token_k1",
    rollout_rs_threshold="0.5_2.0",
    )

    is_weights = weights_proto.batch["rollout_is_weights"]

    if metrics['rollout_corr/rollout_is_mean'] < 0.5 or metrics['rollout_corr/rollout_is_mean'] > 2.0:
    # IS 在 [0.5, 2.0] 之外,异常告警

    if metrics['rollout_corr/rollout_is_eff_sample_size'] < 0.3:
    # 有效样本低于 在 0.3,异常告警
示例:基于指标的条件告警
  • 一些告警条件示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    def check_rollout_correction_health(metrics, config):
    """检查 Rollout Correction 指标是否表明训练健康"""
    warnings = []

    # 检查平均 IS 权重
    mean_weight = metrics['rollout_corr/rollout_is_mean']
    if mean_weight < 0.5 or mean_weight > 2.0:
    warnings.append(f"平均 IS 权重 {mean_weight:.3f} 远离 1.0")

    # 检查有效样本量
    ess = metrics['rollout_corr/rollout_is_eff_sample_size']
    if ess < 0.3:
    warnings.append(f"有效样本量 {ess:.3f} 过低")

    # 检查标准差
    std = metrics['rollout_corr/rollout_is_std']
    if std > 1.0:
    warnings.append(f"IS 权重标准差 {std:.3f} 过高")

    # 检查 KL 散度
    kl = metrics['rollout_corr/kl']
    if abs(kl) > 0.1:
    warnings.append(f"KL 散度 {kl:.3f} 表明存在显著 off-policy 差距")

    # 检查卡方散度
    if 'rollout_corr/chi2_token' in metrics:
    chi2_token = metrics['rollout_corr/chi2_token']
    if chi2_token > 1.0:
    warnings.append(f"卡方散度(token){chi2_token:.3f} 表明存在严重分布偏移")

    if warnings:
    print("Rollout Correction 健康警告:")
    for warning in warnings:
    print(f" - {warning}")
    return False
    else:
    print("Rollout Correction 指标看起来健康")
    return True

    _,_, metrics = compute_rollout_correction_and_rejection_mask(...)
    is_healthy = check_rollout_correction_health(metrics, config)

    if not is_healthy:
    print("考虑:")
    print(" - 收紧 rollout_is_threshold")
    print(" - 切换到几何聚合级别")
    print(" - 检查 Rollout 和训练策略是否差异过大")

Running Examples

  • 从基本的 Token 级截断配置开始:

    1
    bash examples/rollout_correction/run_qwen2_5_7b_fsdp.sh
  • 在调整参数之前监控指标 1-2 个 epoch


Configuration Examples

  • 示例 1:仅 IS 权重(Token 级)

    1
    2
    3
    4
    5
    algorithm:
    rollout_correction:
    rollout_is: token
    rollout_is_threshold: 2.0
    rollout_rs: null # 不进行拒绝采样
  • 示例 2:仅拒绝采样(无 IS 权重)

    1
    2
    3
    4
    5
    algorithm:
    rollout_correction:
    rollout_is: null # 无 IS 权重
    rollout_rs: token_k1
    rollout_rs_threshold: "0.5_2.0"
  • 示例 3:同时使用 IS 和 RS(Token RS)

    1
    2
    3
    4
    5
    6
    algorithm:
    rollout_correction:
    rollout_is: token
    rollout_is_threshold: 2.0
    rollout_rs: token_k1
    rollout_rs_threshold: "0.5_2.0"
  • 示例 5: Bypass Mode + PPO-clip(默认)

    1
    2
    3
    4
    5
    6
    7
    8
    algorithm:
    rollout_correction:
    rollout_is: token
    rollout_is_threshold: 2.0
    rollout_rs: token_k1
    rollout_rs_threshold: "0.5_2.0"
    bypass_mode: true # 跳过 old_log_prob 计算
    loss_type: ppo_clip # PPO 裁剪目标(默认)
  • 示例 6: Bypass Mode + REINFORCE,无 PPO 裁剪,纯策略梯度 + IS 校正

    1
    2
    3
    4
    5
    6
    rollout_correction:
    rollout_is: sequence # 损失中的显式 IS 校正
    rollout_is_threshold: 2.0
    rollout_rs: null # 可选:可以添加拒绝采样
    bypass_mode: true
    loss_type: reinforce # 带显式 IS 权重的 REINFORCE
  • 示例 7: Bypass Mode + PPO-clip + 拒绝采样,PPO 裁剪 + 拒绝采样,IS 由 PPO 比率处理(无显式 IS 权重)

    1
    2
    3
    4
    5
    6
    7
    rollout_correction:
    rollout_is: sequence # 为指标计算
    rollout_is_threshold: 2.0
    rollout_rs: seq_max_k2 # 序列最大 χ²/2 守卫
    rollout_rs_threshold: 2.5
    bypass_mode: true
    loss_type: ppo_clip # PPO 裁剪目标(IS 由比率处理)
    • 问题:这里不担心 PPO Clip 和 rollout_is 两次校准重复了吗?
    • 回答:不用担心,源码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      # Dispatch to appropriate loss function based on loss_type
      if loss_type == "reinforce":
      # REINFORCE: Apply IS weights explicitly
      pg_loss, pg_metrics = compute_policy_loss_reinforce(
      rollout_log_prob=rollout_log_prob,
      log_prob=log_prob,
      advantages=advantages,
      response_mask=effective_mask,
      loss_agg_mode=loss_agg_mode,
      config=config,
      # 下面这一行是核心,REINFORCE 没有 ppo_clip,需要加入 computed_is_weights 作为训推不一致的校正
      rollout_is_weights=computed_is_weights,
      )

      elif loss_type == "ppo_clip":
      # PPO-clip: The ratio π_current/π_old = π_current/π_rollout already handles IS
      # DO NOT apply IS weights - would be double-counting!
      # The clipping mechanism constrains the effective IS ratio
      pg_loss, pg_metrics = compute_policy_loss_vanilla( # type: ignore[call-arg]
      old_log_prob=rollout_log_prob, # = old_log_prob in bypass mode
      log_prob=log_prob,
      advantages=advantages,
      response_mask=effective_mask,
      loss_agg_mode=loss_agg_mode,
      config=config,
      # 下面这一行是核心,保证即使 computed_is_weights 在之前有值(有时会为了上报指标而打开 rollout_is),也不影响 Loss 计算
      rollout_is_weights=None, # Explicitly None - no IS weights for PPO-clip
      )

Troubleshooting

问题:IS 权重分布范围过大

  • 问题表现: rollout_is_std > 1.0,rollout_is_eff_sample_size < 0.3
  • 解决方案:
    • 1)从 sequence 切换到 geometric 级别
    • 2)收紧阈值
    • 3)验证 Rollout 和训练差异是否过大

问题:平均 IS 权重远离 1.0

  • 问题表现:rollout_is_mean < 0.5 或 > 2.0
  • 解决方案:
    • 1)确认已设置 calculate_log_probs=True
    • 2)检查 rollout_log_probs 是否正确传递
    • 3)检查是否存在系统性分布偏移

调试:可视化指标

  • 示例:绘制 IS 权重分布
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import matplotlib.pyplot as plt
    import numpy as np

    def plot_is_metrics(metrics_history):
    """绘制随训练步数变化的 Rollout IS 指标"""
    fig, axes = plt.subplots(2, 3, figsize=(15, 10))

    # 图 1:平均 IS 权重随时间变化
    axes[0, 0].plot(metrics_history['rollout_corr/rollout_is_mean'])
    axes[0, 0].axhline(y=1.0, color='r', linestyle='--', label='理想值')
    axes[0, 0].set_title('平均 IS 权重')
    axes[0, 0].set_xlabel('步数')
    axes[0, 0].legend()

    # 图 2:有效样本量
    axes[0, 1].plot(metrics_history['rollout_corr/rollout_is_eff_sample_size'])
    axes[0, 1].axhline(y=0.5, color='g', linestyle='--', label='良好')
    axes[0, 1].axhline(y=0.3, color='r', linestyle='--', label='警告')
    axes[0, 1].set_title('有效样本量')
    axes[0, 1].set_xlabel('步数')
    axes[0, 1].legend()

    # 图 3:KL 散度随时间变化
    axes[1, 0].plot(metrics_history['rollout_corr/kl'], label='KL')
    axes[1, 0].plot(metrics_history['rollout_corr/k3_kl'], label='K3 KL')
    axes[1, 0].axhline(y=0, color='g', linestyle='--', alpha=0.3)
    axes[1, 0].set_title('KL 散度')
    axes[1, 0].set_xlabel('步数')
    axes[1, 0].legend()

    # 图 4:PPL 比率随时间变化
    axes[1, 1].plot(metrics_history['rollout_corr/ppl_ratio'])
    axes[1, 1].axhline(y=1.0, color='r', linestyle='--', label='理想值')
    axes[1, 1].set_title('PPL 比率(训练/Rollout)')
    axes[1, 1].set_xlabel('步数')
    axes[1, 1].legend()

    # 图 5:卡方散度
    if 'rollout_corr/chi2_token' in metrics_history:
    axes[1, 2].plot(metrics_history['rollout_corr/chi2_token'], label='Token 级')
    if 'rollout_corr/chi2_seq' in metrics_history:
    axes[1, 2].plot(metrics_history['rollout_corr/chi2_seq'], label='序列级')
    axes[1, 2].axhline(y=1.0, color='r', linestyle='--', label='警告')
    axes[1, 2].set_title('卡方散度')
    axes[1, 2].set_xlabel('步数')
    axes[1, 2].legend()
    else:
    axes[1, 2].axis('off')

    plt.tight_layout()
    plt.savefig('rollout_is_metrics.png', dpi=150)
    print("已将图表保存为 rollout_is_metrics.png")

补充:其他事项

Performance Impact

  • 内存开销 :约为模型内存的 1%
  • 计算开销 :取决于级别,约为 1-3%
  • 训练稳定性 :当存在 off-policy 差距时显著提高

Testing

  • 运行测试套件以验证一切正常:

    1
    2
    3
    4
    5
    # 基本单元测试
    python tests/trainer/ppo/test_rollout_corr.py

    # 集成测试(如果 pytest 可用)
    pytest tests/trainer/ppo/test_rollout_corr_integration.py -v
  • 预期输出:所有测试通过