Skip to content

[Fix] Transfer routed experts release ownership - #2064

Open
matrix72c wants to merge 1 commit into
InternLM:mainfrom
matrix72c:fix/routed-experts-release-ownership
Open

matrix72c wants to merge 1 commit into
InternLM:mainfrom
matrix72c:fix/routed-experts-release-ownership

Conversation

@matrix72c

@matrix72c matrix72c commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Motivation

When enable_return_routed_experts=True, an RL rollout can attach Ray ObjectRefs to a RolloutState. The same reference may either be created by the rollout worker or be borrowed from a TraceStore session. Before this change, RolloutState did not record which component owned the reference, while several cleanup helpers unconditionally freed references. This made retryable stale samples unsafe and could invalidate references that were still held by the TraceStore.

Failure chain before this PR

The problematic path is easiest to see for a retryable stale sample:

  1. A rollout group becomes stale in ReplayBuffer and is selected for retry.
  2. The old lifecycle calls reset_rollout_response() to keep the prompt and clear the generated response.
  3. reset_rollout_response() also calls free_object_refs() for every routed-experts ObjectRef it sees. It cannot distinguish a locally created rollout reference from a borrowed TraceStore reference.
  4. For a TraceStore sample, the Trie still contains the same reference because the session has not been released yet. The reset therefore frees a shared object too early.
  5. The retry keeps the existing trace/session lifecycle, and a later export or training read tries to fetch the reference from the Trie. The reference is already invalid, which can surface as an object-fetch failure or as routed-experts/sequence-length validation errors.

There were two related versions of the same ownership bug:

  • Partial-rollout post-processing awaited its input routed-experts reference and then freed it unconditionally. A borrowed reference could therefore be destroyed while the TraceStore still owned it.
  • Generic discard recursively freed references without an explicit release decision. Cleanup correctness depended on the caller knowing whether a session had already been released. In addition, the training-batch cleanup ran only after a successful prepare/fit sequence, so an exception could leave a TraceStore session alive.

Finally, replacing a value in the TraceStore trie did not release routed-experts references from the overwritten value, which could leak Ray objects during reroll/overwrite workloads.

What changed

  • Add RolloutState.routed_experts_owner with the values "rollout" and "trace_store".
  • Make reset_rollout_response() a pure state reset: it clears response fields and routed-experts fields, but never calls ray.free.
  • Add release_owned_routed_experts() for explicit caller-owned release, and make discard_rollout_state(..., release_refs=...) opt in to releasing resources.
  • Mark routed-experts references at every direct rollout producer (RolloutWorker, vLLM rollout parsing, and the VERL tool loop) and at every TraceStore export boundary.
  • On retryable stale samples, call release_owned_routed_experts() unconditionally: "rollout" refs are freed, "trace_store" refs are only detached (they stay valid until their session is released), and untagged refs (restored from legacy checkpoints via ray.put) are also freed so they cannot leak.
  • Make partial-rollout input release explicit. RolloutWorker.generate() opts in for direct rollout inputs; the handler default is non-releasing for safe reuse by other callers.
  • Keep TraceStore session release as the final release point for its references, detach released routes before generic discard, and ensure the trainer performs session release in a finally block.
  • On trie overwrite, park the replaced value instead of freeing its refs immediately: sibling RolloutStates may still borrow those refs, so the overwrite path cannot safely decide reachability. Parked refs are freed (deduplicated against the live tree) by the same session release that frees the trie.

Ownership after this change

Direct rollout:
  producer creates ref -> owner = "rollout"
  caller explicitly releases ref -> reset/discard clears state

TraceStore export:
  Trie/session owns ref -> RolloutState borrows it (owner = "trace_store")
  retryable reset only detaches it
  session release -> Trie frees the ref

The LMDeploy generation protocol is unchanged. Ownership is established at XTuner's producer/export boundaries, and old checkpoints remain loadable because the new field defaults to None.

Impact

