Skip to content

[Feat] Add off-policy masking for partial rollouts - #2003

Merged
YanhuiDua merged 5 commits into
InternLM:mainfrom
YanhuiDua:support-offpolicy-mask
Aug 18, 2026
Merged

YanhuiDua merged 5 commits into
InternLM:mainfrom
YanhuiDua:support-offpolicy-mask

Conversation

@YanhuiDua

@YanhuiDua YanhuiDua commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

背景

在 partial rollout 场景下,同一条 response 中的 token 可能由不同版本的 policy 生成。

现有 sequence staleness 使用 response 中最早的模型版本表示整条样本的 staleness,无法区分:

  • 已经过期的旧 policy token;
  • 仍然可以参与训练的新 policy token。

本 PR 引入 token staleness,使系统能够:

  • 按 token 排除过期的 response token;
  • 当一条样本不再包含任何有效训练 token 时,将其标记为过期;
  • 只重置实际过期样本的 response;
  • 保留同一 rollout group 中其他未过期样本的生成结果。

主要改动

  1. Token 级 staleness mask:增加 max_token_staleness 配置,计算方式与 seq staleness 相同,在 take batch 阶段统一更新 response mask
  2. Token-expired 生命周期: ReplayBuffer 通过统一的生命周期逻辑处理 sequence 和 token staleness,执行时机为 replay_buffer.putrefresh_staleness,与更新 sequence staleness 相同
  3. 增强 tail_batch_trigger_size 语义
  • tail_batch_trigger_size = -1: 关闭过期 group 的 rerollout
  • tail_batch_trigger_size = 0: 立即优先 rerollout 过期 group,但保持普通异步生产和 oversampling 策略
  • tail_batch_trigger_size > 0: 等待 EXPIRED pool 累积到指定 group 数量后进入 tail-batch 模式

说明:这个PR不改动agentic RL的过期语义

token staleness 处理关键阶段

如何采样

  flowchart LR
      A[刷新 staleness] --> B[统计 EXPIRED groups]
      B --> C{tail_batch_trigger_size}
      C -- -1 --> D[采样 ABORTED 或新数据]
      C -- 0 且存在 EXPIRED --> E[优先采样 EXPIRED group]
      C -- 大于0且达到阈值 --> F[进入 tail batch]
      C -- 大于0但未达到阈值 --> D
      E --> G[保持正常异步生产和 oversampling]
      F --> H[关闭本轮 oversampling]
      D --> I[执行 rollout]
      G --> I
      H --> I
Loading

如何判断一个样本是否过期

 flowchart LR
      A[刷新 seq staleness] --> B{超过 seq threshold}
      B -- 是 --> C[state 标记为 EXPIRED]
      B -- 否 --> D{普通 rollout 且配置 token threshold}
      D -- 否 --> E[state 保持有效]
      D -- 是 --> F[计算 effective response mask]
      F --> G{是否存在有效 token}
      G -- 否 --> C
      G -- 是 --> E
      C --> H[StorageItem 标记为 EXPIRED]
      H --> I{是否允许 rerollout}
      I -- 是 --> J[只清空实际过期 state 的 response]
      I -- 否 --> K[丢弃整个 group]
Loading

配置示例

AsyncProduceStrategyConfig(
    max_staleness=2,
    max_token_staleness=0,
    tail_batch_trigger_size=0,
)

该配置表示:

  • sequence staleness 允许额外滞后两个权重同步周期;
  • token staleness 只接受当前权重同步周期内生成的 token;
  • 当样本的有效 token 全部过期后,立即允许 rerollout;
  • immediate rerollout 不会使生产策略切换到 tail-batch 模式。

@jayhenry

Copy link
Copy Markdown
Collaborator

@claude review

1 similar comment
@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

Comment thread xtuner/v1/rl/rollout/worker.py Outdated
Comment thread xtuner/v1/data_proto/rl_data.py Outdated
Comment thread xtuner/v1/rl/agent_loop_manager/agent_loop_manager.py Outdated
Comment thread xtuner/v1/rl/replay_buffer.py
Comment thread xtuner/v1/rl/rollout/utils.py Outdated
@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude: ## Summary

Adds off-policy masking for partial rollouts: response tokens from prior partial-rollout rounds are masked out of the loss via response_mask, with per-token model-step tracking to refresh masks at consumption time in the replay buffer.

