第六、七轮:离线 ort/WebGPU、R9 查看器整周期、安全层截止下沉、enc_once 前瞻 bug(feat 动态轴)、UTF-8、断点 RNG 续跑与 --stop-at、R2 池无关性自检、R8 出题缓存、本机自检 selftest、冒烟 smoke;遗留清单并入审查文档第六节

This commit is contained in:
Hark
2026-10-18 06:40:25 +08:00
committed by cbb
parent b6141d237c
commit 591678703a
29 changed files with 715 additions and 127 deletions
+3
View File
@@ -46,3 +46,6 @@ webapp/models/*.meta.json
# 从头重跑时旧产物挪到这里(不入库) # 从头重跑时旧产物挪到这里(不入库)
ml/archive/ ml/archive/
# onnxruntime-web 本地副本(bash ml/prep/vendor_ort.sh 生成,约 50 MB,不入库)
webapp/vendor/ort/
+97
View File
@@ -82,3 +82,100 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮
1. ~~A 组修复并重跑 `r4`~~:已完成(本机 2026-10-18,rc=0、P99 11.25)。 1. ~~A 组修复并重跑 `r4`~~:已完成(本机 2026-10-18,rc=0、P99 11.25)。
2. 接着 `FROM=gen TO=prep` 跑一次(数小时),拿 `gapDrop` 与 R2 两个口径的数——C-2 / C-3 随之消掉,B-2 的 Node 侧安全层数字可顺手从 m5 前的 bench 拿。 2. 接着 `FROM=gen TO=prep` 跑一次(数小时),拿 `gapDrop` 与 R2 两个口径的数——C-2 / C-3 随之消掉,B-2 的 Node 侧安全层数字可顺手从 m5 前的 bench 拿。
3. B 组等上面的数字齐了再定;C-1 / C-5 / C-6 随第一次真跑逐项核对;A-3(离线 ort 加载)交给实现方。 3. B 组等上面的数字齐了再定;C-1 / C-5 / C-6 随第一次真跑逐项核对;A-3(离线 ort 加载)交给实现方。
---
## 五、实现方处置(第六轮,2026-10-18)
逐条对应第二部分。分歧按第五轮第〇节规则以实现方为准,下一轮用实测数据定。
**A-3 离线 ort 加载**:已修。`ik-policy-glue.js` 与 `r4_probe.html` 的本地前缀改为绝对 URL(`new URL('vendor/ort/', location.href)`),ORT 的 `import()` 不再把它当裸模块名。新增 `ml/prep/vendor_ort.sh`(`setup` 与 `r4` 自动调用)用 `npm pack onnxruntime-web@1.20.1` 把 `ort.min.js`、`ort.webgpu.min.js` 与两套 wasm/mjs 放进 `webapp/vendor/ort/`(33 MB,`.gitignore`);探针检测到本地副本就默认用它,不再依赖间歇不可达的 jsdelivr(`ORT_LOCAL=0` 强制 CDN)。
**GBK 兜底**:除了本机已补的 7 处,`pipeline.sh`、`console.bat`、`console.sh`、监控起的子进程统一 `PYTHONUTF8=1`,所有 Python 读写默认 UTF-8,不再逐处补 `encoding`。
**B-1 R4 口径**:R4 保持纯推理判据(随机权重只代表推理开销,这点不变)。整周期另立 **R9**,用**训练好的模型**在**真查看器**里测:`python -m ml.eval.r4_probe --viewer`(`bench` 阶段自动跑,写 `ml/runs/r9_browser.json`),查看器 `&autotest=30` 跑与 Node bench **同种子同题**的 30 道贴柱题,判据 **整次规划 `planP99` < 45 ms 且饥饿节拍 < 1%**(饥饿 = 执行时钟到点时队列为空 = 实际丢步)。`r4.json` 另带 `viewer_ref`(随机模型的同一测试),只作参考不判定——随机策略几乎每次都要认证多条候选,安全层路径不代表真模型。验收汇总加 R9 一项。方案 R9 行同步改写(原"安全层 P99 < 5 ms"不可测也不对口径)。
**B-2 安全层两把尺**:找到了原因,而且 Node 侧也一样慢,不是浏览器独有。沙箱 Node 单次调用:`gapMatrix` cap20 **3.6 ms** / cap40 **5.5 ms**、`collideFree` 1.5 ms、`criticalPairs` 3.1 ms,而推理只有 **1.5 ms**——**耗时在几何查询,不在模型**。具体三处:
1. 截止只在候选之间检查,单条候选内部最多 40 次查询(≈150 ms)不受限 → 墙钟截止下沉到认证递归内部:第 1 条候选截止 45 ms,其余共享到 30 ms,到点判不过(`certFailWhy.time`),沿已认证刹停段停,安全性不变。
2. `safetyMs` 从推理结束开始计,**把前瞻(第二次推理 + 8 份特征 = 8 次 gapMatrix cap40)算进去了** → 拆成 `lookaheadMs` / `safetyMs` / `planMs` 三项分别记。
3. 前瞻在"卡死"时也触发,随机或差的策略几乎一直卡死 → 改为只在冷启动(新目标 / 目标跳变)前瞻,与仿真 `closed_loop.py`(只在首块前瞻)对齐;卡死由换向锁处理。
沙箱 Node、同 2 道贴柱题、小模型实测(同一机器前后对比):安全层 P99 **200.9 → 51.9 ms**,前瞻 P99 126 → 83 ms(只剩冷启动那几次),整次规划 P99 **236.7 → 63.3 ms**(P50 57.5)。代价是沙箱上 `time` 截止判不过占多数、刹停多(2 vCPU 上每次 gapMatrix 约 4 ms,45 ms 只够约 10 次查询;未训练小模型本身也差)——你机器单核快 2× 左右,真模型候选第 1 条通过率高,R9 以本机实测为准;若本机 R9 仍不过,下一轮把几何查询(特征、投影、认证)整体搬进 Worker,主线程只执行。
另外 bench 与查看器都记 `certQueriesP99`、`certMemoHitRate`(沙箱 0.69–0.72)、`certFailWhy`;验收加"同题安全层 浏览器 / Node ≤ 3×"一项。
**顺带发现并修掉一个真 bug**:第五轮 `enc_once` 后,前瞻的 8 行特征**各不相同**,但 `ortInfer` 只送了第 1 行 → 8 个候选的前瞻价值全是同一个观测算的,冷启动时的候选排序等于随机。现在 ONNX 的 `feat` 轴改为动态(1 行 = 主推理编码一次;K 行 = 前瞻逐行编码,`meta.feat_dyn=true`),导出时对拍 8 行不同特征与 `sample()` 一致(2e-7 量级);第五轮导出的旧模型运行时自动退化为逐行推(正确但慢)。**你本机的 r4 模型需重新导出**(`bash ml/pipeline.sh r4` 会重导)。
**B-3 WebGPU**:`backend not found` 的直接原因是 `ort.min.js` 里**没有 WebGPU 后端**(只在 `ort.webgpu.min.js`)。查看器与探针在 `ep=webgpu` 时改载 `ort.webgpu.min.js`;探针先记 `navigator.gpu` 与适配器信息(区分"没有 GPU"与"包不对");WebGPU 配置改用有界面窗口打开(无头模式拿不到真适配器),跑完自动关。
**D**:`webapp/models/README.md` 已改为 fp32、第 10 步、新输入签名。
**C 组**:第七轮已处置,下一步见第六节。
## 六、实现方处置与待本机验证(第七轮,2026-10-18)
本节是遗留问题的**唯一清单**:不需要计算的已在代码里修完(「本轮已修完」);需要计算的只有一条技术验证命令(「本机技术验证」),由你的 PC 跑,沙箱不跑;做法已全部定下(「已定的做法」);需要你确认的写在「还需要你确认的」。
---
### 6.1 本轮已修完(不需要计算)
| 遗留 | 处置 | 位置 |
|---|---|---|
| A-3 离线 ort 加载失败 | 本地前缀改绝对 URL;`ml/prep/vendor_ort.sh` 放好 ort 本地副本,探针默认用本地、不依赖 CDN | `ik-policy-glue.js`、`r4_probe.html`、`vendor_ort.sh` |
| GBK 读中文 json | 全流程 `PYTHONUTF8=1`(pipeline / console.bat / console.sh / 监控子进程) | `pipeline.sh` 等 |
| B-1 R4 口径 | R4 仍判纯推理;整周期另立 R9(真查看器、训练好的模型、与 bench 同题:`planP99` < 45 ms、饥饿 < 1%),验收加 R9 | `r4_probe.py --viewer`、`acceptance.py` |
| B-2 安全层两把尺 | 原因定位:几何查询慢(gapMatrix 3.6–5.5 ms/次),不是模型。截止下沉到认证内部;耗时拆 4 项记录;前瞻只在冷启动;验收加"同题浏览器 / Node ≤ 3×" | `ik-policy.js`、`policy_bench.cjs` |
| 前瞻 bug(新发现) | enc_once 后前瞻只送 1 行特征 → feat 轴改动态(`meta.feat_dyn`),导出对拍 K 行 | `policy.py`、`export_onnx.py` |
| B-3 WebGPU 零数据 | 根因:`ort.min.js` 不含 WebGPU → 改载 `ort.webgpu.min.js`;有界面窗口测;记录适配器信息 | 同上 |
| C-3 R2 池无关性 | 每次 R2 自动以 4 倍池再测一次,记 `pool_check.independent`(差 < 0.02) | `check_r2.py` |
| C-4 R8 出题耗时 | 出过的题按碰撞指纹缓存,`policy_bench.json` 记 `genS`、`casesCached` | `policy_bench.cjs` |
| C-5 / C-6 本机生命周期与续跑 | 新增一条命令自检(见「本机技术验证」 V1) | `ml/selftest.sh`、`ml/selftest_proc.py` |
| C-1 第 7–13 步零实测 | 靠断点续跑兜底(V1 验证),正式训练分三段跑、出错从该阶段续;另备 `ml/smoke.sh` 全流程冒烟,不必跑 | `ml/smoke.sh`、`configs/smoke.json` |
| 续跑后数值不一致 | `last.pt` 存 torch / CUDA / random 随机数状态,续跑 batch 顺序与一口气跑完一致 | `ml/common/ckpt.py` |
| D 模型目录说明过时 | 已改(fp32、第 10 步、新签名) | `webapp/models/README.md` |
沙箱只做了语法检查与几秒级的对拍,**以上全部以你本机结果为准**。
---
### 6.2 本机技术验证(只验可行性与意外,不做正式训练)
一条命令,5–10 分钟,在仓库旁的副本目录里跑,不碰本仓库的数据与产物,跑完副本可删。
| # | 命令(Git Bash,仓库根目录) | 耗时 | 回传 | 验证什么 |
|---|---|---|---|---|
| V1 | `bash ml/pipeline.sh selftest` | 5–10 分钟 | `ml/runs/selftest.json` | 意外中断能否恢复:gen 截断续写、prep 二次运行、训练 STOP / 续训、APG 续跑;Windows 进程:控制台停止能否杀净整棵树、关掉黑窗口后流程是否继续、TERM 能否送到 python |
判读:`all_pass = true`。没过就把文件发我,不用排查。
不单独跑全流程冒烟:V1 通过即证明断点续跑可靠,正式训练中途某一步出错时,已完成的阶段与训练进度都保留,我修完后从出错那一步接着跑(`FROM=<阶段>`),损失只是出错到发现之间的空闲时间。为缩短这段时间,正式训练按"数据 → 训练 → 后训练与评测"三段跑,每段结束看一眼(见「正式训练」)。`ml/smoke.sh` 保留在仓库里,不作为必跑项。
---
### 6.3 已定的做法
| 事项 | 定为 |
|---|---|
| R9 通过线 | 整次规划 P99 < 45 ms、饥饿节拍 < 1%(60 ms 周期留 15 ms 给渲染) |
| 浏览器测试 | 有界面窗口(无头拿不到真 GPU) |
| R9 本机不过 | 几何查询(特征、投影、认证)整体搬进 Worker,主线程只执行 |
| 训练余量 3 mm、`R2_STRICT`、共识过滤 | 按正式数据段的 `stats.json` / `r2.json` 定:间隙 p10 ≥ 3 mm 保持 3 mm,否则降到 p10;R2 `pool_check.independent = true` 且截距 < 0.10 时打开 `R2_STRICT`;门 1 < 0.90 自动开共识 |
---
### 6.4 还需要你确认的
无。V1 结果回来后,我只按结果改代码;若结果暴露出需要你决定的事,再写进本节。
---
### 6.5 正式训练(以后再跑,不在本轮)
V1 通过后再跑,分三段,每段跑完把回传文件发我:
| 段 | 命令 | 耗时 | 回传 |
|---|---|---|---|
| 数据 | `FROM=kin TO=prep bash ml/pipeline.sh all` | 数小时 | `ml/data/stats.json`、`ml/data/r2.json`、`ml/runs/r4.json` |
| 训练 | `FROM=train TO=m5 bash ml/pipeline.sh all` | 约 3–6 小时 | `ml/runs/m5_gate_*.json` |
| 后训练与评测 | `FROM=apg TO=accept bash ml/pipeline.sh all` | 约 10–20 小时 | `ml/runs/acceptance.json`、`policy_bench.json`、`r9_browser.json` |
任何一段中途出错:把出错信息发我,我修完后用 `FROM=<出错阶段>` 接着跑,前面的不重做。
+19 -13
View File
@@ -1298,7 +1298,10 @@ k 的最大距离,随机 4000 位形采样再放大 5%;末端组对 J1…J6
7.37 / 4.47 / 4.48 / 2.47 / 0.69 mm/°)。间隙取全部 |Δg| ≥ 2 胶囊对的 7.37 / 4.47 / 4.48 / 2.47 / 0.69 mm/°)。间隙取全部 |Δg| ≥ 2 胶囊对的
距离 − (r_i + r_j + margin)。外加关节限位。先用离散 距离 − (r_i + r_j + margin)。外加关节限位。先用离散
<code>collideFree</code> 剔除明显干涉的候选;运行时每条候选最多 40 <code>collideFree</code> 剔除明显干涉的候选;运行时每条候选最多 40
次距离查询、每次推理安全层总预算 30 ms,超出按刹停处理。</li> 次距离查询;墙钟截止下沉到认证内部:第 1 条候选截止 45
ms,其余候选共享到 30
ms,到点即判不过、沿已认证刹停段停。安全层耗时在几何查询而不在模型(沙箱
Node 每次 gapMatrix 3.6–5.5 ms、collideFree 1.5 ms,推理 1.5 ms)。</li>
<li>离散采样检查(现有 <code>pathFine</code>,1.5° + <li>离散采样检查(现有 <code>pathFine</code>,1.5° +
半格)只作快速预筛,不作保证依据。</li> 半格)只作快速预筛,不作保证依据。</li>
<li><strong>可刹停不变式</strong>:只有当"候选前 3 步 + 从第 3 <li><strong>可刹停不变式</strong>:只有当"候选前 3 步 + 从第 3
@@ -1518,16 +1521,14 @@ held-out 闭环仿真(开仿真安全层)测量,由
</tr> </tr>
<tr class="even"> <tr class="even">
<td>R4</td> <td>R4</td>
<td>浏览器推理延迟</td> <td>浏览器纯推理延迟</td>
<td>训练前用随机初始化模型导出 <td>训练前用随机初始化模型导出
ONNX(延迟与权重无关),<code>pipeline.sh r4</code> ONNX(延迟与权重无关),<code>pipeline.sh r4</code> 自动测部署口径 K=8
无头浏览器自动测部署口径 K=8 P99 &lt; 15 ms(2026-10-18 本机实测旧接口 4 P99 &lt; 15 ms。2026-10-18 本机:编码一次 + 16 线程 + 代理 Worker 后 P99
线程 37.0 ms、16 线程 19.5 ms,均不达标)</td> 10.2–11.3 ms,<strong>已通过</strong>(改前 37.0 ms)</td>
<td>先做三项不改模型的改动:编码一次(沙箱 CPU 实测省约 20%)、默认线程 <td>WebGPU 达标即用 WebGPU,否则
16、推理移出主线程;仍不达标则 WebGPU 达标即用 WebGPU,否则 d=128(<code>configs/small.json</code>)。K=8 不降</td>
d=128(<code>configs/small.json</code>,约 1.8M 参数)。K=8 不降</td> <td><strong>M1 前(已结)</strong></td>
<td><strong>M1
前(最大不确定项,训练前结掉;不过流程自动停)</strong></td>
</tr> </tr>
<tr class="odd"> <tr class="odd">
<td>R5</td> <td>R5</td>
@@ -1562,9 +1563,14 @@ d=128(<code>configs/small.json</code>,约 1.8M 参数)。K=8 不降</td>
</tr> </tr>
<tr class="odd"> <tr class="odd">
<td>R9</td> <td>R9</td>
<td>安全层贴柱时耗时超标</td> <td>整周期超标(推理 + 前瞻 + 安全层 &gt; 60 ms
<td>near_column 全程 P99 &lt; 5 ms</td> 周期,执行时钟饥饿丢步)</td>
<td>Web Worker 流水线;提交步数降到 2</td> <td>训练好的模型在<strong>真查看器</strong>里跑与 bench 同一批 30
道贴柱题:整次规划 P99 &lt; 45 ms、饥饿节拍 &lt;
1%(<code>r4_probe --viewer</code> →
<code>r9_browser.json</code>);同题安全层浏览器 / Node 差 ≤ 3×</td>
<td>几何查询(features、投影、认证)移到
Worker,主线程只执行;前瞻只在冷启动时做</td>
<td>M7</td> <td>M7</td>
</tr> </tr>
<tr class="even"> <tr class="even">
+3 -3
View File
@@ -364,7 +364,7 @@ for 每个 batch(随机采样 q0 和目标,不需要老师):
### 8.3 安全层(唯一的硬保证) ### 8.3 安全层(唯一的硬保证)
- **距离下界认证**:对一段运动 q_a → q_b,若两端到障碍的最小距离之和 d(q_a) + d(q_b) > Σ_j r_j·|Δθ_j|,则整段安全;否则二分,直到全部子段通过或判不安全。按组对分别判定:组对 (g_A, g_B) 的间隙只受关节 g_A…g_B−1 带动组 g_B 影响,右端取 Σ_{k=g_A}^{g_B−1} T[k][g_B]·|Δθ_k|,T 为分组杠杆表(组 g_B 胶囊端点到轴 k 的最大距离,随机 4000 位形采样再放大 5%;末端组对 J1…J6 分别为 9.56 / 7.37 / 4.47 / 4.48 / 2.47 / 0.69 mm/°)。间隙取全部 |Δg| ≥ 2 胶囊对的 距离 − (r_i + r_j + margin)。外加关节限位。先用离散 `collideFree` 剔除明显干涉的候选;运行时每条候选最多 40 次距离查询、每次推理安全层总预算 30 ms,超出按刹停处理。 - **距离下界认证**:对一段运动 q_a → q_b,若两端到障碍的最小距离之和 d(q_a) + d(q_b) > Σ_j r_j·|Δθ_j|,则整段安全;否则二分,直到全部子段通过或判不安全。按组对分别判定:组对 (g_A, g_B) 的间隙只受关节 g_A…g_B−1 带动组 g_B 影响,右端取 Σ_{k=g_A}^{g_B−1} T[k][g_B]·|Δθ_k|,T 为分组杠杆表(组 g_B 胶囊端点到轴 k 的最大距离,随机 4000 位形采样再放大 5%;末端组对 J1…J6 分别为 9.56 / 7.37 / 4.47 / 4.48 / 2.47 / 0.69 mm/°)。间隙取全部 |Δg| ≥ 2 胶囊对的 距离 − (r_i + r_j + margin)。外加关节限位。先用离散 `collideFree` 剔除明显干涉的候选;运行时每条候选最多 40 次距离查询;墙钟截止下沉到认证内部:第 1 条候选截止 45 ms,其余候选共享到 30 ms,到点即判不过、沿已认证刹停段停。安全层耗时在几何查询而不在模型(沙箱 Node 每次 gapMatrix 3.6–5.5 ms、collideFree 1.5 ms,推理 1.5 ms)。
- 离散采样检查(现有 `pathFine`,1.5° + 半格)只作快速预筛,不作保证依据。 - 离散采样检查(现有 `pathFine`,1.5° + 半格)只作快速预筛,不作保证依据。
- **可刹停不变式**:只有当"候选前 3 步 + 从第 3 步末按每轴 a_max 减速到停"整段都通过认证时才提交。任何时候系统都持有一条已认证的停车路线,下一次推理即使完全失效也只会停下。 - **可刹停不变式**:只有当"候选前 3 步 + 从第 3 步末按每轴 a_max 减速到停"整段都通过认证时才提交。任何时候系统都持有一条已认证的停车路线,下一次推理即使完全失效也只会停下。
- 耗时:每段一次距离查询覆盖整段,不通过才二分(通常 2–6 次);按排序逐个认证,第一个通过即停;包围球预筛先行。目标每次 < 5 ms,否则放 Web Worker 与推理流水线并行。 - 耗时:每段一次距离查询覆盖整段,不通过才二分(通常 2–6 次);按排序逐个认证,第一个通过即停;包围球预筛先行。目标每次 < 5 ms,否则放 Web Worker 与推理流水线并行。
@@ -444,12 +444,12 @@ for 每个 batch(随机采样 q0 和目标,不需要老师):
| R1 | 老师在相邻任务上左右绕行跳变,标签自相矛盾 | 起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90% | 对 A* 边代价加左右偏好项,强制同区域选同侧 | M1 | | R1 | 老师在相邻任务上左右绕行跳变,标签自相矛盾 | 起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90% | 对 A* 边代价加左右偏好项,强制同区域选同侧 | M1 |
| R2 | 时间律仍有不可观测成分,闭环起不了步 | 近邻样本标签方差 < 10% 步长(`ml/eval/check_r2.py`);起步失败率 = 0(闭环 `start_fail`) | 进一步把剩余路程估计加入输入 | M1 / M5 | | R2 | 时间律仍有不可观测成分,闭环起不了步 | 近邻样本标签方差 < 10% 步长(`ml/eval/check_r2.py`);起步失败率 = 0(闭环 `start_fail`) | 进一步把剩余路程估计加入输入 | M1 / M5 |
| R3 | 距离认证过于保守,贴柱处拒绝率高,影响 30/30 | near_column 上认证拒绝率统计;离线 10⁵ 段 0.01° 审计漏检 = 0 | 训练余量提高到 4–6 mm,接受行程变长;收紧杠杆界(按位形分区) | M2 / M7 | | R3 | 距离认证过于保守,贴柱处拒绝率高,影响 30/30 | near_column 上认证拒绝率统计;离线 10⁵ 段 0.01° 审计漏检 = 0 | 训练余量提高到 4–6 mm,接受行程变长;收紧杠杆界(按位形分区) | M2 / M7 |
| R4 | 浏览器推理延迟 | 训练前用随机初始化模型导出 ONNX(延迟与权重无关),`pipeline.sh r4` 无头浏览器自动测部署口径 K=8 P99 < 15 ms(2026-10-18 本机实测旧接口 4 线程 37.0 ms、16 线程 19.5 ms,均不达标) | 先做三项不改模型的改动:编码一次(沙箱 CPU 实测省约 20%)、默认线程 16、推理移出主线程;仍不达标则 WebGPU 达标即用 WebGPU,否则 d=128(`configs/small.json`,约 1.8M 参数)。K=8 不降 | **M1 前(最大不确定项,训练前结掉;不过流程自动停)** | | R4 | 浏览器纯推理延迟 | 训练前用随机初始化模型导出 ONNX(延迟与权重无关),`pipeline.sh r4` 自动测部署口径 K=8 P99 < 15 ms。2026-10-18 本机:编码一次 + 16 线程 + 代理 Worker 后 P99 10.2–11.3 ms,**已通过**(改前 37.0 ms) | WebGPU 达标即用 WebGPU,否则 d=128(`configs/small.json`)。K=8 不降 | **M1 前(已结)** |
| R5 | MeanFlow 头在 6 轴、位姿目标下穿柱或训练不稳 | near_column 穿柱率(关安全层)≤ 2%;损失不发散 | 同骨干换 WTA K=8 头 | M5 | | R5 | MeanFlow 头在 6 轴、位姿目标下穿柱或训练不稳 | near_column 穿柱率(关安全层)≤ 2%;损失不发散 | 同骨干换 WTA K=8 头 | M5 |
| R6 | 位姿目标的多 IK 分支带来额外多峰 | 不同肘/腕分支子集到达率 ≥ 主集 − 2 个百分点 | 加目标分支标志 | M5 | | R6 | 位姿目标的多 IK 分支带来额外多峰 | 不同肘/腕分支子集到达率 ≥ 主集 − 2 个百分点 | 加目标分支标志 | M5 |
| R7 | APG 压缩多模态或拉长路径(ELMP 中路径 +23%) | 每轮门禁:B/C 类左右绕行比例变化 ≤ 5 个百分点、行程中位数比 ≤ 1.05(`ml/mining/gate.py`) | 加大 BC 锚定权重;行程项 | M6 | | R7 | APG 压缩多模态或拉长路径(ELMP 中路径 +23%) | 每轮门禁:B/C 类左右绕行比例变化 ≤ 5 个百分点、行程中位数比 ≤ 1.05(`ml/mining/gate.py`) | 加大 BC 锚定权重;行程项 | M6 |
| R8 | 卡死换向活锁 | 50 个"两侧都要绕远"对抗用例(J1 起终差 ≥ 140°、终点贴立柱,部署运行时)首次换向锁后 3 s 内到达 ≥ 95%,活锁 = 0(`tests/policy_bench.cjs` r8) | 子目标改为离散绕行锚点分类 | M6 / M7 | | R8 | 卡死换向活锁 | 50 个"两侧都要绕远"对抗用例(J1 起终差 ≥ 140°、终点贴立柱,部署运行时)首次换向锁后 3 s 内到达 ≥ 95%,活锁 = 0(`tests/policy_bench.cjs` r8) | 子目标改为离散绕行锚点分类 | M6 / M7 |
| R9 | 安全层贴柱时耗时超标 | near_column 全程 P99 < 5 ms | Web Worker 流水线;提交步数降到 2 | M7 | | R9 | 整周期超标(推理 + 前瞻 + 安全层 > 60 ms 周期,执行时钟饥饿丢步) | 训练好的模型在**真查看器**里跑与 bench 同一批 30 道贴柱题:整次规划 P99 < 45 ms、饥饿节拍 < 1%(`r4_probe --viewer` → `r9_browser.json`);同题安全层浏览器 / Node 差 ≤ 3× | 几何查询(features、投影、认证)移到 Worker,主线程只执行;前瞻只在冷启动时做 | M7 |
| R10 | 训练耗时估计偏差 | 60000 步 ≤ 3.5 h,即 2000 步测速 ≥ 5000 样本/s(3090 实测 6.2 k/s ≈ 2.8 h) | FK 损失胶囊数 96 → 48;仍不够则步数降到 40000(MeanFlow JVP 受算力限制,增大 batch 无效) | M5 | | R10 | 训练耗时估计偏差 | 60000 步 ≤ 3.5 h,即 2000 步测速 ≥ 5000 样本/s(3090 实测 6.2 k/s ≈ 2.8 h) | FK 损失胶囊数 96 → 48;仍不够则步数降到 40000(MeanFlow JVP 受算力限制,增大 batch 无效) | M5 |
### 10.2 已知缺陷 ### 10.2 已知缺陷
+14 -6
View File
@@ -29,7 +29,7 @@
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 未实测:展开长度 60–150 次推理随批变化,以监控页前 50 步换算的 ETA 为准 | `ml/runs/apg/best.pt` | | 8 APG 微调 | `bash ml/pipeline.sh apg` | 未实测:展开长度 60–150 次推理随批变化,以监控页前 50 步换算的 ETA 为准 | `ml/runs/apg/best.pt` |
| 9 挖错循环 | `bash ml/pipeline.sh mine` | 未实测:每轮 = 5 万题作答 + APG,以第 1 轮实际耗时 × 轮数为准 | `ml/runs/current.pt` | | 9 挖错循环 | `bash ml/pipeline.sh mine` | 未实测:每轮 = 5 万题作答 + APG,以第 1 轮实际耗时 × 轮数为准 | `ml/runs/current.pt` |
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) | | 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) |
| 11 部署评测 | `bash ml/pipeline.sh bench` | 估算约 1 小时(Node CPU,1120 个任务;R8 出题约 2 秒/题) | `ml/runs/policy_bench.json` | | 11 部署评测 | `bash ml/pipeline.sh bench` | 估算约 1 小时(Node CPU,1120 个任务;R8 出题耗时未实测)+ 查看器 R9 约 5 分钟 | `ml/runs/policy_bench.json`、`ml/runs/r9_browser.json` |
| 12 验收汇总 | `bash ml/pipeline.sh accept` | 10–20 分钟 | `ml/runs/acceptance.json` | | 12 验收汇总 | `bash ml/pipeline.sh accept` | 10–20 分钟 | `ml/runs/acceptance.json` |
| 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — | | 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
@@ -61,6 +61,8 @@ bash ml/pipeline.sh kin
检查:最后打印的 JSON 里 `"PASS": true`(fk、姿态、组对间隙、83 维特征、间隙梯度与 JS 一致)。 检查:最后打印的 JSON 里 `"PASS": true`(fk、姿态、组对间隙、83 维特征、间隙梯度与 JS 一致)。
`docs/collide.json` 以后只要重新拟合或重生成 `pair2d`/`roadmap`,这一步、数据、模型都要重来(指纹不符时各脚本会拒绝运行)。 `docs/collide.json` 以后只要重新拟合或重生成 `pair2d`/`roadmap`,这一步、数据、模型都要重来(指纹不符时各脚本会拒绝运行)。
**真跑之前(第七轮新增,5–10 分钟)**:`bash ml/pipeline.sh selftest`(进程生命周期与断点续跑自检 → `ml/runs/selftest.json`,不碰本仓库数据)。通过后正式流程按「数据 → 训练 → 后训练与评测」三段跑,中途出错从该阶段 `FROM=` 续跑。`bash ml/pipeline.sh smoke`(副本里极小模型全流程冒烟,40–70 分钟)备用,不必跑。待办与已定做法见 `docs/ML运行手册审查.md` 第六节。
**R4 延迟(训练前做,自动)**:推理延迟只取决于模型结构,与权重无关,所以用随机初始化模型先测,不过就在训练前改结构,不必等训完。 **R4 延迟(训练前做,自动)**:推理延迟只取决于模型结构,与权重无关,所以用随机初始化模型先测,不过就在训练前改结构,不必等训完。
```bash ```bash
@@ -71,7 +73,10 @@ bash ml/pipeline.sh r4 # 随机初始化模型导出到 ml/runs/r4/traj_pol
探针测 4 种配置:默认线程 + Worker(部署口径)、4 线程(对照旧默认)、默认线程不开 Worker(看 `tickGapMax`:推理是否卡住 20 ms 节拍)、WebGPU。 探针测 4 种配置:默认线程 + Worker(部署口径)、4 线程(对照旧默认)、默认线程不开 Worker(看 `tickGapMax`:推理是否卡住 20 ms 节拍)、WebGPU。
**通过线:部署口径 batch=8 P99 < 15 ms**;wasm 不过而 WebGPU 过也算通过,`r4.json` 记 `deploy_ep: webgpu`,查看器用 `&ep=webgpu`。 **通过线:部署口径 batch=8 P99 < 15 ms**;wasm 不过而 WebGPU 过也算通过,`r4.json` 记 `deploy_ep: webgpu`,查看器用 `&ep=webgpu`。
不通过时流程停在这里(返回码 5):改用 `CFG=ml/configs/small.json`(d=128、ff=512,约 1.8 M 参数)重跑。确认要继续设 `R4_STRICT=0`。 不通过时流程停在这里(返回码 5):改用 `CFG=ml/configs/small.json`(d=128、ff=512,约 1.8 M 参数)重跑。确认要继续设 `R4_STRICT=0`。
找不到浏览器时 `pass` 为 null、流程继续、验收标红;可用 `R4_BROWSER=浏览器路径` 指定。离线时设 `ORT_LOCAL=1` 并把 onnxruntime-web 的 dist 文件放到 `webapp/vendor/ort/`。 找不到浏览器时 `pass` 为 null、流程继续、验收标红;可用 `R4_BROWSER=浏览器路径` 指定。
onnxruntime-web 默认用本地副本 `webapp/vendor/ort/`(`setup` 与 `r4` 会自动执行 `bash ml/prep/vendor_ort.sh` 放好,约 33 MB、不入库),不依赖 CDN;`ORT_LOCAL=0` 强制走 CDN。
WebGPU 配置和查看器参考测试会弹出一个有界面的浏览器窗口(无头模式拿不到真 GPU),跑完自动关闭,期间别动它。
`r4.json` 还带一项 `viewer_ref`:随机模型在真查看器里跑 30 道贴柱题的整次规划耗时,**只作参考**(随机策略几乎每次都要认证多条候选,安全层路径不代表真模型),R9 以第 11 步为准。
手动看:`python ml/serve.py` 后打开 `http://localhost:8000/?policy=../ml/runs/r4/traj_policy_r4.onnx&probe=1`(随机模型默认会被查看器拒绝,`&probe=1` 才放行,徽标标红「随机探针模型」)。 手动看:`python ml/serve.py` 后打开 `http://localhost:8000/?policy=../ml/runs/r4/traj_policy_r4.onnx&probe=1`(随机模型默认会被查看器拒绝,`&probe=1` 才放行,徽标标红「随机探针模型」)。
## 3. 老师一致性门禁 ## 3. 老师一致性门禁
@@ -191,7 +196,9 @@ bash ml/pipeline.sh bench
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、R8 对抗 50 例(J1 起终差 ≥ 140°、终点贴立柱、两侧都要绕远)、一次性贴立柱扩展集 1000 例。各集合数量可用 `--nc-n` / `--route-n` / `--r8-n` 单独调。 跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、R8 对抗 50 例(J1 起终差 ≥ 140°、终点贴立柱、两侧都要绕远)、一次性贴立柱扩展集 1000 例。各集合数量可用 `--nc-n` / `--route-n` / `--r8-n` 单独调。
看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 为 0;`near_column_*` 与 `route40` 全部 `ok`;`near_column_1000` 0 失败;`brakeRate` < 0.02;`r8_adversarial` 的 `recoveryRate` ≥ 0.95、`livelock` = 0。 看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 为 0;`near_column_*` 与 `route40` 全部 `ok`;`near_column_1000` 0 失败;`brakeRate` < 0.02;`r8_adversarial` 的 `recoveryRate` ≥ 0.95、`livelock` = 0。
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 13 步实测。 同一步还会在查看器里跑**同一批** 30 道贴柱题(与 Node 同种子),写 `ml/runs/r9_browser.json`(会弹出浏览器窗口几分钟):
- **R9 通过线:整次规划 `planP99` < 45 ms、饥饿节拍 `starve_rate` < 1%**(整次规划 = 推理 + 前瞻 + 安全层,60 ms 周期留 15 ms 给渲染与执行时钟;饥饿 = 执行时钟到点时队列空了,即实际丢步)。
- 两边对照:`policy_bench.json` 的 `near_column_pos.safetyMsP99` 与浏览器 `safetyP99` 差 > 3× 时验收标红,用 `certQueriesP99`(每次规划新增距离查询)、`certMemoHitRate`、`certFailWhy`(`cert` / `budget` / `time` / `box`)定位。
## 12. 验收汇总 ## 12. 验收汇总
@@ -210,10 +217,11 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
浏览器打开 `http://localhost:8000/?policy=1`:右上角徽标显示「策略模式就绪」后,拖动黄球或松手,手臂由策略闭环执行(不调用任何规划器)。 浏览器打开 `http://localhost:8000/?policy=1`:右上角徽标显示「策略模式就绪」后,拖动黄球或松手,手臂由策略闭环执行(不调用任何规划器)。
- `&ep=webgpu` 改用 WebGPU 后端对比延迟 - `&ep=webgpu` 改用 WebGPU 后端对比延迟
- 控制台 `__POLICY.latency()` 查看推理与安全层的 P50/P99;`__POLICY.stats()` 查看刹停、换向、精修次数 - 控制台 `__POLICY.latency()` 查看推理、前瞻、安全层、整次规划的 P50/P99 与节拍;`__POLICY.stats()` 查看刹停、换向、精修次数
- `&autotest=30` 自动跑与 bench 同一批贴柱题,结果在 `__POLICY.autotest`
- 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守) - 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守)
- 工具偏移 ≠ 0 时自动交回原规划器 - 工具偏移 ≠ 0 时自动交回原规划器
- 离线环境:把 onnxruntime-web 的 `ort.min.js` 与 wasm 文件放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1` - 离线环境:`bash ml/prep/vendor_ort.sh` 把 onnxruntime-web 放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`(WebGPU 同样可用)
## 14. 回传给我 ## 14. 回传给我
@@ -224,7 +232,7 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
| 6 | `ml/runs/bench_base/log.jsonl`(测速)、`ml/runs/base/log.jsonl` | | 6 | `ml/runs/bench_base/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
| 7 | `ml/runs/m5_gate_*.json` | | 7 | `ml/runs/m5_gate_*.json` |
| 8–9 | `ml/runs/apg/log.jsonl`、`ml/runs/mine/r*/report.json` 与 `gate.json` | | 8–9 | `ml/runs/apg/log.jsonl`、`ml/runs/mine/r*/report.json` 与 `gate.json` |
| 11–13 | `ml/runs/policy_bench.json`、`ml/runs/acceptance.json` | | 11–13 | `ml/runs/policy_bench.json`、`ml/runs/r9_browser.json`、`ml/runs/acceptance.json` |
| 任一步出错 | `ml/runs/pipeline.json`、`ml/runs/console.log`(控制台启动时)或终端输出 | | 任一步出错 | `ml/runs/pipeline.json`、`ml/runs/console.log`(控制台启动时)或终端输出 |
## 训练控制台 ## 训练控制台
+5 -4
View File
@@ -16,7 +16,7 @@ from ml.data import Samples, load_tasks
from ml.model.policy import build, wta_loss from ml.model.policy import build, wta_loss
from ml.model.meanflow import meanflow_loss from ml.model.meanflow import meanflow_loss
from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err
from ml.common.ckpt import Saver, atomic_save from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
def load_pool(paths): def load_pool(paths):
@@ -33,7 +33,7 @@ def main():
ap.add_argument('--pool', nargs='*', default=[]); ap.add_argument('--out', default='ml/runs/apg') ap.add_argument('--pool', nargs='*', default=[]); ap.add_argument('--out', default='ml/runs/apg')
ap.add_argument('--config', default='ml/configs/apg.json'); ap.add_argument('--steps', type=int, default=None) ap.add_argument('--config', default='ml/configs/apg.json'); ap.add_argument('--steps', type=int, default=None)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0) ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
ap.add_argument('--resume', default=None, help='从 <out>/last.pt 续跑(2026-10-18 第五轮,审查 N-11:几小时的 APG 断了不再从头来)') ap.add_argument('--stop-at', type=int, default=0, help='自检用:跑到第 N 次按停止请求处理'); ap.add_argument('--resume', default=None, help='从 <out>/last.pt 续跑(2026-10-18 第五轮,审查 N-11:几小时的 APG 断了不再从头来)')
a = ap.parse_args() a = ap.parse_args()
cfg = json.load(open(a.config)); C = cfg['apg'] cfg = json.load(open(a.config)); C = cfg['apg']
if a.steps: C['steps'] = a.steps if a.steps: C['steps'] = a.steps
@@ -56,6 +56,7 @@ def main():
rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model']) rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model'])
if 'opt' in rk: opt.load_state_dict(rk['opt']) if 'opt' in rk: opt.load_state_dict(rk['opt'])
it0 = rk.get('step', 0) it0 = rk.get('step', 0)
if set_rng_state(rk.get('rng')): print('续跑:随机数状态已恢复', flush=True)
bp = os.path.join(a.out, 'best.pt') bp = os.path.join(a.out, 'best.pt')
best = torch.load(bp, map_location='cpu').get('eval', {}).get('success', -1.0) if os.path.exists(bp) else -1.0 best = torch.load(bp, map_location='cpu').get('eval', {}).get('success', -1.0) if os.path.exists(bp) else -1.0
log = open(os.path.join(a.out, 'log.jsonl'), 'a') log = open(os.path.join(a.out, 'log.jsonl'), 'a')
@@ -69,7 +70,7 @@ def main():
M = C['margin_train_mm']; t0 = time.time() M = C['margin_train_mm']; t0 = time.time()
saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存) saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存)
def save_last(n, ev=None): def save_last(n, ev=None):
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev}, os.path.join(a.out, 'last.pt')); saver.mark() atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev, 'rng': rng_state()}, os.path.join(a.out, 'last.pt')); saver.mark()
for it in range(it0, C['steps']): for it in range(it0, C['steps']):
model.train() model.train()
batch = random.choices(train_tasks, weights=w, k=C['batch']) batch = random.choices(train_tasks, weights=w, k=C['batch'])
@@ -149,7 +150,7 @@ def main():
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60} 'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60}
if it % 10 == 0: print(json.dumps(rec), flush=True) if it % 10 == 0: print(json.dumps(rec), flush=True)
log.write(json.dumps(rec) + '\n'); log.flush() log.write(json.dumps(rec) + '\n'); log.flush()
if saver.stop(): if saver.stop() or (a.stop_at and it + 1 >= a.stop_at):
save_last(it + 1); log.close() save_last(it + 1); log.close()
print(f'收到停止请求:已存档 last.pt(第 {it + 1} 次),下次运行自动续跑', flush=True); sys.exit(143) print(f'收到停止请求:已存档 last.pt(第 {it + 1} 次),下次运行自动续跑', flush=True); sys.exit(143)
if saver.due(): save_last(it + 1) if saver.due(): save_last(it + 1)
+19 -1
View File
@@ -6,7 +6,7 @@
训练循环据此先存 last.pt 再以 143 退出。Windows 上 taskkill /F 是硬杀、收不到信号, 训练循环据此先存 last.pt 再以 143 退出。Windows 上 taskkill /F 是硬杀、收不到信号,
所以控制台「停止」先写 STOP 文件、等进程自己存档退出,超时才硬杀(见 monitor.Job.stop)。 所以控制台「停止」先写 STOP 文件、等进程自己存档退出,超时才硬杀(见 monitor.Job.stop)。
""" """
import os, signal, time import os, random, signal, time
import torch import torch
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -42,3 +42,21 @@ class Saver:
return False return False
def mark(self): self.last = time.time() def mark(self): self.last = time.time()
def rng_state():
"""2026-10-18 第七轮(遗留:续跑后数值轨迹与不断点不一致):随机数状态一起存档。
训练取 batch 用 torch.randint(全局生成器,CUDA 上取 CUDA 生成器)、噪声用 torch.randn、APG 选题用 random.choices,
三者状态都恢复后,续跑的 batch 顺序与噪声与一口气跑完一致(同一设备、同一版本;cuDNN 非确定算子另计)。"""
d = {'torch': torch.get_rng_state(), 'py': random.getstate()}
if torch.cuda.is_available(): d['cuda'] = torch.cuda.get_rng_state_all()
return d # 不存 numpy 状态:训练不用 np.random,且 ndarray 过不了 torch.load 的 weights_only 默认
def set_rng_state(d):
if not d: return False
torch.set_rng_state(d['torch'].cpu() if hasattr(d['torch'], 'cpu') else d['torch']); random.setstate(d['py'])
if 'cuda' in d and torch.cuda.is_available():
try: torch.cuda.set_rng_state_all([t.cpu() for t in d['cuda']])
except Exception: pass
return True
+35
View File
@@ -0,0 +1,35 @@
{
"model": {
"d": 32,
"heads": 4,
"ff": 64,
"enc": 1,
"dec": 1
},
"train": {
"batch": 64,
"lr": 0.001,
"warmup": 10,
"steps": 300,
"wd": 0.01,
"betas": [
0.9,
0.95
],
"amp": "bf16",
"grad_clip": 1.0,
"w_mf": 1.0,
"w_fk": 0.5,
"w_smooth": 0.05,
"w_value": 0.1,
"w_sub": 0.1,
"p_neq": 0.25,
"fk_caps": 96,
"ckpt_min": 1,
"eval_every": 100,
"eval_tasks": 16,
"log_every": 10,
"patience_evals": 5
},
"_note": "冒烟专用(bash ml/smoke.sh):只验证流程能跑通,数值无意义"
}
+2
View File
@@ -7,6 +7,8 @@ rem 关闭本窗口只关控制台;已开始的训练继续跑,重新双击
chcp 65001 >nul chcp 65001 >nul
cd /d "%~dp0\.." cd /d "%~dp0\.."
if "%PYTHON%"=="" set "PYTHON=python" if "%PYTHON%"=="" set "PYTHON=python"
rem 2026-10-18 第六轮:Python 按 UTF-8 读写文件(Windows 默认 GBK 读中文 json 会报错)
set "PYTHONUTF8=1"
"%PYTHON%" -c "import torch" >nul 2>nul "%PYTHON%" -c "import torch" >nul 2>nul
if errorlevel 1 ( if errorlevel 1 (
echo 当前 Python(%PYTHON%)不能 import torch。 echo 当前 Python(%PYTHON%)不能 import torch。
+1
View File
@@ -8,4 +8,5 @@ for c in "${PYTHON:-}" python python3; do
done done
[ -n "${PY:-}" ] || { echo "找不到能 import torch 的 Python:先激活训练环境,或设 PYTHON=..."; exit 1; } [ -n "${PY:-}" ] || { echo "找不到能 import torch 的 Python:先激活训练环境,或设 PYTHON=..."; exit 1; }
export PYTHON=$PY export PYTHON=$PY
export PYTHONUTF8=1 # 2026-10-18 第六轮:文件读写统一 UTF-8
exec "$PY" -m ml.monitor --open "$@" exec "$PY" -m ml.monitor --open "$@"
+12 -2
View File
@@ -8,9 +8,11 @@
ml/runs/m5_gate_*.json M5 头门禁 ml/runs/m5_gate_*.json M5 头门禁
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁 ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
ml/runs/r4.json R4 浏览器推理 batch=8 P99 < 15 ms(r4 探针) ml/runs/r4.json R4 浏览器推理 batch=8 P99 < 15 ms(r4 探针)
ml/runs/r9_browser.json R9 查看器整次规划(推理 + 前瞻 + 安全层)P99 < 45 ms、饥饿节拍 < 1%(训练好的模型,bench 阶段测)
webapp/models/*.meta.json 被评测的模型不能是 --random 探针 webapp/models/*.meta.json 被评测的模型不能是 --random 探针
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。 2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。
2026-10-18 第五轮:加 R4、随机探针模型拒绝(N-5)、R8 出题不足额判不通过(N-4);R2 改为参考项(N-2:阈值与池大小耦合、未校准)。""" 2026-10-18 第五轮:加 R4、随机探针模型拒绝(N-5)、R8 出题不足额判不通过(N-4);R2 改为参考项(N-2:阈值与池大小耦合、未校准)。
2026-10-18 第六轮:加 R9(审查 B-1:纯推理口径会放过整周期丢步的配置)与安全层两把尺对照(B-2:同一批题浏览器 / Node 差 > 3× 标红)。"""
import argparse, glob, json, os, re import argparse, glob, json, os, re
import torch import torch
@@ -59,6 +61,9 @@ def main():
add('部署模型是训练产物(非随机探针)', None if mm is None else not mm.get('random_init'), mm is not None and not mm.get('random_init'), 'meta.random_init 不为真') add('部署模型是训练产物(非随机探针)', None if mm is None else not mm.get('random_init'), mm is not None and not mm.get('random_init'), 'meta.random_init 不为真')
r4 = jload('ml/runs/r4.json') r4 = jload('ml/runs/r4.json')
add('R4 浏览器推理 batch=8 P99(ms)', (r4 or {}).get('deploy', {}).get('p99') if r4 else 'missing', bool(r4 and r4.get('pass')), '< 15(随机初始化同结构模型实测)') add('R4 浏览器推理 batch=8 P99(ms)', (r4 or {}).get('deploy', {}).get('p99') if r4 else 'missing', bool(r4 and r4.get('pass')), '< 15(随机初始化同结构模型实测)')
r9 = jload('ml/runs/r9_browser.json'); rr = (r9 or {}).get('result') or {}
add('R9 查看器整次规划 P99(ms)/ 饥饿节拍占比', [rr.get('planP99'), rr.get('starve_rate')] if r9 else 'missing', bool(r9 and r9.get('pass')),
'< 45 ms 且 < 1%(训练好的模型、30 道贴柱题,ml.eval.r4_probe --viewer)')
pb = jload('ml/runs/policy_bench.json') pb = jload('ml/runs/policy_bench.json')
if pb is None: if pb is None:
add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench') add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench')
@@ -78,7 +83,12 @@ def main():
add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95') add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95')
add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0') add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0')
else: add('R8 对抗集', 'missing', False, 'bench 需含 r8') else: add('R8 对抗集', 'missing', False, 'bench 需含 r8')
add('推理延迟 P99(Node CPU,参考)', max((v.get('inferMsP99') or 0) for v in sets.values()), None, '浏览器以 __POLICY.latency() 为准') add('推理延迟 P99(Node CPU,参考)', max((v.get('inferMsP99') or 0) for v in sets.values()), None, '浏览器以 r9_browser.json 为准')
nc = sets.get('near_column_pos') or {}
bs, ns = rr.get('safetyP99'), nc.get('safetyMsP99')
ratio = (bs / ns) if (bs is not None and ns) else None
add('安全层 P99 浏览器 / Node(同一批贴柱题)', [bs, ns, None if ratio is None else round(ratio, 2)], None if ratio is None else ratio <= 3,
'≤ 3×;超了看 certQueriesP99 / certMemoHitRate / certFailWhy 定位')
# 3) 数据 / 训练 / 后训练门禁 # 3) 数据 / 训练 / 后训练门禁
r2 = jload('ml/data/r2.json') r2 = jload('ml/data/r2.json')
+31 -13
View File
@@ -6,8 +6,8 @@
2026-10-18 第五轮(审查 N-2:判据与搜索池耦合):最近 5% 的近邻距离随池大小变,label_std 随之系统偏大/偏小。 2026-10-18 第五轮(审查 N-2:判据与搜索池耦合):最近 5% 的近邻距离随池大小变,label_std 随之系统偏大/偏小。
改为按"近邻距离 → 标签差"做线性回归,取距离 → 0 的截距 label_std_norm 作判据(与池大小无关:池大只是近邻更近、点更多, 改为按"近邻距离 → 标签差"做线性回归,取距离 → 0 的截距 label_std_norm 作判据(与池大小无关:池大只是近邻更近、点更多,
截距估计的是"同一输入下的固有标签分歧");旧口径 label_std_mean 保留作参考。pipeline 默认只警告,R2_STRICT=1 才拦。 截距估计的是"同一输入下的固有标签分歧");旧口径 label_std_mean 保留作参考。pipeline 默认只警告,R2_STRICT=1 才拦。
自检:同一份数据 --pool 50000 与 --pool 200000 两跑,label_std_norm 应一致(差 < 0.02)。 自检(第七轮起每次自动做):--pool 50000 与 4 倍池 200000 各测一次,label_std_norm 差 < 0.02 记 pool_check.independent=true。
2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 峰值 < 0.5 GB,CPU 约 1–2 分钟;近邻对为空时输出 pass=null 而不报错。""" 2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 峰值 < 0.5 GB,连同 4 倍池自检 CPU 约 5–10 分钟、GPU 约 1 分钟;近邻对为空时输出 pass=null 而不报错。"""
import argparse, json, math import argparse, json, math
import torch import torch
@@ -18,13 +18,34 @@ def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/data/r2.json') ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/data/r2.json')
ap.add_argument('--queries', type=int, default=5000); ap.add_argument('--pool', type=int, default=50000) ap.add_argument('--queries', type=int, default=5000); ap.add_argument('--pool', type=int, default=50000)
ap.add_argument('--pool2', type=int, default=0, help='池无关性自检的大池(默认 4×pool)')
ap.add_argument('--frac', type=float, default=0.05); ap.add_argument('--seed', type=int, default=0) ap.add_argument('--frac', type=float, default=0.05); ap.add_argument('--seed', type=int, default=0)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu') ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args() a = ap.parse_args()
d = Samples(a.samples, device=a.device, split='train') d = Samples(a.samples, device=a.device, split='train')
P2 = a.pool2 if a.pool2 else 4 * a.pool
r1 = measure(d, a, a.pool)
if r1.get('pairs', 0) == 0:
out = r1
else:
# 2026-10-18 第七轮(遗留 C-3):池无关性自检并入每次运行——同一批查询、池放大 4 倍再测一次,
# 截距差 < 0.02 才说明 label_std_norm 这把尺可信(independent=true);判定用大池的数(近邻更近、截距更稳)。
r2 = measure(d, a, P2) if min(P2, d.n) > r1['pool'] else None
out = dict(r2 or r1)
out['pool_check'] = {'pool_small': r1['pool'], 'norm_small': r1['label_std_norm'],
'pool_big': r2['pool'] if r2 else None, 'norm_big': r2['label_std_norm'] if r2 else None,
'diff': abs(r2['label_std_norm'] - r1['label_std_norm']) if r2 else None,
'independent': (abs(r2['label_std_norm'] - r1['label_std_norm']) < 0.02) if r2 else None,
'note': '样本数不足 4 倍池时只测一次' if not r2 else '差 < 0.02 → 判据与池无关,R2_STRICT=1 可以放心打开'}
print(json.dumps(out, indent=1))
json.dump(out, open(a.out, 'w'), indent=1)
def measure(d, a, pool):
g = torch.Generator(device=a.device); g.manual_seed(a.seed) g = torch.Generator(device=a.device); g.manual_seed(a.seed)
P = min(a.pool, d.n); Q = min(a.queries, d.n) P = min(pool, d.n); Q = min(a.queries, d.n)
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]; qi = torch.randperm(d.n, device=a.device, generator=g)[:Q] qi = torch.randperm(d.n, device=a.device, generator=g)[:Q] # 查询集与池大小无关(先抽查询)
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]
X = d.X.float(); mu, sd = X[pi].mean(0), X[pi].std(0).clamp(min=1e-6) X = d.X.float(); mu, sd = X[pi].mean(0), X[pi].std(0).clamp(min=1e-6)
Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd
tp, tq = d.TID[pi], d.TID[qi] tp, tq = d.TID[pi], d.TID[qi]
@@ -35,8 +56,7 @@ def main():
v, j = D.min(1); best_d[s:s + 256] = v; best_j[s:s + 256] = j v, j = D.min(1); best_d[s:s + 256] = v; best_j[s:s + 256] = j
fin = torch.isfinite(best_d) fin = torch.isfinite(best_d)
if fin.sum() < 20: if fin.sum() < 20:
out = {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'} return {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'}
print(json.dumps(out)); json.dump(out, open(a.out, 'w'), indent=1); return
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a # 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a
dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1) dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1)
half = dd <= torch.quantile(dd, 0.5) half = dd <= torch.quantile(dd, 0.5)
@@ -50,13 +70,11 @@ def main():
Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float() Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float()
rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均 rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均
std = (rms / math.sqrt(2)) std = (rms / math.sqrt(2))
out = {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(), return {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(),
'label_std_norm': std_norm, 'label_std_slope': coef[1].item(), 'label_std_norm': std_norm, 'label_std_slope': coef[1].item(),
'label_std_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(), 'label_std_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(),
'pass': std_norm < 0.10, 'pass': std_norm < 0.10,
'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'} 'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'}
print(json.dumps(out, indent=1))
json.dump(out, open(a.out, 'w'), indent=1)
if __name__ == '__main__': if __name__ == '__main__':
+61 -9
View File
@@ -9,11 +9,24 @@
判据:部署口径 P99 < 15 ms 即通过;wasm 不过而 WebGPU 过,也判通过并记 deploy_ep=webgpu(查看器用 &ep=webgpu)。 判据:部署口径 P99 < 15 ms 即通过;wasm 不过而 WebGPU 过,也判通过并记 deploy_ep=webgpu(查看器用 &ep=webgpu)。
找不到浏览器 / 页面没回报 → pass=null(流水线警告继续,验收标红)。浏览器路径可用 R4_BROWSER 指定。 找不到浏览器 / 页面没回报 → pass=null(流水线警告继续,验收标红)。浏览器路径可用 R4_BROWSER 指定。
2026-10-18 第五轮新增(审查第四轮缺口 3:R4 需要人开浏览器看,导致 `all` 天生不完整)。 2026-10-18 第五轮新增(审查第四轮缺口 3:R4 需要人开浏览器看,导致 `all` 天生不完整)。
范围说明:这里测的是纯推理;贴柱处安全层耗时(R9)由 bench(tests/policy_bench.cjs 的 safetyMsP99)给。""" 范围说明:R4 判据只看纯推理(随机权重只能代表推理开销)。
2026-10-18 第六轮(审查 B-1/B-2/B-3):
--viewer:在真查看器里跑 autotest(与 Node bench 同一批 30 道贴立柱题),记录整次规划 planMs(推理 + 前瞻 + 安全层)、
节拍间隔与饥饿(丢步)次数。r4 阶段用随机模型跑一次,只作参考(随机策略的安全层路径不代表真模型);
bench 阶段用训练好的模型跑,写 ml/runs/r9_browser.json,判据 = 方案 R9:planMs P99 < 45 ms(60 ms 周期留 15 ms 余量)且饥饿节拍 < 1%。
本地有 webapp/vendor/ort/(bash ml/pipeline.sh setup 会放好)就用本地 ort,不依赖 CDN(第六轮实测 jsdelivr 间歇不可达)。
WebGPU 与查看器两种配置用有界面窗口打开(无头模式没有真 GPU 适配器,第六轮实测 backend not found),跑完自动关闭。"""
import argparse, http.server, json, os, shutil, socket, subprocess, sys, tempfile, threading, time, urllib.parse import argparse, http.server, json, os, shutil, socket, subprocess, sys, tempfile, threading, time, urllib.parse
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
LIMIT_MS = 15.0 LIMIT_MS = 15.0
PLAN_LIMIT_MS = 45.0 # R9:整次规划 P99(60 ms 周期留 15 ms 给渲染与 tick)
STARVE_LIMIT = 0.01 # R9:饥饿节拍占比上限
def ort_local():
if os.environ.get('ORT_LOCAL') in ('0', '1'): return os.environ['ORT_LOCAL'] == '1'
return os.path.exists(os.path.join(ROOT, 'webapp', 'vendor', 'ort', 'ort.min.js'))
def find_browser(): def find_browser():
@@ -64,28 +77,47 @@ def kill_tree(p):
except Exception: p.kill() except Exception: p.kill()
def one(srv, browser, model_url, ep, threads, proxy, n, timeout_s): def open_page(srv, browser, url, timeout_s, headed=False):
srv.result = None; srv.ev.clear() srv.result = None; srv.ev.clear()
q = urllib.parse.urlencode({'model': model_url, 'ep': ep, 'threads': threads, 'proxy': proxy, 'n': n,
**({'ortLocal': '1'} if os.environ.get('ORT_LOCAL') == '1' else {})})
url = f'http://127.0.0.1:{srv.port}/webapp/r4_probe.html?{q}'
prof = tempfile.mkdtemp(prefix='r4prof_') prof = tempfile.mkdtemp(prefix='r4prof_')
args = [browser, '--headless=new', f'--user-data-dir={prof}', '--no-first-run', '--no-default-browser-check', args = [browser] + ([] if headed else ['--headless=new']) + [f'--user-data-dir={prof}', '--no-first-run', '--no-default-browser-check',
'--disable-extensions', '--enable-unsafe-webgpu', url] '--disable-extensions', '--enable-unsafe-webgpu', '--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1280,800', url]
p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
got = srv.ev.wait(timeout_s) got = srv.ev.wait(timeout_s)
kill_tree(p); shutil.rmtree(prof, ignore_errors=True) kill_tree(p); shutil.rmtree(prof, ignore_errors=True)
r = srv.result if got else {'error': f'{timeout_s} s 内页面没有回报'} return srv.result if got else {'error': f'{timeout_s} s 内页面没有回报'}
def one(srv, browser, model_url, ep, threads, proxy, n, timeout_s):
q = urllib.parse.urlencode({'model': model_url, 'ep': ep, 'threads': threads, 'proxy': proxy, 'n': n, **({'ortLocal': '1'} if ort_local() else {})})
r = open_page(srv, browser, f'http://127.0.0.1:{srv.port}/webapp/r4_probe.html?{q}', timeout_s, headed=(ep == 'webgpu'))
r = dict(r or {}, config=f'{ep}·threads={threads}·proxy={proxy}') r = dict(r or {}, config=f'{ep}·threads={threads}·proxy={proxy}')
print(json.dumps(r, ensure_ascii=False), flush=True) print(json.dumps(r, ensure_ascii=False), flush=True)
return r return r
def viewer(srv, browser, model_url, n, probe):
"""真查看器 autotest:整次规划耗时 / 节拍 / 饥饿(R9 口径)"""
q = urllib.parse.urlencode({'policy': model_url, 'autotest': n, **({'probe': '1'} if probe else {}), **({'ortLocal': '1'} if ort_local() else {})})
r = open_page(srv, browser, f'http://127.0.0.1:{srv.port}/webapp/index.html?{q}', 120 + 12 * n, headed=True)
r = dict(r or {})
if r.get('planP99') is not None:
sr = (r.get('starve') or 0) / max(1, r.get('ticks') or 1)
r['starve_rate'] = round(sr, 4)
r['r9_pass'] = r['planP99'] < PLAN_LIMIT_MS and sr < STARVE_LIMIT
print('viewer', json.dumps({k: v for k, v in r.items() if k != 'per'}, ensure_ascii=False), flush=True)
return r
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument('--model', default='ml/runs/r4/traj_policy_r4.onnx'); ap.add_argument('--out', default='ml/runs/r4.json') ap.add_argument('--model', default='ml/runs/r4/traj_policy_r4.onnx'); ap.add_argument('--out', default='ml/runs/r4.json')
ap.add_argument('--n', type=int, default=150); ap.add_argument('--timeout', type=int, default=240) ap.add_argument('--n', type=int, default=150); ap.add_argument('--timeout', type=int, default=240)
ap.add_argument('--viewer', action='store_true', help='只跑查看器 autotest(R9,bench 阶段用训练好的模型)')
ap.add_argument('--viewer-n', type=int, default=30); ap.add_argument('--no-viewer-ref', action='store_true')
a = ap.parse_args() a = ap.parse_args()
if a.viewer: return main_viewer(a)
out = {'rule': f'部署口径 batch=8 P99 < {LIMIT_MS:g} ms', 'model': a.model, 'pass': None} out = {'rule': f'部署口径 batch=8 P99 < {LIMIT_MS:g} ms', 'model': a.model, 'pass': None}
br = find_browser() br = find_browser()
if not br: if not br:
@@ -96,8 +128,11 @@ def main():
mu = '/' + os.path.relpath(os.path.abspath(a.model), ROOT).replace(os.sep, '/') mu = '/' + os.path.relpath(os.path.abspath(a.model), ROOT).replace(os.sep, '/')
runs = [one(srv, br, mu, 'wasm', 'auto', 1, a.n, a.timeout), one(srv, br, mu, 'wasm', '4', 1, a.n, a.timeout), runs = [one(srv, br, mu, 'wasm', 'auto', 1, a.n, a.timeout), one(srv, br, mu, 'wasm', '4', 1, a.n, a.timeout),
one(srv, br, mu, 'wasm', 'auto', 0, a.n, a.timeout), one(srv, br, mu, 'webgpu', 'auto', 0, a.n, a.timeout)] one(srv, br, mu, 'wasm', 'auto', 0, a.n, a.timeout), one(srv, br, mu, 'webgpu', 'auto', 0, a.n, a.timeout)]
srv.httpd.shutdown()
out['runs'] = runs out['runs'] = runs
if not a.no_viewer_ref: # 随机模型在查看器里的整次规划耗时:仅参考,不参与 R4 判定
out['viewer_ref'] = viewer(srv, br, mu, a.viewer_n, True)
out['viewer_ref_note'] = '随机权重:安全层路径不代表真模型,R9 以 bench 阶段 r9_browser.json 为准'
srv.httpd.shutdown()
dep, gpu = runs[0], runs[3] dep, gpu = runs[0], runs[3]
ok = lambda r: r.get('p99') is not None and r['p99'] < LIMIT_MS ok = lambda r: r.get('p99') is not None and r['p99'] < LIMIT_MS
if dep.get('p99') is None and gpu.get('p99') is None: if dep.get('p99') is None and gpu.get('p99') is None:
@@ -115,5 +150,22 @@ def main():
print(f"R4:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } 部署口径 P99 {d.get('p99')} ms({d.get('config')})") print(f"R4:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } 部署口径 P99 {d.get('p99')} ms({d.get('config')})")
def main_viewer(a):
meta = json.load(open(a.model.replace('.onnx', '.meta.json'), encoding='utf-8'))
out = {'rule': f'R9:查看器整次规划 planMs P99 < {PLAN_LIMIT_MS:g} ms 且饥饿节拍 < {STARVE_LIMIT:.0%}', 'model': a.model, 'pass': None}
br = find_browser()
if not br: out['note'] = '没找到 Edge / Chrome(可设 R4_BROWSER=浏览器路径)'
else:
srv = Srv(); mu = '/' + os.path.relpath(os.path.abspath(a.model), ROOT).replace(os.sep, '/')
r = viewer(srv, br, mu, a.viewer_n, bool(meta.get('random_init'))); srv.httpd.shutdown()
out.update(browser=br, result=r)
out['pass'] = r.get('r9_pass')
if out['pass'] is None: out['note'] = r.get('error') or '页面没有给出整次规划耗时'
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
json.dump(out, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
r = out.get('result') or {}
print(f"R9:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } planMs P99 {r.get('planP99')} ms(推理 {r.get('inferP99')} / 安全层 {r.get('safetyP99')}),饥饿 {r.get('starve_rate')}")
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+11 -3
View File
@@ -1,8 +1,10 @@
"""导出部署包(方案 §8.4): """导出部署包(方案 §8.4):
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx [--fp16] python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx [--fp16]
输出 traj_policy.onnx(输入 feat[1,83]、z[K,16,6];输出 act[K,16,6] 归一化增量、value[K]、sub[K,13]) 输出 traj_policy.onnx(输入 feat[1 或 K,83]、z[K,16,6];输出 act[K,16,6] 归一化增量、value[K]、sub[K,13])
与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once)。导出后用 onnxruntime 与 PyTorch 对拍。 与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once)。导出后用 onnxruntime 与 PyTorch 对拍。
2026-10-18 第五轮:改为"编码一次、解码 K 次"(Policy.sample_shared),meta.enc_once=true;运行时据此只送 1 行特征。 2026-10-18 第五轮:改为"编码一次、解码 K 次"(Policy.sample_shared),meta.enc_once=true;运行时据此只送 1 行特征。
2026-10-18 第六轮:feat 轴改为动态(1 行 = 主推理编码一次;K 行 = 前瞻逐行编码),meta.feat_dyn=true。
第五轮的 enc_once 模型 feat 固定 1 行,运行时前瞻只能逐行推(ik-policy.js ortInfer 已兼容),重新导出即可。
--random 导出的是延迟探针(meta.random_init=true):查看器 / bench / 验收都会拒绝把它当策略用。""" --random 导出的是延迟探针(meta.random_init=true):查看器 / bench / 验收都会拒绝把它当策略用。"""
import argparse, json, os import argparse, json, os
import numpy as np import numpy as np
@@ -38,7 +40,7 @@ def main():
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True) os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
feat, z = torch.randn(1, F_DIM), torch.randn(8, H, 6) feat, z = torch.randn(1, F_DIM), torch.randn(8, H, 6)
torch.onnx.export(Wrap(m), (feat, z), a.out, input_names=['feat', 'z'], output_names=['act', 'value', 'sub'], torch.onnx.export(Wrap(m), (feat, z), a.out, input_names=['feat', 'z'], output_names=['act', 'value', 'sub'],
dynamic_axes={'z': {0: 'K'}, 'act': {0: 'K'}, 'value': {0: 'K'}, 'sub': {0: 'K'}}, dynamic_axes={'feat': {0: 'B'}, 'z': {0: 'K'}, 'act': {0: 'K'}, 'value': {0: 'K'}, 'sub': {0: 'K'}},
opset_version=a.opset, dynamo=False) opset_version=a.opset, dynamo=False)
if a.fp16: if a.fp16:
from onnxconverter_common import float16 from onnxconverter_common import float16
@@ -54,8 +56,14 @@ def main():
err8 = max(float((t[i] - t8[i]).abs().max()) for i in range(3)) err8 = max(float((t[i] - t8[i]).abs().max()) for i in range(3))
assert err8 < 1e-4, f'sample_shared 与 sample 不一致:{err8}' assert err8 < 1e-4, f'sample_shared 与 sample 不一致:{err8}'
err = max(float(np.abs(o[i] - t[i].numpy()).max()) for i in range(3)) err = max(float(np.abs(o[i] - t[i].numpy()).max()) for i in range(3))
f8 = torch.randn(8, F_DIM) # 前瞻口径:8 行不同特征,须与 sample() 逐行一致
o8 = s.run(None, {'feat': f8.numpy(), 'z': z.numpy()})
with torch.no_grad(): r8 = m.sample(f8, z)
err_rows = max(float(np.abs(o8[i] - r8[i].numpy()).max()) for i in range(3))
assert err_rows < 1e-3, f'feat 为 K 行时与 sample 不一致:{err_rows}'
err = max(err, err_rows)
meta = {'fp': kin.fp, 'tcp0': kin.tcp0.tolist(), 'head': m.head, 'feat_ver': FEAT_VER, 'f_dim': F_DIM, 'h': H, 'step': STEP, 'fp16': a.fp16, meta = {'fp': kin.fp, 'tcp0': kin.tcp0.tolist(), 'head': m.head, 'feat_ver': FEAT_VER, 'f_dim': F_DIM, 'h': H, 'step': STEP, 'fp16': a.fp16,
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err} 'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'feat_dyn': True, 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err}
json.dump(meta, open(a.out.replace('.onnx', '.meta.json'), 'w'), indent=1) json.dump(meta, open(a.out.replace('.onnx', '.meta.json'), 'w'), indent=1)
print(json.dumps({'onnx': a.out, 'MB': os.path.getsize(a.out) / 1e6, 'max_abs_err_vs_torch': err})) print(json.dumps({'onnx': a.out, 'MB': os.path.getsize(a.out) / 1e6, 'max_abs_err_vs_torch': err}))
+1 -1
View File
@@ -10,7 +10,7 @@ cd "$(dirname "$0")/../.."
PY=${PYTHON:-python} PY=${PYTHON:-python}
ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000} ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000}
mkdir -p ml/runs/mine mkdir -p ml/runs/mine
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs [ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate ${MINE_GATE_N:-2000} --seed 777 --out ml/runs
[ -f ml/runs/current.pt ] || cp ml/runs/apg/best.pt ml/runs/current.pt [ -f ml/runs/current.pt ] || cp ml/runs/apg/best.pt ml/runs/current.pt
SAMPLES="ml/data/samples"; FIXED=""; small=0 SAMPLES="ml/data/samples"; FIXED=""; small=0
for ((r=1; r<=ROUNDS; r++)); do for ((r=1; r<=ROUNDS; r++)); do
+4 -2
View File
@@ -129,12 +129,14 @@ class Policy(nn.Module):
def sample_shared(self, feat1, z): def sample_shared(self, feat1, z):
"""部署接口(ONNX,2026-10-18 第五轮,R4 实测后改):一次观测、K 条候选。 """部署接口(ONNX,2026-10-18 第五轮,R4 实测后改):一次观测、K 条候选。
feat1 [1,F],z [K,16,6] → act [K,16,6],value [K],sub [K,13](value/sub 对 K 行相同,展开只为保持运行时读法)。 feat1 [1,F],z [K,16,6] → act [K,16,6],value [K],sub [K,13](value/sub 对 K 行相同,展开只为保持运行时读法)。
此前部署把同一观测复制 K=8 行喂 sample(),encoder 被重复算 8 次(约占总计算 29%);这里只编码一次再展开给解码器。""" 此前部署把同一观测复制 K=8 行喂 sample(),encoder 被重复算 8 次(约占总计算 29%);这里只编码一次再展开给解码器。
2026-10-18 第六轮:feat 行数也可以等于 K(前瞻:8 个候选末态各一行特征),此时逐行编码,结果与 sample(feat, z) 一致;
expand 对 1→K 与 K→K 都成立,所以 ONNX 里 feat 轴是动态的(meta.feat_dyn)。"""
mem = self.encode(feat1) mem = self.encode(feat1)
v, s = self.heads(mem) v, s = self.heads(mem)
K = z.shape[0] K = z.shape[0]
if self.head == 'wta': if self.head == 'wta':
act, _ = self.wta_modes(mem) act, _ = self.wta_modes(mem[:1])
idx = torch.arange(K, device=z.device) % self.K idx = torch.arange(K, device=z.device) % self.K
return act[0, idx] + 0.0 * z, v.expand(K), s.expand(K, -1) return act[0, idx] + 0.0 * z, v.expand(K), s.expand(K, -1)
m = mem.expand(K, -1, -1) m = mem.expand(K, -1, -1)
+1 -1
View File
@@ -298,7 +298,7 @@ class Job:
if not ck.get('ok'): return {'error': '环境自检未通过:' + ';'.join(ck['problems'])} if not ck.get('ok'): return {'error': '环境自检未通过:' + ';'.join(ck['problems'])}
if frm not in STAGES or to not in STAGES or STAGES.index(frm) > STAGES.index(to): return {'error': '起止阶段无效'} if frm not in STAGES or to not in STAGES or STAGES.index(frm) > STAGES.index(to): return {'error': '起止阶段无效'}
bash = find_bash() bash = find_bash()
e = dict(os.environ, FROM=frm, TO=to, MONITOR='0', PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8', PIPE_OWNER='网页') e = dict(os.environ, FROM=frm, TO=to, MONITOR='0', PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PIPE_OWNER='网页')
py = train_python() py = train_python()
if ' ' not in py: e['PYTHON'] = py.replace('\\', '/') if ' ' not in py: e['PYTHON'] = py.replace('\\', '/')
for k, v in (env or {}).items(): for k, v in (env or {}).items():
+21 -10
View File
@@ -11,9 +11,11 @@
# apg APG 首轮微调(从选定模型开始) # apg APG 首轮微调(从选定模型开始)
# mine 挖错循环(3–6 轮,含门禁回滚) # mine 挖错循环(3–6 轮,含门禁回滚)
# export 导出 ONNX(fp32)到 webapp/models/ # export 导出 ONNX(fp32)到 webapp/models/
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集) # bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)+ 查看器里同一批贴柱题的整次规划耗时(R9 → ml/runs/r9_browser.json)
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json # accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
# all 一键无人值守 kin → accept:已完成阶段自动跳过(断了再跑一次即续跑)、阶段超时、门槛自动分支,并在后台打开训练监控 # all 一键无人值守 kin → accept:已完成阶段自动跳过(断了再跑一次即续跑)、阶段超时、门槛自动分支,并在后台打开训练监控
# selftest 本机自检约 5 分钟(Windows 进程生命周期 + 断点续跑),写 ml/runs/selftest.json(bash ml/selftest.sh)
# smoke 全流程冒烟约 40–70 分钟:在仓库旁的副本里用极小模型把 kin → accept 跑通,并出 2000 条任务的数据统计,写 ml/runs/smoke.json(bash ml/smoke.sh)
# monitor 训练控制台网页 http://localhost:8100:点「开始训练」一键跑全流程、看进度、可停止(也可双击 ml/console.bat / ml/console.sh) # monitor 训练控制台网页 http://localhost:8100:点「开始训练」一键跑全流程、看进度、可停止(也可双击 ml/console.bat / ml/console.sh)
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、 # 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS # CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
@@ -26,6 +28,9 @@
# r4 自动探针(独立文件,不碰 webapp/models)、启动前自检 torch。 # r4 自动探针(独立文件,不碰 webapp/models)、启动前自检 torch。
set -e -o pipefail # pipefail:`node ... | tee` 里 node 失败也算失败 set -e -o pipefail # pipefail:`node ... | tee` 里 node 失败也算失败
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# 2026-10-18 第六轮:所有 Python 子进程按 UTF-8 读写文件(Windows 默认 GBK,读 ensure_ascii=False 写出的中文 json 会 UnicodeDecodeError,
# 第六轮本机实测 :110 即此因;逐处补 encoding 容易漏,统一用 UTF-8 模式兜住)
export PYTHONUTF8=1 PYTHONIOENCODING=utf-8
PY=${PYTHON:-python} PY=${PYTHON:-python}
command -v "$PY" >/dev/null 2>&1 && "$PY" -c "import sys" 2>/dev/null || { echo "找不到可用的 Python($PY),请设 PYTHON=..."; exit 1; } command -v "$PY" >/dev/null 2>&1 && "$PY" -c "import sys" 2>/dev/null || { echo "找不到可用的 Python($PY),请设 PYTHON=..."; exit 1; }
export PYTHON=$PY export PYTHON=$PY
@@ -74,7 +79,7 @@ trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返
train_one() { # $1 = 配置文件 train_one() { # $1 = 配置文件
local cfg=$1 tag; tag=$(basename "$1" .json) local cfg=$1 tag; tag=$(basename "$1" .json)
if [ -f ml/runs/bench_$tag/last.pt ]; then echo "吞吐摸底 ml/runs/bench_$tag 已有,跳过" if [ -f ml/runs/bench_$tag/last.pt ]; then echo "吞吐摸底 ml/runs/bench_$tag 已有,跳过"
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps 2000 --no-eval --config $cfg; fi else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps ${BENCH_STEPS:-2000} --no-eval --config $cfg; fi
local res=""; [ -f ml/runs/$tag/last.pt ] && { res="--resume ml/runs/$tag/last.pt"; echo "发现 ml/runs/$tag/last.pt → 续训"; } local res=""; [ -f ml/runs/$tag/last.pt ] && { res="--resume ml/runs/$tag/last.pt"; echo "发现 ml/runs/$tag/last.pt → 续训"; }
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$tag --config $cfg $res run $PY -m ml.train --samples ml/data/samples --out ml/runs/$tag --config $cfg $res
} }
@@ -82,8 +87,8 @@ m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过
local tag=$1 ck=${CKPT:-ml/runs/$1/best.pt} o local tag=$1 ck=${CKPT:-ml/runs/$1/best.pt} o
[ -f "$ck" ] || { echo "M5:$ck 不存在(训练发散或没训完)→ 按不通过处理"; return 3; } [ -f "$ck" ] || { echo "M5:$ck 不存在(训练发散或没训完)→ 按不通过处理"; return 3; }
# 断点续跑:结果文件比检查点新 → 这一半已经评过,跳过 # 断点续跑:结果文件比检查点新 → 这一半已经评过,跳过
o=ml/runs/m5_${tag}_policy.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --out $o o=ml/runs/m5_${tag}_policy.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${M5_N:-1000} --out $o
o=ml/runs/m5_${tag}_safety.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --safety --out $o o=ml/runs/m5_${tag}_safety.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${M5_N:-1000} --safety --out $o
$PY - "$tag" "$ck" <<'PY' $PY - "$tag" "$ck" <<'PY'
import json, sys import json, sys
tag, ck = sys.argv[1], sys.argv[2] tag, ck = sys.argv[1], sys.argv[2]
@@ -101,12 +106,13 @@ sys.exit(0 if ok else 3)
PY PY
} }
case $stage in case $stage in
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;; setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund); run bash ml/prep/vendor_ort.sh ;;
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;; kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;;
r4) # 浏览器推理延迟(方案 R4):随机初始化模型导出到独立文件(不碰 webapp/models,审查 N-5),无头浏览器自动测 r4) # 浏览器推理延迟(方案 R4):随机初始化模型导出到独立文件(不碰 webapp/models,审查 N-5),无头浏览器自动测
mkdir -p ml/runs/r4 mkdir -p ml/runs/r4
bash ml/prep/vendor_ort.sh || echo "本地 ort 没放好(npm 不通?),探针改用 CDN"
run $PY -m ml.export_onnx --random $CFG --out ml/runs/r4/traj_policy_r4.onnx run $PY -m ml.export_onnx --random $CFG --out ml/runs/r4/traj_policy_r4.onnx
run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json ${R4_ARGS:-}
r=$($PY -c "import json;p=json.load(open('ml/runs/r4.json', encoding='utf-8')).get('pass');print({True:'pass',False:'fail'}.get(p,'null'))") r=$($PY -c "import json;p=json.load(open('ml/runs/r4.json', encoding='utf-8')).get('pass');print({True:'pass',False:'fail'}.get(p,'null'))")
if [ "$r" = fail ] && [ "${R4_STRICT:-1}" = 1 ]; then if [ "$r" = fail ] && [ "${R4_STRICT:-1}" = 1 ]; then
echo "R4 不通过(见 ml/runs/r4.json):部署延迟超标,先别花几十小时训练这个规模的模型。按方案改 CFG=ml/configs/small.json(d=128)重跑;确认要继续设 R4_STRICT=0"; exit 5 echo "R4 不通过(见 ml/runs/r4.json):部署延迟超标,先别花几十小时训练这个规模的模型。按方案改 CFG=ml/configs/small.json(d=128)重跑;确认要继续设 R4_STRICT=0"; exit 5
@@ -139,14 +145,14 @@ case $stage in
|| { echo "MIN_GAP_MM=$MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep"; exit 8; } || { echo "MIN_GAP_MM=$MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep"; exit 8; }
# 已打包完的 shard 跳过(断点续跑,见 run_prep.sh);参数变了由 pstate reset 把旧 samples 移到 ml/archive/ # 已打包完的 shard 跳过(断点续跑,见 run_prep.sh);参数变了由 pstate reset 把旧 samples 移到 ml/archive/
run bash ml/prep/run_prep.sh $NPROC run bash ml/prep/run_prep.sh $NPROC
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ${R2_ARGS:-}
# 第五轮(审查 N-2):R2 判据与搜索池大小有关、阈值未经实测校准 → 默认只警告继续,R2_STRICT=1 才拦 # 第五轮(审查 N-2):R2 判据与搜索池大小有关、阈值未经实测校准 → 默认只警告继续,R2_STRICT=1 才拦
if ! $PY -c "import json,sys;sys.exit(4 if json.load(open('ml/data/r2.json', encoding='utf-8')).get('pass') is False else 0)"; then if ! $PY -c "import json,sys;sys.exit(4 if json.load(open('ml/data/r2.json', encoding='utf-8')).get('pass') is False else 0)"; then
if [ "${R2_STRICT:-0}" = 1 ]; then echo "R2 不通过(R2_STRICT=1):停在这里,把 ml/data/r2.json 发我"; exit 4; fi if [ "${R2_STRICT:-0}" = 1 ]; then echo "R2 不通过(R2_STRICT=1):停在这里,把 ml/data/r2.json 发我"; exit 4; fi
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出" echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
fi ;; fi ;;
train) train_one $CFG ;; train) train_one $CFG ;;
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate ${M5_N:-1000} --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
if m5_one $TAG; then : if m5_one $TAG; then :
elif [ "$TAG" != wta ] && [ "${AUTO_WTA:-1}" = 1 ] && [ -z "${CKPT:-}" ]; then elif [ "$TAG" != wta ] && [ "${AUTO_WTA:-1}" = 1 ] && [ -z "${CKPT:-}" ]; then
# 第五轮(缺口 3):base 头不过 → 流程内自动训 WTA 头再门禁一次,两种头都不过才停 # 第五轮(缺口 3):base 头不过 → 流程内自动训 WTA 头再门禁一次,两种头都不过才停
@@ -154,14 +160,19 @@ case $stage in
$PY -m ml.pstate set m5 branch=wta $PY -m ml.pstate set m5 branch=wta
train_one ml/configs/wta.json train_one ml/configs/wta.json
m5_one wta || { echo "M5:两种头都未通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; } m5_one wta || { echo "M5:两种头都未通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; }
elif [ "${M5_STRICT:-1}" = 0 ]; then echo "M5:不通过,M5_STRICT=0(冒烟)→ 照常继续"; echo ml/runs/$TAG/best.pt > $SEL
else echo "M5:不通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; fi ;; else echo "M5:不通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; fi ;;
apg) ck=$(ckpt); res=""; [ -f ml/runs/apg/last.pt ] && { res="--resume ml/runs/apg/last.pt"; echo "发现 ml/runs/apg/last.pt → 续跑"; } apg) ck=$(ckpt); res=""; [ -f ml/runs/apg/last.pt ] && { res="--resume ml/runs/apg/last.pt"; echo "发现 ml/runs/apg/last.pt → 续跑"; }
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;; run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg ${APG_FIRST_STEPS:+--steps $APG_FIRST_STEPS} $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
mine) run bash ml/mining/loop.sh $ROUNDS $APG_STEPS $MINE_N ;; mine) run bash ml/mining/loop.sh $ROUNDS $APG_STEPS $MINE_N ;;
monitor) run $PY -m ml.monitor ${MONITOR_ARGS:-} ;; monitor) run $PY -m ml.monitor ${MONITOR_ARGS:-} ;;
selftest) run bash ml/selftest.sh ;;
smoke) run bash ml/smoke.sh ;;
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt) export) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;; run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;;
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 --nc1000 --out ml/runs/policy_bench.json ;; bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 ${BENCH_ARGS:---nc1000} --out ml/runs/policy_bench.json
# R9(第六轮,审查 B-1/B-2):查看器里跑同一批 30 道贴柱题,整次规划 P99 < 45 ms、饥饿节拍 < 1%;结果由 accept 判定,这里不拦
run $PY -m ml.eval.r4_probe --viewer --model webapp/models/traj_policy.onnx --out ml/runs/r9_browser.json --viewer-n ${VIEWER_N:-30} ;;
accept) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt) accept) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
run $PY -m ml.eval.acceptance --ckpt $ck --samples ml/data/samples --out ml/runs/acceptance.json ;; run $PY -m ml.eval.acceptance --ckpt $ck --samples ml/data/samples --out ml/runs/acceptance.json ;;
all) # 默认 kin → accept;已完成(rc=0)的阶段自动跳过,从第一个没完成的继续。FRESH=1 从头来;FROM/TO 可只跑一段 all) # 默认 kin → accept;已完成(rc=0)的阶段自动跳过,从第一个没完成的继续。FRESH=1 从头来;FROM/TO 可只跑一段
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# 把 onnxruntime-web 1.20.1 放到 webapp/vendor/ort/(离线部署 &ortLocal=1、R4/R9 探针不依赖 CDN)。
# 2026-10-18 第六轮(审查 A-3 / B-3):第六轮实测 jsdelivr 间歇不可达;WebGPU 需要 ort.webgpu.min.js 与 jsep 版 wasm。
# 已存在且版本对就跳过。用法:bash ml/prep/vendor_ort.sh
set -e
cd "$(dirname "$0")/../.."
V=1.20.1; D=webapp/vendor/ort
if [ -f $D/VERSION ] && [ "$(cat $D/VERSION)" = $V ]; then echo "[ort] $D 已是 $V,跳过"; exit 0; fi
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
(cd "$T" && npm pack onnxruntime-web@$V --silent >/dev/null && tar xzf onnxruntime-web-$V.tgz)
mkdir -p $D
for f in ort.min.js ort.webgpu.min.js ort-wasm-simd-threaded.wasm ort-wasm-simd-threaded.mjs ort-wasm-simd-threaded.jsep.wasm ort-wasm-simd-threaded.jsep.mjs; do
cp "$T/package/dist/$f" $D/
done
echo $V > $D/VERSION
echo "[ort] 已放到 $D($(du -sh $D | cut -f1))"
Executable
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# 本机自检(2026-10-18 第七轮,遗留 C-5 / C-6:Windows 生命周期与断点续跑只在沙箱验证过)。约 5–10 分钟。
# 在仓库旁的副本(默认 ../<仓库名>_selftest)里做,不碰本仓库数据。结果写本仓库 ml/runs/selftest.json,每项 pass=true/false:
# gen_resume 生成 6 条一口气 vs 先生成 3 条、把最后一行截掉一半、再续到 6 条 → 两份文件逐字节相同
# prep_rerun 打包两次,第二次「待打包 0 个」
# train_stop 训练 --stop-at 20 → rc 143、last.pt 第 20 步;续训到 40 步 rc 0
# train_stopfile 训练中途写 ml/runs/STOP → 60 s 内 rc 143 且 last.pt 已存(控制台「停止」走的就是这条路)
# train_term Git Bash 的 timeout 送 TERM 给训练 → 是否来得及存 last.pt(Windows 原生 python 未必收得到,结果决定控制台是否只能靠 STOP 文件)
# apg_resume APG --stop-at 5 → rc 143;--resume 续跑 rc 0
# tree_killed / survive_parent_exit 控制台的进程树停止、关窗口后子树存活(ml/selftest_proc.py)
set -o pipefail
cd "$(dirname "$0")/.."
SRC=$(pwd); PY=${PYTHON:-python}; export PYTHONUTF8=1
D=${SELFTEST_DIR:-$(dirname "$SRC")/$(basename "$SRC")_selftest}
rm -rf "$D"; mkdir -p "$D"
git ls-files -z | tar --null -T - -cf - | (cd "$D" && tar xf -)
[ -d ml/node_modules ] && cp -r ml/node_modules "$D/ml/node_modules"
cd "$D"; mkdir -p ml/runs ml/data
R=ml/runs/selftest_items.jsonl; : > $R
rec() { $PY -c "import json,sys;print(json.dumps({'item':sys.argv[1],'pass':json.loads(sys.argv[2]),'detail':sys.argv[3]},ensure_ascii=False))" "$1" "$2" "$3" >> $R; echo "[selftest] $1 → $2 $3"; }
CFGS=ml/configs/smoke.json
# 1 生成续写
node ml/gen/gen_dataset.cjs --n 6 --seed 11 --out ml/data/a.jsonl 2>/dev/null
node ml/gen/gen_dataset.cjs --n 3 --seed 11 --out ml/data/shard_0011.jsonl.part 2>/dev/null
sz=$(wc -c < ml/data/shard_0011.jsonl.part); head -c $((sz - 40)) ml/data/shard_0011.jsonl.part > t && mv t ml/data/shard_0011.jsonl.part
node ml/gen/gen_dataset.cjs --n 6 --seed 11 --out ml/data/shard_0011.jsonl.part 2>ml/runs/st_gen.log; mv ml/data/shard_0011.jsonl.part ml/data/shard_0011.jsonl
cmp -s ml/data/a.jsonl ml/data/shard_0011.jsonl && rec gen_resume true "$(grep -o '续写[^,]*' ml/runs/st_gen.log | head -1)" || rec gen_resume false "续写结果与一口气生成不同"
rm -f ml/data/a.jsonl
node ml/gen/gen_dataset.cjs --n 40 --seed 12 --out ml/data/shard_0012.jsonl 2>/dev/null # 训练自检用的小数据
# 2 打包二次运行
bash ml/prep/run_prep.sh 4 >/dev/null 2>&1; bash ml/prep/run_prep.sh 4 > ml/runs/st_prep.log 2>&1
grep -q "待打包 0 个" ml/runs/st_prep.log && rec prep_rerun true "" || rec prep_rerun false "$(head -c 200 ml/runs/st_prep.log)"
T="--samples ml/data/samples --no-eval --config $CFGS --steps 40"
step_of() { $PY -c "import torch;print(torch.load('$1',map_location='cpu',weights_only=False)['step'])" 2>/dev/null; }
# 3 --stop-at
$PY -m ml.train $T --out ml/runs/st_a --stop-at 20 > ml/runs/st_a.log 2>&1; rc=$?
s=$(step_of ml/runs/st_a/last.pt)
$PY -m ml.train $T --out ml/runs/st_a --resume ml/runs/st_a/last.pt >> ml/runs/st_a.log 2>&1; rc2=$?
[ $rc = 143 ] && [ "$s" = 20 ] && [ $rc2 = 0 ] && rec train_stop true "rc 143 / 第 $s 步 / 续训 rc 0" || rec train_stop false "rc=$rc step=$s 续训rc=$rc2"
# 4 STOP 文件
rm -f ml/runs/STOP
$PY -m ml.train --samples ml/data/samples --no-eval --config $CFGS --steps 1000000 --out ml/runs/st_b > ml/runs/st_b.log 2>&1 & pid=$!
sleep 25; touch ml/runs/STOP; t0=$(date +%s)
wait $pid; rc=$?; dt=$(( $(date +%s) - t0 )); rm -f ml/runs/STOP
[ $rc = 143 ] && [ -f ml/runs/st_b/last.pt ] && rec train_stopfile true "写 STOP 后 ${dt}s 退出,第 $(step_of ml/runs/st_b/last.pt) 步" || rec train_stopfile false "rc=$rc ${dt}s"
# 5 timeout TERM
if command -v timeout >/dev/null; then
timeout -k 30 25 $PY -m ml.train --samples ml/data/samples --no-eval --config $CFGS --steps 1000000 --out ml/runs/st_c > ml/runs/st_c.log 2>&1; rc=$?
grep -q "收到停止请求" ml/runs/st_c.log && rec train_term true "rc=$rc,TERM 送达并存档" || rec train_term false "rc=$rc,TERM 没送到 python(Windows 原生进程常见)→ 阶段超时只能硬杀,进度靠每 10 分钟存档"
else rec train_term false "没有 timeout 命令"; fi
# 6 APG 续跑
$PY -m ml.apg.apg_finetune --init ml/runs/st_a/last.pt --samples ml/data/samples --out ml/runs/st_apg --steps 10 --stop-at 5 > ml/runs/st_apg.log 2>&1; rc=$?
$PY -m ml.apg.apg_finetune --init ml/runs/st_a/last.pt --samples ml/data/samples --out ml/runs/st_apg --steps 10 --resume ml/runs/st_apg/last.pt >> ml/runs/st_apg.log 2>&1; rc2=$?
[ $rc = 143 ] && [ $rc2 = 0 ] && rec apg_resume true "" || rec apg_resume false "rc=$rc 续跑rc=$rc2(日志 $D/ml/runs/st_apg.log)"
# 7 进程树 / 关窗口
pj=$($PY ml/selftest_proc.py 2>ml/runs/st_proc.err | tail -1)
echo "$pj" > ml/runs/st_proc.json
k=$($PY -c "import json,sys;d=json.loads(sys.argv[1]);print(str(d.get('tree_killed')).lower(), str(d.get('survive_parent_exit')).lower())" "$pj" 2>/dev/null || echo "null null")
rec tree_killed ${k% *} "$pj" ; rec survive_parent_exit ${k#* } ""
mkdir -p "$SRC/ml/runs"
$PY - "$R" "$SRC/ml/runs/selftest.json" "$D" <<'PY'
import json, sys
items = []
for l in open(sys.argv[1], encoding='utf-8'):
try: items.append(json.loads(l))
except Exception: items.append({'item': 'parse', 'pass': False, 'detail': l.strip()[:200]})
out = {'all_pass': all(i['pass'] is True for i in items), 'items': items, 'dir': sys.argv[3]}
json.dump(out, open(sys.argv[2], 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
print('[selftest] 全部通过' if out['all_pass'] else '[selftest] 有未通过项,见 ' + sys.argv[2])
PY
+71
View File
@@ -0,0 +1,71 @@
"""本机自检的进程部分(2026-10-18 第七轮,遗留 C-5:Windows 生命周期从未实测)。由 ml/selftest.sh 调用。
tree :按控制台同一套标志(Windows: CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW;POSIX: 新会话)起 bash → python + node 两个孙进程,
再用控制台同一套停止方法(Windows: taskkill /T /F;POSIX: 按会话 TERM)结束,检查孙进程是否全部消失。
detach :起一个"启动器"进程,它按同一标志拉起子树后立刻退出,检查子树是否还活着(等价于关掉控制台黑窗口后训练是否继续)。
输出一行 JSON:{"tree_killed": bool, "survive_parent_exit": bool, ...}"""
import json, os, subprocess, sys, time, tempfile, shutil
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from ml.monitor import find_bash, kill_session # noqa: E402
FLAGS = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP | 0x08000000} if os.name == 'nt' else {'start_new_session': True}
def alive(pid):
if os.name == 'nt':
r = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True)
return str(pid) in r.stdout
try: os.kill(pid, 0); return True
except OSError: return False
def tree_cmd(d):
py = sys.executable.replace('\\', '/'); dd = d.replace('\\', '/')
return (f'"{py}" -c "import os,time;open(\'{dd}/p.pid\',\'w\').write(str(os.getpid()));time.sleep(300)" & '
f'node -e "require(\'fs\').writeFileSync(\'{dd}/n.pid\',String(process.pid));setInterval(()=>{{}},1000)" & wait')
def wait_pids(d, t=20):
for _ in range(t * 4):
try: return int(open(os.path.join(d, 'p.pid')).read()), int(open(os.path.join(d, 'n.pid')).read())
except Exception: time.sleep(0.25)
return None, None
def main():
bash = find_bash(); out = {'os': os.name, 'bash': bash}
if not bash: out['error'] = '找不到 bash'; print(json.dumps(out, ensure_ascii=False)); return
# tree
d = tempfile.mkdtemp(prefix='st_tree_')
p = subprocess.Popen([bash, '-c', tree_cmd(d)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **FLAGS)
pp, pn = wait_pids(d)
out['tree_started'] = pp is not None and pn is not None
if out['tree_started']:
if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(p.pid)], capture_output=True)
else: kill_session(p.pid)
time.sleep(3)
out['tree_left'] = {'python': alive(pp), 'node': alive(pn)}
out['tree_killed'] = not any(out['tree_left'].values())
for x in (pp, pn): # 清场
if alive(x):
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
else: os.kill(x, 9)
shutil.rmtree(d, ignore_errors=True)
# detach:启动器拉起子树后立即退出
d = tempfile.mkdtemp(prefix='st_det_')
launcher = ('import subprocess,sys;' f'f={FLAGS!r};' 'subprocess.Popen([sys.argv[1],"-c",sys.argv[2]],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**f)')
subprocess.run([sys.executable, '-c', launcher, bash, tree_cmd(d)], timeout=30)
pp, pn = wait_pids(d)
time.sleep(3)
out['survive_parent_exit'] = bool(pp and pn and alive(pp) and alive(pn))
for x in (pp, pn):
if x and alive(x):
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
else: os.kill(x, 9)
shutil.rmtree(d, ignore_errors=True)
print(json.dumps(out, ensure_ascii=False))
if __name__ == '__main__':
main()
Executable
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# 全流程冒烟(2026-10-18 第七轮,遗留 C-1:第 6–13 步从没跑过,第一次真跑要 15–28 h,中途才崩代价太大)。
# 在仓库旁边建一份副本(默认 ../<仓库名>_smoke,可设 SMOKE_DIR),用极小参数把 kin → accept 整条 `all` 跑一遍:
# 2000 条老师任务(够出真实的 keepRate / gapDrop / 间隙分位 / R2,作数据侧可行性依据)、极小模型、M5/APG/挖错各缩到几十题、bench 每集几题、查看器 3 题。
# 训练侧只验证"每个阶段都能跑完、产物格式对、阶段之间衔接对",模型数值没有意义(门槛按冒烟放行:M5_STRICT=0 R4_STRICT=0 R2_STRICT=0);
# 数据侧(stats / r2)数值有意义,汇总进 smoke.json。
# 不碰本仓库的 ml/data、ml/runs、webapp/models。结果:本仓库 ml/runs/smoke.json(每阶段返回码与耗时)+ 副本里的完整日志。
# 用法:bash ml/smoke.sh 预计 40–70 分钟(28 核 + 3090;生成 2000 条约 4 分钟,若门 1 触发共识约 20 分钟;其余阶段多数几分钟)
set -e -o pipefail
cd "$(dirname "$0")/.."
SRC=$(pwd); PY=${PYTHON:-python}
D=${SMOKE_DIR:-$(dirname "$SRC")/$(basename "$SRC")_smoke}
echo "[smoke] 副本目录:$D"
rm -rf "$D"; mkdir -p "$D"
# 只拷受版本管理的文件(含未提交改动)+ 运行需要的本地依赖;数据与产物不拷
git ls-files -z | tar --null -T - -cf - | (cd "$D" && tar xf -)
for x in ml/node_modules webapp/vendor/ort webapp/generated; do [ -d "$x" ] && { mkdir -p "$D/$(dirname $x)"; cp -r "$x" "$D/$x"; }; done
cd "$D"
export N_TASKS=2000 NPROC=${SMOKE_NPROC:-$(( $(nproc) > 24 ? 24 : $(nproc) ))} TARGET_SAMPLES=1 CFG=ml/configs/smoke.json BENCH_STEPS=50 \
M5_N=32 M5_STRICT=0 AUTO_WTA=0 R4_STRICT=0 R2_STRICT=0 R2_ARGS="--queries 2000 --pool 15000" \
R4_ARGS="--viewer-n 3" APG_FIRST_STEPS=20 ROUNDS=1 APG_STEPS=20 MINE_N=128 MINE_GATE_N=64 \
BENCH_ARGS="--nc-n 3 --route-n 3 --r8-n 2" VIEWER_N=3 MONITOR=0 FRESH=1 PIPE_OWNER=冒烟
t0=$(date +%s); rc=0
bash ml/pipeline.sh all 2>&1 | tee ml/runs_smoke.log || rc=$?
mkdir -p "$SRC/ml/runs"
$PY - "$D" "$rc" "$(( $(date +%s) - t0 ))" "$SRC/ml/runs/smoke.json" <<'PY'
import json, os, sys
d, rc, sec, out = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
p = os.path.join(d, 'ml', 'runs', 'pipeline.json')
st = json.load(open(p, encoding='utf-8')) if os.path.exists(p) else {}
stages = {k: {'rc': v.get('rc'), 'min': round((v['end'] - v['start']) / 60, 1) if v.get('end') and v.get('start') else None} for k, v in (st.get('stages') or {}).items()}
acc = os.path.join(d, 'ml', 'runs', 'acceptance.json')
def rd(*p):
try: return json.load(open(os.path.join(d, *p), encoding='utf-8'))
except Exception: return None
stx = rd('ml', 'data', 'stats.json') or {}
data = {k: stx.get(k) for k in ('tasks', 'keepRate', 'gapDrop', 'vertexMinGapMm', 'planMs', 'samplesPerTask', 'consensusDrop')}
data['r2'] = rd('ml', 'data', 'r2.json'); data['gate1'] = rd('ml', 'runs', 'consistency.json')
data['r4'] = rd('ml', 'runs', 'r4.json'); data['r9'] = rd('ml', 'runs', 'r9_browser.json')
pb = rd('ml', 'runs', 'policy_bench.json') or {}
data['r8_genS'] = (pb.get('r8_adversarial') or {}).get('genS')
res = {'rc': rc, 'wall_s': sec, 'dir': d, 'stages': stages, 'acceptance_written': os.path.exists(acc),
'data': data, 'log': os.path.join(d, 'ml', 'runs_smoke.log'),
'note': '训练侧只看每阶段 rc=0 与 acceptance_written;data 段(2000 条任务的统计、R2、门 1、R4、R9 路径、R8 出题耗时)是可行性依据'}
json.dump(res, open(out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
print('[smoke] rc=%d,%d 分钟;结果 %s' % (rc, sec // 60, out))
PY
exit $rc
+5 -2
View File
@@ -8,7 +8,7 @@ import torch
from ml.common.kin_torch import Kin from ml.common.kin_torch import Kin
from ml.common.features import STEP, D2R from ml.common.features import STEP, D2R
from ml.common.ckpt import Saver, atomic_save from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
from ml.data import Samples, load_tasks from ml.data import Samples, load_tasks
from ml.model.policy import build, wta_loss from ml.model.policy import build, wta_loss
from ml.model.meanflow import meanflow_loss, one_step from ml.model.meanflow import meanflow_loss, one_step
@@ -32,6 +32,7 @@ def main():
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/runs/base') ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/runs/base')
ap.add_argument('--config', default='ml/configs/base.json'); ap.add_argument('--steps', type=int, default=None) ap.add_argument('--config', default='ml/configs/base.json'); ap.add_argument('--steps', type=int, default=None)
ap.add_argument('--resume', default=None); ap.add_argument('--no-eval', action='store_true') ap.add_argument('--resume', default=None); ap.add_argument('--no-eval', action='store_true')
ap.add_argument('--stop-at', type=int, default=0, help='自检用:跑到第 N 步按停止请求处理(存 last.pt、rc 143)')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0) ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
a = ap.parse_args() a = ap.parse_args()
cfg = json.load(open(a.config)); tc = cfg['train'] cfg = json.load(open(a.config)); tc = cfg['train']
@@ -51,6 +52,7 @@ def main():
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model']) ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
if 'opt' in ck: opt.load_state_dict(ck['opt']) if 'opt' in ck: opt.load_state_dict(ck['opt'])
step0, best, bad_evals = ck.get('step', 0), ck.get('best', -1.0), ck.get('bad_evals', 0) step0, best, bad_evals = ck.get('step', 0), ck.get('best', -1.0), ck.get('bad_evals', 0)
if set_rng_state(ck.get('rng')): print('续训:随机数状态已恢复(batch 顺序与不断点一致)', flush=True)
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好 success {best:.4f}', flush=True) print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好 success {best:.4f}', flush=True)
total = tc['steps'] total = tc['steps']
lr_at = lambda s: tc['lr'] * (s / tc['warmup'] if s < tc['warmup'] else 0.5 * (1 + math.cos(math.pi * (s - tc['warmup']) / max(1, total - tc['warmup'])))) lr_at = lambda s: tc['lr'] * (s / tc['warmup'] if s < tc['warmup'] else 0.5 * (1 + math.cos(math.pi * (s - tc['warmup']) / max(1, total - tc['warmup']))))
@@ -64,6 +66,7 @@ def main():
saver = Saver(tc.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(原子写) saver = Saver(tc.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(原子写)
def save(name, step, extra=None): def save(name, step, extra=None):
d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'bad_evals': bad_evals, 'fp': kin.fp} d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'bad_evals': bad_evals, 'fp': kin.fp}
if name == 'last.pt': d['rng'] = rng_state()
if extra: d.update(extra) if extra: d.update(extra)
atomic_save(d, os.path.join(a.out, name)) atomic_save(d, os.path.join(a.out, name))
if name == 'last.pt': saver.mark() if name == 'last.pt': saver.mark()
@@ -100,7 +103,7 @@ def main():
if not torch.isfinite(loss): if not torch.isfinite(loss):
print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break
opt.step(); seen += X.shape[0] opt.step(); seen += X.shape[0]
if saver.stop(): if saver.stop() or (a.stop_at and step + 1 >= a.stop_at):
save('last.pt', step + 1); log.close() save('last.pt', step + 1); log.close()
print(f'收到停止请求:已存档 last.pt(第 {step + 1} 步),下次运行自动续训', flush=True); sys.exit(143) print(f'收到停止请求:已存档 last.pt(第 {step + 1} 步),下次运行自动续训', flush=True); sys.exit(143)
if saver.due(): save('last.pt', step + 1) if saver.due(): save('last.pt', step + 1)
+19 -24
View File
@@ -7,7 +7,7 @@
* 恢复 = 首次换向锁后 3 s 内到达(无锁则按到达计);活锁 = 超时未到达且锁 ≥ 2 次 * 恢复 = 首次换向锁后 3 s 内到达(无锁则按到达计);活锁 = 超时未到达且锁 ≥ 2 次
* 2026-10-18:--n 拆成 --nc-n / --route-n / --r8-n;输出 brakeRate(刹停次数 / 推理次数,验收 < 2%)。 * 2026-10-18:--n 拆成 --nc-n / --route-n / --r8-n;输出 brakeRate(刹停次数 / 推理次数,验收 < 2%)。
* 判据:到达(TCP < 1 mm,pose 另需姿态 < 0.5°)且执行轨迹无干涉帧(每步端点+中点 collideFree,外加距离认证复核); * 判据:到达(TCP < 1 mm,pose 另需姿态 < 0.5°)且执行轨迹无干涉帧(每步端点+中点 collideFree,外加距离认证复核);
* 超时 10 s。另报告推理 / 安全层耗时 P50/P99(Node CPU,仅作参考;浏览器延迟用查看器 ?policy=1 实测)。 * 超时 10 s。另报告推理 / 前瞻 / 安全层 / 整次规划耗时与距离查询数(Node CPU;浏览器同一批题由 ml.eval.r4_probe --viewer 在查看器里测,见 R9)。
* 依赖:cd ml && npm install(onnxruntime-node) */ * 依赖:cd ml && npm install(onnxruntime-node) */
'use strict'; 'use strict';
const fs = require('fs'), path = require('path'); const fs = require('fs'), path = require('path');
@@ -28,23 +28,8 @@ if (meta.feat_ver !== POL.FEAT_VER) { console.error('特征版本不符'); proce
if (meta.random_init) { console.error('这是 --random 导出的延迟探针模型,不是训练产物,拒绝评测(2026-10-18 第五轮,审查 N-5)'); process.exit(2); } if (meta.random_init) { console.error('这是 --random 导出的延迟探针模型,不是训练产物,拒绝评测(2026-10-18 第五轮,审查 N-5)'); process.exit(2); }
function lcg(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; } function lcg(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }
function ncCases(N, seed) { /* 贴立柱题移到 webapp/js/policy-cases.js(查看器 autotest 共用同一批题,2026-10-18 第六轮) */
const rnd = lcg(seed); const ncCases = (N, seed) => require('../webapp/js/policy-cases.js').ncCases(S, N, seed);
const rq = () => O.map(id => { const t = L[id]; return t.lo + rnd() * (t.hi - t.lo); });
const d = (a, b) => a.map((v, i) => L[O[i]].cont ? w(b[i] - v) : b[i] - v);
const segOK = (a, b) => { const dd = d(a, b); const m = Math.max(...dd.map(Math.abs)); const n = Math.max(1, Math.ceil(m)); for (let i = 1; i <= n; i++) if (!S.collideFree(a.map((v, k) => v + dd[k] * i / n))) return false; return true; };
const hitsColumn = q => (S.collideDetail(q) || []).some(p => /立柱/.test(p.cnA + p.cnB));
const nearColumn = q => { for (const k of [0, 1, 2]) for (const sg of [-4, 4]) { const p = q.slice(); p[k] += sg; if (hitsColumn(p)) return true; } return false; };
const cases = []; let tries = 0;
while (cases.length < N && tries < 4000000) {
tries++;
const b = rq(); if (!S.collideFree(b) || !nearColumn(b)) continue;
let a = null;
for (let t = 0; t < 200; t++) { const c = rq(); if (S.collideFree(c) && !segOK(c, b)) { a = c; break; } }
if (a) cases.push({ q0: a, T: S.fk(b, 0), R: S.orient(b) });
}
return cases;
}
function r8Cases(N, seed) { function r8Cases(N, seed) {
const rnd = lcg(seed); const rnd = lcg(seed);
const rq = () => O.map(id => { const t = L[id]; return t.lo + rnd() * (t.hi - t.lo); }); const rq = () => O.map(id => { const t = L[id]; return t.lo + rnd() * (t.hi - t.lo); });
@@ -77,12 +62,12 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
(async () => { (async () => {
const sess = await ort.InferenceSession.create(MODEL, { executionProviders: ['cpu'], intraOpNumThreads: 1 }); const sess = await ort.InferenceSession.create(MODEL, { executionProviders: ['cpu'], intraOpNumThreads: 1 });
const infer = POL.ortInfer(ort, sess, { encOnce: !!meta.enc_once }); const infer = POL.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn });
const cert = POL.makeCert(S, {}); const cert = POL.makeCert(S, {});
const out = { model: MODEL, meta: { fp: meta.fp, eval: meta.eval } }; const out = { model: MODEL, meta: { fp: meta.fp, eval: meta.eval } };
const runSet = async (name, cases, mode) => { const runSet = async (name, cases, mode) => {
let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0; let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0;
const inf = [], saf = [], times = [], fails = []; const inf = [], saf = [], look = [], planT = [], cq = [], times = [], fails = []; let hits = 0; const why = {};
for (let ci = 0; ci < cases.length; ci++) { for (let ci = 0; ci < cases.length; ci++) {
const c = cases[ci]; const c = cases[ci];
const R = mode === 'pose' ? c.R : null; const R = mode === 'pose' ? c.R : null;
@@ -101,24 +86,34 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
const { e, oe } = r.err(r.q); const { e, oe } = r.err(r.q);
const rc = r.done && e < 1 && oe < 0.5; const rc = r.done && e < 1 && oe < 0.5;
if (rc) reached++; if (cf) { colTasks++; colFrames += cf; } if (rc && !cf) { ok++; times.push(r.t); } else fails.push({ ci, err: +e.toFixed(1), oerr: +oe.toFixed(1), colFrames: cf, brakes: r.stats.brakes }); if (rc) reached++; if (cf) { colTasks++; colFrames += cf; } if (rc && !cf) { ok++; times.push(r.t); } else fails.push({ ci, err: +e.toFixed(1), oerr: +oe.toFixed(1), colFrames: cf, brakes: r.stats.brakes });
inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); brakes += r.stats.brakes; locks += r.stats.locks; plans += r.stats.plans; inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); cq.push(...r.stats.certQ); hits += r.stats.certHit;
for (const k in r.stats.certWhy) why[k] = (why[k] || 0) + r.stats.certWhy[k]; brakes += r.stats.brakes; locks += r.stats.locks; plans += r.stats.plans;
if (tLock != null) { lockTasks++; if (rc && !cf && r.t - tLock <= 3.0) recovered++; } else if (rc && !cf) recovered++; if (tLock != null) { lockTasks++; if (rc && !cf && r.t - tLock <= 3.0) recovered++; } else if (rc && !cf) recovered++;
if (!rc && r.stats.locks >= 2) livelock++; if (!rc && r.stats.locks >= 2) livelock++;
if ((ci + 1) % 50 === 0) process.stderr.write(`${name} ${ci + 1}/${cases.length}\n`); if ((ci + 1) % 50 === 0) process.stderr.write(`${name} ${ci + 1}/${cases.length}\n`);
} }
out[name] = { n: cases.length, ok, reached, collisionTasks: colTasks, collisionFrames: colFrames, uncertifiedSegs: uncert, brakes, locks, out[name] = { n: cases.length, ok, reached, collisionTasks: colTasks, collisionFrames: colFrames, uncertifiedSegs: uncert, brakes, locks,
plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock, plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock,
timeP50: pct(times, 0.5), inferMsP50: pct(inf, 0.5), inferMsP99: pct(inf, 0.99), safetyMsP50: pct(saf, 0.5), safetyMsP99: pct(saf, 0.99), fails: fails.slice(0, 15) }; timeP50: pct(times, 0.5), inferMsP50: pct(inf, 0.5), inferMsP99: pct(inf, 0.99), safetyMsP50: pct(saf, 0.5), safetyMsP99: pct(saf, 0.99),
// 2026-10-18 第六轮(审查 B-1/B-2):前瞻、整次规划耗时,每次规划的距离查询数与记忆命中率,认证失败原因——与查看器 autotest 同口径对照
lookaheadMsP99: pct(look, 0.99), planMsP50: pct(planT, 0.5), planMsP99: pct(planT, 0.99), certQueriesP50: pct(cq, 0.5), certQueriesP99: pct(cq, 0.99),
certMemoHitRate: (() => { const q = cq.reduce((a, b) => a + b, 0); return q + hits ? +(hits / (q + hits)).toFixed(3) : null; })(), certFailWhy: why, fails: fails.slice(0, 15) };
console.error(name, JSON.stringify({ ok, n: cases.length, collisionFrames: colFrames })); console.error(name, JSON.stringify({ ok, n: cases.length, collisionFrames: colFrames }));
}; };
if (SETS.includes('nc')) { const c = ncCases(+opt('--nc-n') || 30, 20261017); await runSet('near_column_pos', c, 'pos'); await runSet('near_column_pose', c, 'pose'); } if (SETS.includes('nc')) { const c = ncCases(+opt('--nc-n') || 30, 20261017); await runSet('near_column_pos', c, 'pos'); await runSet('near_column_pose', c, 'pose'); }
if (SETS.includes('route')) await runSet('route40', routeCases(+opt('--route-n') || 40), 'pos'); if (SETS.includes('route')) await runSet('route40', routeCases(+opt('--route-n') || 40), 'pos');
if (SETS.includes('r8')) { if (SETS.includes('r8')) {
/* 2026-10-18 第五轮(审查 N-4):出题不足额不再静默——记下要求题数与出题耗时,acceptance 对 n < requested 判不通过 */ /* 2026-10-18 第五轮(审查 N-4):出题不足额不再静默——记下要求题数与出题耗时,acceptance 对 n < requested 判不通过 */
const want = +opt('--r8-n') || 50, tg = Date.now(), cs = r8Cases(want, 8888), genS = (Date.now() - tg) / 1000; const want = +opt('--r8-n') || 50, tg = Date.now();
/* 2026-10-18 第七轮(遗留 C-4:R8 出题慢、耗时未知):出过的题按 碰撞指纹 + 题数 缓存到 ml/runs/,重跑 bench 不再重出 */
const cf = path.join(__dirname, `../ml/runs/r8_cases_${S.modelFingerprint()}_${want}.json`);
let cs = null; try { cs = JSON.parse(fs.readFileSync(cf, 'utf8')); } catch (e) {}
const cached = !!cs;
if (!cs) { cs = r8Cases(want, 8888); if (cs.length === want) try { fs.mkdirSync(path.dirname(cf), { recursive: true }); fs.writeFileSync(cf, JSON.stringify(cs)); } catch (e) {} }
const genS = (Date.now() - tg) / 1000;
if (cs.length < want) console.error(`R8 出题不足:${cs.length}/${want}(${genS.toFixed(0)} s)`); if (cs.length < want) console.error(`R8 出题不足:${cs.length}/${want}(${genS.toFixed(0)} s)`);
await runSet('r8_adversarial', cs, 'pos'); await runSet('r8_adversarial', cs, 'pos');
Object.assign(out.r8_adversarial, { requested: want, genS: +genS.toFixed(1) }); Object.assign(out.r8_adversarial, { requested: want, genS: +genS.toFixed(1), casesCached: cached });
} }
if (SETS.includes('nc1000')) await runSet('near_column_1000', ncCases(1000, 1), 'pos'); if (SETS.includes('nc1000')) await runSet('near_column_1000', ncCases(1000, 1), 'pos');
const s = JSON.stringify(out, null, 1); const s = JSON.stringify(out, null, 1);
+46 -4
View File
@@ -6,6 +6,8 @@
* &threads=N wasm 线程数(默认 min(16, ⌊0.6×逻辑核⌋);2026-10-18 R4 实测 28 核机 16 最优、24 反而变慢、4 线程 P99 37 ms) * &threads=N wasm 线程数(默认 min(16, ⌊0.6×逻辑核⌋);2026-10-18 R4 实测 28 核机 16 最优、24 反而变慢、4 线程 P99 37 ms)
* &proxy=0 关闭推理代理 Worker(默认开:wasm 推理不占主线程,见下) * &proxy=0 关闭推理代理 Worker(默认开:wasm 推理不占主线程,见下)
* &probe=1 允许加载 --random 导出的延迟探针模型(默认拒绝,防把随机策略当真模型跑) * &probe=1 允许加载 --random 导出的延迟探针模型(默认拒绝,防把随机策略当真模型跑)
* &autotest=N 自动跑 N 道贴立柱题(与 tests/policy_bench.cjs 同种子同题),结果 POST 到 &report=(默认 /r4/result);
* 由 python -m ml.eval.r4_probe --viewer 驱动,测部署形态的整次规划耗时与节拍(方案 R9)
* 2026-10-18 第五轮:ort.env.wasm.proxy=true 把 wasm 推理挪到 ort 自带的 Worker。此前推理在主线程同步算, * 2026-10-18 第五轮:ort.env.wasm.proxy=true 把 wasm 推理挪到 ort 自带的 Worker。此前推理在主线程同步算,
* R4 实测时 CDP 15 s 拿不到执行槽,50 Hz tick 也被它拖住——丢步不只来自推理时长本身。 * R4 实测时 CDP 15 s 拿不到执行槽,50 Hz tick 也被它拖住——丢步不只来自推理时长本身。
* 目标变化(松手求解 / 实时追踪)都交给 ArmPolicy.Runner:50 Hz 执行时钟放在 Web Worker(不受 rAF 60 Hz 与后台节流影响), * 目标变化(松手求解 / 实时追踪)都交给 ArmPolicy.Runner:50 Hz 执行时钟放在 Web Worker(不受 rAF 60 Hz 与后台节流影响),
@@ -18,7 +20,10 @@
const qs = new URLSearchParams(location.search); const qs = new URLSearchParams(location.search);
if (!qs.has('policy')) return; if (!qs.has('policy')) return;
const MODEL = qs.get('policy') && qs.get('policy') !== '1' ? qs.get('policy') : 'models/traj_policy.onnx'; const MODEL = qs.get('policy') && qs.get('policy') !== '1' ? qs.get('policy') : 'models/traj_policy.onnx';
const ORT_BASE = qs.get('ortLocal') === '1' ? 'vendor/ort/' : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/'; /* 2026-10-18 第六轮(审查 A-3):本地路径必须是绝对 URL——ORT 用 import() 加载 .mjs,相对前缀 'vendor/ort/' 会被当成裸模块名而失败 */
const ORT_BASE = qs.get('ortLocal') === '1' ? new URL('vendor/ort/', location.href).href : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/';
/* WebGPU 后端只在 ort.webgpu.min.js 里(ort.min.js 只有 wasm,第六轮实测 "[webgpu] backend not found" 即此因) */
const ORT_JS = qs.get('ep') === 'webgpu' ? 'ort.webgpu.min.js' : 'ort.min.js';
const badge = () => document.getElementById('ikBadge'); const badge = () => document.getElementById('ikBadge');
const say = (t, cls) => { const b = badge(); if (b) { b.textContent = t; b.className = 'badge ' + (cls || 'wait'); } console.log('[policy]', t); }; const say = (t, cls) => { const b = badge(); if (b) { b.textContent = t; b.className = 'badge ' + (cls || 'wait'); } console.log('[policy]', t); };
let ready = false, runner = null, running = false, worker = null; let ready = false, runner = null, running = false, worker = null;
@@ -37,7 +42,7 @@ async function boot() {
if (meta.tcp0 && meta.tcp0.some((v, i) => Math.abs(v - S.TCP0[i]) > 0.01)) throw new Error('TCP 与训练时不同:' + S.TCP0 + ' vs ' + meta.tcp0); if (meta.tcp0 && meta.tcp0.some((v, i) => Math.abs(v - S.TCP0[i]) > 0.01)) throw new Error('TCP 与训练时不同:' + S.TCP0 + ' vs ' + meta.tcp0);
const probe = !!meta.random_init; const probe = !!meta.random_init;
if (probe && qs.get('probe') !== '1') throw new Error('这是随机初始化的延迟探针模型(meta.random_init),不是训练产物;仅测延迟时加 &probe=1'); if (probe && qs.get('probe') !== '1') throw new Error('这是随机初始化的延迟探针模型(meta.random_init),不是训练产物;仅测延迟时加 &probe=1');
if (!window.ort) await loadScript(ORT_BASE + 'ort.min.js'); if (!window.ort) await loadScript(ORT_BASE + ORT_JS);
ort.env.wasm.wasmPaths = ORT_BASE; ort.env.wasm.wasmPaths = ORT_BASE;
const hc = navigator.hardwareConcurrency || 1; const hc = navigator.hardwareConcurrency || 1;
ort.env.wasm.numThreads = !self.crossOriginIsolated ? 1 : (+qs.get('threads') || Math.max(1, Math.min(16, Math.floor(hc * 0.6)))); ort.env.wasm.numThreads = !self.crossOriginIsolated ? 1 : (+qs.get('threads') || Math.max(1, Math.min(16, Math.floor(hc * 0.6))));
@@ -49,7 +54,7 @@ async function boot() {
catch (e) { console.warn('[policy] 代理 Worker 不可用,退回主线程推理:', e); proxy = false; ort.env.wasm.proxy = false; sess = null; } catch (e) { console.warn('[policy] 代理 Worker 不可用,退回主线程推理:', e); proxy = false; ort.env.wasm.proxy = false; sess = null; }
} }
if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' });
const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once }); const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn });
runner = new ArmPolicy.Runner(S, infer, {}); runner = new ArmPolicy.Runner(S, infer, {});
/* 预热:首次推理有编译开销 */ /* 预热:首次推理有编译开销 */
await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8); await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8);
@@ -65,18 +70,55 @@ async function boot() {
}, },
stop, stats: () => runner.stats, runner: () => runner, stop, stats: () => runner.stats, runner: () => runner,
latency: () => { const p = (a, x) => { const s = a.slice().sort((u, v) => u - v); return s.length ? s[Math.min(s.length - 1, Math.floor(s.length * x))] : null; }; latency: () => { const p = (a, x) => { const s = a.slice().sort((u, v) => u - v); return s.length ? s[Math.min(s.length - 1, Math.floor(s.length * x))] : null; };
return { inferP50: p(runner.stats.inferMs, .5), inferP99: p(runner.stats.inferMs, .99), safetyP50: p(runner.stats.safetyMs, .5), safetyP99: p(runner.stats.safetyMs, .99), threads: ort.env.wasm.numThreads, ep: ep[0], proxy, encOnce: !!meta.enc_once, probe }; } const st = runner.stats;
return { inferP50: p(st.inferMs, .5), inferP99: p(st.inferMs, .99), safetyP50: p(st.safetyMs, .5), safetyP99: p(st.safetyMs, .99),
lookaheadP99: p(st.lookaheadMs, .99), planP50: p(st.planMs, .5), planP99: p(st.planMs, .99), certQP99: p(st.certQ, .99),
tickGapP99: p(TICKS.gaps, .99), tickGapMax: TICKS.gaps.length ? Math.max(...TICKS.gaps) : null, starve: TICKS.starve, ticks: TICKS.n,
threads: ort.env.wasm.numThreads, ep: ep[0], proxy, encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn, probe }; }
}; };
ready = true; ready = true;
const how = ep[0] + ',' + ort.env.wasm.numThreads + ' 线程' + (proxy ? ',推理在 Worker' : ',推理在主线程'); const how = ep[0] + ',' + ort.env.wasm.numThreads + ' 线程' + (proxy ? ',推理在 Worker' : ',推理在主线程');
if (probe) say('随机探针模型(仅测延迟,动作无意义):' + how, 'badb'); if (probe) say('随机探针模型(仅测延迟,动作无意义):' + how, 'badb');
else say('策略模式就绪(' + how + '):拖动目标即由策略执行', 'ok'); else say('策略模式就绪(' + how + '):拖动目标即由策略执行', 'ok');
if (qs.get('autotest')) autotest(+qs.get('autotest') || 30, meta);
} catch (e) { say('策略模式不可用:' + e.message, 'badb'); console.error(e); } } catch (e) { say('策略模式不可用:' + e.message, 'badb'); console.error(e); }
} }
/* 节拍统计(R9):tick 间隔、饥饿次数(执行时钟到了但队列为空、又没到达 = 实际丢步) */
const TICKS = { gaps: [], last: 0, starve: 0, n: 0 };
/* autotest:同一批贴立柱题(PolicyCases.ncCases,与 Node bench 同种子 20261017)逐题跑,每题限时 10 s,结果 POST 回驱动方 */
async function autotest(N, meta) {
const S = window.IK.solver;
if (!window.PolicyCases) await loadScript(new URL('js/policy-cases.js', location.href).href);
const cases = PolicyCases.ncCases(S, N, 20261017), per = [];
const t0 = performance.now();
for (let i = 0; i < cases.length; i++) {
const c = cases[i]; stop(); await new Promise(r => setTimeout(r, 50));
window.IK.setJoints(c.q0, false); runner.reset(c.q0); runner.setGoal(c.T, null);
const s0 = { plans: runner.stats.plans, brakes: runner.stats.brakes }, ts = performance.now(); let col = 0;
TICKS.last = 0; start();
while (running && performance.now() - ts < 10000) {
await new Promise(r => setTimeout(r, 100));
if (runner.q && !S.collideFree(runner.q)) col++;
}
stop();
const { e } = runner.err(runner.q);
per.push({ i, reached: runner.done && e < 1, err: +e.toFixed(1), colSamples: col, plans: runner.stats.plans - s0.plans, brakes: runner.stats.brakes - s0.brakes, s: +((performance.now() - ts) / 1000).toFixed(2) });
}
const lat = window.__POLICY.latency();
const res = { kind: 'viewer_autotest', model: MODEL, probe: !!meta.random_init, n: cases.length, reached: per.filter(x => x.reached).length,
collisionTasks: per.filter(x => x.colSamples).length, brakes: runner.stats.brakes, plans: runner.stats.plans, certMemoHits: runner.stats.certHit,
certFailWhy: runner.stats.certWhy, wallS: +((performance.now() - t0) / 1000).toFixed(1), coi: self.crossOriginIsolated, ua: navigator.userAgent, ...lat, per };
say('autotest 完成:到达 ' + res.reached + '/' + res.n + ',整次规划 P99 ' + res.planP99 + ' ms', 'ok');
try { await fetch(qs.get('report') || '/r4/result', { method: 'POST', body: JSON.stringify(res) }); } catch (e) { console.warn(e); }
window.__POLICY.autotest = res;
}
function start() { if (running) return; running = true; worker.postMessage(ArmPolicy.DT * 1000); } function start() { if (running) return; running = true; worker.postMessage(ArmPolicy.DT * 1000); }
function stop() { running = false; if (worker) worker.postMessage(0); } function stop() { running = false; if (worker) worker.postMessage(0); }
function onTick() { function onTick() {
if (!running) return; if (!running) return;
const tn = performance.now(); if (TICKS.last) TICKS.gaps.push(tn - TICKS.last); TICKS.last = tn; TICKS.n++;
if (TICKS.gaps.length > 20000) TICKS.gaps.splice(0, 10000);
if (!runner.queue.length && !runner.done && runner.goal) TICKS.starve++;
if (runner.needPlan()) runner.plan().catch(e => { console.error(e); stop(); say('策略推理出错:' + e.message, 'badb'); }); if (runner.needPlan()) runner.plan().catch(e => { console.error(e); stop(); say('策略推理出错:' + e.message, 'badb'); });
const q = runner.tick(); const q = runner.tick();
if (q) window.IK.setJoints(q, false); if (q) window.IK.setJoints(q, false);
+55 -23
View File
@@ -59,12 +59,14 @@ function makeCert(S, opt) {
const L = S.LIM, O = S.ORDER; const L = S.LIM, O = S.ORDER;
const memo = new Map(); const memo = new Map();
const key = q => q.map(v => v.toFixed(5)).join(','); const key = q => q.map(v => v.toFixed(5)).join(',');
let queries = 0, budgetLeft = Infinity; let queries = 0, hits = 0, budgetLeft = Infinity, deadline = Infinity;
const gm = q => { const k = key(q); let M = memo.get(k); if (!M) { M = S.gapMatrix(q, CAP); queries++; budgetLeft--; if (memo.size > 50000) memo.clear(); memo.set(k, M); } return M; }; const clock = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
const gm = q => { const k = key(q); let M = memo.get(k); if (!M) { M = S.gapMatrix(q, CAP); queries++; budgetLeft--; if (memo.size > 50000) memo.clear(); memo.set(k, M); } else hits++; return M; };
const minGap = M => { let m = Infinity; for (let a = 0; a < 7; a++) for (let b = a + 2; b < 7; b++) m = Math.min(m, M[a * 7 + b]); return m; }; const minGap = M => { let m = Infinity; for (let a = 0; a < 7; a++) for (let b = a + 2; b < 7; b++) m = Math.min(m, M[a * 7 + b]); return m; };
const inBox = q => O.every((id, i) => L[id].cont || (q[i] >= L[id].lo - 1e-6 && q[i] <= L[id].hi + 1e-6)); const inBox = q => O.every((id, i) => L[id].cont || (q[i] >= L[id].lo - 1e-6 && q[i] <= L[id].hi + 1e-6));
function seg(a, b, depth) { function seg(a, b, depth) {
if (budgetLeft <= 0) return false; if (budgetLeft <= 0) return false;
if (deadline !== Infinity && clock() > deadline) { budgetLeft = 0; return false; } // 超时与超预算同样判不过(→ 刹停,保守)
const Ma = gm(a), Mb = gm(b); const Ma = gm(a), Mb = gm(b);
const dq = a.map((v, i) => Math.abs(b[i] - v)); const dq = a.map((v, i) => Math.abs(b[i] - v));
let ok = true; let ok = true;
@@ -79,19 +81,22 @@ function makeCert(S, opt) {
const m = a.map((v, i) => 0.5 * (v + b[i])); const m = a.map((v, i) => 0.5 * (v + b[i]));
return seg(a, m, depth + 1) && seg(m, b, depth + 1); return seg(a, m, depth + 1) && seg(m, b, depth + 1);
} }
/* 折线(相邻点按短弧展开)→ {ok, queries, minGap, failAt, why};budget = 本次最多新增距离查询数(超出判不过,why='budget') */ /* 折线(相邻点按短弧展开)→ {ok, queries, minGap, failAt, why};budget = 本次最多新增距离查询数(超出判不过,why='budget')
function path(pts, budget) { 2026-10-18 第六轮(审查 B-2):可选 until = 墙钟截止时刻(performance.now 口径),到点即判不过。
此前 Runner 只在两条候选之间看 30 ms 预算,单条候选内部 40 次查询 × 8 条可把安全层拖到 250 ms。 */
function path(pts, budget, until) {
const q0 = queries; let mg = Infinity; const q0 = queries; let mg = Infinity;
budgetLeft = budget || Infinity; budgetLeft = budget || Infinity; deadline = until || Infinity;
const P = [pts[0].slice()]; const P = [pts[0].slice()];
for (let i = 1; i < pts.length; i++) P.push(P[i - 1].map((v, k) => L[O[k]].cont ? v + wrap180(pts[i][k] - v) : pts[i][k])); for (let i = 1; i < pts.length; i++) P.push(P[i - 1].map((v, k) => L[O[k]].cont ? v + wrap180(pts[i][k] - v) : pts[i][k]));
for (let i = 0; i < P.length; i++) { if (!inBox(P[i])) return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why: 'box' }; } for (let i = 0; i < P.length; i++) { if (!inBox(P[i])) return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why: 'box' }; }
for (let i = 0; i < P.length; i++) mg = Math.min(mg, minGap(gm(P[i]))); for (let i = 0; i < P.length; i++) mg = Math.min(mg, minGap(gm(P[i])));
for (let i = 1; i < P.length; i++) if (!seg(P[i - 1], P[i], 0)) return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why: budgetLeft <= 0 ? 'budget' : 'cert' }; for (let i = 1; i < P.length; i++) if (!seg(P[i - 1], P[i], 0)) { const why = deadline !== Infinity && clock() > deadline ? 'time' : budgetLeft <= 0 ? 'budget' : 'cert'; budgetLeft = Infinity; deadline = Infinity; return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why }; }
budgetLeft = Infinity; budgetLeft = Infinity; deadline = Infinity;
return { ok: true, queries: queries - q0, minGap: mg, unwrapped: P }; return { ok: true, queries: queries - q0, minGap: mg, unwrapped: P };
} }
return { seg: (a, b) => { budgetLeft = Infinity; return seg(a, b, 0); }, path, leverTable: TL, gapMin: q => minGap(gm(q)), gm, clear: () => memo.clear() }; return { seg: (a, b) => { budgetLeft = Infinity; deadline = Infinity; return seg(a, b, 0); }, path, leverTable: TL, gapMin: q => minGap(gm(q)), gm, clear: () => memo.clear(),
counters: () => ({ queries, hits }) };
} }
/* ---------------- 刹停段:从 q 以每步增量 v 起,按 ACC 均匀减速到 0 ---------------- */ /* ---------------- 刹停段:从 q 以每步增量 v 起,按 ACC 均匀减速到 0 ---------------- */
@@ -174,10 +179,12 @@ class Runner {
this.o = Object.assign({ K: 8, commit: 3, margin: 5, /* 2026-10-18:运行时投影余量保持 5 mm(真机护栏,不迁就数据分布);数据侧另行过滤认证间隙 < 3 mm 的老师轨迹,APG 训练余量 3 mm */ this.o = Object.assign({ K: 8, commit: 3, margin: 5, /* 2026-10-18:运行时投影余量保持 5 mm(真机护栏,不迁就数据分布);数据侧另行过滤认证间隙 < 3 mm 的老师轨迹,APG 训练余量 3 mm */
projMax: 0.5, refineMm: 5, arriveMm: 1, arriveDeg: 0.5, projMax: 0.5, refineMm: 5, arriveMm: 1, arriveDeg: 0.5,
stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, seed: 12345, lookahead: true, stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, seed: 12345, lookahead: true,
certBudget: 40, safetyMs: 30 }, opts || {}); // 每条候选最多 40 次距离查询;每次推理安全层总预算 30 ms(超出按刹停处理) certBudget: 40, safetyMs: 30, firstMs: 45 }, opts || {}); // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理)
this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 }); this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 });
this.rng = makeRng(this.o.seed); this.rng = makeRng(this.o.seed);
this.stats = { plans: 0, inferMs: [], safetyMs: [], brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0 }; /* 2026-10-18 第六轮(审查 B-1/B-2):耗时拆开记——inferMs 主推理、lookaheadMs 前瞻(第二次推理 + 8 份特征)、safetyMs 只含认证、
planMs 整次规划(R9 口径:必须 < 60 ms 周期);certQ 每次规划新增的距离查询数,certHit 记忆命中数,certWhy 认证失败原因计数 */
this.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0 };
this.reset(null); this.reset(null);
} }
reset(q0) { reset(q0) {
@@ -224,13 +231,19 @@ class Runner {
const tail = brakeSteps(steps.length ? steps[steps.length - 1] : [0, 0, 0, 0, 0, 0]); const tail = brakeSteps(steps.length ? steps[steps.length - 1] : [0, 0, 0, 0, 0, 0]);
/* 刹停段各步同向等比减速 → 关节空间里是一条直线,认证终点一段即可 */ /* 刹停段各步同向等比减速 → 关节空间里是一条直线,认证终点一段即可 */
if (tail.length) { for (const d of tail) q = q.map((v, j) => v + d[j]); pts.push(q.slice()); } if (tail.length) { for (const d of tail) q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
const c = this.cert.path(pts, this.o.certBudget); const c = this.cert.path(pts, this.o.certBudget, this._until);
if (!c.ok) this.stats.certWhy[c.why || 'collide'] = (this.stats.certWhy[c.why || 'collide'] || 0) + 1;
return { ok: c.ok, tail }; return { ok: c.ok, tail };
} }
async plan() { async plan() {
if (!this.needPlan()) return; if (!this.needPlan()) return;
this.planning = true; this.planning = true;
try { await this._plan(); } finally { this.planning = false; } const t = now(), c0 = this.cert.counters();
try { await this._plan(); } finally {
this.planning = false; this._until = undefined;
const c1 = this.cert.counters();
this.stats.planMs.push(now() - t); this.stats.certQ.push(c1.queries - c0.queries); this.stats.certHit += c1.hits - c0.hits;
}
} }
async _plan() { async _plan() {
const S = this.S, o = this.o, K = o.K; const S = this.S, o = this.o, K = o.K;
@@ -258,9 +271,9 @@ class Runner {
for (let k = 0; k < K; k++) feat.set(f, k * F_DIM); for (let k = 0; k < K; k++) feat.set(f, k * F_DIM);
for (let i = 0; i < z.length; i++) z[i] = this.rng.n(); for (let i = 0; i < z.length; i++) z[i] = this.rng.n();
const t0 = now(); const t0 = now();
const out = await this.infer(feat, z, K); const out = await this.infer(feat, z, K, true); // true:K 行特征相同,enc_once 模型只送 1 行
this.stats.inferMs.push(now() - t0); this.stats.inferMs.push(now() - t0);
const t1 = now(); let t1 = now();
/* 子目标:新目标/跳变后取价值最好的那条输出 */ /* 子目标:新目标/跳变后取价值最好的那条输出 */
let best = 0; for (let k = 1; k < K; k++) if (out.value[k] < out.value[best]) best = k; let best = 0; for (let k = 1; k < K; k++) if (out.value[k] < out.value[best]) best = k;
if (this.needSub) { if (this.needSub) {
@@ -303,8 +316,10 @@ class Runner {
} }
let pool = cands; let pool = cands;
if (this.lock && this.t < this.lock.until) { const f2 = cands.filter(c => Math.abs(c.j1) < 2 || Math.sign(c.j1) === this.lock.side); if (f2.length) pool = f2; } if (this.lock && this.t < this.lock.until) { const f2 = cands.filter(c => Math.abs(c.j1) < 2 || Math.sign(c.j1) === this.lock.side); if (f2.length) pool = f2; }
/* 短前瞻:冷启动 / 卡死时,各候选块末态再推一块,用其价值打分 */ /* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。
if (o.lookahead && (this.cold || stuck)) { 2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐;
前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */
if (o.lookahead && this.cold) {
this.stats.lookahead++; this.stats.lookahead++;
const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6); const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6);
pool.forEach((c, i) => { pool.forEach((c, i) => {
@@ -312,17 +327,24 @@ class Runner {
feat2.set(features(S, { q: qa, qPrev: qb, T: this.goal.T, R: this.goal.R, prefix: [], sub: null }), i * F_DIM); feat2.set(features(S, { q: qa, qPrev: qb, T: this.goal.T, R: this.goal.R, prefix: [], sub: null }), i * F_DIM);
}); });
for (let i = 0; i < z2.length; i++) z2[i] = this.rng.n(); for (let i = 0; i < z2.length; i++) z2[i] = this.rng.n();
const o2 = await this.infer(feat2, z2, pool.length); const o2 = await this.infer(feat2, z2, pool.length, false); // 各行特征不同:必须逐行编码(第六轮修:此前 enc_once 下只送了第 1 行,前瞻价值全错)
pool.forEach((c, i) => { c.score = -2.0 * o2.value[i] - 0.5 * c.value; }); pool.forEach((c, i) => { c.score = -2.0 * o2.value[i] - 0.5 * c.value; });
this.stats.lookaheadMs.push(now() - t1); t1 = now();
} }
this.cold = false; this.cold = false;
pool.sort((a, b) => b.score - a.score); pool.sort((a, b) => b.score - a.score);
/* 安全层:依次认证,第一个通过即提交 */ /* 安全层:依次认证,第一个通过即提交 */
/* 预算(第六轮,审查 B-2):浏览器实测安全层 P99 249 ms。拆开量(沙箱 Node,每次调用):gapMatrix cap20 3.6 ms / cap40 5.5 ms、
collideFree 1.5 ms、criticalPairs 3.1 ms,推理本身 1.5 ms——耗时在几何查询,不在模型。此前截止只在候选之间检查,
单条候选最多 40 次查询(≈150 ms)不受限。现在墙钟截止下沉到认证内部:第 1 条候选截止 t1+firstMs,其余共享到 t1+safetyMs;
到点判不过 → 沿上次已认证的刹停段停,安全性不变,只是可能多刹停。 */
let picked = null; let picked = null;
for (const c of pool) { for (const c of pool) {
if (now() - t1 > o.safetyMs && picked === null && c !== pool[0]) break; if (c !== pool[0] && now() - t1 > o.safetyMs) break;
this._until = t1 + (c === pool[0] ? Math.max(o.firstMs, o.safetyMs) : o.safetyMs);
const v = this._certCommit(qe, c.steps); if (v.ok) { picked = c; this.brakeTail = v.tail; break; } const v = this._certCommit(qe, c.steps); if (v.ok) { picked = c; this.brakeTail = v.tail; break; }
} }
this._until = undefined;
this.stats.safetyMs.push(now() - t1); this.stats.safetyMs.push(now() - t1);
if (picked) { if (picked) {
this.queue.push(...picked.steps); this.lastPlan = [qe.slice()].concat(picked.fut); this.brakeRun = 0; this.queue.push(...picked.steps); this.lastPlan = [qe.slice()].concat(picked.fut); this.brakeRun = 0;
@@ -338,12 +360,22 @@ const now = () => (typeof performance !== 'undefined' ? performance.now() : Date
/* ---------------- onnxruntime 适配(web: ort = window.ort;node: require('onnxruntime-node')) ---------------- */ /* ---------------- onnxruntime 适配(web: ort = window.ort;node: require('onnxruntime-node')) ---------------- */
/* 2026-10-18 第五轮:模型 meta.enc_once=true(编码一次、解码 K 次)时只送第 1 行特征 [1,F]; /* 2026-10-18 第五轮:模型 meta.enc_once=true(编码一次、解码 K 次)时只送第 1 行特征 [1,F];
Runner 仍按 K 行组织特征(K 行本就相同),旧模型(enc_once 缺省)照旧送 [B,F]。 */ Runner 仍按 K 行组织特征(K 行本就相同),旧模型(enc_once 缺省)照旧送 [B,F]。 */
/* 2026-10-18 第六轮:第 4 个参数 shared 表示 K 行特征相同(主推理)。enc_once 模型的 feat 轴是动态的(1 或 K 行):
shared → 送 1 行(编码一次);前瞻各行不同 → 送 B 行逐行编码。 */
function ortInfer(ort, session, opt) { function ortInfer(ort, session, opt) {
const encOnce = !!(opt && opt.encOnce); const encOnce = !!(opt && opt.encOnce), featDyn = !!(opt && opt.featDyn); // featDyn:meta.feat_dyn(第六轮导出),feat 轴可为 1 或 K
return async (feat, z, B) => { const run = async (ft, zz, B) => { const r = await session.run({ feat: ft, z: new ort.Tensor('float32', zz, [B, H, 6]) }); return { act: r.act.data, value: r.value.data, sub: r.sub.data }; };
const ft = encOnce ? new ort.Tensor('float32', feat.subarray(0, F_DIM), [1, F_DIM]) : new ort.Tensor('float32', feat, [B, F_DIM]); return async (feat, z, B, shared) => {
const r = await session.run({ feat: ft, z: new ort.Tensor('float32', z, [B, H, 6]) }); if (!encOnce) return run(new ort.Tensor('float32', feat.slice(0, B * F_DIM), [B, F_DIM]), z, B);
return { act: r.act.data, value: r.value.data, sub: r.sub.data }; if (shared !== false) return run(new ort.Tensor('float32', feat.slice(0, F_DIM), [1, F_DIM]), z, B);
if (featDyn) return run(new ort.Tensor('float32', feat.slice(0, B * F_DIM), [B, F_DIM]), z, B);
/* 第五轮导出的 enc_once 模型 feat 轴固定为 1:前瞻逐行各推一次(正确但慢,重新导出即可) */
const act = new Float32Array(B * H * 6), value = new Float32Array(B), sub = new Float32Array(B * 13);
for (let i = 0; i < B; i++) {
const r = await run(new ort.Tensor('float32', feat.slice(i * F_DIM, (i + 1) * F_DIM), [1, F_DIM]), z.slice(i * H * 6, (i + 1) * H * 6), 1);
act.set(r.act, i * H * 6); value[i] = r.value[0]; sub.set(r.sub, i * 13);
}
return { act, value, sub };
}; };
} }
+32
View File
@@ -0,0 +1,32 @@
/* ============================================================================
* policy-cases.js —— 评测题目生成(Node 与浏览器共用,UMD)
* 2026-10-18 第六轮(审查 B-2):贴立柱题从 tests/policy_bench.cjs 移到这里,查看器 ?policy=…&autotest=N
* 与 Node bench 用同一个种子出同一批题,安全层耗时两边可以逐题对照(浏览器 vs Node 差 > 3× 要解释)。
* 算法与原 bench 逐字一致:同种子出的题与第五轮之前相同。
* ==========================================================================*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.PolicyCases = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function lcg(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }
/* 贴立柱任务:终点贴立柱(±4° 扰动即碰立柱)、起点到终点直线被挡;返回 [{q0, T, R}] */
function ncCases(S, N, seed) {
const L = S.LIM, O = S.ORDER, w = S.wrap180, rnd = lcg(seed);
const rq = () => O.map(id => { const t = L[id]; return t.lo + rnd() * (t.hi - t.lo); });
const d = (a, b) => a.map((v, i) => L[O[i]].cont ? w(b[i] - v) : b[i] - v);
const segOK = (a, b) => { const dd = d(a, b); const m = Math.max(...dd.map(Math.abs)); const n = Math.max(1, Math.ceil(m)); for (let i = 1; i <= n; i++) if (!S.collideFree(a.map((v, k) => v + dd[k] * i / n))) return false; return true; };
const hitsColumn = q => (S.collideDetail(q) || []).some(p => /立柱/.test(p.cnA + p.cnB));
const nearColumn = q => { for (const k of [0, 1, 2]) for (const sg of [-4, 4]) { const p = q.slice(); p[k] += sg; if (hitsColumn(p)) return true; } return false; };
const cases = []; let tries = 0;
while (cases.length < N && tries < 4000000) {
tries++;
const b = rq(); if (!S.collideFree(b) || !nearColumn(b)) continue;
let a = null;
for (let t = 0; t < 200; t++) { const c = rq(); if (S.collideFree(c) && !segOK(c, b)) { a = c; break; } }
if (a) cases.push({ q0: a, T: S.fk(b, 0), R: S.orient(b) });
}
return cases;
}
return { lcg, ncCases };
}));
+5 -4
View File
@@ -1,5 +1,6 @@
策略模型放这里(由 `python -m ml.export_onnx --fp16` 生成): 策略模型放这里(由 `bash ml/pipeline.sh export`,即 `python -m ml.export_onnx --ckpt ml/runs/current.pt` 生成,默认 fp32):
- `traj_policy.onnx` - `traj_policy.onnx`:输入 `feat[1 或 K,83]`、`z[K,16,6]`;输出 `act[K,16,6]`、`value[K]`、`sub[K,13]`
- `traj_policy.meta.json`(碰撞模型指纹 / 特征版本 / TCP,查看器加载时校验) - `traj_policy.meta.json`:碰撞模型指纹、特征版本、TCP、`enc_once` / `feat_dyn`。查看器加载时会校验,`random_init` 为真的延迟探针模型会被拒绝
查看器地址加 `?policy=1` 启用策略模式,详见 `ml/README.md` 第 8 步。 查看器地址加 `?policy=1` 启用策略模式(离线加 `&ortLocal=1`,先跑 `bash ml/prep/vendor_ort.sh`),详见 `ml/README.md` 第 10 步。
两个文件都不入库(`.gitignore`)。
+8 -2
View File
@@ -12,8 +12,14 @@
const post = async () => { o.textContent = JSON.stringify(res, null, 1); try { await fetch('/r4/result', { method: 'POST', body: JSON.stringify(res) }); } catch (e) {} }; const post = async () => { o.textContent = JSON.stringify(res, null, 1); try { await fetch('/r4/result', { method: 'POST', body: JSON.stringify(res) }); } catch (e) {} };
try { try {
const MODEL = qs.get('model'), N = +qs.get('n') || 150, K = 8, F = 83, H = 16; const MODEL = qs.get('model'), N = +qs.get('n') || 150, K = 8, F = 83, H = 16;
const BASE = qs.get('ortLocal') === '1' ? '/webapp/vendor/ort/' : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/'; /* 2026-10-18 第六轮:本地前缀用绝对 URL(ORT 以 import() 加载 .mjs,相对前缀会被当成裸模块名);WebGPU 后端只在 ort.webgpu.min.js 里 */
await new Promise((ok, no) => { const s = document.createElement('script'); s.src = BASE + 'ort.min.js'; s.onload = ok; s.onerror = () => no(new Error('加载 ort 失败:' + s.src)); document.head.appendChild(s); }); const BASE = qs.get('ortLocal') === '1' ? new URL('/webapp/vendor/ort/', location.href).href : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/';
res.ortBase = BASE;
if (res.ep === 'webgpu') { // 先看浏览器给不给适配器,区分"没有 GPU"与"ort 包不含 WebGPU"
res.hasGpu = !!navigator.gpu;
try { const ad = navigator.gpu && await navigator.gpu.requestAdapter(); res.adapter = ad ? ((ad.info && (ad.info.vendor + ' ' + ad.info.architecture)) || 'ok') : null; } catch (e) { res.adapter = 'error: ' + e; }
}
await new Promise((ok, no) => { const s = document.createElement('script'); s.src = BASE + (res.ep === 'webgpu' ? 'ort.webgpu.min.js' : 'ort.min.js'); s.onload = ok; s.onerror = () => no(new Error('加载 ort 失败:' + s.src)); document.head.appendChild(s); });
const meta = await (await fetch(MODEL.replace('.onnx', '.meta.json'))).json(); const meta = await (await fetch(MODEL.replace('.onnx', '.meta.json'))).json();
ort.env.wasm.wasmPaths = BASE; ort.env.wasm.wasmPaths = BASE;
// 与 ik-policy-glue.js 同一套默认值 // 与 ik-policy-glue.js 同一套默认值