This prevents premature freeing of shared TraceStore references, makes direct-rollout cleanup explicit, and closes exception/overwrite cleanup gaps without introducing a global reference registry or changing stale reroll/session semantics.

Tests

The following targeted checks pass locally:

  • ruff check for all changed source and test files
  • ruff format --check for all changed source and test files
  • python -m py_compile for all changed source and test files
  • RL state/rollout/producer/trajectory tests: 95 passed in total
  • Staleness ownership tests: 10 passed
  • Retryable replay ownership tests: 3 passed (release / trace-store detach / untagged release)
  • Rollout logic tests: 52 passed
  • Producer tests: 25 passed
  • Trajectory logging tests: 8 passed
  • TraceStore unit tests: 8 passed (Ray local mode), including trie-overwrite non-freeing and session-release collection of overwritten refs

The full replay-buffer suite was also inspected; an existing async save/resume test does not complete in this shared test environment, so it was not used as a passing signal.

Related issue

Related to #2025. This PR fixes routed-experts ObjectRef lifetime leaks and premature frees on the XTuner RL path. It does not, by itself, bound learner-side materialization or change sequence-parallel transfer order; those memory-footprint issues remain separate follow-up work. The LMDeploy endpoint and generation protocol are unchanged.

@matrix72c
matrix72c force-pushed the fix/routed-experts-release-ownership branch 3 times, most recently from 1979eb8 to b68b221 Compare September 7, 2026 05:54
@matrix72c
matrix72c force-pushed the fix/routed-experts-release-ownership branch from c2b2b5d to d4f0f01 Compare September 17, 2026 02:07
@YifanHe-ailab

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 为 RolloutState 引入 routed_experts_owner"rollout" / "trace_store"),把 reset_rollout_response 改为纯状态重置,新增 release_owned_routed_expertsdiscard_rollout_state(release_refs=...) 的显式释放入口,并在 Trie.insert 覆盖写时新增释放逻辑、在 trainer 中用 finally 兜底 session 释放。方向正确,但新增的 overwrite 释放点重新引入了同类 ownership 风险,且 owner 判定规则被复制到调用方。

ProduceBatchResult impact: not affected —— Status 流转、leftover_* 计数、group_gen_* 计时与 reward 计数均未改动,变更只影响 _apply_staleness_lifecyclerelease_and_discard_rollout_groups 内部的 ref 释放副作用。

RoutedExperts impact: 本 PR 核心 —— 新增 owner 标记、把释放从 reset_rollout_response 移出、discard_rollout_state 改为 opt-in 释放,并在 Trie.insert 增加了一个新的 free 点。

Ray concurrency impact: not affected —— 未改动 @ray.method、装饰器顺序、concurrency_groups 或 actor 构造。

Verdict: REQUEST_CHANGES

Main Flowchart after this PR

flowchart TD
    A[Rollout producer<br/>worker / vllm / verl tool] -->|owner = rollout| S[RolloutState.routed_experts]
    B[TraceStore export_training_trace] -->|owner = trace_store 借用| S
    S --> C{ReplayBuffer<br/>_apply_staleness_lifecycle}
    C -->|retryable 且 owner == rollout| D[release_owned_routed_experts]
    C -->|retryable 且 owner == trace_store| E[仅 detach]
    D --> F[reset_rollout_response<br/>纯状态重置]
    E --> F
    C -->|non-retryable| G[release_and_discard_rollout_groups<br/>discard_rollout_state release_refs=True]
    F -->|session_id 保留,retry 重新生成| H[SessionServer.on_response]
    H --> I[Trie.insert 同 key 覆盖写]
    I --> J[新增:_free_ray_refs old_value]
    B -.借用同一 ObjectRef.-> J
    G --> K[Trie.release<br/>session 最终释放点]

    style C fill:#fff3cd,stroke:#d39e00
    style D fill:#fff3cd,stroke:#d39e00
    style I fill:#f8d7da,stroke:#c82333
    style J fill:#f8d7da,stroke:#c82333
