Compare commits
20
Commits
main
...
fb711cb959
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb711cb959 | ||
|
|
34f19aac61 | ||
|
|
466a71ecea | ||
|
|
fd2f2db991 | ||
|
|
e6388b2f0c | ||
|
|
d70e2b90b8 | ||
|
|
a5e779c515 | ||
|
|
e3bb78388d | ||
|
|
14d72a9794 | ||
|
|
655e1fb93c | ||
|
|
d3b7c7b5ea | ||
|
|
0218d7fdd7 | ||
|
|
8bfe9bc083 | ||
|
|
7e70c4e811 | ||
|
|
b02b8291c4 | ||
|
|
5c74c152e9 | ||
|
|
3c7c814d40 | ||
|
|
c78ec0b48e | ||
|
|
919e704771 | ||
|
|
8ad5b3b4fa |
+43
-38
@@ -1,46 +1,51 @@
|
|||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# 构建产物(可用 python -m arm.* 重新生成)
|
|
||||||
out/
|
|
||||||
|
|
||||||
# 临时调试脚本与截图
|
|
||||||
scratch/
|
|
||||||
|
|
||||||
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
|
||||||
backups/
|
|
||||||
|
|
||||||
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
|
|
||||||
.bak/
|
|
||||||
*.bak_*
|
|
||||||
|
|
||||||
# 编辑器 / 系统文件
|
|
||||||
Thumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# 构建产物:由 python -m arm.* 重新生成,体积大(mesh 14MB + 单文件查看器 16MB×3),
|
|
||||||
# 过去被跟踪导致仓库背着 47MB 生成物;取消跟踪后重建不再脏 git status。
|
|
||||||
# 旧 web/ 单文件产物目录(已删,源码在 webapp/)
|
|
||||||
web/
|
|
||||||
/机器人手臂3D查看器.html
|
|
||||||
webapp/data/
|
|
||||||
webapp/generated/
|
|
||||||
dist/
|
|
||||||
data/cache/
|
|
||||||
|
|
||||||
# 关卡截图落点 = 纯生成物:每次 `python -m tests.verify --shots` 由 shot.py 重写
|
|
||||||
# (其中有 13 张在同一份构建下也不逐字节复现,抗锯齿/量化噪声)。
|
|
||||||
# 比对的权威物在 tests/baselines/ —— shots_stable.md5 是 92 张的 md5 名单,
|
|
||||||
# flaky/ 是那 13 张的参考像素 + thresholds.json 阈值;PNG 本身不需要入库。
|
# flaky/ 是那 13 张的参考像素 + thresholds.json 阈值;PNG 本身不需要入库。
|
||||||
docs/shots/
|
# 临时调试脚本与截图
|
||||||
|
# 关卡截图落点 = 纯生成物:每次 `python -m tests.verify --shots` 由 shot.py 重写
|
||||||
|
# 关卡比对的权威物是 tests/baselines/(md5 名单 + flaky 参考像素),不在这里。
|
||||||
# 其它编辑器的本地配置/会话残留(不是工程产物,也不该跟着版本走)
|
# 其它编辑器的本地配置/会话残留(不是工程产物,也不该跟着版本走)
|
||||||
|
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
||||||
|
# 定位图/ 只是"给用户看的核验图落点",一次性、会反复清空;
|
||||||
|
# 旧 web/ 单文件产物目录(已删,源码在 webapp/)
|
||||||
|
# 构建产物(可用 python -m arm.* 重新生成)
|
||||||
|
# 构建产物:由 python -m arm.* 重新生成,体积大(mesh 14MB + 单文件查看器 16MB×3),
|
||||||
|
# 比对的权威物在 tests/baselines/ —— shots_stable.md5 是 92 张的 md5 名单,
|
||||||
|
# 编辑器 / 系统文件
|
||||||
|
# 过去被跟踪导致仓库背着 47MB 生成物;取消跟踪后重建不再脏 git status。
|
||||||
|
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
|
||||||
|
# (其中有 13 张在同一份构建下也不逐字节复现,抗锯齿/量化噪声)。
|
||||||
|
*.bak_*
|
||||||
|
*.pyc
|
||||||
|
.bak/
|
||||||
.trae/
|
.trae/
|
||||||
.workbuddy/
|
.workbuddy/
|
||||||
.zcode/
|
.zcode/
|
||||||
.zcodeignore
|
.zcodeignore
|
||||||
|
/机器人手臂3D查看器.html
|
||||||
# 定位图/ 只是"给用户看的核验图落点",一次性、会反复清空;
|
Desktop.ini
|
||||||
# 关卡比对的权威物是 tests/baselines/(md5 名单 + flaky 参考像素),不在这里。
|
Thumbs.db
|
||||||
|
__pycache__/
|
||||||
|
backups/
|
||||||
|
data/cache/
|
||||||
|
dist/
|
||||||
|
docs/shots/
|
||||||
|
ml/data/
|
||||||
|
ml/node_modules/
|
||||||
|
ml/runs/
|
||||||
|
out/
|
||||||
|
scratch/
|
||||||
|
web/
|
||||||
|
webapp/data/
|
||||||
|
webapp/generated/
|
||||||
定位图/
|
定位图/
|
||||||
|
|
||||||
|
# 策略模型产物(由 python -m ml.export_onnx 生成,20 MB 级,不入库)
|
||||||
|
webapp/models/*.onnx
|
||||||
|
webapp/models/*.meta.json
|
||||||
|
|
||||||
|
# 从头重跑时旧产物挪到这里(不入库)
|
||||||
|
ml/archive/
|
||||||
|
|
||||||
|
# onnxruntime-web 本地副本(约 33 MB):**入库**,离线/无 npm 时直接可用,不再依赖 CDN(2026-10-18 用户要求)
|
||||||
|
# 来源 onnxruntime-web@1.20.1 dist,六个文件 + VERSION;ml/prep/vendor_ort.sh 在缺失时仍可补齐
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# ML 运行手册审查(ml/README.md)· 精简版
|
||||||
|
|
||||||
|
八轮审查(`0218d7f` → 本次)的已解决条目全部删去,原文在 git 历史里。本文件只留:实测数字 / 已定判据 / 仍待真数据回答的问题 / 正式训练步骤。
|
||||||
|
|
||||||
|
环境:本机 CBB-003(RTX 3090,训练可用 19–20 GB;28 核;Python 3.12 + torch 2.10;Edge + onnxruntime-web 1.20.1,已入库 `webapp/vendor/ort/`)。
|
||||||
|
当前结论:第 1–6 步本机实测可跑;R4 通过;自检 7/8 通过(唯一失败项是 Windows 限制,已接受);**审查条目已全部处置,下一步直接正式训练。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、实测数字(对照用)
|
||||||
|
|
||||||
|
| 项 | 数字 | 口径 |
|
||||||
|
|---|---|---|
|
||||||
|
| 对拍 300 条 | PASS(fk 3.4e-13 mm、特征 6e-8、间隙梯度 1.4e-6) | 第 2 步 |
|
||||||
|
| 生成 | 1.56 s/条 | 第 4 步,80 条 |
|
||||||
|
| 统计 | keepRate 0.977、samplesPerTask 36.5、cert.minGap p50 4.4 / p90 5.9 mm | 88 条,未加 3 mm 过滤 |
|
||||||
|
| 预训练 | 4.92 M 参数、6208 样本/s、60k 步 ≈ 2.8 h、显存峰值 2.76 GiB | batch 1024 |
|
||||||
|
| APG 显存 | batch 48 / plans 150 → 13.08 GiB;batch 128 必 OOM | d=192,1 步 |
|
||||||
|
| R4 部署口径 | wasm·16 线程·proxy P99 **12.67 ms**(< 15 通过);WebGPU P99 27.09,不用 | `feat_dyn` 新模型 |
|
||||||
|
| 自检 | 7/8,3.5 分钟;`train_term=false`(Windows 下 TERM 送不到 python,超时靠 STOP 文件 + 每 10 分钟存档) | `selftest.json` |
|
||||||
|
|
||||||
|
## 二、已定的判据与做法
|
||||||
|
|
||||||
|
| 事项 | 定为 |
|
||||||
|
|---|---|
|
||||||
|
| R9(真查看器、训练好的模型、30 道贴柱题) | 稳态 `planWarmP99` < 45 ms、运动中饥饿 < 1%、认证超时占比 < 5%;冷启动 P99、刹停率、`tickGapMax` 只报 |
|
||||||
|
| R9 不过 | 几何查询(特征、投影、认证)搬进 Worker,主线程只执行 |
|
||||||
|
| APG | batch 48、steps 8000(总任务量同原 128×3000),进程显存硬帽 `VRAM_FRAC=0.78` |
|
||||||
|
| 离线 ort | 只用入库本地副本,缺失即报错,不退回 CDN |
|
||||||
|
| 训练余量 3 mm | `stats.json` 间隙 p10 ≥ 3 mm 保持,否则降到 p10 |
|
||||||
|
| `R2_STRICT` | `pool_check.independent=true` 且截距 < 0.10 时打开 |
|
||||||
|
| 共识过滤 | 门 1 < 0.90 自动开 |
|
||||||
|
| 分歧 | 实现方为准,下一轮数据定 |
|
||||||
|
|
||||||
|
## 三、仍待真数据回答(正式训练会自动产出,不单独验证)
|
||||||
|
|
||||||
|
1. `gapDrop`:3 mm 过滤的真实剔除比例 → `stats.json`。
|
||||||
|
2. R2 池无关性 → `r2.json` 的 `pool_check`。
|
||||||
|
3. R8 出题耗时 → `policy_bench.json` 的 `genS`。
|
||||||
|
4. APG 收敛与机时(batch 48 下首测)→ APG 日志。
|
||||||
|
5. 真模型 R9 → `r9_browser.json`。
|
||||||
|
|
||||||
|
## 四、正式训练(三段,每段跑完回传)
|
||||||
|
|
||||||
|
| 段 | 命令 | 耗时 | 回传 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 数据 | `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=<出错阶段>` 接着跑,前面的不重做。
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,530 @@
|
|||||||
|
# Transformer 避障运动策略:最终方案
|
||||||
|
|
||||||
|
> 基于 `main@3c7c814`(贴立柱修复后的规划器)。本文是方案,代码尚未编写。
|
||||||
|
> 训练机:RTX 3090 24 GB,训练可用 19–20 GB(桌面常驻 4–5 GB,进程硬帽 0.78)。部署:浏览器(后续真机)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 定位与边界
|
||||||
|
|
||||||
|
| 项目 | 决定 |
|
||||||
|
|---|---|
|
||||||
|
| 部署形态 | **无运行时老师**:运行时只有「神经策略 + 碰撞安全层」,不加载 IK 大表、PRM 路网、A*/RRT,也不调用 `solveDyn` / `routeRT` |
|
||||||
|
| 运行方式 | **闭环滚动执行**:执行频率 50 Hz(每步 20 ms);每 60 ms 读一次当前关节角和目标,推理一段 16 步动作块,安全层认证后提交前 3 步 |
|
||||||
|
| 碰撞安全层 | 不做搜索,只对模型给出的候选做**距离下界认证**(§8.3),不安全就换候选或沿已认证刹停段停下 |
|
||||||
|
| 老师的角色 | 只在**离线**出现:生成预训练数据;后训练以可微碰撞代价为主(§7) |
|
||||||
|
| 不做的事 | 不学动力学(速度/加速度上限由时间律与插补器保证);不用相机/点云(环境固定:台架 + P01 立柱,几何已知) |
|
||||||
|
| 收益目标 | 冷启动(新目标到第一个动作)< 10 ms;每次推理 + 安全认证 < 20 ms;消除现有拖拽重规划 P99 约 321 ms 的长尾 |
|
||||||
|
| 底线 | 实时拖拽**碰撞帧 = 0**(由安全层距离认证保证);到达率、行程在 §9 指标上不低于现有规划器 |
|
||||||
|
|
||||||
|
**关于"风险":** 论文证据只能说明"别人在相近条件下做成过",没有一篇论文的条件(低维状态、单根固定立柱、约 4M 参数、浏览器推理、30/30)与本项目完全一致。因此本方案中只有安全层是**硬保证**;每个设计点都已定为唯一选择(§1.1),只有 MeanFlow 头带一个预先写定触发条件的回退(碰撞与否不依赖模型好坏);其余每一项都配有证据等级、本仓库可测的**门禁**(不过不进下一步)和**回退**(§10)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 方案总览
|
||||||
|
|
||||||
|
| 模块 | 最终选择 | 证据等级 | 关键依据 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 模型规模 | 约 4.9M 参数编码器-解码器 Transformer,不用 VLA 大模型 | A | IMPACT 4.67M、300 Hz;π0 3B 单次预填充 46 ms(4090) |
|
||||||
|
| 数据 | 规划器离线造 5 万条任务 → 约 200 万"状态→动作块"样本 | B | MπNets、DRP、ELMP 均用规划器老师 |
|
||||||
|
| 时间律 | 匀速巡航 + 梯形加减速,只依赖"当前速度 + 剩余路程";当前速度作输入 | B | MπNets 将老师路径重定时为匀速 |
|
||||||
|
| 动作表示 | 16 步 × 6 轴关节增量 Δq | A | DRP;Diffusion Policy(位置式优于速度式) |
|
||||||
|
| 动作头 | **MeanFlow 一步生成头**,每次并行采 8 个噪声 → 8 条候选 | B | 沙箱双模态实验(§2.4);MP1 约 6.8 ms |
|
||||||
|
| 块间连续 | 训练时前缀条件化:输入已提交未执行的步 | B | 2512.05964:推理零开销,d≥2 时优于推理时 RTC |
|
||||||
|
| 碰撞修正 | 输出后 1 步解析胶囊距离梯度投影(每轴 ≤ 0.5°,仅 d < 余量时) | B | DiffusionSeeder:少步生成下在环引导收益很小;投影在已知环境下够用 |
|
||||||
|
| 候选排序 | 与已提交块重叠步的一致性打分 + 价值头 | B | BID:一致性打分 + 多样本相对提升 32% |
|
||||||
|
| 预训练 | 行为克隆 + FK 胶囊端点损失 | B | ELMP 式 FK 损失 |
|
||||||
|
| 后训练(主) | APG:可微运动学展开 + 胶囊距离代价反传 | A | ELMP:碰撞率 31.3%→8.9%;1 万示范 + APG 胜 60 万纯 BC |
|
||||||
|
| 后训练(辅) | 无老师挖错循环 + 门禁 | B | DRP 迭代微调 +45% |
|
||||||
|
| 安全层 | 距离下界认证 + 可刹停不变式 | A | Schwarzer 2005 连续认证;Thumm & Althoff 2022 failsafe |
|
||||||
|
| 30/30 手段 | 子目标头、价值头、冷启动/卡死时短前瞻、卡死换向 | B / C | AWE、SHAC、BID |
|
||||||
|
| 老师 | PRM 路网 A* + 捷径平滑(确定性);A* 失败才用 `solveDyn` | B | MπNets:老师不一致碰撞率 13.78%,一致老师 0.94% |
|
||||||
|
| 推理后端 | onnxruntime-web wasm-SIMD 多线程,推理放代理 Worker(不占主线程),线程数 min(16, ⌊0.6×逻辑核⌋);R4 探针同测 WebGPU,wasm 不达标而 WebGPU 达标则部署用 WebGPU | C | 小模型下 WebGPU 调度开销通常占主导,以 R4 实测为准 |
|
||||||
|
|
||||||
|
### 1.1 取舍决定(每项只有一个答案)
|
||||||
|
|
||||||
|
| 选项 | 利 | 弊 | 决定 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 动作头:MeanFlow 一步头 vs WTA K=4 | MeanFlow:连续多模态,能同时表达左/右绕行与位姿目标的多个 IK 分支;沙箱 1 步达到 10 步流匹配质量。WTA:训练稳、实现简单 | MeanFlow:训练需 JVP(训练慢约 1.5–2 倍)、原生版本在部分任务失败(K-MF 表 1)。WTA:只有 4 个固定模式,左右 × 肘/腕分支可能超过 4 个;沙箱分不出两者高下 | **上 MeanFlow**。WTA 不实现;只有 M5 门禁不过(near_column 穿柱率高于 2% 或训练发散)才换成 WTA K=8,二者共用骨干,切换约 1 天 |
|
||||||
|
| Shortcut + 条件 OT 配对 | 无需 JVP | 本项目无任何实测,组合无论文先例 | **不上** |
|
||||||
|
| B 样条动作表示 | 平滑由表示保证;输出维度小 | 贴柱时拐角被抹圆、局部精度下降;只有间接证据;平滑已由梯形时间律 + 平滑损失保证 | **不上**,固定 16 步 Δq |
|
||||||
|
| 输出后梯度投影 | 修掉 1–2 mm 的轻微侵入,成本约 0.1 ms,减少刹停 | 推离数据分布、改变块间一致性;修不了"绕错侧" | **上**,限 1 步、每轴修正量 ≤ 0.5°,仅 d < 余量时触发 |
|
||||||
|
| 短前瞻(多推一块打分) | 避开"走两步就撞墙"的分支 | 推理次数翻倍,加剧最大不确定项(浏览器延迟) | **只在冷启动和卡死换向时上**,平时不做 |
|
||||||
|
| 拒绝采样自蒸馏 | 进一步缩短行程 | 偏向贴柱、余量变小、多样性塌缩;APG 行程项已覆盖同一目标 | **不上** |
|
||||||
|
| ReinFlow 强化学习微调 | 能直接优化到达/卡死/刹停等离散目标 | RL 会重塑左右模式比例;奖励调参与训练多出数天;这些离散目标已由卡死换向规则 + APG 覆盖 | **不上** |
|
||||||
|
| 挖错循环中 E2 错题请老师补路径 | 解决可微代价绕不出的局部极小 | 量小、离线 | **上** |
|
||||||
|
| 老师:PRM 路网 A* vs `solveDyn` | A*:确定性,同类任务左右选择一致,且是路网上的最短;`solveDyn` 覆盖更全 | A*:受路网分辨率限制,个别任务失败 | **A* 为主老师**,失败的任务用 `solveDyn`(固定种子)补 |
|
||||||
|
| 推理后端:WebGPU vs wasm | WebGPU 大模型快 | 本模型约 4M 参数,WebGPU 每次调度 33–67 μs × 150–300 次,调度开销反而大 | **wasm-SIMD 多线程**;M4 实测若 WebGPU P99 更低再换 |
|
||||||
|
| 训练余量 | 越大越不易被认证拒绝 | 越大行程越长、贴柱越难;高于老师轨迹自身间隙(实测最小间隙 p50 4.4 / p90 5.9 mm)会与 BC 标签对抗 | 数据过滤与 APG 碰撞项 **3 mm**(认证最小间隙 < 3 mm 的老师轨迹不进训练集);运行时投影护栏独立保持 **5 mm**,护栏不迁就数据分布 |
|
||||||
|
| 关节噪声增广 σ | 覆盖偏离状态 | 过大产生无意义状态 | **1°** |
|
||||||
|
| 局部牛顿精修 | 末端精度到 < 1 mm | 无 | **上**,同样过安全层 |
|
||||||
|
|
||||||
|
证据等级:**A** 有直接先例与数字 · **B** 有相近先例,需本仓库验证 · **C** 仅推断。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 依据
|
||||||
|
|
||||||
|
### 2.1 业界神经运动规划
|
||||||
|
|
||||||
|
| 工作 | 年份 / 会议 | 输入 | 输出 | 老师与数据量 | 关键做法 | 运行方式 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **MπNets**(NVIDIA) | 2022 CoRL | 点云 + 关节角 + 目标位姿 | 下一步关节位置 | 规划器 300 万+ 问题 | 端到端行为克隆;老师路径重定时为匀速 | 闭环 9 Hz,1 kHz PD 跟踪 |
|
||||||
|
| **Neural MP**(CMU) | 2024 / IROS 2025 | 点云 + 关节角 + 目标 | GMM(LSTM) | 100 万–300 万条 | 测试时采样多条,选穿透最少的(去掉后成功率 81%→31%) | 开环为主 |
|
||||||
|
| **DRP / IMPACT**(CMU) | 2025 CoRL | 点云 + 关节角 + 目标关节角 | 10 步关节增量 | cuRobo 1000 万条 | 大规模预训练 + 迭代师生微调 + "目标被挡停在最近安全处" | 闭环;4.67M 参数,300 Hz |
|
||||||
|
| **ELMP**(ETH) | 2026 arXiv | 点云 + 关节角 + 目标位姿 | 关节增量 | AIT* 约 60 万条 | 行为克隆后 **APG 微调**,碰撞/到达/平滑损失直接反传 | 闭环 30 Hz |
|
||||||
|
|
||||||
|
- ELMP:1 万条老师轨迹 + APG(成功率 72.0%)超过 60 万条纯行为克隆(57.9%);APG 把碰撞率降到 1/3 以下。
|
||||||
|
- 最好的方法在静态场景仍有约 9–10% 碰撞(ELMP-APG 8.9%,DRP 10.16%),实际部署都配检查或反应层。
|
||||||
|
- 共识:闭环、短动作块滚动执行、老师只离线用、必须后训练。
|
||||||
|
|
||||||
|
### 2.2 本项目的条件
|
||||||
|
|
||||||
|
| 条件 | 主流 | 本项目 | 意味着 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 环境 | 未知、多变,靠点云 | 固定且精确已知(台架 + 立柱,1936 个胶囊) | 不需要点云编码器;模型可以小;碰撞检查精确 |
|
||||||
|
| 机器人 | Franka 7 轴 | 自研 6 轴,J1/J3/J6 连续转 | 需处理连续轴展开 |
|
||||||
|
| 碰撞模型 | SDF / 球近似 | 胶囊解析距离,可微 | 直接做 APG;安全层可认证 |
|
||||||
|
| 平台 | 台式 GPU | 浏览器(后续真机) | 模型小,ONNX + WebGPU/wasm |
|
||||||
|
|
||||||
|
资源应投在**闭环稳定性、左右绕行选择和安全层**上,而不是感知和泛化。
|
||||||
|
|
||||||
|
### 2.3 前沿生成式技术取舍
|
||||||
|
|
||||||
|
| 技术 | 结论 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| VLA 大模型(π0/π0.5、GR00T、OpenVLA) | 不采用 | 无图像、无语言指令;3B 参数更慢不更准 |
|
||||||
|
| 一步生成:MeanFlow(MP1) | **采用** | 1 次前向;沙箱双模态实验通过 |
|
||||||
|
| Consistency Policy、OneDP、朴素 1 步流匹配 | 不采用 | CP 原文承认偏向一侧;OneDP 有意取单模态;朴素 1 步 FM 等于条件均值,会穿柱 |
|
||||||
|
| Shortcut / 条件 OT 配对 | 不采用 | 见 §1.1 |
|
||||||
|
| OFP 自蒸馏 | 不采用 | 未找到公开代码;自引导 λ=0.5 时出现模态塌缩 |
|
||||||
|
| 生成过程中代价引导(MPD/EDMP/GPD) | 不采用 | 无论文在 1–2 步下验证;改用输出后投影 |
|
||||||
|
| 推理时 RTC | 不采用 | 原文只验证 5 步去噪且需反向传播;改用训练时前缀条件化 |
|
||||||
|
| 时间集成 / 交叉淡入 | 不采用 | 执行的是未认证的混合块;左右两块平均可能穿柱;π0 弃用 |
|
||||||
|
| FAST 动作分词 | 不采用 | 服务于语言骨干,自回归推理慢 |
|
||||||
|
| B 样条动作表示 | 不采用 | 见 §1.1 |
|
||||||
|
| ReinFlow / DPPO | 不采用 | 见 §1.1 |
|
||||||
|
|
||||||
|
### 2.4 沙箱双模态对照实验
|
||||||
|
|
||||||
|
2D 点从柱左侧到右侧,示教一半从上绕一半从下绕,16 步动作块、每 3 步重推理,与本方案同构。代码 `ml/toy/toy_bimodal.py`,150 个"起终点都贴近中轴"的难例:
|
||||||
|
|
||||||
|
| 动作头 | 单样本、无安全层:到达 / 穿柱回合 | 8 候选 + 安全层:到达 / 穿柱 / 平均刹停 | 推理(CPU,8 候选) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| MSE 回归 | 100% / 0% | 100% / 0% / 0 | 0.13 ms |
|
||||||
|
| WTA K=4 | 100% / 0% | 100% / 0% / 0 | 0.19 ms |
|
||||||
|
| 流匹配 10 步 | 100% / 4.0% | 99.3% / 0% / 3.7 | 2.13 ms |
|
||||||
|
| 朴素流匹配 1 步 | 87.3% / **23.3%** | 81.3% / 0% / 14.0 | 0.25 ms |
|
||||||
|
| **MeanFlow 1 步** | 100% / 2.7% | 100% / 0% / 0.3 | 0.25 ms |
|
||||||
|
|
||||||
|
- 朴素 1 步流匹配会把模态平均进柱子;MeanFlow 1 步达到 10 步流匹配的质量,推理快约 8 倍。
|
||||||
|
- 安全层在所有方法上把穿柱降为 0,代价是刹停。
|
||||||
|
- 该任务过于简单(闭环中状态一偏离中轴即打破对称),**分不出一步头与 WTA 谁更好**;因此 WTA 只作为 M5 门禁不过时的预定回退(§1.1)。
|
||||||
|
- 训练仅 300 步的 MSE 在同一难例上 100% 穿柱、加安全层后 0% 到达,说明欠拟合时平均模态的风险真实存在。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 训练环境
|
||||||
|
|
||||||
|
| 阶段 | 在哪里跑 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 数据生成 | Hark 沙箱(2 vCPU / 7 GB,Node)或用户机器 | 纯 JS,无需 GPU |
|
||||||
|
| 预训练 + 后训练 | 用户 RTX 3090(可用 19–20 GB) | 预训练 batch 1024(峰值 2.76 GiB);APG batch 48 × 8000 步(峰值 13.08 GiB) |
|
||||||
|
| 部署推理 | 浏览器 onnxruntime-web wasm-SIMD 多线程(代理 Worker);ONNX 接口 feat[1,83]、z[K,16,6]:观测编码一次、K=8 条候选共享编码再解码;后续真机控制器 | 与现有查看器同页 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda create -n armtf python=3.11 -y && conda activate armtf
|
||||||
|
pip install torch --index-url https://download.pytorch.org/whl/cu121 # 按本机 CUDA 版本选
|
||||||
|
pip install numpy pyarrow tqdm tensorboard onnx onnxruntime
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 离线数据(老师只在这里出现)
|
||||||
|
|
||||||
|
### 4.1 文件
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `ml/gen/gen_dataset.cjs` | 采样任务 → 调老师 → 校验 → 时间参数化 → 写分片 |
|
||||||
|
| `ml/gen/sampler.cjs` | 起点/目标采样 |
|
||||||
|
| `ml/gen/run_parallel.sh` | 多进程,不同种子 |
|
||||||
|
| `ml/data/shard_XXXX.jsonl` | 原始样本(不入库) |
|
||||||
|
|
||||||
|
复用 `tests/route_bench_setup.cjs` 装配求解器(关节表、`docs/travel.json`、`docs/collide.json`)。
|
||||||
|
|
||||||
|
### 4.2 原始样本
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "s0003_000127", "mode": "pos" | "pose",
|
||||||
|
"q0": [6], "T": [x,y,z], "R": [[3x3]] | null, "tool": 0,
|
||||||
|
"path": [[6], ...],
|
||||||
|
"reached": true,
|
||||||
|
"travel_deg": 123.4, "plan_ms": 812.0,
|
||||||
|
"fp": "collide.json 指纹"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`reached=false` 表示目标不可达,路径终点为离目标最近的安全位形("停靠")。
|
||||||
|
|
||||||
|
### 4.3 采样分布(5 万条任务)
|
||||||
|
|
||||||
|
关节限位:J1/J3/J6 连续 ±180°;J2 [−92, 9],J4 [−111, 16],J5 [−93, 34]。
|
||||||
|
|
||||||
|
| 类别 | 占比 | 采样方式 |
|
||||||
|
|---|---:|---|
|
||||||
|
| A 均匀 | 45% | q0、目标位形 b 均匀采样且无干涉;目标 = `fk(b)`(pose 模式加 `orient(b)`) |
|
||||||
|
| B 直线被挡 | 30% | q0→b 关节直线有干涉 |
|
||||||
|
| C 贴立柱 | 15% | b 满足 `nearColumn`,q0 在立柱另一侧 |
|
||||||
|
| D 目标不可达 | 10% | 目标在立柱内/所有分支都撞/工作空间外;标签为停靠 |
|
||||||
|
|
||||||
|
- pos / pose 各半;老师为 PRM 路网 A* + 捷径平滑(确定性;复用 `routeRT` 的路网图搜索,放宽时间预算);A* 失败的任务用 `solveDyn`(固定种子)补。
|
||||||
|
- 另造三个互不重叠的集合:**门禁集** 2000 条(挖错循环每轮用)、**held-out 验收集** 2000 条、**near_column 扩展验收集** ≥1000 条;两个验收集只在最终验收时用一次。现有三个基准不进训练集。
|
||||||
|
|
||||||
|
### 4.4 过滤
|
||||||
|
|
||||||
|
1. 整条路径用距离下界认证(与 §8.3 同口径)复核,任一段不过即丢弃。仓库 `tests/route_bench.cjs` 上现有规划器有 1/40 例路径细验不过,这类样本必须丢掉。
|
||||||
|
2. `poseRelaxed` 样本只当 pos 模式用。
|
||||||
|
3. 行程 > 同任务 `routeRT` 1.5 倍的先统计,再定是否丢弃;按类别统计丢弃率(C 类 > 20% 说明老师在贴柱场景不可靠)。
|
||||||
|
|
||||||
|
### 4.5 时间参数化 → 闭环训练样本
|
||||||
|
|
||||||
|
1. 时间律:按轴速比 `VREL = [0.6, 0.6, 0.9, 0.9, 1.2, 1.8]` 匀速巡航,梯形加减速,速度只取决于"当前速度 + 剩余路程"(每轴加速度上限 a_max 为待定参数,真机型号确定后设定),dt = 20 ms。
|
||||||
|
2. 得到状态序列 q_0 … q_N,每隔 3 步取一个样本:
|
||||||
|
- 输入:§5.1 全部字段(含当前速度、已提交前缀)
|
||||||
|
- 标签:未来 16 步 Δq_{t+k} = q_{t+k} − q_t(到终点后补零)
|
||||||
|
3. 每条路径约 40–50 个样本 → 约 200 万样本(以 `stats.json` 为准)。
|
||||||
|
4. 协变量偏移对策:关节噪声增广(30% 样本,σ = 1°;噪声态须无干涉且间隙 > 1 mm,标签为 8 步内线性收回老师轨迹的增量);偏离状态的进一步覆盖靠 §7。
|
||||||
|
|
||||||
|
### 4.6 耗时与存储
|
||||||
|
|
||||||
|
- 路网 A* 每条数十毫秒,耗时主要在过滤认证;按 `solveDyn` 0.7 s/条上限估算,沙箱 2 进程不超过 5 小时。
|
||||||
|
- 200 万样本以 float32 `.npy` 存放约 1.5 GB(训练时整体载入内存)。
|
||||||
|
|
||||||
|
### 4.7 检查点
|
||||||
|
|
||||||
|
- [ ] `ml/data/stats.json`:类别、到达率、行程、样本数、补零样本占比
|
||||||
|
- [ ] 老师一致性:起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90%
|
||||||
|
- [ ] 抽 20 条在查看器回放(新增 `?replay=` 参数)
|
||||||
|
- [ ] 各集合与训练集无重叠
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 表示与预处理
|
||||||
|
|
||||||
|
文件 `ml/prep/build_dataset.cjs`(Node,特征直接调用部署运行时 `webapp/js/ik-policy.js` 的同一份实现)→ `ml/data/samples/*.{X,Y,V,SG,TID}.npy` + `*.tasks.jsonl`;按任务 5% 划为验证集。PyTorch 侧特征 `ml/common/features.py` 与 JS 逐项对拍(闭环仿真与 APG 使用)。
|
||||||
|
|
||||||
|
### 5.1 输入
|
||||||
|
|
||||||
|
| 量 | 维度 | 编码 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 当前 q_t | 6 | 每轴 (sin, cos) → 12;限位轴另加归一化值 → +3 |
|
||||||
|
| 当前关节速度 q_t − q_{t−1} | 6 | 按每轴速度上限归一化 |
|
||||||
|
| 当前 TCP 位置与姿态 | 3 + 6 | `fk(q_t)`、`orient(q_t)` 的 6D 表示 |
|
||||||
|
| 目标位置 T | 3 | /600 mm |
|
||||||
|
| 目标姿态 R | 6 | 6D 表示;pos 模式填 0 |
|
||||||
|
| 目标 − 当前 TCP | 3 | /600 mm |
|
||||||
|
| 模式 | 2 | one-hot [pos, pose] |
|
||||||
|
| 已提交未执行前缀 | 3×6 + 3 | 前缀 Δq + 有效长度掩码;训练时随机遮盖 0–3 步 |
|
||||||
|
| 当前子目标 | 6 | 子目标头输出的关节途经点(归一化);无子目标时填当前 q |
|
||||||
|
| 关键胶囊对最小距离 | 8 | 臂各段到立柱的胶囊距离 |
|
||||||
|
|
||||||
|
### 5.2 输出
|
||||||
|
|
||||||
|
- MeanFlow 一步头:每条候选 16 步 × 6 轴 Δq(按每轴最大步长归一化)。
|
||||||
|
- 附加头:价值头(剩余时间/代价)、子目标头(2 个关节途经点 + 是否需要)。
|
||||||
|
|
||||||
|
### 5.3 连续轴
|
||||||
|
|
||||||
|
J1/J3/J6 统一用 `unwrapTo` 展开后再做增量;预处理与部署共用一份实现,对拍时一并验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 模型与预训练
|
||||||
|
|
||||||
|
### 6.1 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
条件特征(约 90 维)──MLP──▶ 8 个条件 token (d=192)
|
||||||
|
│
|
||||||
|
4 层 Transformer encoder
|
||||||
|
│ (memory)
|
||||||
|
16 个动作 token(注入噪声 z + 时间嵌入 (r, t))
|
||||||
|
▼
|
||||||
|
4 层 Transformer decoder(非自回归,一次前向)
|
||||||
|
├─ 动作输出:每 token 6 维 Δq
|
||||||
|
├─ 价值头:1
|
||||||
|
└─ 子目标头:2 个关节途经点 + 是否需要
|
||||||
|
```
|
||||||
|
|
||||||
|
| 超参 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| d_model / 头数 / FFN | 192 / 8 / 768 |
|
||||||
|
| encoder / decoder 层数 | 4 / 4 |
|
||||||
|
| 参数量 | 约 4.9M |
|
||||||
|
|
||||||
|
子目标触发:新目标或目标跳变 > 50 mm 时输出;到达当前子目标 < 5° 后切下一个。
|
||||||
|
|
||||||
|
### 6.2 动作头:MeanFlow 一步生成
|
||||||
|
|
||||||
|
| 项 | 做法 |
|
||||||
|
|---|---|
|
||||||
|
| 多模态 | 连续分布;每次推理并行采 8 个噪声 → 8 条候选 |
|
||||||
|
| 训练 | MeanFlow 平均速度场损失(MP1 配方:25% 样本 r≠t,自适应损失加权),JVP 用 `torch.func.jvp` |
|
||||||
|
| 推理 | 1 次批量前向(r=0, t=1) |
|
||||||
|
| 后处理 | 1 步解析胶囊距离梯度投影,每轴修正 ≤ 0.5°,仅 d < 余量时(JS 实现,不走 ONNX) |
|
||||||
|
| APG 兼容 | 固定噪声后为确定性可微映射 |
|
||||||
|
| 预定回退 | M5 门禁不过(near_column 穿柱率 > 2% 或训练发散)→ 同骨干换 WTA K=8 头(WTA-L1 + 模式 CE),约 1 天 |
|
||||||
|
|
||||||
|
### 6.3 损失
|
||||||
|
|
||||||
|
```
|
||||||
|
L = MeanFlow 损失 1.0
|
||||||
|
+ FK 损失:一步输出(固定噪声)的各连杆胶囊端点位置差 0.5
|
||||||
|
+ 平滑:二阶差分 0.05
|
||||||
|
+ 价值头 L1 + 子目标头 L1 0.1 / 0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 训练配置
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 优化器 | AdamW,β=(0.9, 0.95),wd 0.01 |
|
||||||
|
| 学习率 | 1e-3,预热 1000 步,余弦退火 |
|
||||||
|
| batch | 1024 |
|
||||||
|
| 精度 | bf16 混合精度 |
|
||||||
|
| 步数 | 先跑 2000 步测吞吐再定;参考 200 万样本 × 30 epoch ÷ 1024 ≈ 6 万步;验证集闭环到达率不再上升即停 |
|
||||||
|
| 预计耗时 | RTX 3090 约 1.5–5.6 小时(按 2000 步吞吐实测重算) |
|
||||||
|
|
||||||
|
### 6.5 对拍与验证
|
||||||
|
|
||||||
|
- `ml/eval/collide_torch.py`:可微胶囊距离 PyTorch 批量版,与 JS `collideFree` 对拍 10 万位形:布尔判定 0 差异,距离误差 < 1e-4 mm,梯度与有限差分相对误差 < 1e-3。
|
||||||
|
- 每 epoch 闭环验证:从 1000 个验证任务出发,按部署节奏(每 60 ms 推理、提交 3 步)运行到到达或超时(老师路径时长 × 2,最少 3 s),统计到达率、用时 ≤ 老师 × 1.3 的达标率、碰撞步数(关安全层)、行程比、左/右/穿柱三类比例。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 后训练
|
||||||
|
|
||||||
|
### 7.1 主力:APG 可微微调
|
||||||
|
|
||||||
|
```
|
||||||
|
for 每个 batch(随机采样 q0 和目标,不需要老师):
|
||||||
|
q = q0
|
||||||
|
for t in 0..H-1 (H = 本批老师时长 p90 × 1.2,60–150 次推理 × 3 步 ≈ 3.6–9 s;无老师题按 6 s;每 10 次推理截断反传):
|
||||||
|
chunk = π(obs(q, 目标, 前缀), z) # 每条展开固定一个噪声 z
|
||||||
|
Δq = chunk[0:3] # 与部署一致
|
||||||
|
q = clamp(q + Δq)
|
||||||
|
累计 L_col(q), L_smooth(Δq)
|
||||||
|
L = w_tgt·Σ_段末 L_target(q) + w_col·ΣL_col + w_smooth·ΣL_smooth (+ w_len·行程) + w_bc·L_BC
|
||||||
|
反传,梯度裁剪 ≤ 1.0,lr 1e-4
|
||||||
|
```
|
||||||
|
|
||||||
|
- 碰撞项 L_col = Σ ReLU(margin − 胶囊距离),margin = 训练余量 3 mm(间隙已扣除 `margin_mm`);每步端点与中点,两处取均值(与只查端点同量纲);含臂×立柱、臂×台架、臂×臂(非相邻段)。
|
||||||
|
- 到达项:每个截断段末的位置误差² + λ_rot·‖R − R*‖_F²;不做到达即停,段末项在整条展开上持续给出接近梯度。
|
||||||
|
- 平滑项:速度² + 加速度²。
|
||||||
|
- BC 锚定项:保留预训练损失,防止多模态被压缩;每轮监控左右比例。
|
||||||
|
- 采样分布同 §4.3,B/C/D 类提到 60%,其中贴柱类 40%。
|
||||||
|
|
||||||
|
### 7.2 辅助:无老师挖错循环
|
||||||
|
|
||||||
|
每轮:
|
||||||
|
1. 出题:新种子 5 万任务 + 2000 段模型在环拖拽仿真。
|
||||||
|
2. 闭环作答:完全按部署方式运行,**关闭安全层**以暴露真实错误。
|
||||||
|
3. 验算:GPU 批量胶囊检查。
|
||||||
|
4. 错题分类:E1 撞 / E2 没到(卡在局部极小)/ E3 绕远(> 1.2× 参考)/ E4 末端精度差 / E5 摇摆(≥ 3 次切换)/ E6 拖拽失稳。
|
||||||
|
5. 错题及其 ±5 mm / ±3° 的 10 个邻居加入 APG 采样池(E1、E6 ×2);仅 E2 可离线请老师补少量路径。
|
||||||
|
6. 门禁(用门禁集,不用验收集):到达率上升、A 类每轮降幅 ≤ 0.5 个百分点、上一轮修好的错题 95% 仍对;否则回滚。
|
||||||
|
7. 停止:连续 2 轮提升 < 0.5 个百分点,或满 6 轮。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 部署
|
||||||
|
|
||||||
|
### 8.1 执行循环
|
||||||
|
|
||||||
|
```
|
||||||
|
执行:50 Hz,每步 20 ms,按已提交步走
|
||||||
|
每 60 ms:
|
||||||
|
1. 读当前 q、当前速度、已提交未执行前缀、最新目标
|
||||||
|
2. 推理:8 个噪声 → 8 条 16 步动作块(冷启动或卡死换向时,再各推一块做短前瞻)
|
||||||
|
3. 每条候选做 1 步胶囊距离梯度投影(每轴 ≤ 0.5°,仅 d < 余量时)
|
||||||
|
4. 排序(§8.2)
|
||||||
|
5. 安全层依次认证候选前 3 步 + 刹停段(§8.3),第一个通过的提交给执行器
|
||||||
|
6. 全部不通过 → 沿上一次已认证的刹停段停下,下次继续推理
|
||||||
|
7. 到达:TCP 误差 < 1 mm(pose 另需 < 0.5°)且速度≈0;
|
||||||
|
接近目标(< 5 mm)时做 1–3 次局部牛顿精修(solveTo 单步),精修段同样过安全层认证
|
||||||
|
```
|
||||||
|
|
||||||
|
执行的始终是安全层认证过的那一块,不做新旧块混合。执行时钟放在 Web Worker、按实际 dt 推进,避免 `requestAnimationFrame`(60 Hz)与 20 ms 不整除及后台节流造成的抖动。
|
||||||
|
|
||||||
|
### 8.2 候选排序(防左右摇摆)
|
||||||
|
|
||||||
|
分数 = −Σ w_k‖候选第 k 步 − 已提交块第 k 步‖²(重叠步加权)− λ·价值头剩余代价。冷启动时无已提交块,只用价值头 + 短前瞻打分。
|
||||||
|
|
||||||
|
### 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 次距离查询;墙钟截止下沉到认证内部:第 1 条候选截止 45 ms,其余候选共享到 30 ms,到点即判不过、沿已认证刹停段停。安全层耗时在几何查询而不在模型(沙箱 Node 每次 gapMatrix 3.6–5.5 ms、collideFree 1.5 ms,推理 1.5 ms)。
|
||||||
|
- 离散采样检查(现有 `pathFine`,1.5° + 半格)只作快速预筛,不作保证依据。
|
||||||
|
- **可刹停不变式**:只有当"候选前 3 步 + 从第 3 步末按每轴 a_max 减速到停"整段都通过认证时才提交。任何时候系统都持有一条已认证的停车路线,下一次推理即使完全失效也只会停下。
|
||||||
|
- 耗时:每段一次距离查询覆盖整段,不通过才二分(通常 2–6 次);按排序逐个认证,第一个通过即停;包围球预筛先行。目标每次 < 5 ms,否则放 Web Worker 与推理流水线并行。
|
||||||
|
- `ik-glue.js` 的每帧碰撞守卫保留为第二道保险。
|
||||||
|
|
||||||
|
### 8.4 部署包
|
||||||
|
|
||||||
|
| 有 | 没有 |
|
||||||
|
|---|---|
|
||||||
|
| `traj_policy.onnx`(fp32,约 20 MB;wasm / CPU 后端上 fp16 不保证更快)+ `traj_policy.meta.json`(指纹 / 特征版本 / TCP) | PRM 路网、IK 候选大表 |
|
||||||
|
| 胶囊距离与认证(`collideFree`、包围球预筛、`pair2d`) | `solveDyn` / `routeRT` / RRT / A* |
|
||||||
|
| `fk` / `orient`、局部牛顿精修 `solveTo` 单步 | 全量 IK 枚举 |
|
||||||
|
|
||||||
|
`collide.json` 指纹写进模型;不一致时前端拒绝运行并提示重训。
|
||||||
|
|
||||||
|
### 8.5 near_column 30/30 的保障手段
|
||||||
|
|
||||||
|
"超时"指机械臂在规定时间内没走到目标,是决策问题而非计算速度问题。典型失效:局部极小/活锁(每次都想直穿、每次被安全层挡住)、左右摇摆、绕远。对策(运行时均不需要老师):
|
||||||
|
|
||||||
|
1. **子目标头**:给出 1–2 个关节途经点作为绕柱方向锚点,标签取老师路径拐点(离线)。
|
||||||
|
2. **价值头**:预测剩余时间/代价,用于候选打分与进展判断;APG 中用 TD 自举监督。
|
||||||
|
3. **短前瞻**(仅冷启动和卡死换向时):对每条候选再推一块,用胶囊认证 + 价值头给 32 步后终态打分。
|
||||||
|
4. **卡死检测 + 强制换向**:0.3 s 内价值不降或连续 2 次刹停 → 切到另一绕行方向并锁定 1 s。
|
||||||
|
5. **训练侧加强**:贴柱类占 APG 采样 40%;展开覆盖整段绕行。
|
||||||
|
|
||||||
|
### 8.6 控制周期
|
||||||
|
|
||||||
|
伺服层(参考):Franka FCI 1 ms;UR e-Series 2 ms、CB3 8 ms;ABB EGM 4 ms(滞后 10–20 ms);KUKA RSI 4/12 ms;FANUC DPM 约 8 ms;MoveIt Servo 默认 10 ms;RC 舵机 20 ms。
|
||||||
|
|
||||||
|
策略层(参考):MπNets 9 Hz;Diffusion Policy 10 Hz、预测 16 执行 8;ELMP 30 Hz;π0 50 Hz、约每 0.5 s 推理一次;DRP 300 Hz。业界均为"策略层 10–50 Hz + 伺服层 125 Hz–1 kHz"两层结构。
|
||||||
|
|
||||||
|
本方案:执行 50 Hz、每 60 ms 推理一次、提交 3 步。真机上提交步数 n ≥ ⌈(通信滞后 + 一次推理最坏耗时) / 20 ms⌉ + 1,刹停段另行认证;策略输出 50 Hz 关节目标,由插补器升采样到电机控制器频率。`dt`、n、动作块长度均可配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 评估与验收
|
||||||
|
|
||||||
|
### 9.1 测试集合
|
||||||
|
|
||||||
|
| 集合 | 来源 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| route_bench | `tests/route_bench.cjs`(40 例)+ 60 例随机基准 | 验收 |
|
||||||
|
| near_column_bench | `tests/near_column_bench.cjs`,30 例(pos / pose / rt) | 验收 |
|
||||||
|
| near_column 扩展 | §4.3 新造 ≥1000 例 | 一次性验收 |
|
||||||
|
| rt_drag_bench | `tests/rt_drag_bench.cjs`,12 例(模型在环) | 验收 |
|
||||||
|
| held-out | §4.3 2000 条 | 一次性验收 |
|
||||||
|
| 门禁集 | §4.3 2000 条 | 训练中门禁,不参与验收 |
|
||||||
|
|
||||||
|
新增 `tests/policy_bench.cjs`:①现有规划器 ②策略(关安全层)③策略 + 安全层,输出对比表。
|
||||||
|
|
||||||
|
### 9.2 验收标准(部署形态 = 策略 + 安全层)
|
||||||
|
|
||||||
|
| 指标 | 要求 | 现有规划器 |
|
||||||
|
|---|---|---|
|
||||||
|
| 实际执行的碰撞 | **= 0**(距离认证或 0.01° 审计测量) | rt_drag_bench 0 |
|
||||||
|
| 到达率 | route 全部可达例到达;near pos **30/30**、near rt **30/30**;near_column 扩展集 **0 失败**;held-out 可达任务 ≥ 98%,D 类停靠距离 ≤ 老师 + 5 mm | `route_bench` 40 例:到达 40/40、细验有效 39/40;60 例随机基准有效 60/60;near 30/30、30/30 |
|
||||||
|
| 到达用时 | ≤ 老师路径时长 × 1.3 | — |
|
||||||
|
| 关节行程 | ≤ 现有规划器 × 1.10(按类别分别报告) | 60 例平均 467° |
|
||||||
|
| 冷启动 | < 10 ms | 首次全量 IK 最差约 2 s |
|
||||||
|
| 每次推理 + 安全认证 P99 | < 20 ms | 拖拽重规划 P99 约 321 ms |
|
||||||
|
| 刹停发生率 | < 2% 的推理周期 | — |
|
||||||
|
| 模式摇摆(≥3 次切换) | < 1% 的任务 | — |
|
||||||
|
| 策略单独(关安全层)碰撞率 | < 3% | — |
|
||||||
|
|
||||||
|
说明:30 例全对时,失败率的 95% 置信上界约 9.5%;扩展集 1000 例 0 失败时约 0.3%,因此以扩展集作为 30/30 的统计支撑。
|
||||||
|
|
||||||
|
口径:本表全部按部署形态(`tests/policy_bench.cjs`,与浏览器同一份运行时)或 held-out 闭环仿真(开仿真安全层)测量,由 `ml/eval/acceptance.py` 汇总;M5 门禁与挖错用的是关安全层的纯策略口径,二者互不替代。挖错题与门禁集没有老师参考,到达按定长 6 s 判,「到达用时」「关节行程」两项只在有老师参考的任务上统计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 当前存在的问题与风险
|
||||||
|
|
||||||
|
### 10.1 只能靠实测消除的风险(按里程碑顺序)
|
||||||
|
|
||||||
|
| # | 风险 | 门禁(不过不进下一步) | 回退 | 何时测 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R1 | 老师在相邻任务上左右绕行跳变,标签自相矛盾 | 起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90% | 对 A* 边代价加左右偏好项,强制同区域选同侧 | M1 |
|
||||||
|
| 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 |
|
||||||
|
| 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 |
|
||||||
|
| R6 | 位姿目标的多 IK 分支带来额外多峰 | 不同肘/腕分支子集到达率 ≥ 主集 − 2 个百分点 | 加目标分支标志 | M5 |
|
||||||
|
| 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 |
|
||||||
|
| R9 | 整周期超标(推理 + 前瞻 + 安全层 > 60 ms 周期,执行时钟饥饿丢步) | 训练好的模型在**真查看器**里跑与 bench 同一批 30 道贴柱题:稳态规划 P99 < 45 ms、运动中饥饿节拍 < 1%、认证超时占比 < 5%(`r4_probe --viewer` → `r9_browser.json`;冷启动 P99、刹停率、tickGapMax 只报);同题安全层浏览器 / 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 |
|
||||||
|
|
||||||
|
### 10.2 已知缺陷
|
||||||
|
|
||||||
|
1. 现有规划器(老师)在 `tests/route_bench.cjs` 上有 1/40 例到达但路径细验不过;§4.4 的认证过滤会丢弃这类样本。
|
||||||
|
2. `ci27` 姿态根枚举漏分支(姿态偏差约 97.5°)会影响 pose 数据;APG 姿态项可部分纠正,必要时先修老师。
|
||||||
|
3. 胶囊模型或立柱变动即需重训:数据(用户机器 16 核约 2 h)+ 预训练 1.5–6 h + 后训练数小时。
|
||||||
|
4. 老师一致性沙箱首测 78%(100 任务、373 对邻居,门禁 90%):不一致主要来自 J6 冗余与时间预算造成的非确定性。已改为确定性老师(预算按碰撞判定次数计、每条前清空跨调用缓存)并在 pos 任务冻结 J6,待在用户机器复测;仍不达标则生成时启用 4 邻居共识过滤。
|
||||||
|
5. 安全层耗时:未训练模型(候选为噪声)在 Node 单线程实测每次推理安全层约 20–60 ms,高于 5 ms 目标;训练后候选平滑会下降,以 `policy_bench` 与浏览器 `__POLICY.latency()` 实测为准,超标则把安全层移入 Web Worker 与推理流水线并行。
|
||||||
|
|
||||||
|
### 10.3 需要用户确认
|
||||||
|
|
||||||
|
1. **真机电机/驱动型号与通信方式**:决定每轴加速度上限 a_max、插补频率与提交步数。
|
||||||
|
2. **运行时多候选打分是否算"运行时老师"**:本方案的理解是不算(只对模型自己的候选做距离认证和打分,不做搜索)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 目录规划
|
||||||
|
|
||||||
|
运行步骤、命令与耗时见 `ml/README.md`。所有长步骤都能断点续跑,意外中断最多损失几分钟:生成按条续写(每条任务的随机数只由种子和序号决定),打包按 shard 跳过,预训练与 APG 每 10 分钟原子存一次 `last.pt`,停止时先存档再退出。
|
||||||
|
|
||||||
|
```
|
||||||
|
ml/
|
||||||
|
pipeline.sh 阶段脚本:setup / kin / gate1 / gen / prep / train / m5 / apg / mine / export / bench
|
||||||
|
gen/ sampler / teacher(确定性 PRM A*)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
|
||||||
|
prep/ export_kin.cjs → assets/kin.json;build_dataset.cjs → data/samples/*.npy;run_prep.sh
|
||||||
|
data/ (不入库)shard_*.jsonl stats.json consistency.json samples/
|
||||||
|
common/ kin_torch.py(可微运动学 + 胶囊间隙) features.py(83 维特征) ckpt.py(原子存档、定时存档、体面停止)
|
||||||
|
model/ policy.py(MeanFlow 头;WTA K=8 回退头) meanflow.py
|
||||||
|
data.py train.py 样本加载、预训练
|
||||||
|
sim/closed_loop.py 闭环仿真(验证、挖错、门禁共用)
|
||||||
|
apg/apg_finetune.py APG 可微微调
|
||||||
|
mining/ tasks.py mine_round.py gate.py loop.sh
|
||||||
|
eval/ dump_parity.cjs check_parity.py(JS ↔ PyTorch 对拍)
|
||||||
|
configs/ base.json wta.json small.json apg.json
|
||||||
|
export_onnx.py serve.py README.md
|
||||||
|
webapp/js/ik-policy.js 运行时:特征、距离下界认证、刹停、投影、精修、Runner(浏览器与 Node 共用)
|
||||||
|
webapp/js/ik-policy-glue.js 查看器接入(?policy=1)
|
||||||
|
webapp/models/ traj_policy.onnx traj_policy.meta.json
|
||||||
|
tests/policy_bench.cjs 部署形态评测
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. 里程碑
|
||||||
|
|
||||||
|
| # | 内容 | 产出 | 在哪跑 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| M1 | 数据生成器 + 时间律 + 1000 条试跑;老师一致性门禁 | `ml/gen/*`、stats.json | 沙箱 |
|
||||||
|
| M2 | 可微胶囊距离(PyTorch)与 JS 对拍;距离认证实现与 10⁵ 段审计 | `collide_torch.py`、认证模块 | 沙箱 |
|
||||||
|
| M3 | 5 万条老师数据 → 约 180 万样本 | `samples/*.npy` | 用户机器 |
|
||||||
|
| M4 | 模型 + 训练工程 + 闭环仿真;小数据过拟合;**导出 ONNX 测浏览器延迟** | `train.py`、`closed_loop.py`、延迟报告 | 沙箱 + 浏览器 |
|
||||||
|
| M5 | 预训练 + MeanFlow 头门禁 | checkpoint、门禁报告 | 用户 GPU |
|
||||||
|
| M6 | APG 微调 + 挖错循环 3–6 轮 | 每轮 report.md | 用户 GPU |
|
||||||
|
| M7 | `ik-policy.js` 闭环部署 + 安全层实测 + `policy_bench` + 一次性验收 | 对比表 | 沙箱 + 浏览器 |
|
||||||
|
| M8 | 移除查看器对 `solveDyn`/`routeRT`/路网的运行时依赖(保留在 `ml/gen` 离线使用) | 部署包瘦身 | 沙箱 |
|
||||||
|
|
||||||
|
## 13. 参考文献
|
||||||
|
|
||||||
|
- MπNets:https://arxiv.org/abs/2210.12209
|
||||||
|
- Neural MP:https://arxiv.org/abs/2409.05864
|
||||||
|
- Deep Reactive Policy / IMPACT:https://arxiv.org/abs/2509.06953
|
||||||
|
- ELMP:https://arxiv.org/abs/2607.00215
|
||||||
|
- Diffusion Policy:https://arxiv.org/abs/2303.04137
|
||||||
|
- π0:https://arxiv.org/abs/2410.24164 ;π0.5:https://arxiv.org/abs/2504.16054
|
||||||
|
- FAST:https://arxiv.org/abs/2501.09747
|
||||||
|
- MP1 / MeanFlow:https://arxiv.org/abs/2507.10543
|
||||||
|
- One-Step Flow Policy:https://arxiv.org/abs/2603.12480
|
||||||
|
- 条件 OT 流策略:https://proceedings.mlr.press/v305/sochopoulos25a.html
|
||||||
|
- Kinematic MeanFlow:https://arxiv.org/abs/2610.00864
|
||||||
|
- Consistency Policy:https://arxiv.org/abs/2405.07503 ;OneDP:https://arxiv.org/abs/2410.21257
|
||||||
|
- MPD:https://arxiv.org/abs/2308.01557 ;EDMP:https://arxiv.org/abs/2309.11414 ;GPD:https://arxiv.org/abs/2501.18229 ;DiffusionSeeder:https://arxiv.org/abs/2410.16727
|
||||||
|
- Real-Time Chunking:https://arxiv.org/abs/2506.07339 ;训练时动作条件化:https://arxiv.org/abs/2512.05964
|
||||||
|
- Bidirectional Decoding(BID):https://arxiv.org/abs/2408.17355
|
||||||
|
- DPPO:https://arxiv.org/abs/2409.00588 ;ReinFlow:https://arxiv.org/abs/2505.22094
|
||||||
|
- Schwarzer, Saha, Latombe, *Adaptive Dynamic Collision Checking for Single and Multiple Articulated Robots in Complex Environments*, IEEE T-RO 2005
|
||||||
|
- Thumm & Althoff, *Provably Safe Deep Reinforcement Learning for Robotic Manipulation in Human Environments*, ICRA 2022
|
||||||
|
- Zhou et al., *On the Continuity of Rotation Representations in Neural Networks*, CVPR 2019
|
||||||
|
- AWE(航点提取):https://arxiv.org/abs/2307.14326 ;SHAC:https://arxiv.org/abs/2204.07137
|
||||||
|
- 控制周期:Franka FCI https://frankarobotics.github.io/docs/overview.html ;UR RTDE https://www.universal-robots.com/articles/ur/interface-communication/remote-control-via-tcpip ;ABB EGM 应用手册 3HAC073318;KUKA RSI https://docs.quanser.com/quarc/documentation/kuka_rsi_block.html
|
||||||
+4
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,102 @@
|
|||||||
|
# 避障路径规划优化报告(2026-10-15,基于 1dfdd48)
|
||||||
|
|
||||||
|
## 现状问题(旧 routeOrVia 阶梯)
|
||||||
|
1. **穿模**:`routeAroundPlan` 每段只查 ≤8 点(长段采样间隔 15°+),`coordinated` 二分松弛只查 6 点。
|
||||||
|
60 个"直线被挡"随机场景里 18 条路径在 1° 细验下实际穿模(13 条为 coordinated)。
|
||||||
|
2. **怪动作**:`CRUISE` 巡航位形把 J3/J5/J6 强制拉到固定值(0/-60/0),与障碍无关的腕部和大臂自转也被
|
||||||
|
"归零再转回";回退绕行(retract_pass)先收臂再伸展,路径绕远。
|
||||||
|
3. **单目标**:绕行只认一个目标解 qT,换个肘/腕构型就能直走的情况被忽略。
|
||||||
|
4. **动画顿挫**:`animTo` 每个中间位形各占固定 ms 并各自 smoothstep —— 每过一个 via 都减速到 0;
|
||||||
|
5° 小段与 150° 大段用同样时间。
|
||||||
|
|
||||||
|
## 改动
|
||||||
|
- `ik-solver.js` 新增 6c 节:
|
||||||
|
- `gatherGoals`:候选表里端点安全的 IK 解按加权关节距离取前 6 个(同盆地去重)作为目标集;
|
||||||
|
- 先试多目标直线(1.5° 细验)→ 再试弦中点 J2/J4 微调单中间点 → 再上 **Lazy RRT-Connect**
|
||||||
|
(起点树 ↔ 多根目标树;生长时只查节点 + 边中点,连通后整条路径细验,坏边判死子树);
|
||||||
|
- `shortcut`:随机捷径 + 贪心最远可达 + 逐轴拉直(去掉与障碍无关轴的来回摆);
|
||||||
|
- 加权度量 `PW=[1.6,1.4,1,1,0.7,0.5]`(近端轴代价高),定种子 LCG,结果可复现;
|
||||||
|
- 连续轴段内变化 <179°,保证与查看器短弧插值走的是同一条线;
|
||||||
|
- 规划失败时仍回落旧阶梯与 routeVia,最坏行为不劣于旧版。
|
||||||
|
- 碰撞窄相位前加**胶囊级包围球预筛**(保守、零漏判):`collideFree` 2.9 → 1.8 ms/次,
|
||||||
|
4000 个随机位形与旧实现判定 0 差异。
|
||||||
|
- `ik-glue.js animTo`:各段时长按"最慢轴所需时间"(轴速比 VREL)分配,整条折线一次全局缓入缓出,
|
||||||
|
中间位形不停顿;总时长按路径长度伸缩。
|
||||||
|
|
||||||
|
## 基准(`node tests/route_bench.cjs [solver] 60`,同一批 60 个直线被挡场景)
|
||||||
|
| 指标 | 旧 | 新 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1° 细验无穿模的路径 | 42/60 | 58/60(剩 2 条最大侵入 0.28/0.08 mm,在 0.3 mm 安全裕度内) |
|
||||||
|
| 到达且无穿模 | 37 | 53 |
|
||||||
|
| 两版都成功的 37 例:平均关节总行程 | 417° | 388°(−7%) |
|
||||||
|
| 同上:估算执行时间 | 2.56 s | 2.46 s |
|
||||||
|
| 平均规划耗时(Node 单线程) | 0.72 s | 1.23 s(全集)/ 0.94 s(37 例) |
|
||||||
|
|
||||||
|
到不了的 5 例两版相同,属于目标超出安全可达区(IK 残差 4~235 mm),不是规划问题。
|
||||||
|
|
||||||
|
## 已知取舍 / 后续
|
||||||
|
- 规划耗时上升约 0.3~0.5 s(Worker 里跑,不卡界面);拖拽实时模式预算 150 ms,超时回落旧阶梯。
|
||||||
|
- 执行层仍是关节空间折线;若要上真机,下一步应做带加速度约束的时间最优参数化(TOPP)+ 拐角抛物线过渡,
|
||||||
|
过渡段需重新做碰撞校验。
|
||||||
|
|
||||||
|
## 二期:规划提速(2026-10-15)
|
||||||
|
|
||||||
|
逐段计时(20 个被挡场景):时间主要花在"单中间点微调"(~350 ms/例)、RRT(~280 ms)和平滑(~250 ms 截止);
|
||||||
|
每次碰撞判定约 1.5 ms,其中 80% 的线段距离计算来自 (2,4) 类零件对(P05/M3 × P07/M5,长期相距 ~5 mm)。
|
||||||
|
|
||||||
|
改动:
|
||||||
|
1. **两关节零件对类间隙表**:(2,4) 类的相对位姿只取决于 J3、J4,离线按 1° 网格生成最小间隙表
|
||||||
|
(`webapp/tools/gen_pair2d.cjs` → `docs/collide.json` 的 `pair2d`)。运行时取最近网格点,按杠杆界
|
||||||
|
(J3 3.24 mm/°、J4 2.54 mm/°)给出上下界,能定论就跳过/判撞,否则回退精算。2 万随机姿态与精算判定 0 差异;
|
||||||
|
单次判定 1.60 → 1.25 ms。指纹校验:重新拟合胶囊后未重生成表会自动弃表(只变慢不出错)。
|
||||||
|
2. **前移启发**:撞上的零件对/簇对移到表头,规划中反复撞同一处时首查即短路。
|
||||||
|
3. **单次求解内判定记忆**:routeFrom / gatherGoals / 规划器对同一批 IK 候选不再重复判定。
|
||||||
|
4. **routeFrom 惰性回退**:被挡时先不算"贴边停"位形(findSafeTarget + 二分),规划失败才补算。
|
||||||
|
5. 微调只试前 2 个目标;平滑 50 次随机捷径 / 160 ms 截止,逐轴拉直 1 轮。
|
||||||
|
|
||||||
|
同一批 60 例(`node tests/route_bench.cjs <solver> 60`):
|
||||||
|
|
||||||
|
| 指标 | 一期 | 二期 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 平均规划耗时 | 1239 ms | 837 ms(−32%) |
|
||||||
|
| 到达目标 | 55 | 58 |
|
||||||
|
| 1° 细验不穿模 | 58 | 58 |
|
||||||
|
| 关节总行程均值 | 438° | 436° |
|
||||||
|
| 往返次数均值 | 2.08 | 1.98 |
|
||||||
|
|
||||||
|
尝试过但未采用:基于间隙 + 杠杆界的保守连续检测。(2,4) 类常年仅 ~5 mm 间隙把步长压到 <1°,
|
||||||
|
单次间隙查询又比布尔判定贵,实测比 1.5° 离散检测慢 25%。
|
||||||
|
|
||||||
|
## 第三轮:实时版(2026-10-16)
|
||||||
|
|
||||||
|
### 思路
|
||||||
|
立柱和自身零件都是**静态障碍**,关节空间的自由区永远不变,所以把"安全路网"离线算好、随碰撞模型发布;运行时只做"接入 + 查图"。
|
||||||
|
|
||||||
|
1. **离线路网(PRM)**:`webapp/tools/gen_roadmap.cjs` 均匀采样 2500 个无干涉位形,连 10 近邻,每条边按 1.5° 细验(双线程约 9 分钟)。结果:15103 条有效边、**单一连通分量**。写入 `docs/collide.json` 的 `roadmap` 字段(约 130 KB),按碰撞模型 + 限位指纹校验,不符自动弃用、退回在线 RRT。
|
||||||
|
- 重新拟合胶囊或改限位后,按顺序重跑:`node webapp/tools/gen_pair2d.cjs`,再跑 `node webapp/tools/gen_roadmap.cjs`(pair2d 生成会截掉其后的 roadmap 字段)。
|
||||||
|
2. **在线 Lazy A\***:起点和各 IK 目标接到 12 近邻,只细验路径上的"接入边",失败删边重搜;路网内部边零成本。
|
||||||
|
3. **会话内终身增广**:每条细验通过的接入边连同起点/目标并入内存路网;**超时也保留已验进度**,下一帧接着用(跨帧随时可停)。
|
||||||
|
4. **拖拽热启动**:重规划起点就在上一条已验路径上,"起点 → 段终点"免检;新目标只需验 1~2 条短边。
|
||||||
|
5. **实时管线 `routeRT`**(拖拽 quick 模式自动启用):直连 → 热启动 → 路网 A\*(40 ms)→ 沿旧路径继续走 → 都不行就停在直线上最后一个安全采样点。**不再进入旧的启发式阶梯**(仿真里它的段只抽查少量点,会穿模);松手口径的旧阶梯 / 抬高 via 结果执行前也补了整条 1.5° 细验。
|
||||||
|
6. 热跟随解撞了时补一次 `solveDyn` 全量解(同一目标缓存;拖动中 40 mm 内复用附近目标的安全解,目标停下再精解)。
|
||||||
|
7. 碰撞判定:立柱组(组 0)的胶囊/簇/零件中心变换只算一次永久缓存(4000 个随机位形与上一版判定差异 0)。
|
||||||
|
8. 前端 `ik-glue.js`:动画每帧对显示位形做一次碰撞判定(实时碰撞守护,约 1.2 ms/帧,命中即停并亮红标,`window.__RT_GUARD()` 看统计);被新规划打断时从运动中接续(只缓出),拖拽跟随不再一顿一顿。
|
||||||
|
|
||||||
|
### 实时拖拽仿真(`tests/rt_drag_bench.cjs`,12 个横穿立柱场景 × 480 帧 @60 fps)
|
||||||
|
|
||||||
|
| 指标 | 原版 1dfdd48 | 上一版 919e704 | 实时版 |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 重规划耗时 P50 | 14.9 ms | 2.6 ms | **5.3 ms** |
|
||||||
|
| 重规划耗时 P90 | 43.7 ms | 14.5 ms | **18.2 ms** |
|
||||||
|
| 重规划耗时 P99 | 464 ms | 171 ms | **169 ms** |
|
||||||
|
| 穿模帧(1° 细验) | 15 | 430 | **0** |
|
||||||
|
| 拖完 6 s 内到达 | 9/12 | 10/12 | 10/12 |
|
||||||
|
|
||||||
|
上一版 P50 更低是因为它在拖拽中几乎不绕行(直接停/直走),代价是 430 帧穿模。
|
||||||
|
|
||||||
|
### 松手全量规划(`tests/route_bench.cjs`,60 场景)
|
||||||
|
有效路径 **60/60**(上一版 58),到达 58,平均关节行程 458°(上一版 436°),往返 1.86 次,执行 2.60 s。平均规划耗时 662 ms(与拖拽仿真并行跑测得,偏高)。
|
||||||
|
|
||||||
|
### 剩余问题
|
||||||
|
- 尾部尖峰(最大约 2 s)来自目标刚进入"热跟随解会撞"区域时的首次全量 IK,以及偶发长接入边细验;下一步可把全量 IK 改为分帧增量。
|
||||||
|
- 未做真实浏览器交互与实体电机测试。
|
||||||
+311
@@ -0,0 +1,311 @@
|
|||||||
|
# Transformer 多峰策略端到端训练与浏览器部署
|
||||||
|
|
||||||
|
机械臂避障策略运行手册。目标:在你的机器上从零跑出 Transformer 避障策略,并在查看器里直接用上。
|
||||||
|
方案依据见 `docs/Transformer轨迹拟合训练方案.md`。所有命令在**仓库根目录**执行。
|
||||||
|
|
||||||
|
**一键运行(无人值守)**:先激活训练用的 Python 环境,Windows 双击 `ml/console.bat`(Linux/WSL 运行 `bash ml/console.sh`),浏览器自动打开训练控制台 `http://localhost:8100/`,点「开始训练」即从对拍一路跑到验收。等价的终端命令是 `bash ml/pipeline.sh all`。
|
||||||
|
- **断了再点一次就是续跑**(断电、蓝屏、误关、手动停止都一样),损失不超过几分钟:
|
||||||
|
- 已成功的阶段自动跳过;
|
||||||
|
- 生成:每条任务的随机数只由(种子, 序号)决定,没写完的 `shard_XXXX.jsonl.part` 保留完整的行、从下一条接着写;
|
||||||
|
- 打包:已打包完的 shard 跳过;
|
||||||
|
- 预训练 / WTA / APG:每 10 分钟原子存一次 `last.pt`(含优化器、步数、最好成绩、早停计数;配置项 `ckpt_min`),续跑时自动 `--resume`;
|
||||||
|
- M5:已评完的那一半(比检查点新的结果文件)跳过;挖错:已过门禁的轮次跳过,断在某轮的 APG 用该轮 `last.pt` 续。
|
||||||
|
- 不续的只有几分钟级的短步骤(对拍、一致性门禁、R4、导出、bench、验收),它们整步重跑。
|
||||||
|
- **同一时间只能跑一条**:网页、终端、`all` 共用一把锁 `ml/runs/pipeline.lock`,第二个入口会被直接拒绝。
|
||||||
|
- **关窗口不影响训练**:网页和控制台窗口都可以关,重新打开即可接着看、可停止。
|
||||||
|
- **门槛在流程内自动处理**:R4 推理延迟自动测(不过就在训练前停下);R2 只警告;M5 不过自动改训 WTA 头,两种头都不过才停;每个阶段有超时上限(估时 ×3),卡死会判失败并放锁。
|
||||||
|
也可以按阶段单独执行 `bash ml/pipeline.sh <阶段>`;每一阶段完成后看本手册对应小节的「检查」。
|
||||||
|
|
||||||
|
| 阶段 | 命令 | 预计耗时(28 核 CPU + RTX 3090) | 产出 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 环境 | `bash ml/pipeline.sh setup` | 10 分钟 | 依赖 |
|
||||||
|
| 2 对拍 | `bash ml/pipeline.sh kin` | 约 1 分钟(估算:300 条对拍实测 7.6 s,流程跑 2000 条) | `ml/assets/kin.json`、对拍 PASS |
|
||||||
|
| 2′ R4 延迟 | `bash ml/pipeline.sh r4`(无头 Edge/Chrome 自动测) | 约 5 分钟 | `ml/runs/r4.json` |
|
||||||
|
| 3 老师门禁 | `bash ml/pipeline.sh gate1` | 25–30 分钟(单核) | `ml/data/consistency.json` |
|
||||||
|
| 4 生成数据 | `bash ml/pipeline.sh gen` | 不共识约 1 小时;共识约 4–5 小时 | `ml/data/shard_*.jsonl`、`stats.json` |
|
||||||
|
| 5 打包样本 | `bash ml/pipeline.sh prep` | 15–30 分钟 | `ml/data/samples/`、`ml/data/r2.json` |
|
||||||
|
| 6 预训练 | `bash ml/pipeline.sh train` | 测速 2000 步约 6 分钟,正式约 3 小时 | `ml/runs/base/best.pt` |
|
||||||
|
| 7 M5 门禁 | `bash ml/pipeline.sh m5` | 估算 10–20 分钟 | `ml/runs/m5_gate_base.json` |
|
||||||
|
| 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` |
|
||||||
|
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) |
|
||||||
|
| 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` |
|
||||||
|
| 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
|
||||||
|
|
||||||
|
第 4/5/6 步耗时为 RTX 3090 + 28 核实测换算;标「估算」「未实测」的不要当预算,以每步首次输出的速度为准。
|
||||||
|
第 1 步(安装依赖)建议先手动做;一键流程默认从第 2 步开始,含 2′。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 分歧规则:审查方与实现方意见讨论后仍不一致时,以实现方(Hark)的意见为准先落地;下一轮用实测数据说话(见 docs/ML运行手册审查.md 第二节)。
|
||||||
|
|
||||||
|
## 1. 环境
|
||||||
|
|
||||||
|
- Linux / WSL2,或 **Windows 原生 + Git Bash**(实测第 2/4/5/6 步与闭环仿真可跑);NVIDIA 驱动 + CUDA(`nvidia-smi` 能看到 3090)
|
||||||
|
- Node ≥ 18,Python 3.10–3.12
|
||||||
|
- 脚本统一用 `$PYTHON`(默认 `python`)。Linux 上没有 `python` 命令时:`export PYTHON=python3`。Windows 不要用 `python3`(那是 WindowsApps 假壳)
|
||||||
|
- 先按 CUDA 版本装 PyTorch(https://pytorch.org ),再:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
bash ml/pipeline.sh setup # pip install -r ml/requirements.txt;cd ml && npm install(onnxruntime-node)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 碰撞模型导出与对拍(必须 PASS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh kin
|
||||||
|
```
|
||||||
|
|
||||||
|
检查:最后打印的 JSON 里 `"PASS": true`(fk、姿态、组对间隙、83 维特征、间隙梯度与 JS 一致)。
|
||||||
|
`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 延迟(训练前做,自动)**:推理延迟只取决于模型结构,与权重无关,所以用随机初始化模型先测,不过就在训练前改结构,不必等训完。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh r4 # 随机初始化模型导出到 ml/runs/r4/traj_policy_r4.onnx(不碰 webapp/models/),无头浏览器自动测 → ml/runs/r4.json
|
||||||
|
```
|
||||||
|
|
||||||
|
部署形态:模型每次只编码 1 行观测、解码 K=8 条候选(`meta.enc_once`);wasm 线程默认 `min(16, ⌊0.6×逻辑核⌋)`,推理放在 onnxruntime 的代理 Worker 里不占主线程。
|
||||||
|
探针测 4 种配置:默认线程 + Worker(部署口径)、4 线程(对照旧默认)、默认线程不开 Worker(看 `tickGapMax`:推理是否卡住 20 ms 节拍)、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`。
|
||||||
|
找不到浏览器时 `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` 才放行,徽标标红「随机探针模型」)。
|
||||||
|
|
||||||
|
## 3. 老师一致性门禁
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh gate1
|
||||||
|
```
|
||||||
|
|
||||||
|
看 `consistentByGateRule`:≥ 0.90 直接生成;< 0.90 时第 4 步自动加 `--consensus 4`(每条任务另跑 4 个 ±5 mm 邻居,与多数绕法不一致的样本丢弃,生成成本约 5 倍)。可用 `CONSENSUS=0/1` 手动覆盖。
|
||||||
|
|
||||||
|
## 4. 生成老师数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh gen # 默认 5 万条、进程数 = CPU 核数
|
||||||
|
# 自定义:N_TASKS=50000 NPROC=28 CONSENSUS=1 TARGET_SAMPLES=1700000 bash ml/pipeline.sh gen
|
||||||
|
```
|
||||||
|
|
||||||
|
不共识约 1.6 秒/条/核;共识约 8 秒/条/核。生成完自动统计;**样本数不足 `TARGET_SAMPLES`(默认 170 万)时按实测「样本/任务」自动补生成**(最多补 2 次),不用手动加 `N_TASKS`。
|
||||||
|
|
||||||
|
检查 `ml/data/stats.json`:
|
||||||
|
|
||||||
|
| 字段 | 不共识(实测) | 共识(估算) | 不正常时 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `keepRate` | ≈ 0.97 | 0.80–0.88 | 共识 < 0.75:把 `stats.json` 发我(老师左右跳变比预期多) |
|
||||||
|
| `samplesPerTask` | ≈ 36.5 | ≈ 36.5 | — |
|
||||||
|
| `samplesEvery3` | ≥ 170 万(自动补量后) | ≥ 170 万 | 补量后仍不足:发我 |
|
||||||
|
| `consensusDrop` | 0 | 约任务数的 10–20% | — |
|
||||||
|
| `vertexMinGapMm` | p50 ≈ 4.4、p90 ≈ 5.9 mm | 同 | — |
|
||||||
|
| `gapDrop` | 认证最小间隙 < 3 mm 被剔除的轨迹数(未实测,预计约 10%) | 同 | > 25%:发我(自动补量会按剔除后的样本数补,但老师整体太贴柱) |
|
||||||
|
| `byCatMode` | C 类(贴立柱)各模式都有数千条 | 同 | — |
|
||||||
|
|
||||||
|
## 5. 打包训练样本 + R2 检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh prep
|
||||||
|
```
|
||||||
|
|
||||||
|
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算),30% 样本加 σ = 1° 噪声增广(加噪后间隙 < 3 mm 的不用)。
|
||||||
|
认证最小间隙 < 3 mm 的老师轨迹不进训练集(`MIN_GAP_MM=3`,与 `stats.json` 的 `gapDrop` 同一口径):模型只学离障碍 ≥ 3 mm 的走法,运行时投影护栏仍保持 5 mm。
|
||||||
|
检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万(5% 任务划为 val)。
|
||||||
|
`MIN_GAP_MM` 必须与生成阶段相同(`stats.json` 的 `minGapFilterMm`),不一致时这一步直接报错(返回码 8)。
|
||||||
|
随后自动跑 R2 检查(`ml/data/r2.json`):判据是 `label_std_norm < 0.10`(近邻距离 → 0 的回归截距,与搜索池大小无关);`label_std_mean` 是旧口径(取最近 5% 近邻,随池大小变),只作参考。
|
||||||
|
不通过时默认**只警告、继续训练**(阈值尚未经实测校准),验收汇总列为参考项;`R2_STRICT=1` 才在这里停(返回码 4)。小样本时近邻对不足,`pass` 为 null。
|
||||||
|
|
||||||
|
## 6. 预训练
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/bench_base/、ml/runs/base/
|
||||||
|
```
|
||||||
|
|
||||||
|
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练到 60000 步或闭环到达率连续 5 次不升为止。
|
||||||
|
|
||||||
|
- **通过线(R10):`samples_per_s` ≥ 5000,即 60000 步 ≤ 3.5 小时**。参考:3090、batch 1024 实测 6.2 k 样本/s ≈ 2.8 小时。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48,仍不够再把 `steps` 降到 40000
|
||||||
|
- 训练中每 2000 步打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`time_ok_1p3`、`start_fail`。时限 = 老师时长 × 2(至少 3 s)
|
||||||
|
- 显存预算按 **19–20 GB** 算(Windows + 4K 桌面常驻占 4–5 GB),训练与 APG 进程启动时自设硬帽 `VRAM_FRAC=0.78`(≈18.7 GiB),日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。
|
||||||
|
|
||||||
|
## 7. M5 门禁:选定动作头
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh m5
|
||||||
|
```
|
||||||
|
|
||||||
|
1000 个贴立柱任务(无老师题,定长 6 s 时限)跑两遍,结果在 `ml/runs/m5_gate_base.json`:
|
||||||
|
|
||||||
|
- `policy_only`:关安全层的纯策略指标。**门禁只看这一项:穿柱率 ≤ 2% 通过**,通过后把该模型记为选定模型(`ml/runs/selected_ckpt`),后续阶段自动从它开始
|
||||||
|
- `with_sim_safety`:仿真里的近似安全层(候选逐步干涉就换下一条)。只作参考
|
||||||
|
|
||||||
|
三种口径的关系:M5 是纯策略能力;仿真安全层是近似;**部署能不能过只看第 11 步**(与浏览器同一份运行时,含投影、距离认证、刹停、预算、换向锁)。三者互不替代。
|
||||||
|
|
||||||
|
不通过时**流程内自动**改训 WTA K=8 头(约 3 小时,产物在 `ml/runs/bench_wta/`、`ml/runs/wta/`,不覆盖 MeanFlow),再判一次 → `ml/runs/m5_gate_wta.json`;两种头都不过才停(返回码 3)。`AUTO_WTA=0` 关闭自动分支。
|
||||||
|
单独跑 WTA:`CFG=ml/configs/wta.json bash ml/pipeline.sh train` 再 `CFG=ml/configs/wta.json bash ml/pipeline.sh m5`。
|
||||||
|
|
||||||
|
需要强制指定后续阶段的起点模型时用 `CKPT=路径 bash ml/pipeline.sh apg`。
|
||||||
|
|
||||||
|
## 8. APG 可微微调
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh apg
|
||||||
|
```
|
||||||
|
|
||||||
|
按部署节奏(每次推理提交 3 步、50 Hz)可微展开策略,优化到达、碰撞余量、平滑与行程,并保留 BC 锚定。每 500 步闭环验证,按到达率存 `best.pt`,完成后复制为 `ml/runs/current.pt`。
|
||||||
|
|
||||||
|
- **展开长度**:每批覆盖该批老师时长 p90 × 1.2(无老师题按 6 s),在 60–150 次推理(3.6–9 s)之间;日志 `plans` 字段是本批实际次数
|
||||||
|
- **到达项**:每 10 次推理段末的 TCP 距离与姿态误差,没有「到达即停」。这是有意的:段末项在整条展开上持续给出接近目标的梯度
|
||||||
|
- **碰撞项**:每步端点与中点的逐胶囊对铰链 `ReLU(3 mm − 间隙)`,两处取均值(与只查端点同量纲,`w_col` 不变)。3 mm 与训练数据的过滤线一致,碰撞项不与 BC 标签对抗;运行时投影护栏是独立的 5 mm
|
||||||
|
- `configs/apg.json` 定为 `batch=48`、`steps=8000`(本机实测 `batch=48/plans=150` 峰值 13.08 GiB;`batch=128` 必 OOM;总任务量与原 128×3000 相同)。仍超预算时把 `seg_plans` 从 10 降到 5,不降 `plans_max`(会截掉长任务的绕出段)
|
||||||
|
|
||||||
|
## 9. 挖错循环
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh mine # 默认 6 轮;ROUNDS=3 APG_STEPS=1000 MINE_N=50000 可调
|
||||||
|
```
|
||||||
|
|
||||||
|
每轮:5 万道新题关安全层作答 → 错题分类 E1–E5 → **本轮**错题与邻居进 APG 采样池(不累积历史轮,旧错题由门禁的「上轮修好仍对 ≥ 95%」把关)→ E2(局部极小)请老师补少量路径 → APG 微调 → 门禁集比较,不达标自动回滚。连续 2 轮提升 < 0.5 个百分点自动停止。
|
||||||
|
|
||||||
|
门禁(`ml/runs/mine/r*/gate.json`):到达率不降、A 类降幅 ≤ 0.5 个百分点、上轮修好的题 ≥ 95% 仍对、**R7**:B/C 类左右绕行比例变化 ≤ 5 个百分点、行程中位数比 ≤ 1.05。挖错题与门禁集都是无老师题,到达按定长 6 s 判,`time_ok_1p3` 与行程比只在有老师参考的题上统计(门禁集上为 null)。
|
||||||
|
|
||||||
|
## 10. 导出
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh export # fp32 ONNX + meta.json → webapp/models/(约 20 MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
默认 fp32:部署走 wasm 与 Node CPU,fp16 在 CPU 后端上不保证更快。只有第 13 步用 `&ep=webgpu` 实测 `__POLICY.latency()` 明显受益时,才另导出 fp16 做对比:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install onnxconverter-common
|
||||||
|
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy_fp16.onnx --fp16
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 部署形态评测(策略 + 安全层)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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` 单独调。
|
||||||
|
|
||||||
|
看 `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。
|
||||||
|
同一步还会在查看器里跑**同一批** 30 道贴柱题(与 Node 同种子),写 `ml/runs/r9_browser.json`(会弹出浏览器窗口几分钟):
|
||||||
|
- **R9 通过线:稳态规划 `planWarmP99` < 45 ms、运动中饥饿节拍 `starve_rate` < 1%、认证超时占比 `cert_time_rate` < 5%**(整次规划 = 推理 + 前瞻 + 安全层,60 ms 周期留 15 ms 给渲染与执行时钟;稳态 = 剔除每题静止起步那次冷启动规划,冷启动只是起步延迟,`planColdP99` 只报;饥饿 = 运动中执行时钟到点队列空了,即实际丢步;认证超时一闸防止靠拒绝候选、刹停换来耗时达标)。`brake_rate`、`tickGapMax` 只报不判。
|
||||||
|
- 两边对照:`policy_bench.json` 的 `near_column_pos.safetyMsP99` 与浏览器 `safetyP99` 差 > 3× 时验收标红,用 `certQueriesP99`(每次规划新增距离查询)、`certMemoHitRate`、`certFailWhy`(`cert` / `budget` / `time` / `box`)定位。
|
||||||
|
|
||||||
|
## 12. 验收汇总
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ml/pipeline.sh accept
|
||||||
|
```
|
||||||
|
|
||||||
|
自己补跑 val 全部任务的闭环(开仿真安全层):可达任务到达率 ≥ 98%、D 类停靠距离 ≤ 老师 + 5 mm、起步失败率 ≤ 0.2%;再读第 5–11 步产物核对 R2、R7、R8、R10、M5 与部署形态各项。逐项打印「通过 / 未过」,结果在 `ml/runs/acceptance.json`。
|
||||||
|
|
||||||
|
## 13. 在查看器里使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m arm.viewer.build_viewer # 若 webapp/generated/00_data.js 还没生成(查看器原有构建步骤)
|
||||||
|
python ml/serve.py # 带跨源隔离头的本地服务,wasm 可用多线程
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器打开 `http://localhost:8000/?policy=1`:右上角徽标显示「策略模式就绪」后,拖动黄球或松手,手臂由策略闭环执行(不调用任何规划器)。
|
||||||
|
- `&ep=webgpu` 改用 WebGPU 后端对比延迟
|
||||||
|
- 控制台 `__POLICY.latency()` 查看推理、前瞻、安全层、整次规划的 P50/P99 与节拍;`__POLICY.stats()` 查看刹停、换向、精修次数
|
||||||
|
- `&autotest=30` 自动跑与 bench 同一批贴柱题,结果在 `__POLICY.autotest`
|
||||||
|
- 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守)
|
||||||
|
- 工具偏移 ≠ 0 时自动交回原规划器
|
||||||
|
- 离线环境:`bash ml/prep/vendor_ort.sh` 把 onnxruntime-web 放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`(WebGPU 同样可用)
|
||||||
|
|
||||||
|
## 14. 回传给我
|
||||||
|
|
||||||
|
| 步骤 | 文件 |
|
||||||
|
|---|---|
|
||||||
|
| 2 | 对拍输出(终端 JSON)、`ml/runs/r4.json` |
|
||||||
|
| 3–5 | `ml/data/consistency.json`、`ml/data/stats.json`、`ml/data/samples/index.json`、`ml/data/r2.json` |
|
||||||
|
| 6 | `ml/runs/bench_base/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
|
||||||
|
| 7 | `ml/runs/m5_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/r9_browser.json`、`ml/runs/acceptance.json` |
|
||||||
|
| 任一步出错 | `ml/runs/pipeline.json`、`ml/runs/console.log`(控制台启动时)或终端输出 |
|
||||||
|
|
||||||
|
## 训练控制台
|
||||||
|
|
||||||
|
```text
|
||||||
|
Windows:双击 ml/console.bat Linux / WSL:bash ml/console.sh
|
||||||
|
(等价于 python -m ml.monitor --open。先激活训练用的 Python 环境;或设环境变量 PYTHON=训练环境 python 的完整路径。
|
||||||
|
启动器以「能 import torch」判断 Python 可用;Windows 另需 Git for Windows 提供的 Git Bash)
|
||||||
|
```
|
||||||
|
不要直接双击 `ml/monitor.html`:那样只能看到示例数据(页面顶部有黄色提示)。
|
||||||
|
|
||||||
|
浏览器自动打开 `http://localhost:8100/`,零第三方依赖、不连外网,每 5 秒刷新。
|
||||||
|
|
||||||
|
**自检**:控制台启动时检查训练用 Python 能否 `import torch`、GPU、node、bash,结果显示在「全流程」卡片底部;不过则「开始训练」不可点。
|
||||||
|
**开始**:点「▶ 开始训练」→ 运行方式(续跑 / 从头重跑)、动作头、共识过滤、任务数、进程数、目标样本数、挖错轮数、`MIN_GAP_MM` → 「开始」。控制台在后台执行 `bash ml/pipeline.sh all`。
|
||||||
|
- 续跑:跳过已成功的阶段。改了参数时只作废受影响的阶段(改生成参数 → 从生成起重跑;改动作头 → R4 与训练起重跑;改挖错参数 → 从挖错起重跑),旧产物移到 `ml/archive/<时间>/`,不删除。
|
||||||
|
- 从头重跑(`FRESH=1`):全部旧产物移到 `ml/archive/<时间>/`。
|
||||||
|
**停止**:「■ 停止」时,训练类阶段(预训练 / M5 / APG / 挖错)先收到停止请求(`ml/runs/STOP`),存好 `last.pt` 自行退出(最多等 90 秒),然后结束整个进程树;这一步记为「已停止」并放锁,再点「开始训练」即从断点续跑。终端里按 Ctrl+C 或 `kill` 也会先存档再退出。终端或上一个控制台窗口起的流水线也能在这里停(按锁里记录的进程号)。
|
||||||
|
**网页和控制台窗口都可以关**,训练不受影响(Windows 子进程有自己的隐藏控制台,Linux 在独立会话里),重新双击 `console.bat` 即可接着看。
|
||||||
|
**互斥**:已有一条在跑时(网页、终端或 `all` 起的都算),第二个入口会被拒绝并给出持锁进程号;进程被强杀没来得及放锁时,心跳 5 分钟不刷新即视为失效。
|
||||||
|
**超时**:每个阶段上限 = 估时 ×3(网页「本步粗估 / 上限」;`TIMEOUT_SCALE=2` 整体放宽),超了判卡死、阶段失败、放锁。生成阶段每分钟往终端输出打一行节拍。
|
||||||
|
**安全**:开始/停止只接受本控制台页面发起的请求(自定义请求头 + 同源检查),本机其他网页发不进来。
|
||||||
|
|
||||||
|
页面内容,从上到下:
|
||||||
|
|
||||||
|
| 区域 | 内容 | 数据来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| 流水线 | 12 个阶段的状态(完成 / 运行中 / 失败 / 已停止 / 未开始)与各步用时 | `ml/runs/pipeline.json`(每个阶段开始结束时自动写) |
|
||||||
|
| 全流程 | 全流程百分比(按各步粗估耗时加权)、当前步骤与它的进度、总用时、本步用时、剩余粗估、开始/停止按钮 | 同上 + 各阶段产物 |
|
||||||
|
| GPU | 利用率、显存、温度、功耗 | `nvidia-smi` |
|
||||||
|
| 当前运行 | 正在跑的训练(自动跟随;顶部下拉框可切换 `ml/runs` 下任一次运行):步数、已用/剩余时间、预计完成时刻、评估标记 | `ml/runs/*/log.jsonl` |
|
||||||
|
| 指标卡与曲线 | 预训练:总损失、各损失分量、吞吐(R10 线 5000 样本/s)、闭环评估(到达率 / 碰撞率 / ≤老师时长×1.3 / 起步失败);APG:到达误差、损失分量、展开长度 | 同上 |
|
||||||
|
| 各阶段结果 | 数据统计(含 `gapDrop`)、R2、M5 两种口径、挖错各轮与门禁、部署评测、验收逐项 | `ml/data/*.json`、`ml/runs/*.json` |
|
||||||
|
| 终端输出 | 控制台启动的流水线的命令行输出,报错标红 | `ml/runs/console.log` |
|
||||||
|
|
||||||
|
当前步骤的进度怎么算:生成数据 = 已写出任务条数 / 目标条数(含自动补量);打包 = 已打包分片 / 分片数;预训练 = (测速 2000 步 + 正式步数) / (2000 + 总步数);APG = 步数;挖错 = 已完成轮数 + 本轮 APG 步数(共最多 `ROUNDS` 轮,提升不足会提前停);M5 = 两遍评估;对拍、老师门禁、导出、部署评测、验收无法计量,显示用时与粗估。全流程百分比与剩余时间按 `ml/pstate.py` 里的阶段估时加权(与超时上限同一份表),只作参考。
|
||||||
|
|
||||||
|
同一局域网的手机或其他电脑要看:`python -m ml.monitor --host 0.0.0.0`,用本机 IP 访问;其他机器只能看,开始/停止只接受本机操作(`--remote-control` 放开)。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
- **指纹不符**:`collide.json` 变了。依次重跑第 2、4、5 步并重训。
|
||||||
|
- **训练发散**(loss 出现 nan/inf,自动停在 `diverged.pt`):先把 `lr` 降到 5e-4 重试一次,仍发散就走 WTA 回退(第 7 步)。
|
||||||
|
- **显存/内存不足**:降 `batch`;APG 还可把 `seg_plans` 降到 5。出题与间隙计算按空闲显存自动分块(用 1/4;空闲量本就扣除了桌面占用)。`check_r2` 常驻约 1.6 GB(1.7 M 样本)。
|
||||||
|
- **小样本摸底**:几百条任务时 val 可能为空,训练会提示并跳过闭环验证;R2 检查近邻对不足时 `pass` 为 null,不拦后续步骤。
|
||||||
|
- **返回码**:3 = M5 两种头都没过;4 = R2 不过且 `R2_STRICT=1`;5 = R4 不过;6 = 生成进程崩溃(看 `ml/data/log_*.txt`,采样器连续失败会写明原因);8 = `MIN_GAP_MM` 前后不一致;9 = 已有一条在跑;124 = 阶段超时。
|
||||||
|
- **生成阶段报「采样失败:…连续 N 次未命中」**:多半是 `collide.json` 或关节限位变了,某类位形几乎不存在;把该日志发我。
|
||||||
|
- **浏览器只有 1 线程**:没走 `ml/serve.py`(缺 COOP/COEP 头)。
|
||||||
|
- **查看器徽标「策略模式不可用」**:看控制台报错,多为模型文件缺失、指纹或 TCP 不符。
|
||||||
|
|
||||||
|
## 文件地图
|
||||||
|
|
||||||
|
```
|
||||||
|
ml/
|
||||||
|
pipeline.sh 一条龙阶段脚本(环境变量见文件头);每阶段开始/结束经 pstate.py 写 runs/pipeline.json
|
||||||
|
console.bat / console.sh 训练控制台一键启动(Windows / Linux)
|
||||||
|
monitor.py monitor.html 训练控制台:本地 HTTP 服务(开始/停止/进度/日志接口)+ 零依赖网页(地址带 ?demo=1 看示例数据)
|
||||||
|
pstate.py 流水线状态 runs/pipeline.json、互斥锁 runs/pipeline.lock、阶段估时/超时表(唯一来源)
|
||||||
|
gen/ 老师数据:sampler / teacher(确定性 PRM A*)/ cert.cjs(距离下界认证,实现在 ik-policy.js)/
|
||||||
|
pathdev.cjs(同伦类判定,共识过滤与一致性门禁用)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
|
||||||
|
prep/ export_kin.cjs(→ assets/kin.json)、build_dataset.cjs(→ samples/*.npy)、npy.cjs(.npy 写出)、run_prep.sh
|
||||||
|
common/ kin_torch.py(可微运动学 + 胶囊间隙)、features.py(83 维特征,与 JS 逐项一致)
|
||||||
|
model/ policy.py(Transformer + MeanFlow / WTA 头)、meanflow.py
|
||||||
|
data.py train.py 样本加载、预训练
|
||||||
|
sim/closed_loop.py 部署节奏闭环仿真(验证 / 挖错 / 门禁)
|
||||||
|
apg/apg_finetune.py APG 可微微调
|
||||||
|
mining/ tasks.py(无老师出题)、mine_round.py、gate.py(含 R7)、loop.sh
|
||||||
|
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、r4_probe.py(R4 无头浏览器探针)、acceptance.py(验收汇总)
|
||||||
|
toy/toy_bimodal.py 2D 双模态对照实验(动作头选型依据)
|
||||||
|
export_onnx.py serve.py
|
||||||
|
configs/ base.json(MeanFlow)、wta.json(回退)、small.json(R4 不过时的 d=128)、apg.json
|
||||||
|
webapp/js/ik-policy.js 运行时:特征、距离下界认证、刹停、投影、精修、Runner(浏览器与 Node 共用)
|
||||||
|
webapp/js/ik-policy-glue.js 查看器接入(?policy=1;&threads= &proxy=0 &ep=webgpu &probe=1)
|
||||||
|
webapp/r4_probe.html R4 探针页(r4_probe.py 打开)
|
||||||
|
tests/policy_bench.cjs 部署形态评测(nc / route / r8 / nc1000)
|
||||||
|
```
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""APG 可微微调(方案 §7.1):按部署节奏可微展开策略,直接对"走得到、不碰、平滑"求梯度;不需要老师。
|
||||||
|
python -m ml.apg.apg_finetune --init ml/runs/base/best.pt --samples ml/data/samples --out ml/runs/apg [--pool ml/runs/mine/pool.jsonl]
|
||||||
|
每条展开固定一个噪声 z(确定性可微映射);每次推理提交 3 步(已提交前缀 3 步,与部署一致);
|
||||||
|
截断反传:每 seg_plans 次推理(=3·seg_plans 步)反传一次并截断状态梯度。
|
||||||
|
损失:段末到达项 + Σ碰撞铰链 ReLU(margin_train − 间隙)(逐胶囊对,端点+中点)+ 平滑(速度²+加速度²)+ 行程 + BC 锚定(MeanFlow 损失)。
|
||||||
|
2026-10-18:展开长度不再定长 60 次推理(3.6 s,会截掉老师时长 > 3.6 s 的绕远题),改为覆盖本批 exec_s p90×1.2,上限 plans_max;
|
||||||
|
"到达"仍是每 seg_plans 段末的距离项(无到达即停),属有意设计:段末项给出持续的接近梯度。
|
||||||
|
margin_train 3 mm:与训练数据过滤线一致(认证最小间隙 < 3 mm 的老师轨迹不进训练集),碰撞项不与 BC 标签对抗;
|
||||||
|
运行时投影护栏独立保持 5 mm。端点与中点两个铰链取均值,与只查端点同量纲,w_col 无需重标。"""
|
||||||
|
import argparse, json, math, os, random, sys, time
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.common.features import features, STEP, H, wrap180
|
||||||
|
from ml.data import Samples, load_tasks
|
||||||
|
from ml.model.policy import build, wta_loss
|
||||||
|
from ml.model.meanflow import meanflow_loss
|
||||||
|
from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err
|
||||||
|
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
|
||||||
|
from ml.common import vram
|
||||||
|
|
||||||
|
|
||||||
|
def load_pool(paths):
|
||||||
|
out = []
|
||||||
|
for p in paths or []:
|
||||||
|
if os.path.exists(p):
|
||||||
|
out += [json.loads(l) for l in open(p, encoding='utf-8') if l.strip()]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--init', required=True); ap.add_argument('--samples', nargs='+', default=['ml/data/samples'])
|
||||||
|
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('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
|
||||||
|
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()
|
||||||
|
cfg = json.load(open(a.config)); C = cfg['apg']
|
||||||
|
if a.steps: C['steps'] = a.steps
|
||||||
|
dev = a.device; torch.manual_seed(a.seed); random.seed(a.seed)
|
||||||
|
vram.limit()
|
||||||
|
os.makedirs(a.out, exist_ok=True)
|
||||||
|
ck = torch.load(a.init, map_location=dev)
|
||||||
|
mcfg = ck['cfg']
|
||||||
|
model = build(mcfg).to(dev); model.load_state_dict(ck['model'])
|
||||||
|
kin = Kin(device=dev)
|
||||||
|
data = Samples(a.samples, device=dev, split='train')
|
||||||
|
all_tasks = load_tasks(a.samples)
|
||||||
|
train_tasks = [t for t in all_tasks if t['split'] == 'train'] + load_pool(a.pool)
|
||||||
|
val_tasks = [t for t in all_tasks if t['split'] == 'val'][:C['eval_tasks']]
|
||||||
|
w = [C['cat_weights'].get(t.get('cat', 'A'), 1.0) * (2.0 if t.get('hard') else 1.0) for t in train_tasks]
|
||||||
|
opt = torch.optim.AdamW(model.parameters(), lr=C['lr'], betas=(0.9, 0.95), weight_decay=0.0)
|
||||||
|
amp = dev.startswith('cuda') and C.get('amp') == 'bf16'
|
||||||
|
step = torch.tensor(STEP, device=dev)
|
||||||
|
it0 = 0
|
||||||
|
if a.resume and os.path.exists(a.resume):
|
||||||
|
rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model'])
|
||||||
|
if 'opt' in rk: opt.load_state_dict(rk['opt'])
|
||||||
|
it0 = rk.get('step', 0)
|
||||||
|
if set_rng_state(rk.get('rng')): print('续跑:随机数状态已恢复', flush=True)
|
||||||
|
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
|
||||||
|
log = open(os.path.join(a.out, 'log.jsonl'), 'a')
|
||||||
|
print(f'续跑:从第 {it0} 步继续,当前最好 success {best:.4f}', flush=True)
|
||||||
|
else:
|
||||||
|
log = open(os.path.join(a.out, 'log.jsonl'), 'w')
|
||||||
|
base, _ = evaluate(model.eval(), kin, val_tasks, dev)
|
||||||
|
print('EVAL0', json.dumps(base), flush=True); log.write(json.dumps({'eval': base, 'step': 0}) + '\n')
|
||||||
|
best = base['success']
|
||||||
|
atomic_save({'model': model.state_dict(), 'cfg': mcfg, 'step': 0, 'fp': kin.fp, 'eval': base}, os.path.join(a.out, 'best.pt'))
|
||||||
|
M = C['margin_train_mm']; t0 = time.time()
|
||||||
|
saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存)
|
||||||
|
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, 'rng': rng_state()}, os.path.join(a.out, 'last.pt')); saver.mark()
|
||||||
|
for it in range(it0, C['steps']):
|
||||||
|
model.train()
|
||||||
|
batch = random.choices(train_tasks, weights=w, k=C['batch'])
|
||||||
|
q0, T, R, ex, tr = tasks_to_tensors(batch, dev)
|
||||||
|
B = q0.shape[0]
|
||||||
|
z = torch.randn(B, H, 6, device=dev)
|
||||||
|
q, qp = q0.clone(), q0.clone()
|
||||||
|
queue = torch.zeros(B, 0, 6, device=dev)
|
||||||
|
subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev)
|
||||||
|
opt.zero_grad(set_to_none=True)
|
||||||
|
stats = {'col': 0.0, 'tgt': 0.0, 'smooth': 0.0}
|
||||||
|
# 展开长度随本批任务时长:覆盖本批 exec_s 的 p90 × 1.2(无老师参考题按 6 s),夹在 [plans, plans_max]
|
||||||
|
exb = torch.nan_to_num(ex, nan=6.0)
|
||||||
|
n_plans = int(min(C['plans_max'], max(C['plans'], math.ceil(1.2 * torch.quantile(exb, 0.9).item() / (3 * 0.02)))))
|
||||||
|
nseg = math.ceil(n_plans / C['seg_plans'])
|
||||||
|
seg_loss = torch.zeros((), device=dev); prev_d = torch.zeros(B, 6, device=dev)
|
||||||
|
for p in range(n_plans):
|
||||||
|
tnow = p * 3 * 0.02
|
||||||
|
r = queue.shape[1]
|
||||||
|
pre = torch.zeros(B, 3, 6, device=dev); pre[:, :r] = queue[:, :r]
|
||||||
|
qe = q + queue.sum(1)
|
||||||
|
with torch.no_grad():
|
||||||
|
if (nsub > 0).any():
|
||||||
|
cur = subs[:, 0]; dd = torch.where(kin.cont, wrap180(cur - qe), cur - qe).abs().max(-1).values
|
||||||
|
adv = (nsub > 0) & ((dd < 5) | (tnow - sub_t > 1.5))
|
||||||
|
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, 1:]], 1), subs)
|
||||||
|
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
|
||||||
|
feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=dev), subs[:, 0], (nsub > 0).float())
|
||||||
|
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
|
||||||
|
act, val, sub = model.sample_k(feat, 1, z)
|
||||||
|
act = act[:, 0].float()
|
||||||
|
if p == 0:
|
||||||
|
with torch.no_grad():
|
||||||
|
need = sub[:, 12].float() > 0.5
|
||||||
|
subs = q.detach()[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
|
||||||
|
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub))
|
||||||
|
inc = act.clamp(-1, 1) * step # [B,16,6]
|
||||||
|
commit = inc[:, r:r + 3]
|
||||||
|
if r == 0: # 冷启动:先提交一块,下一轮才开始执行
|
||||||
|
queue = commit; continue
|
||||||
|
# 执行队列中的 3 步(上一轮提交的)
|
||||||
|
for k in range(3):
|
||||||
|
d = queue[:, k]
|
||||||
|
qp = q; q = kin.clamp_box(q + d)
|
||||||
|
# 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致)
|
||||||
|
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M)
|
||||||
|
hinge = torch.relu(M - G['gap'])
|
||||||
|
col = torch.zeros(2 * B, device=dev).index_add(0, G['b'], hinge).view(2, B).mean(0) # 端点与中点取均值:与只查端点时同量纲,w_col 无需重标
|
||||||
|
sm = ((d / step) ** 2).sum(-1) + (((d - prev_d) / step) ** 2).sum(-1)
|
||||||
|
prev_d = d
|
||||||
|
seg_loss = seg_loss + C['w_col'] * col.mean() + C['w_smooth'] * sm.mean() + C['w_len'] * d.abs().sum(-1).mean()
|
||||||
|
stats['col'] += col.mean().item(); stats['smooth'] += sm.mean().item()
|
||||||
|
queue = commit
|
||||||
|
if (p + 1) % C['seg_plans'] == 0 or p == n_plans - 1:
|
||||||
|
xf = kin.cum_xf(q)
|
||||||
|
pe = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(B, 9)
|
||||||
|
pose = (R.abs().sum(-1) > 0).float()
|
||||||
|
lt = ((T - pe) / 100).pow(2).sum(-1) + C['w_rot'] * pose * (Rq - R).pow(2).sum(-1)
|
||||||
|
seg_loss = seg_loss + C['w_tgt'] * lt.mean()
|
||||||
|
stats['tgt'] += lt.mean().item()
|
||||||
|
(seg_loss / nseg).backward()
|
||||||
|
seg_loss = torch.zeros((), device=dev)
|
||||||
|
q, qp, queue, prev_d = q.detach(), qp.detach(), queue.detach(), prev_d.detach()
|
||||||
|
# BC 锚定
|
||||||
|
X, Y, V, SG = data.batch(C['batch'] * 4)
|
||||||
|
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
|
||||||
|
mem = model.encode(X)
|
||||||
|
l_bc = wta_loss(model, mem, Y)[0] if model.head == 'wta' else meanflow_loss(model, mem, Y)[0]
|
||||||
|
v, s = model.heads(mem)
|
||||||
|
l_bc = l_bc + 0.1 * (v.float() - V).abs().mean() + 0.1 * (s.float() - SG).abs().mean()
|
||||||
|
(C['w_bc'] * l_bc).backward()
|
||||||
|
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), C['grad_clip'])
|
||||||
|
opt.step()
|
||||||
|
with torch.no_grad():
|
||||||
|
e, oe = goal_err(kin, q, T, R)
|
||||||
|
rec = {'it': it, 'total': C['steps'], 't': time.time(), 'plans': n_plans, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(),
|
||||||
|
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60, 'vram': vram.peak_gib()}
|
||||||
|
if it % 10 == 0: print(json.dumps(rec), flush=True)
|
||||||
|
log.write(json.dumps(rec) + '\n'); log.flush()
|
||||||
|
if saver.stop() or (a.stop_at and it + 1 >= a.stop_at):
|
||||||
|
save_last(it + 1); log.close()
|
||||||
|
print(f'收到停止请求:已存档 last.pt(第 {it + 1} 次),下次运行自动续跑', flush=True); sys.exit(143)
|
||||||
|
if saver.due(): save_last(it + 1)
|
||||||
|
if (it + 1) % C['eval_every'] == 0 or it == C['steps'] - 1:
|
||||||
|
res, _ = evaluate(model.eval(), kin, val_tasks, dev)
|
||||||
|
res['it'] = it + 1; res['t'] = time.time()
|
||||||
|
print('EVAL', json.dumps(res), flush=True); log.write(json.dumps({'eval': res}) + '\n'); log.flush()
|
||||||
|
sd = {'model': model.state_dict(), 'cfg': mcfg, 'step': it + 1, 'fp': kin.fp, 'eval': res}
|
||||||
|
save_last(it + 1, res) # last 带优化器状态供续跑;best 不带(导出/门禁用)
|
||||||
|
if res['success'] > best + 1e-4:
|
||||||
|
best = res['success']; atomic_save(sd, os.path.join(a.out, 'best.pt'))
|
||||||
|
print(f'done, best success {best:.4f}', flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,62 @@
|
|||||||
|
"""断点存档与"体面停止"(2026-10-18 第五轮补充:用户要求长训练意外中断后能接着跑,不从头来)。
|
||||||
|
|
||||||
|
- atomic_save:先写 <名>.tmp 再 os.replace —— 断电/被杀发生在写盘中途时,旧的 last.pt 仍完整可用。
|
||||||
|
- Saver.due():按墙钟每 every_min 分钟存一次 last.pt(不再只在每 2000 步验证时存,断电最多丢 10 分钟)。
|
||||||
|
- Saver.stop():收到 SIGTERM/SIGINT,或看到停止标记文件 ml/runs/STOP 时返回 True;
|
||||||
|
训练循环据此先存 last.pt 再以 143 退出。Windows 上 taskkill /F 是硬杀、收不到信号,
|
||||||
|
所以控制台「停止」先写 STOP 文件、等进程自己存档退出,超时才硬杀(见 monitor.Job.stop)。
|
||||||
|
"""
|
||||||
|
import os, random, signal, time
|
||||||
|
import torch
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
STOP = os.path.join(ROOT, 'ml', 'runs', 'STOP')
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_save(obj, path):
|
||||||
|
tmp = path + '.tmp'
|
||||||
|
torch.save(obj, tmp)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
|
class Saver:
|
||||||
|
def __init__(self, every_min=10.0):
|
||||||
|
self.every = every_min * 60; self.last = time.time(); self._sig = False; self._chk = 0.0
|
||||||
|
for s in ('SIGTERM', 'SIGINT', 'SIGBREAK', 'SIGHUP'):
|
||||||
|
if hasattr(signal, s):
|
||||||
|
try: signal.signal(getattr(signal, s), self._on)
|
||||||
|
except (ValueError, OSError): pass # 非主线程等情况:只靠 STOP 文件
|
||||||
|
|
||||||
|
def _on(self, *_): self._sig = True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._sig: return True
|
||||||
|
now = time.time()
|
||||||
|
if now - self._chk > 2: # 文件每 2 s 查一次,不拖慢训练步
|
||||||
|
self._chk = now; self._sig = os.path.exists(STOP)
|
||||||
|
return self._sig
|
||||||
|
|
||||||
|
def due(self):
|
||||||
|
if time.time() - self.last >= self.every:
|
||||||
|
self.last = time.time(); return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""策略输入特征(83 维)—— 与 webapp/js/ik-policy.js features() 逐项一致。改动须两边同步并升 FEAT_VER。"""
|
||||||
|
import math
|
||||||
|
import torch
|
||||||
|
|
||||||
|
FEAT_VER = 1
|
||||||
|
VMAX = [60., 60., 90., 90., 120., 180.]
|
||||||
|
DT = 0.02
|
||||||
|
STEP = [v * DT for v in VMAX]
|
||||||
|
ACC = 5.0
|
||||||
|
H, NJ, PREFIX = 16, 6, 3
|
||||||
|
GP = [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (2, 4), (2, 5), (3, 5)]
|
||||||
|
GAP_CAP = 40.0
|
||||||
|
F_DIM = 83
|
||||||
|
D2R = math.pi / 180.0
|
||||||
|
ROT6 = [0, 3, 6, 1, 4, 7] # 行主序 3×3 的前两列
|
||||||
|
|
||||||
|
|
||||||
|
def wrap180(x):
|
||||||
|
return torch.remainder(x + 180.0, 360.0) - 180.0
|
||||||
|
|
||||||
|
|
||||||
|
def features(kin, q, q_prev, T, R, prefix, prefix_len, sub, sub_flag, gm=None, xf=None):
|
||||||
|
"""全部为 batch 张量:
|
||||||
|
q, q_prev [B,6] 度;T [B,3] mm;R [B,9](pos 模式全 0);pose 标志由 R 是否全 0 判定
|
||||||
|
prefix [B,3,6] 增量(度),prefix_len [B];sub [B,6],sub_flag [B](0/1)
|
||||||
|
gm:kin.gaps(q, 40)['gm'] 可传入复用。"""
|
||||||
|
B = q.shape[0]
|
||||||
|
dt = q.dtype
|
||||||
|
step = torch.tensor(STEP, dtype=dt, device=q.device)
|
||||||
|
xf = xf if xf is not None else kin.cum_xf(q)
|
||||||
|
out = []
|
||||||
|
rad = q * D2R
|
||||||
|
out.append(torch.stack([torch.sin(rad), torch.cos(rad)], -1).reshape(B, 12))
|
||||||
|
lo, hi = kin.lo.to(dt), kin.hi.to(dt)
|
||||||
|
idx = [1, 3, 4]
|
||||||
|
out.append(2 * (q[:, idx] - lo[idx]) / (hi[idx] - lo[idx]) - 1)
|
||||||
|
d = torch.where(kin.cont, wrap180(q - q_prev), q - q_prev)
|
||||||
|
out.append(d / step)
|
||||||
|
p = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(B, 9)
|
||||||
|
out.append(p / 600)
|
||||||
|
out.append(Rq[:, ROT6])
|
||||||
|
out.append(T / 600)
|
||||||
|
pose = (R.abs().sum(-1) > 0).to(dt)
|
||||||
|
out.append(R[:, ROT6] * pose[:, None])
|
||||||
|
out.append((T - p) / 600)
|
||||||
|
out.append(((T - p) / 50).clamp(-1, 1))
|
||||||
|
out.append(torch.stack([1 - pose, pose], -1))
|
||||||
|
m = (torch.arange(PREFIX, device=q.device)[None, :] < prefix_len[:, None]).to(dt) # [B,3]
|
||||||
|
out.append((prefix / step * m[:, :, None]).reshape(B, 18))
|
||||||
|
out.append(m)
|
||||||
|
sd = torch.where(kin.cont, wrap180(sub - q), sub - q) / 180 * sub_flag[:, None].to(dt)
|
||||||
|
out.append(sd)
|
||||||
|
out.append(sub_flag[:, None].to(dt))
|
||||||
|
if gm is None:
|
||||||
|
gm = kin.gaps(q, GAP_CAP, xf)['gm']
|
||||||
|
out.append(torch.stack([gm[:, a * 7 + b].clamp(max=GAP_CAP) / GAP_CAP for a, b in GP], -1))
|
||||||
|
f = torch.cat(out, -1)
|
||||||
|
assert f.shape[1] == F_DIM, f.shape
|
||||||
|
return f
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""PyTorch 批量运动学 + 可微胶囊间隙(与 webapp/js/ik-solver.js 同口径)。
|
||||||
|
|
||||||
|
数据来自 ml/assets/kin.json(node ml/prep/export_kin.cjs 生成):
|
||||||
|
- cum_xf / fk / orient:与 JS cumXf / fk(q,0) / orient(q) 一致(角度制输入)
|
||||||
|
- gaps(q, cap):所有 |Δg|≥2 胶囊对的 间隙 = 线段距离 − (r_i + r_j + margin)。
|
||||||
|
宽相位复刻 JS 的簇球筛选(被筛掉的对间隙必 > cap),故 min(gap, cap) 与 JS gapMatrix 逐位同口径;
|
||||||
|
筛选在 no_grad 下做,幸存对的精确距离保留梯度(APG / FK 损失用)。
|
||||||
|
"""
|
||||||
|
import json, math, os
|
||||||
|
import torch
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DEFAULT_KIN = os.path.join(HERE, '..', 'assets', 'kin.json')
|
||||||
|
D2R = math.pi / 180.0
|
||||||
|
|
||||||
|
|
||||||
|
def rot_axis(d, a):
|
||||||
|
"""d: [3] 单位轴;a: [...] 弧度 → [..., 3, 3](Rodrigues,右手)"""
|
||||||
|
c, s = torch.cos(a), torch.sin(a)
|
||||||
|
x, y, z = float(d[0]), float(d[1]), float(d[2])
|
||||||
|
C = 1 - c
|
||||||
|
row0 = torch.stack([c + x * x * C, x * y * C - z * s, x * z * C + y * s], -1)
|
||||||
|
row1 = torch.stack([y * x * C + z * s, c + y * y * C, y * z * C - x * s], -1)
|
||||||
|
row2 = torch.stack([z * x * C - y * s, z * y * C + x * s, c + z * z * C], -1)
|
||||||
|
return torch.stack([row0, row1, row2], -2)
|
||||||
|
|
||||||
|
|
||||||
|
def seg_seg_dist(p1, q1, p2, q2, eps=1e-12):
|
||||||
|
"""两线段最近距离(Ericson, Real-Time Collision Detection §5.1.9),[..., 3] → [...]"""
|
||||||
|
d1, d2, r = q1 - p1, q2 - p2, p1 - p2
|
||||||
|
a = (d1 * d1).sum(-1); e = (d2 * d2).sum(-1); f = (d2 * r).sum(-1)
|
||||||
|
c = (d1 * r).sum(-1); b = (d1 * d2).sum(-1)
|
||||||
|
denom = a * e - b * b
|
||||||
|
s = torch.where(denom > eps, ((b * f - c * e) / denom.clamp_min(eps)).clamp(0, 1), torch.zeros_like(a))
|
||||||
|
t = (b * s + f) / e.clamp_min(eps)
|
||||||
|
# t 越界:夹回 [0,1] 并重算 s
|
||||||
|
t_lo, t_hi = t < 0, t > 1
|
||||||
|
s = torch.where(t_lo, (-c / a.clamp_min(eps)).clamp(0, 1), s)
|
||||||
|
s = torch.where(t_hi, ((b - c) / a.clamp_min(eps)).clamp(0, 1), s)
|
||||||
|
t = t.clamp(0, 1)
|
||||||
|
# 退化(线段长度≈0)
|
||||||
|
s = torch.where(a <= eps, torch.zeros_like(s), s)
|
||||||
|
t = torch.where(e <= eps, torch.zeros_like(t), t)
|
||||||
|
c1 = p1 + d1 * s.unsqueeze(-1)
|
||||||
|
c2 = p2 + d2 * t.unsqueeze(-1)
|
||||||
|
return ((c1 - c2).pow(2).sum(-1) + 1e-18).sqrt()
|
||||||
|
|
||||||
|
|
||||||
|
class Kin:
|
||||||
|
def __init__(self, path=DEFAULT_KIN, device='cpu', dtype=torch.float32):
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
K = json.load(f)
|
||||||
|
self.fp = K['fp']
|
||||||
|
self.device, self.dtype = device, dtype
|
||||||
|
T = lambda x, dt=dtype: torch.tensor(x, dtype=dt, device=device)
|
||||||
|
self.P = T(K['P']); self.D = [list(map(float, d)) for d in K['D']]
|
||||||
|
lim = K['lim']
|
||||||
|
self.lo = T([l[0] for l in lim]); self.hi = T([l[1] for l in lim]); self.cont = T([l[2] for l in lim]).bool()
|
||||||
|
self.tcp0 = T(K['tcp0']); self.capm = float(K['capm'])
|
||||||
|
caps = K['caps']
|
||||||
|
self.cap_g = torch.tensor([c['g'] for c in caps], device=device)
|
||||||
|
self.cap_a = T([c['a'] for c in caps]); self.cap_b = T([c['b'] for c in caps]); self.cap_r = T([c['r'] for c in caps])
|
||||||
|
cl = K['clusters']
|
||||||
|
self.cl_g = torch.tensor([c['g'] for c in cl], device=device)
|
||||||
|
self.cl_c = T([c['c'] for c in cl])
|
||||||
|
cp = K['cpairs']
|
||||||
|
self.cp_a = torch.tensor([p[0] for p in cp], device=device)
|
||||||
|
self.cp_b = torch.tensor([p[1] for p in cp], device=device)
|
||||||
|
self.cp_lim = T([p[2] + p[3] + self.capm for p in cp])
|
||||||
|
# 每个簇对展开成胶囊对(全组合),按簇对连续存放
|
||||||
|
ci, cj, start, cnt = [], [], [], []
|
||||||
|
for p in cp:
|
||||||
|
A, B = cl[p[0]]['caps'], cl[p[1]]['caps']
|
||||||
|
start.append(len(ci)); cnt.append(len(A) * len(B))
|
||||||
|
for i in A:
|
||||||
|
for j in B:
|
||||||
|
ci.append(i); cj.append(j)
|
||||||
|
self.pi = torch.tensor(ci, device=device, dtype=torch.long)
|
||||||
|
self.pj = torch.tensor(cj, device=device, dtype=torch.long)
|
||||||
|
self.cp_start = torch.tensor(start, device=device, dtype=torch.long)
|
||||||
|
self.cp_cnt = torch.tensor(cnt, device=device, dtype=torch.long)
|
||||||
|
hl = 0.5 * (self.cap_b - self.cap_a).norm(dim=-1)
|
||||||
|
self.pair_need = self.cap_r[self.pi] + self.cap_r[self.pj] + self.capm
|
||||||
|
self.pair_bl = hl[self.pi] + hl[self.pj] + self.cap_r[self.pi] + self.cap_r[self.pj] + self.capm
|
||||||
|
gi, gj = self.cap_g[self.pi], self.cap_g[self.pj]
|
||||||
|
self.pair_gp = torch.minimum(gi, gj) * 7 + torch.maximum(gi, gj)
|
||||||
|
self.lever = T(K['lever'])
|
||||||
|
|
||||||
|
# ---------------- 运动学 ----------------
|
||||||
|
def cum_xf(self, q):
|
||||||
|
"""q: [B,6] 度 → Rs [B,7,3,3], Ts [B,7,3]"""
|
||||||
|
B = q.shape[0]
|
||||||
|
R = torch.eye(3, dtype=q.dtype, device=q.device).expand(B, 3, 3)
|
||||||
|
Tt = torch.zeros(B, 3, dtype=q.dtype, device=q.device)
|
||||||
|
Rs, Ts = [R], [Tt]
|
||||||
|
for k in range(6):
|
||||||
|
Rk = rot_axis(self.D[k], q[:, k] * D2R)
|
||||||
|
Pk = self.P[k].to(q.dtype)
|
||||||
|
tk = Pk - torch.einsum('bij,j->bi', Rk, Pk)
|
||||||
|
Tt = torch.einsum('bij,bj->bi', R, tk) + Tt
|
||||||
|
R = R @ Rk
|
||||||
|
Rs.append(R); Ts.append(Tt)
|
||||||
|
return torch.stack(Rs, 1), torch.stack(Ts, 1)
|
||||||
|
|
||||||
|
def fk(self, q, xf=None):
|
||||||
|
Rs, Ts = xf if xf is not None else self.cum_xf(q)
|
||||||
|
return torch.einsum('bij,j->bi', Rs[:, 6], self.tcp0.to(q.dtype)) + Ts[:, 6]
|
||||||
|
|
||||||
|
def orient(self, q, xf=None):
|
||||||
|
Rs, _ = xf if xf is not None else self.cum_xf(q)
|
||||||
|
return Rs[:, 6]
|
||||||
|
|
||||||
|
def caps_world(self, q, xf=None):
|
||||||
|
Rs, Ts = xf if xf is not None else self.cum_xf(q)
|
||||||
|
Rg, Tg = Rs[:, self.cap_g], Ts[:, self.cap_g] # [B,C,3,3], [B,C,3]
|
||||||
|
a = torch.einsum('bcij,cj->bci', Rg, self.cap_a.to(q.dtype)) + Tg
|
||||||
|
b = torch.einsum('bcij,cj->bci', Rg, self.cap_b.to(q.dtype)) + Tg
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
# ---------------- 间隙 ----------------
|
||||||
|
def gaps(self, q, cap=40.0, xf=None, max_pairs=None):
|
||||||
|
"""返回 dict:
|
||||||
|
gm [B,49]:组对最小间隙 min(gap, cap)(gA*7+gB,gA<gB;其余为 cap)
|
||||||
|
b, i, j, gap:幸存胶囊对(gap 可微)
|
||||||
|
"""
|
||||||
|
xf = xf if xf is not None else self.cum_xf(q)
|
||||||
|
Rs, Ts = xf
|
||||||
|
B = q.shape[0]
|
||||||
|
with torch.no_grad():
|
||||||
|
cc = torch.einsum('bkij,kj->bki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g]
|
||||||
|
d2 = (cc[:, self.cp_a] - cc[:, self.cp_b]).pow(2).sum(-1) # [B,M]
|
||||||
|
keep = d2 <= (self.cp_lim + cap).pow(2)
|
||||||
|
bb, mm = keep.nonzero(as_tuple=True)
|
||||||
|
cnt = self.cp_cnt[mm]
|
||||||
|
rep_b = torch.repeat_interleave(bb, cnt)
|
||||||
|
off = torch.repeat_interleave(self.cp_start[mm] - torch.cumsum(cnt, 0) + cnt, cnt) + torch.arange(int(cnt.sum()), device=q.device)
|
||||||
|
pidx = off
|
||||||
|
a, b = self.caps_world(q, xf)
|
||||||
|
with torch.no_grad():
|
||||||
|
pi, pj = self.pi[pidx], self.pj[pidx]
|
||||||
|
mid = 0.5 * (a[rep_b, pi] + b[rep_b, pi] - a[rep_b, pj] - b[rep_b, pj])
|
||||||
|
k2 = mid.pow(2).sum(-1) <= (self.pair_bl[pidx] + cap).pow(2)
|
||||||
|
rep_b, pidx, pi, pj = rep_b[k2], pidx[k2], pi[k2], pj[k2]
|
||||||
|
d = seg_seg_dist(a[rep_b, pi], b[rep_b, pi], a[rep_b, pj], b[rep_b, pj])
|
||||||
|
gap = d - self.pair_need[pidx].to(q.dtype)
|
||||||
|
gm = torch.full((B * 49,), float(cap), dtype=q.dtype, device=q.device)
|
||||||
|
gm = gm.scatter_reduce(0, rep_b * 49 + self.pair_gp[pidx], gap.clamp(max=cap), reduce='amin', include_self=True)
|
||||||
|
return {'gm': gm.view(B, 49), 'b': rep_b, 'i': pi, 'j': pj, 'gap': gap}
|
||||||
|
|
||||||
|
def min_gap(self, q, cap=40.0):
|
||||||
|
return self.gaps(q, cap)['gm'].min(-1).values
|
||||||
|
|
||||||
|
def unwrap_to(self, a, b):
|
||||||
|
"""连续轴取离 a 最近的等价角(与 JS unwrapTo 同口径)"""
|
||||||
|
d = b - a
|
||||||
|
dw = torch.remainder(d + 180.0, 360.0) - 180.0
|
||||||
|
return torch.where(self.cont, a + dw, b)
|
||||||
|
|
||||||
|
def clamp_box(self, q):
|
||||||
|
return torch.where(self.cont, q, torch.maximum(torch.minimum(q, self.hi), self.lo))
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""显存预算(2026-10-18 本机实测后加)。
|
||||||
|
用户定的预算:这台 3090 只有 19–20 GB 能给训练用——Windows + 4K 桌面常驻就占掉 4–5 GB(空载 nvidia-smi 4.4 GB)。
|
||||||
|
limit() 给本进程设硬帽(VRAM_FRAC,默认 0.78 ≈ 18.7 GiB)。超限时在自己的反向里早失败,
|
||||||
|
而不是把桌面挤到没有显存(整机卡顿、别的应用崩),这也是 APG batch=128 原先 OOM 时把显存顶到 23.6 GB 的原因。
|
||||||
|
peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对预算,不用再去猜。
|
||||||
|
实测参考(d=192 同结构、1 步即到位):预训练 batch=1024 → 2.76;APG batch=48/plans=150 → 13.08、batch=128 → OOM。"""
|
||||||
|
import os
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
def limit():
|
||||||
|
if not torch.cuda.is_available(): return False
|
||||||
|
f = float(os.environ.get('VRAM_FRAC', '0.78'))
|
||||||
|
tot = torch.cuda.get_device_properties(0).total_memory / 2**30
|
||||||
|
torch.cuda.set_per_process_memory_fraction(f)
|
||||||
|
print('显存上限:本进程 %.1f GiB(整卡 %.1f GiB × %.2f,其余留给桌面)' % (f * tot, tot, f), flush=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def peak_gib():
|
||||||
|
return round(torch.cuda.max_memory_allocated() / 2**30, 2) if torch.cuda.is_available() else None
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"apg": {
|
||||||
|
"batch": 48, "lr": 1e-4, "steps": 8000, "grad_clip": 1.0, "amp": "bf16",
|
||||||
|
"plans": 60, "plans_max": 150, "seg_plans": 10,
|
||||||
|
"_vram": "batch 由 128 降到 48:2026-10-18 本机实测(d=192 同结构、每进程 19.2 GB 硬帽)batch=128 无论 plans 取 60 还是 150 都 OOM(崩前已分配 17.85 GiB);batch=48/plans=150 峰值 13.08 GiB、plans=60 为 7.67 GiB,加 Windows+4K 桌面常驻 4.4 GB 仍在 19–20 GB 预算内。batch=64/plans=150 要 17.66 GiB(不取)。展开长度 plans 没动——降 plans 会把长任务的绕出段截掉。",
|
||||||
|
"_steps": "2026-10-18 第八轮:batch 128→48 后 steps 3000→8000,保持总任务量(3000×128 ≈ 8000×48)不变;每步展开代价按 batch 线性降,总机时与原估同量级(约 4 h),学习率不动。",
|
||||||
|
"margin_train_mm": 3.0,
|
||||||
|
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
|
||||||
|
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
|
||||||
|
"ckpt_min": 10, "eval_every": 500, "eval_tasks": 512
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
|
||||||
|
"train": {
|
||||||
|
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"model": {"d": 128, "heads": 8, "ff": 512, "enc": 4, "dec": 4},
|
||||||
|
"train": {
|
||||||
|
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"apg": {
|
||||||
|
"batch": 8, "lr": 0.0001, "steps": 10, "grad_clip": 1.0, "amp": "bf16",
|
||||||
|
"plans": 20, "plans_max": 30, "seg_plans": 5,
|
||||||
|
"margin_train_mm": 3.0,
|
||||||
|
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
|
||||||
|
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
|
||||||
|
"ckpt_min": 10, "eval_every": 1000, "eval_tasks": 4
|
||||||
|
},
|
||||||
|
"_note": "冒烟与自检专用(bash ml/pipeline.sh selftest / smoke):只验证流程能跑通,数值无意义。apg 段必须有:selftest.sh 第 6 项原先不带 --config,走的是正式 apg.json 的 batch=128,本机实测第一次反向就 OOM;改成正式值 48 也要约 13 GiB,自检不必吃这么多。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4, "head": "wta", "K": 8},
|
||||||
|
"train": {
|
||||||
|
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@echo off
|
||||||
|
rem 训练控制台一键启动(Windows):双击本文件 → 自动打开 http://localhost:8100 ,在网页上点「开始训练」。
|
||||||
|
rem 需要:带 torch 的 Python(与训练同一个环境)和 Git for Windows(提供 Git Bash)。
|
||||||
|
rem Python 选择顺序:环境变量 PYTHON → 当前 PATH 里的 python。判据是能 import torch(不是 where python:
|
||||||
|
rem WindowsApps 里的假 python 也能被 where 找到,旧写法会"看起来正常、到训练那步才炸",审查 N-10)。
|
||||||
|
rem 关闭本窗口只关控制台;已开始的训练继续跑,重新双击即可接着看、可停止。
|
||||||
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0\.."
|
||||||
|
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
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo 当前 Python(%PYTHON%)不能 import torch。
|
||||||
|
echo 请先在 Anaconda Prompt / 终端里激活训练环境后运行 ml\console.bat,或设置环境变量 PYTHON 为训练环境的 python.exe 完整路径。
|
||||||
|
pause & exit /b 1
|
||||||
|
)
|
||||||
|
"%PYTHON%" -m ml.monitor --open %*
|
||||||
|
pause
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 训练控制台一键启动(Linux / WSL / Git Bash):bash ml/console.sh → 打开 http://localhost:8100 ,在网页上点「开始训练」。
|
||||||
|
# Python 选择与 console.bat 一致:PYTHON → python → python3,判据是能 import torch(审查 N-10)。
|
||||||
|
# 训练进程独立会话运行:关掉终端只关控制台,训练继续;重新运行本脚本即可接着看、可停止。
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
for c in "${PYTHON:-}" python python3; do
|
||||||
|
[ -n "$c" ] && command -v "$c" >/dev/null 2>&1 && "$c" -c "import torch" 2>/dev/null && { PY=$c; break; }
|
||||||
|
done
|
||||||
|
[ -n "${PY:-}" ] || { echo "找不到能 import torch 的 Python:先激活训练环境,或设 PYTHON=..."; exit 1; }
|
||||||
|
export PYTHON=$PY
|
||||||
|
export PYTHONUTF8=1 # 2026-10-18 第六轮:文件读写统一 UTF-8
|
||||||
|
exec "$PY" -m ml.monitor --open "$@"
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
"""样本加载:ml/data/samples/*.{X,Y,V,SG,TID}.npy + *.tasks.jsonl(ml/prep/build_dataset.cjs 产出)
|
||||||
|
python -m ml.data --index ml/data/samples # 汇总样本/任务数,写 index.json
|
||||||
|
全部载入内存(200 万样本约 1.5 GB),按任务 split 字段划分 train / val。"""
|
||||||
|
import argparse, glob, json, os
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
def load_tasks(dirs):
|
||||||
|
tasks = []
|
||||||
|
for d in dirs:
|
||||||
|
for f in sorted(glob.glob(os.path.join(d, '*.tasks.jsonl'))):
|
||||||
|
for line in open(f, encoding='utf-8'):
|
||||||
|
if line.strip():
|
||||||
|
tasks.append(json.loads(line))
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
class Samples:
|
||||||
|
def __init__(self, dirs, device='cpu', split=None):
|
||||||
|
if isinstance(dirs, str): dirs = [dirs]
|
||||||
|
xs, ys, vs, ss, ts = [], [], [], [], []
|
||||||
|
for d in dirs:
|
||||||
|
for fx in sorted(glob.glob(os.path.join(d, '*.X.npy'))):
|
||||||
|
b = fx[:-6]
|
||||||
|
xs.append(np.load(fx)); ys.append(np.load(b + '.Y.npy')); vs.append(np.load(b + '.V.npy'))
|
||||||
|
ss.append(np.load(b + '.SG.npy')); ts.append(np.load(b + '.TID.npy'))
|
||||||
|
if not xs: raise SystemExit(f'没有样本:{dirs}(先跑 ml/prep/run_prep.sh)')
|
||||||
|
self.tasks = load_tasks(dirs)
|
||||||
|
val_tid = {t['tid'] for t in self.tasks if t['split'] == 'val'}
|
||||||
|
X, Y, V, SG, TID = map(np.concatenate, (xs, ys, vs, ss, ts))
|
||||||
|
is_val = np.isin(TID, np.fromiter(val_tid, dtype=np.int64)) if val_tid else np.zeros(len(TID), bool)
|
||||||
|
sel = {'train': ~is_val, 'val': is_val, None: np.ones(len(TID), bool)}[split]
|
||||||
|
t = lambda a: torch.from_numpy(np.ascontiguousarray(a[sel])).to(device)
|
||||||
|
self.X, self.Y, self.V, self.SG, self.TID = t(X), t(Y), t(V), t(SG), t(TID)
|
||||||
|
self.n = self.X.shape[0]
|
||||||
|
|
||||||
|
def batch(self, bs, gen=None):
|
||||||
|
i = torch.randint(0, self.n, (bs,), device=self.X.device, generator=gen)
|
||||||
|
return self.X[i], self.Y[i], self.V[i], self.SG[i]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(); ap.add_argument('--index', default='ml/data/samples'); a = ap.parse_args()
|
||||||
|
tr, va = Samples(a.index, split='train'), Samples(a.index, split='val')
|
||||||
|
tasks = load_tasks([a.index])
|
||||||
|
cats = {}
|
||||||
|
for t in tasks: cats[t['cat'] + '/' + t['mode']] = cats.get(t['cat'] + '/' + t['mode'], 0) + 1
|
||||||
|
info = {'train_samples': tr.n, 'val_samples': va.n, 'tasks': len(tasks), 'val_tasks': sum(t['split'] == 'val' for t in tasks), 'by_cat_mode': cats}
|
||||||
|
json.dump(info, open(os.path.join(a.index, 'index.json'), 'w'), indent=1)
|
||||||
|
print(json.dumps(info, indent=1))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""验收汇总(方案 §9.2 + §10.1 R2/R7/R8/R10):把各阶段产物里的判据集中核对,输出 ml/runs/acceptance.json。
|
||||||
|
python -m ml.eval.acceptance --ckpt ml/runs/current.pt --samples ml/data/samples --out ml/runs/acceptance.json
|
||||||
|
自己跑的一项:held-out(val 划分)全部任务闭环仿真(开仿真安全层)——可达任务到达率 ≥ 98%、D 类停靠距离 ≤ 老师 + 5 mm、起步失败 = 0。
|
||||||
|
其余项读已有文件(缺文件记为 missing,不算通过):
|
||||||
|
ml/runs/policy_bench.json 部署形态:各集合穿模帧 = 0;near 30/30(pos、pose);route 全部到达;nc1000 0 失败;刹停率 < 2%;R8 恢复 ≥ 95%、活锁 = 0
|
||||||
|
ml/data/r2.json R2 近邻标签 std < 10% 步长
|
||||||
|
ml/runs/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(≈ 60000 步 ≤ 3.5 h)
|
||||||
|
ml/runs/m5_gate_*.json M5 头门禁
|
||||||
|
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
|
||||||
|
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 探针
|
||||||
|
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。
|
||||||
|
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 torch
|
||||||
|
|
||||||
|
|
||||||
|
def jload(p):
|
||||||
|
try: return json.load(open(p, encoding='utf-8'))
|
||||||
|
except Exception: return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.model.policy import build
|
||||||
|
from ml.data import load_tasks
|
||||||
|
from ml.sim.closed_loop import evaluate
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||||
|
ap.add_argument('--out', default='ml/runs/acceptance.json')
|
||||||
|
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||||
|
a = ap.parse_args()
|
||||||
|
items = []
|
||||||
|
def add(name, value, ok, rule):
|
||||||
|
items.append({'item': name, 'value': value, 'pass': (None if ok is None else bool(ok)), 'rule': rule})
|
||||||
|
|
||||||
|
# 1) held-out 闭环(仿真安全层)
|
||||||
|
ck = torch.load(a.ckpt, map_location=a.device)
|
||||||
|
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
|
||||||
|
kin = Kin(device=a.device)
|
||||||
|
val = [t for t in load_tasks([a.samples]) if t['split'] == 'val']
|
||||||
|
reach = [t for t in val if t.get('reached') and t['cat'] != 'D']
|
||||||
|
dock = [t for t in val if t['cat'] == 'D']
|
||||||
|
if reach:
|
||||||
|
r, _ = evaluate(model, kin, reach, a.device, safety=True)
|
||||||
|
add('held-out 可达任务到达率', r['success'], r['success'] >= 0.98, '≥ 0.98(仿真安全层)')
|
||||||
|
add('起步失败率(R2)', r['start_fail'], r['start_fail'] <= 0.002, '≤ 0.2%(约 2500 题容许 ≤5 题,0 题判据过脆)')
|
||||||
|
else:
|
||||||
|
add('held-out 可达任务到达率', None, False, 'val 中无可达任务')
|
||||||
|
if dock:
|
||||||
|
_, pers = evaluate(model, kin, dock, a.device, safety=True)
|
||||||
|
err = torch.cat([p['err'] for _, p in pers])
|
||||||
|
ref = torch.tensor([t.get('err') if t.get('err') is not None else float('inf') for t in dock])
|
||||||
|
rate = (err <= ref + 5).float().mean().item()
|
||||||
|
add('D 类停靠距离 ≤ 老师 + 5 mm 的比例', rate, rate >= 0.98, '≥ 0.98')
|
||||||
|
|
||||||
|
# 2) 部署形态
|
||||||
|
mm = jload('webapp/models/traj_policy.meta.json')
|
||||||
|
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')
|
||||||
|
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('planWarmP99'), rr.get('starve_rate'), rr.get('cert_time_rate')] if r9 else 'missing', bool(r9 and r9.get('pass')),
|
||||||
|
'< 45 ms、< 1%、< 5%(训练好的模型、30 道贴柱题,ml.eval.r4_probe --viewer;冷启动 P99 / 刹停率 / tickGapMax 见 r9_browser.json)')
|
||||||
|
pb = jload('ml/runs/policy_bench.json')
|
||||||
|
if pb is None:
|
||||||
|
add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench')
|
||||||
|
else:
|
||||||
|
sets = {k: v for k, v in pb.items() if isinstance(v, dict) and 'n' in v}
|
||||||
|
cf = sum(v['collisionFrames'] for v in sets.values())
|
||||||
|
add('部署形态穿模帧(全部集合)', cf, cf == 0, '= 0')
|
||||||
|
for k in ('near_column_pos', 'near_column_pose', 'route40', 'near_column_1000'):
|
||||||
|
if k in sets: add(f'{k} 通过', f"{sets[k]['ok']}/{sets[k]['n']}", sets[k]['ok'] == sets[k]['n'], '全部通过')
|
||||||
|
else: add(f'{k} 通过', 'missing', False, '全部通过')
|
||||||
|
plans = sum(v.get('plans', 0) for v in sets.values()); br = sum(v.get('brakes', 0) for v in sets.values())
|
||||||
|
add('刹停发生率(刹停 / 推理)', br / plans if plans else None, (br / plans < 0.02) if plans else False, '< 0.02')
|
||||||
|
r8 = sets.get('r8_adversarial')
|
||||||
|
if r8 and r8.get('requested') and r8['n'] < r8['requested']:
|
||||||
|
add('R8 出题数', f"{r8['n']}/{r8['requested']}", False, '出满要求题数(不足则恢复率分母缩水)')
|
||||||
|
if r8:
|
||||||
|
add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95')
|
||||||
|
add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0')
|
||||||
|
else: add('R8 对抗集', 'missing', False, 'bench 需含 r8')
|
||||||
|
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) 数据 / 训练 / 后训练门禁
|
||||||
|
r2 = jload('ml/data/r2.json')
|
||||||
|
add('R2 近邻标签 std(步长比例)', [r2.get('label_std_mean'), r2.get('label_std_norm')] if r2 else 'missing', None, '参考:< 0.10;按近邻距离归一后看 label_std_norm(见 check_r2)')
|
||||||
|
sps = None
|
||||||
|
for f in sorted(glob.glob('ml/runs/bench_*/log.jsonl')):
|
||||||
|
recs = [json.loads(l) for l in open(f) if '"samples_per_s"' in l]
|
||||||
|
if recs: sps = max(sps or 0, recs[-1]['samples_per_s'])
|
||||||
|
add('R10 训练吞吐(样本/s)', sps, sps is not None and sps >= 5000, '≥ 5000(≈ 60000 步 ≤ 3.5 h)')
|
||||||
|
m5 = [jload(f) for f in sorted(glob.glob('ml/runs/m5_gate_*.json'))]
|
||||||
|
m5 = [m for m in m5 if m]
|
||||||
|
add('M5 头门禁', [{'head': m['head'], 'collision_rate': m['policy_only']['collision_rate']} for m in m5] or 'missing',
|
||||||
|
any(m['pass'] for m in m5), '纯策略贴立柱穿柱率 ≤ 0.02')
|
||||||
|
rnum = lambda p: int((re.findall(r'[\\/]r(\d+)[\\/]gate\.json$', p) or ['0'])[0])
|
||||||
|
gates = sorted(glob.glob('ml/runs/mine/r*/gate.json'), key=rnum)
|
||||||
|
if gates:
|
||||||
|
gj = jload(gates[-1])
|
||||||
|
add('R7 左右比例变化(pp)/ 行程比', [gj.get('R7_left_share_shift_pp'), gj.get('R7_travel_median_ratio')], gj.get('R7_ok'), '≤ 5 pp 且 ≤ 1.05')
|
||||||
|
else: add('R7 门禁', 'missing', False, '先跑 mine')
|
||||||
|
|
||||||
|
hard = [i for i in items if i['pass'] is not None]
|
||||||
|
out = {'ckpt': a.ckpt, 'all_pass': all(i['pass'] for i in hard), 'failed': [i['item'] for i in hard if not i['pass']], 'items': items}
|
||||||
|
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)
|
||||||
|
for i in items:
|
||||||
|
mark = {True: '通过', False: '未过', None: '参考'}[i['pass']]
|
||||||
|
print(f"[{mark}] {i['item']}: {i['value']} ({i['rule']})")
|
||||||
|
print('全部通过' if out['all_pass'] else '未通过:' + '、'.join(out['failed']))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""对拍:python -m ml.eval.check_parity [--kin ml/assets/kin.json] [--data ml/data/parity.json]
|
||||||
|
检查 PyTorch 版 fk / orient / 组对间隙 / 83 维特征 与 JS 一致,以及间隙梯度与有限差分一致。"""
|
||||||
|
import argparse, json
|
||||||
|
import torch
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.common.features import features, GAP_CAP
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--kin', default=None); ap.add_argument('--data', default='ml/data/parity.json')
|
||||||
|
a = ap.parse_args()
|
||||||
|
kin = Kin(a.kin) if a.kin else Kin(dtype=torch.float64)
|
||||||
|
D = json.load(open(a.data, encoding='utf-8'))
|
||||||
|
assert D['fp'] == kin.fp, f"指纹不符:parity {D['fp']} vs kin {kin.fp}(重跑 export_kin.cjs)"
|
||||||
|
R = D['recs']; f64 = torch.float64
|
||||||
|
q = torch.tensor([r['q'] for r in R], dtype=f64)
|
||||||
|
qp = torch.tensor([r['qPrev'] for r in R], dtype=f64)
|
||||||
|
T = torch.tensor([r['T'] for r in R], dtype=f64)
|
||||||
|
Rt = torch.tensor([r['R'] if r['R'] else [0.] * 9 for r in R], dtype=f64)
|
||||||
|
pre = torch.zeros(len(R), 3, 6, dtype=f64); pl = torch.zeros(len(R), dtype=torch.long)
|
||||||
|
for n, r in enumerate(R):
|
||||||
|
for k, d in enumerate(r['prefix']): pre[n, k] = torch.tensor(d, dtype=f64)
|
||||||
|
pl[n] = len(r['prefix'])
|
||||||
|
sub = torch.tensor([r['sub'] if r['sub'] else [0.] * 6 for r in R], dtype=f64)
|
||||||
|
sf = torch.tensor([1. if r['sub'] else 0. for r in R], dtype=f64)
|
||||||
|
kin = Kin(a.kin, dtype=f64) if a.kin else kin
|
||||||
|
xf = kin.cum_xf(q)
|
||||||
|
e_fk = (kin.fk(q, xf) - torch.tensor([r['fk'] for r in R], dtype=f64)).abs().max().item()
|
||||||
|
e_or = (kin.orient(q, xf).reshape(-1, 9) - torch.tensor([r['orient'] for r in R], dtype=f64)).abs().max().item()
|
||||||
|
G = kin.gaps(q, GAP_CAP, xf)
|
||||||
|
gm_js = torch.tensor([r['gm'] for r in R], dtype=f64)
|
||||||
|
e_gm = (G['gm'] - gm_js.clamp(max=GAP_CAP)).abs().max().item()
|
||||||
|
col_js = gm_js.min(-1).values <= 0; col_py = G['gm'].min(-1).values <= 0
|
||||||
|
f = features(kin, q, qp, T, Rt, pre, pl, sub, sf, gm=G['gm'], xf=xf)
|
||||||
|
e_f = (f - torch.tensor([r['feat'] for r in R], dtype=f64)).abs().max().item()
|
||||||
|
# 梯度:最小间隙对 q 的梯度 vs 有限差分(取 50 个无干涉且间隙 < 20 的样本)
|
||||||
|
sel = ((G['gm'].min(-1).values > 0.5) & (G['gm'].min(-1).values < 20)).nonzero().flatten()[:50]
|
||||||
|
rel = []
|
||||||
|
if len(sel):
|
||||||
|
qs = q[sel].clone().requires_grad_(True)
|
||||||
|
g = kin.gaps(qs, GAP_CAP)['gm'].min(-1).values
|
||||||
|
gr, = torch.autograd.grad(g.sum(), qs)
|
||||||
|
h = 1e-4
|
||||||
|
for j in range(6):
|
||||||
|
dq = torch.zeros_like(qs); dq[:, j] = h
|
||||||
|
fd = (kin.gaps(qs.detach() + dq, GAP_CAP)['gm'].min(-1).values - kin.gaps(qs.detach() - dq, GAP_CAP)['gm'].min(-1).values) / (2 * h)
|
||||||
|
rel.append(((gr[:, j] - fd).abs() / (fd.abs() + 1e-3)).max().item())
|
||||||
|
res = {'n': len(R), 'fk_max_abs_mm': e_fk, 'orient_max_abs': e_or, 'gap_max_abs_mm': e_gm,
|
||||||
|
'collide_bool_mismatch': int((col_js != col_py).sum()), 'feat_max_abs': e_f,
|
||||||
|
'grad_rel_err_max': max(rel) if rel else None}
|
||||||
|
ok = e_fk < 1e-6 and e_or < 1e-9 and e_gm < 1e-4 and res['collide_bool_mismatch'] == 0 and e_f < 1e-4 and (not rel or max(rel) < 1e-3)
|
||||||
|
res['PASS'] = bool(ok)
|
||||||
|
print(json.dumps(res, indent=1))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""方案 R2 数据侧检查:近邻样本的标签是否自洽(时间律不可观测成分会让相同输入对应不同动作)。
|
||||||
|
python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json
|
||||||
|
做法:特征逐维标准化;随机取 Q 个查询样本,在 P 个样本池里找最近邻(排除同一任务,避免同轨迹相邻帧冒充"近邻");
|
||||||
|
取距离最近的 5% 近邻对,比较前 3 步(实际提交的动作)标签:std ≈ RMS(ΔY)/√2,单位为每步最大步长 STEP 的比例。
|
||||||
|
判据:label_std < 0.10(即 < 10% 步长)。起步失败率在闭环仿真结果的 start_fail 字段(acceptance 汇总)。
|
||||||
|
2026-10-18 第五轮(审查 N-2:判据与搜索池耦合):最近 5% 的近邻距离随池大小变,label_std 随之系统偏大/偏小。
|
||||||
|
改为按"近邻距离 → 标签差"做线性回归,取距离 → 0 的截距 label_std_norm 作判据(与池大小无关:池大只是近邻更近、点更多,
|
||||||
|
截距估计的是"同一输入下的固有标签分歧");旧口径 label_std_mean 保留作参考。pipeline 默认只警告,R2_STRICT=1 才拦。
|
||||||
|
自检(第七轮起每次自动做):--pool 50000 与 4 倍池 200000 各测一次,label_std_norm 差 < 0.02 记 pool_check.independent=true。
|
||||||
|
2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 常驻约 1.6 GB(每样本 776 B × 1.7 M + 分块,第八轮按本机实测改),连同 4 倍池自检 CPU 约 5–10 分钟、GPU 约 1 分钟;近邻对为空时输出 pass=null 而不报错。"""
|
||||||
|
import argparse, json, math
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.data import Samples
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
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('--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('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||||
|
a = ap.parse_args()
|
||||||
|
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)
|
||||||
|
P = min(pool, d.n); Q = min(a.queries, d.n)
|
||||||
|
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)
|
||||||
|
Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd
|
||||||
|
tp, tq = d.TID[pi], d.TID[qi]
|
||||||
|
best_d = torch.empty(Q, device=a.device); best_j = torch.empty(Q, dtype=torch.long, device=a.device)
|
||||||
|
for s in range(0, Q, 256):
|
||||||
|
D = torch.cdist(Xq[s:s + 256], Xp)
|
||||||
|
D.masked_fill_(tq[s:s + 256, None] == tp[None], float('inf'))
|
||||||
|
v, j = D.min(1); best_d[s:s + 256] = v; best_j[s:s + 256] = j
|
||||||
|
fin = torch.isfinite(best_d)
|
||||||
|
if fin.sum() < 20:
|
||||||
|
return {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'}
|
||||||
|
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a
|
||||||
|
dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1)
|
||||||
|
half = dd <= torch.quantile(dd, 0.5)
|
||||||
|
Ya, Yb = d.Y[qi][qf[half], :3].float(), d.Y[pi][jj[half], :3].float()
|
||||||
|
sa = (Ya - Yb).pow(2).mean(-1).mean(-1).sqrt() / math.sqrt(2)
|
||||||
|
A = torch.stack([torch.ones_like(dd[half]), dd[half]], 1)
|
||||||
|
coef = torch.linalg.lstsq(A.cpu(), sa.cpu()[:, None]).solution.squeeze(1)
|
||||||
|
std_norm = max(0.0, coef[0].item())
|
||||||
|
thr = torch.quantile(best_d[fin], a.frac)
|
||||||
|
m = fin & (best_d <= thr)
|
||||||
|
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 步平均
|
||||||
|
std = (rms / math.sqrt(2))
|
||||||
|
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_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(),
|
||||||
|
'pass': std_norm < 0.10,
|
||||||
|
'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* 对拍数据:JS 侧 fk / orient / gapMatrix / 特征(ik-policy.js)→ ml/data/parity.json
|
||||||
|
* node ml/eval/dump_parity.cjs [N=2000]
|
||||||
|
* 然后 python -m ml.eval.check_parity 比对 PyTorch 版。 */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const N = +process.argv[2] || 2000;
|
||||||
|
const { S } = require('../../tests/route_bench_setup.cjs')();
|
||||||
|
const POL = require('../../webapp/js/ik-policy.js');
|
||||||
|
const L = S.LIM, O = S.ORDER;
|
||||||
|
let st = 99; const rnd = () => { st = (st * 1664525 + 1013904223) >>> 0; return st / 4294967296; };
|
||||||
|
const rq = () => O.map(id => L[id].lo + rnd() * (L[id].hi - L[id].lo));
|
||||||
|
const recs = [];
|
||||||
|
for (let n = 0; n < N; n++) {
|
||||||
|
const q = rq(), qp = q.map((v, i) => v - (rnd() - 0.5) * 2);
|
||||||
|
const b = rq(); const T = S.fk(b, 0); const pose = rnd() < 0.5; const R = pose ? S.orient(b) : null;
|
||||||
|
const r = Math.floor(rnd() * 4); const prefix = []; for (let k = 0; k < r; k++) prefix.push(q.map(() => (rnd() - 0.5) * 2));
|
||||||
|
const sub = rnd() < 0.5 ? rq() : null;
|
||||||
|
const st0 = { q, qPrev: qp, T, R, prefix, sub };
|
||||||
|
const f = POL.features(S, st0);
|
||||||
|
recs.push({ q, qPrev: qp, T, R, prefix, sub, fk: S.fk(q, 0), orient: S.orient(q), gm: Array.from(S.gapMatrix(q, 40)), feat: Array.from(f) });
|
||||||
|
}
|
||||||
|
const out = path.join(__dirname, '../data/parity.json');
|
||||||
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
||||||
|
fs.writeFileSync(out, JSON.stringify({ fp: S.modelFingerprint(), recs }));
|
||||||
|
console.log('wrote', out, recs.length);
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""方案 R4 自动探针:无头 Edge / Chrome 里测浏览器推理延迟(部署形态:1 行特征 + K=8 条候选)。
|
||||||
|
python -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json
|
||||||
|
做法:本进程起一个带 COOP/COEP 头的静态服务(根目录 = 仓库),逐个配置用无头浏览器打开 webapp/r4_probe.html,
|
||||||
|
页面把 P50/P95/P99 与 20 ms 节拍最大间隔 POST 回来。配置:
|
||||||
|
wasm·默认线程(与查看器同一默认 min(16, ⌊0.6×逻辑核⌋),推理在 Worker)——部署默认口径
|
||||||
|
wasm·4 线程(第四轮实测的旧默认,作对照)
|
||||||
|
wasm·默认线程·不开 Worker(看推理是否卡主线程:tickGapMax)
|
||||||
|
webgpu
|
||||||
|
判据:部署口径 P99 < 15 ms 即通过;wasm 不过而 WebGPU 过,也判通过并记 deploy_ep=webgpu(查看器用 &ep=webgpu)。
|
||||||
|
找不到浏览器 / 页面没回报 → pass=null(流水线警告继续,验收标红)。浏览器路径可用 R4_BROWSER 指定。
|
||||||
|
2026-10-18 第五轮新增(审查第四轮缺口 3:R4 需要人开浏览器看,导致 `all` 天生不完整)。
|
||||||
|
范围说明: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
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
LIMIT_MS = 15.0
|
||||||
|
PLAN_LIMIT_MS = 45.0 # R9:整次规划 P99(60 ms 周期留 15 ms 给渲染与 tick)
|
||||||
|
STARVE_LIMIT = 0.01 # R9:饥饿节拍占比上限(只计运动中,冷启动不计)
|
||||||
|
TIME_LIMIT = 0.05 # R9:认证因墙钟截止判不过的占比上限——防止靠拒绝候选、刹停换来 planP99 达标(第八轮,viewer_ref 随机模型 time 占失败 70%)
|
||||||
|
# 2026-10-18 第八轮:R9 判稳态 planWarmP99(剔除每题首次冷启动规划)。冷启动发生在静止起步,只是起步延迟,单独报 planColdP99 不设闸;
|
||||||
|
# 否则 30 题 = 30 次冷启动,planP99 测的是题数而不是稳态。
|
||||||
|
|
||||||
|
|
||||||
|
def ort_local():
|
||||||
|
# 2026-10-18 第八轮:ort 已入库(webapp/vendor/ort),默认只用本地;本地缺失直接报错,不再静默退回 CDN
|
||||||
|
# (第七轮本机:npm pack 失败 → 退回 CDN → jsDelivr 不通 → 四种配置全是「加载 ort 失败」却显示跑完)。确要用 CDN 设 ORT_LOCAL=0。
|
||||||
|
if os.environ.get('ORT_LOCAL') == '0': return False
|
||||||
|
if not os.path.exists(os.path.join(ROOT, 'webapp', 'vendor', 'ort', 'ort.min.js')):
|
||||||
|
sys.exit('webapp/vendor/ort/ort.min.js 不存在:git pull 拿入库的 ort,或 bash ml/prep/vendor_ort.sh;确要用 CDN 设 ORT_LOCAL=0')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def find_browser():
|
||||||
|
if os.environ.get('R4_BROWSER'): return os.environ['R4_BROWSER']
|
||||||
|
cands = []
|
||||||
|
for base in (os.environ.get('ProgramFiles(x86)'), os.environ.get('ProgramFiles'), os.environ.get('LOCALAPPDATA')):
|
||||||
|
if base:
|
||||||
|
cands += [os.path.join(base, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
||||||
|
os.path.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe')]
|
||||||
|
for n in ('microsoft-edge', 'microsoft-edge-stable', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'):
|
||||||
|
p = shutil.which(n)
|
||||||
|
if p: cands.append(p)
|
||||||
|
return next((c for c in cands if c and os.path.exists(c)), None)
|
||||||
|
|
||||||
|
|
||||||
|
class Srv:
|
||||||
|
def __init__(self):
|
||||||
|
self.result, self.ev = None, threading.Event()
|
||||||
|
me = self
|
||||||
|
|
||||||
|
class H(http.server.SimpleHTTPRequestHandler):
|
||||||
|
extensions_map = {**http.server.SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript'}
|
||||||
|
|
||||||
|
def __init__(s, *a, **k): super().__init__(*a, directory=ROOT, **k)
|
||||||
|
|
||||||
|
def end_headers(s):
|
||||||
|
s.send_header('Cross-Origin-Opener-Policy', 'same-origin'); s.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||||
|
s.send_header('Cross-Origin-Resource-Policy', 'cross-origin'); s.send_header('Cache-Control', 'no-store')
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
def do_POST(s):
|
||||||
|
n = int(s.headers.get('Content-Length') or 0)
|
||||||
|
try: me.result = json.loads(s.rfile.read(n) or b'{}')
|
||||||
|
except Exception as e: me.result = {'error': 'bad json: %s' % e}
|
||||||
|
me.ev.set(); s.send_response(200); s.end_headers(); s.wfile.write(b'ok')
|
||||||
|
|
||||||
|
def log_message(s, *a): pass
|
||||||
|
|
||||||
|
sk = socket.socket(); sk.bind(('127.0.0.1', 0)); self.port = sk.getsockname()[1]; sk.close()
|
||||||
|
self.httpd = http.server.ThreadingHTTPServer(('127.0.0.1', self.port), H)
|
||||||
|
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def kill_tree(p):
|
||||||
|
if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(p.pid)], capture_output=True)
|
||||||
|
else:
|
||||||
|
try: p.terminate(); p.wait(5)
|
||||||
|
except Exception: p.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def open_page(srv, browser, url, timeout_s, headed=False):
|
||||||
|
srv.result = None; srv.ev.clear()
|
||||||
|
prof = tempfile.mkdtemp(prefix='r4prof_')
|
||||||
|
args = [browser] + ([] if headed else ['--headless=new']) + [f'--user-data-dir={prof}', '--no-first-run', '--no-default-browser-check',
|
||||||
|
'--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)
|
||||||
|
got = srv.ev.wait(timeout_s)
|
||||||
|
kill_tree(p); shutil.rmtree(prof, ignore_errors=True)
|
||||||
|
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}')
|
||||||
|
print(json.dumps(r, ensure_ascii=False), flush=True)
|
||||||
|
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)
|
||||||
|
pl = max(1, r.get('plans') or 1); why = r.get('certFailWhy') or {}
|
||||||
|
r['cert_time_rate'] = round((why.get('time') or 0) / pl, 4); r['brake_rate'] = round((r.get('brakes') or 0) / pl, 4)
|
||||||
|
w = r.get('planWarmP99') if r.get('planWarmP99') is not None else r['planP99']
|
||||||
|
r['r9_pass'] = w < PLAN_LIMIT_MS and sr < STARVE_LIMIT and r['cert_time_rate'] < TIME_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():
|
||||||
|
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('--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()
|
||||||
|
if a.viewer: return main_viewer(a)
|
||||||
|
out = {'rule': f'部署口径 batch=8 P99 < {LIMIT_MS:g} ms', 'model': a.model, 'pass': None}
|
||||||
|
br = find_browser()
|
||||||
|
if not br:
|
||||||
|
out['note'] = '没找到 Edge / Chrome(可设 R4_BROWSER=浏览器路径)'; print(out['note'])
|
||||||
|
else:
|
||||||
|
out['browser'] = br
|
||||||
|
srv = Srv()
|
||||||
|
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),
|
||||||
|
one(srv, br, mu, 'wasm', 'auto', 0, a.n, a.timeout), one(srv, br, mu, 'webgpu', 'auto', 0, a.n, a.timeout)]
|
||||||
|
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]
|
||||||
|
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:
|
||||||
|
out['note'] = '页面没有给出数字(见 runs[].error)'
|
||||||
|
elif ok(dep):
|
||||||
|
out['pass'] = True; out['deploy'] = dep; out['deploy_ep'] = 'wasm'
|
||||||
|
elif ok(gpu):
|
||||||
|
out['pass'] = True; out['deploy'] = gpu; out['deploy_ep'] = 'webgpu'
|
||||||
|
out['note'] = 'wasm 不达标而 WebGPU 达标:查看器部署用 &ep=webgpu'
|
||||||
|
else:
|
||||||
|
out['pass'] = False; out['deploy'] = dep if dep.get('p99') is not None else gpu
|
||||||
|
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)
|
||||||
|
d = out.get('deploy') or {}
|
||||||
|
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:查看器稳态规划 planWarmP99 < {PLAN_LIMIT_MS:g} ms、运动中饥饿节拍 < {STARVE_LIMIT:.0%}、认证超时占比 < {TIME_LIMIT:.0%}(planColdP99 / brake_rate / tickGapMax 只报不判)', '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('planWarmP99')} ms(冷启动 {r.get('planColdP99')};推理 {r.get('inferP99')} / 安全层 {r.get('safetyP99')}),饥饿 {r.get('starve_rate')},认证超时 {r.get('cert_time_rate')},刹停 {r.get('brake_rate')},tickGapMax {r.get('tickGapMax')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""导出部署包(方案 §8.4):
|
||||||
|
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx [--fp16]
|
||||||
|
输出 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 对拍。
|
||||||
|
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 / 验收都会拒绝把它当策略用。"""
|
||||||
|
import argparse, json, os
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.common.features import F_DIM, H, FEAT_VER, STEP
|
||||||
|
from ml.model.policy import build
|
||||||
|
|
||||||
|
|
||||||
|
class Wrap(torch.nn.Module):
|
||||||
|
def __init__(self, m):
|
||||||
|
super().__init__(); self.m = m
|
||||||
|
|
||||||
|
def forward(self, feat, z):
|
||||||
|
return self.m.sample_shared(feat, z)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--ckpt'); ap.add_argument('--random', metavar='CFG', help='R4 测延迟:按配置随机初始化导出,不需要训练'); ap.add_argument('--out', default='webapp/models/traj_policy.onnx')
|
||||||
|
ap.add_argument('--fp16', action='store_true'); ap.add_argument('--opset', type=int, default=17)
|
||||||
|
a = ap.parse_args()
|
||||||
|
if a.random: # 2026-10-18:R4(浏览器 batch=8 P99)提前到训练前测,延迟只取决于结构不取决于权重
|
||||||
|
torch.manual_seed(0); ck = {'cfg': json.load(open(a.random)), 'random_init': True}
|
||||||
|
m = build(ck['cfg']); m.eval()
|
||||||
|
elif a.ckpt:
|
||||||
|
ck = torch.load(a.ckpt, map_location='cpu')
|
||||||
|
m = build(ck['cfg']); m.load_state_dict(ck['model']); m.eval()
|
||||||
|
else: raise SystemExit('需要 --ckpt 或 --random <配置>')
|
||||||
|
kin = Kin()
|
||||||
|
if ck.get('fp') and ck['fp'] != kin.fp: raise SystemExit(f"checkpoint 指纹 {ck['fp']} ≠ 当前碰撞模型 {kin.fp}:须重训")
|
||||||
|
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||||
|
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'],
|
||||||
|
dynamic_axes={'feat': {0: 'B'}, 'z': {0: 'K'}, 'act': {0: 'K'}, 'value': {0: 'K'}, 'sub': {0: 'K'}},
|
||||||
|
opset_version=a.opset, dynamo=False)
|
||||||
|
if a.fp16:
|
||||||
|
from onnxconverter_common import float16
|
||||||
|
import onnx
|
||||||
|
mo = float16.convert_float_to_float16(onnx.load(a.out), keep_io_types=True)
|
||||||
|
onnx.save(mo, a.out)
|
||||||
|
import onnxruntime as ort
|
||||||
|
s = ort.InferenceSession(a.out, providers=['CPUExecutionProvider'])
|
||||||
|
o = s.run(None, {'feat': feat.numpy(), 'z': z.numpy()})
|
||||||
|
with torch.no_grad():
|
||||||
|
t = m.sample_shared(feat, z)
|
||||||
|
t8 = m.sample(feat.expand(8, -1), z) # 与旧的"复制 8 行"口径逐元素一致
|
||||||
|
err8 = max(float((t[i] - t8[i]).abs().max()) for i in range(3))
|
||||||
|
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))
|
||||||
|
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,
|
||||||
|
'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)
|
||||||
|
print(json.dumps({'onnx': a.out, 'MB': os.path.getsize(a.out) / 1e6, 'max_abs_err_vs_torch': err}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# ml/gen — 离线老师数据
|
||||||
|
|
||||||
|
完整流程与命令见 `ml/README.md`(第 3、4 步)。这里的脚本:
|
||||||
|
- `consistency.cjs`:老师一致性门禁(目标 ±5 mm 邻居的同伦类一致率)
|
||||||
|
- `gen_dataset.cjs`:采样 → 确定性老师 → 距离下界认证 → 时间律;`--consensus k` 共识过滤;`--tasks f.jsonl` 按给定任务出解(挖错 E2)
|
||||||
|
- `run_parallel.sh`:多进程生成 + 统计
|
||||||
|
- `stats.cjs`:汇总 `stats.json`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/* 距离下界认证:实现放在 webapp/js/ik-policy.js(部署安全层与离线过滤共用同一份代码)。 */
|
||||||
|
'use strict';
|
||||||
|
module.exports = (S, opt) => require('../../webapp/js/ik-policy.js').makeCert(S, opt);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* M1 门禁:老师一致性(方案 §4.7 / R1)
|
||||||
|
* node ml/gen/consistency.cjs [N=100] [seed=4242]
|
||||||
|
* 取 B/C 类(需要绕行)任务,起点不变、目标位置扰动 ±5 mm 造 4 个邻居,与原任务老师路径比较:
|
||||||
|
* 两条路径按巡航时间 τ 各重采样 32 点,逐点取各轴最大差;最大偏差 < 30° 视为同一绕行同伦类。
|
||||||
|
* 门禁:一致率 ≥ 90%。另报告同一任务重复求解的确定性。 */
|
||||||
|
'use strict';
|
||||||
|
const N = +process.argv[2] || 100, SEED = +process.argv[3] || 4242;
|
||||||
|
const { S, ArmIK } = require('../../tests/route_bench_setup.cjs')();
|
||||||
|
const sm = require('./sampler.cjs')(S, SEED);
|
||||||
|
const teach = require('./teacher.cjs')(S, ArmIK);
|
||||||
|
const PD = require('./pathdev.cjs');
|
||||||
|
const fz = (r, t) => (t.mode === 'pos' && r.ptsJ6Frozen) ? r.ptsJ6Frozen : r.pts; // 与 gen 同口径:pos 任务冻结 J6(gen 里认证不过才回退)
|
||||||
|
const { VMAX } = require('./timelaw.cjs');
|
||||||
|
function resample(P, n) {
|
||||||
|
const seg = []; for (let i = 1; i < P.length; i++) { let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(P[i][j] - P[i - 1][j]) / VMAX[j]); seg.push(m); }
|
||||||
|
const tot = seg.reduce((a, b) => a + b, 0) || 1; const out = [];
|
||||||
|
for (let k = 0; k < n; k++) { let tau = tot * k / (n - 1), i = 0; while (i < seg.length - 1 && tau > seg[i]) { tau -= seg[i]; i++; }
|
||||||
|
const u = seg[i] > 0 ? Math.min(1, tau / seg[i]) : 1; out.push(P[i].map((v, j) => v + (P[i + 1][j] - v) * u)); }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const devAx = (A, B) => { const a = resample(A, 32), b = resample(B, 32); const m = [0, 0, 0, 0, 0, 0]; for (let k = 0; k < 32; k++) for (let j = 0; j < 6; j++) m[j] = Math.max(m[j], Math.abs(a[k][j] - b[k][j])); return m; };
|
||||||
|
const dev = (A, B) => Math.max(...devAx(A, B));
|
||||||
|
const axisHist = [0, 0, 0, 0, 0, 0]; let sameNoJ6 = 0;
|
||||||
|
let tasks = 0, pairs = 0, same = 0, detSame = 0, detN = 0; const devs = [], byCat = {};
|
||||||
|
while (tasks < N) {
|
||||||
|
const t = sm.sample(); if (t.cat !== 'B' && t.cat !== 'C') continue;
|
||||||
|
const base = teach(t); if (!base.ok || !base.reached) continue; base.pts = fz(base, t);
|
||||||
|
tasks++;
|
||||||
|
const again = teach(t); if (again.ok) { detN++; if (dev(base.pts, fz(again, t)) < 1e-6) detSame++; }
|
||||||
|
for (let k = 0; k < 4; k++) {
|
||||||
|
const u = [sm.rnd() - 0.5, sm.rnd() - 0.5, sm.rnd() - 0.5], n = Math.hypot(...u);
|
||||||
|
const T2 = t.T.map((v, i) => v + 5 * u[i] / n);
|
||||||
|
const r = teach(Object.assign({}, t, { T: T2 }));
|
||||||
|
if (!r.ok || !r.reached) continue;
|
||||||
|
const da = devAx(base.pts, fz(r, t)), d = Math.max(...da); devs.push(d); pairs++;
|
||||||
|
if (PD.sameClass(base.pts, fz(r, t), t.mode)) sameNoJ6++;
|
||||||
|
if (d >= 30) axisHist[da.indexOf(d)]++;
|
||||||
|
const c = byCat[t.cat] = byCat[t.cat] || { pairs: 0, same: 0 }; c.pairs++;
|
||||||
|
if (d < 30) { same++; c.same++; }
|
||||||
|
}
|
||||||
|
if (tasks % 10 === 0) process.stderr.write(`${tasks}/${N}\n`);
|
||||||
|
}
|
||||||
|
devs.sort((a, b) => a - b);
|
||||||
|
console.log(JSON.stringify({ tasks, pairs, consistentRate: +(same / pairs).toFixed(3), gate: same / pairs >= 0.9 ? 'PASS' : 'FAIL',
|
||||||
|
devP50: +devs[devs.length >> 1].toFixed(1), devP90: +devs[Math.floor(devs.length * 0.9)].toFixed(1),
|
||||||
|
byCat: Object.fromEntries(Object.entries(byCat).map(([k, v]) => [k, +(v.same / v.pairs).toFixed(3)])),
|
||||||
|
consistentByGateRule: +(sameNoJ6 / pairs).toFixed(3), gateByRule: sameNoJ6 / pairs >= 0.9 ? 'PASS' : 'FAIL', worstAxisOfInconsistent: axisHist, deterministic: detSame + '/' + detN }));
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/* M1 数据生成器(2026-10-17,方案 §4)
|
||||||
|
* node ml/gen/gen_dataset.cjs --n 500 --seed 1 --out ml/data/shard_0001.jsonl [--consensus 4]
|
||||||
|
* --consensus k:邻域共识过滤。起点不变、目标位置各扰动 5 mm 造 k 个邻居跑老师,原任务路径若与多数邻居
|
||||||
|
* 不在同一绕行类(pathdev.sameClass),整条样本 keep=false(why=consensus)。成本约 (1+k) 倍。
|
||||||
|
* 每行一条任务:采样 → 老师 → 距离下界认证过滤 → 局部时间律(50 Hz)→ 写出。
|
||||||
|
* 认证不过的样本也写出(keep=false + 原因),供 stats 统计丢弃率;训练只用 keep=true。
|
||||||
|
* 2026-10-18 断点续写:第 i 条任务的随机数只由 (seed, i) 决定(sampler.reseed);输出文件已存在时保留其中完整的行、
|
||||||
|
* 截掉写了一半的最后一行,从下一条接着写 —— 生成几小时被中断,重跑只补剩下的,不重来、不重复。 */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const args = {}; for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
||||||
|
const N = +args.n || 100, SEED = +args.seed || 1, OUT = args.out || 'ml/data/shard_' + String(SEED).padStart(4, '0') + '.jsonl';
|
||||||
|
const { S, ArmIK } = require('../../tests/route_bench_setup.cjs')();
|
||||||
|
const sampler = require('./sampler.cjs')(S, SEED * 7919 + 13);
|
||||||
|
const teach = require('./teacher.cjs')(S, ArmIK);
|
||||||
|
const { sameClass } = require('./pathdev.cjs');
|
||||||
|
const K_CONS = +args.consensus || 0;
|
||||||
|
/* --tasks file.jsonl:按给定任务(q0/T/R/mode/cat)请老师,不采样(挖错循环 E2 补路径用) */
|
||||||
|
const GIVEN = args.tasks ? fs.readFileSync(args.tasks, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)) : null;
|
||||||
|
const NT = GIVEN ? GIVEN.length : N;
|
||||||
|
const cert = require('./cert.cjs')(S, {});
|
||||||
|
const { timeParam } = require('./timelaw.cjs');
|
||||||
|
const fp = S.modelFingerprint ? S.modelFingerprint() : null;
|
||||||
|
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||||
|
let done = 0;
|
||||||
|
if (fs.existsSync(OUT)) {
|
||||||
|
const keep = [];
|
||||||
|
for (const l of fs.readFileSync(OUT, 'utf8').split('\n')) { if (!l) continue; try { JSON.parse(l); keep.push(l); } catch (e) { break; } }
|
||||||
|
done = Math.min(keep.length, NT);
|
||||||
|
fs.writeFileSync(OUT, keep.slice(0, done).map(l => l + '\n').join(''));
|
||||||
|
if (done) process.stderr.write(`[seed ${SEED}] 续写:已有 ${done}/${NT} 条,从第 ${done + 1} 条继续\n`);
|
||||||
|
}
|
||||||
|
const fd = fs.openSync(OUT, 'a');
|
||||||
|
const taskSeed = i => (Math.imul(SEED, 2654435761) ^ Math.imul(i + 1, 40503) ^ 0x9e3779b9) >>> 0;
|
||||||
|
const r3 = v => Math.round(v * 1000) / 1000;
|
||||||
|
const t00 = performance.now();
|
||||||
|
for (let i = done; i < NT; i++) {
|
||||||
|
sampler.reseed(taskSeed(i));
|
||||||
|
const task = GIVEN ? Object.assign({ cat: 'E2' }, GIVEN[i], { mode: GIVEN[i].R ? 'pose' : 'pos' }) : sampler.sample();
|
||||||
|
const t = teach(task);
|
||||||
|
const rec = { id: 's' + String(SEED).padStart(4, '0') + '_' + String(i).padStart(6, '0'), cat: task.cat, mode: task.mode,
|
||||||
|
q0: task.q0.map(r3), T: task.T.map(r3), R: task.R, fp };
|
||||||
|
if (!t.ok) { rec.keep = false; rec.why = t.why; fs.writeSync(fd, JSON.stringify(rec) + '\n'); continue; }
|
||||||
|
const c0 = performance.now();
|
||||||
|
let c = null;
|
||||||
|
if (t.ptsJ6Frozen) { const cf = cert.path(t.ptsJ6Frozen); if (cf.ok) { t.pts = t.ptsJ6Frozen; t.travel = t.travelOf(t.pts); c = cf; rec.j6Frozen = true; } }
|
||||||
|
if (!c) c = cert.path(t.pts);
|
||||||
|
if (task.cat === 'D' && t.reached) rec.cat = 'Dr'; // "不可达"采样其实可达:单独记类,不当停靠样本
|
||||||
|
Object.assign(rec, { path: t.pts.map(q => q.map(r3)), reached: t.reached, err: r3(t.err), oerr: r3(t.oerr), travel_deg: r3(t.travel),
|
||||||
|
plan_ms: Math.round(t.ms), blocked: t.blocked, poseRelaxed: t.poseRelaxed, detour: t.detour,
|
||||||
|
cert: { ok: c.ok, queries: c.queries, minGap: r3(c.minGap), ms: Math.round(performance.now() - c0), why: c.why || null, failAt: c.failAt == null ? null : c.failAt } });
|
||||||
|
/* 训练可用:认证通过;D 类允许未到达(停靠),其余类必须到达;poseRelaxed 的 pose 样本降为 pos */
|
||||||
|
rec.keep = c.ok && (t.reached || task.cat === 'D');
|
||||||
|
if (rec.keep && K_CONS > 0 && t.reached) {
|
||||||
|
let agree = 0, valid = 0;
|
||||||
|
for (let k = 0; k < K_CONS; k++) {
|
||||||
|
const u = [sampler.rnd() - 0.5, sampler.rnd() - 0.5, sampler.rnd() - 0.5], n = Math.hypot(u[0], u[1], u[2]);
|
||||||
|
const nb = teach(Object.assign({}, task, { T: task.T.map((v, j) => v + 5 * u[j] / n) }));
|
||||||
|
if (!nb.ok || !nb.reached) continue;
|
||||||
|
valid++; if (sameClass(t.pts, nb.ptsJ6Frozen && rec.j6Frozen ? nb.ptsJ6Frozen : nb.pts, task.mode)) agree++;
|
||||||
|
}
|
||||||
|
rec.consensus = { agree, valid };
|
||||||
|
if (valid >= 2 && agree * 2 < valid) { rec.keep = false; rec.why = 'consensus'; }
|
||||||
|
}
|
||||||
|
if (t.poseRelaxed && task.mode === 'pose') { rec.mode = 'pos'; rec.R = null; rec.demoted = true; rec.reached = t.err < 1; }
|
||||||
|
if (rec.keep) { const tp = timeParam(t.pts); rec.traj = tp.traj.map(q => q.map(r3)); rec.exec_s = r3(tp.steps * 0.02); }
|
||||||
|
fs.writeSync(fd, JSON.stringify(rec) + '\n');
|
||||||
|
if ((i + 1) % 25 === 0) process.stderr.write(`[seed ${SEED}] ${i + 1}/${NT} ${((performance.now() - t00) / 1000 / (i + 1 - done)).toFixed(2)} s/条\n`);
|
||||||
|
}
|
||||||
|
fs.closeSync(fd);
|
||||||
|
process.stderr.write(`[seed ${SEED}] done ${NT}(本次 ${NT - done}) in ${((performance.now() - t00) / 1000).toFixed(0)} s\n`);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* 路径偏差(同伦类判定用):两条展开折线按巡航时间 τ 各重采样 n 点,逐轴取最大差(°)。
|
||||||
|
* 判定口径:pos 任务忽略 J6(位置任务里 J6 是冗余自由度),max < 30° 视为同一绕行类。 */
|
||||||
|
'use strict';
|
||||||
|
const { VMAX } = require('./timelaw.cjs');
|
||||||
|
function resample(P, n) {
|
||||||
|
const seg = []; for (let i = 1; i < P.length; i++) { let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(P[i][j] - P[i - 1][j]) / VMAX[j]); seg.push(m); }
|
||||||
|
if (!seg.length) return Array.from({ length: n }, () => P[0].slice());
|
||||||
|
const tot = seg.reduce((a, b) => a + b, 0) || 1; const out = [];
|
||||||
|
for (let k = 0; k < n; k++) { let tau = tot * k / (n - 1), i = 0; while (i < seg.length - 1 && tau > seg[i]) { tau -= seg[i]; i++; }
|
||||||
|
const u = seg[i] > 0 ? Math.min(1, tau / seg[i]) : 1; out.push(P[i].map((v, j) => v + (P[i + 1][j] - v) * u)); }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function devAx(A, B, n) { n = n || 32; const a = resample(A, n), b = resample(B, n); const m = [0, 0, 0, 0, 0, 0]; for (let k = 0; k < n; k++) for (let j = 0; j < 6; j++) m[j] = Math.max(m[j], Math.abs(a[k][j] - b[k][j])); return m; }
|
||||||
|
const SAME_DEG = 30;
|
||||||
|
function sameClass(A, B, mode) { const d = devAx(A, B); return Math.max.apply(null, mode === 'pos' ? d.slice(0, 5) : d) < SAME_DEG; }
|
||||||
|
module.exports = { resample, devAx, sameClass, SAME_DEG };
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 并行生成:bash ml/gen/run_parallel.sh <每进程条数> <进程数> [起始种子=1] ["--consensus 4"]
|
||||||
|
# 例:8 核机器 5 万条 → bash ml/gen/run_parallel.sh 6250 8
|
||||||
|
# 2026-10-18 第五轮(审查 N-7 / N-16 / 缺口 1):
|
||||||
|
# - 每个种子先写 shard_XXXX.jsonl.part,写完才改名 → 中断/并发都不会留下被截断却看似完整的 shard;
|
||||||
|
# - 已存在的 shard_XXXX.jsonl(写完的)直接跳过;没写完的 .part 由 gen_dataset 从断点续写(只补剩下的条数);
|
||||||
|
# - 逐个 wait 子进程并检查返回码(此前裸 `wait` 恒为 0,某个 node 崩了也会继续统计);
|
||||||
|
# - 每 60 s 往控制台打一行节拍(已写条数 + 各进程最新进度),网页「终端输出」可见。
|
||||||
|
set -e
|
||||||
|
N=${1:-500}; P=${2:-2}; S0=${3:-1}; EXTRA=${4:-}
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
mkdir -p ml/data
|
||||||
|
pids=(); seeds=()
|
||||||
|
for ((i=0;i<P;i++)); do
|
||||||
|
s=$((S0+i)); f=ml/data/shard_$(printf %04d $s).jsonl
|
||||||
|
if [ -f "$f" ]; then echo "[gen] 种子 $s 已完成,跳过"; continue; fi
|
||||||
|
( node ml/gen/gen_dataset.cjs --n $N --seed $s --out "$f.part" $EXTRA 2>> ml/data/log_$s.txt && mv "$f.part" "$f" ) &
|
||||||
|
pids+=($!); seeds+=($s)
|
||||||
|
done
|
||||||
|
t0=$(date +%s); tl=$t0
|
||||||
|
while :; do
|
||||||
|
alive=0; for p in "${pids[@]}"; do kill -0 $p 2>/dev/null && alive=$((alive+1)); done
|
||||||
|
[ $alive = 0 ] && break
|
||||||
|
sleep 5
|
||||||
|
if [ $(( $(date +%s) - tl )) -ge 60 ]; then
|
||||||
|
tl=$(date +%s)
|
||||||
|
n=$(cat ml/data/shard_*.jsonl.part 2>/dev/null | wc -l)
|
||||||
|
last=$(for s in "${seeds[@]}"; do tail -n1 ml/data/log_$s.txt 2>/dev/null; done | head -n 2 | tr '\n' ' ')
|
||||||
|
echo "[gen 节拍] $(( (tl-t0)/60 )) 分钟 运行中 $alive/${#pids[@]} 进程 本批已写 $n 条 $last"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fail=0
|
||||||
|
for k in "${!pids[@]}"; do
|
||||||
|
if ! wait ${pids[$k]}; then echo "[gen] 种子 ${seeds[$k]} 失败,最后几行:"; tail -n 5 ml/data/log_${seeds[$k]}.txt || true; fail=1; fi
|
||||||
|
done
|
||||||
|
[ $fail = 0 ] || exit 6
|
||||||
|
node ml/gen/stats.cjs ml/data/shard_*.jsonl > ml/data/stats.json
|
||||||
|
cat ml/data/stats.json
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* 任务采样(方案 §4.3):A 均匀 45% / B 直线被挡 30% / C 贴立柱 15% / D 目标不可达 10%;pos/pose 各半(D 只做 pos) */
|
||||||
|
'use strict';
|
||||||
|
/* 2026-10-18 第五轮(审查 N-16):所有拒绝采样都有次数上限,超限抛错带原因(此前 for(;;) 在 collide.json 改动后可能静默死循环)。 */
|
||||||
|
const CAP = 200000;
|
||||||
|
function giveUp(what, n) { throw new Error(`采样失败:${what} 连续 ${n} 次未命中——多半是 collide.json / 关节限位变了,导致该类位形几乎不存在`); }
|
||||||
|
module.exports = function makeSampler(S, seed) {
|
||||||
|
const L = S.LIM, O = S.ORDER, w = S.wrap180;
|
||||||
|
let st = (seed >>> 0) || 1; const rnd = () => { st = (st * 1664525 + 1013904223) >>> 0; return st / 4294967296; };
|
||||||
|
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 n = Math.max(1, Math.ceil(Math.max(...dd.map(Math.abs)))); 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 freeQ = () => { for (let n = 0; n < CAP; n++) { const q = rq(); if (S.collideFree(q)) return q; } giveUp('无碰位形', CAP); };
|
||||||
|
function sample() {
|
||||||
|
const u = rnd();
|
||||||
|
const cat = u < 0.45 ? 'A' : u < 0.75 ? 'B' : u < 0.90 ? 'C' : 'D';
|
||||||
|
let a, b, T, R = null;
|
||||||
|
if (cat === 'A') { a = freeQ(); b = freeQ(); }
|
||||||
|
else if (cat === 'B') { let n = 0; for (;; n++) { if (n >= 20000) giveUp('B 类直线被挡', n); a = freeQ(); b = freeQ(); if (!segOK(a, b)) break; } }
|
||||||
|
else if (cat === 'C') {
|
||||||
|
let n = 0;
|
||||||
|
for (;; n++) { if (n >= CAP) giveUp('C 类贴立柱终点', n); b = rq(); if (!S.collideFree(b) || !nearColumn(b)) continue; a = null;
|
||||||
|
for (let t = 0; t < 200; t++) { const c = rq(); if (S.collideFree(c) && !segOK(c, b)) { a = c; break; } } if (a) break; }
|
||||||
|
} else {
|
||||||
|
a = freeQ();
|
||||||
|
if (rnd() < 0.6) { let n = 0; for (;; n++) { if (n >= CAP) giveUp('D 类立柱内目标', n); b = rq(); if (hitsColumn(b)) break; } T = S.fk(b, 0); } // 目标位置落在立柱附近/内部
|
||||||
|
else { b = rq(); const p = S.fk(b, 0), o = S.fk(O.map(() => 0), 0); T = p.map((v, i) => o[i] + (v - o[i]) * 1.6); } // 工作空间外
|
||||||
|
}
|
||||||
|
const mode = cat === 'D' ? 'pos' : (rnd() < 0.5 ? 'pos' : 'pose');
|
||||||
|
if (!T) T = S.fk(b, 0);
|
||||||
|
if (mode === 'pose') R = S.orient(b);
|
||||||
|
return { cat, mode, q0: a, T, R, qb: b };
|
||||||
|
}
|
||||||
|
const reseed = v => { st = (v >>> 0) || 1; }; // 2026-10-18:每条任务独立种子,shard 中断后可从第 i 条接着生成
|
||||||
|
return { sample, rnd, segOK, reseed };
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* M1 统计:node ml/gen/stats.cjs ml/data/shard_*.jsonl > ml/data/stats.json */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const files = process.argv.slice(2);
|
||||||
|
/* 2026-10-18:老师样本按认证最小间隙过滤(< MIN_GAP_MM 的轨迹不进训练集,build_dataset.cjs 同值)。
|
||||||
|
安全护栏(运行时投影 5 mm)不迁就数据分布;样本数与补量只按过滤后计。 */
|
||||||
|
const MIN_GAP_MM = +(process.env.MIN_GAP_MM || 3);
|
||||||
|
let gapDrop = 0;
|
||||||
|
const by = {}; let total = 0, keep = 0, steps = 0, samples = 0, padTail = 0;
|
||||||
|
const q = a => { a.sort((x, y) => x - y); return a.length ? { p10: +a[Math.floor(a.length * 0.1)].toFixed(1), p50: +a[a.length >> 1].toFixed(1), p90: +a[Math.floor(a.length * 0.9)].toFixed(1), max: +a[a.length - 1].toFixed(1) } : null; };
|
||||||
|
const plan = [], certMs = [], travel = [], exec = [], gaps = [];
|
||||||
|
for (const f of files) for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
||||||
|
if (!line) continue; const r = JSON.parse(line); total++;
|
||||||
|
const k = r.cat + '/' + r.mode; const b = by[k] = by[k] || { n: 0, reached: 0, certFail: 0, keep: 0, demoted: 0, consensusDrop: 0, j6Frozen: 0 };
|
||||||
|
b.n++; if (r.reached) b.reached++; if (r.cert && !r.cert.ok) b.certFail++; if (r.demoted) b.demoted++; if (r.why === 'consensus') b.consensusDrop++; if (r.j6Frozen) b.j6Frozen++;
|
||||||
|
if (r.plan_ms != null) plan.push(r.plan_ms); if (r.cert) certMs.push(r.cert.ms); if (r.cert) gaps.push(r.cert.minGap);
|
||||||
|
if (r.keep && r.cert && r.cert.minGap != null && r.cert.minGap < MIN_GAP_MM) { gapDrop++; b.gapDrop = (b.gapDrop || 0) + 1; }
|
||||||
|
else if (r.keep) { b.keep++; keep++; travel.push(r.travel_deg); exec.push(r.exec_s);
|
||||||
|
const n = r.traj.length - 1; steps += n; const s = Math.floor(n / 3) + 1; samples += s;
|
||||||
|
for (let t = 0; t <= n; t += 3) if (t + 16 > n) padTail++; }
|
||||||
|
}
|
||||||
|
const out = { tasks: total, keep, keepRate: +(keep / total).toFixed(3), byCatMode: by,
|
||||||
|
planMs: q(plan), certMs: q(certMs), vertexMinGapMm: q(gaps), travelDeg: q(travel), execS: q(exec),
|
||||||
|
samplesEvery3: samples, samplesPerTask: +(samples / keep).toFixed(1), paddedSampleShare: +(padTail / samples).toFixed(3),
|
||||||
|
projectedSamplesFor50k: Math.round(samples / total * 50000),
|
||||||
|
gapDrop, minGapFilterMm: MIN_GAP_MM,
|
||||||
|
consensusDrop: Object.values(by).reduce((a, b) => a + b.consensusDrop, 0) };
|
||||||
|
console.log(JSON.stringify(out, null, 1));
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/* 老师(方案 §4.3):solveDyn 松手口径——有离线路网时内部走 PRM 路网 Lazy A* + 捷径平滑,路网失败再落到 RRT-Connect。
|
||||||
|
* 离线确定性(2026-10-17,M1 一致性门禁 78% 未过后加):
|
||||||
|
* - 每次求解前 resetSession():清空路网增广/坏边/已验段等跨调用状态,结果与样本顺序无关;
|
||||||
|
* - setDeterministic:毫秒预算改按碰撞判定次数计(msPerCheck 默认 0.5,即 300 ms 预算 ≈ 600 次判定),与机器负载无关;
|
||||||
|
* - pos 任务冻结 J6:位置任务里 J6 不影响 TCP,路径上 J6 恒取起点值(认证不过才保留老师原路径,由 gen 决定)。 */
|
||||||
|
'use strict';
|
||||||
|
module.exports = function makeTeacher(S, ArmIK, opt) {
|
||||||
|
opt = opt || {};
|
||||||
|
const L = S.LIM, O = S.ORDER, w = S.wrap180;
|
||||||
|
if (S.setDeterministic) S.setDeterministic(true, opt.msPerCheck || 0.5);
|
||||||
|
return function teach(task) {
|
||||||
|
const o = { tool: 0, step: 0.5, ref: task.q0, from: task.q0.slice() };
|
||||||
|
if (task.mode === 'pose') { o.pose = { R: task.R }; o.priority = 'pos'; }
|
||||||
|
if (S.resetSession) S.resetSession(); else if (S.rtReset) S.rtReset();
|
||||||
|
const t0 = performance.now();
|
||||||
|
const r = S.solveDyn(task.T, o);
|
||||||
|
const ms = performance.now() - t0;
|
||||||
|
if (!r || !r.q) return { ok: false, ms, why: 'noq' };
|
||||||
|
const pts = [task.q0.slice()].concat(r.via || [], [r.q]);
|
||||||
|
for (let i = 1; i < pts.length; i++) pts[i] = pts[i - 1].map((v, k) => L[O[k]].cont ? v + w(pts[i][k] - v) : pts[i][k]);
|
||||||
|
const qf = pts[pts.length - 1], tcp = S.fk(qf, 0);
|
||||||
|
const err = Math.hypot(tcp[0] - task.T[0], tcp[1] - task.T[1], tcp[2] - task.T[2]);
|
||||||
|
const oerr = task.mode === 'pose' ? ArmIK.rotErrDeg(S.orient(qf), task.R) : 0;
|
||||||
|
const reached = err < 1 && oerr < 0.5 && !r.blocked;
|
||||||
|
const travelOf = P => { let t = 0; for (let i = 1; i < P.length; i++) for (let k = 0; k < 6; k++) t += Math.abs(P[i][k] - P[i - 1][k]); return t; };
|
||||||
|
const out = { ok: true, pts, reached, err, oerr, travel: travelOf(pts), ms, blocked: !!r.blocked, poseRelaxed: !!r.poseRelaxed, unsafe: !!r.unsafe, detour: r.detour || null };
|
||||||
|
if (task.mode === 'pos') {
|
||||||
|
const j6 = task.q0[5];
|
||||||
|
if (pts.some(p => Math.abs(p[5] - j6) > 1e-6)) out.ptsJ6Frozen = pts.map(p => { const c = p.slice(); c[5] = j6; return c; });
|
||||||
|
}
|
||||||
|
out.travelOf = travelOf;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* 局部可观测时间律(2026-10-17,方案 §4.5)
|
||||||
|
* 折线按"巡航时间" τ 参数化:每段 τ = max_j |Δq_j| / VMAX_j(各轴同步、主导轴满速)。
|
||||||
|
* 归一化速度 v∈[0,1](1 = 巡航):v ← min(1, v + A·dt, sqrt(2·A·剩余 τ)),只依赖当前速度与剩余路程。
|
||||||
|
* 输出 50 Hz(dt = 20 ms)状态序列,末点精确落在终点。 */
|
||||||
|
'use strict';
|
||||||
|
const VMAX = [60, 60, 90, 90, 120, 180]; // °/s,与 tests/route_bench.cjs 同(轴速比 0.6/0.6/0.9/0.9/1.2/1.8)
|
||||||
|
const A = 5.0; // 1/s²:0→巡航约 0.2 s(a_max 待真机参数)
|
||||||
|
const DT = 0.02;
|
||||||
|
function timeParam(P, opt) {
|
||||||
|
opt = opt || {};
|
||||||
|
const vmax = opt.vmax || VMAX, a = opt.acc || A, dt = opt.dt || DT;
|
||||||
|
const segT = [];
|
||||||
|
for (let i = 1; i < P.length; i++) { let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(P[i][j] - P[i - 1][j]) / vmax[j]); segT.push(m); }
|
||||||
|
const cum = [0]; for (const s of segT) cum.push(cum[cum.length - 1] + s);
|
||||||
|
const total = cum[cum.length - 1];
|
||||||
|
const at = tau => {
|
||||||
|
if (tau >= total) return P[P.length - 1].slice();
|
||||||
|
let i = 0; while (i < segT.length - 1 && cum[i + 1] < tau) i++;
|
||||||
|
const u = segT[i] > 0 ? (tau - cum[i]) / segT[i] : 1;
|
||||||
|
return P[i].map((v, j) => v + (P[i + 1][j] - v) * u);
|
||||||
|
};
|
||||||
|
const out = [P[0].slice()], taus = [0]; let tau = 0, v = 0, guard = 0;
|
||||||
|
while (tau < total - 1e-9 && guard++ < 100000) {
|
||||||
|
const rem = total - tau;
|
||||||
|
const v1 = Math.min(1, v + a * dt, Math.sqrt(2 * a * Math.max(0, rem - 0.5 * v * dt)) + 1e-9);
|
||||||
|
let step = 0.5 * (v + v1) * dt;
|
||||||
|
if (step <= 1e-9) step = Math.min(rem, a * dt * dt); // 防止末端速度掉到 0 卡住
|
||||||
|
tau = Math.min(total, tau + step); v = v1;
|
||||||
|
out.push(at(tau)); taus.push(tau);
|
||||||
|
}
|
||||||
|
return { traj: out, taus, cum, total, steps: out.length - 1 };
|
||||||
|
}
|
||||||
|
module.exports = { timeParam, VMAX, A, DT };
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""门禁(方案 §7.2 第 6 步):新模型须 ①门禁集到达率不降 ②A 类降幅 ≤ 0.5 个百分点 ③上一轮修好的错题 95% 仍对
|
||||||
|
④R7:B/C 类(需绕行)左右比例变化 ≤ 5 个百分点 ⑤R7:两者都到达的题行程中位数比 ≤ 1.05。
|
||||||
|
门禁集为无老师题,到达按定长 6 s 口径(见 closed_loop.NOREF_SEC)。
|
||||||
|
python -m ml.mining.gate --old cur.pt --new cand.pt --tasks ml/runs/gate_tasks.jsonl --out gate.json [--fixed prev_fixed.jsonl]
|
||||||
|
通过则把 cand 复制为 --current(默认 ml/runs/current.pt)。gate.json 含 accept / gain_pp,loop.sh 据此决定继续或停止。"""
|
||||||
|
import argparse, json, shutil
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.model.policy import build
|
||||||
|
from ml.sim.closed_loop import evaluate
|
||||||
|
|
||||||
|
|
||||||
|
def load(path, dev):
|
||||||
|
ck = torch.load(path, map_location=dev)
|
||||||
|
m = build(ck['cfg']).to(dev); m.load_state_dict(ck['model']); return m.eval()
|
||||||
|
|
||||||
|
|
||||||
|
def per_task_success(pers):
|
||||||
|
return torch.cat([p['done'] & (p['col'] == 0) for _, p in pers]).tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--old', required=True); ap.add_argument('--new', required=True); ap.add_argument('--tasks', required=True)
|
||||||
|
ap.add_argument('--fixed', default=None); ap.add_argument('--out', required=True); ap.add_argument('--current', default='ml/runs/current.pt')
|
||||||
|
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||||
|
a = ap.parse_args()
|
||||||
|
kin = Kin(device=a.device)
|
||||||
|
tasks = [json.loads(l) for l in open(a.tasks) if l.strip()]
|
||||||
|
ro, po = evaluate(load(a.old, a.device), kin, tasks, a.device)
|
||||||
|
rn, pn = evaluate(load(a.new, a.device), kin, tasks, a.device)
|
||||||
|
so, sn = per_task_success(po), per_task_success(pn)
|
||||||
|
acc = lambda s, c: (sum(x for x, t in zip(s, tasks) if t['cat'] == c) / max(1, sum(t['cat'] == c for t in tasks)))
|
||||||
|
gain = (sum(sn) - sum(so)) / len(tasks) * 100
|
||||||
|
a_drop = (acc(so, 'A') - acc(sn, 'A')) * 100
|
||||||
|
keep = None
|
||||||
|
if a.fixed:
|
||||||
|
ft = [json.loads(l) for l in open(a.fixed) if l.strip()]
|
||||||
|
if ft:
|
||||||
|
r2, p2 = evaluate(load(a.new, a.device), kin, ft, a.device)
|
||||||
|
keep = sum(per_task_success(p2)) / len(ft)
|
||||||
|
# R7:左右比例与行程
|
||||||
|
cat = [t['cat'] for t in tasks]
|
||||||
|
so_side = torch.cat([p['side'] for _, p in po]); sn_side = torch.cat([p['side'] for _, p in pn])
|
||||||
|
det = torch.tensor([c in ('B', 'C') for c in cat])
|
||||||
|
# 2026-10-18:左右比例按全部 B/C 题的首次 J1 方向统计,不以到达为条件(APG 把一侧绕行全做失败时,到达子集比例可纹丝不动)
|
||||||
|
left = lambda sd: (((sd > 0) & det).sum() / det.sum().clamp(min=1)).item() # side 为首次 J1 方向(closed_loop,第五轮改)
|
||||||
|
ok_o, ok_n = torch.tensor(so, dtype=torch.bool), torch.tensor(sn, dtype=torch.bool)
|
||||||
|
lr_shift = abs(left(sn_side) - left(so_side)) * 100
|
||||||
|
both = ok_o & ok_n
|
||||||
|
tro = torch.cat([p['travel'] for _, p in po]); trn = torch.cat([p['travel'] for _, p in pn])
|
||||||
|
travel_ratio = (trn[both].median() / tro[both].median().clamp(min=1e-6)).item() if both.any() else None
|
||||||
|
r7 = lr_shift <= 5.0 and (travel_ratio is None or travel_ratio <= 1.05)
|
||||||
|
accept = gain >= 0 and a_drop <= 0.5 and (keep is None or keep >= 0.95) and r7
|
||||||
|
out = {'old': ro, 'new': rn, 'gain_pp': gain, 'A_drop_pp': a_drop, 'fixed_kept': keep,
|
||||||
|
'R7_left_share_shift_pp': lr_shift, 'R7_travel_median_ratio': travel_ratio, 'R7_ok': r7, 'accept': accept}
|
||||||
|
# 本轮新修好的题(旧错新对)留给下一轮做"仍对"检查
|
||||||
|
fixed_now = [t for t, x, y in zip(tasks, so, sn) if (not x) and y]
|
||||||
|
with open(a.out.replace('.json', '_fixed.jsonl'), 'w') as f:
|
||||||
|
for t in fixed_now: f.write(json.dumps(t) + '\n')
|
||||||
|
json.dump(out, open(a.out, 'w'), indent=1)
|
||||||
|
if accept: shutil.copy(a.new, a.current)
|
||||||
|
print(json.dumps({k: out[k] for k in ('gain_pp', 'A_drop_pp', 'fixed_kept', 'R7_left_share_shift_pp', 'R7_travel_median_ratio', 'accept')}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 后训练主循环(方案 §7):APG 首轮 → 挖错 3–6 轮(每轮:挖错 → [E2 老师补路径] → APG 微调 → 门禁/回滚)
|
||||||
|
# bash ml/mining/loop.sh [轮数=6] [每轮 APG 步数=1000] [每轮出题数=50000]
|
||||||
|
# 2026-10-18:APG 采样池只用本轮错题(此前累积所有历史轮,后期分布偏向老错题);老错题的保持由门禁"上轮修好仍对"负责。
|
||||||
|
# 统一 $PYTHON;门禁加 R7(左右比例 / 行程)。
|
||||||
|
# 2026-10-18 第五轮(审查 N-11 / 缺口 1):可续跑——已有 gate.json 的轮次直接跳过(读回 gain 与 fixed 链),
|
||||||
|
# 中途断在某轮的 APG 时用该轮 last.pt 续跑;current.pt 只在门禁通过时由 gate.py 更新,本身就是断点。
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
PY=${PYTHON:-python}
|
||||||
|
ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000}
|
||||||
|
mkdir -p ml/runs/mine
|
||||||
|
[ -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
|
||||||
|
SAMPLES="ml/data/samples"; FIXED=""; small=0
|
||||||
|
for ((r=1; r<=ROUNDS; r++)); do
|
||||||
|
D=ml/runs/mine/r$r; mkdir -p $D
|
||||||
|
if [ -f $D/gate.json ]; then
|
||||||
|
gain=$($PY -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
|
||||||
|
echo "round $r 已完成(gain ${gain} pp),跳过"; FIXED="--fixed $D/gate_fixed.jsonl"
|
||||||
|
[ -s ml/data/e2/shard_e2_r$r.jsonl ] && SAMPLES="ml/data/samples ml/data/samples_e2"
|
||||||
|
if $PY -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
|
||||||
|
if [ $small -ge 2 ]; then echo "连续 2 轮提升 < 0.5 pp,停止"; break; fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# 2026-10-18 断点续跑:挖错做完打 mined.ok;E2 老师补路径写 .part 续写、写完改名。重挖时清掉旧的补路径,免得和新题对不上
|
||||||
|
E2F=ml/data/e2/shard_e2_r$r.jsonl
|
||||||
|
if [ -f $D/mined.ok ]; then echo "round $r 挖错已完成,跳过"
|
||||||
|
else
|
||||||
|
rm -f $E2F $E2F.part
|
||||||
|
$PY -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed $((1000+r)) --n $N --out $D
|
||||||
|
touch $D/mined.ok
|
||||||
|
fi
|
||||||
|
# E2(局部极小)请老师补少量路径(唯一保留的后训练老师用途)
|
||||||
|
if [ -s $D/e2_tasks.jsonl ]; then
|
||||||
|
[ -f $E2F ] || { node ml/gen/gen_dataset.cjs --tasks $D/e2_tasks.jsonl --seed $((5000+r)) --out $E2F.part && mv $E2F.part $E2F; }
|
||||||
|
node ml/prep/build_dataset.cjs ml/data/e2/shard_e2_r$r.jsonl ml/data/samples_e2
|
||||||
|
SAMPLES="ml/data/samples ml/data/samples_e2"
|
||||||
|
fi
|
||||||
|
res=""; [ -f ml/runs/apg_r$r/last.pt ] && res="--resume ml/runs/apg_r$r/last.pt"
|
||||||
|
$PY -m ml.apg.apg_finetune --init ml/runs/current.pt --samples $SAMPLES --pool $D/pool.jsonl --out ml/runs/apg_r$r --steps $STEPS $res
|
||||||
|
$PY -m ml.mining.gate --old ml/runs/current.pt --new ml/runs/apg_r$r/best.pt --tasks ml/runs/gate_tasks.jsonl --out $D/gate.json $FIXED
|
||||||
|
FIXED="--fixed $D/gate_fixed.jsonl"
|
||||||
|
gain=$($PY -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
|
||||||
|
echo "round $r gain ${gain} pp"
|
||||||
|
if $PY -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
|
||||||
|
if [ $small -ge 2 ]; then echo "连续 2 轮提升 < 0.5 pp,停止"; break; fi
|
||||||
|
done
|
||||||
|
echo "最终模型:ml/runs/current.pt"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""无老师挖错一轮(方案 §7.2):出题 → 闭环作答(关安全层)→ 验算 → 错题分类 → 错题+邻居进 APG 采样池。
|
||||||
|
python -m ml.mining.mine_round --ckpt ml/runs/apg/best.pt --seed 1001 --n 50000 --out ml/runs/mine/r1
|
||||||
|
造固定门禁集(只做一次):python -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
|
||||||
|
输出:pool.jsonl(错题与 ±5 mm/±3° 邻居,E1/E6 ×2)、e2_tasks.jsonl(卡在局部极小,可离线请老师补路径)、report.json/.md
|
||||||
|
错题类:E1 撞 / E2 没到且离目标 > 20 mm(局部极小)/ E3 用时 > 4 s / E4 没到但 < 20 mm(末端精度)/ E5 J1 换向 ≥ 3 次(摇摆)"""
|
||||||
|
import argparse, json, os
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.model.policy import build
|
||||||
|
from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors
|
||||||
|
from ml.mining.tasks import sample_tasks, neighbours
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--ckpt'); ap.add_argument('--seed', type=int, default=1001); ap.add_argument('--n', type=int, default=50000)
|
||||||
|
ap.add_argument('--out', required=True); ap.add_argument('--bs', type=int, default=512); ap.add_argument('--neigh', type=int, default=10)
|
||||||
|
ap.add_argument('--make-gate', type=int, default=0)
|
||||||
|
ap.add_argument('--mix', default='0.3,0.3,0.3,0.1', help='A,B,C,D 占比;贴立柱门禁集用 0,0,1,0')
|
||||||
|
ap.add_argument('--name', default='gate_tasks.jsonl')
|
||||||
|
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||||
|
a = ap.parse_args()
|
||||||
|
os.makedirs(a.out, exist_ok=True)
|
||||||
|
kin = Kin(device=a.device)
|
||||||
|
if a.make_gate:
|
||||||
|
ts = sample_tasks(kin, a.make_gate, a.seed, mix=tuple(float(x) for x in a.mix.split(',')))
|
||||||
|
with open(os.path.join(a.out, a.name), 'w') as f:
|
||||||
|
for t in ts: f.write(json.dumps(t) + '\n')
|
||||||
|
print('gate tasks', len(ts)); return
|
||||||
|
ck = torch.load(a.ckpt, map_location=a.device)
|
||||||
|
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
|
||||||
|
tasks = sample_tasks(kin, a.n, a.seed)
|
||||||
|
g = torch.Generator(); g.manual_seed(a.seed)
|
||||||
|
cls = {k: [] for k in ('E1', 'E2', 'E3', 'E4', 'E5', 'ok')}
|
||||||
|
for i in range(0, len(tasks), a.bs):
|
||||||
|
tb = tasks[i:i + a.bs]
|
||||||
|
_, per = rollout(model, kin, tb, a.device, K=8, safety=False, seed=a.seed + i, max_sec=6.0, return_traj=True)
|
||||||
|
q0, T, R, _, _ = tasks_to_tensors(tb, a.device)
|
||||||
|
tr = per['traj']
|
||||||
|
e, _ = goal_err(kin, tr[:, -1].to(a.device), T, R)
|
||||||
|
d1 = tr[:, 1:, 0] - tr[:, :-1, 0]
|
||||||
|
sg = torch.sign(torch.where(d1.abs() > 0.05, d1, torch.zeros_like(d1)))
|
||||||
|
sw = torch.zeros(len(tb))
|
||||||
|
for b in range(len(tb)):
|
||||||
|
s = sg[b][sg[b] != 0]
|
||||||
|
sw[b] = (s[1:] != s[:-1]).sum() if s.numel() > 1 else 0
|
||||||
|
for b, t in enumerate(tb):
|
||||||
|
if per['col'][b] > 0: cls['E1'].append(t)
|
||||||
|
elif not per['done'][b]: (cls['E2'] if e[b] > 20 else cls['E4']).append(t)
|
||||||
|
elif per['t_done'][b] > 4.0: cls['E3'].append(t)
|
||||||
|
elif sw[b] >= 3: cls['E5'].append(t)
|
||||||
|
else: cls['ok'].append(t)
|
||||||
|
pool = []
|
||||||
|
for k, lst in cls.items():
|
||||||
|
if k == 'ok': continue
|
||||||
|
rep = 2 if k == 'E1' else 1
|
||||||
|
for t in lst:
|
||||||
|
t = dict(t); t['hard'] = True; t['err'] = k
|
||||||
|
for _ in range(rep): pool.append(t); pool += neighbours(t, a.neigh, g)
|
||||||
|
with open(os.path.join(a.out, 'pool.jsonl'), 'w') as f:
|
||||||
|
for t in pool: f.write(json.dumps(t) + '\n')
|
||||||
|
with open(os.path.join(a.out, 'e2_tasks.jsonl'), 'w') as f:
|
||||||
|
for t in cls['E2'][:2000]: f.write(json.dumps(t) + '\n')
|
||||||
|
n = len(tasks)
|
||||||
|
rep = {'n': n, **{k: len(v) / n for k, v in cls.items()}, 'pool': len(pool)}
|
||||||
|
bycat = {}
|
||||||
|
for k, lst in cls.items():
|
||||||
|
for t in lst: bycat.setdefault(t['cat'], {}).setdefault(k, 0); bycat[t['cat']][k] += 1
|
||||||
|
rep['by_cat'] = bycat
|
||||||
|
json.dump(rep, open(os.path.join(a.out, 'report.json'), 'w'), indent=1)
|
||||||
|
md = ['# 挖错报告', '', f'任务 {n},错题池 {len(pool)}', '', '| 类 | 占比 |', '|---|---:|'] + [f'| {k} | {len(v) / n:.2%} |' for k, v in cls.items()]
|
||||||
|
open(os.path.join(a.out, 'report.md'), 'w').write('\n'.join(md) + '\n')
|
||||||
|
print(json.dumps(rep, indent=1))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""无老师出题(PyTorch):A 均匀 / B 直线被挡 / C 贴立柱 / D 工作空间外;pos/pose 各半。
|
||||||
|
2026-10-18:无老师题没有参考时长与行程,exec_s/travel 置 None(此前写死 3.0/0.0 造成假时长预算);
|
||||||
|
闭环仿真对这类题按定长 6 s 口径判到达,time_ok_1p3 / travel_ratio 只在有老师参考的题上统计。"""
|
||||||
|
import math
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.features import wrap180
|
||||||
|
|
||||||
|
|
||||||
|
def auto_chunk(kin, mb_per_q, lo=256, hi=8192):
|
||||||
|
"""2026-10-18 第五轮(审查 N-6):分块大小按设备空闲显存定,用掉其 1/4。
|
||||||
|
两处每位形峰值不同不是矛盾:kin.gaps 按 (胶囊对下限 + cap)² 筛候选对,cap=0.5 mm 时幸存约 2800 对(≈1.5 MB/位形),
|
||||||
|
column_gap 用 cap=40 mm,40 mm 内幸存的对多一个量级(≈20 MB/位形)。CPU 上取下限 256。"""
|
||||||
|
if kin.lo.device.type == 'cuda':
|
||||||
|
free = torch.cuda.mem_get_info(kin.lo.device)[0]
|
||||||
|
return int(max(lo, min(hi, free * 0.25 / (mb_per_q * 1e6))))
|
||||||
|
return lo
|
||||||
|
|
||||||
|
|
||||||
|
def min_gap_chunked(kin, q, cap=0.5, chunk=None):
|
||||||
|
"""分块算最小间隙:一次 4 万位形会爆内存/显存(2026-10-18 实测出题 OOM);块大小见 auto_chunk(≈1.5 MB/位形)"""
|
||||||
|
chunk = chunk or auto_chunk(kin, 1.5)
|
||||||
|
return torch.cat([kin.gaps(q[i:i + chunk], cap)['gm'].min(-1).values for i in range(0, q.shape[0], chunk)])
|
||||||
|
|
||||||
|
|
||||||
|
def rand_q(kin, n, g):
|
||||||
|
u = torch.rand(n, 6, device=kin.lo.device, generator=g)
|
||||||
|
return kin.lo + u * (kin.hi - kin.lo)
|
||||||
|
|
||||||
|
|
||||||
|
def free_q(kin, n, g, chunk=1024):
|
||||||
|
out = []
|
||||||
|
while sum(x.shape[0] for x in out) < n:
|
||||||
|
q = rand_q(kin, chunk, g)
|
||||||
|
ok = min_gap_chunked(kin, q) > 0
|
||||||
|
out.append(q[ok])
|
||||||
|
return torch.cat(out)[:n]
|
||||||
|
|
||||||
|
|
||||||
|
def straight_blocked(kin, a, b, n=24):
|
||||||
|
d = torch.where(kin.cont, wrap180(b - a), b - a)
|
||||||
|
s = torch.linspace(0, 1, n, device=a.device)
|
||||||
|
pts = (a[:, None] + s[None, :, None] * d[:, None]).reshape(-1, 6)
|
||||||
|
return (min_gap_chunked(kin, pts) <= 0).view(a.shape[0], n).any(-1)
|
||||||
|
|
||||||
|
|
||||||
|
def column_gap(kin, q, chunk=None):
|
||||||
|
chunk = chunk or auto_chunk(kin, 20.0) # cap=40 → ≈20 MB/位形
|
||||||
|
out = []
|
||||||
|
for s in range(0, q.shape[0], chunk): # 与 min_gap_chunked 一样分块防 OOM
|
||||||
|
gm = kin.gaps(q[s:s + chunk], 40)['gm']
|
||||||
|
out.append(torch.stack([gm[:, g] for g in (2, 3, 4, 5, 6)], -1).min(-1).values) # 组 0(立柱/底座)× 组 2..6
|
||||||
|
return torch.cat(out)
|
||||||
|
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def sample_tasks(kin, n, seed, mix=(0.3, 0.3, 0.3, 0.1)):
|
||||||
|
g = torch.Generator(device=kin.lo.device); g.manual_seed(seed)
|
||||||
|
want = [int(round(n * m)) for m in mix]; want[0] += n - sum(want)
|
||||||
|
tasks = []
|
||||||
|
def emit(cat, q0, b, T=None):
|
||||||
|
pose = torch.rand(q0.shape[0], device=q0.device, generator=g) < 0.5
|
||||||
|
if cat == 'D': pose[:] = False
|
||||||
|
xf = kin.cum_xf(b)
|
||||||
|
Tb = kin.fk(b, xf) if T is None else T
|
||||||
|
Rb = kin.orient(b, xf).reshape(-1, 9)
|
||||||
|
for i in range(q0.shape[0]):
|
||||||
|
tasks.append({'cat': cat, 'mode': 'pose' if pose[i] else 'pos', 'q0': q0[i].tolist(), 'T': Tb[i].tolist(),
|
||||||
|
'R': Rb[i].tolist() if pose[i] else None, 'exec_s': None, 'travel': None})
|
||||||
|
if want[0]:
|
||||||
|
emit('A', free_q(kin, want[0], g), free_q(kin, want[0], g))
|
||||||
|
for cat, k in (('B', want[1]), ('C', want[2])):
|
||||||
|
got_a, got_b, have = [], [], 0
|
||||||
|
while have < k:
|
||||||
|
a, b = free_q(kin, 2048, g), free_q(kin, 2048, g)
|
||||||
|
m = straight_blocked(kin, a, b)
|
||||||
|
if cat == 'C': m &= column_gap(kin, b) < 8
|
||||||
|
got_a.append(a[m]); got_b.append(b[m]); have += int(m.sum())
|
||||||
|
if k: emit(cat, torch.cat(got_a)[:k], torch.cat(got_b)[:k])
|
||||||
|
if want[3]:
|
||||||
|
a, b = free_q(kin, want[3], g), rand_q(kin, want[3], g)
|
||||||
|
o = kin.fk(torch.zeros(1, 6, device=a.device))
|
||||||
|
T = o + (kin.fk(b) - o) * 1.6
|
||||||
|
emit('D', a, b, T)
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def neighbours(t, k, g):
|
||||||
|
"""±5 mm / ±3° 邻居"""
|
||||||
|
out = []
|
||||||
|
for _ in range(k):
|
||||||
|
u = torch.randn(3, generator=g); u = u / u.norm() * 5.0
|
||||||
|
nt = dict(t); nt['T'] = [x + float(d) for x, d in zip(t['T'], u)]
|
||||||
|
if t.get('R'):
|
||||||
|
ax = torch.randn(3, generator=g); ax = ax / ax.norm(); ang = math.radians(3.0) * float(torch.rand(1, generator=g))
|
||||||
|
K = torch.tensor([[0, -ax[2], ax[1]], [ax[2], 0, -ax[0]], [-ax[1], ax[0], 0]])
|
||||||
|
Rr = torch.eye(3) + math.sin(ang) * K + (1 - math.cos(ang)) * K @ K
|
||||||
|
nt['R'] = (Rr @ torch.tensor(t['R']).view(3, 3)).flatten().tolist()
|
||||||
|
nt['hard'] = True
|
||||||
|
out.append(nt)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""MeanFlow 损失(Geng et al. 2025;MP1 配方:25% 样本 r≠t,自适应损失加权)。
|
||||||
|
约定:t=0 为数据、t=1 为噪声;z_t = (1−t)·x + t·ε,瞬时速度 v = ε − x;网络输出平均速度 u(z,r,t)。
|
||||||
|
目标 u_tgt = v − (t − r)·du/dt,du/dt = v·∂_z u + ∂_t u 用 torch.func.jvp 一次前向得到(切向量 (v, 0, 1))。"""
|
||||||
|
import torch
|
||||||
|
from torch.func import jvp
|
||||||
|
|
||||||
|
|
||||||
|
def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0):
|
||||||
|
t = torch.sigmoid(torch.randn(B, device=device, dtype=dtype) * sigma + mu)
|
||||||
|
r = torch.sigmoid(torch.randn(B, device=device, dtype=dtype) * sigma + mu)
|
||||||
|
t, r = torch.maximum(t, r), torch.minimum(t, r)
|
||||||
|
eq = torch.rand(B, device=device) >= p_neq
|
||||||
|
r = torch.where(eq, t, r)
|
||||||
|
return r, t
|
||||||
|
|
||||||
|
|
||||||
|
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
|
||||||
|
B = x.shape[0]
|
||||||
|
r, t = sample_rt(B, x.device, x.dtype, p_neq)
|
||||||
|
e = torch.randn_like(x)
|
||||||
|
z = (1 - t)[:, None, None] * x + t[:, None, None] * e
|
||||||
|
v = e - x
|
||||||
|
fn = lambda z_, r_, t_: model.decode(mem, z_, r_, t_)
|
||||||
|
u, dudt = jvp(fn, (z, r, t), (v, torch.zeros_like(r), torch.ones_like(t)))
|
||||||
|
u_tgt = (v - (t - r)[:, None, None] * dudt).detach()
|
||||||
|
err = (u - u_tgt).pow(2).mean(dim=(1, 2))
|
||||||
|
w = 1.0 / (err.detach() + ada_c).pow(ada_p)
|
||||||
|
return (w * err).mean(), err.mean().detach()
|
||||||
|
|
||||||
|
|
||||||
|
def one_step(model, mem, e):
|
||||||
|
B = e.shape[0]
|
||||||
|
one = torch.ones(B, device=e.device, dtype=e.dtype)
|
||||||
|
return e - model.decode(mem, e, torch.zeros_like(one), one)
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""策略网络(方案 §6.1):条件特征 → 8 个条件 token → 4 层 encoder;16 个动作 token(噪声 z + 时间嵌入 (r,t))
|
||||||
|
→ 4 层 decoder(自注意力 + 交叉注意力,非自回归一次前向)→ 每 token 6 维(平均速度场 u)。
|
||||||
|
价值头 / 子目标头只依赖观测(encoder 输出)。
|
||||||
|
注意力用显式 matmul+softmax 实现:torch.func.jvp(MeanFlow 训练)需要前向模式自动微分,SDPA 融合内核不支持。"""
|
||||||
|
import math
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
from ml.common.features import F_DIM, H
|
||||||
|
|
||||||
|
|
||||||
|
class Attn(nn.Module):
|
||||||
|
def __init__(self, d, nh):
|
||||||
|
super().__init__()
|
||||||
|
self.nh, self.dh = nh, d // nh
|
||||||
|
self.q, self.k, self.v, self.o = (nn.Linear(d, d) for _ in range(4))
|
||||||
|
|
||||||
|
def forward(self, x, mem=None):
|
||||||
|
mem = x if mem is None else mem
|
||||||
|
B, Lq, D = x.shape; Lk = mem.shape[1]
|
||||||
|
q = self.q(x).view(B, Lq, self.nh, self.dh).transpose(1, 2)
|
||||||
|
k = self.k(mem).view(B, Lk, self.nh, self.dh).transpose(1, 2)
|
||||||
|
v = self.v(mem).view(B, Lk, self.nh, self.dh).transpose(1, 2)
|
||||||
|
a = torch.softmax(q @ k.transpose(-1, -2) / math.sqrt(self.dh), dim=-1)
|
||||||
|
return self.o((a @ v).transpose(1, 2).reshape(B, Lq, D))
|
||||||
|
|
||||||
|
|
||||||
|
class FFN(nn.Module):
|
||||||
|
def __init__(self, d, f):
|
||||||
|
super().__init__(); self.a = nn.Linear(d, f); self.b = nn.Linear(f, d)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return self.b(F.gelu(self.a(x)))
|
||||||
|
|
||||||
|
|
||||||
|
class EncBlock(nn.Module):
|
||||||
|
def __init__(self, d, nh, f):
|
||||||
|
super().__init__(); self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d); self.sa, self.ff = Attn(d, nh), FFN(d, f)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
x = x + self.sa(self.n1(x))
|
||||||
|
return x + self.ff(self.n2(x))
|
||||||
|
|
||||||
|
|
||||||
|
class DecBlock(nn.Module):
|
||||||
|
def __init__(self, d, nh, f):
|
||||||
|
super().__init__()
|
||||||
|
self.n1, self.n2, self.n3 = nn.LayerNorm(d), nn.LayerNorm(d), nn.LayerNorm(d)
|
||||||
|
self.sa, self.ca, self.ff = Attn(d, nh), Attn(d, nh), FFN(d, f)
|
||||||
|
|
||||||
|
def forward(self, x, mem):
|
||||||
|
x = x + self.sa(self.n1(x))
|
||||||
|
x = x + self.ca(self.n2(x), mem)
|
||||||
|
return x + self.ff(self.n3(x))
|
||||||
|
|
||||||
|
|
||||||
|
def time_emb(t, dim):
|
||||||
|
half = dim // 2
|
||||||
|
fr = torch.exp(-math.log(1000.0) * torch.arange(half, device=t.device, dtype=t.dtype) / half)
|
||||||
|
a = t[:, None] * 1000.0 * fr[None]
|
||||||
|
return torch.cat([torch.sin(a), torch.cos(a)], -1)
|
||||||
|
|
||||||
|
|
||||||
|
class Policy(nn.Module):
|
||||||
|
def __init__(self, d=192, nh=8, ff=768, n_enc=4, n_dec=4, n_cond=8, f_dim=F_DIM, horizon=H, head='meanflow', K=8):
|
||||||
|
super().__init__()
|
||||||
|
self.d, self.n_cond, self.h, self.head, self.K = d, n_cond, horizon, head, K
|
||||||
|
self.cond = nn.Sequential(nn.Linear(f_dim, 2 * d), nn.GELU(), nn.Linear(2 * d, n_cond * d))
|
||||||
|
self.cpos = nn.Parameter(torch.zeros(1, n_cond, d))
|
||||||
|
self.enc = nn.ModuleList(EncBlock(d, nh, ff) for _ in range(n_enc))
|
||||||
|
self.enc_norm = nn.LayerNorm(d)
|
||||||
|
self.ain = nn.Linear(6, d)
|
||||||
|
self.apos = nn.Parameter(torch.zeros(1, horizon, d))
|
||||||
|
self.temb = nn.Sequential(nn.Linear(2 * 64, d), nn.GELU(), nn.Linear(d, d))
|
||||||
|
self.dec = nn.ModuleList(DecBlock(d, nh, ff) for _ in range(n_dec))
|
||||||
|
self.dec_norm = nn.LayerNorm(d)
|
||||||
|
self.aout = nn.Linear(d, 6)
|
||||||
|
self.value = nn.Sequential(nn.Linear(d, d), nn.GELU(), nn.Linear(d, 1))
|
||||||
|
self.sub = nn.Sequential(nn.Linear(d, d), nn.GELU(), nn.Linear(d, 13))
|
||||||
|
if head == 'wta': # 预定回退:同骨干 WTA K 模式头(方案 §6.2)
|
||||||
|
self.mode_emb = nn.Parameter(torch.randn(1, K, 1, d) * 0.02)
|
||||||
|
self.mode_logit = nn.Linear(d, 1)
|
||||||
|
nn.init.normal_(self.cpos, std=0.02); nn.init.normal_(self.apos, std=0.02)
|
||||||
|
nn.init.zeros_(self.aout.weight); nn.init.zeros_(self.aout.bias)
|
||||||
|
|
||||||
|
def encode(self, feat):
|
||||||
|
x = self.cond(feat).view(-1, self.n_cond, self.d) + self.cpos
|
||||||
|
for b in self.enc: x = b(x)
|
||||||
|
return self.enc_norm(x)
|
||||||
|
|
||||||
|
def heads(self, mem):
|
||||||
|
p = mem.mean(1)
|
||||||
|
return self.value(p).squeeze(-1), self.sub(p)
|
||||||
|
|
||||||
|
def decode(self, mem, z, r, t):
|
||||||
|
"""平均速度场 u(z, r, t);z [B,16,6],r,t [B]"""
|
||||||
|
te = self.temb(torch.cat([time_emb(t, 64), time_emb(t - r, 64)], -1))
|
||||||
|
x = self.ain(z) + self.apos + te[:, None]
|
||||||
|
for b in self.dec: x = b(x, mem)
|
||||||
|
return self.aout(self.dec_norm(x))
|
||||||
|
|
||||||
|
def wta_modes(self, mem):
|
||||||
|
"""WTA:K 个模式一次解码 → act [B,K,16,6], logits [B,K]"""
|
||||||
|
B = mem.shape[0]
|
||||||
|
x = (self.ain.bias + self.apos)[:, None] + self.mode_emb # [1,K,16,d]
|
||||||
|
x = x.expand(B, -1, -1, -1).reshape(B * self.K, self.h, self.d)
|
||||||
|
m = mem.repeat_interleave(self.K, 0)
|
||||||
|
for b in self.dec: x = b(x, m)
|
||||||
|
x = self.dec_norm(x)
|
||||||
|
act = self.aout(x).view(B, self.K, self.h, 6)
|
||||||
|
return act, self.mode_logit(x.mean(1)).view(B, self.K)
|
||||||
|
|
||||||
|
def sample(self, feat, z):
|
||||||
|
"""部署接口(ONNX 同签名):返回 (act [B,16,6] 归一化增量, value [B], sub [B,13])
|
||||||
|
meanflow:一步生成 x = z − u(z, 0, 1);
|
||||||
|
wta:取第 0 行观测的 K 个模式,第 b 行输出模式 b mod K(部署时 8 行观测相同;前瞻只用各行的 value)。"""
|
||||||
|
mem = self.encode(feat)
|
||||||
|
v, s = self.heads(mem)
|
||||||
|
B = feat.shape[0]
|
||||||
|
if self.head == 'wta':
|
||||||
|
act, _ = self.wta_modes(mem[:1])
|
||||||
|
idx = torch.arange(B, device=feat.device) % self.K
|
||||||
|
return act[0, idx] + 0.0 * z, v, s # z 不参与计算,保留为输入以统一 ONNX 签名
|
||||||
|
one, zero = torch.ones(B, device=feat.device, dtype=feat.dtype), torch.zeros(B, device=feat.device, dtype=feat.dtype)
|
||||||
|
x = z - self.decode(mem, z, zero, one)
|
||||||
|
return x, v, s
|
||||||
|
|
||||||
|
def sample_shared(self, feat1, z):
|
||||||
|
"""部署接口(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 行相同,展开只为保持运行时读法)。
|
||||||
|
此前部署把同一观测复制 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)
|
||||||
|
v, s = self.heads(mem)
|
||||||
|
K = z.shape[0]
|
||||||
|
if self.head == 'wta':
|
||||||
|
act, _ = self.wta_modes(mem[:1])
|
||||||
|
idx = torch.arange(K, device=z.device) % self.K
|
||||||
|
return act[0, idx] + 0.0 * z, v.expand(K), s.expand(K, -1)
|
||||||
|
m = mem.expand(K, -1, -1)
|
||||||
|
one = torch.ones(K, device=z.device, dtype=z.dtype)
|
||||||
|
x = z - self.decode(m, z, torch.zeros_like(one), one)
|
||||||
|
return x, v.expand(K), s.expand(K, -1)
|
||||||
|
|
||||||
|
def sample_k(self, feat, K, z=None):
|
||||||
|
"""仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13]"""
|
||||||
|
mem = self.encode(feat)
|
||||||
|
v, s = self.heads(mem)
|
||||||
|
B = feat.shape[0]
|
||||||
|
if self.head == 'wta':
|
||||||
|
act, logit = self.wta_modes(mem)
|
||||||
|
if K < self.K:
|
||||||
|
top = logit.topk(K, -1).indices
|
||||||
|
act = act.gather(1, top[:, :, None, None].expand(-1, -1, self.h, 6))
|
||||||
|
return act, v, s
|
||||||
|
z = torch.randn(B * K, self.h, 6, device=feat.device, dtype=feat.dtype) if z is None else z.reshape(B * K, self.h, 6)
|
||||||
|
m = mem.repeat_interleave(K, 0)
|
||||||
|
one = torch.ones(B * K, device=feat.device, dtype=feat.dtype)
|
||||||
|
x = z - self.decode(m, z, torch.zeros_like(one), one)
|
||||||
|
return x.view(B, K, self.h, 6), v, s
|
||||||
|
|
||||||
|
|
||||||
|
def build(cfg):
|
||||||
|
m = cfg.get('model', {})
|
||||||
|
return Policy(d=m.get('d', 192), nh=m.get('heads', 8), ff=m.get('ff', 768), n_enc=m.get('enc', 4), n_dec=m.get('dec', 4),
|
||||||
|
head=m.get('head', 'meanflow'), K=m.get('K', 8))
|
||||||
|
|
||||||
|
|
||||||
|
def wta_loss(model, mem, y, w_all=0.05):
|
||||||
|
"""WTA-L1 + 模式 CE;返回 (loss, 最优模式输出 [B,16,6])"""
|
||||||
|
act, logit = model.wta_modes(mem)
|
||||||
|
l1 = (act - y[:, None]).abs().mean(dim=(2, 3)) # [B,K]
|
||||||
|
best = l1.argmin(-1)
|
||||||
|
loss = l1.gather(1, best[:, None]).mean() + w_all * l1.mean() + 0.1 * F.cross_entropy(logit, best)
|
||||||
|
return loss, act[torch.arange(y.shape[0]), best]
|
||||||
+809
@@ -0,0 +1,809 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Transformer 多峰策略端到端训练与浏览器部署 · 训练监控</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0b0f17; --bg2: #0f1521; --panel: #131a27; --panel2: #18212f; --line: #222c3c; --line2: #2c3850;
|
||||||
|
--text: #e8edf5; --dim: #8c99ad; --dim2: #5d6a7e;
|
||||||
|
--accent: #5aa9ff; --accent2: #7c6cff; --ok: #34d399; --warn: #fbbf24; --bad: #f87171;
|
||||||
|
--c1: #5aa9ff; --c2: #fbbf24; --c3: #34d399; --c4: #f87171; --c5: #a78bfa; --c6: #22d3ee; --c7: #fb923c;
|
||||||
|
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
--r: 14px;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; }
|
||||||
|
body { background: radial-gradient(1200px 600px at 10% -10%, #16223a 0%, transparent 60%), radial-gradient(900px 500px at 100% 0%, #1c1838 0%, transparent 55%), var(--bg);
|
||||||
|
background-attachment: fixed; color: var(--text); font: 14px/1.5 -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; min-height: 100vh; }
|
||||||
|
|
||||||
|
/* ---------- 顶栏 ---------- */
|
||||||
|
header { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 12px 24px;
|
||||||
|
background: rgba(11,15,23,.78); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
|
||||||
|
.brand { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.logo { width: 30px; height: 30px; border-radius: 9px; background: linear-gradient(135deg, var(--accent), var(--accent2)); display: grid; place-items: center; box-shadow: 0 4px 18px #5aa9ff44; }
|
||||||
|
.logo svg { width: 18px; height: 18px; }
|
||||||
|
.brand h1 { font-size: 15px; margin: 0; font-weight: 650; letter-spacing: .3px; }
|
||||||
|
.brand .sub { font-size: 12px; color: var(--dim); margin-top: -2px; }
|
||||||
|
header .sp { flex: 1; }
|
||||||
|
.ctl { display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: 13px; }
|
||||||
|
select, button { background: var(--panel2); color: var(--text); border: 1px solid var(--line2); border-radius: 9px; padding: 6px 10px; font: inherit; font-size: 13px; outline: none; }
|
||||||
|
select:hover, button:hover { border-color: #3d4c68; }
|
||||||
|
button { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
button.on { border-color: var(--accent); color: var(--accent); background: #5aa9ff14; }
|
||||||
|
input[type=range] { accent-color: var(--accent); width: 90px; }
|
||||||
|
.pill { display: inline-flex; align-items: center; gap: 7px; padding: 4px 11px; border-radius: 99px; font-size: 12px; border: 1px solid var(--line2); color: var(--dim); background: var(--panel); }
|
||||||
|
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dim2); }
|
||||||
|
.pill.live { color: var(--ok); border-color: #34d39955; } .pill.live .dot { background: var(--ok); box-shadow: 0 0 0 0 #34d399aa; animation: pulse 1.6s infinite; }
|
||||||
|
.pill.stale { color: var(--warn); border-color: #fbbf2455; } .pill.stale .dot { background: var(--warn); }
|
||||||
|
.pill.dead { color: var(--bad); border-color: #f8717155; } .pill.dead .dot { background: var(--bad); }
|
||||||
|
.pill.demo { color: var(--c5); border-color: #a78bfa55; } .pill.demo .dot { background: var(--c5); }
|
||||||
|
@keyframes pulse { 0% { box-shadow: 0 0 0 0 #34d39999; } 70% { box-shadow: 0 0 0 7px #34d39900; } 100% { box-shadow: 0 0 0 0 #34d39900; } }
|
||||||
|
|
||||||
|
main { padding: 20px 24px 48px; max-width: 1680px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.card { background: linear-gradient(180deg, #151d2b 0%, #121926 100%); border: 1px solid var(--line); border-radius: var(--r); padding: 16px 18px; min-width: 0; box-shadow: 0 1px 0 #ffffff05 inset, 0 8px 24px #00000030; }
|
||||||
|
.card h2 { font-size: 12.5px; font-weight: 600; color: var(--dim); margin: 0 0 12px; display: flex; justify-content: space-between; align-items: center; letter-spacing: .4px; }
|
||||||
|
.card h2 .r { font-weight: 400; color: var(--dim2); font-size: 12px; letter-spacing: 0; }
|
||||||
|
|
||||||
|
/* ---------- 流水线阶段 ---------- */
|
||||||
|
.steps { display: flex; gap: 0; overflow-x: auto; padding: 4px 2px 2px; scrollbar-width: thin; }
|
||||||
|
.step { flex: 1 0 96px; display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; min-width: 96px; }
|
||||||
|
.step::before { content: ""; position: absolute; top: 14px; left: -50%; width: 100%; height: 2px; background: var(--line2); z-index: 0; }
|
||||||
|
.step:first-child::before { display: none; }
|
||||||
|
.step.done::before, .step.active::before { background: linear-gradient(90deg, var(--ok), var(--ok)); }
|
||||||
|
.step .ic { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font: 600 12px var(--mono); background: var(--panel2); border: 2px solid var(--line2); color: var(--dim); z-index: 1; }
|
||||||
|
.step.done .ic { background: #34d39922; border-color: var(--ok); color: var(--ok); }
|
||||||
|
.step.active .ic { background: #5aa9ff22; border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px #5aa9ff1f; }
|
||||||
|
.step.fail .ic { background: #f8717122; border-color: var(--bad); color: var(--bad); }
|
||||||
|
.step .nm { font-size: 12.5px; color: var(--text); white-space: nowrap; } .step .ds { font-size: 11px; color: var(--dim2); white-space: nowrap; }
|
||||||
|
.step.pending .nm { color: var(--dim); }
|
||||||
|
.step.active .ds { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ---------- 进度主卡 + GPU ---------- */
|
||||||
|
.hero { display: grid; grid-template-columns: minmax(0, 2.2fr) minmax(300px, 1fr); gap: 16px; }
|
||||||
|
.prog-top { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
||||||
|
.prog-top .ttl { font-size: 13px; color: var(--dim); } .prog-top .ttl b { color: var(--text); font-weight: 600; font-size: 16px; margin-right: 8px; }
|
||||||
|
.tag { display: inline-block; font-size: 11px; padding: 1px 8px; border-radius: 6px; background: #5aa9ff1c; color: var(--accent); border: 1px solid #5aa9ff33; vertical-align: 2px; }
|
||||||
|
.tag.apg { background: #a78bfa1c; color: var(--c5); border-color: #a78bfa33; } .tag.bench { background: #fbbf241c; color: var(--warn); border-color: #fbbf2433; }
|
||||||
|
.bigpct { font: 700 44px/1 var(--mono); letter-spacing: -1px; background: linear-gradient(90deg, #fff, #b9d7ff); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||||
|
.bigpct small { font-size: 18px; margin-left: 2px; }
|
||||||
|
.bar { position: relative; height: 12px; background: #0d131d; border: 1px solid var(--line); border-radius: 99px; overflow: visible; margin: 16px 0 10px; }
|
||||||
|
.bar .fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--c6)); box-shadow: 0 0 14px #5aa9ff66; transition: width .8s ease; position: relative; }
|
||||||
|
.bar .fill::after { content: ""; position: absolute; inset: 0; border-radius: 99px; background: repeating-linear-gradient(45deg, #ffffff14 0 8px, transparent 8px 16px); animation: move 1.2s linear infinite; }
|
||||||
|
.bar.idle .fill::after { animation: none; display: none; }
|
||||||
|
@keyframes move { from { background-position: 0 0; } to { background-position: 22px 0; } }
|
||||||
|
.bar .mk { position: absolute; top: -4px; width: 2px; height: 20px; border-radius: 1px; background: #ffffff30; }
|
||||||
|
.bar .mk.best { background: var(--ok); }
|
||||||
|
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-top: 14px; }
|
||||||
|
.fact { background: #0f1520; border: 1px solid var(--line); border-radius: 10px; padding: 9px 12px; }
|
||||||
|
.fact .k { font-size: 11.5px; color: var(--dim); } .fact .v { font: 600 15px/1.35 var(--mono); margin-top: 2px; white-space: nowrap; }
|
||||||
|
.gpuwrap { display: flex; gap: 16px; align-items: center; }
|
||||||
|
.ring { position: relative; width: 116px; height: 116px; flex: none; }
|
||||||
|
.ring svg { transform: rotate(-90deg); }
|
||||||
|
.ring .c { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
|
||||||
|
.ring .c b { font: 700 24px/1 var(--mono); display: block; } .ring .c span { font-size: 11px; color: var(--dim); }
|
||||||
|
.gpurows { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
||||||
|
.gr .l { display: flex; justify-content: space-between; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); }
|
||||||
|
.mini { height: 6px; background: #0d131d; border-radius: 99px; overflow: hidden; } .mini > div { height: 100%; border-radius: 99px; transition: width .6s; }
|
||||||
|
.gname { font-size: 12px; color: var(--dim2); margin-top: 12px; }
|
||||||
|
|
||||||
|
/* ---------- 指标卡 ---------- */
|
||||||
|
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
||||||
|
.kpi { position: relative; overflow: hidden; padding-bottom: 12px; }
|
||||||
|
.kpi .v { font: 650 26px/1.15 var(--mono); letter-spacing: -.5px; } .kpi .v small { font-size: 13px; color: var(--dim); margin-left: 4px; font-weight: 500; }
|
||||||
|
.kpi .s { color: var(--dim); font-size: 12px; margin-top: 3px; } .kpi canvas { position: absolute; right: 0; bottom: 0; width: 55%; height: 46px; opacity: .9; }
|
||||||
|
.delta { font: 500 12px var(--mono); margin-left: 6px; }
|
||||||
|
|
||||||
|
/* ---------- 图表 ---------- */
|
||||||
|
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); gap: 16px; }
|
||||||
|
.chart canvas.plot { width: 100%; height: 260px; display: block; cursor: crosshair; }
|
||||||
|
.legend { display: flex; flex-wrap: wrap; gap: 6px 8px; margin-top: 10px; font-size: 12px; color: var(--dim); }
|
||||||
|
.legend span { cursor: pointer; user-select: none; padding: 2px 9px; border-radius: 99px; border: 1px solid var(--line); background: #0f1520; display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
.legend span b { font: 500 11.5px var(--mono); color: var(--text); }
|
||||||
|
.legend span.off { opacity: .4; } .legend i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||||
|
|
||||||
|
/* ---------- 底部面板 ---------- */
|
||||||
|
.side { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 16px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
th, td { text-align: left; padding: 7px 6px; border-bottom: 1px solid #1d2635; white-space: nowrap; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
th { color: var(--dim2); font-weight: 500; font-size: 12px; } td.n { font-family: var(--mono); text-align: right; } th.n { text-align: right; }
|
||||||
|
.ok { color: var(--ok); } .bad { color: var(--bad); } .warn { color: var(--warn); } .dim { color: var(--dim); }
|
||||||
|
.chip { display: inline-block; font-size: 11.5px; padding: 1px 8px; border-radius: 99px; border: 1px solid; }
|
||||||
|
.chip.ok { border-color: #34d39955; background: #34d39914; } .chip.bad { border-color: #f8717155; background: #f8717114; } .chip.dim { border-color: var(--line2); }
|
||||||
|
.chip.warn { border-color: #fbbf2455; background: #fbbf2414; }
|
||||||
|
.empty { display: flex; align-items: center; gap: 10px; color: var(--dim2); font-size: 13px; padding: 18px 4px; }
|
||||||
|
.empty svg { width: 18px; height: 18px; opacity: .6; }
|
||||||
|
.accbanner { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; margin-bottom: 8px; font-weight: 600; }
|
||||||
|
.accbanner.ok { background: #34d39914; color: var(--ok); border: 1px solid #34d39944; } .accbanner.bad { background: #f8717114; color: var(--bad); border: 1px solid #f8717144; }
|
||||||
|
.tip { position: fixed; pointer-events: none; background: #0a0e15f2; border: 1px solid var(--line2); border-radius: 10px; padding: 8px 10px; font: 12px/1.6 var(--mono); display: none; z-index: 30; white-space: nowrap; box-shadow: 0 10px 30px #0008; }
|
||||||
|
.tip .h { color: var(--dim); margin-bottom: 2px; }
|
||||||
|
footer { color: var(--dim2); font-size: 12px; text-align: center; padding-top: 6px; }
|
||||||
|
/* ---------- 控制:开始/停止、对话框、终端 ---------- */
|
||||||
|
button.primary { background: linear-gradient(135deg, var(--accent), var(--accent2)); border: none; color: #fff; font-weight: 600; padding: 7px 16px; box-shadow: 0 4px 16px #5aa9ff40; }
|
||||||
|
button.primary:hover { filter: brightness(1.08); } button.primary:disabled { opacity: .45; cursor: not-allowed; filter: none; }
|
||||||
|
button.danger { color: var(--bad); border-color: #f8717166; } button.danger:hover { background: #f8717114; }
|
||||||
|
.overall { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
||||||
|
.overall .now { font-size: 13px; color: var(--dim); } .overall .now b { display: block; color: var(--text); font-size: 18px; font-weight: 650; margin: 2px 0 2px; }
|
||||||
|
.sublabel { font-size: 12.5px; color: var(--dim); display: flex; justify-content: space-between; margin-top: 4px; }
|
||||||
|
.bar.thin { height: 8px; margin: 8px 0 4px; } .bar.ind .fill { width: 30% !important; animation: ind 1.6s ease-in-out infinite; }
|
||||||
|
@keyframes ind { 0% { margin-left: 0; } 50% { margin-left: 70%; } 100% { margin-left: 0; } }
|
||||||
|
.ctlrow { display: flex; gap: 10px; margin-top: 14px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.ctlrow .hint { font-size: 12px; color: var(--dim2); }
|
||||||
|
.term { background: #070a10; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; height: 260px; overflow: auto; font: 12px/1.55 var(--mono); color: #c9d4e3; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
.term .e { color: var(--bad); } .term .c { color: var(--accent); } .term .g { color: var(--ok); }
|
||||||
|
.modal { position: fixed; inset: 0; background: #0009; backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; z-index: 40; }
|
||||||
|
.modal.show { display: flex; }
|
||||||
|
.dlg { width: min(560px, 92vw); background: #131a27; border: 1px solid var(--line2); border-radius: 16px; padding: 20px 22px; box-shadow: 0 30px 80px #000a; }
|
||||||
|
.dlg h3 { margin: 0 0 4px; font-size: 16px; } .dlg .d { color: var(--dim); font-size: 12.5px; margin-bottom: 14px; }
|
||||||
|
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
|
||||||
|
.form label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--dim); }
|
||||||
|
.form input, .form select { background: #0f1520; color: var(--text); border: 1px solid var(--line2); border-radius: 8px; padding: 7px 9px; font: 13px var(--mono); }
|
||||||
|
.form .full { grid-column: 1 / -1; }
|
||||||
|
.ckline { margin-top: 12px; font-size: 12px; color: var(--dim); } .ckline.bad { color: var(--bad); } .ckline .warn { color: var(--warn); }
|
||||||
|
.demobar { background: #fbbf2418; border: 1px solid #fbbf2455; color: var(--warn); border-radius: 10px; padding: 8px 12px; margin: 10px 0; font-size: 13px; }
|
||||||
|
.dlg .acts { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
|
||||||
|
.dlg .warnbox { margin-top: 12px; font-size: 12px; color: var(--warn); background: #fbbf2410; border: 1px solid #fbbf2433; border-radius: 8px; padding: 8px 10px; display: none; }
|
||||||
|
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #1b2433; border: 1px solid var(--line2); padding: 9px 16px; border-radius: 10px; font-size: 13px; z-index: 50; display: none; box-shadow: 0 10px 30px #0008; }
|
||||||
|
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; } .charts { grid-template-columns: 1fr; } main { padding: 14px; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<div class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h6"/><path d="M7 20v-5l5-6"/><path d="M12 9l5 2"/><circle cx="12" cy="9" r="1.6"/><path d="M17 11l2-3"/></svg></div>
|
||||||
|
<div><h1>Transformer 多峰策略端到端训练与浏览器部署</h1><div class="sub">机械臂避障策略 · 训练监控</div></div>
|
||||||
|
</div>
|
||||||
|
<select id="run" title="选择运行(ml/runs/*)"></select>
|
||||||
|
<select id="scenario" title="示例场景" style="display:none"><option value="train">示例:预训练进行中</option><option value="gen">示例:生成数据中</option><option value="late">示例:挖错第 3 轮</option><option value="idle">示例:还没开始</option></select>
|
||||||
|
<span id="state" class="pill"><span class="dot"></span><span>连接中</span></span>
|
||||||
|
<span class="sp"></span>
|
||||||
|
<label class="ctl">平滑 <input id="smooth" type="range" min="0" max="0.99" step="0.01" value="0.85"><span id="smoothv" style="font-family:var(--mono);width:30px">0.85</span></label>
|
||||||
|
<button id="logy" title="损失图纵轴取对数">对数轴</button>
|
||||||
|
<button id="auto" class="on" title="每 5 秒刷新"><span>●</span>自动刷新</button>
|
||||||
|
<button id="btnStart" class="primary" title="一键运行流水线">▶ 开始训练</button>
|
||||||
|
<button id="btnStop" class="danger" style="display:none" title="停止正在运行的流水线">■ 停止</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card"><h2><span>流水线</span><span class="r" id="stepnote"></span></h2><div class="steps" id="steps"></div></section>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<div class="card" id="pipe"></div>
|
||||||
|
<div class="card"><h2><span>GPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" id="prog"></section>
|
||||||
|
<section class="kpis" id="kpis"></section>
|
||||||
|
<section class="charts" id="charts"></section>
|
||||||
|
|
||||||
|
<section class="side">
|
||||||
|
<div class="card"><h2><span>数据阶段</span><span class="r">ml/data</span></h2><div id="data"></div></div>
|
||||||
|
<div class="card"><h2><span>M5 头门禁</span><span class="r">纯策略穿柱率 ≤ 2%</span></h2><div id="m5"></div></div>
|
||||||
|
<div class="card"><h2><span>挖错循环</span><span class="r">ml/runs/mine</span></h2><div id="mine"></div></div>
|
||||||
|
<div class="card"><h2><span>部署形态评测</span><span class="r">策略 + 安全层</span></h2><div id="bench"></div></div>
|
||||||
|
<div class="card"><h2><span>验收汇总</span><span class="r">方案 §9.2</span></h2><div id="accept"></div></div>
|
||||||
|
</section>
|
||||||
|
<section class="card"><h2><span>终端输出</span><span class="r" id="termnote">ml/runs/console.log</span></h2><div class="term" id="term"></div></section>
|
||||||
|
<footer id="foot"></footer>
|
||||||
|
</main>
|
||||||
|
<div class="tip" id="tip"></div>
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
<div class="modal" id="modal"><div class="dlg">
|
||||||
|
<h3>开始训练</h3>
|
||||||
|
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
|
||||||
|
<div class="form">
|
||||||
|
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="fresh">从头重跑(旧产物移到 ml/archive/)</option></select></label>
|
||||||
|
<label>动作头 CFG<select id="fCfg"><option value="ml/configs/base.json">MeanFlow(base,M5 不过自动转 WTA)</option><option value="ml/configs/wta.json">WTA(wta)</option><option value="ml/configs/small.json">小模型 d=128(R4 不过时用)</option></select></label>
|
||||||
|
<label>共识过滤 CONSENSUS<select id="fCons"><option value="">按老师门禁自动</option><option value="0">关</option><option value="1">开(生成约慢 5 倍)</option></select></label>
|
||||||
|
<label>任务数 N_TASKS<input id="fN" value="50000"></label>
|
||||||
|
<label>生成进程数 NPROC<input id="fP" placeholder="默认 = CPU 核数"></label>
|
||||||
|
<label>目标样本 TARGET_SAMPLES<input id="fT" value="1700000"></label>
|
||||||
|
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
|
||||||
|
<label>老师间隙下限 MIN_GAP_MM<input id="fG" value="3"></label>
|
||||||
|
</div>
|
||||||
|
<div class="warnbox" id="fWarn"></div>
|
||||||
|
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
/* =====================================================================
|
||||||
|
数据层:python -m ml.monitor 提供 /api/runs、/api/log、/api/status。
|
||||||
|
直接双击打开本文件、地址带 ?demo=1,或 FORCE_DEMO=true 时用内置示例数据(每次刷新模拟训练推进)。
|
||||||
|
2026-10-18 新增(用户要求:训练中随时可视化查看进度)。
|
||||||
|
===================================================================== */
|
||||||
|
const FORCE_DEMO = false;
|
||||||
|
const DEMO = FORCE_DEMO || new URLSearchParams(location.search).has('demo') || location.protocol === 'file:';
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const isNum = v => typeof v === 'number' && isFinite(v);
|
||||||
|
const fmt = (v, d = 3) => !isNum(v) ? '—' : (Math.abs(v) >= 1000 ? Math.round(v).toLocaleString() : Math.abs(v) >= 100 ? v.toFixed(1) : Math.abs(v) >= 1 ? v.toFixed(Math.min(d, 3)) : v === 0 ? '0' : v.toPrecision(3));
|
||||||
|
const pct = (v, d = 1) => !isNum(v) ? '—' : (100 * v).toFixed(d) + '%';
|
||||||
|
const hms = s => { if (!isNum(s)) return '—'; s = Math.max(0, s); const h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60); return h ? `${h} 小时 ${m} 分` : m ? `${m} 分 ${Math.floor(s % 60)} 秒` : `${Math.floor(s)} 秒`; };
|
||||||
|
const hmsShort = s => !isNum(s) ? '—' : s >= 3600 ? `${(s / 3600).toFixed(1)} h` : s >= 60 ? `${Math.round(s / 60)} 分` : `${Math.round(s)} 秒`;
|
||||||
|
const ago = t => { const s = Date.now() / 1000 - t; return s < 60 ? `${Math.max(0, Math.floor(s))} 秒前` : s < 3600 ? `${Math.floor(s / 60)} 分钟前` : `${(s / 3600).toFixed(1)} 小时前`; };
|
||||||
|
const ICON_WAIT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>';
|
||||||
|
const empty = t => `<div class="empty">${ICON_WAIT}<span>${t}</span></div>`;
|
||||||
|
const KIND = { train: ['预训练', ''], apg: ['APG 微调', 'apg'], bench: ['测速', 'bench'] };
|
||||||
|
|
||||||
|
const api = DEMO ? demoApi() : async (path) => { const r = await fetch(path, { cache: 'no-store' }); if (!r.ok) throw new Error(r.status); return r.json(); };
|
||||||
|
if (DEMO) $('scenario').style.display = '';
|
||||||
|
|
||||||
|
/* ===================== 图表:零依赖 canvas ===================== */
|
||||||
|
const css = v => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
||||||
|
const COLORS = ['--c1', '--c2', '--c3', '--c4', '--c5', '--c6', '--c7'].map(css);
|
||||||
|
const tip = $('tip');
|
||||||
|
const hexA = (hex, a) => { const n = parseInt(hex.slice(1), 16); return `rgba(${n >> 16},${n >> 8 & 255},${n & 255},${a})`; };
|
||||||
|
|
||||||
|
class Chart {
|
||||||
|
constructor(parent, title, opts = {}) {
|
||||||
|
this.o = opts; this.hidden = new Set(opts.hidden || []);
|
||||||
|
const card = document.createElement('div'); card.className = 'card chart';
|
||||||
|
card.innerHTML = `<h2><span>${title}</span><span class="r">${opts.note || ''}</span></h2><canvas class="plot"></canvas><div class="legend"></div>`;
|
||||||
|
parent.appendChild(card);
|
||||||
|
this.cv = card.querySelector('canvas'); this.lg = card.querySelector('.legend'); this.series = [];
|
||||||
|
this.cv.addEventListener('mousemove', e => this.hover(e));
|
||||||
|
this.cv.addEventListener('mouseleave', () => { tip.style.display = 'none'; this.hx = null; this.draw(); });
|
||||||
|
new ResizeObserver(() => this.draw()).observe(this.cv);
|
||||||
|
}
|
||||||
|
set(series) {
|
||||||
|
this.series = series.map((s, i) => ({ ...s, color: s.color || COLORS[i % COLORS.length] }));
|
||||||
|
const last = s => { for (let i = s.pts.length - 1; i >= 0; i--) if (isNum(s.pts[i][1])) return s.pts[i][1]; return null; };
|
||||||
|
this.lg.innerHTML = this.series.filter(s => !s.noLegend).map(s => `<span data-k="${s.name}" class="${this.hidden.has(s.name) ? 'off' : ''}"><i style="background:${s.color}"></i>${s.label || s.name}<b>${this.o.percent ? pct(last(s)) : fmt(last(s))}</b></span>`).join('');
|
||||||
|
this.lg.querySelectorAll('span').forEach(el => el.onclick = () => { const k = el.dataset.k; this.hidden.has(k) ? this.hidden.delete(k) : this.hidden.add(k); el.classList.toggle('off'); this.draw(); });
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
vis() { return this.series.filter(s => !this.hidden.has(s.name) && s.pts.length); }
|
||||||
|
draw() {
|
||||||
|
const cv = this.cv, dpr = devicePixelRatio || 1, W = cv.clientWidth, H = cv.clientHeight;
|
||||||
|
if (!W) return;
|
||||||
|
cv.width = W * dpr; cv.height = H * dpr;
|
||||||
|
const g = cv.getContext('2d'); g.scale(dpr, dpr); g.clearRect(0, 0, W, H);
|
||||||
|
const S = this.vis(), L = 54, R = 14, T = 10, B = 24;
|
||||||
|
g.font = '11px ui-monospace, Menlo, Consolas, monospace';
|
||||||
|
if (!S.length) { g.fillStyle = css('--dim2'); g.textAlign = 'center'; g.fillText(this.o.emptyText || '暂无数据', W / 2, H / 2); this.map = null; return; }
|
||||||
|
const logy = this.o.logy && this.o.logy();
|
||||||
|
const ty = v => logy ? Math.log10(Math.max(v, 1e-12)) : v;
|
||||||
|
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||||
|
for (const s of S) for (const [x, y] of [...s.pts, ...(s.raw || [])]) { if (!isNum(y)) continue; x0 = Math.min(x0, x); x1 = Math.max(x1, x); if (s.noScale) continue; const v = ty(y); y0 = Math.min(y0, v); y1 = Math.max(y1, v); }
|
||||||
|
if (this.o.xmax && this.o.xmax()) x1 = Math.max(x1, this.o.xmax());
|
||||||
|
if (this.o.xmin0) x0 = Math.min(0, x0);
|
||||||
|
if (this.o.range) [y0, y1] = this.o.range;
|
||||||
|
if (this.o.zero && !logy) y0 = Math.min(0, y0);
|
||||||
|
if (!isFinite(y0)) { y0 = 0; y1 = 1; }
|
||||||
|
if (y1 - y0 < 1e-12) { y1 += 0.5; y0 -= 0.5; }
|
||||||
|
if (!this.o.range) { const pad = (y1 - y0) * 0.08; y1 += pad; if (!(this.o.zero && !logy && y0 === 0)) y0 -= pad; }
|
||||||
|
if (x1 === x0) x1 = x0 + 1;
|
||||||
|
const X = x => L + (x - x0) / (x1 - x0) * (W - L - R), Y = v => T + (1 - (v - y0) / (y1 - y0)) * (H - T - B);
|
||||||
|
this.map = { X, Y, x0, x1, L, R, W, ty };
|
||||||
|
// 网格与刻度
|
||||||
|
g.strokeStyle = '#1e2838'; g.lineWidth = 1; g.fillStyle = css('--dim2'); g.textAlign = 'right';
|
||||||
|
for (let i = 0; i <= 4; i++) {
|
||||||
|
const v = y0 + (y1 - y0) * i / 4, y = Math.round(Y(v)) + .5;
|
||||||
|
g.beginPath(); g.moveTo(L, y); g.lineTo(W - R, y); g.stroke();
|
||||||
|
g.fillText(logy ? fmt(10 ** v) : this.o.percent ? (100 * v).toFixed(0) + '%' : fmt(v), L - 8, y + 4);
|
||||||
|
}
|
||||||
|
g.textAlign = 'center';
|
||||||
|
for (let i = 0; i <= 5; i++) { const x = x0 + (x1 - x0) * i / 5; const lab = x >= 1000 ? (x / 1000).toFixed(x1 - x0 < 5000 ? 1 : 0) + 'k' : x.toFixed(0); g.fillText(lab, Math.max(L + 10, Math.min(X(x), W - R - 12)), H - 6); }
|
||||||
|
// 未到达区域(总步数之后)淡色
|
||||||
|
const lastX = Math.max(...S.map(s => s.pts.length ? s.pts[s.pts.length - 1][0] : -Infinity));
|
||||||
|
if (this.o.xmax && lastX < x1) { g.fillStyle = 'rgba(255,255,255,0.018)'; g.fillRect(X(lastX), T, X(x1) - X(lastX), H - T - B); }
|
||||||
|
// 参考线
|
||||||
|
for (const ref of this.o.refs || []) {
|
||||||
|
const y = Y(ty(ref.y)); g.strokeStyle = hexA(ref.color, .7); g.setLineDash([4, 4]); g.beginPath(); g.moveTo(L, y); g.lineTo(W - R, y); g.stroke(); g.setLineDash([]);
|
||||||
|
g.fillStyle = ref.color; g.textAlign = 'left'; g.fillText(ref.label, L + 6, y - 5);
|
||||||
|
}
|
||||||
|
const path = (pts) => { g.beginPath(); let f = true; for (const [x, y] of pts) { if (!isNum(y)) { f = true; continue; } const px = X(x), py = Y(ty(y)); f ? g.moveTo(px, py) : g.lineTo(px, py); f = false; } };
|
||||||
|
for (const s of S) {
|
||||||
|
if (s.raw) { path(s.raw); g.strokeStyle = hexA(s.color, .22); g.lineWidth = 1; g.stroke(); }
|
||||||
|
if (s.area) {
|
||||||
|
path(s.pts); const pts = s.pts.filter(p => isNum(p[1]));
|
||||||
|
if (pts.length > 1) {
|
||||||
|
g.lineTo(X(pts[pts.length - 1][0]), H - B); g.lineTo(X(pts[0][0]), H - B); g.closePath();
|
||||||
|
const gr = g.createLinearGradient(0, T, 0, H - B); gr.addColorStop(0, hexA(s.color, .28)); gr.addColorStop(1, hexA(s.color, 0)); g.fillStyle = gr; g.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path(s.pts); g.strokeStyle = s.color; g.lineWidth = s.width || 2; g.lineJoin = 'round'; g.lineCap = 'round'; g.setLineDash(s.dash || []); g.stroke(); g.setLineDash([]);
|
||||||
|
if (s.dots) for (const [x, y] of s.pts) if (isNum(y)) { g.beginPath(); g.arc(X(x), Y(ty(y)), 3, 0, 7); g.fillStyle = '#0b0f17'; g.fill(); g.lineWidth = 1.8; g.strokeStyle = s.color; g.stroke(); }
|
||||||
|
if (s.best) { const [bx, by] = s.best; g.beginPath(); g.arc(X(bx), Y(ty(by)), 6, 0, 7); g.strokeStyle = s.color; g.lineWidth = 1.5; g.stroke(); g.fillStyle = s.color; g.textAlign = 'left'; g.fillText('最好', X(bx) + 9, Y(ty(by)) - 6); }
|
||||||
|
}
|
||||||
|
if (this.hx != null) { g.strokeStyle = '#8c99ad88'; g.setLineDash([3, 3]); g.beginPath(); g.moveTo(X(this.hx), T); g.lineTo(X(this.hx), H - B); g.stroke(); g.setLineDash([]);
|
||||||
|
for (const s of S) { const p = nearest(s.pts, this.hx); if (p && isNum(p[1])) { g.beginPath(); g.arc(X(p[0]), Y(ty(p[1])), 4, 0, 7); g.fillStyle = s.color; g.fill(); } } }
|
||||||
|
}
|
||||||
|
hover(e) {
|
||||||
|
if (!this.map) return;
|
||||||
|
const r = this.cv.getBoundingClientRect(), { x0, x1, L, R, W } = this.map;
|
||||||
|
const x = x0 + (e.clientX - r.left - L) / (W - L - R) * (x1 - x0);
|
||||||
|
const rows = []; let hx = null;
|
||||||
|
for (const s of this.vis()) { if (s.noLegend) continue; const p = nearest(s.pts, x); if (p) { hx = hx ?? p[0]; rows.push(`<span style="color:${s.color}">●</span> ${s.label || s.name} ${this.o.percent ? pct(p[1], 2) : fmt(p[1], 4)}`); } }
|
||||||
|
this.hx = hx; this.draw();
|
||||||
|
if (!rows.length) return;
|
||||||
|
tip.innerHTML = `<div class="h">第 ${hx.toLocaleString()} 步</div>` + rows.join('<br>'); tip.style.display = 'block';
|
||||||
|
tip.style.left = Math.min(e.clientX + 16, innerWidth - tip.offsetWidth - 10) + 'px'; tip.style.top = (e.clientY + 16) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function nearest(pts, x) { let lo = 0, hi = pts.length - 1; if (hi < 0) return null; while (hi - lo > 1) { const m = (lo + hi) >> 1; pts[m][0] < x ? lo = m : hi = m; } return Math.abs(pts[lo][0] - x) <= Math.abs(pts[hi][0] - x) ? pts[lo] : pts[hi]; }
|
||||||
|
function spark(cv, pts, color) {
|
||||||
|
const dpr = devicePixelRatio || 1, W = cv.clientWidth, H = cv.clientHeight; if (!W || pts.length < 2) return;
|
||||||
|
cv.width = W * dpr; cv.height = H * dpr; const g = cv.getContext('2d'); g.scale(dpr, dpr);
|
||||||
|
const ys = pts.map(p => p[1]).filter(isNum); const lo = Math.min(...ys), hi = Math.max(...ys) || 1, x0 = pts[0][0], x1 = pts[pts.length - 1][0] || 1;
|
||||||
|
const X = x => (x - x0) / (x1 - x0 || 1) * W, Y = y => H - 4 - (y - lo) / ((hi - lo) || 1) * (H - 10);
|
||||||
|
g.beginPath(); pts.forEach(([x, y], i) => i ? g.lineTo(X(x), Y(y)) : g.moveTo(X(x), Y(y)));
|
||||||
|
g.strokeStyle = color; g.lineWidth = 1.6; g.stroke(); g.lineTo(W, H); g.lineTo(0, H); g.closePath();
|
||||||
|
const gr = g.createLinearGradient(0, 0, 0, H); gr.addColorStop(0, hexA(color, .25)); gr.addColorStop(1, hexA(color, 0)); g.fillStyle = gr; g.fill();
|
||||||
|
}
|
||||||
|
const thin = (pts, n = 400) => { if (pts.length <= n) return pts; const k = pts.length / n, o = []; for (let i = 0; i < n; i++) o.push(pts[Math.floor(i * k)]); o.push(pts[pts.length - 1]); return o; };
|
||||||
|
|
||||||
|
/* ===================== 状态 ===================== */
|
||||||
|
let cur = null, recs = [], evals = [], offset = 0, timer = null, charts = null, lastMtime = 0, lastStatus = null, runsCache = [];
|
||||||
|
let logyOn = false, job = null, userPicked = false, conOff = 0;
|
||||||
|
/* 阶段表:名称、粗估耗时(小时,作全流程百分比权重)、超时上限。
|
||||||
|
2026-10-18 第五轮(审查 N-14):真实模式下由 /api/status 的 stage_meta 下发(来源 ml/pstate.py STAGES,与超时同源);
|
||||||
|
下面这份只给示例数据用,数值与 pstate.py 一致。 */
|
||||||
|
let STAGE_META = [
|
||||||
|
['setup', '安装依赖', 0.2], ['kin', '对拍', 0.2], ['r4', '推理延迟', 0.2], ['gate1', '一致性门禁', 0.5], ['gen', '生成老师轨迹', 5.0],
|
||||||
|
['prep', '打包样本', 0.5], ['train', '预训练', 3.0], ['m5', '头门禁', 0.5], ['apg', 'APG 微调', 4.0], ['mine', '挖错循环', 9.0],
|
||||||
|
['export', '导出', 0.1], ['bench', '部署评测', 1.0], ['accept', '验收汇总', 0.5]];
|
||||||
|
let SM = Object.fromEntries(STAGE_META.map(([k, n, w]) => [k, { n, w, to: Math.max(1, 3 * w) }]));
|
||||||
|
function applyStageMeta(m) {
|
||||||
|
if (!m || !m.length) return;
|
||||||
|
STAGE_META = m.map(x => [x.id, x.name, x.est_h]); SM = Object.fromEntries(m.map(x => [x.id, { n: x.name, w: x.est_h, to: x.timeout_h }]));
|
||||||
|
}
|
||||||
|
/* 开始 / 停止请求带自定义头:跨站网页发不出这种请求(会触发 CORS 预检,控制台不应答),见 monitor.py 审查 N-8 */
|
||||||
|
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'X-Console': '1', 'Content-Type': 'application/json' }, body: body || '{}' }).then(x => x.json()).catch(e => ({ error: String(e) }));
|
||||||
|
const smoothv = () => +$('smooth').value;
|
||||||
|
const ema = (pts, a) => { if (!a) return pts; let m = null; return pts.map(([x, y]) => { if (!isNum(y)) return [x, y]; m = m == null ? y : a * m + (1 - a) * y; return [x, m]; }); };
|
||||||
|
const xOf = r => r.step ?? r.it ?? 0;
|
||||||
|
|
||||||
|
function buildCharts(kind) {
|
||||||
|
const c = $('charts'); c.innerHTML = '';
|
||||||
|
const total = () => { const r = recs[recs.length - 1]; return r && r.total; };
|
||||||
|
if (kind === 'apg') {
|
||||||
|
charts = {
|
||||||
|
a: new Chart(c, '到达误差(mm,展开末)', { logy: () => logyOn, xmax: total, xmin0: true, note: '越低越好 · 浅色为原始值' }),
|
||||||
|
b: new Chart(c, '损失分量', { logy: () => logyOn, xmax: total, xmin0: true }),
|
||||||
|
e: new Chart(c, '闭环评估(val,部署节奏)', { percent: true, range: [0, 1], xmax: total, xmin0: true, note: '每 500 步', emptyText: '第一次评估在第 500 步' }),
|
||||||
|
t: new Chart(c, '展开长度与梯度', { xmax: total, xmin0: true, zero: true, note: '展开 60–150 次推理随批变化' }),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
charts = {
|
||||||
|
a: new Chart(c, '总损失', { logy: () => logyOn, xmax: total, xmin0: true, note: '浅色为原始值' }),
|
||||||
|
b: new Chart(c, '损失分量', { logy: () => logyOn, xmax: total, xmin0: true, hidden: ['smooth'] }),
|
||||||
|
e: new Chart(c, '闭环评估(val,部署节奏,关安全层)', { percent: true, range: [0, 1], xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : '每 2000 步',
|
||||||
|
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
|
||||||
|
t: new Chart(c, '吞吐(样本/s)', { xmax: total, xmin0: true, zero: true, refs: [{ y: 5000, label: 'R10 通过线 5000', color: css('--bad') }] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!cur || !charts) return;
|
||||||
|
const a = smoothv(), kind = cur.kind;
|
||||||
|
const raw = k => thin(recs.filter(r => isNum(r[k])).map(r => [xOf(r), r[k]]));
|
||||||
|
const S = (k, label, extra = {}) => ({ name: k, label: label || k, pts: thin(ema(recs.filter(r => isNum(r[k])).map(r => [xOf(r), r[k]]), a)), ...extra });
|
||||||
|
if (kind === 'apg') {
|
||||||
|
charts.a.set([S('err_mm_mean', '平均到达误差', { raw: raw('err_mm_mean'), area: true })]);
|
||||||
|
charts.b.set([S('tgt', '到达项'), S('col', '碰撞铰链'), S('bc', 'BC 锚定'), S('smooth', '平滑 /10', { pts: thin(ema(recs.filter(r => isNum(r.smooth)).map(r => [xOf(r), r.smooth / 10]), a)) })]);
|
||||||
|
charts.t.set([{ name: 'plans', label: '展开推理次数', pts: thin(recs.filter(r => r.plans).map(r => [xOf(r), r.plans])), width: 1.4 }, S('gnorm', '梯度范数 ×50', { pts: thin(ema(recs.filter(r => isNum(r.gnorm)).map(r => [xOf(r), r.gnorm * 50]), a)) })]);
|
||||||
|
} else {
|
||||||
|
charts.a.set([S('loss', '总损失', { raw: raw('loss'), area: true })]);
|
||||||
|
charts.b.set([S('mf', cur.name.includes('wta') ? 'WTA' : 'MeanFlow'), S('fk_cm', 'FK(cm)'), S('value', '价值头'), S('sub', '子目标头'), S('smooth', '平滑')]);
|
||||||
|
charts.t.set([S('samples_per_s', '样本/s', { area: true })]);
|
||||||
|
}
|
||||||
|
const E = (k, label, best) => { const pts = evals.filter(e => isNum(e[k])).map(e => [e.x, e[k]]); let b = null; if (best && pts.length) b = pts.reduce((m, p) => p[1] > m[1] ? p : m); return { name: k, label, dots: true, pts, best: b }; };
|
||||||
|
charts.e.set([E('success', '到达率', true), E('collision_rate', '碰撞率'), E('time_ok_1p3', '≤ 老师时长×1.3'), E('start_fail', '起步失败')]);
|
||||||
|
renderProgress(); renderKpis();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgress() {
|
||||||
|
const r = recs[recs.length - 1] || {}, kind = cur.kind;
|
||||||
|
const step = xOf(r), total = r.total, done = total ? Math.min(1, step / total) : null;
|
||||||
|
const t0 = recs.find(v => v.t)?.t, elapsed = r.t && t0 ? r.t - t0 : (isNum(r.min) ? r.min * 60 : null);
|
||||||
|
let eta = isNum(r.eta_h) ? r.eta_h * 3600 : null;
|
||||||
|
if (eta == null && elapsed && done) eta = elapsed / done * (1 - done);
|
||||||
|
const running = !DEMO ? (Date.now() / 1000 - lastMtime < 120) : cur.live;
|
||||||
|
const finished = done != null && done >= 0.999;
|
||||||
|
const bestE = evals.length ? evals.reduce((m, e) => (e.success ?? -1) > (m.success ?? -1) ? e : m) : null;
|
||||||
|
const marks = total ? evals.map(e => `<div class="mk ${bestE && e === bestE ? 'best' : ''}" style="left:${100 * e.x / total}%" title="评估 @${e.x}:到达 ${pct(e.success)}"></div>`).join('') : '';
|
||||||
|
const [kl, kc] = KIND[kind];
|
||||||
|
const fin = eta != null && running && !finished ? new Date(Date.now() + eta * 1000).toLocaleString('zh-CN', { hour12: false, month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
|
||||||
|
const fact = (k, v, cls = '') => `<div class="fact"><div class="k">${k}</div><div class="v ${cls}">${v}</div></div>`;
|
||||||
|
$('prog').innerHTML = `<h2><span>当前运行 · 训练曲线</span><span class="r">${finished ? '已完成' : running ? '运行中' : '未在运行'}</span></h2>
|
||||||
|
<div class="prog-top"><div class="ttl"><b>${cur.name}</b><span class="tag ${kc}">${kl}</span>${cur.diverged ? ' <span class="chip bad">已发散 · diverged.pt</span>' : ''}</div>
|
||||||
|
<div class="bigpct">${done != null ? (100 * done).toFixed(1) : '—'}<small>%</small></div></div>
|
||||||
|
<div class="bar ${running && !finished ? '' : 'idle'}"><div class="fill" style="width:${done != null ? 100 * done : 0}%"></div>${marks}</div>
|
||||||
|
<div class="facts">
|
||||||
|
${fact('步数', `${step.toLocaleString()}${total ? ' / ' + total.toLocaleString() : ''}`)}
|
||||||
|
${fact('已用时间', hms(elapsed))}
|
||||||
|
${fact('剩余时间', finished ? '—' : hms(eta))}
|
||||||
|
${fact('预计完成', fin)}
|
||||||
|
${kind === 'apg' ? fact('本批展开', r.plans ? `${r.plans} 次推理` : '—') : fact('学习率', isNum(r.lr) ? r.lr.toExponential(2) : '—')}
|
||||||
|
${fact('日志更新', lastMtime ? ago(lastMtime) : '—', running ? '' : 'warn')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKpis() {
|
||||||
|
const r = recs[recs.length - 1] || {}, e = evals[evals.length - 1] || {}, pe = evals[evals.length - 2] || {}, kind = cur.kind;
|
||||||
|
const best = evals.length ? Math.max(...evals.map(v => v.success ?? -1)) : null;
|
||||||
|
const dlt = (k, good = 1) => { if (!isNum(e[k]) || !isNum(pe[k])) return ''; const d = (e[k] - pe[k]) * 100; if (Math.abs(d) < 0.05) return ''; const up = d > 0; return `<span class="delta ${(up ? 1 : -1) * good > 0 ? 'ok' : 'bad'}">${up ? '▲' : '▼'} ${Math.abs(d).toFixed(1)}pp</span>`; };
|
||||||
|
const last = k => recs.filter(v => isNum(v[k])).slice(-300).map(v => [xOf(v), v[k]]);
|
||||||
|
const evs = k => evals.filter(v => isNum(v[k])).map(v => [v.x, v[k]]);
|
||||||
|
const tiles = [];
|
||||||
|
const tile = (label, v, s, cls, sp, color) => tiles.push({ html: `<div class="card kpi"><h2><span>${label}</span></h2><div class="v ${cls || ''}">${v}</div><div class="s">${s || ' '}</div><canvas></canvas></div>`, sp, color });
|
||||||
|
if (kind === 'apg') {
|
||||||
|
tile('到达误差', `${fmt(r.err_mm_mean)}<small>mm</small>`, '展开末平均', '', last('err_mm_mean'), COLORS[0]);
|
||||||
|
tile('展开内到达比例', pct(r.reach_frac), '误差 < 1 mm 且姿态 < 0.5°', '', last('reach_frac'), COLORS[2]);
|
||||||
|
tile('闭环到达率', pct(e.success) + dlt('success'), `最好 ${pct(best)}`, '', evs('success'), COLORS[2]);
|
||||||
|
tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]);
|
||||||
|
tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}<small>GiB</small>` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]);
|
||||||
|
} else {
|
||||||
|
const sps = r.samples_per_s;
|
||||||
|
tile('总损失', fmt(r.loss, 4), `MF ${fmt(r.mf)} · FK ${fmt(r.fk_cm)} cm`, '', ema(last('loss'), 0.9), COLORS[0]);
|
||||||
|
tile('吞吐', sps ? `${(sps / 1000).toFixed(2)}<small>k 样本/s</small>` : '—', sps ? (sps >= 5000 ? 'R10 达标(≥ 5k/s)' : 'R10 未达标(< 5k/s)') : 'R10 通过线 5k/s', sps ? (sps >= 5000 ? 'ok' : 'bad') : '', last('samples_per_s'), COLORS[5]);
|
||||||
|
tile('闭环到达率', pct(e.success) + dlt('success'), evals.length ? `最好 ${pct(best)} · @${e.x.toLocaleString()} 步` : '尚未评估', '', evs('success'), COLORS[2]);
|
||||||
|
tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层,部署另有护栏', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]);
|
||||||
|
tile('梯度范数', fmt(r.gnorm), '裁剪阈值 1.0', r.gnorm > 5 ? 'warn' : '', ema(last('gnorm'), 0.9), COLORS[4]);
|
||||||
|
tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}<small>GiB</small>` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]);
|
||||||
|
}
|
||||||
|
$('kpis').innerHTML = tiles.map(t => t.html).join('');
|
||||||
|
[...$('kpis').querySelectorAll('canvas')].forEach((cv, i) => spark(cv, tiles[i].sp, tiles[i].color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 轮询 ===================== */
|
||||||
|
async function loadRuns() {
|
||||||
|
const runs = await api('/api/runs'); runsCache = runs;
|
||||||
|
const sel = $('run'), prev = sel.value;
|
||||||
|
sel.innerHTML = runs.map(r => `<option value="${r.name}">${r.name} · ${KIND[r.kind][0]}${r.diverged ? ' · 发散' : ''}</option>`).join('') || '<option value="">ml/runs 下还没有日志</option>';
|
||||||
|
const follow = lastStatus && lastStatus.pipeline && lastStatus.pipeline.progress && lastStatus.pipeline.progress.run;
|
||||||
|
if (!userPicked) sel.value = runs.some(r => r.name === follow) ? follow : (runs[0] ? runs[0].name : ''); // 自动跟随正在跑的那条
|
||||||
|
else if (runs.some(r => r.name === prev)) sel.value = prev;
|
||||||
|
const r = runs.find(v => v.name === sel.value) || runs[0];
|
||||||
|
if (r && (!cur || cur.name !== r.name)) { cur = { ...r }; recs = []; evals = []; offset = 0; buildCharts(r.kind); }
|
||||||
|
else if (r) Object.assign(cur, r);
|
||||||
|
if (!r) { cur = null; $('charts').innerHTML = ''; $('kpis').innerHTML = ''; $('prog').innerHTML = '<h2><span>当前运行 · 训练曲线</span></h2>' + empty('还没有训练日志:预训练开始后这里出现损失、吞吐和评估曲线。'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
lastStatus = await api('/api/status'); applyStageMeta(lastStatus.stage_meta);
|
||||||
|
job = await api('/api/job').catch(() => null);
|
||||||
|
await loadRuns();
|
||||||
|
if (cur) {
|
||||||
|
const d = await api(`/api/log?run=${encodeURIComponent(cur.name)}&offset=${offset}`);
|
||||||
|
if (d.reset && offset) { recs = []; evals = []; }
|
||||||
|
for (const r of d.records || []) {
|
||||||
|
if (r.eval) { const e = r.eval; evals.push({ ...e, x: e.step ?? e.it ?? r.step ?? 0 }); } else recs.push(r);
|
||||||
|
}
|
||||||
|
offset = d.offset ?? offset; lastMtime = d.mtime || lastMtime; cur.live = d.live;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
const P = lastStatus.pipeline, busy = (job && job.running) || (P && P.current);
|
||||||
|
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
|
||||||
|
else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`);
|
||||||
|
else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`);
|
||||||
|
else setPill('', '空闲');
|
||||||
|
renderStatus(lastStatus); renderPipe(lastStatus); renderJob();
|
||||||
|
await pollConsole();
|
||||||
|
} catch (err) {
|
||||||
|
setPill('dead', '连不上控制台(双击 ml/console.bat 或 python -m ml.monitor)'); console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setPill(cls, text) { const st = $('state'); st.className = 'pill ' + cls; st.lastElementChild.textContent = text; }
|
||||||
|
|
||||||
|
/* ===================== 流水线 / GPU / 各阶段面板 ===================== */
|
||||||
|
function stages(s) {
|
||||||
|
const P = s.pipeline;
|
||||||
|
if (P) { // 有流水线记录:以它为准
|
||||||
|
const plan = P.plan && P.plan.length ? P.plan : STAGE_META.map(x => x[0]).filter(k => k !== 'setup');
|
||||||
|
const keys = STAGE_META.map(x => x[0]).filter(k => plan.includes(k) || P.stages[k]);
|
||||||
|
return keys.map(k => {
|
||||||
|
const r = P.stages[k], nm = SM[k].n;
|
||||||
|
if (!r) return [nm, 'pending', '—'];
|
||||||
|
if (P.current === k) return [nm, 'active', P.progress && P.progress.frac != null ? pct(P.progress.frac, 0) : '进行中'];
|
||||||
|
if (r.rc === 0) return [nm, 'done', '用时 ' + hmsShort(r.end - r.start)];
|
||||||
|
if (r.rc === -15 || r.rc === 143) return [nm, 'fail', '已停止'];
|
||||||
|
if (r.rc != null) return [nm, 'fail', `失败(返回码 ${r.rc})`];
|
||||||
|
return [nm, 'fail', '中断'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const D = s.data || {}, has = n => runsCache.find(r => r.name === n || r.name.startsWith(n));
|
||||||
|
const live = n => { const r = has(n); return r && Date.now() / 1000 - r.mtime < 120; };
|
||||||
|
const anyTrain = runsCache.find(r => r.kind === 'train');
|
||||||
|
const m5ok = s.m5 && s.m5.some(m => m.pass), m5fail = s.m5 && s.m5.length && !m5ok;
|
||||||
|
const mineN = (s.mine || []).length, mineDone = (s.mine || []).filter(m => m.gate && m.gate.accept != null).length;
|
||||||
|
const L = [
|
||||||
|
['对拍', D.kin_pass != null ? (D.kin_pass ? 'done' : 'fail') : (D.consistency != null ? 'done' : 'pending'), '—'],
|
||||||
|
['老师门禁', D.consistency != null ? 'done' : 'pending', D.consistency != null ? `一致率 ${pct(D.consistency, 0)}` : '—'],
|
||||||
|
['生成数据', D.stats ? 'done' : D.gen_tasks_written ? 'active' : 'pending', D.stats ? `${(D.stats.tasks / 1000).toFixed(1)}k 条` : D.gen_tasks_written ? `${D.gen_tasks_written.toLocaleString()} 条…` : '—'],
|
||||||
|
['打包样本', D.index ? 'done' : 'pending', D.index ? `${(D.index.train_samples / 1e6).toFixed(2)}M 样本` : '—'],
|
||||||
|
['预训练', s.m5 && s.m5.length ? 'done' : anyTrain ? (live(anyTrain.name) ? 'active' : 'done') : 'pending', anyTrain ? anyTrain.name : '—'],
|
||||||
|
['M5 门禁', m5ok ? 'done' : m5fail ? 'fail' : 'pending', m5ok ? '通过' : m5fail ? '未通过' : '—'],
|
||||||
|
['APG 微调', has('apg') && !has('apg_r') ? (live('apg') ? 'active' : 'done') : has('apg_r') || mineN ? 'done' : 'pending', has('apg') ? 'apg' : '—'],
|
||||||
|
['挖错循环', mineN ? (mineDone === mineN && !live('apg_r') ? 'done' : 'active') : 'pending', mineN ? `第 ${mineN} 轮` : '—'],
|
||||||
|
['导出/评测', s.bench ? 'done' : 'pending', s.bench ? '已评测' : '—'],
|
||||||
|
['验收汇总', s.acceptance ? (s.acceptance.all_pass ? 'done' : 'fail') : 'pending', s.acceptance ? (s.acceptance.all_pass ? '全部通过' : `${s.acceptance.failed.length} 项未过`) : '—'],
|
||||||
|
];
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
function renderStatus(s) {
|
||||||
|
const L = stages(s);
|
||||||
|
$('steps').innerHTML = L.map(([nm, st, ds], i) => `<div class="step ${st}"><div class="ic">${st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1}</div><div class="nm">${nm}</div><div class="ds">${ds}</div></div>`).join('');
|
||||||
|
const nDone = L.filter(x => x[1] === 'done').length; $('stepnote').textContent = `${nDone} / ${L.length} 完成`;
|
||||||
|
|
||||||
|
// GPU
|
||||||
|
const G = s.gpu || [];
|
||||||
|
const BUD = (s.vram_budget_gb || 24) * 1024; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看
|
||||||
|
$('gputime').textContent = G.length ? '实时' : '';
|
||||||
|
$('gpu').innerHTML = G.length ? G.map(g => {
|
||||||
|
const u = g.util ?? 0, C = 2 * Math.PI * 50, col = u > 85 ? css('--ok') : u > 50 ? css('--accent') : css('--warn');
|
||||||
|
const memP = 100 * g.mem_used / BUD, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6');
|
||||||
|
const row = (k, v, p, c) => `<div class="gr"><div class="l"><span>${k}</span><b>${v}</b></div><div class="mini"><div style="width:${Math.min(100, p)}%;background:${c}"></div></div></div>`;
|
||||||
|
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
|
||||||
|
<circle cx="58" cy="58" r="50" stroke="${col}" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="${C}" stroke-dashoffset="${C * (1 - u / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
|
||||||
|
<div class="c"><div><b>${u}%</b><span>利用率</span></div></div></div>
|
||||||
|
<div class="gpurows">${row('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB 预算(整卡 ${(g.mem_total / 1024).toFixed(0)})`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'))}
|
||||||
|
${row('温度', `${g.temp} °C`, g.temp, tc)}${row('功耗', g.power != null ? `${Math.round(g.power)} W` : '—', g.power ? g.power / 3.5 : 0, css('--c7'))}</div></div>
|
||||||
|
<div class="gname">${g.name}</div>`;
|
||||||
|
}).join('<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">') : empty('未检测到 nvidia-smi');
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const D = s.data || {}, st = D.stats, ix = D.index;
|
||||||
|
const row = (k, v, cls = '') => `<tr><td>${k}</td><td class="n ${cls}">${v}</td></tr>`;
|
||||||
|
if (D.consistency == null && !st && !ix && !D.gen_tasks_written) $('data').innerHTML = empty('还没开始生成数据');
|
||||||
|
else {
|
||||||
|
let h = '<table>';
|
||||||
|
h += row('老师一致性', D.consistency == null ? '—' : pct(D.consistency), D.consistency == null ? '' : D.consistency >= 0.9 ? 'ok' : 'warn');
|
||||||
|
h += row('已生成任务', D.gen_tasks_written ? `${D.gen_tasks_written.toLocaleString()} 条 · ${D.shards} 个分片` : '—');
|
||||||
|
if (st) {
|
||||||
|
h += row('保留率', fmt(st.keepRate), st.keepRate >= 0.75 ? 'ok' : 'bad');
|
||||||
|
if (st.gapDrop != null) h += row(`间隙 < ${st.minGapFilterMm ?? 3} mm 剔除`, `${st.gapDrop.toLocaleString()}(${pct(st.gapDrop / Math.max(1, st.keep + st.gapDrop))})`, st.gapDrop / Math.max(1, st.keep + st.gapDrop) > 0.25 ? 'bad' : '');
|
||||||
|
h += row('样本数(每 3 步)', (st.samplesEvery3 || 0).toLocaleString(), st.samplesEvery3 >= 1.7e6 ? 'ok' : 'warn');
|
||||||
|
if (st.consensusDrop != null) h += row('共识丢弃', st.consensusDrop.toLocaleString());
|
||||||
|
if (st.vertexMinGapMm) h += row('最小间隙 p10 / p50 / p90', `${st.vertexMinGapMm.p10 ?? '—'} / ${st.vertexMinGapMm.p50} / ${st.vertexMinGapMm.p90} mm`);
|
||||||
|
}
|
||||||
|
if (ix) h += row('训练 / 验证样本', `${ix.train_samples.toLocaleString()} / ${ix.val_samples.toLocaleString()}`);
|
||||||
|
if (D.r2) h += row('R2 近邻标签 std', D.r2.pass == null ? '未判定' : `${fmt(D.r2.label_std_mean)}(< 0.10)`, D.r2.pass == null ? 'dim' : D.r2.pass ? 'ok' : 'bad');
|
||||||
|
$('data').innerHTML = h + '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('m5').innerHTML = s.m5 && s.m5.length ? '<table><tr><th>动作头</th><th class="n">纯策略穿柱</th><th class="n">到达</th><th class="n">加安全层穿柱</th><th class="n">结论</th></tr>' + s.m5.map(m =>
|
||||||
|
`<tr><td>${m.head}</td><td class="n ${m.policy_only.collision_rate <= 0.02 ? 'ok' : 'bad'}">${pct(m.policy_only.collision_rate, 2)}</td><td class="n">${pct(m.policy_only.success)}</td><td class="n">${pct(m.with_sim_safety.collision_rate, 2)}</td><td class="n"><span class="chip ${m.pass ? 'ok' : 'bad'}">${m.pass ? '通过' : '不通过'}</span></td></tr>`).join('')
|
||||||
|
+ `</table><div class="dim" style="margin-top:8px;font-size:12px">选定模型:<span style="font-family:var(--mono)">${s.selected || '—'}</span></div>` : empty('预训练完成后运行 m5');
|
||||||
|
|
||||||
|
$('mine').innerHTML = s.mine && s.mine.length ? '<table><tr><th>轮</th><th class="n">E1 撞</th><th class="n">E2 极小</th><th class="n">正确</th><th class="n">提升</th><th class="n">R7 左右</th><th class="n">门禁</th></tr>' + s.mine.map(m => {
|
||||||
|
const r = m.report || {}, g = m.gate || {};
|
||||||
|
const chip = g.accept == null ? '<span class="chip dim">进行中</span>' : g.accept ? '<span class="chip ok">接受</span>' : '<span class="chip bad">回滚</span>';
|
||||||
|
return `<tr><td>${m.round}</td><td class="n">${pct(r.E1, 2)}</td><td class="n">${pct(r.E2, 2)}</td><td class="n">${pct(r.ok)}</td><td class="n">${isNum(g.gain_pp) ? '+' + g.gain_pp.toFixed(1) + 'pp' : '—'}</td><td class="n">${isNum(g.R7_left_share_shift_pp) ? g.R7_left_share_shift_pp.toFixed(1) + 'pp' : '—'}</td><td class="n">${chip}</td></tr>`;
|
||||||
|
}).join('') + '</table>' : empty('APG 之后开始挖错');
|
||||||
|
|
||||||
|
$('bench').innerHTML = s.bench ? '<table><tr><th>集合</th><th class="n">通过</th><th class="n">穿模帧</th><th class="n">刹停率</th><th class="n">推理 P99</th></tr>' + Object.entries(s.bench).map(([k, v]) =>
|
||||||
|
`<tr><td>${k}</td><td class="n ${v.ok === v.n ? 'ok' : 'bad'}">${v.ok}/${v.n}</td><td class="n ${v.collisionFrames ? 'bad' : 'ok'}">${v.collisionFrames}</td><td class="n">${pct(v.brakeRate, 2)}</td><td class="n">${v.inferMsP99 ?? '—'} ms</td></tr>`).join('') + '</table>' : empty('导出 ONNX 后运行 bench');
|
||||||
|
|
||||||
|
const A = s.acceptance;
|
||||||
|
$('accept').innerHTML = A ? `<div class="accbanner ${A.all_pass ? 'ok' : 'bad'}">${A.all_pass ? '✓ 全部通过' : `✕ ${A.failed.length} 项未通过`}</div><table>` + A.items.map(i =>
|
||||||
|
`<tr><td>${i.item}</td><td class="n">${typeof i.value === 'number' ? (/率|比例/.test(i.item) && i.value <= 1 ? pct(i.value, 1) : fmt(i.value)) : (Array.isArray(i.value) || i.value == null ? '' : i.value)}</td><td class="n"><span class="chip ${i.pass === true ? 'ok' : i.pass === false ? 'bad' : 'dim'}">${i.pass === true ? '通过' : i.pass === false ? '未过' : '—'}</span></td></tr>`).join('') + '</table>' : empty('最后一步 accept');
|
||||||
|
|
||||||
|
$('foot').textContent = `${DEMO ? '示例数据 · ' : ''}每 5 秒刷新 · 网页可随时关闭,训练不受影响 · ${new Date().toLocaleTimeString('zh-CN', { hour12: false })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 全流程卡片 / 开始停止 / 终端 ===================== */
|
||||||
|
function renderPipe(s) {
|
||||||
|
const P = s.pipeline, running = job && job.running;
|
||||||
|
const ck = s.check || {}, lk = s.lock;
|
||||||
|
const ckline = ck.pending ? '<div class="ckline">环境自检中…</div>'
|
||||||
|
: ck.problems && ck.problems.length ? `<div class="ckline bad">✕ ${ck.problems.join(';')}</div>`
|
||||||
|
: ck.python ? `<div class="ckline">Python ${ck.python} · torch ${ck.torch}${ck.gpu ? ' · ' + ck.gpu : ''}${ck.warn ? ` · <span class="warn">${ck.warn}</span>` : ''}</div>` : '';
|
||||||
|
const who = job && job.external ? `由${(lk && lk.owner || '').startsWith('网页') ? '上一个控制台窗口' : '终端'}启动(pid ${lk && lk.pid},${Math.round(lk && lk.age_s || 0)} 秒前心跳)` : job && job.pid ? `PID ${job.pid}` : '';
|
||||||
|
const btns = `${ckline}<div class="ctlrow">${running || (P && P.current)
|
||||||
|
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint">${who}</span>`
|
||||||
|
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
|
||||||
|
if (!P) { $('pipe').innerHTML = `<h2><span>全流程</span><span class="r">尚未开始</span></h2>${empty('还没运行过流水线。点「开始训练」从对拍一直跑到验收;中途每一步的进度都显示在这里。')}${btns}`; return; }
|
||||||
|
const plan = P.plan && P.plan.length ? P.plan : Object.keys(P.stages);
|
||||||
|
let tot = 0, got = 0;
|
||||||
|
for (const k of plan) { const w = SM[k]?.w || 0.1, r = P.stages[k]; tot += w; if (r && r.rc === 0 && P.current !== k) got += w; else if (P.current === k) got += w * ((P.progress && P.progress.frac) || 0); }
|
||||||
|
const frac = tot ? got / tot : 0, now = Date.now() / 1000;
|
||||||
|
const cur = P.current, cr = cur && P.stages[cur], pr = P.progress || {};
|
||||||
|
const doneN = plan.filter(k => P.stages[k] && P.stages[k].rc === 0 && P.current !== k).length;
|
||||||
|
const failed = plan.find(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0);
|
||||||
|
const allDone = !cur && doneN === plan.length;
|
||||||
|
let remain = 0; for (const k of plan) { const r = P.stages[k]; if (!r || r.rc == null) remain += (SM[k]?.w || 0.1) * (k === cur ? 1 - (pr.frac || 0) : 1); }
|
||||||
|
const head = cur ? `<div class="now">正在运行<b>${SM[cur]?.n || cur}</b>${pr.label || ''}</div>`
|
||||||
|
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${plan.length} 步</div>`
|
||||||
|
: failed ? `<div class="now">状态<b class="bad">停在「${SM[failed]?.n || failed}」</b>${(P.stages[failed].info || {}).timed_out || P.stages[failed].rc === 124 ? '超过时间上限,判定卡死' : P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已停止' : '失败,看终端输出'}</div>`
|
||||||
|
: `<div class="now">状态<b>未在运行</b>已完成 ${doneN} / ${plan.length} 步</div>`;
|
||||||
|
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${plan.length} 步完成</span></h2>
|
||||||
|
<div class="overall">${head}<div class="bigpct">${(100 * frac).toFixed(1)}<small>%</small></div></div>
|
||||||
|
<div class="bar ${cur ? '' : 'idle'}"><div class="fill" style="width:${100 * frac}%"></div></div>
|
||||||
|
${cur ? `<div class="sublabel"><span>当前步骤 · ${SM[cur]?.n}</span><span>${pr.frac != null ? pct(pr.frac) : '无法计量,按用时估'}</span></div>
|
||||||
|
<div class="bar thin ${pr.frac == null ? 'ind' : ''}"><div class="fill" style="width:${pr.frac != null ? 100 * pr.frac : 30}%"></div></div>` : ''}
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><div class="k">总用时</div><div class="v">${hms((cur ? now : Math.max(...Object.values(P.stages).map(r => r.end || r.start))) - P.started)}</div></div>
|
||||||
|
<div class="fact"><div class="k">本步用时</div><div class="v">${cr ? hms(now - cr.start) : '—'}</div></div>
|
||||||
|
<div class="fact"><div class="k">本步粗估 / 上限</div><div class="v">${cur ? hmsShort((SM[cur]?.w || 0) * 3600) + ' / ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600) : '—'}</div></div>
|
||||||
|
<div class="fact"><div class="k">全流程剩余(粗估)</div><div class="v">${allDone ? '—' : '约 ' + hmsShort(remain * 3600)}</div></div>
|
||||||
|
</div>${btns}`;
|
||||||
|
}
|
||||||
|
function renderJob() {
|
||||||
|
const busy = (job && job.running) || (lastStatus && lastStatus.pipeline && lastStatus.pipeline.current);
|
||||||
|
$('btnStart').style.display = busy ? 'none' : ''; $('btnStop').style.display = busy ? '' : 'none';
|
||||||
|
}
|
||||||
|
async function pollConsole() {
|
||||||
|
const d = await api(`/api/console?offset=${conOff}`).catch(() => null); if (!d) return;
|
||||||
|
const t = $('term'), atBottom = t.scrollTop + t.clientHeight >= t.scrollHeight - 30;
|
||||||
|
if (d.reset) t.innerHTML = '';
|
||||||
|
if (d.text) {
|
||||||
|
const esc = x => x.replace(/&/g, '&').replace(/</g, '<');
|
||||||
|
t.insertAdjacentHTML('beforeend', d.text.split('\n').map(l => /error|Error|失败|Traceback|不通过|Terminated/.test(l) ? `<span class="e">${esc(l)}</span>` : /^== |^\$ /.test(l) ? `<span class="c">${esc(l)}</span>` : /PASS|通过|完成/.test(l) ? `<span class="g">${esc(l)}</span>` : esc(l)).join('\n'));
|
||||||
|
if (t.textContent.length > 400000) t.textContent = t.textContent.slice(-200000);
|
||||||
|
}
|
||||||
|
if (!t.textContent) t.innerHTML = '<span style="color:var(--dim2)">点「开始训练」后,这里实时显示命令行输出。</span>';
|
||||||
|
conOff = d.offset; if (atBottom || d.reset) t.scrollTop = t.scrollHeight;
|
||||||
|
}
|
||||||
|
function toast(msg) { const t = $('toast'); t.textContent = msg; t.style.display = 'block'; clearTimeout(toast.h); toast.h = setTimeout(() => t.style.display = 'none', 3500); }
|
||||||
|
function openStart() {
|
||||||
|
const P = lastStatus && lastStatus.pipeline, ck = lastStatus && lastStatus.check;
|
||||||
|
if (ck && ck.problems && ck.problems.length) { toast('环境自检未通过:' + ck.problems.join(';')); return; }
|
||||||
|
const w = $('fWarn'); const upd = () => { const msgs = [];
|
||||||
|
const done = P ? (P.plan || []).filter(k => P.stages[k] && P.stages[k].rc === 0) : [];
|
||||||
|
if ($('fMode').value === 'resume' && done.length) msgs.push(`将跳过已完成的 ${done.length} 步:${done.map(k => SM[k]?.n || k).join('、')}。改了参数会自动改为从头重跑。`);
|
||||||
|
if ($('fMode').value === 'fresh' && P) msgs.push('从头重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。');
|
||||||
|
w.style.display = msgs.length ? 'block' : 'none'; w.innerHTML = msgs.join('<br>'); };
|
||||||
|
$('fMode').onchange = upd; upd();
|
||||||
|
$('modal').classList.add('show');
|
||||||
|
}
|
||||||
|
async function startJob() {
|
||||||
|
const env = { CFG: $('fCfg').value, CONSENSUS: $('fCons').value, N_TASKS: $('fN').value.trim(), NPROC: $('fP').value.trim(), TARGET_SAMPLES: $('fT').value.trim(), ROUNDS: $('fR').value.trim(), MIN_GAP_MM: $('fG').value.trim() };
|
||||||
|
const body = JSON.stringify({ fresh: $('fMode').value === 'fresh', env });
|
||||||
|
const r = DEMO ? await api('/api/start', body) : await post('/api/start', body);
|
||||||
|
if (r.error) { toast('没能开始:' + r.error); return; }
|
||||||
|
$('modal').classList.remove('show'); toast('已开始'); conOff = 0; userPicked = false; poll();
|
||||||
|
}
|
||||||
|
async function stopJob() {
|
||||||
|
if (!confirm('确定停止?训练会先存档再退出(最多等 90 秒),生成/打包按已写完的部分保留。之后点「续跑」从断点接着来。')) return;
|
||||||
|
toast('正在停止…训练阶段会先存档');
|
||||||
|
const r = DEMO ? await api('/api/stop', '{}') : await post('/api/stop');
|
||||||
|
toast(r.error ? '没能停止:' + r.error : '已停止'); poll();
|
||||||
|
}
|
||||||
|
$('btnStart').onclick = openStart; $('btnStop').onclick = stopJob;
|
||||||
|
$('fCancel').onclick = () => $('modal').classList.remove('show'); $('fGo').onclick = startJob;
|
||||||
|
$('modal').onclick = e => { if (e.target === $('modal')) $('modal').classList.remove('show'); };
|
||||||
|
|
||||||
|
/* ===================== 控件 ===================== */
|
||||||
|
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); render(); };
|
||||||
|
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); render(); };
|
||||||
|
$('run').onchange = () => { userPicked = true; cur = null; poll(); };
|
||||||
|
$('scenario').onchange = () => { api.scenario($('scenario').value); cur = null; userPicked = false; conOff = 0; $('run').innerHTML = ''; poll(); };
|
||||||
|
$('auto').onclick = () => { const on = !$('auto').classList.contains('on'); $('auto').classList.toggle('on', on); clearInterval(timer); if (on) timer = setInterval(poll, 5000); };
|
||||||
|
timer = setInterval(poll, 5000); poll();
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
示例数据:两种场景,每次刷新让"运行中"的那条日志向前推进。
|
||||||
|
===================================================================== */
|
||||||
|
function demoApi() {
|
||||||
|
let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
||||||
|
const gauss = () => { let u = 0; for (let i = 0; i < 6; i++) u += rnd(); return (u - 3) * 1.41; };
|
||||||
|
const now = () => Date.now() / 1000;
|
||||||
|
const T0 = now() - 2.05 * 3600;
|
||||||
|
function trainRec(st, total, sps, t0, wta) {
|
||||||
|
const lr = 1e-3 * (st < 1000 ? st / 1000 : 0.5 * (1 + Math.cos(Math.PI * (st - 1000) / (total - 1000))));
|
||||||
|
const mf = (wta ? 0.42 : 0.55) * Math.exp(-st / 9000) + 0.12 + 0.012 * gauss(), fk = 2.6 * Math.exp(-st / 6000) + 0.35 + 0.04 * gauss();
|
||||||
|
const sm = 3.2 * Math.exp(-st / 4000) + 0.6 + 0.06 * gauss(), val = 0.4 * Math.exp(-st / 5000) + 0.05 + 0.006 * gauss(), sub = 0.25 * Math.exp(-st / 7000) + 0.04 + 0.004 * gauss();
|
||||||
|
const sp = sps * (1 - 0.18 * Math.exp(-st / 250)) * (1 + 0.012 * gauss());
|
||||||
|
return { step: st, total, t: t0 + st * 1024 / sps, loss: mf + 0.5 * fk + 0.05 * sm + 0.1 * val + 0.1 * sub, mf, fk_cm: fk, smooth: sm, value: val, sub,
|
||||||
|
gnorm: Math.max(0.15, 0.55 + 2.4 * Math.exp(-st / 2500) + 0.12 * gauss()), lr, samples_per_s: sp, eta_h: (total - st) * 1024 / sps / 3600 };
|
||||||
|
}
|
||||||
|
function trainEval(x, t0, sps) {
|
||||||
|
const q = 1 - Math.exp(-x / 14000);
|
||||||
|
return { eval: { step: x, t: t0 + x * 1024 / sps, n: 512, n_ref: 512, success: Math.min(0.975, 0.08 + 0.87 * q + 0.012 * gauss()), collision_rate: Math.max(0.004, 0.31 * (1 - q) + 0.012 + 0.005 * gauss()),
|
||||||
|
time_ok_1p3: Math.min(0.95, 0.05 + 0.8 * q + 0.012 * gauss()), start_fail: Math.max(0, 0.05 * (1 - q) + 0.003 * gauss()) } };
|
||||||
|
}
|
||||||
|
function trainLog(total, from, to, sps, t0, evalEvery = 2000, wta = false) {
|
||||||
|
const L = [];
|
||||||
|
for (let st = from; st <= to; st += 50) { L.push(trainRec(st, total, sps, t0, wta)); if (evalEvery && st > 0 && st % evalEvery === 0) L.push(trainEval(st, t0, sps)); }
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
function apgRec(it, total, t0) {
|
||||||
|
const q = 1 - Math.exp(-it / 900);
|
||||||
|
return { it, total, t: t0 + it * 3.1, plans: 60 + Math.floor(rnd() * 45), err_mm_mean: Math.max(3, 140 * (1 - q) + 9 + 5 * gauss()), reach_frac: Math.min(1, 0.35 + 0.52 * q + 0.03 * gauss()),
|
||||||
|
bc: 0.9 - 0.15 * q + 0.02 * gauss(), gnorm: Math.max(0.2, 1.2 + 0.25 * gauss()), col: Math.max(0, 2.4 * (1 - q) + 0.15 + 0.06 * gauss()), tgt: Math.max(0, 3.5 * (1 - q) + 0.2 + 0.08 * gauss()), smooth: 40 - 12 * q + 1.5 * gauss() };
|
||||||
|
}
|
||||||
|
function apgLog(total, from, to, t0, base = 0.93) {
|
||||||
|
const L = [];
|
||||||
|
for (let it = from; it <= to; it += 10) { L.push(apgRec(it, total, t0)); if (it > 0 && it % 500 === 0) { const q = 1 - Math.exp(-it / 900); L.push({ eval: { it, success: base + 0.035 * q + 0.004 * gauss(), collision_rate: 0.028 * (1 - q) + 0.006, time_ok_1p3: 0.86 + 0.06 * q, start_fail: 0.001 } }); } }
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
let sc, runs, logs, pipe, jobS, con, genAt = 0;
|
||||||
|
// 示例流水线:stages 里的 [阶段, 开始距今小时, 用时小时]
|
||||||
|
const mkPipe = (done, cur, curStartH) => { const t = now(); const st = {}; let tt = t - curStartH * 3600;
|
||||||
|
for (let i = done.length - 1; i >= 0; i--) { const [k, dur] = done[i]; st[k] = { start: tt - dur * 3600, end: tt, rc: 0, info: {} }; tt -= dur * 3600; }
|
||||||
|
if (cur) st[cur] = { start: t - curStartH * 3600, end: null, rc: null, info: {} };
|
||||||
|
return { started: tt, plan: ['kin', 'r4', 'gate1', 'gen', 'prep', 'train', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, current: cur }; };
|
||||||
|
const CON = {
|
||||||
|
train: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== node ml/eval/dump_parity.cjs 2000\n== python -m ml.eval.check_parity\n{"PASS": true, "fk_mm": 0.0004, "gap_mm": 0.0011}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n样本数 1702000 ≥ 目标,完成\n== bash ml/prep/run_prep.sh 28\n== python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json\n{"pairs": 250, "label_std_mean": 0.071, "pass": true}\n== python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval\nstep 2000 loss 1.912 6150 samples/s\n== python -m ml.train --samples ml/data/samples --out ml/runs/base\n',
|
||||||
|
gen: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== python -m ml.eval.check_parity\n{"PASS": true}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n',
|
||||||
|
late: '... 挖错第 2 轮\nround 2 gain 1.1 pp\n== python -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed 1003 --n 50000 --out ml/runs/mine/r3\n{"n": 50000, "E1": 0.009, "E2": 0.019, "ok": 0.938}\n== python -m ml.apg.apg_finetune --init ml/runs/current.pt --out ml/runs/apg_r3 --steps 1000\n', idle: '' };
|
||||||
|
const GPU = () => [{ name: 'NVIDIA GeForce RTX 3090', util: 93 + Math.round(5 * rnd()), mem_used: 15872 + Math.round(500 * rnd()), mem_total: 24576, temp: 71 + Math.round(3 * rnd()), power: 318 + Math.round(24 * rnd()) }];
|
||||||
|
const DATA = { kin_pass: true, consistency: 0.86, gen_tasks_written: 56412, shards: 56,
|
||||||
|
stats: { tasks: 56412, keep: 43120, gapDrop: 4830, minGapFilterMm: 3, keepRate: 0.85, samplesEvery3: 1702000, samplesPerTask: 36.5, consensusDrop: 7520, vertexMinGapMm: { p10: 3.1, p50: 4.4, p90: 5.9, max: 12.3 } },
|
||||||
|
index: { train_samples: 1617000, val_samples: 85000 }, r2: { label_std_mean: 0.071, pass: true } };
|
||||||
|
function setup(name) {
|
||||||
|
sc = name; seed = 7; con = CON[name] || ''; jobS = { running: name !== 'idle', pid: 21844, from: 'kin', to: 'accept', started: now() - 5 * 3600, rc: null, bash: true }; genAt = 31200;
|
||||||
|
if (name === 'idle') { runs = {}; logs = {}; pipe = null; jobS = { running: false, bash: true }; return; }
|
||||||
|
if (name === 'gen') { runs = {}; logs = {}; pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47]], 'gen', 2.7); pipe.stages.gen.info.target_tasks = 50008; return; }
|
||||||
|
if (name === 'train') { pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38]], 'train', 2.15);
|
||||||
|
runs = { base: { kind: 'train', at: 36000, total: 60000, sps: 6208, t0: T0, live: true },
|
||||||
|
bench_base: { kind: 'bench', at: 2000, total: 2000, sps: 6150, t0: T0 - 700 } };
|
||||||
|
logs = { base: trainLog(60000, 0, 36000, 6208, T0), bench_base: trainLog(2000, 0, 2000, 6150, T0 - 700, 0) };
|
||||||
|
} else { pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38], ['train', 2.9], ['m5', 0.2], ['apg', 2.6]], 'mine', 3.4); pipe.stages.mine.info.rounds = 6;
|
||||||
|
pipe.stages.export = pipe.stages.bench = pipe.stages.accept = undefined; delete pipe.stages.export; delete pipe.stages.bench; delete pipe.stages.accept;
|
||||||
|
const t1 = now() - 9 * 3600;
|
||||||
|
runs = { base: { kind: 'train', at: 60000, total: 60000, sps: 6208, t0: t1 - 3 * 3600 },
|
||||||
|
bench_base: { kind: 'bench', at: 2000, total: 2000, sps: 6150, t0: t1 - 3.2 * 3600 },
|
||||||
|
apg: { kind: 'apg', at: 3000, total: 3000, t0: t1 },
|
||||||
|
apg_r3: { kind: 'apg', at: 620, total: 1000, t0: now() - 620 * 3.1, live: true } };
|
||||||
|
logs = { base: trainLog(60000, 0, 60000, 6208, t1 - 3 * 3600), bench_base: trainLog(2000, 0, 2000, 6150, t1 - 3.2 * 3600, 0),
|
||||||
|
apg: apgLog(3000, 0, 3000, t1), apg_r3: apgLog(1000, 0, 620, now() - 620 * 3.1, 0.955) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setup('train');
|
||||||
|
const mtime = name => { const r = runs[name]; const L = logs[name]; const last = L[L.length - 1]; return r.live ? now() - 2 : (last.t || last.eval?.t || now() - 3600); };
|
||||||
|
const status = () => {
|
||||||
|
let P = pipe && JSON.parse(JSON.stringify(pipe));
|
||||||
|
if (P && P.current) {
|
||||||
|
const r = sc === 'train' ? runs.base : runs.apg_r3;
|
||||||
|
P.progress = !['gen', 'train', 'late'].includes(sc) || (sc !== 'gen' && !(sc === 'train' ? runs.base : runs.apg_r3)) ? { frac: null, label: '进行中(示例)' } : sc === 'gen' ? { frac: Math.min(1, (genAt += 180) / 50008), label: `已生成 ${genAt.toLocaleString()} / 50,008 条任务` }
|
||||||
|
: sc === 'train' ? { frac: (2000 + r.at) / 62000, label: `正式训练 ${r.at.toLocaleString()} / 60,000 步`, run: 'base' }
|
||||||
|
: { frac: (2 + 0.3 + 0.6 * r.at / r.total) / 6, label: `第 3 轮:APG ${r.at} / 1,000 步(共最多 6 轮,提升不足会提前停)`, run: 'apg_r3' };
|
||||||
|
}
|
||||||
|
const gpuIdle = [{ name: 'NVIDIA GeForce RTX 3090', util: 0, mem_used: 412, mem_total: 24576, temp: 38, power: 24 }];
|
||||||
|
const base = { now: now(), gpu: sc === 'train' || sc === 'late' ? GPU() : gpuIdle, pipeline: P, data: DATA, m5: [], selected: null, mine: [], bench: null, acceptance: null };
|
||||||
|
if (sc === 'idle') return { ...base, data: {} };
|
||||||
|
if (sc === 'gen') return { ...base, data: { kin_pass: true, consistency: 0.86, gen_tasks_written: genAt, shards: 28, stats: null, index: null, r2: null } };
|
||||||
|
if (sc === 'train') return base;
|
||||||
|
return { ...base,
|
||||||
|
m5: [{ head: 'base', pass: true, policy_only: { collision_rate: 0.012, success: 0.912 }, with_sim_safety: { collision_rate: 0.0, success: 0.948 } }],
|
||||||
|
selected: 'ml/runs/base/best.pt',
|
||||||
|
mine: [{ round: 'r1', report: { E1: 0.018, E2: 0.031, ok: 0.902 }, gate: { gain_pp: 2.4, R7_left_share_shift_pp: 1.2, accept: true } },
|
||||||
|
{ round: 'r2', report: { E1: 0.011, E2: 0.022, ok: 0.931 }, gate: { gain_pp: 1.1, R7_left_share_shift_pp: 0.8, accept: true } },
|
||||||
|
{ round: 'r3', report: { E1: 0.009, E2: 0.019, ok: 0.938 }, gate: null }],
|
||||||
|
bench: { nc: { n: 30, ok: 30, collisionFrames: 0, brakeRate: 0.004, inferMsP99: 9.8 }, route: { n: 40, ok: 40, collisionFrames: 0, brakeRate: 0.002, inferMsP99: 9.6 },
|
||||||
|
r8: { n: 50, ok: 48, collisionFrames: 0, brakeRate: 0.031, inferMsP99: 10.1 }, nc1000: { n: 1000, ok: 997, collisionFrames: 0, brakeRate: 0.006, inferMsP99: 9.9 } },
|
||||||
|
acceptance: { all_pass: false, failed: ['R8 恢复率'], items: [
|
||||||
|
{ item: 'held-out 可达任务到达率', value: 0.983, pass: true }, { item: '起步失败率(R2)', value: 0.0008, pass: true },
|
||||||
|
{ item: 'D 类停靠距离 ≤ 老师 + 5 mm 的比例', value: 0.991, pass: true }, { item: '部署形态穿模帧', value: 0, pass: true },
|
||||||
|
{ item: 'near_column 30/30', value: '30/30', pass: true }, { item: 'R7 左右比例变化', value: '1.2pp', pass: true },
|
||||||
|
{ item: 'R8 恢复率', value: 0.96, pass: false }, { item: 'R10 训练吞吐(样本/s)', value: 6208, pass: true, rule: '样本/s' } ] } };
|
||||||
|
};
|
||||||
|
const fn = async (path, body) => {
|
||||||
|
const u = new URL(path, 'http://x');
|
||||||
|
if (u.pathname === '/api/runs') return Object.entries(runs).map(([name, r]) => ({ name, kind: r.kind, size: 0, mtime: mtime(name), has_best: true, diverged: false })).sort((a, b) => b.mtime - a.mtime);
|
||||||
|
if (u.pathname === '/api/log') {
|
||||||
|
const name = u.searchParams.get('run'), off = +u.searchParams.get('offset'), r = runs[name];
|
||||||
|
if (!r) return { records: [], offset: 0, reset: true, mtime: now() };
|
||||||
|
if (off === 0) return { records: logs[name], offset: logs[name].length, reset: true, mtime: mtime(name), live: !!r.live };
|
||||||
|
if (r.live && r.at < r.total) { // 模拟推进
|
||||||
|
const step = r.kind === 'apg' ? 10 : 250, from = r.at + (r.kind === 'apg' ? 10 : 50); r.at = Math.min(r.total, r.at + step);
|
||||||
|
const add = r.kind === 'apg' ? apgLog(r.total, from, r.at, r.t0, 0.955) : trainLog(r.total, from, r.at, r.sps, r.t0);
|
||||||
|
if (r.kind !== 'apg') add.forEach(v => { if (v.t) v.t = now(); });
|
||||||
|
logs[name].push(...add); return { records: add, offset: off + add.length, reset: false, mtime: now(), live: true };
|
||||||
|
}
|
||||||
|
return { records: [], offset: off, reset: false, mtime: mtime(name), live: !!r.live };
|
||||||
|
}
|
||||||
|
if (u.pathname === '/api/status') return status();
|
||||||
|
if (u.pathname === '/api/job') return jobS;
|
||||||
|
if (u.pathname === '/api/console') { const off = +u.searchParams.get('offset'); return { text: con.slice(off), offset: con.length, reset: off === 0 || off > con.length }; }
|
||||||
|
if (u.pathname === '/api/start') { const b = JSON.parse(body || '{}'); b.from = b.from || 'kin'; b.to = b.to || 'accept'; jobS = { running: true, pid: 23001, from: b.from, to: b.to, started: now(), rc: null, bash: true };
|
||||||
|
con = `$ ${b.fresh ? 'FRESH=1 ' : ''}bash ml/pipeline.sh all\n(示例数据:真实使用时这里是命令行输出)\n`;
|
||||||
|
if (!pipe) pipe = mkPipe([], b.from, 0.001); else { pipe.current = b.from; pipe.stages[b.from] = { start: now(), end: null, rc: null, info: {} }; }
|
||||||
|
return { ok: true, pid: 23001 }; }
|
||||||
|
if (u.pathname === '/api/stop') { jobS = { ...jobS, running: false, rc: -15, stopped: true };
|
||||||
|
if (pipe && pipe.current) { pipe.stages[pipe.current].end = now(); pipe.stages[pipe.current].rc = -15; pipe.current = null; }
|
||||||
|
Object.values(runs).forEach(r => r.live = false); con += '\n[已在网页上停止]\n'; return { ok: true }; }
|
||||||
|
};
|
||||||
|
fn.scenario = setup;
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+441
@@ -0,0 +1,441 @@
|
|||||||
|
"""训练控制台(本地网页,零第三方依赖,离线可用):一键开始全流程、看进度、可停止。
|
||||||
|
双击 ml/console.bat(Windows)或运行 bash ml/console.sh(Linux/WSL)→ 自动打开 http://localhost:8100
|
||||||
|
也可:python -m ml.monitor [--port 8100] [--host 0.0.0.0] [--open]
|
||||||
|
网页上点「开始训练」→ 选参数 → 后台执行 bash ml/pipeline.sh all(已完成的阶段自动跳过 = 续跑;勾「从头重跑」= FRESH=1),
|
||||||
|
输出写 ml/runs/console.log,网页实时显示;「停止」结束整个进程树。网页和控制台窗口关掉都不影响训练,重新打开即可继续看。
|
||||||
|
只读数据来源:
|
||||||
|
ml/runs/pipeline.json 各阶段开始/结束/返回码(pipeline.sh 自动写)→ 全流程进度
|
||||||
|
ml/runs/*/log.jsonl 预训练(base / wta / bench_*)与 APG(apg / apg_r*)逐步日志与闭环评估
|
||||||
|
ml/runs/m5_gate_*.json、mine/r*/report.json + gate.json、policy_bench.json、acceptance.json
|
||||||
|
ml/data/stats.json / consistency.json / r2.json / samples/index.json、nvidia-smi
|
||||||
|
安全:默认只监听 127.0.0.1;--host 0.0.0.0 时其他机器只能看,开始/停止只接受本机请求(--remote-control 放开)。
|
||||||
|
2026-10-18 新增(用户要求:训练全程可视化,并在网页上点按钮一键开始)。
|
||||||
|
2026-10-18 第五轮(审查第四轮):
|
||||||
|
N-7 是否在跑看 ml/runs/pipeline.lock(与终端同一把锁),不再靠"日志 120 s 内有更新"去猜;
|
||||||
|
N-8 开始/停止要求自定义头 X-Console + 同源 Origin/Sec-Fetch-Site,外站网页发不进来;
|
||||||
|
N-10 启动自检(训练用 Python 能否 import torch、GPU、node、bash),不过就拒绝「开始训练」并在顶栏标红;
|
||||||
|
N-11 Windows 子进程用 CREATE_NO_WINDOW 自带隐藏控制台:关掉控制台黑窗口训练照跑,重开控制台可接着看、可停止;
|
||||||
|
N-9 生成阶段的行数按文件增量统计(只读新增字节),其他阶段不扫 shard;
|
||||||
|
N-12 停止后的状态改写走 ml.pstate(单一写者、原子替换);N-14 阶段名/估时/权重由 ml.pstate.STAGES 下发。"""
|
||||||
|
import argparse, glob, json, os, re, shutil, signal, subprocess, sys, threading, time, webbrowser
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ml import pstate
|
||||||
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
HTML = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'monitor.html')
|
||||||
|
RUNS, DATA = 'ml/runs', 'ml/data'
|
||||||
|
|
||||||
|
|
||||||
|
def jload(p):
|
||||||
|
try:
|
||||||
|
with open(p, encoding='utf-8') as f: return json.load(f)
|
||||||
|
except Exception: return None
|
||||||
|
|
||||||
|
|
||||||
|
def kind_of(name):
|
||||||
|
if name.startswith('apg'): return 'apg'
|
||||||
|
if name.startswith('bench'): return 'bench'
|
||||||
|
return 'train'
|
||||||
|
|
||||||
|
|
||||||
|
def list_runs():
|
||||||
|
out = []
|
||||||
|
for f in glob.glob(os.path.join(ROOT, RUNS, '*', 'log.jsonl')):
|
||||||
|
d = os.path.basename(os.path.dirname(f)); st = os.stat(f)
|
||||||
|
out.append({'name': d, 'kind': kind_of(d), 'size': st.st_size, 'mtime': st.st_mtime,
|
||||||
|
'has_best': os.path.exists(os.path.join(os.path.dirname(f), 'best.pt')),
|
||||||
|
'diverged': os.path.exists(os.path.join(os.path.dirname(f), 'diverged.pt'))})
|
||||||
|
out.sort(key=lambda r: -r['mtime'])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def read_log(name, offset):
|
||||||
|
if not re.fullmatch(r'[\w.\-]+', name or ''): return {'error': 'bad run'}
|
||||||
|
f = os.path.join(ROOT, RUNS, name, 'log.jsonl')
|
||||||
|
if not os.path.exists(f): return {'error': 'no log'}
|
||||||
|
size = os.path.getsize(f)
|
||||||
|
if offset > size: offset = 0 # 日志被新训练覆盖 → 从头读
|
||||||
|
recs = []
|
||||||
|
with open(f, 'rb') as fh:
|
||||||
|
fh.seek(offset); buf = fh.read(min(size - offset, 64 << 20))
|
||||||
|
end = buf.rfind(b'\n') + 1 # 只取完整行,半行留到下次
|
||||||
|
for line in buf[:end].decode('utf-8', 'replace').splitlines():
|
||||||
|
if line.strip():
|
||||||
|
try: recs.append(json.loads(line))
|
||||||
|
except Exception: pass
|
||||||
|
return {'records': recs, 'offset': offset + end, 'size': size, 'reset': offset == 0, 'mtime': os.path.getmtime(f)}
|
||||||
|
|
||||||
|
|
||||||
|
def gpu():
|
||||||
|
try:
|
||||||
|
q = 'name,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw'
|
||||||
|
r = subprocess.run(['nvidia-smi', f'--query-gpu={q}', '--format=csv,noheader,nounits'], capture_output=True, text=True, timeout=3)
|
||||||
|
out = []
|
||||||
|
for line in r.stdout.strip().splitlines():
|
||||||
|
n, u, mu, mt, t, p = [x.strip() for x in line.split(',')]
|
||||||
|
num = lambda x: float(x) if re.fullmatch(r'[\d.]+', x) else None
|
||||||
|
out.append({'name': n, 'util': num(u), 'mem_used': num(mu), 'mem_total': num(mt), 'temp': num(t), 'power': num(p)})
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def last_rec(name):
|
||||||
|
"""读日志最后一条非评估记录(只读文件尾 16 KB)。"""
|
||||||
|
f = os.path.join(ROOT, RUNS, name, 'log.jsonl')
|
||||||
|
try:
|
||||||
|
with open(f, 'rb') as fh:
|
||||||
|
fh.seek(0, 2); n = fh.tell(); fh.seek(max(0, n - 16384)); tail = fh.read().decode('utf-8', 'replace').splitlines()
|
||||||
|
for line in reversed(tail):
|
||||||
|
try:
|
||||||
|
r = json.loads(line)
|
||||||
|
if 'eval' not in r: return r
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class LineCount:
|
||||||
|
"""按文件增量数行:记住每个文件已读到的偏移与行数,只读新增字节(审查 N-9:此前每 5 s 全量重读全部 shard)。"""
|
||||||
|
def __init__(self): self.c = {}; self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def count(self, f):
|
||||||
|
with self.lock:
|
||||||
|
try: size = os.path.getsize(f)
|
||||||
|
except OSError: return 0
|
||||||
|
off, n = self.c.get(f, (0, 0))
|
||||||
|
if size < off: off, n = 0, 0
|
||||||
|
if size > off:
|
||||||
|
with open(f, 'rb') as fh:
|
||||||
|
fh.seek(off)
|
||||||
|
while True:
|
||||||
|
b = fh.read(1 << 22)
|
||||||
|
if not b: break
|
||||||
|
n += b.count(b'\n'); off += len(b)
|
||||||
|
self.c[f] = (off, n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
LC = LineCount()
|
||||||
|
|
||||||
|
|
||||||
|
def newer(pattern, t0):
|
||||||
|
return [f for f in glob.glob(os.path.join(ROOT, pattern)) if os.path.getmtime(f) >= t0 - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def stage_progress(st, s):
|
||||||
|
"""当前阶段的进度:返回 {'frac': 0..1 或 None(无法计量), 'label': 文字}。"""
|
||||||
|
t0, info = s['start'], s.get('info') or {}
|
||||||
|
if st == 'gen':
|
||||||
|
n = sum(LC.count(f) for f in newer(f'{DATA}/shard_*.jsonl', t0) + newer(f'{DATA}/shard_*.jsonl.part', t0))
|
||||||
|
tgt = info.get('target_tasks') or 0
|
||||||
|
return {'frac': min(1, n / tgt) if tgt else None, 'label': f'已生成 {n:,} / {tgt:,} 条任务'}
|
||||||
|
if st == 'prep':
|
||||||
|
nsh = len(glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl')))
|
||||||
|
done = len(newer(f'{DATA}/samples/*.X.npy', t0))
|
||||||
|
if os.path.exists(os.path.join(ROOT, DATA, 'r2.json')) and newer(f'{DATA}/r2.json', t0): return {'frac': 1, 'label': '打包完成,R2 检查完成'}
|
||||||
|
return {'frac': min(0.95, done / nsh) if nsh else None, 'label': f'已打包 {done} / {nsh} 个分片' + (',R2 检查中' if nsh and done >= nsh else '')}
|
||||||
|
if st == 'train':
|
||||||
|
tag, bs = info.get('tag', 'base'), info.get('bench_steps', 2000)
|
||||||
|
b, m = last_rec(f'bench_{tag}') if newer(f'{RUNS}/bench_{tag}/log.jsonl', t0) else None, last_rec(tag) if newer(f'{RUNS}/{tag}/log.jsonl', t0) else None
|
||||||
|
if m and m.get('total'):
|
||||||
|
return {'frac': (bs + m['step']) / (bs + m['total']), 'label': f'正式训练 {m["step"]:,} / {m["total"]:,} 步', 'run': tag}
|
||||||
|
if b: return {'frac': b['step'] / (bs + 60000), 'label': f'测速 {b["step"]:,} / {bs:,} 步', 'run': f'bench_{tag}'}
|
||||||
|
return {'frac': 0, 'label': '加载数据中'}
|
||||||
|
if st == 'apg':
|
||||||
|
r = last_rec('apg') if newer(f'{RUNS}/apg/log.jsonl', t0) else None
|
||||||
|
return {'frac': r['it'] / r['total'] if r and r.get('total') else 0, 'label': f'APG {r["it"]:,} / {r["total"]:,} 步' if r else '准备中', 'run': 'apg'}
|
||||||
|
if st == 'mine':
|
||||||
|
R = info.get('rounds', 6)
|
||||||
|
k = len(newer(f'{RUNS}/mine/r*/gate.json', t0))
|
||||||
|
cur = sorted(newer(f'{RUNS}/apg_r*/log.jsonl', t0), key=os.path.getmtime)
|
||||||
|
frac_in, lab = 0, f'第 {k + 1} 轮:挖错出题'
|
||||||
|
if cur and len(cur) > k:
|
||||||
|
name = os.path.basename(os.path.dirname(cur[-1])); r = last_rec(name)
|
||||||
|
if r and r.get('total'): frac_in = 0.3 + 0.6 * r['it'] / r['total']; lab = f'第 {k + 1} 轮:APG {r["it"]:,} / {r["total"]:,} 步'
|
||||||
|
return {'frac': min(1, (k + frac_in) / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)', 'run': name}
|
||||||
|
return {'frac': min(1, k / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)'}
|
||||||
|
if st == 'm5':
|
||||||
|
tag = info.get('tag', 'base')
|
||||||
|
n = len(newer(f'{RUNS}/m5_{tag}_policy.json', t0)) + len(newer(f'{RUNS}/m5_{tag}_safety.json', t0))
|
||||||
|
return {'frac': n / 2, 'label': ['纯策略评估中', '加安全层评估中', '汇总'][min(n, 2)]}
|
||||||
|
return {'frac': None, 'label': '进行中'}
|
||||||
|
|
||||||
|
|
||||||
|
def pipeline():
|
||||||
|
d = jload(os.path.join(ROOT, RUNS, 'pipeline.json'))
|
||||||
|
if not d: return None
|
||||||
|
cur = d.get('current')
|
||||||
|
if cur and cur in d['stages']:
|
||||||
|
try: d['progress'] = stage_progress(cur, d['stages'][cur])
|
||||||
|
except Exception as e: d['progress'] = {'frac': None, 'label': f'进度读取失败:{e}'}
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
P = lambda *a: os.path.join(ROOT, *a)
|
||||||
|
mine = []
|
||||||
|
for d in sorted(glob.glob(P(RUNS, 'mine', 'r*')), key=lambda x: int(re.sub(r'\D', '', os.path.basename(x)) or 0)):
|
||||||
|
rep, gate = jload(os.path.join(d, 'report.json')), jload(os.path.join(d, 'gate.json'))
|
||||||
|
if rep or gate:
|
||||||
|
mine.append({'round': os.path.basename(d), 'report': rep and {k: rep.get(k) for k in ('n', 'E1', 'E2', 'E3', 'E4', 'E5', 'ok', 'pool')},
|
||||||
|
'gate': gate and {k: gate.get(k) for k in ('gain_pp', 'A_drop_pp', 'fixed_kept', 'R7_left_share_shift_pp', 'R7_travel_median_ratio', 'accept')}})
|
||||||
|
stats = jload(P(DATA, 'stats.json'))
|
||||||
|
if stats: stats = {k: stats.get(k) for k in ('tasks', 'keep', 'gapDrop', 'minGapFilterMm', 'keepRate', 'samplesEvery3', 'samplesPerTask', 'projectedSamplesFor50k', 'consensusDrop', 'vertexMinGapMm', 'execS', 'travelDeg')}
|
||||||
|
shards = glob.glob(P(DATA, 'shard_*.jsonl'))
|
||||||
|
pl = pipeline()
|
||||||
|
if pl and pl.get('current') == 'gen': # 只在生成阶段数行(增量);其他阶段用 stats.json 的 tasks
|
||||||
|
gen_lines = sum(LC.count(f) for f in shards + glob.glob(P(DATA, 'shard_*.jsonl.part')))
|
||||||
|
else: gen_lines = (stats or {}).get('tasks') or 0
|
||||||
|
pb = jload(P(RUNS, 'policy_bench.json'))
|
||||||
|
if pb: pb = {k: {kk: v.get(kk) for kk in ('n', 'ok', 'reached', 'collisionFrames', 'brakeRate', 'recoveryRate', 'livelock', 'inferMsP99', 'safetyMsP99')}
|
||||||
|
for k, v in pb.items() if isinstance(v, dict) and 'n' in v}
|
||||||
|
return {'now': time.time(), 'gpu': gpu(),
|
||||||
|
'vram_budget_gb': float(os.environ.get('VRAM_BUDGET_GB', 20)), # 本机桌面常驻占 4–5 GB,给训练的预算 19–20 GB
|
||||||
|
'pipeline': pl, 'check': CHECK.get(), 'lock': lock_info(), # 2026-10-18 第八轮修:上一版这三项被行尾注释吞掉,网页步骤条/锁状态拿不到
|
||||||
|
'stage_meta': [{'id': k, 'name': n, 'est_h': h, 'timeout_h': pstate.TIMEOUT_H[k]} for k, n, h in pstate.STAGES],
|
||||||
|
'r4': jload(P(RUNS, 'r4.json')),
|
||||||
|
'data': {'consistency': (jload(P(DATA, 'consistency.json')) or {}).get('consistentByGateRule'),
|
||||||
|
'gen_tasks_written': gen_lines, 'shards': len(shards), 'stats': stats,
|
||||||
|
'index': jload(P(DATA, 'samples', 'index.json')), 'r2': jload(P(DATA, 'r2.json'))},
|
||||||
|
'm5': [x for x in (jload(f) for f in sorted(glob.glob(P(RUNS, 'm5_gate_*.json')))) if x],
|
||||||
|
'selected': open(P(RUNS, 'selected_ckpt')).read().strip() if os.path.exists(P(RUNS, 'selected_ckpt')) else None,
|
||||||
|
'mine': mine, 'bench': pb, 'acceptance': jload(P(RUNS, 'acceptance.json'))}
|
||||||
|
|
||||||
|
|
||||||
|
STAGES = pstate.ORDER
|
||||||
|
ENV_KEYS = {'N_TASKS': r'\d+', 'NPROC': r'\d+', 'TARGET_SAMPLES': r'\d+', 'CFG': r'ml/configs/(base|wta|small)\.json',
|
||||||
|
'CONSENSUS': r'[01]?', 'ROUNDS': r'\d+', 'APG_STEPS': r'\d+', 'MINE_N': r'\d+', 'MIN_GAP_MM': r'\d+(\.\d+)?',
|
||||||
|
'FRESH': r'[01]', 'R2_STRICT': r'[01]', 'AUTO_WTA': r'[01]'}
|
||||||
|
CONSOLE = os.path.join(ROOT, RUNS, 'console.log')
|
||||||
|
|
||||||
|
|
||||||
|
def find_bash():
|
||||||
|
"""Windows 优先 Git Bash(System32\\bash.exe 是 WSL,不能用);其他系统用 PATH 里的 bash。"""
|
||||||
|
if os.name == 'nt':
|
||||||
|
for p in [os.environ.get('BASH', ''), r'C:\Program Files\Git\bin\bash.exe', r'C:\Program Files (x86)\Git\bin\bash.exe',
|
||||||
|
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe')]:
|
||||||
|
if p and os.path.exists(p): return p
|
||||||
|
g = shutil.which('git')
|
||||||
|
if g:
|
||||||
|
root = os.path.dirname(os.path.dirname(g)) # 便携版 git 常放在 <根>\mingw64\bin\git.exe,要再上一层才找到 bash
|
||||||
|
for base in dict.fromkeys([root, os.path.dirname(root)]):
|
||||||
|
for sub in ('bin', os.path.join('usr', 'bin')):
|
||||||
|
p = os.path.join(base, sub, 'bash.exe')
|
||||||
|
if os.path.exists(p): return p
|
||||||
|
b = shutil.which('bash')
|
||||||
|
if b and 'windows\\system32' not in b.lower(): return b
|
||||||
|
return None
|
||||||
|
return shutil.which('bash')
|
||||||
|
|
||||||
|
|
||||||
|
def train_python():
|
||||||
|
"""流水线用的 Python:尊重 PYTHON 环境变量(与 console.sh / console.bat 一致),否则用控制台自己的解释器。"""
|
||||||
|
return os.environ.get('PYTHON') or sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
class SelfCheck:
|
||||||
|
"""启动自检(审查 N-10):在后台线程跑一次,结果缓存;「开始训练」前不过就拒绝,网页顶栏显示。"""
|
||||||
|
def __init__(self): self.r = {'pending': True}; threading.Thread(target=self.run, daemon=True).start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
py = train_python(); r = {'python': py, 'bash': find_bash(), 'node': shutil.which('node')}
|
||||||
|
try:
|
||||||
|
o = subprocess.run([py, '-c', 'import json,torch;c=torch.cuda.is_available();print(json.dumps({"torch":torch.__version__,"cuda":c,"gpu":torch.cuda.get_device_name(0) if c else None}))'],
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
|
r.update(json.loads(o.stdout.strip().splitlines()[-1]) if o.returncode == 0 else {'torch': None, 'torch_error': (o.stderr or o.stdout).strip()[-300:]})
|
||||||
|
except Exception as e: r.update(torch=None, torch_error=str(e))
|
||||||
|
probs = []
|
||||||
|
if not r.get('torch'): probs.append(f'这个 Python 没有 torch:{py}(先激活训练环境再启动控制台,或设 PYTHON=...)')
|
||||||
|
if not r['bash']: probs.append('找不到 bash:Windows 请安装 Git for Windows(自带 Git Bash)')
|
||||||
|
if not r['node']: probs.append('找不到 node')
|
||||||
|
if r.get('torch') and not r.get('cuda'): r['warn'] = 'torch 看不到 GPU,训练会极慢'
|
||||||
|
r['problems'] = probs; r['ok'] = not probs
|
||||||
|
self.r = r
|
||||||
|
|
||||||
|
def get(self): return self.r
|
||||||
|
|
||||||
|
|
||||||
|
def lock_info():
|
||||||
|
lk = pstate.lock_read()
|
||||||
|
if not lk: return None
|
||||||
|
return dict(lk, alive=pstate.lock_alive(lk), age_s=time.time() - lk.get('beat', 0))
|
||||||
|
|
||||||
|
|
||||||
|
def kill_session(sid):
|
||||||
|
"""POSIX:结束会话 sid 里的所有进程(含 timeout 另起的进程组)。"""
|
||||||
|
pids = []
|
||||||
|
for d in os.listdir('/proc') if os.path.isdir('/proc') else []:
|
||||||
|
if d.isdigit():
|
||||||
|
try:
|
||||||
|
if os.getsid(int(d)) == sid: pids.append(int(d))
|
||||||
|
except Exception: pass
|
||||||
|
if not pids:
|
||||||
|
try: os.killpg(sid, signal.SIGTERM)
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
|
for p in pids:
|
||||||
|
try: os.kill(p, signal.SIGTERM)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
class Job:
|
||||||
|
def __init__(self):
|
||||||
|
self.p = None; self.info = {}; self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def state(self):
|
||||||
|
with self.lock:
|
||||||
|
run = self.p is not None and self.p.poll() is None
|
||||||
|
if self.p is not None and not run and self.info.get('rc') is None:
|
||||||
|
self.info['rc'] = self.p.returncode; self.info['ended'] = time.time()
|
||||||
|
lk = lock_info()
|
||||||
|
ext = bool(lk and lk['alive']) and not run # 终端或上一个控制台窗口起的流水线
|
||||||
|
return {'running': run or ext, 'external': ext, 'lock': lk, **self.info, 'bash': find_bash() is not None}
|
||||||
|
|
||||||
|
def start(self, frm, to, env):
|
||||||
|
with self.lock:
|
||||||
|
if self.p is not None and self.p.poll() is None: return {'error': '已经在运行'}
|
||||||
|
lk = lock_info()
|
||||||
|
if lk and lk['alive']:
|
||||||
|
return {'error': f"已有一条流水线在运行({lk.get('owner')},pid {lk.get('pid')},{lk['age_s']:.0f} s 前心跳);先停止它"}
|
||||||
|
ck = CHECK.get()
|
||||||
|
if ck.get('pending'): return {'error': '环境自检还没完成,稍等几秒再点'}
|
||||||
|
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': '起止阶段无效'}
|
||||||
|
bash = find_bash()
|
||||||
|
e = dict(os.environ, FROM=frm, TO=to, MONITOR='0', PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PIPE_OWNER='网页')
|
||||||
|
py = train_python()
|
||||||
|
if ' ' not in py: e['PYTHON'] = py.replace('\\', '/')
|
||||||
|
for k, v in (env or {}).items():
|
||||||
|
if k in ENV_KEYS and v not in (None, '') and re.fullmatch(ENV_KEYS[k], str(v)): e[k] = str(v)
|
||||||
|
os.makedirs(os.path.dirname(CONSOLE), exist_ok=True)
|
||||||
|
shown = ' '.join(f'{k}={e[k]}' for k in ('FRESH', 'FROM', 'TO') if k in e)
|
||||||
|
with open(CONSOLE, 'w', encoding='utf-8') as f: f.write(f'$ {shown} bash ml/pipeline.sh all [{time.strftime("%Y-%m-%d %H:%M:%S")}]\n')
|
||||||
|
log = open(CONSOLE, 'a', encoding='utf-8') # 追加模式:网页写入的"已停止"提示不会被子进程覆盖
|
||||||
|
# Windows:CREATE_NO_WINDOW 给子进程树一个隐藏的独立控制台 → 关掉控制台黑窗口不会连带杀训练(审查 N-11)
|
||||||
|
kw = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP | 0x08000000} if os.name == 'nt' else {'start_new_session': True}
|
||||||
|
self.p = subprocess.Popen([bash, 'ml/pipeline.sh', 'all'], cwd=ROOT, env=e, stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, **kw)
|
||||||
|
self.info = {'pid': self.p.pid, 'from': frm, 'to': to, 'started': time.time(), 'rc': None, 'ended': None,
|
||||||
|
'env': {k: e[k] for k in ENV_KEYS if k in e}}
|
||||||
|
return {'ok': True, 'pid': self.p.pid}
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
# 2026-10-18 体面停止:训练类阶段先写 ml/runs/STOP,等训练进程自己存 last.pt 并退出(最多 90 s),再硬杀剩下的。
|
||||||
|
# Windows 的 taskkill /F 收不到信号,不这样做会丢掉上次存档之后的进度。生成/打包本身按 shard/条断点续,直接杀。
|
||||||
|
with self.lock:
|
||||||
|
ours = self.p is not None and self.p.poll() is None
|
||||||
|
lk = lock_info()
|
||||||
|
if not ours and not (lk and lk['alive']): return {'error': '没有在运行'}
|
||||||
|
pid = self.p.pid if ours else int(lk.get('pid') or 0)
|
||||||
|
if not pid: return {'error': '锁里没有 pid,无法停止'}
|
||||||
|
cur = (pstate.load().get('current') or '')
|
||||||
|
if cur in ('train', 'm5', 'apg', 'mine'):
|
||||||
|
open(STOPF, 'w').write(time.strftime('%H:%M:%S'))
|
||||||
|
with open(CONSOLE, 'a', encoding='utf-8') as f: f.write(f'\n[停止请求已发出,等待 {cur} 存档 last.pt…]\n')
|
||||||
|
for _ in range(180):
|
||||||
|
time.sleep(0.5)
|
||||||
|
l2 = pstate.lock_read()
|
||||||
|
if (self.p is not None and self.p.poll() is not None) if ours else not (l2 and pstate.lock_alive(l2)): break
|
||||||
|
with self.lock:
|
||||||
|
if os.name == 'nt':
|
||||||
|
subprocess.run(['taskkill', '/T', '/F', '/PID', str(pid)], capture_output=True)
|
||||||
|
else:
|
||||||
|
if ours:
|
||||||
|
kill_session(pid)
|
||||||
|
try: self.p.wait(15)
|
||||||
|
except subprocess.TimeoutExpired: kill_session(pid); self.p.kill()
|
||||||
|
else: # 只动它自己的会话 / 进程组,绝不按会话杀到用户的终端
|
||||||
|
try:
|
||||||
|
if os.getsid(pid) == pid: kill_session(pid)
|
||||||
|
elif os.getpgid(pid) == pid: os.killpg(pid, signal.SIGTERM)
|
||||||
|
else: os.kill(pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError: pass
|
||||||
|
self.info['stopped'] = True
|
||||||
|
with open(CONSOLE, 'a', encoding='utf-8') as f: f.write(f'\n[已在网页上停止 {time.strftime("%H:%M:%S")}]\n')
|
||||||
|
time.sleep(0.5)
|
||||||
|
pstate.mark_stopped(-15) # 当前阶段记为"已停止"、释放锁(单一写者)
|
||||||
|
try: os.remove(STOPF)
|
||||||
|
except FileNotFoundError: pass
|
||||||
|
return {'ok': True}
|
||||||
|
|
||||||
|
|
||||||
|
STOPF = os.path.join(ROOT, 'ml', 'runs', 'STOP') # 与 ml/common/ckpt.py 的 STOP 同一文件
|
||||||
|
JOB = Job()
|
||||||
|
CHECK = None # main() 里创建(自检要等 find_bash 定义完)
|
||||||
|
|
||||||
|
|
||||||
|
def console(offset):
|
||||||
|
if not os.path.exists(CONSOLE): return {'text': '', 'offset': 0, 'reset': True}
|
||||||
|
size = os.path.getsize(CONSOLE); reset = offset > size or offset == 0
|
||||||
|
if reset: offset = max(0, size - 65536) # 首次只给最后 64 KB
|
||||||
|
with open(CONSOLE, 'rb') as f: f.seek(offset); b = f.read(min(size - offset, 1 << 20))
|
||||||
|
return {'text': b.decode('utf-8', 'replace').replace('\r', ''), 'offset': offset + len(b), 'reset': reset}
|
||||||
|
|
||||||
|
|
||||||
|
class H(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *a): pass
|
||||||
|
|
||||||
|
def send(self, code, body, ctype):
|
||||||
|
b = body if isinstance(body, bytes) else body.encode('utf-8')
|
||||||
|
self.send_response(code); self.send_header('Content-Type', ctype); self.send_header('Content-Length', str(len(b)))
|
||||||
|
self.send_header('Cache-Control', 'no-store'); self.end_headers(); self.wfile.write(b)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
u = urlparse(self.path); q = parse_qs(u.query)
|
||||||
|
try:
|
||||||
|
if u.path in ('/', '/index.html'):
|
||||||
|
with open(HTML, 'rb') as f: return self.send(200, f.read(), 'text/html; charset=utf-8')
|
||||||
|
if u.path == '/api/runs': return self.send(200, json.dumps(list_runs()), 'application/json')
|
||||||
|
if u.path == '/api/log':
|
||||||
|
return self.send(200, json.dumps(read_log(q.get('run', [''])[0], int(q.get('offset', ['0'])[0]))), 'application/json')
|
||||||
|
if u.path == '/api/status': return self.send(200, json.dumps(status()), 'application/json')
|
||||||
|
if u.path == '/api/job': return self.send(200, json.dumps(JOB.state()), 'application/json')
|
||||||
|
if u.path == '/api/console': return self.send(200, json.dumps(console(int(q.get('offset', ['0'])[0]))), 'application/json')
|
||||||
|
self.send(404, 'not found', 'text/plain')
|
||||||
|
except Exception as e:
|
||||||
|
self.send(500, json.dumps({'error': str(e)}), 'application/json')
|
||||||
|
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
u = urlparse(self.path)
|
||||||
|
if not (self.server.remote_control or self.client_address[0] in ('127.0.0.1', '::1')):
|
||||||
|
return self.send(403, json.dumps({'error': '开始/停止只接受本机操作'}), 'application/json')
|
||||||
|
# 审查 N-8:本机任意网页都能向 localhost 发 POST(源地址同为 127.0.0.1)。要求自定义头(跨站请求会触发 CORS 预检,
|
||||||
|
# 本服务不应答 OPTIONS → 浏览器不发),并且 Origin / Sec-Fetch-Site 若存在必须同源。
|
||||||
|
org, sfs, host = self.headers.get('Origin'), self.headers.get('Sec-Fetch-Site'), self.headers.get('Host', '')
|
||||||
|
if self.headers.get('X-Console') != '1' or (sfs and sfs not in ('same-origin', 'none')) or \
|
||||||
|
(org and urlparse(org).netloc not in (host, f'localhost:{self.server.server_port}', f'127.0.0.1:{self.server.server_port}')):
|
||||||
|
return self.send(403, json.dumps({'error': '拒绝:非本控制台页面发起的请求'}), 'application/json')
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get('Content-Length') or 0); body = json.loads(self.rfile.read(n) or b'{}')
|
||||||
|
if u.path == '/api/start':
|
||||||
|
env = dict(body.get('env') or {})
|
||||||
|
if body.get('fresh'): env['FRESH'] = '1'
|
||||||
|
r = JOB.start(body.get('from', 'kin'), body.get('to', 'accept'), env)
|
||||||
|
elif u.path == '/api/stop': r = JOB.stop()
|
||||||
|
else: return self.send(404, 'not found', 'text/plain')
|
||||||
|
self.send(200 if 'ok' in r else 409, json.dumps(r, ensure_ascii=False), 'application/json')
|
||||||
|
except Exception as e:
|
||||||
|
self.send(500, json.dumps({'error': str(e)}), 'application/json')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--port', type=int, default=8100); ap.add_argument('--host', default='127.0.0.1')
|
||||||
|
ap.add_argument('--open', action='store_true', help='启动后自动打开浏览器')
|
||||||
|
ap.add_argument('--remote-control', action='store_true', help='允许其他机器点开始/停止(默认只能看)')
|
||||||
|
a = ap.parse_args()
|
||||||
|
global CHECK
|
||||||
|
CHECK = SelfCheck()
|
||||||
|
srv = ThreadingHTTPServer((a.host, a.port), H); srv.remote_control = a.remote_control
|
||||||
|
url = f'http://localhost:{a.port}/'
|
||||||
|
print(f'训练控制台:{url} (关闭本窗口只关控制台,已开始的训练继续跑;重新打开控制台即可接着看、可停止)', flush=True)
|
||||||
|
if a.open: threading.Timer(0.8, lambda: webbrowser.open(url)).start()
|
||||||
|
try: srv.serve_forever()
|
||||||
|
except KeyboardInterrupt: pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Generated
+227
@@ -0,0 +1,227 @@
|
|||||||
|
{
|
||||||
|
"name": "robot-arm-ml",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "robot-arm-ml",
|
||||||
|
"dependencies": {
|
||||||
|
"onnxruntime-node": "^1.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"globalthis": "^1.0.2",
|
||||||
|
"matcher": "^4.0.0",
|
||||||
|
"semver": "^7.3.5",
|
||||||
|
"serialize-error": "^8.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/globalthis": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/matcher": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/onnxruntime-common": {
|
||||||
|
"version": "1.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.30.0.tgz",
|
||||||
|
"integrity": "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/onnxruntime-node": {
|
||||||
|
"version": "1.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.30.0.tgz",
|
||||||
|
"integrity": "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"os": [
|
||||||
|
"win32",
|
||||||
|
"darwin",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"adm-zip": "^0.6.0",
|
||||||
|
"global-agent": "^4.1.3",
|
||||||
|
"onnxruntime-common": "1.30.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serialize-error": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^0.20.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "robot-arm-ml",
|
||||||
|
"private": true,
|
||||||
|
"description": "Node 端依赖:tests/policy_bench.cjs 用 onnxruntime-node 跑部署形态评测",
|
||||||
|
"dependencies": {
|
||||||
|
"onnxruntime-node": "^1.20.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+205
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一条龙:bash ml/pipeline.sh <阶段> ;阶段按顺序:
|
||||||
|
# setup 安装 Python/Node 依赖(torch 需先按 CUDA 版本装好)
|
||||||
|
# kin 导出运动学/碰撞模型 + JS↔PyTorch 对拍(必须 PASS)
|
||||||
|
# r4 浏览器推理延迟(随机初始化模型导出到 ml/runs/r4/,无头 Edge/Chrome 自动测 → ml/runs/r4.json;不过就停,改模型规模)
|
||||||
|
# gate1 老师一致性门禁(决定生成时是否加 --consensus 4)
|
||||||
|
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
|
||||||
|
# prep 打包训练样本 + R2 标签一致性检查
|
||||||
|
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
|
||||||
|
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型,不过自动训 WTA 头再判
|
||||||
|
# apg APG 首轮微调(从选定模型开始)
|
||||||
|
# mine 挖错循环(3–6 轮,含门禁回滚)
|
||||||
|
# export 导出 ONNX(fp32)到 webapp/models/
|
||||||
|
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)+ 查看器里同一批贴柱题的整次规划耗时(R9 → ml/runs/r9_browser.json)
|
||||||
|
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
|
||||||
|
# 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)
|
||||||
|
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
|
||||||
|
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
|
||||||
|
# 可调(第五轮新增):FRESH=1 从头重跑(旧产物移到 ml/archive/<时间>/,不删;改了生成/模型/挖错参数时只作废受影响的阶段);MIN_GAP_MM(默认 3,gen/prep 共用);
|
||||||
|
# R2_STRICT=1 R2 不过即停;R4_STRICT=0 R4 不过也继续;AUTO_WTA=0 M5 不过不自动训 WTA;TIMEOUT_SCALE 阶段超时倍数
|
||||||
|
# 2026-10-18:按另一台机器的审查改——头分目录不互相覆盖、统一 $PYTHON(Windows Git Bash 可跑)、
|
||||||
|
# gen 自动补量、m5 双口径、export 默认 fp32、新增 accept。
|
||||||
|
# 2026-10-18 第五轮(审查第四轮 N-7/N-10/N-11/N-16 与「无人值守跑完」四个缺口):
|
||||||
|
# 互斥锁(任何入口同一把)、`all` 自动续跑已完成阶段、阶段超时、R2 默认警告继续、M5 不过自动转 WTA、
|
||||||
|
# r4 自动探针(独立文件,不碰 webapp/models)、启动前自检 torch。
|
||||||
|
set -e -o pipefail # pipefail:`node ... | tee` 里 node 失败也算失败
|
||||||
|
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}
|
||||||
|
command -v "$PY" >/dev/null 2>&1 && "$PY" -c "import sys" 2>/dev/null || { echo "找不到可用的 Python($PY),请设 PYTHON=..."; exit 1; }
|
||||||
|
export PYTHON=$PY
|
||||||
|
export NPROC=${NPROC:-$(nproc)}
|
||||||
|
export N_TASKS=${N_TASKS:-50000}
|
||||||
|
export TARGET_SAMPLES=${TARGET_SAMPLES:-1700000}
|
||||||
|
export CFG=${CFG:-ml/configs/base.json}
|
||||||
|
export MIN_GAP_MM=${MIN_GAP_MM:-3} # 老师轨迹认证间隙下限:stats.cjs(补量口径)与 build_dataset.cjs(训练集)必须同值(审查 N-1)
|
||||||
|
export ROUNDS=${ROUNDS:-6} APG_STEPS=${APG_STEPS:-1000} MINE_N=${MINE_N:-50000}
|
||||||
|
TAG=$(basename "$CFG" .json)
|
||||||
|
SEL=ml/runs/selected_ckpt
|
||||||
|
ckpt() { if [ -n "${CKPT:-}" ]; then echo "$CKPT"; elif [ -f $SEL ]; then cat $SEL; else echo ml/runs/$TAG/best.pt; fi; }
|
||||||
|
stage=${1:-help}
|
||||||
|
run() { echo "== $*"; "$@"; }
|
||||||
|
ALL_STAGES="setup kin r4 gate1 gen prep train m5 apg mine export bench accept"
|
||||||
|
stage_info() { case $1 in
|
||||||
|
gen) echo "target_tasks=$(( (N_TASKS + NPROC - 1) / NPROC * NPROC ))" ;;
|
||||||
|
train) echo "tag=$TAG bench_steps=2000" ;;
|
||||||
|
m5) echo "tag=$TAG" ;;
|
||||||
|
mine) echo "rounds=$ROUNDS apg_steps=$APG_STEPS" ;;
|
||||||
|
esac; }
|
||||||
|
# ---- 互斥锁(审查 N-7):网页、终端、`all` 同一把锁;`all` 内部调用的子阶段继承(PIPE_LOCKED)
|
||||||
|
OWN_LOCK=0; BEAT=; IN_STAGE=0
|
||||||
|
case " $ALL_STAGES all " in *" $stage "*)
|
||||||
|
if [ -z "${PIPE_LOCKED:-}" ]; then
|
||||||
|
# Windows Git Bash 的 $$ 是 msys pid,taskkill 要 Windows pid(/proc/$$/winpid)
|
||||||
|
$PY -m ml.pstate lock acquire "${PIPE_OWNER:-终端}:$stage" "$(cat /proc/$$/winpid 2>/dev/null || echo $$)" || exit 9
|
||||||
|
export PIPE_LOCKED=1; OWN_LOCK=1
|
||||||
|
PARENT=$$
|
||||||
|
( while sleep 60; do kill -0 $PARENT 2>/dev/null || exit 0; $PY -m ml.pstate lock beat >/dev/null 2>&1 || true; done ) &
|
||||||
|
BEAT=$!
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
case " $ALL_STAGES " in *" $stage "*)
|
||||||
|
$PY -m ml.pstate begin $stage $(stage_info $stage); IN_STAGE=1 ;;
|
||||||
|
esac
|
||||||
|
on_exit() {
|
||||||
|
rc=$?
|
||||||
|
[ $IN_STAGE = 1 ] && { $PY -m ml.pstate end $stage $rc >/dev/null 2>&1 || true; }
|
||||||
|
if [ $OWN_LOCK = 1 ]; then [ -n "$BEAT" ] && kill $BEAT 2>/dev/null; $PY -m ml.pstate lock release || true; fi
|
||||||
|
exit $rc
|
||||||
|
}
|
||||||
|
trap on_exit EXIT
|
||||||
|
trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返回码、放锁
|
||||||
|
# ---- 训练与门禁的单元(m5 自动分支要对两种头各调一次)
|
||||||
|
train_one() { # $1 = 配置文件
|
||||||
|
local cfg=$1 tag; tag=$(basename "$1" .json)
|
||||||
|
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 ${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 → 续训"; }
|
||||||
|
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$tag --config $cfg $res
|
||||||
|
}
|
||||||
|
m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过
|
||||||
|
local tag=$1 ck=${CKPT:-ml/runs/$1/best.pt} o
|
||||||
|
[ -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 ${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 ${M5_N:-1000} --safety --out $o
|
||||||
|
$PY - "$tag" "$ck" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
tag, ck = sys.argv[1], sys.argv[2]
|
||||||
|
p = json.load(open(f'ml/runs/m5_{tag}_policy.json', encoding='utf-8')); s = json.load(open(f'ml/runs/m5_{tag}_safety.json', encoding='utf-8'))
|
||||||
|
ok = p['collision_rate'] <= 0.02
|
||||||
|
out = {'head': tag, 'ckpt': ck, 'pass': ok,
|
||||||
|
'policy_only': {k: p[k] for k in ('n', 'success', 'collision_rate', 'start_fail')},
|
||||||
|
'with_sim_safety': {k: s[k] for k in ('n', 'success', 'collision_rate', 'brakes_mean', 'start_fail')},
|
||||||
|
'note': '门禁只看 policy_only(纯策略穿柱率);with_sim_safety 为仿真近似安全层,部署口径以 bench 为准'}
|
||||||
|
json.dump(out, open(f'ml/runs/m5_gate_{tag}.json', 'w'), indent=1)
|
||||||
|
print('[%s] 纯策略穿柱率 %.2f%%、到达 %.2f%%;加仿真安全层 穿柱 %.2f%%、到达 %.2f%%' % (
|
||||||
|
tag, 100 * p['collision_rate'], 100 * p['success'], 100 * s['collision_rate'], 100 * s['success']))
|
||||||
|
if ok: open('ml/runs/selected_ckpt', 'w').write(ck); print('M5 门禁:通过 → 后续阶段使用', ck)
|
||||||
|
sys.exit(0 if ok else 3)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
case $stage in
|
||||||
|
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 ;;
|
||||||
|
r4) # 浏览器推理延迟(方案 R4):随机初始化模型导出到独立文件(不碰 webapp/models,审查 N-5),无头浏览器自动测
|
||||||
|
mkdir -p ml/runs/r4
|
||||||
|
[ -f webapp/vendor/ort/ort.min.js ] || [ "${ORT_LOCAL:-1}" = 0 ] || { echo "webapp/vendor/ort 缺失(ort 已入库,先 git pull);确要用 CDN 设 ORT_LOCAL=0"; exit 3; } # 第八轮:不再静默退回 CDN
|
||||||
|
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 ${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'))")
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
if [ "$r" = null ]; then echo "R4 未判定(没找到浏览器或页面未回报),继续;验收汇总会标红"; fi ;;
|
||||||
|
gate1) mkdir -p ml/data; run node ml/gen/consistency.cjs 300 4242 | tee ml/data/consistency.json.tmp; mv ml/data/consistency.json.tmp ml/data/consistency.json ;; # 中断时不留残缺结果
|
||||||
|
gen) per=$(( (N_TASKS + NPROC - 1) / NPROC ))
|
||||||
|
if [ -z "${CONSENSUS:-}" ] && [ -f ml/data/consistency.json ]; then
|
||||||
|
CONSENSUS=$($PY -c "import json;print(1 if json.load(open('ml/data/consistency.json', encoding='utf-8'))['consistentByGateRule']<0.9 else 0)")
|
||||||
|
echo "一致性门禁结果 → CONSENSUS=$CONSENSUS"
|
||||||
|
fi
|
||||||
|
extra=""; [ "${CONSENSUS:-0}" = "1" ] && extra="--consensus 4"
|
||||||
|
run bash ml/gen/run_parallel.sh $per $NPROC 1 "$extra"
|
||||||
|
# 补量:共识过滤与间隙过滤都会降低保留率,样本不足 TARGET_SAMPLES 时按实测"样本/任务"补生成。
|
||||||
|
# 最多 4 次(第五轮由 2 改 4:两种过滤叠加时 2 次可能补不满);已写完的补量 shard 续跑时跳过。
|
||||||
|
s0=$((1 + NPROC))
|
||||||
|
for k in 1 2 3 4; do
|
||||||
|
need=$($PY -c "import json,math;s=json.load(open('ml/data/stats.json', encoding='utf-8'));d=$TARGET_SAMPLES-s['samplesEvery3'];print(0 if d<=0 else math.ceil(d/(s['samplesEvery3']/max(1,s['tasks']))*1.05))")
|
||||||
|
[ "$need" = "0" ] && break
|
||||||
|
echo "样本不足目标 $TARGET_SAMPLES,补生成 $need 条任务(第 $k 次)"
|
||||||
|
per2=$(( (need + NPROC - 1) / NPROC ))
|
||||||
|
old=$($PY -m ml.pstate get gen target_tasks) # 读不到直接失败(审查 N-13:此前空值会被算成 56 这类数)
|
||||||
|
$PY -m ml.pstate set gen target_tasks=$(( old + per2 * NPROC ))
|
||||||
|
run bash ml/gen/run_parallel.sh $per2 $NPROC $s0 "$extra"
|
||||||
|
s0=$((s0 + NPROC))
|
||||||
|
done
|
||||||
|
$PY -c "import json;s=json.load(open('ml/data/stats.json', encoding='utf-8'));import sys;sys.exit(0 if s['samplesEvery3']>=0.9*$TARGET_SAMPLES else 7)" || echo "警告:补 4 次后样本仍不足目标的 90%,按现有样本继续(stats.json 见 samplesEvery3)" ;;
|
||||||
|
prep) # 间隙过滤口径必须与生成时一致(审查 N-1)
|
||||||
|
$PY -c "import json,sys;m=json.load(open('ml/data/stats.json', encoding='utf-8')).get('minGapFilterMm');sys.exit(0 if m is None or float(m)==float($MIN_GAP_MM) else 8)" \
|
||||||
|
|| { echo "MIN_GAP_MM=$MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep"; exit 8; }
|
||||||
|
# 已打包完的 shard 跳过(断点续跑,见 run_prep.sh);参数变了由 pstate reset 把旧 samples 移到 ml/archive/
|
||||||
|
run bash ml/prep/run_prep.sh $NPROC
|
||||||
|
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ${R2_ARGS:-}
|
||||||
|
# 第五轮(审查 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 [ "${R2_STRICT:-0}" = 1 ]; then echo "R2 不通过(R2_STRICT=1):停在这里,把 ml/data/r2.json 发我"; exit 4; fi
|
||||||
|
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
|
||||||
|
fi ;;
|
||||||
|
train) train_one $CFG ;;
|
||||||
|
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 :
|
||||||
|
elif [ "$TAG" != wta ] && [ "${AUTO_WTA:-1}" = 1 ] && [ -z "${CKPT:-}" ]; then
|
||||||
|
# 第五轮(缺口 3):base 头不过 → 流程内自动训 WTA 头再门禁一次,两种头都不过才停
|
||||||
|
echo "M5:$TAG 头不通过 → 自动训练 WTA 回退头(约 3 小时)"
|
||||||
|
$PY -m ml.pstate set m5 branch=wta
|
||||||
|
train_one ml/configs/wta.json
|
||||||
|
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 ;;
|
||||||
|
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 ${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 ;;
|
||||||
|
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)
|
||||||
|
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 ${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)
|
||||||
|
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 可只跑一段
|
||||||
|
FROM=${FROM:-kin}; TO=${TO:-accept}; PLAN=""; on=0
|
||||||
|
for s in $ALL_STAGES; do
|
||||||
|
[ "$s" = "$FROM" ] && on=1; [ $on = 1 ] && PLAN="$PLAN $s"; [ "$s" = "$TO" ] && break
|
||||||
|
done
|
||||||
|
PLAN=${PLAN# }; [ -n "$PLAN" ] || { echo "FROM=$FROM / TO=$TO 不是有效阶段"; exit 1; }
|
||||||
|
# 启动前自检(审查 N-10:不能等到 train 才发现这个 Python 没有 torch)
|
||||||
|
$PY -c "import torch" 2>/dev/null || { echo "当前 Python($($PY -c 'import sys;print(sys.executable)'))没有 torch:请先激活训练环境或设 PYTHON=..."; exit 1; }
|
||||||
|
command -v node >/dev/null || { echo "找不到 node"; exit 1; }
|
||||||
|
fl=""; [ "${FRESH:-0}" = 1 ] && fl="--fresh"
|
||||||
|
$PY -m ml.pstate reset ${PLAN// /,} $fl # 续跑 / 参数变了作废受影响阶段(旧产物移到 ml/archive/<时间>/)
|
||||||
|
TODO=$($PY -m ml.pstate todo)
|
||||||
|
echo "计划:$PLAN"; echo "待跑:${TODO:-(全部已完成)}"
|
||||||
|
# 监控网页随 all 自动在后台启动(MONITOR=0 关闭);端口已被占用说明已经开着,不重复启动
|
||||||
|
if [ "${MONITOR:-1}" = "1" ] && $PY -c "import socket,sys;s=socket.socket();sys.exit(0 if s.connect_ex(('127.0.0.1',8100)) else 1)"; then
|
||||||
|
mkdir -p ml/runs; $PY -m ml.monitor ${MONITOR_ARGS:-} > ml/runs/monitor.log 2>&1 &
|
||||||
|
echo "训练监控已在后台启动:http://localhost:8100/"
|
||||||
|
fi
|
||||||
|
for s in $TODO; do
|
||||||
|
to=$($PY -m ml.pstate timeout $s)
|
||||||
|
if command -v timeout >/dev/null; then
|
||||||
|
# 第五轮(缺口 4):每阶段超时 = 估时 ×3;超时整组进程收到 TERM,阶段记失败、锁释放
|
||||||
|
timeout -k 120 $to bash ml/pipeline.sh $s || { rc=$?; [ $rc = 124 ] && { echo "阶段 $s 超过 $((to/60)) 分钟上限,判定卡死"; $PY -m ml.pstate set $s timed_out=1; }; exit $rc; }
|
||||||
|
else bash ml/pipeline.sh $s; fi
|
||||||
|
done
|
||||||
|
echo "全流程完成:ml/runs/acceptance.json" ;;
|
||||||
|
*) sed -n '2,30p' "$0" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/* 原始老师轨迹 → 闭环训练样本(方案 §4.5 / §5)
|
||||||
|
* node ml/prep/build_dataset.cjs ml/data/shard_0001.jsonl [outDir=ml/data/samples]
|
||||||
|
* 每条 keep=true 的轨迹按 50 Hz 时间律重放,每 3 步取一个样本:
|
||||||
|
* X [N,83] 特征(webapp/js/ik-policy.js features(),与部署同一份代码)
|
||||||
|
* Y [N,16,6] 未来 16 步每步增量 / STEP(到终点后补 0)
|
||||||
|
* V [N] 剩余时间(s)/ 5
|
||||||
|
* SG [N,13] 子目标标签:剩余两个途经点相对当前 q 的差 / 180(不足用终点补)+ 是否需要
|
||||||
|
* TID[N] 任务编号(tasks.jsonl 行号所在的全局 id,用于划分与闭环验证)
|
||||||
|
* 增广:30% 样本对 q_t 加 σ=1° 噪声,标签改为 8 步内线性收回老师轨迹(噪声态须无干涉且间隙 > 1 mm);
|
||||||
|
* 已提交前缀长度 r ∈ {0..3} 均匀随机,前缀取老师接下来 r 步的增量。 */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const IN = process.argv[2], OUTDIR = process.argv[3] || path.join(__dirname, '../data/samples');
|
||||||
|
if (!IN) { console.error('usage: build_dataset.cjs <shard.jsonl> [outDir]'); process.exit(1); }
|
||||||
|
const { S } = require('../../tests/route_bench_setup.cjs')();
|
||||||
|
const POL = require('../../webapp/js/ik-policy.js');
|
||||||
|
const { timeParam } = require('../gen/timelaw.cjs');
|
||||||
|
const { writeNpy } = require('./npy.cjs');
|
||||||
|
const { F_DIM, STEP, H } = POL;
|
||||||
|
const L = S.LIM, O = S.ORDER;
|
||||||
|
const base = path.basename(IN, '.jsonl');
|
||||||
|
let st = 0; for (const ch of base) st = (st * 31 + ch.charCodeAt(0)) >>> 0;
|
||||||
|
const rnd = () => { st = (st * 1664525 + 1013904223) >>> 0; return st / 4294967296; };
|
||||||
|
const gauss = () => Math.sqrt(-2 * Math.log(rnd() + 1e-12)) * Math.cos(2 * Math.PI * rnd());
|
||||||
|
const hash = s => { let h = 2166136261; for (const c of s) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
|
||||||
|
const X = [], Y = [], V = [], SG = [], TID = [], tasks = [];
|
||||||
|
const lines = fs.readFileSync(IN, 'utf8').split('\n').filter(Boolean);
|
||||||
|
let nAug = 0, nAugRej = 0, nGapDrop = 0;
|
||||||
|
/* 2026-10-18:认证最小间隙 < 3 mm 的老师轨迹不进训练集;噪声增广接受地板同步为 3 mm(原 1 mm)。 */
|
||||||
|
const MIN_GAP_MM = +(process.env.MIN_GAP_MM || 3);
|
||||||
|
for (const line of lines) {
|
||||||
|
const r = JSON.parse(line);
|
||||||
|
if (!r.keep) continue;
|
||||||
|
if (r.cert && r.cert.minGap != null && r.cert.minGap < MIN_GAP_MM) { nGapDrop++; continue; } // 与 stats.cjs 同一过滤
|
||||||
|
const tid = hash(r.id) & 0x7fffffff;
|
||||||
|
const split = (hash(r.id + '#split') % 100) < 5 ? 'val' : 'train';
|
||||||
|
const P = r.path;
|
||||||
|
const tp = timeParam(P);
|
||||||
|
const traj = tp.traj, taus = tp.taus, N = traj.length - 1;
|
||||||
|
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, T: r.T, R: r.R, exec_s: N * POL.DT, travel: r.travel_deg, reached: r.reached, err: r.err, split });
|
||||||
|
const vt = tp.cum; // 顶点 τ
|
||||||
|
const inter = P.slice(1, -1).map((p, i) => ({ p, tau: vt[i + 1] }));
|
||||||
|
for (let t = 0; t <= N; t += 3) {
|
||||||
|
let q = traj[t].slice(), qPrev = (t > 0 ? traj[t - 1] : traj[0]).slice();
|
||||||
|
let off = [0, 0, 0, 0, 0, 0], aug = false;
|
||||||
|
if (rnd() < 0.3) {
|
||||||
|
const qn = q.map((v, j) => { let x = v + gauss(); const t2 = L[O[j]]; if (!t2.cont) x = Math.max(t2.lo, Math.min(t2.hi, x)); return x; });
|
||||||
|
nAug++;
|
||||||
|
if (S.collideFree(qn) && S.clearance(qn, 5) > MIN_GAP_MM) { off = qn.map((v, j) => v - q[j]); q = qn; qPrev = qPrev.map((v, j) => v + off[j]); aug = true; }
|
||||||
|
else nAugRej++;
|
||||||
|
}
|
||||||
|
const pos = k => { const i = Math.min(N, t + k); const w = aug ? Math.max(0, 1 - k / 8) : 0; return traj[i].map((v, j) => v + off[j] * w); };
|
||||||
|
const y = new Float32Array(H * 6);
|
||||||
|
let prevP = q;
|
||||||
|
for (let k = 1; k <= H; k++) { const pk = pos(k); for (let j = 0; j < 6; j++) y[(k - 1) * 6 + j] = Math.max(-1, Math.min(1, (pk[j] - prevP[j]) / STEP[j])); prevP = pk; }
|
||||||
|
const rr = Math.floor(rnd() * 4), prefix = [];
|
||||||
|
for (let k = 0; k < rr; k++) { const row = []; for (let j = 0; j < 6; j++) row.push(y[k * 6 + j] * STEP[j]); prefix.push(row); }
|
||||||
|
const remain = inter.filter(v => v.tau > taus[t] + 0.02);
|
||||||
|
const sub = remain.length && rnd() < 0.8 ? remain[0].p : null;
|
||||||
|
const gm = S.gapMatrix(q, POL.GAP_CAP);
|
||||||
|
const f = POL.features(S, { q, qPrev, T: r.T, R: r.R, prefix, sub }, gm);
|
||||||
|
const v1 = remain[0] ? remain[0].p : P[P.length - 1], v2 = remain[1] ? remain[1].p : P[P.length - 1];
|
||||||
|
const sg = new Float32Array(13);
|
||||||
|
for (let j = 0; j < 6; j++) { sg[j] = (v1[j] - q[j]) / 180; sg[6 + j] = (v2[j] - q[j]) / 180; }
|
||||||
|
sg[12] = remain.length ? 1 : 0;
|
||||||
|
X.push(f); Y.push(y); V.push((N - t) * POL.DT / 5); SG.push(sg); TID.push(tid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.mkdirSync(OUTDIR, { recursive: true });
|
||||||
|
const n = X.length;
|
||||||
|
const cat = (arrs, w) => { const o = new Float32Array(n * w); arrs.forEach((a, i) => o.set(a, i * w)); return o; };
|
||||||
|
writeNpy(path.join(OUTDIR, base + '.X.npy'), cat(X, F_DIM), [n, F_DIM]);
|
||||||
|
writeNpy(path.join(OUTDIR, base + '.Y.npy'), cat(Y, H * 6), [n, H, 6]);
|
||||||
|
writeNpy(path.join(OUTDIR, base + '.V.npy'), Float32Array.from(V), [n]);
|
||||||
|
writeNpy(path.join(OUTDIR, base + '.SG.npy'), cat(SG, 13), [n, 13]);
|
||||||
|
writeNpy(path.join(OUTDIR, base + '.TID.npy'), TID, [n], 'int32');
|
||||||
|
// 2026-10-18 断点续跑:tasks.jsonl 最后写、先写 .tmp 再改名 —— 它存在就代表这个 shard 的 5 个 npy 都已写完,run_prep.sh 据此跳过
|
||||||
|
const tf = path.join(OUTDIR, base + '.tasks.jsonl');
|
||||||
|
fs.writeFileSync(tf + '.tmp', tasks.map(t => JSON.stringify(t)).join('\n') + '\n'); fs.renameSync(tf + '.tmp', tf);
|
||||||
|
console.log(JSON.stringify({ shard: base, tasks: tasks.length, samples: n, aug: nAug, augRejected: nAugRej, gapDrop: nGapDrop, featVer: POL.FEAT_VER }));
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/* 导出运动学 + 胶囊碰撞模型给 PyTorch(ml/common/kin_torch.py 读取)
|
||||||
|
* node ml/prep/export_kin.cjs [out=ml/assets/kin.json]
|
||||||
|
* collide.json 重新拟合/重生成 pair2d 后必须重跑(指纹不符时 Python 端拒绝加载)。 */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const { S } = require('../../tests/route_bench_setup.cjs')();
|
||||||
|
const OUT = process.argv[2] || path.join(__dirname, '../assets/kin.json');
|
||||||
|
const COL = JSON.parse(fs.readFileSync(path.join(__dirname, '../../docs/collide.json'), 'utf8'));
|
||||||
|
const ce = S.clusterExport();
|
||||||
|
const out = {
|
||||||
|
fp: S.modelFingerprint(),
|
||||||
|
order: S.ORDER,
|
||||||
|
P: S.ORDER.map(id => S.P[id]), D: S.ORDER.map(id => S.D[id]),
|
||||||
|
lim: S.ORDER.map(id => [S.LIM[id].lo, S.LIM[id].hi, S.LIM[id].cont ? 1 : 0]),
|
||||||
|
tcp0: S.TCP0, capm: S.CAPM,
|
||||||
|
caps: COL.caps.map(c => ({ g: c.g, a: c.a, b: c.b, r: c.r })),
|
||||||
|
clusters: ce.clusters, cpairs: ce.cpairs,
|
||||||
|
lever: S.leverBounds(20000),
|
||||||
|
generated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||||
|
fs.writeFileSync(OUT, JSON.stringify(out));
|
||||||
|
console.log('wrote', OUT, 'caps', out.caps.length, 'clusters', out.clusters.length, 'cpairs', out.cpairs.length, 'fp', out.fp);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* 最小 .npy 写出(float32 / int32,C 序) */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
function writeNpy(file, arr, shape, dtype) {
|
||||||
|
const descr = dtype === 'int32' ? '<i4' : '<f4';
|
||||||
|
let hdr = `{'descr': '${descr}', 'fortran_order': False, 'shape': (${shape.join(', ')}${shape.length === 1 ? ',' : ''}), }`;
|
||||||
|
const pre = 10, total = Math.ceil((pre + hdr.length + 1) / 64) * 64;
|
||||||
|
hdr = hdr + ' '.repeat(total - pre - hdr.length - 1) + '\n';
|
||||||
|
const h = Buffer.alloc(pre); h.write('\x93NUMPY', 0, 'latin1'); h[6] = 1; h[7] = 0; h.writeUInt16LE(hdr.length, 8);
|
||||||
|
const ta = dtype === 'int32' ? Int32Array.from(arr) : (arr instanceof Float32Array ? arr : Float32Array.from(arr));
|
||||||
|
fs.writeFileSync(file, Buffer.concat([h, Buffer.from(hdr, 'latin1'), Buffer.from(ta.buffer, ta.byteOffset, ta.byteLength)]));
|
||||||
|
}
|
||||||
|
module.exports = { writeNpy };
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 并行打包样本:bash ml/prep/run_prep.sh [进程数=CPU 核数]
|
||||||
|
# 2026-10-18 断点续跑:samples/<shard>.tasks.jsonl 存在且比 shard 新 → 该 shard 已打包完,跳过;
|
||||||
|
# shard 已不存在的旧打包结果删掉(不让上一批数据混进来)。中断后重跑只补没打完的 shard。
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
P=${1:-$(nproc)}
|
||||||
|
D=ml/data/samples
|
||||||
|
mkdir -p $D
|
||||||
|
for t in $D/*.tasks.jsonl; do
|
||||||
|
[ -e "$t" ] || continue; b=$(basename "$t" .tasks.jsonl)
|
||||||
|
[ -f ml/data/$b.jsonl ] || { echo "[prep] 删除旧打包 $b(对应 shard 已不存在)"; rm -f $D/$b.*; }
|
||||||
|
done
|
||||||
|
todo=()
|
||||||
|
for f in ml/data/shard_*.jsonl; do
|
||||||
|
b=$(basename "$f" .jsonl)
|
||||||
|
if [ -f $D/$b.tasks.jsonl ] && [ $D/$b.tasks.jsonl -nt "$f" ]; then continue; fi
|
||||||
|
todo+=("$f")
|
||||||
|
done
|
||||||
|
echo "[prep] 共 $(ls ml/data/shard_*.jsonl | wc -l) 个 shard,待打包 ${#todo[@]} 个"
|
||||||
|
[ ${#todo[@]} = 0 ] || printf '%s\n' "${todo[@]}" | xargs -P "$P" -I{} node ml/prep/build_dataset.cjs {} $D
|
||||||
|
${PYTHON:-python} -m ml.data --index $D
|
||||||
Executable
+16
@@ -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))"
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
"""流水线状态与互斥锁(单一写者:所有对 ml/runs/pipeline.json / pipeline.lock 的写都经过本模块,原子替换)。
|
||||||
|
pipeline.sh 自动调用,一般不需要手动运行:
|
||||||
|
python -m ml.pstate reset <阶段1,...> [--fresh] # `all` 开始:保留 rc=0 的已完成阶段(续跑);参数变了只作废受影响的阶段;--fresh 全部作废
|
||||||
|
python -m ml.pstate todo # 打印计划中尚未完成的阶段(空格分隔)
|
||||||
|
python -m ml.pstate begin <阶段> [键=值 ...] # 阶段开始
|
||||||
|
python -m ml.pstate set <阶段> 键=值 ... # 阶段中途更新
|
||||||
|
python -m ml.pstate get <阶段> <键> # 读值;不存在则非零退出(不静默回空)
|
||||||
|
python -m ml.pstate end <阶段> <返回码>
|
||||||
|
python -m ml.pstate timeout <阶段> # 该阶段超时秒数(估时 ×3,TIMEOUT_SCALE 可再乘)
|
||||||
|
python -m ml.pstate lock acquire <所有者> [pid]|release|beat|show
|
||||||
|
2026-10-18 第五轮:
|
||||||
|
- 加锁(审查 N-7):网页 / 终端 / `all` 都走同一把锁 ml/runs/pipeline.lock(pid + 心跳),已有一条在跑时立即拒绝;
|
||||||
|
心跳超过 LOCK_STALE_S 未刷新视为陈旧(进程被强杀),可接管。
|
||||||
|
- 续跑(缺口 1):reset 默认保留已完成阶段;参数变了只作废受它影响的阶段(AFFECTS),其产物移到 ml/archive/,避免新旧参数混用。
|
||||||
|
- 阶段表 STAGES 是估时 / 超时 / 网页进度权重的唯一来源(审查 N-14:网页权重与耗时表同源)。
|
||||||
|
- 停止也走本模块(审查 N-12:此前 monitor 直写 pipeline.json,与子进程竞态)。"""
|
||||||
|
import glob, json, os, shutil, socket, sys, time
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
P = os.path.join(ROOT, 'ml', 'runs', 'pipeline.json')
|
||||||
|
LOCK = os.path.join(ROOT, 'ml', 'runs', 'pipeline.lock')
|
||||||
|
LOCK_STALE_S = 300 # 心跳每 60 s 一次;5 分钟没刷新 = 持锁进程已死
|
||||||
|
|
||||||
|
# 阶段顺序、名称、估时(小时,按 README §耗时表的共识分支上界;网页进度权重即此值)
|
||||||
|
STAGES = [
|
||||||
|
('setup', '安装依赖', 0.2), ('kin', '对拍', 0.2), ('r4', '推理延迟', 0.2), ('gate1', '一致性门禁', 0.5),
|
||||||
|
('gen', '生成老师轨迹', 5.0), ('prep', '打包样本', 0.5), ('train', '预训练', 3.0), ('m5', '头门禁', 0.5),
|
||||||
|
('apg', 'APG 微调', 4.0), ('mine', '挖错循环', 9.0), ('export', '导出', 0.1), ('bench', '部署评测', 1.0),
|
||||||
|
('accept', '验收汇总', 0.5)]
|
||||||
|
ORDER = [s[0] for s in STAGES]
|
||||||
|
EST_H = {s[0]: s[2] for s in STAGES}
|
||||||
|
# m5 不过时流程内自动训 WTA 头(训练 3 h + 门禁),超时按分支后的总量给
|
||||||
|
TIMEOUT_H = {k: max(1.0, 3 * v) for k, v in EST_H.items()}
|
||||||
|
TIMEOUT_H['m5'] = 3 * (EST_H['train'] + 2 * EST_H['m5'])
|
||||||
|
# 参数 → 受它影响的阶段(这些阶段即使上次完成也要重跑,其产物先挪进 ml/archive/)。只改挖错轮数不会让 5 小时的生成作废。
|
||||||
|
_from = lambda st: ORDER[ORDER.index(st):]
|
||||||
|
AFFECTS = {**{k: _from('gen') for k in ('N_TASKS', 'NPROC', 'TARGET_SAMPLES', 'CONSENSUS', 'MIN_GAP_MM')},
|
||||||
|
'CFG': ['r4'] + _from('train'), **{k: _from('mine') for k in ('ROUNDS', 'APG_STEPS', 'MINE_N')}}
|
||||||
|
PARAM_KEYS = list(AFFECTS)
|
||||||
|
# 各阶段产物(相对仓库根,可含通配);作废 / 从头重跑时整体移到 ml/archive/<时间>/,不删除
|
||||||
|
PRODUCTS = {
|
||||||
|
'r4': ['ml/runs/r4', 'ml/runs/r4.json'], 'gate1': ['ml/data/consistency.json'],
|
||||||
|
'gen': ['ml/data/shard_*', 'ml/data/log_*', 'ml/data/stats.json'], 'prep': ['ml/data/samples', 'ml/data/r2.json'],
|
||||||
|
'train': ['ml/runs/base', 'ml/runs/wta', 'ml/runs/small', 'ml/runs/bench_*'],
|
||||||
|
'm5': ['ml/runs/m5_*', 'ml/runs/selected_ckpt'], 'apg': ['ml/runs/apg', 'ml/runs/current.pt'],
|
||||||
|
'mine': ['ml/runs/mine', 'ml/runs/apg_r*', 'ml/data/e2', 'ml/data/samples_e2'],
|
||||||
|
'export': ['webapp/models/traj_policy.onnx', 'webapp/models/traj_policy.meta.json'],
|
||||||
|
'bench': ['ml/runs/policy_bench.json'], 'accept': ['ml/runs/acceptance.json']}
|
||||||
|
|
||||||
|
|
||||||
|
def archive(stages):
|
||||||
|
"""把这些阶段的产物移到 ml/archive/<时间>/(保持相对路径),返回目标目录或 None。"""
|
||||||
|
# 2026-10-18:同一秒内多次归档会撞目录、覆盖旧文件,加序号后缀保证唯一
|
||||||
|
base = os.path.join(ROOT, 'ml', 'archive', time.strftime('%Y%m%d_%H%M%S')); dst, n = base, 1
|
||||||
|
while os.path.exists(dst): dst = '%s_%d' % (base, n); n += 1
|
||||||
|
moved = 0
|
||||||
|
for st in stages:
|
||||||
|
for pat in PRODUCTS.get(st, []):
|
||||||
|
for f in glob.glob(os.path.join(ROOT, pat)):
|
||||||
|
rel = os.path.relpath(f, ROOT); os.makedirs(os.path.dirname(os.path.join(dst, rel)), exist_ok=True)
|
||||||
|
shutil.move(f, os.path.join(dst, rel)); moved += 1
|
||||||
|
return os.path.relpath(dst, ROOT) if moved else None
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
try:
|
||||||
|
with open(P, encoding='utf-8') as f: return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {'started': time.time(), 'plan': None, 'stages': {}, 'current': None, 'params': {}}
|
||||||
|
|
||||||
|
|
||||||
|
def save(d):
|
||||||
|
os.makedirs(os.path.dirname(P), exist_ok=True)
|
||||||
|
tmp = P + '.%d.tmp' % os.getpid()
|
||||||
|
with open(tmp, 'w', encoding='utf-8') as f: json.dump(d, f, ensure_ascii=False, indent=1)
|
||||||
|
os.replace(tmp, P)
|
||||||
|
|
||||||
|
|
||||||
|
def kv(args):
|
||||||
|
out = {}
|
||||||
|
for a in args:
|
||||||
|
k, _, v = a.partition('=')
|
||||||
|
try: v = float(v) if '.' in v else int(v)
|
||||||
|
except ValueError: pass
|
||||||
|
out[k] = v
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def params_now():
|
||||||
|
return {k: os.environ.get(k, '') for k in PARAM_KEYS}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 锁
|
||||||
|
def lock_read():
|
||||||
|
try:
|
||||||
|
with open(LOCK, encoding='utf-8') as f: return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def lock_alive(lk=None):
|
||||||
|
lk = lk if lk is not None else lock_read()
|
||||||
|
return bool(lk) and time.time() - lk.get('beat', 0) < LOCK_STALE_S
|
||||||
|
|
||||||
|
|
||||||
|
def lock_acquire(owner, pid=None):
|
||||||
|
"""原子创建锁文件;已有活锁则返回 (False, 锁内容)。陈旧锁直接接管。"""
|
||||||
|
os.makedirs(os.path.dirname(LOCK), exist_ok=True)
|
||||||
|
now = time.time()
|
||||||
|
rec = {'pid': pid or os.getppid(), 'owner': owner, 'start': now, 'beat': now, 'host': socket.gethostname()}
|
||||||
|
for _ in range(2):
|
||||||
|
try:
|
||||||
|
fd = os.open(LOCK, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||||
|
with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump(rec, f)
|
||||||
|
try: os.remove(os.path.join(ROOT, 'ml', 'runs', 'STOP')) # 上次的停止请求不能让这次一开训就退出
|
||||||
|
except FileNotFoundError: pass
|
||||||
|
return True, rec
|
||||||
|
except FileExistsError:
|
||||||
|
lk = lock_read()
|
||||||
|
if lk is not None and lock_alive(lk): return False, lk
|
||||||
|
try: os.remove(LOCK) # 陈旧锁(或刚好被写了一半):接管
|
||||||
|
except FileNotFoundError: pass
|
||||||
|
return False, lock_read()
|
||||||
|
|
||||||
|
|
||||||
|
def lock_beat():
|
||||||
|
lk = lock_read()
|
||||||
|
if not lk: return
|
||||||
|
lk['beat'] = time.time()
|
||||||
|
tmp = LOCK + '.%d.tmp' % os.getpid()
|
||||||
|
with open(tmp, 'w', encoding='utf-8') as f: json.dump(lk, f)
|
||||||
|
os.replace(tmp, LOCK)
|
||||||
|
|
||||||
|
|
||||||
|
def lock_release():
|
||||||
|
try: os.remove(LOCK)
|
||||||
|
except FileNotFoundError: pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 状态
|
||||||
|
def todo(d=None):
|
||||||
|
d = d or load()
|
||||||
|
return [s for s in (d.get('plan') or []) if (d['stages'].get(s) or {}).get('rc') != 0]
|
||||||
|
|
||||||
|
|
||||||
|
def mark_stopped(rc=-15):
|
||||||
|
"""网页「停止」后调用:当前阶段记为被停止,释放锁。"""
|
||||||
|
d = load(); st = d.get('current')
|
||||||
|
if st and st in d['stages'] and d['stages'][st].get('end') is None:
|
||||||
|
d['stages'][st].update(end=time.time(), rc=rc)
|
||||||
|
d['current'] = None
|
||||||
|
save(d); lock_release()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cmd, *rest = sys.argv[1:] or ['?']
|
||||||
|
if cmd == 'lock':
|
||||||
|
sub = rest[0] if rest else 'show'
|
||||||
|
if sub == 'acquire':
|
||||||
|
ok, lk = lock_acquire(rest[1] if len(rest) > 1 else 'terminal', int(rest[2]) if len(rest) > 2 else None)
|
||||||
|
if not ok:
|
||||||
|
age = time.time() - lk.get('beat', 0)
|
||||||
|
print(f"已有一条流水线在运行({lk.get('owner')},pid {lk.get('pid')},{age:.0f} s 前心跳)。"
|
||||||
|
f"锁文件 {os.path.relpath(LOCK, ROOT)};确认它已经不在了,等 {LOCK_STALE_S // 60} 分钟自动失效或手动删除该文件。", file=sys.stderr)
|
||||||
|
sys.exit(9)
|
||||||
|
elif sub == 'beat': lock_beat()
|
||||||
|
elif sub == 'release': lock_release()
|
||||||
|
else: print(json.dumps(lock_read()))
|
||||||
|
return
|
||||||
|
if cmd == 'timeout':
|
||||||
|
print(int(TIMEOUT_H.get(rest[0], 3.0) * 3600 * float(os.environ.get('TIMEOUT_SCALE') or 1))); return
|
||||||
|
if cmd == 'get':
|
||||||
|
v = (load()['stages'].get(rest[0]) or {}).get('info', {}).get(rest[1])
|
||||||
|
if v is None: print(f'pipeline.json 里没有 {rest[0]}.{rest[1]}', file=sys.stderr); sys.exit(1)
|
||||||
|
print(v); return
|
||||||
|
if cmd == 'todo':
|
||||||
|
print(' '.join(todo())); return
|
||||||
|
d = load(); now = time.time()
|
||||||
|
if cmd == 'reset':
|
||||||
|
plan = rest[0].split(',') if rest and rest[0] else []
|
||||||
|
pn = params_now(); prev = d.get('params') or {}
|
||||||
|
if '--fresh' in rest: bad = set(ORDER)
|
||||||
|
else:
|
||||||
|
changed = [k for k in PARAM_KEYS if prev and prev.get(k) != pn[k]]
|
||||||
|
bad = {st for k in changed for st in AFFECTS[k]}
|
||||||
|
if changed: print('参数与上次不同:' + json.dumps({k: (prev.get(k), pn[k]) for k in changed}, ensure_ascii=False)
|
||||||
|
+ ';受影响的阶段重跑:' + ' '.join(st for st in ORDER if st in bad), file=sys.stderr)
|
||||||
|
keep = {k: v for k, v in d.get('stages', {}).items() if v.get('rc') == 0 and k not in bad}
|
||||||
|
# 受影响 / 未完成的阶段:旧产物挪走,免得续训、跳过 shard 时把旧参数的东西接上
|
||||||
|
arc = archive([st for st in ORDER if st in bad]) if bad else None
|
||||||
|
if arc: print(f'旧产物已移到 {arc}/(未删除)', file=sys.stderr)
|
||||||
|
d = {'started': now, 'plan': plan, 'stages': keep, 'current': None, 'params': pn}
|
||||||
|
done = [s for s in plan if s in keep]
|
||||||
|
if done: print('跳过已完成的 %d 个阶段:%s' % (len(done), ' '.join(done)), file=sys.stderr)
|
||||||
|
save(d); return
|
||||||
|
elif cmd == 'begin':
|
||||||
|
st = rest[0]
|
||||||
|
d['stages'][st] = {'start': now, 'end': None, 'rc': None, 'info': kv(rest[1:]), 'timeout_h': TIMEOUT_H.get(st)}
|
||||||
|
d['current'] = st
|
||||||
|
elif cmd == 'set':
|
||||||
|
s = d['stages'].setdefault(rest[0], {'start': now, 'end': None, 'rc': None, 'info': {}})
|
||||||
|
s['info'].update(kv(rest[1:]))
|
||||||
|
elif cmd == 'end':
|
||||||
|
st, rc = rest[0], int(rest[1]) if len(rest) > 1 else 0
|
||||||
|
s = d['stages'].setdefault(st, {'start': now, 'info': {}})
|
||||||
|
s['end'], s['rc'] = now, rc
|
||||||
|
if d.get('current') == st: d['current'] = None
|
||||||
|
else:
|
||||||
|
print(__doc__); return
|
||||||
|
save(d)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Python ≥ 3.10。torch 请按 CUDA 版本从 https://pytorch.org 安装(RTX 3090 用 cu121/cu124 均可),再装其余:
|
||||||
|
torch>=2.2
|
||||||
|
numpy>=1.24
|
||||||
|
onnx>=1.15
|
||||||
|
onnxruntime>=1.17
|
||||||
|
onnxconverter-common>=1.14
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/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"
|
||||||
|
# 第八轮:连同未跟踪的新文件一起复制(本机实测:新加 vram.py 未 add 时三项假失败)
|
||||||
|
git ls-files -z -co --exclude-standard | 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
|
||||||
|
try: v=json.loads(sys.argv[2])
|
||||||
|
except Exception: v=False # 值不是合法 JSON 时记 false,不能让整项静默消失(第七轮实测:'none' 抛异常 → 两项没进结果)
|
||||||
|
print(json.dumps({'item':sys.argv[1],'pass':v,'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
|
||||||
|
gd=$($PY -c "import json,sys
|
||||||
|
f=lambda p:[json.dumps({**r,'plan_ms':0,'cert':{**r.get('cert',{}),'ms':0}},sort_keys=True) for r in (json.loads(l) for l in open(p,encoding='utf-8') if l.strip())]
|
||||||
|
a,b=f(sys.argv[1]),f(sys.argv[2]);print('OK' if a==b else '不同 %d/%d 条'%(sum(x!=y for x,y in zip(a,b))+abs(len(a)-len(b)),max(len(a),len(b))))" ml/data/a.jsonl ml/data/shard_0011.jsonl)
|
||||||
|
if [ "$gd" = OK ]; then rec gen_resume true "$(grep -o '续写[^,]*' ml/runs/st_gen.log | head -1)(逐条一致;不含 plan_ms/cert.ms 两个墙钟字段,第七轮实测 cmp -s 恒不等)"
|
||||||
|
else rec gen_resume false "续写与一口气生成不同:$gd"; fi
|
||||||
|
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 --config $CFGS --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 --config $CFGS --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
|
||||||
|
try: d=json.loads(sys.argv[1])
|
||||||
|
except Exception: d={}
|
||||||
|
print(json.dumps(d.get('tree_killed')), json.dumps(d.get('survive_parent_exit')))" "$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
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""本机自检的进程部分(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':
|
||||||
|
# 中文 Windows 的 tasklist 表头是 GBK,text=True 会让读线程解码崩掉(stdout 变 None)→ 按字节读再容错解码
|
||||||
|
r = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True, errors='replace')
|
||||||
|
return str(pid) in (r.stdout or '')
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
WAITERR = ['']
|
||||||
|
|
||||||
|
|
||||||
|
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 as e: WAITERR[0] = type(e).__name__ + ': ' + str(e)[:90]; time.sleep(0.25)
|
||||||
|
return None, None # 起不来时把最后一次异常带进结果,别让 tree_started=false 变成没有原因的失败
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
"""本地预览查看器(带 COOP/COEP 头 → 页面跨源隔离 → onnxruntime-web 可用多线程 wasm)
|
||||||
|
python ml/serve.py [端口=8000] 然后打开 http://localhost:8000/?policy=1
|
||||||
|
注:跨源隔离下从 CDN 加载 ort.min.js 需要 CDN 返回 CORP 头;jsDelivr 支持。若被拦,用 &ortLocal=1 并把
|
||||||
|
onnxruntime-web 的 dist 文件(ort.min.js、ort-wasm-simd-threaded*.wasm/.mjs)放到 webapp/vendor/ort/。"""
|
||||||
|
import http.server, os, sys
|
||||||
|
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'webapp'))
|
||||||
|
class H(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
|
||||||
|
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||||
|
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
|
||||||
|
self.send_header('Cache-Control', 'no-store')
|
||||||
|
super().end_headers()
|
||||||
|
extensions_map = {**http.server.SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript'}
|
||||||
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
||||||
|
print(f'http://localhost:{port}/?policy=1')
|
||||||
|
http.server.ThreadingHTTPServer(('', port), H).serve_forever()
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"""闭环仿真(验证 / 挖错 / 门禁共用,PyTorch 批量,无梯度)——按部署节奏运行:
|
||||||
|
50 Hz 执行;队列 ≤ 3 步时推理一次(已提交前缀 r = 队列长度),提交接下来 3 步;
|
||||||
|
冷启动用价值头对候选末态打分(短前瞻),之后按与上一计划的一致性选候选;
|
||||||
|
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点(到达 < 5° 或 1.5 s 超时后切下一个)。
|
||||||
|
safety=False 时不做任何碰撞拦截(暴露真实错误,挖错用);safety=True 时近似部署安全层(候选提交步有干涉则换下一条,全不过则停一步)。
|
||||||
|
python -m ml.sim.closed_loop --ckpt ml/runs/base/best.pt --samples ml/data/samples --split val --n 1000 [--safety]"""
|
||||||
|
import argparse, json, math, os
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.common.features import features, STEP, H, GAP_CAP, D2R, wrap180
|
||||||
|
|
||||||
|
DT = 0.02
|
||||||
|
|
||||||
|
|
||||||
|
FIRST_DEG = 5.0 # R7 首次方向判定阈值(度):小于它的抖动不算选边
|
||||||
|
|
||||||
|
def tasks_to_tensors(tasks, device):
|
||||||
|
q0 = torch.tensor([t['q0'] for t in tasks], dtype=torch.float32, device=device)
|
||||||
|
T = torch.tensor([t['T'] for t in tasks], dtype=torch.float32, device=device)
|
||||||
|
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tasks], dtype=torch.float32, device=device)
|
||||||
|
# 无老师参考(挖错/门禁题)时 exec_s/travel 为 None → NaN;调用方据此区分口径
|
||||||
|
ex = torch.tensor([t['exec_s'] if t.get('exec_s') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
||||||
|
tr = torch.tensor([t['travel'] if t.get('travel') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
||||||
|
return q0, T, R, ex, tr
|
||||||
|
|
||||||
|
|
||||||
|
NOREF_SEC = 6.0 # 无老师参考题的定长时限(s)
|
||||||
|
|
||||||
|
|
||||||
|
def goal_err(kin, q, T, R):
|
||||||
|
xf = kin.cum_xf(q)
|
||||||
|
p = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(-1, 9)
|
||||||
|
e = (T - p).norm(dim=-1)
|
||||||
|
pose = R.abs().sum(-1) > 0
|
||||||
|
tr = (Rq * R).sum(-1)
|
||||||
|
oe = torch.where(pose, torch.acos(((tr - 1) / 2).clamp(-1, 1)) / D2R, torch.zeros_like(e))
|
||||||
|
return e, oe
|
||||||
|
|
||||||
|
|
||||||
|
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5):
|
||||||
|
"""局部 DLS 精修:n 步增量 [B,n,6]"""
|
||||||
|
step = torch.tensor(STEP, device=q.device)
|
||||||
|
pose = (R.abs().sum(-1) > 0).float()
|
||||||
|
Rt = R.view(-1, 3, 3)
|
||||||
|
def err(x):
|
||||||
|
xf = kin.cum_xf(x); p = kin.fk(x, xf); Rq = kin.orient(x, xf)
|
||||||
|
M = Rt @ Rq.transpose(1, 2)
|
||||||
|
rv = 0.5 * torch.stack([M[:, 2, 1] - M[:, 1, 2], M[:, 0, 2] - M[:, 2, 0], M[:, 1, 0] - M[:, 0, 1]], -1) * 100 * pose[:, None]
|
||||||
|
return torch.cat([T - p, rv], -1)
|
||||||
|
out = []; x = q.clone()
|
||||||
|
for _ in range(n):
|
||||||
|
e0 = err(x); J = []
|
||||||
|
for j in range(6):
|
||||||
|
dx = torch.zeros_like(x); dx[:, j] = 1e-3
|
||||||
|
J.append(-(err(x + dx) - e0) / 1e-3)
|
||||||
|
J = torch.stack(J, -1) # [B,6(err),6(q)]
|
||||||
|
A = J @ J.transpose(1, 2) + lam ** 2 * torch.eye(6, device=q.device)
|
||||||
|
y = torch.linalg.solve(A, e0.unsqueeze(-1))
|
||||||
|
dq = (J.transpose(1, 2) @ y).squeeze(-1)
|
||||||
|
sc = (dq.abs() / (step * scale)).max(-1, keepdim=True).values.clamp(min=1)
|
||||||
|
dq = dq / sc
|
||||||
|
out.append(dq); x = x + dq
|
||||||
|
return torch.stack(out, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False):
|
||||||
|
g = torch.Generator(device=device); g.manual_seed(seed)
|
||||||
|
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
|
||||||
|
B = q0.shape[0]
|
||||||
|
step = torch.tensor(STEP, device=device)
|
||||||
|
has_ref = torch.isfinite(ex)
|
||||||
|
limit = torch.where(has_ref, torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC)) if max_sec is None else torch.full_like(ex, max_sec)
|
||||||
|
nmax = int(math.ceil(limit.max().item() / DT))
|
||||||
|
q, qp = q0.clone(), q0.clone()
|
||||||
|
queue = torch.zeros(B, 0, 6, device=device)
|
||||||
|
last = None
|
||||||
|
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device); sub_t = torch.zeros(B, device=device)
|
||||||
|
done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device)
|
||||||
|
col = torch.zeros(B, device=device); travel = torch.zeros(B, device=device); brakes = torch.zeros(B, device=device)
|
||||||
|
traj = [q.clone()] if return_traj else None
|
||||||
|
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
|
||||||
|
j1_acc = torch.zeros(B, device=device); j1_max = torch.zeros(B, device=device); j1_min = torch.zeros(B, device=device)
|
||||||
|
j1_first = torch.zeros(B, device=device) # 2026-10-18 第五轮(审查 N-3):J1 累计偏离首次超过 FIRST_DEG 时的方向(真正的"首次方向")
|
||||||
|
cold = True
|
||||||
|
for it in range(nmax):
|
||||||
|
tnow = it * DT
|
||||||
|
if queue.shape[1] <= 3:
|
||||||
|
r = queue.shape[1]
|
||||||
|
pre = torch.zeros(B, 3, 6, device=device); pre[:, :r] = queue[:, :r]
|
||||||
|
qe = q + queue.sum(1)
|
||||||
|
e, oe = goal_err(kin, qe, T, R)
|
||||||
|
near = (e < 5) & ((R.abs().sum(-1) == 0) | (oe < 5))
|
||||||
|
# 子目标推进
|
||||||
|
if (nsub > 0).any():
|
||||||
|
cur = subs[:, 0]
|
||||||
|
dd = torch.where(kin.cont, wrap180(cur - qe), cur - qe).abs().max(-1).values
|
||||||
|
adv = (nsub > 0) & ((dd < 5) | (tnow - sub_t > 1.5))
|
||||||
|
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, 1:]], 1), subs)
|
||||||
|
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
|
||||||
|
sf = (nsub > 0).float()
|
||||||
|
feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=device), subs[:, 0], sf)
|
||||||
|
z = torch.randn(B * K, H, 6, device=device, generator=g)
|
||||||
|
act, val, sub = model.sample_k(feat, K, z)
|
||||||
|
inc = act.clamp(-1, 1) * step
|
||||||
|
if cold:
|
||||||
|
sub0 = sub
|
||||||
|
need = sub0[:, 12] > 0.5
|
||||||
|
subs = q[:, None] + sub0[:, :12].view(B, 2, 6) * 180
|
||||||
|
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub)); sub_t.fill_(tnow)
|
||||||
|
fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6]
|
||||||
|
if last is None:
|
||||||
|
# 短前瞻:候选末态的价值
|
||||||
|
qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6)
|
||||||
|
f2 = features(kin, qa, qb, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0), torch.zeros(B * K, 3, 6, device=device),
|
||||||
|
torch.zeros(B * K, device=device), qa, torch.zeros(B * K, device=device))
|
||||||
|
v2, _ = model.heads(model.encode(f2))
|
||||||
|
score = -v2.view(B, K)
|
||||||
|
else:
|
||||||
|
n = min(fut.shape[2], last.shape[1] - 3)
|
||||||
|
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
|
||||||
|
d2 = (((fut[:, :, :n] - last[:, None, 3:3 + n]) / step) ** 2).sum(-1)
|
||||||
|
score = -(d2 * w).sum(-1)
|
||||||
|
order = score.argsort(-1, descending=True)
|
||||||
|
pick = order[:, 0]
|
||||||
|
commit = inc[torch.arange(B), pick, r:r + 3]
|
||||||
|
if safety:
|
||||||
|
ok_any = torch.zeros(B, dtype=torch.bool, device=device)
|
||||||
|
chosen = torch.zeros(B, 3, 6, device=device)
|
||||||
|
for kk in range(K):
|
||||||
|
ck = inc[torch.arange(B), order[:, kk], r:r + 3]
|
||||||
|
pts = qe[:, None] + torch.cumsum(ck, 1)
|
||||||
|
mids = torch.cat([qe[:, None], pts[:, :-1]], 1) + 0.5 * ck
|
||||||
|
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
||||||
|
okk = (kin.gaps(allq, 0.5)['gm'].min(-1).values > 0).view(B, 6).all(-1)
|
||||||
|
take = okk & ~ok_any
|
||||||
|
chosen = torch.where(take[:, None, None], ck, chosen); ok_any |= okk
|
||||||
|
commit = torch.where(ok_any[:, None, None], chosen, torch.zeros_like(chosen))
|
||||||
|
brakes += (~ok_any).float()
|
||||||
|
if near.any():
|
||||||
|
ref = dls_steps(kin, qe, T, R)
|
||||||
|
commit = torch.where(near[:, None, None], ref, commit)
|
||||||
|
last = torch.cat([qe[:, None], fut[torch.arange(B), pick]], 1)
|
||||||
|
queue = torch.cat([queue, commit], 1)
|
||||||
|
cold = False
|
||||||
|
d = queue[:, 0]; queue = queue[:, 1:]
|
||||||
|
d = torch.where(done[:, None], torch.zeros_like(d), d)
|
||||||
|
qp = q; q = kin.clamp_box(q + d)
|
||||||
|
travel += d.abs().sum(-1)
|
||||||
|
j1_acc = j1_acc + d[:, 0]; j1_max = torch.maximum(j1_max, j1_acc); j1_min = torch.minimum(j1_min, j1_acc)
|
||||||
|
j1_first = torch.where((j1_first == 0) & (j1_acc.abs() >= FIRST_DEG), torch.sign(j1_acc), j1_first)
|
||||||
|
if it + 1 == n_start: moved_start = travel.clone()
|
||||||
|
if return_traj: traj.append(q.clone())
|
||||||
|
mid = qp + 0.5 * (q - qp)
|
||||||
|
gm = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
|
||||||
|
hit = ((gm[:B] <= 0) | (gm[B:] <= 0)) & ~done
|
||||||
|
col += hit.float()
|
||||||
|
e, oe = goal_err(kin, q, T, R)
|
||||||
|
newly = (~done) & (e < 1) & (oe < 0.5)
|
||||||
|
t_done = torch.where(newly, torch.full_like(t_done, (it + 1) * DT), t_done); done |= newly
|
||||||
|
if done.all(): break
|
||||||
|
ok = done & (t_done <= limit)
|
||||||
|
if moved_start is None: moved_start = travel.clone()
|
||||||
|
e_end, _ = goal_err(kin, q, T, R)
|
||||||
|
start_fail = (moved_start < 1.0) & ~(done & (t_done <= 0.5))
|
||||||
|
okr = ok & has_ref & torch.isfinite(tr)
|
||||||
|
res = {'n': B, 'n_ref': int(has_ref.sum()), 'success': ok.float().mean().item(),
|
||||||
|
'time_ok_1p3': (ok & has_ref & (t_done <= 1.3 * ex)).float().sum().item() / max(1, int(has_ref.sum())) if has_ref.any() else None,
|
||||||
|
'collision_rate': (col > 0).float().mean().item(), 'collision_steps_mean': col.mean().item(),
|
||||||
|
'travel_ratio_median': (travel[okr] / tr[okr].clamp(min=1)).median().item() if okr.any() else None,
|
||||||
|
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item(),
|
||||||
|
'start_fail': start_fail.float().mean().item(),
|
||||||
|
'time_budget': 'teacher×2(≥3 s)' if bool(has_ref.all()) else ('fixed %.0f s' % (max_sec or NOREF_SEC) if not has_ref.any() else 'mixed')}
|
||||||
|
# 绕行侧:J1 累计位移的正向/负向极值哪个更大(+1 / −1),供 R7 左右比例门禁
|
||||||
|
# 首次方向;全程没偏离到 FIRST_DEG 的(基本不绕 J1)退回净方向
|
||||||
|
side = torch.where(j1_first != 0, j1_first, torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max)))
|
||||||
|
per = {'done': done.cpu(), 't_done': t_done.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
|
||||||
|
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu()}
|
||||||
|
if return_traj: per['traj'] = torch.stack(traj, 1).cpu()
|
||||||
|
return res, per
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
|
||||||
|
if not tasks: # 小样本摸底时 val 可能为空
|
||||||
|
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan')}, []
|
||||||
|
agg, pers = {}, []
|
||||||
|
for i in range(0, len(tasks), bs):
|
||||||
|
r, p = rollout(model, kin, tasks[i:i + bs], device, K=K, safety=safety, seed=seed + i)
|
||||||
|
w = r['n']; pers.append((r, p))
|
||||||
|
for k, v in r.items():
|
||||||
|
if k == 'n' or v is None or isinstance(v, str): continue
|
||||||
|
agg[k] = agg.get(k, 0.0) + v * w
|
||||||
|
n = sum(r['n'] for r, _ in pers)
|
||||||
|
out = {k: v / n for k, v in agg.items()}; out['n'] = n
|
||||||
|
out['n_ref'] = sum(r['n_ref'] for r, _ in pers)
|
||||||
|
out['time_budget'] = pers[0][0]['time_budget'] if len({r['time_budget'] for r, _ in pers}) == 1 else 'mixed'
|
||||||
|
return out, pers
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
from ml.model.policy import build
|
||||||
|
from ml.data import load_tasks
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||||
|
ap.add_argument('--tasks', default=None, help='任务 jsonl(覆盖 --samples/--split)')
|
||||||
|
ap.add_argument('--split', default='val'); ap.add_argument('--n', type=int, default=1000)
|
||||||
|
ap.add_argument('--safety', action='store_true'); ap.add_argument('--K', type=int, default=8)
|
||||||
|
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default=None)
|
||||||
|
a = ap.parse_args()
|
||||||
|
ck = torch.load(a.ckpt, map_location=a.device)
|
||||||
|
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
|
||||||
|
kin = Kin(device=a.device)
|
||||||
|
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
|
||||||
|
if a.tasks: tasks = [json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()]
|
||||||
|
else: tasks = [t for t in load_tasks([a.samples]) if t['split'] == a.split]
|
||||||
|
tasks = tasks[:a.n]
|
||||||
|
res, pers = evaluate(model, kin, tasks, a.device, K=a.K, safety=a.safety)
|
||||||
|
by = {}
|
||||||
|
flat_done = torch.cat([p['done'] for _, p in pers]); flat_col = torch.cat([p['col'] for _, p in pers])
|
||||||
|
for t, d, c in zip(tasks, flat_done.tolist(), flat_col.tolist()):
|
||||||
|
k = t.get('cat', '?') + '/' + t.get('mode', '?'); b = by.setdefault(k, [0, 0, 0]); b[0] += 1; b[1] += int(d); b[2] += int(c > 0)
|
||||||
|
res['by_cat_mode'] = {k: {'n': v[0], 'success': v[1] / v[0], 'collision': v[2] / v[0]} for k, v in by.items()}
|
||||||
|
s = json.dumps(res, indent=1); print(s)
|
||||||
|
if a.out: # 先写 .tmp 再改名:中断不会留下半截结果被当成已完成(M5 续跑按文件存在跳过)
|
||||||
|
open(a.out + '.tmp', 'w').write(s); os.replace(a.out + '.tmp', a.out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+48
@@ -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
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 2D 绕柱玩具实验:比较不同动作头在"左右(上下)双模态绕行"下的闭环表现
|
||||||
|
import torch, torch.nn as nn, numpy as np, math, time, sys, json
|
||||||
|
torch.manual_seed(0); np.random.seed(0); torch.set_num_threads(2)
|
||||||
|
R=1.0; H=16; STEP=0.05
|
||||||
|
def demo(s,g,mode):
|
||||||
|
via=np.array([0.0, 1.35*mode])
|
||||||
|
pts=[s, np.array([-0.9,0.95*mode]) if s[0]<-0.9 else s, via, np.array([0.9,0.95*mode]), g]
|
||||||
|
P=np.array(pts); L=np.r_[0,np.cumsum(np.linalg.norm(np.diff(P,axis=0),axis=1))]
|
||||||
|
n=int(L[-1]/STEP)+1; u=np.linspace(0,L[-1],n)
|
||||||
|
x=np.interp(u,L,P[:,0]); y=np.interp(u,L,P[:,1]); Q=np.c_[x,y]
|
||||||
|
# 平滑
|
||||||
|
for _ in range(20): Q[1:-1]=0.5*Q[1:-1]+0.25*(Q[:-2]+Q[2:])
|
||||||
|
return Q
|
||||||
|
def sample_task(hard=False):
|
||||||
|
sy=np.random.uniform(-0.3,0.3) if hard else np.random.uniform(-1.5,1.5)
|
||||||
|
gy=np.random.uniform(-0.3,0.3) if hard else np.random.uniform(-1.5,1.5)
|
||||||
|
return np.array([np.random.uniform(-2.5,-1.6),sy]), np.array([np.random.uniform(1.6,2.5),gy])
|
||||||
|
def collide_path(Q):
|
||||||
|
return np.any(np.linalg.norm(Q,axis=1)<R)
|
||||||
|
def build(n=3000):
|
||||||
|
O=[];A=[]
|
||||||
|
for _ in range(n):
|
||||||
|
s,g=sample_task(hard=np.random.rand()<0.4)
|
||||||
|
d=s[1]+g[1]
|
||||||
|
mode = np.sign(d) if abs(d)>0.3 else (1 if np.random.rand()<0.5 else -1)
|
||||||
|
Q=demo(s,g,mode)
|
||||||
|
if collide_path(Q): continue
|
||||||
|
D=np.diff(Q,axis=0); D=np.r_[D,np.zeros((H,2))]
|
||||||
|
for i in range(len(Q)-1):
|
||||||
|
O.append(np.r_[Q[i],g]); A.append(D[i:i+H].reshape(-1)/STEP)
|
||||||
|
return torch.tensor(np.array(O),dtype=torch.float32), torch.tensor(np.array(A),dtype=torch.float32)
|
||||||
|
O,A=build(); print('samples',len(O),flush=True)
|
||||||
|
AD=2*H
|
||||||
|
def mlp(i,o,h=256): return nn.Sequential(nn.Linear(i,h),nn.SiLU(),nn.Linear(h,h),nn.SiLU(),nn.Linear(h,h),nn.SiLU(),nn.Linear(h,o))
|
||||||
|
class MSE(nn.Module):
|
||||||
|
def __init__(s): super().__init__(); s.f=mlp(4,AD)
|
||||||
|
def loss(s,o,a): return ((s.f(o)-a)**2).mean()
|
||||||
|
def sample(s,o,k=1): return s.f(o).unsqueeze(1).expand(-1,k,-1)
|
||||||
|
class WTA(nn.Module):
|
||||||
|
K=4
|
||||||
|
def __init__(s): super().__init__(); s.f=mlp(4,AD*s.K+s.K)
|
||||||
|
def out(s,o):
|
||||||
|
y=s.f(o); return y[:,:AD*s.K].view(-1,s.K,AD), y[:,AD*s.K:]
|
||||||
|
def loss(s,o,a):
|
||||||
|
p,l=s.out(o); e=(p-a[:,None]).abs().mean(-1); k=e.argmin(1)
|
||||||
|
return e.gather(1,k[:,None]).mean()+0.5*nn.functional.cross_entropy(l,k)
|
||||||
|
def sample(s,o,k=1):
|
||||||
|
p,l=s.out(o)
|
||||||
|
if k==1: return p.gather(1,l.argmax(1)[:,None,None].expand(-1,1,AD))
|
||||||
|
o_=l.argsort(1,descending=True); return p.gather(1,o_[:,:,None].expand(-1,-1,AD)) # 按概率排序
|
||||||
|
class FM(nn.Module):
|
||||||
|
def __init__(s,steps): super().__init__(); s.f=mlp(4+AD+1,AD); s.steps=steps
|
||||||
|
def loss(s,o,a):
|
||||||
|
e=torch.randn_like(a); t=torch.rand(len(a),1); z=(1-t)*a+t*e
|
||||||
|
return ((s.f(torch.cat([o,z,t],1))-(e-a))**2).mean()
|
||||||
|
def sample(s,o,k=1):
|
||||||
|
B=len(o); oo=o.repeat_interleave(k,0); z=torch.randn(B*k,AD); dt=1/s.steps
|
||||||
|
for i in range(s.steps):
|
||||||
|
t=torch.full((B*k,1),1-i*dt); z=z-dt*s.f(torch.cat([oo,z,t],1))
|
||||||
|
return z.view(B,k,AD)
|
||||||
|
class MF(nn.Module): # MeanFlow (Geng et al. 2025)
|
||||||
|
def __init__(s): super().__init__(); s.f=mlp(4+AD+2,AD)
|
||||||
|
def u(s,o,z,r,t): return s.f(torch.cat([o,z,r,t],1))
|
||||||
|
def loss(s,o,a):
|
||||||
|
B=len(a); e=torch.randn_like(a)
|
||||||
|
t=torch.rand(B,1); r=torch.rand(B,1); t,r=torch.maximum(t,r),torch.minimum(t,r)
|
||||||
|
eq=(torch.rand(B,1)<0.75); r=torch.where(eq,t,r)
|
||||||
|
z=(1-t)*a+t*e; v=e-a
|
||||||
|
fn=lambda z_,r_,t_: s.u(o,z_,r_,t_)
|
||||||
|
u,dudt=torch.func.jvp(fn,(z,r,t),(v,torch.zeros_like(r),torch.ones_like(t)))
|
||||||
|
tgt=(v-(t-r)*dudt).detach()
|
||||||
|
return ((u-tgt)**2).mean()
|
||||||
|
def sample(s,o,k=1):
|
||||||
|
B=len(o); oo=o.repeat_interleave(k,0); e=torch.randn(B*k,AD)
|
||||||
|
x=e-s.u(oo,e,torch.zeros(B*k,1),torch.ones(B*k,1)); return x.view(B,k,AD)
|
||||||
|
def train(m,iters=6000,bs=512):
|
||||||
|
opt=torch.optim.AdamW(m.parameters(),1e-3,weight_decay=1e-4)
|
||||||
|
sch=torch.optim.lr_scheduler.CosineAnnealingLR(opt,iters)
|
||||||
|
for it in range(iters):
|
||||||
|
idx=torch.randint(0,len(O),(bs,)); l=m.loss(O[idx],A[idx])
|
||||||
|
opt.zero_grad(); l.backward(); opt.step(); sch.step()
|
||||||
|
return m
|
||||||
|
def seg_collide(p,q):
|
||||||
|
for a in np.linspace(0,1,6):
|
||||||
|
if np.linalg.norm(p+(q-p)*a)<R: return True
|
||||||
|
return False
|
||||||
|
@torch.no_grad()
|
||||||
|
def rollout(m,s,g,k=1,safety=False,commit=3,T=500):
|
||||||
|
p=s.copy(); hits=0; ups=[]; brakes=0
|
||||||
|
for t in range(0,T,commit):
|
||||||
|
o=torch.tensor(np.r_[p,g],dtype=torch.float32)[None]
|
||||||
|
C=m.sample(o,k)[0].numpy().reshape(-1,H,2)*STEP
|
||||||
|
chosen=None
|
||||||
|
for c in C:
|
||||||
|
path=p+np.cumsum(c[:commit],0)
|
||||||
|
if not safety: chosen=c; break
|
||||||
|
pts=np.r_[p[None],path]
|
||||||
|
if not any(seg_collide(pts[i],pts[i+1]) for i in range(commit)): chosen=c; break
|
||||||
|
if chosen is None: brakes+=1; continue
|
||||||
|
for d in chosen[:commit]:
|
||||||
|
q=p+d
|
||||||
|
if seg_collide(p,q): hits+=1
|
||||||
|
p=q
|
||||||
|
if abs(p[0])<0.5: ups.append(np.sign(p[1]))
|
||||||
|
if np.linalg.norm(p-g)<0.1: return dict(ok=1,hits=hits,brakes=brakes,steps=t,mode=np.mean(ups) if ups else 0)
|
||||||
|
return dict(ok=0,hits=hits,brakes=brakes,steps=T,mode=np.mean(ups) if ups else 0)
|
||||||
|
def evaluate(m,k,safety,n=150,hard=True):
|
||||||
|
np.random.seed(123); res=[rollout(m,*sample_task(hard),k=k,safety=safety) for _ in range(n)]
|
||||||
|
ok=np.mean([r['ok'] for r in res]); col=np.mean([r['hits']>0 for r in res])
|
||||||
|
br=np.mean([r['brakes'] for r in res]); up=np.mean([r['mode']>0 for r in res if r['ok']]) if ok>0 else float('nan')
|
||||||
|
return dict(reach=round(ok,3),collide_ep=round(col,3),brakes=round(br,1),frac_up=round(up,2))
|
||||||
|
methods={'MSE':lambda:MSE(),'WTA4':lambda:WTA(),'FM10':lambda:FM(10),'FM1':lambda:FM(1),'MeanFlow1':lambda:MF()}
|
||||||
|
which=sys.argv[1:] or list(methods)
|
||||||
|
out={}
|
||||||
|
for name in which:
|
||||||
|
torch.manual_seed(0); t0=time.time(); m=train(methods[name]()); tt=time.time()-t0
|
||||||
|
o=torch.tensor(np.r_[[-2,0],[2,0]],dtype=torch.float32)[None]
|
||||||
|
t1=time.time(); [m.sample(o,8) for _ in range(50)]; lat=(time.time()-t1)/50*1000
|
||||||
|
r={'train_s':round(tt),'infer_ms_k8':round(lat,2)}
|
||||||
|
r['hard_raw_k1']=evaluate(m,1,False); r['hard_safe_k8']=evaluate(m,4 if name=='WTA4' else (1 if name=='MSE' else 8),True)
|
||||||
|
r['easy_raw_k1']=evaluate(m,1,False,hard=False)
|
||||||
|
out[name]=r; print(name,json.dumps(r,ensure_ascii=False),flush=True)
|
||||||
|
json.dump(out,open(f'/workspace/work/toy/res_{"_".join(which)}.json','w'),indent=1)
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
"""预训练(方案 §6):MeanFlow 一步头 + FK 损失 + 平滑 + 价值头 + 子目标头。
|
||||||
|
python -m ml.train --samples ml/data/samples --out ml/runs/base [--config ml/configs/base.json]
|
||||||
|
吞吐摸底:python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval
|
||||||
|
通过线(方案 R10):全程 60000 步 ≤ 3.5 h,即 samples_per_s ≥ 5000(3090 实测 6.2 k/s ≈ 2.8 h);日志 eta_h 为剩余小时数。
|
||||||
|
每 eval_every 步做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按 success 存 best.pt;连续 patience_evals 次不升即停。"""
|
||||||
|
import argparse, json, math, os, sys, time
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ml.common.kin_torch import Kin
|
||||||
|
from ml.common.features import STEP, D2R
|
||||||
|
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
|
||||||
|
from ml.common import vram
|
||||||
|
from ml.data import Samples, load_tasks
|
||||||
|
from ml.model.policy import build, wta_loss
|
||||||
|
from ml.model.meanflow import meanflow_loss, one_step
|
||||||
|
from ml.sim.closed_loop import evaluate
|
||||||
|
|
||||||
|
|
||||||
|
def fk_points(kin, q, idx):
|
||||||
|
"""选定胶囊端点 + TCP 的世界坐标 [B, 2*len(idx)+1, 3]"""
|
||||||
|
xf = kin.cum_xf(q)
|
||||||
|
a, b = kin.caps_world(q, xf)
|
||||||
|
return torch.cat([a[:, idx], b[:, idx], kin.fk(q, xf)[:, None]], 1)
|
||||||
|
|
||||||
|
|
||||||
|
def q_from_feat(X):
|
||||||
|
s, c = X[:, 0:12:2], X[:, 1:12:2]
|
||||||
|
return torch.atan2(s, c) / D2R
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
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('--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)
|
||||||
|
a = ap.parse_args()
|
||||||
|
cfg = json.load(open(a.config)); tc = cfg['train']
|
||||||
|
if a.steps: tc['steps'] = a.steps
|
||||||
|
os.makedirs(a.out, exist_ok=True)
|
||||||
|
torch.manual_seed(a.seed)
|
||||||
|
dev = a.device
|
||||||
|
vram.limit()
|
||||||
|
kin = Kin(device=dev)
|
||||||
|
data = Samples(a.samples, device=dev, split='train')
|
||||||
|
val_tasks = [t for t in load_tasks(a.samples) if t['split'] == 'val'][:tc['eval_tasks']]
|
||||||
|
print(f'train samples {data.n}, val tasks {len(val_tasks)}, device {dev}', flush=True)
|
||||||
|
model = build(cfg).to(dev)
|
||||||
|
print(f'params {sum(p.numel() for p in model.parameters()) / 1e6:.2f} M', flush=True)
|
||||||
|
opt = torch.optim.AdamW(model.parameters(), lr=tc['lr'], betas=tuple(tc['betas']), weight_decay=tc['wd'])
|
||||||
|
step0, best, bad_evals = 0, -1.0, 0
|
||||||
|
if a.resume:
|
||||||
|
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
|
||||||
|
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)
|
||||||
|
if set_rng_state(ck.get('rng')): print('续训:随机数状态已恢复(batch 顺序与不断点一致)', flush=True)
|
||||||
|
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好 success {best:.4f}', flush=True)
|
||||||
|
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']))))
|
||||||
|
amp = dev.startswith('cuda') and tc.get('amp') == 'bf16'
|
||||||
|
stepv = torch.tensor(STEP, device=dev)
|
||||||
|
fk_idx = torch.linspace(0, kin.cap_a.shape[0] - 1, tc['fk_caps'], device=dev).long()
|
||||||
|
log = open(os.path.join(a.out, 'log.jsonl'), 'a' if a.resume else 'w') # 新训练覆盖旧日志,续训才追加
|
||||||
|
if not val_tasks and not a.no_eval:
|
||||||
|
print('没有 val 任务(样本太少),本次跳过闭环验证', flush=True); a.no_eval = True
|
||||||
|
t0, seen, diverged = time.time(), 0, False
|
||||||
|
saver = Saver(tc.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(原子写)
|
||||||
|
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}
|
||||||
|
if name == 'last.pt': d['rng'] = rng_state()
|
||||||
|
if extra: d.update(extra)
|
||||||
|
atomic_save(d, os.path.join(a.out, name))
|
||||||
|
if name == 'last.pt': saver.mark()
|
||||||
|
step = step0 - 1 # 续训时已训满:循环一次不跑,下面照常收尾
|
||||||
|
for step in range(step0, total):
|
||||||
|
for gpar in opt.param_groups: gpar['lr'] = lr_at(step)
|
||||||
|
X, Y, V, SG = data.batch(tc['batch'])
|
||||||
|
model.train()
|
||||||
|
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
|
||||||
|
mem = model.encode(X)
|
||||||
|
if model.head == 'wta':
|
||||||
|
l_mf, xh = wta_loss(model, mem, Y); mf_raw = l_mf.detach(); xh = xh.float()
|
||||||
|
else:
|
||||||
|
l_mf, mf_raw = meanflow_loss(model, mem, Y, p_neq=tc['p_neq'])
|
||||||
|
e = torch.randn_like(Y)
|
||||||
|
xh = one_step(model, mem, e).float()
|
||||||
|
v, s = model.heads(mem)
|
||||||
|
l_val = (v.float() - V).abs().mean(); l_sub = (s.float() - SG).abs().mean()
|
||||||
|
d2 = xh[:, 2:] - 2 * xh[:, 1:-1] + xh[:, :-2]
|
||||||
|
l_sm = d2.pow(2).mean()
|
||||||
|
l_fk = torch.zeros((), device=dev)
|
||||||
|
if tc['w_fk'] > 0:
|
||||||
|
q = q_from_feat(X)
|
||||||
|
ks = torch.tensor([3, 7, 11, 15], device=dev)
|
||||||
|
qh = q[:, None] + torch.cumsum(xh.clamp(-1, 1) * stepv, 1)[:, ks]
|
||||||
|
qt = q[:, None] + torch.cumsum(Y * stepv, 1)[:, ks]
|
||||||
|
B4 = qh.shape[0] * 4
|
||||||
|
ph = fk_points(kin, qh.reshape(B4, 6), fk_idx); pt = fk_points(kin, qt.reshape(B4, 6), fk_idx)
|
||||||
|
l_fk = ((ph - pt).norm(dim=-1) / 10).mean()
|
||||||
|
loss = tc['w_mf'] * l_mf + tc['w_fk'] * l_fk + tc['w_smooth'] * l_sm + tc['w_value'] * l_val + tc['w_sub'] * l_sub
|
||||||
|
opt.zero_grad(set_to_none=True)
|
||||||
|
loss.backward()
|
||||||
|
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), tc['grad_clip'])
|
||||||
|
if not torch.isfinite(loss):
|
||||||
|
print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break
|
||||||
|
opt.step(); seen += X.shape[0]
|
||||||
|
if saver.stop() or (a.stop_at and step + 1 >= a.stop_at):
|
||||||
|
save('last.pt', step + 1); log.close()
|
||||||
|
print(f'收到停止请求:已存档 last.pt(第 {step + 1} 步),下次运行自动续训', flush=True); sys.exit(143)
|
||||||
|
if saver.due(): save('last.pt', step + 1)
|
||||||
|
if step % tc['log_every'] == 0:
|
||||||
|
el = time.time() - t0
|
||||||
|
rec = {'step': step, 'total': total, 't': time.time(), 'loss': loss.item(), 'mf': mf_raw.item(), 'fk_cm': l_fk.item(), 'smooth': l_sm.item(),
|
||||||
|
'value': l_val.item(), 'sub': l_sub.item(), 'gnorm': gn.item(), 'lr': lr_at(step), 'samples_per_s': seen / max(el, 1e-6),
|
||||||
|
'eta_h': (total - step - 1) * tc['batch'] / max(seen / max(el, 1e-6), 1e-6) / 3600, 'vram': vram.peak_gib()}
|
||||||
|
print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + '\n'); log.flush()
|
||||||
|
if not a.no_eval and (step + 1) % tc['eval_every'] == 0:
|
||||||
|
model.eval()
|
||||||
|
res, _ = evaluate(model, kin, val_tasks, dev)
|
||||||
|
res['step'] = step + 1; res['t'] = time.time()
|
||||||
|
print('EVAL', json.dumps(res), flush=True); log.write(json.dumps({'eval': res}) + '\n'); log.flush()
|
||||||
|
save('last.pt', step + 1)
|
||||||
|
if res['success'] > best + 1e-4:
|
||||||
|
best = res['success']; bad_evals = 0; save('best.pt', step + 1, {'eval': res})
|
||||||
|
else:
|
||||||
|
bad_evals += 1
|
||||||
|
if bad_evals >= tc['patience_evals']: print('闭环到达率不再上升,停止', flush=True); break
|
||||||
|
if diverged: # 发散的权重不能覆盖 last.pt / best.pt:last.pt 保留发散前最后一次存档,M5 门禁会因此判不过 → 自动 WTA
|
||||||
|
print('训练发散,last.pt / best.pt 保留发散前的存档', flush=True); return
|
||||||
|
save('last.pt', step + 1)
|
||||||
|
if a.no_eval: save('best.pt', step + 1)
|
||||||
|
print(f'done, best success {best:.4f}, {(time.time() - t0) / 3600:.2f} h', flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* 贴立柱目标基准(2026-10-17,用户报告"立柱另一侧 → 贴立柱目标误判在立柱区内")
|
||||||
|
* 目标位形 b:本身无干涉,但 J1/J2/J3 任一轴 ±4° 就撞立柱(= 贴着立柱)。
|
||||||
|
* 起点 a:无干涉,且 a→b 关节直线被挡(立柱另一侧)。
|
||||||
|
* 模式:pos = 松手位置任务;pose = 松手位置+姿态任务(姿态取 b 的真实姿态,b 本身就是安全精确解);
|
||||||
|
* rt = 拖拽实时管线,目标不动连续重规划直到到达或 3 s。
|
||||||
|
* 判据:到达(TCP 误差 < 1 mm,pose 另要姿态 < 0.5°)且整条路径 1° 细验无干涉。
|
||||||
|
* node tests/near_column_bench.cjs <solver.js> [N=30] [modes=pos,pose,rt] */
|
||||||
|
const setup = require('./route_bench_setup.cjs');
|
||||||
|
const { S, ArmIK } = setup(process.argv[2]);
|
||||||
|
const N = +process.argv[3] || 30, MODES = (process.argv[4] || 'pos,pose,rt').split(',');
|
||||||
|
const L = S.LIM, O = S.ORDER, w = S.wrap180;
|
||||||
|
let s = 20261017; const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
||||||
|
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 => { const det = S.collideDetail(q) || []; return det.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 < 400000) {
|
||||||
|
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({ a, b });
|
||||||
|
}
|
||||||
|
if (process.env.DUMP) require('fs').writeFileSync(process.env.DUMP, JSON.stringify(cases));
|
||||||
|
const out = {};
|
||||||
|
for (const mode of MODES) {
|
||||||
|
let ok = 0, reached = 0, valid = 0, unsafe = 0, relaxed = 0, ms = 0, worst = 0; const fails = [];
|
||||||
|
cases.forEach((c, ci) => {
|
||||||
|
const T = S.fk(c.b, 0), R = S.orient(c.b);
|
||||||
|
const opt = { tool: 0, step: 0.5, ref: c.a, from: c.a.slice() };
|
||||||
|
if (mode === 'pose') { opt.pose = { R }; opt.priority = 'pos'; }
|
||||||
|
if (S.rtReset) S.rtReset();
|
||||||
|
const t0 = performance.now();
|
||||||
|
let pts = [c.a.slice()], r = null;
|
||||||
|
if (mode === 'rt') {
|
||||||
|
let q = c.a.slice();
|
||||||
|
for (let it = 0; it < 40; it++) { // 每次重规划后沿路径走到头(目标不动;最多 40 次重规划)
|
||||||
|
r = S.solveRoute(T, Object.assign({}, opt, { from: q.slice(), ref: q, warm: q, noVia: true, quick: true }));
|
||||||
|
if (!r || !r.q) break;
|
||||||
|
const seg = (r.via || []).concat([r.q]);
|
||||||
|
for (const p of seg) pts.push(p);
|
||||||
|
const nq = r.q.slice();
|
||||||
|
const tcp = S.fk(nq, 0);
|
||||||
|
if (Math.hypot(tcp[0] - T[0], tcp[1] - T[1], tcp[2] - T[2]) < 1) { q = nq; break; }
|
||||||
|
q = nq;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r = S.solveDyn(T, opt);
|
||||||
|
pts = pts.concat(r.via || [], [r.q]);
|
||||||
|
}
|
||||||
|
const t = performance.now() - t0; ms += t; worst = Math.max(worst, t);
|
||||||
|
for (let i = 1; i < pts.length; i++) pts[i] = pts[i - 1].map((v, k) => L[O[k]].cont ? v + w(pts[i][k] - v) : pts[i][k]);
|
||||||
|
let pv = true; for (let i = 1; i < pts.length; i++) if (!segOK(pts[i - 1], pts[i])) { pv = false; break; }
|
||||||
|
const qf = pts[pts.length - 1], tcp = S.fk(qf, 0);
|
||||||
|
const err = Math.hypot(tcp[0] - T[0], tcp[1] - T[1], tcp[2] - T[2]);
|
||||||
|
const oerr = mode === 'pose' ? ArmIK.rotErrDeg(S.orient(qf), R) : 0;
|
||||||
|
const rc = err < 1 && oerr < 0.5;
|
||||||
|
if (rc) reached++; if (pv) valid++; if (rc && pv) ok++;
|
||||||
|
if (r && r.unsafe) unsafe++; if (r && r.poseRelaxed) relaxed++;
|
||||||
|
if (!(rc && pv)) fails.push({ ci, err: +err.toFixed(1), oerr: +oerr.toFixed(1), pathOK: pv, unsafe: !!(r && r.unsafe), blocked: !!(r && r.blocked) });
|
||||||
|
});
|
||||||
|
out[mode] = { n: cases.length, ok, reached, validPath: valid, unsafe, poseRelaxed: relaxed, msMean: Math.round(ms / cases.length), msMax: Math.round(worst), fails: fails.slice(0, 12) };
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(out, null, 1));
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* 部署形态评测(方案 §9):策略 + 安全层(webapp/js/ik-policy.js 的 Runner,与浏览器同一份代码)
|
||||||
|
* node tests/policy_bench.cjs [model=webapp/models/traj_policy.onnx] [sets=nc,route,r8] [--nc1000] [--nc-n 30] [--route-n 40] [--r8-n 50] [--out file.json]
|
||||||
|
* 集合:nc = near_column_bench 同款 30 个贴立柱任务(pos 与 pose 各跑一遍)
|
||||||
|
* route = route_bench 同款 40 个直线被挡任务(pos)
|
||||||
|
* nc1000= 一次性扩展贴立柱集 1000 例(新种子,验收用;加 --nc1000 开启)
|
||||||
|
* r8 = 方案 R8 对抗集 50 例:J1 起终差 ≥ 140°、直线被挡、终点贴立柱(两侧都要绕远);
|
||||||
|
* 恢复 = 首次换向锁后 3 s 内到达(无锁则按到达计);活锁 = 超时未到达且锁 ≥ 2 次
|
||||||
|
* 2026-10-18:--n 拆成 --nc-n / --route-n / --r8-n;输出 brakeRate(刹停次数 / 推理次数,验收 < 2%)。
|
||||||
|
* 判据:到达(TCP < 1 mm,pose 另需姿态 < 0.5°)且执行轨迹无干涉帧(每步端点+中点 collideFree,外加距离认证复核);
|
||||||
|
* 超时 10 s。另报告推理 / 前瞻 / 安全层 / 整次规划耗时与距离查询数(Node CPU;浏览器同一批题由 ml.eval.r4_probe --viewer 在查看器里测,见 R9)。
|
||||||
|
* 依赖:cd ml && npm install(onnxruntime-node) */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const flag = k => argv.includes(k);
|
||||||
|
const opt = k => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; };
|
||||||
|
const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n']);
|
||||||
|
const pos = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VALOPT.has(argv[i - 1])));
|
||||||
|
const MODEL = pos[0] || path.join(__dirname, '../webapp/models/traj_policy.onnx');
|
||||||
|
const SETS = (pos[1] || 'nc,route').split(',').concat(flag('--nc1000') ? ['nc1000'] : []);
|
||||||
|
let ort; try { ort = require(path.join(__dirname, '../ml/node_modules/onnxruntime-node')); } catch (e) { ort = require('onnxruntime-node'); }
|
||||||
|
const { S, ArmIK } = require('./route_bench_setup.cjs')();
|
||||||
|
const POL = require('../webapp/js/ik-policy.js');
|
||||||
|
const L = S.LIM, O = S.ORDER, w = S.wrap180;
|
||||||
|
const meta = JSON.parse(fs.readFileSync(MODEL.replace('.onnx', '.meta.json'), 'utf8'));
|
||||||
|
if (meta.fp !== S.modelFingerprint()) { console.error('模型碰撞指纹', meta.fp, '≠ 当前', S.modelFingerprint(), ':须重训'); process.exit(2); }
|
||||||
|
if (meta.feat_ver !== POL.FEAT_VER) { console.error('特征版本不符'); 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; }; }
|
||||||
|
/* 贴立柱题移到 webapp/js/policy-cases.js(查看器 autotest 共用同一批题,2026-10-18 第六轮) */
|
||||||
|
const ncCases = (N, seed) => require('../webapp/js/policy-cases.js').ncCases(S, N, seed);
|
||||||
|
function r8Cases(N, seed) {
|
||||||
|
const 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;
|
||||||
|
for (let t = 0; t < 200; t++) {
|
||||||
|
const a = rq(); if (!S.collideFree(a) || !nearColumn(a)) continue; // 2026-10-18:起点也贴柱 → 两侧都要绕远(方案 R8 口径)
|
||||||
|
if (Math.abs(d(a, b)[0]) < 140 || segOK(a, b)) continue;
|
||||||
|
cases.push({ q0: a, T: S.fk(b, 0), R: null }); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
function routeCases(N) {
|
||||||
|
const rnd = lcg(12345);
|
||||||
|
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 / 1)); for (let i = 1; i <= n; i++) if (!S.collideFree(a.map((v, k) => v + dd[k] * i / n))) return false; return true; };
|
||||||
|
const cases = []; let tries = 0;
|
||||||
|
while (cases.length < N && tries < 20000) { tries++; const a = rq(), b = rq(); if (!S.collideFree(a) || !S.collideFree(b)) continue; if (segOK(a, b)) continue; cases.push({ q0: a, T: S.fk(b, 0), R: null }); }
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x, y) => x - y); return +s[Math.min(s.length - 1, Math.floor(s.length * p))].toFixed(2); };
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const sess = await ort.InferenceSession.create(MODEL, { executionProviders: ['cpu'], intraOpNumThreads: 1 });
|
||||||
|
const infer = POL.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn });
|
||||||
|
const cert = POL.makeCert(S, {});
|
||||||
|
const out = { model: MODEL, meta: { fp: meta.fp, eval: meta.eval } };
|
||||||
|
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;
|
||||||
|
const inf = [], saf = [], look = [], planT = [], planW = [], cq = [], times = [], fails = []; let hits = 0; const why = {};
|
||||||
|
for (let ci = 0; ci < cases.length; ci++) {
|
||||||
|
const c = cases[ci];
|
||||||
|
const R = mode === 'pose' ? c.R : null;
|
||||||
|
const r = new POL.Runner(S, infer, { seed: 1000 + ci });
|
||||||
|
r.reset(c.q0); r.setGoal(c.T, R);
|
||||||
|
let prev = c.q0.slice(), cf = 0, tLock = null;
|
||||||
|
for (let k = 0; k < 500 && !r.done; k++) {
|
||||||
|
if (r.needPlan()) await r.plan();
|
||||||
|
if (tLock == null && r.stats.locks > 0) tLock = r.t;
|
||||||
|
const q = r.tick();
|
||||||
|
const mid = prev.map((v, j) => 0.5 * (v + q[j]));
|
||||||
|
if (!S.collideFree(q) || !S.collideFree(mid)) cf++;
|
||||||
|
else if (!cert.seg(prev, q)) uncert++;
|
||||||
|
prev = q.slice();
|
||||||
|
}
|
||||||
|
const { e, oe } = r.err(r.q);
|
||||||
|
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 });
|
||||||
|
inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); planW.push(...r.stats.planWarmMs); 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 (!rc && r.stats.locks >= 2) livelock++;
|
||||||
|
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,
|
||||||
|
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),
|
||||||
|
// 2026-10-18 第六轮(审查 B-1/B-2):前瞻、整次规划耗时,每次规划的距离查询数与记忆命中率,认证失败原因——与查看器 autotest 同口径对照
|
||||||
|
lookaheadMsP99: pct(look, 0.99), planMsP50: pct(planT, 0.5), planMsP99: pct(planT, 0.99), planWarmMsP99: pct(planW, 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 }));
|
||||||
|
};
|
||||||
|
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('r8')) {
|
||||||
|
/* 2026-10-18 第五轮(审查 N-4):出题不足额不再静默——记下要求题数与出题耗时,acceptance 对 n < requested 判不通过 */
|
||||||
|
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)`);
|
||||||
|
await runSet('r8_adversarial', cs, 'pos');
|
||||||
|
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');
|
||||||
|
const s = JSON.stringify(out, null, 1);
|
||||||
|
console.log(s);
|
||||||
|
if (opt('--out')) fs.writeFileSync(opt('--out'), s);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const setup=require('./route_bench_setup.cjs');
|
||||||
|
const solverPath=process.argv[2];const N=+process.argv[3]||40;const live=process.argv[4]==='live';
|
||||||
|
const {S}=setup(solverPath);
|
||||||
|
const L=S.LIM,O=S.ORDER,w=S.wrap180;
|
||||||
|
let s=12345;const rnd=()=>{s=(s*1664525+1013904223)>>>0;return s/4294967296};
|
||||||
|
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);
|
||||||
|
function segOK(a,b){const dd=d(a,b);const m=Math.max(...dd.map(Math.abs));const n=Math.max(1,Math.ceil(m/1));for(let i=1;i<=n;i++){if(!S.collideFree(a.map((v,k)=>v+dd[k]*i/n)))return false}return true}
|
||||||
|
// build cases: free start, free goal config, straight blocked
|
||||||
|
const cases=[];let tries=0;
|
||||||
|
while(cases.length<N&&tries<20000){tries++;const a=rq(),b=rq();if(!S.collideFree(a)||!S.collideFree(b))continue;
|
||||||
|
if(segOK(a,b))continue; // only obstacle cases
|
||||||
|
cases.push({from:a,T:S.fk(b,0)});}
|
||||||
|
const W=[1,1,1,1,1,1];
|
||||||
|
let res={n:cases.length,reached:0,valid:0,blocked:0,len:0,lenDirect:0,ms:0,rev:0,segs:0,timeExec:0,fail:[]};
|
||||||
|
const VMAX=[60,60,90,90,120,180]; // deg/s assumed
|
||||||
|
function execTime(pts){let t=0;for(let i=1;i<pts.length;i++){const dd=d(pts[i-1],pts[i]);t+=Math.max(...dd.map((x,k)=>Math.abs(x)/VMAX[k]))+ (pts.length>2&&i<pts.length-1?0.25:0)}return t}
|
||||||
|
for(const c of cases){
|
||||||
|
const t0=performance.now();
|
||||||
|
const opt={tool:0,step:0.5,ref:c.from,from:c.from.slice()};
|
||||||
|
if(live){opt.noVia=true;opt.quick=true;opt.warm=c.from}
|
||||||
|
const r=live?S.solveRoute(c.T,opt):S.solveDyn(c.T,opt);
|
||||||
|
const ms=performance.now()-t0;res.ms+=ms;
|
||||||
|
const pts=[c.from].concat(r.via||[],[r.q]);
|
||||||
|
// unwrap
|
||||||
|
for(let i=1;i<pts.length;i++)pts[i]=pts[i-1].map((v,k)=>L[O[k]].cont?v+w(pts[i][k]-v):pts[i][k]);
|
||||||
|
let ok=true;for(let i=1;i<pts.length;i++)if(!segOK(pts[i-1],pts[i])){ok=false;break}
|
||||||
|
const tcp=S.fk(r.q,0);const err=Math.hypot(tcp[0]-c.T[0],tcp[1]-c.T[1],tcp[2]-c.T[2]);
|
||||||
|
const reached=err<1&&!r.blocked;
|
||||||
|
if(reached)res.reached++; if(ok)res.valid++; if(r.blocked)res.blocked++;
|
||||||
|
let len=0;for(let i=1;i<pts.length;i++)len+=d(pts[i-1],pts[i]).reduce((a,x)=>a+Math.abs(x),0);
|
||||||
|
const direct=d(c.from,r.q).reduce((a,x)=>a+Math.abs(x),0);
|
||||||
|
// reversals
|
||||||
|
let rev=0;for(let k=0;k<6;k++){let sg=0;for(let i=1;i<pts.length;i++){const x=d(pts[i-1],pts[i])[k];if(Math.abs(x)<2)continue;const s2=Math.sign(x);if(sg&&s2!==sg)rev++;sg=s2}}
|
||||||
|
res.per=res.per||[];res.per.push({ms:Math.round(ms),reached,ok,len:Math.round(len),rev,segs:pts.length-1,exec:+execTime(pts).toFixed(2),detour:r.detour||null});
|
||||||
|
if(reached&&ok){res.len+=len;res.lenDirect+=direct;res.rev+=rev;res.segs+=pts.length-1;res.timeExec+=execTime(pts)}
|
||||||
|
else res.fail.push({reached,ok,err:+err.toFixed(1),detour:r.detour||null,blocked:!!r.blocked});
|
||||||
|
}
|
||||||
|
const g=res.reached;const both=cases.length-res.fail.length;
|
||||||
|
require('fs').writeFileSync(process.argv[5]||require('path').join(require('os').tmpdir(),'route_bench_per.json'),JSON.stringify(res.per));console.log(JSON.stringify({n:res.n,reached:res.reached,validPath:res.valid,goodBoth:both,blocked:res.blocked,
|
||||||
|
avgPlanMs:+(res.ms/res.n).toFixed(0),avgJointTravel:+(res.len/both).toFixed(0),avgDirect:+(res.lenDirect/both).toFixed(0),
|
||||||
|
avgReversals:+(res.rev/both).toFixed(2),avgSegs:+(res.segs/both).toFixed(2),avgExecS:+(res.timeExec/both).toFixed(2),
|
||||||
|
failTypes:res.fail.slice(0,12)}));
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/* 路径规划基准的求解器装配(Node):node tests/route_bench.cjs [solver.js] [N] */
|
||||||
|
const fs=require('fs'),path=require('path');
|
||||||
|
const ROOT=path.resolve(__dirname,'..');
|
||||||
|
module.exports=function(solverPath,collidePath){
|
||||||
|
const ArmIK=require(solverPath||path.join(ROOT,'webapp/js/ik-solver.js'));
|
||||||
|
const JMap=/* 关节表取自 arm/kinematics.py JOINTS(无需 mesh_arm.json) */ {"J1": {"rear": [-51.0, 128.0, 610.0], "d": [1, 0, 0]}, "J2": {"rear": [19.5, 100.0, 490.0], "d": [0, 1, 0]}, "J3": {"rear": [19.5, 128.0, 445.0], "d": [0, 0, -1]}, "J4": {"rear": [19.5, 100.0, 332.0], "d": [0, 1, 0]}, "J5": {"rear": [19.5, 100.0, 222.0], "d": [0, 1, 0]}, "J6": {"rear": [19.5, 128.0, 176.0], "d": [0, 0, -1]}};
|
||||||
|
const travel=JSON.parse(fs.readFileSync(ROOT+'/docs/travel.json','utf8'));
|
||||||
|
const COL=JSON.parse(fs.readFileSync(collidePath||ROOT+'/docs/collide.json','utf8'));
|
||||||
|
const TR={};for(const t of travel){const a=COL.adj[t.axis];TR[t.axis]={lo:a?a[0]:t.lo,hi:a?a[1]:t.hi,continuous:!!t.continuous};}
|
||||||
|
const S=ArmIK.make({joints:JMap,travel:TR,tcp:[19.5,128,93],collide:COL});
|
||||||
|
return {S,ArmIK};
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* 实时拖拽仿真基准:目标沿直线从 A 的 TCP 拖到 B 的 TCP(中间被立柱挡住),每帧 16.7 ms。
|
||||||
|
* 每帧:手臂沿当前路径按各轴速度上限前进;求解器空闲就用"此刻位形"作起点重规划(quick 模式)。
|
||||||
|
* 统计:重规划耗时分布、穿模帧(1° 细验)、拖完 6 s 后的到达误差。
|
||||||
|
* node tests/rt_drag_bench.cjs <solver.js> [场景数=12] [帧数=120] */
|
||||||
|
const setup = require('./route_bench_setup.cjs');
|
||||||
|
const { S } = setup(process.argv[2]);
|
||||||
|
const NC = +process.argv[3] || 12, NF = +process.argv[4] || 120, DT = 1 / 60, TAIL = 360;
|
||||||
|
const L = S.LIM, O = S.ORDER, w = S.wrap180;
|
||||||
|
let s = 777; const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
||||||
|
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 / 1)); for (let i = 1; i <= n; i++) if (!S.collideFree(a.map((v, k) => v + dd[k] * i / n))) return false; return true; };
|
||||||
|
const VMAX = [60, 60, 90, 90, 120, 180];
|
||||||
|
const cases = []; let tries = 0;
|
||||||
|
while (cases.length < NC && tries < 50000) { tries++; const a = rq(), b = rq(); if (!S.collideFree(a) || !S.collideFree(b) || segOK(a, b)) continue; cases.push([a, b]); }
|
||||||
|
const lat = []; let badFrames = 0, frames = 0, reached = 0, stuck = 0;
|
||||||
|
for (const [a, b] of cases) {
|
||||||
|
const TA = S.fk(a, 0), TB = S.fk(b, 0);
|
||||||
|
let q = a.slice(), path = [], busyUntil = 0, tNow = 0;
|
||||||
|
for (let f = 0; f < NF + TAIL; f++) {
|
||||||
|
tNow = f * DT * 1000;
|
||||||
|
const u = Math.min(1, f / NF), T = TA.map((v, i) => v + (TB[i] - v) * u);
|
||||||
|
if (tNow >= busyUntil) { // 求解器空闲:派发一次重规划(结果在耗时后才生效)
|
||||||
|
const t0 = performance.now();
|
||||||
|
const r = S.solveRoute(T, { tool: 0, step: 0.5, ref: q, warm: q, from: q.slice(), noVia: true, quick: true });
|
||||||
|
const ms = performance.now() - t0; lat.push(ms);
|
||||||
|
busyUntil = tNow + ms;
|
||||||
|
if (r && r.q) { path = (r.via || []).concat([r.q]); }
|
||||||
|
}
|
||||||
|
/* 按轴速上限沿折线前进 */
|
||||||
|
let budget = 1;
|
||||||
|
const q0 = q.slice();
|
||||||
|
while (path.length && budget > 1e-9) {
|
||||||
|
const tgt = q.map((v, i) => L[O[i]].cont ? v + w(path[0][i] - v) : path[0][i]);
|
||||||
|
const dd = tgt.map((v, i) => v - q[i]);
|
||||||
|
const need = Math.max(...dd.map((x, i) => Math.abs(x) / (VMAX[i] * DT)));
|
||||||
|
if (need <= budget) { q = tgt; path.shift(); budget -= need; }
|
||||||
|
else { q = q.map((v, i) => v + dd[i] * budget / need); budget = 0; }
|
||||||
|
}
|
||||||
|
frames++;
|
||||||
|
if (!segOK(q0, q)) badFrames++;
|
||||||
|
}
|
||||||
|
const tcp = S.fk(q, 0), err = Math.hypot(tcp[0] - TB[0], tcp[1] - TB[1], tcp[2] - TB[2]);
|
||||||
|
if (err < 1) reached++; else stuck++;
|
||||||
|
}
|
||||||
|
lat.sort((x, y) => x - y);
|
||||||
|
const pct = p => +lat[Math.min(lat.length - 1, Math.floor(lat.length * p))].toFixed(1);
|
||||||
|
console.log(JSON.stringify({ cases: cases.length, replans: lat.length, latP50: pct(0.5), latP90: pct(0.9), latP99: pct(0.99), latMax: pct(1),
|
||||||
|
latMean: +(lat.reduce((x, y) => x + y, 0) / lat.length).toFixed(1), collidingFrames: badFrames, frames, reachedAfter6s: reached }));
|
||||||
@@ -360,5 +360,7 @@
|
|||||||
<script src="js/urlparams.js"></script>
|
<script src="js/urlparams.js"></script>
|
||||||
<script src="js/ik-solver.js"></script>
|
<script src="js/ik-solver.js"></script>
|
||||||
<script src="js/ik-glue.js"></script>
|
<script src="js/ik-glue.js"></script>
|
||||||
|
<script src="js/ik-policy.js"></script>
|
||||||
|
<script src="js/ik-policy-glue.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+57
-8
@@ -330,9 +330,30 @@ function setJoints(q, doAnim, via, ms) {
|
|||||||
refreshTarget();
|
refreshTarget();
|
||||||
if (COL) refreshJointLabelsLazy(ORDER.map(id => ang[id]));
|
if (COL) refreshJointLabelsLazy(ORDER.map(id => ang[id]));
|
||||||
}
|
}
|
||||||
|
/* 实时碰撞守护(2026-10-15):动画每帧对当前显示位形做一次碰撞判定(~1.2 ms/帧),
|
||||||
|
命中立即停住并亮红标。规划给的折线本已按 1.5° 细验,这里是最后一道独立防线,
|
||||||
|
也让"此刻是否碰撞"实时可见。统计供测试/控制台:window.__RT_GUARD()。 */
|
||||||
|
const RTG = { frames: 0, hits: 0, ms: 0, last: null };
|
||||||
|
window.__RT_GUARD = () => Object.assign({}, RTG, { avgMs: RTG.frames ? RTG.ms / RTG.frames : 0 });
|
||||||
|
function rtGuard(qNow) {
|
||||||
|
if (!S || !S.collideFree) return true;
|
||||||
|
const t = performance.now();
|
||||||
|
const ok = S.collideFree(qNow);
|
||||||
|
RTG.ms += performance.now() - t; RTG.frames++;
|
||||||
|
if (!ok) {
|
||||||
|
RTG.hits++; RTG.last = qNow.slice();
|
||||||
|
const b = document.getElementById('ikBadge');
|
||||||
|
if (b) { b.textContent = '实时碰撞守护:检测到干涉,已停住'; b.className = 'badge badb'; }
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
function animTo(q, ms, via) {
|
function animTo(q, ms, via) {
|
||||||
|
/* 实时跟随:上一段动画还在走时被新规划打断,新动画从"正在运动"接续——
|
||||||
|
不再从零速缓入(否则每次重规划都顿一下),只在终点缓出。 */
|
||||||
|
const moving = !!anim;
|
||||||
if (anim) { anim = null; }
|
if (anim) { anim = null; }
|
||||||
const from = ORDER.map(id => ang[id]);
|
const from = ORDER.map(id => ang[id]);
|
||||||
|
const guardOn = !S.collideFree || S.collideFree(from); // 起点已在干涉里(脱困动作)不设守护
|
||||||
/* 连续轴(J1 肩抬举 / J3 大臂自转 / J6 腕自转)走「短弧」。
|
/* 连续轴(J1 肩抬举 / J3 大臂自转 / J6 腕自转)走「短弧」。
|
||||||
三者物理上无机械限位,θ 与 θ±360 是同一物理位形(applyJoint 走 cos/sin);
|
三者物理上无机械限位,θ 与 θ±360 是同一物理位形(applyJoint 走 cos/sin);
|
||||||
若按数值直接线性插值,+151.9° → −167.0° 会反向扫 318.9° 绕一整圈(实测)。
|
若按数值直接线性插值,+151.9° → −167.0° 会反向扫 318.9° 绕一整圈(实测)。
|
||||||
@@ -340,7 +361,7 @@ function animTo(q, ms, via) {
|
|||||||
end 保存求解器给的代表角,末帧落回它——与插值终点恰好相差 360° 的整数倍,
|
end 保存求解器给的代表角,末帧落回它——与插值终点恰好相差 360° 的整数倍,
|
||||||
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。
|
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。
|
||||||
via:绕行中间位形(solveRoute 判直线被挡时给的两段式路径),逐段同样按短弧折算,
|
via:绕行中间位形(solveRoute 判直线被挡时给的两段式路径),逐段同样按短弧折算,
|
||||||
每段各 ms 毫秒——渲染走的段与碰撞校验走的段是同一批,不存在"校验过了但动画穿模"。 */
|
渲染走的段与碰撞校验走的段是同一批,不存在"校验过了但动画穿模"。 */
|
||||||
const segs = [];
|
const segs = [];
|
||||||
let p = from;
|
let p = from;
|
||||||
for (const raw of (via || []).concat([q])) {
|
for (const raw of (via || []).concat([q])) {
|
||||||
@@ -352,17 +373,37 @@ function animTo(q, ms, via) {
|
|||||||
segs.push(t); p = t;
|
segs.push(t); p = t;
|
||||||
}
|
}
|
||||||
const end = q.slice();
|
const end = q.slice();
|
||||||
const total = ms * segs.length;
|
/* 时间参数化(2026-10-15):旧版每段固定 ms 且每段各自 smoothstep —— 每个中间位形都
|
||||||
|
减速到 0 再起步(顿挫),且 5° 的小段和 150° 的大段用同样时间(小段慢吞吞、大段猛甩)。
|
||||||
|
现在:各段时长 ∝ 该段"最慢轴所需时间"(按轴速比 VMAX 折算,近端轴更慢),
|
||||||
|
整条折线只做一次全局 smoothstep:只在起点/终点加减速,途经中间位形不停顿。
|
||||||
|
走的仍是校验过的那条折线(中间位形经捷径平滑后通常只剩 1~2 个,折角很少)。 */
|
||||||
|
const VREL = [0.6, 0.6, 0.9, 0.9, 1.2, 1.8];
|
||||||
|
const pts = [from].concat(segs), dur = [];
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
let t = 0;
|
||||||
|
for (let j = 0; j < 6; j++) t = Math.max(t, Math.abs(pts[i][j] - pts[i - 1][j]) / VREL[j]);
|
||||||
|
dur.push(Math.max(t, 1e-3));
|
||||||
|
}
|
||||||
|
const sum = dur.reduce((a, b) => a + b, 0);
|
||||||
|
/* 总时长:ms 视为"标准 90° 移动"的时长,按路径实际长度伸缩,夹在 [0.6ms, 3.5ms] */
|
||||||
|
const total = Math.max(ms * 0.6, Math.min(ms * 3.5, ms * sum / 150));
|
||||||
|
const cum = [0]; for (const d of dur) cum.push(cum[cum.length - 1] + d / sum);
|
||||||
anim = { from: from, to: segs[segs.length - 1], t0: performance.now(), ms: total };
|
anim = { from: from, to: segs[segs.length - 1], t0: performance.now(), ms: total };
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (!anim) return;
|
if (!anim) return;
|
||||||
const el = performance.now() - anim.t0;
|
const el = performance.now() - anim.t0;
|
||||||
const si = Math.min(segs.length - 1, Math.floor(el / ms));
|
const u = Math.min(1, el / total);
|
||||||
const k = Math.min(1, (el - si * ms) / ms);
|
const k = u;
|
||||||
const e = k * k * (3 - 2 * k);
|
const e = moving ? 1 - (1 - u) * (1 - u) // 接续运动:只缓出
|
||||||
const a0 = si === 0 ? from : segs[si - 1], a1 = segs[si];
|
: u * u * (3 - 2 * u); // 静止起步:全局一次缓入缓出
|
||||||
|
let si = 0; while (si < dur.length - 1 && e > cum[si + 1]) si++;
|
||||||
|
const f = Math.min(1, Math.max(0, (e - cum[si]) / (cum[si + 1] - cum[si])));
|
||||||
|
const a0 = pts[si], a1 = pts[si + 1];
|
||||||
|
const qNow = a0.map((v, i) => v + (a1[i] - v) * f);
|
||||||
|
if (guardOn && !rtGuard(qNow)) { anim = null; return; } // 停在上一帧(已判无干涉)的位形
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const id = ORDER[i], a = a0[i] + (a1[i] - a0[i]) * e;
|
const id = ORDER[i], a = qNow[i];
|
||||||
ang[id] = a;
|
ang[id] = a;
|
||||||
const sl = $('s' + id), v = $('v' + id);
|
const sl = $('s' + id), v = $('v' + id);
|
||||||
if (sl) sl.value = a;
|
if (sl) sl.value = a;
|
||||||
@@ -492,10 +533,12 @@ function renderResult(res, src) {
|
|||||||
: res.blocked
|
: res.blocked
|
||||||
? '路径被立柱挡住 · 已停在边界(目标差 ' + dsc.toFixed(1) + ' mm)'
|
? '路径被立柱挡住 · 已停在边界(目标差 ' + dsc.toFixed(1) + ' mm)'
|
||||||
: res.detour
|
: res.detour
|
||||||
? (res.detour === 'retract_pass' ? '避障到达 · 模拟人类回退绕过立柱'
|
? (res.detour === 'planned' ? '避障到达 · 规划绕行(' + ((res.via || []).length) + ' 个中间位形)'
|
||||||
|
: res.detour === 'retract_pass' ? '避障到达 · 模拟人类回退绕过立柱'
|
||||||
: (res.detour === 'gentle' ? '避障到达 · 微调协同避开立柱'
|
: (res.detour === 'gentle' ? '避障到达 · 微调协同避开立柱'
|
||||||
: (res.detour === 'coordinated' ? '避障到达 · 平滑协同跨越立柱'
|
: (res.detour === 'coordinated' ? '避障到达 · 平滑协同跨越立柱'
|
||||||
: '绕行立柱后到达')))
|
: '绕行立柱后到达')))
|
||||||
|
: (res.poseRelaxed && posOk) ? '位置到达 · 精确姿态会撞立柱,已取最接近的无碰撞姿态(偏差 ' + dori.toFixed(1) + '\u00B0)'
|
||||||
: (posOk && oriOk) ? '已到达'
|
: (posOk && oriOk) ? '已到达'
|
||||||
: oriPrio ? '姿态到达·位置最近 ' + dsc.toFixed(1) + ' mm'
|
: oriPrio ? '姿态到达·位置最近 ' + dsc.toFixed(1) + ' mm'
|
||||||
: mixMid ? '混合折中 · 位置差 ' + dsc.toFixed(1) + ' mm · 姿态残差 ' + dori.toFixed(2) + '\u00B0'
|
: mixMid ? '混合折中 · 位置差 ' + dsc.toFixed(1) + ' mm · 姿态残差 ' + dori.toFixed(2) + '\u00B0'
|
||||||
@@ -665,6 +708,10 @@ function runSolve() {
|
|||||||
TGT = [X, Y, Z];
|
TGT = [X, Y, Z];
|
||||||
lastTool = +$('ikTool').value || 0;
|
lastTool = +$('ikTool').value || 0;
|
||||||
refreshTarget();
|
refreshTarget();
|
||||||
|
/* 策略模式(?policy=1,ik-policy-glue.js):目标交给闭环策略执行,不调用规划器(2026-10-17) */
|
||||||
|
if (window.__POLICY && window.__POLICY.active() && window.__POLICY.setGoal(TGT.slice(), poseOn ? Rsolver() : null, lastTool)) {
|
||||||
|
badge.textContent = '策略执行中…'; badge.className = 'badge wait'; return;
|
||||||
|
}
|
||||||
badge.textContent = '计算中…'; badge.className = 'badge wait';
|
badge.textContent = '计算中…'; badge.className = 'badge wait';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(),
|
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(),
|
||||||
@@ -703,6 +750,7 @@ function runSolveLive() {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
lastTool = +$('ikTool').value || 0;
|
lastTool = +$('ikTool').value || 0;
|
||||||
|
if (window.__POLICY && window.__POLICY.active() && window.__POLICY.setGoal(TGT.slice(), poseOn ? Rsolver() : null, lastTool)) { settle(true); return; }
|
||||||
/* 实时追踪原来走 solveTo —— 一个碰撞判定都不做:实测把目标球横拖过立柱,
|
/* 实时追踪原来走 solveTo —— 一个碰撞判定都不做:实测把目标球横拖过立柱,
|
||||||
1300 帧里 870 帧处于穿模状态。改用 solveRoute:按"从当前位形这条直线能不能走"
|
1300 帧里 870 帧处于穿模状态。改用 solveRoute:按"从当前位形这条直线能不能走"
|
||||||
挑解,走不通就贴着障碍停(blocked),不再把手势直接映射进立柱里。 */
|
挑解,走不通就贴着障碍停(blocked),不再把手势直接映射进立柱里。 */
|
||||||
@@ -1128,6 +1176,7 @@ function boot() {
|
|||||||
boot();
|
boot();
|
||||||
|
|
||||||
window.IK = { solver: S, setTarget: p => { TGT = p.slice(); syncInputs(); refreshTarget(); },
|
window.IK = { solver: S, setTarget: p => { TGT = p.slice(); syncInputs(); refreshTarget(); },
|
||||||
|
setJoints: setJoints, curQ: () => ORDER.map(id => ang[id]),
|
||||||
clampAxis: clampAxis, refreshJointLabels: refreshJointLabelsLazy,
|
clampAxis: clampAxis, refreshJointLabels: refreshJointLabelsLazy,
|
||||||
getTarget: () => TGT.slice(), run: runSolve, TCP0: TCP0, LIM: LIM,
|
getTarget: () => TGT.slice(), run: runSolve, TCP0: TCP0, LIM: LIM,
|
||||||
setView: setView, getView: () => VIEW,
|
setView: setView, getView: () => VIEW,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
* ik-policy-glue.js —— 查看器接入 Transformer 策略(URL 加 ?policy=1 启用)
|
||||||
|
* ?policy=1 用 models/traj_policy.onnx
|
||||||
|
* ?policy=models/xxx.onnx 指定模型
|
||||||
|
* &ep=webgpu 改用 WebGPU 后端(默认 wasm-SIMD 多线程)
|
||||||
|
* &threads=N wasm 线程数(默认 min(16, ⌊0.6×逻辑核⌋);2026-10-18 R4 实测 28 核机 16 最优、24 反而变慢、4 线程 P99 37 ms)
|
||||||
|
* &proxy=0 关闭推理代理 Worker(默认开:wasm 推理不占主线程,见下)
|
||||||
|
* &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。此前推理在主线程同步算,
|
||||||
|
* R4 实测时 CDP 15 s 拿不到执行槽,50 Hz tick 也被它拖住——丢步不只来自推理时长本身。
|
||||||
|
* 目标变化(松手求解 / 实时追踪)都交给 ArmPolicy.Runner:50 Hz 执行时钟放在 Web Worker(不受 rAF 60 Hz 与后台节流影响),
|
||||||
|
* 每 3 步推理一次,安全层认证后才执行;ik-glue 的逐帧碰撞守卫不参与(策略执行不走 animTo),由 Runner 安全层保证。
|
||||||
|
* onnxruntime-web 默认从 jsDelivr 加载;离线部署把 ort.min.js 与 *.wasm 放到 vendor/ort/ 并加 &ortLocal=1。
|
||||||
|
* 多线程 wasm 需要页面跨源隔离(COOP/COEP 头,见 ml/README.md);没有时自动退回单线程。
|
||||||
|
* ==========================================================================*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
const qs = new URLSearchParams(location.search);
|
||||||
|
if (!qs.has('policy')) return;
|
||||||
|
const MODEL = qs.get('policy') && qs.get('policy') !== '1' ? qs.get('policy') : 'models/traj_policy.onnx';
|
||||||
|
/* 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 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;
|
||||||
|
window.__POLICY = { active: () => ready, setGoal: () => false, stats: () => runner && runner.stats };
|
||||||
|
|
||||||
|
function loadScript(src) { return new Promise((res, rej) => { const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = () => rej(new Error('load ' + src)); document.head.appendChild(s); }); }
|
||||||
|
async function waitIK() { for (let i = 0; i < 200 && !(window.IK && window.IK.solver && window.ArmPolicy); i++) await new Promise(r => setTimeout(r, 50)); if (!window.IK) throw new Error('IK 未就绪'); }
|
||||||
|
|
||||||
|
async function boot() {
|
||||||
|
try {
|
||||||
|
await waitIK();
|
||||||
|
const S = window.IK.solver;
|
||||||
|
const meta = await (await fetch(MODEL.replace('.onnx', '.meta.json'))).json();
|
||||||
|
if (meta.fp !== S.modelFingerprint()) throw new Error('模型碰撞指纹 ' + meta.fp + ' ≠ 当前 ' + S.modelFingerprint() + ':collide.json 已变,须重训');
|
||||||
|
if (meta.feat_ver !== ArmPolicy.FEAT_VER) throw new Error('特征版本不符:模型 ' + meta.feat_ver + ' / 运行时 ' + ArmPolicy.FEAT_VER);
|
||||||
|
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;
|
||||||
|
if (probe && qs.get('probe') !== '1') throw new Error('这是随机初始化的延迟探针模型(meta.random_init),不是训练产物;仅测延迟时加 &probe=1');
|
||||||
|
if (!window.ort) await loadScript(ORT_BASE + ORT_JS);
|
||||||
|
ort.env.wasm.wasmPaths = ORT_BASE;
|
||||||
|
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))));
|
||||||
|
const ep = qs.get('ep') === 'webgpu' ? ['webgpu', 'wasm'] : ['wasm'];
|
||||||
|
let sess = null, proxy = ep[0] === 'wasm' && qs.get('proxy') !== '0'; // proxy 只支持 wasm EP
|
||||||
|
if (proxy) {
|
||||||
|
ort.env.wasm.proxy = true;
|
||||||
|
try { sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); }
|
||||||
|
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' });
|
||||||
|
const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn });
|
||||||
|
runner = new ArmPolicy.Runner(S, infer, {});
|
||||||
|
/* 预热:首次推理有编译开销 */
|
||||||
|
await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8);
|
||||||
|
const tick = URL.createObjectURL(new Blob(['let h=null;onmessage=e=>{clearInterval(h);if(e.data>0)h=setInterval(()=>postMessage(0),e.data)}'], { type: 'text/javascript' }));
|
||||||
|
worker = new Worker(tick);
|
||||||
|
worker.onmessage = onTick;
|
||||||
|
window.__POLICY = {
|
||||||
|
active: () => ready,
|
||||||
|
setGoal: (T, R, tool) => {
|
||||||
|
if (tool) { say('策略模式暂不支持工具偏移 ≠ 0,已交回规划器', 'badb'); return false; }
|
||||||
|
if (!running || !runner.q) runner.reset(window.IK.curQ());
|
||||||
|
runner.setGoal(T, R); start(); return true;
|
||||||
|
},
|
||||||
|
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; };
|
||||||
|
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), planWarmP99: p(st.planWarmMs, .99), planColdP99: p(st.planColdMs, .99), coldPlans: st.planColdMs.length, 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;
|
||||||
|
const how = ep[0] + ',' + ort.env.wasm.numThreads + ' 线程' + (proxy ? ',推理在 Worker' : ',推理在主线程');
|
||||||
|
if (probe) say('随机探针模型(仅测延迟,动作无意义):' + how, 'badb');
|
||||||
|
else say('策略模式就绪(' + how + '):拖动目标即由策略执行', 'ok');
|
||||||
|
if (qs.get('autotest')) autotest(+qs.get('autotest') || 30, meta);
|
||||||
|
} 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 stop() { running = false; if (worker) worker.postMessage(0); }
|
||||||
|
function onTick() {
|
||||||
|
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 && !runner.cold) TICKS.starve++;
|
||||||
|
if (runner.needPlan()) runner.plan().catch(e => { console.error(e); stop(); say('策略推理出错:' + e.message, 'badb'); });
|
||||||
|
const q = runner.tick();
|
||||||
|
if (q) window.IK.setJoints(q, false);
|
||||||
|
if (runner.done && !runner.queue.length) {
|
||||||
|
stop();
|
||||||
|
const { e, oe } = runner.err(runner.q);
|
||||||
|
say('策略到达 · 误差 ' + e.toFixed(2) + ' mm' + (runner.goal.R ? ' / ' + oe.toFixed(2) + '°' : '') + ' · 刹停 ' + runner.stats.brakes + ' 次', 'ok');
|
||||||
|
} else if (runner.brakeRun >= 2) say('策略:安全层连续拦截,减速重规划中…', 'wait');
|
||||||
|
}
|
||||||
|
boot();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
* ik-policy.js —— Transformer 避障策略的闭环运行时(浏览器 / Node 通用,UMD)
|
||||||
|
* 方案文档 §5 / §8:50 Hz 执行,每 3 步(60 ms)推理一次,8 个噪声 → 8 条 16 步动作块,
|
||||||
|
* 1 步胶囊梯度投影 → 排序 → 距离下界认证(前 3 步 + 刹停段)→ 提交;全不过则沿已认证刹停段停。
|
||||||
|
* 不依赖任何规划器(solveDyn / routeRT / 路网):只用求解器的 fk / orient / gapMatrix / 胶囊对间隙。
|
||||||
|
* 特征定义与 ml/common/features.py 逐项一致(ml/eval 对拍)。改特征必须同时改两边并升 FEAT_VER。
|
||||||
|
* ==========================================================================*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.ArmPolicy = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const FEAT_VER = 1;
|
||||||
|
const VMAX = [60, 60, 90, 90, 120, 180]; // °/s(与 ml/gen/timelaw.cjs 同)
|
||||||
|
const DT = 0.02;
|
||||||
|
const STEP = VMAX.map(v => v * DT); // 每步最大增量(°)
|
||||||
|
const ACC = 5.0; // 归一化加速度 1/s²(与时间律同)
|
||||||
|
const H = 16, NJ = 6, PREFIX = 3;
|
||||||
|
const GP = [[0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [2, 4], [2, 5], [3, 5]]; // 特征里的 8 个组对
|
||||||
|
const GAP_CAP = 40;
|
||||||
|
const F_DIM = 83;
|
||||||
|
const D2R = Math.PI / 180;
|
||||||
|
const wrap180 = a => { a = ((a + 180) % 360 + 360) % 360 - 180; return a; };
|
||||||
|
|
||||||
|
/* ---------------- 特征(83 维) ---------------- */
|
||||||
|
function features(S, st, gm) {
|
||||||
|
const q = st.q, L = S.LIM, O = S.ORDER, f = new Float32Array(F_DIM);
|
||||||
|
let o = 0;
|
||||||
|
for (let j = 0; j < 6; j++) { f[o++] = Math.sin(q[j] * D2R); f[o++] = Math.cos(q[j] * D2R); }
|
||||||
|
for (const j of [1, 3, 4]) { const t = L[O[j]]; f[o++] = 2 * (q[j] - t.lo) / (t.hi - t.lo) - 1; }
|
||||||
|
const qp = st.qPrev || q;
|
||||||
|
for (let j = 0; j < 6; j++) { const d = L[O[j]].cont ? wrap180(q[j] - qp[j]) : q[j] - qp[j]; f[o++] = d / STEP[j]; }
|
||||||
|
const p = S.fk(q, 0), R = S.orient(q);
|
||||||
|
for (let k = 0; k < 3; k++) f[o++] = p[k] / 600;
|
||||||
|
for (const k of [0, 3, 6, 1, 4, 7]) f[o++] = R[k];
|
||||||
|
for (let k = 0; k < 3; k++) f[o++] = st.T[k] / 600;
|
||||||
|
if (st.R) for (const k of [0, 3, 6, 1, 4, 7]) f[o++] = st.R[k]; else o += 6;
|
||||||
|
for (let k = 0; k < 3; k++) f[o++] = (st.T[k] - p[k]) / 600;
|
||||||
|
for (let k = 0; k < 3; k++) f[o++] = Math.max(-1, Math.min(1, (st.T[k] - p[k]) / 50));
|
||||||
|
f[o++] = st.R ? 0 : 1; f[o++] = st.R ? 1 : 0;
|
||||||
|
const pre = st.prefix || [];
|
||||||
|
for (let k = 0; k < PREFIX; k++) for (let j = 0; j < 6; j++) f[o++] = k < pre.length ? pre[k][j] / STEP[j] : 0;
|
||||||
|
for (let k = 0; k < PREFIX; k++) f[o++] = k < pre.length ? 1 : 0;
|
||||||
|
if (st.sub) { for (let j = 0; j < 6; j++) { const d = L[O[j]].cont ? wrap180(st.sub[j] - q[j]) : st.sub[j] - q[j]; f[o++] = d / 180; } f[o++] = 1; }
|
||||||
|
else o += 7;
|
||||||
|
const G = gm || S.gapMatrix(q, GAP_CAP);
|
||||||
|
for (const [a, b] of GP) f[o++] = Math.min(G[a * 7 + b], GAP_CAP) / GAP_CAP;
|
||||||
|
if (o !== F_DIM) throw new Error('feature dim ' + o);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 距离下界认证(ml/gen/cert.cjs 复用本实现) ---------------- */
|
||||||
|
function makeCert(S, opt) {
|
||||||
|
opt = opt || {};
|
||||||
|
const CAP = opt.cap || 40, MIN_DEG = opt.minDeg || 0.002;
|
||||||
|
/* 分组杠杆表 TL[k][g]:组对 (gA,gB) 只用关节 gA..gB−1 带动组 gB 的杠杆(比全局上界紧得多) */
|
||||||
|
const TL = opt.leverTable || S.leverTable(4000);
|
||||||
|
const L = S.LIM, O = S.ORDER;
|
||||||
|
const memo = new Map();
|
||||||
|
const key = q => q.map(v => v.toFixed(5)).join(',');
|
||||||
|
let queries = 0, hits = 0, budgetLeft = Infinity, deadline = Infinity;
|
||||||
|
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 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) {
|
||||||
|
if (budgetLeft <= 0) return false;
|
||||||
|
if (deadline !== Infinity && clock() > deadline) { budgetLeft = 0; return false; } // 超时与超预算同样判不过(→ 刹停,保守)
|
||||||
|
const Ma = gm(a), Mb = gm(b);
|
||||||
|
const dq = a.map((v, i) => Math.abs(b[i] - v));
|
||||||
|
let ok = true;
|
||||||
|
for (let ga = 0; ga < 7 && ok; ga++) for (let gb = ga + 2; gb < 7; gb++) {
|
||||||
|
const lhs = Ma[ga * 7 + gb] + Mb[ga * 7 + gb];
|
||||||
|
let rhs = 0; for (let k = ga; k < gb; k++) rhs += TL[k][gb] * dq[k];
|
||||||
|
if (!(lhs > rhs)) { ok = false; break; }
|
||||||
|
}
|
||||||
|
if (ok) return true;
|
||||||
|
if (minGap(Ma) <= 0 || minGap(Mb) <= 0) return false;
|
||||||
|
if (Math.max.apply(null, dq) < MIN_DEG || depth > 40) return false;
|
||||||
|
const m = a.map((v, i) => 0.5 * (v + b[i]));
|
||||||
|
return seg(a, m, depth + 1) && seg(m, b, depth + 1);
|
||||||
|
}
|
||||||
|
/* 折线(相邻点按短弧展开)→ {ok, queries, minGap, failAt, why};budget = 本次最多新增距离查询数(超出判不过,why='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;
|
||||||
|
budgetLeft = budget || Infinity; deadline = until || Infinity;
|
||||||
|
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 = 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 = 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; deadline = Infinity;
|
||||||
|
return { ok: true, queries: queries - q0, minGap: mg, unwrapped: P };
|
||||||
|
}
|
||||||
|
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 ---------------- */
|
||||||
|
function brakeSteps(v) {
|
||||||
|
let n = 0; for (let j = 0; j < 6; j++) n = Math.max(n, Math.ceil(Math.abs(v[j]) / (STEP[j] * ACC * DT) - 1e-9));
|
||||||
|
const out = [];
|
||||||
|
for (let k = 1; k <= n; k++) out.push(v.map(x => x * (1 - k / (n + 1))));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 1 步胶囊梯度投影(每轴 ≤ maxDeg,仅 gap < margin) ---------------- */
|
||||||
|
/* pairs:本次推理开始时在 qe 处取的近距胶囊对(S.criticalPairs(qe, margin+10)),候选各步只对这些对求间隙与有限差分(微秒级) */
|
||||||
|
function project(S, q, margin, maxDeg, pairs) {
|
||||||
|
if (!pairs || !pairs.length) return q;
|
||||||
|
const gs = pairs.map(p => S.pairGap(q, p[0], p[1]));
|
||||||
|
const g0 = Math.min.apply(null, gs);
|
||||||
|
if (g0 >= margin) return q;
|
||||||
|
pairs = pairs.filter((p, i) => gs[i] < margin + 2).slice(0, 6);
|
||||||
|
const h = 0.01, grad = [0, 0, 0, 0, 0, 0];
|
||||||
|
for (let j = 0; j < 6; j++) {
|
||||||
|
const qa = q.slice(); qa[j] += h;
|
||||||
|
const g1 = Math.min.apply(null, pairs.map(p => S.pairGap(qa, p[0], p[1])));
|
||||||
|
grad[j] = (g1 - g0) / h;
|
||||||
|
}
|
||||||
|
const n2 = grad.reduce((a, x) => a + x * x, 0);
|
||||||
|
if (n2 < 1e-12) return q;
|
||||||
|
const need = margin - g0;
|
||||||
|
return q.map((v, j) => v + Math.max(-maxDeg, Math.min(maxDeg, need * grad[j] / n2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 局部 DLS 精修(接近目标时) ---------------- */
|
||||||
|
function refineStep(S, q, T, Rt, maxScale) {
|
||||||
|
const e = x => {
|
||||||
|
const p = S.fk(x, 0), r = [T[0] - p[0], T[1] - p[1], T[2] - p[2]];
|
||||||
|
if (!Rt) return r;
|
||||||
|
const R = S.orient(x); // 旋转误差向量 ≈ 0.5·vee(R_t Rᵀ − R Rtᵀ),按 100 mm/rad 折算到 mm 量级
|
||||||
|
const M = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
for (let i = 0; i < 3; i++) for (let k = 0; k < 3; k++) { let s = 0; for (let m = 0; m < 3; m++) s += Rt[i * 3 + m] * R[k * 3 + m]; M[i * 3 + k] = s; }
|
||||||
|
return r.concat([0.5 * (M[7] - M[5]) * 100, 0.5 * (M[2] - M[6]) * 100, 0.5 * (M[3] - M[1]) * 100]);
|
||||||
|
};
|
||||||
|
const e0 = e(q), m = e0.length, h = 1e-3, J = [];
|
||||||
|
for (let j = 0; j < 6; j++) { const qa = q.slice(); qa[j] += h; const e1 = e(qa); J.push(e1.map((v, i) => -(v - e0[i]) / h)); }
|
||||||
|
// Δq = Jᵀ (J Jᵀ + λ²I)⁻¹ e,m ≤ 6
|
||||||
|
const lam = 0.5, A = [];
|
||||||
|
for (let i = 0; i < m; i++) { A.push([]); for (let k = 0; k < m; k++) { let s = i === k ? lam * lam : 0; for (let j = 0; j < 6; j++) s += J[j][i] * J[j][k]; A[i].push(s); } }
|
||||||
|
const y = solveLin(A, e0.slice());
|
||||||
|
const dq = [0, 0, 0, 0, 0, 0];
|
||||||
|
for (let j = 0; j < 6; j++) { let s = 0; for (let i = 0; i < m; i++) s += J[j][i] * y[i]; dq[j] = s; }
|
||||||
|
let sc = 1; for (let j = 0; j < 6; j++) sc = Math.max(sc, Math.abs(dq[j]) / (STEP[j] * (maxScale || 0.5)));
|
||||||
|
return { dq: dq.map(v => v / sc), err: Math.hypot(e0[0], e0[1], e0[2]) };
|
||||||
|
}
|
||||||
|
function solveLin(A, b) {
|
||||||
|
const n = b.length;
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
let p = c; for (let r = c + 1; r < n; r++) if (Math.abs(A[r][c]) > Math.abs(A[p][c])) p = r;
|
||||||
|
[A[c], A[p]] = [A[p], A[c]]; [b[c], b[p]] = [b[p], b[c]];
|
||||||
|
for (let r = c + 1; r < n; r++) { const f = A[r][c] / A[c][c]; for (let k = c; k < n; k++) A[r][k] -= f * A[c][k]; b[r] -= f * b[c]; }
|
||||||
|
}
|
||||||
|
const x = new Array(n).fill(0);
|
||||||
|
for (let r = n - 1; r >= 0; r--) { let s = b[r]; for (let k = r + 1; k < n; k++) s -= A[r][k] * x[k]; x[r] = s / A[r][r]; }
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 正态噪声 ---------------- */
|
||||||
|
function makeRng(seed) {
|
||||||
|
let s = (seed >>> 0) || 1;
|
||||||
|
const u = () => { s = (s * 1664525 + 1013904223) >>> 0; return (s + 0.5) / 4294967296; };
|
||||||
|
return { u, n: () => Math.sqrt(-2 * Math.log(u())) * Math.cos(2 * Math.PI * u()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
* 闭环运行器
|
||||||
|
* infer(feat Float32Array[B*F], z Float32Array[B*H*6], B) → Promise<{act[B*H*6], value[B], sub[B*13]}>
|
||||||
|
* 用法:r = new Runner(S, infer, opts); r.reset(q0); r.setGoal(T, R);
|
||||||
|
* 每 20 ms 调 r.tick();r.needPlan() 为真时 await r.plan()(tick 与 plan 可并行:plan 只在队列 ≥3 步时发起)
|
||||||
|
* ==========================================================================*/
|
||||||
|
class Runner {
|
||||||
|
constructor(S, infer, opts) {
|
||||||
|
this.S = S; this.infer = infer;
|
||||||
|
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,
|
||||||
|
stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, seed: 12345, lookahead: true,
|
||||||
|
certBudget: 40, safetyMs: 30, firstMs: 45 }, opts || {}); // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理)
|
||||||
|
this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 });
|
||||||
|
this.rng = makeRng(this.o.seed);
|
||||||
|
/* 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: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0 };
|
||||||
|
this.reset(null);
|
||||||
|
}
|
||||||
|
reset(q0) {
|
||||||
|
this.q = q0 ? q0.slice() : null; this.qPrev = this.q ? this.q.slice() : null;
|
||||||
|
this.queue = []; this.brakeTail = []; this.lastPlan = null; this.goal = null;
|
||||||
|
this.subs = []; this.subT = 0; this.vals = []; this.brakeRun = 0; this.lock = null; this.t = 0; this.cold = true; this.done = false;
|
||||||
|
}
|
||||||
|
setGoal(T, R) {
|
||||||
|
const jump = !this.goal || Math.hypot(T[0] - this.goal.T[0], T[1] - this.goal.T[1], T[2] - this.goal.T[2]) > 50 || (!!R !== !!this.goal.R);
|
||||||
|
this.goal = { T: T.slice(), R: R ? R.slice() : null };
|
||||||
|
if (jump) { this.needSub = true; this.subs = []; this.cold = true; this.vals = []; this.lock = null; }
|
||||||
|
this.done = false;
|
||||||
|
}
|
||||||
|
qEnd() { let q = this.q.slice(); for (const d of this.queue) q = q.map((v, j) => v + d[j]); return q; }
|
||||||
|
err(q) {
|
||||||
|
const p = this.S.fk(q, 0), T = this.goal.T;
|
||||||
|
const e = Math.hypot(T[0] - p[0], T[1] - p[1], T[2] - p[2]);
|
||||||
|
let oe = 0;
|
||||||
|
if (this.goal.R) { const R = this.S.orient(q); let tr = 0; for (let i = 0; i < 9; i++) tr += R[i] * this.goal.R[i]; oe = Math.acos(Math.max(-1, Math.min(1, (tr - 1) / 2))) / D2R; }
|
||||||
|
return { e, oe };
|
||||||
|
}
|
||||||
|
needPlan() { return !!this.goal && !this.done && !this.planning && this.queue.length <= this.o.commit; }
|
||||||
|
tick() {
|
||||||
|
if (!this.q) return null;
|
||||||
|
const d = this.queue.shift();
|
||||||
|
this.qPrev = this.q.slice();
|
||||||
|
if (d) this.q = this.q.map((v, j) => v + d[j]);
|
||||||
|
this.t += DT;
|
||||||
|
if (this.goal && !this.queue.length) {
|
||||||
|
const { e, oe } = this.err(this.q);
|
||||||
|
if (e < this.o.arriveMm && oe < this.o.arriveDeg) this.done = true;
|
||||||
|
}
|
||||||
|
return this.q;
|
||||||
|
}
|
||||||
|
_cand(act, k, r) { // 第 k 条候选:增量(°),丢掉模型的前 r 步(已提交前缀),取接下来的步
|
||||||
|
const inc = [];
|
||||||
|
for (let s = 0; s < H; s++) { const row = []; for (let j = 0; j < 6; j++) row.push(Math.max(-1, Math.min(1, act[(k * H + s) * 6 + j])) * STEP[j]); inc.push(row); }
|
||||||
|
return inc;
|
||||||
|
}
|
||||||
|
_certCommit(qe, steps) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
|
||||||
|
const pts = [qe.slice()]; let q = qe.slice();
|
||||||
|
for (const d of steps) { q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
|
||||||
|
for (let i = 1; i < pts.length; i++) if (!this.S.collideFree(pts[i])) return { ok: false, tail: [] };
|
||||||
|
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()); }
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
async plan() {
|
||||||
|
if (!this.needPlan()) return;
|
||||||
|
this.planning = true;
|
||||||
|
const t = now(), c0 = this.cert.counters(), wasCold = this.cold; // 第八轮:冷启动(含前瞻)与稳态分开记,R9 判稳态
|
||||||
|
try { await this._plan(); } finally {
|
||||||
|
this.planning = false; this._until = undefined;
|
||||||
|
const c1 = this.cert.counters();
|
||||||
|
const dt = now() - t; this.stats.planMs.push(dt); (wasCold ? this.stats.planColdMs : this.stats.planWarmMs).push(dt); this.stats.certQ.push(c1.queries - c0.queries); this.stats.certHit += c1.hits - c0.hits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _plan() {
|
||||||
|
const S = this.S, o = this.o, K = o.K;
|
||||||
|
const r = Math.min(this.queue.length, PREFIX), prefix = this.queue.slice(0, r);
|
||||||
|
const qe = this.qEnd();
|
||||||
|
this.stats.plans++;
|
||||||
|
/* 接近目标:局部 DLS 精修代替模型 */
|
||||||
|
const ee = this.err(qe);
|
||||||
|
if (ee.e < o.refineMm && (!this.goal.R || ee.oe < 5)) {
|
||||||
|
if (ee.e < o.arriveMm && ee.oe < o.arriveDeg) return;
|
||||||
|
const steps = []; let q = qe.slice();
|
||||||
|
for (let k = 0; k < o.commit; k++) { const rs = refineStep(S, q, this.goal.T, this.goal.R, 0.3); steps.push(rs.dq); q = q.map((v, j) => v + rs.dq[j]); }
|
||||||
|
const c = this._certCommit(qe, steps);
|
||||||
|
if (c.ok) { this.queue.push(...steps); this.brakeTail = c.tail; this.stats.refine++; return; }
|
||||||
|
}
|
||||||
|
/* 子目标推进 */
|
||||||
|
if (this.subs.length) {
|
||||||
|
const sg = this.subs[0]; let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs((S.LIM[S.ORDER[j]].cont ? wrap180(sg[j] - qe[j]) : sg[j] - qe[j])));
|
||||||
|
if (m < o.subReachDeg || this.t - this.subT > o.subTimeout) { this.subs.shift(); this.subT = this.t; }
|
||||||
|
}
|
||||||
|
const gm = S.gapMatrix(this.q, GAP_CAP);
|
||||||
|
const st = { q: this.q, qPrev: this.qPrev, T: this.goal.T, R: this.goal.R, prefix, sub: this.subs[0] || null };
|
||||||
|
const f = features(S, st, gm);
|
||||||
|
const feat = new Float32Array(K * F_DIM), z = new Float32Array(K * H * 6);
|
||||||
|
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();
|
||||||
|
const t0 = now();
|
||||||
|
const out = await this.infer(feat, z, K, true); // true:K 行特征相同,enc_once 模型只送 1 行
|
||||||
|
this.stats.inferMs.push(now() - t0);
|
||||||
|
let t1 = now();
|
||||||
|
/* 子目标:新目标/跳变后取价值最好的那条输出 */
|
||||||
|
let best = 0; for (let k = 1; k < K; k++) if (out.value[k] < out.value[best]) best = k;
|
||||||
|
if (this.needSub) {
|
||||||
|
this.needSub = false; this.subT = this.t;
|
||||||
|
const sb = out.sub.subarray(best * 13, best * 13 + 13);
|
||||||
|
if (sb[12] > 0.5) this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180));
|
||||||
|
}
|
||||||
|
/* 候选:去掉前缀后的提交步,投影,打分 */
|
||||||
|
const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64);
|
||||||
|
for (let k = 0; k < K; k++) {
|
||||||
|
const inc = this._cand(out.act, k, r);
|
||||||
|
let steps = inc.slice(r, r + o.commit), q = qe.slice();
|
||||||
|
const adj = [];
|
||||||
|
for (const d of steps) {
|
||||||
|
let qn = q.map((v, j) => v + d[j]);
|
||||||
|
const qp = project(S, qn, o.margin, o.projMax, near);
|
||||||
|
if (qp !== qn) this.stats.projected++;
|
||||||
|
adj.push(qp.map((v, j) => v - q[j])); q = qp;
|
||||||
|
}
|
||||||
|
steps = adj;
|
||||||
|
const fut = []; let qq = qe.slice(); for (let s = r; s < H; s++) { qq = qq.map((v, j) => v + inc[s][j]); fut.push(qq); }
|
||||||
|
let j1 = 0; for (let s = 0; s < H; s++) j1 += inc[s][0];
|
||||||
|
let score = -2.0 * out.value[k];
|
||||||
|
if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐(时间平移 3 步)
|
||||||
|
let c = 0, w = 1;
|
||||||
|
for (let s = 0; s < fut.length && s + o.commit < this.lastPlan.length; s++, w *= 0.85) {
|
||||||
|
let d2 = 0; for (let j = 0; j < 6; j++) d2 += Math.pow((fut[s][j] - this.lastPlan[s + o.commit][j]) / STEP[j], 2);
|
||||||
|
c += w * d2;
|
||||||
|
}
|
||||||
|
score -= 0.02 * c;
|
||||||
|
}
|
||||||
|
cands.push({ k, steps, fut, j1, score, value: out.value[k] });
|
||||||
|
}
|
||||||
|
/* 卡死检测:价值 0.3 s 内不降 或 连续 2 次刹停 → 锁定另一绕行方向 1 s */
|
||||||
|
const vbest = out.value[best]; this.vals.push(vbest); if (this.vals.length > o.stuckPlans) this.vals.shift();
|
||||||
|
const stuck = this.brakeRun >= 2 || (this.vals.length >= o.stuckPlans && this.vals[this.vals.length - 1] > this.vals[0] * 0.99 && ee.e > 20);
|
||||||
|
if (stuck && !(this.lock && this.t < this.lock.until)) {
|
||||||
|
const lastDir = this.lastPlan ? Math.sign(this.lastPlan[this.lastPlan.length - 1][0] - this.lastPlan[0][0]) || 1 : 1;
|
||||||
|
this.lock = { side: -lastDir, until: this.t + o.lockSec }; this.stats.locks++; this.vals = [];
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
/* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。
|
||||||
|
2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐;
|
||||||
|
前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */
|
||||||
|
if (o.lookahead && this.cold) {
|
||||||
|
this.stats.lookahead++;
|
||||||
|
const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6);
|
||||||
|
pool.forEach((c, i) => {
|
||||||
|
const qa = c.fut[c.fut.length - 1], qb = c.fut[Math.max(0, c.fut.length - 2)];
|
||||||
|
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();
|
||||||
|
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; });
|
||||||
|
this.stats.lookaheadMs.push(now() - t1); t1 = now();
|
||||||
|
}
|
||||||
|
this.cold = false;
|
||||||
|
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;
|
||||||
|
for (const c of pool) {
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
this._until = undefined;
|
||||||
|
this.stats.safetyMs.push(now() - t1);
|
||||||
|
if (picked) {
|
||||||
|
this.queue.push(...picked.steps); this.lastPlan = [qe.slice()].concat(picked.fut); this.brakeRun = 0;
|
||||||
|
} else {
|
||||||
|
this.stats.brakes++; this.brakeRun++;
|
||||||
|
if (this.brakeTail.length) { this.queue.push(...this.brakeTail); this.brakeTail = []; }
|
||||||
|
this.lastPlan = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
||||||
|
|
||||||
|
/* ---------------- onnxruntime 适配(web: ort = window.ort;node: require('onnxruntime-node')) ---------------- */
|
||||||
|
/* 2026-10-18 第五轮:模型 meta.enc_once=true(编码一次、解码 K 次)时只送第 1 行特征 [1,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) {
|
||||||
|
const encOnce = !!(opt && opt.encOnce), featDyn = !!(opt && opt.featDyn); // featDyn:meta.feat_dyn(第六轮导出),feat 轴可为 1 或 K
|
||||||
|
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 }; };
|
||||||
|
return async (feat, z, B, shared) => {
|
||||||
|
if (!encOnce) return run(new ort.Tensor('float32', feat.slice(0, B * F_DIM), [B, F_DIM]), z, B);
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { FEAT_VER, VMAX, DT, STEP, ACC, H, F_DIM, GP, GAP_CAP, features, makeCert, brakeSteps, project, refineStep, Runner, ortInfer, wrap180 };
|
||||||
|
}));
|
||||||
+953
-32
File diff suppressed because it is too large
Load Diff
@@ -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 };
|
||||||
|
}));
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
策略模型放这里(由 `bash ml/pipeline.sh export`,即 `python -m ml.export_onnx --ckpt ml/runs/current.pt` 生成,默认 fp32):
|
||||||
|
- `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、`enc_once` / `feat_dyn`。查看器加载时会校验,`random_init` 为真的延迟探针模型会被拒绝
|
||||||
|
|
||||||
|
查看器地址加 `?policy=1` 启用策略模式(离线加 `&ortLocal=1`,先跑 `bash ml/prep/vendor_ort.sh`),详见 `ml/README.md` 第 10 步。
|
||||||
|
两个文件都不入库(`.gitignore`)。
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>R4 推理延迟探针</title>
|
||||||
|
<!-- 2026-10-18 第五轮:由 ml/eval/r4_probe.py 在无头浏览器里打开,测部署形态的单次推理(1 行特征 + K=8 条候选)。
|
||||||
|
参数:model= 模型 URL;ep=wasm|webgpu;threads=auto|N;proxy=0|1;n= 次数;ortLocal=1 用 vendor/ort/。
|
||||||
|
结果 POST 到 /r4/result;同时记录 20 ms 节拍的最大间隔(推理是否卡主线程)。也可手动打开看页面上的结果。 -->
|
||||||
|
<pre id="o">运行中…</pre>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
const qs = new URLSearchParams(location.search), o = document.getElementById('o');
|
||||||
|
const res = { ep: qs.get('ep') || 'wasm', threadsArg: qs.get('threads') || 'auto', hc: navigator.hardwareConcurrency, coi: self.crossOriginIsolated, ua: navigator.userAgent };
|
||||||
|
const post = async () => { o.textContent = JSON.stringify(res, null, 1); try { await fetch('/r4/result', { method: 'POST', body: JSON.stringify(res) }); } catch (e) {} };
|
||||||
|
try {
|
||||||
|
const MODEL = qs.get('model'), N = +qs.get('n') || 150, K = 8, F = 83, H = 16;
|
||||||
|
/* 2026-10-18 第六轮:本地前缀用绝对 URL(ORT 以 import() 加载 .mjs,相对前缀会被当成裸模块名);WebGPU 后端只在 ort.webgpu.min.js 里 */
|
||||||
|
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();
|
||||||
|
ort.env.wasm.wasmPaths = BASE;
|
||||||
|
// 与 ik-policy-glue.js 同一套默认值
|
||||||
|
ort.env.wasm.numThreads = !self.crossOriginIsolated ? 1 : (res.threadsArg === 'auto' ? Math.max(1, Math.min(16, Math.floor(res.hc * 0.6))) : +res.threadsArg);
|
||||||
|
res.threads = ort.env.wasm.numThreads;
|
||||||
|
res.proxy = res.ep === 'wasm' && qs.get('proxy') !== '0';
|
||||||
|
const opts = { executionProviders: res.ep === 'webgpu' ? ['webgpu'] : ['wasm'], graphOptimizationLevel: 'all' };
|
||||||
|
let sess = null;
|
||||||
|
if (res.proxy) { ort.env.wasm.proxy = true; try { sess = await ort.InferenceSession.create(MODEL, opts); } catch (e) { res.proxyError = String(e); res.proxy = false; ort.env.wasm.proxy = false; } }
|
||||||
|
if (!sess) sess = await ort.InferenceSession.create(MODEL, opts);
|
||||||
|
res.encOnce = !!meta.enc_once;
|
||||||
|
const z = () => new Float32Array(K * H * 6).map(() => Math.random() * 2 - 1);
|
||||||
|
/* proxy 模式下 ORT 会把输入 buffer 转移给 Worker(detach),feat 必须每次新建 */
|
||||||
|
const run = () => { const feat = new Float32Array((res.encOnce ? 1 : K) * F).map(() => Math.random() - 0.5);
|
||||||
|
return sess.run({ feat: new ort.Tensor('float32', feat, [res.encOnce ? 1 : K, F]), z: new ort.Tensor('float32', z(), [K, H, 6]) }); };
|
||||||
|
for (let i = 0; i < 10; i++) await run();
|
||||||
|
let last = performance.now(); const gaps = [];
|
||||||
|
const tk = setInterval(() => { const t = performance.now(); gaps.push(t - last); last = t; }, 20);
|
||||||
|
const ts = [];
|
||||||
|
for (let i = 0; i < N; i++) { const t = performance.now(); await run(); ts.push(performance.now() - t); await new Promise(r => setTimeout(r, 40)); }
|
||||||
|
clearInterval(tk);
|
||||||
|
const p = (a, x) => { const s = a.slice().sort((u, v) => u - v); return +s[Math.min(s.length - 1, Math.floor(s.length * x))].toFixed(2); };
|
||||||
|
Object.assign(res, { n: N, K, p50: p(ts, .5), p95: p(ts, .95), p99: p(ts, .99), tickGapP99: p(gaps, .99), tickGapMax: +Math.max(...gaps).toFixed(1) });
|
||||||
|
} catch (e) { res.error = String(e && e.message || e); }
|
||||||
|
await post();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* 生成两关节零件对类的间隙表,写入 docs/collide.json 的 pair2d 字段。
|
||||||
|
* node webapp/tools/gen_pair2d.cjs 默认生成 (2,4) 类(P05/M3 × P07/M5,J3×J4)
|
||||||
|
* 重新拟合胶囊(scratch/fit_capsules.py 重写 collide.json)后必须重跑;
|
||||||
|
* 求解器按指纹校验,不符会自动弃表回退精算(只是变慢,不会算错)。 */
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const ROOT = path.resolve(__dirname, '..', '..');
|
||||||
|
const setup = require(path.join(ROOT, 'tests/route_bench_setup.cjs'));
|
||||||
|
const COLF = path.join(ROOT, 'docs/collide.json');
|
||||||
|
/* 文本级追加/替换,保持 fit_capsules.py 写出的原格式不动(避免整文件 diff) */
|
||||||
|
let text = fs.readFileSync(COLF, 'utf8');
|
||||||
|
const cut = text.indexOf(',\n "pair2d":');
|
||||||
|
if (cut >= 0) text = text.slice(0, cut) + '\n}\n';
|
||||||
|
fs.writeFileSync(COLF + '.tmp', text);
|
||||||
|
const { S } = setup(undefined, COLF + '.tmp');
|
||||||
|
fs.unlinkSync(COLF + '.tmp');
|
||||||
|
const H = 1.0, classes = [[2, 4]];
|
||||||
|
const out = {};
|
||||||
|
for (const [ga, gb] of classes) {
|
||||||
|
const ci = ga * 7 + gb, ja = ga, jb = ga + 1;
|
||||||
|
const la = S.LIM[S.ORDER[ja]], lb = S.LIM[S.ORDER[jb]];
|
||||||
|
const lo = [la.cont ? -180 : la.lo, lb.cont ? -180 : lb.lo];
|
||||||
|
const n = [la.cont ? Math.round(360 / H) : Math.round((la.hi - la.lo) / H) + 1,
|
||||||
|
lb.cont ? Math.round(360 / H) : Math.round((lb.hi - lb.lo) / H) + 1];
|
||||||
|
const buf = Buffer.alloc(n[0] * n[1] * 2);
|
||||||
|
const t0 = Date.now();
|
||||||
|
for (let i = 0; i < n[0]; i++) {
|
||||||
|
for (let j = 0; j < n[1]; j++) {
|
||||||
|
const q = [0, 0, 0, 0, 0, 0];
|
||||||
|
q[ja] = lo[0] + i * H; q[jb] = lo[1] + j * H;
|
||||||
|
const g = S.classGap(q, ci);
|
||||||
|
buf.writeInt16LE(Math.max(-32767, Math.min(32767, Math.floor(g * 100))), 2 * (i * n[1] + j)); // 向下取整:保守
|
||||||
|
}
|
||||||
|
if (i % 60 === 0) console.error('class', ga + '' + gb, i + '/' + n[0], ((Date.now() - t0) / 1000).toFixed(0) + 's');
|
||||||
|
}
|
||||||
|
out[ci] = { joints: [S.ORDER[ja], S.ORDER[jb]], lo: lo, h: H, n: n, fp: S.classFingerprint(ci), b64: buf.toString('base64') };
|
||||||
|
}
|
||||||
|
const body = text.replace(/\s*\}\s*$/, '');
|
||||||
|
fs.writeFileSync(COLF, body + ',\n "pair2d": ' + JSON.stringify(out) +
|
||||||
|
',\n "pair2d_generated": ' + JSON.stringify(new Date().toISOString()) + '\n}\n');
|
||||||
|
console.log('pair2d written', Object.keys(out));
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* 离线生成关节空间安全路网(PRM),写入 docs/collide.json 的 roadmap 字段。
|
||||||
|
* node webapp/tools/gen_roadmap.cjs [节点数=2500] [k=10]
|
||||||
|
* 障碍是静态的(立柱 + 自身零件),路网只依赖碰撞模型与限位;重新拟合胶囊 / 改限位后必须重跑
|
||||||
|
* (求解器按指纹校验,不符自动弃用路网,退回在线 RRT —— 只是变慢,不会算错)。
|
||||||
|
* 多线程:worker_threads 按 CPU 数并行细验边。 */
|
||||||
|
const fs = require('fs'), path = require('path'), os = require('os');
|
||||||
|
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
|
||||||
|
const ROOT = path.resolve(__dirname, '..', '..');
|
||||||
|
const COLF = path.join(ROOT, 'docs/collide.json');
|
||||||
|
function stripped() { // 去掉旧 roadmap 字段(文本级,保持原格式)
|
||||||
|
let text = fs.readFileSync(COLF, 'utf8');
|
||||||
|
const cut = text.indexOf(',\n "roadmap":');
|
||||||
|
if (cut >= 0) text = text.slice(0, cut) + '\n}\n';
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (!isMainThread) {
|
||||||
|
const { S } = require(path.join(ROOT, 'tests/route_bench_setup.cjs'))(undefined, workerData.tmp);
|
||||||
|
parentPort.on('message', ({ Q, edges }) => {
|
||||||
|
const ok = edges.map(([a, b]) => S.edgeFree(Q[a], S.unwrapTo ? S.unwrapTo(Q[a], Q[b]) : Q[b]) ? 1 : 0);
|
||||||
|
parentPort.postMessage(ok);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
const N = +process.argv[2] || 2500, K = +process.argv[3] || 10;
|
||||||
|
const text = stripped(), tmp = COLF + '.rmtmp';
|
||||||
|
fs.writeFileSync(tmp, text);
|
||||||
|
const { S } = require(path.join(ROOT, 'tests/route_bench_setup.cjs'))(undefined, tmp);
|
||||||
|
const O = S.ORDER, L = S.LIM;
|
||||||
|
let seed = 20261015;
|
||||||
|
const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; };
|
||||||
|
const Q = [];
|
||||||
|
let tries = 0;
|
||||||
|
while (Q.length < N) {
|
||||||
|
tries++;
|
||||||
|
const q = O.map(id => { const t = L[id]; return t.cont ? -180 + 360 * rnd() : t.lo + (t.hi - t.lo) * rnd(); });
|
||||||
|
const qr = q.map(v => Math.round(v * 100) / 100);
|
||||||
|
if (S.collideFree(qr)) Q.push(qr);
|
||||||
|
}
|
||||||
|
console.error('nodes', N, 'free ratio', (N / tries).toFixed(3));
|
||||||
|
const E = new Set();
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const d = [];
|
||||||
|
for (let j = 0; j < N; j++) if (j !== i) d.push([j, S.wd(Q[i], Q[j])]);
|
||||||
|
d.sort((a, b) => a[1] - b[1]);
|
||||||
|
for (let m = 0; m < K; m++) { const j = d[m][0]; E.add(i < j ? i * 65536 + j : j * 65536 + i); }
|
||||||
|
}
|
||||||
|
const edges = [...E].map(k => [Math.floor(k / 65536), k % 65536]);
|
||||||
|
console.error('candidate edges', edges.length);
|
||||||
|
const nw = Math.max(1, os.cpus().length), ws = [];
|
||||||
|
for (let w = 0; w < nw; w++) ws.push(new Worker(__filename, { workerData: { tmp } }));
|
||||||
|
const CH = 200, okAll = new Uint8Array(edges.length);
|
||||||
|
let next = 0, done = 0; const t0 = Date.now();
|
||||||
|
await new Promise(res => {
|
||||||
|
const feed = w => {
|
||||||
|
if (next >= edges.length) { if (done >= edges.length) res(); return; }
|
||||||
|
const lo = next; next = Math.min(edges.length, next + CH);
|
||||||
|
w.once('message', ok => { okAll.set(ok, lo); done += ok.length;
|
||||||
|
if ((done / CH) % 10 === 0) console.error('edges', done + '/' + edges.length, ((Date.now() - t0) / 1000).toFixed(0) + 's');
|
||||||
|
if (done >= edges.length) res(); else feed(w); });
|
||||||
|
w.postMessage({ Q, edges: edges.slice(lo, next) });
|
||||||
|
};
|
||||||
|
ws.forEach(feed);
|
||||||
|
});
|
||||||
|
ws.forEach(w => w.terminate());
|
||||||
|
fs.unlinkSync(tmp);
|
||||||
|
const adjL = Array.from({ length: N }, () => []);
|
||||||
|
let ne = 0;
|
||||||
|
edges.forEach(([a, b], i) => { if (okAll[i]) { adjL[a].push(b); adjL[b].push(a); ne++; } });
|
||||||
|
/* 连通分量统计 */
|
||||||
|
const comp = new Int32Array(N).fill(-1); let nc = 0, big = 0;
|
||||||
|
for (let i = 0; i < N; i++) if (comp[i] < 0) { let st = [i], sz = 0; comp[i] = nc; while (st.length) { const u = st.pop(); sz++; for (const v of adjL[u]) if (comp[v] < 0) { comp[v] = nc; st.push(v); } } big = Math.max(big, sz); nc++; }
|
||||||
|
console.error('valid edges', ne, 'components', nc, 'largest', big);
|
||||||
|
const qb = Buffer.alloc(N * 12); Q.forEach((q, i) => q.forEach((v, k) => qb.writeInt16LE(Math.round(v * 100), (i * 6 + k) * 2)));
|
||||||
|
const off = Buffer.alloc((N + 1) * 4), adj = Buffer.alloc(ne * 2 * 2);
|
||||||
|
let p = 0; for (let i = 0; i < N; i++) { off.writeUInt32LE(p, i * 4); for (const v of adjL[i]) { adj.writeUInt16LE(v, p * 2); p++; } } off.writeUInt32LE(p, N * 4);
|
||||||
|
const rm = { n: N, k: K, res: 1.5, fp: S.modelFingerprint(), q: qb.toString('base64'), off: off.toString('base64'), adj: adj.toString('base64'),
|
||||||
|
generated: new Date().toISOString(), stats: { edges: ne, components: nc, largest: big } };
|
||||||
|
const body = text.replace(/\s*\}\s*$/, '');
|
||||||
|
fs.writeFileSync(COLF, body + ',\n "roadmap": ' + JSON.stringify(rm) + '\n}\n');
|
||||||
|
console.log('roadmap written', rm.stats);
|
||||||
|
})();
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
1.20.1
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
var ortWasmThreaded = (() => {
|
||||||
|
var _scriptName = import.meta.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
async function(moduleArg = {}) {
|
||||||
|
var moduleRtn;
|
||||||
|
|
||||||
|
function e(){l.buffer!=q.buffer&&t();return q}function w(){l.buffer!=q.buffer&&t();return ba}function ca(){l.buffer!=q.buffer&&t();return da}function ea(){l.buffer!=q.buffer&&t();return fa}function z(){l.buffer!=q.buffer&&t();return ha}function A(){l.buffer!=q.buffer&&t();return ia}function ja(){l.buffer!=q.buffer&&t();return ka}function la(){l.buffer!=q.buffer&&t();return ma}
|
||||||
|
var B=Object.assign({},moduleArg),na,oa,pa=new Promise((a,b)=>{na=a;oa=b}),qa="object"==typeof window,C="function"==typeof importScripts,D="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,E=C&&"em-pthread"==self.name;if(D){const {createRequire:a}=await import("module");var require=a(import.meta.url),ra=require("worker_threads");global.Worker=ra.Worker;E=(C=!ra.pc)&&"em-pthread"==ra.workerData}"use strict";
|
||||||
|
B.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(B.Fb||(B.Fb=new Map)).set(a,b)};B.unmountExternalData=()=>{delete B.Fb};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,shared:!0})).buffer.constructor;"use strict";
|
||||||
|
let ta=()=>{const a=(c,d,f)=>(...g)=>{const k=F,m=d?.();g=c(...g);const p=d?.();m!==p&&(c=p,f(m),d=f=null);return F!=k?sa():g},b=c=>async(...d)=>{try{if(B.Eb)throw Error("Session already started");const f=B.Eb={fc:d[0],errors:[]},g=await c(...d);if(B.Eb!==f)throw Error("Session mismatch");B.Gb?.flush();const k=f.errors;if(0<k.length){let m=await Promise.all(k);m=m.filter(p=>p);if(0<m.length)throw Error(m.join("\n"));}return g}finally{B.Eb=null}};B._OrtCreateSession=a(B._OrtCreateSession,()=>B._OrtCreateSession,
|
||||||
|
c=>B._OrtCreateSession=c);B._OrtRun=b(a(B._OrtRun,()=>B._OrtRun,c=>B._OrtRun=c));B._OrtRunWithBinding=b(a(B._OrtRunWithBinding,()=>B._OrtRunWithBinding,c=>B._OrtRunWithBinding=c));B._OrtBindInput=a(B._OrtBindInput,()=>B._OrtBindInput,c=>B._OrtBindInput=c);ta=void 0};
|
||||||
|
B.jsepInit=(a,b)=>{ta?.();if("webgpu"===a){[B.Gb,B.Ub,B.Yb,B.Nb,B.Xb,B.jb,B.Zb,B.bc,B.Vb,B.Wb,B.$b]=b;const c=B.Gb;B.jsepRegisterBuffer=(d,f,g,k)=>c.registerBuffer(d,f,g,k);B.jsepGetBuffer=d=>c.getBuffer(d);B.jsepCreateDownloader=(d,f,g)=>c.createDownloader(d,f,g);B.jsepOnReleaseSession=d=>{c.onReleaseSession(d)};B.jsepOnRunStart=d=>c.onRunStart(d);B.cc=(d,f)=>{c.upload(d,f)}}else if("webnn"===a){[B.Gb,B.ac,B.Ob,B.jsepEnsureTensor,B.dc,B.jsepDownloadTensor]=b;B.jsepReleaseTensorId=B.Ob;const c=B.Gb;
|
||||||
|
B.jsepOnRunStart=d=>c.onRunStart(d);B.jsepRegisterMLContext=(d,f)=>{c.registerMLContext(d,f)};B.jsepOnReleaseSession=d=>{c.onReleaseSession(d)};B.jsepCreateMLTensorDownloader=(d,f)=>c.createMLTensorDownloader(d,f);B.jsepRegisterMLTensor=(d,f,g)=>c.registerMLTensor(d,f,g)}};var ua=Object.assign({},B),va="./this.program",wa=(a,b)=>{throw b;},G="",xa,ya,za;
|
||||||
|
if(D){var fs=require("fs"),Aa=require("path");G=require("url").fileURLToPath(new URL("./",import.meta.url));xa=(a,b)=>{a=Ba(a)?new URL(a):Aa.normalize(a);return fs.readFileSync(a,b?void 0:"utf8")};za=a=>{a=xa(a,!0);a.buffer||(a=new Uint8Array(a));return a};ya=(a,b,c,d=!0)=>{a=Ba(a)?new URL(a):Aa.normalize(a);fs.readFile(a,d?void 0:"utf8",(f,g)=>{f?c(f):b(d?g.buffer:g)})};!B.thisProgram&&1<process.argv.length&&(va=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);wa=(a,b)=>{process.exitCode=
|
||||||
|
a;throw b;}}else if(qa||C)C?G=self.location.href:"undefined"!=typeof document&&document.currentScript&&(G=document.currentScript.src),_scriptName&&(G=_scriptName),G.startsWith("blob:")?G="":G=G.substr(0,G.replace(/[?#].*/,"").lastIndexOf("/")+1),D||(xa=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.send(null);return b.responseText},C&&(za=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),ya=(a,b,c)=>{var d=new XMLHttpRequest;
|
||||||
|
d.open("GET",a,!0);d.responseType="arraybuffer";d.onload=()=>{200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)});D&&"undefined"==typeof performance&&(global.performance=require("perf_hooks").performance);var Ca=console.log.bind(console),Da=console.error.bind(console);D&&(Ca=(...a)=>fs.writeSync(1,a.join(" ")+"\n"),Da=(...a)=>fs.writeSync(2,a.join(" ")+"\n"));var Ea=Ca,H=Da;Object.assign(B,ua);ua=null;
|
||||||
|
if(E){var Fa;if(D){var Ga=ra.parentPort;Ga.on("message",b=>onmessage({data:b}));Object.assign(globalThis,{self:global,importScripts:()=>{},postMessage:b=>Ga.postMessage(b),performance:global.performance||{now:Date.now}})}var Ha=!1;H=function(...b){b=b.join(" ");D?fs.writeSync(2,b+"\n"):console.error(b)};self.alert=function(...b){postMessage({Mb:"alert",text:b.join(" "),qc:Ia()})};B.instantiateWasm=(b,c)=>new Promise(d=>{Fa=f=>{f=new WebAssembly.Instance(f,Ja());c(f);d()}});self.onunhandledrejection=
|
||||||
|
b=>{throw b.reason||b;};function a(b){try{var c=b.data,d=c.cmd;if("load"===d){let f=[];self.onmessage=g=>f.push(g);self.startWorker=()=>{postMessage({cmd:"loaded"});for(let g of f)a(g);self.onmessage=a};for(const g of c.handlers)if(!B[g]||B[g].proxy)B[g]=(...k)=>{postMessage({Mb:"callHandler",oc:g,args:k})},"print"==g&&(Ea=B[g]),"printErr"==g&&(H=B[g]);l=c.wasmMemory;t();Fa(c.wasmModule)}else if("run"===d){Ka(c.pthread_ptr,0,0,1,0,0);La(c.pthread_ptr);Ma();Na();Ha||(Oa(),Ha=!0);try{Pa(c.start_routine,
|
||||||
|
c.arg)}catch(f){if("unwind"!=f)throw f;}}else"cancel"===d?Ia()&&Qa(-1):"setimmediate"!==c.target&&("checkMailbox"===d?Ha&&Ra():d&&(H(`worker: received unknown command ${d}`),H(c)))}catch(f){throw Sa(),f;}}self.onmessage=a}var Ta;B.wasmBinary&&(Ta=B.wasmBinary);var l,Ua,I=!1,Va,q,ba,da,fa,ha,ia,ka,J,Wa,ma;
|
||||||
|
function t(){var a=l.buffer;B.HEAP8=q=new Int8Array(a);B.HEAP16=da=new Int16Array(a);B.HEAPU8=ba=new Uint8Array(a);B.HEAPU16=fa=new Uint16Array(a);B.HEAP32=ha=new Int32Array(a);B.HEAPU32=ia=new Uint32Array(a);B.HEAPF32=ka=new Float32Array(a);B.HEAPF64=ma=new Float64Array(a);B.HEAP64=J=new BigInt64Array(a);B.HEAPU64=Wa=new BigUint64Array(a)}
|
||||||
|
if(!E){l=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0});if(!(l.buffer instanceof SharedArrayBuffer))throw H("requested a shared WebAssembly.Memory but the returned buffer is not a SharedArrayBuffer, indicating that while the browser has SharedArrayBuffer it does not have WebAssembly threads support - you may need to set a flag"),D&&H("(on node you may need: --experimental-wasm-threads --experimental-wasm-bulk-memory and/or recent version)"),Error("bad memory");t()}
|
||||||
|
var Xa=[],Ya=[],Za=[],$a=0,ab=null,bb=null;function cb(){$a--;if(0==$a&&(null!==ab&&(clearInterval(ab),ab=null),bb)){var a=bb;bb=null;a()}}function db(a){a="Aborted("+a+")";H(a);I=!0;Va=1;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa(a);throw a;}var eb=a=>a.startsWith("data:application/octet-stream;base64,"),Ba=a=>a.startsWith("file://"),fb;
|
||||||
|
function gb(a){if(a==fb&&Ta)return new Uint8Array(Ta);if(za)return za(a);throw"both async and sync fetching of the wasm failed";}function hb(a){if(!Ta&&(qa||C)){if("function"==typeof fetch&&!Ba(a))return fetch(a,{credentials:"same-origin"}).then(b=>{if(!b.ok)throw`failed to load wasm binary file at '${a}'`;return b.arrayBuffer()}).catch(()=>gb(a));if(ya)return new Promise((b,c)=>{ya(a,d=>b(new Uint8Array(d)),c)})}return Promise.resolve().then(()=>gb(a))}
|
||||||
|
function ib(a,b,c){return hb(a).then(d=>WebAssembly.instantiate(d,b)).then(c,d=>{H(`failed to asynchronously prepare wasm: ${d}`);db(d)})}function jb(a,b){var c=fb;return Ta||"function"!=typeof WebAssembly.instantiateStreaming||eb(c)||Ba(c)||D||"function"!=typeof fetch?ib(c,a,b):fetch(c,{credentials:"same-origin"}).then(d=>WebAssembly.instantiateStreaming(d,a).then(b,function(f){H(`wasm streaming compile failed: ${f}`);H("falling back to ArrayBuffer instantiation");return ib(c,a,b)}))}
|
||||||
|
function Ja(){kb={O:lb,Aa:mb,b:nb,aa:ob,B:pb,qa:qb,Y:rb,_:sb,ra:tb,oa:ub,ha:vb,na:wb,L:xb,Z:yb,W:zb,pa:Ab,X:Bb,wa:Cb,F:Db,Q:Eb,P:Fb,E:Gb,u:Hb,q:Ib,G:Jb,A:Kb,R:Lb,ua:Mb,ka:Nb,U:Ob,ba:Pb,H:Qb,ja:La,ta:Rb,t:Sb,x:Tb,o:Ub,l:Vb,c:Wb,n:Xb,j:Yb,w:Zb,p:$b,g:ac,s:bc,m:cc,e:dc,k:ec,i:fc,h:gc,d:hc,ea:ic,fa:jc,ga:kc,ca:lc,da:mc,T:nc,f:oc,D:pc,I:qc,M:rc,y:sc,sa:tc,V:uc,v:vc,z:wc,N:xc,S:yc,za:zc,ya:Ac,la:Bc,ma:Cc,$:Dc,C:Ec,K:Fc,ia:Gc,J:Hc,a:l,xa:Ic,va:Jc,r:Kc};return{a:kb}}
|
||||||
|
var Lc={868340:(a,b,c,d,f)=>{if("undefined"==typeof B||!B.Fb)return 1;a=K(a>>>0);a.startsWith("./")&&(a=a.substring(2));a=B.Fb.get(a);if(!a)return 2;b>>>=0;c>>>=0;d>>>=0;if(b+c>a.byteLength)return 3;try{const g=a.subarray(b,b+c);switch(f){case 0:w().set(g,d>>>0);break;case 1:B.cc(d,g);break;default:return 4}return 0}catch{return 4}},869023:(a,b,c)=>{B.dc(a,w().subarray(b>>>0,b+c>>>0))},869086:()=>B.ac(),869127:a=>{B.Ob(a)},869163:()=>{B.Vb()},869194:()=>{B.Wb()},869223:()=>{B.$b()},869248:a=>B.Ub(a),
|
||||||
|
869281:a=>B.Yb(a),869313:(a,b,c)=>{B.Nb(a,b,c,!0)},869352:(a,b,c)=>{B.Nb(a,b,c)},869385:()=>"undefined"!==typeof wasmOffsetConverter,869442:a=>{B.jb("Abs",a,void 0)},869493:a=>{B.jb("Neg",a,void 0)},869544:a=>{B.jb("Floor",a,void 0)},869597:a=>{B.jb("Ceil",a,void 0)},869649:a=>{B.jb("Reciprocal",a,void 0)},869707:a=>{B.jb("Sqrt",a,void 0)},869759:a=>{B.jb("Exp",a,void 0)},869810:a=>{B.jb("Erf",a,void 0)},869861:a=>{B.jb("Sigmoid",a,void 0)},869916:(a,b,c)=>{B.jb("HardSigmoid",a,{alpha:b,beta:c})},
|
||||||
|
869995:a=>{B.jb("Log",a,void 0)},870046:a=>{B.jb("Sin",a,void 0)},870097:a=>{B.jb("Cos",a,void 0)},870148:a=>{B.jb("Tan",a,void 0)},870199:a=>{B.jb("Asin",a,void 0)},870251:a=>{B.jb("Acos",a,void 0)},870303:a=>{B.jb("Atan",a,void 0)},870355:a=>{B.jb("Sinh",a,void 0)},870407:a=>{B.jb("Cosh",a,void 0)},870459:a=>{B.jb("Asinh",a,void 0)},870512:a=>{B.jb("Acosh",a,void 0)},870565:a=>{B.jb("Atanh",a,void 0)},870618:a=>{B.jb("Tanh",a,void 0)},870670:a=>{B.jb("Not",a,void 0)},870721:(a,b,c)=>{B.jb("Clip",
|
||||||
|
a,{min:b,max:c})},870790:a=>{B.jb("Clip",a,void 0)},870842:(a,b)=>{B.jb("Elu",a,{alpha:b})},870900:a=>{B.jb("Gelu",a,void 0)},870952:a=>{B.jb("Relu",a,void 0)},871004:(a,b)=>{B.jb("LeakyRelu",a,{alpha:b})},871068:(a,b)=>{B.jb("ThresholdedRelu",a,{alpha:b})},871138:(a,b)=>{B.jb("Cast",a,{to:b})},871196:a=>{B.jb("Add",a,void 0)},871247:a=>{B.jb("Sub",a,void 0)},871298:a=>{B.jb("Mul",a,void 0)},871349:a=>{B.jb("Div",a,void 0)},871400:a=>{B.jb("Pow",a,void 0)},871451:a=>{B.jb("Equal",a,void 0)},871504:a=>
|
||||||
|
{B.jb("Greater",a,void 0)},871559:a=>{B.jb("GreaterOrEqual",a,void 0)},871621:a=>{B.jb("Less",a,void 0)},871673:a=>{B.jb("LessOrEqual",a,void 0)},871732:(a,b,c,d,f)=>{B.jb("ReduceMean",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},871891:(a,b,c,d,f)=>{B.jb("ReduceMax",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},872049:(a,b,c,d,f)=>{B.jb("ReduceMin",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>
|
||||||
|
0,f>>>0)):[]})},872207:(a,b,c,d,f)=>{B.jb("ReduceProd",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},872366:(a,b,c,d,f)=>{B.jb("ReduceSum",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},872524:(a,b,c,d,f)=>{B.jb("ReduceL1",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},872681:(a,b,c,d,f)=>{B.jb("ReduceL2",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>
|
||||||
|
0,f>>>0)):[]})},872838:(a,b,c,d,f)=>{B.jb("ReduceLogSum",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},872999:(a,b,c,d,f)=>{B.jb("ReduceSumSquare",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},873163:(a,b,c,d,f)=>{B.jb("ReduceLogSumExp",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},873327:a=>{B.jb("Where",a,void 0)},873380:(a,b,c)=>{B.jb("Transpose",a,{perm:b?Array.from(z().subarray(b>>>
|
||||||
|
0,c>>>0)):[]})},873488:(a,b,c,d)=>{B.jb("DepthToSpace",a,{blocksize:b,mode:K(c),format:d?"NHWC":"NCHW"})},873621:(a,b,c,d)=>{B.jb("DepthToSpace",a,{blocksize:b,mode:K(c),format:d?"NHWC":"NCHW"})},873754:(a,b,c,d,f,g,k,m,p,n,r,v,x,h,u)=>{B.jb("ConvTranspose",a,{format:p?"NHWC":"NCHW",autoPad:b,dilations:[c],group:d,kernelShape:[f],pads:[g,k],strides:[m],wIsConst:()=>!!e()[n>>>0],outputPadding:r?Array.from(z().subarray(r>>>0,v>>>0)):[],outputShape:x?Array.from(z().subarray(x>>>0,h>>>0)):[],activation:K(u)})},
|
||||||
|
874155:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("ConvTranspose",a,{format:m?"NHWC":"NCHW",autoPad:b,dilations:Array.from(z().subarray(c>>>0,(c>>>0)+2>>>0)),group:d,kernelShape:Array.from(z().subarray(f>>>0,(f>>>0)+2>>>0)),pads:Array.from(z().subarray(g>>>0,(g>>>0)+4>>>0)),strides:Array.from(z().subarray(k>>>0,(k>>>0)+2>>>0)),wIsConst:()=>!!e()[p>>>0],outputPadding:n?Array.from(z().subarray(n>>>0,r>>>0)):[],outputShape:v?Array.from(z().subarray(v>>>0,x>>>0)):[],activation:K(h)})},874720:(a,b,c,d,f,g,k,
|
||||||
|
m,p,n,r,v,x,h,u)=>{B.jb("ConvTranspose",a,{format:p?"NHWC":"NCHW",autoPad:b,dilations:[c],group:d,kernelShape:[f],pads:[g,k],strides:[m],wIsConst:()=>!!e()[n>>>0],outputPadding:r?Array.from(z().subarray(r>>>0,v>>>0)):[],outputShape:x?Array.from(z().subarray(x>>>0,h>>>0)):[],activation:K(u)})},875121:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("ConvTranspose",a,{format:m?"NHWC":"NCHW",autoPad:b,dilations:Array.from(z().subarray(c>>>0,(c>>>0)+2>>>0)),group:d,kernelShape:Array.from(z().subarray(f>>>0,(f>>>
|
||||||
|
0)+2>>>0)),pads:Array.from(z().subarray(g>>>0,(g>>>0)+4>>>0)),strides:Array.from(z().subarray(k>>>0,(k>>>0)+2>>>0)),wIsConst:()=>!!e()[p>>>0],outputPadding:n?Array.from(z().subarray(n>>>0,r>>>0)):[],outputShape:v?Array.from(z().subarray(v>>>0,x>>>0)):[],activation:K(h)})},875686:(a,b)=>{B.jb("GlobalAveragePool",a,{format:b?"NHWC":"NCHW"})},875777:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("AveragePool",a,{format:h?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?Array.from(z().subarray(g>>>
|
||||||
|
0,k>>>0)):[],kernel_shape:m?Array.from(z().subarray(m>>>0,p>>>0)):[],pads:n?Array.from(z().subarray(n>>>0,r>>>0)):[],strides:v?Array.from(z().subarray(v>>>0,x>>>0)):[]})},876192:(a,b)=>{B.jb("GlobalAveragePool",a,{format:b?"NHWC":"NCHW"})},876283:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("AveragePool",a,{format:h?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?Array.from(z().subarray(g>>>0,k>>>0)):[],kernel_shape:m?Array.from(z().subarray(m>>>0,p>>>0)):[],pads:n?Array.from(z().subarray(n>>>
|
||||||
|
0,r>>>0)):[],strides:v?Array.from(z().subarray(v>>>0,x>>>0)):[]})},876698:(a,b)=>{B.jb("GlobalMaxPool",a,{format:b?"NHWC":"NCHW"})},876785:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("MaxPool",a,{format:h?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?Array.from(z().subarray(g>>>0,k>>>0)):[],kernel_shape:m?Array.from(z().subarray(m>>>0,p>>>0)):[],pads:n?Array.from(z().subarray(n>>>0,r>>>0)):[],strides:v?Array.from(z().subarray(v>>>0,x>>>0)):[]})},877196:(a,b)=>{B.jb("GlobalMaxPool",
|
||||||
|
a,{format:b?"NHWC":"NCHW"})},877283:(a,b,c,d,f,g,k,m,p,n,r,v,x,h)=>{B.jb("MaxPool",a,{format:h?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?Array.from(z().subarray(g>>>0,k>>>0)):[],kernel_shape:m?Array.from(z().subarray(m>>>0,p>>>0)):[],pads:n?Array.from(z().subarray(n>>>0,r>>>0)):[],strides:v?Array.from(z().subarray(v>>>0,x>>>0)):[]})},877694:(a,b,c,d,f)=>{B.jb("Gemm",a,{alpha:b,beta:c,transA:d,transB:f})},877798:a=>{B.jb("MatMul",a,void 0)},877852:(a,b,c,
|
||||||
|
d)=>{B.jb("ArgMax",a,{keepDims:!!b,selectLastIndex:!!c,axis:d})},877960:(a,b,c,d)=>{B.jb("ArgMin",a,{keepDims:!!b,selectLastIndex:!!c,axis:d})},878068:(a,b)=>{B.jb("Softmax",a,{axis:b})},878131:(a,b)=>{B.jb("Concat",a,{axis:b})},878191:(a,b,c,d,f)=>{B.jb("Split",a,{axis:b,numOutputs:c,splitSizes:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},878331:a=>{B.jb("Expand",a,void 0)},878385:(a,b)=>{B.jb("Gather",a,{axis:Number(b)})},878456:(a,b)=>{B.jb("GatherElements",a,{axis:Number(b)})},878535:(a,b,c,
|
||||||
|
d,f,g,k,m,p,n,r)=>{B.jb("Resize",a,{antialias:b,axes:c?Array.from(z().subarray(c>>>0,d>>>0)):[],coordinateTransformMode:K(f),cubicCoeffA:g,excludeOutside:k,extrapolationValue:m,keepAspectRatioPolicy:K(p),mode:K(n),nearestMode:K(r)})},878881:(a,b,c,d,f,g,k)=>{B.jb("Slice",a,{starts:b?Array.from(z().subarray(b>>>0,c>>>0)):[],ends:d?Array.from(z().subarray(d>>>0,f>>>0)):[],axes:g?Array.from(z().subarray(g>>>0,k>>>0)):[]})},879097:a=>{B.jb("Tile",a,void 0)},879149:(a,b,c)=>{B.jb("InstanceNormalization",
|
||||||
|
a,{epsilon:b,format:c?"NHWC":"NCHW"})},879263:(a,b,c)=>{B.jb("InstanceNormalization",a,{epsilon:b,format:c?"NHWC":"NCHW"})},879377:a=>{B.jb("Range",a,void 0)},879430:(a,b)=>{B.jb("Einsum",a,{equation:K(b)})},879511:(a,b,c,d,f)=>{B.jb("Pad",a,{mode:b,value:c,pads:d?Array.from(z().subarray(d>>>0,f>>>0)):[]})},879638:(a,b,c,d,f,g)=>{B.jb("BatchNormalization",a,{epsilon:b,momentum:c,spatial:!!f,trainingMode:!!d,format:g?"NHWC":"NCHW"})},879807:(a,b,c,d,f,g)=>{B.jb("BatchNormalization",a,{epsilon:b,momentum:c,
|
||||||
|
spatial:!!f,trainingMode:!!d,format:g?"NHWC":"NCHW"})},879976:(a,b,c)=>{B.jb("CumSum",a,{exclusive:Number(b),reverse:Number(c)})},880073:(a,b,c)=>{B.jb("DequantizeLinear",a,{axis:b,blockSize:c})},880163:(a,b,c,d,f,g,k,m,p)=>{B.jb("Attention",a,{numHeads:b,isUnidirectional:c,maskFilterValue:d,scale:f,doRotary:g,qkvHiddenSizes:k?Array.from(z().subarray(Number(m)>>>0,Number(m)+k>>>0)):[],pastPresentShareBuffer:!!p})},880435:a=>{B.jb("BiasAdd",a,void 0)},880490:a=>{B.jb("BiasSplitGelu",a,void 0)},880551:a=>
|
||||||
|
{B.jb("FastGelu",a,void 0)},880607:(a,b,c,d,f,g,k,m,p,n,r,v,x,h,u,y)=>{B.jb("Conv",a,{format:v?"NHWC":"NCHW",auto_pad:b,dilations:c?Array.from(z().subarray(c>>>0,d>>>0)):[],group:f,kernel_shape:g?Array.from(z().subarray(g>>>0,k>>>0)):[],pads:m?Array.from(z().subarray(m>>>0,p>>>0)):[],strides:n?Array.from(z().subarray(n>>>0,r>>>0)):[],w_is_const:()=>!!e()[x>>>0],activation:K(h),activation_params:u?Array.from(ja().subarray(u>>>0,y>>>0)):[]})},881103:a=>{B.jb("Gelu",a,void 0)},881155:(a,b,c,d)=>{B.jb("GroupQueryAttention",
|
||||||
|
a,{numHeads:b,kvNumHeads:c,scale:d})},881268:(a,b,c,d)=>{B.jb("LayerNormalization",a,{axis:b,epsilon:c,simplified:!!d})},881379:(a,b,c,d)=>{B.jb("LayerNormalization",a,{axis:b,epsilon:c,simplified:!!d})},881490:(a,b,c,d,f,g)=>{B.jb("MatMulNBits",a,{k:b,n:c,accuracyLevel:d,bits:f,blockSize:g})},881617:(a,b,c,d,f,g)=>{B.jb("MultiHeadAttention",a,{numHeads:b,isUnidirectional:c,maskFilterValue:d,scale:f,doRotary:g})},881776:(a,b)=>{B.jb("QuickGelu",a,{alpha:b})},881840:(a,b,c,d,f)=>{B.jb("RotaryEmbedding",
|
||||||
|
a,{interleaved:!!b,numHeads:c,rotaryEmbeddingDim:d,scale:f})},881979:(a,b,c)=>{B.jb("SkipLayerNormalization",a,{epsilon:b,simplified:!!c})},882081:(a,b,c)=>{B.jb("SkipLayerNormalization",a,{epsilon:b,simplified:!!c})},882183:(a,b,c,d)=>{B.jb("GatherBlockQuantized",a,{gatherAxis:b,quantizeAxis:c,blockSize:d})},882304:a=>{B.Zb(a)},882338:(a,b)=>B.bc(a,b,B.Eb.fc,B.Eb.errors)};function mb(a,b,c){return Mc(async()=>{await B.Xb(a,b,c)})}function lb(){return"undefined"!==typeof wasmOffsetConverter}
|
||||||
|
function Nc(a){this.name="ExitStatus";this.message=`Program terminated with exit(${a})`;this.status=a}
|
||||||
|
var Oc=a=>{a.terminate();a.onmessage=()=>{}},Rc=a=>{0==L.length&&(Pc(),Qc(L[0]));var b=L.pop();if(!b)return 6;M.push(b);N[a.Ab]=b;b.Ab=a.Ab;var c={cmd:"run",start_routine:a.hc,arg:a.Qb,pthread_ptr:a.Ab};D&&b.unref();b.postMessage(c,a.mc);return 0},O=0,P=(a,b,...c)=>{for(var d=2*c.length,f=Sc(),g=Tc(8*d),k=g>>>3,m=0;m<c.length;m++){var p=c[m];"bigint"==typeof p?(J[k+2*m]=1n,J[k+2*m+1]=p):(J[k+2*m]=0n,la()[k+2*m+1>>>0]=p)}a=Vc(a,0,d,g,b);Wc(f);return a};
|
||||||
|
function Ic(a){if(E)return P(0,1,a);Va=a;if(!(0<O)){for(var b of M)Oc(b);for(b of L)Oc(b);L=[];M=[];N=[];I=!0}wa(a,new Nc(a))}function Xc(a){if(E)return P(1,0,a);Dc(a)}var Dc=a=>{Va=a;if(E)throw Xc(a),"unwind";Ic(a)},L=[],M=[],Yc=[],N={};function Zc(){for(var a=B.numThreads-1;a--;)Pc();Xa.unshift(()=>{$a++;$c(()=>cb())})}var bd=a=>{var b=a.Ab;delete N[b];L.push(a);M.splice(M.indexOf(a),1);a.Ab=0;ad(b)};function Na(){Yc.forEach(a=>a())}
|
||||||
|
var Qc=a=>new Promise(b=>{a.onmessage=g=>{g=g.data;var k=g.cmd;if(g.targetThread&&g.targetThread!=Ia()){var m=N[g.targetThread];m?m.postMessage(g,g.transferList):H(`Internal error! Worker sent a message "${k}" to target pthread ${g.targetThread}, but that thread no longer exists!`)}else if("checkMailbox"===k)Ra();else if("spawnThread"===k)Rc(g);else if("cleanupThread"===k)bd(N[g.thread]);else if("killThread"===k)g=g.thread,k=N[g],delete N[g],Oc(k),ad(g),M.splice(M.indexOf(k),1),k.Ab=0;else if("cancelThread"===
|
||||||
|
k)N[g.thread].postMessage({cmd:"cancel"});else if("loaded"===k)a.loaded=!0,D&&!a.Ab&&a.unref(),b(a);else if("alert"===k)alert(`Thread ${g.threadId}: ${g.text}`);else if("setimmediate"===g.target)a.postMessage(g);else if("callHandler"===k)B[g.handler](...g.args);else k&&H(`worker sent an unknown command ${k}`)};a.onerror=g=>{H(`${"worker sent an error!"} ${g.filename}:${g.lineno}: ${g.message}`);throw g;};D&&(a.on("message",g=>a.onmessage({data:g})),a.on("error",g=>a.onerror(g)));var c=[],d=[],f;for(f of d)B.hasOwnProperty(f)&&
|
||||||
|
c.push(f);a.postMessage({cmd:"load",handlers:c,wasmMemory:l,wasmModule:Ua})});function $c(a){E?a():Promise.all(L.map(Qc)).then(a)}function Pc(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});L.push(a)}var cd=a=>{for(;0<a.length;)a.shift()(B)},Ma=()=>{var a=Ia(),b=A()[a+52>>>2>>>0];a=A()[a+56>>>2>>>0];dd(b,b-a);Wc(b)},Pa=(a,b)=>{O=0;a=ed(a,b);0<O?Va=a:Qa(a)};class fd{constructor(a){this.Jb=a-24}}var gd=0,hd=0;
|
||||||
|
function nb(a,b,c){a>>>=0;var d=new fd(a);b>>>=0;c>>>=0;A()[d.Jb+16>>>2>>>0]=0;A()[d.Jb+4>>>2>>>0]=b;A()[d.Jb+8>>>2>>>0]=c;gd=a;hd++;throw gd;}function jd(a,b,c,d){return E?P(2,1,a,b,c,d):ob(a,b,c,d)}
|
||||||
|
function ob(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;if("undefined"==typeof SharedArrayBuffer)return H("Current environment does not support SharedArrayBuffer, pthreads are not available!"),6;var f=[];if(E&&0===f.length)return jd(a,b,c,d);a={hc:c,Ab:a,Qb:d,mc:f};return E?(a.Mb="spawnThread",postMessage(a,f),0):Rc(a)}
|
||||||
|
var kd="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,ld=(a,b,c)=>{b>>>=0;var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.buffer&&kd)return kd.decode(a.buffer instanceof SharedArrayBuffer?a.slice(b,c):a.subarray(b,c));for(d="";b<c;){var f=a[b++];if(f&128){var g=a[b++]&63;if(192==(f&224))d+=String.fromCharCode((f&31)<<6|g);else{var k=a[b++]&63;f=224==(f&240)?(f&15)<<12|g<<6|k:(f&7)<<18|g<<12|k<<6|a[b++]&63;65536>f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>
|
||||||
|
10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d},K=(a,b)=>(a>>>=0)?ld(w(),a,b):"";function pb(a,b,c){return E?P(3,1,a,b,c):0}function qb(a,b){if(E)return P(4,1,a,b)}
|
||||||
|
var md=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},nd=(a,b,c,d)=>{c>>>=0;if(!(0<d))return 0;var f=c;d=c+d-1;for(var g=0;g<a.length;++g){var k=a.charCodeAt(g);if(55296<=k&&57343>=k){var m=a.charCodeAt(++g);k=65536+((k&1023)<<10)|m&1023}if(127>=k){if(c>=d)break;b[c++>>>0]=k}else{if(2047>=k){if(c+1>=d)break;b[c++>>>0]=192|k>>6}else{if(65535>=k){if(c+2>=d)break;b[c++>>>0]=224|k>>12}else{if(c+3>=d)break;b[c++>>>0]=240|k>>
|
||||||
|
18;b[c++>>>0]=128|k>>12&63}b[c++>>>0]=128|k>>6&63}b[c++>>>0]=128|k&63}}b[c>>>0]=0;return c-f},od=(a,b,c)=>nd(a,w(),b,c);function rb(a,b){if(E)return P(5,1,a,b)}function sb(a,b,c){if(E)return P(6,1,a,b,c)}function tb(a,b,c){return E?P(7,1,a,b,c):0}function ub(a,b){if(E)return P(8,1,a,b)}function vb(a,b,c){if(E)return P(9,1,a,b,c)}function wb(a,b,c,d){if(E)return P(10,1,a,b,c,d)}function xb(a,b,c,d){if(E)return P(11,1,a,b,c,d)}function yb(a,b,c,d){if(E)return P(12,1,a,b,c,d)}
|
||||||
|
function zb(a){if(E)return P(13,1,a)}function Ab(a,b){if(E)return P(14,1,a,b)}function Bb(a,b,c){if(E)return P(15,1,a,b,c)}var Cb=()=>{db("")},pd,Q=a=>{for(var b="";w()[a>>>0];)b+=pd[w()[a++>>>0]];return b},qd={},rd={},sd={},R;
|
||||||
|
function td(a,b,c={}){var d=b.name;if(!a)throw new R(`type "${d}" must have a positive integer typeid pointer`);if(rd.hasOwnProperty(a)){if(c.Sb)return;throw new R(`Cannot register type '${d}' twice`);}rd[a]=b;delete sd[a];qd.hasOwnProperty(a)&&(b=qd[a],delete qd[a],b.forEach(f=>f()))}function S(a,b,c={}){if(!("argPackAdvance"in b))throw new TypeError("registerType registeredInstance requires argPackAdvance");return td(a,b,c)}
|
||||||
|
var ud=(a,b,c)=>{switch(b){case 1:return c?d=>e()[d>>>0]:d=>w()[d>>>0];case 2:return c?d=>ca()[d>>>1>>>0]:d=>ea()[d>>>1>>>0];case 4:return c?d=>z()[d>>>2>>>0]:d=>A()[d>>>2>>>0];case 8:return c?d=>J[d>>>3]:d=>Wa[d>>>3];default:throw new TypeError(`invalid integer width (${b}): ${a}`);}};
|
||||||
|
function Db(a,b,c){a>>>=0;c>>>=0;b=Q(b>>>0);S(a,{name:b,fromWireType:d=>d,toWireType:function(d,f){if("bigint"!=typeof f&&"number"!=typeof f)throw null===f?f="null":(d=typeof f,f="object"===d||"array"===d||"function"===d?f.toString():""+f),new TypeError(`Cannot convert "${f}" to ${this.name}`);"number"==typeof f&&(f=BigInt(f));return f},argPackAdvance:T,readValueFromPointer:ud(b,c,-1==b.indexOf("u")),Db:null})}var T=8;
|
||||||
|
function Eb(a,b,c,d){a>>>=0;b=Q(b>>>0);S(a,{name:b,fromWireType:function(f){return!!f},toWireType:function(f,g){return g?c:d},argPackAdvance:T,readValueFromPointer:function(f){return this.fromWireType(w()[f>>>0])},Db:null})}var vd=[],U=[];function Wb(a){a>>>=0;9<a&&0===--U[a+1]&&(U[a]=void 0,vd.push(a))}
|
||||||
|
var V=a=>{if(!a)throw new R("Cannot use deleted val. handle = "+a);return U[a]},W=a=>{switch(a){case void 0:return 2;case null:return 4;case !0:return 6;case !1:return 8;default:const b=vd.pop()||U.length;U[b]=a;U[b+1]=1;return b}};function wd(a){return this.fromWireType(A()[a>>>2>>>0])}var xd={name:"emscripten::val",fromWireType:a=>{var b=V(a);Wb(a);return b},toWireType:(a,b)=>W(b),argPackAdvance:T,readValueFromPointer:wd,Db:null};function Fb(a){return S(a>>>0,xd)}
|
||||||
|
var yd=(a,b)=>{switch(b){case 4:return function(c){return this.fromWireType(ja()[c>>>2>>>0])};case 8:return function(c){return this.fromWireType(la()[c>>>3>>>0])};default:throw new TypeError(`invalid float width (${b}): ${a}`);}};function Gb(a,b,c){a>>>=0;c>>>=0;b=Q(b>>>0);S(a,{name:b,fromWireType:d=>d,toWireType:(d,f)=>f,argPackAdvance:T,readValueFromPointer:yd(b,c),Db:null})}
|
||||||
|
function Hb(a,b,c,d,f){a>>>=0;c>>>=0;b=Q(b>>>0);-1===f&&(f=4294967295);f=m=>m;if(0===d){var g=32-8*c;f=m=>m<<g>>>g}var k=b.includes("unsigned")?function(m,p){return p>>>0}:function(m,p){return p};S(a,{name:b,fromWireType:f,toWireType:k,argPackAdvance:T,readValueFromPointer:ud(b,c,0!==d),Db:null})}
|
||||||
|
function Ib(a,b,c){function d(g){var k=A()[g>>>2>>>0];g=A()[g+4>>>2>>>0];return new f(e().buffer,g,k)}a>>>=0;var f=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array][b];c=Q(c>>>0);S(a,{name:c,fromWireType:d,argPackAdvance:T,readValueFromPointer:d},{Sb:!0})}
|
||||||
|
function Jb(a,b){a>>>=0;b=Q(b>>>0);var c="std::string"===b;S(a,{name:b,fromWireType:function(d){var f=A()[d>>>2>>>0],g=d+4;if(c)for(var k=g,m=0;m<=f;++m){var p=g+m;if(m==f||0==w()[p>>>0]){k=K(k,p-k);if(void 0===n)var n=k;else n+=String.fromCharCode(0),n+=k;k=p+1}}else{n=Array(f);for(m=0;m<f;++m)n[m]=String.fromCharCode(w()[g+m>>>0]);n=n.join("")}X(d);return n},toWireType:function(d,f){f instanceof ArrayBuffer&&(f=new Uint8Array(f));var g="string"==typeof f;if(!(g||f instanceof Uint8Array||f instanceof
|
||||||
|
Uint8ClampedArray||f instanceof Int8Array))throw new R("Cannot pass non-string to std::string");var k=c&&g?md(f):f.length;var m=zd(4+k+1),p=m+4;A()[m>>>2>>>0]=k;if(c&&g)od(f,p,k+1);else if(g)for(g=0;g<k;++g){var n=f.charCodeAt(g);if(255<n)throw X(p),new R("String has UTF-16 code units that do not fit in 8 bits");w()[p+g>>>0]=n}else for(g=0;g<k;++g)w()[p+g>>>0]=f[g];null!==d&&d.push(X,m);return m},argPackAdvance:T,readValueFromPointer:wd,Db(d){X(d)}})}
|
||||||
|
var Ad="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0,Bd=(a,b)=>{var c=a>>1;for(var d=c+b/2;!(c>=d)&&ea()[c>>>0];)++c;c<<=1;if(32<c-a&&Ad)return Ad.decode(w().slice(a,c));c="";for(d=0;!(d>=b/2);++d){var f=ca()[a+2*d>>>1>>>0];if(0==f)break;c+=String.fromCharCode(f)}return c},Cd=(a,b,c)=>{c??=2147483647;if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var f=0;f<c;++f){var g=a.charCodeAt(f);ca()[b>>>1>>>0]=g;b+=2}ca()[b>>>1>>>0]=0;return b-d},Dd=a=>2*a.length,Ed=(a,b)=>
|
||||||
|
{for(var c=0,d="";!(c>=b/4);){var f=z()[a+4*c>>>2>>>0];if(0==f)break;++c;65536<=f?(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023)):d+=String.fromCharCode(f)}return d},Fd=(a,b,c)=>{b>>>=0;c??=2147483647;if(4>c)return 0;var d=b;c=d+c-4;for(var f=0;f<a.length;++f){var g=a.charCodeAt(f);if(55296<=g&&57343>=g){var k=a.charCodeAt(++f);g=65536+((g&1023)<<10)|k&1023}z()[b>>>2>>>0]=g;b+=4;if(b+4>c)break}z()[b>>>2>>>0]=0;return b-d},Gd=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=
|
||||||
|
d&&57343>=d&&++c;b+=4}return b};
|
||||||
|
function Kb(a,b,c){a>>>=0;b>>>=0;c>>>=0;c=Q(c);if(2===b){var d=Bd;var f=Cd;var g=Dd;var k=m=>ea()[m>>>1>>>0]}else 4===b&&(d=Ed,f=Fd,g=Gd,k=m=>A()[m>>>2>>>0]);S(a,{name:c,fromWireType:m=>{for(var p=A()[m>>>2>>>0],n,r=m+4,v=0;v<=p;++v){var x=m+4+v*b;if(v==p||0==k(x))r=d(r,x-r),void 0===n?n=r:(n+=String.fromCharCode(0),n+=r),r=x+b}X(m);return n},toWireType:(m,p)=>{if("string"!=typeof p)throw new R(`Cannot pass non-string to C++ string type ${c}`);var n=g(p),r=zd(4+n+b);A()[r>>>2>>>0]=n/b;f(p,r+4,n+b);
|
||||||
|
null!==m&&m.push(X,r);return r},argPackAdvance:T,readValueFromPointer:wd,Db(m){X(m)}})}function Lb(a,b){a>>>=0;b=Q(b>>>0);S(a,{Tb:!0,name:b,argPackAdvance:0,fromWireType:()=>{},toWireType:()=>{}})}var Mb=()=>1;function Nb(a){Ka(a>>>0,!C,1,!qa,131072,!1);Na()}var Hd=a=>{if(!I)try{if(a(),!(0<O))try{E?Qa(Va):Dc(Va)}catch(b){b instanceof Nc||"unwind"==b||wa(1,b)}}catch(b){b instanceof Nc||"unwind"==b||wa(1,b)}};
|
||||||
|
function La(a){a>>>=0;"function"===typeof Atomics.nc&&(Atomics.nc(z(),a>>>2,a).value.then(Ra),a+=128,Atomics.store(z(),a>>>2,1))}var Ra=()=>{var a=Ia();a&&(La(a),Hd(Id))};function Ob(a,b){a>>>=0;a==b>>>0?setTimeout(Ra):E?postMessage({targetThread:a,cmd:"checkMailbox"}):(a=N[a])&&a.postMessage({cmd:"checkMailbox"})}var Jd=[];function Pb(a,b,c,d,f){b>>>=0;d/=2;Jd.length=d;c=f>>>0>>>3;for(f=0;f<d;f++)Jd[f]=J[c+2*f]?J[c+2*f+1]:la()[c+2*f+1>>>0];return(b?Lc[b]:Kd[a])(...Jd)}
|
||||||
|
function Qb(a){a>>>=0;E?postMessage({cmd:"cleanupThread",thread:a}):bd(N[a])}function Rb(a){D&&N[a>>>0].ref()}var Md=(a,b)=>{var c=rd[a];if(void 0===c)throw a=Ld(a),c=Q(a),X(a),new R(`${b} has unknown type ${c}`);return c},Nd=(a,b,c)=>{var d=[];a=a.toWireType(d,c);d.length&&(A()[b>>>2>>>0]=W(d));return a};function Sb(a,b,c){b>>>=0;c>>>=0;a=V(a>>>0);b=Md(b,"emval::as");return Nd(b,c,a)}var Od=a=>{try{a()}catch(b){db(b)}};
|
||||||
|
function Pd(){var a=Y,b={};for(let [c,d]of Object.entries(a))b[c]="function"==typeof d?(...f)=>{Qd.push(c);try{return d(...f)}finally{I||(Qd.pop(),F&&1===Z&&0===Qd.length&&(Z=0,O+=1,Od(Rd),"undefined"!=typeof Fibers&&Fibers.sc()))}}:d;return b}var Z=0,F=null,Sd=0,Qd=[],Td={},Ud={},Vd=0,Wd=null,Xd=[];function sa(){return new Promise((a,b)=>{Wd={resolve:a,reject:b}})}
|
||||||
|
function Yd(){var a=zd(65548),b=a+12;A()[a>>>2>>>0]=b;A()[a+4>>>2>>>0]=b+65536;b=Qd[0];var c=Td[b];void 0===c&&(c=Vd++,Td[b]=c,Ud[c]=b);b=c;z()[a+8>>>2>>>0]=b;return a}function Zd(){var a=z()[F+8>>>2>>>0];a=Y[Ud[a]];--O;return a()}
|
||||||
|
function $d(a){if(!I){if(0===Z){var b=!1,c=!1;a((d=0)=>{if(!I&&(Sd=d,b=!0,c)){Z=2;Od(()=>ae(F));"undefined"!=typeof Browser&&Browser.Kb.Rb&&Browser.Kb.resume();d=!1;try{var f=Zd()}catch(m){f=m,d=!0}var g=!1;if(!F){var k=Wd;k&&(Wd=null,(d?k.reject:k.resolve)(f),g=!0)}if(d&&!g)throw f;}});c=!0;b||(Z=1,F=Yd(),"undefined"!=typeof Browser&&Browser.Kb.Rb&&Browser.Kb.pause(),Od(()=>be(F)))}else 2===Z?(Z=0,Od(ce),X(F),F=null,Xd.forEach(Hd)):db(`invalid state: ${Z}`);return Sd}}
|
||||||
|
function Mc(a){return $d(b=>{a().then(b)})}function Tb(a){a>>>=0;return Mc(()=>{a=V(a);return a.then(W)})}var de=[];function Ub(a,b,c,d){c>>>=0;d>>>=0;a=de[a>>>0];b=V(b>>>0);return a(null,b,c,d)}var ee={},fe=a=>{var b=ee[a];return void 0===b?Q(a):b};function Vb(a,b,c,d,f){c>>>=0;d>>>=0;f>>>=0;a=de[a>>>0];b=V(b>>>0);c=fe(c);return a(b,b[c],d,f)}var ge=()=>"object"==typeof globalThis?globalThis:Function("return this")();function Xb(a){a>>>=0;if(0===a)return W(ge());a=fe(a);return W(ge()[a])}
|
||||||
|
var he=a=>{var b=de.length;de.push(a);return b},ie=(a,b)=>{for(var c=Array(a),d=0;d<a;++d)c[d]=Md(A()[b+4*d>>>2>>>0],"parameter "+d);return c},je=(a,b)=>Object.defineProperty(b,"name",{value:a});function ke(a){var b=Function;if(!(b instanceof Function))throw new TypeError(`new_ called with constructor type ${typeof b} which is not a function`);var c=je(b.name||"unknownFunctionName",function(){});c.prototype=b.prototype;c=new c;a=b.apply(c,a);return a instanceof Object?a:c}
|
||||||
|
function Yb(a,b,c){b=ie(a,b>>>0);var d=b.shift();a--;var f="return function (obj, func, destructorsRef, args) {\n",g=0,k=[];0===c&&k.push("obj");for(var m=["retType"],p=[d],n=0;n<a;++n)k.push("arg"+n),m.push("argType"+n),p.push(b[n]),f+=` var arg${n} = argType${n}.readValueFromPointer(args${g?"+"+g:""});\n`,g+=b[n].argPackAdvance;f+=` var rv = ${1===c?"new func":"func.call"}(${k.join(", ")});\n`;d.Tb||(m.push("emval_returnValue"),p.push(Nd),f+=" return emval_returnValue(retType, destructorsRef, rv);\n");
|
||||||
|
m.push(f+"};\n");a=ke(m)(...p);c=`methodCaller<(${b.map(r=>r.name).join(", ")}) => ${d.name}>`;return he(je(c,a))}function Zb(a){a=fe(a>>>0);return W(B[a])}function $b(a,b){b>>>=0;a=V(a>>>0);b=V(b);return W(a[b])}function ac(a){a>>>=0;9<a&&(U[a+1]+=1)}function bc(){return W([])}function cc(a){a=V(a>>>0);for(var b=Array(a.length),c=0;c<a.length;c++)b[c]=a[c];return W(b)}function dc(a){return W(fe(a>>>0))}function ec(){return W({})}
|
||||||
|
function fc(a){a>>>=0;for(var b=V(a);b.length;){var c=b.pop();b.pop()(c)}Wb(a)}function gc(a,b,c){b>>>=0;c>>>=0;a=V(a>>>0);b=V(b);c=V(c);a[b]=c}function hc(a,b){b>>>=0;a=Md(a>>>0,"_emval_take_value");a=a.readValueFromPointer(b);return W(a)}
|
||||||
|
function ic(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);z()[b>>>2>>>0]=a.getUTCSeconds();z()[b+4>>>2>>>0]=a.getUTCMinutes();z()[b+8>>>2>>>0]=a.getUTCHours();z()[b+12>>>2>>>0]=a.getUTCDate();z()[b+16>>>2>>>0]=a.getUTCMonth();z()[b+20>>>2>>>0]=a.getUTCFullYear()-1900;z()[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;z()[b+28>>>2>>>0]=a}
|
||||||
|
var le=a=>0===a%4&&(0!==a%100||0===a%400),me=[0,31,60,91,121,152,182,213,244,274,305,335],ne=[0,31,59,90,120,151,181,212,243,273,304,334];
|
||||||
|
function jc(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);z()[b>>>2>>>0]=a.getSeconds();z()[b+4>>>2>>>0]=a.getMinutes();z()[b+8>>>2>>>0]=a.getHours();z()[b+12>>>2>>>0]=a.getDate();z()[b+16>>>2>>>0]=a.getMonth();z()[b+20>>>2>>>0]=a.getFullYear()-1900;z()[b+24>>>2>>>0]=a.getDay();var c=(le(a.getFullYear())?me:ne)[a.getMonth()]+a.getDate()-1|0;z()[b+28>>>2>>>0]=c;z()[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());c=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
|
||||||
|
var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(c!=d&&a.getTimezoneOffset()==Math.min(d,c))|0;z()[b+32>>>2>>>0]=a}
|
||||||
|
function kc(a){a>>>=0;var b=new Date(z()[a+20>>>2>>>0]+1900,z()[a+16>>>2>>>0],z()[a+12>>>2>>>0],z()[a+8>>>2>>>0],z()[a+4>>>2>>>0],z()[a>>>2>>>0],0),c=z()[a+32>>>2>>>0],d=b.getTimezoneOffset(),f=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),g=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),k=Math.min(g,f);0>c?z()[a+32>>>2>>>0]=Number(f!=g&&k==d):0<c!=(k==d)&&(f=Math.max(g,f),b.setTime(b.getTime()+6E4*((0<c?k:f)-d)));z()[a+24>>>2>>>0]=b.getDay();c=(le(b.getFullYear())?me:ne)[b.getMonth()]+
|
||||||
|
b.getDate()-1|0;z()[a+28>>>2>>>0]=c;z()[a>>>2>>>0]=b.getSeconds();z()[a+4>>>2>>>0]=b.getMinutes();z()[a+8>>>2>>>0]=b.getHours();z()[a+12>>>2>>>0]=b.getDate();z()[a+16>>>2>>>0]=b.getMonth();z()[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function lc(a,b,c,d,f,g,k){return E?P(16,1,a,b,c,d,f,g,k):-52}function mc(a,b,c,d,f,g){if(E)return P(17,1,a,b,c,d,f,g)}
|
||||||
|
function nc(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;var f=(new Date).getFullYear(),g=new Date(f,0,1),k=new Date(f,6,1);f=g.getTimezoneOffset();var m=k.getTimezoneOffset(),p=Math.max(f,m);A()[a>>>2>>>0]=60*p;z()[b>>>2>>>0]=Number(f!=m);a=n=>n.toLocaleTimeString(void 0,{hour12:!1,timeZoneName:"short"}).split(" ")[1];g=a(g);k=a(k);m<f?(od(g,c,17),od(k,d,17)):(od(g,d,17),od(k,c,17))}
|
||||||
|
var oe=[],pe=(a,b)=>{oe.length=0;for(var c;c=w()[a++>>>0];){var d=105!=c;d&=112!=c;b+=d&&b%8?4:0;oe.push(112==c?A()[b>>>2>>>0]:106==c?J[b>>>3]:105==c?z()[b>>>2>>>0]:la()[b>>>3>>>0]);b+=d?8:4}return oe};function oc(a,b,c){a>>>=0;b=pe(b>>>0,c>>>0);return Lc[a](...b)}function pc(a,b,c){a>>>=0;b=pe(b>>>0,c>>>0);return Lc[a](...b)}var qc=()=>{},rc=()=>Date.now();function sc(a,b){return H(K(a>>>0,b>>>0))}var tc=()=>{O+=1;throw"unwind";};function uc(){return 4294901760}var vc;
|
||||||
|
vc=()=>performance.timeOrigin+performance.now();var wc=()=>D?require("os").cpus().length:navigator.hardwareConcurrency;function xc(){db("Cannot use emscripten_pc_get_function without -sUSE_OFFSET_CONVERTER");return 0}
|
||||||
|
function yc(a){a>>>=0;var b=w().length;if(a<=b||4294901760<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var f=Math;d=Math.max(a,d);a:{f=(f.min.call(f,4294901760,d+(65536-d%65536)%65536)-l.buffer.byteLength+65535)/65536;try{l.grow(f);t();var g=1;break a}catch(k){}g=void 0}if(g)return!0}return!1}var qe=()=>{db("Cannot use convertFrameToPC (needed by __builtin_return_address) without -sUSE_OFFSET_CONVERTER");return 0},re={},se=a=>{a.forEach(b=>{var c=qe();c&&(re[c]=b)})};
|
||||||
|
function zc(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();se(a);re.Pb=qe();re.ec=a;return re.Pb}function Ac(a,b,c){a>>>=0;b>>>=0;if(re.Pb==a)var d=re.ec;else d=Error().stack.toString().split("\n"),"Error"==d[0]&&d.shift(),se(d);for(var f=3;d[f]&&qe()!=a;)++f;for(a=0;a<c&&d[a+f];++a)z()[b+4*a>>>2>>>0]=qe();return a}
|
||||||
|
var te={},ve=()=>{if(!ue){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:va||"./this.program"},b;for(b in te)void 0===te[b]?delete a[b]:a[b]=te[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);ue=c}return ue},ue;
|
||||||
|
function Bc(a,b){if(E)return P(18,1,a,b);a>>>=0;b>>>=0;var c=0;ve().forEach((d,f)=>{var g=b+c;f=A()[a+4*f>>>2>>>0]=g;for(g=0;g<d.length;++g)e()[f++>>>0]=d.charCodeAt(g);e()[f>>>0]=0;c+=d.length+1});return 0}function Cc(a,b){if(E)return P(19,1,a,b);a>>>=0;b>>>=0;var c=ve();A()[a>>>2>>>0]=c.length;var d=0;c.forEach(f=>d+=f.length+1);A()[b>>>2>>>0]=d;return 0}function Ec(a){return E?P(20,1,a):52}function Fc(a,b,c,d){return E?P(21,1,a,b,c,d):52}function Gc(a,b,c,d){return E?P(22,1,a,b,c,d):70}
|
||||||
|
var we=[null,[],[]];function Hc(a,b,c,d){if(E)return P(23,1,a,b,c,d);b>>>=0;c>>>=0;d>>>=0;for(var f=0,g=0;g<c;g++){var k=A()[b>>>2>>>0],m=A()[b+4>>>2>>>0];b+=8;for(var p=0;p<m;p++){var n=w()[k+p>>>0],r=we[a];0===n||10===n?((1===a?Ea:H)(ld(r,0)),r.length=0):r.push(n)}f+=m}A()[d>>>2>>>0]=f;return 0}var xe=[31,29,31,30,31,30,31,31,30,31,30,31],ye=[31,28,31,30,31,30,31,31,30,31,30,31];function ze(a){var b=Array(md(a)+1);nd(a,b,0,b.length);return b}var Ae=(a,b)=>{e().set(a,b>>>0)};
|
||||||
|
function Jc(a,b,c,d){function f(h,u,y){for(h="number"==typeof h?h.toString():h||"";h.length<u;)h=y[0]+h;return h}function g(h,u){return f(h,u,"0")}function k(h,u){function y(Uc){return 0>Uc?-1:0<Uc?1:0}var aa;0===(aa=y(h.getFullYear()-u.getFullYear()))&&0===(aa=y(h.getMonth()-u.getMonth()))&&(aa=y(h.getDate()-u.getDate()));return aa}function m(h){switch(h.getDay()){case 0:return new Date(h.getFullYear()-1,11,29);case 1:return h;case 2:return new Date(h.getFullYear(),0,3);case 3:return new Date(h.getFullYear(),
|
||||||
|
0,2);case 4:return new Date(h.getFullYear(),0,1);case 5:return new Date(h.getFullYear()-1,11,31);case 6:return new Date(h.getFullYear()-1,11,30)}}function p(h){var u=h.Bb;for(h=new Date((new Date(h.Cb+1900,0,1)).getTime());0<u;){var y=h.getMonth(),aa=(le(h.getFullYear())?xe:ye)[y];if(u>aa-h.getDate())u-=aa-h.getDate()+1,h.setDate(1),11>y?h.setMonth(y+1):(h.setMonth(0),h.setFullYear(h.getFullYear()+1));else{h.setDate(h.getDate()+u);break}}y=new Date(h.getFullYear()+1,0,4);u=m(new Date(h.getFullYear(),
|
||||||
|
0,4));y=m(y);return 0>=k(u,h)?0>=k(y,h)?h.getFullYear()+1:h.getFullYear():h.getFullYear()-1}a>>>=0;b>>>=0;c>>>=0;d>>>=0;var n=A()[d+40>>>2>>>0];d={kc:z()[d>>>2>>>0],jc:z()[d+4>>>2>>>0],Hb:z()[d+8>>>2>>>0],Lb:z()[d+12>>>2>>>0],Ib:z()[d+16>>>2>>>0],Cb:z()[d+20>>>2>>>0],ub:z()[d+24>>>2>>>0],Bb:z()[d+28>>>2>>>0],rc:z()[d+32>>>2>>>0],ic:z()[d+36>>>2>>>0],lc:n?K(n):""};c=K(c);n={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y",
|
||||||
|
"%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var r in n)c=c.replace(new RegExp(r,"g"),n[r]);var v="Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),x="January February March April May June July August September October November December".split(" ");n={"%a":h=>v[h.ub].substring(0,3),"%A":h=>
|
||||||
|
v[h.ub],"%b":h=>x[h.Ib].substring(0,3),"%B":h=>x[h.Ib],"%C":h=>g((h.Cb+1900)/100|0,2),"%d":h=>g(h.Lb,2),"%e":h=>f(h.Lb,2," "),"%g":h=>p(h).toString().substring(2),"%G":p,"%H":h=>g(h.Hb,2),"%I":h=>{h=h.Hb;0==h?h=12:12<h&&(h-=12);return g(h,2)},"%j":h=>{for(var u=0,y=0;y<=h.Ib-1;u+=(le(h.Cb+1900)?xe:ye)[y++]);return g(h.Lb+u,3)},"%m":h=>g(h.Ib+1,2),"%M":h=>g(h.jc,2),"%n":()=>"\n","%p":h=>0<=h.Hb&&12>h.Hb?"AM":"PM","%S":h=>g(h.kc,2),"%t":()=>"\t","%u":h=>h.ub||7,"%U":h=>g(Math.floor((h.Bb+7-h.ub)/7),
|
||||||
|
2),"%V":h=>{var u=Math.floor((h.Bb+7-(h.ub+6)%7)/7);2>=(h.ub+371-h.Bb-2)%7&&u++;if(u)53==u&&(y=(h.ub+371-h.Bb)%7,4==y||3==y&&le(h.Cb)||(u=1));else{u=52;var y=(h.ub+7-h.Bb-1)%7;(4==y||5==y&&le(h.Cb%400-1))&&u++}return g(u,2)},"%w":h=>h.ub,"%W":h=>g(Math.floor((h.Bb+7-(h.ub+6)%7)/7),2),"%y":h=>(h.Cb+1900).toString().substring(2),"%Y":h=>h.Cb+1900,"%z":h=>{h=h.ic;var u=0<=h;h=Math.abs(h)/60;return(u?"+":"-")+String("0000"+(h/60*100+h%60)).slice(-4)},"%Z":h=>h.lc,"%%":()=>"%"};c=c.replace(/%%/g,"\x00\x00");
|
||||||
|
for(r in n)c.includes(r)&&(c=c.replace(new RegExp(r,"g"),n[r](d)));c=c.replace(/\0\0/g,"%");r=ze(c);if(r.length>b)return 0;Ae(r,a);return r.length-1}function Kc(a,b,c,d){return Jc(a>>>0,b>>>0,c>>>0,d>>>0)}E||Zc();for(var Be=Array(256),Ce=0;256>Ce;++Ce)Be[Ce]=String.fromCharCode(Ce);pd=Be;R=B.BindingError=class extends Error{constructor(a){super(a);this.name="BindingError"}};B.InternalError=class extends Error{constructor(a){super(a);this.name="InternalError"}};U.push(0,1,void 0,1,null,1,!0,1,!1,1);
|
||||||
|
B.count_emval_handles=()=>U.length/2-5-vd.length;
|
||||||
|
var Kd=[Ic,Xc,jd,pb,qb,rb,sb,tb,ub,vb,wb,xb,yb,zb,Ab,Bb,lc,mc,Bc,Cc,Ec,Fc,Gc,Hc],kb,Y=function(){function a(c,d){Y=c.exports;Y=Pd();Y=De();Yc.push(Y.ib);Ya.unshift(Y.Ba);Ua=d;cb();return Y}var b=Ja();$a++;if(B.instantiateWasm)try{return B.instantiateWasm(b,a)}catch(c){H(`Module.instantiateWasm callback failed with error: ${c}`),oa(c)}fb||=B.locateFile?eb("ort-wasm-simd-threaded.jsep.wasm")?"ort-wasm-simd-threaded.jsep.wasm":B.locateFile?B.locateFile("ort-wasm-simd-threaded.jsep.wasm",G):G+"ort-wasm-simd-threaded.jsep.wasm":
|
||||||
|
(new URL("ort-wasm-simd-threaded.jsep.wasm",import.meta.url)).href;jb(b,function(c){a(c.instance,c.module)}).catch(oa);return{}}(),Ld=a=>(Ld=Y.Ca)(a),Oa=()=>(Oa=Y.Da)();B._OrtInit=(a,b)=>(B._OrtInit=Y.Ea)(a,b);B._OrtGetLastError=(a,b)=>(B._OrtGetLastError=Y.Fa)(a,b);B._OrtCreateSessionOptions=(a,b,c,d,f,g,k,m,p,n)=>(B._OrtCreateSessionOptions=Y.Ga)(a,b,c,d,f,g,k,m,p,n);B._OrtAppendExecutionProvider=(a,b)=>(B._OrtAppendExecutionProvider=Y.Ha)(a,b);
|
||||||
|
B._OrtAddFreeDimensionOverride=(a,b,c)=>(B._OrtAddFreeDimensionOverride=Y.Ia)(a,b,c);B._OrtAddSessionConfigEntry=(a,b,c)=>(B._OrtAddSessionConfigEntry=Y.Ja)(a,b,c);B._OrtReleaseSessionOptions=a=>(B._OrtReleaseSessionOptions=Y.Ka)(a);B._OrtCreateSession=(a,b,c)=>(B._OrtCreateSession=Y.La)(a,b,c);B._OrtReleaseSession=a=>(B._OrtReleaseSession=Y.Ma)(a);B._OrtGetInputOutputCount=(a,b,c)=>(B._OrtGetInputOutputCount=Y.Na)(a,b,c);B._OrtGetInputName=(a,b)=>(B._OrtGetInputName=Y.Oa)(a,b);
|
||||||
|
B._OrtGetOutputName=(a,b)=>(B._OrtGetOutputName=Y.Pa)(a,b);B._OrtFree=a=>(B._OrtFree=Y.Qa)(a);B._OrtCreateTensor=(a,b,c,d,f,g)=>(B._OrtCreateTensor=Y.Ra)(a,b,c,d,f,g);B._OrtGetTensorData=(a,b,c,d,f)=>(B._OrtGetTensorData=Y.Sa)(a,b,c,d,f);B._OrtReleaseTensor=a=>(B._OrtReleaseTensor=Y.Ta)(a);B._OrtCreateRunOptions=(a,b,c,d)=>(B._OrtCreateRunOptions=Y.Ua)(a,b,c,d);B._OrtAddRunConfigEntry=(a,b,c)=>(B._OrtAddRunConfigEntry=Y.Va)(a,b,c);B._OrtReleaseRunOptions=a=>(B._OrtReleaseRunOptions=Y.Wa)(a);
|
||||||
|
B._OrtCreateBinding=a=>(B._OrtCreateBinding=Y.Xa)(a);B._OrtBindInput=(a,b,c)=>(B._OrtBindInput=Y.Ya)(a,b,c);B._OrtBindOutput=(a,b,c,d)=>(B._OrtBindOutput=Y.Za)(a,b,c,d);B._OrtClearBoundOutputs=a=>(B._OrtClearBoundOutputs=Y._a)(a);B._OrtReleaseBinding=a=>(B._OrtReleaseBinding=Y.$a)(a);B._OrtRunWithBinding=(a,b,c,d,f)=>(B._OrtRunWithBinding=Y.ab)(a,b,c,d,f);B._OrtRun=(a,b,c,d,f,g,k,m)=>(B._OrtRun=Y.bb)(a,b,c,d,f,g,k,m);B._OrtEndProfiling=a=>(B._OrtEndProfiling=Y.cb)(a);
|
||||||
|
B._JsepOutput=(a,b,c)=>(B._JsepOutput=Y.db)(a,b,c);B._JsepGetNodeName=a=>(B._JsepGetNodeName=Y.eb)(a);
|
||||||
|
var Ia=()=>(Ia=Y.fb)(),zd=B._malloc=a=>(zd=B._malloc=Y.gb)(a),X=B._free=a=>(X=B._free=Y.hb)(a),Ka=(a,b,c,d,f,g)=>(Ka=Y.kb)(a,b,c,d,f,g),Sa=()=>(Sa=Y.lb)(),Vc=(a,b,c,d,f)=>(Vc=Y.mb)(a,b,c,d,f),ad=a=>(ad=Y.nb)(a),Qa=a=>(Qa=Y.ob)(a),Id=()=>(Id=Y.pb)(),dd=(a,b)=>(dd=Y.qb)(a,b),Wc=a=>(Wc=Y.rb)(a),Tc=a=>(Tc=Y.sb)(a),Sc=()=>(Sc=Y.tb)(),ed=B.dynCall_ii=(a,b)=>(ed=B.dynCall_ii=Y.vb)(a,b),be=a=>(be=Y.wb)(a),Rd=()=>(Rd=Y.xb)(),ae=a=>(ae=Y.yb)(a),ce=()=>(ce=Y.zb)();B.___start_em_js=882450;B.___stop_em_js=882672;
|
||||||
|
function De(){var a=Y;a=Object.assign({},a);var b=d=>f=>d(f)>>>0,c=d=>()=>d()>>>0;a.Ca=b(a.Ca);a.fb=c(a.fb);a.gb=b(a.gb);a.emscripten_main_runtime_thread_id=c(a.emscripten_main_runtime_thread_id);a.sb=b(a.sb);a.tb=c(a.tb);return a}B.stackSave=()=>Sc();B.stackRestore=a=>Wc(a);B.stackAlloc=a=>Tc(a);B.UTF8ToString=K;B.stringToUTF8=od;B.lengthBytesUTF8=md;var Ee;bb=function Fe(){Ee||Ge();Ee||(bb=Fe)};
|
||||||
|
function Ge(){0<$a||(E?(na(B),E||cd(Ya),startWorker(B)):(cd(Xa),0<$a||Ee||(Ee=!0,B.calledRun=!0,I||(E||cd(Ya),na(B),E||cd(Za)))))}Ge();moduleRtn=pa;
|
||||||
|
|
||||||
|
|
||||||
|
return moduleRtn;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
export default ortWasmThreaded;
|
||||||
|
var isPthread = globalThis.self?.name === 'em-pthread';
|
||||||
|
var isNode = typeof globalThis.process?.versions?.node == 'string';
|
||||||
|
if (isNode) isPthread = (await import('worker_threads')).workerData === 'em-pthread';
|
||||||
|
|
||||||
|
// When running as a pthread, construct a new instance on startup
|
||||||
|
isPthread && ortWasmThreaded();
|
||||||
Binary file not shown.
+81
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
var ortWasmThreaded = (() => {
|
||||||
|
var _scriptName = import.meta.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
async function(moduleArg = {}) {
|
||||||
|
var moduleRtn;
|
||||||
|
|
||||||
|
function aa(){g.buffer!=l.buffer&&m();return l}function n(){g.buffer!=l.buffer&&m();return ba}function r(){g.buffer!=l.buffer&&m();return ca}function u(){g.buffer!=l.buffer&&m();return da}function ea(){g.buffer!=l.buffer&&m();return fa}var w=Object.assign({},moduleArg),ha,x,ia=new Promise((a,b)=>{ha=a;x=b}),ja="object"==typeof window,A="function"==typeof importScripts,B="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,C=A&&"em-pthread"==self.name;
|
||||||
|
if(B){const {createRequire:a}=await import("module");var require=a(import.meta.url),D=require("worker_threads");global.Worker=D.Worker;C=(A=!D.lb)&&"em-pthread"==D.workerData}"use strict";w.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(w.Ua||(w.Ua=new Map)).set(a,b)};w.unmountExternalData=()=>{delete w.Ua};
|
||||||
|
var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,shared:!0})).buffer.constructor,ka=Object.assign({},w),la="./this.program",E=(a,b)=>{throw b;},F="",ma,G,H;
|
||||||
|
if(B){var fs=require("fs"),na=require("path");F=require("url").fileURLToPath(new URL("./",import.meta.url));ma=(a,b)=>{a=oa(a)?new URL(a):na.normalize(a);return fs.readFileSync(a,b?void 0:"utf8")};H=a=>{a=ma(a,!0);a.buffer||(a=new Uint8Array(a));return a};G=(a,b,c,d=!0)=>{a=oa(a)?new URL(a):na.normalize(a);fs.readFile(a,d?void 0:"utf8",(f,h)=>{f?c(f):b(d?h.buffer:h)})};!w.thisProgram&&1<process.argv.length&&(la=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);E=(a,b)=>{process.exitCode=
|
||||||
|
a;throw b;}}else if(ja||A)A?F=self.location.href:"undefined"!=typeof document&&document.currentScript&&(F=document.currentScript.src),_scriptName&&(F=_scriptName),F.startsWith("blob:")?F="":F=F.substr(0,F.replace(/[?#].*/,"").lastIndexOf("/")+1),B||(ma=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.send(null);return b.responseText},A&&(H=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),G=(a,b,c)=>{var d=new XMLHttpRequest;
|
||||||
|
d.open("GET",a,!0);d.responseType="arraybuffer";d.onload=()=>{200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)});B&&"undefined"==typeof performance&&(global.performance=require("perf_hooks").performance);var pa=console.log.bind(console),qa=console.error.bind(console);B&&(pa=(...a)=>fs.writeSync(1,a.join(" ")+"\n"),qa=(...a)=>fs.writeSync(2,a.join(" ")+"\n"));var ra=pa,I=qa;Object.assign(w,ka);ka=null;
|
||||||
|
if(C){var sa;if(B){var ta=D.parentPort;ta.on("message",b=>onmessage({data:b}));Object.assign(globalThis,{self:global,importScripts:()=>{},postMessage:b=>ta.postMessage(b),performance:global.performance||{now:Date.now}})}var ua=!1;I=function(...b){b=b.join(" ");B?fs.writeSync(2,b+"\n"):console.error(b)};self.alert=function(...b){postMessage({Za:"alert",text:b.join(" "),nb:K()})};w.instantiateWasm=(b,c)=>new Promise(d=>{sa=f=>{f=new WebAssembly.Instance(f,va());c(f);d()}});self.onunhandledrejection=
|
||||||
|
b=>{throw b.reason||b;};function a(b){try{var c=b.data,d=c.cmd;if("load"===d){let f=[];self.onmessage=h=>f.push(h);self.startWorker=()=>{postMessage({cmd:"loaded"});for(let h of f)a(h);self.onmessage=a};for(const h of c.handlers)if(!w[h]||w[h].proxy)w[h]=(...k)=>{postMessage({Za:"callHandler",kb:h,args:k})},"print"==h&&(ra=w[h]),"printErr"==h&&(I=w[h]);g=c.wasmMemory;m();sa(c.wasmModule)}else if("run"===d){wa(c.pthread_ptr,0,0,1,0,0);xa(c.pthread_ptr);ya();za();ua||=!0;try{Aa(c.start_routine,c.arg)}catch(f){if("unwind"!=
|
||||||
|
f)throw f;}}else"cancel"===d?K()&&Ba(-1):"setimmediate"!==c.target&&("checkMailbox"===d?ua&&Ca():d&&(I(`worker: received unknown command ${d}`),I(c)))}catch(f){throw Da(),f;}}self.onmessage=a}var L;w.wasmBinary&&(L=w.wasmBinary);var g,Ea,Fa=!1,M,l,ba,ca,da,N,fa;
|
||||||
|
function m(){var a=g.buffer;w.HEAP8=l=new Int8Array(a);w.HEAP16=new Int16Array(a);w.HEAPU8=ba=new Uint8Array(a);w.HEAPU16=new Uint16Array(a);w.HEAP32=ca=new Int32Array(a);w.HEAPU32=da=new Uint32Array(a);w.HEAPF32=new Float32Array(a);w.HEAPF64=fa=new Float64Array(a);w.HEAP64=N=new BigInt64Array(a);w.HEAPU64=new BigUint64Array(a)}
|
||||||
|
if(!C){g=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0});if(!(g.buffer instanceof SharedArrayBuffer))throw I("requested a shared WebAssembly.Memory but the returned buffer is not a SharedArrayBuffer, indicating that while the browser has SharedArrayBuffer it does not have WebAssembly threads support - you may need to set a flag"),B&&I("(on node you may need: --experimental-wasm-threads --experimental-wasm-bulk-memory and/or recent version)"),Error("bad memory");m()}
|
||||||
|
var Ga=[],Ha=[],Ia=[],O=0,Ja=null,P=null;function Ka(){O--;if(0==O&&(null!==Ja&&(clearInterval(Ja),Ja=null),P)){var a=P;P=null;a()}}function La(a){a="Aborted("+a+")";I(a);Fa=!0;M=1;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");x(a);throw a;}var Ma=a=>a.startsWith("data:application/octet-stream;base64,"),oa=a=>a.startsWith("file://"),Na;function Oa(a){if(a==Na&&L)return new Uint8Array(L);if(H)return H(a);throw"both async and sync fetching of the wasm failed";}
|
||||||
|
function Pa(a){if(!L&&(ja||A)){if("function"==typeof fetch&&!oa(a))return fetch(a,{credentials:"same-origin"}).then(b=>{if(!b.ok)throw`failed to load wasm binary file at '${a}'`;return b.arrayBuffer()}).catch(()=>Oa(a));if(G)return new Promise((b,c)=>{G(a,d=>b(new Uint8Array(d)),c)})}return Promise.resolve().then(()=>Oa(a))}function Qa(a,b,c){return Pa(a).then(d=>WebAssembly.instantiate(d,b)).then(c,d=>{I(`failed to asynchronously prepare wasm: ${d}`);La(d)})}
|
||||||
|
function Ra(a,b){var c=Na;return L||"function"!=typeof WebAssembly.instantiateStreaming||Ma(c)||oa(c)||B||"function"!=typeof fetch?Qa(c,a,b):fetch(c,{credentials:"same-origin"}).then(d=>WebAssembly.instantiateStreaming(d,a).then(b,function(f){I(`wasm streaming compile failed: ${f}`);I("falling back to ArrayBuffer instantiation");return Qa(c,a,b)}))}
|
||||||
|
function va(){Sa={j:Ta,b:Ua,E:Va,g:Wa,V:Xa,A:Ya,C:Za,W:$a,T:ab,L:bb,S:cb,o:db,B:eb,y:fb,U:gb,z:hb,_:ib,Z:jb,P:kb,w:lb,F:mb,k:nb,O:xa,Y:ob,I:pb,J:qb,K:rb,G:sb,H:tb,v:ub,q:vb,l:wb,p:xb,e:yb,X:zb,x:Ab,d:Bb,f:Cb,i:Db,u:Eb,t:Fb,s:Gb,Q:Hb,R:Ib,D:Jb,h:Kb,n:Lb,M:Mb,m:Nb,a:g,r:Ob,N:Pb,c:Qb};return{a:Sa}}
|
||||||
|
var Rb={837620:(a,b,c,d,f)=>{if("undefined"==typeof w||!w.Ua)return 1;a=Q(a>>>0);a.startsWith("./")&&(a=a.substring(2));a=w.Ua.get(a);if(!a)return 2;b>>>=0;c>>>=0;d>>>=0;if(b+c>a.byteLength)return 3;try{const h=a.subarray(b,b+c);switch(f){case 0:n().set(h,d>>>0);break;case 1:w.mb(d,h);break;default:return 4}return 0}catch{return 4}},838303:()=>"undefined"!==typeof wasmOffsetConverter};function Ta(){return"undefined"!==typeof wasmOffsetConverter}
|
||||||
|
function Sb(a){this.name="ExitStatus";this.message=`Program terminated with exit(${a})`;this.status=a}
|
||||||
|
var Tb=a=>{a.terminate();a.onmessage=()=>{}},Wb=a=>{0==R.length&&(Ub(),Vb(R[0]));var b=R.pop();if(!b)return 6;S.push(b);T[a.Ra]=b;b.Ra=a.Ra;var c={cmd:"run",start_routine:a.cb,arg:a.ab,pthread_ptr:a.Ra};B&&b.unref();b.postMessage(c,a.ib);return 0},U=0,V=(a,b,...c)=>{for(var d=2*c.length,f=$b(),h=ac(8*d),k=h>>>3,q=0;q<c.length;q++){var y=c[q];"bigint"==typeof y?(N[k+2*q]=1n,N[k+2*q+1]=y):(N[k+2*q]=0n,ea()[k+2*q+1>>>0]=y)}a=bc(a,0,d,h,b);cc(f);return a};
|
||||||
|
function Ob(a){if(C)return V(0,1,a);M=a;if(!(0<U)){for(var b of S)Tb(b);for(b of R)Tb(b);R=[];S=[];T=[];Fa=!0}E(a,new Sb(a))}function dc(a){if(C)return V(1,0,a);Jb(a)}var Jb=a=>{M=a;if(C)throw dc(a),"unwind";Ob(a)},R=[],S=[],ec=[],T={};function fc(){for(var a=w.numThreads-1;a--;)Ub();Ga.unshift(()=>{O++;gc(()=>Ka())})}var ic=a=>{var b=a.Ra;delete T[b];R.push(a);S.splice(S.indexOf(a),1);a.Ra=0;hc(b)};function za(){ec.forEach(a=>a())}
|
||||||
|
var Vb=a=>new Promise(b=>{a.onmessage=h=>{h=h.data;var k=h.cmd;if(h.targetThread&&h.targetThread!=K()){var q=T[h.targetThread];q?q.postMessage(h,h.transferList):I(`Internal error! Worker sent a message "${k}" to target pthread ${h.targetThread}, but that thread no longer exists!`)}else if("checkMailbox"===k)Ca();else if("spawnThread"===k)Wb(h);else if("cleanupThread"===k)ic(T[h.thread]);else if("killThread"===k)h=h.thread,k=T[h],delete T[h],Tb(k),hc(h),S.splice(S.indexOf(k),1),k.Ra=0;else if("cancelThread"===
|
||||||
|
k)T[h.thread].postMessage({cmd:"cancel"});else if("loaded"===k)a.loaded=!0,B&&!a.Ra&&a.unref(),b(a);else if("alert"===k)alert(`Thread ${h.threadId}: ${h.text}`);else if("setimmediate"===h.target)a.postMessage(h);else if("callHandler"===k)w[h.handler](...h.args);else k&&I(`worker sent an unknown command ${k}`)};a.onerror=h=>{I(`${"worker sent an error!"} ${h.filename}:${h.lineno}: ${h.message}`);throw h;};B&&(a.on("message",h=>a.onmessage({data:h})),a.on("error",h=>a.onerror(h)));var c=[],d=[],f;for(f of d)w.hasOwnProperty(f)&&
|
||||||
|
c.push(f);a.postMessage({cmd:"load",handlers:c,wasmMemory:g,wasmModule:Ea})});function gc(a){C?a():Promise.all(R.map(Vb)).then(a)}function Ub(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});R.push(a)}var jc=a=>{for(;0<a.length;)a.shift()(w)},ya=()=>{var a=K(),b=u()[a+52>>>2>>>0];a=u()[a+56>>>2>>>0];kc(b,b-a);cc(b)},lc=[],mc,Aa=(a,b)=>{U=0;var c=lc[a];c||(a>=lc.length&&(lc.length=a+1),lc[a]=c=mc.get(a));a=c(b);0<U?M=a:Ba(a)};
|
||||||
|
class nc{constructor(a){this.Xa=a-24}}var oc=0,pc=0;function Ua(a,b,c){a>>>=0;var d=new nc(a);b>>>=0;c>>>=0;u()[d.Xa+16>>>2>>>0]=0;u()[d.Xa+4>>>2>>>0]=b;u()[d.Xa+8>>>2>>>0]=c;oc=a;pc++;throw oc;}function qc(a,b,c,d){return C?V(2,1,a,b,c,d):Va(a,b,c,d)}
|
||||||
|
function Va(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;if("undefined"==typeof SharedArrayBuffer)return I("Current environment does not support SharedArrayBuffer, pthreads are not available!"),6;var f=[];if(C&&0===f.length)return qc(a,b,c,d);a={cb:c,Ra:a,ab:d,ib:f};return C?(a.Za="spawnThread",postMessage(a,f),0):Wb(a)}
|
||||||
|
var rc="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,sc=(a,b,c)=>{b>>>=0;var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.buffer&&rc)return rc.decode(a.buffer instanceof SharedArrayBuffer?a.slice(b,c):a.subarray(b,c));for(d="";b<c;){var f=a[b++];if(f&128){var h=a[b++]&63;if(192==(f&224))d+=String.fromCharCode((f&31)<<6|h);else{var k=a[b++]&63;f=224==(f&240)?(f&15)<<12|h<<6|k:(f&7)<<18|h<<12|k<<6|a[b++]&63;65536>f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>
|
||||||
|
10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d},Q=(a,b)=>(a>>>=0)?sc(n(),a,b):"";function Wa(a,b,c){return C?V(3,1,a,b,c):0}function Xa(a,b){if(C)return V(4,1,a,b)}
|
||||||
|
var tc=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b},uc=(a,b,c,d)=>{c>>>=0;if(!(0<d))return 0;var f=c;d=c+d-1;for(var h=0;h<a.length;++h){var k=a.charCodeAt(h);if(55296<=k&&57343>=k){var q=a.charCodeAt(++h);k=65536+((k&1023)<<10)|q&1023}if(127>=k){if(c>=d)break;b[c++>>>0]=k}else{if(2047>=k){if(c+1>=d)break;b[c++>>>0]=192|k>>6}else{if(65535>=k){if(c+2>=d)break;b[c++>>>0]=224|k>>12}else{if(c+3>=d)break;b[c++>>>0]=240|k>>
|
||||||
|
18;b[c++>>>0]=128|k>>12&63}b[c++>>>0]=128|k>>6&63}b[c++>>>0]=128|k&63}}b[c>>>0]=0;return c-f},W=(a,b,c)=>uc(a,n(),b,c);function Ya(a,b){if(C)return V(5,1,a,b)}function Za(a,b,c){if(C)return V(6,1,a,b,c)}function $a(a,b,c){return C?V(7,1,a,b,c):0}function ab(a,b){if(C)return V(8,1,a,b)}function bb(a,b,c){if(C)return V(9,1,a,b,c)}function cb(a,b,c,d){if(C)return V(10,1,a,b,c,d)}function db(a,b,c,d){if(C)return V(11,1,a,b,c,d)}function eb(a,b,c,d){if(C)return V(12,1,a,b,c,d)}
|
||||||
|
function fb(a){if(C)return V(13,1,a)}function gb(a,b){if(C)return V(14,1,a,b)}function hb(a,b,c){if(C)return V(15,1,a,b,c)}var ib=()=>{La("")},jb=()=>1;function kb(a){wa(a>>>0,!A,1,!ja,131072,!1);za()}function xa(a){a>>>=0;"function"===typeof Atomics.jb&&(Atomics.jb(r(),a>>>2,a).value.then(Ca),a+=128,Atomics.store(r(),a>>>2,1))}
|
||||||
|
var Ca=()=>{var a=K();if(a&&(xa(a),a=vc,!Fa))try{if(a(),!(0<U))try{C?Ba(M):Jb(M)}catch(b){b instanceof Sb||"unwind"==b||E(1,b)}}catch(b){b instanceof Sb||"unwind"==b||E(1,b)}};function lb(a,b){a>>>=0;a==b>>>0?setTimeout(Ca):C?postMessage({targetThread:a,cmd:"checkMailbox"}):(a=T[a])&&a.postMessage({cmd:"checkMailbox"})}var wc=[];function mb(a,b,c,d,f){b>>>=0;d/=2;wc.length=d;c=f>>>0>>>3;for(f=0;f<d;f++)wc[f]=N[c+2*f]?N[c+2*f+1]:ea()[c+2*f+1>>>0];return(b?Rb[b]:xc[a])(...wc)}
|
||||||
|
function nb(a){a>>>=0;C?postMessage({cmd:"cleanupThread",thread:a}):ic(T[a])}function ob(a){B&&T[a>>>0].ref()}
|
||||||
|
function pb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);r()[b>>>2>>>0]=a.getUTCSeconds();r()[b+4>>>2>>>0]=a.getUTCMinutes();r()[b+8>>>2>>>0]=a.getUTCHours();r()[b+12>>>2>>>0]=a.getUTCDate();r()[b+16>>>2>>>0]=a.getUTCMonth();r()[b+20>>>2>>>0]=a.getUTCFullYear()-1900;r()[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;r()[b+28>>>2>>>0]=a}
|
||||||
|
var X=a=>0===a%4&&(0!==a%100||0===a%400),yc=[0,31,60,91,121,152,182,213,244,274,305,335],zc=[0,31,59,90,120,151,181,212,243,273,304,334];
|
||||||
|
function qb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);r()[b>>>2>>>0]=a.getSeconds();r()[b+4>>>2>>>0]=a.getMinutes();r()[b+8>>>2>>>0]=a.getHours();r()[b+12>>>2>>>0]=a.getDate();r()[b+16>>>2>>>0]=a.getMonth();r()[b+20>>>2>>>0]=a.getFullYear()-1900;r()[b+24>>>2>>>0]=a.getDay();var c=(X(a.getFullYear())?yc:zc)[a.getMonth()]+a.getDate()-1|0;r()[b+28>>>2>>>0]=c;r()[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());c=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
|
||||||
|
var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(c!=d&&a.getTimezoneOffset()==Math.min(d,c))|0;r()[b+32>>>2>>>0]=a}
|
||||||
|
function rb(a){a>>>=0;var b=new Date(r()[a+20>>>2>>>0]+1900,r()[a+16>>>2>>>0],r()[a+12>>>2>>>0],r()[a+8>>>2>>>0],r()[a+4>>>2>>>0],r()[a>>>2>>>0],0),c=r()[a+32>>>2>>>0],d=b.getTimezoneOffset(),f=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),h=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),k=Math.min(h,f);0>c?r()[a+32>>>2>>>0]=Number(f!=h&&k==d):0<c!=(k==d)&&(f=Math.max(h,f),b.setTime(b.getTime()+6E4*((0<c?k:f)-d)));r()[a+24>>>2>>>0]=b.getDay();c=(X(b.getFullYear())?yc:zc)[b.getMonth()]+
|
||||||
|
b.getDate()-1|0;r()[a+28>>>2>>>0]=c;r()[a>>>2>>>0]=b.getSeconds();r()[a+4>>>2>>>0]=b.getMinutes();r()[a+8>>>2>>>0]=b.getHours();r()[a+12>>>2>>>0]=b.getDate();r()[a+16>>>2>>>0]=b.getMonth();r()[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function sb(a,b,c,d,f,h,k){return C?V(16,1,a,b,c,d,f,h,k):-52}function tb(a,b,c,d,f,h){if(C)return V(17,1,a,b,c,d,f,h)}
|
||||||
|
function ub(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;var f=(new Date).getFullYear(),h=new Date(f,0,1),k=new Date(f,6,1);f=h.getTimezoneOffset();var q=k.getTimezoneOffset(),y=Math.max(f,q);u()[a>>>2>>>0]=60*y;r()[b>>>2>>>0]=Number(f!=q);a=v=>v.toLocaleTimeString(void 0,{hour12:!1,timeZoneName:"short"}).split(" ")[1];h=a(h);k=a(k);q<f?(W(h,c,17),W(k,d,17)):(W(h,d,17),W(k,c,17))}var Ac=[];
|
||||||
|
function vb(a,b,c){a>>>=0;b>>>=0;c>>>=0;Ac.length=0;for(var d;d=n()[b++>>>0];){var f=105!=d;f&=112!=d;c+=f&&c%8?4:0;Ac.push(112==d?u()[c>>>2>>>0]:106==d?N[c>>>3]:105==d?r()[c>>>2>>>0]:ea()[c>>>3>>>0]);c+=f?8:4}return Rb[a](...Ac)}var wb=()=>{},xb=()=>Date.now();function yb(a,b){return I(Q(a>>>0,b>>>0))}var zb=()=>{U+=1;throw"unwind";};function Ab(){return 4294901760}var Bb;Bb=()=>performance.timeOrigin+performance.now();var Cb=()=>B?require("os").cpus().length:navigator.hardwareConcurrency;
|
||||||
|
function Db(){La("Cannot use emscripten_pc_get_function without -sUSE_OFFSET_CONVERTER");return 0}function Eb(a){a>>>=0;var b=n().length;if(a<=b||4294901760<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var f=Math;d=Math.max(a,d);a:{f=(f.min.call(f,4294901760,d+(65536-d%65536)%65536)-g.buffer.byteLength+65535)/65536;try{g.grow(f);m();var h=1;break a}catch(k){}h=void 0}if(h)return!0}return!1}
|
||||||
|
var Bc=()=>{La("Cannot use convertFrameToPC (needed by __builtin_return_address) without -sUSE_OFFSET_CONVERTER");return 0},Y={},Cc=a=>{a.forEach(b=>{var c=Bc();c&&(Y[c]=b)})};function Fb(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();Cc(a);Y.$a=Bc();Y.bb=a;return Y.$a}
|
||||||
|
function Gb(a,b,c){a>>>=0;b>>>=0;if(Y.$a==a)var d=Y.bb;else d=Error().stack.toString().split("\n"),"Error"==d[0]&&d.shift(),Cc(d);for(var f=3;d[f]&&Bc()!=a;)++f;for(a=0;a<c&&d[a+f];++a)r()[b+4*a>>>2>>>0]=Bc();return a}
|
||||||
|
var Dc={},Fc=()=>{if(!Ec){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:la||"./this.program"},b;for(b in Dc)void 0===Dc[b]?delete a[b]:a[b]=Dc[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);Ec=c}return Ec},Ec;
|
||||||
|
function Hb(a,b){if(C)return V(18,1,a,b);a>>>=0;b>>>=0;var c=0;Fc().forEach((d,f)=>{var h=b+c;f=u()[a+4*f>>>2>>>0]=h;for(h=0;h<d.length;++h)aa()[f++>>>0]=d.charCodeAt(h);aa()[f>>>0]=0;c+=d.length+1});return 0}function Ib(a,b){if(C)return V(19,1,a,b);a>>>=0;b>>>=0;var c=Fc();u()[a>>>2>>>0]=c.length;var d=0;c.forEach(f=>d+=f.length+1);u()[b>>>2>>>0]=d;return 0}function Kb(a){return C?V(20,1,a):52}function Lb(a,b,c,d){return C?V(21,1,a,b,c,d):52}function Mb(a,b,c,d){return C?V(22,1,a,b,c,d):70}
|
||||||
|
var Gc=[null,[],[]];function Nb(a,b,c,d){if(C)return V(23,1,a,b,c,d);b>>>=0;c>>>=0;d>>>=0;for(var f=0,h=0;h<c;h++){var k=u()[b>>>2>>>0],q=u()[b+4>>>2>>>0];b+=8;for(var y=0;y<q;y++){var v=n()[k+y>>>0],z=Gc[a];0===v||10===v?((1===a?ra:I)(sc(z,0)),z.length=0):z.push(v)}f+=q}u()[d>>>2>>>0]=f;return 0}var Hc=[31,29,31,30,31,30,31,31,30,31,30,31],Ic=[31,28,31,30,31,30,31,31,30,31,30,31];function Jc(a){var b=Array(tc(a)+1);uc(a,b,0,b.length);return b}var Kc=(a,b)=>{aa().set(a,b>>>0)};
|
||||||
|
function Pb(a,b,c,d){function f(e,p,t){for(e="number"==typeof e?e.toString():e||"";e.length<p;)e=t[0]+e;return e}function h(e,p){return f(e,p,"0")}function k(e,p){function t(Xb){return 0>Xb?-1:0<Xb?1:0}var J;0===(J=t(e.getFullYear()-p.getFullYear()))&&0===(J=t(e.getMonth()-p.getMonth()))&&(J=t(e.getDate()-p.getDate()));return J}function q(e){switch(e.getDay()){case 0:return new Date(e.getFullYear()-1,11,29);case 1:return e;case 2:return new Date(e.getFullYear(),0,3);case 3:return new Date(e.getFullYear(),
|
||||||
|
0,2);case 4:return new Date(e.getFullYear(),0,1);case 5:return new Date(e.getFullYear()-1,11,31);case 6:return new Date(e.getFullYear()-1,11,30)}}function y(e){var p=e.Sa;for(e=new Date((new Date(e.Ta+1900,0,1)).getTime());0<p;){var t=e.getMonth(),J=(X(e.getFullYear())?Hc:Ic)[t];if(p>J-e.getDate())p-=J-e.getDate()+1,e.setDate(1),11>t?e.setMonth(t+1):(e.setMonth(0),e.setFullYear(e.getFullYear()+1));else{e.setDate(e.getDate()+p);break}}t=new Date(e.getFullYear()+1,0,4);p=q(new Date(e.getFullYear(),
|
||||||
|
0,4));t=q(t);return 0>=k(p,e)?0>=k(t,e)?e.getFullYear()+1:e.getFullYear():e.getFullYear()-1}a>>>=0;b>>>=0;c>>>=0;d>>>=0;var v=u()[d+40>>>2>>>0];d={gb:r()[d>>>2>>>0],fb:r()[d+4>>>2>>>0],Va:r()[d+8>>>2>>>0],Ya:r()[d+12>>>2>>>0],Wa:r()[d+16>>>2>>>0],Ta:r()[d+20>>>2>>>0],Qa:r()[d+24>>>2>>>0],Sa:r()[d+28>>>2>>>0],ob:r()[d+32>>>2>>>0],eb:r()[d+36>>>2>>>0],hb:v?Q(v):""};c=Q(c);v={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y",
|
||||||
|
"%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var z in v)c=c.replace(new RegExp(z,"g"),v[z]);var Yb="Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),Zb="January February March April May June July August September October November December".split(" ");v={"%a":e=>Yb[e.Qa].substring(0,3),
|
||||||
|
"%A":e=>Yb[e.Qa],"%b":e=>Zb[e.Wa].substring(0,3),"%B":e=>Zb[e.Wa],"%C":e=>h((e.Ta+1900)/100|0,2),"%d":e=>h(e.Ya,2),"%e":e=>f(e.Ya,2," "),"%g":e=>y(e).toString().substring(2),"%G":y,"%H":e=>h(e.Va,2),"%I":e=>{e=e.Va;0==e?e=12:12<e&&(e-=12);return h(e,2)},"%j":e=>{for(var p=0,t=0;t<=e.Wa-1;p+=(X(e.Ta+1900)?Hc:Ic)[t++]);return h(e.Ya+p,3)},"%m":e=>h(e.Wa+1,2),"%M":e=>h(e.fb,2),"%n":()=>"\n","%p":e=>0<=e.Va&&12>e.Va?"AM":"PM","%S":e=>h(e.gb,2),"%t":()=>"\t","%u":e=>e.Qa||7,"%U":e=>h(Math.floor((e.Sa+
|
||||||
|
7-e.Qa)/7),2),"%V":e=>{var p=Math.floor((e.Sa+7-(e.Qa+6)%7)/7);2>=(e.Qa+371-e.Sa-2)%7&&p++;if(p)53==p&&(t=(e.Qa+371-e.Sa)%7,4==t||3==t&&X(e.Ta)||(p=1));else{p=52;var t=(e.Qa+7-e.Sa-1)%7;(4==t||5==t&&X(e.Ta%400-1))&&p++}return h(p,2)},"%w":e=>e.Qa,"%W":e=>h(Math.floor((e.Sa+7-(e.Qa+6)%7)/7),2),"%y":e=>(e.Ta+1900).toString().substring(2),"%Y":e=>e.Ta+1900,"%z":e=>{e=e.eb;var p=0<=e;e=Math.abs(e)/60;return(p?"+":"-")+String("0000"+(e/60*100+e%60)).slice(-4)},"%Z":e=>e.hb,"%%":()=>"%"};c=c.replace(/%%/g,
|
||||||
|
"\x00\x00");for(z in v)c.includes(z)&&(c=c.replace(new RegExp(z,"g"),v[z](d)));c=c.replace(/\0\0/g,"%");z=Jc(c);if(z.length>b)return 0;Kc(z,a);return z.length-1}function Qb(a,b,c,d){return Pb(a>>>0,b>>>0,c>>>0,d>>>0)}C||fc();
|
||||||
|
var xc=[Ob,dc,qc,Wa,Xa,Ya,Za,$a,ab,bb,cb,db,eb,fb,gb,hb,sb,tb,Hb,Ib,Kb,Lb,Mb,Nb],Sa,Z=function(){function a(c,d){Z=c.exports;Z=Lc();ec.push(Z.Ea);mc=Z.Fa;Ha.unshift(Z.$);Ea=d;Ka();return Z}var b=va();O++;if(w.instantiateWasm)try{return w.instantiateWasm(b,a)}catch(c){I(`Module.instantiateWasm callback failed with error: ${c}`),x(c)}Na||=w.locateFile?Ma("ort-wasm-simd-threaded.wasm")?"ort-wasm-simd-threaded.wasm":w.locateFile?w.locateFile("ort-wasm-simd-threaded.wasm",F):F+"ort-wasm-simd-threaded.wasm":
|
||||||
|
(new URL("ort-wasm-simd-threaded.wasm",import.meta.url)).href;Ra(b,function(c){a(c.instance,c.module)}).catch(x);return{}}();w._OrtInit=(a,b)=>(w._OrtInit=Z.aa)(a,b);w._OrtGetLastError=(a,b)=>(w._OrtGetLastError=Z.ba)(a,b);w._OrtCreateSessionOptions=(a,b,c,d,f,h,k,q,y,v)=>(w._OrtCreateSessionOptions=Z.ca)(a,b,c,d,f,h,k,q,y,v);w._OrtAppendExecutionProvider=(a,b)=>(w._OrtAppendExecutionProvider=Z.da)(a,b);w._OrtAddFreeDimensionOverride=(a,b,c)=>(w._OrtAddFreeDimensionOverride=Z.ea)(a,b,c);
|
||||||
|
w._OrtAddSessionConfigEntry=(a,b,c)=>(w._OrtAddSessionConfigEntry=Z.fa)(a,b,c);w._OrtReleaseSessionOptions=a=>(w._OrtReleaseSessionOptions=Z.ga)(a);w._OrtCreateSession=(a,b,c)=>(w._OrtCreateSession=Z.ha)(a,b,c);w._OrtReleaseSession=a=>(w._OrtReleaseSession=Z.ia)(a);w._OrtGetInputOutputCount=(a,b,c)=>(w._OrtGetInputOutputCount=Z.ja)(a,b,c);w._OrtGetInputName=(a,b)=>(w._OrtGetInputName=Z.ka)(a,b);w._OrtGetOutputName=(a,b)=>(w._OrtGetOutputName=Z.la)(a,b);w._OrtFree=a=>(w._OrtFree=Z.ma)(a);
|
||||||
|
w._OrtCreateTensor=(a,b,c,d,f,h)=>(w._OrtCreateTensor=Z.na)(a,b,c,d,f,h);w._OrtGetTensorData=(a,b,c,d,f)=>(w._OrtGetTensorData=Z.oa)(a,b,c,d,f);w._OrtReleaseTensor=a=>(w._OrtReleaseTensor=Z.pa)(a);w._OrtCreateRunOptions=(a,b,c,d)=>(w._OrtCreateRunOptions=Z.qa)(a,b,c,d);w._OrtAddRunConfigEntry=(a,b,c)=>(w._OrtAddRunConfigEntry=Z.ra)(a,b,c);w._OrtReleaseRunOptions=a=>(w._OrtReleaseRunOptions=Z.sa)(a);w._OrtCreateBinding=a=>(w._OrtCreateBinding=Z.ta)(a);
|
||||||
|
w._OrtBindInput=(a,b,c)=>(w._OrtBindInput=Z.ua)(a,b,c);w._OrtBindOutput=(a,b,c,d)=>(w._OrtBindOutput=Z.va)(a,b,c,d);w._OrtClearBoundOutputs=a=>(w._OrtClearBoundOutputs=Z.wa)(a);w._OrtReleaseBinding=a=>(w._OrtReleaseBinding=Z.xa)(a);w._OrtRunWithBinding=(a,b,c,d,f)=>(w._OrtRunWithBinding=Z.ya)(a,b,c,d,f);w._OrtRun=(a,b,c,d,f,h,k,q)=>(w._OrtRun=Z.za)(a,b,c,d,f,h,k,q);w._OrtEndProfiling=a=>(w._OrtEndProfiling=Z.Aa)(a);var K=()=>(K=Z.Ba)();w._malloc=a=>(w._malloc=Z.Ca)(a);w._free=a=>(w._free=Z.Da)(a);
|
||||||
|
var wa=(a,b,c,d,f,h)=>(wa=Z.Ga)(a,b,c,d,f,h),Da=()=>(Da=Z.Ha)(),bc=(a,b,c,d,f)=>(bc=Z.Ia)(a,b,c,d,f),hc=a=>(hc=Z.Ja)(a),Ba=a=>(Ba=Z.Ka)(a),vc=()=>(vc=Z.La)(),kc=(a,b)=>(kc=Z.Ma)(a,b),cc=a=>(cc=Z.Na)(a),ac=a=>(ac=Z.Oa)(a),$b=()=>($b=Z.Pa)();w.___start_em_js=838360;w.___stop_em_js=838421;
|
||||||
|
function Lc(){var a=Z;a=Object.assign({},a);var b=d=>()=>d()>>>0,c=d=>f=>d(f)>>>0;a.Ba=b(a.Ba);a.Ca=c(a.Ca);a.emscripten_main_runtime_thread_id=b(a.emscripten_main_runtime_thread_id);a.Oa=c(a.Oa);a.Pa=b(a.Pa);return a}w.stackSave=()=>$b();w.stackRestore=a=>cc(a);w.stackAlloc=a=>ac(a);w.UTF8ToString=Q;w.stringToUTF8=W;w.lengthBytesUTF8=tc;var Mc;P=function Nc(){Mc||Oc();Mc||(P=Nc)};
|
||||||
|
function Oc(){0<O||(C?(ha(w),C||jc(Ha),startWorker(w)):(jc(Ga),0<O||Mc||(Mc=!0,w.calledRun=!0,Fa||(C||jc(Ha),ha(w),C||jc(Ia)))))}Oc();moduleRtn=ia;
|
||||||
|
|
||||||
|
|
||||||
|
return moduleRtn;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
export default ortWasmThreaded;
|
||||||
|
var isPthread = globalThis.self?.name === 'em-pthread';
|
||||||
|
var isNode = typeof globalThis.process?.versions?.node == 'string';
|
||||||
|
if (isNode) isPthread = (await import('worker_threads')).workerData === 'em-pthread';
|
||||||
|
|
||||||
|
// When running as a pthread, construct a new instance on startup
|
||||||
|
isPthread && ortWasmThreaded();
|
||||||
BIN
Binary file not shown.
Vendored
+1827
File diff suppressed because one or more lines are too long
Vendored
+2644
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user