# PR #1829 完整报告

- 仓库：`radixark/miles`
- 标题：fix: require explicit off-policy correction for async PPO training
- 合并时间：2026-07-29 09:26
- 原文链接：http://prhub.com.cn/radixark/miles/pull/1829

---

# 执行摘要

- 一句话：强制异步 PPO 显式选择 off-policy 校正，防止静默错训
- 推荐动作：值得精读。该 PR 以极小改动（48 行）解决了一个隐蔽且影响训练正确性的问题，设计上选择了快速失败而非自动默认，避免替用户做可能错误的策略选择。重点关注三点：一是错误信息直接列出三个可选方案的可操作性；二是将校验放在 `train_async.py::train` 最前而非参数解析阶段，确保 GPU 等资源分配前失败；三是用 `use_critic` 简单判别 PPO 估计器的取舍。建议同时审查仓库内所有异步 PPO 示例是否有显式配置这三个 flag，以免被新契约打断。

# 功能与动机

PR body 明确指出：`use_rollout_logprobs=False` 使 trainer 用当前 actor 重算 `log_probs` 并作为 PPO 比率分母，`Clipping is then anchored to a policy that never generated the trajectory`，且 `The weight_versions recorded on samples are only a metric — nothing enforces consistency`。框架虽已提供三种显式校正方案，但默认配置下这种陈旧性会被静默忽略，因此需要一个强制契约来防止用户无意中运行错误目标函数的 PPO 训练。

# 实现拆解

1. **新增校验函数**：在 `miles/utils/arguments.py` 中新增 `validate_async_off_policy_correction(args)`，先判断 `args.use_critic`（PPO 风格估计器的判别标志），非 PPO 估计器直接返回；否则断言 `use_rollout_logprobs`、`use_tis`、`keep_old_actor` 三者至少一个为真，不满足则抛出包含完整方案说明的 `AssertionError`，错误信息直接列出三种 flag 的语义。
2. **入口接线**：在 `train_async.py` 的 `train()` 函数顶部（`configure_logger` 之前、GPU 分配与 rollout 引擎创建之前）调用该校验，保证在任何资源占用发生前快速失败；同时更新 import。
3. **测试配套**：在 `tests/fast/utils/test_arguments.py` 中新增 `_make_async_ppo_args` 辅助构造 `SimpleNamespace` 参数，并通过 `TestValidateAsyncOffPolicyCorrection` 的三个测试方法覆盖：默认拒绝路径、三种校正方案各自通过、`use_critic=False` 的非 PPO 估计器不受影响。作者在 PR body 中说明，因该测试文件 import 了 `miles.utils.arguments` 而依赖 sglang，故在分支上直接执行了提取出的函数体验证相同用例，CI 中运行真实测试文件。
4. **无配置 / 部署变更**：未涉及 YAML、Docker 或 CI 配置，纯参数契约与入口校验改动。

关键文件：
- `miles/utils/arguments.py`（模块 参数校验；类别 source；类型 core-logic；符号 validate_async_off_policy_correction）: 新增 validate_async_off_policy_correction 校验函数，定义异步 PPO 必须显式选择行为策略校正方案的契约，是本 PR 的核心逻辑。
- `train_async.py`（模块 异步训练；类别 source；类型 dependency-wiring；符号 train）: 异步训练入口快速失败接线，使校验在 GPU 分配、rollout 引擎创建等资源操作之前生效。
- `tests/fast/utils/test_arguments.py`（模块 参数；类别 test；类型 test-coverage；符号 _make_async_ppo_args, TestValidateAsyncOffPolicyCorrection, test_ppo_without_correction_is_rejected, test_ppo_with_any_correction_passes）: 覆盖三种校正方案放行、默认拒绝与非 PPO 放行三个分支，防止校验逻辑回归；是 PR 唯一测试配套。

关键符号：validate_async_off_policy_correction, train

## 关键源码片段

### `miles/utils/arguments.py`

新增 validate_async_off_policy_correction 校验函数，定义异步 PPO 必须显式选择行为策略校正方案的契约，是本 PR 的核心逻辑。

```python
# 新增于 miles/utils/arguments.py 的异步 PPO 策略契约校验函数
def validate_async_off_policy_correction(args) -> None:
    """强制为异步 PPO 训练显式选择行为策略校正方案。

    异步训练循环中，下一轮 rollout 会在当前权重更新发布前启动，
    样本可能来自落后于 actor 的陈旧策略（stale policy）。默认情况下
    log_probs 由当前 actor 重算并作为 PPO 比率分母，导致裁剪与
    KL 优势计算锚定在一个从未生成过该轨迹的策略上。
    """
    # 非 PPO 估计器（use_critic=False，如 GRPO）没有该问题，直接放行
    if not args.use_critic:
        return
    # 三个内置方案必须至少显式启用其一，否则视为配置错误
    assert args.use_rollout_logprobs or args.use_tis or args.keep_old_actor, (
        'Async PPO training requires an explicit behavior-policy correction, because rollouts are '
        'generated before the current weight update while log probs are recomputed by the current '
        'actor by default. Pass one of: --use-rollout-logprobs (use the rollout engine\'s log probs '
        'as the ratio denominator), --use-tis (truncated importance sampling correction), or '
        '--keep-old-actor (recompute the denominator with the weights the rollout engines used).'
    )

```