Loading

核心原理实现与单测

核心原理是“谁创建谁释放”:直接 rollout 产出的 ref 归 "rollout",TraceStore 导出的 ref 仅为借用,最终释放点是 session release。实现上,所有直接生产点(worker.py:1279/1287/1312vllm.py:486/495utils.py:219/244agent_loop_verl_tool.py:141)与导出点(agent_in_localhost_loop.py:267agent_in_sandbox_loop.py:353)都已完整标注,未发现遗漏。

单测覆盖情况:test_reset_rollout_response_only_clears_fieldstest_release_owned_routed_experts_only_frees_direct_rollout_refstest_discard_trace_store_state_detaches_without_freeing_trace_ref 均走真实代码路径,仅在 ray / free_object_refs 这一项目外边界打桩,覆盖到位;test_postprocess_does_not_free_input_refs_by_default 与更新后的 opt-in 用例覆盖了 release_input_routed_experts 两侧语义。两个新增 Trie 用例使用真实 ray.put ref,但均未覆盖“ref 已导出给 RolloutState 借用”这一关键场景(见 W1)。rl_trainer.pyfinally 兜底正确闭合了异常路径下 session 泄漏的缺口。

抽象与信息隐藏评估

  • Warning xtuner/v1/rl/replay_buffer.py:492-494:调用方重复了 release_owned_routed_experts 内部已封装的 owner 判定,使同一条 ownership 规则散落在两处,削弱了新抽象的信息隐藏并直接导致下文 W3 的行为分歧。

单测建议

  • Warning tests/rl/test_replay_buffer.py:127:154:两个新增用例 mock 了项目内的 release_owned_routed_experts 并只断言调用次数,验证的是被复制的调用方 guard 而非真实释放行为,即使 helper 错误释放了 TraceStore ref 也会通过。

其他 Issues

  • Warning xtuner/v1/rl/rollout/trace_store.py:212-229:覆盖写时的 retained_refs 只能看到 trie 内与替换值中的 ref,看不到已导出给 RolloutState"trace_store" 借用,retry 复用同一 session_id 重新 insert 时会释放兄弟 segment 仍在借用的 ref,等于在第二个位置重新引入本 PR 所修复的故障类型。
  • Warning xtuner/v1/rl/rollout/trace_store.py:217-225:每次覆盖写都会全量遍历 session trie 并对每个 TokenizedSegmentmodel_dump() 深拷贝 token_ids/labels/logprobs,在单线程 store actor 的 on_response 热路径上引入 O(session 总 token 数) 开销。
  • Warning xtuner/v1/data_proto/rl_data.py:241-246:owner 为 None(旧 checkpoint 恢复的样本,其 ref 会被 _restore_nested_objectrefs 重新 ray.put 成活跃对象)时既不释放也被调用方的 == "rollout" 判定跳过,随后被 reset_rollout_response 置空,相比改动前会泄漏该 Ray 对象。

Comment thread xtuner/v1/rl/rollout/trace_store.py Outdated
Comment on lines +212 to +229
old_value = node.value
if old_value is not None and old_value is not value:
# A rerolled turn may overwrite an existing key. Release refs
# that are no longer reachable, while preserving refs shared by
# another trie value or by the replacement value itself.
retained_refs = _collect_ray_ref_keys(value)

def collect_other_values(current: TreeNode) -> None:
if current is not node and current.value is not None:
_collect_ray_ref_keys(current.value, retained_refs)
for child in current.children.values():
collect_other_values(child)

collect_other_values(self.root)
if retained_refs:
_free_ray_refs(old_value, _exclude=retained_refs)
else:
_free_ray_refs(old_value)

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: [正确性] [复杂] 这里的 retained_refs 只统计了 trie 内其他节点与替换值中的 ref,看不到已经导出给 RolloutState 借用的同一批 ref,因此覆盖写会在 session release 之前就 ray.internal.free 掉借用中的对象,等于在第二个位置重新引入了本 PR 要修复的 premature free。

