Skip to content

【No.9】新增 Agentic Rollout 停止原因分布与轮数分位数指标 - #325

Closed
ldemon2333 wants to merge 1 commit into
redai-studio:mainfrom
ldemon2333:feat/task9
Closed

ldemon2333 wants to merge 1 commit into
redai-studio:mainfrom
ldemon2333:feat/task9

Conversation

@ldemon2333

@ldemon2333 ldemon2333 commented Sep 17, 2026 •

Copy link
Copy Markdown

关联 issue

关联 issue:【No.9】Agentic Rollout 停止原因分布与轮数分位数指标。

变更内容

为 rollout 样本指标新增停止原因分布和轮数分位数,并保留现有 num_turn/min、num_turn/mean、num_turn/max 指标。聚合时把动态错误明细归入固定类别,避免异常原文、URL、请求 ID 不断生成新的监控指标键。

停止原因按类别分别统计 count 和 ratio。聚合函数返回的键为:

  • stop_reason/{reason}/count:当前批次中该原因的样本数。
  • stop_reason/{reason}/ratio:该原因的样本数除以当前批次全部有效样本数;非空批次的所有原因占比之和为 1。

例如一个批次共有 100 条样本,其中 env_done 72 条、max_turns 18 条、缺少有效停止原因 10 条,则聚合结果包含:

stop_reason/env_done/count = 72
stop_reason/env_done/ratio = 0.72
stop_reason/max_turns/count = 18
stop_reason/max_turns/ratio = 0.18
stop_reason/unknown/count = 10
stop_reason/unknown/ratio = 0.10

经现有训练日志入口上报后,键会增加 rollout/ 前缀,例如 rollout/stop_reason/env_done/count;评估入口则增加 eval/{dataset}/ 前缀。

样本元数据中的停止原因仍由各 rollout 或 Agent 应用写入;指标出口统一归一化为以下 13 个固定类别。每个非空批次输出全部类别的 count 和 ratio,未出现类别为 0,因此停止原因指标始终为 26 个键。空批次返回 {}。

类别 含义与当前来源
env_done 环境已结束,见 DeepEyes、DeepEyes Agentic、WebShop、ALFWorld、Search QA 等流程。
max_turns 达到 Agent 或环境允许的最大轮数。
finish_length 单次生成因长度限制结束。
budget_exhausted 旧 DeepEyes rollout 的上下文或生成预算耗尽。
finish_abort 旧 DeepEyes rollout 的生成请求被中止。
completed 旧 DeepEyes rollout 未命中更具体原因时的正常完成兜底。
context_exhausted ALFWorld Agent 的累计上下文预算耗尽。
llm_unavailable Search QA Agent 的模型请求在重试后仍不可用。
discarded_by_pipeline DeepEyes V2 Agentic 会话在流水线目标关闭后被丢弃。
format_error DeepEyes V2 Agentic 输出格式不符合约定。
env_error 原因值为 env_error 或以 env_error: 开头;只保留类别,错误明细仍留在原始元数据中。
other 已提供非空原因字符串,但不属于固定类别,例如自定义原因、大小写不同的 Env_Done。
unknown 本 PR 的聚合兜底:rollout_stop_reason 和 stop_reason 均缺失、为空或不是有效字符串。

同时新增 num_turn/p50、num_turn/p90、num_turn/p95、num_turn/p99,用于观察轨迹轮数分布和长尾。

实现方式

  • 以纯函数聚合样本元数据,不修改输入、不保留跨批次状态。优先读取 rollout_stop_reason,兼容现有 Agentic 的 stop_reason 字段;缺失或空原因归入 unknown。
  • 对传入指标入口的每条样本计数,包括缺少奖励的样本。所有原因占比均使用同一个输入样本总数作为分母。
  • 从 rollout_turns 读取轮数,采用线性插值计算分位数;缺失或非法值沿用单轮默认值 1。
  • 空输入返回空指标字典;现有部分轨迹续跑的模型版本差统计兼容 metadata=None。
  • 复用 rollout/ 与 eval/{dataset}/ 上报路径,保持 rollout 调度和停止条件不变。

