Prhub

#1790 openenv/tbench2: score the shared-server leg natively; retire the adapter compensation

原始 PR 作者 nblintao 合并时间 2026-07-31 05:31 文件变更 7 提交数 5 评论 6 代码增减 +159 / -209

执行摘要

共享服务器腿改原生打分,退役适配补偿

PR body 说明这是 #1675 的 follow-up,解决该 PR review 中关于退役 native_evaluate=False 分支的讨论。核心动机有三:(1) 让两条 agent-function leg(共享服务器 / Daytona 沙箱)统一为单一打分协议,消除 adapter 侧针对旧 tbench2_env 部署的补偿机制;(2) 前提是上游先把 docker 模式补齐——OpenEnv#965/#972 只修了 local 模式,docker 模式仍用裸 pytest 从 /task 打分,直接翻转会造成 fidelity regression,因此先等 OpenEnv#1012 合入(04d259ea6);(3) 面对无法预检源码的远程共享服务器,用运行时契约守卫把「旧部署静默返回看似合理的错误奖励」变成「每个样本大声丢弃」,避免污染训练信号。

值得精读,尤其适合想理解「删除兼容分支时如何保正确性」的工程师。三个看点:(1) 运行时契约守卫的设计——当 preflight 不可行时,用回复自身的契约指纹(info.harness)把静默误打分变成大声失败;(2) 跨仓库边界的演进编排——miles 侧 PR 明确依赖上游 OpenEnv#1012 且给出双向不兼容的切换顺序;(3) review 中 Shi-Dong 抓出的 0.0 vs None 差异,是「删除看似等价代码」时最容易漏掉的行为边界。

讨论亮点

两条 review 交锋均来自 Shi-Dong 的逐行核对,质量很高:

  1. 0.0 vs None 的语义差异(正确性):Shi-Dong 对照 OpenEnv#1012 源码(04d259ea6)指出,被删除的 _parse_reward_marker 在 reward.txt 缺失时返回 None(丢弃样本),而上游 evaluate 在 reward.txt 缺失时给 0.0——这意味着删除补偿后,服务端 test.sh 崩溃等 infra 失败会以假 0.0 混入训练信号。nblintao 承认差异并提交 huggingface/OpenEnv#1025 修复上游。

  2. 超时默认值 vs 服务端 verifier 预算(性能/运维):Shi-Dong 提示 OpenEnv 默认给 verifier 900 秒,600s 的消息超时默认值会掐断合法 evaluate。nblintao 将 OPENENV_MESSAGE_TIMEOUT_S 默认值提到 1200s 并写入文档(必须超过服务端最长的 verifier budget),同时创建 OpenEnv#1026 建议两侧超时自动协商管理。

实现拆解

  1. 退役 adapter 侧补偿机制(openenv_agent_function.py,+75/-162):删除 _TASK_WORKDIR / _TB2_TESTS_SRC 两个环境变量、_apply_workdir、_CANONICAL_EVAL_CMD、_REWARD_MARKER / _TESTSH_RC_MARKER 及 _parse_reward_marker / _parse_testsh_rc 解析器,以及 testsh_rc 指标管线;模块 docstring 中的 OPENENV_TASK_WORKDIR / OPENENV_TB2_TESTS_SRC 条目随之移除。原因:这些补偿只服务于旧 tbench2_env 部署(裸 pytest 打分、WORKDIR 不在 /app),OpenEnv#1012 后服务端自身具备同款契约,补偿成为必须清理的双协议债务。

  2. 统一两条 leg 的 episode 接线:_multi_turn 的关键字参数从三个(run_body / native_evaluate / post_episode)减为两个(run_body / post_episode),native_evaluate 参数整体删除;openenv_daytona_agent_function.py 的 run_episode 不再传 native_evaluate=True。两条腿现在只剩两个真实差异:run_body(环境如何创建:连共享服务器 vs 建 Daytona 沙箱)与 post_episode(共享服务器需要 trial-dir 清理,一次性的沙箱不需要)。

  3. 新增运行时契约守卫:新增 _obs_info 辅助函数读取 observation.info;_multi_turn 的 evaluate 处理要求回复携带 info.harness == "tests/test.sh"(当前上游才会发射的标记),否则记警告并以 reward=None 丢弃样本,与 #1675 已有的 error/None-reward 丢弃逻辑叠加。这是对「远程服务器无法做源码 preflight」的替代方案;已知行为变化:不带 tests/test.sh 的任务目录(走服务端 pytest fallback,无 harness 标记)也会被丢弃——这是刻意取舍,因为该路径与旧服务器不可区分,且 89 个官方任务与合成池都带 test.sh。

  4. scan_golden.py 采用 no-verdict 语义(+16/-6):golden 回放的 evaluate 处理从「缺 reward 记 0.0」改为「reward 缺失 / error 非空 / harness 非 canonical 都记为 ERR(reward=None + error 字段)」,避免把服务器端打分失败误记为任务的 0.0 基线;日志尾部捕获条件同步从 reward < 1.0 改为 reward is None or < 1.0。

  5. 配套改动:测试侧 _FakeEnv / _FakeResult 改为模拟携带 harness 标记的契约服务器,evaluate 分支直接返回 reward + info;新增 test_old_server_reward_is_not_trusted 用 _OldServerEnv 模拟旧服务器(reward=1.0 但 info 只有 {tests_passed, exit_code}),断言 reward=None 且 episode 本身跑完(turns == 2)。配置侧 OPENENV_MESSAGE_TIMEOUT_S 默认值从 600 提到 1200(evaluate 现在在单次 env 操作内跑完整 verifier 预算,官方任务最高 3600s;超时发生在完整轨迹生成之后,是最昂贵的失败点)。文档侧 README 与 openenv_launch_common.py 的预检报错把安装下限从「#965/#972 合并」改为「≥ #1012 合并(04d259ea6)」并去掉「not upstream main」这类旧表述。

