docs: ml 审查精简版(763→85 行)——删去前五轮已解决条目,只留:R4 实测基线(部署口径 P99 11.07 通过、查看器 infer/safety 30.4/61.6+63.5/248.8)、A 组三处会断流程(r4_probe.html 复用 detached tensor、pipeline.sh 7 处 GBK json.load、ortLocal+proxy 离线路径失效)、B 组三项待判(R4 双口径、safetyMs Node vs 浏览器两把尺/R9 无数据、WebGPU 零数据)、C 组六项未实测(6–13 步、gapDrop、R2 池无关性、R8 出题耗时、Windows 生命周期、续跑本机复核)、D 组 webapp/models/README 过时

This commit is contained in:
cbb
2026-10-18 03:17:04 +08:00
parent a5e779c515
commit d70e2b90b8
+70 -643
View File
@@ -1,658 +1,85 @@
# ML 运行手册审查(ml/README.md)
# ML 运行手册审查(ml/README.md)· 精简版
审查对象:`ml/README.md`(12 阶段运行手册)+ `ml/pipeline.sh` + `ml/gen|prep|model|sim|apg|mining|eval` 全部代码。
判据来源:`docs/Transformer轨迹拟合训练方案.md`(方案)。
硬件:本机 CBB-003,RTX 3090 24 GB(driver 617.42)、28 核 CPU、Python 3.12.10 + torch 2.10.0+cu130 + onnxruntime 1.22.0、Node v24.15.0。
方法:代码逐行读 + 在本机真跑第 2/4/5/6 步(两种头)与闭环仿真。**APG 及之后(8–11 步)未跑**,被中断,不声称可用。
六轮历史(`0218d7f` → `a5e779c`):第一轮 13 条、第二轮可行性、第三~五轮处置、第六轮本机实跑。**已解决的条目按用户要求删去,完整原文在 git 历史里**。本文件只剩三部分:仍有效的实测数字 / 仍未解决的问题 / 已复核正常的项,供实现方下一轮处置。
结论:流程骨架是通的(对拍 PASS、数据生成、打包、预训练、闭环仿真都实测跑过)。问题集中在**判据自相矛盾**和**指标口径不一致**两类,不是代码写错。
环境:本机 CBB-003(RTX 3090、28 核、Python 3.12 + torch 2.10 + onnxruntime 1.22、Edge + onnxruntime-web 1.20.1)。
当前结论:第 1–6 步已实测可跑;**第 7–13 步零证据**;R4 部署口径(纯推理)已判通过;阻断本机跑 `r4` / `all` 的三处见 A 组。
---
## 一、实测记录(可复现)
## 一、仍有效的实测数字(后续对照用)
冒烟产物落在 gitignore 目录 `ml/data/_smoke/`、`ml/runs/_smoke/`,工作树干净,`ml/assets/kin.json` 未重新生成。
| 步 | 命令 | 实测结果 | 手册声称 | 判定 |
|---|---|---|---|---|
| 2 对拍 | `node ml/eval/dump_parity.cjs 300` + `python -m ml.eval.check_parity` | `PASS: true`,fk 3.4e-13 mm、orient 3.3e-16、间隙 2.5e-13 mm、83 维特征 6.0e-8、间隙梯度相对误差 1.4e-6,7.6 s | 2 分钟,须 PASS | 符合 |
| 4 生成 | `node ml/gen/gen_dataset.cjs --n 80 --seed 7` | **1.56 s/条**,80 条 122 s;老师 plan p50 291 ms / p90 1490 ms,认证 p50 247 ms / p90 944 ms | 约 2 秒/核 | 符合(偏乐观余量) |
| 4 统计 | `node ml/gen/stats.cjs`(88 条) | keepRate **0.977**、samplesPerTask 36.5、projectedSamplesFor50k **178.4 万**、exec_s p50 2.3 / p90 3.0 / max 3.7 s、travel p50 350°、**cert.minGap p50 4.4 / p90 5.9 mm**、尾部补零样本占比 14.7% | keepRate≈0.95、projected≈180 万 | 符合,但 minGap 没进检查项(见 P0-2、口径-4) |
| 5 打包 | `node ml/prep/build_dataset.cjs` + `python -m ml.data --index` | 86 任务 → train 3004 / val 136 样本,`index.json` 键名与手册一致;增广 847 提交 / 5 拒绝 | train 170 万 / val 9 万(5 万条口径) | 比例自洽(5% split) |
| 6 预训练 MeanFlow | `python -m ml.train --steps 250 --no-eval --config ml/configs/base.json` | 参数量 **4.92 M**,吞吐 **6 208 样本/s**(step 200,batch=1024) | 1.5–6 小时;方案 R10 要 ≥2 万样本/s | 与 1.5–6 h 自洽(60 000 步 ≈ **2.8 h**),**与 R10 判据冲突** |
| 6 预训练 WTA | `--config ml/configs/wta.json --steps 40` | 跑通,参数量 4.93 M(同骨干换头成立) | 回退约 1 天 | 通路成立 |
| 闭环仿真 | `python -m ml.sim.closed_loop --split val --n 3` | 跑通 9.7 s,按 cat/mode 出 success/collision(未训模型 0/0,符合预期) | — | 通路成立 |
吞吐换算:60 000 步 × 1024 = 6 144 万样本 ÷ 6 200 = **9 910 s ≈ 2.75 h**(不含 eval)。2 万样本/s 意味着 51 分钟跑完 60 000 步——当前配置在 3090 上不可能达到。
---
## 二、P0:会让人在终端前停住或误判
### P0-1 训练吞吐判据自相矛盾(方案 R10 vs 手册)
- 方案 §10.1 R10:`2000 步吞吐 ≥ 2 万样本/s`,缓解写的是"降 epoch 或增大 batch"。
- 实测 6 208 样本/s → 按 R10 判"不达标",但 60 000 步 2.75 h 完全落在手册 1.5–6 h 的预算内。
- 训练是算力受限(MeanFlow 的 `torch.func.jvp` 双前向 + FK 损失),增大 batch 只拉长每步墙钟,samples/s 不上去。R10 给的缓解措施对它自己定的阈值无效。
- 手册 §6 只写"测吞吐",**没写通过线**:`ml/train.py:98` 打出 `samples_per_s`,读者看到 6k 无从判断该继续还是改配置。
建议:R10 阈值改为终态判据("60 000 步 ≤ 3 h")或把 samples/s 线降到 ~5 千;手册 §6 补一行"实测参考:本机 3090 batch=1024 → 6.2 k 样本/s ≈ 2.8 h/60 k 步"。
### P0-2 共识过滤分支的期望数字整段缺失
- `ml/pipeline.sh:27-31`:`consistentByGateRule < 0.9` 时自动加 `--consensus 4`。
- 方案 §10.1 R1 记录首测一致率 **78%**(100 任务 / 373 对邻居,门禁 90%)→ **默认走的就是 consensus 分支**。
- `ml/gen/gen_dataset.cjs:51`:邻居多数不同意就 `keep=false`(why=consensus)→ keepRate 与样本量双双下降。
- 手册 §4 的 `keepRate≈0.95`、`projectedSamplesFor50k≈180 万`(实测无共识 0.977 / 178 万)是**关共识**的口径;开共识后没有对应期望值,也没写"样本不够要不要加 N_TASKS"。
- §13 回传表要求按 `stats.json` 判下一步 → 判据缺一条分支。
建议:§4 增加共识分支的两行期望(keepRate≈? / projected≈?),并明确"低于 X 就把 N_TASKS 提到 8 万"这类动作。
### P0-3 WTA 回退会覆盖 MeanFlow 的训练产物
- `ml/pipeline.sh:34-35`:`train` 阶段输出目录写死 `ml/runs/bench` 与 `ml/runs/base`,`CFG` 只换头不换目录。
- `ml/pipeline.sh:37`(m5)与 `:44`(apg)写死读 `ml/runs/base/best.pt`。
- `ml/train.py:57`:`log.jsonl` 以 append 打开 → 两次不同头的日志混在同一文件。
- 后果:手册 §7 的 `CFG=ml/configs/wta.json bash ml/pipeline.sh train` 会盖掉已训好的 MeanFlow `best.pt/last.pt`,对比物消失;而方案 §1.1 选头的依据恰恰是两者对比(沙箱里两者 100%/0%,"分不出高下")。
建议:`OUT=${OUT:-ml/runs/base_$HEADTAG}`,m5/apg/bench 用 `CKPT=` 变量指;两头的 run 分目录共存。
### P0-4 手册要求 Linux/WSL2,但本机 Windows 原生可跑,只差一处
- 实测第 2/4/5/6 步与闭环仿真在 Windows + Git Bash 下全部跑通(Python 用 `python`)。
- `ml/prep/run_prep.sh:8` 硬用 `python3`,而 Windows 的 `python3` 是 `WindowsApps` 假壳(`python3 -c` 直接 exit 1)→ 第 5 步在**最后一步**失败,`set -e` 会让 `pipeline.sh all` 在此中止(前面的 npy 已写好,重跑代价大)。
- 同类:`ml/pipeline.sh` 内联的 `python -c`(gate1 的 CONSENSUS 判定、m5 的打印)用的是 `python`,在 Windows 没问题 → 两个脚本各用一种,本身就是不一致。
建议:统一 `${PYTHON:-python}`(或 `${PYTHON:-python3}`);手册 §1 补一句"Windows 原生实测可跑,需 Git Bash + `python` 在 PATH"。
---
## 三、口径不一致("这一步通过"不等于"部署能过")
### 口径-1 闭环仿真的安全层 ≠ 部署运行时的安全层
- 仿真只有"逐步候选干涉则换下一条"(`ml/sim/closed_loop.py:118-130`)。
- 运行时多出:1 步胶囊梯度投影(`webapp/js/ik-policy.js:107`)、刹停段 + 每条候选 40 次距离查询预算(`:177`)、安全层单次推理 30 ms 预算(`:322`)、卡死检测与换向锁 1 s(`:296-304`)。
- §6 EVAL、§7 M5 门禁、§9 挖错/门禁**全部**用仿真那套(默认 `safety=False`)。
- 于是 §7 的"穿柱率 ≤2%"和 §11 的"collisionFrames 必须为 0"是两把尺,互不预示;手册没有任何一处说明这点。
建议:§7 明说"M5 是纯策略指标,§11 是策略+安全层指标";m5 对同一门禁集加跑一遍 `--safety`,两个口径都写进 `m5_gate.json`。
### 口径-2 门禁集/挖错题的时长预算是假的
- `ml/mining/tasks.py:47` 每题写死 `exec_s: 3.0, travel: 0.0`。
- `ml/sim/closed_loop.py:67` 超时 = `clamp(2*exec_s, min=3)` = 6 s;`ml/mining/mine_round.py:38` 又用 `max_sec=6.0` 覆盖。
- 后果:§7 M5 的 1000 题、§9 的门禁集里,"到达"一律 6 s 硬顶、`time_ok_1p3` 一律 3.9 s;而 §6 训练期 EVAL 用的是老师真实时长 ×1.3。两个"到达率"不可比却共用同名指标。`travel=0` 还使 `travel_ratio_median`(`closed_loop.py:154`)在门禁集上直接返回 None。
- 方案 §10.1 R8"3 s 内恢复 ≥95%、活锁=0"在 12 步里没有执行点。
建议:`tasks.py` 出题时带上老师时长(或明确写"门禁集按定长 6 s 口径");R8 补对抗用例集与命令。
### 口径-3 APG 的"到达"是定长 3.6 s 窗口内的距离塑形,且碰撞只查端点
- `configs/apg.json`:`plans:60`,每次推理提交 3 步 × `DT=0.02` → 展开 **3.6 s**(`ml/apg/apg_finetune.py:67`),无"到达即停";到达项只在每个 10 块段末用 `((T-pe)/100)²`(`:104-109`)。
- 实测这批老师轨迹 exec_s p50 2.3 / p90 3.0 / max 3.7 s → 样本刚好压线;真数据里老师时长 >3.6 s 的绕远长任务(正是 C/B 难题)APG **永远看不到绕出段**。
- 碰撞 hinge 只打每步末端点(`:95-97`),仿真与运行时都查端点+中点(`closed_loop.py:142-144`、`ik-policy.js:222`)。
- 手册 §8 写"直接优化到达、碰撞余量",读者会以为是全 horizon 到终点。
建议:§8 补一句"定长 3.6 s 滚动 + 段末距离替代到达项";把 `plans` 提到覆盖 exec_s p90(≥55 s 不现实,折中:对 exec_s>3.6 s 的题按比例加 plans,或在 README 说明这是已知截断)。
### 口径-4 训练余量 5 mm 压在老师轨迹自身的间隙分布上
- 实测 88 条 keep 轨迹 `cert.minGap` **p50 4.4 mm / p90 5.9 mm**(`ml/gen/stats.cjs:18` 已算出 `vertexMinGapMm`)。
- `docs/collide.json` 的 `margin_mm = 0.3`,`kin.gaps()` 的 gap 已减掉它 → 这 4.4 mm 是实打实的几何间隙。
- `configs/apg.json` 的 `margin_train_mm = 5.0` + 运行时投影目标 `margin:5`(`ik-policy.js:174`)→ **约一半 BC 标签落在 5 mm 以下**,APG 碰撞项与 BC 项方向相反。
- 手册 §4 让检查的三项(keepRate / byCatMode / projectedSamples)里没有 `vertexMinGapMm`;方案 §7.1 只写"margin=认证+5",没和老师的间隙分布对齐。
建议:§4 检查项加 `vertexMinGapMm` 并给期望区间;要么 `margin_train_mm` 降到老师分布 p50 附近(~4 mm),要么承认 BC/APG 有对抗并把它当成设计取舍写下来。
---
## 四、缺项与细节
1. **验收表落不了地**:方案 §9 要求 held-out 可达 ≥98%、D 类停靠 ≤老师+5 mm、刹停发生率 <2%、R7 左右比例与行程门禁、R8 对抗用例——12 步里都没有对应命令。`ml/mining/gate.py:41` 只查 3 项(到达增益 / A 类降幅 / 上轮仍对);`tests/policy_bench.cjs:81` 已经算出 `brakes/locks/uncertifiedSegs` 但没进任何门禁。
2. **挖错池累积**:`ml/mining/loop.sh:20` `ls ml/runs/mine/r*/pool.jsonl` 把**所有历史轮**的错题池喂给每轮 APG,池子逐轮变大,且 `mine_round.py:57` 对 E1 复制 ×2、每题再带 10 个邻居。手册 §9 只说"错题与邻居进采样池",没说会跨年累积 → 后期轮次的采样分布严重偏向老错题(A 类降幅门禁是唯一的兜底)。
3. **fp16 导出方向可疑**:手册 §10 把 `--fp16` 报错当偶发("去掉 --fp16 手动导出 fp32")。但部署端是 wasm(`webapp/js/ik-policy-glue.js:36` 默认 wasm)和 Node CPU(`tests/policy_bench.cjs:55` `executionProviders:['cpu']`),fp16 在 CPU EP 上要么缺 kernel 要么靠大量 Cast 兜底,不保证比 fp32 快。参数量实测 4.92 M → fp32 ≈ 20 MB(与手册一致),fp16 省的 10 MB 对浏览器收益有限。**本机 `onnxconverter_common` 未装,此条未实测。**
4. **`--n` 同时控制两个集合**:`tests/policy_bench.cjs:85-86` 的 `+opt('--n')` 对 nc 和 route 一起生效,手册说的"30 例 / 40 例"只在默认值成立。
5. **val 为空时崩**:小样本摸底(几百条、5% split 可能 0 个 val 任务)下 `ml/sim/closed_loop.py:161` 的 `evaluate` 返回空 dict,`ml/train.py:106` 取 `res['success']` KeyError。真实数据不会撞,但手册鼓励先小跑摸底。
6. **文件地图缺 4 个真实文件**:`ml/gen/cert.cjs`(认证实现在 ik-policy.js,这里是跳转壳)、`ml/gen/pathdev.cjs`(同伦类判定,共识过滤与门禁都依赖它)、`ml/prep/npy.cjs`、`ml/toy/toy_bimodal.py`(方案 §沙箱 的依据)。
7. **R2 无执行点**:方案要求"近邻样本标签方差 <10% 步长、起步失败率=0",手册 §3–§5 的检查项里没有。
---
## 五、按优先级的改动清单
| 优先级 | 改什么 | 落点 |
| 项 | 数字 | 口径 |
|---|---|---|
| 1 | 吞吐判据对齐:R10 改成终态判据或降线,§6 写实测参考值 | `docs/Transformer轨迹拟合训练方案.md` §10.1、`ml/README.md` §6 |
| 2 | train 阶段输出目录随头分开,m5/apg/bench 用 CKPT 变量 | `ml/pipeline.sh:34-37,44,48` |
| 3 | `python3` → `${PYTHON:-python}`,§1 说明 Windows 原生可跑 | `ml/prep/run_prep.sh:8` |
| 4 | §4 补共识分支的 keepRate/样本量期望与"加 N_TASKS"动作;检查项加 `vertexMinGapMm` | `ml/README.md` §3–§4 |
| 5 | §7 说明 M5 与 §11 两个口径,m5 加 `--safety` 对照 | `ml/pipeline.sh:36-42`、`ml/README.md` §7 |
| 6 | 出题带真实老师时长(或明写定长 6 s),补 R7/R8 执行点 | `ml/mining/tasks.py:47`、`ml/mining/gate.py:41`、`ml/README.md` §9 |
| 7 | §8 明写 APG 的 3.6 s 截断与只查端点;`margin_train_mm` 与老师间隙分布对齐 | `ml/README.md` §8、`ml/configs/apg.json` |
| 8 | §10 默认 fp32 + `__POLICY.latency()` 实测后才考虑 fp16(仅 WebGPU);loop.sh 池子只取本轮;文件地图补全;evaluate 空任务保护 | `ml/README.md` §10/文件地图、`ml/mining/loop.sh:20`、`ml/sim/closed_loop.py:161` |
| 对拍 300 条 | PASS(fk 3.4e-13 mm、特征 6e-8、间隙梯度 1.4e-6),7.6 s | 第 2 步 |
| 生成 | 1.56 s/条(80 条 122 s) | 第 4 步 |
| 统计 | keepRate 0.977、samplesPerTask 36.5、projected 178.4 万、exec_s p50 2.3 / p90 3.0 / max 3.7 s、cert.minGap p50 4.4 / p90 5.9 mm | 88 条,**未加 3 mm 过滤** |
| 预训练 | 4.92 M 参数、6208 样本/s(batch=1024)→ 60k 步 ≈ 2.8 h | 250 步冒烟 |
未验证范围:第 7(m5)、8(apg)、9(mine)、10(export)、11(bench)步与浏览器 `?policy=1` 第 12 步都没跑过(`ml/node_modules` 未安装、`webapp/models/` 无模型、`onnxconverter_common` 未装)。这些通路只做过静态代码核对。
R4(随机初始化模型;延迟只取决于结构,与是否训练无关):
| 接口 | 线程 | proxy | P50 | P95 | P99 |
|---|---|---|---|---|---|
| **enc_once(部署口径)** | 16 | 开 | 7.36 | 9.48 | **11.07** |
| enc_once | 4 | 开 | 10.22 | 11.69 | 12.92 |
| enc_once | 16 | 关 | 7.43 | 9.21 | 10.79 |
| 旧接口(对照) | 16 | 开 | 9.53 | 11.29 | 16.47 |
| 旧接口 | 4 | 开 | 13.88 | 16.35 | 17.19 |
tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮前默认(4 线程、无 proxy、旧接口、真查看器)P99 37.0;线程扫描 8 线程 24.9 / 16 线程 19.5 / 24 线程 25.8 → 16 最优,超线程反而挤。**部署口径纯推理 P99 11.07 < 15,R4 判通过**;`enc_once` 相对旧接口省约 30–33%。
真查看器同一会话(随机权重、`?policy=1&probe=1`):inferMs P50/P99/max = **30.36 / 61.64 / 68**,safetyMs = **63.52 / 248.76 / 320.6**;622 次 plan、brakes 4、locks 48、projected 283、lookahead 219。单变量对照:`__POLICY.stop()` 后纯 infer 7.5 / 9.7 ms → 差额是主线程排队,不是模型慢。
---
## 二、仍未解决的问题
### A. 会直接中断本机一键流程的三处(已定位、未修)
**A-1 探针页复用 detached tensor**(`webapp/r4_probe.html:28`、`:30`)
proxy=1 时 ORT 把输入 buffer transfer 给 Worker,而页面把 `feat` 建在循环外、`run()` 每次复用同一个 `Float32Array` → 第二次 `run()` 报 `Tensor's size(83) does not match data length(0)`。两种 proxy 配置全挂 → `r4.json` `pass:null`、`r4_probe.py` 非零退出、阶段 rc=1(`all` 因此会停)。
修法:把 `feat` 的分配移进 `run()`(每次新建,一行)。判据:`bash ml/pipeline.sh r4` 四种配置都出数、`pass` 非 null。
证据:改过一行的副本四种配置全部正常(上表 16 线程行即出自副本);查看器 `Runner._plan` 每次新建、不受影响(`ik-policy.js:257-258`)。
**A-2 `pipeline.sh` 7 处 `json.load(open(...))` 无 `encoding='utf-8'`**(`:90 :110 :118 :127 :136 :138 :144`)
已复现:`:110` 读 `r4.json`(`r4_probe.py:113` 以 `ensure_ascii=False` 写出、含中文)→ Windows GBK 报 `UnicodeDecodeError: 'gbk' codec can't decode byte 0xa7`。后果不是"读不到就用默认",而是命令替换为空 → 上层分支走错。
其余 6 处同写法;`stats.json` 由 node `JSON.stringify` 写出(不转义非 ASCII),文件里一旦有中文就同样炸。统一补 `encoding='utf-8'` 即可。
**A-3 `&ortLocal=1` + 默认 proxy 必然失败**
`ik-policy-glue.js` 把 `ort.env.wasm.wasmPaths` 设为相对前缀 `vendor/ort/`,ORT 对 `.mjs` 走 `import()` 需要绝对说明符 → `no available backend found. ERR: [wasm] TypeError: Failed to resolve module specifier 'vendor/ort/ort-wasm-simd-threaded.mjs'`。README 明确支持的离线部署路径在默认配置下不可用。(`ortLocal=1` + `proxy=0` 未测;`wasmPaths` 与 proxy 无关,大概率同样失败。)
### B. 待实现方判断(数据已摆在上面;分歧按第五轮第〇节规则,以实现方为准)
**B-1 R4 判据用哪个口径。** 纯推理 P99 11.07 判通过;但同一台机、同一会话,整周期 plan = infer 61.6 + safety 248.8(P99),随机权重下 1/3 的规划 >100 ms。如果 R4 的意义是"浏览器能不能按 60 ms 周期推一次",纯推理口径会放过实际丢步的配置。供下一轮的建议(不要求现在改):`r4.json` 记两个数 `infer_p99` 与 `plan_p99 = infer + safety`,后者另设一条线(如 <45 ms,给 60 ms 周期留余量)。
**B-2 `safetyMs` 两把尺。** bench 在 Node 里跑同一份 `ik-policy.js`(CPU EP),浏览器里同一段代码 p99 249 ms——互不可替代,与"仿真安全层 ≠ 部署运行时"是同一类问题换了个位置。判据:同一批贴柱题两边各测一次,差 >3× 就要解释。建议核对 `certBudget=40` 在贴柱位形下的实际查询次数与 `makeCert` 的 memo 命中率——249 ms 的量级不是"随机策略质量"能解释的;真模型是否不同未知。**R9 至今无有效数据**(探针只测纯推理;闭环丢步率因探针一运行 CDP 就拿不到执行槽而没采到——这本身是主线程拥挤的证据)。
**B-3 WebGPU 是判定顺序里的备用通过路径,但至今零数据。** 第五轮判定顺序写了"wasm 不过而 WebGPU 过,也判通过",本机 headless 报 `[webgpu] backend not found`,且 CDN 间歇不可达(jsdelivr 多次 000、unpkg 200)→ 需要一条真能测到 WebGPU 的路(非 headless,或 A-3 修好后本地放 ort)。
### C. 尚未实测(跑之前必须知道这些是空的)
1. **第 6–13 步全部没跑过**:train 只有 250 步吞吐冒烟;m5 / apg / mine / export / bench / accept 零实测;`all` 端到端从未完整跑通。一次完整流程机时下界约 15–28 h,其中 **apg 2–4 h/轮、mine 6 轮都是估的**。
2. **3 mm 间隙过滤的真实剔除比例(`gapDrop`)没测过**:只测过沙箱 30 条(剔 9/29,不具代表性);上表 p50 4.4 mm 是未过滤口径。生成后看 `stats.json` 的 `gapDrop`。
3. **R2 新判据(`label_std_norm`)的池无关性**:需在 1.7 M 样本上 `--pool 50000` 与 `--pool 200000` 两跑、差 <0.02 才成立;没做。默认 `R2_STRICT=0` 只警告,开着就是一道未校准的闸。
4. **R8 出题耗时**:沙箱 2 题 9 分钟没出完;本机未测(`policy_bench.json` 会记 `requested` / `genS`,不足额时看警告)。
5. **Windows 侧生命周期全部未实测**:关控制台黑窗口训练是否继续(`CREATE_NO_WINDOW`)、`/proc/$$/winpid` + `taskkill /T` 能否结束整树、Git Bash 自带 `timeout` 能否把 TERM 送到 python / node。
6. **断点续跑的沙箱自测未在本机复核**:gen 截断续写(「续写:已有 N 条」)、train STOP 存档(rc 143、`last.pt` 第 N 步)、apg `--resume`、prep 二次运行「待打包 0 个」。
### D. 小遗留
`webapp/models/README.md` 过时:还写"由 `--fp16` 生成"、指向"第 8 步"(现为第 10 步、默认 fp32)。
---
# 第二轮:对修订版(提交 `d3b7c7b`)的可行性评估
## 三、第六轮已复核正常(不在问题清单里)
评估对象:另一台机器按上述审查做的修订(18 文件、+493/−156)。方式:**只读代码 + 只读 diff,本轮没有跑任何东西**,第一轮的实测数字(6208 样本/s、keepRate 0.977、minGap p50 4.4 mm、1.56 s/条)继续沿用。上文第一轮的正文按 0218d7f 时的代码写,路径与章节号(`ml/runs/base`、12 步)在修订后已变,不逐条订正,以本节为准。
- 锁:`r4` 阶段拿到 `ml/runs/pipeline.lock`,结束已删。
- N-5 三处防护在位:查看器拒绝 `random_init`(提示 `&probe=1`)、`policy_bench.cjs` 拒绝、`acceptance` 有"部署模型是训练产物"项。
- `enc_once` 运行时对接一致:只送 1 行特征(`ik-policy-glue.js:52` / `ik-policy.js:344`),旧模型回退 `[B,F]`。
- `r4` 导出:19.76 MB、`enc_once=true`、与 torch 对拍 2.2e-7。
## 一、第一轮 13 条的处置
## 四、建议顺序
| 第一轮条目 | 状态 | 落点 |
|---|---|---|
| P0-1 R10 吞吐判据矛盾 | 已修 | 方案 §10.1 R10 改为"60000 步 ≤ 3.5 h,即 ≥5000 样本/s",缓解改成 `fk_caps` 96→48 / `steps`→40000(增大 batch 明确标注无效);`ml/train.py:101` 加 `eta_h` |
| P0-2 共识分支缺期望数字 | 已修 | README §4 加两张表(不共识实测 / 共识估算 + 异常动作);`ml/gen/stats.cjs:20` 加 `consensusDrop`、分位数加 p10;`ml/pipeline.sh` gen 阶段按实测"样本/任务"自动补量到 `TARGET_SAMPLES=1700000`(最多 2 次) |
| P0-3 WTA 回退覆盖 MeanFlow 产物 | 已修 | `pipeline.sh`:输出 `ml/runs/$TAG`、`ml/runs/bench_$TAG`;m5 通过后写 `ml/runs/selected_ckpt`,apg/export/accept 经 `ckpt()` 取;`ml/train.py:58` 与 `apg_finetune.py:51` 日志改为新训覆盖、续训追加 |
| P0-4 `python3` 在 Windows 断第 5 步 | 已修 | `run_prep.sh:8` 与各脚本统一 `${PYTHON:-python}`;README §1 加"Windows 原生 + Git Bash 实测可跑",并写明 Windows 不要用 `python3` |
| 口径-1 仿真安全层 ≠ 运行时 | 已修(说明层) | m5 同跑 `--safety` 一遍,产出 `m5_gate_<tag>.json` 的 `policy_only` / `with_sim_safety` 两块;README §7 明写"三口径互不替代,部署只看第 11 步" |
| 口径-2 门禁题假时长 | 已修 | `ml/mining/tasks.py:54` `exec_s/travel` 置 None;`ml/sim/closed_loop.py:24` `NOREF_SEC=6.0`,`time_ok_1p3`/`travel_ratio` 只在有老师参考的题上统计,新增 `n_ref`、`time_budget` 字段 |
| 口径-3 APG 定长 3.6 s + 只查端点 | 已修(但引入新问题,见下 B-2) | `apg_finetune.py:70` 展开长度随批取 `exec_s p90 × 1.2`,夹在 60–150 次推理;`:102` 铰链改端点 + 中点 |
| 口径-4 5 mm 余量压老师间隙分布 | 已改,方向不认同 | `configs/apg.json` 与 `webapp/js/ik-policy.js:174` 一起降到 3 mm;方案决策表"训练余量 5 mm"改为 3 mm,R3 回退改"提高到 4–6 mm" |
| 缺项-1 §9 验收表无执行点 | 已补 | 新增 `ml/eval/acceptance.py`(第 12 步 `accept`):held-out 到达率 ≥98%、D 类停靠 ≤老师+5 mm、起步失败=0,并汇总 R2/R7/R8/R10/M5/部署形态 |
| 缺项-2 挖错池累积 | 已修 | `ml/mining/loop.sh:21` 改为 `--pool $D/pool.jsonl` 只用本轮 |
| 缺项-3 fp16 部署 | 已修 | `pipeline.sh` export 去掉 `--fp16`,README §10 改默认 fp32,只在 `&ep=webgpu` 实测受益才另出 fp16 对比 |
| 缺项-4 `--n` 混控两集合 | 已修 | 拆成 `--nc-n` / `--route-n` / `--r8-n`,positional 过滤改用白名单 `VALOPT` |
| 缺项-5 val 为空崩 | 已修 | `closed_loop.evaluate` 空任务返回 NaN 字典;`train.py:59` 无 val 任务时提示并自动 `--no-eval` |
| 缺项-6 文件地图缺 4 文件 | 已修 | README 文件地图补 cert.cjs / pathdev.cjs / npy.cjs / toy_bimodal.py |
| 缺项-7 R2 无执行点 | 已补 | 新增 `ml/eval/check_r2.py`,prep 末尾自动跑 |
| 附带 | 新增 | R7(左右比例 + 行程)进 `ml/mining/gate.py:47-53`;R8 对抗集 + `brakeRate` 进 `tests/policy_bench.cjs`;出题与间隙计算分块防 OOM(`tasks.py:9` `min_gap_chunked`) |
## 二、修订新引入的问题(按严重度)
### B-1(最高)3 mm 是拿真机护栏迁就数据分布
依据是本轮实测"老师最小间隙 p50 4.4 mm",但正解是在认证环节把 `cert.minGap < 3 mm` 的老师样本过滤掉(`stats.cjs` 已经算这个数,逐条记录里就有),而不是把**运行时投影余量**一起降下来。认证只保证扫掠间隙 > 0,`collide.json` 的 `margin_mm` 只有 0.3 mm,`project()` 是唯一主动把臂往外推的力量(`ik-policy.js:279`)。方案 R3 原文写的回退是"训练余量**提高到** 3–6 mm 以缓解认证过保守",修订把这个值定成 3 = 把回退方向当成了默认。贴柱真实距离从 ≥5 mm 变 ≥3 mm 是机械/安全决定,需要单独确认,不该由 ML 口径推出。
### B-2 APG 两处改动耦合且没重标定
- `apg_finetune.py:102-104`:端点与中点两个 hinge 直接 `view(2, B).sum(0)` 相加 → 碰撞项数值约为旧版 **2 倍**,而 `w_col=2.0` 未动。
- `:70` + `plans_max=150`:展开最坏 60→150 次推理(2.5 倍),每步 `kin.gaps` 的位形数又 ×2(端点+中点)→ 单个 APG step 代价上界约 **4 倍**。
- 同时 margin 5→3 让铰链激活的样本数下降。一增一减方向相反,幅度都没实测。
- README §8 的"2–4 小时"沿用旧口径,没随改动更新。
后验表现无法预判:APG 之后若行程变长/贴柱反而更近,第一嫌疑是这个组合而不是学习率。
### B-3 新代码路径一次没跑,check_r2 自身可能成为闸门
- `check_r2.py:39`:`m.sum()==0` 时对空张量取 `torch.quantile(std, 0.9)` 直接报错(小样本摸底必撞;`label_std_mean` 也是 NaN)。
- 默认 `--queries 20000 --pool 200000`,分块距离矩阵 1024×200000 ≈ 0.8 GB/块,`torch.where` 再复制一份 → 峰值 2–3 GB 显存;`Samples` 本身又把 X/Y/SG 全量上卡(约 1.5 GB)。CPU 兜底时 20000×200000 fp32 = 16 GB 内存,docstring 说"降到 50000"仍然不轻。
- 它挂在第 5 步末尾,README 写"不通过先发我,不要直接训练"→ 一个自身可能 OOM/崩的硬门禁。
### B-4 R7 的左右比例被成功率污染,会漏报它想抓的东西
`gate.py:47` 的 `left(...)` 以"到达"为条件掩码。APG 把某一侧绕行全做失败时,剩余成功集里的左右比例可以纹丝不动 → 而 R7 的目标(方案:APG 压缩多模态)恰恰是这种情况。建议按全部题的首次 J1 方向统计,不筛到达。
### B-5 r8 集合没实现"两侧都要绕远"
`policy_bench.cjs:47-64` 只约束终点贴立柱 + |ΔJ1|≥140° + 直线被挡,**起点没做 `nearColumn` 检查**。方案 §10.1 R8 的口径是"两侧都要绕远",现在起点对侧无约束 → 恢复率会比设计意图偏松(判据偏松是危险的偏)。
### B-6 估时里"实测"两字一处用错、两处仍是拍的
- §2 写"1 分钟(实测 7.6 s 对拍)"——7.6 s 是 `dump_parity 300` 的数,pipeline 跑 2000 条,不同口径。
- §6 的"测速 6 分钟 / 正式 3 小时"由 6.2k 样本/s 换算,可信。
- §7 的 10–20 分钟(1000 题 ×2 遍 × 定长 6 s)、§11 的"约 1 小时"(题数已从 1070 增到 1120,且 r8 采样每条最多 200 次 `collideDetail` 重试)、§8 的 2–4 小时(见 B-2)都无依据。
### B-7 方案自标的最大不确定项 R4 仍然是空的
策略推理在 wasm 多线程、batch=8 下的 P99 从未量过:`webapp/models/` 只有一个 README.md(至今没导出过任何模型),方案 §1.1 表里的 0.19 ms 是 2D 玩具沙箱(`ml/toy/toy_bimodal.py`)的数,不是这个 4.92 M 模型。而 60 ms 一次推理的部署节奏、`safetyMs:30` 预算、`certBudget:40` 全靠它。R4 不过就要改模型规模(d=128)或提交步数(3→2),是伤筋动骨的返工,却排在第 13 步才第一次被测。
### B-8 杂项
- `build_dataset.cjs:46` 噪声增广的接受地板仍是 `S.clearance(qn, 5) > 1`,与新的 3 mm 投影不同步(一松一紧,不是矛盾但没对齐)。
- `acceptance.py:44` 用 `start_fail == 0` 硬判:held-out 约 2500 题里任何一题 0.5 s 内行程 <1° 就整项未过,判据脆。
- `pipeline.sh` m5 不通过 `sys.exit(3)` → `all` 会中止(有意,但应写进"常见问题")。
- `requirements.txt` 仍列 `onnxconverter-common`(fp16 已非默认,留着无害)。
- `tasks.py:36` 的 `column_gap(kin, b)` 仍在 2048 行整批上算(`free_q`/`straight_blocked` 已分块),单卡可过但和"已分块防 OOM"的说法不完全一致。
## 三、可行性判断
1. **前段(第 1–6 步)可执行**:数据生成、打包、两种头、闭环仿真、对拍都在本机实测过,数字与手册自洽。修订把 R10、共识补量、目录覆盖、`python3` 这几个会真卡住人的点解决了,这部分我认可。
2. **后段(第 7–13 步)逻辑闭合但无证据**:判据齐全、口径终于分开了,可"APG+挖错能不能把纯策略穿柱率压到 ≤2%"、"机时是不是 2–4 h"、"held-out 能不能 98%"一个都没跑过。别把 §8–§12 的估时当预算。
3. **不建议按现值直接跑第 7 步之前**:B-1(3 mm)是需要你拍板的安全决定,B-2(w_col 没跟着重标)会让后段任何异常都无法归因。
## 四、第 7 步之前建议先做的两件事(都不需要训练)
1. **结掉 R4**:用一个随机初始化的 4.92 M 模型导出 fp32 ONNX,走 `ml/serve.py` + `?policy=1`,在目标浏览器实测 `__POLICY.latency()` 的 batch=8 P99 与安全层 P99(方案要求 <15 ms)。不过就要改 `d`/`commit`,此时改配置的成本远低于训完 3 小时再改。
2. **把 margin 决定单独拍板**:在"过滤老师数据保 5 mm"与"放宽护栏到 3 mm"之间选一个,再把 `w_col` 与 §8 估时按选择重算。这两条是本轮唯一拦住往下跑的理由。
---
---
# 第三轮:对第二轮 B-1~B-8 的处置 + 新增训练控制台
修订方:沙箱(Hark)。基于 `655e1fb`。沙箱只做了 CPU 小数据冒烟(见第四节),所有耗时、keepRate、gapDrop 比例仍以你机器上的实测为准。
本轮改动以补丁形式交付(`git apply review2_and_monitor.patch`),确认后再推送。
## 一、B-1~B-8 处置
| 条目 | 处置 | 改动位置 |
|---|---|---|
| B-1 3 mm 拿护栏迁就数据 | **采纳你的方向**:运行时投影护栏恢复 **5 mm**;改为在数据侧剔除认证最小间隙 `cert.minGap < 3 mm` 的老师轨迹;APG 碰撞项余量 3 mm 与过滤线一致(碰撞项不与 BC 标签对抗)。护栏比数据更保守,二者不再绑定 | `webapp/js/ik-policy.js:174` margin 3→5;`ml/gen/stats.cjs` 新增 `gapDrop`、`minGapFilterMm`,样本数与补量只按过滤后计;`ml/prep/build_dataset.cjs` 同一过滤(`MIN_GAP_MM=3`,可用环境变量改),打包输出也报 `gapDrop` |
| B-2 端点+中点 hinge ×2、w_col 未重标 | 两处 hinge 由求和改为**取均值**,与只查端点同量纲,`w_col=2.0` 不用动。展开 60–150 次的单步代价不再拍估时:手册 §8/§9 改为"未实测,以监控页前 50 步换算的 ETA 为准" | `ml/apg/apg_finetune.py:104`;README 阶段表 |
| B-3 check_r2 空匹配崩、显存峰值 | 近邻对 < 20 时输出 `pass: null` 不报错;默认 5000 查询 × 50000 池、分块 256、`masked_fill_` 原地,GPU 峰值 < 0.5 GB。`pass: false` 时 prep 以返回码 4 结束、一键流程停下;`null` 不拦 | `ml/eval/check_r2.py`;`ml/pipeline.sh` prep |
| B-4 R7 以到达为条件会漏报 | 左右比例改为按**全部** B/C 题的首次 J1 方向统计,不筛到达 | `ml/mining/gate.py:47` |
| B-5 r8 起点没贴柱 | 起点也要求 `nearColumn`,两侧都要绕远 | `tests/policy_bench.cjs` r8Cases |
| B-6 估时口径 | 对拍改为"约 1 分钟(估算:300 条实测 7.6 s,流程跑 2000 条)";M5、部署评测标"估算";APG、挖错标"未实测" | README 阶段表 |
| B-7 R4 排在最后 | **提到训练前**:新增 `bash ml/pipeline.sh r4`,用随机初始化模型导出 fp32 ONNX(延迟只与结构有关),`python ml/serve.py` + `?policy=1` 测 `__POLICY.latency()`;不过就把 `model.d` 192→128、`ff` 768→512 后重测 | `ml/export_onnx.py` 新增 `--random <配置>`;`pipeline.sh` r4;README §2;方案 R4 行"M1 前" |
| B-8 杂项 | 噪声增广接受地板 1 mm → 3 mm(与过滤线一致);验收起步失败 `= 0` → `≤ 0.2%`;`column_gap` 分块 256(沙箱实测约 20 MB/位形,整批 2048 会爆);m5 返回码 3 中止 `all` 写进常见问题;`set -o pipefail` + 门禁结果先写 `.tmp` 再改名(中断不留残缺 `consistency.json`) | `build_dataset.cjs`、`acceptance.py`、`tasks.py`、`pipeline.sh`、README 常见问题 |
| `onnxconverter-common` | 保留(`--fp16` 仍可用于 WebGPU 对比) | — |
方案文档同步:决策表"训练余量"改为"数据过滤与 APG 碰撞项 3 mm、运行时护栏 5 mm";APG 碰撞项写明端点与中点取均值;R4 检测时点改为训练前。HTML 已重新生成。
**需要你确认的一处**:3 mm 过滤会剔除多少老师轨迹没实测过。沙箱 30 条小样本里剔除了 9/29(p10 = 1 mm),量太小不具代表性;你那边第一轮实测 p50 4.4 mm。生成完看 `stats.json` 的 `gapDrop`:> 25% 说明老师整体太贴柱,把 `stats.json` 发我再定。自动补量会按过滤后的样本数补,所以样本总量不受影响,只是生成时间相应变长。
## 二、新增:训练控制台(一键运行 + 全程可视化)
### 怎么用
1. Windows 双击 `ml/console.bat`(Linux/WSL:`bash ml/console.sh`),浏览器自动打开 `http://localhost:8100/`。
2. 点右上角「▶ 开始训练」,在对话框里选起始阶段、结束阶段和参数,点「开始」。
3. 之后整个流程(对拍 → 老师门禁 → 生成 → 打包 → 预训练 → M5 → APG → 挖错 → 导出 → 部署评测 → 验收)都在这一页看;「■ 停止」随时停,再开始默认从停下的那一步继续。
网页可以关,训练不受影响;关闭控制台窗口会结束训练。R2、M5 不通过时流程会停在那一步。
### 组成与接口
| 文件 | 作用 |
|---|---|
| `ml/console.bat` / `ml/console.sh` | 一键启动:`python -m ml.monitor --open` |
| `ml/monitor.py` | Python 标准库 HTTP 服务,默认只监听 127.0.0.1:8100 |
| `ml/monitor.html` | 网页,零依赖(原生 canvas 画图),不连外网;`?demo=1` 或直接双击打开时用内置示例数据 |
| `ml/pstate.py` | 写 `ml/runs/pipeline.json`:`pipeline.sh` 每个阶段开始/结束时自动调用(终端里跑也会写) |
| 接口 | 方法 | 说明 |
|---|---|---|
| `/` | GET | 网页 |
| `/api/runs` | GET | `ml/runs/*/log.jsonl` 列表(名称、类型 train/apg/bench、更新时间、是否发散) |
| `/api/log?run=&offset=` | GET | 增量读日志:只返回 offset 之后的完整行,日志被新训练覆盖时自动从头读 |
| `/api/status` | GET | GPU、`pipeline.json` + 当前阶段进度、数据统计、R2、M5、挖错各轮、部署评测、验收 |
| `/api/job` | GET | 控制台启动的流水线进程:是否在跑、PID、起止阶段、返回码、是否手动停止 |
| `/api/console?offset=` | GET | `ml/runs/console.log` 增量(首次只给最后 64 KB) |
| `/api/start` | POST | `{"from":"kin","to":"accept","env":{...}}` → 后台 `bash ml/pipeline.sh all`;env 只接受白名单:`N_TASKS NPROC TARGET_SAMPLES CFG CONSENSUS ROUNDS APG_STEPS MINE_N`,逐个正则校验 |
| `/api/stop` | POST | 结束整个进程树(Windows `taskkill /T /F`,其他系统进程组 SIGTERM,10 秒后 SIGKILL),当前阶段记为返回码 -15 |
安全:开始/停止只接受本机请求(`--host 0.0.0.0` 开放给局域网时其他机器只能看,`--remote-control` 才放开);已经有流水线在跑(控制台自己起的,或终端里起的、日志 2 分钟内有更新)时拒绝再开始。
Windows 找 bash 的顺序:环境变量 `BASH` → `C:\Program Files\Git\bin\bash.exe` → `C:\Program Files (x86)\Git\...` → `%LOCALAPPDATA%\Programs\Git\...` → `git` 所在目录推断;不用 `System32\bash.exe`(那是 WSL)。控制台把自己的 `sys.executable` 作为 `PYTHON` 传给流水线(路径含空格时不传,回落到 `python`)。
### 流水线脚本配套改动
- `bash ml/pipeline.sh all` 支持 `FROM` / `TO` 只跑一段(如 `FROM=gen TO=train`);在终端里用 `all` 时也会在后台自动起控制台(`MONITOR=0` 关闭)。
- 每个阶段开始写 `begin`(生成阶段带 `target_tasks`,补量时更新;预训练带 `tag`;挖错带 `rounds`),退出时由 `trap EXIT` 写返回码。
- `pipeline.json` 在从中间续跑时保留计划之外各步的记录,所以流水线卡片能显示"之前已完成"。
- 训练与 APG 日志每条记录新增 `total`(总步数)与 `t`(时间戳),评估记录也带 `t`,用于进度条、已用时间与 ETA。
### 网页内容
流水线阶段条 → 全流程卡(总百分比按各步粗估耗时加权、当前步骤及其进度、总用时、本步用时、剩余粗估、开始/停止)+ GPU 仪表 → 当前运行(自动跟随正在跑的训练,可手动切换)→ 指标卡(带趋势小图)→ 4 张曲线(预训练:总损失含原始值淡线、各分量、闭环评估带"最好"标记、吞吐带 R10 5000 线;APG:到达误差、损失分量、闭环评估、展开长度)→ 数据 / M5 / 挖错 / 部署评测 / 验收面板 → 终端输出(报错标红)。顶栏有平滑系数、对数纵轴、自动刷新开关。
当前步骤进度的口径:生成 = 已写任务条数 / 目标条数;打包 = 已打包分片 / 分片数;预训练 = (测速 2000 步 + 正式步数) / (2000 + 总步数);APG = 步数;挖错 = 已完成轮数 + 本轮 APG 步数;M5 = 两遍评估;对拍、老师门禁、导出、部署评测、验收无法计量,显示用时与粗估。
## 三、本轮改动文件清单
```
新增 ml/monitor.py ml/monitor.html ml/pstate.py ml/console.bat ml/console.sh
修改 ml/pipeline.sh r4 / monitor 阶段、all 支持 FROM/TO 与自动起控制台、阶段状态记录、pipefail、R2 不通过停下、补量更新目标
ml/train.py 日志加 total、t
ml/apg/apg_finetune.py 日志加 total、t;端点+中点 hinge 取均值;docstring 余量说明
ml/gen/stats.cjs gapDrop / minGapFilterMm,样本数按过滤后计
ml/prep/build_dataset.cjs 剔除 minGap < 3 mm 的轨迹;增广地板 3 mm;输出 gapDrop
ml/eval/check_r2.py 空匹配保护、默认规模、分块 256
ml/eval/acceptance.py 起步失败 ≤ 0.2%
ml/mining/gate.py R7 左右比例不以到达为条件
ml/mining/tasks.py column_gap 分块
ml/export_onnx.py --random
tests/policy_bench.cjs r8 起点贴柱
webapp/js/ik-policy.js 投影护栏 5 mm
ml/README.md 一键运行、R4、训练控制台、过滤口径、估时标注、回传文件、常见问题、文件地图
docs/Transformer轨迹拟合训练方案.md / .html 余量决策、APG 碰撞项、R4 时点
```
## 四、沙箱验证(CPU,2 核,小数据)
| 项 | 结果 |
|---|---|
| `stats.cjs` 过滤 | 30 条任务:keep 20、gapDrop 9;`MIN_GAP_MM=4.4` 时 keep 16、gapDrop 13,口径一致 |
| `build_dataset.cjs` | 20 条任务 761 样本,gapDrop 9,增广拒绝 5/226 |
| `check_r2.py` | 小样本可跑,不再报错 |
| APG 2 步 + 门禁(含新 R7) | 通过 |
| `column_gap` 分块 | 300 位形 / 块 100 正常;块 512 在 7 GB 内存下被杀 → 默认 256 |
| `export_onnx --random` | 走到导出一步,沙箱缺 `onnx` 包未完成,需在你机器上跑 `r4` |
| 控制台接口 | 网页开始 `gen→prep`(6 条任务):进度 2/6 → 4/6 → 打包 0/2 → 完成,返回码 0;停止 `gate1`:进程树结束、该步记为已停止、不留残缺 `consistency.json`、"已停止"提示不被覆盖 |
| 网页 | jsdom 跑 4 个示例场景 + 开始/停止流程,无脚本错误;沙箱没有浏览器,没有截图 |
| 未验证 | Windows 下 `console.bat` 与 Git Bash 查找、`taskkill` 停止;`policy_bench.cjs` 完整运行(沙箱无 `onnxruntime-node`) |
## 五、你机器上的建议顺序
1. `git apply review2_and_monitor.patch`(或等我推送后 `git pull`)。
2. `bash ml/pipeline.sh setup`(如依赖已装可跳过)→ `bash ml/pipeline.sh r4` + 浏览器测延迟,把 `__POLICY.latency()` 发我。
3. 双击 `ml/console.bat` → 「开始训练」,起始「对拍」、结束「打包样本」,跑完看 `gapDrop` 与 R2。
4. 没问题再「开始训练」从「预训练」跑到「验收汇总」。
---
---
# 第四轮:对第三轮(`14d72a9`)与训练控制台的审查
审查对象:B-1~B-8 的处置代码 + 新增控制台(`ml/monitor.py` 315 行、`ml/monitor.html` 795 行、`ml/pstate.py`、`ml/console.bat|sh`)+ `ml/pipeline.sh` 改动。
方式:逐行读代码;只跑了三个秒级 shell/python 验证(不碰 GPU、不跑流水线)。**控制台与第 7 步之后仍然零实测。**
## 一、B-1~B-8 处置核对
| 条目 | 代码事实 | 判定 |
|---|---|---|
| B-1 护栏 | `ik-policy.js:174` margin 回 5;`stats.cjs:7` 与 `build_dataset.cjs:30` 同一 `MIN_GAP_MM=3` 过滤,`gapDrop` 两边都报;增广地板同步到 3(`:49`) | 方向对,已落地。尾巴见 N-1 |
| B-2 hinge 量纲 | `apg_finetune.py:104` 由 `sum(0)` 改 `mean(0)` → 与只查端点同量纲,`w_col` 无需重标 | 已解决 |
| B-3 check_r2 | `fin.sum()<20` → `pass:null` 提前返回;默认 5000×50000、分块 256(一块 256×50000 fp32 ≈ 51 MB)、`masked_fill_` 原地;`pipeline.sh:72` 在 `pass is False` 时 `exit 4` | 崩溃与显存问题解决了;判据本身新增误拒风险,见 N-2 |
| B-4 R7 条件 | `gate.py:47` 分母改 `det.sum()`,不再以到达为条件 | 解决了,但口径名不副实,见 N-3 |
| B-5 r8 起点 | `policy_bench.cjs:57` 起点也要求 `nearColumn` | 已落地;出题成本未知,见 N-4 |
| B-6 估时 | 对拍改「估算:300 条实测 7.6 s,流程跑 2000 条」;M5/bench 标「估算」、APG/mine 标「未实测」 | 已解决 |
| B-7 R4 前置 | `export_onnx.py:24` 新增 `--random <配置>`(`meta.random_init`),`pipeline.sh:100` 的 `r4` 阶段,README 表新增「2′ R4 延迟」 | 思路对(延迟只取决于结构)。但导出路径会覆盖真模型,见 N-5 |
| B-8 杂项 | 起步失败 `≤0.2%`(`acceptance.py:44` 带理由)、`column_gap` 分块 256、`set -e -o pipefail`、`consistency.json` 先 `.tmp` 再 `mv` | 已解决 |
| 附带 | `pstate` 原子写(`os.replace`)、训练/APG 日志加 `total`/`t`、`all` 支持 `FROM/TO` | 结构合理 |
我这条也纠正自己一处:静态读 `pipeline.sh:117` 时我认为「端口已被占用就不重复启动」的判断写反了,实测**我错了**——端口空闲时 guard 退 0(启动)、被占用退 1(跳过),逻辑正确。
## 二、控制台的问题(按严重度,N-x 供下一轮出方案)
### N-7(最高:会真丢数据)并发无锁,且重启后"已在跑"守卫在最危险的阶段失效
- `monitor.py:212` 判断「终端里是不是另有一条在跑」的依据是 `ml/runs/*/log.jsonl` 120 秒内有更新。但 **gate1 / gen / prep / m5 / bench / accept 这些阶段根本不写 `log.jsonl`**(只有 train/apg 写)→ 在 4–5 小时的 `gen` 期间,网页「开始训练」会被放行。
- 一旦二次启动:`run_parallel.sh:8` 用同一批种子(`pipeline.sh:57` 固定 `S0=1`)生成同名 `ml/data/shard_%04d.jsonl`,而 `gen_dataset.cjs:23` 是 `fs.openSync(OUT,'w')` → **两边互相截断,谁都没法要**。
- 反方向同样裸奔:`pipeline.sh` 内部没有任何锁,网页在跑时终端敲 `bash ml/pipeline.sh all` 照样起跑。
- 触发概率不低:README 与审查文档都明确鼓励「网页可以关、重新打开接着看」,而 monitor 重启后 `self.p=None`,它自己的「已经在运行」判据(`:207`)也一起失效。
### N-8(安全)`/api/start`、`/api/stop` 没有 Origin / CSRF 校验
`monitor.py:288` 的门槛是 `client_address ∈ {127.0.0.1,::1}`,`--remote-control` 只管远端 IP。浏览器里任意第三方网页发往 `http://localhost:8100/api/stop` 的 POST,源地址就是 127.0.0.1 → **陌生网页可以停掉你跑了几小时的训练,或塞进一条新流水线**(响应被同源策略挡住,读不走数据,但破坏成立)。文档里「开始/停止只接受本机请求」给出的保证比代码实际提供的强。
### N-9(性能:整轮都在白付 IO)每次刷新把全部 shard 全文读两遍数行数
`monitor.py:157-161` 无条件对每个 shard 做 `sum(1 for _ in fh)`,`stage_progress` 的 gen 分支(`:100-101`)再读一遍;网页 5 秒轮询一次(`monitor.html:664`)。按 ~5 KB/条 × 5 万条 ≈ 250 MB 估算(**未实测**,口径是记录里带 `traj` 顶点数组),即每 5 秒约 500 MB 顺序读,而且训练/APG 阶段那段统计并没有关掉 → 与 28 个 node 写盘、训练数据加载抢同一块盘。
### N-10(环境)`console.bat` 找 Python 的方式会"看起来正常、三小时后炸"
- `.bat:6` 用 `where python` 判存在。Windows 的 WindowsApps 假壳也能通过 `where`(本机 `python3 -c` 实测 exit 1 就是它的行为),存在性检查形同虚设;`pipeline.sh:25` 用的是更强的 `command -v && python -c "import sys"`,两个启动器强弱不一致。
- 双击 `.bat` 不会激活 conda/venv → monitor 只用标准库,照样起得来;`:218` 把自己的 `sys.executable` 当 `PYTHON` 传给流水线 → `gen`/`prep` 全是 node 不受影响,**到 `train` 才 ImportError**,最坏的失败姿势。
- `console.sh` 认 `$PYTHON`,`.bat` 不认。
### N-11(生命周期)几小时的训练挂在"别关那个黑窗口"上,且中间步骤没有续跑能力
`CREATE_NEW_PROCESS_GROUP` 不脱离控制台,关窗口即杀整棵树。`train` 有 `--resume`,但 **apg 没有 resume 参数、`mine` 轮内无 resume、`gen` 补量会从头重扫全部 shard**。Linux 侧文档给了 `nohup`,Windows 侧没有等价做法。
### N-16(可观测性 + 潜在死循环)生成阶段网页看不到任何节拍
`run_parallel.sh:8` 把每个进程的进度 `2> ml/data/log_$s.txt` 重定向到文件,不流向 `console.log` → 4–5 小时的 gen 期间,网页「终端输出」面板是空的,只能看行数进度。叠上 `sampler.cjs:19-20` 的 C 类采样是**无上限 `for(;;)`**(`nearColumn` 命中率一旦因 `collide.json` 重拟合塌掉就永远出不来),故障表现就是"网页上一个字都没有、任务数不再增长",最难判断是慢还是死。
### N-5(数据踩踏)`r4` 的随机模型和真模型同名同路径
`pipeline.sh:100` 是 `--random $CFG --out webapp/models/traj_policy.onnx`,与第 10 步导出的是**同一个文件**。`meta.random_init` 没有任何消费方检查(`ik-policy-glue.js:30` 只查 fp/feat_ver/tcp0,`policy_bench.cjs:22` 同)→ 训完之后补做 R4 会把真模型盖掉,随后 `bench`/`accept`/查看器 `?policy=1` 会把随机策略当真模型跑,徽标照样显示「策略模式就绪」。
### N-2(新闸门可能误拒)R2 判据与搜索池耦合,现在是硬停
池从 20 万降到 5 万(1.7 M 样本的约 3%)→ 最近邻系统性更远 → `label_std` 系统性偏大;而阈值 0.10 是按哪个池定的没有任何记录。`pass:false` 现在以 `exit 4` 停掉一键流程,等于在最可能出假阴性的配置上加了一道会拦掉 3 小时训练的闸。代码已经输出 `nn_dist_threshold` 但没有任何地方看它。
### N-3(口径名不副实)R7 的"左右"不是首次方向
`closed_loop.py:172` 的 `side = j1_max >= -j1_min`,是**整条展开的 J1 累计行程正负极值比较**(净方向),而 `gate.py:46` 注释与第三轮文档写的是「首次 J1 方向」。来回摆动的题会被记成净方向;R7 想抓的「一侧被压缩」在"两侧都走但先左后右收回来"这种情况下会被误判。另外 `left(sd, ok)` 的 `ok` 形参现在没人用。
### N-4(成本未知)r8 三重拒绝采样的出题耗时没测
起点贴柱 + 终点贴柱 + `|ΔJ1|≥140°` + 直线被挡,四个条件串联,`tries` 上限 4e6、内层再 200 次找起点。README §11 的「R8 出题约 2 秒/题」是估的;更要紧的是 `while (cases.length<N && tries<4e6)` 到上限就**静默返回不足 50 题**(不报错),R8 判据 `≥95%` 的分母随之缩水。
### N-13(显示不可信)补量时的 `target_tasks` 可能算错但不报错
`pipeline.sh:65` 用内层 `python -c` 读旧 target 再相加。内层失败(例如 `pstate begin` 因 `|| true` 没写成、`pipeline.json` 缺 gen 记录)→ 空替换 → 实测 `$(( + per2*NPROC ))` 得 56 这类值(**我验证过:不中断、不报错**)→ 旧目标丢失,进度条 >100% 或倒退。
### N-14(总百分比偏乐观)网页权重与修订后的耗时表不一致
`monitor.html:341` 的 `STAGE_META`:`gen=1.0 / train=3.0 / mine=9.0`。共识分支的 gen 实际 4–5 小时(README 自己写的),权重仍按 1 个单位 → 「全流程总百分比」「剩余粗估」在最可能走的那条分支上系统性偏乐观。
### N-12(状态竞态)`pipeline.json` 有两个写者,其中一个非原子
`pstate.save()` 走 `.tmp` + `os.replace`(`:20-24`);`monitor.stop()` 却在 `:247` 手工 `open(PJ,'w')` 直写,且与 `:244` 的读之间没有锁 → 与子进程 `begin/end` 撞车会丢阶段记录。另:Ctrl+C / SIGKILL 时 bash 的 `trap EXIT` 不执行 → `current` 悬空 → 网页显示幻影「进行中」,同时 `:210` 的 7 天窗口把它当"终端可能还在跑"的候选。
### N-1(配置面)`MIN_GAP_MM` 没有贯通
默认两边都是 3 ✓。但它是两个 node 脚本各自读的环境变量:只给 `gen`(影响 `stats`/补量口径)不给 `prep`(影响实际进训练集的条数),或反过来,就会让「补量算的样本数」和「真实样本数」分叉,而且没有任何一致性检查。`monitor.py:175` 的 env 白名单里也没有它,网页上根本设不了。
### N-6(次要)分块尺寸自相矛盾
`column_gap` 默认 256、`min_gap_chunked` 默认 512,注释给的理由分别是「约 20 MB/位形」和「约 1.5 MB/位形」——同一台机器上同一个 `kin.gaps` 的峰值差 13 倍,说明其中至少一个是沙箱小内存机器上看来的数,24 GB 显存下 256 会把出题拖慢。未实测。
### N-15(误导风险,低)`file://` 双击就是"看起来在训练"
`monitor.html:216` 在 `file:` 协议下自动进 demo 模式并按刷新推进假数据;有 `示例数据` 标签(`:470`)但 README 没强调"别双击 html"。对不熟悉的人有误读空间。
(说明:`/api/log` 的 `run` 参数用 `re.fullmatch(r'[\w.\-]+')` 卡住了 `/` 与 `\`,路径逃不出去——这条我核过,不是问题。)
## 三、第四轮的可行性判断
1. **B-1~B-8 全部处置到位**,方向都对(尤其 B-1 改成"数据侧过滤 + 护栏回 5 mm"是我更认可的那条路)。剩下的 N-1/N-3/N-5/N-6 属于口径与副作用的尾巴,不影响能不能跑。
2. **控制台:读侧可用,写侧现在不能用。** 进度/日志/统计的接口设计干净(纯标准库、无危险输入、增量读日志、覆盖检测都对),拿来在终端跑阶段、网页只看进度是合适的。但 N-7(同名 shard 互相截断 + 守卫在最坏阶段失灵)与 N-8(浏览器可驱动的无校验控制口)这两个洞,恰好都长在「一键开始/停止」这条路径上;不补掉就别用它起训练,只把它当仪表盘。
3. **后 6 步依然零实测**,而且现在多了两道新闸:R2 的 `exit 4`(N-2 有误拒概率)和 M5 的 `exit 3`。前 6 步的可行性结论不变。
4. **不改代码就能规避的用法**(供你这两天先往下走):终端按阶段跑;网页只看进度不点开始;`r4` 手工加 `--out ml/runs/r4_probe.onnx`;`MIN_GAP_MM` 别设(保持两边默认 3);`console.bat` 换成在激活过的环境里 `python -m ml.monitor --open`。
## 四、给下一轮的待处置清单(按优先级)
| # | 要解决的问题 | 判据(怎样算解决) |
|---|---|---|
| 1 | N-7 流水线互斥 | 一条真锁(`ml/runs/pipeline.lock` + pid + 心跳,或改 shard 文件名为 `shard_<seed>_<runid>`),**任何**入口(网页/终端/`all`)都走同一把锁;并发起第二条时立刻拒绝而不是等日志判活 |
| 2 | N-8 控制口鉴权 | `start/stop` 校验 `Origin`/`Sec-Fetch-Site` 或首次访问生成的本机 token;文档口径与代码一致 |
| 3 | N-9 刷新代价 | 行数统计只在 gen 阶段且带缓存(增量 tail 或按文件大小比例),其他阶段不扫 shard;给出一次 /api/status 的实测耗时 |
| 4 | N-5 随机模型踩踏 | `r4` 输出独立路径(或不写 `webapp/models/`),并在 glue/bench/acceptance 里对 `random_init` 显式拒绝或红字警示 |
| 5 | N-10 启动器环境 | `.bat` 改用 `python -c "import torch"` 作可用性判据并尊重 `%PYTHON%`;monitor 启动时自检并把"当前 Python / 有无 torch / GPU"显示在网页上 |
| 6 | N-11 生命周期 | 给出 Windows 等价的脱离控制台起法(或训练进程自托管 + 端口锁),apg/mine 补 `--resume` 语义或至少文档化"断了从哪重跑" |
| 7 | N-16 gen 阶段可观测 | gen 的每进程节拍汇入 `console.log`(或网页读 `ml/data/log_*.txt`),并给 C 类采样加 tries 上限与告警 |
| 8 | N-2 R2 阈值 | 判据与池大小解耦(报告 `nn_dist_threshold` 并按配对距离归一,或池改成全量的固定比例并写明);`pass:false` 先降为"警告 + 需人工确认" |
| 9 | N-3 / N-4 / N-14 / N-13 / N-12 / N-1 / N-6 | 口径对齐(首次方向 vs 净方向)、r8 不足额时报 `n<50`、网页权重与耗时表同源、`target_tasks` 读失败可见、状态文件单一写者、`MIN_GAP_MM` 贯通并校验、分块尺寸按本机实测统一 |
## 五、修法分层(阻塞 / 必要 / 可选)
判据口径:**阻塞 = 不修就会毁掉已有产出或让几小时白跑;必要 = 不修会让你误判或断一次损失一整轮;可选 = 只是显示不准,不损失工作。**
### A. 阻塞:修完这 4 条才可以用网页「开始训练」,或用任何方式挂 4 小时以上的流程
| # | 要修的 | 落点 | 算修好了的判据 |
|---|---|---|---|
| A-1 | N-7 流水线互斥 | `monitor.py:207-213` 的判活依据、`run_parallel.sh:8` 的 `shard_%04d.jsonl` 命名、`pipeline.sh`(终端入口也要一把锁) | ① 已经有一条在跑时,第二个入口(网页或终端)立刻被拒并指名锁文件;② 两轮生成的输出文件名不重叠(带 run-id),故意并发一次,两边的 shard 都不被截断 |
| A-2 | N-5 随机模型踩真模型 | `pipeline.sh:100` 的 `--out`、`ik-policy-glue.js:29-32`、`policy_bench.cjs:22`、`acceptance.py` | 跑一次 `r4` 后:`webapp/models/traj_policy.onnx` 仍是训练产物(或 r4 走独立文件名),且 `random_init:true` 的模型在查看器徽标 / bench 输出 / acceptance 任一处都被显式标成"随机探针"而不是"策略模式就绪" |
| A-3 | N-10 启动器拿不到带 torch 的 Python | `console.bat:6-7`、`monitor.py:218`、monitor 启动自检 | 用一个没有 torch 的解释器起 monitor 时,网页顶栏直接标红"当前 Python 无 torch,不能开始训练"并拒绝 `/api/start`;`.bat` 尊重 `%PYTHON%`,与 `console.sh` 行为一致;用 WindowsApps 假壳路径能测出"拒绝"而不是"跑起来到 train 才炸" |
| A-4 | N-8 控制口鉴权 | `monitor.py:286-289` | 带外站 `Origin`(或无 `Sec-Fetch-Site: same-origin`)的 POST `/api/stop` 返回 403;本机网页正常;`--remote-control` 语义不变 |
### B. 必要:在对应那一步之前完成(不挡 A 组之前的操作)
| # | 要修的 | 什么时候必须完成 | 落点 / 判据 |
|---|---|---|---|
| B-1 | N-16 gen 阶段没有节拍 + C 类采样无上限 | **第一次挂 gen(4–5 小时)之前** | `run_parallel.sh:8` 的 `2> ml/data/log_$s.txt` 汇入 console 或网页可读;`sampler.cjs:19-20` 的 `for(;;)` 加 tries 上限并打印原因。判据:故意造一个命中率塌掉的 `collide.json`,gen 要么报"采样不出"要么报错退出,不会静默空转 |
| B-2 | N-2 R2 阈值与搜索池耦合,且已改成 `exit 4` 硬停 | **第一次跑 prep→train 之前** | `check_r2.py` 把 `nn_dist_threshold` 与池大小一起判,或阈值按配对距离归一;`pipeline.sh:72` 先降为"警告 + 需确认"。判据:同一份数据用 `--pool 50000` 与 `--pool 200000` 两跑,结论一致,否则判据不成立 |
| B-3 | N-11 apg/mine 无 resume,训练挂在"别关黑窗口"上 | **第一次跑第 8 步之前** | apg 支持 `--resume`、`loop.sh` 支持从 `r<k>` 续跑,或文档逐阶段写清"断了从哪重跑、损失多少";Windows 给出等价 `nohup` 的脱离控制台起法。判据:中途强杀一次,续跑不重复已完成的轮次 |
### C. 可选(先记账,等实测数出来再排)
N-9 刷新的全量行数统计、N-14 网页权重与耗时表同源、N-13 补量 `target_tasks` 丢失、N-12 `pipeline.json` 双写者与悬空 `current`、N-3 R7 净方向 vs 首次方向、N-4 r8 不足额静默、N-1 `MIN_GAP_MM` 贯通、N-6 分块尺寸不统一、N-15 `file://` demo 误导。
### 建议的并行方式
- 你(或另一台机器)现在做 **A-1…A-4**;A 组四条都不需要 GPU、不碰训练,改动面小。
- 与此同时本机按终端逐阶段跑 `r4`(用 `--out ml/runs/r4_probe.onnx`)→ `kin` → `gate1` → `gen` → `prep` → `train` → `m5`,把四个未知数拿回来:**gapDrop 比例、R2 在 1.7 M 样本上的实际判定、samples_per_s 与 eta_h、M5 双口径数字**。
- 拿到这四个数,B-1/B-2/B-3 的优先级和 C 组要不要做才有依据;现在给 C 组排序只是猜。
## 六、目标改成「一条命令无人值守跑完」,需要怎么改
上一节按"能不能马上开始跑"分层,那是把流程切成两段的做法;如果目标是 `bash ml/pipeline.sh all`(或网页一个按钮)从 `kin` 一路到 `accept` 无人值守跑完、中间不需要人在终端前守着,那么 A/B/C 的排法不适用——需要的是下面四个结构性缺口一起补,缺任何一个都谈不上"完整流程"。约束我不放松:5 mm 护栏、M5 ≤2%、`collisionFrames=0` 三道门槛照旧。
### 缺口 1:状态机不闭环——"续跑"要变成一个动作,而不是让人挑起始阶段
现状:`all` 每次 `pstate reset` 把计划内阶段的记录清空重跑;`FROM/TO` 要人判断该从哪续;`gen` 重跑会把所有 seed 从头再生;`loop.sh` 重跑会把已完成的轮次再挖一遍;只有 `train` 有 `--resume`。所以一次中断(断电、关窗口、OOM、门禁停)= 人工重建上下文。
改成:
- `pstate reset` 加 `--keep-done`:`all` 启动时读 `ml/runs/pipeline.json`,`rc==0` 的阶段跳过并保留用时,从第一个未完成阶段继续;网页「开始训练」不再问起始阶段(只问参数)。
- 阶段内幂等:`run_parallel.sh` 跳过已存在且行数达标的 `shard_%04d.jsonl`;`loop.sh` 跳过已存在 `gate.json` 的轮次并复用 `current.pt`;`gen` 补量的目标条数落进 `pipeline.json`(重入时不重复补)。
- `train` 的 `--resume ml/runs/$TAG/last.pt` 由 pipeline.sh 自动判断(存在就带),apg 加同样的 `--resume`(`last.pt` 已经在存)。
- 判据:任意时刻强杀整棵进程树,再点一次「开始训练」,日志与网页显示"跳过 k 个已完成阶段,从第 j 步继续",且各产物文件不被重复生成。
### 缺口 2:入口不互斥、进程不脱离控制台
现状即 N-7 + N-11:靠"120 s 内有日志更新"猜有没有别的实例(gen/prep/m5/bench 阶段猜不到),shard 同名互相 `openSync(OUT,'w')` 截断;`CREATE_NEW_PROCESS_GROUP` 不脱离控制台,关黑窗口就杀整条链。
改成:
- 一把真锁 `ml/runs/pipeline.lock`(pid + 心跳时间戳),网页/终端/`all` 三个入口都拿,超时未刷新即视为陈旧可抢占并打印提示。
- monitor 起子进程时加 `DETACHED_PROCESS`(POSIX 侧 `start_new_session` 已有),训练与控制台进程解耦;控制台窗口关掉只关面板。
- 判据:故意并发两个入口,第二个立刻被拒并指名持锁 pid;关闭控制台窗口 10 分钟后训练进程仍在跑且网页重开能接上。
### 缺口 3:三道"要人拍板"的门槛,要变成流程内的自动分支
现在这三处会把无人值守的长跑停死在人不在场的时刻:
| 门槛 | 现状 | 改成 |
|---|---|---|
| R2(`check_r2` `exit 4`) | 判据与池大小耦合(5 万池 = 1.7 M 的 3%,最近邻系统性偏远 → `label_std` 系统性偏大),误拒就停 3 小时训练 | 阈值与池解耦(同时看已有的 `nn_dist_threshold`,或按配对距离分位归一),并在 `pipeline.json` 记 `pool`;不通过时默认**警告并继续**,`R2_STRICT=1` 才拦 |
| M5(`exit 3`) | 不过就停下等人手敲两条 `CFG=wta` 命令 | 流程内自动分支:base 不过 → 自动 `train CFG=wta` + `m5 CFG=wta`;两头都不过才停(并把两个 `m5_gate_*.json` 一并落盘)。目录已分开了,这条纯接线 |
| gate1 → CONSENSUS | 已经自动 ✓ | 保留,但把 `consensusDrop`/`gapDrop` 双阈值写进 `pipeline.json`,让 gen 的补量次数按实际保留率放开(现在硬编码最多 2 次,共识 + 间隙过滤叠加时 2 次可能补不满 170 万 → 让它按 `TARGET_SAMPLES` 收敛到 4 次) |
| R4(浏览器延迟) | 需要人开浏览器看 `__POLICY.latency()`,于是被排除在 `all` 之外 → 流程天生不完整 | 加自动探针:`ml/eval/r4_probe.py` 起 `ml/serve.py`(已有 COOP/COEP)→ headless Edge 打开 `?policy=1` → 用页面暴露的 `__POLICY.setGoal(T,R,tool)` 注入 30 个贴柱目标 → 回收 `__POLICY.latency()`/`stats()` 写 `ml/runs/r4.json` 并判 P99 < 15 ms;探针用 `--random` 导出的独立文件名模型,不碰真模型(顺手解掉 N-5) |
判据:`all` 的计划里含 `r4`,且全程不需要人做任何一次浏览器/终端判断;三道门槛的"停"都只在两种头/两种配置都失败时才发生。
### 缺口 4:无人值守 = 失败要能被发现,长阶段要有心跳
现状:gen 的每进程节拍被 `2> ml/data/log_$s.txt` 吞掉;`sampler.cjs:19` 的 C 类采样是无上限 `for(;;)`(`collide.json` 重拟合后命中率塌掉就是静默死循环);补量的 `target_tasks` 读失败会算成 56 这类值而不报错(我验过)。
改成:
- 每个阶段在 `pipeline.json` 带 `timeout_h`(按表里估时 ×3)与心跳(`info.lastBeat`);超时 → 非零退出 → 阶段标失败,网页顶栏红字,锁释放。
- gen 的 `log_$s.txt` 尾行汇入 console(或网页直接读这些文件),并给 C 类采样加 tries 上限 + 失败原因落盘。
- monitor 启动自检并把"当前 Python 路径 / 有无 torch / GPU 名 / 锁持有者"显示在顶栏(解 N-10:不能等到 `train` 才 ImportError)。
### 这样改完之后的机时与剩余风险(明说,不当已解决)
- 一次成功的完整流程下界约 **15–28 小时**:gate1 0.5 + gen 1–5 + prep 0.5 + train 3 + m5 0.3(选中 wta 再加 3.3)+ apg 2–4 + mine 6×1–2 + export/bench/accept 1.5。其中 **apg 与 mine 每轮的机时仍是零实测**,补量/过滤叠加时 gen 可能更长。
- 补了这四组缺口,"跑不完"的风险从"中途停了没人知道、只能人肉重建上下文"降到"某一步真失败且已落盘可查";但 **跑通 ≠ 达标**:M5 的 2%、held-out 98%、`collisionFrames=0` 能不能过,仍然要这一次真跑才知道。这是我不会用改造掩盖掉的唯一一件事。
- 改动量估计(都不动模型结构与数据口径):`pipeline.sh` ~60 行、`monitor.py` ~70 行、`pstate.py` ~15 行、`run_parallel.sh`/`loop.sh` 各 ~10 行、`check_r2.py` ~15 行、新增 `r4_probe.py` ~80 行、`sampler.cjs` ~5 行。
- 顺序建议(一次提交内做完,不分批放出去):缺口 1 + 2 是"敢挂长跑"的前提,先做;缺口 4 的心跳与超时和它们同一个改动面;缺口 3 的 r4 探针最大但独立,可以并行走。
## 七、R4 实测(本机 2026-10-18,随机初始化模型,不需要训练)
复现:`python -m ml.export_onnx --random ml/configs/base.json --out webapp/models/traj_policy_r4.onnx`(3.6 s,fp32 19.77 MB,ONNX↔torch 最大误差 4.8e-7)→ `python ml/serve.py 8000` → 打开 `?policy=models/traj_policy_r4.onnx`。
环境:RTX 3090 机器、28 核、Edge(Qoder Browser Connector)、onnxruntime-web 1.20.1、`crossOriginIsolated=true`、wasm EP、`graphOptimizationLevel:'all'`。模型 = `configs/base.json`(d192/heads8/ff768/enc4/dec4,4.92 M 参数),batch=8 即部署的 8 个候选。
| 配置 | batch=8 P50 | P95 | P99 | 说明 |
|---|---|---|---|---|
| **部署现状(4 线程,在真查看器里)** | **33.2 ms** | 36.1 | **37.0** | 150 次;徽标"wasm,4 线程" |
| 4 线程(空白页,无渲染) | 32.2 | — | 35.9 | 与上一行差 ~1 ms → 查看器渲染不抢这块 |
| 8 线程 | 22.2 | 24.5 | 24.9 | 140 次 |
| **16 线程** | **13.7** | — | **19.5** | 160 次,最好的线程数 |
| 24 线程 | 19.2 | 23.8 | 25.8 | 比 16 差 → 超线程反而挤 |
batch 缩放(4 线程,同一会话):B=1 → 6.2/7.2;B=2 → 10.3;B=4 → 18.2/20.1;B=8 → 33.2/37.0;B=16 → 60.1;B=64 → 80.1(P50/P99)。
结论:
1. **R4 判据不过,差 2.4 倍**(P99 37.0 vs 要求 < 15)。而且这不是"训练完再优化"的项——延迟只取决于结构与线程,随机权重与真权重同速。
2. 一行改 `ik-policy-glue.js:35` 的 `Math.min(4, hardwareConcurrency)`(本机 16 最优)能把 P99 从 37 拉到 19.5,**仍然不达标**。所以达标必须动结构,两个方向都不是"降精度换速度"的取舍:① 部署的 8 行输入是同一观测复制 8 份,encoder 被重复算 8 次(`Runner._plan` 把 `f` 复制 K 遍、`Policy.sample_k` 里 `mem.repeat_interleave(K,0)`)→ 改成"编码一次、解码 8 次"是纯冗余消除;② K=8 降到 4(B=4 在 16 线程下大概率 <10 ms),这是候选数与选优质量的权衡,要用 M5/闭环数字判损失,不能拍。
3. **周期账**:部署每 3 步(60 ms)一次推理,安全层单次预算 `safetyMs:30`。4 线程下推理 33 ms 已吃掉一半以上——安全层只要超过 27 ms 就必然丢步;16 线程下 13.7 + ≤30 才谈得上有余量。也就是说 R4 与 R9(贴柱处安全层全程 P99 < 5 ms)是同一个 60 ms 周期的两半,必须一起测。
4. **没拿到的数(如实)**:闭环实际倍率/丢步率没采到——我挂的探针一运行,CDP 连 15 s 都取不到执行槽(连 `list_console_messages` 都超时),标签页要到切走才恢复。这本身就是主线程(投影有限差分 + 认证递归都在主线程)在 4 线程 wasm 下已经很挤的证据。R9 因此仍无数据。
5. 残留:`webapp/models/traj_policy_r4.onnx`(+`.meta.json`,共 19.8 MB) 是未跟踪文件,`meta.random_init=true`;serve.py 已停,浏览器标签已释放。它同时验证了 N-5 的隐患是真的——这个文件与第 10 步 `export` 的默认输出同名同目录,只差文件名后缀。
---
---
# 第五轮:对第四轮的处置(实现方回复,2026-10-18)
处置对象:第四轮 N-1~N-16、第六节「无人值守跑完」四个缺口、第七节 R4 实测。
结论先说:**第四轮全部认可,按第六节的目标一次改完**(不走 A/B/C 分批)。三道门槛(运行时 5 mm 护栏、M5 ≤ 2%、`collisionFrames = 0`)不动;「跑通 ≠ 达标」原样保留。
## 〇、分歧裁决规则(用户 2026-10-18 确定)
审查方与实现方意见不一致、讨论后仍定不下来时,**以实现方(Hark)的意见为准**,按它落地,不再停下来等第三方拍板。
审查方的异议照常写进下一轮文档,附上判据;下一轮如果实测数据证明实现方错了,按数据改,不另起争论。
本轮实际只有两处分歧:N-6 和 R4 的改法(见第二节)。按上述规则,两处均按实现方意见落地。
## 一、逐条处置
| 条目 | 处置(代码落点) | 自测 |
|---|---|---|
| N-7 并发无锁 | `ml/pstate.py` 新增互斥锁 `ml/runs/pipeline.lock`(`O_EXCL` 原子创建;pid + 每 60 s 心跳;5 分钟无心跳视为失效可接管)。`pipeline.sh` 在**任何**阶段入口先拿锁,`all` 的子阶段继承;监控页判断是否在跑只看锁,不再看日志新鲜度。生成改为先写 `shard_XXXX.jsonl.part`、写完才改名 | 先起 gate1,再起 gen → 立即拒绝(返回码 9,报出持锁 pid);TERM 后阶段记 143、锁释放 |
| N-8 控制口无校验 | `/api/start`、`/api/stop` 要求自定义头 `X-Console: 1`,`Origin` 和 `Sec-Fetch-Site` 有值时必须同源。跨站页面加自定义头会触发 CORS 预检,本服务不应答 OPTIONS,浏览器就不会发出请求 | 不带头 → 403;带头但 `Origin: https://evil.com` → 403;同源 → 200 |
| N-9 刷新全量读 shard | 行数按文件增量统计(记住偏移,只读新增字节),只在 gen 阶段做;其他阶段用 `stats.json` 的 `tasks` | — |
| N-10 启动器 Python | `console.bat` 尊重 `%PYTHON%`,判据改为 `import torch`;`console.sh` 依次试 `PYTHON` / `python` / `python3`,判据相同。monitor 启动后后台自检(训练用 Python、torch 版本、GPU、node、bash),不过则网页「开始训练」不可点、`/api/start` 拒绝;`all` 开头再查一次 `import torch` | 沙箱自检输出正确(CPU torch 给出「看不到 GPU」警告) |
| N-11 生命周期 | Windows 子进程用 `CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP`(自带隐藏控制台,关掉控制台黑窗口不连带结束);锁里记 Windows pid(Git Bash 的 `/proc/$$/winpid`),重开的控制台可按锁停止。APG 新增 `--resume`(`last.pt` 带优化器状态);`loop.sh` 跳过已有 `gate.json` 的轮次、本轮 APG 断了从该轮 `last.pt` 续跑;`train` 自动带 `--resume`,测速 2000 步做过就跳过 | APG 2 步 → `--resume` 续到 4 步,日志「从第 2 步继续」,`last.pt` step=4 含 opt |
| N-16 gen 无节拍 / 死循环 | `run_parallel.sh` 每 60 s 往控制台打一行节拍(已写条数 + 各进程最新进度),逐个 `wait` 子进程查返回码(原来的裸 `wait` 恒为 0),任一种子失败即返回码 6 并打印该种子日志尾部。`sampler.cjs` 所有拒绝采样设次数上限,超限抛错并写明「哪一类位形连续 N 次未命中」 | 4 条任务小跑:重跑时已完成种子跳过;补量第 1 次触发并写对 `target_tasks` |
| N-5 随机模型踩真模型 | `r4` 导出到 `ml/runs/r4/traj_policy_r4.onnx`,不再写 `webapp/models/`;查看器遇到 `random_init` 默认拒绝(`&probe=1` 才放行,徽标标红「随机探针模型」);`policy_bench.cjs` 拒绝;`acceptance` 新增「部署模型是训练产物」一项 | bench 加载随机模型 → 拒绝退出 |
| N-2 R2 与池耦合 | 判据改为 `label_std_norm`:在距离较近的一半近邻对上做「标签差 ~ 近邻距离」线性回归,取距离 → 0 的截距。池变大只会让近邻更近、点更多,截距估的都是同一输入下的固有分歧;旧口径 `label_std_mean` 保留为参考,并记录 `pool`。不通过默认**警告并继续**,`R2_STRICT=1` 才停;验收里 R2 改为参考项 | 小样本可跑通(数值无意义);池无关性要用 1.7 M 样本的 `--pool 50000` 与 `--pool 200000` 两跑验证(差 < 0.02 为成立) |
| N-3 R7 口径 | `closed_loop.py` 新增 `j1_first`:J1 累计偏离首次超过 5° 时的方向,即真正的首次方向;全程不到 5° 的退回净方向。`gate.py` 去掉无用形参 | 语法检查 |
| N-4 r8 不足额静默 | bench 记录 `requested` 与出题耗时 `genS`,不足额时打印警告;acceptance 对 `n < requested` 判不通过 | 见第三节 |
| N-12 双写者 | 停止后的状态改写走 `pstate.mark_stopped()`(原子替换);`pipeline.json` 只有 pstate 一个写者 | 网页停止 → 阶段记 −15、`current` 清空、锁删除 |
| N-13 target 读失败 | 新增 `pstate get`,读不到时非零退出;`set -e` 下直接失败,不再算出 56 这类值 | `get gen target_tasks` 缺失 → rc 1 |
| N-14 权重不同源 | 阶段名、估时、超时上限统一在 `pstate.STAGES`,由 `/api/status` 下发给网页;gen 权重 1.0 → 5.0(共识分支上界) | 示例页 4 个场景无报错 |
| N-1 `MIN_GAP_MM` | `pipeline.sh` 统一 `export MIN_GAP_MM=3`,并作为参数记进 `pipeline.json`(改了会使生成及之后阶段作废);prep 开头比对 `stats.json` 的 `minGapFilterMm`,不一致返回码 8;网页可设 | `MIN_GAP_MM=4` 跑 prep → 报错退出 |
| N-6 分块 | **分歧,见第二节**。改为按空闲显存自动定块(用 1/4,256~8192) | — |
| N-15 file:// demo | 示例模式在页面顶部显示黄色提示条;README 写明不要直接双击 html | — |
第六节四个缺口的对应:
| 缺口 | 落地 |
|---|---|
| 1 状态机闭环 | `pstate reset` 默认保留 rc=0 的阶段;`all` 只跑 `pstate todo` 列出的阶段。**参数变了只作废受影响的阶段**(`AFFECTS` 表:生成参数 → gen 起;`CFG` → r4 和 train 起;挖错参数 → mine 起),被作废阶段的产物移到 `ml/archive/<时间>/`,不删除;`FRESH=1` 全部作废。网页不再问起始阶段,只选「续跑 / 从头重跑」 |
| 2 互斥 + 脱离控制台 | 见 N-7、N-11 |
| 3 门槛自动分支 | R2 默认警告继续;M5 base 不过 → 流程内自动训 WTA 再判,两头都不过才停(`AUTO_WTA=0` 关闭);补量上限 2 → 4 次,补完仍不足 90% 目标时警告并继续;R4 自动探针(第二节) |
| 4 心跳与超时 | 每阶段 `timeout_h` = 估时 ×3(m5 按含 WTA 分支计),`all` 用 `timeout -k 120` 包住每个阶段,超时整组进程收到 TERM,阶段记 `timed_out`、放锁;网页显示「本步粗估 / 上限」与「超过时间上限,判定卡死」 |
自测(沙箱、秒级):用 `TIMEOUT_SCALE=0.002` 把 gate1 上限压到 10 s → 返回码 124,阶段标 `timed_out`,node 子进程全部结束,锁释放。
## 二、两处分歧(按第〇节规则以实现方意见落地)
### N-6:不是自相矛盾
`kin.gaps` 按 `d² ≤ (胶囊对下限 + cap)²` 筛候选对。`min_gap_chunked` 用 `cap = 0.5 mm`,每个随机位形幸存约 2800 对(≈ 1.5 MB);`column_gap` 用 `cap = 40 mm`,40 mm 以内幸存的对多一个量级(≈ 20 MB)。两处的峰值差 13 倍是 cap 不同造成的,两个默认块大小在各自的 cap 下都对。
第四轮说得对的是「24 GB 显存上 256 偏小、拖慢出题」,所以块大小改为按空闲显存自动计算(`tasks.auto_chunk`),两处只需传入各自的每位形峰值。
### R4:33 ms 里不全是模型本身的开销
第四轮的结论是「必须动结构,二选一:去掉重复 encoder,或 K=8 → 4」。我的处置不同:
1. **推理移出主线程(第四轮没提,最重要)**:原来 `ik-policy-glue.js` 没开 `ort.env.wasm.proxy`,wasm 推理在主线程同步执行。第七节第 4 条里「CDP 15 s 拿不到执行槽」就是这个原因;50 Hz 的 `tick` 也被一起拖住,所以丢步不只来自推理时长本身。现在默认开代理 Worker(`&proxy=0` 关),创建失败时自动退回主线程并在徽标上写明。
2. **编码一次**:同意第四轮的方向 ①。ONNX 接口改为 `feat[1,83]`、`z[K,16,6]`(`Policy.sample_shared`,`meta.enc_once`);导出时断言与旧接口逐元素一致(误差 < 1e-4);运行时按 `meta.enc_once` 只送 1 行,旧模型照常可用。按 base.json 粗算,encoder 约占总计算 29%,可省约 25%;**沙箱 CPU 实测**(onnxruntime 2 vCPU,200 次):旧接口 P50 10.03 / P99 10.34 ms → 新接口 8.03 / 8.18 ms,省约 20%。
3. **线程数**:默认 `min(16, ⌊0.6×逻辑核⌋)`(28 核机 = 16,即第七节实测最优),`&threads=N` 可覆盖。
4. **K=8 不降**:降 K 损失的是选优质量,要有 M5 / 闭环数字才能判,现在没有依据。
5. **WebGPU 必须测**:方案 R4 行本来就写了要同测 WebGPU,第七节没测。探针现在 4 种配置都测:默认线程 + Worker(部署口径)、4 线程(对照)、默认线程不开 Worker(看 `tickGapMax`,即推理是否卡住 20 ms 节拍)、WebGPU。
**判定顺序**:部署口径 P99 < 15 ms 即通过;wasm 不过而 WebGPU 过,也判通过并记 `deploy_ep: webgpu`;两者都不过,流程在训练前停下(返回码 5),改 `CFG=ml/configs/small.json`(d=128)重跑。
**预估(未实测,不当已解决)**:16 线程旧接口 P99 19.5 ms × (1 − 0.2~0.25) ≈ 14.6~15.6 ms,正好踩在线上;WebGPU 在 3090 上的数字是这一轮最关键的未知数。
**R9**:探针只测纯推理。贴柱处安全层耗时(R9)仍以 bench 的 `safetyMsP99` 为准;推理移出主线程后,安全层独占主线程,R4 与 R9 合起来要小于 60 ms 周期这笔账,等真机数据再核。
## 三、断点续跑(用户 2026-10-18 追加要求)
要求:长时间的步骤意外中断(断电、蓝屏、误关、手动停止)后能接着跑,不从头来。阶段级跳过上一版已有,这一版把**阶段内部**也做成可续:
| 阶段 | 续跑粒度 | 做法 |
|---|---|---|
| gen | 每条任务 | 第 i 条任务的随机数只由(种子, i)决定(`sampler.reseed`);`.part` 里保留完整的行、截掉写了一半的最后一行,从下一条接着写。沙箱验证:截断后续写的结果与一次写完逐条一致(id、q0、keep 全同)。 |
| prep | 每个 shard | `tasks.jsonl` 最后写且先写 `.tmp` 再改名,它存在且比 shard 新就跳过;shard 已不存在的旧打包删掉。 |
| train / WTA | 最多 10 分钟 | 每 10 分钟原子写 `last.pt`(`ckpt_min`,先写 `.tmp` 再 `os.replace`,写到一半断电旧档仍完整),含优化器、步数、最好成绩、早停计数;发散时不再用发散权重覆盖 `last.pt` / `best.pt`。 |
| apg / 挖错 | 最多 10 分钟 | 同上;挖错已过门禁的轮次跳过。 |
| m5 | 半个门禁 | 比检查点新的 `m5_<头>_policy.json` / `_safety.json` 跳过;结果文件改为原子写;检查点不存在按不通过处理(直接进入 WTA 分支)。 |
**停止也不丢进度**:网页「停止」在训练类阶段先写 `ml/runs/STOP`,训练进程 2 秒内看到后存档、以 143 退出,最多等 90 秒才硬杀(Windows 的 `taskkill /F` 收不到信号,所以靠文件)。终端 Ctrl+C / `kill` 走信号,同样先存档。新一次运行拿到锁时清掉旧的 STOP。
沙箱验证:STOP 文件 → rc 143、`last.pt` 在第 180 步,续训从 180 到 200 正常收尾;`kill -TERM` → rc 143 并存档;`kill -9`(模拟断电)→ 定时存档的 `last.pt` 完好可读;打包第二次运行「待打包 0 个」。
不续的只有几分钟级的步骤(kin、gate1、r4、export、bench、accept),整步重跑。
## 四、仍未验证(如实)
- **Windows 侧全部没有实测**:`CREATE_NO_WINDOW` 下关掉黑窗口训练能否继续跑;`/proc/$$/winpid` 与 `taskkill /T` 能否结束整棵树;Git Bash 自带的 `timeout` 能否把 TERM 送到 python.exe / node.exe 子进程(Linux 下已验证会结束整个进程组)。
- **R4 探针没在真浏览器跑过**(沙箱没有 Chromium):onnxruntime-web 1.20 的 proxy 从 jsDelivr 加载能否建出 Worker(建不出会自动退回并写明 `proxyError`)、无头模式下 WebGPU 是否可用。
- R2 新判据的池无关性只能在 1.7 M 样本上验证(判据见第一节表)。
- R8 出题耗时:沙箱 2 vCPU 出 2 题 9 分钟仍未出完,已停掉,改在本机测;`policy_bench.json` 的 r8 段会记录请求题数、实际题数和出题耗时。
- 训练 / APG / 挖错的机时仍然零实测,第七节之后的结论不变。
## 五、请在本机按顺序验证
1. `git apply review5_fixes.patch`(基于 `e3bb783`)。
2. 激活训练环境,双击 `ml/console.bat`:页面「全流程」卡片底部应显示 Python / torch / GPU;用 `set PYTHON=C:\不存在\python.exe` 再双击,应直接提示「不能 import torch」。
3. 终端单跑 `bash ml/pipeline.sh r4`,把 `ml/runs/r4.json` 发我:4 种配置的 P50/P99、`proxy` 是否为 true、`tickGapMax`、WebGPU 数字。
4. 网页「开始训练」(续跑),在 gen 节拍出现后:① 另开 Git Bash 跑 `bash ml/pipeline.sh gen`,应被拒绝(返回码 9);② 关掉控制台黑窗口,任务管理器里 node 进程应仍在;③ 重新双击 `console.bat`,页面应显示「由上一个控制台窗口启动」,点「停止」应结束全部 node 进程。
5. 其余照常跑到 prep,把 `stats.json`(`gapDrop`)与 `r2.json`(`label_std_norm` 和 `label_std_mean` 两个口径)发我。
6. 断点续跑:gen 跑到一半点「停止」再点「续跑」,`ml/data/log_*.txt` 应出现「续写:已有 N 条」;train 跑过 10 分钟后点「停止」,终端应出现「已存档 last.pt(第 N 步)」,续跑后出现「续训:从第 N 步继续」。
1. A 组三条都是分钟级修复;修完立刻重跑 `bash ml/pipeline.sh r4`,验收 `pass` 非 null。
2. 然后 `FROM=gen TO=prep` 跑一次(数小时),拿 `gapDrop` 与 R2 两个口径的数——C-2 / C-3 随之消掉,B-2 的 Node 侧安全层数字可顺手从 m5 前的 bench 拿。
3. B 组等上面的数字齐了再定;C-1 / C-5 / C-6 随第一次真跑逐项核对。