停止原因字段的兼容差异

来源 当前写入方式 本 PR 的处理
旧 DeepEyes rollout examples/deepeyes/rollout.py 第 432 行 写入 rollout_stop_reason 和 rollout_turns 优先读取 rollout_stop_reason
DeepEyes Agentic 应用 examples/deepeyes_agentic/app/agent.py 第 66 行 返回 metadata.stop_reason 和 metadata.num_turn 主字段无有效值时兼容读取 stop_reason;轮数仍读 rollout_turns
通用 SessionForest 导出 relax/agentic/session/state.py 第 743 行 合并已有元数据,写入轨迹记录与 rollout_turns,没有统一生成 rollout_stop_reason 两个原因字段均无有效值时计入 unknown

Agent 进程输出的元数据经 session/service.py 第 2745 行的导出入口 和 第 2832 行的构样本逻辑 合入样本。因此只读取任务建议的 rollout_stop_reason 会漏掉现有 Agentic 示例的停止原因。聚合端兼容读取已有字段即可补齐。

DeepEyes Agentic 的 metadata.num_turn 来自 env.turn,与导出轨迹中已提交的模型响应节点数可能不同。例如生成因长度限制结束时,应用会在调用 env.step 前退出。因此本 PR 不将 metadata.num_turn 作为轮数回退来源,统一使用 SessionForest 导出的 rollout_turns;该字段缺失或无效时沿用默认值 1。

测试与验收

验收场景与测试函数对应

以下 15 个场景对应 tests/distributed/ray/test_rollout_metadata_metrics.py 中的实际断言。部分场景共用同一个测试函数;指标测试文件现有 12 个函数,经参数化展开为 80 个用例。

场景 实际验证的验收标准 对应测试函数
空输入 辅助函数与总入口均返回 {};将 RuntimeWarning 视为错误时仍通过。空批次下总入口允许 args=None。 test_rollout_metadata_empty_input
单条样本 所选原因的 count 和 ratio 均为 1,其余 12 类均为 0;输入轮数为 3 时,七个轮数指标均为 3。 test_rollout_metadata_stop_reason_precedence
多类别样本 四样本示例的计数为 2/1/1,占比为 0.5/0.25/0.25,P50/P90/P95/P99 为 1.5/3.4/3.7/3.94;预期值使用明确常量。 test_rollout_metadata_distribution_and_percentiles
缺失元数据 None、{} 和字段缺失均得到 unknown/ratio = 1,轮数统计为 1;num_turn 和轨迹记录中的 turn_count 不替代 rollout_turns,并验证总入口。 test_rollout_metadata_missing_fields
原因兼容 主字段优先,无效时回退到 stop_reason;去除首尾空白,非字符串或缺失值使用有效回退值或 unknown。显式 unknown 阻止回退,未知有效主字段归入 other;精确验证 env_error: 合并及相似前缀、协议原因值归入 other。 test_rollout_metadata_stop_reason_precedence;test_rollout_metadata_missing_fields
异常轮数 接受零、Python/NumPy 整数及整数浮点数;布尔值、字符串、小数、负数、NaN、无穷和容器默认使用 1,不触发数值统计警告。 test_rollout_metadata_turn_normalization
分母独立 共用四样本数据混合缺失奖励与 COMPLETED、PENDING、TRUNCATED、ABORTED 状态;直接聚合和总入口均对每条输入计数一次。 test_rollout_metadata_distribution_and_percentiles;test_rollout_metadata_total_metrics_preserve_existing_statistics
纯函数 聚合前后深拷贝相等,元数据对象身份不变;重复调用和颠倒输入顺序得到一致数值,连续批次的键集合相同;此前出现的原因在下一批没有样本时计数和占比重置为 0。 test_rollout_metadata_is_read_only_and_batch_local
聚合恒等式 原因计数和等于输入样本数,占比和在浮点容差内等于 1;七个轮数统计均为有限 Python 标量,原因指标为 Python 浮点数,原因键有序,固定 13 类共 26 键。 test_rollout_metadata_distribution_and_percentiles
总入口回归 新指标及旧轮数最小值、均值、最大值符合预期常量;所检查的奖励、截断率、响应长度、前缀缓存和图像数量指标保持预期值,样本不变。 test_rollout_metadata_total_metrics_preserve_existing_statistics
部分轨迹续跑模式下缺失元数据 启用部分轨迹续跑且 rollout_id=7 时,metadata=None 对应版本差 0;另一条起始版本为 5 的样本使版本差均值/最小值/最大值为 1/0/2,批量大小为 2,输入不变。 test_rollout_metadata_partial_staleness_missing_metadata
上报入口 训练和评估各覆盖普通、动态原因输入,执行真实总入口与日志函数;最终原因键集合严格等于加前缀的 26 个固定键,数值、步骤、刷新及训练性能指标正确。 test_rollout_metadata_log_publication(4 个用例)
SessionForest 导出 SessionOutput.from_payload 读入原因后,一轮和三轮 SessionForest 导出分别覆盖已知、动态错误及未知原因,指标归一化且原文保留;既有用例验证轮数与元数据透传。 test_rollout_metadata_session_forest_export(turn_count=1/3);test_session_forest_build_sample_and_session_spec
固定类别保留 逐一验证 13 个固定类别原样计数,计数和与占比和均为 1。 test_rollout_metadata_known_reasons_preserved
动态键有界性 100 个不同 URL/ID 错误、100 个不同万字符自定义原因和 2 条缺失原因,经真实总入口只生成 26 个键;合并计数为 100/100/2,比例正确,输入不变。 test_rollout_metadata_dynamic_reasons_have_bounded_keys