RoutedExperts impact: TraceStore ref 的释放点从“仅 session release”变成“session release + trie 覆盖写”,而后者无法感知 XTuner 侧的借用方。

可复现链路:

  1. agent_in_sandbox_loop.py_build_rollout_states 会从同一个 session_id 展开出多个 segment state,每个都持有 data["routed_experts"] 并标记 owner="trace_store"
  2. replay_buffer.py_apply_staleness_lifecycleexpired_mask 逐条判定,只有过期条目会被 reset/detach,未过期的兄弟 segment 仍在借用这些 ref。
  3. reset_rollout_response 不清除 session_id,retry 会复用同一 session 重新生成,SessionServer.on_response 以相同的 old_prompt / new_prompt 再次 store.insert
  4. 命中本段覆盖写分支后 _free_ray_refs(old_value) 释放旧 ref,而兄弟 segment 仍指向它,后续 trainer 侧 ray.get 会变成对象取不到或 routed-experts/seq-len 校验失败。

建议二选一:覆盖写不做释放,保持 Trie.release 作为唯一释放点;或者在 session 内维护 ref 的引用计数(导出时 +1、session release 时统一归零),只在计数归零时才 free。

Comment thread xtuner/v1/data_proto/rl_data.py Outdated
Comment on lines +241 to +246
if rollout_state.routed_experts_owner != "rollout":
logger.warning(
"Skipping release of routed_experts with unknown owner "
f"{rollout_state.routed_experts_owner!r} (session_id={rollout_state.session_id!r})."
)
return

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: [兼容性] owner is None 会直接 return 而不释放。旧 checkpoint 恢复时 _restore_nested_objectrefsray.put 生成的是真 ref 且 owner 为 None,随后被 reset_rollout_response 丢弃,相对 PR 前反而新增泄漏。建议未知 owner 时按 rollout 处理或 free 后清空。

RoutedExperts impact: 恢复 checkpoint 后过期的 state 其 ref 永久留在 Ray 对象存储。

Comment thread xtuner/v1/rl/replay_buffer.py Outdated
Comment on lines 489 to 494
# Direct rollout refs belong to this state/rollout path;
# TraceStore refs remain borrowed until the session is
# released by TraceStore.
if item.routed_experts_owner == "rollout":
release_owned_routed_experts(item)
reset_rollout_response(item)

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: [设计] owner 判定规则同时写在调用方和 release_owned_routed_experts 内部,属于同一规则散落在多个调用者:helper 已经能识别 "trace_store" 并跳过,这里再加一层 == "rollout" 并没有增加安全性,反而把“未知 owner 怎么办”的策略切成了两半——调用方按“不是 rollout 就不释放”,helper 按“不是 rollout 就 warn 并保留”,于是 owner is None 的 state 两边都不负责,直接导致上面 rl_data.py 的兼容性问题。

建议调用方无条件调用 release_owned_routed_experts(item),把 owner 语义完全收敛到 rl_data.py 一处(信息隐藏),调用方只表达“这条 state 到期了,释放它自己拥有的资源”这一意图。这样以后新增 owner 取值时也只需改一个地方。

RoutedExperts impact: 当前分工使 owner 未知的 state 不被任何一方释放。

Comment thread xtuner/v1/rl/rollout/trace_store.py Outdated
Comment on lines +217 to +225
retained_refs = _collect_ray_ref_keys(value)

def collect_other_values(current: TreeNode) -> None:
if current is not node and current.value is not None:
_collect_ray_ref_keys(current.value, retained_refs)
for child in current.children.values():
collect_other_values(child)

collect_other_values(self.root)

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: [性能] 每次覆盖写都遍历整棵 session trie,且 _collect_ray_ref_keys 对每个 TokenizedSegmentmodel_dump(),会深拷贝全部 token_ids/labels/logprobs,复杂度约 O(session 总 token 数)。这发生在单线程 RolloutTraceStore actor 的 on_response 每轮热路径上。建议只收集 expert_key,或维护 per-session ref 计数。