文件 模块 状态 重要度
examples/experimental/openenv/openenv_agent_function.py 环境适配 modified 8.63
examples/experimental/openenv/tests/test_openenv_agent_function.py 适配器测试 modified 6.7
examples/experimental/openenv/scan_golden.py 回放脚本 modified 5.77
examples/experimental/openenv/openenv_daytona_agent_function.py 云沙箱 modified 5.74
examples/experimental/openenv/openenv_launch_common.py 启动预检 modified 4.22
examples/experimental/openenv/tests/test_openenv_daytona_agent_function.py 沙箱测试 modified 3.73
examples/experimental/openenv/README.md 文档说明 modified 2.19

关键符号

_multi_turn _obs_info run_episode test_old_server_reward_is_not_trusted _apply_workdir ( 已删除 ) _parse_reward_marker ( 已删除 ) _parse_testsh_rc ( 已删除 )

关键源码片段

examples/experimental/openenv/openenv_agent_function.py core-logic

本 PR 的核心:删除 adapter 侧全套补偿机制(_apply_workdir / _CANONICAL_EVAL_CMD / marker 解析器),新增 _obs_info 与 evaluate 契约守卫,并把 OPENENV_MESSAGE_TIMEOUT_S 默认值从 600s 提到 1200s。净删 162 行,是双协议统一的主战场。

def _obs_info(result: Any) -> dict:
    """读取 StepResult 上 Observation 的 info 字典(缺失时返回空 dict)。    契约守卫靠它读 evaluate 回复的 info.harness——这是判断服务器是否
    满足 canonical 打分契约的运行时指纹(对远端服务器无法做源码 preflight)。
    """
    obs = getattr(result, "observation", result)
    return getattr(obs, "info", None) or {}
​
​
# 运行时契约守卫(_multi_turn 的 evaluate 处理段;逻辑与 scan_golden.py 完全同语义):
# 旧部署不会报错,只会每个 episode 返回一个看起来合理的(但打错分的)
# reward。所以守卫只认一个证据:evaluate 回复必须携带当前上游
# (>= OpenEnv#1012,04d259ea6)才会发射的 harness 标记;任何其它情况
# —— 旧服务端 bare-pytest 的 {tests_passed, exit_code} info、服务端打分
# error、reward 缺失 —— 一律按 " 无裁决 " 丢弃样本,而不是记一条假 0.0:
#
# info = _obs_info(res)
# if (getattr(res, "reward", None) is None
# or _obs_field(res, "error")
# or info.get("harness") != "tests/test.sh"):
# logger.warning(
# "evaluate produced no canonical verdict "
# f"(error={_obs_field(res, 'error')!r}, harness={info.get('harness')!r}); "
# "dropping episode"
# )
# reward = None # 训练包装器据此丢弃样本
# else:
# reward = float(getattr(res, "reward"))
examples/experimental/openenv/scan_golden.py core-logic

golden 回放脚本同步采用 no-verdict 语义:服务器端打分失败或 harness 非 canonical 时记为 ERR(reward=None + error 字段),而不是伪造 0.0 基线,避免把基础设施故障错误归结到任务上。

                m = {
                    "solve_exit": solve_exit,
                    "solve_s": round(solve_s, 1),
                    "eval_s": round(time.monotonic() - t, 1),
                }
                # 与 agent loop 守卫相同的 no-verdict 语义:服务端打分失败
                # 或 harness 非 canonical 时记为 ERR,而不是伪造一个 0.0
                # 把基础设施故障错记到任务头上。
                raw_reward = getattr(res, "reward", None)
                eval_error = oaf._obs_field(res, "error")
                harness = str(oaf._obs_info(res).get("harness", ""))
                if raw_reward is None or eval_error or harness != "tests/test.sh":
                    m["reward"] = None
                    m["error"] = f"no canonical verdict (error={eval_error!r}, harness={harness!r})"
                else:
                    m["reward"] = float(raw_reward)
                if capture_logs and (m["reward"] is None or m["reward"] < 1.0):
                    # evaluate 输出本身携带 test.sh 日志尾部;磁盘上的
                    # /logs/verifier 只在 verify 窗口内存在
                    m["test_log_tail"] = (oaf._obs_field(res, "output") or "")[-800:]
                    res = await env.step(action(action_type="exec", command="tail -c 1200 /tmp/solve.log 2>&1"))
                    m["solve_log_tail"] = oaf._obs_field(res, "output")
                return m

评论区精华

reward.txt 缺失时的语义差异:0.0 vs None 正确性

Shi-Dong 对照 OpenEnv#1012 源码(04d259ea6)指出:被删除的 _parse_reward_marker 在 reward.txt 缺失时返回 None(丢弃样本),而上游 evaluate 在该场景给 0.0——删除补偿机制后,服务端 test.sh 未写出裁决的失败会被记成假 0.0 而非丢弃,训练信号可能被污染。

结论:nblintao 承认差异并提交 huggingface/OpenEnv#1025 修复上游;本 PR 的删除按计划推进,语义差由上游 follow-up 收敛。 · 已解决

消息超时默认值与服务端 verifier 预算 性能

Shi-Dong 提示 OpenEnv 默认给 verifier 900 秒,本 PR 改前的 600s 消息超时默认值会掐断合法 evaluate——且失败发生在完整轨迹生成之后,是最昂贵的失败点。

结论:nblintao 将 OPENENV_MESSAGE_TIMEOUT_S 默认值提到 1200s 并写入文档(必须超过服务端最长 verifier budget),另建 OpenEnv#1026 建议两侧超时自动协商管理。 · 已解决

风险与影响

  1. client/server 强耦合的切换窗口:新旧双向不兼容——旧 client 的补偿逻辑在 tests 被 withheld 的新服务器上会失效,新 client 在旧服务器上会丢弃一切样本。PR body 明确要求步骤 2(服务器升级重启)与 3(本 PR 合入)由部署方同步执行,这是最大的运营风险。

  2. 残余静默误打分路径:Shi-Dong 指出的 reward.txt 缺失 → 0.0 语义差在 OpenEnv#1025 合入前依然存在:服务端 test.sh 异常退出且 /logs/verifier 无裁决时,上游会给 0.0 而非丢弃,假负样本仍可能进入训练数据。

  3. 行为变更波及自定义任务:不带 tests/test.sh 的任务目录现在一律被丢弃(之前走服务端 pytest fallback 还能打分)。官方 89 任务与合成池无影响,自定义任务集需要核对。

  4. 超时配置敏感:OPENENV_MESSAGE_TIMEOUT_S 默认 1200s 只保证覆盖服务端默认 900s 预算;声明更大 [verifier].timeout_sec(官方套件最高 3600s)的任务集仍需手动调大,否则会在最昂贵的时点(完整轨迹生成后)失败。

  5. 验证缺口:session-server TITO 接线 + GRPO 训练循环未经本 PR 的 GPU 验证(PR 明确 defer 到切换时的 4-GPU smoke),依赖 #1675 的 4×H200 结果覆盖两条腿共享的代码路径。

  1. 部署/运维:shared-server 部署必须升级 tbench2_env ≥ 04d259ea6 并重启;升级后旧 client 的补偿逻辑失效,故需与本 PR 同步上线。一个正向副作用:新版服务器把 tests/ 和 solution/ 从 /task 撤走(verifier-asset withholding),同时封掉了旧部署暴露给 agent 的 reward 作弊洞。

  2. 系统:agent loop 净减约 200 行,双协议并存的维护复杂度消除;奖励语义与官方 Terminal-Bench-2 对齐(服务端跑 canonical tests/test.sh、真实 WORKDIR、真实 verifier 预算)。

  3. 团队:与上游 huggingface/OpenEnv 的契约耦合加深,仓库内多处以「≥ #1012 合并(04d259ea6)」作为安装下限;正在进行的 OpenEnv#1025 / #1026 follow-up 会继续收紧剩余语义差。

跨仓库协调部署 旧服务端静默误打分风险 行为变更:无 test.sh 任务被丢弃 超时默认依赖服务端预算 GPU 端到端验证缺失

关联 Issue

#965 fix(tbench2_env): canonical test.sh scoring, real timeouts, task-image workdir
#1012 fix(tbench2_env): bring Docker mode up to the canonical scoring contract
#972 [fix] [test] attention_output_gate TP slice when num_kv_heads < TP
#1012 fix(fully_async): per-task timeout + exception-safe requeue in rollout worker

完整报告

参与讨论