验证结果与范围

使用 uv 虚拟环境执行 CPU 验证,环境为 Python 3.12、NumPy 1.26.4、PyTorch 2.10.0+cpu、Ray 2.58.0:

python -m pytest -q \
  tests/distributed/ray/test_rollout_metadata_metrics.py \
  tests/test_agentic_rollout.py \
  tests/utils/test_multimodal_rollout_stats.py \
  tests/utils/test_metric_utils_rloo.py \
  --tb=short --disable-warnings -rs

125 项通过。

同一个有界键回归测试在修复前提交 6891f0f 上失败,输出含 URL 和动态错误原文;在修复后通过。全部 pre-commit 检查在与提交内容一致的隔离工作树中通过。

  • 在包含提交内容的隔离工作树中执行 pre-commit run --all-files,全部检查通过。
  • 上述针对性测试通过;未执行全仓库测试套件。
  • 回归测试覆盖空输入、字段归一化、分母口径、输入不变性、SessionForest 导出及训练和评估上报。

变更类型

  • 新功能:增加可兼容现有使用方式的指标。
  • 缺陷修复:限制停止原因指标基数,缺席类别补零,兼容空输入和缺失元数据。

补充:DeepEyes V2 动态停止原因的指标键问题

Bug 内容

DeepEyes V2 示例的 agent.py:229 有一条错误结束分支,会把错误原文拼进停止原因:

if obs.done:
    stop_reason = "env_done" if obs.error is None else f"env_error:{obs.error}"

该值通过 metadata.stop_reason 返回,经 Session 导出合入样本。修复前,公共聚合函数把原因原文直接拼成 stop_reason/{reason}/count|ratio,于是同一类错误的不同消息、URL、请求 ID 都变成不同指标。

以两条终止错误样本为例:

# 修复前:同类错误按明细拆成不同指标,每个指标的 count 都是 1
rollout/stop_reason/env_error:timeout request/42/count
rollout/stop_reason/env_error:timeout request/43/count

# 修复后:合并为同一类别
rollout/stop_reason/env_error/count = 2
rollout/stop_reason/env_error/ratio = 2 / 本批样本数