Issues

Critical

  • xtuner/v1/rl/rollout/worker.py: Missing else branch in the early-exit path when enable_partial_rollout is False. Execution falls through to the generation loop and makes unnecessary backend requests.

Warning

  • xtuner/v1/data_proto/rl_data.py:329: cast on a potentially-None response_mask won't prevent a runtime crash in zip(..., strict=True). Samples without a response_mask (e.g., pre-existing on-policy samples in the replay buffer) will crash when masking is refreshed.
  • xtuner/v1/rl/agent_loop_manager/agent_loop_manager.py:143-145: Configuration validation uses assert which is stripped under -O. Should raise ValueError. Same issue in disagg_agent_loop_manager.py.
  • xtuner/v1/rl/replay_buffer.py:507: task_mask_offpolicy is now a required parameter — this is a breaking interface change for external callers.
  • xtuner/v1/rl/rollout/utils.py:296-302: Off-policy masking path assumes routed_experts from the backend always covers the complete sequence. If the backend returns only new-token routing (as in some LMDeploy configs), this replacement would be incorrect.

Nit

  • Style: PR uses from __future__ import annotations in produce_utils.py (commit 3) which changes forward-ref quoting style — fine, but the commit message doesn't mention this cleanup.

ProduceBatchResult impact

Not directly affected — no fields on ProduceBatchResult are added or changed. However, the response_mask field on RolloutState (which feeds into rollout_states inside the result) is now mutated by refresh_response_mask at consumption time — downstream consumers should expect masked-out tokens.

RoutedExperts impact

The off-policy masking path replaces routed_experts instead of concatenating. Old history refs are freed via free_object_refs — ownership transfer is correct for this path. The non-masking path retains the existing concatenation semantics unchanged.

Ray concurrency impact

Not affected — no changes to Ray actor methods, decorators, or concurrency groups.

Verdict

REQUEST_CHANGES — the missing else branch in worker.py is a regression for all users with enable_partial_rollout=False (the default).

@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

Comment thread xtuner/v1/rl/agent_loop_manager/disagg_agent_loop_manager.py Outdated
@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude: ## Summary

PATCH 4/4 addresses all critical and most warning-level issues from the previous review (missing else branch, cast on None, assertValueError, breaking interface). Good work on the fix iteration.

One remaining issue:

Warning

  • disagg_agent_loop_manager.py:84-87: The assertValueError conversion from PATCH 4 was applied to agent_loop_manager.py but missed in disagg_agent_loop_manager.py. Same rationale: assert is stripped under -O.

ProduceBatchResult impact

Not affected — no ProduceBatchResult fields are added or changed. The response_mask on RolloutState (inside rollout_states) is now mutated by refresh_response_mask at consumption time.

RoutedExperts impact

Off-policy path replaces (rather than concatenates) routed_experts, freeing the old ref via free_object_refs. Ownership is correct for this path.

Ray concurrency impact

Not affected.

Verdict

COMMENT — the remaining assert is a minor fix (one-line change in disagg_agent_loop_manager.py). The critical issues from the first review are resolved.

@YanhuiDua
YanhuiDua force-pushed the support-offpolicy-mask branch from 39de861 to 0561379 Compare August 11, 2026 11:14
Comment thread xtuner/v1/rl/rollout/utils.py Outdated
Comment thread xtuner/v1/rl/replay_buffer.py Outdated
@jayhenry

jayhenry commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

对 partial rollout 样本做 off-policy mask 时, off-policy 的定义应该也要跟当前"过期"的定义一致?
或者新增一个字段 max_trainable_response_staleness: int | None = None 替换 off policy partial rollout

  • None:所有未过期 token 都可训练,对应原来的 False。
  • 0:只训练当前权重同步周期生成的 token。
  • N:允许额外 N 个同步周期的 token 参与 loss。
  • max_staleness:仍只负责整组过期、reset 和 rerollout,保持修改前语义。

@jayhenry

Copy link
Copy Markdown
Collaborator

@claude review