Comment thread tests/rl/test_replay_buffer.py Outdated
)
stale.routed_experts_owner = "rollout"

with patch("xtuner.v1.rl.replay_buffer.release_owned_routed_experts") as release_refs:

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: [测试] 这两个用例 mock 了项目内的 release_owned_routed_experts,只验证了调用方那层重复的 owner 判断,helper 真实行为(哪些 ref 被 free、routed_experts 是否置空)完全没覆盖——即便 helper 误释放 trace_store ref 也照样通过。建议改为 patch 项目外边界 ray_utils.free_object_refs,断言真实释放结果。

@matrix72c
matrix72c force-pushed the fix/routed-experts-release-ownership branch from d4f0f01 to 4f3da9d Compare September 20, 2026 02:25
@matrix72c

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 4f3da9d:

  • Trie overwrite premature free (W1) + hot-path cost (W2): accepted. Trie.insert no longer frees overwritten refs immediately — the trie cannot see refs still borrowed by sibling RolloutStates. Overwritten values are parked and freed by the session release (Trie.release stays the single release point), deduplicated against the live tree via a shared seen set. This also removes the full-trie scan + model_dump deep copies from the on_response hot path; the overwrite path is now O(1).
  • Owner-guard duplicated at call site (W4): accepted. _apply_staleness_lifecycle now calls release_owned_routed_experts(item) unconditionally; owner semantics live in one place (rl_data.py).
  • Untagged owner leak (W5): accepted with a safer rule. Only "trace_store" is protected now; refs with no owner tag (legacy-checkpoint restores re-ray.put by _restore_nested_objectrefs, no other borrower) are released instead of leaking. We did not adopt "treat unknown as rollout" blindly — borrowing must be explicitly declared.
  • Tests mocked the helper under test (W3): accepted. The replay-buffer cases now patch the outer boundary ray_utils.free_object_refs and assert real release/detach behavior; added an owner=None case. TraceStore cases assert overwrite does not free borrowed refs (ray.get still succeeds) and that session release collects parked refs with cross-tree dedup.

@YifanHe-ailab
YifanHe-ailab self-requested a review September 20, 2026 06:12
if rollout_state.status == Status.FAILED:
error_msg = rollout_state.error_msg
status = rollout_state.status
release_owned_routed_experts(rollout_state)

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.

能否只修改reset_rollout_response 和 discard_rollout_state 内部的行为,这样就不用在每个调用点新增调用 release_owned_routed_experts 了

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

status: Status,
prompt_tokens: int,
completion_tokens: int,
release_input_routed_experts: bool = False,

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.

为什么要增加这个参数,什么情况下 release_input_routed_experts = False

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

handler 无法判断它是本进程产出的还是从 TraceStore 借来的,现在就一个调用方,也可以先简化掉。

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.

我建议这个简化掉,不要增加接口的复杂性,默认就是True的配置就好

@YanhuiDua

Copy link
Copy Markdown
Collaborator

长期设计看,ObjectRef 的生命周期管理仍然过于分散:ReplayBuffer、PartialRolloutHandler、RolloutWorker、TraceStore、TrainerWorker 和 RLTrainer 都需要理解引用所有权并参与清理,reset/discard 还需要通过 release_refs 等参数控制行为,后续很容易继续引入重复释放或泄漏问题。