在什么情况下触发

DeepEyes V2 上述分支需要同时满足:obs.done=True、obs.error 含错误信息,并且该轨迹成功导出样本、进入指标汇总。错误明细随请求变化时,修复前便会持续新增指标名称。

当前普通搜索异常不会直接触发该分支。 env_deepeyes_v2.py:283 虽然写入 error=str(exc),但返回 done=False;当前工具处理函数也均返回 done=False。因此,不能声称现有示例发生一次搜索超时就会污染停止原因指标。该示例的动态拼接分支在环境返回“带错误的终止观察”时才会执行;其他自定义 Agent 直接返回动态 metadata.stop_reason 也能触发同一问题。

这里由 DeepEyes V2 的错误原因拼接方式暴露出公共聚合入口缺少限制。已复现的是“动态原因进入指标键”的公共路径,尚无 DeepEyes V2 搜索异常贯通到监控服务的线上复现记录。

影响范围与规模

  • 范围:所有使用该公共汇总函数的训练、评估和自定义 rollout 都可能受影响,不局限于 DeepEyes V2;只统计实际进入汇总的样本,未导出或此前已丢弃的任务不计入。
  • 规模:修复前,本批有 $D$ 个不同原因就生成 $2D$ 个原因指标。不同异常原文跨批次不断出现时,指标名称集合没有固定上限。回归用例的 202 条样本包含 201 个不同原因,旧逻辑会生成 402 个原因键,修复后固定为 26 个。
  • 后果:同类错误的趋势被拆散,长指标名增加日志和监控存储、查询负担;原因中的 / 混入分组路径,异常原文可能随指标名称传到监控后端。
  • 边界:属于 P2 可观测性问题。未发现其改变梯度、奖励、样本内容或停止流程;本次未测得线上监控性能下降比例,也未将其认定为 GPU 故障。

如何修改

修复位于公共聚合函数 _compute_rollout_metadata_stats,不修改 DeepEyes V2 的环境执行或停止条件:

原因输入 指标类别
白名单内的固定原因 保留原类别
env_error、env_error:<任意明细> 统一为 env_error
其他非空原因字符串 other
主、次字段均缺失、空白或非字符串 unknown

保留 rollout_stop_reason 优先、stop_reason 回退的字段选择顺序,再执行类别归一化。每个非空批次为 13 个固定类别输出共 26 个计数/占比键,缺席类别补零;空输入仍返回 {}。原始元数据不变,错误明细仍可用于排查。

@rai-studio-bot

rai-studio-bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Nyanpasu 审查看板

审查状态: ✅ 已通过

审查版本: 22f0a9fb8a23b54321afac478a80d967c17716a5

已复审 PR #325 修订 22f0a9f。新增 rollout 停止原因计数/占比与 turn 分位数指标,并修复空输入与 metadata=None 两处健壮性问题。作者的修复以 13 类白名单归一化停止原因:env_error:<明细> 收敛为 env_error,未登记值归入 other,每批次固定输出 26 个键并补零;已独立验证该逻辑及白名单对全部真实生产者的覆盖。证据更正:我此前称 deepeyes_v2_agentic 的 env_error:<明细> 链路可复现,经按 AST 复核该示例全部 12 处 ToolObs 构造均为 done=False,agent.py:230 的该分支不可达,故撤回“仓库内可复现”的表述;防御性归一化仍然有效,自定义 Agent 写入动态 metadata.stop_reason 的通用风险依然成立。无遗留问题,批准合并。

编号 问题 优先级 状态 规则来源
F1 P2:stop_reason 原始字符串直接作指标 key(具名链路经复核不可达,通用风险仍成立;已由白名单归一化修复) P2 ✅ 已解决 —
Powered by Nyanpasu with deepseek-v4.1-flash-ali medium, please check the suggestions carefully.

@rai-studio-bot rai-studio-bot left a comment

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.