Comment thread xtuner/v1/data_proto/rl_data.py
Comment thread xtuner/v1/rl/agent_loop_manager/produce_utils.py Outdated
Comment thread xtuner/v1/rl/agent_loop_manager/produce_utils.py Outdated
Comment thread xtuner/v1/data_proto/utils.py Outdated
Comment thread xtuner/v1/rl/agent_loop_manager/__init__.py
Comment thread xtuner/v1/train/rl_trainer.py
Comment thread xtuner/v1/rl/agent_loop_manager/producer.py
Comment thread tests/rl/test_staleness_policy.py Outdated
Comment thread tests/rl/test_staleness_policy.py Outdated
Comment thread tests/rl/test_multi_task_agent_loop_manager.py Outdated
@YanhuiDua
YanhuiDua force-pushed the support-offpolicy-mask branch from 04f0f12 to 1f40fff Compare August 13, 2026 06:41
@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

Comment thread xtuner/v1/train/rl_trainer.py
Comment thread xtuner/v1/train/rl_trainer.py Outdated
Comment thread xtuner/v1/data_proto/rl_data.py
Comment thread tests/rl/test_multi_task_agent_loop_manager.py Outdated
Comment thread tests/rl/test_staleness_policy.py
Comment thread tests/rl/test_prepare_train_data.py Outdated
Comment thread tests/rl/test_multi_task_agent_loop_manager.py
@jayhenry
jayhenry force-pushed the support-offpolicy-mask branch from 1f40fff to 8d65892 Compare August 13, 2026 09:29
@jayhenry

Copy link
Copy Markdown
Collaborator

@claude review

@YanhuiDua
YanhuiDua force-pushed the support-offpolicy-mask branch from 8d65892 to 43c9e5e Compare August 13, 2026 11:40
@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

@YanhuiDua
YanhuiDua force-pushed the support-offpolicy-mask branch from 8694013 to b26bc3f Compare August 13, 2026 13:21
@jayhenry

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 引入 token 级 staleness:新增 max_token_staleness 配置,在 ReplayBuffer 侧用统一的 _apply_staleness_lifecycle 处理 sequence/token 两级过期(只重置真正过期 state 的 response,保留同组新鲜成员),在 take_train_batch 侧按 token 新鲜度重算 response_mask,并把 tail_batch_trigger_size 扩展为 -1 / 0 / N 三态语义(默认值由 0 改为 -1)。

ProduceBatchResult impact: 三处 trainer-visible 变化 —(1) produced_tokens 语义由「整条 response 长度」改为「本轮新增 token 数」;(2) token-expiry 会把原本 COMPLETED 的 group 翻成 EXPIRED,影响 leftover_completed / leftover_expired / produced_samples;(3) tail_batch_trigger_size=0 使 EXPIRED group 变为可重试(此前直接丢弃)。

RoutedExperts impact: token-expiry 只对过期 state 调 reset_rollout_response(内部 free_object_refs),同组保留的 fresh state 会在 EXPIRED pool 中继续持有 routed_experts ObjectRef 直到 rerollout 完成或整组丢弃 —持有窗口变长,但 owner 唯一、非 retryable 分支走 discard_rollout_state,未发现泄漏。

Ray concurrency impact: not affected(无 ray.method、concurrency group 或 actor 构造改动)。

Main Flowchart after this PR

flowchart TD
    A[put / refresh_staleness] --> B[_apply_staleness_lifecycle]
    B --> C{seq_staleness >= stale_threshold}
    C -- 是 --> F[标记该 state EXPIRED]
    C -- 否 --> D{agentic group 或 token_stale_threshold 为空}
    D -- 是 --> E[保持原状态]
    D -- 否 --> G[calculate_effective_response_mask]
    G --> H{存在有效 token}
    H -- 是 --> E
    H -- 否 --> F
    F --> I{expired_groups_retryable}
    I -- 是 --> J[只重置过期 state 的 response<br/>整组进 EXPIRED pool]
    I -- 否 --> K[整组丢弃<br/>含仍新鲜成员]
    E --> L[take_batch]
    J --> M[从 EXPIRED pool rerollout]
    M --> A
    L --> N[take_train_batch 重算 response_mask]
    N --> O[_prepare_train_data / 训练]
    style K fill:#ffcccc,stroke:#cc0000
    style N fill:#ffe0b2,stroke:#e65100
    style G fill:#e3f2fd,stroke:#1565c0
    style J fill:#e3f2fd,stroke:#1565c0
Loading

核心原理实现与单测

核心实现为三段:calculate_effective_response_mask(semantic mask ∩ token staleness mask)、ReplayBuffer._apply_staleness_lifecycle(put/refresh 两个时机统一做 seq+token 过期判定与清理)、take_train_batch(消费期写回 effective mask)。

真实代码路径已被覆盖的部分:

  • mask 纯函数行为(阈值放宽、与 semantic mask 求交、rerollout 后无 semantic mask):tests/rl/test_staleness_policy.py::TestTokenStalenessMask
  • 生命周期经公开 put / refresh_staleness / get + 真实 Naive/Pandas storage:tests/rl/test_replay_buffer.py 新增 5 例,含 token/seq 过期保留新鲜成员、agentic group 跳过、非 retryable 整组丢弃。
  • 消费期 mask 经公开 AgentLoopManager.produce_batchtests/rl/test_multi_task_agent_loop_manager.py 2 例(普通与 agentic 分支)。
  • tail_batch_trigger_size=0 的采样语义与 disagg put-time consumer step:tests/rl/test_producer.py 2 例。

覆盖缺口见「单测建议」。

抽象与信息隐藏评估

  • Warning xtuner/v1/rl/agent_loop_manager/producer.py#L210disagg_producer.py#L265 同):max_token_staleness 的 docstring 声明「不会使 group 过期或重跑,只收缩 response_mask」,与实现的 token-expired 生命周期相反,配合新默认 tail_batch_trigger_size=-1 会静默丢弃整组。
  • Warning xtuner/v1/rl/agent_loop_manager/produce_utils.py#L621:同一条 token-staleness 规则(含 agentic 判定)在 produce_utilsreplay_buffer 两个 Module 各实现一遍,且 guard 不一致,规则的 Locality 被打散。

单测建议

  • Warning xtuner/v1/rl/replay_buffer.py#L490:PR 的头号行为「EXPIRED group 保留 fresh 成员后重跑」只有 buffer 字段级断言,缺少覆盖重跑路径的用例(该行为依赖 worker.generatestatus == COMPLETED 早退才不会覆写保留的 response)。
  • Warning xtuner/v1/rl/agent_loop_manager/produce_utils.py#L173produced_tokens 改为只统计本轮新增 token,这一 ProduceBatchResult 字段的语义变化没有任何断言覆盖。

其他 Issues

  • Warning xtuner/v1/rl/agent_loop_manager/producer.py#L233disagg_producer.py#L278 同):tail_batch_trigger_size 旧默认值 0 的语义被翻转(原「丢弃过期组」→ 现「立即 rerollout」),显式写 0 的存量配置行为静默改变且无提示。

Verdict

REQUEST_CHANGES

Comment thread xtuner/v1/rl/agent_loop_manager/producer.py Outdated
Comment thread xtuner/v1/rl/agent_loop_manager/produce_utils.py Outdated
Comment thread xtuner/v1/rl/agent_loop_manager/producer.py

async def put_generated_group(self, group: list[RolloutState]) -> bool:
produced_tokens = sum(len(item.response_ids) for item in group if item.response_ids is not None)
produced_tokens = sum(len(item.response_ids or []) - len(item.response_model_steps or []) for item in group)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [测试] produced_tokens 语义改为「仅本轮新增 token」(原为整条 response 长度),这是 ProduceBatchResult 的 trainer-visible 字段,但没有任何断言覆盖。建议补一例:同一 group 连续两轮 partial rollout,断言第二轮只计入新增 token。

Comment thread xtuner/v1/rl/replay_buffer.py
@@ -92,11 +95,6 @@ def default_should_continue_fn(completed_count: int, batch_size: int, **kwargs)


def calculate_stale_threshold(max_staleness: int, sync_weights_interval: int) -> int:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

保留这个验证?

@YanhuiDua YanhuiDua Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉也没有必要,因为sync_weight_interval 会在RL trainer中检查是否大于0,token和seq的staleness作为AsyncproducerConfig中检查是否大于0

@YanhuiDua
YanhuiDua force-pushed the support-offpolicy-mask branch from bbc8daa to b191566 Compare August 18, 2026 02:56
@YanhuiDua
YanhuiDua merged commit 52212b2 into InternLM:main Aug 18, 2026
5 of 6 checks passed
@YanhuiDua
YanhuiDua deleted the support-offpolicy-mask branch August 18, 2026 03:00
longboat2010 added a commit to Ascend-SHL-SACT/xtuner that referenced this pull request Aug 20, 2026
…Split) (#30)

* [ci] change lmdeploy version and related packages (InternLM#1996)

* [ci] update vllm case in gpu

* fir error on rl vllm case

* fix script error

* fix error

* [Fix] Fix Ray generate concurrency group metadata (InternLM#2005)

* Fix Ray generate concurrency group metadata

* fix docs build error

* [Fix] Avoid scanning Transformers lazy module during test collection (InternLM#2008)

* test: avoid scanning Transformers lazy module

* test: call dense decoder with keyword arguments

* test: preserve Qwen3.5 vision interpolation dtype

* Fix custom preprocess and postprocess in judger pools (InternLM#2000)

* [ci] Add Intern-S2-Preview RL ETE coverage and user examples (InternLM#2010)

* [CI] Expand Claude review guidance (InternLM#2012)

* [CI] Use latest Claude Opus model (InternLM#2013)

* [Fix] discard expired states when tail batch is disabled (InternLM#2006)

* discard expired states when tail batch is disabled

* delete bind function and add retryable attr

* [GLM-5.2] Preserve HF config compatibility for vLLM (InternLM#1998)

* [GLM-5.2] Preserve HF export compatibility fields

Keep legacy routing metadata required by vLLM and trim unused RoPE defaults from exported configs.

* [Testing] Add HF config export contract checker

* [Skills] Handle missing HF config exporters

* [Fix] Supervise GLM-5.2 assistant stop tokens (InternLM#1997)

* [Fix] Supervise GLM-5.2 assistant stop tokens

* [SKILL] Add chat-template audit and implementation skill

Co-authored-by: Cursor <cursoragent@cursor.com>

* [SKILL] Remove Python environment instruction

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* [CI] Improve Claude action completion and tool access (InternLM#2015)

* [CI] Improve Claude review completion

* [CI] Expose Claude review turn budget

* [CI] Expand sandboxed Claude review tools

* [CI] Rely on isolated review runner

* [CI] Expand Claude comment capabilities

* [CI] Define Claude review section order

* [CI] Add Claude test review guidance

* [Fix] Add GLM-5.2 MuonSplit and AdamW-only gradient clipping (InternLM#2001)

* Fix GLM-5.2 Muon splitting and gradient clipping

* Fix MuonSplit parameter typing

* Refactor gradient clipping policy

* [CI] Split Claude review analysis and publication (InternLM#2016)

* [CI] Split Claude review into analysis and publish phases

* fix(ci): publish Claude review summary deterministically

* fix(ci): scope Claude review to prepared PR diff

* fix(ci): harden Claude review credentials

* fix(ci): prepare deterministic Claude review bundle

* refactor(ci): simplify Claude review workflow

* [CI] Streamline Claude review output and publication (InternLM#2019)

* [CI] Fix Claude review summary publication

Allow the StructuredOutput tool required by --json-schema instead of denying every tool, and give the summary phase a second turn. Limit reported findings to Warning severity and above to keep reviews concise.

* [CI] Streamline Claude review prompt and deduplication

Limit review output to supported Warning+ findings, preserve the summary contract, and add a normalized discussion index for efficient deduplication.

* [CI] Refine Claude review comment format

Classify inline findings, keep routine comments within 150 characters, and state the required summary section order.

* add qwen3.5 test cases about ep_size (InternLM#2011)

* add ep case

* update config

* add rl mtp config

* debug

* update step

* debug

* debug

* add ci debug env

* update config

* update description

* [ci] optimize ete false positive (InternLM#2028)

* Reduce ETE false positives from tight KL/time thresholds and offline resume first checks.

Loosen qwen3-5 VL RL mismatch_k3_kl and qwen3-rl-lmdeploy KL/time budgets, and compare only the first-run tracker prefix when phase=first sees a merged offline file.

* Check mismatch_k3_kl per-step value < 0.001 instead of baseline drift.

Add method=value for RL metric bounds and apply it to all K3 KL checks so ETE no longer fails on run-to-run abs diffs.

* Align qwen3-5-rl-vl-lmdeploy-mtp-ep K3 KL check to per-step value < 0.001.

* update

* Harden ETE checks: fix RL resume meta, drop SFT round(,2), slowdown-only time.

Resolve colocate RL meta for update_meta, compare SFT relative error without two-decimal rounding, and only penalize time/step regressions for s2-preview.

* drop faild (InternLM#1945)

Improve agent rollout failure handling

* [Feat] Add off-policy masking for partial rollouts (InternLM#2003)

* Add token staleness masking during batch take

* Add lifecycle-aware sampling for token-expired groups

* Refine expired rollout sampling and tail-batch semantics

* Refine staleness lifecycle for expired groups

* fix claude comments

* [Fix] fix  RL MTP config handling for non-compose models (InternLM#2031)

Fix RL MTP config handling for non-compose models

* support transformers 5.14.1 (InternLM#2024)

* support transformers 5.14.1

* update

* [ci] Optimize ete case, relax sft metric after dropping round (InternLM#2034)

* [ci] relax SFT metric thresholds after dropping round(,2)

Align loss and text_tokens tolerances with observed run-to-run drift so ETE baseline checks match unrounded relative error semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [ci] use absolute diff for text_tokens in SFT metric checks

Add per-metric comparison method support and compare token counts with exact-match threshold 0 while keeping relative drift checks for loss metrics.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: JuliaLin <julialin@JuliaLindeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>

* [Fix] Preserve concurrent trace sessions in disaggregated RL (InternLM#2021)

* [Fix] Preserve concurrent rollout trace sessions

* [Fix] Address trace-session lifecycle review

* refactor(rl): centralize terminal trace cleanup

* refactor(rl): use non-retryable cleanup terminology

* style(rl): apply docformatter

* fix(rl): invalidate trace store cache without Ray

---------

Co-authored-by: zhulinJulia24 <145004780+zhulinJulia24@users.noreply.github.com>
Co-authored-by: kkscilife <126147887+kkscilife@users.noreply.github.com>
Co-authored-by: Yanhui Duan <dyh10280@163.com>
Co-authored-by: Penghao Zhao <henryzhao1989@163.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: 纪焘 (Tao Ji) <taoji.cs@gmail.com>
Co-authored-by: liukuikun <24622904+Harold-lkk@users.noreply.github.com>
Co-authored-by: PengchengShi00 <146822991+PengchengShi00@users.noreply.github.com>
Co-authored-by: Haian Huang(深度眸) <1286304229@qq.com>
Co-authored-by: JuliaLin <julialin@JuliaLindeMacBook-Pro.local>
Co-authored-by: matrix72 <60974665+matrix72c@users.noreply.github.com>
@YifanHe-ailab

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 引入 token 级 staleness:新增 max_token_staleness 配置,把 seq/token 两级过期统一到 ReplayBuffer._apply_staleness_lifecycle(只重置真正过期的 state,保留同组新鲜成员),在 take_train_batch 侧按 token 新鲜度重算 response_mask,并把 tail_batch_trigger_size 扩展为 -1 / 0 / N 三态语义(默认由 0 改为 -1)。核心正确性逻辑扎实且测试覆盖较完整,遗留一个数据路径性能问题和一个覆盖缺口。

ProduceBatchResult impact: 三处 trainer-visible 变化 —(1) token expiry 会把原本 COMPLETED 的 group 翻成 EXPIRED,影响 leftover_completed / leftover_expired / produced_samples;(2) produced_tokens 语义由「整条 response 长度」改为「本轮新增 token 数」;(3) tail_batch_trigger_size=0 使 EXPIRED group 变为可重试(此前直接丢弃)。

RoutedExperts impact: token expiry 只对过期 state 调 reset_rollout_response(内部 free_object_refs),同组保留的 fresh state 会在 EXPIRED pool 中继续持有 routed_experts object ref 直到 rerollout 完成或整组丢弃 —持有窗口变长,但 owner 唯一、非 retryable 分支走 discard_rollout_state,未发现泄漏。

Ray concurrency impact: not affected(无 ray.method、concurrency group 或 actor 构造改动)。

核心原理实现与单测

核心实现为三段,均有真实代码路径覆盖:

  • mask 计算rl_data.py#L324-L386 的 semantic mask ∩ token staleness mask,以及 agentic / 空 response 的 None 短路 → tests/rl/test_staleness_policy.py::TestTokenStalenessMask(阈值放宽、与 semantic mask 求交、rerollout 后无 semantic mask)。
  • 统一生命周期replay_buffer.py#L442-L494,在 putrefresh_staleness 两个时机做 seq+token 过期判定与清理 → tests/rl/test_replay_buffer.py 新增 5 例,全部经公开 put / refresh_staleness / get + 真实 Naive/Pandas storage,覆盖 token/seq 过期保留新鲜成员、agentic group 跳过、非 retryable 整组丢弃。
  • 消费期写回 maskproduce_utils.py#L613-L628tests/rl/test_multi_task_agent_loop_manager.py 2 例,经公开 AgentLoopManager.produce_batch 覆盖普通与 agentic 两条分支。

此外 tail_batch_trigger_size=0 的采样语义、disagg put-time consumer step、response_mask=None 的 rerollout 训练路径分别由 tests/rl/test_producer.py(2 例)与 tests/rl/test_prepare_train_data.py 覆盖。覆盖缺口见「单测建议」。

单测建议

  • Warning xtuner/v1/rl/replay_buffer.py#L456-L458:NOTE 声明的「已 EXPIRED group 中保留的 COMPLETED state 在等待 rerollout 期间也会继续过期」是本次扩大 refresh 状态集的直接原因,但新增 5 例都从非 EXPIRED group 起步,该转换没有任何用例覆盖。

其他 Issues

  • Warning xtuner/v1/rl/replay_buffer.py#L463-L477refresh_staleness 在持锁且无 await 的循环内为每条记录物化完整的 per-token mask 列表,而结果只用于 not any(...) 判空,配合本 PR 扩大的扫描范围(新增 EXPIRED 状态、disagg 每步两次刷新)显著加重了 buffer 锁与事件循环的负担,且未附带 benchmark。

Verdict

COMMENT

Comment on lines +463 to +477
token_level_effective_masks = calculate_group_effective_response_masks(
group,
current_train_step=current_train_step,
token_stale_threshold=token_stale_threshold,
)

for item in group:
if retryable:
# Tail batch may reroll this sample. Keep prompt and multimodal
# training inputs, but release the stale response and routed experts.
reset_rollout_response(item)
else:
# No consumer can retry this terminal group. Release all optional
# state before dropping the group's final strong references.
for index, item in enumerate(group):
if expired_mask[index]:
continue
if stale_threshold is not None and item.seq_staleness >= stale_threshold:
expired_mask[index] = True
continue
effective_mask = token_level_effective_masks[index]
if effective_mask is not None and not any(effective_mask):
expired_mask[index] = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 这里为每条记录物化了完整的 per-token mask 列表(rl_data.py 内每个 state 要分配 2~3 个 len(response_ids) 长度的 list),但结果只用于 L476 的 not any(...) 判空。

该循环位于 refresh_stalenessasync with self._lock 内且中间没有 await,会阻塞并发的 put/get/count 和宿主 event loop。改动前同一循环只做 refresh_seq_staleness(一次 min())。同时扫描范围也变大了:refresh 状态集新增了 Status.EXPIRED,disagg 路径每个 train step 还会刷新两次。

建议判空时短路,不要构造 mask(materialize 只保留给 take_train_batch):

def _has_trainable_token(item, *, current_train_step, token_stale_threshold) -> bool:
    ...
    return any(
        sem and calculate_seq_staleness(step, current_train_step) < token_stale_threshold
        for sem, step in zip(semantic_mask, item.response_model_steps)
    )

.claude/CLAUDE.md,数据路径改动需要 before/after benchmark 或说明为何不需要。

ProduceBatchResult impact: 不改变任何字段取值,仅影响 produce_time_s 与整体吞吐。

Comment on lines +456 to +458
# NOTE: An EXPIRED group may still contain COMPLETED states whose responses were preserved.
# Refresh the group again so those states can also expire while waiting for rerollout.
expired_mask = [item.status == Status.EXPIRED for item in group]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [测试] 这条 NOTE 描述的转换没有用例覆盖:新增 5 例都从非 EXPIRED group 起步。建议补一例把混合 EXPIRED/COMPLETED 的 group 在更大的 current_train_step 上再过一次 refresh_staleness,断言保留的 fresh state 最终也翻为 EXPIRED 并被 reset。

ProduceBatchResult impact: 该转换决定 leftover_expiredleftover_completed 的归属,回归时不会被现有断言发现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants