ml: 按运行手册审查修订——头分目录不互相覆盖(ml/runs/<CFG>、selected_ckpt/CKPT);统一 $PYTHON(Windows Git Bash 可跑);gen 按实测样本/任务自动补量;R10 改终态判据(≥5k 样本/s≈60k 步≤3.5h);m5 纯策略/仿真安全层双口径;无老师题定长 6 s 口径;APG 展开随批时长、端点+中点铰链、余量 3 mm(投影同步);挖错池只用本轮;门禁加 R7;bench 加 R8 对抗集与刹停率、--nc-n/--route-n/--r8-n;新增 check_r2 与 acceptance;export 默认 fp32;出题分块防 OOM;val 为空保护

This commit is contained in:
Hark
2026-10-18 06:40:24 +08:00
committed by cbb
parent 8800a68cad
commit f8cdabb781
18 changed files with 493 additions and 156 deletions
+94 -50
View File
@@ -3,32 +3,34 @@
目标:在你的机器上从零跑出 Transformer 避障策略,并在查看器里直接用上。
方案依据见 `docs/Transformer轨迹拟合训练方案.md`。所有命令在**仓库根目录**执行。
最省事的跑法是按阶段执行 `bash ml/pipeline.sh <阶段>`,每一阶段完成后看一眼本手册对应小节的「检查」再进下一步。
按阶段执行 `bash ml/pipeline.sh <阶段>`,每一阶段完成后看本手册对应小节的「检查」再进下一步。
| 阶段 | 命令 | 预计耗时(16 核 CPU + RTX 3090) | 产出 |
| 阶段 | 命令 | 预计耗时(28 核 CPU + RTX 3090) | 产出 |
|---|---|---|---|
| 1 环境 | `bash ml/pipeline.sh setup` | 10 分钟 | 依赖 |
| 2 对拍 | `bash ml/pipeline.sh kin` | 2 分钟 | `ml/assets/kin.json`、对拍 PASS |
| 2 对拍 | `bash ml/pipeline.sh kin` | 1 分钟(实测 7.6 s 对拍) | `ml/assets/kin.json`、对拍 PASS |
| 3 老师门禁 | `bash ml/pipeline.sh gate1` | 25–30 分钟(单核) | `ml/data/consistency.json` |
| 4 生成数据 | `bash ml/pipeline.sh gen` | 1–2 小时;加共识过滤 5–9 小时 | `ml/data/shard_*.jsonl`、`stats.json` |
| 5 打包样本 | `bash ml/pipeline.sh prep` | 15–30 分钟 | `ml/data/samples/` |
| 6 预训练 | `bash ml/pipeline.sh train` | 先 2000 步测速,正式约 1.5–6 小时 | `ml/runs/base/best.pt` |
| 7 M5 门禁 | `bash ml/pipeline.sh m5` | 10 分钟 | `ml/runs/m5_gate.json` |
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 以前 100 步测速为准(预计 2–4 小时) | `ml/runs/apg/best.pt` |
| 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` | 以前 100 步速度为准(预计 2–4 小时) | `ml/runs/apg/best.pt` |
| 9 挖错循环 | `bash ml/pipeline.sh mine` | 每轮约 1–2 小时,3–6 轮 | `ml/runs/current.pt` |
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx` |
| 11 评测 | `bash ml/pipeline.sh bench` | 约 1 小时(Node CPU,1070 个任务) | `ml/runs/policy_bench.json` |
| 12 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) |
| 11 部署评测 | `bash ml/pipeline.sh bench` | 约 1 小时(Node CPU,1120 个任务) | `ml/runs/policy_bench.json` |
| 12 验收汇总 | `bash ml/pipeline.sh accept` | 10–20 分钟 | `ml/runs/acceptance.json` |
| 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
耗时除第 4 步外均为估算,以每步首次输出的速度为准。
第 2/4/5/6 步耗时为 RTX 3090 + 28 核实测换算,其余为估算,以每步首次输出的速度为准。
---
## 1. 环境
- Linux 或 WSL2;NVIDIA 驱动 + CUDA(`nvidia-smi` 能看到 3090)
- Linux / WSL2,或 **Windows 原生 + Git Bash**(实测第 2/4/5/6 步与闭环仿真可跑);NVIDIA 驱动 + CUDA(`nvidia-smi` 能看到 3090)
- Node ≥ 18,Python 3.10–3.12
- 先按 CUDA 版本装 PyTorch(https://pytorch.org ,例:`pip install torch --index-url https://download.pytorch.org/whl/cu124`),再:
- 脚本统一用 `$PYTHON`(默认 `python`)。Linux 上没有 `python` 命令时:`export PYTHON=python3`。Windows 不要用 `python3`(那是 WindowsApps 假壳)
- 先按 CUDA 版本装 PyTorch(https://pytorch.org ),再:
```bash
git pull
@@ -50,59 +52,88 @@ bash ml/pipeline.sh kin
bash ml/pipeline.sh gate1
```
看 `consistentByGateRule`:≥ 0.90 直接生成;< 0.90 时第 4 步会自动加 `--consensus 4`(过滤与邻居绕法不一致的样本,生成成本约 5 倍)。也可手动用 `CONSENSUS=0/1` 覆盖。
看 `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=16 CONSENSUS=1 bash ml/pipeline.sh gen
# 自定义:N_TASKS=50000 NPROC=28 CONSENSUS=1 TARGET_SAMPLES=1700000 bash ml/pipeline.sh gen
```
每条约 2 秒/核(不共识)。检查 `ml/data/stats.json`:`keepRate` 应在 0.95 左右,`byCatMode` 里 C 类(贴立柱)有足够样本,`projectedSamplesFor50k` 约 180 万。
不共识约 1.6 秒/条/核;共识约 8 秒/条/核。生成完自动统计;**样本数不足 `TARGET_SAMPLES`(默认 170 万)时按实测「样本/任务」自动补生成**(最多补 2 次),不用手动加 `N_TASKS`。
## 5. 打包训练样本
检查 `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(p10 预计约 3) | 同 | p10 < 2.5 mm:发我(APG 与投影的 3 mm 余量要随之下调) |
| `byCatMode` | C 类(贴立柱)各模式都有数千条 | 同 | — |
## 5. 打包训练样本 + R2 检查
```bash
bash ml/pipeline.sh prep
```
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算)。检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万。
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算),30% 样本加 σ = 1° 噪声增广。
检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万(5% 任务划为 val)。
随后自动跑 R2 检查(`ml/data/r2.json`):近邻样本前 3 步标签差 `label_std_mean < 0.10`(10% 步长)为通过;不通过先发我,不要直接训练。
## 6. 预训练
```bash
bash ml/pipeline.sh train
bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/bench_base/、ml/runs/base/
```
先跑 2000 步测吞吐(`ml/runs/bench/log.jsonl` 的 `samples_per_s`),再正式训练到 `configs/base.json` 的 60000 步或闭环到达率连续 5 次不升为止。
训练中每 2000 步打印一行 `EVAL {...}`:闭环(部署节奏)`success` 到达率、`collision_rate`、`time_ok_1p3`。
显存不够就把 `configs/base.json` 的 `batch` 改成 512。中断后加 `--resume ml/runs/base/last.pt` 续训:
先跑 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)
- 显存不够把 `batch` 改成 512。中断后续训(日志追加;新训练会覆盖旧日志):
```bash
python -m ml.train --samples ml/data/samples --out ml/runs/base --resume ml/runs/base/last.pt
```
## 7. M5 门禁:MeanFlow 头是否可用
## 7. M5 门禁:选定动作头
```bash
bash ml/pipeline.sh m5
```
在 1000 个贴立柱任务上关安全层闭环运行。穿柱率 ≤ 2% 通过;不通过(或训练日志出现「训练发散」)则换预定回退的 WTA K=8 头重训:
1000 个贴立柱任务(无老师题,定长 6 s 时限)跑两遍,结果在 `ml/runs/m5_gate_base.json`:
- `policy_only`:关安全层的纯策略指标。**门禁只看这一项:穿柱率 ≤ 2% 通过**,通过后把该模型记为选定模型(`ml/runs/selected_ckpt`),后续阶段自动从它开始
- `with_sim_safety`:仿真里的近似安全层(候选逐步干涉就换下一条)。只作参考
三种口径的关系:M5 是纯策略能力;仿真安全层是近似;**部署能不能过只看第 11 步**(与浏览器同一份运行时,含投影、距离认证、刹停、预算、换向锁)。三者互不替代。
不通过(或训练日志出现「训练发散」)则换 WTA K=8 头。WTA 的产物在独立目录,不会覆盖 MeanFlow:
```bash
CFG=ml/configs/wta.json bash ml/pipeline.sh train && bash ml/pipeline.sh m5
CFG=ml/configs/wta.json bash ml/pipeline.sh train # → ml/runs/bench_wta/、ml/runs/wta/
CFG=ml/configs/wta.json bash ml/pipeline.sh m5 # → ml/runs/m5_gate_wta.json;通过则选定 wta
```
需要强制指定后续阶段的起点模型时用 `CKPT=路径 bash ml/pipeline.sh apg`。
## 8. APG 可微微调
```bash
bash ml/pipeline.sh apg
```
按部署节奏可微展开策略,直接优化到达、碰撞余量(5 mm)、平滑与行程,并保留 BC 锚定。每 500 步闭环验证,按到达率存 `best.pt`,完成后复制为 `ml/runs/current.pt`。
显存不够把 `configs/apg.json` 的 `batch` 降到 64。
按部署节奏(每次推理提交 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 − 间隙)`。3 mm ≈ 老师轨迹最小间隙的 p10(p50 4.4 mm),避免碰撞项与 BC 标签对抗;运行时投影余量同为 3 mm
- 显存不够把 `configs/apg.json` 的 `batch` 降到 64,或把 `seg_plans` 从 10 降到 5
## 9. 挖错循环
@@ -110,18 +141,21 @@ bash ml/pipeline.sh apg
bash ml/pipeline.sh mine # 默认 6 轮;ROUNDS=3 APG_STEPS=1000 MINE_N=50000 可调
```
每轮:5 万道新题关安全层作答 → 错题分类 E1–E5 → 错题与邻居进 APG 采样池 → E2(局部极小)请老师补少量路径 → APG 微调 → 门禁集比较,不达标自动回滚。连续 2 轮提升 < 0.5 个百分点自动停止。每轮报告在 `ml/runs/mine/r*/report.md` 与 `gate.json`。
每轮: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 # fp16 ONNX + meta.json → webapp/models/
bash ml/pipeline.sh export # fp32 ONNX + meta.json → webapp/models/(约 20 MB)
```
fp16 转换报错时去掉 `--fp16` 手动导出 fp32(约 20 MB,同样可用):
默认 fp32:部署走 wasm 与 Node CPU,fp16 在 CPU 后端上不保证更快。只有第 13 步用 `&ep=webgpu` 实测 `__POLICY.latency()` 明显受益时,才另导出 fp16 做对比:
```bash
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx
pip install onnxconverter-common
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy_fp16.onnx --fp16
```
## 11. 部署形态评测(策略 + 安全层)
@@ -130,11 +164,20 @@ python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_poli
bash ml/pipeline.sh bench
```
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、一次性贴立柱扩展集 1000 例。
验收看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 必须为 0;`near_column_*` 的 `ok` 30/30;`near_column_1000` 0 失败为目标。
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 12 步实测。
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、R8 对抗 50 例(J1 起终差 ≥ 140°、终点贴立柱、两侧都要绕远)、一次性贴立柱扩展集 1000 例。各集合数量可用 `--nc-n` / `--route-n` / `--r8-n` 单独调。
## 12. 在查看器里使用
看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 为 0;`near_column_*` 与 `route40` 全部 `ok`;`near_column_1000` 0 失败;`brakeRate` < 0.02;`r8_adversarial` 的 `recoveryRate` ≥ 0.95、`livelock` = 0。
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 13 步实测。
## 12. 验收汇总
```bash
bash ml/pipeline.sh accept
```
自己补跑 val 全部任务的闭环(开仿真安全层):可达任务到达率 ≥ 98%、D 类停靠距离 ≤ 老师 + 5 mm、起步失败 = 0;再读第 5–11 步产物核对 R2、R7、R8、R10、M5 与部署形态各项。逐项打印「通过 / 未过」,结果在 `ml/runs/acceptance.json`。
## 13. 在查看器里使用
```bash
python -m arm.viewer.build_viewer # 若 webapp/generated/00_data.js 还没生成(查看器原有构建步骤)
@@ -147,24 +190,23 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
- 工具偏移 ≠ 0 时自动交回原规划器
- 离线环境:把 onnxruntime-web 的 `ort.min.js` 与 wasm 文件放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`
## 13. 回传给我
每完成一步把对应文件发我即可,我据此判断下一步或改代码:
## 14. 回传给我
| 步骤 | 文件 |
|---|---|
| 2 | 对拍输出(终端 JSON) |
| 3–5 | `ml/data/consistency.json`、`ml/data/stats.json`、`ml/data/samples/index.json` |
| 6 | `ml/runs/bench/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
| 7 | `ml/runs/m5_gate.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–12 | `ml/runs/policy_bench.json`、浏览器 `__POLICY.latency()` 输出 |
| 11–13 | `ml/runs/policy_bench.json`、`ml/runs/acceptance.json`、浏览器 `__POLICY.latency()` 输出 |
## 常见问题
- **指纹不符**:`collide.json` 变了。依次重跑第 2、4、5 步并重训。
- **训练发散**(loss 出现 nan/inf,自动停在 `diverged.pt`):先把 `lr` 降到 5e-4 重试一次,仍发散就走 WTA 回退(第 7 步)。
- **显存不足**:降 `batch`;APG 还可把 `seg_plans` 从 10 降到 5。
- **显存/内存不足**:降 `batch`;APG 还可把 `seg_plans` 降到 5。出题与间隙计算已分块(每块 512 个位形)。
- **小样本摸底**:几百条任务时 val 可能为空,训练会提示并跳过闭环验证。
- **浏览器只有 1 线程**:没走 `ml/serve.py`(缺 COOP/COEP 头)。
- **查看器徽标「策略模式不可用」**:看控制台报错,多为模型文件缺失、指纹或 TCP 不符。
@@ -172,19 +214,21 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
```
ml/
pipeline.sh 一条龙阶段脚本
gen/ 老师数据:sampler / teacher(确定性 PRM A*)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
prep/ export_kin.cjs(→ assets/kin.json)、build_dataset.cjs(→ samples/*.npy)、run_prep.sh
pipeline.sh 一条龙阶段脚本(环境变量见文件头)
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、loop.sh
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)
mining/ tasks.py(无老师出题)、mine_round.py、gate.py(含 R7)、loop.sh
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、acceptance.py(验收汇总)
toy/toy_bimodal.py 2D 双模态对照实验(动作头选型依据)
export_onnx.py serve.py
configs/ base.json(MeanFlow)、wta.json(回退)、apg.json
webapp/js/ik-policy.js 运行时:特征、距离下界认证、刹停、投影、精修、Runner(浏览器与 Node 共用)
webapp/js/ik-policy-glue.js 查看器接入(?policy=1)
tests/policy_bench.cjs 部署形态评测
tests/policy_bench.cjs 部署形态评测(nc / route / r8 / nc1000)
```
+15 -8
View File
@@ -2,7 +2,10 @@
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 损失)。"""
损失:段末到达项 + Σ碰撞铰链 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 5 → 3 mm:老师轨迹自身最小间隙 p50 4.4 / p90 5.9 mm,5 mm 会让约一半 BC 标签与碰撞项对抗;3 mm ≈ 老师 p10。"""
import argparse, json, math, os, random, time
import torch
@@ -45,7 +48,7 @@ def main():
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)
log = open(os.path.join(a.out, 'log.jsonl'), 'a')
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']
@@ -62,9 +65,12 @@ def main():
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}
nseg = math.ceil(C['plans'] / C['seg_plans'])
# 展开长度随本批任务时长:覆盖本批 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(C['plans']):
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]
@@ -92,15 +98,16 @@ def main():
for k in range(3):
d = queue[:, k]
qp = q; q = kin.clamp_box(q + d)
G = kin.gaps(q, M)
# 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致)
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M)
hinge = torch.relu(M - G['gap'])
col = torch.zeros(B, device=dev).index_add(0, G['b'], hinge)
col = torch.zeros(2 * B, device=dev).index_add(0, G['b'], hinge).view(2, B).sum(0)
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 == C['plans'] - 1:
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()
@@ -122,7 +129,7 @@ def main():
opt.step()
with torch.no_grad():
e, oe = goal_err(kin, q, T, R)
rec = {'it': it, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(),
rec = {'it': it, '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}
if it % 10 == 0: print(json.dumps(rec), flush=True)
log.write(json.dumps(rec) + '\n'); log.flush()
+2 -2
View File
@@ -1,8 +1,8 @@
{
"apg": {
"batch": 128, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16",
"plans": 60, "seg_plans": 10,
"margin_train_mm": 5.0,
"plans": 60, "plans_max": 150, "seg_plans": 10,
"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},
"eval_every": 500, "eval_tasks": 512
+104
View File
@@ -0,0 +1,104 @@
"""验收汇总(方案 §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 最后一轮左右比例 / 行程门禁
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。"""
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, '= 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) 部署形态
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:
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, '浏览器以 __POLICY.latency() 为准')
# 3) 数据 / 训练 / 后训练门禁
r2 = jload('ml/data/r2.json')
add('R2 近邻标签 std(步长比例)', r2['label_std_mean'] if r2 else 'missing', bool(r2 and r2['pass']), '< 0.10')
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()
+46
View File
@@ -0,0 +1,46 @@
"""方案 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 新增(审查:R2 无执行点)。GPU 上约 1 分钟;CPU 上把 --pool 降到 50000。"""
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=20000); ap.add_argument('--pool', type=int, default=200000)
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')
g = torch.Generator(device=a.device); g.manual_seed(a.seed)
P = min(a.pool, d.n); Q = min(a.queries, d.n)
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]; qi = torch.randperm(d.n, device=a.device, generator=g)[:Q]
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, 1024):
D = torch.cdist(Xq[s:s + 1024], Xp)
D = torch.where(tq[s:s + 1024, None] == tp[None], torch.full_like(D, float('inf')), D)
v, j = D.min(1); best_d[s:s + 1024] = v; best_j[s:s + 1024] = j
fin = torch.isfinite(best_d)
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))
out = {'pairs': int(m.sum()), 'nn_dist_threshold': thr.item(), 'label_std_mean': std.mean().item(),
'label_std_p90': torch.quantile(std, 0.9).item(), 'pass': std.mean().item() < 0.10,
'rule': 'label_std_mean < 0.10(10% 步长)'}
print(json.dumps(out, indent=1))
json.dump(out, open(a.out, 'w'), indent=1)
if __name__ == '__main__':
main()
+3 -2
View File
@@ -3,7 +3,7 @@
const fs = require('fs');
const files = process.argv.slice(2);
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 ? { 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 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++;
@@ -17,5 +17,6 @@ for (const f of files) for (const line of fs.readFileSync(f, 'utf8').split('\n')
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) };
projectedSamplesFor50k: Math.round(samples / total * 50000),
consensusDrop: Object.values(by).reduce((a, b) => a + b.consensusDrop, 0) };
console.log(JSON.stringify(out, null, 1));
+18 -4
View File
@@ -1,4 +1,6 @@
"""门禁(方案 §7.2 第 6 步):新模型须 ①门禁集到达率不降 ②A 类降幅 ≤ 0.5 个百分点 ③上一轮修好的错题 95% 仍对。
"""门禁(方案 §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
@@ -38,15 +40,27 @@ def main():
if ft:
r2, p2 = evaluate(load(a.new, a.device), kin, ft, a.device)
keep = sum(per_task_success(p2)) / len(ft)
accept = gain >= 0 and a_drop <= 0.5 and (keep is None or keep >= 0.95)
out = {'old': ro, 'new': rn, 'gain_pp': gain, 'A_drop_pp': a_drop, 'fixed_kept': keep, 'accept': accept}
# 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])
left = lambda sd, ok: (((sd > 0) & det & ok).sum() / ((det & ok).sum().clamp(min=1))).item()
ok_o, ok_n = torch.tensor(so, dtype=torch.bool), torch.tensor(sn, dtype=torch.bool)
lr_shift = abs(left(sn_side, ok_n) - left(so_side, ok_o)) * 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', 'accept')}))
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__':
+9 -7
View File
@@ -1,29 +1,31 @@
#!/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(左右比例 / 行程)。
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 ] || python -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate 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
python -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed $((1000+r)) --n $N --out $D
$PY -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed $((1000+r)) --n $N --out $D
# E2(局部极小)请老师补少量路径(唯一保留的后训练老师用途)
if [ -s $D/e2_tasks.jsonl ]; then
node ml/gen/gen_dataset.cjs --tasks $D/e2_tasks.jsonl --seed $((5000+r)) --out ml/data/e2/shard_e2_r$r.jsonl
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
POOLS=$(ls ml/runs/mine/r*/pool.jsonl)
python -m ml.apg.apg_finetune --init ml/runs/current.pt --samples $SAMPLES --pool $POOLS --out ml/runs/apg_r$r --steps $STEPS
python -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
$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
$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=$(python -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
gain=$($PY -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
echo "round $r gain ${gain} pp"
if python -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
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"
+12 -5
View File
@@ -1,20 +1,27 @@
"""无老师出题(PyTorch):A 均匀 / B 直线被挡 / C 贴立柱 / D 工作空间外;pos/pose 各半。"""
"""无老师出题(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 min_gap_chunked(kin, q, cap=0.5, chunk=512):
"""分块算最小间隙:随机位形每个约 2800 个幸存胶囊对、峰值约 1.5 MB/位形,一次 4 万位形会爆内存/显存(2026-10-18 实测出题 OOM),每块 512"""
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=4096):
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 = kin.gaps(q, 0.5)['gm'].min(-1).values > 0
ok = min_gap_chunked(kin, q) > 0
out.append(q[ok])
return torch.cat(out)[:n]
@@ -23,7 +30,7 @@ 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 (kin.gaps(pts, 0.5)['gm'].min(-1).values <= 0).view(a.shape[0], n).any(-1)
return (min_gap_chunked(kin, pts) <= 0).view(a.shape[0], n).any(-1)
def column_gap(kin, q):
@@ -44,7 +51,7 @@ def sample_tasks(kin, n, seed, mix=(0.3, 0.3, 0.3, 0.1)):
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': 3.0, 'travel': 0.0})
'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])):
+66 -27
View File
@@ -3,49 +3,88 @@
# setup 安装 Python/Node 依赖(torch 需先按 CUDA 版本装好)
# kin 导出运动学/碰撞模型 + JS↔PyTorch 对拍(必须 PASS)
# gate1 老师一致性门禁(决定生成时是否加 --consensus 4)
# gen 生成 5 万条老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤)
# prep 打包训练样本
# train 预训练(先 2000 步摸吞吐,再正式训练)
# m5 MeanFlow 头门禁:贴立柱集关安全层穿柱率 ≤ 2%(不过 → 用 configs/wta.json 重训)
# apg APG 首轮微调
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
# prep 打包训练样本 + R2 标签一致性检查
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型
# apg APG 首轮微调(从选定模型开始)
# mine 挖错循环(3–6 轮,含门禁回滚)
# export 导出 ONNX(fp16)到 webapp/models/
# bench 部署形态评测(Node,策略 + 安全层)
# all 依次执行 kin → bench
# export 导出 ONNX(fp32)到 webapp/models/
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
# all 依次执行 kin → accept
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
# 2026-10-18:按另一台机器的审查改——头分目录不互相覆盖、统一 $PYTHON(Windows Git Bash 可跑)、
# gen 自动补量、m5 双口径、export 默认 fp32、新增 accept。
set -e
cd "$(dirname "$0")/.."
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
NPROC=${NPROC:-$(nproc)}
N_TASKS=${N_TASKS:-50000}
TARGET_SAMPLES=${TARGET_SAMPLES:-1700000}
CFG=${CFG:-ml/configs/base.json}
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 "== $*"; "$@"; }
case $stage in
setup) run pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run python -m ml.eval.check_parity ;;
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;;
gate1) mkdir -p ml/data; run node ml/gen/consistency.cjs 300 4242 | tee ml/data/consistency.json ;;
gen) per=$(( (N_TASKS + NPROC - 1) / NPROC ))
if [ -z "${CONSENSUS:-}" ] && [ -f ml/data/consistency.json ]; then
CONSENSUS=$(python -c "import json;print(1 if json.load(open('ml/data/consistency.json'))['consistentByGateRule']<0.9 else 0)")
CONSENSUS=$($PY -c "import json;print(1 if json.load(open('ml/data/consistency.json'))['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" ;;
prep) run bash ml/prep/run_prep.sh $NPROC ;;
train) run python -m ml.train --samples ml/data/samples --out ml/runs/bench --steps 2000 --no-eval --config $CFG
run python -m ml.train --samples ml/data/samples --out ml/runs/base --config $CFG ;;
m5) [ -f ml/runs/nc_gate.jsonl ] || run python -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
run python -m ml.sim.closed_loop --ckpt ml/runs/base/best.pt --tasks ml/runs/nc_gate.jsonl --n 1000 --out ml/runs/m5_gate.json
python - <<'PY'
import json; r = json.load(open('ml/runs/m5_gate.json'))
print('贴立柱集(关安全层)穿柱率 %.2f%%,到达率 %.2f%%' % (100 * r['collision_rate'], 100 * r['success']))
print('M5 门禁:' + ('通过' if r['collision_rate'] <= 0.02 else '不通过 → CFG=ml/configs/wta.json bash ml/pipeline.sh train'))
run bash ml/gen/run_parallel.sh $per $NPROC 1 "$extra"
# 补量:共识过滤会降低保留率,样本不足 TARGET_SAMPLES 时按实测"样本/任务"补生成(最多 2 次)
s0=$((1 + NPROC))
for k in 1 2; do
need=$($PY -c "import json,math;s=json.load(open('ml/data/stats.json'));d=$TARGET_SAMPLES-s['samplesEvery3'];print(0 if d<=0 else math.ceil(d/(s['samplesEvery3']/s['tasks'])*1.05))")
[ "$need" = "0" ] && break
echo "样本不足目标 $TARGET_SAMPLES,补生成 $need 条任务"
run bash ml/gen/run_parallel.sh $(( (need + NPROC - 1) / NPROC )) $NPROC $s0 "$extra"
s0=$((s0 + NPROC))
done ;;
prep) run bash ml/prep/run_prep.sh $NPROC
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ;;
train) run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$TAG --steps 2000 --no-eval --config $CFG
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$TAG --config $CFG ;;
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
ck=${CKPT:-ml/runs/$TAG/best.pt}
run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --out ml/runs/m5_${TAG}_policy.json
run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --safety --out ml/runs/m5_${TAG}_safety.json
$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')); s = json.load(open(f'ml/runs/m5_{tag}_safety.json'))
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)
else:
print('M5 门禁:不通过' + (' → CFG=ml/configs/wta.json bash ml/pipeline.sh train && CFG=ml/configs/wta.json bash ml/pipeline.sh m5' if tag != 'wta' else '(两种头都未过,把 m5_gate_*.json 发我)'))
sys.exit(3)
PY
;;
apg) run python -m ml.apg.apg_finetune --init ml/runs/base/best.pt --samples ml/data/samples --out ml/runs/apg; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
apg) ck=$(ckpt); run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
mine) run bash ml/mining/loop.sh ${ROUNDS:-6} ${APG_STEPS:-1000} ${MINE_N:-50000} ;;
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=ml/runs/base/best.pt
run python -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx --fp16 ;;
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route --nc1000 --out ml/runs/policy_bench.json ;;
all) for s in kin gate1 gen prep train m5 apg mine export bench; do bash ml/pipeline.sh $s; done ;;
*) sed -n '2,16p' "$0" ;;
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 --nc1000 --out ml/runs/policy_bench.json ;;
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) for s in kin gate1 gen prep train m5 apg mine export bench accept; do bash ml/pipeline.sh $s; done ;;
*) sed -n '2,20p' "$0" ;;
esac
+1 -1
View File
@@ -34,7 +34,7 @@ for (const line of lines) {
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, split });
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) {
+1 -1
View File
@@ -5,4 +5,4 @@ cd "$(dirname "$0")/../.."
P=${1:-$(nproc)}
mkdir -p ml/data/samples
ls ml/data/shard_*.jsonl | xargs -P "$P" -I{} node ml/prep/build_dataset.cjs {} ml/data/samples
python3 -m ml.data --index ml/data/samples
${PYTHON:-python} -m ml.data --index ml/data/samples
+31 -9
View File
@@ -17,11 +17,15 @@ 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)
ex = torch.tensor([t.get('exec_s', 3.0) or 3.0 for t in tasks], dtype=torch.float32, device=device)
tr = torch.tensor([t.get('travel', 0.0) or 0.0 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)
@@ -64,7 +68,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
B = q0.shape[0]
step = torch.tensor(STEP, device=device)
limit = torch.clamp(2 * ex, min=3.0) if max_sec is None else torch.full_like(ex, max_sec)
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)
@@ -73,6 +78,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
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)
cold = True
for it in range(nmax):
tnow = it * DT
@@ -138,6 +145,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
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)
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
@@ -148,26 +157,39 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
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)
res = {'n': B, 'success': ok.float().mean().item(),
'time_ok_1p3': (ok & (t_done <= 1.3 * ex)).float().mean().item(),
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[ok] / tr[ok].clamp(min=1)).median().item() if ok.any() and (tr > 0).any() else None,
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item()}
per = {'done': done.cpu(), 't_done': t_done.cpu(), 'col': col.cpu(), 'travel': travel.cpu()}
'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 左右比例门禁
side = 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: continue
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
+7 -3
View File
@@ -1,6 +1,7 @@
"""预训练(方案 §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 --steps 2000 --no-eval
吞吐摸底: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, time
import torch
@@ -54,7 +55,9 @@ def main():
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')
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
bad_evals, t0, seen = 0, time.time(), 0
def save(name, step, extra=None):
d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'fp': kin.fp}
@@ -95,7 +98,8 @@ def main():
if step % tc['log_every'] == 0:
el = time.time() - t0
rec = {'step': step, '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)}
'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}
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()