建议后续将 routed experts 抽象为独立的分布式 RoutedExpertsStore:

  • 每个节点部署 NodeStore,在本地保存实际 routed-experts shard 和对应 ObjectRef;
  • 由统一的 Directory 维护 resource_id -> shard manifest 以及 owner/consumer lease;
  • RolloutState、TraceStore 和 ReplayBuffer 只保存轻量 RoutedExpertsHandle(resource_id, shape, dtype, ...),不直接保存和释放 ObjectRef;
  • rollout 写入后创建 owner lease,训练消费前申请 consumer lease,session/group 完成后释放 owner lease;
  • 当 owner 和 consumer lease 全部释放后,由 Directory 通知各 NodeStore 删除本地 shard;
  • lease 应使用唯一 lease_id 集合维护,而不是裸整数引用计数,以保证 acquire/release 在 Ray task retry 下具备幂等性。

这样可以将所有物理释放操作收敛到 NodeStore,业务组件只管理 Handle 和 lease,并进一步删除 routed_experts_owner、release_owned_routed_experts()、discard_rollout_state(release_refs=...)、TraceStore 递归遍历 ObjectRef,以及 TrainerWorker 直接调用 free_object_refs() 等分散逻辑。

@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 为 RolloutState 引入 routed_experts_owner"rollout" / "trace_store"),把 reset_rollout_response 改为纯状态重置,新增 release_owned_routed_experts()discard_rollout_state(release_refs=...) 显式释放入口,并在 Trie.insert 覆盖写时改为暂存(park)替换值、在 trainer 用 finally 兜底 session 释放。相比上一轮(d4f0f01e),此前 5 条 Warning 均已处理;但 park 策略把原本由 Ray 引用计数自然回收的对象改为长期 pin 住,且 partial rollout 仍自行复刻了一份 owner 判定规则。

ProduceBatchResult impact: not affected —— Status 流转、leftover_* 计数、group_gen_* 计时与 reward 计数均未改动,变更只影响 ref 释放的副作用。

RoutedExperts impact: 本 PR 核心 —— 新增 owner 标记;释放从 reset_rollout_response 移出;trie 覆盖写改为暂存后由 session release 统一释放(见 W1);partial rollout 的释放规则与公共 helper 不一致(见 W2)。

Ray concurrency impact: not affected —— 未改动 @ray.method、装饰器顺序、concurrency_groups 或 actor 构造。

Verdict: REQUEST_CHANGES

Main Flowchart after this PR

flowchart TD
    A[Rollout producer<br/>worker / vllm / verl tool] -->|owner = rollout| S[RolloutState.routed_experts]
    B[TraceStore export_training_trace] -->|owner = trace_store 借用| S
    S --> P{enable_partial_rollout?}
    P -->|是| Q[PartialRolloutHandler.postprocess<br/>release_input_routed_experts=True]
    Q --> Q1[owner == rollout 才释放 history<br/>owner is None 被跳过]
    S --> C{ReplayBuffer<br/>_apply_staleness_lifecycle}
    C -->|retryable| D[release_owned_routed_experts<br/>trace_store 仅 detach,其余释放]
    D --> F[reset_rollout_response<br/>纯状态重置]
    C -->|non-retryable| G[release_and_discard_rollout_groups<br/>discard_rollout_state release_refs=True]
    H[SessionServer.on_response] --> I[Trie.insert 同 key 覆盖写]
    I --> J[新增:_overwritten_values.append<br/>旧值被 actor 长期持有]
    J --> K[Trie.release<br/>session 最终释放点]
    G --> K
    style D fill:#fff3cd,stroke:#d39e00
    style Q1 fill:#f8d7da,stroke:#c82333
    style I fill:#f8d7da,stroke:#c82333
    style J fill:#f8d7da,stroke:#c82333
Loading

核心原理实现与单测

核心原理是“谁创建谁释放”:直接 rollout 产出的 ref 归 "rollout",TraceStore 导出的 ref 仅为借用,最终释放点是 session release。所有直接生产点(worker.py:1279/1287/1312vllm.py:486/495utils.py:219/244agent_loop_verl_tool.py:141)与导出点(agent_in_localhost_loop.py:267agent_in_sandbox_loop.py:353)标注完整,未发现遗漏。

单测方面,上一轮“mock 项目内 helper”的问题已修正:test_retryable_stale_rollout_refs_are_released / ..._trace_store_refs_are_only_detached / ..._unowned_refs_are_released 均通过 _apply_staleness_lifecycle 走真实代码路径,只在项目外边界 ray_utils.free_object_refs 打桩并断言真实释放/detach 结果;test_reset_rollout_response_only_clears_fieldstest_discard_trace_store_state_detaches_without_freeing_trace_ref 覆盖了纯重置与 detach 语义;两个新增 Trie 用例使用真实 ray.put ref,验证了覆盖写不释放与 session release 跨树去重。缺口是 W2 所述的 owner is None history ref 分支:tests/rl/test_rollout_logic.py:1726:1775 只覆盖 "rollout""trace_store" 两种取值。rl_trainer.py:1050-1067finally 正确闭合了异常路径下 session 泄漏的缺口。

抽象与信息隐藏评估

  • Warning xtuner/v1/rl/rollout/utils.py:220-225postprocess 自行复刻了 == "rollout" 判定,与 rl_data.py:234-249 中 helper “非 trace_store 即释放”的策略相反,导致 owner 为 None 的 history ref(旧 checkpoint 经 _restore_nested_objectrefs 重新 ray.put)既不被释放也被拼接结果覆盖而泄漏。

其他 Issues

  • Warning xtuner/v1/rl/rollout/trace_store.py:190-196:覆盖写改为把整个旧 TokenizedSegment 存入 _overwritten_values 后,store actor 会把旧 routed-experts 对象及 token_ids/labels/logprobs 一直 pin 到 session release,reroll 密集的 MoE session 会持续累积对象存储与 actor 内存(改动前丢弃句柄即可由 Ray 引用计数回收,borrower 自持句柄,本就不会提前失效)。

Comment on lines +190 to 196
old_value = node.value
if old_value is not None and old_value is not value:
# A rerolled turn may overwrite an existing key. The old value's
# refs may still be borrowed by RolloutStates, so park the value
# instead of freeing immediately; the session release frees it.
self._overwritten_values.append(old_value)
node.value = value

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: [性能] 改动前 node.value = value 丢弃句柄后,Ray 引用计数即可回收旧对象(borrower 自持句柄,不会提前失效);改为 park 整个 TokenizedSegment 后,actor 会把旧 routed-experts 及 token_ids/labels/logprobs 一直 pin 到 session release,reroll 密集的 MoE session 会持续累积对象存储与 actor 内存。建议恢复丢弃句柄,或只 park expert_key

RoutedExperts impact: TraceStore 侧 ref 的存活期从“覆盖即可回收”延长到“整个 session 生命周期”。

Comment on lines +220 to +225
if release_input_routed_experts:
if history_routed_experts_owner == "rollout":
free_object_refs(
history_routed_experts_ref
if isinstance(history_routed_experts_ref, list)
else [history_routed_experts_ref]

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: [设计] 这里自行复刻了 owner 判定,且与公共 helper 的策略相反:release_owned_routed_experts 是“非 trace_store 即释放”(作者上一轮已明确采纳“未标记即释放”的规则),而此处是“仅 rollout 才释放”。于是 owner 为 None 的 history ref 两边都不负责:这类 ref 真实存在——旧 checkpoint 经 replay_buffer._restore_nested_objectrefs 重新 ray.put 后是活跃对象但字段默认 None,若该 state 为 ABORTED 并走 partial rollout 续跑,第 218 行的拼接结果会直接覆盖 routed_experts,旧对象再无句柄可释放。

建议此处复用同一条规则(改为 != "trace_store",或直接调用 helper 的语义),把 owner 策略收敛到 rl_data.py 一处;同时补一个 routed_experts_owner=None 的 history 用例——现有 tests/rl/test_rollout_logic.py:1726:1775 只覆盖了 "rollout""trace_store"

RoutedExperts impact: 未标记 owner 的 history ref 在 partial rollout 拼接后泄漏在 Ray 对象存储中。

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