整体评估:本次改动为纯增量,且附带两处合理的健壮性修复——空样本提前返回 {}(两个调用点 _log_rollout_data/_log_eval_rollout_data 均已做保护),以及 partial-rollout staleness 计算容忍 metadata=None。stop-reason 优先级、turn 归一化与分位数逻辑清晰且测试充分(48 项新指标测试覆盖空输入、归一化、分母语义、只读性与训练/评估发布)。zip(strict=True) 与 int | None 均满足仓库 >=3.10 要求,无兼容性问题。

一项非阻塞建议见行内 P3:stop_reason 以原始字符串直接作为指标 key,建议归一化以避免无界的指标命名空间。不阻塞合并。

Powered by Nyanpasu with claude-opus-5-google[1m] medium, please check the suggestions carefully.

Comment on lines +4922 to +4924
for reason, count in sorted(reason_counts.items()):
metrics[f"stop_reason/{reason}/count"] = float(count)
metrics[f"stop_reason/{reason}/ratio"] = count / len(samples)

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.

P3 优先级:P3

stop_reason 直接以原始字符串作为指标 key(stop_reason/{reason}/count|ratio)。当前 Agentic 路径的 stop_reason 是有界枚举(end_turn/max_tokens/tool_use),但该函数对任意字符串来者不拒——测试用例中的 env_error:detail/part 即说明 reason 可能携带明细。若某些环境把可变明细(错误信息、ID 等)写入 stop_reason,会在 wandb 等后端产生无界增长的指标 key,且含 / 时还会被解析成嵌套路径。

建议对 reason 做归一化/白名单(例如仅保留已知枚举、其余归入 other,或截断到首个分隔符),以避免指标命名空间膨胀。非阻塞。

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.

补充证据:该风险在仓库内已有具体触发链,特此从"潜在"升级为"可复现"。

  1. examples/deepeyes_v2_agentic/app/env_deepeyes_v2.py:288 在搜索异常时写入 error=str(exc)(任意异常消息,非枚举)。
  2. examples/deepeyes_v2_agentic/app/agent.py:230 将其插值为 stop_reason = f"env_error:{obs.error}"。
  3. 同文件 agent.py:239 通过 metadata.stop_reason 返回,经 session 导出合入样本元数据(即本 PR 描述中引用的 relax/agentic/session/service.py 导出链路)。

因此本 PR 会生成形如 stop_reason/env_error:<异常原文>/count 的指标 key:异常消息长度与内容不可控,且常含空格、:、URL 等字符,在 wandb 等后端会产生无界且难以阅读的嵌套指标名。该路径在搜索/网络类异常下即可触发。

依据本仓库既有惯例,建议按白名单归一化——examples/on_policy_distillation/agentic_opd/alfworld/reward_alfworld.py:33 的 STOP_REASONS = ("env_done", "max_turns", "finish_length", "context_exhausted") 即为同类做法,且它对白名单内所有原因每步都输出(缺失为 0),时间序列不会出现缺口。可考虑保留 env_error 类别本身、丢弃其后可变明细,其余未知值归入 other。

优先级调整为 P2(可复现的可操作边界问题),仍不阻塞合并。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

已修复,针对 deepeyes_v2_agentic 例子,env_error: 统一为 env_error

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.

核对确认:你的判断是对的,我此前"该路径在搜索/网络类异常下即可触发"的表述不成立,特此更正。

我按 AST 逐一检查了 examples/deepeyes_v2_agentic/app/env_deepeyes_v2.py 中全部 12 处 ToolObs(...) 构造:全部为 done=False,没有通过位置参数传入 done,也不存在对 obs.error 的后续赋值;agent.py 自身不构造 ToolObs。因此在 agent.py:230 的 if obs.done: 分支内 obs.error 恒为 None,f"env_error:{obs.error}" 当前是不可达分支——搜索超时等异常(返回 done=False)不会产生 env_error:<明细>。

结论修正如下:

  • 上述具体链路不可复现,我 P2 证据中"仓库内可复现"的部分予以撤回,优先级依据(P2)相应失效。
  • 但"外部 Agent 自行写入动态 metadata.stop_reason 会使指标键无界"这一前提仍然成立(正如你在 PR 描述中所说明的)。因此本次白名单归一化仍是有效的防御性修复,我对此修复的认可不变。

感谢补充这一细节,更正后的证据记录不影响 PR 结论(已批准)。

@ldemon2333 ldemon2333 changed the title feat(rollout): add stop reasons and turn percentiles 【No.9】新增 Agentic Rollout 停止原因分布与轮数分位数指标 Sep 17, 2026
# ⭐ Feature

- Aggregate stop-reason counts and ratios over all input samples.
- Add turn-count P50/P90/P95/P99 while retaining min/mean/max.
- Handle empty batches and missing metadata without changing rollout control flow.

---

# 🐛 Bug Fix

- Bound stop-reason metrics to 13 categories and emit zeros for absent categories.
- Fold env_error details into env_error and route unrecognized reasons to other.
- Preserve raw sample metadata and primary-field precedence during normalization.

---

# ✅ Tests

- Cover long dynamic reasons, bounded keys, zero filling, SessionOutput exports, and logging.
- Validate 125 CPU tests; one existing Megatron-dependent test is skipped.
@ldemon2333

Copy link
Copy Markdown
Author

@SigureMo 任务已完成,请求 review。

@SigureMo

Copy link
Copy Markdown
Member

没有成功报名第 9 题哦,目前成功报名的是 3、4 两题

@ldemon2333

Copy link
Copy Markdown
Author

没有成功报名第 9 题哦,目前成功报名的是 3、4 两题

哦哦,没事,就先放在这里好了,给其他同学参考

@rai-studio-bot rai-studio-bot left a comment

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.

本次修订(22f0a9fb)已解决此前的 P2 问题(作者亦在讨论串中确认修复),未发现遗留问题。

已验证:

  • 白名单生效:13 个类别每批次固定输出 26 个 stop_reason/* 指标,缺失类别补 0,时间序列不再出现缺口。
  • env_error:<明细> 收敛为 env_error,其余未登记值归入 other——原始字符串不再进入指标 key,此前的无界指标名问题已消除。
  • 白名单覆盖全部真实生产者:遍历 relax/ 与 examples/ 中所有写入顶层 stop_reason/rollout_stop_reason 的位置(含字典字面量、三元表达式、f-string 与 **info 展开),实际取值集合为 budget_exhausted、completed、context_exhausted、discarded_by_pipeline、env_done、env_error:*、finish_abort、finish_length、format_error、llm_unavailable、max_turns,均已在白名单内。测试中归入 other 的 end_turn/max_tokens/tool_use 仅用于 Anthropic 兼容 API 的出站响应构造(relax/agentic/session/service.py),no_tool_call 仅存在于 env_infos 列表,二者都不会写入顶层字段,因此排除是正确的。
  • 新增的 test_rollout_metadata_dynamic_reasons_have_bounded_keys 用 100 条 env_error:<明细> 与 100 条超长自定义 reason 断言 key 集合恰为 26 个,直接锁定了该问题;test_rollout_metadata_known_reasons_preserved 对每个类别做恒等断言,可防止白名单笔误。

验证方式说明: 我将 _compute_rollout_metadata_stats 的真实源码按 AST 抽取后配合 NumPy 1.26.4 执行,覆盖类别恒等映射、env_error 收敛、other 归并、26 键上界、补零、ratio 求和为 1、空输入与不改写入参等语义,全部通过。此沙箱内 pytest 无法运行(对平凡用例亦 core dump,属环境问题),故未执行完整测试文件,建议以 CI 结果为准。

Powered by Nyanpasu with deepseek-v4.1-flash-ali medium, please check the suggestions carefully.

@SigureMo

SigureMo commented Oct 8, 2026

Copy link
Copy Markdown
Member

本赛题已经由 #330 合入并锁定,感谢参与~

@SigureMo SigureMo closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants