From b02db74237d6383c761a08bf597701126ebdda25 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Thu, 16 Jul 2026 23:30:56 -0400 Subject: [PATCH] docs: apply Codex plan review fixes (tail-INFRA rejudge, slot deadlock guard) --- .../designs/2026-07-16-gate-speedup-design.md | 4 +- .../plans/2026-07-16-gate-speedup.md | 91 +++++++++++++++++-- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/research-wiki/designs/2026-07-16-gate-speedup-design.md b/research-wiki/designs/2026-07-16-gate-speedup-design.md index f4c22c7..7501343 100644 --- a/research-wiki/designs/2026-07-16-gate-speedup-design.md +++ b/research-wiki/designs/2026-07-16-gate-speedup-design.md @@ -1,4 +1,4 @@ -# gate 验证提速设计(v2):连续并发 gate + Redis 复用 +# gate 验证提速设计(v3):连续并发 gate + Redis 复用 > 2026-07-16。背景:Video-MME 900 训练实测,epoch 1 单题型 gate 双臂验证 ~3h(两臂串行 × 块串行 × 题型串行三重串行,~6 题型/step)→ step ~18h,3 epochs 不可行。v1 设计的"预灌 BaselineCache"经 Codex 审查发现统计致命伤已废弃(见 §7)。本版方案:**②″ 连续并发 gate**(题型并行 + 双臂并行 + 逐对连续早停,一次调度重构)+ **③ Redis 复用保障**。统计内核(e-值公式、四出口、配对翻转、信息量阶梯、题尽/试用期通道)一行不动。 @@ -9,7 +9,7 @@ | 块=8 不是统计需要 | TRM4 注释原文"块大小=推理并发度,块内跑满"——并发 8 时代的工程遗迹。e-process 为 anytime-valid 上鞅(Ville 不等式),在**预先声明的样本顺序**下任意时刻停下判定假阳率仍 ≤ 1/e_confirm;逐对判定是比逐块更细的合法 optional stopping | | **统计消费必须按固定阶梯序前缀,不得按完成到达序**(Codex v2 复审 C1) | 到达序消费不合法:base 臂可缓存命中瞬间返回、cand 臂必新鲜跑,两臂延迟不对称,配对完成时间由 cand 延迟主导;若 cand 延迟与对错相关(走满 40 步的慢轨迹更易错),早到翻转对系统性偏向 W 型 → e-值虚高 → 假接受。**修法**:推理全并发乱序执行,但 (W,L) 更新与 gate_decision 只在"阶梯序最长已配齐前缀"延伸时推进——判定顺序回到预先声明的阶梯序,无条件合法;INFRA 单元视为"已解决(剔除)"不阻塞前缀推进 | | 冻结后 in-flight 结果丢弃合法 | 在固定前缀消费下,停止时刻 τ 之后的样本不进入统计是标准 optional stopping;丢弃不引入偏差(到达序消费下则不成立,故必须配合上一条) | -| 题型间并行无实质依赖 | 各题型 agent 只加载自己的 skill 文件(12 题型各有专属 .md),A 型 accept 改 A 的文件,对 B 型推理内容零影响。现行字母序滚动版本只是记账先后,非实质依赖。唯一共享文件 default-strategy.md 仅在题型缺专属文件时 fallback——本次 12 型全有专属文件,不触发;**启动时 fail-fast 断言:同 step 内多个题型不得映射同一 target_file**,违反即退回串行(防未来配置漂移) | +| 题型间并行无实质依赖 | 各题型 agent 只加载自己的 skill 文件(12 题型各有专属 .md),A 型 accept 改 A 的文件,对 B 型推理内容零影响。现行字母序滚动版本只是记账先后,非实质依赖。唯一共享文件 default-strategy.md 仅在题型缺专属文件时 fallback——本次 12 型全有专属文件,不触发;**启动时 fail-fast 断言:同 step 内多个题型不得映射同一 target_file**,违反即中止(RuntimeError,防未来配置漂移) | ## 2. 改动 ②″:连续并发 gate 调度器 diff --git a/research-wiki/plans/2026-07-16-gate-speedup.md b/research-wiki/plans/2026-07-16-gate-speedup.md index fd3cffa..4d33132 100644 --- a/research-wiki/plans/2026-07-16-gate-speedup.md +++ b/research-wiki/plans/2026-07-16-gate-speedup.md @@ -16,7 +16,10 @@ - 所有命令在 `Video-Tree-TRM` conda 环境:`conda run -n Video-Tree-TRM pytest ...` - 禁止 `except Exception: pass`;中文 docstring;loguru 日志 -- 现有纯函数复用,不重写:`_ladder_units` / `_load_run_rows` / `_infra_question_ids_from_db` / `_candidate_correctness_from_db` / `_count_infra_units` / `_check_infra_guard` / `_finalize_outcome` / `materialize_candidate_skill` / `unit_correctness_view` / `pair_block` / `gate_decision` +- 现有函数复用,不重写。路径钉死(Codex I4:勿在 core 层找它们): + - `app/harness/validate.py`:`_ladder_units` / `_load_run_rows` / `_infra_question_ids_from_db` / `_candidate_correctness_from_db` / `_count_infra_units` / `_check_infra_guard` / `_finalize_outcome` / `materialize_candidate_skill` + - `app/harness/question_units.py`:`unit_correctness_view` / `build_units` / `flatten_units` + - `core/evolution`(纯函数,零改动):`pair_block` / `gate_decision` / `classify_quadrants` - 每个 Task 结束跑 `conda run -n Video-Tree-TRM pytest tests/unit -x -q` 保持全绿后才 commit --- @@ -90,7 +93,11 @@ def test_prefix_consumes_in_ladder_order_after_head_arrives() -> None: def test_freeze_on_terminal_verdict_stops_consumption() -> None: - """过线(此处 8 连胜 E≥20 confirmed)即冻结,后续已配齐单元不再消费。""" + """过线即冻结,后续已配齐单元不再消费。 + + 数值:W 连胜 L=0 时 E=(2^(W+1)-1)/(W+1),W=6→18.14<20,W=7→31.875≥20, + 故 7 连胜恰好 confirmed 过线(Codex 复核)。 + """ run = _mk_run(12) for i in range(12): run.slots[i].base = False @@ -98,7 +105,23 @@ def test_freeze_on_terminal_verdict_stops_consumption() -> None: _advance_prefix(run, _PARAMS) assert run.frozen and run.verdict is not None assert run.verdict.decision == "accept_confirmed" - assert run.n_used < 12 # 早停,没吃满 + assert run.n_used == 7 # 第 7 个净胜恰好过线,早停不吃满 + + +def test_tail_infra_reaches_terminal_not_continue() -> None: + """尾部全 INFRA:剔除后须重判(n_remaining 归 0 → 题尽第四出口), + verdict 不得停留在 continue(Codex plan 审 C1 回归锁)。""" + run = _mk_run(4) + run.slots[0].base = False + run.slots[0].cand_per_q = {"q0": True} + run.slots[1].base = True + run.slots[1].cand_per_q = {"q1": True} + for i in (2, 3): + run.slots[i].base_infra = True + run.slots[i].cand_per_q = {f"q{i}": True} + _advance_prefix(run, _PARAMS) + assert run.verdict is not None and run.verdict.decision != "continue" + assert run.frozen def test_infra_unit_skipped_not_counted() -> None: @@ -127,8 +150,10 @@ def test_ties_hit_futility_early_and_freeze() -> None: run.slots[i].cand_per_q = {f"q{i}": True} _advance_prefix(run, _PARAMS) assert run.frozen - assert run.verdict is not None and run.verdict.decision != "continue" - assert run.w == 0 and run.l == 0 + # 精确锁定 futility 出口:首个消费后 W=L=0,n_remaining=2, + # 乐观 E=E(2,0)=(2^3-1)/3=2.33<3 → 立即 reject_futility(Codex 复核) + assert run.verdict is not None and run.verdict.decision == "reject_futility" + assert run.n_used == 1 and run.w == 0 and run.l == 0 ``` - [ ] **Step 2: 跑测试确认失败** @@ -238,6 +263,16 @@ def _advance_prefix(run: _GateRun, params: GateParams) -> None: run.prefix_ptr += 1 if slot.excluded(): run.n_excluded += 1 + # 剔除使 n_remaining 缩小,必须重判(Codex plan 审 C1):否则尾部全 INFRA + # 时 verdict 停留在 "continue",绕过题尽第四出口且 _finalize_outcome + # 查 stop_reason 映射 KeyError。n_used==0(纯前导 INFRA)时无证据可判,跳过。 + if run.n_used > 0: + n_remaining = (len(run.slots) - run.n_excluded) - run.n_used + run.verdict = gate_decision( + run.w, run.l, run.n_used, n_remaining, params=params + ) + if run.verdict.decision != "continue": + run.frozen = True continue uid = slot.unit.unit_id assert slot.base is not None and slot.cand_per_q is not None @@ -270,7 +305,7 @@ def _advance_prefix(run: _GateRun, params: GateParams) -> None: - [ ] **Step 4: 跑测试确认通过** Run: `conda run -n Video-Tree-TRM pytest tests/unit/test_gate_prefix.py -x -q` -Expected: 5 passed +Expected: 6 passed - [ ] **Step 5: Commit** @@ -429,6 +464,14 @@ async def test_question_slots_caps_inflight() -> None: await asyncio.gather(*[_job(1) for _ in range(6)], *[_job(2) for _ in range(3)]) assert peak["max"] <= 2 + + +@pytest.mark.asyncio +async def test_question_slots_rejects_oversized_request() -> None: + """申请槽数超宽度:fail-fast ValueError 而非自死锁(Codex C2 回归锁)。""" + slots = _QuestionSlots(1) + with pytest.raises(ValueError, match="自死锁"): + await slots.acquire(2) ``` - [ ] **Step 2: 跑测试确认失败** @@ -449,11 +492,18 @@ class _QuestionSlots: def __init__(self, width: int) -> None: assert width > 0, f"并发宽度必须为正: {width}" + self._width = width self._sem = asyncio.Semaphore(width) self._acquire_lock = asyncio.Lock() async def acquire(self, n: int) -> None: - """原子获取 n 个题槽。""" + """原子获取 n 个题槽。 + + fail-fast:n > 宽度时任务持锁等待永不满足的槽位 → 自死锁 + (AR pair 单元 2 题 + width=1 的病态配置,Codex plan 审 C2),直接报错。 + """ + if n > self._width: + raise ValueError(f"单次申请题槽 {n} 超过并发宽度 {self._width},将自死锁") async with self._acquire_lock: for _ in range(n): await self._sem.acquire() @@ -511,7 +561,11 @@ async def _run_unit_arm( infra_qids = _infra_question_ids_from_db(log, r.run_id, questions) run.infra_denom += r.total if infra_qids: - run.errors += 1 + # 单元级去重(Codex plan 审 I3):同一单元双臂都 INFRA 只计 1 个 error, + # 与设计 §2.3"分子=INFRA 单元数(任一臂)"及旧块实现口径一致 + # (旧实现 cand 不跑 base-INFRA 单元,天然无双计)。 + if not slot.excluded(): + run.errors += 1 if arm == "base": slot.base_infra = True else: @@ -536,7 +590,7 @@ async def _run_unit_arm( - [ ] **Step 4: 跑测试确认通过** Run: `conda run -n Video-Tree-TRM pytest tests/unit/test_gate_unit_arm.py tests/unit/test_gate_prefix.py -x -q` -Expected: 10 passed +Expected: 12 passed - [ ] **Step 5: Commit** @@ -802,7 +856,7 @@ async def validate_skills_concurrent( - [ ] **Step 5: 跑测试确认通过** Run: `conda run -n Video-Tree-TRM pytest tests/unit/test_gate_concurrent.py tests/unit/test_gate_prefix.py tests/unit/test_gate_unit_arm.py -x -q` -Expected: 13 passed +Expected: 15 passed 注意:`test_out_of_order_arrival_still_ladder_order` 里 cand 臂识别依赖 `run_id.endswith("_cand")`,与实现的 run_id 命名 `{prefix}_{arm}` 一致;若失败先核对 run_id 拼接。 - [ ] **Step 6: Commit** @@ -978,6 +1032,22 @@ def test_shared_target_file_fails_fast() -> None: _assert_disjoint_target_files( {"OCR Problems": "default-strategy.md", "Spatial Reasoning": "default-strategy.md"} ) + + +# ---- runner 级红绿(Codex plan 审 I6):进化并行 + Phase D 字母序 + 共享 gate_log ---- +# 实现说明:_gate_batch_skills 是 Runner 方法,构造完整 Runner 成本高。用 +# `Runner.__new__(Runner)` 裸实例 + 手工挂属性(_config/_paths/_evolve_llm/ +# _gate_questions_by_id/_gate_units_by_id/_current_version/_record_run/ +# _class_baseline_acc/_accept_skill/_record_rejected_skill 等按 AttributeError +# 逐个补假实现),monkeypatch 以下三点后调用 _gate_batch_skills: +# 1. core.evolution.evolve_single_skill → 假实现:记录 (task_type, 开始/结束时间), +# 各 sleep 0.05s;断言两题型的时间窗重叠(证明 gather 并行而非串行)。 +# 2. app.harness.runner.validate_skills_concurrent → 假实现:记录收到的 specs 与 +# log 参数,返回两题型的假 ValidationOutcome(一 accept 一 reject)。 +# 3. runner._accept_skill / _record_rejected_skill → 假实现:append 到调用序列表; +# 断言调用顺序 == sorted(题型)(Phase D 字母序),且 accept/reject 分派正确。 +# 若挂属性成本失控(超过 ~60 行 fixture),降级为拆出可测纯函数并在 PR 说明, +# 但字母序与并行两条断言不得省略。 ``` - [ ] **Step 2: 跑测试确认失败** @@ -1261,6 +1331,7 @@ git commit -m "feat: parallel evolve + continuous gate wiring in runner (algo #6 - Modify: `app/harness/checkpoint.py:61`(从字段清单删 `"gate_block"`) - Modify: `config/*.yaml` 全部含 `gate_block:` 的文件(train_videomme / default / question_gen_180_补 / question_gen_360 / train_action_recognition / train_ar30)删该行 - Modify/Delete: `tests/unit/test_gate_block_unit.py`、`tests/unit/test_harness_validate.py` 等引用被删符号的测试 +- Modify: `tests/integration/test_checkpoint_pair.py:128`(含 `gate_block`,Codex I7;grep 范围必须覆盖 tests/integration/ 与 tests/e2e/) - [ ] **Step 1: 全局定位被删符号的引用**