Prhub

#7082 [trainer] feat: V1 replay buffer eviction/refill handling for stale, DAPO-filtered, and failed rollout groups

原始 PR 作者 Begunner 合并时间 2026-07-23 21:41 文件变更 17 提交数 21 评论 12 代码增减 +1551 / -464

执行摘要

统一 V1 ReplayBuffer 对过期 /DAPO 过滤 / 失败 rollout 组的驱逐回填

DAPO (Direct Advantage Policy Optimization) 需要过滤掉所有轨迹奖励相同的组(零方差),以避免训练不稳定。同时需要统一处理 off-policy 过期组(stale)和失败组(failure)的驱逐和回填。原先的 ReplayBuffer 只支持简单的 drop 策略,且没有分离同步和异步模式。本 PR 的目标是提供一个统一的 eviction/refill 矩阵,支持不同模式下的不同行为,并添加日志以便监控。PR body 中说明:'Unify V1 ReplayBuffer eviction/refill handling for stale, DAPO-filtered, and failed rollout groups.'

本 PR 值得精读,因为它实现了 DAPO 算法的采样支持,并展示了 ver_l 框架的可扩展设计(类分离、工厂方法、配置驱动)。review 中的技术讨论(P1-P3)对理解工程权衡很有帮助。建议计划使用 DAPO 的团队仔细阅读 PR body 中的配置矩阵,并在实验前确认 max_num_gen_batches 的警告已生效。对于 _dapo_filtered_keys 的性能优化(缓存)可作为后续改进项。

讨论亮点

Review 中主要讨论:

  • tongyx361 (P1)algorithm.filter_groups.max_num_gen_batches 被忽略,可能导致请求堆积 OOM 或训练 hang。要求至少打印警告。作者在后续 commit 中可能已添加警告,但 final diff 中未见。结论:已承诺但未最终确认。
  • tongyx361 (P2)_resolve_filter_groups_metric 在自定义 sampler 时仍被调用,违反“自定义 sampler 拥有过滤语义”的意图。作者通过添加 has_custom_sampler 条件来解决,测试 test_custom_sampler_skips_builtin_filter_groups_validation 验证了该行为。结论:已解决。
  • tongyx361 (P3):WandB 表每次重建并上传完整历史,造成二次开销(O(steps²))。作者利用 packaging.version.Version 判断 wandb 版本:0.20+ 使用增量上传,旧版本回退为全量重建(仍存在开销但避免破坏 API)。定位:verl/utils/tracking.py
  • wuxibin89:建议将 ReplayBuffer 分离为同步/异步两个类(已在实现中);并建议对 _dapo_filtered_keys 添加缓存(避免每次 poll 全量扫描),但未在代码中实现。
  • tongyx361 (额外):失败组若零轨迹不应计入 sampleable_keys,否则可能导致 materialize 返回空 batch。此问题要求回归测试,后续 commit 可能已部分处理(add optional failure refilling),但 review 时未闭合。

实现拆解

  1. 类分离与工厂方法:在 trainer_base.py 中,_build_replay_buffer 根据 trainer_mode 实例化 ReplayBuffer(sync)或 ReplayBufferAsync(async)。ReplayBufferAsync 继承自 ReplayBuffer 并统一处理所有三种 eviction 情况,回填相同数量(k)的 prompt。
  2. DAPO 过滤逻辑:在 replay_buffer.py 中添加 _dapo_filtered_keys,遍历所有终端 prompt 的轨迹奖励,若某组所有轨迹在配置的 metric(如 acc)上完全相同(零方差),则排除该组。sync 模式下回填 2k 个 prompt,由 max_inflight_gen_batches 限制并发。
  3. 统一 eviction 指标聚合:将 _accumulate_drop_metrics 重命名为 _accumulate_eviction_metrics,支持 dict 值聚合(使用 Counter)。MetricsAggregator 相应更新。修改了 verl/trainer/ppo/v1/utils.py
  4. 终端状态发布同步:在 agent_loop_tq.py_settle_session_tasks 中,确保所有 session 任务完成后才设置 prompt 状态为 finished/failure,防止晚写入数据被采样。
  5. 日志与配置:在 tracking.py 新增 DapoFilteredRewardTableLogger,使用 WandB 表格记录过滤奖励分布。Tracking.finish() 确保所有后端被正确刷出。config/algorithm.py 添加 filter_groups 配置字段,ppo_trainer.yaml 提供示例。

测试覆盖:新增 test_trainer_base_on_cpu.py 测试类选择和自定义采样器跳过;扩展 test_replay_buffer_on_cpu.py 测试各模式下的 eviction/refill;新增 test_agent_loop_tq_on_cpu.py 测试 session 任务 settle;扩展 test_metrics_aggregator_on_cpu.py 验证聚合正确性;扩展 test_tracking_on_cpu.py 测试 WandB 表和 finish。

文件 模块 状态 重要度
verl/trainer/ppo/v1/replay_buffer.py 回放缓冲区 modified 8.93
verl/trainer/ppo/v1/trainer_base.py 训练器基类 modified 7.56
verl/utils/tracking.py 跟踪模块 modified 8.49
verl/trainer/ppo/v1/agent_loop_tq.py 代理循环 modified 6.88
tests/trainer/ppo/v1/test_trainer_base_on_cpu.py 训练器测试 added 7.95
tests/trainer/ppo/v1/test_replay_buffer_on_cpu.py 回放缓冲测试 modified 7.82
tests/trainer/ppo/v1/test_agent_loop_tq_on_cpu.py 代理循环测试 added 7.24

关键符号

_accumulate_eviction_metrics _validate_mode_config _sampleable_terminal_keys _clear_groups _has_enough_samples _dapo_filtered_keys _resolve_filter_groups_metric _build_replay_buffer finish DapoFilteredRewardTableLogger.log DapoFilteredRewardTableLogger._log_to_wandb _settle_session_tasks

关键源码片段

verl/trainer/ppo/v1/replay_buffer.py dependency-wiring

核心文件:统一 eviction/refill 逻辑,分离 ReplayBufferAsync,实现 DAPO 过滤和采样控制。

# DAPO 过滤奖励计数在 metrics dict 中的键
DAPO_FILTERED_REWARD_COUNTS_KEY = '_dapo_filtered_reward_counts'def _accumulate_eviction_metrics(acc: dict, new: dict, stale_count: int) -> None:
    """将一次 poll 迭代的驱逐指标合并入累积字典 ``acc``。    ``stale_count`` 用于加权平均 staleness,使均值跨迭代保持每样本平均。
    """
    stale_count_key = next((k for k in new if k.endswith('/off_policy/evicted_samples')), None)
    prev_stale_total = acc.get(stale_count_key, 0) if stale_count_key else 0
​
    for key, value in new.items():
        if key.endswith('/evicted_samples_staleness/mean'):
            denom = prev_stale_total + stale_count
            acc[key] = (acc.get(key, 0.0) * prev_stale_total + value * stale_count) / denom if denom else value
        elif key.endswith('/evicted_samples_staleness/max'):
            acc[key] = max(acc.get(key, value), value)
        elif key.endswith('/evicted_samples_staleness/min'):
            acc[key] = min(acc.get(key, value), value)
        elif key == DAPO_FILTERED_REWARD_COUNTS_KEY:
            # 字典值:合并过滤奖励的计数
            merged = Counter(acc.get(key, {}))
            merged.update(value)
            acc[key] = dict(merged)
        else:
            acc[key] = acc.get(key, 0) + value

评论区精华

max_num_gen_batches 被忽略可能 OOM 正确性

tongyx361 指出 algorithm.filter_groups.max_num_gen_batches 当前被忽略,可能导致生成请求堆积耗尽资源,请求至少加警告。

结论:作者在后续 commit 中可能添加了警告,但 review 未最终确认。PR 合并前未见到明确修复。 · unresolved

自定义 sampler 时仍调用内置过滤验证 正确性

tongyx361 指出 _resolve_filter_groups_metric() 在 custom_sampler 时仍被调用,与注释矛盾。

结论:作者在 trainer_base.py 中通过 has_custom_sampler 条件跳过内置验证,新增测试验证。 · 已解决

WandB 表每次重建全量上传导致二次开销 性能

tongyx361 指出 DapoFilteredRewardTableLogger 每次调用都重建并上传完整历史,造成 O(steps²) 开销。

结论:作者使用 wandb 版本判断:>=0.20 时增量上传;旧版本回退全量重建(仍存在开销)。代码已合入。 · 已解决

_dapo_filtered_keys 全量重算,建议加缓存 性能

wuxibin89 指出 _dapo_filtered_keys 在每个 poll_interval 全量扫描所有终端 prompt,建议只计算新增 finished prompt。

结论:作者未实现缓存,目前仍然全量重算。可能接受作为未来优化。 · unresolved

失败组零轨迹时不应计入 sampleable_keys 正确性

tongyx361 指出当 prompt 标记为 failure 但所有 session 均失败导致零轨迹时,其 UID 仍被计入 sampleable_keys,可能导致 materialize 返回空 batch。要求回归测试。

结论:后续 commit 'add optional failure refilling' 可能部分处理,但 review 时未明确确认。 · unresolved

风险与影响

  1. 性能风险 (P3):在 wandb 版本低于 0.20 时,DapoFilteredRewardTableLogger 每次 step 上传完整历史表,导致二次开销,训练越长影响越大。
  2. OOM 风险 (P1)max_num_gen_batches 被忽略,若生成批次过大且回填量超出界限,可能耗尽显存。代码中未加硬上限。
  3. 正确性风险:失败组若所有 session 均失败,finished_keysfailure_keys 可能无关联轨迹,但 prompt 仍计入 sampleable_keys,导致 _materialize_batch() 返回空 batch 或报错(tongyx361 指出)。
  4. 兼容性风险:分离 ReplayBufferAsync 后,外部代码直接实例化 ReplayBuffer 不受影响,但 ReplayBuffer 构造参数新增了 filter_groups_metric, train_batch_size 等非必填字段,不会破坏旧调用。
  5. 测试局限性:CPU 测试使用本地 TransferQueue,未覆盖分布式或 GPU 环境下的网络延迟和并发竞争。

用户影响:用户可通过配置 algorithm.filter_groups.enable=Truealgorithm.filter_groups.metric=acc 启用 DAPO 动态采样。通过 trainer.v1.sampler.max_off_policy_strategy=drop 启用过期丢弃。默认行为与之前一致,无需迁移。

系统影响:V1 ReplayBuffer 逻辑复杂度增加,类层次结构化改良。新增的 WandB 表日志增加网络开销,但只在启用 filter_groups 时生效。Tracking.finish() 的正确调用依赖 main_ppo.py 修改。

团队影响:需维护 sync/async 两套 eviction 路径。测试已覆盖核心场景,但未来修改需小心保持矩阵一致性。

max_num_gen_batches 忽略可能 OOM WandB 表二次开销 失败组零轨迹可采样问题 全量重算 DAPO 过滤性能 自定义采样器接口绑定

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论