执行摘要
- 一句话:class-based rollout 成为默认,旧路径改为环境变量门控
- 推荐动作:值得精读。该 PR 展示了"实验变产品"的典型演进:极性翻转 + fail-fast + adapter 兼容三层保护,是基础设施迁移的教科书式操作。重点关注
miles/utils/arguments.py 的 v1-only 校验模式、inference_rollout_common.py 的 lifecycle 收尾设计,以及 tests/fast/examples/experimental/test_verifiers_run.py 的环境一致性参数化测试。后续可跟进 v1 模块移除的 PR。
功能与动机
PR body 明确指出:MILES_EXPERIMENTAL_ROLLOUT_REFACTOR 自 1 月起就门控着 class-based rollout 路径(#484/#491),而此后新增的 fully-async、snapshot eval、agentic tool calls、nemo-gym、openenv、verifiers 全都依赖它,"The experiment is the product"。因此需要翻转极性,让 InferenceRolloutFn 成为默认,v1 仅通过 MILES_USE_LEGACY_ROLLOUT_V1=1 保留,为后续彻底移除 v1 模块铺路。
实现拆解
1. 极性翻转:环境开关从 opt-in 变为 opt-out
miles/utils/environ.py 用 use_legacy_rollout_v1() 取代 enable_experimental_rollout_refactor(),默认读取 MILES_USE_LEGACY_ROLLOUT_V1 且默认为 0(即默认走 class-based 路径)。所有消费点同步翻转判断:miles/utils/arguments.py 的 resolve_rollout_function_paths 默认返回 miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn;miles/ray/rollout/rollout_manager.py 将 use_experimental_refactor 属性改为 use_legacy_rollout_v1,train/eval 调用点取反;fully_async_rollout.py、inference_rollout_train.py、checkpoint_eval 桩也一并更新。
2. v1-only 特性 fail-fast
_resolve_rollout_functions 和 miles_validate_args 新增两道拦截:--mask-offpolicy-in-partial-rollout(class-based 路径 resume 后不重扩展 loss mask)和 --opd-log-prob-top-k 配合 student-side 策略(需要 v1 generate 才产生的 opd_student_top_logprobs)在参数校验阶段直接 raise ValueError,提示设置 MILES_USE_LEGACY_ROLLOUT_V1=1,而不是在 run 中途深埋报错。
3. 补齐默认路径的观测缺口(review 驱动)
miles/rollout/inference_rollout/inference_rollout_common.py 的 generate_and_rm 引入 TrajectoryLifecycle 事件(attempt_start / gen_start / attempt_end),用 try/finally 保证异常与 sibling 取消时也收尾;同时支持 per-sample generate_function_path 覆盖。miles/rollout/sglang_rollout.py 的私有函数 _sample_text_preview / _reward_log_summary / _len_or_value 被提升为 generate_utils/sample_utils.py 的共享 sample_text_preview / reward_log_summary,并在 class-based 与 fully-async 驱动中复用,避免长上下文与 OPD 大 reward 刷爆 Ray 日志。
4. 适配层与示例修复
load_rollout_function / load_generate_function 的 LegacyRolloutFnAdapter / LegacyGenerateFnAdapter 保持不变,保证旧自定义函数在默认路径下继续工作;examples/experimental/verifiers/run.py 改为根据同一份 effective env(含 resolve_extra_env_vars 合并后的结果)选择 adapter 与 Ray runtime env,消除 launcher 读 os.environ 与 runtime env 不一致的隐患;miles/utils/external_utils/command_utils.py 抽出 resolve_extra_env_vars 公共函数。
5. 清理 opt-in 与配套
14 个 launcher/example 删除 =1;30 个 e2e 测试去掉 opt-in;tests/conftest.py 的 autouse fixture 从 enable 改为清除 ambient MILES_USE_LEGACY_ROLLOUT_V1(幂等防护);6 个 LoRA snapshot 从记录 runtime env 移除该变量、2 个 OPD shell snapshot 增加 pin;tests/fast/examples/experimental/test_verifiers_run.py 新增参数化测试覆盖 ambient / extra_env_vars 四种组合;generate-endpoint.md、customization.md 等 10 个文档页面重写,将默认路径表述对齐为 class-based。
关键文件:
miles/utils/arguments.py(模块 参数校验;类别 source;类型 dependency-wiring;符号 resolve_rollout_function_paths, _resolve_rollout_functions, miles_validate_args): 默认路径选择与 v1-only 特性 fail-fast 的核心决策点,直接决定 rollout 函数解析与参数校验行为
miles/utils/environ.py(模块 环境开关;类别 source;类型 core-logic;符号 use_legacy_rollout_v1): 环境开关极性翻转的源头,旧 flag 被移除、新 flag 语义反转为 opt-out
miles/rollout/inference_rollout/inference_rollout_common.py(模块 轨迹引擎;类别 source;类型 core-logic;符号 generate_and_rm): class-based 路径成为默认后补齐 dashboard 生命周期事件与 per-sample generate override,是 review 驱动的关键补强
miles/ray/rollout/rollout_manager.py(模块 调度器;类别 source;类型 dependency-wiring;符号 RolloutManager): RolloutManager 的 train/eval/loader 三处调用点随开关翻转,是调度侧的主消费方
miles/rollout/sglang_rollout.py(模块 旧版路径;类别 source;类型 core-logic;符号 generate_rollout): v1 路径随翻转被降级为 legacy,本地日志辅助函数迁移到共享 sample_utils
miles/rollout/generate_utils/sample_utils.py(模块 采样工具;类别 source;类型 core-logic;符号 sample_text_preview, reward_log_summary, _len_or_value): 样本预览与 reward 摘要从 v1 专用提升为两条路径共享的工具,防止 class-based 默认路径日志轰炸
tests/fast/rollout/inference_rollout/test_lifecycle_attempt.py(模块 生命周期;类别 test;类型 test-coverage;符号 RecordingSink, test_attempt_ends_once_after_success, test_attempt_ends_once_after_abort, test_attempt_ends_when_generate_raises): 新增生命周期事件顺序测试,覆盖成功/abort/异常/sibling 取消四种路径,锁定 try/finally 收尾行为
tests/fast/examples/experimental/test_verifiers_run.py(模块 验证器;类别 test;类型 test-coverage;符号 test_adapter_and_ray_runtime_use_the_same_legacy_flag): 新增参数化测试验证 launcher 的 adapter 选择与 Ray runtime env 使用同一份 legacy flag,锁定 #2528 修复
examples/experimental/verifiers/run.py(模块 验证器;类别 source;类型 core-logic;符号 execute): 修复 adapter 选择读 os.environ 与 Ray runtime env 不一致的隐患,改为基于合并后的 effective env 决策
miles/utils/external_utils/command_utils.py(模块 启动命令;类别 source;类型 core-logic;符号 resolve_extra_env_vars): 抽出 resolve_extra_env_vars 供 verifiers 示例复用,避免启动命令与 runtime env 拼接逻辑分叉
关键符号:use_legacy_rollout_v1, resolve_rollout_function_paths, _resolve_rollout_functions, miles_validate_args, generate_and_rm, sample_text_preview, reward_log_summary, resolve_extra_env_vars, execute
关键源码片段
miles/utils/arguments.py
默认路径选择与 v1-only 特性 fail-fast 的核心决策点,直接决定 rollout 函数解析与参数校验行为
# miles/utils/arguments.py — 默认路径选择与 v1-only 特性 fail-fast
def resolve_rollout_function_paths(args) -> tuple[str, str]:
"""返回 (rollout, eval) 函数路径。默认 class-based,legacy 需显式 opt-in。"""
if use_legacy_rollout_v1():
# v1 旧路径:`MILES_USE_LEGACY_ROLLOUT_V1=1` 时保留的 sglang_rollout
standard_path = "miles.rollout.sglang_rollout.generate_rollout"
else:
# 新默认:class-based `InferenceRolloutFn`
standard_path = "miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn"
rollout_path = args.rollout_function_path or standard_path
if args.fully_async:
# 完全异步只存在于 class-based 世界
rollout_path = "miles.rollout.fully_async_rollout.FullyAsyncRolloutFn"
...
def _resolve_rollout_functions(args) -> None:
# v1-only 特性在新默认路径上 fail-fast,而不是在 run 中途深埋报错;
# 提示用户设置 `MILES_USE_LEGACY_ROLLOUT_V1=1` 回退旧路径
if args.partial_rollout and args.mask_offpolicy_in_partial_rollout and not use_legacy_rollout_v1():
raise ValueError(
"--mask-offpolicy-in-partial-rollout does not re-extend the loss mask on the "
"class-based rollout path yet; set MILES_USE_LEGACY_ROLLOUT_V1=1"
)
if args.fully_async:
assert (
not use_legacy_rollout_v1()
), "--fully-async needs the class-based rollout API; unset MILES_USE_LEGACY_ROLLOUT_V1"
...
miles/utils/environ.py
环境开关极性翻转的源头,旧 flag 被移除、新 flag 语义反转为 opt-out
# miles/utils/environ.py — 环境开关极性翻转的核心
import os
_printed_legacy_rollout_v1 = False
def use_legacy_rollout_v1() -> bool:
# 默认返回 False:所有任务默认走 class-based `InferenceRolloutFn`;
# 只有显式设置 `MILES_USE_LEGACY_ROLLOUT_V1=1` 才回退到 v1 `sglang_rollout` 路径
result = bool(int(os.environ.get("MILES_USE_LEGACY_ROLLOUT_V1", "0")))
global _printed_legacy_rollout_v1
if result and not _printed_legacy_rollout_v1:
# 只在首次命中时打一条提示,避免每个进程都刷屏
print("MILES_USE_LEGACY_ROLLOUT_V1=1 is enabled: using the deprecated v1 rollout path")
_printed_legacy_rollout_v1 = True
return result
miles/rollout/inference_rollout/inference_rollout_common.py
class-based 路径成为默认后补齐 dashboard 生命周期事件与 per-sample generate override,是 review 驱动的关键补强
# miles/rollout/inference_rollout/inference_rollout_common.py — 新默认路径的生命周期收尾
async def generate_and_rm(
state: GenerateState,
sample: Sample | list[Sample],
sampling_params: dict[str, Any],
evaluation: bool = False,
) -> Sample | list[Sample]:
args = state.args
# partial rollout 时,按 loss mask 屏蔽上一轮 off-policy 片段
if args.partial_rollout and args.mask_offpolicy_in_partial_rollout and sample.response_length > 0:
sample.loss_mask = [0] * sample.response_length
# 已有完整回复的样本直接返回,不再重复生成
if sample.status in (Sample.Status.COMPLETED, Sample.Status.TRUNCATED):
assert sample.response is not None
if not args.group_rm:
assert sample.reward is not None
return sample
# dashboard 生命周期探针:信号量等待即“排队”时间;
# attempt_start 在排队前发出,attempt_end 在 reward 计算前发出
sink = None if evaluation else TrajectoryLifecycle().sink
if sink is not None:
sink.attempt_start(sample)
try:
async with state.generate_fn_semaphore:
if state.aborted:
# 已终止的 rollout 不再触发 generate
sample.status = Sample.Status.ABORTED
return sample
if sink is not None:
sink.gen_start(sample)
# per-sample override:eval dataset 可自行指定 generate function
generate_fn = load_generate_function(sample.generate_function_path) or state.generate_function
output = await generate_fn(
GenerateFnInput(
state=state,
sample=sample,
sampling_params=deepcopy(sampling_params),
evaluation=evaluation,
)
)
sample = output.samples
finally:
# 无论成功、抛异常还是被 sibling 取消,都必须 attempt_end,
# 否则 dashboard 会认为该轨迹一直 running
if sink is not None:
sink.attempt_end(sample)
...
评论区精华
guapisolo 在 review 中提出 4 条 [P2] 意见,全部在合入前解决:
-
新默认路径缺失 TrajectoryLifecycle 事件(design,涉及 inference_rollout_common.generate_and_rm):"The legacy path emits attempt_start, gen_start, and attempt_end around the semaphore/generate call, and the dashboard consumes those events for its per-sample queue/generation timeline. Consequently, --use-miles-dashboard jobs on the new default path have an empty trajectory stream even though generation succeeds." 已通过 a6e305d 提交在 class-based 路径补齐生命周期探针。
-
日志截断与 reward 摘要缺失(performance):"inference_rollout_train.py interpolates the complete prompt, response, and raw reward... every rollout can now send very large messages through Ray log forwarding and storage." 已通过 6c21659 提交将 v1 的预览/摘要逻辑共享到 sample_utils.py。
-
verifiers launcher 与 Ray runtime env 不一致(correctness):"That can select VerifiersRolloutFn while RolloutManager uses the v1 ABI, raising TypeError on the first rollout. I put the root-cause fix in #2528." 已通过 75050be 提交合入 #2528 的修复,并由新增参数化测试锁定。
-
lifecycle 在 failure/cancellation 未 close(correctness):"Both paths skip the normal-path attempt_end()... the dashboard can leave an already-finished trajectory in the running state indefinitely." 已通过 e2b289a 提交(#2534)用 try/finally 保证 attempt_end 兜底。
- 新默认路径缺失 TrajectoryLifecycle 事件导致 dashboard 轨迹流为空 (design): 在 class-based 路径补上相同的生命周期插桩,同时避免专用生成器产生重复 per-turn span。已通过 a6e305d 提交实现,并由新增 test_lifecycle_attempt.py 锁定。
- 新默认路径的 first/finish 日志未截断,长上下文与 OPD 大 reward 会刷爆 Ray 日志 (performance): 将 legacy 的预览/摘要格式提升为
sample_utils.py 共享函数并应用到 class-based 与 fully-async 驱动。已通过 6c21659 提交解决。
- verifiers launcher 的 os.environ 与 Ray runtime env 不一致会选中错误 adapter 导致 TypeError (correctness): 根因修复在 #2528:adapter 选择与传播的 flag 使用同一份 effective env,CLI 覆盖优先于 ambient 值。已通过 75050be 提交合入本分支,并由新增参数化测试锁定。
- generate 抛异常或 sibling 取消时 lifecycle attempt 未关闭 (correctness): 从异常/取消清理路径统一发出
attempt_end() 并保留原始异常。已通过 e2b289a 提交(#2534)用 try/finally 实现。
风险与影响
-
风险:
- 约 30 个 e2e 测试静默切换路径:这些纯 GRPO/SFT 任务原先走 v1,现在默认走 class-based。PR body 列出了三处刻意差异——per-group task 异常改为记录并重提交(不再崩溃)、abort fan-out 失败变为 fatal、eval prompt cache 从进程级改为实例级——首轮 CI 需要观察这些行为变化是否影响训练语义。
- 环境变量极性反转的兼容性:旧的
MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1 已被删除,任何仍在设置该变量的脚本/文档会静默失效;反之 ambient MILES_USE_LEGACY_ROLLOUT_V1=1 会污染本地开发与 snapshot 录制,已通过 tests/conftest.py 的 CLEARED_ENV 防护,但外部用户的遗留 shell 环境仍可能误切换到 v1。
- 可观测性盲区:dashboard 的轨迹事件只由 class-based 路径发出;若用户 opt-in v1,dashboard 轨迹流将为空(v1 侧未补 sink)。这是门控翻转带来的新的功能不对称。
- v1-only 特性未被全覆盖:audio_data payload、per-eval-dataset
custom_generate_function_path、kimi-style call_processor 多模态 prompt 等 gap 无 in-repo 消费者,未做 fail-fast,遇到才会暴露。
- 影响:用户与任务:所有未显式设置 legacy 变量的 rollout 任务默认切换到 InferenceRolloutFn,但 Legacy adapter 保证旧 --rollout-function-path / --custom-generate-function-path 调用约定不变,sft_rollout、random_async、multi-LoRA 注入函数不受影响。
系统与团队:这是 v1 移除计划的前置里程碑,MILES_USE_LEGACY_ROLLOUT_V1 为迁移窗口期的安全网;CI 中 test_qwen3_0.6B_verifiers 成为唯一显式 legacy pin,维持 v1 覆盖直到路径删除。
范围:84 个文件、443 个 launch-script snapshot 测试全绿,10 个文档页面同步更新,对团队文档维护和 example 演进影响较大。
-
风险标记:默认路径切换, 84 文件联动, 环境变量极性反转, 30 个 e2e 静默改道, 可观测性行为差异
关联脉络
- PR #2528 fix(verifiers): align adapter with Ray runtime env: 本 PR 分支合入的修复,解决 verifiers launcher 的 adapter 选择与 Ray runtime env 使用不同环境变量导致 TypeError 的问题
- PR #2534 fix(rollout): finalize lifecycle attempts on failure: 本 PR 分支合入的修复,用 try/finally 保证 generate 失败或取消时 attempt_end 仍触发
- PR #2531 fix(flops): stop assuming every HF config has intermediate_size: 同属 rollout/FSDP 训练链路收敛,与 class-based 默认路径切换共同推进 megatron/FSDP 路径的稳定性
- PR #2536 Carry the rollout id on both agentic paths: 同为 rollout 路径收敛(agentic v1/v2 rollout_id 一致性),与本 PR 的默认路径翻转互为铺垫
- PR #2485 clean up fully async example and mv to examples/infra_features: fully_async 示例重构依赖 class-based 成为默认,本 PR 后 fully_async 不再需要 opt-in 环境变量
- PR #2484 docs: add disaggregated RL rollout guide: 分离式 rollout 文档基于 class-based 路径编写,与本 PR 的文档默认路径对齐工作直接相关
- PR #2498 fix: require shared rewards within rollouts: rollout 数据转换的奖励一致性修复,同属 rollout 默认路径的服务质量保障
- PR #2219 [feat] Add training log-prob reuse to skip the redundant forward-only pass: megatron 训练侧 log-prob 复用改动,与本 PR 共同构成 rollout+训练主路径的演进
参与讨论