### `tests/fast/utils/test_arguments.py`

覆盖三种校正方案放行、默认拒绝与非 PPO 放行三个分支，防止校验逻辑回归；是 PR 唯一测试配套。

```python
# 构造带默认值的异步 PPO 训练参数，便于测试覆盖不同组合
# 默认 use_critic=True 表示 PPO 风格估计器，三个校正方案均未开启
def _make_async_ppo_args(**overrides) -> SimpleNamespace:
    defaults = dict(
        use_critic=True,
        use_rollout_logprobs=False,
        use_tis=False,
        keep_old_actor=False,
    )
    defaults.update(overrides)
    return SimpleNamespace(**defaults)


class TestValidateAsyncOffPolicyCorrection:
    # 三个校正方案均未设置时，必须触发 AssertionError 并提示可选方案
    def test_ppo_without_correction_is_rejected(self):
        with pytest.raises(AssertionError, match='behavior-policy correction'):
            validate_async_off_policy_correction(_make_async_ppo_args())

    # 任一校正方案单独启用即可满足契约
    @pytest.mark.parametrize('flag', ['use_rollout_logprobs', 'use_tis', 'keep_old_actor'])
    def test_ppo_with_any_correction_passes(self, flag):
        validate_async_off_policy_correction(_make_async_ppo_args(**{flag: True}))

    # GRPO 等非 PPO 估计器（use_critic=False）不受新契约影响
    def test_non_ppo_estimators_are_unaffected(self):
        validate_async_off_policy_correction(_make_async_ppo_args(use_critic=False))

```

# 评论区精华

PR 无实质技术讨论。唯一评论是 gemini-code-assist[bot] 的服务终止通告：`The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.`。reviewer Zhichenzzz 直接提交了 APPROVED，未留下审查意见。

- 暂无高价值评论线程

# 风险与影响

- 风险：
 1. **破坏性变更风险**：所有未设置任一校正 flag 的异步 PPO 训练会在启动时立即失败。这是有意行为，但现有训练脚本和配置若未及时补上 flag 会被中断，需要迁移现有命令。
 2. **校验粒度不足**：`validate_async_off_policy_correction` 只检查 flag 是否存在，并不验证校正机制本身是否真正生效（例如 `--keep-old-actor` 的旧 actor 权重是否确实与 rollout 引擎版本对齐），也无法检测样本 `weight_versions` 的一致性，属于“配置契约”而非“运行时正确性保障”。
 3. **判别符依赖**：以 `use_critic` 作为 PPO 估计器的判别标志，若未来其他估计器（如带 critic 的非 PPO 算法）也启用 `use_critic=True`，可能被该校验误拦截；当前与框架内 `--advantage-estimator ppo` 的语义一致。
 4. **测试覆盖受限**：`tests/fast/utils/test_arguments.py` 依赖 sglang，在未安装 sglang 的轻量 CI 环境下可能无法执行新增测试，需依赖作者手工验证或等待 CI 完整环境。
 - 影响：异步 PPO 训练的所有现有用户都会受到影响：默认命令将不再可用，必须显式选择三种校正方案之一，否则训练在资源分配前即失败，避免了在错误目标函数下浪费算力。同步入口 `train.py`、非 PPO 估计器（如 GRPO）以及使用 `use_critic=False` 的配置完全不受影响。对团队而言，需要同步更新异步 PPO 相关文档、示例和默认配置模板，明确标注该契约；对系统而言，这次防御性校验降低了训练结果不可信的风险，但对架构和运行性能无影响。
 - 风险标记：异步 PPO 默认配置破坏性失败 , 校验只查 flag 存在性 , 测试依赖 sglang

# 关联脉络

- PR #1735 [PPO] Share Actor/Critic GPUs: 同为 PPO 异步训练路径的演进，调整 actor/critic 权重管理与训练循环，与本 PR 的权重发布 / 策略陈旧问题属于同一功能线。
- PR #1926 Fix weight sync selector for frozen speculative drafts: 修复权重同步选择器，与 --keep-old-actor 方案共用权重发布链路，后续版本保障旧 actor 权重版本对齐。