Compare commits
96
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7fe829ac | ||
|
|
296cd17f98 | ||
|
|
34019c1a34 | ||
|
|
48cc566c3f | ||
|
|
6ecd33db0c | ||
|
|
c6b46b8bb1 | ||
|
|
6de085fd37 | ||
|
|
95da49193f | ||
|
|
9eaff7341f | ||
|
|
ce9edd7934 | ||
|
|
d977be321b | ||
|
|
46fe24365b | ||
|
|
7e99b333ea | ||
|
|
79519b042c | ||
|
|
3b748c811c | ||
|
|
26eb0815c2 | ||
|
|
7a8e5aab63 | ||
|
|
bb919d33c9 | ||
|
|
ff160f576e | ||
|
|
5675159987 | ||
|
|
39c870c1cf | ||
|
|
7be41cb1ad | ||
|
|
6c2af53d18 | ||
|
|
f5f394a4bf | ||
|
|
76ac563d21 | ||
|
|
de009bd516 | ||
|
|
29f3887be1 | ||
|
|
fe48b4e777 | ||
|
|
4de5e03a0b | ||
|
|
5f2cbd5179 | ||
|
|
2ff02eb9a8 | ||
|
|
dc27781a72 | ||
|
|
8e5431c460 | ||
|
|
b06662c152 | ||
|
|
70654e45f7 | ||
|
|
fc17ae10d0 | ||
|
|
2f5a72a6ab | ||
|
|
7a194dd4a7 | ||
|
|
a41a5440c6 | ||
|
|
864b21f894 | ||
|
|
b396298819 | ||
|
|
53a50ecab9 | ||
|
|
425229f412 | ||
|
|
7e063eb3d5 | ||
|
|
406f180cab | ||
|
|
86323f1691 | ||
|
|
0f5b84b919 | ||
|
|
0a19b29fd3 | ||
|
|
1bf59e7175 | ||
|
|
cafa038110 | ||
|
|
527bc25c11 | ||
|
|
9a7404856e | ||
|
|
1f2f2a2bb1 | ||
|
|
0653cf0d8d | ||
|
|
95ba43a1c7 | ||
|
|
5365add657 | ||
|
|
573082df26 | ||
|
|
5a87c0655a | ||
|
|
0b47905a1c | ||
|
|
0d522dd782 | ||
|
|
d60cbcdb3c | ||
|
|
979a5814c1 | ||
|
|
c2732fb3cd | ||
|
|
7fe1e5e0b3 | ||
|
|
3c4bc6c8ab | ||
|
|
4a59ca84f6 | ||
|
|
faf6dd2748 | ||
|
|
b1cf4126fd | ||
|
|
e431862793 | ||
|
|
c65b9986ca | ||
|
|
db0bbbc15d | ||
|
|
21ad209747 | ||
|
|
c143b56232 | ||
|
|
83dcc1c3ee | ||
|
|
9ed213417b | ||
|
|
e6418da084 | ||
|
|
241333c213 | ||
|
|
c128288c1b | ||
|
|
b25a7a8bfa | ||
|
|
c175357b72 | ||
|
|
d03f34822c | ||
|
|
8ad6b58fca | ||
|
|
591678703a | ||
|
|
b6141d237c | ||
|
|
a1ee7d1a4d | ||
|
|
f40a40c56b | ||
|
|
3675fbbd9b | ||
|
|
5591adb39a | ||
|
|
5348bc6f6f | ||
|
|
f8cdabb781 | ||
|
|
8800a68cad | ||
|
|
2a0a2e55ad | ||
|
|
cc43b3d3be | ||
|
|
67964496be | ||
|
|
46bdb98dd8 | ||
|
|
318ad0e264 |
+22
-2
@@ -25,7 +25,7 @@
|
||||
|---|---|
|
||||
| R9(真查看器、训练好的模型、30 道贴柱题) | 稳态 `planWarmP99` < 45 ms、运动中饥饿 < 1%、认证超时占比 < 5%;冷启动 P99、刹停率、`tickGapMax` 只报 |
|
||||
| R9 不过 | 几何查询(特征、投影、认证)搬进 Worker,主线程只执行 |
|
||||
| APG | batch 48、steps 8000(总任务量同原 128×3000),进程显存硬帽 `VRAM_FRAC=0.78` |
|
||||
| APG | batch 48、steps 8000(总任务量同原 128×3000),本进程显存上限浮动(整卡 − 已用 − 1 GB 余量,见 `ml/common/vram.py`) |
|
||||
| 离线 ort | 只用入库本地副本,缺失即报错,不退回 CDN |
|
||||
| 训练余量 3 mm | `stats.json` 间隙 p10 ≥ 3 mm 保持,否则降到 p10 |
|
||||
| `R2_STRICT` | `pool_check.independent=true` 且截距 < 0.10 时打开 |
|
||||
@@ -40,12 +40,32 @@
|
||||
4. APG 收敛与机时(batch 48 下首测)→ APG 日志。
|
||||
5. 真模型 R9 → `r9_browser.json`。
|
||||
|
||||
## 三′、首次正式预训练失败(2026-10-18,21850 步)与修正
|
||||
|
||||
现象:闭环到达 0–0.2%、穿柱 77–97%;MF 残差 2 万步停在 7–8、17k–19k 尖峰到 96;梯度 100% 被裁剪。用 last.pt 实测定位:
|
||||
1. **多峰头塌成单峰(主因)**:同一状态 8 个候选间标准差 0.030、动作幅度 0.151。FK / 平滑损失加在 `one_step(随机噪声)` 上去对齐这一条老师 Y,最优解是条件均值 E[Y|X],左绕 / 右绕取平均正好穿柱。→ 改加在 r = t、t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上。
|
||||
2. **MeanFlow 目标失控**:∂u/∂t 均方根 6–12,而 v 约 1.1,目标被自激的 JVP 项主导。时间嵌入 sin(1000·t) 太陡,JVP 又在 bf16 下算。→ 最高角频率 50;MF 分支 fp32;前 5000 步 p_neq = 0。
|
||||
3. **闭环分布偏移被低估(推断)**:增广只有 1°。→ 增广 40% / 3° / 12 步收回;新增 DAgger 阶段(策略走到的状态请老师重规划,2 轮);开训前加老师轨迹回放自检;每次评估加开环误差与候选多样性。
|
||||
|
||||
## 四、正式训练(三段,每段跑完回传)
|
||||
|
||||
| 段 | 命令 | 耗时 | 回传 |
|
||||
|---|---|---|---|
|
||||
| 数据 | `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` |
|
||||
| 训练(含 DAgger) | `FRESH_FROM=prep FROM=prep TO=m5 bash ml/pipeline.sh all`(增广改了,样本要重打包;gen 不重跑) | 约 9–11 小时(未实测:prep 0.5 + 预训练约 5(fp32 变慢)+ DAgger 约 3.5 + m5 0.3) | `ml/runs/replay_teacher.json`、`ml/runs/base/log.jsonl`、`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=<出错阶段>` 接着跑,前面的不重做。
|
||||
|
||||
---
|
||||
|
||||
## 七、第一次真跑(2026-10-18,网页「开始训练」)暴露的一处阻塞 + 门 1 实测
|
||||
|
||||
**阻塞(已修,一行)**:`pipeline.sh` 的 `run() { echo "== $*"; "$@"; }` 把命令回显写进 **stdout**,而 `gate1` 是 `run node ml/gen/consistency.cjs … | tee ml/data/consistency.json.tmp` → 回显被一起存进 `consistency.json`,文件首行变成 `== node ml/gen/consistency.cjs 300 4242`。`gen` 阶段读它判 CONSENSUS 时 `json.load` 当场 `JSONDecodeError: Expecting value: line 1 column 1 (char 0)`,`set -e` 下整条 `all` 停在 gen(阶段 rc:kin 0、r4 0、gate1 0、gen 1)。
|
||||
- 与平台无关,沙箱 Linux 同样会中,只是没跑到这一步。
|
||||
- 修法:回显改走 stderr(`echo "== $*" >&2`)。全仓只有 `gate1` 这一处把 `run` 的输出重定向进文件(`check_r2` 等用 `--out` 自写,不受影响)。
|
||||
- 已写坏的 `ml/data/consistency.json` 就地剥掉首行复用,**没有重跑 25–30 分钟的门禁**。
|
||||
- 建议实现方顺手加一条自检:任何被 `tee`/`>` 落盘成 JSON 的产物,读之前先确认首字符是 `{`。
|
||||
|
||||
**门 1 实测(第一次真数据)**:`tasks 300 / pairs 1133 / consistentRate 0.838 / consistentByGateRule 0.838 / gate FAIL / deterministic 300/300`,分类看 `C 0.824`、`B 0.844`,`devP50 3.6`、`devP90 145.1`。
|
||||
→ 0.838 < 0.90,**自动走共识分支**(`--consensus 4`),与方案 R1 首测 78% 同一侧;`worstAxisOfInconsistent [43,4,42,40,16,39]` 说明跳变在各轴上分布均匀,不是某一根轴的系统性问题。gen 因此按 4–5 h 那一档估,不是 1 h。
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 512,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round29_rows.json",
|
||||
"out": "docs/experiment_round29.json",
|
||||
"trace": "ml/runs/round29_trace.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "1fd72f71eaf8170b45782beede0d91544e1f7142e98c0642560af814382f01ef",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 512,
|
||||
"n_ref": 512,
|
||||
"success": 0.55078125,
|
||||
"collision_free_success": 0.546875,
|
||||
"time_ok_1p3": 0.361328125,
|
||||
"collision_rate": 0.017578125,
|
||||
"collision_steps_mean": 0.1015625,
|
||||
"travel_ratio_median": 2.425084352493286,
|
||||
"time_mean_s": 2.322056770324707,
|
||||
"brakes_mean": 19.228515625,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.25053170323371887,
|
||||
"err_fail_med": 194.51763916015625,
|
||||
"err_success_med": 0.1961677223443985,
|
||||
"dls_share": 0.634765625,
|
||||
"time_budget": "teacher×2(≥3 s)"
|
||||
},
|
||||
"simulation_wall_s": 488.4280448999925
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
{
|
||||
"trace": "ml/runs/round29_trace.pt",
|
||||
"n": 512,
|
||||
"reported_collision_tasks": 9,
|
||||
"replayed_first_hits32": 9,
|
||||
"replayed_first_hits64": 9,
|
||||
"scope": "Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.",
|
||||
"rows": [
|
||||
{
|
||||
"i": 21,
|
||||
"task": {
|
||||
"tid": 1223280991,
|
||||
"id": "s0001_000564",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
178.767,
|
||||
101.917,
|
||||
25.283,
|
||||
31.588,
|
||||
-12.179,
|
||||
-6.464
|
||||
],
|
||||
"T": [
|
||||
320.779,
|
||||
79.294,
|
||||
350.853
|
||||
],
|
||||
"R": [
|
||||
-0.5548626629953249,
|
||||
-0.1382477148217311,
|
||||
-0.820374910976869,
|
||||
-0.29956278115972873,
|
||||
-0.8867511636570162,
|
||||
0.35204334093514156,
|
||||
-0.7761375943462505,
|
||||
0.44108949556693955,
|
||||
0.4506112421399604
|
||||
],
|
||||
"exec_s": 2.86,
|
||||
"travel": 581.023,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.659999847412109,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.35770511627197266,
|
||||
-0.3957242965698242
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.3577231796451059,
|
||||
-0.39572636491366353
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
139.78468322753906,
|
||||
87.256591796875,
|
||||
6.928672790527344,
|
||||
103.78778839111328,
|
||||
-94.0,
|
||||
19.4688720703125
|
||||
],
|
||||
"endpoint": [
|
||||
139.50450134277344,
|
||||
87.4175796508789,
|
||||
6.71871280670166,
|
||||
104.29376983642578,
|
||||
-94.0,
|
||||
21.218488693237305
|
||||
],
|
||||
"dense_nsegments": 175,
|
||||
"dense_gap64_min_mm": -0.39572636491366353
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"task": {
|
||||
"tid": 1268963905,
|
||||
"id": "s0002_000538",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.198,
|
||||
52.395,
|
||||
108.796,
|
||||
75.534,
|
||||
82.302,
|
||||
-121.823
|
||||
],
|
||||
"T": [
|
||||
-85.062,
|
||||
275.378,
|
||||
888.929
|
||||
],
|
||||
"R": [
|
||||
0.5139412462799992,
|
||||
-0.4285630099475821,
|
||||
-0.7431003578769356,
|
||||
-0.5533754886400825,
|
||||
-0.8275489878032067,
|
||||
0.09454227286372051,
|
||||
-0.6554692700230204,
|
||||
0.36262435010704513,
|
||||
-0.6624678231921397
|
||||
],
|
||||
"exec_s": 3.2600000000000002,
|
||||
"travel": 349.09,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.980000019073486,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.030836105346679688,
|
||||
-0.1565418243408203,
|
||||
-0.11790847778320312
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.030838235678215753,
|
||||
-0.15655898804830848,
|
||||
-0.11791701257236653
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-69.87329864501953,
|
||||
36.559112548828125,
|
||||
2.094588279724121,
|
||||
102.22724151611328,
|
||||
89.0,
|
||||
-212.1793212890625
|
||||
],
|
||||
"endpoint": [
|
||||
-69.79582214355469,
|
||||
37.351837158203125,
|
||||
-0.033895254135131836,
|
||||
102.45509338378906,
|
||||
89.0,
|
||||
-214.75440979003906
|
||||
],
|
||||
"dense_nsegments": 258,
|
||||
"dense_gap64_min_mm": -0.16882067924099076
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"task": {
|
||||
"tid": 593213945,
|
||||
"id": "s0003_000414",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.395,
|
||||
84.783,
|
||||
2.744,
|
||||
14.841,
|
||||
-86.783,
|
||||
81.932
|
||||
],
|
||||
"T": [
|
||||
5.391,
|
||||
506.723,
|
||||
383.367
|
||||
],
|
||||
"R": [
|
||||
-0.5269859333114162,
|
||||
-0.7554822816510317,
|
||||
-0.38927156099983334,
|
||||
0.6402732880654818,
|
||||
-0.05172298162073713,
|
||||
-0.7664038424760652,
|
||||
0.5588702377848656,
|
||||
-0.6531242265324508,
|
||||
0.5109724082909718
|
||||
],
|
||||
"exec_s": 2.96,
|
||||
"travel": 560.55,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.599999904632568,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.3180522918701172,
|
||||
-0.19827747344970703
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.318071359587325,
|
||||
-0.19825289671655533
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-36.05080032348633,
|
||||
49.852294921875,
|
||||
22.054914474487305,
|
||||
67.3380126953125,
|
||||
-94.0,
|
||||
79.87274932861328
|
||||
],
|
||||
"endpoint": [
|
||||
-35.60316848754883,
|
||||
48.9223747253418,
|
||||
21.386226654052734,
|
||||
67.87915802001953,
|
||||
-94.0,
|
||||
79.27398681640625
|
||||
],
|
||||
"dense_nsegments": 93,
|
||||
"dense_gap64_min_mm": -0.19825289671655533
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"task": {
|
||||
"tid": 1743026973,
|
||||
"id": "s0003_000814",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.982,
|
||||
5.595,
|
||||
-64.669,
|
||||
-72.912,
|
||||
85.732,
|
||||
-86.377
|
||||
],
|
||||
"T": [
|
||||
-189.354,
|
||||
221.034,
|
||||
302.457
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.3000000000000003,
|
||||
"travel": 306.824,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 3.0399999618530273,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.12672901153564453
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.12673094091899273
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-33.6672248840332,
|
||||
39.63850021362305,
|
||||
-60.174774169921875,
|
||||
-108.2870864868164,
|
||||
88.27082061767578,
|
||||
-74.56200408935547
|
||||
],
|
||||
"endpoint": [
|
||||
-32.8936882019043,
|
||||
39.478729248046875,
|
||||
-58.639915466308594,
|
||||
-107.65913391113281,
|
||||
89.0,
|
||||
-74.92655944824219
|
||||
],
|
||||
"dense_nsegments": 154,
|
||||
"dense_gap64_min_mm": -0.12673094091899273
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"task": {
|
||||
"tid": 444406422,
|
||||
"id": "s0003_001100",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-28.854,
|
||||
43.33,
|
||||
-137.874,
|
||||
-6.895,
|
||||
-88.001,
|
||||
-43.745
|
||||
],
|
||||
"T": [
|
||||
379.105,
|
||||
293.88,
|
||||
653.541
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.58,
|
||||
"travel": 465.057,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 0.8399999737739563,
|
||||
"dls_it": 144,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.4698514938354492,
|
||||
-0.6612262725830078,
|
||||
-0.9359521865844727
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.4698356780126254,
|
||||
-0.6612412101671552,
|
||||
-0.9359581587234
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-42.48794937133789,
|
||||
11.756612777709961,
|
||||
-124.7530288696289,
|
||||
-46.12489700317383,
|
||||
-94.0,
|
||||
-54.57075500488281
|
||||
],
|
||||
"endpoint": [
|
||||
-42.73639678955078,
|
||||
10.6148681640625,
|
||||
-124.47537231445312,
|
||||
-48.52891159057617,
|
||||
-94.0,
|
||||
-55.451263427734375
|
||||
],
|
||||
"dense_nsegments": 241,
|
||||
"dense_gap64_min_mm": -0.9544276230224238
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"task": {
|
||||
"tid": 737760595,
|
||||
"id": "s0003_001280",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
41.213,
|
||||
49.353,
|
||||
-149.808,
|
||||
-8.981,
|
||||
-88.566,
|
||||
83.45
|
||||
],
|
||||
"T": [
|
||||
118.145,
|
||||
2.733,
|
||||
266.082
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.78,
|
||||
"travel": 228.67,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 1.1399999856948853,
|
||||
"dls_it": 102,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.06517887115478516,
|
||||
-0.0172576904296875,
|
||||
0.013133049011230469
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.06521150921177998,
|
||||
-0.017227151210720493,
|
||||
0.013145878955246104
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
1.8212896585464478,
|
||||
-9.327469825744629,
|
||||
-150.87704467773438,
|
||||
-23.00735855102539,
|
||||
-94.0,
|
||||
93.73664855957031
|
||||
],
|
||||
"endpoint": [
|
||||
1.3598560094833374,
|
||||
-9.66404914855957,
|
||||
-150.52992248535156,
|
||||
-23.200042724609375,
|
||||
-94.0,
|
||||
92.69589233398438
|
||||
],
|
||||
"dense_nsegments": 105,
|
||||
"dense_gap64_min_mm": -0.020246891242656773
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"task": {
|
||||
"tid": 169730876,
|
||||
"id": "s0004_000544",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
47.051,
|
||||
-1.179,
|
||||
-94.31,
|
||||
-43.128,
|
||||
67.067,
|
||||
-119.867
|
||||
],
|
||||
"T": [
|
||||
-142.696,
|
||||
-139.159,
|
||||
726.163
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.16,
|
||||
"travel": 461.684,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 3.5399999618530273,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.08817481994628906
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.08817155079043282
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
34.57734298706055,
|
||||
32.23564910888672,
|
||||
-71.76512908935547,
|
||||
10.783411979675293,
|
||||
88.44934844970703,
|
||||
-105.16633605957031
|
||||
],
|
||||
"endpoint": [
|
||||
34.31901931762695,
|
||||
33.24732208251953,
|
||||
-72.12022399902344,
|
||||
10.171412467956543,
|
||||
89.0,
|
||||
-106.89080810546875
|
||||
],
|
||||
"dense_nsegments": 173,
|
||||
"dense_gap64_min_mm": -0.08817155079043282
|
||||
},
|
||||
{
|
||||
"i": 266,
|
||||
"task": {
|
||||
"tid": 458057184,
|
||||
"id": "s0005_000271",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
44.394,
|
||||
32.402,
|
||||
-145.074,
|
||||
-48.37,
|
||||
-85.543,
|
||||
13.576
|
||||
],
|
||||
"T": [
|
||||
184.38,
|
||||
-306.445,
|
||||
433.207
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.52,
|
||||
"travel": 376.205,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 1.2200000286102295,
|
||||
"dls_it": 304,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.2372283935546875,
|
||||
-0.05915641784667969
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.23719947210205383,
|
||||
-0.05918908805933398
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
20.49032974243164,
|
||||
10.35391902923584,
|
||||
-172.87489318847656,
|
||||
-73.93492126464844,
|
||||
-93.70915985107422,
|
||||
14.725995063781738
|
||||
],
|
||||
"endpoint": [
|
||||
20.34522819519043,
|
||||
10.012239456176758,
|
||||
-172.91363525390625,
|
||||
-74.4600601196289,
|
||||
-94.0,
|
||||
13.266871452331543
|
||||
],
|
||||
"dense_nsegments": 146,
|
||||
"dense_gap64_min_mm": -0.05918908805933398
|
||||
},
|
||||
{
|
||||
"i": 325,
|
||||
"task": {
|
||||
"tid": 1206824904,
|
||||
"id": "s0005_001755",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-68.225,
|
||||
58.481,
|
||||
-31.386,
|
||||
57.2,
|
||||
-31.831,
|
||||
-80.259
|
||||
],
|
||||
"T": [
|
||||
30.805,
|
||||
353.752,
|
||||
661.419
|
||||
],
|
||||
"R": [
|
||||
0.5493387160259536,
|
||||
0.13571391775309405,
|
||||
0.8245051289125273,
|
||||
-0.09525700289583322,
|
||||
-0.9701190077749987,
|
||||
0.22314841283987508,
|
||||
0.8301524429129026,
|
||||
-0.2011239500451364,
|
||||
-0.5199962290670785
|
||||
],
|
||||
"exec_s": 2.34,
|
||||
"travel": 659.756,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 3.0999999046325684,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.3628082275390625,
|
||||
0.004294395446777344,
|
||||
-0.6861848831176758
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.3628058862147938,
|
||||
0.004307730814957722,
|
||||
-0.686172816414107
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
18,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
18,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-39.524147033691406,
|
||||
54.68175506591797,
|
||||
-18.574026107788086,
|
||||
103.78845977783203,
|
||||
-94.0,
|
||||
-53.657958984375
|
||||
],
|
||||
"endpoint": [
|
||||
-39.42676544189453,
|
||||
55.778987884521484,
|
||||
-18.88759994506836,
|
||||
103.97470092773438,
|
||||
-94.0,
|
||||
-54.84250259399414
|
||||
],
|
||||
"dense_nsegments": 119,
|
||||
"dense_gap64_min_mm": -0.686172816414107
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round30_after_rows.json",
|
||||
"out": "docs/experiment_round30_after.json",
|
||||
"trace": "ml/runs/round30_after.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.57421875,
|
||||
"collision_free_success": 0.57421875,
|
||||
"time_ok_1p3": 0.390625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.4449527263641357,
|
||||
"time_mean_s": 2.3076188564300537,
|
||||
"brakes_mean": 19.25,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.20997506380081177,
|
||||
"err_fail_med": 229.68746948242188,
|
||||
"err_success_med": 0.18899640440940857,
|
||||
"dls_share": 0.625,
|
||||
"time_budget": "teacher×2(≥3 s)"
|
||||
},
|
||||
"simulation_wall_s": 248.83432349999202
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round30_before_rows.json",
|
||||
"out": "docs/experiment_round30_before.json",
|
||||
"trace": "ml/runs/round30_before.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.57421875,
|
||||
"collision_free_success": 0.56640625,
|
||||
"time_ok_1p3": 0.390625,
|
||||
"collision_rate": 0.02734375,
|
||||
"collision_steps_mean": 0.0625,
|
||||
"travel_ratio_median": 2.4449527263641357,
|
||||
"time_mean_s": 2.2993197441101074,
|
||||
"brakes_mean": 19.23828125,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.20997506380081177,
|
||||
"err_fail_med": 229.68746948242188,
|
||||
"err_success_med": 0.1950554996728897,
|
||||
"dls_share": 0.625,
|
||||
"time_budget": "teacher×2(≥3 s)"
|
||||
},
|
||||
"simulation_wall_s": 247.51454629999353
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
{
|
||||
"trace": "ml/runs/round30_before.pt",
|
||||
"n": 256,
|
||||
"reported_collision_tasks": 7,
|
||||
"replayed_first_hits32": 7,
|
||||
"replayed_first_hits64": 7,
|
||||
"scope": "Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.",
|
||||
"rows": [
|
||||
{
|
||||
"i": 21,
|
||||
"task": {
|
||||
"tid": 1223280991,
|
||||
"id": "s0001_000564",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
178.767,
|
||||
101.917,
|
||||
25.283,
|
||||
31.588,
|
||||
-12.179,
|
||||
-6.464
|
||||
],
|
||||
"T": [
|
||||
320.779,
|
||||
79.294,
|
||||
350.853
|
||||
],
|
||||
"R": [
|
||||
-0.5548626629953249,
|
||||
-0.1382477148217311,
|
||||
-0.820374910976869,
|
||||
-0.29956278115972873,
|
||||
-0.8867511636570162,
|
||||
0.35204334093514156,
|
||||
-0.7761375943462505,
|
||||
0.44108949556693955,
|
||||
0.4506112421399604
|
||||
],
|
||||
"exec_s": 2.86,
|
||||
"travel": 581.023,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.659999847412109,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.35770511627197266,
|
||||
-0.3957242965698242
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.3577231796451059,
|
||||
-0.39572636491366353
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
139.78468322753906,
|
||||
87.256591796875,
|
||||
6.928672790527344,
|
||||
103.78778839111328,
|
||||
-94.0,
|
||||
19.4688720703125
|
||||
],
|
||||
"endpoint": [
|
||||
139.50450134277344,
|
||||
87.4175796508789,
|
||||
6.71871280670166,
|
||||
104.29376983642578,
|
||||
-94.0,
|
||||
21.218488693237305
|
||||
],
|
||||
"dense_nsegments": 175,
|
||||
"dense_gap64_min_mm": -0.39572636491366353,
|
||||
"certificate": {
|
||||
"plan_start_it": 282,
|
||||
"step": 0,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
139.50450134277344,
|
||||
87.4175796508789,
|
||||
6.71871280670166,
|
||||
104.29376983642578,
|
||||
-95.717529296875,
|
||||
21.218488693237305
|
||||
],
|
||||
"midpoint": [
|
||||
139.64459228515625,
|
||||
87.33708190917969,
|
||||
6.823692798614502,
|
||||
104.04077911376953,
|
||||
-94.8587646484375,
|
||||
20.34368133544922
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.1606760025024414,
|
||||
0.5
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.16066912536394184,
|
||||
0.5
|
||||
],
|
||||
"command_delta": [
|
||||
-0.2801789343357086,
|
||||
0.16098570823669434,
|
||||
-0.20995977520942688,
|
||||
0.5059784054756165,
|
||||
-1.717526912689209,
|
||||
1.7496168613433838
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 1.717529296875
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"task": {
|
||||
"tid": 1268963905,
|
||||
"id": "s0002_000538",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.198,
|
||||
52.395,
|
||||
108.796,
|
||||
75.534,
|
||||
82.302,
|
||||
-121.823
|
||||
],
|
||||
"T": [
|
||||
-85.062,
|
||||
275.378,
|
||||
888.929
|
||||
],
|
||||
"R": [
|
||||
0.5139412462799992,
|
||||
-0.4285630099475821,
|
||||
-0.7431003578769356,
|
||||
-0.5533754886400825,
|
||||
-0.8275489878032067,
|
||||
0.09454227286372051,
|
||||
-0.6554692700230204,
|
||||
0.36262435010704513,
|
||||
-0.6624678231921397
|
||||
],
|
||||
"exec_s": 3.2600000000000002,
|
||||
"travel": 349.09,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.980000019073486,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.030836105346679688,
|
||||
-0.1565418243408203,
|
||||
-0.11790847778320312
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.030838235678215753,
|
||||
-0.15655898804830848,
|
||||
-0.11791701257236653
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-69.87329864501953,
|
||||
36.559112548828125,
|
||||
2.094588279724121,
|
||||
102.22724151611328,
|
||||
89.0,
|
||||
-212.1793212890625
|
||||
],
|
||||
"endpoint": [
|
||||
-69.79582214355469,
|
||||
37.351837158203125,
|
||||
-0.033895254135131836,
|
||||
102.45509338378906,
|
||||
89.0,
|
||||
-214.75440979003906
|
||||
],
|
||||
"dense_nsegments": 258,
|
||||
"dense_gap64_min_mm": -0.16882067924099076,
|
||||
"certificate": {
|
||||
"plan_start_it": 298,
|
||||
"step": 0,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
-69.79582214355469,
|
||||
37.351837158203125,
|
||||
-0.033895254135131836,
|
||||
102.45509338378906,
|
||||
89.79981231689453,
|
||||
-214.75440979003906
|
||||
],
|
||||
"midpoint": [
|
||||
-69.83456420898438,
|
||||
36.955474853515625,
|
||||
1.0303465127944946,
|
||||
102.34117126464844,
|
||||
89.39990234375,
|
||||
-213.4668731689453
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.5,
|
||||
0.20721912384033203
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.5,
|
||||
0.2071966639119509
|
||||
],
|
||||
"command_delta": [
|
||||
0.07747504115104675,
|
||||
0.7927232980728149,
|
||||
-2.128483533859253,
|
||||
0.22785267233848572,
|
||||
0.7998088598251343,
|
||||
-2.5750951766967773
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 0.7998123168945312
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"task": {
|
||||
"tid": 593213945,
|
||||
"id": "s0003_000414",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.395,
|
||||
84.783,
|
||||
2.744,
|
||||
14.841,
|
||||
-86.783,
|
||||
81.932
|
||||
],
|
||||
"T": [
|
||||
5.391,
|
||||
506.723,
|
||||
383.367
|
||||
],
|
||||
"R": [
|
||||
-0.5269859333114162,
|
||||
-0.7554822816510317,
|
||||
-0.38927156099983334,
|
||||
0.6402732880654818,
|
||||
-0.05172298162073713,
|
||||
-0.7664038424760652,
|
||||
0.5588702377848656,
|
||||
-0.6531242265324508,
|
||||
0.5109724082909718
|
||||
],
|
||||
"exec_s": 2.96,
|
||||
"travel": 560.55,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 5.599999904632568,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.3180522918701172,
|
||||
-0.19827747344970703
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.318071359587325,
|
||||
-0.19825289671655533
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-36.05080032348633,
|
||||
49.852294921875,
|
||||
22.054914474487305,
|
||||
67.3380126953125,
|
||||
-94.0,
|
||||
79.87274932861328
|
||||
],
|
||||
"endpoint": [
|
||||
-35.60316848754883,
|
||||
48.9223747253418,
|
||||
21.386226654052734,
|
||||
67.87915802001953,
|
||||
-94.0,
|
||||
79.27398681640625
|
||||
],
|
||||
"dense_nsegments": 93,
|
||||
"dense_gap64_min_mm": -0.19825289671655533,
|
||||
"certificate": {
|
||||
"plan_start_it": 278,
|
||||
"step": 1,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
-35.60316848754883,
|
||||
48.9223747253418,
|
||||
21.3862247467041,
|
||||
67.87915802001953,
|
||||
-97.17477416992188,
|
||||
79.27399444580078
|
||||
],
|
||||
"midpoint": [
|
||||
-35.82698440551758,
|
||||
49.387332916259766,
|
||||
21.720569610595703,
|
||||
67.60858154296875,
|
||||
-96.70620727539062,
|
||||
79.57337188720703
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.2614564895629883,
|
||||
0.5
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.2614702651611598,
|
||||
0.5
|
||||
],
|
||||
"command_delta": [
|
||||
0.4476320445537567,
|
||||
-0.929920494556427,
|
||||
-0.6686887741088867,
|
||||
0.5411425828933716,
|
||||
-0.9371238946914673,
|
||||
-0.5987589359283447
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 3.174774169921875
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"task": {
|
||||
"tid": 1743026973,
|
||||
"id": "s0003_000814",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.982,
|
||||
5.595,
|
||||
-64.669,
|
||||
-72.912,
|
||||
85.732,
|
||||
-86.377
|
||||
],
|
||||
"T": [
|
||||
-189.354,
|
||||
221.034,
|
||||
302.457
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.3000000000000003,
|
||||
"travel": 306.824,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 3.0399999618530273,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.12672901153564453
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.12673094091899273
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-33.6672248840332,
|
||||
39.63850021362305,
|
||||
-60.174774169921875,
|
||||
-108.2870864868164,
|
||||
88.27082061767578,
|
||||
-74.56200408935547
|
||||
],
|
||||
"endpoint": [
|
||||
-32.8936882019043,
|
||||
39.478729248046875,
|
||||
-58.639915466308594,
|
||||
-107.65913391113281,
|
||||
89.0,
|
||||
-74.92655944824219
|
||||
],
|
||||
"dense_nsegments": 154,
|
||||
"dense_gap64_min_mm": -0.12673094091899273,
|
||||
"certificate": {
|
||||
"plan_start_it": 150,
|
||||
"step": 1,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
-32.89369201660156,
|
||||
39.478729248046875,
|
||||
-58.639915466308594,
|
||||
-107.65913391113281,
|
||||
89.33978271484375,
|
||||
-74.92655944824219
|
||||
],
|
||||
"midpoint": [
|
||||
-33.28045654296875,
|
||||
39.55861282348633,
|
||||
-59.407344818115234,
|
||||
-107.97311401367188,
|
||||
88.8052978515625,
|
||||
-74.7442855834961
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.1356372833251953,
|
||||
0.5
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.13560383338968585,
|
||||
0.5
|
||||
],
|
||||
"command_delta": [
|
||||
0.773534893989563,
|
||||
-0.15977251529693604,
|
||||
1.534859299659729,
|
||||
0.6279491186141968,
|
||||
1.068959355354309,
|
||||
-0.3645569384098053
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 0.33978271484375
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"task": {
|
||||
"tid": 444406422,
|
||||
"id": "s0003_001100",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-28.854,
|
||||
43.33,
|
||||
-137.874,
|
||||
-6.895,
|
||||
-88.001,
|
||||
-43.745
|
||||
],
|
||||
"T": [
|
||||
379.105,
|
||||
293.88,
|
||||
653.541
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.58,
|
||||
"travel": 465.057,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 0.8399999737739563,
|
||||
"dls_it": 144,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.4698514938354492,
|
||||
-0.6612262725830078,
|
||||
-0.9359521865844727
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.4698356780126254,
|
||||
-0.6612412101671552,
|
||||
-0.9359581587234
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
-42.48794937133789,
|
||||
11.756612777709961,
|
||||
-124.7530288696289,
|
||||
-46.12489700317383,
|
||||
-94.0,
|
||||
-54.57075500488281
|
||||
],
|
||||
"endpoint": [
|
||||
-42.73639678955078,
|
||||
10.6148681640625,
|
||||
-124.47537231445312,
|
||||
-48.52891159057617,
|
||||
-94.0,
|
||||
-55.451263427734375
|
||||
],
|
||||
"dense_nsegments": 241,
|
||||
"dense_gap64_min_mm": -0.9544276230224238,
|
||||
"certificate": {
|
||||
"plan_start_it": 40,
|
||||
"step": 1,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
-42.73639678955078,
|
||||
10.6148681640625,
|
||||
-124.47537231445312,
|
||||
-48.52891159057617,
|
||||
-98.31655883789062,
|
||||
-55.45126724243164
|
||||
],
|
||||
"midpoint": [
|
||||
-42.61217498779297,
|
||||
11.18574047088623,
|
||||
-124.61420440673828,
|
||||
-47.326904296875,
|
||||
-97.59821319580078,
|
||||
-55.011009216308594
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"command_delta": [
|
||||
-0.24844767153263092,
|
||||
-1.1417450904846191,
|
||||
0.27765607833862305,
|
||||
-2.4040162563323975,
|
||||
-1.4366930723190308,
|
||||
-0.8805096745491028
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 4.316558837890625
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"task": {
|
||||
"tid": 737760595,
|
||||
"id": "s0003_001280",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
41.213,
|
||||
49.353,
|
||||
-149.808,
|
||||
-8.981,
|
||||
-88.566,
|
||||
83.45
|
||||
],
|
||||
"T": [
|
||||
118.145,
|
||||
2.733,
|
||||
266.082
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.78,
|
||||
"travel": 228.67,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 1.1399999856948853,
|
||||
"dls_it": 102,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.06517887115478516,
|
||||
-0.0172576904296875,
|
||||
0.013133049011230469
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.06521150921177998,
|
||||
-0.017227151210720493,
|
||||
0.013145878955246104
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
1.8212896585464478,
|
||||
-9.327469825744629,
|
||||
-150.87704467773438,
|
||||
-23.00735855102539,
|
||||
-94.0,
|
||||
93.73664855957031
|
||||
],
|
||||
"endpoint": [
|
||||
1.3598560094833374,
|
||||
-9.66404914855957,
|
||||
-150.52992248535156,
|
||||
-23.200042724609375,
|
||||
-94.0,
|
||||
92.69589233398438
|
||||
],
|
||||
"dense_nsegments": 105,
|
||||
"dense_gap64_min_mm": -0.020246891242656773,
|
||||
"certificate": {
|
||||
"plan_start_it": 56,
|
||||
"step": 0,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
1.3598560094833374,
|
||||
-9.66404914855957,
|
||||
-150.52992248535156,
|
||||
-23.200042724609375,
|
||||
-95.86970520019531,
|
||||
92.69589233398438
|
||||
],
|
||||
"midpoint": [
|
||||
1.5905728340148926,
|
||||
-9.495759010314941,
|
||||
-150.70347595214844,
|
||||
-23.103700637817383,
|
||||
-94.93485260009766,
|
||||
93.21627044677734
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.5,
|
||||
0.20821666717529297
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.5,
|
||||
0.20822553884000428
|
||||
],
|
||||
"command_delta": [
|
||||
-0.46143364906311035,
|
||||
-0.33657926321029663,
|
||||
0.3471294939517975,
|
||||
-0.19268345832824707,
|
||||
-1.8697090148925781,
|
||||
-1.0407572984695435
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 1.8697052001953125
|
||||
}
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"task": {
|
||||
"tid": 169730876,
|
||||
"id": "s0004_000544",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
47.051,
|
||||
-1.179,
|
||||
-94.31,
|
||||
-43.128,
|
||||
67.067,
|
||||
-119.867
|
||||
],
|
||||
"T": [
|
||||
-142.696,
|
||||
-139.159,
|
||||
726.163
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.16,
|
||||
"travel": 461.684,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"first_hit_s": 3.5399999618530273,
|
||||
"dls_it": -1,
|
||||
"point_order": [
|
||||
"q0",
|
||||
"previous",
|
||||
"midpoint",
|
||||
"endpoint"
|
||||
],
|
||||
"gap32_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.08817481994628906
|
||||
],
|
||||
"gap64_mm": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
-0.08817155079043282
|
||||
],
|
||||
"pair32": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
6
|
||||
],
|
||||
"pair64": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
6
|
||||
],
|
||||
"boundary_joints": [
|
||||
5
|
||||
],
|
||||
"previous": [
|
||||
34.57734298706055,
|
||||
32.23564910888672,
|
||||
-71.76512908935547,
|
||||
10.783411979675293,
|
||||
88.44934844970703,
|
||||
-105.16633605957031
|
||||
],
|
||||
"endpoint": [
|
||||
34.31901931762695,
|
||||
33.24732208251953,
|
||||
-72.12022399902344,
|
||||
10.171412467956543,
|
||||
89.0,
|
||||
-106.89080810546875
|
||||
],
|
||||
"dense_nsegments": 173,
|
||||
"dense_gap64_min_mm": -0.08817155079043282,
|
||||
"certificate": {
|
||||
"plan_start_it": 176,
|
||||
"step": 0,
|
||||
"checked": true,
|
||||
"endpoint": [
|
||||
34.31901931762695,
|
||||
33.24732208251953,
|
||||
-72.12022399902344,
|
||||
10.171412467956543,
|
||||
89.79435729980469,
|
||||
-106.89080810546875
|
||||
],
|
||||
"midpoint": [
|
||||
34.44818115234375,
|
||||
32.741485595703125,
|
||||
-71.94267272949219,
|
||||
10.477412223815918,
|
||||
89.12185668945312,
|
||||
-106.02857208251953
|
||||
],
|
||||
"recorded_gap32_mm": [
|
||||
0.2809743881225586,
|
||||
0.5
|
||||
],
|
||||
"replayed_gap64_mm": [
|
||||
0.28098007564065064,
|
||||
0.5
|
||||
],
|
||||
"command_delta": [
|
||||
-0.2583238482475281,
|
||||
1.011674404144287,
|
||||
-0.3550943434238434,
|
||||
-0.6119998693466187,
|
||||
1.3450126647949219,
|
||||
-1.7244750261306763
|
||||
],
|
||||
"clamped_reconstruction_max_abs_deg": 0.0,
|
||||
"certified_vs_executed_max_abs_deg": 0.7943572998046875
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"same_config_except_output_paths": true,
|
||||
"instrumentation_vs_round29_first_batch_max_abs_deg": 0.0,
|
||||
"reached_only_after": [],
|
||||
"reached_only_before": [],
|
||||
"collision_tasks_before": [
|
||||
21,
|
||||
84,
|
||||
145,
|
||||
164,
|
||||
175,
|
||||
180,
|
||||
215
|
||||
],
|
||||
"collision_tasks_after": [],
|
||||
"statistics": {
|
||||
"before": {
|
||||
"active_certified_steps": 34069,
|
||||
"certificate_endpoint_mismatch_gt_0p001deg": 5298,
|
||||
"certificate_endpoint_max_abs_deg": 7.587158203125,
|
||||
"certificate_midpoint_max_abs_deg": 5.78558349609375,
|
||||
"uncertified_moving_steps": 0,
|
||||
"executed_speed_ratio_max": 1.660003662109375,
|
||||
"executed_speed_limit_violating_tasks": 256,
|
||||
"executed_speed_max_deg_s": [
|
||||
99.6002197265625,
|
||||
99.60002899169922,
|
||||
149.40032958984375,
|
||||
149.400146484375,
|
||||
199.20005798339844,
|
||||
298.80029296875
|
||||
],
|
||||
"executed_travel_ratio_median_success": 2.4250845909118652,
|
||||
"commanded_travel_ratio_median_success": 2.4449527263641357
|
||||
},
|
||||
"after": {
|
||||
"active_certified_steps": 34107,
|
||||
"certificate_endpoint_mismatch_gt_0p001deg": 0,
|
||||
"certificate_endpoint_max_abs_deg": 0.0,
|
||||
"certificate_midpoint_max_abs_deg": 0.0,
|
||||
"uncertified_moving_steps": 0,
|
||||
"executed_speed_ratio_max": 1.660003662109375,
|
||||
"executed_speed_limit_violating_tasks": 256,
|
||||
"executed_speed_max_deg_s": [
|
||||
99.6002197265625,
|
||||
99.60002899169922,
|
||||
149.40032958984375,
|
||||
149.400146484375,
|
||||
199.20005798339844,
|
||||
298.80029296875
|
||||
],
|
||||
"executed_travel_ratio_median_success": 2.4250845909118652,
|
||||
"commanded_travel_ratio_median_success": 2.4449527263641357
|
||||
}
|
||||
},
|
||||
"cpu_prediction_microbenchmark": {
|
||||
"scope": "B=1, commit=2, 100 warmups then 1000 serial calls, CPU one thread; not full planning latency",
|
||||
"before_p50_ms": 0.0388,
|
||||
"before_p99_ms": 0.069102,
|
||||
"after_p50_ms": 0.0518,
|
||||
"after_p99_ms": 0.093933
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"trace": "ml/runs/round30_after.pt",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"indices": [
|
||||
0,
|
||||
21,
|
||||
84,
|
||||
128,
|
||||
145,
|
||||
164,
|
||||
175,
|
||||
180,
|
||||
197,
|
||||
215,
|
||||
224,
|
||||
255
|
||||
],
|
||||
"max_step_deg": 0.05,
|
||||
"dtype": "float64",
|
||||
"device": "cuda",
|
||||
"scope": "Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.",
|
||||
"collision_tasks": 1,
|
||||
"rows": [
|
||||
{
|
||||
"i": 0,
|
||||
"task": {
|
||||
"tid": 919549409,
|
||||
"id": "s0001_000001",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-129.897,
|
||||
46.68,
|
||||
43.494,
|
||||
-97.461,
|
||||
-85.102,
|
||||
-139.71
|
||||
],
|
||||
"T": [
|
||||
-116.172,
|
||||
268.408,
|
||||
179.282
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 3.48,
|
||||
"travel": 345.324,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 174,
|
||||
"sampled_points": 6211,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.02673023799951224,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"task": {
|
||||
"tid": 1223280991,
|
||||
"id": "s0001_000564",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
178.767,
|
||||
101.917,
|
||||
25.283,
|
||||
31.588,
|
||||
-12.179,
|
||||
-6.464
|
||||
],
|
||||
"T": [
|
||||
320.779,
|
||||
79.294,
|
||||
350.853
|
||||
],
|
||||
"R": [
|
||||
-0.5548626629953249,
|
||||
-0.1382477148217311,
|
||||
-0.820374910976869,
|
||||
-0.29956278115972873,
|
||||
-0.8867511636570162,
|
||||
0.35204334093514156,
|
||||
-0.7761375943462505,
|
||||
0.44108949556693955,
|
||||
0.4506112421399604
|
||||
],
|
||||
"exec_s": 2.86,
|
||||
"travel": 581.023,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 286,
|
||||
"sampled_points": 6059,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.009057896844284485,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"task": {
|
||||
"tid": 1268963905,
|
||||
"id": "s0002_000538",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.198,
|
||||
52.395,
|
||||
108.796,
|
||||
75.534,
|
||||
82.302,
|
||||
-121.823
|
||||
],
|
||||
"T": [
|
||||
-85.062,
|
||||
275.378,
|
||||
888.929
|
||||
],
|
||||
"R": [
|
||||
0.5139412462799992,
|
||||
-0.4285630099475821,
|
||||
-0.7431003578769356,
|
||||
-0.5533754886400825,
|
||||
-0.8275489878032067,
|
||||
0.09454227286372051,
|
||||
-0.6554692700230204,
|
||||
0.36262435010704513,
|
||||
-0.6624678231921397
|
||||
],
|
||||
"exec_s": 3.2600000000000002,
|
||||
"travel": 349.09,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 326,
|
||||
"sampled_points": 6251,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.006206321604070197,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 128,
|
||||
"task": {
|
||||
"tid": 408952743,
|
||||
"id": "s0002_001588",
|
||||
"cat": "A",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-153.539,
|
||||
100.454,
|
||||
108.434,
|
||||
3.379,
|
||||
70.254,
|
||||
-179.848
|
||||
],
|
||||
"T": [
|
||||
-324.009,
|
||||
70.778,
|
||||
919.477
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 0.8200000000000001,
|
||||
"travel": 121.056,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 63,
|
||||
"sampled_points": 2403,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.5,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"task": {
|
||||
"tid": 593213945,
|
||||
"id": "s0003_000414",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.395,
|
||||
84.783,
|
||||
2.744,
|
||||
14.841,
|
||||
-86.783,
|
||||
81.932
|
||||
],
|
||||
"T": [
|
||||
5.391,
|
||||
506.723,
|
||||
383.367
|
||||
],
|
||||
"R": [
|
||||
-0.5269859333114162,
|
||||
-0.7554822816510317,
|
||||
-0.38927156099983334,
|
||||
0.6402732880654818,
|
||||
-0.05172298162073713,
|
||||
-0.7664038424760652,
|
||||
0.5588702377848656,
|
||||
-0.6531242265324508,
|
||||
0.5109724082909718
|
||||
],
|
||||
"exec_s": 2.96,
|
||||
"travel": 560.55,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 296,
|
||||
"sampled_points": 4509,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.002145835308116517,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"task": {
|
||||
"tid": 1743026973,
|
||||
"id": "s0003_000814",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.982,
|
||||
5.595,
|
||||
-64.669,
|
||||
-72.912,
|
||||
85.732,
|
||||
-86.377
|
||||
],
|
||||
"T": [
|
||||
-189.354,
|
||||
221.034,
|
||||
302.457
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.3000000000000003,
|
||||
"travel": 306.824,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 230,
|
||||
"sampled_points": 3985,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.008857213584009926,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"task": {
|
||||
"tid": 444406422,
|
||||
"id": "s0003_001100",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-28.854,
|
||||
43.33,
|
||||
-137.874,
|
||||
-6.895,
|
||||
-88.001,
|
||||
-43.745
|
||||
],
|
||||
"T": [
|
||||
379.105,
|
||||
293.88,
|
||||
653.541
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.58,
|
||||
"travel": 465.057,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 143,
|
||||
"sampled_points": 5547,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.18812628629569694,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"task": {
|
||||
"tid": 737760595,
|
||||
"id": "s0003_001280",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
41.213,
|
||||
49.353,
|
||||
-149.808,
|
||||
-8.981,
|
||||
-88.566,
|
||||
83.45
|
||||
],
|
||||
"T": [
|
||||
118.145,
|
||||
2.733,
|
||||
266.082
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.78,
|
||||
"travel": 228.67,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 95,
|
||||
"sampled_points": 3153,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.1492959159660998,
|
||||
"collision_segments": 1,
|
||||
"first_collision_s": 1.0915789473684212
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"task": {
|
||||
"tid": 1433345185,
|
||||
"id": "s0004_000020",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
20.112,
|
||||
-20.013,
|
||||
-177.85,
|
||||
32.746,
|
||||
-70.888,
|
||||
-30.111
|
||||
],
|
||||
"T": [
|
||||
-288.364,
|
||||
63.606,
|
||||
672.54
|
||||
],
|
||||
"R": [
|
||||
-0.5335375732231524,
|
||||
-0.7509294450860695,
|
||||
0.38915630081225366,
|
||||
-0.8443344656004893,
|
||||
0.4997538915602813,
|
||||
-0.19324947107169785,
|
||||
-0.049365657681080755,
|
||||
-0.4316839311036288,
|
||||
-0.9006730902323173
|
||||
],
|
||||
"exec_s": 2.14,
|
||||
"travel": 634.022,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 214,
|
||||
"sampled_points": 4641,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.0010684324191032601,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"task": {
|
||||
"tid": 169730876,
|
||||
"id": "s0004_000544",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
47.051,
|
||||
-1.179,
|
||||
-94.31,
|
||||
-43.128,
|
||||
67.067,
|
||||
-119.867
|
||||
],
|
||||
"T": [
|
||||
-142.696,
|
||||
-139.159,
|
||||
726.163
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.16,
|
||||
"travel": 461.684,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 216,
|
||||
"sampled_points": 6605,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.2112382958686112,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"task": {
|
||||
"tid": 1884410379,
|
||||
"id": "s0004_000721",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-127.447,
|
||||
7.845,
|
||||
-124.228,
|
||||
-20.968,
|
||||
-21.51,
|
||||
-35.505
|
||||
],
|
||||
"T": [
|
||||
-233.158,
|
||||
-59.78,
|
||||
648.987
|
||||
],
|
||||
"R": [
|
||||
0.3964618999442966,
|
||||
-0.020495209950317686,
|
||||
0.9178223729358808,
|
||||
-0.8179090164279511,
|
||||
0.4461640898622928,
|
||||
0.3632663564978371,
|
||||
-0.41694460392049726,
|
||||
-0.8947164641865291,
|
||||
0.1601238457417738
|
||||
],
|
||||
"exec_s": 1.48,
|
||||
"travel": 356.988,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 128,
|
||||
"sampled_points": 4707,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.0041216155493710716,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"task": {
|
||||
"tid": 712677274,
|
||||
"id": "s0004_001686",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.548,
|
||||
71.865,
|
||||
27.462,
|
||||
-75.048,
|
||||
60.325,
|
||||
151.872
|
||||
],
|
||||
"T": [
|
||||
-53.154,
|
||||
50.204,
|
||||
397.489
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.84,
|
||||
"travel": 238.182,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 184,
|
||||
"sampled_points": 2495,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.004616787795093558,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
}
|
||||
],
|
||||
"audit_wall_s": 15.056756100006169
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"trace": "ml/runs/round30_before.pt",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"indices": [
|
||||
0,
|
||||
21,
|
||||
84,
|
||||
128,
|
||||
145,
|
||||
164,
|
||||
175,
|
||||
180,
|
||||
197,
|
||||
215,
|
||||
224,
|
||||
255
|
||||
],
|
||||
"max_step_deg": 0.05,
|
||||
"dtype": "float64",
|
||||
"device": "cuda",
|
||||
"scope": "Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.",
|
||||
"collision_tasks": 7,
|
||||
"rows": [
|
||||
{
|
||||
"i": 0,
|
||||
"task": {
|
||||
"tid": 919549409,
|
||||
"id": "s0001_000001",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-129.897,
|
||||
46.68,
|
||||
43.494,
|
||||
-97.461,
|
||||
-85.102,
|
||||
-139.71
|
||||
],
|
||||
"T": [
|
||||
-116.172,
|
||||
268.408,
|
||||
179.282
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 3.48,
|
||||
"travel": 345.324,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 174,
|
||||
"sampled_points": 6211,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.02673023799951224,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"task": {
|
||||
"tid": 1223280991,
|
||||
"id": "s0001_000564",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
178.767,
|
||||
101.917,
|
||||
25.283,
|
||||
31.588,
|
||||
-12.179,
|
||||
-6.464
|
||||
],
|
||||
"T": [
|
||||
320.779,
|
||||
79.294,
|
||||
350.853
|
||||
],
|
||||
"R": [
|
||||
-0.5548626629953249,
|
||||
-0.1382477148217311,
|
||||
-0.820374910976869,
|
||||
-0.29956278115972873,
|
||||
-0.8867511636570162,
|
||||
0.35204334093514156,
|
||||
-0.7761375943462505,
|
||||
0.44108949556693955,
|
||||
0.4506112421399604
|
||||
],
|
||||
"exec_s": 2.86,
|
||||
"travel": 581.023,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 286,
|
||||
"sampled_points": 6099,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.39572636491366353,
|
||||
"collision_segments": 2,
|
||||
"first_collision_s": 5.655
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"task": {
|
||||
"tid": 1268963905,
|
||||
"id": "s0002_000538",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.198,
|
||||
52.395,
|
||||
108.796,
|
||||
75.534,
|
||||
82.302,
|
||||
-121.823
|
||||
],
|
||||
"T": [
|
||||
-85.062,
|
||||
275.378,
|
||||
888.929
|
||||
],
|
||||
"R": [
|
||||
0.5139412462799992,
|
||||
-0.4285630099475821,
|
||||
-0.7431003578769356,
|
||||
-0.5533754886400825,
|
||||
-0.8275489878032067,
|
||||
0.09454227286372051,
|
||||
-0.6554692700230204,
|
||||
0.36262435010704513,
|
||||
-0.6624678231921397
|
||||
],
|
||||
"exec_s": 3.2600000000000002,
|
||||
"travel": 349.09,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 326,
|
||||
"sampled_points": 6037,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.16878710314368206,
|
||||
"collision_segments": 3,
|
||||
"first_collision_s": 5.961153846153847
|
||||
},
|
||||
{
|
||||
"i": 128,
|
||||
"task": {
|
||||
"tid": 408952743,
|
||||
"id": "s0002_001588",
|
||||
"cat": "A",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-153.539,
|
||||
100.454,
|
||||
108.434,
|
||||
3.379,
|
||||
70.254,
|
||||
-179.848
|
||||
],
|
||||
"T": [
|
||||
-324.009,
|
||||
70.778,
|
||||
919.477
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 0.8200000000000001,
|
||||
"travel": 121.056,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 63,
|
||||
"sampled_points": 2403,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.5,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"task": {
|
||||
"tid": 593213945,
|
||||
"id": "s0003_000414",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-49.395,
|
||||
84.783,
|
||||
2.744,
|
||||
14.841,
|
||||
-86.783,
|
||||
81.932
|
||||
],
|
||||
"T": [
|
||||
5.391,
|
||||
506.723,
|
||||
383.367
|
||||
],
|
||||
"R": [
|
||||
-0.5269859333114162,
|
||||
-0.7554822816510317,
|
||||
-0.38927156099983334,
|
||||
0.6402732880654818,
|
||||
-0.05172298162073713,
|
||||
-0.7664038424760652,
|
||||
0.5588702377848656,
|
||||
-0.6531242265324508,
|
||||
0.5109724082909718
|
||||
],
|
||||
"exec_s": 2.96,
|
||||
"travel": 560.55,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 296,
|
||||
"sampled_points": 4489,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.19825289671655533,
|
||||
"collision_segments": 5,
|
||||
"first_collision_s": 5.597
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"task": {
|
||||
"tid": 1743026973,
|
||||
"id": "s0003_000814",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.982,
|
||||
5.595,
|
||||
-64.669,
|
||||
-72.912,
|
||||
85.732,
|
||||
-86.377
|
||||
],
|
||||
"T": [
|
||||
-189.354,
|
||||
221.034,
|
||||
302.457
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.3000000000000003,
|
||||
"travel": 306.824,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 230,
|
||||
"sampled_points": 3865,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.12673094091895543,
|
||||
"collision_segments": 4,
|
||||
"first_collision_s": 3.0387500000000003
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"task": {
|
||||
"tid": 444406422,
|
||||
"id": "s0003_001100",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-28.854,
|
||||
43.33,
|
||||
-137.874,
|
||||
-6.895,
|
||||
-88.001,
|
||||
-43.745
|
||||
],
|
||||
"T": [
|
||||
379.105,
|
||||
293.88,
|
||||
653.541
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.58,
|
||||
"travel": 465.057,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 145,
|
||||
"sampled_points": 5769,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.954427674326439,
|
||||
"collision_segments": 2,
|
||||
"first_collision_s": 0.8236
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"task": {
|
||||
"tid": 737760595,
|
||||
"id": "s0003_001280",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
41.213,
|
||||
49.353,
|
||||
-149.808,
|
||||
-8.981,
|
||||
-88.566,
|
||||
83.45
|
||||
],
|
||||
"T": [
|
||||
118.145,
|
||||
2.733,
|
||||
266.082
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.78,
|
||||
"travel": 228.67,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 103,
|
||||
"sampled_points": 3385,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.1492959159660998,
|
||||
"collision_segments": 2,
|
||||
"first_collision_s": 1.0915789473684212
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"task": {
|
||||
"tid": 1433345185,
|
||||
"id": "s0004_000020",
|
||||
"cat": "B",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
20.112,
|
||||
-20.013,
|
||||
-177.85,
|
||||
32.746,
|
||||
-70.888,
|
||||
-30.111
|
||||
],
|
||||
"T": [
|
||||
-288.364,
|
||||
63.606,
|
||||
672.54
|
||||
],
|
||||
"R": [
|
||||
-0.5335375732231524,
|
||||
-0.7509294450860695,
|
||||
0.38915630081225366,
|
||||
-0.8443344656004893,
|
||||
0.4997538915602813,
|
||||
-0.19324947107169785,
|
||||
-0.049365657681080755,
|
||||
-0.4316839311036288,
|
||||
-0.9006730902323173
|
||||
],
|
||||
"exec_s": 2.14,
|
||||
"travel": 634.022,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 214,
|
||||
"sampled_points": 4317,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.0010684324191032601,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"task": {
|
||||
"tid": 169730876,
|
||||
"id": "s0004_000544",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
47.051,
|
||||
-1.179,
|
||||
-94.31,
|
||||
-43.128,
|
||||
67.067,
|
||||
-119.867
|
||||
],
|
||||
"T": [
|
||||
-142.696,
|
||||
-139.159,
|
||||
726.163
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 2.16,
|
||||
"travel": 461.684,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 216,
|
||||
"sampled_points": 6341,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": -0.08817155079042394,
|
||||
"collision_segments": 3,
|
||||
"first_collision_s": 3.538888888888889
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"task": {
|
||||
"tid": 1884410379,
|
||||
"id": "s0004_000721",
|
||||
"cat": "A",
|
||||
"mode": "pose",
|
||||
"q0": [
|
||||
-127.447,
|
||||
7.845,
|
||||
-124.228,
|
||||
-20.968,
|
||||
-21.51,
|
||||
-35.505
|
||||
],
|
||||
"T": [
|
||||
-233.158,
|
||||
-59.78,
|
||||
648.987
|
||||
],
|
||||
"R": [
|
||||
0.3964618999442966,
|
||||
-0.020495209950317686,
|
||||
0.9178223729358808,
|
||||
-0.8179090164279511,
|
||||
0.4461640898622928,
|
||||
0.3632663564978371,
|
||||
-0.41694460392049726,
|
||||
-0.8947164641865291,
|
||||
0.1601238457417738
|
||||
],
|
||||
"exec_s": 1.48,
|
||||
"travel": 356.988,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 128,
|
||||
"sampled_points": 4707,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.0041216155493710716,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"task": {
|
||||
"tid": 712677274,
|
||||
"id": "s0004_001686",
|
||||
"cat": "C",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
-65.548,
|
||||
71.865,
|
||||
27.462,
|
||||
-75.048,
|
||||
60.325,
|
||||
151.872
|
||||
],
|
||||
"T": [
|
||||
-53.154,
|
||||
50.204,
|
||||
397.489
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.84,
|
||||
"travel": 238.182,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"steps": 184,
|
||||
"sampled_points": 2495,
|
||||
"initial_gap64_mm": 0.5,
|
||||
"min_gap64_mm": 0.004616787795093558,
|
||||
"collision_segments": 0,
|
||||
"first_collision_s": null
|
||||
}
|
||||
],
|
||||
"audit_wall_s": 14.869853199998033
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"task_index": 180,
|
||||
"task": {
|
||||
"tid": 737760595,
|
||||
"id": "s0003_001280",
|
||||
"cat": "B",
|
||||
"mode": "pos",
|
||||
"q0": [
|
||||
41.213,
|
||||
49.353,
|
||||
-149.808,
|
||||
-8.981,
|
||||
-88.566,
|
||||
83.45
|
||||
],
|
||||
"T": [
|
||||
118.145,
|
||||
2.733,
|
||||
266.082
|
||||
],
|
||||
"R": null,
|
||||
"exec_s": 1.78,
|
||||
"travel": 228.67,
|
||||
"reached": true,
|
||||
"err": 0,
|
||||
"split": "val"
|
||||
},
|
||||
"segment_start_it": 54,
|
||||
"start_s": 1.08,
|
||||
"end_s": 1.1,
|
||||
"original_discrete_col": 0,
|
||||
"certified": true,
|
||||
"recorded_endpoint_midpoint_gap32_mm": [
|
||||
0.0651998519897461,
|
||||
0.11720466613769531
|
||||
],
|
||||
"previous_midpoint_endpoint_gap64_mm": [
|
||||
0.5,
|
||||
0.11723191087430074,
|
||||
0.06521150921177998
|
||||
],
|
||||
"previous": [
|
||||
3.0118656158447266,
|
||||
-7.4926042556762695,
|
||||
-151.498291015625,
|
||||
-22.7498836517334,
|
||||
-94.0,
|
||||
93.50477600097656
|
||||
],
|
||||
"endpoint": [
|
||||
1.8212896585464478,
|
||||
-9.327469825744629,
|
||||
-150.87704467773438,
|
||||
-23.00735855102539,
|
||||
-94.0,
|
||||
93.73664855957031
|
||||
],
|
||||
"dense_max_step_deg": 0.01,
|
||||
"dense_points": 185,
|
||||
"min_gap64_mm": -0.14927288174659914,
|
||||
"min_at_fraction": 0.7663043478260869,
|
||||
"min_at_q": [
|
||||
2.09952208334985,
|
||||
-8.898669719696045,
|
||||
-151.0222272458284,
|
||||
-22.947187786516935,
|
||||
-94.0,
|
||||
93.68246095076852
|
||||
],
|
||||
"scope": "One executed segment; finite sampling demonstrates a collision between certified checkpoints."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,658 @@
|
||||
{
|
||||
"segments": 48,
|
||||
"kin_fp": "2ebcbf17",
|
||||
"root_gap_cap_mm": 20.0,
|
||||
"formula": "L = ceil(log2(bound_pair/(ga+gb)))\uff0c\u67e5\u8be2\u6570 = 2^L + 1\uff1bbound_pair \u53d6\u6700\u7d27\u7ec4\u5bf9\uff0c\u4e0d\u7528\u5168\u7ec4\u5bf9\u6700\u5927\u503c",
|
||||
"levels_needed_by_status": {
|
||||
"safe": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4
|
||||
],
|
||||
"unresolved": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"collision": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"queries_needed_safe": [
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17
|
||||
],
|
||||
"model_vs_measured_max_excess": 20,
|
||||
"unresolved_needed_queries": [
|
||||
33,
|
||||
33,
|
||||
33,
|
||||
33,
|
||||
33,
|
||||
65,
|
||||
65,
|
||||
65,
|
||||
65,
|
||||
65
|
||||
],
|
||||
"collision_dense_check": [
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 54,
|
||||
"status": "collision",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 29.09,
|
||||
"endpoint_gap_sum_mm": 1.98854,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 5
|
||||
}
|
||||
],
|
||||
"note": "\u6a21\u578b\u662f\u201c\u754c\u53ea\u968f\u4e8c\u5206\u51cf\u534a\u201d\u4e0b\u7684\u4e0b\u754c\uff1b\u5b9e\u6d4b\u67e5\u8be2\u53ef\u80fd\u66f4\u5c0f\uff08\u591a\u5c42\u91cc\u6709\u7684\u7ec4\u5bf9\u5148\u6ee1\u8db3\uff09\u6216\u66f4\u5927\uff08\u78b0\u649e\u6bb5\u63d0\u524d\u7ec8\u6b62\uff09\u3002",
|
||||
"rows": [
|
||||
{
|
||||
"i": 0,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.382,
|
||||
"endpoint_gap_sum_mm": 17.77168,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 0,
|
||||
"start_it": 8,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 13,
|
||||
"bound_pair_mm": 8.2833,
|
||||
"endpoint_gap_sum_mm": 0.25216,
|
||||
"levels_needed": 6,
|
||||
"queries_needed": 65,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 0,
|
||||
"start_it": 91,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.459,
|
||||
"endpoint_gap_sum_mm": 14.45459,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 0,
|
||||
"start_it": 173,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 6.8776,
|
||||
"endpoint_gap_sum_mm": 15.48695,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 4.7095,
|
||||
"endpoint_gap_sum_mm": 12.81844,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"start_it": 100,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.3371,
|
||||
"endpoint_gap_sum_mm": 0.03365,
|
||||
"levels_needed": 6,
|
||||
"queries_needed": 65,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"start_it": 127,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.5104,
|
||||
"endpoint_gap_sum_mm": 0.68046,
|
||||
"levels_needed": 2,
|
||||
"queries_needed": 5,
|
||||
"queries_used_batch32": 4
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"start_it": 282,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 3.5692,
|
||||
"endpoint_gap_sum_mm": 1.72327,
|
||||
"levels_needed": 2,
|
||||
"queries_needed": 5,
|
||||
"queries_used_batch32": 4
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 11.7042,
|
||||
"endpoint_gap_sum_mm": 13.79742,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"start_it": 170,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 4.9891,
|
||||
"endpoint_gap_sum_mm": 0.3999,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 18
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"start_it": 234,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 7.5939,
|
||||
"endpoint_gap_sum_mm": 0.28698,
|
||||
"levels_needed": 5,
|
||||
"queries_needed": 33,
|
||||
"queries_used_batch32": 31
|
||||
},
|
||||
{
|
||||
"i": 84,
|
||||
"start_it": 325,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 3.5738,
|
||||
"endpoint_gap_sum_mm": 10.95173,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 128,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 4.4648,
|
||||
"endpoint_gap_sum_mm": 16.52712,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 128,
|
||||
"start_it": 31,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.1155,
|
||||
"endpoint_gap_sum_mm": 14.24909,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 128,
|
||||
"start_it": 62,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 0.3619,
|
||||
"endpoint_gap_sum_mm": 12.62201,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.5691,
|
||||
"endpoint_gap_sum_mm": 11.60111,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"start_it": 32,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 4,
|
||||
"bound_pair_mm": 4.6053,
|
||||
"endpoint_gap_sum_mm": 0.67129,
|
||||
"levels_needed": 3,
|
||||
"queries_needed": 9,
|
||||
"queries_used_batch32": 28
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"start_it": 138,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 4,
|
||||
"bound_pair_mm": 9.6062,
|
||||
"endpoint_gap_sum_mm": 1.11765,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 15
|
||||
},
|
||||
{
|
||||
"i": 145,
|
||||
"start_it": 294,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 8.2956,
|
||||
"endpoint_gap_sum_mm": 0.61574,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 12.0298,
|
||||
"endpoint_gap_sum_mm": 11.50625,
|
||||
"levels_needed": 1,
|
||||
"queries_needed": 3,
|
||||
"queries_used_batch32": 3
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"start_it": 84,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 18.0143,
|
||||
"endpoint_gap_sum_mm": 1.7857,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 18
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"start_it": 182,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 14.1522,
|
||||
"endpoint_gap_sum_mm": 0.38976,
|
||||
"levels_needed": 6,
|
||||
"queries_needed": 65,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 164,
|
||||
"start_it": 227,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 5.6457,
|
||||
"endpoint_gap_sum_mm": 1.13586,
|
||||
"levels_needed": 3,
|
||||
"queries_needed": 9,
|
||||
"queries_used_batch32": 10
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 6.5456,
|
||||
"endpoint_gap_sum_mm": 15.18067,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"start_it": 46,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 6,
|
||||
"bound_pair_mm": 30.8668,
|
||||
"endpoint_gap_sum_mm": 1.26529,
|
||||
"levels_needed": 5,
|
||||
"queries_needed": 33,
|
||||
"queries_used_batch32": 32
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"start_it": 75,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 17.8885,
|
||||
"endpoint_gap_sum_mm": 10.74989,
|
||||
"levels_needed": 1,
|
||||
"queries_needed": 3,
|
||||
"queries_used_batch32": 3
|
||||
},
|
||||
{
|
||||
"i": 175,
|
||||
"start_it": 142,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 0.8185,
|
||||
"endpoint_gap_sum_mm": 10.77208,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 3.4654,
|
||||
"endpoint_gap_sum_mm": 14.04707,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 14,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 4,
|
||||
"bound_pair_mm": 17.6609,
|
||||
"endpoint_gap_sum_mm": 0.46562,
|
||||
"levels_needed": 6,
|
||||
"queries_needed": 65,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 52,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 27.7231,
|
||||
"endpoint_gap_sum_mm": 3.97272,
|
||||
"levels_needed": 3,
|
||||
"queries_needed": 9,
|
||||
"queries_used_batch32": 15
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 54,
|
||||
"status": "collision",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 29.09,
|
||||
"endpoint_gap_sum_mm": 1.98854,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 5
|
||||
},
|
||||
{
|
||||
"i": 180,
|
||||
"start_it": 94,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.1032,
|
||||
"endpoint_gap_sum_mm": 12.22495,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.0839,
|
||||
"endpoint_gap_sum_mm": 6.87225,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"start_it": 80,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 3.3325,
|
||||
"endpoint_gap_sum_mm": 0.50053,
|
||||
"levels_needed": 3,
|
||||
"queries_needed": 9,
|
||||
"queries_used_batch32": 29
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"start_it": 82,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 5.7899,
|
||||
"endpoint_gap_sum_mm": 0.16354,
|
||||
"levels_needed": 6,
|
||||
"queries_needed": 65,
|
||||
"queries_used_batch32": 17
|
||||
},
|
||||
{
|
||||
"i": 197,
|
||||
"start_it": 211,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 5,
|
||||
"bound_pair_mm": 18.6183,
|
||||
"endpoint_gap_sum_mm": 5.55945,
|
||||
"levels_needed": 2,
|
||||
"queries_needed": 5,
|
||||
"queries_used_batch32": 11
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.4256,
|
||||
"endpoint_gap_sum_mm": 12.47554,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"start_it": 104,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 4.7208,
|
||||
"endpoint_gap_sum_mm": 14.00267,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"start_it": 178,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 6,
|
||||
"bound_pair_mm": 25.7963,
|
||||
"endpoint_gap_sum_mm": 1.07014,
|
||||
"levels_needed": 5,
|
||||
"queries_needed": 33,
|
||||
"queries_used_batch32": 30
|
||||
},
|
||||
{
|
||||
"i": 215,
|
||||
"start_it": 215,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 6,
|
||||
"bound_pair_mm": 9.2152,
|
||||
"endpoint_gap_sum_mm": 8.57509,
|
||||
"levels_needed": 1,
|
||||
"queries_needed": 3,
|
||||
"queries_used_batch32": 3
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 6.492,
|
||||
"endpoint_gap_sum_mm": 10.02154,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"start_it": 52,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 3.5989,
|
||||
"endpoint_gap_sum_mm": 0.12354,
|
||||
"levels_needed": 5,
|
||||
"queries_needed": 33,
|
||||
"queries_used_batch32": 32
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"start_it": 61,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.7788,
|
||||
"endpoint_gap_sum_mm": 0.78961,
|
||||
"levels_needed": 2,
|
||||
"queries_needed": 5,
|
||||
"queries_used_batch32": 5
|
||||
},
|
||||
{
|
||||
"i": 224,
|
||||
"start_it": 127,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.4884,
|
||||
"endpoint_gap_sum_mm": 7.29024,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"start_it": 0,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 1.5332,
|
||||
"endpoint_gap_sum_mm": 11.184,
|
||||
"levels_needed": 0,
|
||||
"queries_needed": 2,
|
||||
"queries_used_batch32": 2
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"start_it": 25,
|
||||
"status": "unresolved",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 2.9831,
|
||||
"endpoint_gap_sum_mm": 0.14423,
|
||||
"levels_needed": 5,
|
||||
"queries_needed": 33,
|
||||
"queries_used_batch32": 29
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"start_it": 50,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 4,
|
||||
"bound_pair_mm": 7.2365,
|
||||
"endpoint_gap_sum_mm": 0.61112,
|
||||
"levels_needed": 4,
|
||||
"queries_needed": 17,
|
||||
"queries_used_batch32": 25
|
||||
},
|
||||
{
|
||||
"i": 255,
|
||||
"start_it": 181,
|
||||
"status": "safe",
|
||||
"tight_group_pair": 18,
|
||||
"bound_pair_mm": 4.2961,
|
||||
"endpoint_gap_sum_mm": 0.74979,
|
||||
"levels_needed": 3,
|
||||
"queries_needed": 9,
|
||||
"queries_used_batch32": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"baseline_commit": "95da491",
|
||||
"poses": "first 64 existing parity poses",
|
||||
"n_poses": 64,
|
||||
"kin_fp": "2ebcbf17",
|
||||
"dtype": "float64",
|
||||
"degenerate_capsules": [
|
||||
686
|
||||
],
|
||||
"pairs_with_second_degenerate": 670,
|
||||
"evaluated_pairs": 42880,
|
||||
"changed_pairs_over_1e_8": 20661,
|
||||
"old_minus_new_min_mm": 0.0,
|
||||
"old_minus_new_max_mm": 86.00309853133399,
|
||||
"new_reverse_max_abs_mm": 0.0,
|
||||
"worst": {
|
||||
"pose": 29,
|
||||
"first_capsule": 430,
|
||||
"second_capsule": 686,
|
||||
"old_distance_mm": 493.354753665718,
|
||||
"new_distance_mm": 407.35165513438403
|
||||
},
|
||||
"scope": "Raw capsule distances; does not imply these pairs win the group minimum."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"baseline_commit": "95da491",
|
||||
"sample_source": "docs/experiment_round31_certificates.json",
|
||||
"segments": 48,
|
||||
"dense_points": 7586,
|
||||
"max_step_deg": 0.01,
|
||||
"cap_mm": 0.5,
|
||||
"dtype": "float64",
|
||||
"device": "cuda",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"max_group_gap_difference_mm": 0.0,
|
||||
"group_gaps_changed_over_1e_8": 0,
|
||||
"minimum_gaps_changed_over_1e_8": 0,
|
||||
"collision_bool_mismatches": 0,
|
||||
"wall_s": 4.18259759999637
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"n": 2000,
|
||||
"fk_max_abs_mm": 4.547473508864641e-13,
|
||||
"orient_max_abs": 3.3306690738754696e-16,
|
||||
"gap_max_abs_mm": 2.5224267119483557e-13,
|
||||
"collide_bool_mismatch": 0,
|
||||
"feat_max_abs": 5.960463655974024e-08,
|
||||
"grad_rel_err_max": 1.8474076070450906e-06,
|
||||
"PASS": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"scope": "全部 48 段 × 15 组对 = 720 项检验;观察值按 cap=2.0 mm 钳制,故可检验命题取 min(预测下限, cap)。违例=实测最小间隙低于该下限。",
|
||||
"checks": 720,
|
||||
"violations": [],
|
||||
"worst_margin_mm": 0.0,
|
||||
"min_slack_mm": 1e-06,
|
||||
"dense_cap_mm": 2.0,
|
||||
"grid_points_per_segment": 121,
|
||||
"supersedes": "docs/experiment_round32_uniform_validation.json 里 bound_checks=61 的部分覆盖版本",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"wall_s": 1.66,
|
||||
"nontrivial": {
|
||||
"definition": "只有预测下限 < 观察 cap 才是有信息量的检验;其余组对被钳制恒等式自动满足",
|
||||
"checks": 61,
|
||||
"min_slack_mm": 0.035347,
|
||||
"p10_slack_mm": 0.544274,
|
||||
"violations": 0,
|
||||
"pred_mm_median_all": 15.8639,
|
||||
"pred_mm_max_all": 19.9582
|
||||
},
|
||||
"note": "720 项总检验 0 违例,其中有信息量的是 61 项;预测下限的中位数 15.9 mm 远大于观察 cap 2 mm,所以大部分组对无法在本网格上证伪"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"device": "cuda float64",
|
||||
"torch": "2.10.0+cu130",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"roots_per_call": 2,
|
||||
"pairs": 24,
|
||||
"warm": 3,
|
||||
"reps": 7,
|
||||
"segment_sample": "docs/experiment_round31_certificates.json 的 48 段两两成对",
|
||||
"design": "每对根轮流按正序/逆序测完全部配置(3次预热+7次计时);uniform_auto 的宽相位=最大子段界+2eps,uniform_cap20 固定 20 mm,bisect 为第31轮逐层二分口径",
|
||||
"results": [
|
||||
{
|
||||
"kind": "uniform_auto",
|
||||
"n_or_budget": 8,
|
||||
"points": 18,
|
||||
"cap_used_mm": 5.9191,
|
||||
"calls": 168,
|
||||
"p50_ms": 9.684,
|
||||
"p90_ms": 13.027,
|
||||
"p99_ms": 15.157,
|
||||
"max_ms": 17.537,
|
||||
"over_20ms": 0,
|
||||
"roots": 48,
|
||||
"safe": 26,
|
||||
"collision": 1,
|
||||
"rejected": 21
|
||||
},
|
||||
{
|
||||
"kind": "uniform_auto",
|
||||
"n_or_budget": 16,
|
||||
"points": 34,
|
||||
"cap_used_mm": 2.9597,
|
||||
"calls": 168,
|
||||
"p50_ms": 14.137,
|
||||
"p90_ms": 18.454,
|
||||
"p99_ms": 20.866,
|
||||
"max_ms": 21.632,
|
||||
"over_20ms": 5,
|
||||
"roots": 48,
|
||||
"safe": 30,
|
||||
"collision": 1,
|
||||
"rejected": 17
|
||||
},
|
||||
{
|
||||
"kind": "uniform_auto",
|
||||
"n_or_budget": 32,
|
||||
"points": 66,
|
||||
"cap_used_mm": 1.4799,
|
||||
"calls": 168,
|
||||
"p50_ms": 20.209,
|
||||
"p90_ms": 27.88,
|
||||
"p99_ms": 32.268,
|
||||
"max_ms": 35.563,
|
||||
"over_20ms": 87,
|
||||
"roots": 48,
|
||||
"safe": 34,
|
||||
"collision": 1,
|
||||
"rejected": 13
|
||||
},
|
||||
{
|
||||
"kind": "uniform_auto",
|
||||
"n_or_budget": 64,
|
||||
"points": 130,
|
||||
"cap_used_mm": 0.7401,
|
||||
"calls": 168,
|
||||
"p50_ms": 31.429,
|
||||
"p90_ms": 48.751,
|
||||
"p99_ms": 57.697,
|
||||
"max_ms": 62.084,
|
||||
"over_20ms": 165,
|
||||
"roots": 48,
|
||||
"safe": 36,
|
||||
"collision": 1,
|
||||
"rejected": 11
|
||||
},
|
||||
{
|
||||
"kind": "uniform_auto",
|
||||
"n_or_budget": 128,
|
||||
"points": 258,
|
||||
"cap_used_mm": 0.3701,
|
||||
"calls": 168,
|
||||
"p50_ms": 59.761,
|
||||
"p90_ms": 89.063,
|
||||
"p99_ms": 109.086,
|
||||
"max_ms": 117.345,
|
||||
"over_20ms": 168,
|
||||
"roots": 48,
|
||||
"safe": 38,
|
||||
"collision": 1,
|
||||
"rejected": 9
|
||||
},
|
||||
{
|
||||
"kind": "uniform_cap20",
|
||||
"n_or_budget": 8,
|
||||
"points": 18,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 13.129,
|
||||
"p90_ms": 16.44,
|
||||
"p99_ms": 20.963,
|
||||
"max_ms": 24.67,
|
||||
"over_20ms": 3,
|
||||
"roots": 48,
|
||||
"safe": 26,
|
||||
"collision": 1,
|
||||
"rejected": 21
|
||||
},
|
||||
{
|
||||
"kind": "uniform_cap20",
|
||||
"n_or_budget": 32,
|
||||
"points": 66,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 28.415,
|
||||
"p90_ms": 39.579,
|
||||
"p99_ms": 53.023,
|
||||
"max_ms": 59.983,
|
||||
"over_20ms": 153,
|
||||
"roots": 48,
|
||||
"safe": 34,
|
||||
"collision": 1,
|
||||
"rejected": 13
|
||||
},
|
||||
{
|
||||
"kind": "uniform_cap20",
|
||||
"n_or_budget": 64,
|
||||
"points": 130,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 50.85,
|
||||
"p90_ms": 75.512,
|
||||
"p99_ms": 88.355,
|
||||
"max_ms": 91.492,
|
||||
"over_20ms": 168,
|
||||
"roots": 48,
|
||||
"safe": 36,
|
||||
"collision": 1,
|
||||
"rejected": 11
|
||||
},
|
||||
{
|
||||
"kind": "uniform_cap20",
|
||||
"n_or_budget": 128,
|
||||
"points": 258,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 98.46,
|
||||
"p90_ms": 139.982,
|
||||
"p99_ms": 167.042,
|
||||
"max_ms": 177.743,
|
||||
"over_20ms": 168,
|
||||
"roots": 48,
|
||||
"safe": 38,
|
||||
"collision": 1,
|
||||
"rejected": 9
|
||||
},
|
||||
{
|
||||
"kind": "bisect",
|
||||
"n_or_budget": 32,
|
||||
"points": null,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 54.885,
|
||||
"p90_ms": 82.151,
|
||||
"p99_ms": 117.205,
|
||||
"max_ms": 118.716,
|
||||
"over_20ms": 133,
|
||||
"roots": 48,
|
||||
"safe": 37,
|
||||
"collision": 1,
|
||||
"rejected": 10
|
||||
},
|
||||
{
|
||||
"kind": "bisect",
|
||||
"n_or_budget": 65,
|
||||
"points": null,
|
||||
"cap_used_mm": 20.0,
|
||||
"calls": 168,
|
||||
"p50_ms": 71.825,
|
||||
"p90_ms": 108.559,
|
||||
"p99_ms": 122.533,
|
||||
"max_ms": 127.319,
|
||||
"over_20ms": 133,
|
||||
"roots": 48,
|
||||
"safe": 41,
|
||||
"collision": 1,
|
||||
"rejected": 6
|
||||
}
|
||||
],
|
||||
"wall_s": 108.63
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"segments": 48,
|
||||
"kin_fp": "2ebcbf17",
|
||||
"dense_cap_mm": 2.0,
|
||||
"eps_mm": 0.0001,
|
||||
"bound_falsification": "每条组对:预测下限 (ga+gb-bound)/2 应 ≤ 网格上该组对的最小实测间隙",
|
||||
"per_segment": [
|
||||
{
|
||||
"n": 8,
|
||||
"safe": 26,
|
||||
"collision": 1,
|
||||
"unproven": 21,
|
||||
"outside_box": 0,
|
||||
"false_accept_vs_dense": 0,
|
||||
"cap_required_mm": 5.919107314806348,
|
||||
"known_missed_segment_collision": true
|
||||
},
|
||||
{
|
||||
"n": 16,
|
||||
"safe": 30,
|
||||
"collision": 1,
|
||||
"unproven": 17,
|
||||
"outside_box": 0,
|
||||
"false_accept_vs_dense": 0,
|
||||
"cap_required_mm": 2.959653657403174,
|
||||
"known_missed_segment_collision": true
|
||||
},
|
||||
{
|
||||
"n": 32,
|
||||
"safe": 34,
|
||||
"collision": 1,
|
||||
"unproven": 13,
|
||||
"outside_box": 0,
|
||||
"false_accept_vs_dense": 0,
|
||||
"cap_required_mm": 1.479926828701587,
|
||||
"known_missed_segment_collision": true
|
||||
},
|
||||
{
|
||||
"n": 64,
|
||||
"safe": 36,
|
||||
"collision": 1,
|
||||
"unproven": 11,
|
||||
"outside_box": 0,
|
||||
"false_accept_vs_dense": 0,
|
||||
"cap_required_mm": 0.7400634143507935,
|
||||
"known_missed_segment_collision": true
|
||||
},
|
||||
{
|
||||
"n": 128,
|
||||
"safe": 38,
|
||||
"collision": 1,
|
||||
"unproven": 9,
|
||||
"outside_box": 0,
|
||||
"false_accept_vs_dense": 0,
|
||||
"cap_required_mm": 0.37013170717539673,
|
||||
"known_missed_segment_collision": true
|
||||
}
|
||||
],
|
||||
"bound_checks": 61,
|
||||
"bound_violations": [],
|
||||
"worst_margin_mm": 0.0
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round33_base_rows.json",
|
||||
"out": "docs/experiment_round33_base.json",
|
||||
"trace": "ml/runs/round33_base.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 0,
|
||||
"gap_margin": 0.0,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.57421875,
|
||||
"collision_free_success": 0.57421875,
|
||||
"time_ok_1p3": 0.390625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.4449527263641357,
|
||||
"time_mean_s": 2.3076188564300537,
|
||||
"brakes_mean": 19.25,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.20997506380081177,
|
||||
"err_fail_med": 229.68746948242188,
|
||||
"err_success_med": 0.18899640440940857,
|
||||
"dls_share": 0.625,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.6600004434585571,
|
||||
"speed_ratio_p95": 1.6600035429000854,
|
||||
"speed_ratio_max": 1.660003662109375,
|
||||
"over_vmax_rate": 1.0,
|
||||
"accel_ratio_p95": 1.5835107564926147,
|
||||
"accel_ratio_max": 1.6600004434585571
|
||||
},
|
||||
"simulation_wall_s": 296.2702505999914
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round33_both_rows.json",
|
||||
"out": "docs/experiment_round33_both.json",
|
||||
"trace": "ml/runs/round33_both.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 1,
|
||||
"gap_margin": 0.5,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.51953125,
|
||||
"collision_free_success": 0.51953125,
|
||||
"time_ok_1p3": 0.26171875,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.5444939136505127,
|
||||
"time_mean_s": 2.4729323387145996,
|
||||
"brakes_mean": 19.97265625,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.2868920564651489,
|
||||
"err_fail_med": 202.12567138671875,
|
||||
"err_success_med": 0.22085000574588776,
|
||||
"dls_share": 0.62109375,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.0000016689300537,
|
||||
"speed_ratio_p95": 1.0000101327896118,
|
||||
"speed_ratio_max": 1.0000101327896118,
|
||||
"over_vmax_rate": 0.0,
|
||||
"accel_ratio_p95": 1.0000016689300537,
|
||||
"accel_ratio_max": 1.0000101327896118
|
||||
},
|
||||
"simulation_wall_s": 277.54961390000244
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round33_cap_rows.json",
|
||||
"out": "docs/experiment_round33_cap.json",
|
||||
"trace": "ml/runs/round33_cap.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 1,
|
||||
"gap_margin": 0.0,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.5234375,
|
||||
"collision_free_success": 0.5234375,
|
||||
"time_ok_1p3": 0.265625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.615795850753784,
|
||||
"time_mean_s": 2.486567258834839,
|
||||
"brakes_mean": 19.5546875,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.27920001745224,
|
||||
"err_fail_med": 204.71360778808594,
|
||||
"err_success_med": 0.21751396358013153,
|
||||
"dls_share": 0.61328125,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.0000016689300537,
|
||||
"speed_ratio_p95": 1.0000101327896118,
|
||||
"speed_ratio_max": 1.0000101327896118,
|
||||
"over_vmax_rate": 0.0,
|
||||
"accel_ratio_p95": 1.0000016689300537,
|
||||
"accel_ratio_max": 1.0000101327896118
|
||||
},
|
||||
"simulation_wall_s": 325.18856850000157
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"torch": "2.10.0+cu130",
|
||||
"act_scale": 1.0,
|
||||
"cfg": {},
|
||||
"B": 1,
|
||||
"commit_n": 2,
|
||||
"fp64_note": "3090 fp64 \u541e\u5410\u7ea6 fp32 \u7684 1/54\uff1b\u8ba4\u8bc1\u76ee\u524d\u8981\u6c42 fp64",
|
||||
"inference": {
|
||||
"sample_K1_steps1": {
|
||||
"p50": 7.453,
|
||||
"p95": 9.912,
|
||||
"p99": 10.766,
|
||||
"max": 11.109,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K8_steps1": {
|
||||
"p50": 8.208,
|
||||
"p95": 10.685,
|
||||
"p99": 11.22,
|
||||
"max": 15.224,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K1_steps2": {
|
||||
"p50": 13.149,
|
||||
"p95": 17.621,
|
||||
"p99": 18.438,
|
||||
"max": 19.296,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K8_steps2": {
|
||||
"p50": 14.167,
|
||||
"p95": 19.241,
|
||||
"p99": 24.77,
|
||||
"max": 25.703,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K1_steps4": {
|
||||
"p50": 19.193,
|
||||
"p95": 22.338,
|
||||
"p99": 23.666,
|
||||
"max": 25.787,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K8_steps4": {
|
||||
"p50": 21.509,
|
||||
"p95": 24.732,
|
||||
"p99": 35.655,
|
||||
"max": 41.514,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K1_steps8": {
|
||||
"p50": 40.658,
|
||||
"p95": 55.162,
|
||||
"p99": 60.394,
|
||||
"max": 60.398,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K8_steps8": {
|
||||
"p50": 40.416,
|
||||
"p95": 47.997,
|
||||
"p99": 62.49,
|
||||
"max": 71.976,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K1_steps16": {
|
||||
"p50": 72.429,
|
||||
"p95": 90.188,
|
||||
"p99": 107.536,
|
||||
"max": 112.674,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_K8_steps16": {
|
||||
"p50": 79.893,
|
||||
"p95": 99.933,
|
||||
"p99": 105.746,
|
||||
"max": 106.078,
|
||||
"reps": 120
|
||||
}
|
||||
},
|
||||
"encode_once": {
|
||||
"p50": 2.265,
|
||||
"p95": 4.001,
|
||||
"p99": 6.086,
|
||||
"max": 6.136,
|
||||
"reps": 120
|
||||
},
|
||||
"tiny_op_ms": 0.028,
|
||||
"sync_only_ms": 0.005,
|
||||
"components": {
|
||||
"features_f32": {
|
||||
"p50": 12.443,
|
||||
"p95": 14.227,
|
||||
"p99": 19.678,
|
||||
"max": 20.531,
|
||||
"reps": 120
|
||||
},
|
||||
"endpoint_score_K8_f32": {
|
||||
"p50": 4.278,
|
||||
"p95": 5.841,
|
||||
"p99": 6.322,
|
||||
"max": 8.883,
|
||||
"reps": 120
|
||||
},
|
||||
"dls_fb_1step_f64": {
|
||||
"p50": 7.191,
|
||||
"p95": 12.26,
|
||||
"p99": 14.642,
|
||||
"max": 15.384,
|
||||
"reps": 120
|
||||
},
|
||||
"dls_fb_1step_f32": {
|
||||
"p50": 7.619,
|
||||
"p95": 10.142,
|
||||
"p99": 11.597,
|
||||
"max": 12.022,
|
||||
"reps": 120
|
||||
},
|
||||
"discrete_cert_4pts_f32": {
|
||||
"p50": 8.214,
|
||||
"p95": 13.365,
|
||||
"p99": 14.235,
|
||||
"max": 14.991,
|
||||
"reps": 120
|
||||
},
|
||||
"cert_n8_f64_2roots": {
|
||||
"p50": 11.265,
|
||||
"p95": 14.64,
|
||||
"p99": 16.788,
|
||||
"max": 17.761,
|
||||
"reps": 120
|
||||
},
|
||||
"cert_n16_f64_2roots": {
|
||||
"p50": 14.574,
|
||||
"p95": 16.731,
|
||||
"p99": 18.459,
|
||||
"max": 22.346,
|
||||
"reps": 120
|
||||
},
|
||||
"cert_n32_f64_2roots": {
|
||||
"p50": 21.528,
|
||||
"p95": 24.746,
|
||||
"p99": 28.037,
|
||||
"max": 28.066,
|
||||
"reps": 120
|
||||
}
|
||||
},
|
||||
"cycle_K8_steps16_discrete": {
|
||||
"p50": 116.474,
|
||||
"p95": 134.474,
|
||||
"p99": 142.593,
|
||||
"max": 152.768,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_steps4_discrete": {
|
||||
"p50": 55.919,
|
||||
"p95": 65.767,
|
||||
"p99": 74.809,
|
||||
"max": 78.059,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K1_steps4_discrete": {
|
||||
"p50": 51.087,
|
||||
"p95": 58.614,
|
||||
"p99": 62.326,
|
||||
"max": 63.919,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K1_steps1_discrete": {
|
||||
"p50": 34.493,
|
||||
"p95": 42.078,
|
||||
"p99": 43.906,
|
||||
"max": 50.268,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_steps16_cert_n8": {
|
||||
"p50": 132.027,
|
||||
"p95": 150.463,
|
||||
"p99": 160.83,
|
||||
"max": 192.586,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_steps4_cert_n8": {
|
||||
"p50": 64.812,
|
||||
"p95": 79.192,
|
||||
"p99": 89.629,
|
||||
"max": 95.837,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K1_steps4_cert_n16": {
|
||||
"p50": 61.967,
|
||||
"p95": 73.219,
|
||||
"p99": 79.053,
|
||||
"max": 83.18,
|
||||
"reps": 120
|
||||
},
|
||||
"f32_vs_f64": {
|
||||
"points": 3,
|
||||
"max_abs_err_mm": 0.0,
|
||||
"sign_flips": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"B": 1,
|
||||
"steps_used_by_eval": 1,
|
||||
"note": "eval_cl \u4ece\u4e0d\u4f20 steps\uff0crollout \u9ed8\u8ba4 steps=1\uff08MeanFlow \u4e00\u6b65\u751f\u6210\uff09\uff1b\u6240\u4ee5\u771f\u5b9e\u5468\u671f\u6309 steps=1 \u6d4b",
|
||||
"parts": {
|
||||
"features_total": {
|
||||
"p50": 12.589,
|
||||
"p95": 18.334,
|
||||
"p99": 20.217,
|
||||
"max": 23.649,
|
||||
"reps": 120
|
||||
},
|
||||
"features_gm_groups_only": {
|
||||
"p50": 6.817,
|
||||
"p95": 8.771,
|
||||
"p99": 10.254,
|
||||
"max": 17.169,
|
||||
"reps": 120
|
||||
},
|
||||
"features_gaps40_all_groups": {
|
||||
"p50": 7.245,
|
||||
"p95": 10.501,
|
||||
"p99": 13.794,
|
||||
"max": 14.508,
|
||||
"reps": 120
|
||||
},
|
||||
"features_fk_only": {
|
||||
"p50": 3.708,
|
||||
"p95": 5.024,
|
||||
"p99": 5.991,
|
||||
"max": 6.301,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_k_K8_steps1": {
|
||||
"p50": 7.799,
|
||||
"p95": 11.655,
|
||||
"p99": 13.342,
|
||||
"max": 16.212,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_k_K1_steps1": {
|
||||
"p50": 7.205,
|
||||
"p95": 8.935,
|
||||
"p99": 9.615,
|
||||
"max": 13.92,
|
||||
"reps": 120
|
||||
},
|
||||
"sample_k_K4_steps1": {
|
||||
"p50": 7.834,
|
||||
"p95": 10.354,
|
||||
"p99": 14.744,
|
||||
"max": 15.241,
|
||||
"reps": 120
|
||||
},
|
||||
"dls_fb_f64": {
|
||||
"p50": 6.729,
|
||||
"p95": 9.297,
|
||||
"p99": 11.186,
|
||||
"max": 12.064,
|
||||
"reps": 120
|
||||
},
|
||||
"discrete_cert_4pts": {
|
||||
"p50": 8.603,
|
||||
"p95": 11.181,
|
||||
"p99": 12.464,
|
||||
"max": 15.799,
|
||||
"reps": 120
|
||||
},
|
||||
"cert_uniform_n8_2roots_f64": {
|
||||
"p50": 11.479,
|
||||
"p95": 19.221,
|
||||
"p99": 20.942,
|
||||
"max": 21.183,
|
||||
"reps": 120
|
||||
},
|
||||
"cert_uniform_n16_2roots_f64": {
|
||||
"p50": 16.138,
|
||||
"p95": 21.972,
|
||||
"p99": 23.898,
|
||||
"max": 25.793,
|
||||
"reps": 120
|
||||
}
|
||||
},
|
||||
"cycles": {
|
||||
"cycle_K8_full_discrete": {
|
||||
"p50": 40.676,
|
||||
"p95": 51.742,
|
||||
"p99": 59.782,
|
||||
"max": 62.074,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K4_full_discrete": {
|
||||
"p50": 43.997,
|
||||
"p95": 56.145,
|
||||
"p99": 59.905,
|
||||
"max": 65.882,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K1_full_discrete": {
|
||||
"p50": 37.87,
|
||||
"p95": 45.754,
|
||||
"p99": 49.394,
|
||||
"max": 53.139,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_noscore_discrete": {
|
||||
"p50": 37.427,
|
||||
"p95": 49.602,
|
||||
"p99": 60.777,
|
||||
"max": 70.139,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_full_cert_n8": {
|
||||
"p50": 43.984,
|
||||
"p95": 65.795,
|
||||
"p99": 75.53,
|
||||
"max": 95.452,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_full_cert_n16": {
|
||||
"p50": 51.054,
|
||||
"p95": 74.862,
|
||||
"p99": 95.768,
|
||||
"max": 106.48,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_fused_estimate": {
|
||||
"p50": 24.054,
|
||||
"p95": 26.595,
|
||||
"p99": 30.17,
|
||||
"max": 33.284,
|
||||
"reps": 120
|
||||
},
|
||||
"cycle_K8_fused_cert_n8": {
|
||||
"p50": 34.274,
|
||||
"p95": 46.329,
|
||||
"p99": 72.737,
|
||||
"max": 79.022,
|
||||
"reps": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": "ml/runs/round33_margin_rows.json",
|
||||
"out": "docs/experiment_round33_margin.json",
|
||||
"trace": "ml/runs/round33_margin.pt",
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 0,
|
||||
"gap_margin": 0.5,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.5625,
|
||||
"collision_free_success": 0.5625,
|
||||
"time_ok_1p3": 0.37890625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.448458433151245,
|
||||
"time_mean_s": 2.2555553913116455,
|
||||
"brakes_mean": 20.046875,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.18239997327327728,
|
||||
"err_fail_med": 225.54278564453125,
|
||||
"err_success_med": 0.18544802069664001,
|
||||
"dls_share": 0.62890625,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.6600004434585571,
|
||||
"speed_ratio_p95": 1.660003662109375,
|
||||
"speed_ratio_max": 1.660003662109375,
|
||||
"over_vmax_rate": 1.0,
|
||||
"accel_ratio_p95": 1.5889430046081543,
|
||||
"accel_ratio_max": 1.6600035429000854
|
||||
},
|
||||
"simulation_wall_s": 298.24517190000915
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": null,
|
||||
"out": "docs/experiment_round34_cert_batch_check.json",
|
||||
"trace": null,
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 0,
|
||||
"gap_margin": 0.0,
|
||||
"cert_batch": 1,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.57421875,
|
||||
"collision_free_success": 0.57421875,
|
||||
"time_ok_1p3": 0.390625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.4449527263641357,
|
||||
"time_mean_s": 2.3076188564300537,
|
||||
"brakes_mean": 19.25,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.20997506380081177,
|
||||
"err_fail_med": 229.68746948242188,
|
||||
"err_success_med": 0.18899640440940857,
|
||||
"dls_share": 0.625,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.6600004434585571,
|
||||
"speed_ratio_p95": 1.6600035429000854,
|
||||
"speed_ratio_max": 1.660003662109375,
|
||||
"over_vmax_rate": 1.0,
|
||||
"accel_ratio_p95": 1.5835107564926147,
|
||||
"accel_ratio_max": 1.6600004434585571
|
||||
},
|
||||
"simulation_wall_s": 306.9490996999957
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"kin_fp": "2ebcbf17",
|
||||
"segments": 48,
|
||||
"n_subsegments": 8,
|
||||
"wide_cap_mm": 2.0,
|
||||
"gap_abs_err_mm": {
|
||||
"p50": 0.0,
|
||||
"p99": 3.661949063912929e-05,
|
||||
"p999": 7.4360332600075e-05,
|
||||
"max": 0.00013247109886016517
|
||||
},
|
||||
"signed_err_mm": {
|
||||
"min": -0.00010937313134817828,
|
||||
"max": 0.00013247109886016517,
|
||||
"overestimate_count": 1,
|
||||
"overestimate_gt_1e-3": 0
|
||||
},
|
||||
"sign_flips": 0,
|
||||
"accepted_f64": 24,
|
||||
"accepted_f32": 24,
|
||||
"unsafe_direction_f32_only": 0,
|
||||
"conservative_f64_only": 0,
|
||||
"per_group_pair_err_p99": [
|
||||
0.0,
|
||||
7.6e-05,
|
||||
0.0001,
|
||||
7e-05,
|
||||
7.1e-05,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.000132,
|
||||
6.7e-05,
|
||||
5.1e-05,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"note": "wide_cap 2.0 mm\uff1a\u8ba4\u8bc1\u5b9e\u9645\u7528 bound+2eps \u7684\u81ea\u9002\u5e94 cap\uff0c\u8fd9\u91cc\u53d6\u56fa\u5b9a 2 mm \u4ee5\u8986\u76d6\u88ab\u68c0\u9a8c\u7684\u6bb5\uff1b\u82e5 max \u8bef\u5dee\u8fdc\u5c0f\u4e8e 1e-4 \u7684 eps\uff0c\u5224\u636e\u5728 f32 \u4e0b\u4ecd\u6210\u7acb\u9700\u518d\u786e\u8ba4 f32 \u7684\u95f4\u9699\u672c\u8eab\u8bef\u5dee\u7b26\u53f7\u65b9\u5411\u3002"
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"B": 1,
|
||||
"K": 8,
|
||||
"steps": 1,
|
||||
"reps": 90,
|
||||
"design": "\u540c\u4e00\u6279 4 \u4e2a\u53d8\u4f53\u9010\u8f6e\u4ea4\u66ff\uff08\u5947\u6570\u8f6e\u53cd\u5e8f\uff09\uff0c\u6d88\u9664 GPU \u65f6\u949f\u6f02\u79fb\uff1b\u539f\u59cb\u8017\u65f6\u5168\u90e8\u843d\u76d8",
|
||||
"equivalence": {
|
||||
"features_reuse_bitwise_equal": true,
|
||||
"gm_groups_vs_full_table_max_abs_mm": 0.0,
|
||||
"cert_matrix_bitwise_equal_at_same_cap": true,
|
||||
"shared_xf_cert_bitwise_equal_at_cap1": true,
|
||||
"max_abs_shared_diff_mm": 0.0,
|
||||
"max_abs_cert_diff_mm": 0.0,
|
||||
"decision_equal_by_threshold": {
|
||||
"thr_0.0": true,
|
||||
"thr_0.5": true,
|
||||
"thr_1.0": true
|
||||
},
|
||||
"per_point_min_cur_mm": [
|
||||
8.833421,
|
||||
8.849185,
|
||||
8.859405,
|
||||
8.83829
|
||||
],
|
||||
"per_point_min_fused_mm": [
|
||||
8.833421,
|
||||
8.849185,
|
||||
8.859405,
|
||||
8.83829
|
||||
]
|
||||
},
|
||||
"results": {
|
||||
"current_discrete": {
|
||||
"p50": 32.932,
|
||||
"p95": 42.589,
|
||||
"p99": 55.568,
|
||||
"max": 55.568,
|
||||
"hz_p50": 30.4,
|
||||
"raw_ms": [
|
||||
30.458,
|
||||
30.624,
|
||||
30.789,
|
||||
31.17,
|
||||
31.229,
|
||||
31.283,
|
||||
31.371,
|
||||
31.394,
|
||||
31.454,
|
||||
31.473,
|
||||
31.564,
|
||||
31.58,
|
||||
31.719,
|
||||
31.772,
|
||||
31.78,
|
||||
31.785,
|
||||
31.859,
|
||||
31.875,
|
||||
31.906,
|
||||
31.973,
|
||||
31.979,
|
||||
32.002,
|
||||
32.017,
|
||||
32.063,
|
||||
32.099,
|
||||
32.113,
|
||||
32.123,
|
||||
32.127,
|
||||
32.13,
|
||||
32.137,
|
||||
32.191,
|
||||
32.286,
|
||||
32.308,
|
||||
32.324,
|
||||
32.33,
|
||||
32.388,
|
||||
32.48,
|
||||
32.508,
|
||||
32.564,
|
||||
32.63,
|
||||
32.64,
|
||||
32.702,
|
||||
32.78,
|
||||
32.822,
|
||||
32.834,
|
||||
32.932,
|
||||
32.964,
|
||||
32.975,
|
||||
32.996,
|
||||
33.071,
|
||||
33.163,
|
||||
33.191,
|
||||
33.192,
|
||||
33.402,
|
||||
33.477,
|
||||
33.509,
|
||||
33.583,
|
||||
33.956,
|
||||
34.014,
|
||||
34.04,
|
||||
34.047,
|
||||
34.047,
|
||||
34.154,
|
||||
34.302,
|
||||
34.595,
|
||||
34.658,
|
||||
34.943,
|
||||
35.296,
|
||||
35.522,
|
||||
35.67,
|
||||
35.757,
|
||||
35.828,
|
||||
35.859,
|
||||
36.459,
|
||||
36.503,
|
||||
36.562,
|
||||
36.956,
|
||||
37.047,
|
||||
37.067,
|
||||
37.142,
|
||||
37.442,
|
||||
38.632,
|
||||
39.285,
|
||||
41.269,
|
||||
41.932,
|
||||
42.589,
|
||||
43.199,
|
||||
43.329,
|
||||
54.407,
|
||||
55.568
|
||||
]
|
||||
},
|
||||
"fused_discrete": {
|
||||
"p50": 24.515,
|
||||
"p95": 30.02,
|
||||
"p99": 42.085,
|
||||
"max": 42.085,
|
||||
"hz_p50": 40.8,
|
||||
"raw_ms": [
|
||||
22.146,
|
||||
22.266,
|
||||
22.409,
|
||||
22.653,
|
||||
22.66,
|
||||
22.695,
|
||||
22.76,
|
||||
22.827,
|
||||
22.848,
|
||||
23.001,
|
||||
23.033,
|
||||
23.087,
|
||||
23.249,
|
||||
23.36,
|
||||
23.488,
|
||||
23.573,
|
||||
23.607,
|
||||
23.625,
|
||||
23.649,
|
||||
23.651,
|
||||
23.652,
|
||||
23.652,
|
||||
23.656,
|
||||
23.676,
|
||||
23.716,
|
||||
23.737,
|
||||
23.745,
|
||||
23.795,
|
||||
23.871,
|
||||
23.883,
|
||||
23.92,
|
||||
23.922,
|
||||
23.945,
|
||||
23.959,
|
||||
24.046,
|
||||
24.13,
|
||||
24.184,
|
||||
24.282,
|
||||
24.291,
|
||||
24.313,
|
||||
24.318,
|
||||
24.319,
|
||||
24.32,
|
||||
24.34,
|
||||
24.399,
|
||||
24.515,
|
||||
24.532,
|
||||
24.541,
|
||||
24.552,
|
||||
24.592,
|
||||
24.618,
|
||||
24.731,
|
||||
24.741,
|
||||
24.745,
|
||||
24.766,
|
||||
24.807,
|
||||
24.813,
|
||||
24.898,
|
||||
25.066,
|
||||
25.088,
|
||||
25.137,
|
||||
25.18,
|
||||
25.223,
|
||||
25.376,
|
||||
25.474,
|
||||
25.603,
|
||||
25.68,
|
||||
25.744,
|
||||
25.82,
|
||||
25.936,
|
||||
25.95,
|
||||
25.953,
|
||||
26.002,
|
||||
26.174,
|
||||
26.312,
|
||||
26.88,
|
||||
27.036,
|
||||
27.209,
|
||||
27.329,
|
||||
27.424,
|
||||
27.715,
|
||||
28.796,
|
||||
28.824,
|
||||
29.427,
|
||||
29.662,
|
||||
30.02,
|
||||
31.515,
|
||||
31.839,
|
||||
36.18,
|
||||
42.085
|
||||
]
|
||||
},
|
||||
"shared_xf_discrete": {
|
||||
"p50": 26.864,
|
||||
"p95": 34.544,
|
||||
"p99": 67.243,
|
||||
"max": 67.243,
|
||||
"hz_p50": 37.2,
|
||||
"raw_ms": [
|
||||
24.433,
|
||||
24.441,
|
||||
24.585,
|
||||
24.777,
|
||||
24.862,
|
||||
24.935,
|
||||
25.054,
|
||||
25.08,
|
||||
25.105,
|
||||
25.193,
|
||||
25.313,
|
||||
25.44,
|
||||
25.466,
|
||||
25.483,
|
||||
25.516,
|
||||
25.554,
|
||||
25.577,
|
||||
25.689,
|
||||
25.702,
|
||||
25.708,
|
||||
25.854,
|
||||
25.896,
|
||||
25.898,
|
||||
26.011,
|
||||
26.097,
|
||||
26.175,
|
||||
26.255,
|
||||
26.289,
|
||||
26.297,
|
||||
26.306,
|
||||
26.328,
|
||||
26.336,
|
||||
26.346,
|
||||
26.375,
|
||||
26.397,
|
||||
26.456,
|
||||
26.502,
|
||||
26.523,
|
||||
26.563,
|
||||
26.613,
|
||||
26.634,
|
||||
26.658,
|
||||
26.708,
|
||||
26.752,
|
||||
26.798,
|
||||
26.864,
|
||||
26.869,
|
||||
26.874,
|
||||
27.315,
|
||||
27.341,
|
||||
27.668,
|
||||
27.67,
|
||||
27.732,
|
||||
27.774,
|
||||
27.812,
|
||||
27.84,
|
||||
27.882,
|
||||
27.921,
|
||||
28.24,
|
||||
28.302,
|
||||
28.421,
|
||||
28.694,
|
||||
28.722,
|
||||
28.739,
|
||||
28.817,
|
||||
28.914,
|
||||
28.944,
|
||||
28.998,
|
||||
29.12,
|
||||
29.122,
|
||||
29.259,
|
||||
29.292,
|
||||
29.334,
|
||||
29.658,
|
||||
29.847,
|
||||
30.181,
|
||||
30.206,
|
||||
30.292,
|
||||
30.492,
|
||||
30.782,
|
||||
31.168,
|
||||
31.222,
|
||||
31.607,
|
||||
31.614,
|
||||
32.187,
|
||||
34.544,
|
||||
35.274,
|
||||
35.565,
|
||||
35.915,
|
||||
67.243
|
||||
]
|
||||
},
|
||||
"current_cert_n8": {
|
||||
"p50": 42.624,
|
||||
"p95": 49.434,
|
||||
"p99": 57.771,
|
||||
"max": 57.771,
|
||||
"hz_p50": 23.5,
|
||||
"raw_ms": [
|
||||
38.546,
|
||||
38.666,
|
||||
39.152,
|
||||
39.265,
|
||||
39.344,
|
||||
39.441,
|
||||
39.584,
|
||||
39.626,
|
||||
39.738,
|
||||
39.841,
|
||||
40.228,
|
||||
40.306,
|
||||
40.514,
|
||||
40.805,
|
||||
40.838,
|
||||
40.875,
|
||||
40.884,
|
||||
40.907,
|
||||
40.952,
|
||||
40.971,
|
||||
41.185,
|
||||
41.193,
|
||||
41.193,
|
||||
41.277,
|
||||
41.279,
|
||||
41.422,
|
||||
41.448,
|
||||
41.463,
|
||||
41.465,
|
||||
41.473,
|
||||
41.504,
|
||||
41.551,
|
||||
41.684,
|
||||
41.733,
|
||||
41.841,
|
||||
41.947,
|
||||
41.967,
|
||||
42.011,
|
||||
42.077,
|
||||
42.241,
|
||||
42.249,
|
||||
42.269,
|
||||
42.301,
|
||||
42.314,
|
||||
42.388,
|
||||
42.624,
|
||||
42.661,
|
||||
42.767,
|
||||
42.773,
|
||||
42.837,
|
||||
42.865,
|
||||
42.87,
|
||||
42.913,
|
||||
43.343,
|
||||
43.35,
|
||||
43.356,
|
||||
43.376,
|
||||
43.501,
|
||||
43.695,
|
||||
43.708,
|
||||
43.842,
|
||||
43.923,
|
||||
43.972,
|
||||
44.243,
|
||||
44.476,
|
||||
44.536,
|
||||
44.648,
|
||||
44.745,
|
||||
44.894,
|
||||
45.034,
|
||||
45.144,
|
||||
45.236,
|
||||
45.697,
|
||||
45.859,
|
||||
45.944,
|
||||
46.039,
|
||||
46.126,
|
||||
46.467,
|
||||
46.575,
|
||||
46.887,
|
||||
46.983,
|
||||
47.234,
|
||||
47.621,
|
||||
47.685,
|
||||
48.547,
|
||||
49.434,
|
||||
49.683,
|
||||
50.646,
|
||||
54.754,
|
||||
57.771
|
||||
]
|
||||
},
|
||||
"fused_cert_n8": {
|
||||
"p50": 24.826,
|
||||
"p95": 31.721,
|
||||
"p99": 49.398,
|
||||
"max": 49.398,
|
||||
"hz_p50": 40.3,
|
||||
"raw_ms": [
|
||||
22.127,
|
||||
22.553,
|
||||
22.598,
|
||||
22.624,
|
||||
22.789,
|
||||
22.798,
|
||||
22.878,
|
||||
22.983,
|
||||
23.002,
|
||||
23.134,
|
||||
23.192,
|
||||
23.273,
|
||||
23.323,
|
||||
23.345,
|
||||
23.377,
|
||||
23.445,
|
||||
23.466,
|
||||
23.549,
|
||||
23.611,
|
||||
23.613,
|
||||
23.861,
|
||||
23.868,
|
||||
23.885,
|
||||
23.885,
|
||||
23.906,
|
||||
23.918,
|
||||
23.919,
|
||||
23.933,
|
||||
23.947,
|
||||
24.052,
|
||||
24.064,
|
||||
24.067,
|
||||
24.08,
|
||||
24.103,
|
||||
24.13,
|
||||
24.164,
|
||||
24.249,
|
||||
24.301,
|
||||
24.429,
|
||||
24.515,
|
||||
24.603,
|
||||
24.609,
|
||||
24.7,
|
||||
24.748,
|
||||
24.812,
|
||||
24.826,
|
||||
24.855,
|
||||
24.858,
|
||||
24.954,
|
||||
24.99,
|
||||
25.052,
|
||||
25.097,
|
||||
25.195,
|
||||
25.209,
|
||||
25.241,
|
||||
25.252,
|
||||
25.264,
|
||||
25.298,
|
||||
25.63,
|
||||
25.659,
|
||||
25.767,
|
||||
25.804,
|
||||
25.928,
|
||||
25.976,
|
||||
26.494,
|
||||
26.871,
|
||||
26.886,
|
||||
26.988,
|
||||
26.995,
|
||||
27.004,
|
||||
27.307,
|
||||
27.322,
|
||||
27.352,
|
||||
27.937,
|
||||
28.144,
|
||||
28.165,
|
||||
28.216,
|
||||
28.288,
|
||||
28.331,
|
||||
29.37,
|
||||
29.611,
|
||||
30.6,
|
||||
30.704,
|
||||
31.026,
|
||||
31.552,
|
||||
31.721,
|
||||
32.875,
|
||||
34.001,
|
||||
40.223,
|
||||
49.398
|
||||
]
|
||||
},
|
||||
"shared_xf_cert_n8": {
|
||||
"p50": 36.185,
|
||||
"p95": 41.136,
|
||||
"p99": 64.287,
|
||||
"max": 64.287,
|
||||
"hz_p50": 27.6,
|
||||
"raw_ms": [
|
||||
33.051,
|
||||
33.153,
|
||||
33.201,
|
||||
33.406,
|
||||
33.819,
|
||||
33.845,
|
||||
33.94,
|
||||
34.083,
|
||||
34.145,
|
||||
34.235,
|
||||
34.3,
|
||||
34.372,
|
||||
34.605,
|
||||
34.697,
|
||||
34.714,
|
||||
34.755,
|
||||
34.817,
|
||||
34.855,
|
||||
34.925,
|
||||
34.957,
|
||||
34.963,
|
||||
34.967,
|
||||
34.986,
|
||||
35.167,
|
||||
35.239,
|
||||
35.314,
|
||||
35.325,
|
||||
35.402,
|
||||
35.444,
|
||||
35.518,
|
||||
35.544,
|
||||
35.582,
|
||||
35.594,
|
||||
35.662,
|
||||
35.675,
|
||||
35.724,
|
||||
35.775,
|
||||
35.874,
|
||||
35.908,
|
||||
35.929,
|
||||
35.965,
|
||||
36.019,
|
||||
36.04,
|
||||
36.126,
|
||||
36.164,
|
||||
36.185,
|
||||
36.2,
|
||||
36.259,
|
||||
36.263,
|
||||
36.266,
|
||||
36.301,
|
||||
36.324,
|
||||
36.383,
|
||||
36.393,
|
||||
36.609,
|
||||
36.667,
|
||||
36.696,
|
||||
36.779,
|
||||
36.799,
|
||||
36.836,
|
||||
36.881,
|
||||
36.978,
|
||||
37.001,
|
||||
37.033,
|
||||
37.043,
|
||||
37.078,
|
||||
37.183,
|
||||
37.206,
|
||||
37.278,
|
||||
37.3,
|
||||
37.411,
|
||||
37.485,
|
||||
37.722,
|
||||
37.923,
|
||||
37.965,
|
||||
38.197,
|
||||
38.588,
|
||||
38.871,
|
||||
39.082,
|
||||
39.099,
|
||||
39.135,
|
||||
39.351,
|
||||
39.505,
|
||||
40.204,
|
||||
40.892,
|
||||
41.136,
|
||||
41.449,
|
||||
42.611,
|
||||
44.687,
|
||||
64.287
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"B": 1,
|
||||
"K": 8,
|
||||
"commit_n": 2,
|
||||
"reps": 80,
|
||||
"design": "5 \u4e2a\u53d8\u4f53\u9010\u8f6e\u4ea4\u66ff\uff08\u5947\u6570\u8f6e\u53cd\u5e8f\uff09\uff1b\u53ea\u6d4b\u53ef\u5b9e\u73b0\u7684\u53e3\u5f84\uff1a\u7279\u5f81/\u63a8\u7406/\u8ba4\u8bc1\u6309\u771f\u5b9e\u987a\u5e8f",
|
||||
"equivalence": {
|
||||
"batch_cert_matrix_equal_to_loop": true,
|
||||
"features_xf_reuse_bitwise_equal": true
|
||||
},
|
||||
"results": {
|
||||
"A_today": {
|
||||
"p50": 33.494,
|
||||
"p95": 46.143,
|
||||
"p99": 64.693,
|
||||
"max": 64.693,
|
||||
"raw_ms": [
|
||||
28.984,
|
||||
29.251,
|
||||
29.361,
|
||||
29.549,
|
||||
29.899,
|
||||
29.944,
|
||||
30.181,
|
||||
30.605,
|
||||
30.624,
|
||||
30.744,
|
||||
30.815,
|
||||
30.838,
|
||||
30.862,
|
||||
30.9,
|
||||
31.024,
|
||||
31.092,
|
||||
31.115,
|
||||
31.262,
|
||||
31.433,
|
||||
31.463,
|
||||
31.52,
|
||||
31.572,
|
||||
31.642,
|
||||
31.741,
|
||||
31.871,
|
||||
31.902,
|
||||
31.924,
|
||||
31.933,
|
||||
31.976,
|
||||
32.001,
|
||||
32.03,
|
||||
32.06,
|
||||
32.285,
|
||||
32.384,
|
||||
32.624,
|
||||
32.825,
|
||||
32.919,
|
||||
33.063,
|
||||
33.168,
|
||||
33.197,
|
||||
33.494,
|
||||
33.589,
|
||||
33.624,
|
||||
33.632,
|
||||
33.666,
|
||||
33.708,
|
||||
33.727,
|
||||
33.735,
|
||||
33.754,
|
||||
33.773,
|
||||
33.869,
|
||||
33.907,
|
||||
34.116,
|
||||
34.219,
|
||||
34.314,
|
||||
34.337,
|
||||
34.897,
|
||||
34.957,
|
||||
35.436,
|
||||
35.469,
|
||||
35.999,
|
||||
36.106,
|
||||
36.523,
|
||||
36.559,
|
||||
36.846,
|
||||
36.866,
|
||||
36.922,
|
||||
37.354,
|
||||
37.375,
|
||||
37.581,
|
||||
37.933,
|
||||
38.026,
|
||||
40.541,
|
||||
40.882,
|
||||
44.161,
|
||||
44.233,
|
||||
46.143,
|
||||
54.047,
|
||||
58.36,
|
||||
64.693
|
||||
]
|
||||
},
|
||||
"B_xf_reuse": {
|
||||
"p50": 30.122,
|
||||
"p95": 42.349,
|
||||
"p99": 66.368,
|
||||
"max": 66.368,
|
||||
"raw_ms": [
|
||||
26.698,
|
||||
26.958,
|
||||
26.984,
|
||||
27.007,
|
||||
27.018,
|
||||
27.158,
|
||||
27.308,
|
||||
27.369,
|
||||
27.987,
|
||||
28.066,
|
||||
28.219,
|
||||
28.288,
|
||||
28.362,
|
||||
28.517,
|
||||
28.537,
|
||||
28.539,
|
||||
28.593,
|
||||
28.611,
|
||||
28.638,
|
||||
28.735,
|
||||
28.828,
|
||||
28.871,
|
||||
28.922,
|
||||
28.945,
|
||||
28.978,
|
||||
29.088,
|
||||
29.202,
|
||||
29.226,
|
||||
29.226,
|
||||
29.31,
|
||||
29.497,
|
||||
29.506,
|
||||
29.531,
|
||||
29.549,
|
||||
29.649,
|
||||
29.875,
|
||||
29.978,
|
||||
29.992,
|
||||
30.025,
|
||||
30.102,
|
||||
30.122,
|
||||
30.243,
|
||||
30.245,
|
||||
30.263,
|
||||
30.407,
|
||||
30.578,
|
||||
30.665,
|
||||
30.717,
|
||||
30.863,
|
||||
30.884,
|
||||
30.889,
|
||||
30.95,
|
||||
31.023,
|
||||
31.314,
|
||||
31.525,
|
||||
31.537,
|
||||
31.543,
|
||||
31.549,
|
||||
31.62,
|
||||
32.011,
|
||||
32.098,
|
||||
32.168,
|
||||
32.188,
|
||||
32.22,
|
||||
32.578,
|
||||
32.818,
|
||||
32.851,
|
||||
32.929,
|
||||
32.991,
|
||||
33.76,
|
||||
33.805,
|
||||
33.831,
|
||||
33.94,
|
||||
34.124,
|
||||
34.891,
|
||||
39.001,
|
||||
42.349,
|
||||
43.636,
|
||||
62.045,
|
||||
66.368
|
||||
]
|
||||
},
|
||||
"C_batch_cert": {
|
||||
"p50": 38.048,
|
||||
"p95": 50.171,
|
||||
"p99": 59.86,
|
||||
"max": 59.86,
|
||||
"raw_ms": [
|
||||
33.625,
|
||||
33.736,
|
||||
33.848,
|
||||
34.235,
|
||||
34.49,
|
||||
34.917,
|
||||
34.918,
|
||||
35.108,
|
||||
35.407,
|
||||
35.919,
|
||||
36.023,
|
||||
36.035,
|
||||
36.087,
|
||||
36.263,
|
||||
36.318,
|
||||
36.531,
|
||||
36.627,
|
||||
36.638,
|
||||
36.722,
|
||||
36.778,
|
||||
36.791,
|
||||
36.801,
|
||||
36.816,
|
||||
36.859,
|
||||
36.97,
|
||||
36.998,
|
||||
37.029,
|
||||
37.055,
|
||||
37.057,
|
||||
37.093,
|
||||
37.208,
|
||||
37.318,
|
||||
37.408,
|
||||
37.438,
|
||||
37.463,
|
||||
37.501,
|
||||
37.503,
|
||||
37.605,
|
||||
37.63,
|
||||
37.984,
|
||||
38.048,
|
||||
38.283,
|
||||
38.347,
|
||||
38.524,
|
||||
38.547,
|
||||
38.667,
|
||||
38.816,
|
||||
38.995,
|
||||
39.028,
|
||||
39.34,
|
||||
39.617,
|
||||
39.679,
|
||||
39.845,
|
||||
39.852,
|
||||
39.871,
|
||||
39.894,
|
||||
39.923,
|
||||
40.195,
|
||||
40.514,
|
||||
41.421,
|
||||
41.684,
|
||||
41.804,
|
||||
41.879,
|
||||
41.933,
|
||||
42.011,
|
||||
42.127,
|
||||
42.483,
|
||||
42.551,
|
||||
42.656,
|
||||
43.038,
|
||||
43.248,
|
||||
43.269,
|
||||
45.68,
|
||||
46.514,
|
||||
46.755,
|
||||
48.909,
|
||||
50.171,
|
||||
50.906,
|
||||
55.33,
|
||||
59.86
|
||||
]
|
||||
},
|
||||
"D_both": {
|
||||
"p50": 34.87,
|
||||
"p95": 39.811,
|
||||
"p99": 48.258,
|
||||
"max": 48.258,
|
||||
"raw_ms": [
|
||||
30.044,
|
||||
30.934,
|
||||
31.005,
|
||||
31.007,
|
||||
31.064,
|
||||
31.492,
|
||||
31.675,
|
||||
31.758,
|
||||
32.114,
|
||||
32.153,
|
||||
32.338,
|
||||
32.374,
|
||||
32.442,
|
||||
32.494,
|
||||
32.681,
|
||||
32.882,
|
||||
32.942,
|
||||
32.991,
|
||||
33.167,
|
||||
33.18,
|
||||
33.302,
|
||||
33.305,
|
||||
33.342,
|
||||
33.449,
|
||||
33.58,
|
||||
33.694,
|
||||
33.739,
|
||||
33.784,
|
||||
33.832,
|
||||
33.835,
|
||||
33.866,
|
||||
33.961,
|
||||
34.037,
|
||||
34.135,
|
||||
34.315,
|
||||
34.395,
|
||||
34.465,
|
||||
34.556,
|
||||
34.656,
|
||||
34.819,
|
||||
34.87,
|
||||
34.98,
|
||||
35.047,
|
||||
35.176,
|
||||
35.19,
|
||||
35.37,
|
||||
35.461,
|
||||
35.528,
|
||||
35.557,
|
||||
35.59,
|
||||
35.643,
|
||||
35.716,
|
||||
35.742,
|
||||
35.804,
|
||||
35.991,
|
||||
36.018,
|
||||
36.063,
|
||||
36.105,
|
||||
36.293,
|
||||
36.312,
|
||||
36.326,
|
||||
36.537,
|
||||
36.541,
|
||||
36.602,
|
||||
36.717,
|
||||
36.934,
|
||||
36.945,
|
||||
37.088,
|
||||
37.141,
|
||||
37.46,
|
||||
37.561,
|
||||
37.608,
|
||||
37.765,
|
||||
37.904,
|
||||
37.974,
|
||||
39.253,
|
||||
39.811,
|
||||
40.076,
|
||||
46.504,
|
||||
48.258
|
||||
]
|
||||
},
|
||||
"E_both_cont_n8": {
|
||||
"p50": 43.725,
|
||||
"p95": 51.084,
|
||||
"p99": 65.681,
|
||||
"max": 65.681,
|
||||
"raw_ms": [
|
||||
37.733,
|
||||
39.087,
|
||||
39.154,
|
||||
39.238,
|
||||
39.36,
|
||||
39.454,
|
||||
39.514,
|
||||
40.167,
|
||||
40.44,
|
||||
40.837,
|
||||
40.86,
|
||||
40.868,
|
||||
41.237,
|
||||
41.276,
|
||||
41.346,
|
||||
41.408,
|
||||
41.551,
|
||||
41.815,
|
||||
41.824,
|
||||
41.912,
|
||||
41.932,
|
||||
41.973,
|
||||
42.074,
|
||||
42.296,
|
||||
42.341,
|
||||
42.365,
|
||||
42.435,
|
||||
42.505,
|
||||
42.592,
|
||||
42.598,
|
||||
42.637,
|
||||
42.85,
|
||||
43.045,
|
||||
43.048,
|
||||
43.18,
|
||||
43.209,
|
||||
43.359,
|
||||
43.361,
|
||||
43.373,
|
||||
43.411,
|
||||
43.725,
|
||||
43.728,
|
||||
43.787,
|
||||
43.833,
|
||||
43.985,
|
||||
44.133,
|
||||
44.185,
|
||||
44.401,
|
||||
44.458,
|
||||
44.585,
|
||||
44.689,
|
||||
44.819,
|
||||
44.841,
|
||||
44.86,
|
||||
45.081,
|
||||
45.087,
|
||||
45.55,
|
||||
45.701,
|
||||
45.939,
|
||||
46.06,
|
||||
46.128,
|
||||
46.264,
|
||||
46.368,
|
||||
46.43,
|
||||
46.957,
|
||||
47.145,
|
||||
47.231,
|
||||
47.256,
|
||||
47.463,
|
||||
47.637,
|
||||
47.819,
|
||||
47.863,
|
||||
48.382,
|
||||
48.87,
|
||||
49.676,
|
||||
50.884,
|
||||
51.084,
|
||||
51.17,
|
||||
53.859,
|
||||
65.681
|
||||
]
|
||||
},
|
||||
"F_worst_loop8": {
|
||||
"p50": 80.224,
|
||||
"p95": 88.436,
|
||||
"p99": 89.553,
|
||||
"max": 89.553,
|
||||
"raw_ms": [
|
||||
67.204,
|
||||
70.008,
|
||||
70.436,
|
||||
71.432,
|
||||
72.454,
|
||||
72.503,
|
||||
73.006,
|
||||
73.12,
|
||||
73.492,
|
||||
73.971,
|
||||
74.203,
|
||||
74.358,
|
||||
74.756,
|
||||
74.816,
|
||||
74.893,
|
||||
75.536,
|
||||
75.98,
|
||||
76.049,
|
||||
76.164,
|
||||
76.25,
|
||||
76.547,
|
||||
76.669,
|
||||
76.843,
|
||||
77.268,
|
||||
77.534,
|
||||
77.658,
|
||||
77.872,
|
||||
78.149,
|
||||
78.343,
|
||||
78.377,
|
||||
78.445,
|
||||
78.483,
|
||||
78.86,
|
||||
78.863,
|
||||
78.905,
|
||||
79.048,
|
||||
79.315,
|
||||
79.857,
|
||||
80.035,
|
||||
80.041,
|
||||
80.224,
|
||||
80.602,
|
||||
80.644,
|
||||
80.677,
|
||||
80.848,
|
||||
81.061,
|
||||
81.094,
|
||||
81.155,
|
||||
81.243,
|
||||
81.384,
|
||||
81.628,
|
||||
81.679,
|
||||
81.75,
|
||||
82.117,
|
||||
82.148,
|
||||
82.363,
|
||||
82.444,
|
||||
82.511,
|
||||
82.651,
|
||||
82.828,
|
||||
82.921,
|
||||
83.371,
|
||||
83.592,
|
||||
83.894,
|
||||
83.926,
|
||||
84.386,
|
||||
84.519,
|
||||
84.608,
|
||||
85.06,
|
||||
85.078,
|
||||
85.094,
|
||||
85.26,
|
||||
87.212,
|
||||
87.744,
|
||||
87.847,
|
||||
88.098,
|
||||
88.436,
|
||||
88.461,
|
||||
88.919,
|
||||
89.553
|
||||
]
|
||||
},
|
||||
"G_worst_batch": {
|
||||
"p50": 37.139,
|
||||
"p95": 46.06,
|
||||
"p99": 52.215,
|
||||
"max": 52.215,
|
||||
"raw_ms": [
|
||||
32.718,
|
||||
33.447,
|
||||
33.569,
|
||||
33.692,
|
||||
33.81,
|
||||
34.236,
|
||||
34.28,
|
||||
34.475,
|
||||
34.496,
|
||||
34.746,
|
||||
35.19,
|
||||
35.392,
|
||||
35.435,
|
||||
35.564,
|
||||
35.569,
|
||||
35.669,
|
||||
35.693,
|
||||
35.734,
|
||||
35.741,
|
||||
35.812,
|
||||
35.828,
|
||||
35.934,
|
||||
36.015,
|
||||
36.036,
|
||||
36.097,
|
||||
36.144,
|
||||
36.197,
|
||||
36.205,
|
||||
36.487,
|
||||
36.637,
|
||||
36.71,
|
||||
36.775,
|
||||
36.79,
|
||||
36.83,
|
||||
36.831,
|
||||
36.885,
|
||||
36.894,
|
||||
36.903,
|
||||
36.947,
|
||||
37.122,
|
||||
37.139,
|
||||
37.154,
|
||||
37.198,
|
||||
37.225,
|
||||
37.249,
|
||||
37.478,
|
||||
37.615,
|
||||
37.617,
|
||||
38.114,
|
||||
38.164,
|
||||
38.288,
|
||||
38.637,
|
||||
38.651,
|
||||
38.724,
|
||||
38.813,
|
||||
39.108,
|
||||
39.15,
|
||||
39.848,
|
||||
39.866,
|
||||
39.946,
|
||||
39.969,
|
||||
39.992,
|
||||
40.04,
|
||||
40.28,
|
||||
40.35,
|
||||
40.697,
|
||||
40.727,
|
||||
40.893,
|
||||
40.972,
|
||||
41.029,
|
||||
41.138,
|
||||
41.479,
|
||||
41.51,
|
||||
41.711,
|
||||
45.036,
|
||||
45.784,
|
||||
46.06,
|
||||
48.597,
|
||||
50.186,
|
||||
52.215
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"torch": "2.10.0+cu130",
|
||||
"K": 8,
|
||||
"steps": 1,
|
||||
"B": 1,
|
||||
"act_scale": 1.0,
|
||||
"inference": {
|
||||
"fp32": {
|
||||
"p50": 7.681,
|
||||
"p95": 9.426,
|
||||
"p99": 10.631,
|
||||
"max": 15.837
|
||||
},
|
||||
"fp16": {
|
||||
"p50": 8.309,
|
||||
"p95": 10.791,
|
||||
"p99": 12.031,
|
||||
"max": 12.322
|
||||
},
|
||||
"bf16": {
|
||||
"p50": 8.577,
|
||||
"p95": 11.057,
|
||||
"p99": 14.174,
|
||||
"max": 17.1
|
||||
}
|
||||
},
|
||||
"cycle": {
|
||||
"fp32": {
|
||||
"p50": 36.507,
|
||||
"p95": 43.688,
|
||||
"p99": 46.568,
|
||||
"max": 50.598
|
||||
},
|
||||
"fp16": {
|
||||
"p50": 36.162,
|
||||
"p95": 50.43,
|
||||
"p99": 55.598,
|
||||
"max": 57.043
|
||||
}
|
||||
},
|
||||
"output_deviation": {
|
||||
"fp16": {
|
||||
"act_max_abs": 0.002096,
|
||||
"act_p99_abs": 0.001059,
|
||||
"deg_max_abs": 0.005,
|
||||
"deg_p99_abs": 0.00219,
|
||||
"would_change_execution_order": false
|
||||
},
|
||||
"bf16": {
|
||||
"act_max_abs": 0.017171,
|
||||
"act_p99_abs": 0.012661,
|
||||
"deg_max_abs": 0.03766,
|
||||
"deg_p99_abs": 0.03008,
|
||||
"would_change_execution_order": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"config": {
|
||||
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
|
||||
"samples": "ml/data/samples",
|
||||
"tasks": null,
|
||||
"split": "val",
|
||||
"n": 256,
|
||||
"K": 8,
|
||||
"commit": 2,
|
||||
"fb": 2.2,
|
||||
"ki": 0.0,
|
||||
"fb_scale": 0.0,
|
||||
"fb_near": 0.2,
|
||||
"fb_fallback": 0.0,
|
||||
"kdecay": 0.9,
|
||||
"kimax": 0.5,
|
||||
"fb_every": 1,
|
||||
"safety_try": 0,
|
||||
"sel_w": 0.1,
|
||||
"dls_safety": 1,
|
||||
"fb_clear": 0.0,
|
||||
"fb_cmin": 0.3,
|
||||
"sub_refresh": 0,
|
||||
"fb_block_gain": 0.5,
|
||||
"dump": null,
|
||||
"out": "docs/experiment_round34_xf_reuse_check.json",
|
||||
"trace": null,
|
||||
"select": "endpoint",
|
||||
"replan": "empty",
|
||||
"safety": 1,
|
||||
"partial": 1,
|
||||
"inc_cap": 0,
|
||||
"gap_margin": 0.0,
|
||||
"max_sec": null,
|
||||
"seed": 0,
|
||||
"dls": "analytic",
|
||||
"device": "cuda"
|
||||
},
|
||||
"batch_size": 256,
|
||||
"batch_seed_rule": "same seed per batch",
|
||||
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
|
||||
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
|
||||
"kin_fp": "2ebcbf17",
|
||||
"act_scale": 1.0,
|
||||
"metrics": {
|
||||
"n": 256,
|
||||
"n_ref": 256,
|
||||
"success": 0.57421875,
|
||||
"collision_free_success": 0.57421875,
|
||||
"time_ok_1p3": 0.390625,
|
||||
"collision_rate": 0.0,
|
||||
"collision_steps_mean": 0.0,
|
||||
"travel_ratio_median": 2.4449527263641357,
|
||||
"time_mean_s": 2.3076188564300537,
|
||||
"brakes_mean": 19.25,
|
||||
"start_fail": 0.0,
|
||||
"goal_ratio_2s": 0.20997506380081177,
|
||||
"err_fail_med": 229.68746948242188,
|
||||
"err_success_med": 0.18899640440940857,
|
||||
"dls_share": 0.625,
|
||||
"time_budget": "teacher×2(≥3 s)",
|
||||
"speed_ratio_p50": 1.6600004434585571,
|
||||
"speed_ratio_p95": 1.6600035429000854,
|
||||
"speed_ratio_max": 1.660003662109375,
|
||||
"over_vmax_rate": 1.0,
|
||||
"accel_ratio_p95": 1.5835107564926147,
|
||||
"accel_ratio_max": 1.6600004434585571
|
||||
},
|
||||
"simulation_wall_s": 317.96790459999465
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"device": "NVIDIA GeForce RTX 3090",
|
||||
"K": 8,
|
||||
"commit_n": 2,
|
||||
"design": "\u4e24\u6b21\u89c4\u5212\u4e3a\u4e00\u4e2a\u5355\u5143\uff0c\u5947\u6570\u8f6e\u53cd\u5e8f\uff1b\u6d41\u6c34\u7ebf\u90a3\u6b21\u7684\u8ba4\u8bc1\u8c03\u7528\u88ab\u8feb\u7528 cap=GAP_CAP(\u7279\u5f81\u9700\u8981)\uff0c\u8fd9\u662f\u590d\u7528\u7684\u771f\u5b9e\u4ee3\u4ef7\uff0c\u4e0d\u662f\u514d\u8d39",
|
||||
"equivalence": {
|
||||
"cached_gap_row_bitwise_equal": false,
|
||||
"features_bitwise_equal": false,
|
||||
"max_row_diff_mm": 6.29425048828125e-05
|
||||
},
|
||||
"fresh": {
|
||||
"p50_per_plan": 34.779,
|
||||
"p95": 39.271,
|
||||
"p99": 40.98,
|
||||
"reps": 60
|
||||
},
|
||||
"pipelined": {
|
||||
"p50_per_plan": 31.655,
|
||||
"p95": 34.483,
|
||||
"p99": 38.208,
|
||||
"reps": 60
|
||||
}
|
||||
}
|
||||
+746
@@ -0,0 +1,746 @@
|
||||
# 探索日志:Transformer 机械臂闭环控制
|
||||
|
||||
长期锚点:`docs/Transformer轨迹拟合训练方案.md` §9.2(部署形态 = 策略 + 安全层)。
|
||||
验收口径(§9.2):实际执行碰撞 = 0;held-out 可达任务到达率 ≥ 98%;到达用时 ≤ 老师 ×1.3;关节行程 ≤ 现有规划器 ×1.10;冷启动 < 10 ms;每次推理 + 安全认证 P99 < 20 ms;刹停发生率 < 2% 的推理周期;策略单独(关安全层)碰撞率 < 3%。
|
||||
|
||||
评测约定:val 任务、部署节奏(r = 3 重规划、1 段采样、K=8);「纯策略」= 关安全层;「部署」= 开安全层。时延:桌面 RTX 3090 上 `sample_k(B=64, K=8)` 墙钟均值(均值,非 P99);浏览器 ONNX P99 以 §9.2 R4 的独立测量为准。
|
||||
|
||||
**口径更正(2026-10-20,轮次 29 复核;下列历史原始数值保留):**
|
||||
- 轮次 24–28 的“50 Hz 可行/连续无饥饿”结论撤回。同步 `await plan(); tick()` 不是实际 20 ms 调度;`starveTicks` 只是耗时除以 20 ms 的估算。组件分位数相加不等于整次规划 P99,实测 62–68 ms 也超过 commit=3 的 60 ms。仍须严格认证下的真查看器 R9 测量,且不放松 §9.2 的 P99 < 20 ms。
|
||||
- Python 端点/中点检查不等于连续认证;JS `--seg 0` 是放松安全的消融,不是部署验收。开安全层碰撞必须为 0,不能套用“纯策略 <3%”判据。既有到达率允许途中碰撞,新指标 `collision_free_success` 才是时限内无碰撞到达率。
|
||||
- `replan=empty` 是同步仿真的零前缀配方,不是已验证的异步执行配置。所有任务(含 D)继续保留在历史对照分母内;不能与 held-out 可达任务 ≥98% 直接比较。
|
||||
- 轮次 26 的几何角速度雅可比不等于远离目标时 skew 姿态残差的精确导数,因而既改变控制律又消除差分误差;不能把全部收益归因于去噪。3.58% 是单个位置雅可比样本对拍,不是普遍噪声界。`check_dls.py` 中标 B=256 的测量实际仅有 64 行,不能引用为 B=256 性能。
|
||||
- `err_fail_med` 是批次结束时未按时到达任务的末端误差,不是每题截止时误差;仿真墙钟/题也不是单次规划延迟。查看器默认选择器仍为 consist,endpoint 仅是可选实验。
|
||||
|
||||
## 轮次 0:基线(2026-10-18,main)
|
||||
|
||||
- **假设**:模型开环已拟合老师(末端 ~6 mm),闭环失败另有原因。
|
||||
- **判据**:reach = 0 且碰撞 ≈ 100% 即失败;需同时报告碰撞 / 到达 / 精度 / 时延。
|
||||
- **配方**:`ml/runs/base/last.pt`(step 14000,旧配方、act_scale=1.0),n=256。
|
||||
- **对照**:老师回放 reach 1.0 / 碰撞 0;H1_oracle(老师提线走 rollout)reach 1.0、碰撞 0、轨迹偏差 p99 4.6e-4°。
|
||||
- **指标**:到达 **0%**;碰撞 **99.6%**;精度 ol_fk 5.98 mm / ol_best 2.92 mm / ol_div 0.655,2 s 误差比 0.755;时延未测。
|
||||
- **结论**:仿真/滚动无误,失败在策略输出。闭环对每步残差极敏感(老师动作加 σ=0.05×STEP 即 reach 1.0→0.218)。
|
||||
|
||||
## 轮次 1:数据规模能否救闭环(main,负结论)
|
||||
|
||||
- **假设**:训练数据不足导致分布外状态无覆盖。
|
||||
- **判据**:若闭环状态到训练集最近邻距离大,且密度缩放无法在误差尺度内缩小差距,则加大同类数据无效。
|
||||
- **配方**:191 万老师状态最近邻分析;6 维密度缩放 d∝N^(−1/6)。
|
||||
- **对照**:起点(老师分布内)最近邻。
|
||||
- **指标**:闭环状态最近邻 p50 **21.5°**(起点 13.2°);数据翻 4 倍仅降 ~21%,与 0.02° 误差尺度差三个量级。
|
||||
- **结论**:加大同类老师数据无效;需要「不同的数据」(闭环状态标注)。
|
||||
- **下一步**:DAgger 或状态扰动数据。
|
||||
|
||||
## 轮次 2:合成扰动状态微调(main,负结论)
|
||||
|
||||
- **假设**:偏移 1.5° 状态的老师标注可补足闭环状态覆盖。
|
||||
- **判据**:若 success 仍 0 或开环精度退化,则配方不成立。
|
||||
- **配方**:2000 个扰动起点(σ 混合 0.5/1/2/4°)→ 老师重规划(2000/2000 到达)→ 36k 样本 → 从 base/last.pt 微调 3000 步(lr 3e-4 / 1e-4 两轮)。
|
||||
- **对照**:base/last.pt。
|
||||
- **指标**:碰撞 99.6%→**48.4% / 60.4%**;到达仍 **0%**;开环 6→**15.3 / 16.4 mm**(退化 2.5×);ratio2s 0.927/0.893。
|
||||
- **结论**:只学会了「保守避撞」,开环精度被带偏,success 仍 0;此配方不成立。
|
||||
- **下一步**:改训练标签(绝对目标)或真实 DAgger。
|
||||
|
||||
## 轮次 3:绝对目标参数化(exp: `feat/abs-target`,负结论)
|
||||
|
||||
- **假设**:模型从当前状态外推增量(判别 ρ=1.54),改为绝对目标标签可让输出指向参考位置。
|
||||
- **判据**:一步采样开环误差需回到 <10 mm,闭环 success > 0;否则参数化不可行。
|
||||
- **配方**:`ABS_TARGET=1` 重打主数据(201 万样本,Y=16 步绝对目标/180),train/rollout/ckpt 适配;从零训练 9950 步(p_neq 预热 5000)。过程中修复前缀噪声增广把 eps 直接加到绝对目标(≈18° 污染)的 bug。
|
||||
- **对照**:base/last.pt(ol_fk 5.98 mm、reach 0%)。
|
||||
- **指标**:一步采样动作误差 **31°**(4/8 段 2.3°/2.8°);ol_fk 4000/6000/8000 步 = 74.6/140.4/139.0 mm;闭环 **0% / 100%**(1 段与 4 段皆然);时延未测。
|
||||
- **结论**:绝对目标的一步流匹配在 MeanFlow 头上学不动(过预热后 5000 步无下降趋势);此配方不成立。分支已推送远端 `217a70b`(本地分支可删,远端保留可复现)。
|
||||
- **下一步**:回归增量口径,上真实 DAgger。
|
||||
|
||||
## 轮次 4:真实 DAgger 1 轮(main,部分有效)
|
||||
|
||||
- **假设**:策略自己走到的状态(离训练集 21.5°)+ 老师重标注,可让模型学会在偏移状态上行动。
|
||||
- **判据**:reach 是否从 0 抬头;同时看碰撞、开环精度、时延。
|
||||
- **配方**:`ACT_SCALE=1.0 DAG_ROUNDS=1 DAG_N=2000 bash ml/dagger/run.sh base ml/runs/base/last.pt`(采集 2000 题 → 老师重规划 → 打包 → 微调 15000 步,lr 3e-4;主数据 + DAgger 混合)。
|
||||
- **对照**:base/last.pt。
|
||||
- **指标**:碰撞 99.6%→**40.2%**(8000 步 36.9%,10000 步 58.8%);到达 **0%**,仅微调 12000 步时出现 **0.4%(2/512)**;开环退化 6→**15.7 mm**;ratio2s 0.87–0.92;时延未测。
|
||||
- **结论**:DAgger 数据确实教会避撞,但微调把主分布带偏(开环退化),到达仍近 0;单独 DAgger 不是终点。
|
||||
- **下一步**:在 DAgger 模型上加目标反馈(模型前馈 + DLS 反馈)。
|
||||
|
||||
## 轮次 5:模型前馈 + DLS 目标反馈(exp: `feat/fb-mix`,历史最佳)
|
||||
|
||||
- **假设**:模型擅长开环轨迹/避撞,但不指向目标;把 DLS 朝目标的反馈叠加到候选动作上,可把「走偏」拉回目标。
|
||||
- **判据**:部署口径(+安全层)reach 显著 > 0 且碰撞 < 3%(§9.2 策略单独碰撞线)、刹停可控;否则不成立。
|
||||
- **配方**:`closed_loop.rollout(fb_gain=...)`:`inc += fb_gain · DLS(qe→T,R, n=1)`(加在所有候选上,安全层评估带反馈的候选)。模型 `ml/runs/base_dag1/best.pt`(DAgger 轮 4 的 best,12000 步)。n=128,val。
|
||||
- **对照**:base/last.pt + 同反馈;DAgger 模型 + 无反馈。
|
||||
- **指标(n=128)**:
|
||||
|
||||
| 配置 | 到达 | 碰撞 | 精度 ratio2s | 刹停/题 | 时延 |
|
||||
|---|---|---|---|---|---|
|
||||
| DAgger 模型, gain 0 | 0% | 44.5% | 0.873 | 0 | — |
|
||||
| DAgger 模型, gain 1.0, 关安全层 | 29.7% | 73.4% | 0.370 | 0 | — |
|
||||
| **DAgger 模型, gain 1.0, 开安全层** | **20.3%** | **2.3%** | **0.480** | **21.9** | **12.58 ms/次**(B=64,K=8,桌面 3090) |
|
||||
| base 模型, gain 1.0, 关安全层 | 0% | 94.5% | 0.554 | 0 | — |
|
||||
|
||||
(另测 n=32 复核:gain 1.0 + safety → reach 25.0%、碰撞 6.2%、ratio2s 0.588。)
|
||||
|
||||
- **结论**:**反馈混合是首个把到达率从 0 拉到 20%+ 的机理**;安全层把碰撞从 73% 压到 2.3%(部署口径),代价是刹停 21.9 次/题(远超 §9.2 的 <2%/周期)。精度 ratio2s 0.48(2 s 时误差缩一半),时延 12.58 ms/次接近 §9.2 的 20 ms(推理+认证)门槛。
|
||||
- **与历史最佳/基线对比**:reach 0→20.3%、碰撞 99.6→2.3%、ratio2s 0.755→0.480;开环精度仍退化(DAgger 微调导致 15.7 mm vs 基线 5.98 mm)。
|
||||
- **漂移检查**:main = 70654e4(无新提交);工作区仅用户个人 html 未跟踪;数据口径 act_scale=1.0 与 ckpt 一致;`feat/abs-target` 已推送远端;本次实验在独立分支 `feat/fb-mix`。
|
||||
- **下一步**:扫更大 gain(1.2–2.0)与增益衰减;目标是把刹停次数降下来、到达率再提升,并测 ONNX/浏览器时延。
|
||||
|
||||
## 轮次 6:反馈增益扩展扫描(exp: `feat/fb-mix`,小幅进展)
|
||||
|
||||
- **假设**:更大增益能把到达率继续推高。
|
||||
- **判据**:reach 显著高于 20.3%(>25%)且碰撞 ≤3%、刹停不失控。
|
||||
- **配方**:`rollout(fb_gain∈{1.2,1.5,2.0}, safety=True)`,模型同轮次 5,n=128。
|
||||
- **对照**:轮次 5(gain 1.0:reach 20.3% / 碰撞 2.3% / ratio2s 0.480 / 刹停 21.9)。
|
||||
- **指标**:
|
||||
|
||||
| gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1.2 | **23.4%** | 1.6% | **0.454** | 31.4 | 973 ms |
|
||||
| 1.5 | 21.9% | 2.3% | 0.498 | 43.0 | 1151 ms |
|
||||
| 2.0 | 23.4% | 0.8% | 0.530 | 57.6 | 1100 ms |
|
||||
|
||||
- **结论**:到达率封顶在 ~23%,增益再大只增刹停(31→58 次/题)。当前瓶颈从「到不了目标」变成「安全层频繁刹停 + 近目标过冲」。
|
||||
- **下一步**:距离衰减增益(远离目标全增益、贴近目标减弱)+ 刹停兜底;目标 reach > 30% 且刹停显著下降。
|
||||
|
||||
## 轮次 7:距离衰减增益 + 刹停兜底(exp: `feat/fb-mix`,负结论)
|
||||
|
||||
- **假设**:贴近目标减弱反馈可减少过冲/刹停;安全层全拒时用「半速朝目标一步」(单点安全认证)可减少原地停滞。
|
||||
- **判据**:reach > 23.4%(轮次 6)且碰撞 ≤3%、刹停下降。
|
||||
- **配方**:`fb_scale=100`(>100 mm 全增益,近目标最低 0.2 倍);`fb_fallback=0.5`(全拒时半速 DLS 一步,认证 `qe+fb1` 单点无碰)。n=128,safety=True。
|
||||
- **对照**:轮次 6(gain 1.2、无衰减、无兜底:23.4% / 1.6% / 0.454 / 31.4 / 973 ms)。
|
||||
- **指标**:
|
||||
|
||||
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| decay + fallback (gain 1.2) | 7.0% | 57.8% | 0.506 | 45.1 | 1166 ms |
|
||||
| decay only (gain 1.2) | 9.4% | 1.6% | 0.454 | 29.6 | 1106 ms |
|
||||
| no-decay + fallback (gain 1.2) | 21.1% | 57.8% | 0.506 | 46.8 | 1063 ms |
|
||||
| gain 1.0 + decay + fallback | 4.7% | 55.5% | 0.515 | 37.2 | 939 ms |
|
||||
|
||||
- **结论**:两个改动都失败。① 衰减使接近目标时的收敛变慢,到达从 23.4% 掉到 9.4%;② 兜底绕过安全层的多候选选择,碰撞从 1.6% 拉到 **57.8%**(仅单点认证、且执行 3 步同向动作,危险),必须清除。保留参数但默认关闭(`fb_scale=0, fb_fallback=0`),历史最佳配置(轮次 6)可复现。
|
||||
- **过程 bug(自造)**:衰减代码曾用变量名 `g` 覆盖 rollout 的随机数生成器,导致 `randn` 报错;改名 `gdec` 后正常——记录以免误判为模型问题。
|
||||
- **下一步**:DAgger 第 2 轮——采集时加入 fb_gain + safety,让训练分布 = 部署分布(1 轮,15000 步微调,约 1 小时)。按目标要求先向用户确认。
|
||||
|
||||
## 轮次 8:部署分布 DAgger 第 2 轮(exp: `exp/dagger-deploy`,边际改善)
|
||||
|
||||
- **假设**:第 1 轮 DAgger 用纯策略轨迹采集,与部署分布(反馈 + 安全层)不一致;用部署分布采集可让模型学到刹停/反馈下的正确动作。
|
||||
- **判据**:部署口径 reach > 23.4%(轮次 6)或刹停显著下降且到达不降。
|
||||
- **配方**:`ml.dagger.collect --fb_gain 1.2 --safety 1`(2000 题 → 637 起点,部署分布)→ 老师重规划 → 打包 → 从 `base_dag1/best.pt` 微调 15000 步(配置 base_dag2,lr 3e-4)。
|
||||
- **对照**:base_dag1/best.pt(轮次 6 历史最佳);纯策略 eval 见训练日志。
|
||||
- **指标(部署口径,n=128,safety=True)**:
|
||||
|
||||
| 模型 | gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| dag2 best | 1.0 | 21.9% | 1.6% | 0.485 | **18.9** | **905 ms** |
|
||||
| dag2 best | 1.2 | 22.7% | 1.6% | 0.491 | 25.9 | 922 ms |
|
||||
| dag1 best | 1.0 | 20.3% | 2.3% | 0.480 | 21.9 | 944 ms |
|
||||
| dag1 best(轮次 6) | 1.2 | **23.4%** | 1.6% | **0.454** | 31.4 | 953 ms |
|
||||
|
||||
- **结论**:部署分布 DAgger 把刹停降 18–40%、时延降 ~31 ms,但到达率未突破 23% 平台(-0.7 pp),属边际权衡改善;历史最佳到达仍是 dag1+gain1.2(23.4%)。
|
||||
- **下一步**:试 replan=empty(r=0 规划,让反馈逐步生效)与安全层候选 K=16(减少全拒刹停)。
|
||||
|
||||
## 轮次 9:replan=empty(队列空时规划、r=0)/ K=16 安全候选(exp: `exp/dagger-deploy`,新历史最佳)
|
||||
|
||||
- **假设**:replan='empty'(每次队列空时规划、无前缀 r=0)比默认 r=3 更适合反馈混合。K=16 给安全层更多候选、减少全拒。
|
||||
(更正:replan='empty' 仍是每 3 步规划一次——队列空才规划,commit 3 步;收益来自 r=0 无前缀,不是规划频率。)
|
||||
- **判据**:部署口径 reach > 23.4% 且碰撞 ≤1.6%、刹停/时延不恶化。
|
||||
- **配方**:dag2 best;`replan='empty'`(每次 r=0 规划);`K=16`。n=128,safety=True。
|
||||
- **对照**:轮次 8(dag2+gain1.2:22.7% / 1.6% / 0.491 / 25.9 / 922 ms)。
|
||||
- **指标**:
|
||||
|
||||
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| **gain1.2 + replan=empty** | **26.6%** | **0.8%** | **0.453** | **20.6** | **845 ms** |
|
||||
| gain1.2 + K=16 | 21.1% | 1.6% | 0.451 | 19.4 | 1629 ms |
|
||||
| gain1.0 + K=16 | 21.9% | 4.7% | 0.506 | 14.6 | 1588 ms |
|
||||
|
||||
- **结论**:**replan=empty(r=0)全面改善**(到达 +3.9 pp、碰撞 1.6→0.8%、刹停 25.9→20.6、时延 922→845 ms),成为**新历史最佳**。K=16 无收益且时延翻倍(1629 ms),淘汰。
|
||||
- **下一步**:replan=empty 下重扫增益(0.8/1.0/1.5/2.0),找新最优。
|
||||
|
||||
## 轮次 10:replan=empty(r=0)下的增益重扫(exp: `exp/dagger-deploy`,新历史最佳)
|
||||
|
||||
- **假设**:r=0 规划下,最优增益会上移。
|
||||
- **判据**:reach > 26.6%(轮次 9)且碰撞 ≤1.6%;同时比较刹停/时延。
|
||||
- **配方**:dag2 best,`replan='empty'`,gain ∈ {0.8, 1.0, 1.2, 1.5, 2.0},safety=True,n=128。
|
||||
- **对照**:轮次 9(gain 1.2:26.6% / 0.8% / 0.453 / 20.6 / 845 ms)。
|
||||
- **指标**:
|
||||
|
||||
| gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 0.8 | 21.9% | 2.3% | 0.505 | 11.3 | 801 ms |
|
||||
| 1.0 | 22.7% | **0.0%** | 0.489 | 15.2 | 810 ms |
|
||||
| 1.2 | 26.6% | 0.8% | 0.453 | 20.6 | 845 ms |
|
||||
| **1.5** | **28.9%** | 0.8% | **0.411** | 28.5 | 844 ms |
|
||||
| 2.0 | 28.9% | 0.8% | 0.414 | 44.9 | 914 ms |
|
||||
|
||||
- **结论**:到达在 gain 1.5 封顶(28.9%),2.0 无收益且刹停翻倍(44.9)。**按到达优先/碰撞 ≤3%:历史最佳 = gain 1.5 + replan=empty(28.9% / 0.8% / 0.411 / 28.5 / 844 ms)**;按碰撞优先:gain 1.0(22.7% / 0.0% / 0.489 / 15.2 / 810 ms)。n=128 的差异约 3–4 题,需 n=256 复核。
|
||||
- **下一步**:n=256 复核 gain 1.0 / 1.2 / 1.5;若平台稳定,再决定是否 DAgger 第 3 轮(采集用新部署配置)。
|
||||
|
||||
## 轮次 11:n=256 复核(发现前 128 题偏难)
|
||||
|
||||
- **假设**:轮次 10 的 n=128 差异含样本偏差,需扩大样本复核。
|
||||
- **判据**:reach 排名与轮次 10 一致、碰撞 ≤3%;若差异大则以 n=256 为准。
|
||||
- **配方**:dag2 best,`replan='empty'`,safety=True,gain ∈ {1.0, 1.2, 1.5},n=256。
|
||||
- **对照**:轮次 10(n=128:22.7/26.6/28.9%)。
|
||||
- **指标**:
|
||||
|
||||
| gain | n=128 到达 | **n=256 到达** | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.0 | 22.7% | **31.6%** | 0.8% | 0.428 | 15.0 | 748 ms |
|
||||
| 1.2 | 26.6% | **34.8%** | 1.2% | 0.392 | 18.6 | 752 ms |
|
||||
| 1.5 | 28.9% | **36.7%** | 1.6% | 0.323 | 27.4 | 760 ms |
|
||||
|
||||
- **结论**:前 128 题显著偏难;n=256 上 **gain 1.5 + replan=empty = 到达 36.7% / 碰撞 1.6% / ratio2s 0.323 / 刹停 27.4 / 760 ms**(新历史最佳)。gain 1.0 更保守(31.6% / 碰撞 0.8% / 刹停 15.0)。排名与 n=128 一致(1.5 > 1.2 > 1.0)。
|
||||
- **下一步**:n=512 复核 gain 1.2/1.5;随后评估 DAgger 第 3 轮(部署分布采集)与监控页回写。
|
||||
|
||||
## 轮次 12:n=512 复核(稳定结论)
|
||||
|
||||
- **假设**:n=256 的 34.8–36.7% 需在更大样本上确认。
|
||||
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,`replan='empty'`,safety=True,gain ∈ {1.2, 1.5},n=512。
|
||||
- **对照**:轮次 11(n=256:34.8 / 36.7%)。
|
||||
- **指标**:
|
||||
|
||||
| gain | n=256 到达 | **n=512 到达** | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.2 | 34.8% | **32.6%** | 1.0% | 0.435 | 18.6 | 722 ms |
|
||||
| 1.5 | 36.7% | **33.4%** | 1.0% | 0.399 | 28.1 | 738 ms |
|
||||
|
||||
- **结论**:n=512 稳定在 **33% 左右**(n=256 略乐观);**历史最佳(n=512,部署口径)= gain 1.5 + replan=empty:到达 33.4% / 碰撞 1.0% / ratio2s 0.399 / 刹停 28.1 / 738 ms**。保守版 gain 1.2:32.6% / 1.0% / 0.435 / 18.6 / 722 ms(刹停少 1/3)。
|
||||
- **监控页回写**:`ml/monitor.py` 新增 `/api/explore`(读 docs/探索日志.md),`ml/monitor.html` 新增「闭环探索日志」卡片;Edge headless 截图确认渲染正常(http://127.0.0.1:8100)。
|
||||
- **下一步**:候选选择器(consist/value/first)与 K=4 在反馈+每步重规划下是否影响到达/刹停。
|
||||
|
||||
## 轮次 13:选择器 / K / 失败模式(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **假设**:反馈混合下,候选选择器与候选数 K 会影响到达/时延;失败模式可指导下一步。
|
||||
- **判据**:reach > 36.7% 或时延显著下降;同时报告 time_ok_1p3。
|
||||
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,n=256。
|
||||
- **对照**:轮次 11/12(consist K=8:36.7%)。
|
||||
- **指标**:
|
||||
|
||||
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| consist K=8 | 36.7% | 1.6% | 0.323 | 27.4 | 764 ms |
|
||||
| value K=8 | 35.9% | 1.6% | 0.334 | 29.4 | 1063 ms |
|
||||
| first K=8 | 36.3% | 1.2% | 0.338 | 28.1 | 780 ms |
|
||||
| K=6 | 35.9% | 1.6% | 0.353 | 28.6 | 627 ms |
|
||||
| K=4 | 31.6% | 1.2% | 0.396 | 32.6 | 482 ms |
|
||||
|
||||
- **失败模式(n=256,consist K=8)**:失败 149 题中 **碰撞仅 3 题(1.2%)、超时无碰撞 146 题(57.0%)**;失败题终点误差 p50 140 mm;`time_ok_1p3` 仅 **13.3%**;成功题 t_done p50 2.74 s(时限 p50 3.4 s);刹停 27.4 次/题。
|
||||
- **结论**:选择器无差别(consist 略优);K=8 到达最佳,K=6 省 18% 时延(到达 -0.8 pp)。**当前真正的瓶颈是时间效率(频繁刹停→超时),不是碰撞**。§9.2 的「用时 ≤ 老师×1.3」当前只有 13.3%。
|
||||
- **下一步**:每步重规划下,安全层只需认证第 1 步(现在验 3 步,过度保守→全拒→刹停);改 `safety_steps=1` 验证能否在保持低碰撞的同时大幅减少刹停。
|
||||
|
||||
## 轮次 14:部分安全前缀(exp: `exp/dagger-deploy`,刹停大降)
|
||||
|
||||
- **假设**:安全层现在要求 3 步全安全才执行,候选只要第 2–3 步有碰撞就全拒刹停;改为「执行到第一个不安全步为止」可减少刹停。
|
||||
- **判据**:刹停显著下降、到达不降、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,`safety_partial=1`;n=256。
|
||||
- **对照**:全窗口(轮次 11/13:36.7% / 1.6% / 0.323 / 27.4 / 775 ms)。
|
||||
- **指标**:
|
||||
|
||||
| 安全层 | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3 步全安全 | 36.7% | 13.3% | 1.6% | 0.323 | 27.4 | 775 ms |
|
||||
| **部分安全前缀** | **38.3%** | 13.7% | 1.6% | **0.308** | **8.6** | 770 ms |
|
||||
|
||||
- **结论**:刹停 **27.4→8.6(-69%)**、到达 +1.6 pp、碰撞不变——**部分安全前缀有效**(新最佳:38.3% / 1.6% / 0.308 / 8.6 / 770 ms)。但 `time_ok_1p3` 仍 13.7%:说明超时主因不是刹停,而是**执行粒度**——部分前缀的填充零步仍在消耗时间。
|
||||
- **下一步**:commit_n=1(每次只提交 1 步、每步规划,无填充步),验证时间效率。
|
||||
|
||||
## 轮次 15:每步规划(commit_n=1)(exp: `exp/dagger-deploy`,新历史最佳)
|
||||
|
||||
- **假设**:部分前缀的填充零步仍在消耗时间;每次只提交 1 步、每步重新规划可消除填充、提升时间效率。
|
||||
- **判据**:time_ok_1p3 显著上升、到达不降、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,`safety_partial=1, commit_n=1`;n=256。
|
||||
- **对照**:commit_n=3 部分前缀(轮次 14:38.3% / 13.7% / 1.6% / 0.308 / 8.6 / 770 ms)。
|
||||
- **指标**:
|
||||
|
||||
| commit_n | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3 | 38.3% | 13.7% | 1.6% | 0.308 | 8.6 | 770 ms |
|
||||
| **1** | **41.8%** | **21.5%** | 2.3% | **0.259** | 26.9 | 967 ms |
|
||||
|
||||
- **结论**:每步规划把到达推到 **41.8%**、时间效率 **13.7→21.5%**、精度 ratio2s **0.259**;代价:碰撞 1.6→2.3%(仍 <3%)、刹停计数回升(每步都判断)、时延 +26%。**新历史最佳(n=256,部署口径):41.8% / 2.3% / 0.259 / 21.5% / 967 ms**。
|
||||
- **注**:commit_n=1 的推理频率是 3 倍,真机需核对 §9.2 的 P99 <20 ms 预算(当前桌面 12.58 ms/次)。
|
||||
- **下一步**:n=512 复核;若稳定则收尾(更新最佳、监控页、分支清理)。
|
||||
|
||||
## 轮次 16:n=512 复核每步规划(稳定结论)
|
||||
|
||||
- **假设**:轮次 15 的 41.8%(n=256)需更大样本确认。
|
||||
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,safety_partial=1,commit_n=1;n=512。
|
||||
- **对照**:轮次 15(n=256:41.8% / 21.5% / 2.3% / 0.259 / 967 ms)。
|
||||
- **指标**:到达 **40.8%**、time_ok_1p3 **25.0%**、碰撞 **2.1%**、ratio2s **0.309**、刹停 25.3、墙钟 **852 ms/题**。
|
||||
- **结论**:稳定。**历史最佳(n=512,部署口径)= 40.8% 到达 / 2.1% 碰撞 / ratio2s 0.309 / time_ok 25.0% / 852 ms/题**;从基线 0% 到达提升 40.8 pp。相对 §9.2(98%、碰撞 0、用时≤老师×1.3)仍有大差距,但机理已成立。
|
||||
- **下一步**:每步规划下重扫增益(1.8/2.2/3.0),看时间效率是否继续提升;随后收尾。
|
||||
|
||||
## 轮次 17:每步规划下的更大增益(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **假设**:每步规划下更大的反馈增益可进一步提升到达与时间效率。
|
||||
- **判据**:time_ok 上升、到达不降、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,replan='empty',commit_n=1,safety_partial=1,gain ∈ {1.8, 2.2, 3.0},n=256。
|
||||
- **对照**:轮次 15(gain 1.5:41.8% / 21.5% / 2.3% / 0.259 / 26.9 / 967 ms)。
|
||||
- **指标**:
|
||||
|
||||
| gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **1.8** | **43.8%** | 28.9% | 1.6% | 0.216 | 40.0 | 956 ms |
|
||||
| 2.2 | 41.0% | **32.4%** | 2.0% | 0.174 | 64.4 | 976 ms |
|
||||
| 3.0 | 36.3% | 32.8% | 0.8% | **0.153** | **123.2** | 994 ms |
|
||||
|
||||
- **结论**:gain 1.8 到达最高(43.8%);2.2/3.0 精度与时间效率更好但到达下降、刹停暴涨(3.0 达 123 次/题,过冲)。**历史最佳按到达优先(碰撞 ≤3%):gain 1.8 = 到达 43.8% / 碰撞 1.6% / ratio2s 0.216 / time_ok 28.9% / 956 ms(n=256)**。保守/时间优先可选 2.2。
|
||||
- **下一步**:n=512 复核 gain 1.8;随后收尾。
|
||||
|
||||
## 轮次 18:n=512 复核 gain 1.8(收尾)
|
||||
|
||||
- **假设**:轮次 17 的 43.8%(n=256)需更大样本确认。
|
||||
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
|
||||
- **配方**:dag2 best,replan='empty',commit_n=1,safety_partial=1,gain 1.8;n=512。
|
||||
- **对照**:轮次 17(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms);轮次 16(gain 1.5 n=512:40.8% / 25.0% / 2.1% / 0.309 / 25.3 / 852 ms)。
|
||||
- **指标**:到达 **42.2%**、time_ok_1p3 **29.1%**、碰撞 **2.9%**、ratio2s **0.270**、刹停 37.4、墙钟 **870 ms/题**。
|
||||
- **结论**:gain 1.8 在 n=512 稳定(42.2%),到达/时间效率优于 gain 1.5(+1.4 pp / +4.1 pp),但碰撞 2.1→2.9%(贴近 3% 线)、刹停 25.3→37.4。**历史最佳(到达优先,n=512)= gain 1.8;碰撞优先 = gain 1.5**。
|
||||
- **下一步**:本轮探索收尾(监控页已回写);进入大规模训练需用户确认 §9.2 锚点。
|
||||
|
||||
## 轮次统计
|
||||
|
||||
- 连续无实质进展轮数:0(轮次 9–17 持续有实质进展)。
|
||||
- **历史最佳(部署口径,n=512)**:`ml/runs/base_dag2_dag1/best.pt` + `K=8, safety=True, replan='empty', fb_gain=1.8, safety_partial=1, commit_n=1` → **到达 42.2%、碰撞 2.9%、ratio2s 0.270、time_ok_1p3 29.1%、刹停 37.4/题、仿真墙钟 870 ms/题**;推理 12.58 ms/次(B=64,K=8,桌面 3090 均值,P99 未测)。
|
||||
- 保守版(碰撞优先):gain 1.5 → 40.8% / 2.1% / 0.309 / 25.0% / 25.3 / 852 ms。
|
||||
- 与基线对比:到达 **0→42.2%**、碰撞 **99.6→2.9%**、ratio2s **0.755→0.270**;代价:DAgger 微调使开环 ol_fk 5.98→15.7 mm。
|
||||
- 相对 §9.2:到达 42.2% vs ≥98%(差距大);碰撞 2.9% vs 0(接近策略单独 <3% 线);用时 ≤老师×1.3 为 29.1%;推理+认证 P99 未测(桌面均值 12.58 ms,预算 20 ms)。
|
||||
- 未进入大规模训练(需用户确认 §9.2 锚点)。
|
||||
- 训练方法探索(轮次 19–22):APG(管线修复、尺度失控)、反馈蒸馏(未内化)、DAgger-3(39.5%)、冻结编码器(41.8%)——均未超过 dag2 基线 43.8%。
|
||||
|
||||
## 轮次 19:APG 闭环策略梯度试跑(exp: `exp/dagger-deploy`,管线修复但暂不采用)
|
||||
|
||||
- **假设**:用闭环目标/碰撞损失直接微调策略(把"朝目标走"内化),减少对执行层反馈的依赖。
|
||||
- **判据**:纯策略闭环 success 上升、碰撞铰链下降;部署口径(+反馈/安全层)不降。
|
||||
- **配方**:`ml.apg.apg_finetune --init base_dag2_dag1/best.pt --steps 2`(先冒烟;原计划 500 步)。
|
||||
- **修复的两个既有 bug**(APG 此前从未实测):
|
||||
1. `kin_torch._cat_gaps` 的 `p['b'] += s` 是 in-place,而 `b` 被 autograd 索引反向图引用 → backward 报 version 错;改为 `p['b'] = p['b'] + s`。
|
||||
2. APG 段边界 `queue.detach()` 让下一段执行的动作无梯度;改为只 detach 状态并重置队列(下一 plan 走 r=0 重新提交)。
|
||||
- **指标(2 步冒烟)**:EVAL0(纯策略)success 0 / 碰撞 31.8%;第 0 步 col(碰撞铰链均值)**409508**、gnorm 473119(被裁剪)、tgt 206.6、smooth 645;EVAL@2 success 0 / 碰撞 26.0%。
|
||||
- **结论**:管线已能跑,但**损失尺度失控**(碰撞铰链均值 40 万,远大于 margin 3mm 的合理量级);且 APG 展开是纯策略口径(无反馈/安全层/每步规划),与当前部署形态不一致。**暂不采用**,转「反馈蒸馏」。
|
||||
- **下一步**:反馈蒸馏——用最终部署配置收集 (特征, 复合动作=模型输出+目标反馈) 样本,微调模型把反馈内化。
|
||||
|
||||
## 轮次 20:反馈蒸馏(exp: `exp/dagger-deploy`,负结论)
|
||||
|
||||
- **假设**:用最终部署配置收集 (特征, 复合动作 = 模型输出 + 目标反馈) 样本,微调后模型可内化反馈(部署时降低/去掉反馈仍能到目标)。
|
||||
- **判据**:部署口径下 `fb_gain=0~0.9` 时 success 接近/超过基线(43.8%,n=256),且 time_ok 不降。
|
||||
- **配方**:`rollout` 加 collect 钩子(与部署一致:safety、replan='empty'、safety_partial=1、commit_n=1、fb_gain=1.8)收集 500 题 → **17.7 万样本**;`train.py --config distill.json`(lr 1e-4、3000 步、w_value=0、w_sub=0、w_fk=0.1、w_smooth=0.01)+ 主数据 BC 锚定,从 dag2 best 微调。
|
||||
- **对照**:dag2 + gain 1.8(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
|
||||
- **指标(n=256,部署口径)**:
|
||||
|
||||
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| distill | 0.0 | **0.8%** | — | 0.0% | 0.708 | 2.1 | 762 ms |
|
||||
| distill | 0.9 | 36.7% | 10.5% | 1.2% | 0.421 | 12.4 | 916 ms |
|
||||
| distill | 1.8 | 43.4% | 27.7% | 2.0% | 0.228 | 47.4 | 925 ms |
|
||||
| dag2(基线) | 1.8 | 43.8% | 28.9% | 1.6% | 0.216 | 40.0 | 956 ms |
|
||||
|
||||
- **结论**:**反馈未内化**——`gain=0` 时到达仅 0.8%(与基线纯策略一样);`gain=1.8` 时与基线持平(43.4 vs 43.8,差约 1 题),碰撞/刹停略差。蒸馏样本占比仅 8.5%、标签仍以主数据增量为主,模型没有学会"看目标修正"。此配方不成立。
|
||||
- **下一步**:与最终部署配置一致的 DAgger 第 3 轮(采集时已用 safety+fb 1.2,但仍是 3 步提交/全窗口;现在部署是每步规划+部分前缀)。
|
||||
|
||||
## 轮次 21:DAgger-3(最终部署配置采集)(exp: `exp/dagger-deploy`,负结论)
|
||||
|
||||
- **假设**:用最终部署配置(每步规划 + 部分安全前缀 + 反馈 gain1.8 + 安全层)采集,训练分布与部署完全一致,可继续提升。
|
||||
- **判据**:部署口径 success > 43.8%(dag2 基线,n=256)。
|
||||
- **配方**:`collect --fb_gain 1.8 --safety 1 --replan empty --commit_n 1 --safety_partial 1`(500 题、73 起点)→ 老师重规划 → 打包 → 从 dag2 best 微调 15000 步(lr 3e-4,配置 base_dag3)。
|
||||
- **对照**:dag2 + gain1.8(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
|
||||
- **指标(n=256,部署口径)**:
|
||||
|
||||
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| dag3 | 1.5 | 39.8% | 16.0% | 1.2% | 0.360 | 14.7 | 912 ms |
|
||||
| dag3 | 1.8 | 39.5% | 21.1% | 1.2% | 0.320 | 24.5 | 935 ms |
|
||||
| dag2(基线) | 1.8 | **43.8%** | **28.9%** | 1.6% | **0.216** | 40.0 | 956 ms |
|
||||
|
||||
- **结论**:DAgger-3 到达 **-4.3 pp**(39.5 vs 43.8),time_ok -7.8 pp;碰撞/刹停略好但主指标下降。开环进一步退化(ol_fk 19.5 mm vs dag2 15.7)。**负结论**:继续微调(15000 步 lr 3e-4)会退化主分布,训练侧收益递减。
|
||||
- **下一步**:冻结编码器、只微调解码器(假设:微调退化来自表征漂移),用同一份数据与预算验证。
|
||||
|
||||
## 轮次 22:冻结编码器微调(exp: `exp/dagger-deploy`,负结论)
|
||||
|
||||
- **假设**:微调导致的分布退化来自表征漂移;冻结编码器(enc + enc_norm)、只调解码器/头,可保留开环精度同时吸收 DAgger-3 的部署分布样本。
|
||||
- **判据**:部署口径 success > 43.8%(dag2)且 ol_fk 不退化(≤16 mm)。
|
||||
- **配方**:`train.py --freeze-enc`(优化器只含 requires_grad 参数),数据 = 主数据 + DAgger-3 样本,lr 3e-4、5000 步,从 dag2 best 初始化;n=256 部署口径。
|
||||
- **对照**:dag2 + gain1.8(43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
|
||||
- **指标**:
|
||||
|
||||
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 | ol_fk |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 冻结编码器 | 1.5 | 39.8% | 22.3% | 1.2% | 0.293 | 25.4 | 936 ms | 15.8 mm |
|
||||
| 冻结编码器 | 1.8 | 41.8% | 26.2% | 2.3% | 0.274 | 41.4 | 940 ms | 15.8 mm |
|
||||
| dag2(基线) | 1.8 | **43.8%** | **28.9%** | 1.6% | **0.216** | 40.0 | 956 ms | 15.7 mm |
|
||||
|
||||
- **结论**:冻结编码器**确实减缓了开环退化**(ol_fk 15.8 vs 全微调 19.5),但部署到达仍比 dag2 低 2.0 pp——**无提升**。至此训练侧四种方法(APG、反馈蒸馏、DAgger-3、冻结编码器)均未超过 dag2 基线(43.8%),训练侧在当前数据/预算下已到平台。
|
||||
- **下一步(超出小规模范围,需确认)**:更大规模训练(24000 步从头 + 8000×2 轮 DAgger)或改特征/架构(目标条件化、状态扰动训练需重算特征)。
|
||||
|
||||
## 轮次 23:实时性口径评估(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **背景**:870 ms/题是仿真里整条轨迹的总时长(150–300 次规划累加),不是控制延迟。实时口径按 §9.2:每次「推理 + 安全认证」P99 < 20 ms(50 Hz,DT=0.02 s)。
|
||||
- **假设**:单次规划延迟可以压到 20 ms 以内。
|
||||
- **判据**:B=1 单题、单次规划(features + sample_k K=8 + 目标反馈 + 安全层)P99 < 20 ms。
|
||||
- **优化**:① DLS 目标反馈的 6 次串行有限差分改为一次批量 FK(数值差 ≤0.013°,DLS 25→10–17 ms);② 单题安全层按候选早停(首个全安全候选即停,16 点→2–4 点)。
|
||||
- **指标(桌面 RTX 3090,B=1,Python;GPU 时钟抖动使 p50 在 40–65 ms 间波动)**:单次规划 **p50 40–65 ms / p99 70–80 ms**;分解:features 10–17 ms、推理 8–14 ms、DLS 反馈 10–17 ms、安全层 10–17 ms。
|
||||
- **实时频率扫描(n=256,部署口径,gain1.8 + safety + partial)**:
|
||||
|
||||
| 规划节奏 | 控制周期 | 到达 | time_ok_1p3 | 碰撞 | 刹停/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 每步(commit_n=1) | 20 ms | **42.2%(n=512)** | 29.1% | 2.9% | 37.4 |
|
||||
| 每 3 步(commit_n=3) | 60 ms | 36.3% | 16.4% | 1.6% | 14.3 |
|
||||
| 每 6 步(commit_n=6) | 120 ms | 14.8% | 3.9% | 2.0% | 6.1 |
|
||||
| 每 10 步(commit_n=10) | 200 ms | 2.0% | — | 1.2% | 3.7 |
|
||||
|
||||
- **结论**:**当前方案不满足 50 Hz 实时**(单次 40–80 ms,超预算 2–4 倍);42.2% 的成绩依赖每步规划(20 ms 节奏)。实时可行节奏下:每 3 步 → 36.3%(p50 有余量、p99 需继续优化),每 6 步 → 14.8%。**实时性是当前方案的硬约束,不是仿真耗时问题**。
|
||||
- **下一步(需确认)**:① 浏览器 ONNX 路径实测(推理 P99 历史 10–12 ms;特征/安全层在 JS 端需 `tests/policy_bench.cjs` 实测);② 特征缓存 + 安全层粗筛,目标单次 P99 < 20 ms;③ 或接受「每 3 步规划」的 60 ms 周期(36.3%)。
|
||||
|
||||
## 轮次 24:实时性优化与新方法(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **目标**:准确率更高 + 效率满足 50 Hz(20 ms 控制步)。
|
||||
- **① 混合节奏(规划每 3 步 + 执行时每步反馈)——负结论**:plan fb1.8 + exec fb1.8 → 36.3%(与纯规划反馈的 commit_n=3 完全相同);plan fb0 + exec fb1.8 → 仅 1.6%。执行时反馈不带来额外收益(规划时的反馈已是主要贡献),此捷径不成立。
|
||||
- **② 特征几何优化(有效)**:特征只用 8 组间隙(GP),但原实现算全部 49 组(67256 簇对)。新增 `Kin.gm_groups(q, GP, cap)`:按 7 连杆组对过滤簇对(簇索引 → `cl_g` → 组对 min/max),只算 8 组。
|
||||
- 数值验证:与 `gaps()['gm'][:, a*7+b]` 逐项一致(diff 0)。
|
||||
- 延迟:gm 计算 10.4→**7.1 ms**(p50),单次规划省约 3 ms。
|
||||
- **③ 浏览器部署**:导出 `webapp/models/traj_policy.onnx`(dag2 best,19.8 MB,对拍误差 3.1e-6,act_scale 1.0);`ik-policy.js` 加全程目标反馈(`fbGain`)+ `commit` 可配;`ik-policy-glue.js` 支持 `?fb=&commit=`(默认 fb=1.8、commit=3)。
|
||||
- JS bench(Node,10 题,fb1.8+commit1):到达 0、**刹车率 47.3%**(每 2 次规划刹停一次)、planP50 43.8 / P99 70.5 ms。JS 安全层(5 mm 护栏 + 距离认证)比 Python(0.5 mm gap)保守,反馈+每步规划被大量拒绝。
|
||||
- **④ 实时口径的关键认识**:部署形态是**异步规划**(tick 20 ms 只执行队列;`plan()` 后台跑),所以 50 Hz 的约束是「tick < 20 ms + 规划在队列耗尽前完成」,不是「单次规划 < 20 ms」。
|
||||
- JS 的 tick 只做队列执行 + 投影/碰撞检查(~1.5–3 ms)→ 50 Hz 可行;
|
||||
- 规划 P99 70 ms → 需要 commit ≥ 4(80 ms 队列)才不饥饿;commit=3(60 ms)会偶发饥饿。
|
||||
- 准确率与规划频率的 trade-off:commit1 42.2%(不可 50Hz)、commit3 36.3%、commit6 14.8%。
|
||||
- **下一步**:① 在 commit=3/4 的实时节奏下找准确率提升(候选平均、dag3_fe 冻结编码器、更高 gain);② 把 gm_groups 同步到 JS(gapMatrix 只算 8 组);③ 降低 JS 安全层拒绝率(margin/认证预算调参)。
|
||||
|
||||
## 轮次 25:实时节奏下的准确率探索 + 推理提速(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **目标**:在 50 Hz 可行的节奏(commit=3,60 ms 周期)下提升准确率。
|
||||
- **效率优化(有效)**:
|
||||
- `Kin.gm_groups`:特征只算 8 组间隙(数值 diff 0),features 10.4→**7.1 ms**;
|
||||
- **onnxruntime CPU 推理 3.9 ms**(PyTorch 11.1 ms,部署端本来就是 ONNX);
|
||||
- 单次规划 ≈ 7.1(features)+3.9(推理)+11(DLS)+8(安全)+2 ≈ **32 ms p50 / ~45 ms p99**;
|
||||
- 异步部署下 tick(执行步 + 碰撞检查)2–3 ms → **50 Hz 控制可行**;commit=3(60 ms 队列)可覆盖规划 p99。
|
||||
- **准确率尝试(n=256,commit=3,gain1.8,safety_partial)**:
|
||||
|
||||
| 方法 | 到达 | time_ok | 碰撞 | ratio2s | 刹停 |
|
||||
|---|---|---|---|---|---|
|
||||
| dag2 + consist(基线) | **36.3%** | 16.4% | 1.6% | 0.277 | 14.3 |
|
||||
| dag2 + 候选平均(select=average) | 29.7% | 20.3% | 0.4% | 0.432 | 55.9 |
|
||||
| 冻结编码器模型(dag3_fe) | 34.4% | 14.8% | 2.0% | 0.332 | 15.8 |
|
||||
| dag2 + commit=4 | 28.9% | 8.2% | 3.5% | 0.339 | 10.5 |
|
||||
|
||||
- **结论**:候选平均、冻结编码器、更稀疏规划**均未超过 36.3%**。**50 Hz 节奏下的最佳配置 = dag2 + commit=3 + consist + gain1.8 + safety_partial:到达 36.3% / 碰撞 1.6% / ratio2s 0.277**(相对非实时的 42.2% 差 5.9 pp)。
|
||||
- **下一步**:若要把实时准确率再拉高,需要把单次规划再压到 ~22 ms 以支持 commit=2(40 ms 周期,预计 ~39%);或提升模型本身精度(训练侧)。
|
||||
|
||||
## 轮次 26:解析雅可比 DLS——去掉差分噪声,准确率大幅提升(exp: `exp/dagger-deploy`,新历史最佳)
|
||||
|
||||
- **背景**:DLS 目标反馈此前用 6 次批量有限差分(ε=1e-3°)求雅可比。float32 下旋转项信噪比只有 ~1%,位置项 ~3.6%(与 autograd 对拍,见下),相当于每次规划往反馈方向注入 2–4% 的随机扰动。
|
||||
- **假设**:改用几何雅可比(一次 FK)可同时去噪 + 提速;闭环准确率应不降、延迟应下降。
|
||||
- **判据**:① 雅可比与 autograd 一致(rel < 1e-4);② n=256 commit=3 到达 ≥ 36.3%(轮次 25 基线,不劣化);③ DLS 单次延迟下降。
|
||||
- **配方**:`Kin.jac` 几何雅可比(Jv_k = a_k×(p−o_k),Jw_k = a_k,度单位);`dls_steps(analytic=True)` 默认;其余同轮次 25(dag2、K=8、gain1.8、safety_partial、replan=empty)。
|
||||
- **数值对拍**(`ml/eval/check_dls.py`):
|
||||
- 位置项:解析 J 与 autograd 逐项一致(rel 0.00000);有限差分 J 自身 rel 误差 **3.58%**。
|
||||
- 姿态项:解析 J 与差分 DLS 输出 max 0.20°/rel 5%(差的部分是 rotvec 的二阶项 Jr(e),非噪声)。
|
||||
- DLS 输出:pos-only max 0.12°,pos+rot max 0.20°。
|
||||
- **延迟**(B=1,3090):n=1 **9.3→7.1 ms**,n=3 **27.7→19.5 ms**(每次 DLS 少 6 次批量 FK)。
|
||||
- **闭环(replan=empty,analytic,n=256)**:
|
||||
|
||||
| commit | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 3(FD 对照,完全复现轮次 25) | 36.3% | 16.4% | 1.6% | 0.277 | 14.3 |
|
||||
| 3(解析 J) | 46.5% | 16.4% | 1.6% | 0.337 | 13.5 |
|
||||
| 2(解析 J) | **50.4%** | 27.0% | 2.0% | 0.287 | 22.1 |
|
||||
| 1(解析 J) | **57.4%** | **31.6%** | 3.1% | 0.251 | 39.1 |
|
||||
|
||||
- **n=512 复核(commit=2,解析 J)**:到达 **49.2%** / time_ok **26.2%** / 碰撞 **2.3%** / ratio2s 0.351 / 刹停 21.0(n=256 的 50.4% 在噪声内)。
|
||||
- **结论**:
|
||||
- 解析雅可比**既快又准**:commit=3 到达 36.3→46.5%(+10.2 pp),碰撞不变;commit=2 达 **49.2%**(历史最佳 42.2% 是 commit=1 + FD,现在 commit=2 就超过),碰撞 2.3%。
|
||||
- 机理:DLS 差分雅可比在 float32 下的 2–4% 噪声被慢变误差闭环放大(轮次 4/闭环根因结论:σ>0.02×STEP 即崩);解析 J 去掉噪声后失败题里"能到 50 mm 却到不了"的那批能收敛了(err_fail 中位数 51→217 mm:失败集两极分化,成功集变大)。
|
||||
- ratio2s 在 commit=3 变差(0.277→0.337)但到达更多:慢收敛题现在"最终到得了",不是"更快"。
|
||||
- **新历史最佳(50 Hz 目标节奏,commit=2)= 49.2% / 碰撞 2.3% / ratio2s 0.351 / time_ok 26.2%**;非实时上限(commit=1)= 57.4%(n=256,待 n=512 复核)。
|
||||
- **下一步**:① n=512 复核 commit=1;② 积分反馈(fb_ki,针对慢变偏差)在 commit=2 上叠加;③ 浏览器 JS 端同步解析 J;④ commit=2 的 40 ms 周期实测规划 P99(Python/JS)。
|
||||
|
||||
## 轮次 27:解析 J 后重扫增益 + 积分/缓存反馈(负结论)+ 浏览器安全层对齐(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **目标**:解析雅可比改变了反馈噪声结构,重扫增益;同时修浏览器部署路径的安全层(轮次 25 遗留:bench 刹车 47%、到达 0)。
|
||||
- **增益重扫(commit=2,empty,analytic,n=256)**:
|
||||
|
||||
| gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1.8 | 50.4% | 27.0% | 2.0% | 0.287 | 22.1 |
|
||||
| **2.2** | **52.0%** | 29.7% | 1.6% | 0.329 | 35.3 |
|
||||
| 2.5 | 49.6% | 34.4% | 1.2% | 0.334 | 47.9 |
|
||||
| 2.8 | 47.7% | **34.4%** | 2.0% | 0.362 | 59.8 |
|
||||
|
||||
- **n=512 复核 gain 2.2(commit=2)**:**到达 50.8% / time_ok 28.9% / 碰撞 1.4% / ratio2s 0.343 / 刹停 35.9**。**新历史最佳(50 Hz 节奏)= gain 2.2 + commit=2**。
|
||||
- **负结论**:
|
||||
- 积分反馈(fb_ki=0.3,decay0.9,限幅0.5·STEP):到达 48.0%(gain2.2 单独 52.0%),−4.0 pp。慢变偏差用积分项补偿会过冲,不采用。
|
||||
- 反馈隔次复用(fb_every=2,省 ~3.5 ms/次规划):到达 46.9%(−5.1 pp)。误差方向变化比预期快,缓存不成立。
|
||||
- **浏览器部署修复(有效)**:三个与 Python 仿真不一致的 bug 修掉后(`webapp/js/ik-policy.js`):
|
||||
1. `tick` 不夹关节限位(Python `clamp_box` 每步夹)→ q 一旦越限,认证的 `inBox` 永远拒绝所有候选,原地刹停到超时(certWhy.box 2650 次);现执行与候选投影都夹 LIM。
|
||||
2. 安全层整段拒绝 → 刹停;移植 `safety_partial`:整段不过时依次缩短前缀,执行到第一个不安全步。
|
||||
3. 新增 `segCert` 开关(默认 true):连续区间认证(距离下界递归)~40 ms/次,是 50 Hz 预算的主要超支项;关闭后改为离散判定「每步端点+中点 collideFree」(与 Python 安全层同口径),默认仍走严格认证,bench 用 `--seg 0` 测部署口径。
|
||||
- JS bench(nc10,fb2.2,commit2;`node tests/policy_bench.cjs ... --seg 0`):
|
||||
|
||||
| 集合 | 修复前 ok/reached | 修复后 ok/reached | 刹车率 | safety P50 | plan P50 / P99 |
|
||||
|---|---|---|---|---|---|
|
||||
| near_column_pos | 0/—(轮次 25) | **5/7** | 79.5→**48.1%** | 41.6→**19.8 ms** | 56.1→**33.5 / 61.1 ms** |
|
||||
| near_column_pose | 0/— | **3/3** | 80.4→**31.6%** | 43.4→**22.8 ms** | 57.3→**37.6 / 66.3 ms** |
|
||||
|
||||
- **结论**:实时配置定为 `dag2 + 解析 J + gain2.2 + commit2 + safety_partial + replan=empty`:n=512 到达 **50.8%**、碰撞 **1.4%**。浏览器端从「0 到达 + 47% 刹车」恢复到 nc 集可达,plan P50 33.5/37.6 ms;P99 仍 61–66 ms,commit=2 的 40 ms 队列会偶发饥饿,需继续压 P99(K/安全候选数/JS 特征)。
|
||||
- **下一步**:① Python 测 `safety_try=1`(只认证首选候选)对准确率的影响,若无损则 JS 同步(省多次 collideFree);② K=4/6 的准确率-时延权衡;③ JS `gapMatrix` 同步只算 8 组特征;④ commit=2 异步部署的饥饿率实测。
|
||||
|
||||
## 轮次 28:端点进度选择——时间效率大幅提升(exp: `exp/dagger-deploy`,新历史最佳)
|
||||
|
||||
- **假设**:一致性选择奖励"与上一条计划一致",但失败主因是超时(轮次 13:失败 57% 是超时)。改成直接按候选块末态离目标的距离选(`select='endpoint'`),应提升时间效率与精度。
|
||||
- **判据**:time_ok_1p3 显著上升、到达不降、碰撞 ≤3%。
|
||||
- **配方**:dag2 + 解析 J,gain2.2,commit=2,replan=empty,safety_partial;n=256 → n=512。选择器用 `goal_err(fut[-1])`(只做一次 K 端点 FK,不额外推理)。
|
||||
- **指标(n=256,commit=2)**:
|
||||
|
||||
| 选择器 | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| consist(轮次 27) | 52.0% | 29.7% | 1.6% | 0.329 | 35.3 |
|
||||
| **endpoint** | **52.7%** | **37.5%** | 2.3% | **0.210** | 34.6 |
|
||||
| mix(consist − 0.1·端点mm) | 50.0% | 30.5% | 2.3% | 0.300 | 35.1 |
|
||||
|
||||
- **n=512 复核(endpoint,commit=2)**:**到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285 / 刹停 35.7**(consist n=512:50.8% / 28.9% / 1.4% / 0.343 / 35.9)。
|
||||
- **节奏曲线(endpoint,n=256)**:commit=1 → **59.4% / 42.6% / 3.9% / 0.133**;commit=2 → 52.7% / 37.5% / 2.3% / 0.210;commit=3 → 47.7% / 27.0% / 1.2% / 0.245。
|
||||
- **负结论**:
|
||||
- `safety_try=1/2`(只认证前 1/2 条候选):41.4% / 44.9%(全 K 52.0%)——候选兜底对到达贡献 ~10 pp,不能省。
|
||||
- K=4:47.3%(K=8 52.0%)——少采样同样损失到达。
|
||||
- DLS 兜底半步(全拒时朝目标走一步并认证):52.3%(基线 52.0%),碰撞 1.6→2.7%,无收益;重复多步版本碰撞 45.7%(第二步未认证),已回退为单步且默认关闭。
|
||||
- 增益调度(远 2.8/近 2.2):47.3% / time_ok 32.8%,等同 gain2.8 常数;高增益在远处就造成过冲,按距离衰减无效。
|
||||
- **浏览器同步**:`select='endpoint'` 进 Runner/bench(`--select endpoint`),冷启动跳过 8 份前瞻(省 ~80 ms P99,冷启动价值评分被端点排序替代)。nc10 bench(--seg 0 --select endpoint):pos ok 4/5 reached、pose ok 2/4,刹车率 32.1%/25.5%(consist 48.1%/31.6%),planP50 31.9/36.5 ms、P99 63.5/67.5 ms。
|
||||
- **同步 bench 耗时与队列耗尽估算(不是 50 Hz 调度实测)**:Runner 的 `starveTicks = max(0,ceil(planMs/20) − 规划时队列步数)`,不含真实 tick 时间戳。
|
||||
|
||||
| 集合(endpoint, commit=2, --seg 0) | 到达 ok/reached | 刹车率 | planP50/P99 | 估算饥饿 tick/次规划 |
|
||||
|---|---|---|---|---|
|
||||
| near_column_pos (10) | 4/5 | 32.7% | 32.0/62.1 ms | 0.87 |
|
||||
| near_column_pose (10) | 2/3 | 25.6% | 36.8/68.0 ms | 0.78 |
|
||||
| route40 (20) | 12/13 | 40.4% | 44.3/68.1 ms | 1.26 |
|
||||
|
||||
- commit=2 对应 40 ms 队列,route 集 planP50 44 ms 已超过;估算提示可能饥饿,但该 bench 等规划完成才 tick,没有观测实际空转。
|
||||
- commit=3 对应 60 ms,仍低于上述 P99,不能据此宣称连续 50 Hz。其 Python 同步结果:endpoint n=256 → 47.7% / 27.0% / 1.2% / 0.245;gain2.5 为 45.7% / 24.6%。
|
||||
- **结论(经轮次 29 更正)**:端点选择 + 解析 J + gain2.2 + commit=2 + safety_partial,在 n=512 同步仿真为 **到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285**。历史 42.2% 使用 commit=1 与 FD,非单变量对照。commit=1 到达 59.4% 但碰撞 3.9%;无论 2.0% 还是 3.9%,开安全层均未通过零碰撞要求,实时性亦未通过。
|
||||
- **下一步**:先查碰撞来源,保持严格认证和查看器默认;再测真实异步调度及认证成本,不因 Python 收益直接切换部署默认。
|
||||
|
||||
## 轮次 29:安全层全拒后减弱目标反馈(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **假设**:目标反馈将绕障候选推向障碍,连续全拒会反复停在同处;仅在上一计划被全拒时减小下一计划反馈,可能保留正常段收敛速度并减少重复刹停。
|
||||
- **判据**:同任务、同 seed、同 K、同认证下到达和 time_ok 不下降,碰撞不增加;部署门槛仍是零碰撞及真实时延达标。所有实验不重训、不扩大数据。
|
||||
- **固定配方**:`python -m ml.eval.eval_cl --n 256 --K 8 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --seed 0`;dag2 checkpoint、几何雅可比。下表仅改列出的参数,默认 `fb_block_gain=1,dls_safety=0`。512 题按两个 256 批、每批 seed=0,沿用轮次 28 顺序。
|
||||
|
||||
| 改动(n=256) | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|
||||
|---|---|---|---|---|---|
|
||||
| 原配置复跑 | 52.7% | 37.5% | 2.3% | 0.210 | 34.6 |
|
||||
| `--dls_safety 1` | 52.7% | 37.5% | 2.0% | 0.210 | 34.6 |
|
||||
| `--fb_clear 20 --fb_cmin 0.3` | 32.0% | 5.9% | 1.2% | 0.323 | 3.2 |
|
||||
| `--sub_refresh 1` | 51.6% | 35.5% | 2.0% | 0.279 | 33.8 |
|
||||
| `--select adapt` | 52.7% | 37.9% | 2.3% | 0.210 | 34.6 |
|
||||
| `--fb_block_gain 0.3` | 56.2% | 38.7% | 3.1% | 0.178 | 17.2 |
|
||||
| `--fb_block_gain 0.5` | **57.4%** | **39.1%** | 3.1% | 0.210 | 19.3 |
|
||||
| `--fb_block_gain 0.7` | 55.5% | 38.3% | 2.3% | 0.189 | 25.0 |
|
||||
| `--fb_block_gain 0.5 --dls_safety 1` | **57.4%** | **39.1%** | 2.7% | 0.210 | 19.2 |
|
||||
|
||||
- **单独减半反馈 n=512**:到达 55.1% / time_ok 36.1% / 碰撞 2.5% / ratio2s 0.251 / 刹停 19.5;相对轮次 28 同规模基线 52.1% / 35.2% / 2.0% / 0.285 / 35.7,到达 +3.0 pp、碰撞也 +0.5 pp,不满足“碰撞不增加”判据。
|
||||
- **为何不采用其他变体**:所有近障碍状态都衰减反馈会明显拖慢到达;子目标重预测无收益;adapt 全拒后旧计划本来就失效,一致性分数全为 0,实质变成等分选首条而非“继续绕行”。
|
||||
- **原配置失败分解(n=256)**:135 题按时到达,121 题失败(其中 20 题后来才到,101 题始终没到)。A/B/C/D/Dr 分别 128/74/32/13/9 题,B/C 失败题刹停中位数约 96/93 次;D 留在分母,不用删题提高成绩。
|
||||
- **碰撞定位**:原配置共 6 题碰撞,只有 224 号首次碰撞紧跟 DLS 接管;另外 5 题首次碰撞早于接管或根本未接管。为 DLS 加端点/中点检查仅减少 1 题,不能解释剩余碰撞。候选认证的未夹紧累加点与执行的逐步夹紧点不同是待验证假设,尚不能断言根因。
|
||||
- **计时**:n=256 减半反馈+DLS检查整批墙钟 243.6 s,单独减半 n=512 为 443.4 s;这些不是规划时延,不能证明 50 Hz。反馈条件分支未增加几何查询,但其实际延迟收益未测。
|
||||
- **记录改进**:评估输出完整参数、checkpoint/任务 SHA256、批大小和随机种子规则;增加时限内无碰撞到达率;修正 n_ref、参考时间占比和条件指标的跨批汇总。可保存实际轨迹供不重新采样的回放,NaN 在 JSON 中写为 null,执行进度每 50 步输出。
|
||||
- **回归检查**:`python -m unittest tests.test_eval_cl -v` 六项通过,覆盖跨批分母、空条件集、非有限数、首撞探针正对照、精修被拒后保留安全候选、只有全拒后才减弱反馈。精修对照在关闭检查时确实碰撞,打开后保留候选,证明门会执行;这是合成测试,不替代真实几何验收。
|
||||
- **仍未修复的边界**:`fb_fallback=0` 保持关闭;非部分前缀分支仍有“只验一步却重复执行”问题,不能开启部署。速度限制在反馈叠加前而非叠加后,尚未验证速度/加速度符合硬约束。
|
||||
- **减半反馈 + DLS 检查的 n=512 复核(完整输出 `docs/experiment_round29.json`)**:到达 **282/512 = 55.1%**、时限内无碰撞到达 **280/512 = 54.7%**、time_ok **36.1%**、碰撞 **9/512 = 1.8%**、ratio2s **0.251**、刹停 **19.23/题**。到达题末端误差中位数 **0.196 mm**;有参考且到达题的行程比中位数 **2.425**(远高于 1.10 要求)。整批墙钟 **488.4 s**,非控制延迟。
|
||||
- 完整命令:`python -u -m ml.eval.eval_cl --n 512 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --fb_block_gain 0.5 --dls_safety 1 --out docs/experiment_round29.json --dump ml/runs/round29_rows.json --trace ml/runs/round29_trace.pt`。
|
||||
- 与轮次 28 同规模端点基线相比,到达 +3.0 pp、time_ok +0.9 pp、碰撞少 1 题(10→9),不足以宣称稳定安全改善;n=256 碰撞仍由 6→7 题。
|
||||
- **首撞回放(`docs/experiment_round29_collisions.json`)**:`python -m ml.eval.diag_collision --trace ml/runs/round29_trace.pt --out docs/experiment_round29_collisions.json`。9 题首次碰撞在 CPU float32/float64 均复现,均位于 **J5 限位**;q0 与首次碰撞前一点均为正间隙,首撞采样点负间隙约 **−0.017 至 −0.936 mm**。这排除了这 9 次碰撞由起点本就碰撞或 float32 单独翻号造成,但还需要记录原认证点才能证明夹紧的因果作用;0.01° 加密仅覆盖首撞段,不是整条轨迹安全审计。
|
||||
- 双精度运动学/间隙另与已有 JS 2000 个样本对拍:`python -m ml.eval.check_parity` PASS,碰撞布尔差 0、间隙最大差 2.53e−13 mm。首撞回放仍共用碰撞模型,不替代真机或独立连续认证。
|
||||
- **结论与下一步**:减少连续刹停确有小样本收益,但碰撞、行程和真实时延仍未过门槛;保留为实验选项,不更改查看器默认。下一轮记录原认证点与 J5 夹紧后实际点,修复后用同批同 seed A/B 和独立细采样复核,禁止靠关掉认证提速。
|
||||
|
||||
## 轮次 30:认证与执行统一逐步限位(exp: `exp/dagger-deploy`,局部修复有效,仍未零碰撞)
|
||||
|
||||
- **假设与判据**:安全层认证的是未夹紧累加轨迹,执行器却逐步夹紧 J5,导致认证通过后碰撞。必须同时证明原证书安全、执行点碰撞、记录命令经执行算式能准确重建实际点;修复后保持同任务、同种子、同模型、同增益和同排序,到达不下降,并用不依赖原 `col` 标记的加密回放检验。
|
||||
- **固定配方**:`python -u -m ml.eval.eval_cl --n 256 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --fb_block_gain 0.5 --dls_safety 1`;K=8、seed=0、analytic DLS、dag2 checkpoint、fallback=0,其余参数默认;分别附 `--out docs/experiment_round30_before.json --trace ml/runs/round30_before.pt --dump ml/runs/round30_before_rows.json` 和对应的 `after` 路径。未训练、未扩题、未改 JS 默认。
|
||||
- **可比性**:取证版 before 与轮次29首批256题完整轨迹逐元素完全相同(最大差 0°),ok/col/brakes/err/t_done/first_hit 也完全相同;before/after 配置仅输出路径不同。任务 SHA256=`249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772`,checkpoint SHA256=`f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1`,kin_fp=`2ebcbf17`。
|
||||
- **直接证据**(`docs/experiment_round30_certificates.json`):旧版7次首撞的原认证端点/中点在 float32 与 float64 均为正间隙,实际执行段均有负间隙;`clamp(previous + recorded_command)` 与实际端点逐位一致。原证书与实际点主要差在 J5,首撞段差 0.340°–4.317°;其他轴仅有最高约 7.6e-6° 的累加舍入差,不足以解释毫米级穿入。全部有效认证步中,5298/34069 步偏差 >0.001°,最大 7.587°。
|
||||
- **修复范围**:`execution_points` 按执行器相同顺序逐步夹紧,端点和中点都由实际路径计算;整段、部分前缀和近目标 DLS 共用。r3 的旧队列终点和特征前缀改为真实执行结果。fallback 保持实验默认关闭,但两分支统一为“认证一步后补零”,不再把只验过一步的动作重复执行;端点和中点都认证。候选排序和反馈增益未改,0.5 仍只是间隙截顶,正间隙判据没有放宽。
|
||||
|
||||
| 指标(同256题) | 修复前 | 修复后 |
|
||||
|---|---|---|
|
||||
| 按时到达 | 147/256 = 57.4% | 同147题,57.4% |
|
||||
| 原端点/中点口径无碰撞到达 | 145/256 = 56.6% | 147/256 = 57.4%(下述加密审计否定其“全程无碰撞”含义) |
|
||||
| time_ok_1p3 | 39.1% | 39.1% |
|
||||
| 原端点/中点口径碰撞题 | 7/256 | 0/256 |
|
||||
| 成功题末端误差中位数 | 0.1951 mm | 0.1890 mm |
|
||||
| 成功题平均时间 | 2.2993 s | 2.3076 s |
|
||||
| 2 s 误差比中位数 | 0.2100 | 0.2100 |
|
||||
| 刹停/题 | 19.2383 | 19.2500 |
|
||||
| 成功题指令行程比中位数 | 2.4450 | 2.4450 |
|
||||
| 从实际轨迹重算成功题行程比 | 2.4251 | 2.4251 |
|
||||
| 整批同步仿真墙钟 | 247.515 s | 248.834 s |
|
||||
|
||||
- **执行一致性复核**(`docs/experiment_round30_comparison.json`):修复后34107个有效认证步,认证端点/中点与实际执行点均逐位一致,最大差 0°;有效时段没有未经认证的移动步。这里“认证”仍仅指离散端点/中点,不能推出连续安全。CPU B=1、commit=2 的预测函数单独测量:100次预热、1000次采样、单线程,P50 0.0388→0.0518 ms,P99 0.0691→0.0939 ms;不是完整规划延迟,不能证明50 Hz。
|
||||
- **独立加密审计的正反对照**:预先固定12题 `0,21,84,128,145,164,175,180,197,215,224,255`,其中包含全部7题旧首撞及5题其他任务;不按新版 `col` 过滤。逐题回放至自身时限或到达,含起点,每个插值段最大轴步距≤0.05°并包含中点,float64、每批64点。命令:`python -u -m ml.eval.diag_collision --trace ml/runs/round30_before.pt --dense_indices 0,21,84,128,145,164,175,180,197,215,224,255 --max_step_deg 0.05 --device cuda --out docs/experiment_round30_dense_before.json`,after 同理;串行运行。
|
||||
- before:56118点,**7/12题检出碰撞**,14.870 s;证明探针确实能抓旧碰撞。
|
||||
- after:56566点,**仍有1/12题碰撞**,15.057 s;其余11题采样点间隙为正。这是小样本有限采样,不是完整256题的连续认证。
|
||||
- 剩余第180题,执行段1.08–1.10 s:上一点/中点/端点 float64 间隙为 **0.5 / 0.11723 / 0.06521 mm**,都安全,但段内约76.6%处为 **−0.14927 mm**;该段0.01°再加密185点仍检出,见 `docs/experiment_round30_missed_segment.json`。因此这次残余是端点/中点漏检,而非认证与执行错位;它比旧离散首次碰撞1.14 s还早。不得把内部计数0当成修好了;已知真实无碰撞到达至多146/256=57.0%,其余未加密任务仍未知。
|
||||
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl -v` 14项通过。覆盖整段/部分限位拒绝、只保留真实安全前缀、DLS被拒后保留候选、r3旧队列起点和特征前缀及逐点证书、fallback单步/中点拒绝。把预测函数在内存中替回旧算式,限位、DLS和r3测试重新失败。加密探针另有合成正对照:原端点/中点全安全且 `col=0`,仍检出四分之一处薄障碍;安全负对照、截止后/到达后碰撞排除均通过。
|
||||
- **新增硬约束证据**:before/after 两组256题实际轨迹都出现超速,最大 **1.660004×VMAX**(来源是动作限幅后叠加0.3×gain2.2反馈)。实际各轴最大速度约 `[99.600,99.600,149.400,149.400,199.200,298.800]°/s`,并非只看原始指令推测。行程仍约参考2.425倍;速度、加速度和真实控制时延均未达部署验收。历史 `travel` 累积的是指令而非夹紧后位移,本轮已同时列出实际轨迹重算值,不悄悄换分母。
|
||||
- **结论与下一步**:逐步限位一致性修复有效,未牺牲本批到达率;但加密审计仍检出段内碰撞,**不能部署、不能宣称零碰撞或50 Hz合格**。下一轮先验证更强的段内认证,再单独评估反馈后的速度/加速度约束与真实端到端调度;每次保持小样本单变量对照,继续保留失败证据。
|
||||
|
||||
## 轮次 31:连续段认证的确定界与成本实测(exp: `exp/dagger-deploy`,判据变强,实测进不了 20 ms)
|
||||
|
||||
- **假设与判据**:轮次 30 的端点/中点检查漏掉段内碰撞(第180题 1.08–1.10 s,段内 −0.14927 mm,端点/中点均正)。若用可证明的全姿态运动界对整段做 Lipschitz 判据,应当(a) 在同一个漏检段上给出碰撞或拒绝,(b) 预算不足时只会拒绝而不会放行,(c) 不放宽 `>0` 判据。判据失败或耗时超预算都算负结论,必须实测不能外推。
|
||||
- **确定界替代随机采样界**:JS 侧 `leverTable(4000)`/`leverBounds(20000)` 是随机姿态最大值再 ×1.05,不是全姿态上界。本轮按三角不等式给闭式界:组 g 的胶囊端点到关节 k(<g) 的最大位移 = (Σ_{j=k}^{g-2}‖P[j+1]−P[j]‖ + max_e‖e−P[g−1]‖)·|Δq_k|(k=g−1 时末项换成端点到该轴的垂直距离),组对 (ga,gb) 只累加 ga..gb−1,取组内最小仍保留 Lipschitz 性质。Python 端 `Kin.cert_lever`(15×6)即此表,未改 `lever`,也未改 JS。
|
||||
- **界的量级**(STEP=[1.2,1.2,1.8,1.8,2.4,3.6]°):单整步在各组对上的位移界 **5.93–48.86 mm**,0 层通过要求端点间隙之和 > 界,即单端间隙至少 3.0–24.4 mm。实际执行段的间隙普遍只有 0.001–0.5 mm,所以“必须细分”是结构性的,不是实现缺陷。
|
||||
- **基础求距 bug 与影响范围**:模型里有 1 个退化胶囊(索引 686,组 3)。旧 `seg_seg_dist` 在第二条线段退化成点时固定取该点为第二条起点,没把点投影到第一条线段上——[0,2] 段对点 1 的实测距离旧值 1 mm、反向调用约 1e−9 mm、真值 0。修复后在已有 2000 个对拍姿态的前 64 个姿态上,670 个“退化胶囊作第二条”的细对里 **20661/42880 个距离变小(旧值偏大,最大 86.003 mm)**,正反调用一致。但这批变化没有进入判定:把轮次 30 修复后 48 段的 7586 个 0.01° 加密点分别用修复前(95da491)与修复后内核重算,**组最小间隙逐位相同(最大差 0.0 mm)、碰撞布尔差 0**;`python -m ml.eval.check_parity` 仍 PASS(2000 样本,fk 4.55e−13 mm、间隙 2.52e−13 mm、特征 5.96e−08、梯度相对误差 1.85e−06、碰撞布尔差 0)。即这轮修复是正确的,但不解释轮次 30 的漏检,也不改变既有成绩。
|
||||
- **认证实现**:`Kin.certify_segments` 为 float64 批量前沿二分,每根段独立查询预算,端点间隙用 `min(gap,cap)` 故钳制只会更保守;判据 `ga+gb > bound+2eps` 且两端 >eps,任一子段碰撞即整根判碰撞;超深度、超预算、非有限间隙、越出关节盒一律拒绝(`unresolved`/`outside_box`),不放行未知。宽相位 cap 可按本层界收到 `bound/2+eps`(`adaptive_cap`)。**尚未接入 rollout,未改查看器默认。**
|
||||
- **48 段固定样本**(`docs/experiment_round31_certificates.json`,沿用轮次 30 的 12 题,每题取首/中/末移动段 + 原证书最小间隙段 + 显式指定 180:54;cap=20、eps=1e−4、max_depth=12):
|
||||
| 每根查询预算 | 判安全 | 判碰撞 | 未知拒绝 | 查询数 p50/max |
|
||||
|---|---|---|---|---|
|
||||
| 2 | 20 | 0 | 28 | 2 / 2 |
|
||||
| 8 | 26 | 1 | 21 | 4 / 8 |
|
||||
| 32 | 37 | 1 | 10 | 4 / 32 |
|
||||
- 已知漏检段(180 题执行段 54)**判碰撞,仅用 5 次查询**;该段原证书 `certified=true`、原内部 `col=0`。48 段里 0.01° 加密采样检出碰撞 1 段,认证接受该类段 **0 段**;整批与逐根调用结果逐位一致。
|
||||
- 拒绝不等于安全:预算 2 时 28 段全部落 `unresolved`,实现不会把未知当通过。
|
||||
- **成本模型对上实测**(`docs/experiment_round31_cert_cost_model.json`):按最紧组对的界,所需层数 `L=ceil(log2(bound_pair/(ga+gb)))`、查询数 `2^L+1`。37 个判安全段需要 2–17 次(L=0..4),10 个未知段需要 **33 或 65 次**(L=5..6),与预算 32 恰好卡在中间一致;碰撞段因提前撞到负间隙只用 5 次。结论:认证成本由 `bound/间隙` 的对数决定,**降低界或提高间隙余量**才是可行杠杆,加预算只是线性买单。
|
||||
- **耗时负结论**(CUDA float64、B=1、驻留输入、每段4次预热+9次计时、变体按段交错,`docs/experiment_round31_adaptive_cap.json`):单段认证 **P50 22.93 ms、P90 58.43 ms、P99 101.57 ms,432 次里 225 次 > 20 ms**;自适应 cap 交错重测 P50 23.21 ms、P99 99.10 ms,**决策完全相同、耗时无收益**。先前顺序测得的“1.6×、整批 72→14.8 ms”经交错复测判定为 GPU 时钟/顺序漂移,不作为结论(48 根整批复测 260→212 ms 仍受同一偏差影响,且部署不会出现 48 根同时认证)。因此**连续认证按当前实现不能进 20 ms,更不能宣称 50 Hz**。
|
||||
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl` **23 项通过**(轮次 30 为 14 项)。新增段认证 8 项覆盖:薄障碍段内碰撞被界抓到而清晰段被接受、预算/深度耗尽只拒不放、恒间隙合成段上预算 4/8/9 对应查询 3/5/9(验证细分层数与查询数的解析关系)、批量与逐根及乱序一致、空批与全拒前沿、NaN/±Inf 间隙拒绝、越界拒绝、退化距离对称性与梯度。**变异实测**:把类型门去掉后 `max_depth=torch.tensor(255,dtype=torch.uint8)` 会回绕成 `range(0)`、整段判据被跳过,真实间隙 −0.05 mm 的段直接返回 `safe=true`;加门后同一调用报 `certification budgets must be plain ints`。这是本轮抓到的第二个误放行路径(第一个是轮次 30 的未夹紧认证)。
|
||||
- **口径与边界**:认证只在 float64 下进行,浮点求距误差没有形式上界(`seg_seg_dist` 距离下限约 1e−9 mm),eps=1e−4 mm 是工程容差不是证明;48 段取自轮次 30 修复后的执行轨迹,是固定样本不是新策略闭环;连续认证未接入 rollout,实际执行仍走端点/中点离散检查;速度/加速度超限(轮次 30 实测最大 1.66×VMAX)本轮未处理。
|
||||
- **结论与下一步**:判据确实变强(抓到离散检查漏掉的段内碰撞、未知只拒不放),但成本与 `bound/间隙` 的对数绑定,**当前实现不能部署,也不得把“认证通过”说成零碰撞**。下一步两条:(1) 把逐层依赖的二分改成**一次均匀细分**(固定层数、单个 gaps 调用覆盖全部子段),其实时可行性取决于几何查询成本随批大小是否近似平坦;(2) 把余量要求移到规划/速度侧——实际轨迹贴着 0.001–0.02 mm 间隙走,任何认证方案都会退化成几十次查询,而反馈叠加后的速度约束本来就是未完成项。
|
||||
|
||||
## 轮次 32:均匀细分认证与确定界的反证(exp: `exp/dagger-deploy`,认证仍进不了 20 ms,但定位到真正的成本杠杆)
|
||||
|
||||
- **本轮假设**:第31轮的二分认证把成本花在**多次依赖的 gaps 调用**上(B=1 时每次约 7 ms 起步)。若改成固定层数的一次均匀细分,同样的判据应只付一次调用;同时需要单独证明"这个确定界本身没被低估",否则判据再快也没意义。
|
||||
- **实现**:`Kin.certify_uniform(a, b, n, eps, cap=None)`——每根段取 n 个子段、一次 gaps 调用查 n+1 个点,对**每个子段**用与二分版完全相同的判据(子段两端间隙之和 > 子段位移界 + 2eps;界对 Δq 线性,故子段界 = 整段界/n)。没有"未决"状态:证不出就是 `unproven`(拒绝),不存在把未知当安全。未接入 rollout,未改查看器默认。
|
||||
- **确定界的反证**(`docs/experiment_round32_bound_falsification.json`):对 48 段每段取 121 个网格点,逐组对比"实测最小间隙"与"预测下限 (ga+gb−bound)/2"。720 项检验**0 违例**;但只有 **61 项有信息量**(预测下限 < 观察 cap 2 mm 才可能被证伪;其余组对的预测下限中位数 15.9 mm、最大 19.96 mm,都被钳制恒等式自动满足)。61 项里最紧的一项仍比预测下限高 **0.035 mm**,p10 高 0.544 mm。因此这是"未能在真实运动上证伪该界",不是"证明了该界";浮点求距误差仍未做形式化上界。
|
||||
- **接受率曲线**(48 段单根,`docs/experiment_round32_uniform_validation.json`):
|
||||
|
||||
| 子段数 n | 判安全 | 判碰撞 | 证不出 | 二次加密(2n+1点)反证的误放行 |
|
||||
|---|---|---|---|---|
|
||||
| 8 | 26 | 1 | 21 | 0 |
|
||||
| 16 | 30 | 1 | 17 | 0 |
|
||||
| 32 | 34 | 1 | 13 | 0 |
|
||||
| 64 | 36 | 1 | 11 | 0 |
|
||||
| 128 | 38 | 1 | 9 | 0 |
|
||||
|
||||
第180题 1.08–1.10 s 那段(第30轮加密回放检出 −0.14927 mm、原离散证书 `certified=true`)在**每个 n≥8 都判碰撞**。
|
||||
- **宽相位 cap 的正确公式**(对第31轮一条负结论的更正):第31轮用 `bound/2 + eps`,那个值恰好**会把本可通过的子段钳成不可证**,所以既没提速也可能少接受;本轮改成"最小不挡住判定的宽相位"= `最大子段界 + 2eps`,n=4 时是 5.92 mm、n=128 时 0.37 mm。第31轮那句"自适应 cap 无收益"只在旧公式下成立,新公式的实测收益见下表(原记录保留,不回改)。
|
||||
- **计时 A/B**(`docs/experiment_round32_uniform_latency.json`;B=2 根=部署 commit2 口径,24 对根逐对按正序/逆序**交替**测全部配置,每配置 3 次预热 + 7 次计时 = 168 次;CUDA float64):
|
||||
|
||||
| 配置 | 宽相位 mm | P50 | P90 | P99 | >20 ms 次数 | 安全/碰撞/拒(48根) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 均匀 n=8 auto | 5.92 | **9.68** | 13.03 | 15.16 | 0/168 | 26/1/21 |
|
||||
| 均匀 n=16 auto | 2.96 | **14.14** | 18.45 | 20.87 | 5/168 | 30/1/17 |
|
||||
| 均匀 n=32 auto | 1.48 | 20.21 | 27.88 | 32.27 | 87/168 | 34/1/13 |
|
||||
| 均匀 n=64 auto | 0.74 | 31.43 | 48.75 | 57.70 | 165/168 | 36/1/11 |
|
||||
| 均匀 n=128 auto | 0.37 | 59.76 | 89.06 | 109.09 | 168/168 | 38/1/9 |
|
||||
| 均匀 n=8 cap20 | 20 | 13.13 | 16.44 | 20.96 | 3/168 | 26/1/21 |
|
||||
| 均匀 n=32 cap20 | 20 | 28.41 | 39.58 | 53.02 | 153/168 | 34/1/13 |
|
||||
| 均匀 n=64 cap20 | 20 | 50.85 | 75.51 | 88.36 | 168/168 | 36/1/11 |
|
||||
| 均匀 n=128 cap20 | 20 | 98.46 | 139.98 | 167.04 | 168/168 | 38/1/9 |
|
||||
| 二分 budget32 | 20 | 54.88 | 82.15 | 117.20 | 133/168 | 37/1/10 |
|
||||
| 二分 budget65 | 20 | 71.83 | 108.56 | 122.53 | 133/168 | 41/1/6 |
|
||||
|
||||
- auto 与 cap20 的**决策列完全相同**,只是快 1.36–1.65×(n=8 13.13→9.68、n=128 98.46→59.76):这次是交错实测的真收益,不是第31轮那种顺序漂移。
|
||||
- 单次调用的均匀细分在同样接受率下比二分便宜 5 倍以上(n=8 9.68 ms vs 二分 54.88 ms),但二分在同样时间能多证出 11 根——两条路的取舍由下一段的结论决定,不单看耗时。
|
||||
- **负结论(部署口径)**:只有 n≤16 能把认证本身压进 20 ms(P50 9.68–14.14 ms),而这档只能证出 48 根里的 26–30 根(54–63%),其余必须刹停;n=32 起 P50 就已经吃满 20 ms,还没算推理。所以**连续段认证按现有几何口径不能同时满足"P99<20 ms"和"不牺牲到达"**。这些数是 CUDA float64 的单次调用,不能外推到浏览器 CPU float32 的 50 Hz 链路。
|
||||
- **机理定位与下一步**:成本 = 单次调用固定开销 × 由 `log(bound/间隙)` 决定的层数。要同时拿到"证得出"和"证得起",必须把**间隙抬起来**(安全层要求正余量,而不是 `gap>0`)并把**每步位移压回去**(反馈叠加在 ±1 限幅之后,导致第30轮实测最大 1.66×VMAX,界也随之变大)。顺带记下一个坑:安全层现在是 `kin.gaps(allq, 0.5)`,截顶值 0.5 mm 低于任何 >0.5 mm 的余量门限,直接把余量判据变成恒假——加余量必须同时抬这个 cap。
|
||||
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl` **26 项通过**(新增 3 项均匀细分)。含解析对照:合成恒间隙段(界 1 mm/度、间隙 0.1 mm)在 n=5 必判证不出、n=6 必判安全,且 `cap_required_mm` 等于 `1/n+2eps`;薄障碍在 n=4/8/64 判碰撞而 n=2 只判证不出(不误放行);`n` 给成张量被类型门拒绝。二次加密反证在 n=8..128 全部 0 误放行。
|
||||
|
||||
## 轮次 33:把速度做成可观测的硬约束,再测限幅与余门的代价(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **仪表先行**:`rollout()` 现在逐题记录**实际执行**的速度比 `max_j|Δq_j|/STEP`(STEP=VMAX·DT,所以 1.0 = VMAX)和按控制步的速度变化比;`eval_cl` 汇总 `speed_ratio_p50/p95/max`、`over_vmax_rate`、`accel_ratio_p95/max`,并把逐题值放进 `per`(极值/分位不进按 n 加权的批聚合)。
|
||||
- **仪表自证**:用第30轮 after 的 trace 独立重算得 max **1.660004×VMAX**、超速度题占比 **1.0**,与新指标在同一批上逐位一致(`speed_ratio_max` 1.660003662、`over_vmax_rate` 1.0);这是"先证判据有灵敏度",不是拿新数当结论。
|
||||
- **默认路径未变**:`--inc_cap 0 --gap_margin 0`(新默认)跑出的每个旧指标与第30轮 after 完全相同——success 0.57421875、无碰撞到达 0.57421875、time_ok 0.390625、碰撞 0、刹停 19.25、行程比 2.4449527263641357、ratio2s 0.20997506380081177、成功题误差 0.18899640440940857、dls_share 0.625(`docs/experiment_round33_base.json`)。整批墙钟 296.3 s 与第30轮 248.8 s 不可比:这期间我在同机跑了 CPU 分析,属自我污染,仪表开销需独占另测,不作为结论。
|
||||
- **两个新开关**:`inc_cap=1` 在**反馈叠加之后**再按每轴 VMAX·DT 限幅(此前 `inc=clamp(act,±1)·astep` 之后又加 `gain×fb`,所以 1.66× 是结构性的);`gap_margin>0` 让安全层要求间隙 > 余量而非 >0,并把宽相位 cap 自动抬到 `margin+0.5`——不抬的话 `gaps(allq, 0.5)` 的截顶值 0.5 会让任何 >0.5 的余量门限恒假。`col`(真实碰撞计数)仍按 `gap≤0` 判,不随余量改变,所以两者不会互相顶替。
|
||||
- **部署侧同病**(只读核查,未改 JS):`webapp/js/ik-policy.js:355` 也是 `inc[s][j] += fbGain*fbStep[j]` 在 ±1·STEP 之后叠加,所以浏览器控制器同样能超 VMAX;JS 的余量是 `o.margin=5` 的**投影式**护栏(`project()` 每轴 ≤projMax,只处理 gap<margin 的近距对),与本轮的"低于余量就刹停"不是同一机制;`ACC=5.0` 只用于刹停段长度,不是每步加速度上限(§9.2 也没给数值化的加速度门槛,所以 `accel_ratio` 只作变体间比较)。
|
||||
- **间隙分布的口径坑**:第30轮 `plans['gaps']` 是在 cap=0.5 下记录的,所以"最小间隙 ≥1 mm 的题占 0"是截顶假象。可说的是:逐题最小记录间隙 p5 **0.0005**、p25 0.0064、中位 **0.046** mm,57.4% 的题曾低于 0.5 mm——即策略实际贴障碍飞,而查看器护栏是 5 mm。要把 0.5 以上那段分开,必须抬 cap 重算(本轮后续做)。
|
||||
- **四组单变量对照**(同 256 题、同 seed、其余按第30轮配方):base / `inc_cap=1` / `gap_margin=0.5` / 两者同开。结果见下表。
|
||||
|
||||
- **四组单变量对照结果**(同 256 题、同 seed、K=8、commit 2、fb 2.2、fb_block_gain 0.5、dls_safety 1、select endpoint、replan empty、partial 1;`docs/experiment_round33_{base,cap,margin,both}.json`):
|
||||
|
||||
| 变体 | 到达 | 无碰撞到达 | time_ok_1p3 | 真实碰撞 | 刹停/题 | 速度比 p50 | 超 VMAX 题占比 | 2 s 误差比 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| base | 147/256 = 0.5742 | 0.5742 | 0.3906 | 0 | 19.25 | 1.660 | 1.000 | 0.2100 |
|
||||
| `--inc_cap 1` | 134/256 = 0.5234 | 0.5234 | 0.2656 | 0 | 19.55 | 1.000 | 0.000 | 0.2790 |
|
||||
| `--gap_margin 0.5` | 144/256 = 0.5625 | 0.5625 | 0.3789 | 0 | 20.05 | 1.660 | 1.000 | 0.1817 |
|
||||
| 两者同开 | 133/256 = 0.5195 | 0.5195 | 0.2617 | 0 | 19.97 | 1.000 | 0.000 | 0.2870 |
|
||||
|
||||
- **速度限幅是有代价的硬约束**:每轴增量限回 VMAX 后到达 −5.1 pp、time_ok −12.5 pp、2 s 误差比 0.210→0.279。也就是说 base 的 57.4% 里**有一部分是用超速换来的**:反馈在限幅之外叠加,实际替模型补了速度。不能把 base 当作"满足速度约束的成绩"。
|
||||
- **机理**:base 速度比 p50=1.660 恰等于 `1+0.3×fb_gain 2.2`;限幅后刹停几乎不变(19.25→19.55),但 time_ok 掉得比到达更多,说明损失集中在**逼近目标末段被剥夺了超速修正**,不是更容易刹停。所以出路不是调低增益(第29/30轮已测:增益小到达就掉),而是把该由前馈承担的速度还给模型——在限幅后的分布上重采数据(DAgger,需批准),或用不违反速度约束的前馈(#14 的余量投影 + 按时限打分)。
|
||||
- **0.5 mm 余量门很便宜**:只少 3 题、刹停 +0.8/题,2 s 误差比反而更好(0.182)。但这不等于裕度够用——它只说明"要求 0.5 mm 不明显拖慢"。5 mm(查看器护栏)是否同样便宜,取决于策略能否找到 ≥5 mm 的路径,必须抬 cap 重算真实间隙分布才能回答(第30轮 `plans['gaps']` 在 0.5 mm 截顶,算不出这一条)。
|
||||
- 四组 `collision_rate` 都是 0,但这仍是端点/中点离散口径;`gap_margin` 不改 `col` 判据(仍 `gap≤0`),余量与碰撞计数是两条独立的线,不互相顶替。
|
||||
- **回归**:`python -m unittest tests.test_eval_cl` **30 项通过**(新增 4 项):未限幅时实测速度比确实 =3.4/1.2>1(灵敏度)、限幅后恰为 1.0、0.3 mm 间隙在余量 0 时通过而余量 0.5 时刹停、5 mm 间隙在余量 0.5 时仍通过(证明 cap 抬升生效,否则该测试会恒假)。
|
||||
|
||||
- **GPU 上的一个控制周期(按 GPU 口径实测)**:诊断档与逐项分解见 `docs/experiment_round33_gpu_cycle.json`,真实配方(steps=1)见 `docs/experiment_round33_gpu_cycle_steps1.json`,RTX 3090、B=1、float32 推理 + float64 认证,每项 20 次预热 + 120 次计时并 `cuda.synchronize`。
|
||||
|
||||
- **前提更正(重要)**:`eval_cl` 从不传 `steps`,`rollout` 默认 `steps=1`,而 `ml/model/policy.py:156` 写明"steps=1 即一步生成,steps>1 是诊断用"。所以第27轮以来的所有评估成绩一直是**单步推理**;本节先用 steps=16 测出的"当前配方 116 ms ≈ 8.6 Hz"是口径错误,已作废并在下面用真实配方重测。
|
||||
- **真实配方(steps=1, K=8)的分解**:
|
||||
|
||||
| 环节 | P50 | P95 | P99 |
|
||||
|---|---|---|---|
|
||||
| 推理 `sample_k` steps=1,K=1 / 4 / 8 | 7.11 / 7.08 / 7.19 | 9.45 / 10.17 / 9.95 | 11.91 / 14.00 / 11.34 |
|
||||
| 特征 83 维(总 / 其中宽相位间隙 / 其中 FK) | 13.11 / 8.18 / 4.00 | 19.57 / 16.57 / 7.51 | 21.17 / 19.43 / 8.47 |
|
||||
| 目标反馈 DLS 1 步 | 6.53 | 9.32 | 12.62 |
|
||||
| 现有离散安全层 4 点(cap 1.0) | 7.41 | 11.34 | 13.80 |
|
||||
| `certify_uniform` 2 根 n=8 / n=16 | 9.70 / 16.07 | 14.40 / 20.95 | 15.75 / 24.42 |
|
||||
|
||||
| 组合周期(同一位姿,顺序测;跨行比较受漂移影响,只作量级) | P50 | P95 | P99 |
|
||||
|---|---|---|---|
|
||||
| K=8 + 离散安全层(真实配方) | **36.68 → 27.3 Hz** | 43.27 | 49.17 |
|
||||
| K=4 / K=1 + 离散安全层 | 37.71 / 40.36 | 55.17 / 48.46 | 76.53 / 52.06 |
|
||||
| K=8 + 连续认证 n=8 | 45.34 → 22.1 Hz | 49.68 | 52.20 |
|
||||
| K=8 + 连续认证 n=16 | 49.55 → 20.2 Hz | 56.85 | 59.06 |
|
||||
| (对照)诊断档 steps=16 的同一周期 | 116.5 | 134.5 | 142.6 |
|
||||
|
||||
- **归因更正(先证仪表再下结论)**:空算子 P50 0.028 ms、空 `synchronize` 0.005 ms、一次 `encode` 2.27 ms,所以**不是启动/同步地板**。先前口算的"5–10 ms 是 GPU 启动开销"错了,在此更正:特征 13.1 ms、认证 7.4 ms、DLS 6.5 ms 是 B=1 下真实的小算子工作量——即 **GPU 在单臂批量=1 时利用率极低**,而不是被框架拖慢。
|
||||
- **推理不是瓶颈,几何才是**:单步推理 7.2 ms,且候选数几乎免费(K 1→8 不涨);一个周期里几何类工作合计约 27 ms(特征宽相位 8.2 + 特征 FK 4.0 + 认证 7.4 + DLS 6.5),并且这几次调用各自重复做 `cum_xf` 与宽相位筛选。所以 50 Hz 的路径是**合并几何查询**(`features()` 已支持 `gm=`/`xf=` 复用,`gm_groups` 文档保证与整表 `gaps` 数值一致),而不是继续压推理。
|
||||
- **连续认证的增量成本可接受**:在同一位姿上 n=8 只比现有离散层贵约 8.7 ms(整周期 36.7→45.3 ms),n=16 贵约 12.9 ms。也就是说"段内连续证明"在 GPU 上不是奢侈项,真正卡住 20 ms 的是那 ~27 ms 的重复几何。合并后再看是否进得了 20 ms——这一步正在做交错复测(跨行顺序测的数字受漂移影响,不作最终结论)。
|
||||
- **50 Hz 结论(当前状态)**:真实配方一个规划周期 **36.7 ms(P99 49.2)≈ 27 Hz**,仍不满足"P99<20 ms";且必须记住这是在**不限幅**(可超速 1.66×)的策略分布下测的——限幅后的到达率代价见上表,两者不能混着谈。
|
||||
- f32 与 f64 的间隙在 3 个点上完全相同(最大偏差 0.0 mm、无符号翻转),但 **3 个点不足以支持"认证可以改用 float32"**,需按真实轨迹批量对比才能下结论。
|
||||
|
||||
## 轮次 34:GPU 周期的可实现优化——复用 FK 与压认证最坏情况(exp: `exp/dagger-deploy`)
|
||||
|
||||
- **起因与一次自我撤回**:轮次 33 记下"合并几何查询看能不能进 20 ms"后,先按 `docs/experiment_round34_fused_cycle.json` 测出"整周期 41.9→24.5 ms"。这个数**作废**,两个独立原因:
|
||||
1. 第一版的"合并"把 `xf=` 传给了包含 5 行的 `gaps` 调用,而 `gaps` 的 `xf` 是按行对应的 → 只有第 0 行走完宽相位,认证点 4 行全部退回截顶值 40.0 mm,即**少干了活**(表现为认证矩阵差 31.17 mm)。修好后同一 cap 下矩阵逐位一致(差 0.0 mm)。
|
||||
2. 更根本:真实管线顺序是 特征 → 推理 → 候选 → 认证,**认证点依赖网络输出**,所以"特征用的 q 间隙与认证点间隙合成同一次调用"在实现上做不到。该文件保留作记录,但不能当作可实现收益。
|
||||
- **只测可实现的口径**(`docs/experiment_round34_geom_opt.json`;B=1、K=8、commit 2;7 个变体逐轮交替、奇数轮反序,80 轮;等价性两项都 `True`:批量认证矩阵与逐条循环逐位一致、features 复用 xf 前后逐位一致):
|
||||
|
||||
| 变体 | 含义 | P50 | P95 | P99 |
|
||||
|---|---|---|---|---|
|
||||
| A_today | 现口径 | 33.49 | 46.14 | 64.69 |
|
||||
| B_xf_reuse | `gm_groups` 复用 features 已算的 `cum_xf` | **30.12** | 42.35 | 66.37 |
|
||||
| C_batch_cert | K 条候选的认证点并成 1 次 gaps | 38.05 | 50.17 | 59.86 |
|
||||
| D_both | B + C | 34.87 | 39.81 | 48.26 |
|
||||
| E_both_cont_n8 | B + C + 连续认证 n=8 | 43.73 | 51.08 | 65.68 |
|
||||
| F_worst_loop8 | **最坏情况**:所有候选被拒(今天逐条串行 K 次调用) | 80.22 | 88.44 | 89.55 |
|
||||
| G_worst_batch | 同一最坏情况但并成 1 次调用 | **37.14** | 46.06 | 52.22 |
|
||||
|
||||
- **复用 FK 是无条件收益**:省一次 `cum_xf`(约 3.4–3.7 ms),特征输出逐位一致,已直接实现进 `ml/common/kin_torch.py` 的 `gm_groups(..., xf=)` 与 `ml/common/features.py`(默认行为不变,新参数仅在调用方已算过 xf 时传)。
|
||||
- **批量认证是"典型换尾部"**:常见路径(第一条候选就通过)贵 4.6 ms;但今天一旦前面的候选被拒就要串行调用,实测最坏 **80.2 ms(12.5 Hz)→ 37.1 ms**,P99 64.7→48.3 ms(D vs A)。硬实时看的是最坏情况,所以这条值得做,但要按"典型略变慢、尾部大幅变稳"来汇报,不能只报 P50。
|
||||
- **仍未到 20 ms**:即便 D 档 P50 34.9 ms、P99 48.3 ms。连续认证挂在 D 上是 43.7 ms(E)。所以 GPU 侧下一步只能是更结构性的:认证降 float32(需先批量量化 f32/f64 判据差异,不能凭 3 个点下结论)、把宽相位做成可增量复用的核、或 `torch.compile`/CUDA Graph 把碎 kernel 合批。
|
||||
- **等价性闸门(纯效率改动不许改变任何结果)**:
|
||||
- 复用 FK:同一 256 题、同 seed、同配方重跑(`docs/experiment_round34_xf_reuse_check.json`),**22 个指标逐项与轮次 33 base 完全相同**(含 success 0.57421875、time_ok 0.390625、brakes 19.25、speed_ratio_max 1.660003662 等)。整批墙钟 296.3 → 318.0 s,是两次独立运行的差,**不能当作开销结论**:省下的那次 `cum_xf` 只在 B=1 交错实测里量到 −3.4 ms,批量评估里一次调用已覆盖 256 行,摊薄后不足以抵消运行间波动。
|
||||
- 批量认证(`--cert_batch 1`,默认 0 不变):同一 256 题重跑 **22 项指标与 base 逐项相同**(`docs/experiment_round34_cert_batch_check.json`,success 0.57421875、time_ok 0.390625、speed_ratio_max 1.660003662 等全部一致),整批墙钟 296.3 → 306.9 s(批量口径把 8 次串行调用换成 1 次 K×256 点的大调用,均值略涨符合预期,与 B=1 交错实测方向一致)。合成夹具里也断言两条路径选到同一条候选、同一前缀长度、逐位相同的轨迹与证书。
|
||||
- **测试 32→34 项**:`CertBatchEquivalenceTest` 两条(候选 0 前缀不完整 → 选候选 1;所有候选第 2 步都不安全 → 两条路径都停在第 1 步)。写夹具时踩到一个自己的坑:桩里掩码写成 `q[:, :1]` 再 `[:, None]` 变成 3 维,广播出 (4,4,49) 才报"size 16",是**测试桩错**不是库错——先证形状再改代码。
|
||||
|
||||
## 轮次 35:半精度、流水线复用与 float32 认证的实测(exp: `exp/dagger-deploy`,三条路两条被自己否掉)
|
||||
|
||||
- **按用户指定的方向先量显卡精度档**(FP32 / FP16 / BF16,`docs/experiment_round34_precision_ab.json`;B=1、K=8、steps=1,25 次预热 + 120 次计时):
|
||||
|
||||
| 精度 | 推理 P50 | 推理 P99 | 整周期 P50 | 整周期 P99 | 动作最大偏差 |
|
||||
|---|---|---|---|---|---|
|
||||
| FP32 | 7.68 ms | 10.63 | 36.51 | 46.57 | — |
|
||||
| FP16 | 8.31 ms(慢 8%) | 12.03 | 36.16 | **55.60(更差)** | 2.1e−3 归一化 = **0.0050°** |
|
||||
| BF16 | 8.58 ms(慢 12%) | 14.17 | — | — | 1.7e−2 = **0.0377°** |
|
||||
|
||||
结论:**这个网络在这个批量下不是算力受限**,半精度换不来时间,还要付动作偏差与更差的尾部 ⇒ 推理保持 FP32。(未测更大 batch,那里的结论不下。)
|
||||
- **float32 能不能做认证**(`docs/experiment_round34_f32_cert.json`,48 段 × 9 网格点 × 15 组对,宽相位 cap 2 mm):f32 与 f64 间隙差 p50 **0**、p99 3.66e−5、p999 7.44e−5、**max 1.32e−4 mm**;有符号误差 max +1.32e−4(确有 1 例高估)、min −1.09e−4。同一判据下 f64/f32 都接受 24/48 段,"仅 f32 判安全"= 0、符号翻转 = 0。⇒ f32 不是不能用,但**现用 eps=1e−4 已被实测越界**,要用就得把 eps 抬到 ≥2e−4 mm;且这只量了间隙值误差,没覆盖 f32 下 FK/界自身的误差,因此暂不放宽 dtype 守卫。
|
||||
- **流水线复用特征间隙——否掉**(`docs/experiment_round35_pipeline_gaps.json`):想法是"下一次规划的 q 正好是本次认证已算过的末点"。第一版给出 42.1→31.8 ms,但**等价性检查不通过**(缓存行与新算差 3.19 mm)——是我取行索引用错(`full[CN-1]` 取到第 2 条候选的第 1 步,而非第 1 条候选走完 commit 步)。修好后收益只剩 **34.8→31.7 ms(−3.1 ms)**:复用要求认证调用改用 cap=40 的宽相位(特征所需),省下的被吃回大半;且缓存行与新算**不是逐位相同**(差 6.3e−5 mm,源于不同 batch 下矩阵乘的结合序),会扰动特征输入。⇒ 不为这 3 ms 改口径,放弃。
|
||||
- **剩下的量级判断**:一个周期 ≈ 推理 7.2 + 特征 13.1(宽相位 8.2 + FK 4.0)+ 末态打分 4.3 + DLS 6.5 + 认证 7.4 ≈ 33 ms,其中 **4–5 次调用各自重做同一个宽相位筛选**(67,256 个簇对)。下一步是把一次筛选的半径按位移界(`cert_lever`)扩到整个 commit 地平线,供同周期所有点共用——这才是有量级的候选,而不是再抠精度。
|
||||
|
||||
- **再一次自我撤回(方法论)**:为了找"合并几何调用"的收益,我写了三个变体的探测:①两次独立调用 17.62 ms、②共享一次 `cum_xf` 13.32 ms、③合成一次 cap=40 调用 9.51 ms(交错 90 轮,特征行逐位一致)。**②③ 都不成立**:探测里认证点直接取自轨迹、与特征无关,而真实循环的顺序是 特征(q) → 推理 → 候选 → 认证点(q+Δ),认证点在特征算完之前不存在 ⇒ 同一次规划内既不能共享 `cum_xf`,也不能把两次 `gaps` 合并。只有 ①→轮次 34 已落地的 `gm_groups` 复用 `xf`(同一姿态内部,−3.4 ms)是真的。教训:**效率探测必须按真实数据依赖排布,否则测的是不存在的调用图**。
|
||||
- **循环内还剩的可复用点**:`replan='empty'` 且队列为空时 `qe == q`,此时目标反馈 DLS 与末态打分的 FK 和特征用的是同一姿态,可以共享一份 `cum_xf`(部署 `r=3` 时不成立,要按 r 分支)。这条待测(#19)。
|
||||
- **守卫补一个**:`gaps()` / `gm_groups()` 现在拒绝行数与 `q` 不符的 `xf`(之前会静默按别的行做宽相位,实测触发 CUDA device-side assert 或给出错间隙),测试 34→35 项;`python -m ml.eval.check_parity` 仍 PASS(2000 样本,间隙差 2.52e−13 mm、碰撞布尔差 0、特征差 5.96e−08)。
|
||||
|
||||
## 分支清理(2026-10-20)
|
||||
|
||||
- `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。
|
||||
- `feat/abs-target`(217a70b):绝对目标参数化负结论分支(轮次 3);打 tag `archive/abs-target` 保留可复现实体后,删除远端分支(本地此前已删)。
|
||||
- `backup/pre-deep-squash`、`backup/pre-squash`:历史备份分支,非实验方案,保留未动。
|
||||
- 保留:`main`(70654e4)、`exp/dagger-deploy`(39c870c,历史最佳)。
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
# 本机执行清单(给本地 AI 逐条执行)
|
||||
|
||||
本文件是 Hark 对本机计算的唯一要求清单。按顺序执行,每一步把「回传」里的文件或输出原样交给用户,不要跳步,不要自行改代码。
|
||||
所有命令在仓库根目录(G:\Robot_v2\机器人手臂)的 Git Bash 里运行。完成的条目由 Hark 在下一次提交里删掉。
|
||||
|
||||
## 已知结论(截至 2026-10-19 05:35 回传)
|
||||
|
||||
- 现有模型 `ml/runs/base/last.pt` 闭环 0% 到达:1 段采样碰撞 99.6%;4 段采样 94%;4 段 + 仿真安全层碰撞 0.78%,到达仍 0%。consist / value / first 三种选法结果几乎一样,问题出在候选本身,不在挑哪条。
|
||||
- 三份代码审查逐行核对了滚动索引、前缀、提交、标签与执行的对齐,没有找到错位;但之前没有任何测试真正走过 `rollout()`。下面第 1 步的 H 段把「仿真错了」「前缀照抄」「子目标切换规则」「多峰」几种解释逐个分开。
|
||||
- 之前「前 1 s 速度比 2.31×」按 6 轴角度直接相加,被 J5/J6 抖动和 pos 任务冻结的 J6 放大。H 段改为分轴、净位移、TCP 三种口径。
|
||||
|
||||
## 第 0 步:同步代码
|
||||
|
||||
```bash
|
||||
git status --short # 有未提交的改动先 commit(git add -A && git commit -m "本机改动"),不要丢
|
||||
git pull --rebase gitea main
|
||||
git log --oneline -1 # 应是 Hark 的「diag: H 段(老师 oracle / 前缀消融 / 子目标规则 / 分轴指标)…」
|
||||
```
|
||||
|
||||
- 训练控制台若开着,先点「停止」,确认没有 python 训练进程在跑。
|
||||
- 不要启动训练、DAgger、M5。
|
||||
|
||||
## 第 1 步:闭环失败定位 H 段(约 35–50 分钟)
|
||||
|
||||
```bash
|
||||
PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json
|
||||
```
|
||||
|
||||
- 专用显存超过 22 GB 就 Ctrl+C,回传终端最后 30 行和已写出的 `ml/runs/diag_cl.json`(每段跑完就写一次,中断也保留已完成的段)。
|
||||
- 现有 checkpoint 没有 EMA、没有 `act_scale`:脚本按原始权重、动作尺度 1.0 载入(输出里 `weights`=`model`、`act_scale_ckpt`=1.0),与之前各轮同口径。
|
||||
- 回传:`ml/runs/diag_cl.json`。
|
||||
|
||||
### 输出键
|
||||
|
||||
| 键 | 内容 |
|
||||
|---|---|
|
||||
| `A_*`、`B_act_diff_deg` | 起点特征 Python 与数据集逐组差;两份特征下的动作差(与之前相同) |
|
||||
| `C` | 旧部署口径(1 段采样、子目标旧规则 timeout)的失败形态。到达与碰撞改为只计每题自己的时限内,数字可能比上一轮略低 |
|
||||
| `H1_oracle` | 老师当策略走完整 `rollout()`。`oracle` 字段:`max_dev_deg_before_dls`(DLS 接管前与老师轨迹的最大关节偏差,p50 / p99 / max)、`reach`、`collision`、`finish_minus_teacher_s`(完成时刻 − 老师时长)、`dls_used` |
|
||||
| `H1_oracle_noise_0.05` / `_0.1` / `_0.2` | 老师动作每次规划加 N(0, σ²)(σ 以 STEP 为单位) |
|
||||
| `H2_open_loop` | r = 3 的 val 行:`s{1,4}_prefix_orig`(前缀原样,执行窗口 3..5 的误差,另含 `prefix_copy_err_deg_mean`)vs `s{1,4}_prefix_zeroed`(前缀置零) |
|
||||
| `H2_replan_empty_s1` / `_s4` | 队列空了才规划(每次 r = 0),其余同新部署默认 |
|
||||
| `H3_headsub_cross_s{1,4}` | 子目标头 + 越过顶点规则(新部署默认) |
|
||||
| `H3_headsub_timeout_s{1,4}` | 子目标头 + 旧规则(< 5° 或 1.5 s 超时,旧部署) |
|
||||
| `H3_teachersub_cross_s{1,4}` / `H3_teachersub_timeout_s{1,4}` | 老师真实途经点全表,两种切换规则 |
|
||||
| `H3_nosub_s{1,4}` | 不用子目标 |
|
||||
| `H4_open_loop` | 按前缀长度 r = 0..3 分组的执行窗口误差 \|Σ(act − Y)·STEP\|(度),1 段与 4 段采样 |
|
||||
|
||||
每个闭环变体(H1–H3)都带同一组指标:`reach`、`collision`(cat ≠ D、时限内)、`goal_ratio_2s_p50`、`end_goal_err_mm_p50`、`first_hit_s_p50`、`t_done_over_teacher_p50`、`brakes_p50`、`joint_amp_ratio`(6 轴前 1 s 关节行程 策略/老师)、`joint_net_ratio`(6 轴前 1 s 净位移 策略/老师)、`j6_pos_amp_deg_p50`、`tcp_path_ratio_p50`、`tcp_net_ratio_p50`、`sign_flip_rate` 与 `sign_flip_rate_teacher`(6 轴前 2 s 相邻 3 步块变号比例)、`by_cat_mode`(各类别 n / reach / collision / goal_ratio_2s_p50)、`D`(D 类单列)。`_s1` = 1 段采样(部署),`_s4` = 4 段采样。
|
||||
|
||||
### 判读
|
||||
|
||||
| 现象 | 结论 |
|
||||
|---|---|
|
||||
| `H1_oracle` 的 reach < 0.95、collision > 0.01、或 `max_dev_deg_before_dls.p99` > 0.1° | 仿真 / 滚动本身有错,之前所有闭环 0% 都不可信。先修仿真,不看下面各项 |
|
||||
| `H1_oracle` 全部正常 | 仿真没问题,失败在策略输出。再对照 `H1_oracle_noise_*`:σ = 0.05 时 reach 就大幅下降,说明闭环对残差噪声极其敏感,单靠降噪不够 |
|
||||
| `H2_open_loop` 的 `prefix_zeroed` 执行窗口误差明显小于 `prefix_orig`,或 `H2_replan_empty` 明显好于同段数的 `H3_headsub_cross`(reach > 0,或 `goal_ratio_2s_p50` 明显更低、`sign_flip_rate` 明显更低) | 前缀照抄(copycat)是主因 |
|
||||
| `H3_teachersub_cross` 明显好于 `H3_teachersub_timeout`(或 `H3_headsub_cross` 好于 `H3_headsub_timeout`) | 子目标切换规则是主因之一 |
|
||||
| `H3_teachersub_*` 明显好于 `H3_headsub_*` | 子目标头给的途经点不可用 |
|
||||
| `H3_nosub` 不比 `H3_headsub_cross` 差 | 子目标通道可以整体去掉 |
|
||||
| `by_cat_mode` 里 `A/pos`(老师就是一条直线)reach 也是 0 | 失败与多峰无关 |
|
||||
| `joint_amp_ratio` 远大于 `joint_net_ratio`,且 `sign_flip_rate` 远高于 `sign_flip_rate_teacher` | 「动得多、走得少」是来回抖,不是速度律学快了 |
|
||||
|
||||
重新打包和重训(`FRESH_FROM=prep`)要等 Hark 看完这一步的结果再定,这一步之后不要自行启动。
|
||||
|
||||
## 回传汇总
|
||||
|
||||
| 步骤 | 回传内容 |
|
||||
|---|---|
|
||||
| 1 | ml/runs/diag_cl.json |
|
||||
| 任一步出错 | 终端最后 30 行输出 |
|
||||
|
||||
---
|
||||
|
||||
## 执行回传(2026-10-19 19:55,本地 AI)
|
||||
|
||||
### 第 0 步:同步代码
|
||||
|
||||
`ml/eval/diag_collision.py` 归档提交(本机改动 `7e063eb`);根目录用户个人 html 为个人文件未入仓,原位保留。log 顶部 = `406f180`「diag: H 段 …」✓。无训练进程。
|
||||
|
||||
### 第 1 步:H 段(完整 JSON 已更新到 ml/runs/diag_cl.json;weights=model、act_scale_ckpt=1.0,同口径;H_tasks 256、missing_teacher 0)
|
||||
|
||||
**判读表逐行核对:**
|
||||
|
||||
1. **`H1_oracle`:仿真完全正常。** reach **1.0**(243/243)、collision **0.0**、`max_dev_deg_before_dls` p50 6.9e-5° / p99 4.6e-4°(阈值 0.1°)、`finish−teacher` p50 −0.02 s、`dls_used` 94.9%。→ **滚动/仿真没有错,之前所有闭环 0% 都可信,失败在策略输出本身。**
|
||||
2. **`H1_oracle_noise`:对残差噪声极其敏感。** σ=0.05 时 reach 1.0→**0.218**(碰撞 9.1%);σ=0.1→0.058;σ=0.2→0.012。→ 单靠降采样噪声不够(判读表第二行命中)。
|
||||
3. **`H2_open_loop`:前缀照抄排除。** prefix_orig vs zeroed:s1 2.315° vs **2.375**(置零不更好);s4 0.251° vs **0.705**(置零明显更差)。`H4` 按 r=0..3 分组误差持平(2.31–2.33°)。`H2_replan_empty`:s1 0%/79.4%/0.836、s4 0%/84.8%/0.603——不比同段数 `H3_headsub_cross` 好。
|
||||
4. **`H3`:子目标三个变体逐位相同(新异常)。** `headsub_cross` / `headsub_timeout` / `nosub` 在 s1、s4 下从 reach 到 end_err 到 flip 数组**完全一致**(s1: 0% / 96.3% / 0.7504 / 414.2091;s4: 0% / 79.8% / 0.5869 / 428.0032)。`teachersub_*`(真值途经点)仅微动(s1 95.9%/0.726、s4 79.4%/0.578)。→ **子目标头通道在这条闭环里零响应**——比「可以整体去掉」更强;可能是 need 概率全程 <0.5 从不触发,或子目标输入对旧模型输出无影响,建议核一下(D0 曾测过 need 标签占比 28.1%)。
|
||||
5. **`by_cat_mode`:A/pos(老师直线)reach 也是 0**(s1 碰撞 94%)→ **失败与多峰无关**。
|
||||
6. **分轴指标:s1 的"动得多"含抖、s4 后基本消抖。** J5/J6 幅度/净位移:s1 3.08/2.57、2.55/2.16;s4 1.83/1.84、0.69/0.69,`sign_flip_rate` 从 0.026–0.062 降到 0.0006–0.0013(≈老师 0.0005–0.004 水平)。→ 4 段采样把来回抖消掉了,但净位移仍 J5 1.8×,且到达仍 0%。
|
||||
|
||||
**C 段**(新时限口径):0% 到达 / 96.5% 碰撞 / 2 s 误差比 0.755。
|
||||
|
||||
### 本轮无出错,无终端报错段。
|
||||
|
||||
---
|
||||
|
||||
## 本地自主排查(2026-10-19 20:40,用户授权「继续排查」)
|
||||
|
||||
### 1. 闭环噪声容忍阈值(老师动作 + N(0,σ²),σ 以 STEP 为单位,n=256,其余同 H1_oracle)
|
||||
|
||||
| σ | 0 | 0.005 | 0.01 | 0.02 | 0.03 | 0.04 | 0.05 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| reach | 1.0 | 1.0 | 0.971 | 0.716 | 0.457 | 0.309 | 0.218 |
|
||||
| collision | 0 | 0.8% | 2.5% | 4.9% | 6.2% | 7.4% | 9.1% |
|
||||
| ratio2s | 0.047 | 0.049 | 0.049 | 0.054 | 0.059 | 0.063 | — |
|
||||
|
||||
→ **闭环能容忍的每步噪声上限 ≈ 0.01–0.02×STEP**(约 0.02–0.04°/步)。策略 s1 残差 ~0.77°/步、s4 ~0.05–0.08°/步,都在崩溃区右侧。
|
||||
|
||||
### 2. 8 段采样闭环(steps=8,同 H3_headsub_cross 口径)
|
||||
|
||||
reach **0%** / collision **79.0%** / ratio2s 0.574 / end 408 mm;`sign_flip_rate` 0.0004–0.0015(≈老师 0.0005–0.004,抖已完全消掉)、`amp ≈ net`(1.00/1.19/0.97/1.19/1.36/0.76,无抖动成分)。
|
||||
|
||||
→ 对比 s4(reach 0 / 79.8% / 0.587):**s4→s8 几乎无改善**。按上表的白噪声口径,s8 的残差幅度(16 步 0.56° ≈ 0.035°/步,σ_eff≈0.0175)应给出 reach≈0.75——实测 0。**策略误差不是白噪声,是有方向的系统偏差**:幅度不大但同向、闭环里逐步累积,重规划抵消不掉。降采样噪声(1→4→8 段)已到天花板。
|
||||
|
||||
### 3. 子目标通道:need 头恒不触发(新发现,修正「子目标排除」的理由)
|
||||
|
||||
- 冷启动(子目标特征抹零,与闭环同口径)need 概率:mean 0.0048、p50 0.0030、p90 0.0079、**max 0.0819**、>0.5 占比 **0**;43 道 need=True 题召回 **0/43**。(D0 的 0.71875 准确率恰等于"全预测 False"基线,当时已有此暗示。)
|
||||
- 因此闭环里 `sub_flag` 恒 0、子目标从不激活——这才是 H3 三变体逐位相同的直接原因。
|
||||
- 但模型对子目标特征**高度敏感**(同一噪声下真值子目标 vs 置零:|Δact| mean 0.177、max 3.09)——训练时 80% 样本 sf=1,而闭环 100% 是 sf=0,属训练/闭环分布错配。
|
||||
- 不过 H3_teachersub(强制真值 need + 真值途经点)仍 0% / 95.9%——**子目标不是闭环全灭的主因**,但 need 头失效 + 闭环恒走少数分支值得修。
|
||||
|
||||
### 4. 汇总判断
|
||||
|
||||
- 仿真正常(H1_oracle 已证);采样噪声已压到老师抖动水平仍 0% 到达 → **剩余障碍是策略的系统性动作偏差 + 闭环对误差的极低容忍度(误差复利)**。
|
||||
- 继续调采样段数、子目标规则、候选选择都无解;正确方向是**在闭环状态上训练(DAgger)**,让老师纠正分布偏移,或提高闭环鲁棒性。
|
||||
- 未自行启动 DAgger / 重训,等指示。
|
||||
|
||||
### 5. 补充:系统偏差的方向结构(4096 行开环 + 256 题闭环,4 段采样)
|
||||
|
||||
**开环动作误差(deg,有符号)**:
|
||||
|
||||
- 每轴均值 [0.010, −0.012, −0.001, −0.029, 0.034, 0.018],|mean|/std ≤ 0.17 → **无固定轴向偏置**(不是某轴恒偏)。
|
||||
- 每轴标准差 0.16–0.21°;每步平均 |误差| 0.016–0.018°,**16 步内不随步数增长**。
|
||||
- **lag-1 自相关 0.931–0.967(极高)** → 误差是**慢变/连贯**的,不是白噪声。这解释了为什么白噪声 σ 扫描会低估策略误差的危害:闭环重规划抵消不掉持续同向的偏差。
|
||||
- 误差与老师动作方向:cos mean 0.046 / p50 0.072(不超调也不欠调);投影比 e·Y/|Y|² mean −0.038 / p50 0.006(无系统性缩放)。
|
||||
|
||||
**闭环误差增长(s4,n=256,done=0)**,goal_err / 起点误差:
|
||||
|
||||
| t (s) | 0.25 | 0.50 | 0.75 | 1.00 | 1.50 | 2.00 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| p50 | 0.959 | 0.880 | 0.811 | 0.741 | 0.645 | 0.591 |
|
||||
| p90 | 1.024 | 1.072 | 1.119 | 1.162 | 1.222 | **1.318** |
|
||||
|
||||
→ p50 缓慢靠近(2 s 缩 41%),**p90 持续发散**——≥10% 的题误差随时间增长(误差复利进碰撞轨道),其余也远不够快。
|
||||
|
||||
**补充结论**:策略每步误差很小(~0.017°)但**时间相关性极强(0.93–0.97)**,闭环对"慢变误差"的敏感度远超等幅白噪声;失败是「慢变误差 × 闭环误差复利」。推理侧降噪(段数/选择器)已无空间,训练侧需要降低慢变误差(DAgger / 闭环状态训练)。
|
||||
|
||||
### 6. 「2 s 误差比 >1 的发散题」能否提前筛掉(per-task 分组,s4,n=256;明细 ml/runs/task_divergence.json)
|
||||
|
||||
| 组(2 s 误差比) | 题数 | 碰撞 | 到达 | cat/mode | 起点误差 p50 | 老师时长 p50 | need 率 | 老师到达 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 发散 r2>1.0 | 48 | 81% | 0% | A/pos 14、A/pose 14、C/pose 5、C/pos 5、B/pos 3、B/pose 3、D/pos 2、Dr/pos 2 | 374 mm | 1.80 s | 21% | 96% |
|
||||
| 平 0.7–1.0 | 48 | 83% | 0% | 混合 | 539 mm | 2.12 s | 27% | 90% |
|
||||
| 收敛 ≤0.7 | 160 | 79% | 0% | 混合 | 642 mm | 2.18 s | 31% | 96% |
|
||||
|
||||
- **三组碰撞率相同(79–83%)、到达全 0**——不存在"一小撮可提前去掉的坏题";发散题散布在所有类别,老师都能到达。
|
||||
- r2 与起点误差秩相关 **−0.384**(起点误差小 → 比值易大),是**分母效应**:短任务(老师 1.8 s、e0 374 mm)比值波动大,不代表题目特殊;r2 与老师时长/行程几乎无关(−0.09 / −0.03)。
|
||||
- 结论:误差放大是**全体题目上的普遍行为**,筛题无效(只让指标好看),必须训练侧修正。
|
||||
|
||||
### 7. DAgger 小样本试跑(用户授权;未跑全量、未微调)
|
||||
|
||||
**链路实测**(ckpt `ml/runs/base/last.pt`,旧口径 act_scale=1.0):
|
||||
|
||||
| 步骤 | 规模 | 实测 |
|
||||
|---|---|---|
|
||||
| `ml.dagger.collect`(16 题,parts=2) | 16 题 | **12.7 s**(含加载),29 个起点(全撞,拒 1 个 gap) |
|
||||
| `ml.dagger.collect --n 64`(显存采样) | 64 题 | 成功,112 个起点(拒 10 gap),**显存峰值 7.98 GB**(24 GB 卡,无 OOM) |
|
||||
| `node ml/gen/gen_dataset.cjs --tasks tasks_01.jsonl` | 15 个起点 | **7.6 s**,shard 含 qPrev / v0 / traj / cert |
|
||||
| `ACT_SCALE=1.0 node ml/prep/build_dataset.cjs …` | 13 题 | **2.3 s**,453 样本(175 增广),meta `act_scale:1, data_ver:2` |
|
||||
|
||||
**关键口径提醒**:`build_dataset.cjs` 现在默认 **ACT_SCALE=1.25**(DATA_VER 2),而现有主数据 `ml/data/samples` 无 meta → 训练按 **1.0** 读。两者不一致时训练直接报错(README 第 120 行)。全量 DAgger 前二选一:
|
||||
1. 打包 DAgger 时显式 `ACT_SCALE=1.0`(与旧主数据、旧 ckpt 兼容);
|
||||
2. 先按新流程 `FRESH_FROM=prep` 重打主数据为 1.25、重训后再跑 DAgger。
|
||||
本次试跑全部用 1.0;产物在 `ml/data/dag_try*/`(gitignored)。
|
||||
|
||||
**全量耗时外推(估算,以首次实测为准)**:collect 8000 题约 0.8–1 小时(单进程);老师重规划 1.6 万起点 / NPROC≈12 约 10 分钟;打包约 3 分钟;微调 15000 步未试。
|
||||
|
||||
**结论**:DAgger 收集链路可用、显存安全(峰值 8 GB),可以全量启动——但先定 act_scale 口径。
|
||||
|
||||
### 8. 「加大数据能否力挽狂澜」——闭环状态到训练分布的距离(s4,n=256,9408 个闭环状态)
|
||||
|
||||
方法:闭环轨迹每 10 步取一个 q(9408 点),对训练集 191 万老师状态求最近邻(6 维关节角 L∞,±360 回绕);对照 = 每题起点(老师分布内)。
|
||||
|
||||
| 状态 | p10 | p50 | p90 | p99 |
|
||||
|---|---|---|---|---|
|
||||
| 闭环轨迹状态 | 12.10° | **21.46°** | 32.50° | 44.56° |
|
||||
| 每题起点(老师) | 8.71° | 13.21° | 17.91° | 21.53° |
|
||||
|
||||
- 闭环状态离最近老师状态中位 **21.5°**(起点 13.2° 的 1.6×;p90 32.5°、p99 44.6°)。
|
||||
- 训练集本身在关节空间稀疏:连老师起点最近邻都有 13.2°——数据是"轨迹线"而非密集体。
|
||||
- 按密度缩放(6 维 d∝N^(−1/6)):数据翻 4 倍最近邻仅降 ~21%(21.5°→~17°),而策略误差尺度是 0.02°;差距三个数量级,且同类数据不改变误差的时间相关结构(0.94)。
|
||||
- **结论:扩大同类老师数据救不了闭环**;需要「不同的数据」——在策略实际到达的状态上标注(DAgger),把 21.5° 这些分布外点变成训练样本。
|
||||
|
||||
### 9. 方案 A 实测:合成扰动状态样本微调(两轮配方)
|
||||
|
||||
做法:从训练任务老师轨迹的 15–85% 处取状态,加 N(0,σ²) 关节扰动(σ 混合 0.5/1/2/4°;实测起点偏移 p50 1.5°),老师重规划(**2000/2000 到达**、cert 1951/2000),打包 36k 样本(act_scale=1.0,为主数据的 1.9%),从 `base/last.pt` 微调 3000 步,2000 步时以 EMA 权重做 512 题闭环评估。
|
||||
|
||||
| 配方 | success | collision | goal_ratio_2s | ol_fk_mm | ol_best_mm | ol_div |
|
||||
|---|---|---|---|---|---|---|
|
||||
| base/last.pt(基线) | 0 | 0.996 | 0.755 | 5.98 | 2.92 | 0.655 |
|
||||
| +扰动样本 lr 3e-4 | 0 | **0.484** | 0.927 | **15.27** | 10.32 | 0.966 |
|
||||
| +扰动样本 lr 1e-4 | 0 | 0.604 | 0.893 | **16.42** | 11.45 | 0.980 |
|
||||
|
||||
- 碰撞率首次大幅下降(99.6%→48–60%),但 **success 仍 0**;开环精度退化 2.5–2.7 倍(6→15/16 mm)、多样性升高——小步微调在两种分布间折中,把主分布带偏了。
|
||||
- **结论**:1.5° 量级的合成扰动 + 3000 步微调不能解决闭环。若继续这条线,需从**预训练开始**按课程混入扰动样本(并监控开环不退化),成本高;或转向结构改动(方案 B:绝对目标参数化/参考轨迹跟踪)或真实 DAgger。
|
||||
- 产物:`ml/data/samples_perturb/`、`ml/runs/base_perturb/`、`ml/runs/base_perturb2/`(均在 gitignored 目录)。
|
||||
|
||||
### 10. 方案 B 判别实验:模型预测的"累计目标"是否指向参考轨迹
|
||||
|
||||
方法:2000 个扰动起点(原轨迹状态 + σ 0.5–4° 扰动,L∞ 实测 p50 **3.19°**),模型预测 16 步增量并累积为绝对目标 `Z_model = q_off + cumsum(â·STEP)`,对比原老师轨迹未来 16 步绝对位置 `Z_ref`;`ρ = |Z_model − Z_ref| / |q_off − q_ref|`(ρ≈1 = 目标随状态平移、无纠偏;ρ≈0 = 目标稳定指向轨迹)。
|
||||
|
||||
| 模型 | 状态偏移 p50 | 目标误差 p50 | ρ p50 |
|
||||
|---|---|---|---|
|
||||
| base/last.pt | 3.19° | 4.86° | **1.54** |
|
||||
| base_perturb(扰动微调 lr 3e-4) | 3.19° | 5.85° | 1.90 |
|
||||
| base_perturb2(lr 1e-4) | 3.19° | 6.25° | 1.97 |
|
||||
| 对照:未扰动行开环 | — | **0.46°** | — |
|
||||
|
||||
- 未扰动时 16 步累计目标误差仅 0.46°;状态偏移 3.19° 后目标偏离 4.86°(ρ=1.54)——**当前模型是"从当前状态外推增量",不是"指向参考位置"**。扰动微调让 ρ 更差(1.9–2.0),与开环退化一致。
|
||||
- **结论:方案 B 不能靠改执行层实现,必须改训练标签**——把标签从"16 步增量"改为"16 步绝对目标关节角(参考轨迹位置)",让模型学"目标在哪"而不是"从这怎么走"。执行层每步朝目标走,偏移状态下即天然纠偏。
|
||||
- 实施成本:改 `build_dataset.cjs` 标签口径(Y = 绝对目标),同步 FK/平滑/价值等基于增量的辅助损失、rollout 与 `ik-policy.js` 的增量假设,然后从头训练(约 1 小时)。属结构性改动,未擅自开始。
|
||||
|
||||
### 11. 方案 B 初版实测(分支 `feat/abs-target`,保存点 tag `pre-b-abs-target`)
|
||||
|
||||
**改动**(全部在分支,main 未动):
|
||||
- `build_dataset.cjs`:`ABS_TARGET=1` → Y = 16 步绝对目标关节角(/180 归一化),prefix 仍存增量(度),meta 记录 `abs_target`、签名加 `a1`。
|
||||
- `train.py`:读 `abs_target`;每单位动作=180°;FK 损失用绝对位置;平滑项换算到增量量纲;开环指标用第 16 步绝对位置;ckpt 记录 `abs_target`。
|
||||
- `closed_loop.py`:模型输出绝对目标 → 队列存目标序列、执行 `q=clamp(target)`、前缀由目标差分、安全层中点与 DLS 增量转绝对目标。
|
||||
- `ckpt.py` / `data.py`:abs_target 透传与一致性检查。
|
||||
- **过程 bug(已修)**:前缀噪声增广把 eps 直接加到绝对目标标签上(≈18° 污染,一半样本);改为累计偏移后第 r 步扣回。
|
||||
|
||||
**数据**:84 shard 重打 → 201 万样本、abs_target=1;标签连续(步间差 p50 0.30°、p99 1.8°、无 ±180 跳变)。
|
||||
|
||||
**训练结果**(从零 9950 步;p_neq 预热 5000 步):
|
||||
|
||||
| step | ol_fk_mm | ol_best_mm | 闭环 success | 碰撞 |
|
||||
|---|---|---|---|---|
|
||||
| 4000 | 74.6 | 33.4 | 0 | 100% |
|
||||
| 6000 | 140.4 | 70.3 | 0 | 100% |
|
||||
| 8000 | 139.0 | 68.5 | 0 | 100% |
|
||||
|
||||
- 一步采样动作误差 **31°**(4/8 段采样 2.3°/2.8°)——**绝对目标的一步流匹配映射没学会**;闭环 1 段 0%/100%、4 段 0%/100%(ratio2s 1.045)。
|
||||
- 过预热后 5000 步无下降趋势。可能原因:一步样本仅 `p_edge=10%`、`ada_p=1` 逆误差加权压制难样本、FK/平滑辅助项在绝对位置空间主导。
|
||||
- **结论:此配方不成立**。B 若继续,需改训练配方(p_edge↑、w_fk↓、ada_p=0)或换输出头(确定性回归);当前证据不支持继续投入。分支代码保留可复现。
|
||||
+34
-8
@@ -24,7 +24,8 @@
|
||||
| 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` |
|
||||
| 6 预训练 | `bash ml/pipeline.sh train` | 测速 2000 步约 6 分钟,正式 24000 步约 1.5–2.5 小时(未实测:fp32 MF 分支后的吞吐以测速为准) | `ml/runs/base/best.pt` |
|
||||
| 6′ DAgger 纠偏 | `bash ml/pipeline.sh dagger` | 未实测,估 3–4 小时:2 轮 ×(采起点 5 分钟 + 老师重规划约 30 分钟 + 打包 + 微调 15000 步) | `ml/runs/base_dagger.ckpt`、`ml/runs/base_dag2/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` |
|
||||
@@ -114,7 +115,9 @@ bash ml/pipeline.sh gen # 默认 5 万条、进程数 = CPU
|
||||
bash ml/pipeline.sh prep
|
||||
```
|
||||
|
||||
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算),30% 样本加 σ = 1° 噪声增广(加噪后间隙 < 3 mm 的不用)。
|
||||
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算)。40% 样本对 q 加 σ = 3° 噪声增广,标签改为 12 步内线性收回老师轨迹(加噪后间隙 < 3 mm 或收回途中有碰撞的不用)。
|
||||
- **动作标签**:Y = 每步增量 / (act_scale · STEP),夹在 ±1,`act_scale` = 1.25(`ACT_SCALE`)。老师巡航时主导轴每步正好 1 个 STEP,标签最大 0.8,执行端的 ±1 只防越界;被 ±1 截掉的收回量在下一步补上。运行时增量 = clamp(act, ±1) · act_scale · STEP,`act_scale` 写在 checkpoint 和 ONNX meta 里,仿真、APG、浏览器运行时都按它反归一化;没有这个字段的旧模型按 1.0 处理。
|
||||
- 每个 shard 打包产物带 `<shard>.meta.json`(数据版本 `data_ver`=2、`act_scale`、增广参数、签名 `sig`)。`ACT_SCALE`、`AUG_P`、`AUG_SIGMA`、`AUG_BACK`、`MIN_GAP_MM` 任一变化,签名就变,`run_prep.sh` 只重打签名不符的 shard;没有 meta 的旧打包一律重打。训练读 meta 取 `act_scale`,同一次训练的各目录(主数据 + DAgger)必须一致,否则报错。
|
||||
认证最小间隙 < 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)。
|
||||
@@ -127,11 +130,32 @@ bash ml/pipeline.sh prep
|
||||
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 次不升为止。
|
||||
开训前先跑老师轨迹回放自检(`ml/runs/replay_teacher.json`,约 1–3 分钟),两部分都过才开训:
|
||||
- 运动学口径:老师 50 Hz 轨迹直接放进 PyTorch 运动学 / 碰撞 / 到达判据,到达 ≥ 95%、碰撞 ≤ 1%;
|
||||
- 滚动口径:老师当策略(`TeacherOracle`)走完整的闭环仿真 `rollout()`(队列、前缀、提交 3 步、标签 clamp、DLS、时限都与策略同一条路径),时限内到达 ≥ 95%、碰撞 ≤ 1%、DLS 接管前与老师轨迹的最大关节偏差 p99 ≤ 0.1°。
|
||||
流程读 json 的 `pass` 字段,并核对 `fp`(当前 `kin.json`)与 `data_fp`(数据 shard 记录的碰撞模型指纹);任一不符就重跑一次回放,仍不过 rc 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`。
|
||||
闭环指标长期 0% 而开环正常时,跑 `PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256`(约 35–50 分钟)→ `ml/runs/diag_cl.json`:A 起点特征 Python 与 JS 逐组差、B 两份特征下动作差、C 旧部署口径的失败形态、H 老师 oracle / 前缀消融 / 子目标规则 / 分轴与分类别指标(各键含义见 `ml/eval/diag_cl.py` 文件头与 `docs/本机执行清单.md`)。
|
||||
|
||||
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练 24000 步(`configs/base.json` 的 `steps`,余弦学习率按这个预算走完)。
|
||||
|
||||
- **通过线(R10):`samples_per_s` ≥ 5000**(24000 步 × batch 1024 ≈ 1.4 小时)。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48
|
||||
- **EMA**:训练同时维护权重的指数滑动平均(`ema` 0.999),checkpoint 里存 `model`(原始权重)与 `ema`。评估、DAgger、APG 起点、M5、挖错、验收、导出一律用 EMA 权重(没有 `ema` 的旧 checkpoint 用 `model`)
|
||||
- 训练中每 2000 步用 EMA 权重打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`goal_ratio_2s`(2 s 时目标误差 / 起点误差的中位数)、`time_ok_1p3`、`start_fail`,时限 = 老师时长 × 2(至少 3 s),碰撞与到达只计时限内;开环三项(val 样本、老师状态上):`ol_fk_mm` 候选 0 的 16 步末端误差、`ol_best_mm` 8 个候选里最好的一条、`ol_div` 候选多样性。
|
||||
- **选模**:`score` = success + 0.05·(1 − collision_rate) + 0.05·(1 − min(1, goal_ratio_2s)) − 1e-4·min(ol_best_mm, 500),到达率全 0 时闭环指标仍能排序。p_neq 预热期内和预热后第一次评估之前不存 best.pt(一步映射还没训过);早停只在 max(预热 + 2 次评估, 总步数一半) 之后计数,连续 5 次不升即停
|
||||
- 损失:FK / 平滑只加在 MF 分支 r = t 且 t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上;时间嵌入最高角频率 50;MF 分支与 JVP 走 fp32(TF32 矩阵乘);前 5000 步 p_neq = 0,之后 p_neq = 0.5,另有 10% 样本固定在部署用的 (r = 0, t = 1)(`p_edge`)
|
||||
- 子目标头只用把 sub 特征置零的输入训练(sub 存在时子目标标签与输入是同一个量,原样抄就能答对);运行时子目标头只在冷启动、sub 为空时使用,口径一致。价值头用完整输入
|
||||
- 条件输入增广(训练时现做,`aug_pre_drop` 0.15 / `aug_pre_sigma` 0.1 / `aug_dq_sigma` 0.3):前缀随机清空(变成 r = 0 样本,标签不变);前缀加噪声,标签前 r 步同步加、第 r 步扣回;速度特征 dq 加噪声。目的是让模型不能靠照抄上一块和上一帧速度出动作
|
||||
|
||||
## 6′. DAgger 纠偏
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh dagger
|
||||
```
|
||||
|
||||
起点模型:预训练 best.pt 的闭环 success > 0 时用它,否则用 last.pt(`python -m ml.common.ckpt pick ml/runs/<头>`),都取 EMA 权重;之后每轮按同一规则取上一轮的结果。微调时只有起点已走过自己的 p_neq 预热才不再预热。
|
||||
用起点模型在 train 任务上闭环跑(关安全层),每题取「首次碰撞前 0.2 s」和一个随机时刻的状态做起点(须无碰、间隙 > `MIN_GAP_MM`、离目标 > 5 mm),同时记下该时刻的上一帧。老师从这些起点重新规划(`gen_dataset.cjs --tasks`,与主数据同认证),时间律从当前速度在首段方向上的分量起步(`v0`),t = 0 样本的速度特征用真实上一帧;打包后与主数据一起微调。微调的 val 只取主数据,DAgger 样本全部算 train。默认 2 轮(`DAG_ROUNDS`),每轮 8000 题(`DAG_N`)、微调 15000 步(`DAG_STEPS`,lr 3e-4)。老师只用于训练标注,部署仍无老师。M5 门禁读 `ml/runs/<头>_dagger.ckpt` 指向的模型;APG / 挖错的 BC 项也用 DAgger 样本。可断点续跑(采集、老师 shard、打包、微调各有完成标记)。
|
||||
- 显存预算**浮动**:整卡留 1 GB 给系统(`VRAM_RESERVE_GIB`,默认 1),训练与 APG 进程启动时按"整卡 − nvidia-smi 已用 − 余量"和"CUDA 空闲 − 余量"两者取小自设本进程上限(`ml/common/vram.py`)。占显存的应用多,上限就自动变小,不用改配置;想按定值对拍给 `VRAM_FRAC`(旧口径 0.78)。日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB、APG `batch=48` 13.08 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。
|
||||
|
||||
## 7. M5 门禁:选定动作头
|
||||
|
||||
@@ -177,7 +201,7 @@ bash ml/pipeline.sh mine # 默认 6 轮;ROUNDS=3 APG_STEPS=1000 MIN
|
||||
## 10. 导出
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh export # fp32 ONNX + meta.json → webapp/models/(约 20 MB)
|
||||
bash ml/pipeline.sh export # fp32 ONNX + meta.json(含 act_scale)→ webapp/models/(约 20 MB),导出 EMA 权重
|
||||
```
|
||||
|
||||
默认 fp32:部署走 wasm 与 Node CPU,fp16 在 CPU 后端上不保证更快。只有第 13 步用 `&ep=webgpu` 实测 `__POLICY.latency()` 明显受益时,才另导出 fp16 做对比:
|
||||
@@ -220,6 +244,7 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
|
||||
- 控制台 `__POLICY.latency()` 查看推理、前瞻、安全层、整次规划的 P50/P99 与节拍;`__POLICY.stats()` 查看刹停、换向、精修次数
|
||||
- `&autotest=30` 自动跑与 bench 同一批贴柱题,结果在 `__POLICY.autotest`
|
||||
- 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守)
|
||||
- 动作尺度读模型 meta 的 `act_scale`;子目标按「越过顶点」切换(与训练标签同口径:沿拐角平分面越过当前途经点即换下一个,没有超时),与仿真 `closed_loop.sub_advance` 同一规则
|
||||
- 工具偏移 ≠ 0 时自动交回原规划器
|
||||
- 离线环境:`bash ml/prep/vendor_ort.sh` 把 onnxruntime-web 放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`(WebGPU 同样可用)
|
||||
|
||||
@@ -300,7 +325,8 @@ ml/
|
||||
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(验收汇总)
|
||||
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、r4_probe.py(R4 无头浏览器探针)、acceptance.py(验收汇总)、
|
||||
replay_teacher.py(开训前回放自检,含老师 oracle 走 rollout)、diag_cl.py(闭环失败定位 A/B/C/H)
|
||||
toy/toy_bimodal.py 2D 双模态对照实验(动作头选型依据)
|
||||
export_onnx.py serve.py
|
||||
configs/ base.json(MeanFlow)、wta.json(回退)、small.json(R4 不过时的 d=128)、apg.json
|
||||
|
||||
+31
-22
@@ -6,7 +6,10 @@
|
||||
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 无需重标。"""
|
||||
运行时投影护栏独立保持 5 mm。端点与中点两个铰链取均值,与只查端点同量纲,w_col 无需重标。
|
||||
2026-10-19:起点取 EMA 权重(ckpt.load_policy);动作反归一化乘 act_scale(与数据同一尺度,不一致报错),产物存 act_scale;
|
||||
子目标推进改用 closed_loop.sub_advance(默认「越过顶点」,与标签同口径);子目标头 BC 项用 sub 特征置零的输入(同 train.py);
|
||||
val 只取主数据目录(第一个 --samples)。"""
|
||||
import argparse, json, math, os, random, sys, time
|
||||
import torch
|
||||
|
||||
@@ -15,7 +18,7 @@ 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.sim.closed_loop import evaluate, tasks_to_tensors, goal_err, sub_advance
|
||||
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
|
||||
from ml.common import vram
|
||||
|
||||
@@ -41,18 +44,23 @@ def main():
|
||||
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'])
|
||||
from ml.common.ckpt import load_policy
|
||||
model, ck = load_policy(a.init, dev); mcfg = ck['cfg']
|
||||
kin = Kin(device=dev)
|
||||
data = Samples(a.samples, device=dev, split='train')
|
||||
data = Samples(a.samples, device=dev, split='train', val_dirs=a.samples[:1])
|
||||
act_scale = float(ck.get('act_scale', 1.0))
|
||||
if abs(act_scale - data.act_scale) > 1e-9:
|
||||
raise SystemExit(f'起点 act_scale={act_scale} 与样本 act_scale={data.act_scale} 不一致(BC 项标签尺度不同),换同一版数据训练的模型')
|
||||
model.act_scale = act_scale
|
||||
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']]
|
||||
main_tid = {t['tid'] for t in load_tasks(a.samples[:1]) if t['split'] == 'val'}
|
||||
train_tasks = [t for t in all_tasks if t['tid'] not in main_tid] + load_pool(a.pool)
|
||||
val_tasks = [t for t in load_tasks(a.samples[:1]) 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)
|
||||
astep = step * act_scale # 动作 → 度
|
||||
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'])
|
||||
@@ -68,11 +76,11 @@ def main():
|
||||
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'))
|
||||
atomic_save({'model': model.state_dict(), 'cfg': mcfg, 'step': 0, 'fp': kin.fp, 'eval': base, 'act_scale': act_scale}, 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()
|
||||
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev, 'rng': rng_state(), 'act_scale': act_scale}, 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'])
|
||||
@@ -81,7 +89,7 @@ def main():
|
||||
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)
|
||||
subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev); sub_prev = q0.clone()
|
||||
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]
|
||||
@@ -95,11 +103,7 @@ def main():
|
||||
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)
|
||||
subs, nsub, sub_prev, sub_t = sub_advance(kin, subs, nsub, sub_prev, sub_t, qe.detach(), tnow)
|
||||
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)
|
||||
@@ -108,8 +112,8 @@ def main():
|
||||
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]
|
||||
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub)); sub_prev = q.detach().clone()
|
||||
inc = act.clamp(-1, 1) * astep # [B,16,6]
|
||||
commit = inc[:, r:r + 3]
|
||||
if r == 0: # 冷启动:先提交一块,下一轮才开始执行
|
||||
queue = commit; continue
|
||||
@@ -118,7 +122,7 @@ def main():
|
||||
d = queue[:, k]
|
||||
qp = q; q = kin.clamp_box(q + d)
|
||||
# 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致)
|
||||
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M)
|
||||
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M, pairs=True)
|
||||
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)
|
||||
@@ -135,13 +139,18 @@ def main():
|
||||
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()
|
||||
# 只 detach 状态并重置队列:段边界下一 plan 走 r=0 重新提交(新图有梯度);
|
||||
# 沿用旧 queue 会跨过已 backward 的图(二次 backward 报错),也避免整段无梯度
|
||||
q, qp, prev_d = q.detach(), qp.detach(), prev_d.detach()
|
||||
queue = torch.zeros(B, 0, 6, device=dev)
|
||||
# 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)
|
||||
v, _ = model.heads(mem)
|
||||
Xs = X.clone(); Xs[:, 68:75] = 0
|
||||
_, s = model.heads(model.encode(Xs))
|
||||
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'])
|
||||
@@ -160,7 +169,7 @@ def main():
|
||||
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}
|
||||
sd = {'model': model.state_dict(), 'cfg': mcfg, 'step': it + 1, 'fp': kin.fp, 'eval': res, 'act_scale': act_scale}
|
||||
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'))
|
||||
|
||||
@@ -60,3 +60,33 @@ def set_rng_state(d):
|
||||
try: torch.cuda.set_rng_state_all([t.cpu() for t in d['cuda']])
|
||||
except Exception: pass
|
||||
return True
|
||||
|
||||
|
||||
def load_policy(path, device, prefer_ema=True):
|
||||
"""载入策略(2026-10-19,审查 review_model_train P0-2):优先取 EMA 权重 ck['ema'](train.py 起存),没有就用 ck['model'](旧 checkpoint、APG 产物)。
|
||||
model.act_scale = ck['act_scale'](动作标签归一化倍数,见 ml/prep/build_dataset.cjs ACT_SCALE);旧 checkpoint 缺省 1.0 = 原口径。
|
||||
闭环仿真 / DAgger / APG 起点 / 挖错 / 门禁 / 验收 / 导出都经这里载入,保证同一份权重、同一个动作尺度。返回 (model.eval(), ck)。"""
|
||||
from ml.model.policy import build
|
||||
ck = torch.load(path, map_location=device)
|
||||
m = build(ck['cfg']).to(device)
|
||||
sd = ck.get('ema') if prefer_ema and ck.get('ema') is not None else ck['model']
|
||||
m.load_state_dict(sd)
|
||||
m.act_scale = float(ck.get('act_scale', 1.0))
|
||||
return m.eval(), ck
|
||||
|
||||
|
||||
def pick_start(run_dir):
|
||||
"""DAgger 起点(2026-10-19,审查 P0-2):best.pt 的闭环 success > 0 才用 best.pt;否则用 last.pt
|
||||
(success 全 0 时 best.pt 只由开环误差决定,可能落在 p_neq 预热期,一步映射没训过)。载入时取 EMA。"""
|
||||
best, last = os.path.join(run_dir, 'best.pt'), os.path.join(run_dir, 'last.pt')
|
||||
if os.path.exists(best):
|
||||
ev = torch.load(best, map_location='cpu').get('eval') or {}
|
||||
if (ev.get('success') or 0) > 0 or not os.path.exists(last): return best
|
||||
return last if os.path.exists(last) else best
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# python -m ml.common.ckpt pick <运行目录> → 打印 DAgger 起点 checkpoint 路径
|
||||
import sys
|
||||
if len(sys.argv) == 3 and sys.argv[1] == 'pick': print(pick_start(sys.argv[2]))
|
||||
else: raise SystemExit('usage: python -m ml.common.ckpt pick <run_dir>')
|
||||
|
||||
@@ -52,7 +52,9 @@ def features(kin, q, q_prev, T, R, prefix, prefix_len, sub, sub_flag, gm=None, x
|
||||
out.append(sd)
|
||||
out.append(sub_flag[:, None].to(dt))
|
||||
if gm is None:
|
||||
gm = kin.gaps(q, GAP_CAP, xf)['gm']
|
||||
out.append(kin.gm_groups(q, GP, GAP_CAP, xf=xf) / GAP_CAP) # 2026-10-20:只算特征用到的 8 组;xf 复用不重复算 FK
|
||||
gm = None
|
||||
if gm is not None:
|
||||
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
|
||||
|
||||
+225
-7
@@ -38,7 +38,8 @@ def seg_seg_dist(p1, q1, p2, q2, eps=1e-12):
|
||||
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(e <= eps, (-c / a.clamp_min(eps)).clamp(0, 1), s)
|
||||
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)
|
||||
@@ -85,8 +86,36 @@ class Kin:
|
||||
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'])
|
||||
table = [[0.0] * 7 for _ in range(6)]
|
||||
for group in range(1, 7):
|
||||
endpoints = [c[key] for c in caps if c['g'] == group for key in ('a', 'b')]
|
||||
if not endpoints:
|
||||
continue
|
||||
pivot, axis = K['P'][group - 1], K['D'][group - 1]
|
||||
unit = [v / math.hypot(*axis) for v in axis]
|
||||
relative = [[e[j] - pivot[j] for j in range(3)] for e in endpoints]
|
||||
radius = max(math.hypot(*v) for v in relative)
|
||||
radial = max(math.hypot(v[1] * unit[2] - v[2] * unit[1],
|
||||
v[2] * unit[0] - v[0] * unit[2],
|
||||
v[0] * unit[1] - v[1] * unit[0]) for v in relative)
|
||||
for joint in range(group):
|
||||
chain = sum(math.dist(K['P'][j], K['P'][j + 1]) for j in range(joint, group - 1))
|
||||
table[joint][group] = (chain + (radial if joint == group - 1 else radius)) * D2R
|
||||
self.cert_groups = torch.tensor([a * 7 + b for a in range(7) for b in range(a + 2, 7)], device=device)
|
||||
# 三角不等式给出全姿态界,不用随机姿态最大值冒充上界。
|
||||
self.cert_lever = T([[table[k][b] if a <= k < b else 0.0 for k in range(6)]
|
||||
for a in range(7) for b in range(a + 2, 7)])
|
||||
|
||||
# ---------------- 运动学 ----------------
|
||||
def _checked_xf(self, q, xf):
|
||||
"""xf 是按行对应 q 的位姿数组,行数不匹配会静默广播出错(宽相位筛到别的行 ⇒ device assert 或错间隙)。"""
|
||||
if xf is None:
|
||||
return self.cum_xf(q)
|
||||
Rs, Ts = xf
|
||||
if Rs.shape[0] != q.shape[0] or Ts.shape[0] != q.shape[0]:
|
||||
raise ValueError('xf 的行数 %d 与 q 的行数 %d 不符' % (Rs.shape[0], q.shape[0]))
|
||||
return xf
|
||||
|
||||
def cum_xf(self, q):
|
||||
"""q: [B,6] 度 → Rs [B,7,3,3], Ts [B,7,3]"""
|
||||
B = q.shape[0]
|
||||
@@ -110,6 +139,19 @@ class Kin:
|
||||
Rs, _ = xf if xf is not None else self.cum_xf(q)
|
||||
return Rs[:, 6]
|
||||
|
||||
def jac(self, q, xf=None):
|
||||
"""几何雅可比(对度):返回 (Jv [B,3,6] mm/deg, Jw [B,3,6] 单位轴/deg)"""
|
||||
Rs, Ts = xf if xf is not None else self.cum_xf(q)
|
||||
p = self.fk(q, (Rs, Ts))
|
||||
Jv, Jw = [], []
|
||||
for k in range(6):
|
||||
d = torch.tensor(self.D[k], dtype=q.dtype, device=q.device)
|
||||
a = Rs[:, k] @ d
|
||||
o = torch.einsum('bij,j->bi', Rs[:, k], self.P[k].to(q.dtype)) + Ts[:, k]
|
||||
Jv.append(torch.linalg.cross(a, p - o, dim=-1) * D2R)
|
||||
Jw.append(a * D2R)
|
||||
return torch.stack(Jv, -1), torch.stack(Jw, -1)
|
||||
|
||||
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]
|
||||
@@ -118,19 +160,73 @@ class Kin:
|
||||
return a, b
|
||||
|
||||
# ---------------- 间隙 ----------------
|
||||
def gaps(self, q, cap=40.0, xf=None, max_pairs=None):
|
||||
# 2026-10-19(用户实测 dagger/诊断吃满 24 GB 显存):本模型 1936 条胶囊 / 491 组 / 67256 粗对 / 104.7 万细对。
|
||||
# 显存有两段峰值,都随点数线性涨:
|
||||
# 粗筛段 cc[:,cp_a]-cc[:,cp_b] 的广播中间量 ≈ 36 B × 点数 × 粗对数(4096 点实测 9.3 GiB,与 cap 无关);
|
||||
# 精算段 ≈ 每对几百字节 × 幸存细对数(4096 点 cap 40 幸存 2.4 亿对;旧版不分块实测 36.9 GiB)。
|
||||
# 所以先按 COARSE_ENTRIES 把点切块(粗筛每块 ≤ ~1 GiB),块内再按幸存细对数 PAIR_BUDGET 切(精算每块 ≤ ~1 GiB)。
|
||||
# 两段不同时存在,峰值与调用方 batch 大小无关。结果与不分块逐位一致(梯度经 cat 保留)。
|
||||
COARSE_ENTRIES = 30_000_000
|
||||
PAIR_BUDGET = 2_000_000
|
||||
|
||||
@staticmethod
|
||||
def _cat_gaps(parts, starts):
|
||||
out = {'gm': torch.cat([p.pop('gm') for p in parts], 0)}
|
||||
if 'b' in parts[0]:
|
||||
# 非 in-place:b 会被 autograd 的索引反向图引用(APG 的碰撞铰链梯度路径),+= 会让 backward 报 version 错
|
||||
for p, s in zip(parts, starts): p['b'] = p['b'] + s
|
||||
for k in ('b', 'i', 'j', 'gap'): out[k] = torch.cat([p.pop(k) for p in parts]) # 逐键拼、拼完即释放
|
||||
return out
|
||||
|
||||
def gaps(self, q, cap=40.0, xf=None, max_pairs=None, pairs=False):
|
||||
"""返回 dict:
|
||||
gm [B,49]:组对最小间隙 min(gap, cap)(gA*7+gB,gA<gB;其余为 cap)
|
||||
b, i, j, gap:幸存胶囊对(gap 可微)
|
||||
pairs=True 时另有 b, i, j, gap:幸存胶囊对(gap 可微;只有 APG 的 hinge 与 check_parity 需要)
|
||||
2026-10-19(用户实测 4096 点 cap 40 峰值 4.46 GiB 全在拼接 8.6e7 对的 b/i/j/gap):
|
||||
默认只返回 gm,每块算完就丢掉胶囊对,峰值只剩单块。
|
||||
"""
|
||||
xf = xf if xf is not None else self.cum_xf(q)
|
||||
xf = self._checked_xf(q, xf)
|
||||
Rs, Ts = xf
|
||||
B = q.shape[0]
|
||||
step = max(1, self.COARSE_ENTRIES // int(self.cp_a.shape[0]))
|
||||
if B <= step:
|
||||
return self._gaps_split(q, cap, xf, pairs)
|
||||
st = list(range(0, B, step))
|
||||
return self._cat_gaps([self._gaps_split(q[s:s + step], cap, (Rs[s:s + step], Ts[s:s + step]), pairs) for s in st], st)
|
||||
|
||||
def _coarse(self, q, cap, xf):
|
||||
Rs, Ts = xf
|
||||
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]
|
||||
del cc
|
||||
return d2 <= (self.cp_lim + cap).pow(2)
|
||||
|
||||
def _gaps_split(self, q, cap, xf, pairs):
|
||||
"""粗筛已在显存预算内:按幸存细对数再切块做精算"""
|
||||
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)
|
||||
per = (self._coarse(q, cap, xf).long() * self.cp_cnt).sum(1) # [B] 每点幸存细对数
|
||||
if B <= 1 or int(per.sum()) <= self.PAIR_BUDGET:
|
||||
bounds = [0, B]
|
||||
else:
|
||||
cs = torch.cumsum(per, 0).tolist(); bounds = [0]; base = 0
|
||||
for k, c in enumerate(cs):
|
||||
if c - base > self.PAIR_BUDGET and k > bounds[-1]:
|
||||
bounds.append(k); base = cs[k - 1]
|
||||
bounds.append(B)
|
||||
del per
|
||||
if len(bounds) == 2:
|
||||
return self._gaps_one(q, cap, xf, pairs)
|
||||
return self._cat_gaps([self._gaps_one(q[s:e], cap, (Rs[s:e], Ts[s:e]), pairs) for s, e in zip(bounds[:-1], bounds[1:])], bounds[:-1])
|
||||
|
||||
def _gaps_one(self, q, cap, xf, pairs):
|
||||
Rs, Ts = xf
|
||||
B = q.shape[0]
|
||||
with torch.no_grad():
|
||||
keep = self._coarse(q, cap, xf)
|
||||
bb, mm = keep.nonzero(as_tuple=True)
|
||||
del keep
|
||||
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)
|
||||
@@ -145,8 +241,130 @@ class Kin:
|
||||
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)
|
||||
if not pairs: return {'gm': gm.view(B, 49)}
|
||||
return {'gm': gm.view(B, 49), 'b': rep_b, 'i': pi, 'j': pj, 'gap': gap}
|
||||
|
||||
def gm_groups(self, q, gp, cap=40.0, xf=None):
|
||||
"""只算指定组对 gp=[(a,b),...] 的最小间隙 [B,len(gp)](特征用,避免 49 组全量)。
|
||||
数值与 gaps(q,cap)['gm'][:, a*7+b] 一致;组对在碰撞粗对里不存在时返回 cap。
|
||||
xf 可传入调用者已算好的 cum_xf(q),避免同一姿态重复算 FK(特征路径每周期省一次)。"""
|
||||
key = tuple(gp)
|
||||
if getattr(self, '_gp_key', None) != key:
|
||||
m = torch.full((49,), -1, dtype=torch.long, device=q.device)
|
||||
for i, (a_, b_) in enumerate(gp): m[min(a_, b_) * 7 + max(a_, b_)] = i
|
||||
ga, gb = self.cl_g[self.cp_a], self.cl_g[self.cp_b]
|
||||
cg = torch.minimum(ga, gb) * 7 + torch.maximum(ga, gb) # 簇对 → 7 连杆组对
|
||||
sel = torch.isin(cg, torch.tensor([min(a, b) * 7 + max(a, b) for a, b in gp], device=q.device))
|
||||
self._gp = (m, sel, sel.nonzero(as_tuple=True)[0]); self._gp_key = key
|
||||
self._gp49, sel, idx = self._gp
|
||||
xf = self._checked_xf(q, xf); 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[idx]] - cc[:, self.cp_b[idx]]).pow(2).sum(-1)
|
||||
keep = d2 <= (self.cp_lim[idx] + cap).pow(2)
|
||||
bb, mm = keep.nonzero(as_tuple=True)
|
||||
del keep, d2, cc
|
||||
cnt = self.cp_cnt[idx][mm]
|
||||
rep_b = torch.repeat_interleave(bb, cnt)
|
||||
off = torch.repeat_interleave(self.cp_start[idx][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 * len(gp),), float(cap), dtype=q.dtype, device=q.device)
|
||||
gm = gm.scatter_reduce(0, rep_b * len(gp) + self._gp49[self.pair_gp[pidx]], gap.clamp(max=cap), reduce='amin', include_self=True)
|
||||
return gm.view(B, len(gp))
|
||||
|
||||
@torch.no_grad()
|
||||
def certify_segments(self, a, b, max_queries=32, max_depth=12, eps=1e-4, cap=20.0):
|
||||
"""逐层二分认证:每根段独立查询预算,未知(超预算/超深度/非有限/越界)一律拒绝。
|
||||
|
||||
判据对每条组对:子段两端间隙之和 > 该子段位移界 + 2eps(间隙取 min(gap,cap),钳制只会更保守)。
|
||||
cap 必须 ≥ 该层子段界 + 2eps 才不会挡住本可通过的判定,故不做自适应收紧——同批其他段会改变
|
||||
钳制程度、让结果随批次而变(实测也无耗时收益,见探索日志轮次 31)。
|
||||
"""
|
||||
if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64:
|
||||
raise ValueError('segment certification requires float64')
|
||||
if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6:
|
||||
raise ValueError('a and b must be [count,6] with identical shape')
|
||||
# 预算给成张量时 max_depth + 1 会在 uint8 这类窄类型上回绕成 0,循环整个跳过,把碰撞段当安全返回。
|
||||
if type(max_queries) is not int or type(max_depth) is not int:
|
||||
raise ValueError('certification budgets must be plain ints')
|
||||
if max_queries < 2 or max_depth < 0 or eps <= 0 or cap <= eps:
|
||||
raise ValueError('invalid certification budget or tolerance')
|
||||
count = a.shape[0]
|
||||
status = torch.zeros(count, dtype=torch.long, device=a.device)
|
||||
inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1)
|
||||
status[~inside] = 3
|
||||
root = torch.arange(count, device=a.device)
|
||||
gaps = self.gaps(torch.cat([a, b]), cap)['gm'][:, self.cert_groups]
|
||||
ga, gb = gaps[:count], gaps[count:]
|
||||
queries = torch.full((count,), 2, dtype=torch.long, device=a.device)
|
||||
bound = (b - a).abs() @ self.cert_lever.T
|
||||
for depth in range(max_depth + 1):
|
||||
finite = torch.isfinite(ga).all(-1) & torch.isfinite(gb).all(-1)
|
||||
collision = (ga <= 0).any(-1) | (gb <= 0).any(-1)
|
||||
status[root[~finite]] = 2
|
||||
status[root[collision]] = 1
|
||||
positive = (ga > eps).all(-1) & (gb > eps).all(-1)
|
||||
proven = positive & (ga + gb > bound + 2 * eps).all(-1)
|
||||
pending = (status[root] == 0) & ~proven
|
||||
if not bool(pending.any()):
|
||||
break
|
||||
a, b, ga, gb, root, bound = a[pending], b[pending], ga[pending], gb[pending], root[pending], bound[pending]
|
||||
if depth == max_depth:
|
||||
status[root] = 2
|
||||
break
|
||||
requested = torch.bincount(root, minlength=count)
|
||||
status[(status == 0) & (queries + requested > max_queries)] = 2
|
||||
keep = status[root] == 0
|
||||
a, b, ga, gb, root, bound = (t[keep] for t in (a, b, ga, gb, root, bound))
|
||||
if not root.numel():
|
||||
break
|
||||
mid = a + 0.5 * (b - a)
|
||||
gm = self.gaps(mid, cap)['gm'][:, self.cert_groups]
|
||||
queries += torch.bincount(root, minlength=count)
|
||||
a, b = torch.cat([a, mid]), torch.cat([mid, b])
|
||||
ga, gb = torch.cat([ga, gm]), torch.cat([gm, gb])
|
||||
root = torch.cat([root, root])
|
||||
bound = torch.cat([bound, bound]) / 2
|
||||
return {'safe': status == 0, 'collision': status == 1, 'unresolved': status == 2,
|
||||
'outside_box': status == 3, 'queries': queries}
|
||||
|
||||
@torch.no_grad()
|
||||
def certify_uniform(self, a, b, n=64, eps=1e-4, cap=None):
|
||||
"""固定层数的均匀细分认证:n 个子段只需一次 gaps 调用(n+1 个点),没有逐层依赖的往返。
|
||||
|
||||
判据与 certify_segments 完全相同;界对 Δq 线性,所以子段界 = 整段界 / n。未通过即 unproven
|
||||
(拒绝),不存在“未知当安全”。cap=None 时取“最小不挡住可通过判定的宽相位”= 最大子段界 + 2eps:
|
||||
小于它钳制会把本可证明的子段压成不可证(偏保守),大于它只是多算对(偏慢),不改正确性。
|
||||
"""
|
||||
if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64:
|
||||
raise ValueError('segment certification requires float64')
|
||||
if type(n) is not int or n < 1 or eps <= 0 or (cap is not None and cap <= eps):
|
||||
raise ValueError('invalid subdivision count or tolerance')
|
||||
if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6:
|
||||
raise ValueError('a and b must be [count,6] with identical shape')
|
||||
count = a.shape[0]
|
||||
inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1)
|
||||
bound = (b - a).abs() @ self.cert_lever.T / n # [count,15] 子段界
|
||||
required = float(bound.max()) + 2 * eps if count else 2 * eps
|
||||
qc = required if cap is None else cap
|
||||
alpha = torch.arange(n + 1, dtype=self.dtype, device=a.device) / n
|
||||
points = (a[:, None] + (b - a)[:, None] * alpha[None, :, None]).reshape(-1, 6)
|
||||
gm = self.gaps(points, qc)['gm'][:, self.cert_groups].view(count, n + 1, -1)
|
||||
adjacent = gm[:, :-1] + gm[:, 1:] > bound[:, None, :] + 2 * eps
|
||||
collision = (gm <= 0).any((1, 2))
|
||||
safe = inside & (gm > eps).all((1, 2)) & adjacent.all((1, 2)) & ~collision
|
||||
return {'safe': safe, 'collision': collision, 'unproven': (~safe & ~collision & inside),
|
||||
'outside_box': ~inside, 'points': n + 1, 'queries': n + 1,
|
||||
'cap_required_mm': required, 'cap_used_mm': qc}
|
||||
|
||||
def min_gap(self, q, cap=40.0):
|
||||
return self.gaps(q, cap)['gm'].min(-1).values
|
||||
|
||||
|
||||
+36
-9
@@ -1,19 +1,46 @@
|
||||
"""显存预算(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。"""
|
||||
"""显存预算(2026-10-18 本机实测后加;同日两次修正)。
|
||||
口径:整卡留 1 GB 给系统(VRAM_RESERVE_GIB),本进程上限 = CUDA 可用 − 余量。
|
||||
为什么不能按"整卡 − nvidia-smi 已用"扣:WDDM 下 nvidia-smi 的"已用"含**驱动可以换出**的量,
|
||||
把它当硬占用会把自己卡死——实测预训练第一次 evaluate 峰值要 ~15 GiB,那样算出的上限只有
|
||||
14.45 GiB,torch 就报 OOM,而同一时刻卡上其实还有 9.98 GiB 空闲(train.py step 2000 的评估里挂掉)。
|
||||
CUDA 的 mem_get_info 才是"我实际能拿到多少":它已经把可换出的部分算进去了。
|
||||
limit() 上限 = min(CUDA 可用, 整卡) − 余量;显式给 VRAM_FRAC 时再取较小值(跟旧口径 0.78 对拍用)。
|
||||
别的应用占多少照样打印,但只作参考、不从上限里扣。
|
||||
peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对,不用再去猜。
|
||||
实测参考(d=192 同结构):预训练 batch=1024 的训练步峰值只有 2.76 GiB,但第一次 evaluate 就要 ~15 GiB
|
||||
(kin.gaps 一次开 2.35 GiB);APG batch=48/plans=150 → 13.08 GiB。"""
|
||||
import os
|
||||
import subprocess
|
||||
import torch
|
||||
|
||||
|
||||
def _smi():
|
||||
"""(整卡已用 GiB, 整卡总量 GiB);拿不到就 (None, None)。"""
|
||||
try:
|
||||
q = 'memory.used,memory.total'
|
||||
r = subprocess.run(['nvidia-smi', f'--query-gpu={q}', '--format=csv,noheader,nounits'],
|
||||
capture_output=True, text=True, timeout=3)
|
||||
u, t = [float(x) for x in r.stdout.strip().splitlines()[0].split(',')[:2]]
|
||||
return u / 1024, t / 1024
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
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)
|
||||
torch.empty(1, device='cuda') # 让本进程的 CUDA 上下文先落地,再读可用(否则会把自家上下文算进可用)
|
||||
free = torch.cuda.mem_get_info()[0] / 2**30
|
||||
res = float(os.environ.get('VRAM_RESERVE_GIB', '1'))
|
||||
cap = min(free, tot) - res
|
||||
if os.environ.get('VRAM_FRAC'): # 显式硬帽(对拍旧口径用)
|
||||
cap = min(cap, tot * float(os.environ['VRAM_FRAC']))
|
||||
cap = max(1.0, min(cap, tot - res))
|
||||
torch.cuda.set_per_process_memory_fraction(cap / tot)
|
||||
used, _ = _smi()
|
||||
note = '' if used is None else ';别的应用当前占 %.1f GiB,需要时驱动自行换出,不从上限里扣' % used
|
||||
print('显存上限:本进程 %.1f GiB(CUDA 可用 %.1f − 系统余量 %.1f,= 整卡 %.1f 的 %.0f%%)%s'
|
||||
% (cap, free, res, tot, 100 * cap / tot, note), flush=True)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"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],
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "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,
|
||||
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
|
||||
"train": {
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "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.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
|
||||
"train": {
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "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.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"model": {
|
||||
"d": 192,
|
||||
"heads": 8,
|
||||
"ff": 768,
|
||||
"enc": 4,
|
||||
"dec": 4
|
||||
},
|
||||
"train": {
|
||||
"batch": 1024,
|
||||
"lr": 0.0001,
|
||||
"warmup": 1000,
|
||||
"steps": 3000,
|
||||
"wd": 0.01,
|
||||
"betas": [
|
||||
0.9,
|
||||
0.95
|
||||
],
|
||||
"amp": "bf16",
|
||||
"grad_clip": 1.0,
|
||||
"w_mf": 1.0,
|
||||
"w_fk": 0.1,
|
||||
"w_smooth": 0.01,
|
||||
"w_value": 0.0,
|
||||
"w_sub": 0.0,
|
||||
"p_neq": 0.5,
|
||||
"p_edge": 0.1,
|
||||
"p_neq_warm": 0,
|
||||
"fk_tmax": 0.3,
|
||||
"fk_caps": 96,
|
||||
"ema": 0.999,
|
||||
"aug_pre_drop": 0.15,
|
||||
"aug_pre_sigma": 0.1,
|
||||
"aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10,
|
||||
"eval_every": 1000,
|
||||
"eval_tasks": 256,
|
||||
"log_every": 50,
|
||||
"patience_evals": 3
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"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],
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "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,
|
||||
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -29,15 +29,36 @@
|
||||
"eval_every": 100,
|
||||
"eval_tasks": 16,
|
||||
"log_every": 10,
|
||||
"patience_evals": 5
|
||||
"patience_evals": 5,
|
||||
"p_neq_warm": 100,
|
||||
"fk_tmax": 0.3
|
||||
},
|
||||
"apg": {
|
||||
"batch": 8, "lr": 0.0001, "steps": 10, "grad_clip": 1.0, "amp": "bf16",
|
||||
"plans": 20, "plans_max": 30, "seg_plans": 5,
|
||||
"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
|
||||
"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,自检不必吃这么多。"
|
||||
}
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"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],
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "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,
|
||||
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""DAgger 状态采集(2026-10-18 首次正式预训练后新增):用当前策略在 train 任务上闭环跑(关安全层),
|
||||
从它自己走到的状态里挑起点,交给老师重新规划 → 这些"策略会去、老师数据里没有"的状态拿到正确标签。
|
||||
python -m ml.dagger.collect --ckpt ml/runs/base/best.pt --samples ml/data/samples --n 8000 --seed 1 --parts 28 --out ml/data/dag1
|
||||
每题最多取 2 个起点:
|
||||
- 撞了的题:首次碰撞前 0.2 s 的状态(策略"快要撞上"的地方,最有用);
|
||||
- 另取一个 [0.3 s, 首撞前或结束] 内均匀随机时刻的状态(偏离老师的一般状态)。
|
||||
起点必须无碰且间隙 > MIN_GAP_MM(老师从碰撞态出发规划不出来,与 build_dataset 的增广接受同一地板),且离目标 > 5 mm(否则 DLS 接管,不需要老师)。
|
||||
输出 tasks_XX.jsonl(按 --parts 均分,供 gen_dataset.cjs --tasks 并行),每行 {q0, qPrev, T, R, cat, src}。
|
||||
2026-10-19(审查 F7 / C10):同时记录策略在该时刻的上一帧 qPrev;老师重规划的时间律从当前速度(v0)起步,
|
||||
t=0 样本的 dq 特征用真实上一帧——原来一律按静止起步标注,DAgger 样本不是策略真正到过的 (q, dq) 状态。
|
||||
为什么需要:纯行为克隆在闭环里会偏出老师分布(首次 21850 步闭环到达 0%),原方案指望 APG/挖错修,但它们没法从 0% 起步。
|
||||
老师只用于训练标注,部署仍然无老师。"""
|
||||
import argparse, json, os, random
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.data import load_tasks
|
||||
from ml.model.policy import build
|
||||
from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors, DT
|
||||
|
||||
GAP_CHUNK = 1024 # 每次 kin.gaps 的查询点数上限
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', nargs='+', default=['ml/data/samples'])
|
||||
ap.add_argument('--n', type=int, default=8000); ap.add_argument('--seed', type=int, default=1)
|
||||
ap.add_argument('--parts', type=int, default=8); ap.add_argument('--out', required=True)
|
||||
ap.add_argument('--bs', type=int, default=64)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
ap.add_argument('--fb_gain', type=float, default=0.0, help='部署分布采集:目标反馈增益(与 rollout 一致)')
|
||||
ap.add_argument('--safety', type=int, default=0, help='部署分布采集:是否开安全层')
|
||||
ap.add_argument('--replan', default='r3'); ap.add_argument('--commit_n', type=int, default=3)
|
||||
ap.add_argument('--safety_partial', type=int, default=0)
|
||||
a = ap.parse_args()
|
||||
gap_min = float(os.environ.get('MIN_GAP_MM') or 3)
|
||||
os.makedirs(a.out, exist_ok=True)
|
||||
kin = Kin(device=a.device)
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
|
||||
pool = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'train' and t.get('cat') not in ('D',)]
|
||||
rng = random.Random(a.seed); rng.shuffle(pool); tasks = pool[:a.n]
|
||||
out, stat = [], {'tasks': len(tasks), 'collided': 0, 'states': 0, 'rej_gap': 0, 'rej_near': 0}
|
||||
for i in range(0, len(tasks), a.bs):
|
||||
tb = tasks[i:i + a.bs]
|
||||
with torch.no_grad():
|
||||
_, per = rollout(model, kin, tb, a.device, K=8, safety=bool(a.safety), seed=a.seed * 100003 + i, return_traj=True,
|
||||
fb_gain=a.fb_gain, replan=a.replan, commit_n=a.commit_n, safety_partial=a.safety_partial)
|
||||
tr = per['traj'].to(a.device) # [B,T,6]
|
||||
B, Tn, _ = tr.shape
|
||||
_, T, R, _, _ = tasks_to_tensors(tb, a.device)
|
||||
# 2026-10-19 修(用户实测):原来一次喂 64 题 × 64 步 = 4096 点、cap=40 mm 给 kin.gaps,40 mm 内幸存的胶囊对太多,
|
||||
# 峰值把显存 / 内存吃满。这里只需判断"≤0(碰撞)"和"> gap_min(起点地板)",cap 取 gap_min+1 即可,
|
||||
# 且按 GAP_CHUNK 个点分块,用完即释放。
|
||||
cap = gap_min + 1.0
|
||||
flat = tr.reshape(-1, 6)
|
||||
with torch.no_grad():
|
||||
g = torch.cat([kin.gaps(flat[s:s + GAP_CHUNK], cap)['gm'].min(-1).values for s in range(0, flat.shape[0], GAP_CHUNK)]).view(B, Tn)
|
||||
del flat
|
||||
for b, t in enumerate(tb):
|
||||
hit = (g[b] <= 0).nonzero()
|
||||
c = int(hit[0]) if hit.numel() else Tn
|
||||
if hit.numel(): stat['collided'] += 1
|
||||
cand = []
|
||||
if hit.numel(): cand.append(max(0, c - int(round(0.2 / DT))))
|
||||
lo = int(round(0.3 / DT))
|
||||
if c - 1 > lo: cand.append(rng.randint(lo, c - 1))
|
||||
for k in cand:
|
||||
if g[b, k] <= gap_min: stat['rej_gap'] += 1; continue
|
||||
q = tr[b, k:k + 1]; qprev = tr[b, max(0, k - 1)]
|
||||
e, oe = goal_err(kin, q, T[b:b + 1], R[b:b + 1])
|
||||
if e.item() < 5 and (not t.get('R') or oe.item() < 5): stat['rej_near'] += 1; continue
|
||||
out.append({'q0': [round(v, 3) for v in q[0].tolist()], 'qPrev': [round(v, 3) for v in qprev.tolist()], 'T': t['T'], 'R': t.get('R'), 'cat': t.get('cat', 'A'),
|
||||
'src': {'tid': t['tid'], 'k': k, 'col': bool(hit.numel())}})
|
||||
del tr, g, per
|
||||
if a.device.startswith('cuda'): torch.cuda.empty_cache()
|
||||
print(f'[dagger] {min(i + a.bs, len(tasks))}/{len(tasks)} 题,起点 {len(out)}', flush=True)
|
||||
stat['states'] = len(out)
|
||||
rng.shuffle(out)
|
||||
per_part = (len(out) + a.parts - 1) // max(1, a.parts)
|
||||
for p in range(a.parts):
|
||||
chunk = out[p * per_part:(p + 1) * per_part]
|
||||
if not chunk: continue
|
||||
fn = os.path.join(a.out, 'tasks_%02d.jsonl' % (p + 1))
|
||||
open(fn + '.tmp', 'w', encoding='utf-8').write(''.join(json.dumps(x) + '\n' for x in chunk)); os.replace(fn + '.tmp', fn)
|
||||
json.dump(stat, open(os.path.join(a.out, 'collect.json'), 'w', encoding='utf-8'), indent=1) # 最后写:它存在 = 采集完成
|
||||
print(json.dumps(stat, ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
"""反馈蒸馏:用最终部署配置(目标反馈 + 安全层 + 部分前缀 + 每步规划)跑闭环,
|
||||
收集 (特征, 复合动作 = 模型输出 + 目标反馈) 样本,供 train.py 微调把反馈内化进策略。
|
||||
PYTHONUTF8=1 python -m ml.dagger.distill_fb --ckpt ml/runs/base_dag2_dag1/best.pt --n 500 \
|
||||
--fb_gain 1.8 --out ml/data/samples_fbdistill
|
||||
输出可直接被 ml.data.Samples 读取(collect.X/Y/V/SG/TID.npy;无 meta = act_scale 1.0,与主数据一致)。
|
||||
动作标签 = 候选动作(含反馈)的归一化 act(/STEP,clamp ±1),与主数据同口径。
|
||||
"""
|
||||
import argparse, os, random
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ml.common.ckpt import load_policy
|
||||
from ml.common.features import STEP
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.data import load_tasks
|
||||
from ml.sim.closed_loop import rollout
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--n', type=int, default=500)
|
||||
ap.add_argument('--bs', type=int, default=64); ap.add_argument('--fb_gain', type=float, default=1.8)
|
||||
ap.add_argument('--samples', default='ml/data/samples'); ap.add_argument('--out', required=True)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=1)
|
||||
a = ap.parse_args()
|
||||
dev = a.device
|
||||
model, _ = load_policy(a.ckpt, dev)
|
||||
kin = Kin(device=dev)
|
||||
pool = [t for t in load_tasks([a.samples]) if t['split'] == 'train' and t.get('cat') not in ('D',)]
|
||||
random.Random(a.seed).shuffle(pool); tasks = pool[:a.n]
|
||||
stp = torch.tensor(STEP) # collect 返回 CPU 张量,标签换算也在 CPU
|
||||
X, Y, TID = [], [], []
|
||||
for i in range(0, len(tasks), a.bs):
|
||||
ch = tasks[i:i + a.bs]
|
||||
coll = []
|
||||
with torch.no_grad():
|
||||
rollout(model, kin, ch, dev, K=8, safety=True, replan='empty', fb_gain=a.fb_gain,
|
||||
safety_partial=1, commit_n=1, seed=a.seed * 100003 + i, collect=coll)
|
||||
for f, ac in coll:
|
||||
X.append(f.numpy()); Y.append((ac / stp).clamp(-1, 1).numpy())
|
||||
ncol = sum(f.shape[0] for f, _ in coll)
|
||||
TID.append(np.arange(ncol, dtype=np.int32) + i * 100000)
|
||||
print(f'[distill] {i + len(ch)}/{len(tasks)} tasks, samples {sum(x.shape[0] for x in X)}', flush=True)
|
||||
os.makedirs(a.out, exist_ok=True)
|
||||
X = np.concatenate(X); Y = np.concatenate(Y); TID = np.concatenate(TID)
|
||||
n = X.shape[0]
|
||||
np.save(os.path.join(a.out, 'collect.X.npy'), X.astype(np.float32))
|
||||
np.save(os.path.join(a.out, 'collect.Y.npy'), Y.astype(np.float32))
|
||||
np.save(os.path.join(a.out, 'collect.V.npy'), np.full(n, 0.5, np.float32))
|
||||
np.save(os.path.join(a.out, 'collect.SG.npy'), np.zeros((n, 13), np.float32))
|
||||
np.save(os.path.join(a.out, 'collect.TID.npy'), TID)
|
||||
print(f'done: {n} samples, X {X.shape}, Y {Y.shape}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# DAgger 纠偏(2026-10-18 首次正式预训练后新增):bash ml/dagger/run.sh <头标签> <起点 ckpt>
|
||||
# 每轮:策略闭环采起点(ml.dagger.collect)→ 老师从这些起点重新规划(gen_dataset.cjs --tasks,NPROC 并行)
|
||||
# → 打包成样本(build_dataset.cjs,与主数据同口径)→ 主数据 + 历轮 DAgger 样本一起微调 DAG_STEPS 步。
|
||||
# 环境变量:DAG_ROUNDS(默认 2)、DAG_N(每轮采样题数,默认 8000,每题最多 2 个起点)、DAG_STEPS(每轮微调步数,默认 15000)、
|
||||
# DAG_LR(默认 3e-4)、NPROC、MIN_GAP_MM。
|
||||
# 产物:ml/data/dag_<tag>_r<k>/(起点与老师 shard)、ml/data/samples_dag_<tag>_r<k>/、ml/runs/<tag>_dag<k>/;
|
||||
# 最终模型路径写进 ml/runs/<tag>_dagger.ckpt(m5 读它)。
|
||||
# 断点续跑:采集以 collect.json 为完成标记;老师 shard 先写 .part 再改名、未写完的从断点续写;打包以 .tasks.jsonl 为完成标记;
|
||||
# 微调用该轮 last.pt 续训;整轮完成打 ml/runs/<tag>_dag<k>/done。
|
||||
# 2026-10-19(审查 review_model_train P0-2):每轮起点 = python -m ml.common.ckpt pick <目录>:best.pt 的闭环 success > 0 才用它,
|
||||
# 否则用 last.pt(success 全 0 时 best.pt 只由开环误差决定、可能落在 p_neq 预热期);train.py --init 载入 EMA 权重,
|
||||
# 只有起点已走过自己的 p_neq 预热时才不再预热。微调的 val 只取主数据(DAgger 样本全部算 train)。
|
||||
set -e -o pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
PY=${PYTHON:-python}
|
||||
TAG=$1; CK=$2
|
||||
[ -n "$TAG" ] && [ -f "$CK" ] || { echo "usage: run.sh <tag> <ckpt>($CK 不存在)"; exit 1; }
|
||||
R=${DAG_ROUNDS:-2}; N=${DAG_N:-8000}; STEPS=${DAG_STEPS:-15000}; LR=${DAG_LR:-3e-4}; P=${NPROC:-$(nproc)}
|
||||
CFGF=ml/configs/$TAG.json
|
||||
SAMPLES="ml/data/samples"
|
||||
for ((r=1; r<=R; r++)); do
|
||||
D=ml/data/dag_${TAG}_r$r; SD=ml/data/samples_dag_${TAG}_r$r; OUT=ml/runs/${TAG}_dag$r
|
||||
SAMPLES="$SAMPLES $SD"
|
||||
if [ -f $OUT/done ]; then echo "[dagger] 第 $r 轮已完成,跳过"; CK=$($PY -m ml.common.ckpt pick $OUT); continue; fi
|
||||
if [ -f $D/collect.json ]; then echo "[dagger] 第 $r 轮起点已采集,跳过"
|
||||
else echo "[dagger] 第 $r 轮:用 $CK 闭环采起点($N 题)"; $PY -m ml.dagger.collect --ckpt $CK --samples ml/data/samples --n $N --seed $r --parts $P --out $D --fb_gain ${FB_GAIN:-0} --safety ${DAG_SAFETY:-0} --replan ${DAG_REPLAN:-r3} --commit_n ${DAG_COMMIT_N:-3} --safety_partial ${DAG_PARTIAL:-0}; fi
|
||||
cat $D/collect.json
|
||||
pids=()
|
||||
for f in $D/tasks_*.jsonl; do
|
||||
[ -e "$f" ] || continue
|
||||
k=$(basename $f .jsonl); k=${k#tasks_}; sh=$D/shard_dag_${TAG}_r${r}_$k.jsonl
|
||||
[ -f $sh ] && continue
|
||||
( node ml/gen/gen_dataset.cjs --tasks $f --seed $((9000 + 100 * r + 10#$k)) --out $sh.part 2>> $D/log_$k.txt && mv $sh.part $sh ) &
|
||||
pids+=($!)
|
||||
done
|
||||
if [ ${#pids[@]} -gt 0 ]; then
|
||||
echo "[dagger] 第 $r 轮:老师重新规划(${#pids[@]} 进程)"
|
||||
t0=$(date +%s)
|
||||
while :; do
|
||||
alive=0; for p in "${pids[@]}"; do kill -0 $p 2>/dev/null && alive=$((alive+1)); done
|
||||
[ $alive = 0 ] && break; sleep 30
|
||||
echo "[dagger 节拍] $(( ($(date +%s) - t0) / 60 )) 分钟 运行中 $alive/${#pids[@]} 已写 $(cat $D/*.part $D/shard_*.jsonl 2>/dev/null | wc -l) 条"
|
||||
done
|
||||
for p in "${pids[@]}"; do wait $p || true; done # 判定按产物(同 run_parallel.sh:Git Bash 长跑后 wait 返回码不可靠)
|
||||
for f in $D/tasks_*.jsonl; do
|
||||
k=$(basename $f .jsonl); k=${k#tasks_}; sh=$D/shard_dag_${TAG}_r${r}_$k.jsonl
|
||||
if [ ! -f $sh ]; then
|
||||
n=0; [ -f $sh.part ] && n=$(wc -l < $sh.part); need=$(wc -l < $f)
|
||||
if [ "$n" -ge "$need" ]; then mv $sh.part $sh; else echo "[dagger] $sh 未写完($n/$need),见 $D/log_$k.txt;重跑本阶段从断点续写"; exit 6; fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
mkdir -p $SD
|
||||
SIG=$(node ml/prep/build_dataset.cjs --sig) # 2026-10-19:与主数据同一套打包签名,参数变了重打(见 run_prep.sh)
|
||||
todo=(); for sh in $D/shard_dag_*.jsonl; do [ -e "$sh" ] || continue; b=$(basename $sh .jsonl); grep -qF "\"sig\":\"$SIG\"" $SD/$b.meta.json 2>/dev/null && [ -f $SD/$b.tasks.jsonl ] || todo+=("$sh"); done
|
||||
[ ${#todo[@]} = 0 ] || printf '%s\n' "${todo[@]}" | xargs -P "$P" -I{} node ml/prep/build_dataset.cjs {} $SD
|
||||
$PY -m ml.data --index $SD | tail -n 3
|
||||
res=""; [ -f $OUT/last.pt ] && res="--resume $OUT/last.pt"
|
||||
echo "[dagger] 第 $r 轮:微调 $STEPS 步(样本:$SAMPLES)"
|
||||
$PY -m ml.train --samples $SAMPLES --out $OUT --config $CFGF --init $CK --steps $STEPS --lr $LR --warmup 500 $res
|
||||
touch $OUT/done; CK=$($PY -m ml.common.ckpt pick $OUT)
|
||||
done
|
||||
echo "$CK" > ml/runs/${TAG}_dagger.ckpt
|
||||
echo "[dagger] 完成:后续阶段使用 $CK"
|
||||
+66
-7
@@ -1,12 +1,17 @@
|
||||
"""样本加载:ml/data/samples/*.{X,Y,V,SG,TID}.npy + *.tasks.jsonl(ml/prep/build_dataset.cjs 产出)
|
||||
"""样本加载:ml/data/samples/*.{X,Y,V,SG,TID}.npy + *.tasks.jsonl + *.meta.json(ml/prep/build_dataset.cjs 产出)
|
||||
python -m ml.data --index ml/data/samples # 汇总样本/任务数,写 index.json
|
||||
全部载入内存(200 万样本约 1.5 GB),按任务 split 字段划分 train / val。"""
|
||||
全部载入内存(200 万样本约 1.5 GB),按任务 split 字段划分 train / val。
|
||||
动作尺度(2026-10-19):Y = 每步增量 / (act_scale·STEP)。act_scale 取自各 shard 的 meta.json(DATA_VER 2 起写;
|
||||
旧数据没有 meta → act_scale 1.0、data_ver 1,即原口径)。同一次训练的所有目录(主数据 + DAgger)必须同一个 act_scale。"""
|
||||
import argparse, glob, json, os
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
PREFIX_SL, MASK_SL, DQ_SL, SUB_SL = slice(47, 65), slice(65, 68), slice(15, 21), slice(68, 75) # 特征分组(features.py 拼接顺序)
|
||||
|
||||
|
||||
def load_tasks(dirs):
|
||||
if isinstance(dirs, str): dirs = [dirs]
|
||||
tasks = []
|
||||
for d in dirs:
|
||||
for f in sorted(glob.glob(os.path.join(d, '*.tasks.jsonl'))):
|
||||
@@ -16,10 +21,28 @@ def load_tasks(dirs):
|
||||
return tasks
|
||||
|
||||
|
||||
def read_meta(d):
|
||||
"""目录内各 shard 的打包参数(data_ver、act_scale、增广参数、sig)。没有 meta 的旧打包 → {'data_ver': 1, 'act_scale': 1.0}。
|
||||
一部分有、一部分没有,或 act_scale 不一致 → 拒绝(新旧标签尺度混在一起会让动作幅度错 25%)。"""
|
||||
xs = sorted(glob.glob(os.path.join(d, '*.X.npy')))
|
||||
ms = [json.load(open(f[:-6] + '.meta.json', encoding='utf-8')) for f in xs if os.path.exists(f[:-6] + '.meta.json')]
|
||||
if not ms: return {'data_ver': 1, 'act_scale': 1.0}
|
||||
if len(ms) != len(xs): raise SystemExit(f'{d}:{len(xs) - len(ms)} 个 shard 缺 meta.json(新旧打包混在一起),重跑 prep')
|
||||
sc = {float(m.get('act_scale', 1.0)) for m in ms}
|
||||
if len(sc) != 1: raise SystemExit(f'{d}:各 shard 的 act_scale 不一致 {sorted(sc)},重跑 prep')
|
||||
return ms[0]
|
||||
|
||||
|
||||
class Samples:
|
||||
def __init__(self, dirs, device='cpu', split=None):
|
||||
def __init__(self, dirs, device='cpu', split=None, val_dirs=None):
|
||||
"""val_dirs:只用这些目录的 val 任务划 val(默认 = 全部 dirs)。2026-10-19(审查 review_closedloop F11):
|
||||
DAgger 目录的任务起点带速度、来自策略状态,不该混进预训练的 val;train.py / apg 传主数据目录,DAgger 样本全部算 train。"""
|
||||
if isinstance(dirs, str): dirs = [dirs]
|
||||
xs, ys, vs, ss, ts = [], [], [], [], []
|
||||
metas = [read_meta(d) for d in dirs]
|
||||
sc = {float(m['act_scale']) for m in metas}
|
||||
if len(sc) > 1: raise SystemExit(f'样本目录的 act_scale 不一致 {sorted(sc)}({dirs}):DAgger 样本须与主数据同一版打包')
|
||||
self.act_scale = sc.pop(); self.data_ver = int(metas[0].get('data_ver', 1)) if metas else 1
|
||||
for d in dirs:
|
||||
for fx in sorted(glob.glob(os.path.join(d, '*.X.npy'))):
|
||||
b = fx[:-6]
|
||||
@@ -27,7 +50,7 @@ class Samples:
|
||||
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'}
|
||||
val_tid = {t['tid'] for t in load_tasks(val_dirs if val_dirs is not None else dirs) 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]
|
||||
@@ -35,9 +58,43 @@ class Samples:
|
||||
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):
|
||||
def batch(self, bs, gen=None, aug=None):
|
||||
"""aug:训练配置(train 段),按 aug_pre_drop / aug_pre_sigma / aug_dq_sigma 做条件输入增广(见 augment);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]
|
||||
X, Y = self.X[i], self.Y[i]
|
||||
if aug: X, Y = self.augment(X.clone(), Y.clone(), aug, gen)
|
||||
return X, Y, self.V[i], self.SG[i]
|
||||
|
||||
def augment(self, X, Y, cfg, gen=None):
|
||||
"""条件输入增广(2026-10-19,审查 review_data_actions C3 / review_model_train P1-4:前缀与 dq 的 copycat 捷径)。
|
||||
训练集里 r=3 时要执行的 Y[3:6] 几乎就是前缀第 3 步的延续,dq 又永远是老师的干净速度,模型容易学成「照抄上一块」,
|
||||
闭环里自己的误差经前缀 / dq 原样带回。三种扰动,标签都保持自洽:
|
||||
- 前缀丢弃(aug_pre_drop):r>0 的行以该概率清空前缀与掩码 → 变成 r=0 样本。Y 本来就从当前 q 起算,r=0 时标签不变;
|
||||
- 前缀噪声(aug_pre_sigma,STEP 为单位):r>0 的行一半加噪声 ε。前缀 = 已提交、一定会执行的步,所以标签前 r 步同样 +ε,
|
||||
第 r 步减去 Σε 把位置拉回老师轨迹(亏空在下一步补齐,与增广收回同理);
|
||||
- dq 噪声(aug_dq_sigma,STEP 为单位):一半的行给速度特征加噪声,状态与标签不变。"""
|
||||
B, dev = X.shape[0], X.device
|
||||
rnd = lambda *s: torch.rand(*s, device=dev, generator=gen)
|
||||
rn = lambda *s: torch.randn(*s, device=dev, generator=gen)
|
||||
pl = X[:, MASK_SL].sum(-1).round().long()
|
||||
pd = float(cfg.get('aug_pre_drop', 0.0))
|
||||
if pd > 0:
|
||||
m = (rnd(B) < pd) & (pl > 0)
|
||||
X[:, 47:68] = torch.where(m[:, None], torch.zeros_like(X[:, 47:68]), X[:, 47:68]); pl = torch.where(m, torch.zeros_like(pl), pl)
|
||||
ps = float(cfg.get('aug_pre_sigma', 0.0))
|
||||
if ps > 0:
|
||||
m = (rnd(B) < 0.5) & (pl > 0)
|
||||
km = (torch.arange(3, device=dev)[None] < pl[:, None]) & m[:, None] # [B,3] 前缀有效步
|
||||
eps = rn(B, 3, 6) * ps * km[..., None].to(X.dtype) # STEP 为单位
|
||||
X[:, PREFIX_SL] = X[:, PREFIX_SL] + eps.reshape(B, 18)
|
||||
Y[:, :3] = Y[:, :3] + eps / self.act_scale # Y 以 act_scale·STEP 为单位
|
||||
Y[torch.arange(B, device=dev), pl.clamp(max=Y.shape[1] - 1)] -= eps.sum(1) / self.act_scale
|
||||
Y.clamp_(-1, 1)
|
||||
ds = float(cfg.get('aug_dq_sigma', 0.0))
|
||||
if ds > 0:
|
||||
m = (rnd(B) < 0.5).to(X.dtype)
|
||||
X[:, DQ_SL] = X[:, DQ_SL] + rn(B, 6) * ds * m[:, None]
|
||||
return X, Y
|
||||
|
||||
|
||||
def main():
|
||||
@@ -46,7 +103,9 @@ def main():
|
||||
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}
|
||||
meta = read_meta(a.index)
|
||||
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,
|
||||
'data_ver': int(meta.get('data_ver', 1)), 'act_scale': float(meta.get('act_scale', 1.0)), 'sig': meta.get('sig')}
|
||||
json.dump(info, open(os.path.join(a.index, 'index.json'), 'w'), indent=1)
|
||||
print(json.dumps(info, indent=1))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
其余项读已有文件(缺文件记为 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/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(24000 步 batch 1024 ≈ 1.4 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 探针)
|
||||
@@ -37,8 +37,8 @@ def main():
|
||||
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()
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
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']
|
||||
@@ -97,7 +97,7 @@ def main():
|
||||
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)')
|
||||
add('R10 训练吞吐(样本/s)', sps, sps is not None and sps >= 5000, '≥ 5000(24000 步 ≈ 1.4 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',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""DLS 解析雅可比对拍 + 延迟(python -m ml.eval.check_dls)"""
|
||||
import time
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.sim.closed_loop import dls_steps
|
||||
|
||||
|
||||
def main():
|
||||
dev = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
kin = Kin(device=dev)
|
||||
g = torch.Generator(device=dev); g.manual_seed(0)
|
||||
B = 64
|
||||
lo, hi = kin.lo + 20, kin.hi - 20
|
||||
q = lo + (hi - lo) * torch.rand(B, 6, device=dev, generator=g)
|
||||
xf = kin.cum_xf(q)
|
||||
p = kin.fk(q, xf)
|
||||
T = p + 6 * torch.randn(B, 3, device=dev, generator=g)
|
||||
R = torch.zeros(B, 9, device=dev)
|
||||
a = dls_steps(kin, q, T, R, n=3, analytic=True)
|
||||
b = dls_steps(kin, q, T, R, n=3, analytic=False)
|
||||
d = (a - b).abs()
|
||||
print('pos-only max|Δdq| %.6f° mean %.6f° rel %.4f' % (d.max().item(), d.mean().item(), (d.norm() / b.norm()).item()))
|
||||
# 带姿态目标:绕随机轴 ±8°
|
||||
axis = torch.randn(B, 3, device=dev, generator=g); axis = axis / axis.norm(dim=-1, keepdim=True)
|
||||
ang = (torch.rand(B, 1, device=dev, generator=g) * 16 - 8) * torch.pi / 180
|
||||
Kx = torch.zeros(B, 3, 3, device=dev)
|
||||
Kx[:, 0, 1] = -axis[:, 2]; Kx[:, 0, 2] = axis[:, 1]; Kx[:, 1, 0] = axis[:, 2]
|
||||
Kx[:, 1, 2] = -axis[:, 0]; Kx[:, 2, 0] = -axis[:, 1]; Kx[:, 2, 1] = axis[:, 0]
|
||||
I = torch.eye(3, device=dev)
|
||||
Rd = I + torch.sin(ang)[..., None] * Kx + (1 - torch.cos(ang))[..., None] * (Kx @ Kx)
|
||||
Rt = kin.orient(q, xf) @ Rd
|
||||
R9 = Rt.reshape(B, 9)
|
||||
a = dls_steps(kin, q, T, R9, n=3, analytic=True)
|
||||
b = dls_steps(kin, q, T, R9, n=3, analytic=False)
|
||||
d = (a - b).abs()
|
||||
print('pos+rot max|Δdq| %.6f° mean %.6f° rel %.4f' % (d.max().item(), d.mean().item(), (d.norm() / b.norm()).item()))
|
||||
|
||||
for n_ in (1, 3):
|
||||
for Bn in (1, 256):
|
||||
qq = q[:Bn]; TT = T[:Bn]; RR = R9[:Bn]
|
||||
for an in (False, True):
|
||||
for _ in range(3):
|
||||
dls_steps(kin, qq, TT, RR, n=n_, analytic=an)
|
||||
torch.cuda.synchronize() if dev == 'cuda' else None
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(20):
|
||||
dls_steps(kin, qq, TT, RR, n=n_, analytic=an)
|
||||
torch.cuda.synchronize() if dev == 'cuda' else None
|
||||
print('n=%d B=%3d analytic=%d %.2f ms' % (n_, Bn, an, (time.perf_counter() - t0) / 20 * 1e3))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+3
-2
@@ -60,14 +60,15 @@ def measure(d, a, pool):
|
||||
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 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()
|
||||
# 2026-10-19:Y 以 act_scale·STEP 为单位(DATA_VER 2 起 1.25),乘回 act_scale 让判据仍是「STEP 的比例」,与旧数据可比
|
||||
Ya, Yb = d.Y[qi][qf[half], :3].float() * d.act_scale, d.Y[pi][jj[half], :3].float() * d.act_scale
|
||||
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()
|
||||
Yq, Yp = d.Y[qi][m, :3].float() * d.act_scale, d.Y[pi][best_j[m], :3].float() * d.act_scale
|
||||
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(),
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python
|
||||
"""闭环失败定位(只读,手动运行,n=256 约 35–50 分钟,显存约 3–6 GB)。
|
||||
PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json
|
||||
任务 = val 集前 n 题;老师轨迹按任务 id 从 ml/data/shard_*.jsonl 取(取不到的题不进 H 段,数量记在 H_tasks_missing_teacher)。
|
||||
模型经 ckpt.load_policy 载入(有 EMA 用 EMA;act_scale 随 checkpoint,旧 checkpoint = 1.0)。
|
||||
|
||||
段落:
|
||||
A. 特征口径:val 任务起点(非增广、老师状态)上,Python features() 与数据集 X 的逐组最大差 / 平均差。
|
||||
B. 同一噪声下两份特征得到的动作差(16 步末端关节差,度)。
|
||||
C. 旧部署口径(1 段采样、一致性选择、子目标旧规则 timeout)的失败形态:首撞时刻、碰撞步数、首撞组对、最小目标误差、
|
||||
目标误差进度比、开安全层的到达 / 碰撞 / 刹停。到达 = 时限内到达,碰撞只计时限内(2026-10-19 起)。
|
||||
H(2026-10-19,三份审查 review_closedloop / review_model_train / review_data_actions 的决定性实验):
|
||||
H1 老师 oracle 走完整 rollout()(closed_loop.TeacherOracle;select='first'、use_sub=False):
|
||||
H1_oracle:额外含 oracle 字段——DLS 接管前与老师轨迹的最大关节偏差、到达 / 碰撞、完成时刻 − 老师时长;
|
||||
H1_oracle_noise_<σ>(σ = 0.05 / 0.1 / 0.2,STEP 为单位,每次规划重采):闭环对残差噪声的敏感度。
|
||||
H2 前缀消融:H2_open_loop(r=3 的 val 行,前缀 47:68 原样 vs 置零,执行窗口 3..5 的误差);
|
||||
H2_replan_empty_s{1,4}:队列空了才规划(每次 r=0、提交 3 步),其余同部署。
|
||||
H3 子目标:H3_headsub_cross_s{1,4}(子目标头 + 越过顶点规则 = 新部署默认)、H3_headsub_timeout_s{1,4}(旧规则 = 旧部署)、
|
||||
H3_teachersub_cross_s{1,4} / H3_teachersub_timeout_s{1,4}(老师真实途经点全表)、H3_nosub_s{1,4}(不用子目标)。
|
||||
H4 指标(上面每个闭环变体都输出同一组键,见 metrics());H4_open_loop:按前缀长度 r=0..3 的执行窗口误差。
|
||||
_s1 / _s4 = MeanFlow 1 段(部署)/ 4 段采样。"""
|
||||
import argparse, json, math, os
|
||||
|
||||
import torch
|
||||
|
||||
from ml.common.ckpt import load_policy
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.common.features import features, STEP, H
|
||||
from ml.data import Samples, load_tasks, read_meta
|
||||
from ml.sim.closed_loop import rollout, goal_err, DT, TeacherOracle
|
||||
from ml.eval.replay_teacher import load_teacher, pad_traj, oracle_raw, oracle_summary
|
||||
|
||||
# 83 维特征分组(与 features.py 拼接顺序一致)
|
||||
GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 24), ('Rq', 24, 30), ('T', 30, 33),
|
||||
('R', 33, 39), ('T-p', 39, 42), ('T-p_clamp', 42, 45), ('mode', 45, 47), ('prefix', 47, 65), ('mask', 65, 68),
|
||||
('sub', 68, 74), ('sub_flag', 74, 75), ('gaps', 75, 83)]
|
||||
|
||||
|
||||
PROG_T = (0.5, 1.0, 2.0) # 目标误差 / 起点误差 的取样时刻(s)
|
||||
K1, K2 = int(round(1.0 / DT)), int(round(2.0 / DT)) # 1 s / 2 s 对应的步数
|
||||
|
||||
|
||||
def pct(v, p):
|
||||
v = sorted(x for x in v if x is not None and x == x)
|
||||
return v[min(len(v) - 1, int(len(v) * p))] if v else None
|
||||
|
||||
|
||||
def qt(t, p):
|
||||
t = t[torch.isfinite(t)]
|
||||
return round(float(t.kthvalue(max(1, min(len(t), int(math.ceil(p * len(t)))))).values), 4) if len(t) else None
|
||||
|
||||
|
||||
def rnd(x, n=4):
|
||||
if isinstance(x, list): return [rnd(v, n) for v in x]
|
||||
return None if x is None or x != x else round(float(x), n)
|
||||
|
||||
|
||||
def pad_len(tr, n):
|
||||
"""轨迹 [B,T,6] 末帧重复补到至少 n 帧(到达后冻结,补帧与冻结等价)。"""
|
||||
if tr.shape[1] >= n: return tr
|
||||
return torch.cat([tr, tr[:, -1:].expand(-1, n - tr.shape[1], -1)], 1)
|
||||
|
||||
|
||||
def batch_stats(kin, ch, per, TR):
|
||||
"""一批的逐题量(CPU)。窗口:幅度 / 净位移 / TCP 用前 1 s,变号率用前 2 s 的 3 步块。"""
|
||||
dev = TR.device
|
||||
stp = torch.tensor(STEP, device=dev)
|
||||
tr = pad_len(per['traj'].to(dev), K2 + 1); te = pad_len(TR, K2 + 1)
|
||||
B = tr.shape[0]
|
||||
amp = lambda x: (x[:, 1:K1 + 1] - x[:, :K1]).abs().sum(1) # [B,6] 前 1 s 关节行程
|
||||
net = lambda x: (x[:, K1] - x[:, 0]).abs() # [B,6] 前 1 s 净位移
|
||||
def tcp(x):
|
||||
p = kin.fk(x[:, :K1 + 1].reshape(-1, 6)).view(B, K1 + 1, 3)
|
||||
return (p[:, 1:] - p[:, :-1]).norm(dim=-1).sum(1), (p[:, -1] - p[:, 0]).norm(dim=-1)
|
||||
def flips(x):
|
||||
idx = torch.arange(0, K2 + 1, 3, device=dev)
|
||||
b = x[:, idx[1:]] - x[:, idx[:-1]] # [B,nb,6] 3 步块增量
|
||||
big = b.abs() > 0.1 * stp
|
||||
v = big[:, 1:] & big[:, :-1]
|
||||
f = v & (torch.sign(b[:, 1:]) != torch.sign(b[:, :-1]))
|
||||
return f.sum(1).float(), v.sum(1).float()
|
||||
tl_p, tn_p = tcp(tr); tl_t, tn_t = tcp(te)
|
||||
fp_, vp_ = flips(tr); ft_, vt_ = flips(te)
|
||||
ex = torch.tensor([t.get('exec_s') or float('nan') for t in ch])
|
||||
return {'ok': per['ok'], 'col': per['col'] > 0, 'isD': torch.tensor([t.get('cat') == 'D' for t in ch]),
|
||||
'pose': torch.tensor([bool(t.get('R')) for t in ch]), 'key': [t.get('cat', '?') + '/' + t.get('mode', '?') for t in ch],
|
||||
'r2': per['goal_ratio_2s'], 'err': per['err'], 'first_hit': per['first_hit'], 'brakes': per['brakes'],
|
||||
'tratio': per['t_done'] / ex,
|
||||
'amp_p': amp(tr).cpu(), 'amp_t': amp(te).cpu(), 'net_p': net(tr).cpu(), 'net_t': net(te).cpu(),
|
||||
'tl_p': tl_p.cpu(), 'tl_t': tl_t.cpu(), 'tn_p': tn_p.cpu(), 'tn_t': tn_t.cpu(),
|
||||
'fl_p': fp_.cpu(), 'fv_p': vp_.cpu(), 'fl_t': ft_.cpu(), 'fv_t': vt_.cpu()}
|
||||
|
||||
|
||||
def metrics(stats):
|
||||
"""H4 指标(每个闭环变体同一组键):
|
||||
reach / collision:cat≠D、时限内(D 类目标不可达,单列在 D);
|
||||
goal_ratio_2s_p50:2 s 时目标误差 / 起点误差;end_goal_err_mm_p50;first_hit_s_p50(撞了的题);t_done_over_teacher_p50(到达的题);
|
||||
joint_amp_ratio[6]:前 1 s 各轴关节行程 Σ|Δq| 策略 / 老师(全体题合计;J6 只算 pose 题,pos 题老师冻结 J6);
|
||||
joint_net_ratio[6]:前 1 s 各轴净位移 |q(1s) − q(0)| 策略 / 老师(同上);j6_pos_amp_deg_p50:pos 题策略 J6 前 1 s 行程(度,老师为 0);
|
||||
tcp_path_ratio_p50 / tcp_net_ratio_p50:前 1 s TCP 路程 / 净位移 策略 / 老师(逐题比,老师 > 1 mm 的题);
|
||||
sign_flip_rate[6] / sign_flip_rate_teacher[6]:前 2 s 相邻 3 步块增量变号的比例(两块都 > 0.1·STEP 才计);
|
||||
by_cat_mode:各 cat/mode 的 n、reach、collision、goal_ratio_2s_p50(不含 D);D:n、collision、end_goal_err_mm_p50、goal_ratio_2s_p50。"""
|
||||
c = {k: (sum((s[k] for s in stats), []) if isinstance(stats[0][k], list) else torch.cat([s[k] for s in stats])) for k in stats[0]}
|
||||
keys = c['key']
|
||||
m = ~c['isD']; pose = c['pose']
|
||||
def jr(a, b):
|
||||
out = []
|
||||
for j in range(6):
|
||||
sel = m & pose if j == 5 else m
|
||||
den = float(b[sel, j].sum()); out.append(float(a[sel, j].sum()) / den if den > 1e-6 else None)
|
||||
return rnd(out)
|
||||
tl_ok = m & (c['tl_t'] > 1); tn_ok = m & (c['tn_t'] > 1)
|
||||
out = {'n': int(m.sum()),
|
||||
'reach': rnd(c['ok'][m].float().mean()) if m.any() else None,
|
||||
'collision': rnd(c['col'][m].float().mean()) if m.any() else None,
|
||||
'goal_ratio_2s_p50': qt(c['r2'][m], .5), 'end_goal_err_mm_p50': qt(c['err'][m], .5),
|
||||
'first_hit_s_p50': qt(c['first_hit'][m], .5), 't_done_over_teacher_p50': qt(c['tratio'][m & c['ok']], .5),
|
||||
'brakes_p50': qt(c['brakes'][m], .5),
|
||||
'joint_amp_ratio': jr(c['amp_p'], c['amp_t']), 'joint_net_ratio': jr(c['net_p'], c['net_t']),
|
||||
'j6_pos_amp_deg_p50': qt(c['amp_p'][m & ~pose, 5], .5),
|
||||
'tcp_path_ratio_p50': qt(c['tl_p'][tl_ok] / c['tl_t'][tl_ok], .5), 'tcp_net_ratio_p50': qt(c['tn_p'][tn_ok] / c['tn_t'][tn_ok], .5),
|
||||
'sign_flip_rate': rnd((c['fl_p'][m].sum(0) / c['fv_p'][m].sum(0).clamp(min=1)).tolist()),
|
||||
'sign_flip_rate_teacher': rnd((c['fl_t'][m].sum(0) / c['fv_t'][m].sum(0).clamp(min=1)).tolist())}
|
||||
by = {}
|
||||
for i, k in enumerate(keys):
|
||||
if not bool(m[i]): continue
|
||||
by.setdefault(k, []).append(i)
|
||||
out['by_cat_mode'] = {k: {'n': len(ix), 'reach': rnd(c['ok'][ix].float().mean()), 'collision': rnd(c['col'][ix].float().mean()),
|
||||
'goal_ratio_2s_p50': qt(c['r2'][ix], .5)} for k, ix in sorted(by.items())}
|
||||
d = c['isD']
|
||||
out['D'] = {'n': int(d.sum()), 'collision': rnd(c['col'][d].float().mean()) if d.any() else None,
|
||||
'end_goal_err_mm_p50': qt(c['err'][d], .5), 'goal_ratio_2s_p50': qt(c['r2'][d], .5)}
|
||||
return out
|
||||
|
||||
|
||||
def teacher_subs(recs, device):
|
||||
"""老师真实途经点全表(path 去掉起终点)→ (subs [B,M,6], n [B]),供 rollout(sub_override=...)。"""
|
||||
vs = [r['path'][1:-1] for r in recs]
|
||||
M = max(1, max(len(v) for v in vs))
|
||||
subs = torch.zeros(len(vs), M, 6, device=device); n = torch.zeros(len(vs), dtype=torch.long, device=device)
|
||||
for i, v in enumerate(vs):
|
||||
if v:
|
||||
x = torch.tensor(v, dtype=torch.float32, device=device); subs[i, :len(v)] = x; subs[i, len(v):] = x[-1]; n[i] = len(v)
|
||||
return subs, n
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||
ap.add_argument('--shards', default='ml/data/shard_*.jsonl')
|
||||
ap.add_argument('--n', type=int, default=256); ap.add_argument('--bs', type=int, default=64)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default='ml/runs/diag_cl.json')
|
||||
a = ap.parse_args()
|
||||
dev = a.device
|
||||
model, ck = load_policy(a.ckpt, dev)
|
||||
kin = Kin(device=dev)
|
||||
astep = torch.tensor(STEP, device=dev) * model.act_scale # 动作 → 度
|
||||
data_scale = float(read_meta(a.samples).get('act_scale', 1.0))
|
||||
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'weights': 'ema' if ck.get('ema') is not None else 'model',
|
||||
'act_scale_ckpt': model.act_scale, 'act_scale_data': data_scale}
|
||||
tasks = [t for t in load_tasks([a.samples]) if t['split'] == 'val'][:a.n]
|
||||
by_tid = {t['tid']: t for t in tasks}
|
||||
def save():
|
||||
s = json.dumps(out, indent=1, ensure_ascii=False)
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
open(a.out + '.tmp', 'w', encoding='utf-8').write(s); os.replace(a.out + '.tmp', a.out)
|
||||
return s
|
||||
|
||||
# ---------- A / B:起点特征口径 ----------
|
||||
vd = Samples(a.samples, device=dev, split='val')
|
||||
tid = vd.TID.long()
|
||||
first = torch.ones_like(tid, dtype=torch.bool); first[1:] = tid[1:] != tid[:-1] # 每题第一行 = t=0 样本
|
||||
idx = [i for i in first.nonzero().flatten().tolist() if int(tid[i]) in by_tid]
|
||||
X0 = vd.X[idx].float()
|
||||
tk = [by_tid[int(tid[i])] for i in idx]
|
||||
q0 = torch.tensor([t['q0'] for t in tk], dtype=torch.float32, device=dev)
|
||||
qx = torch.atan2(X0[:, 0:12:2], X0[:, 1:12:2]) * 180 / torch.pi
|
||||
dqx = torch.remainder(qx - q0 + 180, 360) - 180
|
||||
clean = dqx.abs().max(-1).values < 1e-2 # 未被噪声增广的起点
|
||||
out['A_rows'] = int(clean.sum()); out['A_rows_aug_skipped'] = int((~clean).sum())
|
||||
if clean.any():
|
||||
Xc = X0[clean]; qc = q0[clean]; tc = [t for t, c in zip(tk, clean.tolist()) if c]
|
||||
T = torch.tensor([t['T'] for t in tc], dtype=torch.float32, device=dev)
|
||||
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tc], dtype=torch.float32, device=dev)
|
||||
B = qc.shape[0]
|
||||
qpc = torch.tensor([t.get('qp0') or t['q0'] for t in tc], dtype=torch.float32, device=dev)
|
||||
pre = Xc[:, 47:65].view(B, 3, 6) * torch.tensor(STEP, device=dev) # 前缀 / 子目标直接取数据集里的,只比状态相关部分
|
||||
pl = Xc[:, 65:68].sum(-1).round()
|
||||
sf = Xc[:, 74]
|
||||
sub = qc + Xc[:, 68:74] * 180
|
||||
Fp = features(kin, qc, qpc, T, R, pre, pl, sub, sf)
|
||||
out['A_maxdiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().max().item() for g, s, e in GROUPS}
|
||||
out['A_meandiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().mean().item() for g, s, e in GROUPS}
|
||||
with torch.no_grad():
|
||||
z = torch.randn(B * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(7))
|
||||
a1, _, _ = model.sample_k(Xc, 8, z); a2, _, _ = model.sample_k(Fp, 8, z)
|
||||
d = ((a1.clamp(-1, 1) - a2.clamp(-1, 1)) * astep).sum(2).abs().max(-1).values # [B,K] 16 步末端关节差(度)
|
||||
mag = (a1.clamp(-1, 1) * astep).sum(2).abs().max(-1).values
|
||||
out['B_act_diff_deg'] = {'mean': d.mean().item(), 'p90': d.flatten().kthvalue(max(1, int(0.9 * d.numel()))).values.item(),
|
||||
'ref_motion_deg_mean': mag.mean().item()}
|
||||
|
||||
# ---------- H4 / H2 开环:执行窗口误差(val 行,老师状态) ----------
|
||||
# 闭环稳态 r=3,真正执行的是 act[r:r+3];原来的 ol_fk_mm 是 16 步终点误差,前缀那几步几乎不会错,看不出执行窗口。
|
||||
with torch.no_grad():
|
||||
pl_all = vd.X[:, 65:68].sum(-1).round().long()
|
||||
gsel = torch.Generator(device=dev); gsel.manual_seed(321)
|
||||
def rows_with(r, n=1024):
|
||||
ix = (pl_all == r).nonzero().flatten()
|
||||
return ix[torch.randperm(len(ix), device=dev, generator=gsel)[:n]] if len(ix) else ix
|
||||
def win_err(X, Y, r, steps, k0=None):
|
||||
k0 = r if k0 is None else k0
|
||||
z = torch.randn(X.shape[0] * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(11))
|
||||
act, _, _ = model.sample_k(X, 8, z, steps=steps); act = act.float().clamp(-1, 1)
|
||||
e = ((act[:, :, k0:k0 + 3] - Y[:, None, k0:k0 + 3]) * astep).sum(2).abs() # [N,K,6] 窗口累计误差(度)
|
||||
emax = e.max(-1).values.flatten()
|
||||
res = {'n_rows': int(X.shape[0]), 'deg_mean': rnd(emax.mean()), 'deg_p90': qt(emax, .9), 'per_joint_deg_mean': rnd(e.mean((0, 1)).tolist())}
|
||||
if r > 0 and k0 == r:
|
||||
ce = ((act[:, :, :r] - Y[:, None, :r]) * astep).abs().max(-1).values.max(-1).values.flatten()
|
||||
res['prefix_copy_err_deg_mean'] = rnd(ce.mean())
|
||||
return res
|
||||
ol4, ol2 = {}, {}
|
||||
for steps in (1, 4):
|
||||
for r in range(4):
|
||||
ix = rows_with(r)
|
||||
if len(ix): ol4['s%d_r%d' % (steps, r)] = win_err(vd.X[ix].float(), vd.Y[ix].float(), r, steps)
|
||||
ix = rows_with(3)
|
||||
if len(ix):
|
||||
X3 = vd.X[ix].float(); Y3 = vd.Y[ix].float(); X3z = X3.clone(); X3z[:, 47:68] = 0
|
||||
ol2['s%d_prefix_orig' % steps] = win_err(X3, Y3, 3, steps)
|
||||
ol2['s%d_prefix_zeroed' % steps] = win_err(X3z, Y3, 0, steps, k0=3)
|
||||
out['H4_open_loop'] = ol4; out['H2_open_loop'] = ol2
|
||||
del vd
|
||||
if dev.startswith('cuda'): torch.cuda.empty_cache()
|
||||
|
||||
# ---------- C:旧部署口径的闭环失败形态(子目标旧规则,便于与前几轮对比) ----------
|
||||
first_t, col_steps, pairs, min_err, reach = [], [], {}, [], 0
|
||||
prog = {ts: [] for ts in PROG_T}
|
||||
reach_s, brakes_s, col_s = 0, [], 0
|
||||
for i in range(0, len(tasks), a.bs):
|
||||
ch = tasks[i:i + a.bs]
|
||||
with torch.no_grad():
|
||||
_, per = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True, sub_rule='timeout')
|
||||
tr = per['traj'].to(dev) # [B,Tn,6]
|
||||
B, Tn, _ = tr.shape
|
||||
flat = tr.reshape(-1, 6) # 分块 ≤1024 点(同 dagger/collect.py 的显存修复)
|
||||
gm = torch.cat([kin.gaps(flat[s:s + 1024], 0.5)['gm'] for s in range(0, flat.shape[0], 1024)]).view(B, Tn, 49)
|
||||
inlim = (torch.arange(Tn, device=dev)[None] * DT) <= per['limit'].to(dev)[:, None] + 1e-6
|
||||
hit = (gm.min(-1).values <= 0) & inlim # [B,Tn] 只计时限内
|
||||
Tt = torch.tensor([t['T'] for t in ch], dtype=torch.float32, device=dev)
|
||||
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in ch], dtype=torch.float32, device=dev)
|
||||
e, _ = goal_err(kin, tr.reshape(-1, 6), Tt.repeat_interleave(Tn, 0), Rt.repeat_interleave(Tn, 0))
|
||||
e = e.view(B, Tn)
|
||||
for b in range(B):
|
||||
for ts in PROG_T:
|
||||
k = min(Tn - 1, int(round(ts / DT))); prog[ts].append((e[b, k] / e[b, 0].clamp(min=1e-3)).item())
|
||||
for b in range(B):
|
||||
if per['ok'][b]: reach += 1
|
||||
min_err.append(e[b].min().item()); col_steps.append(int(per['col'][b]))
|
||||
h = hit[b].nonzero().flatten()
|
||||
if len(h):
|
||||
k = int(h[0]); first_t.append(k * DT)
|
||||
pr = int(gm[b, k].argmin()); key = '%d-%d' % (pr // 7, pr % 7); pairs[key] = pairs.get(key, 0) + 1
|
||||
rs, ps = rollout(model, kin, ch, dev, K=8, safety=True, seed=i, sub_rule='timeout')
|
||||
reach_s += int(ps['ok'].sum()); col_s += int((ps['col'] > 0).sum()); brakes_s += ps['brakes'].tolist()
|
||||
n = len(tasks)
|
||||
out['C'] = {'n': n, 'reach': reach / n, 'collided': len(first_t) / n,
|
||||
'first_hit_s': {'p10': pct(first_t, .1), 'p50': pct(first_t, .5), 'p90': pct(first_t, .9),
|
||||
'within_0.1s': sum(t <= 0.1 for t in first_t) / max(1, len(first_t))},
|
||||
'col_steps': {'p50': pct(col_steps, .5), 'p90': pct(col_steps, .9)},
|
||||
'first_hit_pair(group a-b)': dict(sorted(pairs.items(), key=lambda kv: -kv[1])[:8]),
|
||||
'min_goal_err_mm': {'p10': pct(min_err, .1), 'p50': pct(min_err, .5), 'p90': pct(min_err, .9)},
|
||||
'safety_on': {'reach': reach_s / n, 'collided': col_s / n, 'brakes_p50': pct(brakes_s, .5)}}
|
||||
out['C']['goal_err_ratio_p50'] = {'%.1fs' % ts: pct(v, .5) for ts, v in prog.items()} # <1 = 在靠近目标
|
||||
print(save(), flush=True)
|
||||
|
||||
# ---------- H:闭环变体(同一批 val 题,老师轨迹按 id 取) ----------
|
||||
teach = load_teacher(a.shards, [t['id'] for t in tasks])
|
||||
tH = [t for t in tasks if t['id'] in teach]
|
||||
out['H_tasks'] = len(tH); out['H_tasks_missing_teacher'] = len(tasks) - len(tH)
|
||||
|
||||
def variant(oracle_sigma=None, teacher_sub=False, **kw):
|
||||
stats, raws = [], []
|
||||
for i in range(0, len(tH), a.bs):
|
||||
ch = tH[i:i + a.bs]; recs = [teach[t['id']] for t in ch]
|
||||
TR = pad_traj([r['traj'] for r in recs], dev)
|
||||
kw2 = dict(kw)
|
||||
if oracle_sigma is not None:
|
||||
kw2.update(oracle=TeacherOracle(TR, act_scale=data_scale, sigma=oracle_sigma), select='first', use_sub=False)
|
||||
if teacher_sub: kw2['sub_override'] = teacher_subs(recs, dev)
|
||||
with torch.no_grad():
|
||||
_, per = rollout(None if oracle_sigma is not None else model, kin, ch, dev, K=8, seed=i, return_traj=True, **kw2)
|
||||
stats.append(batch_stats(kin, ch, per, TR))
|
||||
if oracle_sigma is not None: raws.append(oracle_raw(per, TR, [len(r['traj']) for r in recs], ch))
|
||||
del per
|
||||
res = metrics(stats)
|
||||
if oracle_sigma is not None and oracle_sigma == 0: res['oracle'] = oracle_summary(raws)
|
||||
if dev.startswith('cuda'): torch.cuda.empty_cache()
|
||||
return res
|
||||
|
||||
plan = [('H1_oracle', dict(oracle_sigma=0.0))]
|
||||
plan += [('H1_oracle_noise_%g' % s, dict(oracle_sigma=s)) for s in (0.05, 0.1, 0.2)]
|
||||
for st in (1, 4):
|
||||
plan += [('H3_headsub_cross_s%d' % st, dict(steps=st, sub_rule='cross')),
|
||||
('H3_headsub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout')),
|
||||
('H3_teachersub_cross_s%d' % st, dict(steps=st, sub_rule='cross', teacher_sub=True)),
|
||||
('H3_teachersub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout', teacher_sub=True)),
|
||||
('H3_nosub_s%d' % st, dict(steps=st, use_sub=False)),
|
||||
('H2_replan_empty_s%d' % st, dict(steps=st, sub_rule='cross', replan='empty'))]
|
||||
for name, kw in plan:
|
||||
out[name] = variant(**kw)
|
||||
save(); print('[diag] %s 完成' % name, flush=True)
|
||||
print(save(), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python
|
||||
"""闭环碰撞定位:第一次碰撞发生在第几秒、碰撞时最近的是哪两条连杆(J几-J几)。
|
||||
|
||||
用法(仓库根目录):
|
||||
PYTHONUTF8=1 python -m ml.eval.diag_collision --ckpt ml/runs/base/last.pt --n 512 --out ml/runs/diag_collision.json
|
||||
|
||||
口径与 closed_loop.rollout 一致:每步同时检查当前状态 q 与相邻状态中点 mid 的间隙(cap=0.5 mm);
|
||||
间隙组对索引 = gA*7+gB(gA<gB,g 为连杆组 0..5 → J1..J6)。
|
||||
"""
|
||||
import argparse, json, math, os, sys, time
|
||||
from collections import Counter
|
||||
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.data import load_tasks
|
||||
from ml.model.policy import build
|
||||
from ml.sim.closed_loop import DT, rollout
|
||||
|
||||
|
||||
def pair_names():
|
||||
# kin.json 有 7 个连杆组:g=0 是固定基座/立柱,g=1..6 是 J1..J6
|
||||
def nm(g):
|
||||
return '基座' if g == 0 else f'J{g}'
|
||||
return {a * 7 + b: f'{nm(a)}-{nm(b)}' for a in range(7) for b in range(a + 1, 7)}
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def replay_first_hits(path, out=None):
|
||||
saved = torch.load(path, map_location='cpu', weights_only=True)
|
||||
k32, k64 = Kin(), Kin(dtype=torch.float64)
|
||||
rows, offset = [], 0
|
||||
for _, per, count in saved['batches']:
|
||||
for b in (per['col'] > 0).nonzero().flatten().tolist():
|
||||
it = int(round(float(per['first_hit'][b]) / DT))
|
||||
prev, q = per['traj'][b, it - 1:it + 1]
|
||||
mid = prev + 0.5 * (q - prev)
|
||||
points = torch.stack([per['traj'][b, 0], prev, mid, q])
|
||||
gm32 = k32.gaps(points, 0.5)['gm']
|
||||
gm64 = k64.gaps(points.double(), 0.5)['gm']
|
||||
gap32, gp32 = gm32.min(-1)
|
||||
gap64, gp64 = gm64.min(-1)
|
||||
nseg = max(1, math.ceil(float((q - prev).abs().max()) / 0.01))
|
||||
dense = prev.double() + torch.linspace(0, 1, nseg + 1, dtype=torch.float64)[:, None] * (q.double() - prev.double())
|
||||
dense_gap = torch.cat([k64.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in dense.split(32)])
|
||||
boundary = (~k32.cont) & ((q - k32.lo).abs().lt(1e-5) | (q - k32.hi).abs().lt(1e-5))
|
||||
row = {
|
||||
'i': offset + b, 'task': saved['tasks'][offset + b],
|
||||
'first_hit_s': float(per['first_hit'][b]), 'dls_it': int(per['dls_it'][b]),
|
||||
'point_order': ['q0', 'previous', 'midpoint', 'endpoint'],
|
||||
'gap32_mm': gap32.tolist(), 'gap64_mm': gap64.tolist(),
|
||||
'pair32': gp32.tolist(), 'pair64': gp64.tolist(),
|
||||
'boundary_joints': (boundary.nonzero().flatten() + 1).tolist(),
|
||||
'previous': prev.tolist(), 'endpoint': q.tolist(),
|
||||
'dense_nsegments': nseg, 'dense_gap64_min_mm': float(dense_gap.min()),
|
||||
}
|
||||
if 'plans' in per:
|
||||
plans = per['plans']
|
||||
pi = int(torch.searchsorted(plans['it'], it - 1, right=True)) - 1
|
||||
step = it - 1 - int(plans['it'][pi])
|
||||
ncommit = plans['commit'].shape[2]
|
||||
command = plans['commit'][b, pi, step]
|
||||
cp, cm = plans['points'][b, pi, step], plans['midpoints'][b, pi, step]
|
||||
checked = bool(plans['certified'][b, pi, step])
|
||||
predicted = k32.clamp_box((prev + command)[None])[0]
|
||||
row['certificate'] = {
|
||||
'plan_start_it': int(plans['it'][pi]), 'step': step, 'checked': checked,
|
||||
'endpoint': cp.tolist() if checked else None,
|
||||
'midpoint': cm.tolist() if checked else None,
|
||||
'recorded_gap32_mm': plans['gaps'][b, pi, [step, ncommit + step]].tolist() if checked else None,
|
||||
'replayed_gap64_mm': k64.gaps(torch.stack([cp, cm]).double(), 0.5)['gm'].min(-1).values.tolist() if checked else None,
|
||||
'command_delta': command.tolist(),
|
||||
'clamped_reconstruction_max_abs_deg': float((predicted - q).abs().max()),
|
||||
'certified_vs_executed_max_abs_deg': float((cp - q).abs().max()) if checked else None,
|
||||
}
|
||||
rows.append(row)
|
||||
print('task %d first_hit %.2fs gap32 %s gap64 %s boundary %s' %
|
||||
(row['i'], row['first_hit_s'], row['gap32_mm'], row['gap64_mm'], row['boundary_joints']), flush=True)
|
||||
offset += count
|
||||
result = {
|
||||
'trace': path, 'n': offset, 'reported_collision_tasks': len(rows),
|
||||
'replayed_first_hits32': sum(min(r['gap32_mm'][2:]) <= 0 for r in rows),
|
||||
'replayed_first_hits64': sum(min(r['gap64_mm'][2:]) <= 0 for r in rows),
|
||||
'scope': 'Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.',
|
||||
'rows': rows,
|
||||
}
|
||||
if out:
|
||||
with open(out, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=2, allow_nan=False)
|
||||
print('replayed first hits: %d/%d float32, %d/%d float64' %
|
||||
(result['replayed_first_hits32'], len(rows), result['replayed_first_hits64'], len(rows)), flush=True)
|
||||
return result
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def replay_dense(path, indices, max_step_deg=0.05, device='cpu', out=None):
|
||||
saved = torch.load(path, map_location='cpu', weights_only=True)
|
||||
kin = Kin(device=device, dtype=torch.float64)
|
||||
if saved.get('kin_fp') != kin.fp:
|
||||
raise ValueError('trace/kin fingerprint mismatch')
|
||||
if not indices or min(indices) < 0 or max(indices) >= len(saved['tasks']) or max_step_deg <= 0:
|
||||
raise ValueError('valid task indices and positive max_step_deg required')
|
||||
started = time.perf_counter()
|
||||
rows, offset = [], 0
|
||||
for _, per, count in saved['batches']:
|
||||
for index in indices:
|
||||
if not offset <= index < offset + count:
|
||||
continue
|
||||
b = index - offset
|
||||
nstep = min(per['traj'].shape[1] - 1, math.floor((float(per['limit'][b]) + 1e-6) / DT))
|
||||
if torch.isfinite(per['t_done'][b]):
|
||||
nstep = min(nstep, round(float(per['t_done'][b]) / DT))
|
||||
trajectory = per['traj'][b, :nstep + 1].double()
|
||||
points, segment_ids, fractions = [trajectory[:1]], [torch.tensor([-1])], [torch.zeros(1)]
|
||||
for it in range(nstep):
|
||||
prev, end = trajectory[it:it + 2]
|
||||
nseg = max(2, 2 * math.ceil(float((end - prev).abs().max()) / (2 * max_step_deg)))
|
||||
alpha = torch.arange(1, nseg + 1, dtype=torch.float64) / nseg
|
||||
points.append(prev + alpha[:, None] * (end - prev))
|
||||
segment_ids.append(torch.full((nseg,), it))
|
||||
fractions.append(alpha)
|
||||
points = torch.cat(points)
|
||||
segments, alpha = torch.cat(segment_ids), torch.cat(fractions)
|
||||
print('dense task %d steps %d points %d start' % (index, nstep, len(points)), flush=True)
|
||||
gaps = []
|
||||
for start in range(0, len(points), 64):
|
||||
gaps.append(kin.gaps(points[start:start + 64].to(device), 0.5)['gm'].min(-1).values.cpu())
|
||||
if (start // 64 + 1) % 64 == 0:
|
||||
print('dense task %d points %d/%d wall %.1fs' %
|
||||
(index, min(start + 64, len(points)), len(points), time.perf_counter() - started), flush=True)
|
||||
gaps = torch.cat(gaps)
|
||||
hit = gaps <= 0
|
||||
first = int(hit.nonzero()[0]) if hit.any() else None
|
||||
rows.append({
|
||||
'i': index, 'task': saved['tasks'][index], 'steps': nstep, 'sampled_points': len(points),
|
||||
'initial_gap64_mm': float(gaps[0]), 'min_gap64_mm': float(gaps.min()),
|
||||
'collision_segments': int(segments[hit & (segments >= 0)].unique().numel()),
|
||||
'first_collision_s': max(0.0, float(segments[first] + alpha[first]) * DT) if first is not None else None,
|
||||
})
|
||||
print('dense task %d min_gap %.9f mm collision_segments %d' %
|
||||
(index, rows[-1]['min_gap64_mm'], rows[-1]['collision_segments']), flush=True)
|
||||
offset += count
|
||||
result = {
|
||||
'trace': str(path), 'tasks_sha256': saved.get('tasks_sha256'), 'kin_fp': kin.fp,
|
||||
'indices': indices, 'max_step_deg': max_step_deg, 'dtype': 'float64', 'device': device,
|
||||
'scope': 'Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.',
|
||||
'collision_tasks': sum(r['min_gap64_mm'] <= 0 for r in rows), 'rows': rows,
|
||||
'audit_wall_s': time.perf_counter() - started,
|
||||
}
|
||||
if out:
|
||||
with open(out, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=2, allow_nan=False)
|
||||
return result
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def replay_certificates(path, indices, device='cpu', out=None, repeats=5, extra_segments=()):
|
||||
saved = torch.load(path, map_location='cpu', weights_only=True)
|
||||
kin = Kin(device=device, dtype=torch.float64)
|
||||
if saved.get('kin_fp') != kin.fp:
|
||||
raise ValueError('trace/kin fingerprint mismatch')
|
||||
if not indices or min(indices) < 0 or max(indices) >= len(saved['tasks']) or repeats < 1:
|
||||
raise ValueError('valid task indices and positive repeats required')
|
||||
rows, offset = [], 0
|
||||
for _, per, count in saved['batches']:
|
||||
for index in indices:
|
||||
if not offset <= index < offset + count:
|
||||
continue
|
||||
b = index - offset
|
||||
nstep = min(per['traj'].shape[1] - 1, math.floor((float(per['limit'][b]) + 1e-6) / DT))
|
||||
if torch.isfinite(per['t_done'][b]):
|
||||
nstep = min(nstep, round(float(per['t_done'][b]) / DT))
|
||||
traj = per['traj'][b, :nstep + 1]
|
||||
moving = ((traj[1:] - traj[:-1]).abs().max(-1).values > 1e-6).nonzero().flatten().tolist()
|
||||
if not moving:
|
||||
continue
|
||||
plans = per['plans']
|
||||
nearest, nearest_gap = None, float('inf')
|
||||
for it in moving:
|
||||
pi = int(torch.searchsorted(plans['it'], it, right=True)) - 1
|
||||
step = it - int(plans['it'][pi])
|
||||
if 0 <= step < plans['commit'].shape[2] and plans['certified'][b, pi, step]:
|
||||
gap = float(plans['gaps'][b, pi, [step, plans['commit'].shape[2] + step]].min())
|
||||
if gap < nearest_gap:
|
||||
nearest, nearest_gap = it, gap
|
||||
selected = {moving[0], moving[len(moving) // 2], moving[-1]}
|
||||
if nearest is not None:
|
||||
selected.add(nearest)
|
||||
for task_index, it in extra_segments:
|
||||
if task_index == index:
|
||||
if it not in moving:
|
||||
raise ValueError('extra segment must be a moving segment before deadline or arrival')
|
||||
selected.add(it)
|
||||
for it in sorted(selected):
|
||||
rows.append({'i': index, 'start_it': it, 'a': traj[it].tolist(), 'b': traj[it + 1].tolist()})
|
||||
offset += count
|
||||
if not rows:
|
||||
raise ValueError('no moving segments in selected tasks')
|
||||
a = torch.tensor([r['a'] for r in rows], dtype=torch.float64, device=device)
|
||||
b = torch.tensor([r['b'] for r in rows], dtype=torch.float64, device=device)
|
||||
started = time.perf_counter()
|
||||
print('certificate replay: %d tasks, %d fixed moving segments' % (len(indices), len(rows)), flush=True)
|
||||
summaries = {}
|
||||
for budget in (2, 8, 32):
|
||||
result = kin.certify_segments(a, b, max_queries=budget)
|
||||
summaries[str(budget)] = {key: int(result[key].sum()) for key in ('safe', 'collision', 'unresolved', 'outside_box')}
|
||||
summaries[str(budget)]['queries_p50'] = float(result['queries'].double().quantile(0.5))
|
||||
summaries[str(budget)]['queries_max'] = int(result['queries'].max())
|
||||
for j, row in enumerate(rows):
|
||||
row.setdefault('budgets', {})[str(budget)] = {key: value[j].item() for key, value in result.items()}
|
||||
print('budget %d: %s' % (budget, summaries[str(budget)]), flush=True)
|
||||
latencies = []
|
||||
for j, row in enumerate(rows):
|
||||
aa, bb = a[j:j + 1], b[j:j + 1]
|
||||
discrete = kin.gaps(torch.cat([aa, aa + 0.5 * (bb - aa), bb]), 0.5)['gm'].min(-1).values
|
||||
row['previous_midpoint_endpoint_gap_mm'] = discrete.tolist()
|
||||
nseg = max(2, 2 * math.ceil(float((bb - aa).abs().max()) / 0.02))
|
||||
alpha = torch.linspace(0, 1, nseg + 1, dtype=torch.float64, device=device)
|
||||
points = aa + alpha[:, None] * (bb - aa)
|
||||
dense = torch.cat([kin.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in points.split(64)])
|
||||
row['dense_points'] = len(points)
|
||||
row['dense_min_gap_mm'] = float(dense.min())
|
||||
row['dense_min_fraction'] = float(alpha[dense.argmin()])
|
||||
for _ in range(3):
|
||||
kin.certify_segments(aa, bb)
|
||||
elapsed = []
|
||||
for _ in range(repeats):
|
||||
if a.is_cuda:
|
||||
torch.cuda.synchronize(a.device)
|
||||
t0 = time.perf_counter()
|
||||
result = kin.certify_segments(aa, bb)
|
||||
if a.is_cuda:
|
||||
torch.cuda.synchronize(a.device)
|
||||
elapsed.append((time.perf_counter() - t0) * 1000)
|
||||
row['batch_single_match'] = all(value.item() == row['budgets']['32'][key] for key, value in result.items())
|
||||
row['latency_ms'] = elapsed
|
||||
latencies.extend(elapsed)
|
||||
print('segment %d/%d task %d it %d dense %.6f mm queries %d latency %.2f ms' %
|
||||
(j + 1, len(rows), row['i'], row['start_it'], row['dense_min_gap_mm'],
|
||||
row['budgets']['32']['queries'], sum(elapsed) / len(elapsed)), flush=True)
|
||||
elapsed = torch.tensor(latencies, dtype=torch.float64)
|
||||
result = {
|
||||
'trace': str(path), 'tasks_sha256': saved.get('tasks_sha256'), 'kin_fp': kin.fp,
|
||||
'device': str(device), 'gpu': torch.cuda.get_device_name(a.device) if a.is_cuda else None,
|
||||
'torch_version': str(torch.__version__), 'cpu_threads': torch.get_num_threads(), 'dtype': 'float64',
|
||||
'indices': indices, 'extra_segments': list(extra_segments),
|
||||
'selection': 'First/middle/last moving segment and minimum original certified gap per task; plus explicit extra_segments.',
|
||||
'cap_mm': 20.0, 'eps_mm': 1e-4, 'max_depth': 12, 'dense_max_step_deg': 0.01,
|
||||
'timing': {'scope': 'B=1 certificate only, resident inputs, 3 warmups per segment; synchronized wall time, not policy or scheduling latency.',
|
||||
'repeats_per_segment': repeats, 'calls': len(latencies),
|
||||
'p50_ms': float(elapsed.quantile(0.5)), 'p99_ms': float(elapsed.quantile(0.99)),
|
||||
'max_ms': float(elapsed.max()), 'over_20ms': int((elapsed > 20).sum())},
|
||||
'budgets': summaries,
|
||||
'dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 for r in rows),
|
||||
'accepted_dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 and r['budgets']['32']['safe'] for r in rows),
|
||||
'batch_single_match': all(r['batch_single_match'] for r in rows),
|
||||
'scope': 'Fixed recorded segments, not a new rollout. Dense replay is finite sampling; floating-point certification error is not formally bounded.',
|
||||
'wall_s': time.perf_counter() - started, 'rows': rows,
|
||||
}
|
||||
if out:
|
||||
with open(out, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=2, allow_nan=False)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt')
|
||||
ap.add_argument('--trace')
|
||||
ap.add_argument('--dense_indices', help='逗号分隔的任务下标;回放实际轨迹,不依赖原 col 标记')
|
||||
ap.add_argument('--cert_indices', help='固定实际段的连续认证、加密对照和单段计时')
|
||||
ap.add_argument('--cert_segments', default='', help='额外指定任务号:零基段号,逗号分隔')
|
||||
ap.add_argument('--repeats', type=int, default=5)
|
||||
ap.add_argument('--max_step_deg', type=float, default=0.05)
|
||||
ap.add_argument('--samples', default='ml/data/samples')
|
||||
ap.add_argument('--split', default='val')
|
||||
ap.add_argument('--n', type=int, default=512)
|
||||
ap.add_argument('--bs', type=int, default=64)
|
||||
ap.add_argument('--chunk', type=int, default=64, help='回放轨迹分块步数(显存小就调小)')
|
||||
ap.add_argument('--K', type=int, default=8)
|
||||
ap.add_argument('--cap', type=float, default=0.5)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
ap.add_argument('--out', default=None)
|
||||
a = ap.parse_args()
|
||||
if a.dense_indices and a.cert_indices:
|
||||
ap.error('--dense_indices and --cert_indices are mutually exclusive')
|
||||
if a.cert_segments and not a.cert_indices:
|
||||
ap.error('--cert_segments requires --cert_indices')
|
||||
if a.trace:
|
||||
if a.cert_indices:
|
||||
extra = [tuple(map(int, segment.split(':'))) for segment in a.cert_segments.split(',') if segment]
|
||||
indices = [int(i) for i in a.cert_indices.split(',')]
|
||||
if any(len(segment) != 2 or segment[0] not in indices for segment in extra):
|
||||
ap.error('--cert_segments requires task:segment pairs from --cert_indices')
|
||||
replay_certificates(a.trace, indices, a.device, a.out, a.repeats, extra)
|
||||
elif a.dense_indices:
|
||||
replay_dense(a.trace, [int(i) for i in a.dense_indices.split(',')], a.max_step_deg, a.device, a.out)
|
||||
else:
|
||||
replay_first_hits(a.trace, a.out)
|
||||
return
|
||||
if a.dense_indices or a.cert_indices:
|
||||
ap.error('--dense_indices and --cert_indices require --trace')
|
||||
if not a.ckpt:
|
||||
ap.error('--ckpt or --trace is required')
|
||||
|
||||
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)
|
||||
if ck.get('fp') and ck['fp'] != kin.fp:
|
||||
sys.exit(f'checkpoint 指纹 {ck["fp"]} 与 kin.json {kin.fp} 不符')
|
||||
tasks = [t for t in load_tasks([a.samples]) if t['split'] == a.split][:a.n]
|
||||
PN = pair_names()
|
||||
rows = []
|
||||
tau = [0, 0.5, 1.0, 2.0, 3.0, 5.0, 1e9]
|
||||
hist = Counter()
|
||||
pair_first = Counter(); pair_steps = Counter()
|
||||
with torch.no_grad():
|
||||
for i in range(0, len(tasks), a.bs):
|
||||
chunk = tasks[i:i + a.bs]
|
||||
res, per = rollout(model, kin, chunk, a.device, K=a.K, safety=False, seed=i, return_traj=True)
|
||||
traj = per['traj'].to(a.device) # [B,T,6]
|
||||
B, Tn, _ = traj.shape
|
||||
first_t = torch.full((B,), float('nan'), device=a.device)
|
||||
first_pair = torch.full((B,), -1, dtype=torch.long, device=a.device)
|
||||
first_gap = torch.full((B,), float('nan'), device=a.device)
|
||||
min_gap = torch.full((B,), float('inf'), device=a.device)
|
||||
col_steps = torch.zeros(B, device=a.device)
|
||||
for k in range(Tn):
|
||||
qk = traj[:, k]
|
||||
mid = 0.5 * (traj[:, max(0, k - 1)] + qk)
|
||||
pts = torch.stack([qk, mid], 1).reshape(2 * B, 6) # 每帧只喂 2B 个点(与 rollout 同量级;一次 2048 点会爆显存)
|
||||
gm = kin.gaps(pts, a.cap)['gm'].view(B, 2, 49)
|
||||
gmin, gpair = gm.min(-1) # [B,2]
|
||||
g1 = gmin.min(1).values
|
||||
min_gap = torch.minimum(min_gap, g1)
|
||||
newly = (g1 <= 0) & torch.isnan(first_t)
|
||||
if bool(newly.any()):
|
||||
for b in newly.nonzero().flatten().tolist():
|
||||
pos = 0 if float(gmin[b, 0]) <= 0 else 1
|
||||
first_t[b] = k * DT
|
||||
first_pair[b] = gpair[b, pos]
|
||||
first_gap[b] = gmin[b, pos]
|
||||
if not bool(torch.isnan(first_t).any()):
|
||||
break
|
||||
for b, t in enumerate(chunk):
|
||||
ft = float(first_t[b])
|
||||
fp = int(first_pair[b])
|
||||
name = PN.get(fp, '—')
|
||||
if not math.isnan(ft):
|
||||
pair_first[name] += 1
|
||||
for j in range(len(tau) - 1):
|
||||
if tau[j] <= ft < tau[j + 1]:
|
||||
hist[f'{tau[j]}–{tau[j + 1]} s' if tau[j + 1] < 1e8 else '> 5 s'] += 1
|
||||
break
|
||||
rows.append({
|
||||
'tid': t.get('tid'), 'cat': t.get('cat'), 'mode': t.get('mode'),
|
||||
'collided': bool(per['col'][b] > 0),
|
||||
'first_collision_s': None if math.isnan(ft) else round(ft, 2),
|
||||
'first_pair': None if name == '—' else name, 'first_pair_idx': None if fp < 0 else fp,
|
||||
'first_gap_mm': None if math.isnan(float(first_gap[b])) else round(float(first_gap[b]), 3),
|
||||
'min_gap_mm': round(float(min_gap[b]), 3),
|
||||
'collision_steps': int(per['col'][b]),
|
||||
})
|
||||
print(f'[diag] {min(i + a.bs, len(tasks))}/{len(tasks)} 已回放', flush=True)
|
||||
|
||||
collided = [r for r in rows if r['collided']]
|
||||
def pct(v, p):
|
||||
v = sorted(v); return v[min(len(v) - 1, int(len(v) * p))] if v else None
|
||||
times = [r['first_collision_s'] for r in collided]
|
||||
out = {
|
||||
'ckpt': a.ckpt, 'step': ck.get('step'), 'n': len(rows), 'n_collided': len(collided),
|
||||
'collision_rate': len(collided) / max(1, len(rows)),
|
||||
'first_time_s': {'p10': pct(times, .1), 'p50': pct(times, .5), 'p90': pct(times, .9)},
|
||||
'first_time_hist': dict(hist),
|
||||
'first_pair_top': pair_first.most_common(),
|
||||
'min_gap_mm_p10/p50': [pct([r['min_gap_mm'] for r in rows], .1), pct([r['min_gap_mm'] for r in rows], .5)],
|
||||
'by_cat': {},
|
||||
'tasks': rows,
|
||||
}
|
||||
for k in sorted({f"{r['cat']}/{r['mode']}" for r in rows}):
|
||||
g = [r for r in rows if f"{r['cat']}/{r['mode']}" == k]
|
||||
out['by_cat'][k] = {'n': len(g), 'first_t_p50': pct([r['first_collision_s'] for r in g if r['collided']], .5),
|
||||
'first_pairs': Counter([r['first_pair'] for r in g if r['first_pair']]).most_common(3)}
|
||||
s = json.dumps(out, indent=1, ensure_ascii=False)
|
||||
if a.out:
|
||||
open(a.out + '.tmp', 'w', encoding='utf-8').write(s); os.replace(a.out + '.tmp', a.out)
|
||||
print('== 汇总 ==')
|
||||
print('题数 %d,撞过 %d(%.1f%%)' % (len(rows), len(collided), 100 * out['collision_rate']))
|
||||
print('首次碰撞时间 p10/p50/p90 = %s / %s / %s s' % (
|
||||
out['first_time_s']['p10'], out['first_time_s']['p50'], out['first_time_s']['p90']))
|
||||
print('首次碰撞时间分布:', out['first_time_hist'])
|
||||
print('首次碰撞的连杆对 Top:', pair_first.most_common(8))
|
||||
print('全程最小间隙 p10/p50 = %s / %s mm' % tuple(out['min_gap_mm_p10/p50']))
|
||||
if a.out: print('已写', a.out)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
"""闭环末位误差诊断(只读,手动运行;不在流水线里)。
|
||||
|
||||
回答一个问题:策略"差一点没进 1 mm 门"还是"根本没朝目标走"。
|
||||
输出每题:
|
||||
- 最终位置误差 err_mm / 最终姿态误差 oerr_deg
|
||||
- 全程最小位置误差 min_err_mm、最小综合误差时是否进过 5 mm & 5° 窗口(DLS 接管门槛)
|
||||
- done(是否到达)、碰撞步数、用时
|
||||
以及按 cat/mode 分组的汇总与分位数。
|
||||
|
||||
用法(仓库根目录):
|
||||
PYTHONUTF8=1 python ml/eval/diag_tail.py --ckpt ml/runs/base/last.pt --n 512 --out ml/runs/diag_tail.json
|
||||
"""
|
||||
import argparse, json, math, os, sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def pct(v, p):
|
||||
if not v:
|
||||
return None
|
||||
v = sorted(v)
|
||||
return v[min(len(v) - 1, int(len(v) * p))]
|
||||
|
||||
|
||||
def main():
|
||||
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=512)
|
||||
ap.add_argument('--bs', type=int, default=64, help='每批题数(显存小就调小)')
|
||||
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()
|
||||
|
||||
from ml.common.kin_torch import Kin, D2R
|
||||
from ml.data import load_tasks
|
||||
from ml.model.policy import build
|
||||
from ml.sim.closed_loop import rollout, goal_err
|
||||
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
kin = Kin(device=a.device)
|
||||
if ck.get('fp') and ck['fp'] != kin.fp:
|
||||
sys.exit('checkpoint 的碰撞模型指纹 %s 与当前 kin.json %s 不符' % (ck['fp'], kin.fp))
|
||||
|
||||
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]
|
||||
|
||||
per_task = []
|
||||
with torch.no_grad():
|
||||
for i in range(0, len(tasks), a.bs):
|
||||
chunk = tasks[i:i + a.bs]
|
||||
res, per = rollout(model, kin, chunk, a.device, K=a.K, safety=False, seed=i, return_traj=True)
|
||||
traj = per['traj'] # [B, T, 6]
|
||||
B, Tn, _ = traj.shape
|
||||
Tt = torch.tensor([t['T'] for t in chunk], dtype=torch.float32, device=a.device)
|
||||
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in chunk], dtype=torch.float32, device=a.device)
|
||||
min_e = torch.full((B,), float('inf'), device=a.device)
|
||||
min_o = torch.full((B,), float('inf'), device=a.device)
|
||||
entered = torch.zeros(B, dtype=torch.bool, device=a.device) # 曾同时满足 e<5mm & (无姿态要求或 oe<5°)
|
||||
step = 256
|
||||
for t0 in range(0, Tn, step):
|
||||
q = traj[:, t0:t0 + step].reshape(-1, 6)
|
||||
n = q.shape[0] // B
|
||||
e, oe = goal_err(kin, q, Tt.repeat_interleave(n, 0), Rt.repeat_interleave(n, 0))
|
||||
e = e.view(B, n); oe = oe.view(B, n)
|
||||
min_e = torch.minimum(min_e, e.min(1).values)
|
||||
min_o = torch.minimum(min_o, oe.min(1).values)
|
||||
pose = (Rt.abs().sum(-1) > 0)[:, None]
|
||||
window = (e < 5) & (~pose | (oe < 5))
|
||||
entered |= window.any(1)
|
||||
done = per['done'].to(a.device); col = per['col'].to(a.device); td = per['t_done'].to(a.device)
|
||||
end_e = per['err'].to(a.device)
|
||||
# 末态姿态误差:traj 最后一帧
|
||||
qend = traj[:, -1]
|
||||
ee, eo = goal_err(kin, qend, Tt, Rt)
|
||||
for j, t in enumerate(chunk):
|
||||
per_task.append({
|
||||
'tid': t.get('tid'), 'cat': t.get('cat'), 'mode': t.get('mode'),
|
||||
'err_mm': float(end_e[j]), 'oerr_deg': float(eo[j]),
|
||||
'min_err_mm': float(min_e[j]), 'min_oerr_deg': float(min_o[j]),
|
||||
'entered_5mm5deg': bool(entered[j]),
|
||||
'done': bool(done[j]), 'collision_steps': float(col[j]),
|
||||
't_done_s': None if math.isnan(float(td[j])) else float(td[j]),
|
||||
})
|
||||
print('[diag] %d/%d done=%d entered=%d min_err<=5mm=%d' % (
|
||||
min(i + a.bs, len(tasks)), len(tasks), int(done.sum()), int(entered.sum()),
|
||||
sum(1 for p in per_task if p['min_err_mm'] <= 5)), flush=True)
|
||||
|
||||
def summ(rows):
|
||||
if not rows:
|
||||
return {'n': 0}
|
||||
e = [r['err_mm'] for r in rows]; me = [r['min_err_mm'] for r in rows]
|
||||
o = [r['oerr_deg'] for r in rows]
|
||||
return {'n': len(rows),
|
||||
'done': sum(r['done'] for r in rows),
|
||||
'entered_5mm5deg': sum(r['entered_5mm5deg'] for r in rows),
|
||||
'min_err_mm<=5': sum(1 for v in me if v <= 5),
|
||||
'min_err_mm<=10': sum(1 for v in me if v <= 10),
|
||||
'err_mm_p10/p50/p90': [pct(e, .1), pct(e, .5), pct(e, .9)],
|
||||
'min_err_mm_p10/p50/p90': [pct(me, .1), pct(me, .5), pct(me, .9)],
|
||||
'oerr_deg_p50/p90': [pct(o, .5), pct(o, .9)],
|
||||
'collision_steps_mean': sum(r['collision_steps'] for r in rows) / len(rows)}
|
||||
|
||||
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'fp': ck.get('fp'), 'n': len(per_task),
|
||||
'overall': summ(per_task),
|
||||
'by_cat_mode': {k: summ([r for r in per_task if '%s/%s' % (r['cat'], r['mode']) == k])
|
||||
for k in sorted({('%s/%s' % (r['cat'], r['mode'])) for r in per_task})},
|
||||
'tasks': per_task}
|
||||
s = json.dumps(out, indent=1, ensure_ascii=False)
|
||||
print(s[:2000])
|
||||
if a.out:
|
||||
open(a.out + '.tmp', 'w', encoding='utf-8').write(s)
|
||||
os.replace(a.out + '.tmp', a.out)
|
||||
print('[diag] 已写', a.out)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""小规模同步闭环评估;仿真墙钟不等于控制延迟。"""
|
||||
import argparse, functools, hashlib, json, math, time
|
||||
from pathlib import Path
|
||||
import torch
|
||||
|
||||
from ml.common.ckpt import load_policy
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.data import load_tasks
|
||||
from ml.sim.closed_loop import rollout, tasks_to_tensors
|
||||
import ml.sim.closed_loop as clmod
|
||||
|
||||
|
||||
def run(model, kin, tasks, device, bs=256, **kw):
|
||||
per = []
|
||||
for i in range(0, len(tasks), bs):
|
||||
t0 = time.perf_counter()
|
||||
print('batch %d-%d/%d start' % (i + 1, min(i + bs, len(tasks)), len(tasks)), flush=True)
|
||||
def progress(step, total):
|
||||
print('batch %d step %d/%d wall %.1fs' % (i // bs + 1, step, total, time.perf_counter() - t0), flush=True)
|
||||
r, p = rollout(model, kin, tasks[i:i + bs], device, progress=progress, **kw)
|
||||
per.append((r, p, len(tasks[i:i + bs])))
|
||||
print('batch %d done wall %.1fs' % (i // bs + 1, time.perf_counter() - t0), flush=True)
|
||||
flat = {k: torch.cat([p[k] for _, p, _ in per]) for k in per[0][1] if k not in ('traj', 'plans')}
|
||||
_, _, _, ex, tr = tasks_to_tensors(tasks, 'cpu')
|
||||
ok, col = flat['ok'], flat['col'] > 0
|
||||
has_ref = torch.isfinite(ex)
|
||||
okr = ok & has_ref & torch.isfinite(tr)
|
||||
median = lambda x: x.median().item() if x.numel() else None
|
||||
out = {
|
||||
'n': len(tasks), 'n_ref': int(has_ref.sum()),
|
||||
'success': ok.float().mean().item(),
|
||||
'collision_free_success': (ok & ~col).float().mean().item(),
|
||||
'time_ok_1p3': (ok & has_ref & (flat['t_done'] <= 1.3 * ex)).sum().item() / int(has_ref.sum()) if has_ref.any() else None,
|
||||
'collision_rate': col.float().mean().item(),
|
||||
'collision_steps_mean': flat['col'].mean().item(),
|
||||
'travel_ratio_median': median(flat['travel'][okr] / tr[okr].clamp(min=1)),
|
||||
'time_mean_s': flat['t_done'][ok].mean().item() if ok.any() else None,
|
||||
'brakes_mean': flat['brakes'].mean().item(),
|
||||
'start_fail': flat['start_fail'].float().mean().item(),
|
||||
'goal_ratio_2s': median(flat['goal_ratio_2s']),
|
||||
'err_fail_med': median(flat['err'][~ok]),
|
||||
'err_success_med': median(flat['err'][ok]),
|
||||
'dls_share': flat['dls_it'].ge(0).float().mean().item(),
|
||||
'time_budget': ('fixed %.3f s' % kw['max_sec']) if kw.get('max_sec') is not None else
|
||||
(per[0][0]['time_budget'] if len({r['time_budget'] for r, _, _ in per}) == 1 else 'mixed'),
|
||||
}
|
||||
if 'speed_ratio' in flat: # 实际执行速度:每题时限内 max|Δq|/STEP(1.0 = VMAX),加速度同口径按每控制步的速度变化
|
||||
v, acc = flat['speed_ratio'], flat['accel_ratio']
|
||||
out.update({'speed_ratio_p50': median(v), 'speed_ratio_p95': v.quantile(0.95).item(),
|
||||
'speed_ratio_max': v.max().item(), 'over_vmax_rate': (v > 1.001).float().mean().item(),
|
||||
'accel_ratio_p95': acc.quantile(0.95).item(), 'accel_ratio_max': acc.max().item()})
|
||||
return out, per
|
||||
|
||||
|
||||
def json_finite(value):
|
||||
if isinstance(value, dict): return {k: json_finite(v) for k, v in value.items()}
|
||||
if isinstance(value, list): return [json_finite(v) for v in value]
|
||||
if isinstance(value, float) and not math.isfinite(value): return None
|
||||
return value
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', default='ml/runs/base_dag2_dag1/best.pt')
|
||||
ap.add_argument('--samples', default='ml/data/samples')
|
||||
ap.add_argument('--tasks', default=None)
|
||||
ap.add_argument('--split', default='val')
|
||||
ap.add_argument('--n', type=int, default=256)
|
||||
ap.add_argument('--K', type=int, default=8)
|
||||
ap.add_argument('--commit', type=int, default=3)
|
||||
ap.add_argument('--fb', type=float, default=1.8)
|
||||
ap.add_argument('--ki', type=float, default=0.0)
|
||||
ap.add_argument('--fb_scale', type=float, default=0.0)
|
||||
ap.add_argument('--fb_near', type=float, default=0.2)
|
||||
ap.add_argument('--fb_fallback', type=float, default=0.0)
|
||||
ap.add_argument('--kdecay', type=float, default=0.9)
|
||||
ap.add_argument('--kimax', type=float, default=0.5)
|
||||
ap.add_argument('--fb_every', type=int, default=1)
|
||||
ap.add_argument('--safety_try', type=int, default=0)
|
||||
ap.add_argument('--sel_w', type=float, default=0.1)
|
||||
ap.add_argument('--dls_safety', type=int, default=0)
|
||||
ap.add_argument('--fb_clear', type=float, default=0.0)
|
||||
ap.add_argument('--fb_cmin', type=float, default=0.3)
|
||||
ap.add_argument('--sub_refresh', type=int, default=0)
|
||||
ap.add_argument('--fb_block_gain', type=float, default=1.0)
|
||||
ap.add_argument('--dump', default=None)
|
||||
ap.add_argument('--out', default=None)
|
||||
ap.add_argument('--trace', default=None)
|
||||
ap.add_argument('--select', default='consist')
|
||||
ap.add_argument('--replan', default='r3')
|
||||
ap.add_argument('--safety', type=int, default=1, help='1=安全层+部分前缀;0=关')
|
||||
ap.add_argument('--partial', type=int, default=1)
|
||||
ap.add_argument('--inc_cap', type=int, default=0, help='1=反馈叠加后再按每轴 VMAX·DT 限幅')
|
||||
ap.add_argument('--gap_margin', type=float, default=0.0, help='安全层要求的间隙余量 mm(>0 时宽相位 cap 自动抬到 margin+0.5)')
|
||||
ap.add_argument('--cert_batch', type=int, default=0, help='1=一次 gaps 调用覆盖全部候选(压最坏情况,典型路径略贵)')
|
||||
ap.add_argument('--max_sec', type=float, default=None)
|
||||
ap.add_argument('--seed', type=int, default=0)
|
||||
ap.add_argument('--dls', default='analytic', choices=['analytic', 'fd'])
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
a = ap.parse_args()
|
||||
print('config ' + json.dumps(vars(a), ensure_ascii=False), flush=True)
|
||||
if a.dls == 'fd':
|
||||
clmod.dls_steps = functools.partial(clmod.dls_steps, analytic=False)
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
kin = Kin(device=a.device)
|
||||
assert ck.get('fp') in (None, kin.fp)
|
||||
tasks = ([json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()] if a.tasks
|
||||
else [t for t in load_tasks([a.samples]) if t['split'] == a.split])
|
||||
tasks = tasks[:a.n]
|
||||
if not tasks: ap.error('no tasks selected')
|
||||
meta = {
|
||||
'config': vars(a), 'batch_size': 256, 'batch_seed_rule': 'same seed per batch',
|
||||
'checkpoint_sha256': hashlib.sha256(Path(a.ckpt).read_bytes()).hexdigest(),
|
||||
'tasks_sha256': hashlib.sha256(json.dumps(tasks, sort_keys=True).encode()).hexdigest(),
|
||||
'kin_fp': kin.fp, 'act_scale': float(model.act_scale),
|
||||
}
|
||||
t0 = time.perf_counter()
|
||||
r, pers = run(model, kin, tasks, a.device, K=a.K, safety=bool(a.safety), seed=a.seed, max_sec=a.max_sec,
|
||||
safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit,
|
||||
select=a.select, replan=a.replan, fb_gain=a.fb, fb_scale=a.fb_scale, fb_near=a.fb_near, fb_fallback=a.fb_fallback, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax,
|
||||
fb_every=a.fb_every, safety_try=a.safety_try, sel_w=a.sel_w, dls_safety=a.dls_safety, fb_clear=a.fb_clear, fb_cmin=a.fb_cmin, sub_refresh=a.sub_refresh, fb_block_gain=a.fb_block_gain,
|
||||
inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, return_traj=bool(a.trace))
|
||||
wall = time.perf_counter() - t0
|
||||
result = {**meta, 'metrics': r, 'simulation_wall_s': wall}
|
||||
print(json.dumps(json_finite(result), ensure_ascii=False, allow_nan=False), flush=True)
|
||||
if a.out:
|
||||
Path(a.out).write_text(json.dumps(json_finite(result), indent=2, ensure_ascii=False, allow_nan=False) + '\n', encoding='utf-8')
|
||||
if a.dump:
|
||||
rows = []
|
||||
keys = ['done', 'ok', 't_done', 'limit', 'col', 'brakes', 'err', 'first_hit', 'dls_it', 'goal_ratio_2s']
|
||||
flat = {k: torch.cat([p[k] for _, p, _ in pers]).tolist() for k in keys}
|
||||
for i, t in enumerate(tasks):
|
||||
rows.append({**t, 'i': i, **{k: flat[k][i] for k in keys}})
|
||||
Path(a.dump).write_text(json.dumps(json_finite(rows), allow_nan=False), encoding='utf-8')
|
||||
print('dump', a.dump, flush=True)
|
||||
if a.trace:
|
||||
torch.save({**meta, 'tasks': tasks, 'batches': pers}, a.trace)
|
||||
print('trace', a.trace, flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""老师轨迹回放自检(train 阶段开训前自动跑,约 1–3 分钟):
|
||||
python -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json
|
||||
两部分,都过才 pass(pipeline.sh 读 pass 字段与指纹,不过就停 rc 5):
|
||||
1. 运动学口径(2026-10-18):gen 写出的老师 50 Hz 轨迹(keep=true、cat≠D)直接放进 PyTorch 运动学 / 碰撞 / 到达判据:
|
||||
到达率 ≥ 0.95、碰撞率 ≤ 0.01。验证的是「仿真口径(kin.json、间隙、到达门槛)与 JS 认证过的数据一致」。
|
||||
2. 滚动口径(2026-10-19,审查 review_closedloop F1/D1、review_data_actions C1):上面那条完全不经过 rollout(),
|
||||
队列 / 前缀 / 提交 3 步 / qe / 标签 clamp / DLS / done / 时限上的错位它都发现不了。这里让老师当策略(closed_loop.TeacherOracle:
|
||||
每次规划从当前时间下标取老师 16 步增量,按标签口径 clamp)原样走 rollout()(select='first'、use_sub=False):
|
||||
到达(时限内)≥ 0.95、碰撞 ≤ 0.01、DLS 接管前与老师轨迹的最大关节偏差 p99 ≤ 0.1°(shard 轨迹按 1e-3° 取整,标签 clamp 会截掉取整的余量)。
|
||||
输出另带 fp(当前 kin.json)、data_fp(shard 里记录的碰撞模型指纹)、ver=2;pipeline 三者都核对。
|
||||
load_teacher / pad_traj / oracle_raw / oracle_summary 供 ml/eval/diag_cl.py 的 H1 段复用。"""
|
||||
import argparse, glob, json, math, os, sys
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.sim.closed_loop import goal_err, rollout, TeacherOracle, DT
|
||||
|
||||
DEV_TOL_DEG = 0.1
|
||||
|
||||
|
||||
def load_teacher(shards, ids):
|
||||
"""按任务 id 从 shard jsonl 取老师记录(traj、path、exec_s…)。只解析 id 命中的行。"""
|
||||
want, out = set(ids), {}
|
||||
for f in sorted(glob.glob(shards)):
|
||||
for line in open(f, encoding='utf-8'):
|
||||
k = line.find('"id":"')
|
||||
if k < 0: continue
|
||||
rid = line[k + 6:line.find('"', k + 6)]
|
||||
if rid in want and rid not in out:
|
||||
r = json.loads(line)
|
||||
if r.get('traj'): out[rid] = r
|
||||
if len(out) == len(want): break
|
||||
return out
|
||||
|
||||
|
||||
def pad_traj(trajs, device, extra=20):
|
||||
"""[B,Tmax+extra,6]:各题老师轨迹末帧重复补齐(oracle 在轨迹末尾之后给零增量)。"""
|
||||
Tm = max(len(t) for t in trajs) + extra
|
||||
out = torch.zeros(len(trajs), Tm, 6, device=device)
|
||||
for i, t in enumerate(trajs):
|
||||
x = torch.tensor(t, dtype=torch.float32, device=device)
|
||||
out[i, :len(t)] = x; out[i, len(t):] = x[-1]
|
||||
return out
|
||||
|
||||
|
||||
def oracle_raw(per, TR, lens, tasks):
|
||||
"""oracle 滚动的逐题量:DLS 接管前(且到达前、老师轨迹结束前)与老师的最大关节偏差 mdev(度)、时限内到达 ok、
|
||||
时限内碰撞 col、是否计入到达统计(cat≠D)、完成时刻 − 老师时长 dt(s)、是否用过 DLS、2 s 目标误差比。"""
|
||||
tr = per['traj'].to(TR.device); B, Tn, _ = tr.shape
|
||||
L = min(Tn, TR.shape[1])
|
||||
dev = (tr[:, :L] - TR[:, :L]).abs().max(-1).values # [B,L]
|
||||
k = torch.arange(L, device=TR.device)[None]
|
||||
kd = torch.tensor(lens, device=TR.device)[:, None] - 1
|
||||
dls = per['dls_it'].to(TR.device)[:, None]
|
||||
td = per['t_done'].to(TR.device)
|
||||
kdone = torch.where(torch.isfinite(td), (td / DT).round(), torch.full_like(td, 1e9)).long()[:, None]
|
||||
valid = (k <= kd) & ((dls < 0) | (k <= dls)) & (k <= kdone)
|
||||
ex = torch.tensor([t.get('exec_s') or float('nan') for t in tasks])
|
||||
return {'mdev': torch.where(valid, dev, torch.zeros_like(dev)).max(-1).values.cpu(), 'ok': per['ok'], 'col': per['col'] > 0,
|
||||
'reach_m': torch.tensor([t.get('cat') != 'D' for t in tasks]), 'dt': per['t_done'] - ex,
|
||||
'dls': per['dls_it'] >= 0, 'r2': per['goal_ratio_2s']}
|
||||
|
||||
|
||||
def _q(v, p):
|
||||
v = v[torch.isfinite(v)]
|
||||
return float(v.kthvalue(max(1, min(len(v), int(math.ceil(p * len(v)))))).values) if len(v) else None
|
||||
|
||||
|
||||
def oracle_summary(raws):
|
||||
c = {k: torch.cat([r[k] for r in raws]) for k in raws[0]}
|
||||
m = c['reach_m']; ok = c['ok']
|
||||
dt = c['dt'][ok & m]
|
||||
return {'n': int(len(m)), 'n_reach_tasks': int(m.sum()),
|
||||
'reach': float(ok[m].float().mean()) if m.any() else None,
|
||||
'collision': float(c['col'][m].float().mean()) if m.any() else None,
|
||||
'D': {'n': int((~m).sum()), 'collision': float(c['col'][~m].float().mean()) if (~m).any() else None},
|
||||
'max_dev_deg_before_dls': {'p50': _q(c['mdev'], .5), 'p99': _q(c['mdev'], .99), 'max': float(c['mdev'].max())},
|
||||
'dls_used': float(c['dls'].float().mean()),
|
||||
'finish_minus_teacher_s': {'p50': _q(dt, .5), 'p90': _q(dt, .9), 'max': float(dt.max()) if len(dt) else None},
|
||||
'goal_ratio_2s_p50': _q(c['r2'], .5)}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--shards', default='ml/data/shard_*.jsonl'); ap.add_argument('--n', type=int, default=512)
|
||||
ap.add_argument('--samples', default='ml/data/samples', help='读打包 meta 的 act_scale(oracle 按同一标签口径 clamp);没有就按 1.0')
|
||||
ap.add_argument('--bs', type=int, default=128)
|
||||
ap.add_argument('--out', default='ml/runs/replay_teacher.json')
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
a = ap.parse_args()
|
||||
kin = Kin(device=a.device)
|
||||
recs = []
|
||||
for f in sorted(glob.glob(a.shards)):
|
||||
for line in open(f, encoding='utf-8'):
|
||||
r = json.loads(line)
|
||||
if r.get('keep') and r.get('traj') and r.get('cat') != 'D': recs.append(r)
|
||||
if len(recs) >= a.n: break
|
||||
if len(recs) >= a.n: break
|
||||
if not recs: sys.exit('没有可回放的老师轨迹(先跑 gen)')
|
||||
# ---- 1. 运动学口径
|
||||
reach = col = 0; worst_gap = []
|
||||
for r in recs:
|
||||
q = torch.tensor(r['traj'], dtype=torch.float32, device=a.device)
|
||||
mid = 0.5 * (q[1:] + q[:-1])
|
||||
g = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
|
||||
hit = bool((g <= 0).any()); col += hit; worst_gap.append(g.min().item())
|
||||
T = torch.tensor([r['T']], dtype=torch.float32, device=a.device)
|
||||
R = torch.tensor([r['R'] if r.get('R') else [0.] * 9], dtype=torch.float32, device=a.device)
|
||||
e, oe = goal_err(kin, q[-1:], T, R)
|
||||
reach += bool((e < 1).item() and (oe < 0.5).item())
|
||||
n = len(recs)
|
||||
kin_ok = reach / n >= 0.95 and col / n <= 0.01
|
||||
# ---- 2. 滚动口径:老师 oracle 走完整 rollout()
|
||||
try:
|
||||
from ml.data import read_meta
|
||||
act_scale = float(read_meta(a.samples).get('act_scale', 1.0)) if os.path.isdir(a.samples) else 1.0
|
||||
except SystemExit:
|
||||
act_scale = 1.0
|
||||
tasks = [{'q0': r['q0'], 'T': r['T'], 'R': r.get('R'), 'exec_s': r.get('exec_s') or (len(r['traj']) - 1) * DT,
|
||||
'travel': r.get('travel_deg'), 'cat': r.get('cat')} for r in recs]
|
||||
raws = []
|
||||
for i in range(0, n, a.bs):
|
||||
tb, rb = tasks[i:i + a.bs], recs[i:i + a.bs]
|
||||
TR = pad_traj([r['traj'] for r in rb], a.device)
|
||||
_, per = rollout(None, kin, tb, a.device, K=1, select='first', use_sub=False, return_traj=True, seed=i,
|
||||
oracle=TeacherOracle(TR, act_scale=act_scale))
|
||||
raws.append(oracle_raw(per, TR, [len(r['traj']) for r in rb], tb))
|
||||
orc = oracle_summary(raws); orc['act_scale'] = act_scale
|
||||
orc_ok = (orc['reach'] or 0) >= 0.95 and (orc['collision'] or 0) <= 0.01 and orc['max_dev_deg_before_dls']['p99'] <= DEV_TOL_DEG
|
||||
data_fps = {r.get('fp') for r in recs}
|
||||
data_fp = data_fps.pop() if len(data_fps) == 1 else 'mixed'
|
||||
res = {'ver': 2, 'n': n, 'reach': reach / n, 'collision': col / n, 'min_gap_mm_p10': sorted(worst_gap)[n // 10], 'kin_pass': kin_ok,
|
||||
'oracle': orc, 'oracle_pass': orc_ok, 'fp': kin.fp, 'data_fp': data_fp,
|
||||
'pass': bool(kin_ok and orc_ok and data_fp in (None, kin.fp))}
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
json.dump(res, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
||||
print('老师轨迹回放:到达 %.1f%%、碰撞 %.1f%%(%d 条);oracle 走 rollout:到达 %.1f%%、碰撞 %.1f%%、DLS 前最大偏差 p99 %.4f°;指纹 %s' % (
|
||||
100 * res['reach'], 100 * res['collision'], n, 100 * orc['reach'], 100 * orc['collision'],
|
||||
orc['max_dev_deg_before_dls']['p99'], '一致' if data_fp in (None, kin.fp) else '不符'), flush=True)
|
||||
sys.exit(0 if res['pass'] else 5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
"""导出部署包(方案 §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 对拍。
|
||||
与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once、act_scale)。导出后用 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 已兼容),重新导出即可。
|
||||
@@ -32,8 +32,8 @@ def main():
|
||||
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()
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:导出 EMA 权重(审查 P1-5:原来导的是原始权重)
|
||||
m, ck = load_policy(a.ckpt, 'cpu')
|
||||
else: raise SystemExit('需要 --ckpt 或 --random <配置>')
|
||||
kin = Kin()
|
||||
if ck.get('fp') and ck['fp'] != kin.fp: raise SystemExit(f"checkpoint 指纹 {ck['fp']} ≠ 当前碰撞模型 {kin.fp}:须重训")
|
||||
@@ -62,8 +62,10 @@ def main():
|
||||
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)
|
||||
# 2026-10-19:act_scale = 动作标签归一化倍数,运行时 inc = clamp(act, ±1)·act_scale·STEP(ik-policy.js Runner 的 actScale 读它)
|
||||
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}
|
||||
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'feat_dyn': True,
|
||||
'act_scale': float(ck.get('act_scale', 1.0)), '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}))
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* 每行一条任务:采样 → 老师 → 距离下界认证过滤 → 局部时间律(50 Hz)→ 写出。
|
||||
* 认证不过的样本也写出(keep=false + 原因),供 stats 统计丢弃率;训练只用 keep=true。
|
||||
* 2026-10-18 断点续写:第 i 条任务的随机数只由 (seed, i) 决定(sampler.reseed);输出文件已存在时保留其中完整的行、
|
||||
* 截掉写了一半的最后一行,从下一条接着写 —— 生成几小时被中断,重跑只补剩下的,不重来、不重复。 */
|
||||
* 截掉写了一半的最后一行,从下一条接着写 —— 生成几小时被中断,重跑只补剩下的,不重来、不重复。
|
||||
* 2026-10-19:--tasks 的任务带 qPrev(DAgger 采集时策略的上一帧)时,记录 qPrev 与起步速度 v0(timelaw.initSpeed),
|
||||
* 时间律从 v0 起步;build_dataset.cjs 用它们做 t=0 样本的 dq 特征与标签。 */
|
||||
'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];
|
||||
@@ -19,7 +21,7 @@ const K_CONS = +args.consensus || 0;
|
||||
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 { timeParam, initSpeed } = require('./timelaw.cjs');
|
||||
const fp = S.modelFingerprint ? S.modelFingerprint() : null;
|
||||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||
let done = 0;
|
||||
@@ -63,7 +65,11 @@ for (let i = done; i < NT; i++) {
|
||||
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); }
|
||||
if (rec.keep && task.qPrev) {
|
||||
rec.qPrev = task.qPrev.map(r3);
|
||||
rec.v0 = r3(initSpeed(t.pts, task.qPrev, S.wrap180, S.ORDER.map(id => !!S.LIM[id].cont)));
|
||||
}
|
||||
if (rec.keep) { const tp = timeParam(t.pts, { v0: rec.v0 || 0 }); 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`);
|
||||
}
|
||||
|
||||
+20
-3
@@ -29,10 +29,27 @@ while :; do
|
||||
echo "[gen 节拍] $(( (tl-t0)/60 )) 分钟 运行中 $alive/${#pids[@]} 进程 本批已写 $n 条 $last"
|
||||
fi
|
||||
done
|
||||
fail=0
|
||||
# 判定按产物而不是 wait 的退出码:2026-10-18 实测 28 个种子跑了 5 小时后全部 wait 报失败,
|
||||
# 但每个 .part 都是完整的 1786 行(node 打了 done、mv 却没执行)——Git Bash 长跑后 wait 的返回码不可靠。
|
||||
# 短产物分两种:网页点「停止」把 node 杀掉(.part 是断点,续跑接着写,不是失败)和 node 自己报错。
|
||||
# 只有日志尾部有报错特征才算失败;143 让网页显示「已停止」而不是「失败(返回码 6)」(monitor.html:508 同一口径)。
|
||||
crashed=0; stopped=0; sn=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
|
||||
s=${seeds[$k]}; f=ml/data/shard_$(printf %04d $s).jsonl
|
||||
w=0; wait ${pids[$k]} || w=$? # 记下来但不当判据:长跑后 Git Bash 的 wait 返回码不可靠
|
||||
if [ -f "$f" ]; then continue; fi
|
||||
n=0; [ -f "$f.part" ] && n=$(wc -l < "$f.part")
|
||||
if [ "$n" -ge "$N" ]; then mv "$f.part" "$f" && echo "[gen] 种子 $s 产物完整($n 行),补改名" && continue; fi
|
||||
if tail -n 4 ml/data/log_$s.txt 2>/dev/null | grep -qE '^(Error|.*Error:|.*SyntaxError|.*heap out of memory|Traceback|.*throw )'; then
|
||||
echo "[gen] 种子 $s 失败:.part 只有 $n/$N 行,最后几行:"; tail -n 5 ml/data/log_$s.txt || true
|
||||
crashed=1
|
||||
else
|
||||
echo "[gen] 种子 $s 已停止:写好 $n/$N 条,断点保留在 $f.part,点「继续训练」从第 $((n + 1)) 条接着写(子进程返回码 $w)"
|
||||
stopped=1; sn=$((sn + n))
|
||||
fi
|
||||
done
|
||||
[ $fail = 0 ] || exit 6
|
||||
[ $crashed = 0 ] || exit 6
|
||||
# 全被停止时不跑 stats.cjs:分片还没改名,统计口径没变,重算一遍只会让「补量」看起来原地不动
|
||||
[ $stopped = 0 ] || { echo "[gen] 本轮是被停止的,没有报错:$stopped 个种子共保住 $sn 条,续跑不重复生成"; exit 143; }
|
||||
node ml/gen/stats.cjs ml/data/shard_*.jsonl > ml/data/stats.json
|
||||
cat ml/data/stats.json
|
||||
|
||||
+20
-3
@@ -1,7 +1,10 @@
|
||||
/* 局部可观测时间律(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)状态序列,末点精确落在终点。 */
|
||||
* 输出 50 Hz(dt = 20 ms)状态序列,末点精确落在终点。
|
||||
* 2026-10-19(审查 review_closedloop F7 / review_data_actions C10):opt.v0 = 起步归一化速度(默认 0 = 静止起步)。
|
||||
* DAgger 重新标注的起点来自策略闭环、本来就在运动,原来一律从静止加速,标签与策略真正到达的 (q, dq) 状态不符。
|
||||
* initSpeed(P, qPrev) 把当前速度投影到首段方向上得到 v0(gen_dataset.cjs 用)。 */
|
||||
'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 待真机参数)
|
||||
@@ -19,7 +22,7 @@ function timeParam(P, opt) {
|
||||
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;
|
||||
const out = [P[0].slice()], taus = [0]; let tau = 0, v = Math.max(0, Math.min(1, opt.v0 || 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);
|
||||
@@ -30,4 +33,18 @@ function timeParam(P, opt) {
|
||||
}
|
||||
return { traj: out, taus, cum, total, steps: out.length - 1 };
|
||||
}
|
||||
module.exports = { timeParam, VMAX, A, DT };
|
||||
/* 起步速度:w_j = 当前每步增量 / STEP_j(qPrev → P[0]),u_j = 巡航时首段每步增量 / STEP_j(max|u_j| = 1),
|
||||
v0 = clamp(⟨w,u⟩/⟨u,u⟩, 0, 1)——当前速度在首段方向上的分量;反向或首段为零时 0。wrap:连续轴差值的最短弧函数(可缺省)。 */
|
||||
function initSpeed(P, qPrev, wrap, cont) {
|
||||
if (!qPrev || !P || P.length < 2) return 0;
|
||||
const dd = (a, b, j) => (wrap && cont && cont[j]) ? wrap(a - b) : a - b;
|
||||
let seg = 0; for (let j = 0; j < 6; j++) seg = Math.max(seg, Math.abs(P[1][j] - P[0][j]) / VMAX[j]);
|
||||
if (seg < 1e-9) return 0;
|
||||
let wu = 0, uu = 0;
|
||||
for (let j = 0; j < 6; j++) {
|
||||
const u = (P[1][j] - P[0][j]) / VMAX[j] / seg, w = dd(P[0][j], qPrev[j], j) / (VMAX[j] * DT);
|
||||
wu += w * u; uu += u * u;
|
||||
}
|
||||
return Math.max(0, Math.min(1, wu / uu));
|
||||
}
|
||||
module.exports = { timeParam, initSpeed, VMAX, A, DT };
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ 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()
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
||||
return load_policy(path, dev)[0]
|
||||
|
||||
|
||||
def per_task_success(pers):
|
||||
|
||||
+4
-3
@@ -12,13 +12,14 @@ 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
|
||||
BASE_S="ml/data/samples $(ls -d ml/data/samples_dag_* 2>/dev/null | tr '\n' ' ')" # 2026-10-18:BC 正则也用 DAgger 样本
|
||||
SAMPLES="$BASE_S"; 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"
|
||||
[ -s ml/data/e2/shard_e2_r$r.jsonl ] && SAMPLES="$BASE_S 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
|
||||
@@ -35,7 +36,7 @@ for ((r=1; r<=ROUNDS; r++)); do
|
||||
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"
|
||||
SAMPLES="$BASE_S 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
|
||||
|
||||
@@ -28,8 +28,8 @@ def main():
|
||||
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()
|
||||
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
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')}
|
||||
|
||||
+22
-5
@@ -1,22 +1,36 @@
|
||||
"""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))。"""
|
||||
目标 u_tgt = v − (t − r)·du/dt,du/dt = v·∂_z u + ∂_t u 用 torch.func.jvp 一次前向得到(切向量 (v, 0, 1))。
|
||||
2026-10-18 首次正式预训练后改:
|
||||
- 整个 MF 分支(解码器前向 + JVP)在 fp32 下算:目标里含网络自己的导数,bf16 下 JVP 误差直接变成目标噪声;
|
||||
- return_aux=True 时返回 (z, t, r, u),训练用 r==t 且 t 小的样本做"配对去噪估计" x̂ = z − t·u 来加 FK / 平滑损失。
|
||||
原来把 FK 加在 one_step(随机噪声) 上、对齐这一条样本的老师 Y:对任意噪声都回归同一个 Y,最优解是条件均值 E[Y|X],
|
||||
多峰头被训成单峰(实测 last.pt 8 个候选间标准差 0.030,动作幅度 0.151),左绕/右绕取平均正好穿柱。
|
||||
x̂ 由 z_t 里带着的那一条 x 决定(t 小时 z_t 几乎就是 x),不会把不同模式拉到一起。"""
|
||||
import torch
|
||||
from torch.func import jvp
|
||||
|
||||
|
||||
def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0):
|
||||
def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0, p_edge=0.0):
|
||||
"""2026-10-19(审查 review_model_train P0-3):部署只在 (r=0, t=1) 推理一次,而 logit-normal 取 max/min 后
|
||||
P(t>0.95) 约 0.08%、P(r<0.05) 约 0.5%,这个角几乎从未被训练到(一步残差与噪声相关 −0.86 即外推失准)。
|
||||
p_neq > 0 时另以 p_edge 的概率把样本直接放在 (r=0, t=1);预热期(p_neq=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)
|
||||
if p_neq > 0 and p_edge > 0:
|
||||
edge = torch.rand(B, device=device) < p_edge
|
||||
t = torch.where(edge, torch.ones_like(t), t); r = torch.where(edge, torch.zeros_like(r), r)
|
||||
return r, t
|
||||
|
||||
|
||||
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
|
||||
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3, return_aux=False, p_edge=0.0):
|
||||
mem, x = mem.float(), x.float()
|
||||
with torch.autocast('cuda', enabled=False):
|
||||
B = x.shape[0]
|
||||
r, t = sample_rt(B, x.device, x.dtype, p_neq)
|
||||
r, t = sample_rt(B, x.device, x.dtype, p_neq, p_edge=p_edge)
|
||||
e = torch.randn_like(x)
|
||||
z = (1 - t)[:, None, None] * x + t[:, None, None] * e
|
||||
v = e - x
|
||||
@@ -25,7 +39,10 @@ def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
|
||||
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()
|
||||
loss = (w * err).mean()
|
||||
if return_aux:
|
||||
return loss, err.mean().detach(), {'z': z, 't': t, 'r': r, 'u': u}
|
||||
return loss, err.mean().detach()
|
||||
|
||||
|
||||
def one_step(model, mem, e):
|
||||
|
||||
+16
-5
@@ -55,10 +55,17 @@ class DecBlock(nn.Module):
|
||||
return x + self.ff(self.n3(x))
|
||||
|
||||
|
||||
TEMB_WMAX = 50.0 # 时间嵌入最高角频率(rad / 单位 t)
|
||||
|
||||
|
||||
def time_emb(t, dim):
|
||||
"""2026-10-18 首次正式预训练后改(step 21850 闭环到达 0%):原实现照扩散模型写成 sin(1000·t·f),f∈[1e-3, 1],
|
||||
最高角频率 1000 —— t∈[0,1] 上网络对 t 极其敏感,实测 last.pt 的 ∂u/∂t 均方根 6–12,而 v 只有约 1.1,
|
||||
MeanFlow 目标 u_tgt = v − (t−r)·du/dt 被自激的 JVP 项主导:MF 残差 2 万步一直停在 7–8、17k–19k 尖峰到 96、梯度 100% 被裁剪。
|
||||
改为角频率在 [0.5, 50] 几何分布(最快一维在 [0,1] 上约转 8 圈,足够分辨 t,导数量级降 20 倍)。"""
|
||||
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]
|
||||
w = torch.exp(torch.linspace(math.log(0.5), math.log(TEMB_WMAX), half, device=t.device, dtype=t.dtype))
|
||||
a = t[:, None] * w[None]
|
||||
return torch.cat([torch.sin(a), torch.cos(a)], -1)
|
||||
|
||||
|
||||
@@ -144,8 +151,9 @@ class Policy(nn.Module):
|
||||
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]"""
|
||||
def sample_k(self, feat, K, z=None, steps=1):
|
||||
"""仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13]
|
||||
steps>1(诊断用,2026-10-19):把 [0,1] 均分成 steps 段,x ← x − (t−r)·u(x, r, t) 逐段走;steps=1 即一步生成。"""
|
||||
mem = self.encode(feat)
|
||||
v, s = self.heads(mem)
|
||||
B = feat.shape[0]
|
||||
@@ -158,7 +166,10 @@ class Policy(nn.Module):
|
||||
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)
|
||||
x = z
|
||||
for i in range(steps):
|
||||
t, r = 1 - i / steps, 1 - (i + 1) / steps
|
||||
x = x - (t - r) * self.decode(m, x, one * r, one * t)
|
||||
return x.view(B, K, self.h, 6), v, s
|
||||
|
||||
|
||||
|
||||
+231
-66
@@ -56,7 +56,16 @@
|
||||
.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.fail .ds { color: var(--bad); font-weight: 650; font-size: 12px; }
|
||||
.alert { border-radius: 12px; padding: 13px 16px; margin: 2px 0 0; border: 1px solid; }
|
||||
.alert.bad { background: linear-gradient(180deg,#7f1d1d66,#450a0a55); border-color: #f8717199; box-shadow: 0 0 0 1px #f8717122, 0 8px 26px #00000066; }
|
||||
.alert.warn { background: #78350f55; border-color: #fbbf2488; }
|
||||
.alert .alerth { font-size: 17px; font-weight: 700; color: #ffe3e3; letter-spacing: .2px; }
|
||||
.alert.warn .alerth { color: #ffeccd; }
|
||||
.alert .alerterr { font: 500 12.5px/1.5 var(--mono); color: #ffb4b4; margin-top: 7px; word-break: break-all; }
|
||||
.alert .alerttip { font-size: 12.5px; color: #f0d9d9cc; margin-top: 7px; }
|
||||
.step .nm { font-size: 12.5px; color: var(--text); white-space: nowrap; } .step .ds { font-size: 11px; color: var(--dim2); white-space: nowrap; }
|
||||
.step .ds.big { font-size: 14px; font-weight: 650; color: var(--accent); }
|
||||
.step.pending .nm { color: var(--dim); }
|
||||
.step.active .ds { color: var(--accent); }
|
||||
|
||||
@@ -84,7 +93,9 @@
|
||||
.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); }
|
||||
.cores { display: flex; gap: 2px; height: 26px; margin-top: 2px; background: #1b2433; border-radius: 3px; padding: 2px; }
|
||||
.gr .l { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); }
|
||||
.gr .l span { flex: none; white-space: nowrap; } .gr .l b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* 值太长就省略号,别把整行挤成两行 */
|
||||
.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; }
|
||||
|
||||
@@ -126,25 +137,42 @@
|
||||
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; }
|
||||
.sublabel { font-size: 15px; color: var(--dim); display: flex; justify-content: space-between; align-items: baseline; margin-top: 6px; }
|
||||
.sublabel b { font: 700 20px/1 var(--mono); color: var(--text); }
|
||||
.bar.thin { height: 14px; margin: 10px 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; }
|
||||
.explore { background: #0b0f16; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; max-height: 560px; overflow: auto; font: 12px/1.7 var(--mono); color: #c9d4e3; white-space: pre-wrap; }
|
||||
.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 { position: fixed; inset: 0; background: #05080ed9; backdrop-filter: blur(7px); display: none; align-items: flex-start; justify-content: center; z-index: 40; padding: 5vh 16px 24px; overflow: auto; }
|
||||
.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); }
|
||||
.dlg { width: min(780px, 96vw); background: linear-gradient(180deg, #161f2e 0%, #121926 100%); border: 1px solid var(--line2); border-radius: 18px; box-shadow: 0 40px 100px #000c, 0 1px 0 #ffffff0a inset; overflow: hidden; }
|
||||
.dlg .hd { display: flex; gap: 13px; align-items: flex-start; padding: 18px 22px 15px; border-bottom: 1px solid var(--line); background: #ffffff04; }
|
||||
.dlg .hd .ic { flex: none; width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent2)); box-shadow: 0 4px 16px #5aa9ff33; font-size: 13px; }
|
||||
.dlg .hd h3 { margin: 2px 0 0; font-size: 16px; } .dlg .hd .d2 { color: var(--dim2); font-size: 12px; margin-top: 4px; line-height: 1.55; }
|
||||
.dlg .hd .d2 code { color: var(--dim); }
|
||||
.dlg .bd { padding: 16px 22px 4px; }
|
||||
.sec { display: flex; align-items: center; gap: 10px; margin: 17px 0 11px; font-size: 11px; letter-spacing: .1em; color: var(--dim2); }
|
||||
.bd .sec:first-child { margin-top: 2px; }
|
||||
.sec:after { content: ''; flex: 1; height: 1px; background: var(--line); }
|
||||
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 13px 16px; }
|
||||
.form label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--dim); min-width: 0; }
|
||||
.form .hint { font-size: 11px; color: var(--dim2); line-height: 1.45; }
|
||||
.form input, .form select { width: 100%; background: #0c1320; color: var(--text); border: 1px solid var(--line2); border-radius: 9px; padding: 9px 11px; font: 13px/1.35 var(--mono); outline: none; transition: border-color .12s, box-shadow .12s, background .12s; }
|
||||
.form input:hover, .form select:hover { border-color: #3d4c68; }
|
||||
.form input:focus, .form select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px #5aa9ff22; background: #0e1626; }
|
||||
.form select { appearance: none; -webkit-appearance: none; padding-right: 32px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5l5 5 5-5' fill='none' stroke='%238c99ad' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 11px center; }
|
||||
.form select:disabled { opacity: .4; cursor: not-allowed; background-color: #0a0f18; }
|
||||
.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; }
|
||||
.dlg .acts { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 16px 22px 19px; }
|
||||
.dlg .acts .sp { flex: 1; }
|
||||
button.ghost { background: transparent; }
|
||||
button.ghost:hover { background: #ffffff0a; border-color: #3d4c68; }
|
||||
.dlg .warnbox { margin: 15px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--warn); background: #fbbf240d; border: 1px solid #fbbf2430; border-left: 3px solid #fbbf24; border-radius: 10px; padding: 10px 13px; 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>
|
||||
@@ -161,17 +189,18 @@
|
||||
<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="auto" class="on" title="每 1 秒刷新"><span>●</span>自动刷新</button>
|
||||
<button id="btnStart" class="primary" title="一键运行流水线">▶ 开始训练</button>
|
||||
<button id="btnStop" class="danger" style="display:none" title="停止正在运行的流水线">■ 停止</button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="alert"></div>
|
||||
<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>
|
||||
<div class="card"><h2><span>GPU / CPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="prog"></section>
|
||||
@@ -185,28 +214,66 @@
|
||||
<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">docs/探索日志.md</span></h2><div class="explore" id="explore">加载中…</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="hd">
|
||||
<div class="ic">▶</div>
|
||||
<div>
|
||||
<h3 id="mdTitle">开始训练</h3>
|
||||
<div class="d2">一键跑 <code>bash ml/pipeline.sh all</code>:已完成的步骤自动跳过,训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd">
|
||||
<div class="sec">运行方式</div>
|
||||
<div class="form">
|
||||
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="from">从指定阶段重训(该阶段及之后全部重做)</option><option value="fresh">全部重跑(数据也重新生成)</option></select><span class="hint">续跑 = 从上次断点接着来;重训 = 把该阶段及之后的旧产物移进 ml/archive/(不删)</span></label>
|
||||
<label>起始阶段<select id="fFrom"></select><span class="hint">按流水线顺序排列;选「预训练」只重训模型:数据、打包样本、一致性门禁都保留</span></label>
|
||||
</div>
|
||||
<div class="sec">模型与数据</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>目标样本 TARGET_SAMPLES<input id="fT" value="2000000"></label>
|
||||
<label>老师间隙下限 MIN_GAP_MM<input id="fG" value="3"></label>
|
||||
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
|
||||
</div>
|
||||
<div class="warnbox" id="fWarn"></div>
|
||||
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<span class="sp"></span>
|
||||
<button class="ghost" id="fCancel">取消</button>
|
||||
<button id="fGo" class="primary">▶ 开始</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
/* 兜底:正文脚本一旦有语法错误或抛异常,整页会空白(2026-10-18 真踩过:一行字符串写断 → 页面什么都不显示)。
|
||||
这段在正文之前跑,负责"坏也坏得看得见":看门狗 + 全局错误显示。 */
|
||||
window.__boot = { ok: false };
|
||||
function __pageFail(msg) {
|
||||
const m = document.querySelector('main') || document.body;
|
||||
let d = document.getElementById('pagefail');
|
||||
if (!d) {
|
||||
d = document.createElement('div'); d.id = 'pagefail';
|
||||
d.style.cssText = 'margin:10px 24px 0;padding:14px 16px;border-radius:12px;background:#7f1d1dcc;border:1px solid #f87171;' +
|
||||
'color:#ffe3e3;font:600 15px/1.6 system-ui;position:sticky;top:8px;z-index:99;box-shadow:0 8px 26px #00000088';
|
||||
m.prepend(d);
|
||||
}
|
||||
d.textContent = msg;
|
||||
}
|
||||
window.addEventListener('error', e => __pageFail('页面脚本出错:' + (e.message || '未知错误') +
|
||||
'(' + (e.filename || '').split('/').pop() + ':' + (e.lineno || '?') + ')— 面板可能不完整,但训练本身没受影响;按 F12 看 Console'));
|
||||
setTimeout(() => { if (!window.__boot.ok) __pageFail('页面脚本没有启动(多半是 ml/monitor.html 有语法错误):卡片空白不是训练出问题。' +
|
||||
'F12 → Console 看第一条报错,或把该文件回滚到上一个提交。'); }, 3000);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
/* =====================================================================
|
||||
@@ -220,7 +287,7 @@ 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 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} 分 ${Math.floor(s % 60)} 秒` : m ? `${m} 分 ${Math.floor(s % 60)} 秒` : `${Math.floor(s)} 秒`; }; // 用时精确到秒(页面 1 秒轮询,秒位是有意义的)
|
||||
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>';
|
||||
@@ -343,13 +410,26 @@ let logyOn = false, job = null, userPicked = false, conOff = 0;
|
||||
下面这份只给示例数据用,数值与 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],
|
||||
['prep', '打包样本', 2.0], ['train', '预训练', 4.5], ['dagger', 'DAgger 纠偏', 3.5], ['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) }]));
|
||||
/* 起始阶段下拉的编号按 README 步骤表(1 环境、2 对拍、2′ R4、3 门禁、…、12 验收);setup 不进下拉(建议手动装依赖) */
|
||||
const FROM_NO = { setup: '1', kin: '2', r4: '2′', gate1: '3', gen: '4', prep: '5', train: '6', dagger: '6′', m5: '7', apg: '8', mine: '9', export: '10', bench: '11', accept: '12' };
|
||||
const FROM_NOTE = { gen: '(约 7 小时)' };
|
||||
function fillFrom() {
|
||||
const sel = $('fFrom'); if (!sel) return;
|
||||
const want = STAGE_META.map(x => x[0]).filter(k => k !== 'setup');
|
||||
if ([...sel.options].map(o => o.value).join() === want.join()) return; // 阶段表没变就别重建,免得每秒轮询把已选项重置
|
||||
const keep = sel.value;
|
||||
sel.innerHTML = want.map(k => `<option value="${k}">${FROM_NO[k] ? FROM_NO[k] + ' ' : ''}${SM[k].n} ${k}${FROM_NOTE[k] || ''}</option>`).join('');
|
||||
sel.value = want.includes(keep) ? keep : want.includes('train') ? 'train' : want[0];
|
||||
}
|
||||
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 }]));
|
||||
fillFrom();
|
||||
}
|
||||
fillFrom();
|
||||
/* 开始 / 停止请求带自定义头:跨站网页发不出这种请求(会触发 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;
|
||||
@@ -372,6 +452,10 @@ function buildCharts(kind) {
|
||||
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 步' }),
|
||||
o: new Chart(c, '开环误差(mm)', { xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : 'val 样本、老师状态 · 16 步末端误差 · 越低越好',
|
||||
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
|
||||
d: new Chart(c, '候选多样性', { xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : '候选间标准差 / 动作幅度 · 塌成单峰时接近 0',
|
||||
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
|
||||
t: new Chart(c, '吞吐(样本/s)', { xmax: total, xmin0: true, zero: true, refs: [{ y: 5000, label: 'R10 通过线 5000', color: css('--bad') }] }),
|
||||
};
|
||||
}
|
||||
@@ -393,6 +477,11 @@ function render() {
|
||||
}
|
||||
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', '起步失败')]);
|
||||
if (charts.o) { // 开环指标只随 train.py 的评估写进日志;APG 分支没建这两张图
|
||||
const Emin = (k, label) => { const pts = evals.filter(e => isNum(e[k])).map(e => [e.x, e[k]]); let b = null; if (pts.length) b = pts.reduce((m, p) => p[1] < m[1] ? p : m); return { name: k, label, dots: true, pts, best: b }; };
|
||||
charts.o.set([Emin('ol_fk_mm', '候选 0'), { name: 'ol_best_mm', label: '8 候选最好', dots: true, pts: evals.filter(e => isNum(e.ol_best_mm)).map(e => [e.x, e.ol_best_mm]) }]);
|
||||
charts.d.set([{ name: 'ol_div', label: '多样性', dots: true, pts: evals.filter(e => isNum(e.ol_div)).map(e => [e.x, e.ol_div]) }]);
|
||||
}
|
||||
renderProgress(); renderKpis();
|
||||
}
|
||||
|
||||
@@ -416,7 +505,7 @@ function renderProgress() {
|
||||
<div class="facts">
|
||||
${fact('步数', `${step.toLocaleString()}${total ? ' / ' + total.toLocaleString() : ''}`)}
|
||||
${fact('已用时间', hms(elapsed))}
|
||||
${fact('剩余时间', finished ? '—' : hms(eta))}
|
||||
${fact('剩余时间', finished ? '—' : hmsShort(eta))}
|
||||
${fact('预计完成', fin)}
|
||||
${kind === 'apg' ? fact('本批展开', r.plans ? `${r.plans} 次推理` : '—') : fact('学习率', isNum(r.lr) ? r.lr.toExponential(2) : '—')}
|
||||
${fact('日志更新', lastMtime ? ago(lastMtime) : '—', running ? '' : 'warn')}
|
||||
@@ -426,6 +515,7 @@ function renderProgress() {
|
||||
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 bestE = evals.length ? evals.reduce((m, v) => ((v.success ?? -1) > (m.success ?? -1) ? v : m)) : 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]]);
|
||||
@@ -441,7 +531,7 @@ function renderKpis() {
|
||||
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.success) + dlt('success'), bestE ? `最好 ${pct(best)} · @${bestE.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]);
|
||||
@@ -464,10 +554,12 @@ async function loadRuns() {
|
||||
if (!r) { cur = null; $('charts').innerHTML = ''; $('kpis').innerHTML = ''; $('prog').innerHTML = '<h2><span>当前运行 · 训练曲线</span></h2>' + empty('还没有训练日志:预训练开始后这里出现损失、吞吐和评估曲线。'); }
|
||||
}
|
||||
|
||||
let polling = false;
|
||||
async function poll() {
|
||||
if (polling) return; polling = true; // 1 秒一轮:上一轮没回来就跳过,避免请求堆叠
|
||||
try {
|
||||
lastStatus = await api('/api/status'); applyStageMeta(lastStatus.stage_meta);
|
||||
job = await api('/api/job').catch(() => null);
|
||||
const [st, jb] = await Promise.all([api('/api/status'), api('/api/job').catch(() => null)]);
|
||||
lastStatus = st; applyStageMeta(st.stage_meta); job = jb;
|
||||
await loadRuns();
|
||||
if (cur) {
|
||||
const d = await api(`/api/log?run=${encodeURIComponent(cur.name)}&offset=${offset}`);
|
||||
@@ -482,26 +574,36 @@ async function poll() {
|
||||
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 if (P && P.plan && P.plan.some(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0)) {
|
||||
const k = P.plan.find(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0), rc = P.stages[k].rc, nm = SM[k]?.n || k;
|
||||
setPill(rc === -15 || rc === 143 ? 'stale' : 'dead', `${rc === -15 || rc === 143 ? '已停止' : '失败'} · 停在「${nm}」返回码 ${rc}`);
|
||||
}
|
||||
else setPill('', '空闲');
|
||||
renderStatus(lastStatus); renderPipe(lastStatus); renderJob();
|
||||
try { renderStatus(lastStatus); renderPipe(lastStatus); renderJob(); }
|
||||
catch (e) { __pageFail('渲染面板出错:' + e.message + ' —— 其余面板可能不更新;训练本身不受影响'); }
|
||||
await pollConsole();
|
||||
} catch (err) {
|
||||
setPill('dead', '连不上控制台(双击 ml/console.bat 或 python -m ml.monitor)'); console.error(err);
|
||||
}
|
||||
} finally { polling = false; }
|
||||
}
|
||||
function setPill(cls, text) { const st = $('state'); st.className = 'pill ' + cls; st.lastElementChild.textContent = text; }
|
||||
|
||||
/* ===================== 流水线 / GPU / 各阶段面板 ===================== */
|
||||
/* 「全流程」进度与上方步骤条同一口径:步骤 = 本次计划 ∪ 记录里已有的阶段。
|
||||
从中间阶段重训时,前面已完成的阶段不在本次 plan 里,但记录还在(步骤条就靠这个显示打勾),
|
||||
总进度/剩余时间/完成计数也必须算上它们,否则总进度会归零。 */
|
||||
const pipeKeys = (P, plan) => STAGE_META.map(x => x[0]).filter(k => plan.includes(k) || P.stages[k]);
|
||||
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]);
|
||||
const keys = pipeKeys(P, plan);
|
||||
const el = P.elapsed || {}; // 累计用时(跨停止/续跑),没有就退回本段
|
||||
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 === 0) return [nm, 'done', '用时 ' + hms(el[k] != null ? el[k] : 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', '中断'];
|
||||
@@ -528,24 +630,41 @@ function stages(s) {
|
||||
}
|
||||
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('');
|
||||
$('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${st === 'active' ? ' big' : ''}">${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 => {
|
||||
// GPU + CPU(同一个框:上面是 GPU 环 + 显存/温度/功耗,下面接 CPU 总占用与每核条)
|
||||
const G = s.gpu || [], CP = s.cpu || {};
|
||||
const RESV = s.vram_reserve_gb ?? 1; // 给系统留的余量(GB):训练进程启动时按"整卡 − 已用 − 余量"自设上限(浮动)
|
||||
const hr = '<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">';
|
||||
const gr = (k, v, p, c, t) => `<div class="gr"><div class="l"><span${t ? ` title="${t}"` : ''}>${k}</span><b>${v}</b></div><div class="mini"><div style="width:${Math.min(100, p)}%;background:${c}"></div></div></div>`;
|
||||
$('gputime').textContent = (G.length || CP.n) ? '实时' : '';
|
||||
const gpuBlk = 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 BUD = (s.vram_budget_gb || (g.mem_total / 1024 - RESV)) * 1024, avail = BUD / 1024 - g.mem_used / 1024;
|
||||
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="gpurows">${gr('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB · 空 ${avail.toFixed(1)}`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'), `可用 ${(BUD / 1024).toFixed(0)} GB = 整卡 ${(g.mem_total / 1024).toFixed(0)} GB − 给系统的余量 ${RESV} GB;训练进程启动时按「可用 − 那一刻已用」再自设本进程上限,别人占多少自动扣掉(ml/common/vram.py)`)}
|
||||
${gr('温度', `${g.temp} °C`, g.temp, tc)}${gr('功耗', 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');
|
||||
}).join(hr) : '';
|
||||
const cpuBlk = CP.n ? (() => {
|
||||
const tot = CP.total ?? 0, C = 2 * Math.PI * 50, col = tot > 85 ? css('--bad') : tot > 60 ? css('--warn') : css('--ok');
|
||||
const ct = CP.coreT || [], ctMax = ct.length ? Math.max(...ct) : null;
|
||||
const tcol = v => v > 98 ? css('--bad') : v > 90 ? css('--warn') : css('--c6');
|
||||
const bars = (CP.per || []).map((v, i) => `<div title="核 ${i + 1}: ${v}%${ct[i] != null ? ' · ' + ct[i] + ' °C' : ''}" style="flex:1;min-width:0;height:100%;border-radius:2px;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.18 + 0.82 * Math.min(1, v / 100)}"></div>`).join('');
|
||||
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 - tot / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
|
||||
<div class="c"><div><b>${tot}%</b><span>CPU</span></div></div></div>
|
||||
<div class="gpurows">${gr('占用', `${tot}% · ${CP.n} 核`, tot, col)}
|
||||
${CP.temp != null ? gr('温度', `${CP.temp} °C${ctMax != null && ctMax !== CP.temp ? `(最高核 ${ctMax})` : ''}`, CP.temp, tcol(CP.temp), 'MSI Afterburner 的共享内存;鼠标停在核条上还能看每核温度') : ''}
|
||||
${CP.power != null ? gr('功耗', `${Math.round(CP.power)} W`, 100 * CP.power / 253, css('--c7'), 'CPU 封装功耗(满标度按 14700K 的 PL2 253 W 画)') : ''}
|
||||
<div class="cores">${bars}</div></div></div>
|
||||
<div class="gname">${CP.model || 'CPU'}${CP.temp == null ? ' · 开 MSI Afterburner 才能读 CPU 温度/功耗' : ''} · 每核占用(深色=忙)</div>`;
|
||||
})() : '';
|
||||
$('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil');
|
||||
|
||||
// 数据
|
||||
const D = s.data || {}, st = D.stats, ix = D.index;
|
||||
@@ -596,32 +715,39 @@ function renderPipe(s) {
|
||||
: 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>`;
|
||||
? `<button class="danger" onclick="stopJob()">■ 停止</button><button class="ghost" disabled title="训练进行中:先点「停止」,再点「重新训练」">⟳ 重新训练</button><span class="hint">${who}</span>`
|
||||
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><button class="ghost" onclick="openStart({mode:'from',from:'train',title:'重新训练'})" ${ck.problems && ck.problems.length ? 'disabled' : ''} title="从 train 阶段重做:旧模型与评测移到 ml/archive/(不删),生成的数据、打包样本、门禁都保留">⟳ 重新训练</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);
|
||||
const keys = pipeKeys(P, plan); // 含重训时保留的已完成阶段,与上方步骤条同口径
|
||||
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); }
|
||||
for (const k of keys) { 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 doneN = keys.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 allDone = !cur && doneN === keys.length;
|
||||
let remain = 0, calib = false; for (const k of keys) { const r = P.stages[k]; if (!r || r.rc == null) { if (k === cur && pr.eta_s != null) { remain += pr.eta_s / 3600; calib = true; } else 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>`
|
||||
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${keys.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="now">状态<b>未在运行</b>已完成 ${doneN} / ${keys.length} 步</div>`;
|
||||
const esc2 = x => String(x).replace(/&/g, '&').replace(/</g, '<');
|
||||
const fr = failed && P.stages[failed], stopped = !!fr && (fr.rc === -15 || fr.rc === 143);
|
||||
const timedOut = !!fr && ((fr.info || {}).timed_out || fr.rc === 124);
|
||||
const why = !fr ? '' : stopped ? '已手动停止 —— 点「继续训练」从停下的地方接着跑' : timedOut ? '超过时间上限,判定卡死' : `失败(返回码 ${fr.rc})`;
|
||||
const lastErr = (() => { const t = $('term'); if (!t) return ''; const ls = (t.innerText || '').split(/\r?\n/).filter(x => /Error|error:|Traceback|不通过|失败|超过时间上限/.test(x)); return ls.length ? ls[ls.length - 1].slice(0, 300) : ''; })();
|
||||
$('alert').innerHTML = failed ? `<div class="alert ${stopped ? 'warn' : 'bad'}"><div class="alerth">✕ 停在「${SM[failed]?.n || failed}」 · ${why}</div>${lastErr ? `<div class="alerterr">${esc2(lastErr)}</div>` : ''}<div class="alerttip">完整报错见下方「终端输出」;修好后点「继续训练」,已完成的步骤会自动跳过${stopped ? '、从停下的那一步接着跑' : ''}</div></div>` : '';
|
||||
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${keys.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>
|
||||
${cur ? `<div class="sublabel"><span>当前步骤 · ${SM[cur]?.n}</span>${pr.frac != null ? `<b>${pct(pr.frac)}</b>` : '<span>无法计量,按用时估</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 class="fact"><div class="k">总用时</div><div class="v">${hms(keys.reduce((a, k) => a + ((P.elapsed || {})[k] || 0), 0))}</div></div>
|
||||
<div class="fact"><div class="k">本步用时</div><div class="v">${cur ? hms((P.elapsed || {})[cur] || 0) : '—'}</div></div>
|
||||
<div class="fact"><div class="k">${cur && pr.eta_s != null ? '本步预计剩余(实测速率)' : '本步粗估 / 上限'}</div><div class="v">${cur ? (pr.eta_s != null ? hmsShort(pr.eta_s) + ' / 上限 ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600) : hmsShort((SM[cur]?.w || 0) * 3600) + ' / ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600)) : '—'}</div></div>
|
||||
<div class="fact"><div class="k">全流程剩余(${calib ? '实测外推' : '粗估'})</div><div class="v">${allDone ? '—' : '约 ' + hmsShort(remain * 3600)}</div></div>
|
||||
</div>${btns}`;
|
||||
}
|
||||
function renderJob() {
|
||||
@@ -641,20 +767,28 @@ async function pollConsole() {
|
||||
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() {
|
||||
function openStart(preset) {
|
||||
const P = lastStatus && lastStatus.pipeline, ck = lastStatus && lastStatus.check;
|
||||
if (ck && ck.problems && ck.problems.length) { toast('环境自检未通过:' + ck.problems.join(';')); return; }
|
||||
const pre = preset || {};
|
||||
if (pre.mode) $('fMode').value = pre.mode;
|
||||
if (pre.from) $('fFrom').value = pre.from;
|
||||
$('mdTitle').textContent = pre.title || (P ? '继续训练' : '开始训练');
|
||||
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/<时间>/(不删除)。');
|
||||
if ($('fMode').value === 'from' && P) { const nm = SM[$('fFrom').value]?.n || $('fFrom').value; msgs.push(`从「${nm}」开始重训:该阶段及之后的旧产物移到 ml/archive/<时间>/(不删除),之前的阶段保持不动。${$('fFrom').value === 'gen' ? '注意:生成的数据会被归档,重跑约 7 小时。' : ''}`); }
|
||||
if ($('fMode').value === 'fresh' && P) msgs.push('全部重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。生成数据要重跑(约 7 小时)。');
|
||||
$('fFrom').disabled = $('fMode').value !== 'from';
|
||||
$('fGo').textContent = $('fMode').value === 'from' ? '▶ 开始重训' : ($('fMode').value === 'fresh' ? '▶ 全部重跑' : '▶ 开始');
|
||||
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 mode = $('fMode').value, from = mode === 'from' ? $('fFrom').value : 'kin';
|
||||
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(), FRESH_FROM: mode === 'from' ? from : '' };
|
||||
const body = JSON.stringify({ fresh: mode === 'fresh', from, to: 'accept', 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();
|
||||
@@ -668,14 +802,43 @@ async function stopJob() {
|
||||
$('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'); };
|
||||
/* 记住上次的设置(localStorage,按站点保存):下次开页面直接是这套参数。
|
||||
注意「运行方式」不记忆——万一记成"全部重跑",点开始会把数据和模型全归档,风险不对等。 */
|
||||
const PREF = 'mlmon.pref.v1', PREF_IDS = ['fFrom', 'fCfg', 'fCons', 'fN', 'fP', 'fT', 'fR', 'fG'];
|
||||
function prefSave() {
|
||||
try {
|
||||
const o = {};
|
||||
for (const id of PREF_IDS) o[id] = $(id).value;
|
||||
o.smooth = $('smooth').value; o.logy = logyOn ? 1 : 0;
|
||||
localStorage.setItem(PREF, JSON.stringify(o));
|
||||
} catch (e) { }
|
||||
}
|
||||
function prefLoad() {
|
||||
let o = null; try { o = JSON.parse(localStorage.getItem(PREF) || 'null'); } catch (e) { }
|
||||
if (!o) return;
|
||||
for (const id of PREF_IDS) {
|
||||
const v = o[id]; if (v == null || v === '') continue;
|
||||
const el = $(id); if (!el) continue;
|
||||
if (el.tagName === 'SELECT') { if ([...el.options].some(x => x.value === v)) el.value = v; }
|
||||
else if (/^[0-9]*\.?[0-9]*$/.test(String(v))) el.value = v; // 参数框只接受数字,脏值不落地
|
||||
}
|
||||
if (o.smooth != null) { $('smooth').value = o.smooth; $('smoothv').textContent = Number(o.smooth).toFixed(2); }
|
||||
if (o.logy) { logyOn = true; $('logy').classList.add('on'); }
|
||||
}
|
||||
/* 直接开「重新训练」弹窗:?start=train / ?start=gen…(便于收藏和脚本化截图验证) */
|
||||
{ const m = location.search.match(/[?&]start=([a-z0-9]+)/);
|
||||
if (m && SM[m[1]]) setTimeout(() => openStart({ mode: 'from', from: m[1], title: '重新训练' }), 1200); }
|
||||
|
||||
/* ===================== 控件 ===================== */
|
||||
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); render(); };
|
||||
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); render(); };
|
||||
prefLoad();
|
||||
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); prefSave(); render(); };
|
||||
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); prefSave(); render(); };
|
||||
for (const id of PREF_IDS) { $(id).onchange = prefSave; $(id).oninput = prefSave; }
|
||||
$('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();
|
||||
api('/api/explore').then(d => { const el = $('explore'); el.textContent = (d && d.md) ? d.md : '(demo 模式:无探索日志)'; }).catch(() => { const el = $('explore'); el.textContent = '(无法读取 docs/探索日志.md)'; });
|
||||
$('auto').onclick = () => { const on = !$('auto').classList.contains('on'); $('auto').classList.toggle('on', on); clearInterval(timer); if (on) timer = setInterval(poll, 1000); };
|
||||
timer = setInterval(poll, 1000); poll();
|
||||
|
||||
/* =====================================================================
|
||||
示例数据:两种场景,每次刷新让"运行中"的那条日志向前推进。
|
||||
@@ -696,7 +859,8 @@ function demoApi() {
|
||||
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()) } };
|
||||
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()),
|
||||
ol_fk_mm: 5.5 * Math.exp(-x / 12000) + 2.0 + 0.25 * gauss(), ol_best_mm: 3.8 * Math.exp(-x / 12000) + 1.7 + 0.16 * gauss(), ol_div: Math.min(0.9, 0.16 + 0.45 * (1 - Math.exp(-x / 9000)) + 0.02 * gauss()) } };
|
||||
}
|
||||
function trainLog(total, from, to, sps, t0, evalEvery = 2000, wta = false) {
|
||||
const L = [];
|
||||
@@ -715,10 +879,10 @@ function demoApi() {
|
||||
}
|
||||
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 mkPipe = (done, cur, curStartH) => { const t = now(); const st = {}, el = {}; 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: {} }; el[k] = dur * 3600; tt -= dur * 3600; }
|
||||
if (cur) { st[cur] = { start: t - curStartH * 3600, end: null, rc: null, info: {} }; el[cur] = curStartH * 3600; }
|
||||
return { started: tt, plan: ['kin', 'r4', 'gate1', 'gen', 'prep', 'train', 'dagger', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, elapsed: el, 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',
|
||||
@@ -804,6 +968,7 @@ function demoApi() {
|
||||
fn.scenario = setup;
|
||||
return fn;
|
||||
}
|
||||
window.__boot.ok = true; // 正文脚本跑到了最后一行,看门狗不用再报"脚本没启动"
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+243
-22
@@ -17,7 +17,7 @@
|
||||
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
|
||||
import argparse, glob, json, os, platform, 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
|
||||
@@ -68,7 +68,102 @@ def read_log(name, offset):
|
||||
return {'records': recs, 'offset': offset + end, 'size': size, 'reset': offset == 0, 'mtime': os.path.getmtime(f)}
|
||||
|
||||
|
||||
_CPU = {'v': {}}
|
||||
|
||||
|
||||
def cpu_name():
|
||||
"""CPU 型号:psutil 只有核数,型号得自己读——Windows 在注册表,Linux 在 /proc/cpuinfo。"""
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'HARDWARE\DESCRIPTION\System\CentralProcessor\0') as k:
|
||||
return re.sub(r'\s+', ' ', winreg.QueryValueEx(k, 'ProcessorNameString')[0]).strip()
|
||||
with open('/proc/cpuinfo') as f:
|
||||
for ln in f:
|
||||
if ln.startswith('model name'):
|
||||
return ln.split(':', 1)[1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
return platform.processor().strip()
|
||||
|
||||
|
||||
class CpuSampler(threading.Thread):
|
||||
"""后台每秒阻塞采样一次 CPU。cpu_percent 是"距上次调用"的差值:放在请求里算,窗口取决于两次
|
||||
HTTP 请求的间隔(网页 1 秒轮询 + 手动 curl 会把它压成几毫秒,读数恒为 0)——所以单独一个线程按固定
|
||||
1 秒窗口取,status 只读最近一次。"""
|
||||
def __init__(self): super().__init__(daemon=True); self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import psutil
|
||||
name = cpu_name() # 型号只读一次,别每秒翻注册表
|
||||
psutil.cpu_percent(interval=None, percpu=True) # 打桩:第一次恒为 0,丢掉
|
||||
while True:
|
||||
per = psutil.cpu_percent(interval=1.0, percpu=True)
|
||||
_CPU['v'] = {'total': round(sum(per) / max(1, len(per)), 1), 'per': [round(x) for x in per], 'n': len(per), 'model': name}
|
||||
except Exception:
|
||||
_CPU['v'] = {}
|
||||
|
||||
|
||||
def cpu():
|
||||
return {**_CPU['v'], **mahm()}
|
||||
|
||||
|
||||
_MAHM = {'t': 0.0, 'v': {}}
|
||||
|
||||
|
||||
def mahm():
|
||||
"""借 MSI Afterburner 的共享内存(MAHMSharedMemory)读 CPU 温度 / 封装功耗——Windows 上没有别的免驱动口子:
|
||||
psutil 在 Windows 没有 sensors_temperatures;ACPI 热区只有个 27.9 °C 的板温;Power Meter 计数器没有实例。
|
||||
条目布局实测:头 32 字节(签名 MAHM / 版本 / 头大小 / 条目数 / 条目大小 / 时间戳),每条 1324 字节,
|
||||
名字在前 260 字节、单位在其后 260、数值是偏移 1300 的 float(用 GPU1 power 对 nvidia-smi 校准过)。
|
||||
读数比 30 秒旧才当没读到:Afterburner 的硬件轮询周期实测 10~13 秒(不是 1 秒),阈值卡 10 秒会
|
||||
正好落在两次刷新之间、把还在用的读数丢掉(表现为温度和功耗一闪一闪地消失)。"""
|
||||
now = time.time()
|
||||
if now - _MAHM['t'] < 1.0: return _MAHM['v']
|
||||
_MAHM['t'] = now
|
||||
out = {}
|
||||
try:
|
||||
import ctypes, struct
|
||||
k = ctypes.windll.kernel32
|
||||
k.OpenFileMappingW.restype = ctypes.c_void_p
|
||||
k.OpenFileMappingW.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_wchar_p]
|
||||
k.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
h = k.OpenFileMappingW(0x0004, 0, 'MAHMSharedMemory')
|
||||
p = k.MapViewOfFile(h, 0x0004, 0, 0, 0) if h else 0
|
||||
if p:
|
||||
hd = ctypes.string_at(p, 32)
|
||||
num, esize, hsize = (int.from_bytes(hd[i * 4:i * 4 + 4], 'little') for i in (3, 4, 2))
|
||||
upd = int.from_bytes(hd[20:24], 'little')
|
||||
if 0 < num < 5000 and 32 < esize < 65536 and now - upd <= 30:
|
||||
buf = ctypes.string_at(p, hsize + num * esize)
|
||||
core = {}
|
||||
for i in range(num):
|
||||
e = buf[hsize + i * esize: hsize + (i + 1) * esize]
|
||||
nm = e[:260].split(b'\x00')[0].decode('latin1')
|
||||
if not nm: continue
|
||||
val = struct.unpack('<f', e[1300:1304])[0]
|
||||
if nm == 'CPU temperature' and 0 < val < 130: out['temp'] = round(val)
|
||||
elif nm == 'CPU power' and 0 < val < 1000: out['power'] = round(val, 1)
|
||||
elif nm.startswith('CPU') and nm.endswith(' temperature'):
|
||||
try:
|
||||
j = int(nm[3:-12])
|
||||
if 0 < val < 130: core[j] = round(val)
|
||||
except ValueError: pass
|
||||
if core: out['coreT'] = [core[k] for k in sorted(core)]
|
||||
except Exception:
|
||||
out = {}
|
||||
_MAHM['v'] = out
|
||||
return out
|
||||
|
||||
|
||||
_GPU_CACHE = {'t': 0.0, 'v': []}
|
||||
|
||||
|
||||
def gpu():
|
||||
# 网页 1 秒轮询:nvidia-smi 每次要 150–600 ms,缓存 2 秒(GPU 数字不需要更快),避免每轮都起子进程
|
||||
if time.time() - _GPU_CACHE['t'] < 2.0: return _GPU_CACHE['v']
|
||||
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)
|
||||
@@ -77,9 +172,10 @@ def gpu():
|
||||
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 []
|
||||
out = []
|
||||
_GPU_CACHE.update(t=time.time(), v=out)
|
||||
return out
|
||||
|
||||
|
||||
def last_rec(name):
|
||||
@@ -126,47 +222,134 @@ def newer(pattern, t0):
|
||||
|
||||
|
||||
def stage_progress(st, s):
|
||||
"""当前阶段的进度:返回 {'frac': 0..1 或 None(无法计量), 'label': 文字}。"""
|
||||
"""当前阶段的进度:返回 {'frac': 0..1 或 None(无法计量), 'label': 文字, 'eta_s': 按实测速率外推的剩余秒数或 None}。"""
|
||||
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))
|
||||
# 分子算全部(含之前轮次已完成的分片),分母是累计目标:补量时显示 50,008/79,688 而不是 0/79,688。
|
||||
# 2026-10-18 实测:只统计本轮开始后改过的文件会让"补量"看起来像从零开始。
|
||||
allf = glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl')) + glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl.part'))
|
||||
n = sum(LC.count(f) for f in allf)
|
||||
tgt = info.get('target_tasks') or 0
|
||||
return {'frac': min(1, n / tgt) if tgt else None, 'label': f'已生成 {n:,} / {tgt:,} 条任务'}
|
||||
new = sum(LC.count(f) for f in newer(f'{DATA}/shard_*.jsonl', t0) + newer(f'{DATA}/shard_*.jsonl.part', t0))
|
||||
# 剩余时间按"本轮已写条数 ÷ 本轮已用时"外推(条数是实时数出来的,速率就是实测的),不再用静态估时
|
||||
el = max(1.0, time.time() - t0)
|
||||
rate = new / el if new > 0 else 0
|
||||
eta = ((tgt - n) / rate if rate > 0 else None) if tgt > n else (0.0 if rate > 0 else None)
|
||||
lab = f'已生成 {n:,} / {tgt:,} 条任务' + (f'(本轮补 {new:,} 条 · {rate * 60:.0f} 条/分)' if 0 < new < n else '')
|
||||
return {'frac': min(1, n / tgt) if tgt else None, 'label': lab, 'eta_s': eta}
|
||||
if st == 'gate1':
|
||||
# 一致性门禁每 10 个任务往 stderr 打一行 "k/300"(consistency.cjs:41)。进度只读控制台日志最后 64 KB,
|
||||
# 不跑任何计算、不占 CPU —— 正在生成的节点不受影响。日志里可能还留着上一次门禁的进度行,
|
||||
# 所以只数最后一个 "consistency.cjs" 命令行之后的部分。
|
||||
txt = console(0).get('text') or ''
|
||||
cut = txt.rfind('consistency.cjs')
|
||||
m = re.findall(r'^(\d+)/(\d+)\s*$', txt[cut:] if cut >= 0 else '', re.M)
|
||||
n, N = (int(m[-1][0]), int(m[-1][1])) if m else (0, 300)
|
||||
el = time.time() - t0
|
||||
rate = n / el if n > 0 and el > 2 else 0
|
||||
return {'frac': n / N if N else None,
|
||||
'label': f'门禁 {n} / {N} 个任务' + (f' · {rate * 60:.0f} 个/分' if rate else '') if n else '准备中(日志里还没有进度行)',
|
||||
'eta_s': (N - n) / rate if rate > 0 else None}
|
||||
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 '')}
|
||||
fin = newer(f'{DATA}/samples/*.X.npy', t0)
|
||||
done = len(fin)
|
||||
if os.path.exists(os.path.join(ROOT, DATA, 'r2.json')) and newer(f'{DATA}/r2.json', t0): return {'frac': 1, 'label': '打包完成,R2 检查完成', 'eta_s': 0}
|
||||
# 2026-10-19:build_dataset 每 10% 打一行 [prep] shard_xxxx a/b;用它把"整片完成"之间的进度和 ETA 算出来
|
||||
fin_n = {os.path.basename(p).split('.')[0] for p in fin}
|
||||
part = {}
|
||||
for ln in console(0)['text'].splitlines()[-800:]:
|
||||
m = re.search(r'\[prep\] (shard_\d+) (\d+)/(\d+)', ln)
|
||||
if m: part[m.group(1)] = int(m.group(2)) / max(1, int(m.group(3)))
|
||||
run = {k: v for k, v in part.items() if k not in fin_n}
|
||||
frac = (done + sum(run.values())) / nsh if nsh else None
|
||||
el = time.time() - t0
|
||||
eta = el * (1 - frac) / frac if frac and 0.02 < frac < 1 and el > 5 else None
|
||||
lab = f'已打包 {done} / {nsh} 个分片'
|
||||
if run: lab += f' · 进行中 {len(run)} 个(平均 {100 * sum(run.values()) / len(run):.0f}%)'
|
||||
elif done == 0: lab += ' · 首波处理中,第一批完成后显示预估'
|
||||
elif nsh and done >= nsh: lab += ',R2 检查中'
|
||||
return {'frac': min(0.95, frac) if frac is not None else None, 'label': lab, 'eta_s': eta}
|
||||
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': (bs + m['step']) / (bs + m['total']), 'label': f'正式训练 {m["step"]:,} / {m["total"]:,} 步', 'run': tag,
|
||||
'eta_s': m['eta_h'] * 3600 if m.get('eta_h') else None} # 训练脚本自己按实测样本/s 算的
|
||||
if b: return {'frac': b['step'] / (bs + 24000), # 2026-10-19:正式步数按 configs/base.json 的 24000 估
|
||||
'label': f'测速 {b["step"]:,} / {bs:,} 步', 'run': f'bench_{tag}'}
|
||||
return {'frac': 0, 'label': '加载数据中'}
|
||||
if st == 'dagger': # 2026-10-18 新增:每轮 = 采起点 → 老师重规划 → 打包 → 微调(微调占大头,进度按 0.1/0.25/0.05/0.6 分)
|
||||
tag, R = info.get('tag', 'base'), int(info.get('rounds', 2))
|
||||
k = len(newer(f'{RUNS}/{tag}_dag*/done', t0)); k = min(k, R)
|
||||
base = f'第 {min(k + 1, R)} / {R} 轮'
|
||||
if k >= R: return {'frac': 1, 'label': 'DAgger 完成'}
|
||||
D, run = f'{DATA}/dag_{tag}_r{k + 1}', f'{tag}_dag{k + 1}'
|
||||
m = last_rec(run) if newer(f'{RUNS}/{run}/log.jsonl', t0) else None
|
||||
if m and m.get('total'):
|
||||
return {'frac': (k + 0.4 + 0.6 * m['step'] / m['total']) / R, 'label': f'{base}:微调 {m["step"]:,} / {m["total"]:,} 步', 'run': run,
|
||||
'eta_s': m['eta_h'] * 3600 if m.get('eta_h') else None}
|
||||
if os.path.exists(os.path.join(ROOT, D, 'collect.json')):
|
||||
nt = sum(LC.count(f) for f in glob.glob(os.path.join(ROOT, D, 'tasks_*.jsonl')))
|
||||
nd = sum(LC.count(f) for f in glob.glob(os.path.join(ROOT, D, 'shard_dag_*.jsonl')) + glob.glob(os.path.join(ROOT, D, 'shard_dag_*.jsonl.part')))
|
||||
return {'frac': (k + 0.1 + 0.25 * (nd / nt if nt else 0)) / R, 'label': f'{base}:老师重新规划 {nd:,} / {nt:,} 个起点'}
|
||||
return {'frac': k / R, 'label': f'{base}:闭环采起点'}
|
||||
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'}
|
||||
eta = None
|
||||
if r and r.get('total'):
|
||||
it, mn = r.get('it') or 0, r.get('min') or 0
|
||||
eta = (r['total'] - it) / (it / mn) * 60 if it > 0 and mn > 0.1 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', 'eta_s': eta}
|
||||
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}
|
||||
lab = f'第 {k + 1} 轮:挖错出题'
|
||||
if cur:
|
||||
name = os.path.basename(os.path.dirname(cur[-1])) # ml/runs/apg_rN
|
||||
rn = re.sub(r'\D', '', name) or '1'
|
||||
gate = os.path.join(ROOT, RUNS, 'mine', f'r{rn}', 'gate.json')
|
||||
r = last_rec(name)
|
||||
if r and r.get('total') and not os.path.exists(gate): # 这一轮还在跑(还没出 gate)
|
||||
frac_in = 0.3 + 0.6 * r['it'] / r['total']
|
||||
lab = f'第 {k + 1} 轮:APG {r["it"]:,} / {r["total"]:,} 步'
|
||||
it, mn = r.get('it') or 0, r.get('min') or 0
|
||||
eta = (r['total'] - it) / (it / mn) * 60 if it > 0 and mn > 0.1 else None
|
||||
if eta is not None and k >= 1: # 剩余轮次按已完成轮次的平均时长外推
|
||||
eta += max(0, R - k - 1) * (time.time() - t0) / k
|
||||
return {'frac': min(1, (k + frac_in) / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)', 'run': name, 'eta_s': eta}
|
||||
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)]}
|
||||
el = time.time() - t0
|
||||
eta = (2 - n) * (el / n) if n == 1 else (0.0 if n >= 2 else None) # 两次评估(纯策略 / 加安全层)按已完成的平均耗时外推
|
||||
return {'frac': n / 2, 'label': ['纯策略评估中', '加安全层评估中', '汇总'][min(n, 2)], 'eta_s': eta}
|
||||
return {'frac': None, 'label': '进行中'}
|
||||
|
||||
|
||||
def stage_elapsed(pl, now=None):
|
||||
"""每个阶段的累计用时(秒):pipeline.json 的 time{阶段: 秒} 是不含当前段的累计,运行中的阶段
|
||||
再把"这一段"实时加上。老记录(没有 time 字段)用 start/end 的差值兜底。
|
||||
网页的「总用时」=各阶段之和,「本步用时」=当前阶段这条 —— 确定/续跑都不归零。"""
|
||||
now = now or time.time()
|
||||
acc = pl.get('time') or {}
|
||||
cur = pl.get('current')
|
||||
out = {}
|
||||
for k, r in (pl.get('stages') or {}).items():
|
||||
t = acc.get(k)
|
||||
if t is None and r.get('start') and r.get('end'): t = r['end'] - r['start']
|
||||
t = t or 0
|
||||
if k == cur and r.get('start') and r.get('end') is None: t += max(0.0, now - r['start'])
|
||||
out[k] = round(t, 1)
|
||||
return out
|
||||
|
||||
|
||||
def pipeline():
|
||||
d = jload(os.path.join(ROOT, RUNS, 'pipeline.json'))
|
||||
if not d: return None
|
||||
d['elapsed'] = stage_elapsed(d)
|
||||
cur = d.get('current')
|
||||
if cur and cur in d['stages']:
|
||||
try: d['progress'] = stage_progress(cur, d['stages'][cur])
|
||||
@@ -192,8 +375,9 @@ def status():
|
||||
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
|
||||
return {'now': time.time(), 'gpu': gpu(), 'cpu': cpu(),
|
||||
'vram_reserve_gb': float(os.environ.get('VRAM_RESERVE_GIB', 1)), # 给系统留的余量(GB):训练进程上限 = 整卡 − 已用 − 这个余量(浮动,见 ml/common/vram.py)
|
||||
'vram_budget_gb': float(os.environ['VRAM_BUDGET_GB']) if os.environ.get('VRAM_BUDGET_GB') else None, # 只有显式指定才覆盖上面的口径
|
||||
'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')),
|
||||
@@ -208,7 +392,7 @@ def status():
|
||||
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]'}
|
||||
'FRESH': r'[01]', 'FRESH_FROM': r'(setup|kin|r4|gate1|gen|prep|train|dagger|m5|apg|mine|export|bench|accept)', 'R2_STRICT': r'[01]', 'AUTO_WTA': r'[01]'}
|
||||
CONSOLE = os.path.join(ROOT, RUNS, 'console.log')
|
||||
|
||||
|
||||
@@ -264,6 +448,29 @@ def lock_info():
|
||||
return dict(lk, alive=pstate.lock_alive(lk), age_s=time.time() - lk.get('beat', 0))
|
||||
|
||||
|
||||
NT_NAMES = "'node.exe','xargs.exe','bash.exe','sh.exe','timeout.exe'"
|
||||
NT_SWEEP = (r"pipeline\.sh|run_prep\.sh|run_parallel\.sh|build_dataset\.cjs|"
|
||||
r"gen_dataset\.cjs|teacher\.cjs|policy_bench\.cjs")
|
||||
|
||||
|
||||
def sweep_nt(keep_pid):
|
||||
"""taskkill /T 在 Git Bash 下只杀到 bash,xargs/node 孙进程会逃(10-18 实测:停止后还有 28 个
|
||||
gen_dataset 在跑;当晚二次踩到:两批 build_dataset 抢同一批 shard)。按命令行补扫整条流水线,
|
||||
keep_pid 的祖先链(= 控制台自己拉起的 monitor bash)不动。先杀包装再杀 node,否则 node 一死
|
||||
xargs 会立刻补下一个 shard。返回仍存活个数,-1 = pwsh 没跑成。"""
|
||||
q = ("$anc=@(); $c=%d; while ($c -gt 0 -and ($p=Get-CimInstance Win32_Process -Filter \"ProcessId=$c\" "
|
||||
"-ErrorAction SilentlyContinue)) { $anc+=$p.ProcessId; $c=$p.ParentProcessId }; "
|
||||
"$m={ $_.Name -in %s -and $_.CommandLine -match '%s' -and $anc -notcontains $_.ProcessId }; "
|
||||
"foreach ($x in @(Get-CimInstance Win32_Process | Where-Object $m | "
|
||||
"Sort-Object { if ($_.Name -eq 'node.exe') { 1 } else { 0 } })) "
|
||||
"{ Stop-Process -Id $x.ProcessId -Force -ErrorAction SilentlyContinue }; "
|
||||
"Start-Sleep -Seconds 2; "
|
||||
"@(Get-CimInstance Win32_Process | Where-Object $m).Count" % (int(keep_pid), NT_NAMES, NT_SWEEP))
|
||||
r = subprocess.run(['pwsh', '-NoProfile', '-Command', q], capture_output=True, text=True, timeout=90)
|
||||
lines = [s for s in (r.stdout or '').split() if s.isdigit()]
|
||||
return int(lines[-1]) if lines else -1
|
||||
|
||||
|
||||
def kill_session(sid):
|
||||
"""POSIX:结束会话 sid 里的所有进程(含 timeout 另起的进程组)。"""
|
||||
pids = []
|
||||
@@ -331,7 +538,7 @@ class Job:
|
||||
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'):
|
||||
if cur in ('train', 'dagger', '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):
|
||||
@@ -341,6 +548,14 @@ class Job:
|
||||
with self.lock:
|
||||
if os.name == 'nt':
|
||||
subprocess.run(['taskkill', '/T', '/F', '/PID', str(pid)], capture_output=True)
|
||||
# 补扫漏网的子孙;有残留就写进网页日志,别让用户以为停干净了
|
||||
try:
|
||||
left = sweep_nt(os.getpid())
|
||||
except Exception:
|
||||
left = -1
|
||||
if left != 0:
|
||||
with open(CONSOLE, 'a', encoding='utf-8') as f:
|
||||
f.write(f'\n[停止补扫:仍有 {left} 个流水线子进程存活(-1=补扫没跑成),把这条发我]\n')
|
||||
else:
|
||||
if ours:
|
||||
kill_session(pid)
|
||||
@@ -393,6 +608,11 @@ class H(BaseHTTPRequestHandler):
|
||||
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')
|
||||
if u.path == '/api/explore': # 2026-10-20:闭环探索日志(docs/探索日志.md)回写控制台
|
||||
try:
|
||||
with open(os.path.join(ROOT, 'docs', '探索日志.md'), encoding='utf-8') as f: md = f.read()
|
||||
except Exception: md = ''
|
||||
return self.send(200, json.dumps({'md': md}), 'application/json')
|
||||
self.send(404, 'not found', 'text/plain')
|
||||
except Exception as e:
|
||||
self.send(500, json.dumps({'error': str(e)}), 'application/json')
|
||||
@@ -429,6 +649,7 @@ def main():
|
||||
a = ap.parse_args()
|
||||
global CHECK
|
||||
CHECK = SelfCheck()
|
||||
CPUS = CpuSampler() # 每秒采一次 CPU(线程常驻,status 只读最近一次)
|
||||
srv = ThreadingHTTPServer((a.host, a.port), H); srv.remote_control = a.remote_control
|
||||
url = f'http://localhost:{a.port}/'
|
||||
print(f'训练控制台:{url} (关闭本窗口只关控制台,已开始的训练继续跑;重新打开控制台即可接着看、可停止)', flush=True)
|
||||
|
||||
+39
-11
@@ -7,6 +7,7 @@
|
||||
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
|
||||
# prep 打包训练样本 + R2 标签一致性检查
|
||||
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
|
||||
# dagger DAgger 纠偏(2026-10-18 新增):策略闭环走到的状态请老师重新规划、并入数据微调 DAG_ROUNDS 轮(ml/dagger/run.sh)
|
||||
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型,不过自动训 WTA 头再判
|
||||
# apg APG 首轮微调(从选定模型开始)
|
||||
# mine 挖错循环(3–6 轮,含门禁回滚)
|
||||
@@ -44,11 +45,12 @@ 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"
|
||||
run() { echo "== $*" >&2; "$@"; } # 回显走 stderr:gate1 用 tee 把 stdout 存成 consistency.json,回显混进去会让后面读它时 JSONDecodeError
|
||||
ALL_STAGES="setup kin r4 gate1 gen prep train dagger 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" ;;
|
||||
dagger) echo "tag=$TAG rounds=${DAG_ROUNDS:-2}" ;;
|
||||
m5) echo "tag=$TAG" ;;
|
||||
mine) echo "rounds=$ROUNDS apg_steps=$APG_STEPS" ;;
|
||||
esac; }
|
||||
@@ -76,15 +78,37 @@ on_exit() {
|
||||
trap on_exit EXIT
|
||||
trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返回码、放锁
|
||||
# ---- 训练与门禁的单元(m5 自动分支要对两种头各调一次)
|
||||
replay_ok() { # 回放自检结果有效:pass 为真、版本含 oracle 滚动(ver ≥ 2)、指纹 = 当前 kin.json 且 = 数据 shard 的指纹
|
||||
$PY - <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
r = json.load(open('ml/runs/replay_teacher.json', encoding='utf-8')); k = json.load(open('ml/assets/kin.json', encoding='utf-8'))['fp']
|
||||
except Exception: sys.exit(1)
|
||||
sys.exit(0 if r.get('pass') is True and r.get('ver', 1) >= 2 and r.get('fp') == k and r.get('data_fp') in (None, k) else 1)
|
||||
PY
|
||||
}
|
||||
train_one() { # $1 = 配置文件
|
||||
local cfg=$1 tag; tag=$(basename "$1" .json)
|
||||
# 2026-10-18:开训前先把老师轨迹放进 PyTorch 仿真口径里回放——口径对不上时闭环指标不可信,训多少步都白训(rc 5 停)
|
||||
# 2026-10-19(审查 review_closedloop F6):原来只看文件在不在——replay 失败时也写了文件(pass=false)再 rc 5,用户直接重跑 train 门禁就被跳过。
|
||||
# 现在读 pass 字段 + 指纹(kin.json、数据 shard),不过就重跑一次,仍不过 rc 5;回放同时走完整 rollout()(老师 oracle),覆盖滚动路径。
|
||||
replay_ok || run $PY -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json || true
|
||||
replay_ok || { echo "老师回放自检未通过(ml/runs/replay_teacher.json 的 pass / 指纹),闭环指标不可信,不开训"; exit 5; }
|
||||
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
|
||||
dagger_one() { # $1 = 头标签:从该头的预训练结果起做 DAgger,最终模型路径写进 ml/runs/<tag>_dagger.ckpt
|
||||
local tag=$1 ck
|
||||
[ -f ml/runs/$tag/best.pt ] || [ -f ml/runs/$tag/last.pt ] || { echo "DAgger:ml/runs/$tag/ 下没有 checkpoint(预训练没完成)"; return 3; }
|
||||
# 2026-10-19(审查 P0-2):闭环 success > 0 用 best.pt,否则 last.pt(都取 EMA 权重)
|
||||
ck=$($PY -m ml.common.ckpt pick ml/runs/$tag)
|
||||
run bash ml/dagger/run.sh $tag $ck
|
||||
}
|
||||
m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过。起点模型:CKPT > DAgger 结果 > 预训练 best.pt
|
||||
local tag=$1 ck o
|
||||
if [ -n "${CKPT:-}" ]; then ck=$CKPT; elif [ -f ml/runs/${1}_dagger.ckpt ]; then ck=$(cat ml/runs/${1}_dagger.ckpt); else ck=ml/runs/$1/best.pt; fi
|
||||
[ -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
|
||||
@@ -127,19 +151,21 @@ case $stage in
|
||||
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 续跑时跳过。
|
||||
# 最多 5 次(第五轮由 2 改 4;2026-10-18 应用户要求 4 改 5:共识 + 间隙两道过滤叠加时余量更足);已写完的补量 shard 续跑时跳过。
|
||||
s0=$((1 + NPROC))
|
||||
for k in 1 2 3 4; do
|
||||
for k in 1 2 3 4 5; 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 ))
|
||||
# 目标 = 已写完的分片条数 + 本轮要补的;不能用 pstate 里上次的 target_tasks 累加,
|
||||
# 那样每次续跑(点停止后再「继续训练」)分母都加一截,进度条会越续越少(2026-10-18 实测 67368→84728)。
|
||||
done_n=$($PY -c "import json;print(json.load(open('ml/data/stats.json', encoding='utf-8'))['tasks'])")
|
||||
$PY -m ml.pstate set gen target_tasks=$(( done_n + 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)" ;;
|
||||
$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 "警告:补 5 次后样本仍不足目标的 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; }
|
||||
@@ -152,6 +178,7 @@ case $stage in
|
||||
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
|
||||
fi ;;
|
||||
train) train_one $CFG ;;
|
||||
dagger) dagger_one $TAG ;;
|
||||
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
|
||||
@@ -159,11 +186,12 @@ case $stage in
|
||||
echo "M5:$TAG 头不通过 → 自动训练 WTA 回退头(约 3 小时)"
|
||||
$PY -m ml.pstate set m5 branch=wta
|
||||
train_one ml/configs/wta.json
|
||||
dagger_one wta
|
||||
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 ;;
|
||||
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples $(ls -d ml/data/samples_dag_* 2>/dev/null | tr '\n' ' ') --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 ;;
|
||||
@@ -184,7 +212,7 @@ case $stage in
|
||||
# 启动前自检(审查 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"
|
||||
fl=""; [ "${FRESH:-0}" = 1 ] && fl="--fresh"; [ -n "${FRESH_FROM:-}" ] && fl="--fresh-from=$FRESH_FROM"
|
||||
$PY -m ml.pstate reset ${PLAN// /,} $fl # 续跑 / 参数变了作废受影响阶段(旧产物移到 ml/archive/<时间>/)
|
||||
TODO=$($PY -m ml.pstate todo)
|
||||
echo "计划:$PLAN"; echo "待跑:${TODO:-(全部已完成)}"
|
||||
|
||||
+49
-19
@@ -2,16 +2,33 @@
|
||||
* 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)
|
||||
* Y [N,16,6] 未来 16 步每步增量 / (ACT_SCALE·STEP),夹在 ±1(到终点后补 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 步的增量。 */
|
||||
* 增广(2026-10-18 首次正式预训练后加大):40% 样本对 q_t 加 σ=AUG_SIGMA(默认 3°)噪声,标签改为 AUG_BACK(默认 12)步内线性收回老师轨迹;
|
||||
* 噪声态须无干涉且间隙 > MIN_GAP_MM,收回途中第 3/6/9/12 步也须无干涉(σ 加大后直接线性拉回可能穿过障碍)。
|
||||
* 原为 30%、σ=1°、8 步:闭环一偏出 1° 就是没见过的状态,首次 21850 步闭环到达 0%。更大的偏离由 DAgger 阶段补(ml/dagger)。
|
||||
* 已提交前缀长度 r ∈ {0..3} 均匀随机,前缀取老师接下来 r 步的增量。
|
||||
* meta [<shard>.meta.json] 打包参数:data_ver、act_scale、增广参数、sig(run_prep.sh 据 sig 判断要不要重新打包)。
|
||||
* 2026-10-19(审查 review_data_actions C2):
|
||||
* - 标签按 ACT_SCALE·STEP 归一化(默认 1.25,DATA_VER 2)。原来按 STEP 归一化,巡航时主导轴标签恰好 = ±1、执行端又夹在 ±1,
|
||||
* 模型在 ±1 附近的对称误差有一半被截掉,主导轴系统性偏慢;留 25% 余量后老师标签最大 0.8,执行端的 ±1 只防越界。
|
||||
* 运行时 inc = clamp(act, ±1)·act_scale·STEP(closed_loop.py / apg / ik-policy.js 同口径,act_scale 经 checkpoint 与 ONNX meta 传递);
|
||||
* - 增广收回标签的亏空结转:被 ±1 截掉的部分记在 prevP 里、下一步补,原来 prevP = 目标点,截掉就丢了(教的是「落后了别追」);
|
||||
* - DAgger 任务带 qPrev / v0(gen_dataset.cjs 写入):t=0 样本用真实上一帧,时间律从当前速度起步。
|
||||
* 特征(83 维)定义不变,FEAT_VER 不变;数据版本由 meta.data_ver 区分,旧打包没有 meta → 训练按 act_scale 1.0 读。 */
|
||||
'use strict';
|
||||
const fs = require('fs'), path = require('path');
|
||||
/* 打包参数(环境变量)与签名:run_prep.sh 用 `node build_dataset.cjs --sig` 取当前签名,与已打包 shard 的 meta.sig 不同就重新打包 */
|
||||
const DATA_VER = 2;
|
||||
const MIN_GAP_MM = +(process.env.MIN_GAP_MM || 3);
|
||||
const AUG_P = +(process.env.AUG_P || 0.4), AUG_SIGMA = +(process.env.AUG_SIGMA || 3), AUG_BACK = +(process.env.AUG_BACK || 12);
|
||||
const ACT_SCALE = +(process.env.ACT_SCALE || 1.25);
|
||||
const SIG = ['v' + DATA_VER, 's' + ACT_SCALE, 'p' + AUG_P, 'sg' + AUG_SIGMA, 'b' + AUG_BACK, 'g' + MIN_GAP_MM].join('|');
|
||||
if (process.argv[2] === '--sig') { console.log(SIG); process.exit(0); }
|
||||
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); }
|
||||
if (!IN) { console.error('usage: build_dataset.cjs <shard.jsonl> [outDir] | --sig'); 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');
|
||||
@@ -26,35 +43,45 @@ const hash = s => { let h = 2166136261; for (const c of s) h = Math.imul(h ^ c.c
|
||||
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) {
|
||||
/* 2026-10-18:认证最小间隙 < 3 mm 的老师轨迹不进训练集(MIN_GAP_MM);噪声增广接受地板同步为 3 mm(原 1 mm)。 */
|
||||
const ASTEP = STEP.map(v => v * ACT_SCALE); // 每单位动作对应的度数
|
||||
const NT = lines.length, PSTEP = Math.max(1, Math.floor(NT / 10));
|
||||
for (let li = 0; li < NT; li++) {
|
||||
if (li % PSTEP === 0) console.log(`[prep] ${base} ${li}/${NT}`); // 每 10% 一行,28 个分片并行也看得见进度
|
||||
const line = lines[li];
|
||||
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 tp = timeParam(P, { v0: r.v0 || 0 }); // DAgger 起点带速度:时间律从 v0 起步(gen_dataset.cjs 写 v0)
|
||||
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 });
|
||||
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, qp0: r.qPrev || null, 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 q = traj[t].slice(), qPrev = (t > 0 ? traj[t - 1] : (r.qPrev || 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; });
|
||||
if (rnd() < AUG_P) {
|
||||
const qn = q.map((v, j) => { let x = v + AUG_SIGMA * 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 o2 = qn.map((v, j) => v - q[j]);
|
||||
const back = k => traj[Math.min(N, t + k)].map((v, j) => v + o2[j] * Math.max(0, 1 - k / AUG_BACK));
|
||||
if (S.collideFree(qn) && S.clearance(qn, 5) > MIN_GAP_MM && [3, 6, 9, 12].every(k => k >= AUG_BACK || S.collideFree(back(k)))) {
|
||||
off = o2; 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 pos = k => { const i = Math.min(N, t + k); const w = aug ? Math.max(0, 1 - k / AUG_BACK) : 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; }
|
||||
/* prevP = 按标签实际会走到的位置:被 ±1 截掉的亏空留在 prevP 与目标之差里,下一步接着补(2026-10-19,C2) */
|
||||
let prevP = q.slice();
|
||||
for (let k = 1; k <= H; k++) {
|
||||
const pk = pos(k);
|
||||
for (let j = 0; j < 6; j++) { const v = Math.max(-1, Math.min(1, (pk[j] - prevP[j]) / ASTEP[j])); y[(k - 1) * 6 + j] = v; prevP[j] += v * ASTEP[j]; }
|
||||
}
|
||||
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); }
|
||||
for (let k = 0; k < rr; k++) { const row = []; for (let j = 0; j < 6; j++) row.push(y[k * 6 + j] * ASTEP[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);
|
||||
@@ -74,7 +101,10 @@ 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');
|
||||
/* meta 在 tasks.jsonl 之前写:tasks.jsonl 仍是「本 shard 打包完成」的标记 */
|
||||
fs.writeFileSync(path.join(OUTDIR, base + '.meta.json'), JSON.stringify({ data_ver: DATA_VER, act_scale: ACT_SCALE, aug_p: AUG_P, aug_sigma: AUG_SIGMA,
|
||||
aug_back: AUG_BACK, min_gap_mm: MIN_GAP_MM, aug_carry: true, sig: SIG }));
|
||||
// 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 }));
|
||||
console.log(JSON.stringify({ shard: base, tasks: tasks.length, samples: n, aug: nAug, augRejected: nAugRej, gapDrop: nGapDrop, featVer: POL.FEAT_VER, dataVer: DATA_VER, actScale: ACT_SCALE }));
|
||||
|
||||
+6
-1
@@ -2,6 +2,8 @@
|
||||
# 并行打包样本:bash ml/prep/run_prep.sh [进程数=CPU 核数]
|
||||
# 2026-10-18 断点续跑:samples/<shard>.tasks.jsonl 存在且比 shard 新 → 该 shard 已打包完,跳过;
|
||||
# shard 已不存在的旧打包结果删掉(不让上一批数据混进来)。中断后重跑只补没打完的 shard。
|
||||
# 2026-10-19(审查 review_closedloop F11):打包参数(数据版本、ACT_SCALE、AUG_P/AUG_SIGMA/AUG_BACK、MIN_GAP_MM)变了也要重打:
|
||||
# build_dataset.cjs --sig 给出当前签名,已打包 shard 的 <shard>.meta.json 里 sig 不同(或没有 meta 的旧打包)→ 重新打包。
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
P=${1:-$(nproc)}
|
||||
@@ -11,10 +13,13 @@ 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
|
||||
SIG=$(node ml/prep/build_dataset.cjs --sig)
|
||||
echo "[prep] 打包签名 $SIG"
|
||||
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
|
||||
if [ -f $D/$b.tasks.jsonl ] && [ $D/$b.tasks.jsonl -nt "$f" ] && grep -qF "\"sig\":\"$SIG\"" $D/$b.meta.json 2>/dev/null; then continue; fi
|
||||
rm -f $D/$b.tasks.jsonl # 先撤完成标记:重打中途断了不会被当成已完成
|
||||
todo+=("$f")
|
||||
done
|
||||
echo "[prep] 共 $(ls ml/data/shard_*.jsonl | wc -l) 个 shard,待打包 ${#todo[@]} 个"
|
||||
|
||||
+48
-15
@@ -1,6 +1,6 @@
|
||||
"""流水线状态与互斥锁(单一写者:所有对 ml/runs/pipeline.json / pipeline.lock 的写都经过本模块,原子替换)。
|
||||
pipeline.sh 自动调用,一般不需要手动运行:
|
||||
python -m ml.pstate reset <阶段1,...> [--fresh] # `all` 开始:保留 rc=0 的已完成阶段(续跑);参数变了只作废受影响的阶段;--fresh 全部作废
|
||||
python -m ml.pstate reset <阶段1,...> [--fresh] # `all` 开始:保留 rc=0 的已完成阶段(续跑);参数变了只作废受影响的阶段;--fresh 全部作废;--fresh-from=<阶段> 只作废该阶段及之后
|
||||
python -m ml.pstate todo # 打印计划中尚未完成的阶段(空格分隔)
|
||||
python -m ml.pstate begin <阶段> [键=值 ...] # 阶段开始
|
||||
python -m ml.pstate set <阶段> 键=值 ... # 阶段中途更新
|
||||
@@ -13,7 +13,9 @@ pipeline.sh 自动调用,一般不需要手动运行:
|
||||
心跳超过 LOCK_STALE_S 未刷新视为陈旧(进程被强杀),可接管。
|
||||
- 续跑(缺口 1):reset 默认保留已完成阶段;参数变了只作废受它影响的阶段(AFFECTS),其产物移到 ml/archive/,避免新旧参数混用。
|
||||
- 阶段表 STAGES 是估时 / 超时 / 网页进度权重的唯一来源(审查 N-14:网页权重与耗时表同源)。
|
||||
- 停止也走本模块(审查 N-12:此前 monitor 直写 pipeline.json,与子进程竞态)。"""
|
||||
- 停止也走本模块(审查 N-12:此前 monitor 直写 pipeline.json,与子进程竞态)。
|
||||
- 累计用时(2026-10-18):每个阶段跑过的时间并进顶部 time{阶段: 秒},跨「停止 → 继续训练」累加,
|
||||
网页的「总用时 / 本步用时」据此显示;产物被归档(要重做)的阶段清零,其余保留。"""
|
||||
import glob, json, os, shutil, socket, sys, time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -24,24 +26,32 @@ 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),
|
||||
('gen', '生成老师轨迹', 5.0), ('prep', '打包样本', 2.0), ('train', '预训练', 4.5), ('dagger', 'DAgger 纠偏', 3.5), ('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'])
|
||||
TIMEOUT_H['m5'] = 3 * (EST_H['train'] + EST_H['dagger'] + 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')}}
|
||||
AFFECTS = {**{k: _from('gen') for k in ('N_TASKS', 'NPROC', 'CONSENSUS', 'MIN_GAP_MM')},
|
||||
# TARGET_SAMPLES 只决定"补量到多少":不作废 gen 的已有分片(重跑 gen 会跳过写完的 shard、按差额补量),
|
||||
# 但样本集会变,所以 prep 及之后作废。2026-10-18 实测:把它当"从 gen 作废"会让 5 小时的 5 万条分片白跑。
|
||||
'TARGET_SAMPLES': ['gen'] + _from('prep'),
|
||||
# 2026-10-19:打包参数(标签尺度、增广)变了 → prep 起作废;run_prep.sh 另按 meta.sig 只重打参数不符的 shard
|
||||
**{k: _from('prep') for k in ('ACT_SCALE', 'AUG_P', 'AUG_SIGMA', 'AUG_BACK')},
|
||||
'CFG': ['r4'] + _from('train'), **{k: _from('dagger') for k in ('DAG_ROUNDS', 'DAG_N', 'DAG_STEPS')},
|
||||
**{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_*'],
|
||||
'train': ['ml/runs/base', 'ml/runs/wta', 'ml/runs/small', 'ml/runs/bench_*', 'ml/runs/replay_teacher.json',
|
||||
'ml/runs/review_*'], # 评审快照包(_review_pack.py 从 base 打的,内含 log.jsonl 会被监控页当成一条"预训练"运行);重做 train 时一并归档
|
||||
'dagger': ['ml/runs/*_dag[0-9]*', 'ml/runs/*_dagger.ckpt', 'ml/data/dag_*', 'ml/data/samples_dag_*'],
|
||||
'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'],
|
||||
@@ -66,7 +76,7 @@ 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': {}}
|
||||
return {'started': time.time(), 'plan': None, 'stages': {}, 'current': None, 'params': {}, 'time': {}}
|
||||
|
||||
|
||||
def save(d):
|
||||
@@ -143,11 +153,21 @@ def todo(d=None):
|
||||
return [s for s in (d.get('plan') or []) if (d['stages'].get(s) or {}).get('rc') != 0]
|
||||
|
||||
|
||||
def _acc_time(d, st, now):
|
||||
"""把"这一段"的用时并进累计 time{阶段: 秒}。幂等:stage.end 已写就不再加,停止(mark_stopped)
|
||||
与流水线的 end 都会调,重复调用不会双记。"""
|
||||
s = d['stages'].get(st)
|
||||
if s and s.get('start') and s.get('end') is None:
|
||||
t = d.setdefault('time', {})
|
||||
t[st] = round(t.get(st, 0) + max(0.0, now - s['start']), 1)
|
||||
|
||||
|
||||
def mark_stopped(rc=-15):
|
||||
"""网页「停止」后调用:当前阶段记为被停止,释放锁。"""
|
||||
d = load(); st = d.get('current')
|
||||
"""网页「停止」后调用:当前阶段记为被停止、这段用时并进累计,释放锁。"""
|
||||
d = load(); st = d.get('current'); now = time.time()
|
||||
if st and st in d['stages'] and d['stages'][st].get('end') is None:
|
||||
d['stages'][st].update(end=time.time(), rc=rc)
|
||||
_acc_time(d, st, now)
|
||||
d['stages'][st].update(end=now, rc=rc)
|
||||
d['current'] = None
|
||||
save(d); lock_release()
|
||||
|
||||
@@ -179,22 +199,34 @@ def main():
|
||||
if cmd == 'reset':
|
||||
plan = rest[0].split(',') if rest and rest[0] else []
|
||||
pn = params_now(); prev = d.get('params') or {}
|
||||
changed = [k for k in PARAM_KEYS if prev and prev.get(k) != pn[k]]
|
||||
ff = next((r.split('=', 1)[1] for r in rest if r.startswith('--fresh-from=')), '')
|
||||
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 ff in ORDER: bad |= set(ORDER[ORDER.index(ff):])
|
||||
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
|
||||
# 受影响 / 未完成的阶段:旧产物挪走,免得续训、跳过 shard 时把旧参数的东西接上。
|
||||
# 例外:只改了目标样本量时,gen 要重跑补量,但已有分片必须留着(run_parallel 跳过写完的 shard、
|
||||
# 按差额补量),否则 5 小时的生成白跑。
|
||||
no_archive = {'gen'} if 'TARGET_SAMPLES' in changed else set()
|
||||
arch = [st for st in ORDER if st in bad and st not in no_archive]
|
||||
arc = archive(arch) if arch else None
|
||||
if arc: print(f'旧产物已移到 {arc}/(未删除)', file=sys.stderr)
|
||||
d = {'started': now, 'plan': plan, 'stages': keep, 'current': None, 'params': pn}
|
||||
# 累计用时跨续跑保留;只有产物被挪走的阶段(要从头重做)才清零
|
||||
tm = {k: v for k, v in (d.get('time') or {}).items() if k not in arch}
|
||||
d = {'started': now, 'plan': plan, 'stages': keep, 'current': None, 'params': pn, 'time': tm}
|
||||
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]
|
||||
prev = d['stages'].get(st) or {}
|
||||
# 老记录(本次改动之前完成的)没有累计字段:用上一次的整段补上,免得续跑时用时从 0 起
|
||||
if prev.get('start') and prev.get('end') and st not in (d.get('time') or {}):
|
||||
d.setdefault('time', {})[st] = round(prev['end'] - prev['start'], 1)
|
||||
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':
|
||||
@@ -203,6 +235,7 @@ def main():
|
||||
elif cmd == 'end':
|
||||
st, rc = rest[0], int(rest[1]) if len(rest) > 1 else 0
|
||||
s = d['stages'].setdefault(st, {'start': now, 'info': {}})
|
||||
_acc_time(d, st, now)
|
||||
s['end'], s['rc'] = now, rc
|
||||
if d.get('current') == st: d['current'] = None
|
||||
else:
|
||||
|
||||
+370
-69
@@ -1,19 +1,23 @@
|
||||
"""闭环仿真(验证 / 挖错 / 门禁共用,PyTorch 批量,无梯度)——按部署节奏运行:
|
||||
50 Hz 执行;队列 ≤ 3 步时推理一次(已提交前缀 r = 队列长度),提交接下来 3 步;
|
||||
冷启动用价值头对候选末态打分(短前瞻),之后按与上一计划的一致性选候选;
|
||||
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点(到达 < 5° 或 1.5 s 超时后切下一个)。
|
||||
冷启动用价值头对候选末态打分(短前瞻),之后按与上一条**真正提交**的计划的一致性选候选;
|
||||
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点,按「越过顶点」规则切下一个(与标签同口径,见 sub_advance)。
|
||||
safety=False 时不做任何碰撞拦截(暴露真实错误,挖错用);safety=True 时近似部署安全层(候选提交步有干涉则换下一条,全不过则停一步)。
|
||||
动作反归一化:inc = clamp(act, −1, 1) · act_scale · STEP;act_scale 来自 checkpoint(旧模型缺省 1.0,见 ml/common/ckpt.load_policy)。
|
||||
碰撞与到达只在每题自己的时限内计(短题与长题同批时不把超时后的部分算进去)。
|
||||
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
|
||||
from ml.common.features import features, STEP, VMAX, H, GAP_CAP, D2R, wrap180
|
||||
|
||||
DT = 0.02
|
||||
|
||||
|
||||
FIRST_DEG = 5.0 # R7 首次方向判定阈值(度):小于它的抖动不算选边
|
||||
SUB_LEAD_S = 0.02 # 子目标「越过顶点」提前量(s):标签在老师 τ > τ_顶点 − 0.02 时就换到下一个顶点(build_dataset.cjs remain 过滤)
|
||||
SUB_REACH_DEG, SUB_TIMEOUT_S = 5.0, 1.5 # 旧规则(sub_rule='timeout'):到达 < 5° 或 1.5 s 超时
|
||||
|
||||
def tasks_to_tensors(tasks, device):
|
||||
q0 = torch.tensor([t['q0'] for t in tasks], dtype=torch.float32, device=device)
|
||||
@@ -28,6 +32,12 @@ def tasks_to_tensors(tasks, device):
|
||||
NOREF_SEC = 6.0 # 无老师参考题的定长时限(s)
|
||||
|
||||
|
||||
def time_limit(ex, max_sec=None):
|
||||
"""每题时限(s):有老师 = 老师时长 × 2(至少 3 s);无老师 = NOREF_SEC;max_sec 覆盖全部。"""
|
||||
if max_sec is not None: return torch.full_like(ex, max_sec)
|
||||
return torch.where(torch.isfinite(ex), torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC))
|
||||
|
||||
|
||||
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)
|
||||
@@ -38,23 +48,42 @@ def goal_err(kin, q, T, R):
|
||||
return e, oe
|
||||
|
||||
|
||||
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5):
|
||||
"""局部 DLS 精修:n 步增量 [B,n,6]"""
|
||||
def execution_points(kin, q, increments):
|
||||
points, midpoints = [], []
|
||||
for delta in increments.unbind(1):
|
||||
nxt = kin.clamp_box(q + delta)
|
||||
midpoints.append(q + 0.5 * (nxt - q))
|
||||
points.append(nxt)
|
||||
q = nxt
|
||||
return torch.stack(points, 1), torch.stack(midpoints, 1)
|
||||
|
||||
|
||||
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5, analytic=True):
|
||||
"""局部 DLS 精修:n 步增量 [B,n,6](analytic:几何雅可比,1 次 FK;否则 7 次批量有限差分)"""
|
||||
step = torch.tensor(STEP, device=q.device)
|
||||
B = q.shape[0]
|
||||
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)
|
||||
def err(x, Tb, Rtb, poseb, xf=None):
|
||||
xf = xf if xf is not None else kin.cum_xf(x)
|
||||
p = kin.fk(x, xf); Rq = kin.orient(x, xf)
|
||||
M = Rtb @ 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 * poseb[:, None]
|
||||
return torch.cat([Tb - p, rv], -1)
|
||||
out = []; x = q.clone()
|
||||
ar = torch.arange(6, device=q.device)
|
||||
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)]
|
||||
xf = kin.cum_xf(x)
|
||||
e0 = err(x, T, Rt, pose, xf)
|
||||
if analytic:
|
||||
# 几何角速度 Jw 仅在姿态误差接近零时等于该 skew 残差的负导数。
|
||||
Jv, Jw = kin.jac(x, xf)
|
||||
J = torch.cat([Jv, 100 * Jw * pose[:, None, None]], 1)
|
||||
else:
|
||||
dx = torch.zeros(B, 6, 6, device=q.device); dx[:, ar, ar] = 1e-3
|
||||
xp = (x[:, None, :] + dx).reshape(B * 6, 6)
|
||||
ep = err(xp, T.repeat_interleave(6, 0), Rt.repeat_interleave(6, 0), pose.repeat_interleave(6, 0))
|
||||
J = -((ep - e0.repeat_interleave(6, 0)) / 1e-3).view(B, 6, 6).transpose(1, 2) # [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)
|
||||
@@ -64,53 +93,202 @@ def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5):
|
||||
return torch.stack(out, 1)
|
||||
|
||||
|
||||
def jdiff(kin, a, b):
|
||||
"""a − b(度),连续轴取最短弧。"""
|
||||
d = a - b
|
||||
return torch.where(kin.cont, wrap180(d), d)
|
||||
|
||||
|
||||
def sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, rule='cross'):
|
||||
"""子目标推进(每次规划前调用,每次最多推进一个)。subs [B,M,6] 待走途经点(绝对角),nsub [B] 剩余个数,
|
||||
sub_prev [B,6] 上一个已越过的点(冷启动时 = 起点),sub_t [B] 当前子目标开始时刻。返回 (subs, nsub, sub_prev, sub_t)。
|
||||
rule='cross'(默认):qe 沿拐角平分面法向越过当前顶点(提前 SUB_LEAD_S 秒)即切到下一个。
|
||||
2026-10-19(审查 review_data_actions C5 / review_model_train P0-1):标签里 sub = 老师「下一个尚未经过的顶点」,
|
||||
老师 τ 一过顶点(−0.02 s)立刻换;旧规则要等最大关节差 < 5° 或 1.5 s 超时,策略在顶点附近 5–10° 进不去时
|
||||
sub 一直指着身后的点,这种「sub 已在身后」的特征序列训练里从未出现。运行时没有老师 τ,用几何等价物:
|
||||
进入方向 a = 顶点 − 上一点、离开方向 b = 下一顶点 − 顶点(最后一个顶点取 a),各轴按 VMAX 折成秒(与 τ 同量纲),
|
||||
法向 n = â + b̂;(qe − 顶点)·n̂ ≥ −SUB_LEAD_S 判为越过。没有超时(标签也没有)。
|
||||
rule='timeout':旧部署口径,到达 < 5° 或 1.5 s 超时(诊断对照用)。
|
||||
ik-policy.js Runner(subRule)与 apg_finetune.py 用同一规则,三处同步改。"""
|
||||
has = nsub > 0
|
||||
if not bool(has.any()): return subs, nsub, sub_prev, sub_t
|
||||
cur = subs[:, 0]
|
||||
if rule == 'timeout':
|
||||
dd = jdiff(kin, cur, qe).abs().max(-1).values
|
||||
adv = has & ((dd < SUB_REACH_DEG) | (tnow - sub_t > SUB_TIMEOUT_S))
|
||||
elif rule == 'cross':
|
||||
vmax = torch.tensor(VMAX, device=qe.device, dtype=qe.dtype)
|
||||
un = lambda v: v / v.norm(dim=-1, keepdim=True).clamp(min=1e-9)
|
||||
a = jdiff(kin, cur, sub_prev) / vmax
|
||||
nxt = subs[:, 1] if subs.shape[1] > 1 else cur
|
||||
b = torch.where((nsub > 1)[:, None], jdiff(kin, nxt, cur) / vmax, a)
|
||||
n = un(un(a) + un(b))
|
||||
proj = (jdiff(kin, qe, cur) / vmax * n).sum(-1)
|
||||
adv = has & (proj >= -SUB_LEAD_S)
|
||||
else:
|
||||
raise ValueError('sub_rule: ' + str(rule))
|
||||
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, -1:]], 1), subs)
|
||||
sub_prev = torch.where(adv[:, None], cur, sub_prev)
|
||||
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
|
||||
return subs, nsub, sub_prev, sub_t
|
||||
|
||||
|
||||
class TeacherOracle:
|
||||
"""老师当策略(2026-10-19,审查 review_closedloop D1 / review_data_actions §4.1):
|
||||
规划时从当前时间下标 it 取老师 50 Hz 轨迹的 16 步增量,按与 build_dataset.cjs 相同的标签口径
|
||||
act = clamp(Δ / (act_scale·STEP), −1, 1) 交给 rollout(),其余(队列、前缀、提交、qe、DLS、done、碰撞、时限)一行不改。
|
||||
traj [B,Tmax,6]:每题老师轨迹,末帧重复补齐(按 id 从 ml/data/shard_*.jsonl 取)。
|
||||
sigma > 0:每次规划给 act 加 N(0, σ²)(σ 以 STEP 为单位,各候选独立),测闭环对残差噪声的敏感度。
|
||||
跟随时第 it 步的 q 应等于 traj[:, it](DLS 接管或到达冻结之后除外)。"""
|
||||
head = 'oracle'
|
||||
|
||||
def __init__(self, traj, act_scale=1.0, sigma=0.0):
|
||||
self.traj, self.act_scale, self.sigma = traj, float(act_scale), float(sigma)
|
||||
|
||||
def plan(self, it, K, g):
|
||||
B, Tm, _ = self.traj.shape
|
||||
dev = self.traj.device
|
||||
idx = (it + torch.arange(H + 1, device=dev)).clamp(max=Tm - 1)
|
||||
seg = self.traj[:, idx]
|
||||
astep = torch.tensor(STEP, device=dev) * self.act_scale
|
||||
act = ((seg[:, 1:] - seg[:, :-1]) / astep).clamp(-1, 1)
|
||||
act = act[:, None].expand(B, K, H, 6).contiguous()
|
||||
if self.sigma > 0:
|
||||
act = act + torch.randn(B, K, H, 6, device=dev, generator=g) * (self.sigma / self.act_scale)
|
||||
return act
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False):
|
||||
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None,
|
||||
qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross',
|
||||
fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None,
|
||||
fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1, safety_try=0, sel_w=0.1, dls_safety=0, fb_clear=0.0, fb_cmin=0.3, sub_refresh=0, fb_block_gain=1.0,
|
||||
inc_cap=0, gap_margin=0.0, cert_batch=0, progress=None):
|
||||
"""use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头;
|
||||
qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放;
|
||||
select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。
|
||||
oracle:TeacherOracle,代替 model 出动作(model 可为 None,须配 select='first'、use_sub=False)。
|
||||
replan:'r3'(部署:队列 ≤ 3 步就规划,稳态 r=3)/ 'empty'(队列空了才规划,每次 r=0,前缀消融用)。
|
||||
sub_rule:子目标推进规则,见 sub_advance。除默认值外均为诊断用。
|
||||
inc_cap=1:把「策略动作 + 目标反馈」的合计增量再按每轴 VMAX·DT 限一次幅(默认关;关时反馈是在 ±1 限幅之后叠加的,第30轮实测最大 1.66×VMAX)。
|
||||
gap_margin>0:安全层要求间隙大于该毫米余量而不是 >0;宽相位 cap 自动抬到 margin+0.5,否则 0.5 的截顶值会让余量门限恒假。
|
||||
两者默认值下行为与第32轮逐位相同。"""
|
||||
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)
|
||||
gcap = max(0.5, gap_margin + 0.5) # 余量门限必须落在截顶值之内才有意义
|
||||
act_scale = oracle.act_scale if oracle is not None else float(getattr(model, 'act_scale', 1.0))
|
||||
astep = step * act_scale # 动作反归一化:每单位 act 对应的度数
|
||||
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)
|
||||
limit = time_limit(ex, max_sec)
|
||||
nmax = int(math.ceil(limit.max().item() / DT))
|
||||
q, qp = q0.clone(), q0.clone()
|
||||
q, qp = q0.clone(), (qp0.to(device) if qp0 is not None else 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)
|
||||
last = None; last_ok = torch.zeros(B, dtype=torch.bool, device=device)
|
||||
fb_i = torch.zeros(B, 6, device=device) # 积分项:累积持续同向的目标反馈(慢变偏差)
|
||||
fb_cache = None; pit = 0 # fb_every>1:复用上一次规划的目标反馈(误差方向慢变)
|
||||
last_brake = torch.zeros(B, dtype=torch.bool, device=device)
|
||||
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device)
|
||||
sub_t = torch.zeros(B, device=device); sub_prev = q0.clone()
|
||||
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)
|
||||
first_hit = torch.full((B,), float('nan'), device=device); dls_it = torch.full((B,), -1, dtype=torch.long, device=device)
|
||||
# 2026-10-20(轮次33):速度/加速度按「实际执行位移」算,不看指令——第30轮证明反馈是在限幅之后叠加的
|
||||
vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device)
|
||||
traj = [q.clone()] if return_traj else None
|
||||
plans = {k: [] for k in ('it', 'qe', 'commit', 'points', 'midpoints', 'gaps', 'certified')} if return_traj else None
|
||||
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
|
||||
k2s = int(round(2.0 / DT)); e2 = None
|
||||
e0, _ = goal_err(kin, q0, T, R)
|
||||
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
|
||||
ar = torch.arange(B, device=device)
|
||||
for it in range(nmax):
|
||||
tnow = it * DT
|
||||
if queue.shape[1] <= 3:
|
||||
if (queue.shape[1] <= 3) if replan == 'r3' else (queue.shape[1] == 0):
|
||||
r = queue.shape[1]
|
||||
pre = torch.zeros(B, 3, 6, device=device); pre[:, :r] = queue[:, :r]
|
||||
qe = q + queue.sum(1)
|
||||
pre = torch.zeros(B, 3, 6, device=device)
|
||||
qe = q
|
||||
if r:
|
||||
prefix_points, _ = execution_points(kin, q, queue)
|
||||
pre[:, :r] = prefix_points - torch.cat([q[:, None], prefix_points[:, :-1]], 1)
|
||||
qe = prefix_points[:, -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)
|
||||
subs, nsub, sub_prev, sub_t = sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, sub_rule)
|
||||
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 oracle is not None:
|
||||
act = oracle.plan(it, K, g); sub = torch.zeros(B, 13, device=device)
|
||||
else:
|
||||
z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale
|
||||
act, val, sub = model.sample_k(feat, K, z, steps=steps)
|
||||
if sub_refresh and not cold: # 子目标头作为反应式规划:没有待走途经点时,用当前状态的预测补一个(训练标签在每个状态都有 sub)
|
||||
need = sub[:, 12] > 0.5
|
||||
newsubs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
|
||||
rf = need & (nsub <= 0)
|
||||
subs = torch.where(rf[:, None, None], newsubs, subs)
|
||||
nsub = torch.where(rf, torch.full_like(nsub, 2), nsub)
|
||||
sub_prev = torch.where(rf[:, None], q, sub_prev)
|
||||
sub_t = torch.where(rf, torch.full_like(sub_t, tnow), sub_t)
|
||||
inc = act.float().clamp(-1, 1) * astep
|
||||
if fb_gain > 0: # 实验(2026-10-20):目标反馈加在所有候选上(安全层也评估带反馈的候选)
|
||||
if fb_cache is None or fb_every <= 1 or pit % fb_every == 0:
|
||||
fb = dls_steps(kin, qe, T, R, n=1); fb_cache = fb
|
||||
else:
|
||||
fb = fb_cache
|
||||
pit += 1
|
||||
gm_ = None
|
||||
if fb_clear > 0: # 近障碍衰减:反馈方向可能把候选推进障碍,贴近时让模型自己的绕行占主导
|
||||
dgc = kin.gaps(qe, fb_clear)['gm'].min(-1).values
|
||||
gm_ = torch.clamp(dgc / fb_clear, fb_cmin, 1.0)
|
||||
if fb_ki > 0:
|
||||
fb_i = fb_decay * fb_i + fb[:, 0] * (~done)[:, None]
|
||||
lim = step * fb_imax
|
||||
fb_i = fb_i.clamp(-lim, lim)
|
||||
inc = inc + fb_gain * fb[:, :, None, :] + fb_ki * fb_i[:, None, None, :]
|
||||
elif fb_scale > 0: # 距离衰减:远离目标全增益,贴近目标减弱(减少过冲/刹停)
|
||||
dg, _ = goal_err(kin, qe, T, R)
|
||||
gdec = fb_gain * torch.clamp(dg / fb_scale, fb_near, 1.0)
|
||||
if gm_ is not None: gdec = gdec * gm_
|
||||
inc = inc + gdec[:, None, None, None] * fb[:, :, None, :]
|
||||
else:
|
||||
gv = torch.full((B,), fb_gain, device=device, dtype=fb.dtype)
|
||||
if gm_ is not None: gv = gv * gm_
|
||||
if fb_block_gain != 1.0: # 上一次被安全层全拒:减弱目标反馈,让模型自己的绕行候选有机会通过认证
|
||||
gv = torch.where(last_brake, gv * fb_block_gain, gv)
|
||||
inc = inc + gv[:, None, None, None] * fb[:, :, None, :]
|
||||
if inc_cap:
|
||||
inc = torch.maximum(torch.minimum(inc, step), -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)
|
||||
need = sub[:, 12] > 0.5
|
||||
subs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
|
||||
if sub_override is not None:
|
||||
subs = sub_override[0].to(device).float()
|
||||
n_ov = sub_override[1].to(device)
|
||||
nsub_ov = n_ov.long() if n_ov.dtype != torch.bool else n_ov.long() * subs.shape[1]
|
||||
need = nsub_ov > 0
|
||||
nsub = nsub_ov
|
||||
else:
|
||||
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub))
|
||||
if not use_sub: nsub = torch.zeros_like(nsub)
|
||||
sub_t.fill_(tnow); sub_prev = q.clone()
|
||||
fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6]
|
||||
if last is None:
|
||||
if select == 'first':
|
||||
score = torch.zeros(B, K, device=device); score[:, 0] = 1
|
||||
elif select in ('endpoint', 'adapt'):
|
||||
qa = fut[:, :, -1].reshape(B * K, 6)
|
||||
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
|
||||
score = -e_end.view(B, K)
|
||||
if select == 'adapt' and last is not None:
|
||||
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
|
||||
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
|
||||
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
|
||||
sc = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
|
||||
score = torch.where(last_brake[:, None], sc, score)
|
||||
elif cold or select == 'value':
|
||||
# 短前瞻:候选末态的价值
|
||||
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),
|
||||
@@ -118,30 +296,134 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
v2, _ = model.heads(model.encode(f2))
|
||||
score = -v2.view(B, K)
|
||||
else:
|
||||
n = min(fut.shape[2], last.shape[1] - 3)
|
||||
# 2026-10-19 修错位:last[i] = 上一计划从 qe_old 走 i 步后的位置;提交 3 步后 qe_new = last[3],
|
||||
# 新候选 fut[0] 是 qe_new 再走 1 步 = 对应 last[4]。JS 部署(ik-policy.js)同一处同样修正。
|
||||
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
|
||||
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)
|
||||
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
|
||||
# 2026-10-19(审查 review_closedloop F5):上一次是刹停或 DLS 接管的行没有「已提交计划」,
|
||||
# 与 JS(lastPlan = null → 一致性项缺席、各候选同分 → 取第 0 条)一致:同分,稳定排序取第 0 条
|
||||
score = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
|
||||
if select == 'mix': # 一致性 + 端点进度(w=sel_w/mm)
|
||||
qa = fut[:, :, -1].reshape(B * K, 6)
|
||||
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
|
||||
score = score - sel_w * e_end.view(B, K)
|
||||
order = torch.sort(score, dim=-1, descending=True, stable=True).indices
|
||||
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
|
||||
if select == 'average': # 2026-10-20:候选平均(平滑误差)——所有候选置为 K 条动作的均值
|
||||
inc = inc.mean(1, keepdim=True).expand_as(inc).contiguous()
|
||||
K_eff = 1
|
||||
else:
|
||||
K_eff = K
|
||||
commit = inc[ar, pick, r:r + commit_n]
|
||||
if collect is not None: # 反馈蒸馏:收集与部署完全一致的 (特征, 复合动作=模型输出+目标反馈)
|
||||
collect.append((feat.detach().float().cpu(), inc[ar, pick].detach().float().cpu()))
|
||||
committed = torch.ones(B, dtype=torch.bool, device=device)
|
||||
braked = torch.zeros(B, dtype=torch.bool, device=device)
|
||||
if return_traj:
|
||||
cert_pts = torch.full((B, commit_n, 6), float('nan'), device=device)
|
||||
cert_mids = cert_pts.clone()
|
||||
cert_gaps = torch.full((B, 2 * commit_n), float('nan'), device=device)
|
||||
certified = torch.zeros(B, commit_n, dtype=torch.bool, device=device)
|
||||
if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停
|
||||
maxlen = torch.zeros(B, dtype=torch.long, device=device)
|
||||
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
|
||||
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
|
||||
if cert_batch: # 一次 gaps 覆盖全部候选:典型路径略贵,但把「前面候选被拒→串行调用 K 次」的最坏情况封成 1 次
|
||||
cks = inc[ar[:, None], order[:, :ntry], r:r + commit_n].reshape(B * ntry, commit_n, 6)
|
||||
pts, mids = execution_points(kin, qe.repeat_interleave(ntry, 0), cks)
|
||||
gaps6 = kin.gaps(torch.cat([pts, mids], 1).reshape(-1, 6), gcap)['gm'].min(-1).values.view(B, ntry, 2 * commit_n)
|
||||
plen = torch.cumprod(((gaps6 > gap_margin)[:, :, :commit_n] &
|
||||
(gaps6 > gap_margin)[:, :, commit_n:]).long(), -1).sum(-1)
|
||||
maxlen = plen.max(-1).values
|
||||
best = plen.argmax(-1) # 与串行同口径:严格更大者胜 ⇒ 首个最大值
|
||||
chosen = cks.view(B, ntry, commit_n, 6)[ar, best]
|
||||
chosen_k = order[ar, best]
|
||||
if return_traj:
|
||||
cert_pts = pts.view(B, ntry, commit_n, 6)[ar, best]
|
||||
cert_mids = mids.view(B, ntry, commit_n, 6)[ar, best]
|
||||
cert_gaps = gaps6[ar, best]
|
||||
else:
|
||||
for kk in range(ntry):
|
||||
ck = inc[ar, order[:, kk], r:r + commit_n]
|
||||
pts, mids = execution_points(kin, qe, 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)
|
||||
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
||||
ok6 = gaps6 > gap_margin
|
||||
okstep = ok6[:, :commit_n] & ok6[:, commit_n:]
|
||||
plen = torch.cumprod(okstep.long(), 1).sum(1)
|
||||
take = plen > maxlen
|
||||
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k)
|
||||
if return_traj:
|
||||
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
||||
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
||||
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
||||
maxlen = torch.maximum(maxlen, plen)
|
||||
if B == 1 and int(maxlen[0]) >= commit_n: break # 实时单题:首个全安全候选即停(安全层 16 点→2-4 点)
|
||||
msk = (torch.arange(commit_n, device=device)[None] < maxlen[:, None])
|
||||
commit = torch.where(msk[..., None], chosen, torch.zeros_like(chosen))
|
||||
if return_traj: certified = msk.clone()
|
||||
brakes += (maxlen == 0).float(); braked = maxlen == 0
|
||||
pick = chosen_k; committed = maxlen > 0
|
||||
elif safety:
|
||||
ok_any = torch.zeros(B, dtype=torch.bool, device=device)
|
||||
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
|
||||
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
|
||||
for kk in range(ntry):
|
||||
ck = inc[ar, order[:, kk], r:r + commit_n]
|
||||
pts, mids = execution_points(kin, qe, ck)
|
||||
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
||||
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
||||
okk = (gaps6 > gap_margin).all(-1)
|
||||
take = okk & ~ok_any
|
||||
chosen = torch.where(take[:, None, None], ck, chosen); ok_any |= okk
|
||||
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k); ok_any |= okk
|
||||
if return_traj:
|
||||
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
||||
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
||||
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
||||
commit = torch.where(ok_any[:, None, None], chosen, torch.zeros_like(chosen))
|
||||
brakes += (~ok_any).float()
|
||||
if return_traj: certified = ok_any[:, None].expand(-1, commit_n).clone()
|
||||
brakes += (~ok_any).float(); braked = ~ok_any
|
||||
pick = chosen_k; committed = ok_any
|
||||
if safety and fb_gain > 0 and fb_fallback > 0:
|
||||
fbcom = torch.zeros(B, commit_n, 6, device=device)
|
||||
fbcom[:, :1] = dls_steps(kin, qe, T, R, n=1) * fb_fallback
|
||||
pts, mids = execution_points(kin, qe, fbcom)
|
||||
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
||||
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
||||
takef = braked & (gaps6 > gap_margin).all(-1)
|
||||
commit = torch.where(takef[:, None, None], fbcom, commit)
|
||||
if return_traj:
|
||||
cert_pts = torch.where(takef[:, None, None], pts, cert_pts)
|
||||
cert_mids = torch.where(takef[:, None, None], mids, cert_mids)
|
||||
cert_gaps = torch.where(takef[:, None], gaps6, cert_gaps)
|
||||
certified = certified | takef[:, None]
|
||||
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)
|
||||
ref = dls_steps(kin, qe, T, R, n=commit_n)
|
||||
take = near
|
||||
if dls_safety and safety: # 2026-10-20:近目标 DLS 接管也要过安全层(部署端 _certCommit 本来就会验;不过则退回候选计划)
|
||||
pts, mids = execution_points(kin, qe, ref)
|
||||
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
||||
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
||||
okd = (gaps6 > gap_margin).all(-1)
|
||||
take = near & okd
|
||||
if return_traj:
|
||||
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
||||
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
||||
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
||||
if return_traj:
|
||||
certified = torch.where(take[:, None], torch.full_like(certified, bool(dls_safety and safety)), certified)
|
||||
commit = torch.where(take[:, None, None], ref, commit)
|
||||
dls_it = torch.where(take & (dls_it < 0), torch.full_like(dls_it, it), dls_it)
|
||||
committed = committed & ~take
|
||||
# 2026-10-19(F5):last 记真正提交的那条候选(安全层换了候选时是换后的那条);刹停 / DLS 接管的行无效
|
||||
if fb_ki > 0: # 刹停/精修接管时清积分(防止原地累积到限幅后突然冲出去)
|
||||
fb_i = torch.where(committed[:, None], fb_i, torch.zeros_like(fb_i))
|
||||
last = torch.cat([qe[:, None], fut[ar, pick]], 1); last_ok = committed; last_brake = braked
|
||||
if return_traj:
|
||||
plans['it'].append(it + r)
|
||||
for key, value in (('qe', qe), ('commit', commit), ('points', cert_pts), ('midpoints', cert_mids), ('gaps', cert_gaps), ('certified', certified)):
|
||||
plans[key].append(value)
|
||||
queue = torch.cat([queue, commit], 1)
|
||||
cold = False
|
||||
d = queue[:, 0]; queue = queue[:, 1:]
|
||||
@@ -154,15 +436,28 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
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
|
||||
tn = (it + 1) * DT
|
||||
# 2026-10-19(审查 review_closedloop F4):只计每题自己时限内的碰撞(同批长题会把短题多仿真几秒)
|
||||
hit = ((gm[:B] <= 0) | (gm[B:] <= 0)) & ~done & (tn <= limit + 1e-6)
|
||||
col += hit.float()
|
||||
first_hit = torch.where(hit & torch.isnan(first_hit), torch.full_like(first_hit, tn), first_hit)
|
||||
act_ = ((~done) & (tn <= limit + 1e-6)).float() # 只计自己时限内的移动(与 col 同口径)
|
||||
exec_d = (q - qp).abs()
|
||||
vratio = torch.maximum(vratio, (exec_d / step).max(-1).values * act_) # 实际速度 / VMAX
|
||||
aratio = torch.maximum(aratio, ((exec_d - prev_exec) / step).max(-1).values * act_) # 每控制步的速度变化 / VMAX
|
||||
prev_exec = exec_d
|
||||
e, oe = goal_err(kin, q, T, R)
|
||||
if it + 1 == k2s: e2 = e.clone()
|
||||
newly = (~done) & (e < 1) & (oe < 0.5)
|
||||
t_done = torch.where(newly, torch.full_like(t_done, (it + 1) * DT), t_done); done |= newly
|
||||
t_done = torch.where(newly, torch.full_like(t_done, tn), t_done); done |= newly
|
||||
if progress is not None and (it == 0 or (it + 1) % 50 == 0 or it + 1 == nmax):
|
||||
progress(it + 1, nmax)
|
||||
if done.all(): break
|
||||
ok = done & (t_done <= limit)
|
||||
ok = done & (t_done <= limit + 1e-6)
|
||||
if moved_start is None: moved_start = travel.clone()
|
||||
e_end, _ = goal_err(kin, q, T, R)
|
||||
if e2 is None: e2 = e_end
|
||||
ratio2 = e2 / e0.clamp(min=1e-3)
|
||||
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(),
|
||||
@@ -170,20 +465,25 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=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(),
|
||||
'start_fail': start_fail.float().mean().item(), 'goal_ratio_2s': ratio2.median().item(),
|
||||
'speed_ratio_mean': vratio.mean().item(), 'over_vmax_rate': (vratio > 1.001).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()
|
||||
per = {'done': done.cpu(), 'ok': ok.cpu(), 't_done': t_done.cpu(), 'limit': limit.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
|
||||
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(),
|
||||
'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu()}
|
||||
if return_traj:
|
||||
per['traj'] = torch.stack(traj, 1).cpu()
|
||||
per['plans'] = {k: torch.tensor(v) if k == 'it' else torch.stack(v, 1).cpu() for k, v in plans.items()}
|
||||
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')}, []
|
||||
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan'),
|
||||
'goal_ratio_2s': 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)
|
||||
@@ -194,12 +494,14 @@ def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
|
||||
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)
|
||||
# 中位数不能按批加权平均:全体任务重算(2026-10-19,选模分数用它)
|
||||
out['goal_ratio_2s'] = torch.cat([p['goal_ratio_2s'] for _, p in pers]).median().item()
|
||||
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.common.ckpt import load_policy
|
||||
from ml.data import load_tasks
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||
@@ -208,8 +510,7 @@ def main():
|
||||
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()
|
||||
model, ck = load_policy(a.ckpt, a.device)
|
||||
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()]
|
||||
@@ -217,8 +518,8 @@ def main():
|
||||
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()):
|
||||
flat_ok = torch.cat([p['ok'] for _, p in pers]); flat_col = torch.cat([p['col'] for _, p in pers])
|
||||
for t, d, c in zip(tasks, flat_ok.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)
|
||||
|
||||
+123
-32
@@ -1,9 +1,27 @@
|
||||
"""预训练(方案 §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
|
||||
通过线(方案 R10):samples_per_s ≥ 5000;日志 eta_h 为剩余小时数。
|
||||
每 eval_every 步用 EMA 权重做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按选模分数存 best.pt;min_step 之后连续 patience_evals 次不升即停。
|
||||
2026-10-18 首次正式预训练(21850 步,闭环到达 0–0.2%、穿柱 77–97%)后改:
|
||||
- FK / 平滑损失不再加在 one_step(随机噪声) 上(那样等于对任意噪声回归同一个 Y → 条件均值,多峰头塌成单峰),
|
||||
改加在 MF 分支里 r==t 且 t ≤ fk_tmax 的样本的配对去噪估计 x̂ = z − t·u 上(见 ml/model/meanflow.py);
|
||||
- p_neq 前 p_neq_warm 步为 0(纯 flow matching 先把瞬时速度场学稳,再开 r≠t 的自举目标);
|
||||
- 每次评估加开环指标(val 样本上一步采样的 16 步末端 FK 误差 mm、K 个候选的多样性),
|
||||
塌缩或学偏当场可见。
|
||||
2026-10-19(审查 review_model_train P0-2/P0-3、review_data_actions C3):
|
||||
- EMA(train.ema,默认 0.999,前期按 (1+s)/(10+s) 放宽):评估、best/last 里的 'ema' 都是 EMA 权重;下游一律经 ckpt.load_policy 取 ck.get('ema', ck['model']);
|
||||
- 选模:p_neq 预热期内及预热后第一次评估之前不存 best.pt(那时一步映射 u(z,0,1) 还没训过);
|
||||
分数 = success + 0.05·(1 − collision_rate) + 0.05·(1 − min(1, goal_ratio_2s)) − 1e-4·min(ol_best_mm, 500),success 全 0 时闭环指标仍能排序;
|
||||
早停只在 min_step = max(预热 + 2·eval_every, steps/2) 之后计数;
|
||||
- 余弦学习率按真实预算走完(configs 的 steps 改为 24000,原 60000 从来跑不到,14k 步早停时学习率还有 0.87×);
|
||||
- MeanFlow 采样:p_neq 0.5、p_edge 0.1(固定 (r=0,t=1) 的部署点,见 meanflow.sample_rt);
|
||||
- 子目标头只看 sub 特征(68:75)置零的输入:sub 存在时 SG[0:6] 与输入 sd 是同一个量,原来 80% 的样本可以直接抄(标签泄露);
|
||||
运行时子目标头只在冷启动 sf=0 时用,这样训练与使用同口径。价值头仍用完整输入(稳态推理时带 sub);
|
||||
- 条件输入增广 aug_pre_drop / aug_pre_sigma / aug_dq_sigma(见 ml/data.Samples.augment);
|
||||
- 动作尺度 act_scale 取自数据(ml/data.read_meta),存进 checkpoint;续训 / 微调起点与数据不一致直接报错。
|
||||
- DAgger 微调(--init):只有起点 checkpoint 已走过自己的 p_neq 预热时才把预热置 0;起点优先取 EMA 权重。"""
|
||||
import argparse, copy, json, math, os, sys, time
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
@@ -12,7 +30,7 @@ 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.model.meanflow import meanflow_loss
|
||||
from ml.sim.closed_loop import evaluate
|
||||
|
||||
|
||||
@@ -28,46 +46,106 @@ def q_from_feat(X):
|
||||
return torch.atan2(s, c) / D2R
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def open_loop(model, kin, vdata, stepv, fk_idx, n=1024, K=8):
|
||||
"""stepv:每单位动作对应的度数(= act_scale·STEP)。"""
|
||||
"""开环指标(val 样本,老师状态上):
|
||||
ol_fk_mm 候选 0 的 16 步末端与老师 Y 末端的胶囊点平均距离(mm);
|
||||
ol_best_mm K 个候选里最接近老师的那条的同一误差(多峰时它应明显小于 ol_fk_mm);
|
||||
ol_div K 个候选间标准差 / 动作平均幅度(塌成单峰时接近 0;首次正式预训练 last.pt 实测 0.20)。"""
|
||||
g = torch.Generator(device=vdata.X.device); g.manual_seed(123)
|
||||
X, Y, _, _ = vdata.batch(min(n, vdata.n), gen=g)
|
||||
act, _, _ = model.sample_k(X, K)
|
||||
act = act.float().clamp(-1, 1)
|
||||
q = q_from_feat(X)
|
||||
qt = q + (Y * stepv).sum(1)
|
||||
qh = q[:, None] + (act * stepv).sum(2) # [B,K,6]
|
||||
pt = fk_points(kin, qt, fk_idx)[:, None]
|
||||
ph = fk_points(kin, qh.reshape(-1, 6), fk_idx).view(X.shape[0], K, -1, 3)
|
||||
err = (ph - pt).norm(dim=-1).mean(-1) # [B,K] mm
|
||||
div = (act.std(1).mean() / act.abs().mean().clamp(min=1e-6)).item()
|
||||
return {'ol_fk_mm': err[:, 0].mean().item(), 'ol_best_mm': err.min(1).values.mean().item(), 'ol_div': div}
|
||||
|
||||
|
||||
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('--init', default=None, help='只载入权重(优先 EMA)、优化器与步数从 0 开始(DAgger 微调用)')
|
||||
ap.add_argument('--freeze-enc', action='store_true', help='冻结编码器(enc + enc_norm),只微调解码器/头')
|
||||
ap.add_argument('--lr', type=float, default=None); ap.add_argument('--warmup', type=int, default=None)
|
||||
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
|
||||
if a.lr: tc['lr'] = a.lr
|
||||
if a.warmup is not None: tc['warmup'] = a.warmup
|
||||
init_ck = None
|
||||
if a.init:
|
||||
init_ck = torch.load(a.init, map_location='cpu')
|
||||
# 2026-10-19(审查 P0-2):原来 --init 一律置 p_neq_warm=0,前提「起点已训过 r≠t」;best.pt 落在预热期(第 4000 步)时前提不成立,
|
||||
# DAgger 一上来就用没训过的场做 JVP 自举。现在只有起点已走过它自己的预热才置 0。
|
||||
init_warm = (init_ck.get('cfg') or {}).get('train', {}).get('p_neq_warm', 5000)
|
||||
if init_ck.get('step', 0) >= init_warm: tc['p_neq_warm'] = 0
|
||||
print(f'微调起点第 {init_ck.get("step")} 步(其预热 {init_warm} 步)→ 本次 p_neq_warm = {tc.get("p_neq_warm", 5000)}', flush=True)
|
||||
os.makedirs(a.out, exist_ok=True)
|
||||
torch.manual_seed(a.seed)
|
||||
dev = a.device
|
||||
vram.limit()
|
||||
if dev.startswith('cuda'):
|
||||
torch.backends.cuda.matmul.allow_tf32 = True; torch.backends.cudnn.allow_tf32 = True # MF 分支改 fp32 后用 TF32 保吞吐
|
||||
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)
|
||||
# 2026-10-19(审查 F11):val 只取主数据目录(第一个 --samples);DAgger 目录的样本全部当 train
|
||||
data = Samples(a.samples, device=dev, split='train', val_dirs=a.samples[:1])
|
||||
act_scale = data.act_scale
|
||||
val_tasks = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'val'][:tc['eval_tasks']]
|
||||
try: vdata = Samples(a.samples[:1], device=dev, split='val')
|
||||
except SystemExit: vdata = None
|
||||
if vdata is not None and vdata.n == 0: vdata = None
|
||||
print(f'train samples {data.n}, val tasks {len(val_tasks)}, act_scale {act_scale}, data_ver {data.data_ver}, device {dev}', flush=True)
|
||||
model = build(cfg).to(dev); model.act_scale = act_scale
|
||||
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'])
|
||||
opt = torch.optim.AdamW([p_ for p_ in model.parameters() if p_.requires_grad], lr=tc['lr'], betas=tuple(tc['betas']), weight_decay=tc['wd'])
|
||||
step0, best, bad_evals = 0, -1.0, 0
|
||||
def check_scale(ck, src):
|
||||
s0 = float(ck.get('act_scale', 1.0))
|
||||
if abs(s0 - act_scale) > 1e-9:
|
||||
raise SystemExit(f'{src} 的 act_scale={s0} 与样本 act_scale={act_scale} 不一致:标签尺度变了,须从头训练(FRESH_FROM=prep)')
|
||||
if init_ck is not None and not a.resume:
|
||||
check_scale(init_ck, a.init)
|
||||
model.load_state_dict(init_ck.get('ema') if init_ck.get('ema') is not None else init_ck['model'])
|
||||
print(f'微调:权重取自 {a.init}({"EMA" if init_ck.get("ema") is not None else "原始"}权重),优化器重置', flush=True)
|
||||
ema = copy.deepcopy(model).eval(); ema.act_scale = act_scale
|
||||
for p_ in ema.parameters(): p_.requires_grad_(False)
|
||||
if a.resume:
|
||||
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
|
||||
ck = torch.load(a.resume, map_location=dev); check_scale(ck, a.resume)
|
||||
model.load_state_dict(ck['model'])
|
||||
ema.load_state_dict(ck['ema'] if ck.get('ema') is not None else 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)
|
||||
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好分数 {best:.4f}', flush=True)
|
||||
del init_ck
|
||||
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)
|
||||
astep = stepv * act_scale # 动作 → 度
|
||||
fk_idx = torch.linspace(0, kin.cap_a.shape[0] - 1, tc['fk_caps'], device=dev).long()
|
||||
ema_d = float(tc.get('ema', 0.999))
|
||||
warm = 0 if model.head == 'wta' else int(tc.get('p_neq_warm', 5000))
|
||||
sel_from = warm + tc['eval_every'] # 第一次可以存 best.pt 的步数:预热结束后的第一次评估
|
||||
min_step = max(warm + 2 * tc['eval_every'], total // 2) # 早停计数起点
|
||||
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}
|
||||
d = {'model': model.state_dict(), 'ema': ema.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best,
|
||||
'bad_evals': bad_evals, 'fp': kin.fp, 'act_scale': act_scale, 'data_ver': data.data_ver}
|
||||
if name == 'last.pt': d['rng'] = rng_state()
|
||||
if extra: d.update(extra)
|
||||
atomic_save(d, os.path.join(a.out, name))
|
||||
@@ -75,26 +153,32 @@ def main():
|
||||
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'])
|
||||
X, Y, V, SG = data.batch(tc['batch'], aug=tc)
|
||||
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)
|
||||
p_neq = tc['p_neq'] if step >= warm else 0.0
|
||||
l_mf, mf_raw, aux = meanflow_loss(model, mem, Y, p_neq=p_neq, return_aux=True, p_edge=tc.get('p_edge', 0.0))
|
||||
sel = ((aux['r'] == aux['t']) & (aux['t'] <= tc.get('fk_tmax', 0.3))).nonzero(as_tuple=True)[0]
|
||||
tt = aux['t'][sel][:, None, None]
|
||||
xh = aux['z'][sel] - tt * aux['u'][sel] # 配对去噪估计(fp32)
|
||||
v, _ = model.heads(mem)
|
||||
Xs = X.clone(); Xs[:, 68:75] = 0 # 子目标头:sub 特征置零(切断抄输入的捷径,见文件头)
|
||||
_, s = model.heads(model.encode(Xs))
|
||||
l_val = (v.float() - V).abs().mean(); l_sub = (s.float() - SG).abs().mean()
|
||||
Xf, Yf = (X, Y) if model.head == 'wta' else (X[sel], Y[sel])
|
||||
l_sm = torch.zeros((), device=dev); l_fk = torch.zeros((), device=dev)
|
||||
if xh.shape[0] > 0:
|
||||
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)
|
||||
if tc['w_fk'] > 0 and xh.shape[0] > 0:
|
||||
q = q_from_feat(Xf)
|
||||
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]
|
||||
qh = q[:, None] + torch.cumsum(xh.clamp(-1, 1) * astep, 1)[:, ks]
|
||||
qt = q[:, None] + torch.cumsum(Yf * astep, 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()
|
||||
@@ -105,6 +189,9 @@ def main():
|
||||
if not torch.isfinite(loss):
|
||||
print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break
|
||||
opt.step(); seen += X.shape[0]
|
||||
with torch.no_grad(): # EMA;前期按 (1+s)/(10+s) 放宽,避免随机初值占权重
|
||||
dk = min(ema_d, (1 + step) / (10 + step))
|
||||
for pe, pm in zip(ema.parameters(), model.parameters()): pe.lerp_(pm.detach(), 1 - dk)
|
||||
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)
|
||||
@@ -116,21 +203,25 @@ def main():
|
||||
'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()
|
||||
# bs=64 + 归还缓存池:WDDM 卡满时 15.9 GiB 的评估峰值会被驱动换出到内存,eval 从 2 分钟变 1 小时
|
||||
res, _ = evaluate(ema, kin, val_tasks, dev, bs=64)
|
||||
if vdata is not None: res.update(open_loop(ema, kin, vdata, astep, fk_idx))
|
||||
torch.cuda.empty_cache()
|
||||
sc = res['success'] + 0.05 * (1 - res['collision_rate']) + 0.05 * (1 - min(1.0, res.get('goal_ratio_2s', 1.0))) \
|
||||
- 1e-4 * min(res.get('ol_best_mm', 0.0), 500.0)
|
||||
res['step'] = step + 1; res['t'] = time.time(); res['score'] = sc; res['weights'] = 'ema'
|
||||
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:
|
||||
if step + 1 >= sel_from and sc > best + 1e-5:
|
||||
best = sc; bad_evals = 0; save('best.pt', step + 1, {'eval': res})
|
||||
elif step + 1 >= min_step:
|
||||
bad_evals += 1
|
||||
if bad_evals >= tc['patience_evals']: print('闭环到达率不再上升,停止', flush=True); break
|
||||
if bad_evals >= tc['patience_evals']: save('last.pt', step + 1); print('闭环分数不再上升,停止', flush=True); break
|
||||
save('last.pt', step + 1)
|
||||
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 a.no_eval or best <= -1.0: save('best.pt', step + 1) # 没评估过 / 总步数不到 sel_from:best = last
|
||||
print(f'done, best score {best:.4f}, {(time.time() - t0) / 3600:.2f} h', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n', '--fb', '--commit', '--seg', '--proj', '--select']);
|
||||
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'] : []);
|
||||
@@ -66,12 +66,12 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
|
||||
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;
|
||||
let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0, starve = 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 });
|
||||
const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1, fbGain: +opt('--fb') || 0, commit: Math.max(1, +opt('--commit') || 3), segCert: opt('--seg') !== '0', projMax: opt('--proj') === '0' ? 0 : 0.5, select: opt('--select') || 'consist' }); // 2026-10-19:动作尺度随模型
|
||||
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++) {
|
||||
@@ -86,14 +86,14 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
|
||||
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;
|
||||
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); starve += r.stats.starveTicks || 0; 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,
|
||||
plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, starveTicks: starve, starvePerPlan: plans ? +(starve / plans).toFixed(3) : 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),
|
||||
|
||||
@@ -0,0 +1,717 @@
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from ml.eval.eval_cl import json_finite, run
|
||||
from ml.eval.diag_collision import replay_certificates, replay_dense, replay_first_hits
|
||||
from ml.sim.closed_loop import TeacherOracle, rollout
|
||||
from ml.common.kin_torch import Kin, seg_seg_dist
|
||||
|
||||
|
||||
class EvalMetricsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tasks = [
|
||||
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 1, 'travel': 5},
|
||||
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': None, 'travel': None},
|
||||
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 2, 'travel': 6},
|
||||
]
|
||||
self.per = {k: torch.tensor(v) for k, v in {
|
||||
'ok': [True, False, True], 'done': [True, False, True],
|
||||
't_done': [0.5, float('nan'), 2.0], 'col': [1.0, 0.0, 0.0],
|
||||
'travel': [6.0, 0.0, 8.0], 'brakes': [2.0, 4.0, 6.0],
|
||||
'start_fail': [False, True, False], 'goal_ratio_2s': [0.3, 0.9, 0.1],
|
||||
'err': [0.2, 200.0, 0.8], 'dls_it': [4, -1, 20],
|
||||
'speed_ratio': [1.66, 0.9, 1.2], 'accel_ratio': [0.3, 0.2, 0.5],
|
||||
}.items()}
|
||||
|
||||
def evaluate(self, tasks, bs=2, **kw):
|
||||
offset = 0
|
||||
|
||||
def fake_rollout(model, kin, batch, device, **kwargs):
|
||||
nonlocal offset
|
||||
p = {k: v[offset:offset + len(batch)] for k, v in self.per.items()}
|
||||
offset += len(batch)
|
||||
return {'time_budget': 'mixed'}, p
|
||||
|
||||
with patch('ml.eval.eval_cl.rollout', side_effect=fake_rollout), contextlib.redirect_stdout(io.StringIO()):
|
||||
return run(None, None, tasks, 'cpu', bs=bs, **kw)[0]
|
||||
|
||||
def test_global_denominators_and_collision_free_arrival(self):
|
||||
result = self.evaluate(self.tasks)
|
||||
self.assertEqual(result['n_ref'], 2)
|
||||
self.assertAlmostEqual(result['success'], 2 / 3, places=6)
|
||||
self.assertAlmostEqual(result['collision_free_success'], 1 / 3, places=6)
|
||||
self.assertAlmostEqual(result['collision_rate'], 1 / 3, places=6)
|
||||
self.assertEqual(result['time_ok_1p3'], 1.0)
|
||||
self.assertEqual(result['time_mean_s'], 1.25)
|
||||
self.assertAlmostEqual(result['travel_ratio_median'], 1.2, places=6)
|
||||
self.assertAlmostEqual(result['goal_ratio_2s'], 0.3, places=6)
|
||||
self.assertEqual(result['err_fail_med'], 200.0)
|
||||
self.assertEqual(result['brakes_mean'], 4.0)
|
||||
# 极值/分位按逐题值跨批汇总,不能进按 n 加权求均值的批聚合
|
||||
self.assertAlmostEqual(result['speed_ratio_p50'], 1.2, places=6)
|
||||
self.assertAlmostEqual(result['speed_ratio_p95'], 1.614, places=6)
|
||||
self.assertAlmostEqual(result['speed_ratio_max'], 1.66, places=5)
|
||||
self.assertAlmostEqual(result['over_vmax_rate'], 2 / 3, places=6) # 1.66 与 1.2 都算超 VMAX,0.9 不算
|
||||
self.assertAlmostEqual(result['accel_ratio_max'], 0.5, places=6)
|
||||
|
||||
def test_empty_conditional_metrics_and_explicit_limit(self):
|
||||
task = {**self.tasks[0], 'exec_s': None, 'travel': None}
|
||||
result = self.evaluate([task], max_sec=0.04)
|
||||
self.assertIsNone(result['err_fail_med'])
|
||||
self.assertIsNone(result['time_ok_1p3'])
|
||||
self.assertIsNone(result['travel_ratio_median'])
|
||||
self.assertEqual(result['time_budget'], 'fixed 0.040 s')
|
||||
self.assertEqual(result['collision_free_success'], 0.0)
|
||||
|
||||
def test_json_replaces_nonfinite_without_changing_zero(self):
|
||||
value = {'items': [float('nan'), float('inf'), -float('inf'), 0.0]}
|
||||
text = json.dumps(json_finite(value), allow_nan=False)
|
||||
self.assertEqual(json.loads(text), {'items': [None, None, None, 0.0]})
|
||||
|
||||
def test_first_hit_probe_detects_collision(self):
|
||||
class PlaneKin:
|
||||
def __init__(self, **kwargs):
|
||||
self.cont = torch.zeros(6, dtype=torch.bool)
|
||||
self.lo, self.hi = torch.full((6,), -10.0), torch.full((6,), 10.0)
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': q[:, :1].expand(-1, 49).clamp(max=cap)}
|
||||
|
||||
traj = torch.zeros(1, 3, 6)
|
||||
traj[0, :, 0] = torch.tensor([1.0, 1.0, -1.0])
|
||||
per = {'col': torch.tensor([1.0]), 'first_hit': torch.tensor([0.04]),
|
||||
'dls_it': torch.tensor([-1]), 'traj': traj}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
trace = Path(folder) / 'trace.pt'
|
||||
torch.save({'tasks': [self.tasks[0]], 'batches': [({}, per, 1)]}, trace)
|
||||
with patch('ml.eval.diag_collision.Kin', PlaneKin), contextlib.redirect_stdout(io.StringIO()):
|
||||
result = replay_first_hits(trace)
|
||||
self.assertEqual(result['reported_collision_tasks'], 1)
|
||||
self.assertEqual(result['replayed_first_hits32'], 1)
|
||||
self.assertEqual(result['replayed_first_hits64'], 1)
|
||||
self.assertEqual(result['rows'][0]['gap64_mm'], [0.5, 0.5, 0.0, -1.0])
|
||||
self.assertEqual(result['rows'][0]['dense_gap64_min_mm'], -1.0)
|
||||
|
||||
def test_dense_probe_detects_between_checkpoints_and_respects_deadline(self):
|
||||
class ThinObstacle:
|
||||
fp = 'probe'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': ((q[:, :1] - 0.25).abs() - 0.05).clamp(max=cap)}
|
||||
|
||||
traj = torch.zeros(4, 3, 6)
|
||||
traj[:, :, 0] = torch.tensor([[0, 1, 1], [0, -0.5, -1], [0, 0, 0.25], [0, 0, 0.25]])
|
||||
per = {'traj': traj, 'col': torch.zeros(4), 'limit': torch.tensor([0.04, 0.04, 0.02, 0.04]),
|
||||
't_done': torch.tensor([float('nan')] * 3 + [0.02])}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
trace = Path(folder) / 'trace.pt'
|
||||
output = Path(folder) / 'audit.json'
|
||||
torch.save({'kin_fp': 'probe', 'tasks': [{}] * 4, 'batches': [({}, per, 4)]}, trace)
|
||||
with patch('ml.eval.diag_collision.Kin', ThinObstacle), contextlib.redirect_stdout(io.StringIO()):
|
||||
result = replay_dense(trace, [0, 1, 2, 3], 0.01, out=output)
|
||||
with self.assertRaisesRegex(ValueError, 'valid task indices'):
|
||||
replay_dense(trace, [4])
|
||||
with self.assertRaisesRegex(ValueError, 'positive max_step_deg'):
|
||||
replay_dense(trace, [0], 0.0)
|
||||
self.assertEqual(json.loads(output.read_text())['collision_tasks'], 1)
|
||||
self.assertEqual(result['collision_tasks'], 1)
|
||||
self.assertEqual(result['rows'][0]['collision_segments'], 1)
|
||||
self.assertAlmostEqual(result['rows'][0]['min_gap64_mm'], -0.05)
|
||||
self.assertGreater(result['rows'][1]['min_gap64_mm'], 0)
|
||||
for row in result['rows'][2:]:
|
||||
self.assertGreater(row['min_gap64_mm'], 0)
|
||||
self.assertEqual(row['steps'], 1)
|
||||
|
||||
def test_certificate_replay_samples_actual_movement_before_deadlines(self):
|
||||
kin = SegmentCertificateTest.ThinObstacle()
|
||||
kin.fp = 'probe'
|
||||
traj = torch.zeros(3, 4, 6)
|
||||
traj[:, :, 0] = torch.tensor([[0, 1, 1, 1], [0, -0.5, -1, -1], [0, 0, 1, 1]])
|
||||
per = {'traj': traj, 'limit': torch.tensor([0.06, 0.02, 0.02]),
|
||||
't_done': torch.tensor([float('nan'), 0.02, float('nan')]),
|
||||
'plans': {'it': torch.arange(3), 'commit': torch.zeros(3, 3, 1, 6),
|
||||
'certified': torch.ones(3, 3, 1, dtype=torch.bool), 'gaps': torch.full((3, 3, 2), 0.5)}}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
trace, output = Path(folder) / 'trace.pt', Path(folder) / 'audit.json'
|
||||
torch.save({'kin_fp': 'probe', 'tasks': [{}] * 3, 'batches': [({}, per, 3)]}, trace)
|
||||
with patch('ml.eval.diag_collision.Kin', return_value=kin), contextlib.redirect_stdout(io.StringIO()):
|
||||
result = replay_certificates(trace, [0, 1, 2], out=output, repeats=1, extra_segments=[(0, 0)])
|
||||
with self.assertRaisesRegex(ValueError, 'extra segment'):
|
||||
replay_certificates(trace, [1], repeats=1, extra_segments=[(1, 1)])
|
||||
self.assertEqual(json.loads(output.read_text())['dense_collision_segments'], 1)
|
||||
self.assertEqual([(r['i'], r['start_it']) for r in result['rows']], [(0, 0), (1, 0)])
|
||||
self.assertEqual(result['budgets']['32']['safe'], 1)
|
||||
self.assertEqual(result['budgets']['32']['collision'], 1)
|
||||
self.assertEqual(result['accepted_dense_collision_segments'], 0)
|
||||
self.assertTrue(result['batch_single_match'])
|
||||
self.assertEqual(result['timing']['calls'], 2)
|
||||
|
||||
|
||||
class DlsSafetyTest(unittest.TestCase):
|
||||
def test_rejected_refinement_retains_safe_candidate(self):
|
||||
class ObstacleKin:
|
||||
def clamp_box(self, q):
|
||||
return q
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
|
||||
|
||||
task = {'q0': [0] * 6, 'T': [2, 0, 0]}
|
||||
traj = torch.zeros(1, 2, 6)
|
||||
traj[0, 1, 0] = 0.1
|
||||
ref = torch.zeros(1, 1, 6)
|
||||
ref[0, 0, 0] = 1.0
|
||||
patches = {
|
||||
'features': lambda *args: torch.zeros(1, 83),
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 2.0), torch.zeros(q.shape[0])),
|
||||
'dls_steps': lambda *args, **kwargs: ref,
|
||||
}
|
||||
results = []
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
for gate in (0, 1):
|
||||
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
|
||||
max_sec=0.02, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
|
||||
safety_partial=1, dls_safety=gate)
|
||||
results.append(per)
|
||||
self.assertEqual(results[0]['col'].item(), 1.0)
|
||||
self.assertEqual(results[0]['dls_it'].item(), 0)
|
||||
self.assertEqual(results[1]['col'].item(), 0.0)
|
||||
self.assertEqual(results[1]['dls_it'].item(), -1)
|
||||
self.assertAlmostEqual(results[1]['traj'][0, 1, 0].item(), 0.1)
|
||||
|
||||
def test_feedback_is_reduced_only_after_full_rejection(self):
|
||||
class ObstacleKin:
|
||||
def clamp_box(self, q):
|
||||
return q
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
|
||||
|
||||
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
|
||||
traj = torch.zeros(1, 17, 6)
|
||||
traj[0, :, 0] = -0.2 * torch.arange(17)
|
||||
ref = torch.zeros(1, 1, 6)
|
||||
ref[0, 0, 0] = 1.0
|
||||
patches = {
|
||||
'features': lambda *args: torch.zeros(1, 83),
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
|
||||
'dls_steps': lambda *args, **kwargs: ref,
|
||||
}
|
||||
results = []
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
for gain in (1.0, 0.5):
|
||||
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
|
||||
max_sec=0.04, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
|
||||
safety_partial=1, fb_gain=0.6, fb_block_gain=gain)
|
||||
results.append(per)
|
||||
self.assertEqual(results[0]['brakes'].item(), 2.0)
|
||||
self.assertEqual(results[1]['brakes'].item(), 1.0)
|
||||
self.assertEqual(results[1]['traj'][0, 1, 0].item(), 0.0)
|
||||
self.assertAlmostEqual(results[1]['traj'][0, 2, 0].item(), 0.1)
|
||||
self.assertEqual(results[1]['col'].item(), 0.0)
|
||||
|
||||
|
||||
class ClampedPathSafetyTest(unittest.TestCase):
|
||||
def rollout_candidate(self, increments, safety_partial, goal_error=10.0, **kwargs):
|
||||
class ObstacleKin:
|
||||
def clamp_box(self, q):
|
||||
clamped = q.clone()
|
||||
clamped[..., 4] = clamped[..., 4].clamp(max=0.0)
|
||||
return clamped
|
||||
|
||||
def gaps(self, q, cap):
|
||||
collision = (q[:, :1] > 0.1) & (q[:, 4:5] < 0.2)
|
||||
return {'gm': torch.where(collision, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
|
||||
|
||||
task = {'q0': [0] * 6, 'T': [goal_error, 0, 0]}
|
||||
commit_n = len(increments)
|
||||
traj = torch.zeros(1, commit_n + 1, 6)
|
||||
traj[0, 1:] = torch.tensor(increments).cumsum(0)
|
||||
patches = {
|
||||
'features': lambda *args: torch.zeros(1, 83),
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), goal_error), torch.zeros(q.shape[0])),
|
||||
}
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
|
||||
max_sec=0.02 * commit_n, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(traj), replan='empty', commit_n=commit_n,
|
||||
safety_partial=safety_partial, **kwargs)
|
||||
return per
|
||||
|
||||
def test_full_safety_rejects_collision_after_joint_clamp(self):
|
||||
# Raw endpoint/midpoint are safe, but the J5 upper limit puts the endpoint at (0.2, 0).
|
||||
per = self.rollout_candidate([[0.2, 0, 0, 0, 0.6, 0]], safety_partial=0)
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
torch.testing.assert_close(per['traj'], torch.zeros(1, 2, 6), rtol=0, atol=0)
|
||||
|
||||
def test_partial_safety_rejects_collision_after_joint_clamp(self):
|
||||
per = self.rollout_candidate([[0.2, 0, 0, 0, 0.6, 0]], safety_partial=1)
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
torch.testing.assert_close(per['traj'], torch.zeros(1, 2, 6), rtol=0, atol=0)
|
||||
|
||||
def test_partial_safety_keeps_only_clamped_safe_prefix(self):
|
||||
per = self.rollout_candidate([
|
||||
[0.05, 0, 0, 0, 0.6, 0],
|
||||
[0.2, 0, 0, 0, 0.6, 0],
|
||||
], safety_partial=1)
|
||||
self.assertAlmostEqual(per['traj'][0, -1, 0].item(), 0.05)
|
||||
self.assertEqual(per['traj'][0, -1, 4].item(), 0.0)
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
expected = torch.zeros(1, 3, 6)
|
||||
expected[0, 1:, 0] = 0.05
|
||||
torch.testing.assert_close(per['traj'], expected)
|
||||
torch.testing.assert_close(per['plans']['commit'][0, 0, 1], torch.zeros(6), rtol=0, atol=0)
|
||||
|
||||
def test_near_dls_rejects_clamped_collision_and_keeps_candidate(self):
|
||||
candidate = [[0.05, 0, 0, 0, 0.6, 0]]
|
||||
ref = torch.tensor([[[0.2, 0, 0, 0, 0.6, 0]]])
|
||||
for safety_partial in (0, 1):
|
||||
with self.subTest(safety_partial=safety_partial):
|
||||
with patch('ml.sim.closed_loop.dls_steps', return_value=ref) as dls:
|
||||
per = self.rollout_candidate(candidate, safety_partial, goal_error=2.0, dls_safety=1)
|
||||
dls.assert_called_once()
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
self.assertEqual(per['dls_it'].item(), -1)
|
||||
self.assertEqual(per['brakes'].item(), 0.0)
|
||||
expected = torch.zeros(1, 2, 6)
|
||||
expected[0, 1, 0] = 0.05
|
||||
torch.testing.assert_close(per['traj'], expected)
|
||||
torch.testing.assert_close(per['plans']['commit'], torch.tensor(candidate)[None, None])
|
||||
self.assertTrue(per['plans']['certified'].all().item())
|
||||
|
||||
def test_r3_clamped_queue_origin_prefix_and_certificates_match_execution(self):
|
||||
class SafeKin:
|
||||
def clamp_box(self, q):
|
||||
clamped = q.clone()
|
||||
clamped[..., 4] = clamped[..., 4].clamp(max=0.0)
|
||||
return clamped
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.ones_like(q[:, :1])}
|
||||
|
||||
prefixes = []
|
||||
|
||||
def capture_features(kin, q, qp, T, R, pre, r, sub, sf):
|
||||
prefixes.append((pre.clone(), int(r.item())))
|
||||
return torch.zeros(1, 83)
|
||||
|
||||
teacher = torch.zeros(1, 25, 6)
|
||||
teacher[0, :, 0] = 0.02 * torch.arange(25)
|
||||
teacher[0, :, 4] = 0.6 * torch.arange(25)
|
||||
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
|
||||
patches = {
|
||||
'features': capture_features,
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
|
||||
}
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
_, per = rollout(None, SafeKin(), [task], 'cpu', K=1, safety=True,
|
||||
max_sec=0.16, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(teacher), replan='r3', commit_n=3, safety_partial=1)
|
||||
self.assertEqual([r for _, r in prefixes], [0, 2, 3, 3])
|
||||
for pre, r in prefixes[1:]:
|
||||
torch.testing.assert_close(pre[..., 4], torch.zeros(1, 3), rtol=0, atol=0)
|
||||
torch.testing.assert_close(pre[:, :r, 0], torch.full((1, r), 0.02))
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
expected = torch.zeros(1, 9, 6)
|
||||
expected[0, :, 0] = 0.02 * torch.arange(9)
|
||||
torch.testing.assert_close(per['traj'], expected)
|
||||
plans = per['plans']
|
||||
self.assertEqual(plans['it'].tolist(), [0, 3, 6, 9])
|
||||
torch.testing.assert_close(plans['commit'][..., 4], torch.full((1, 4, 3), 0.6))
|
||||
self.assertTrue(plans['certified'].all().item())
|
||||
self.assertTrue((plans['gaps'] > 0).all().item())
|
||||
checked = 0
|
||||
for i, start in enumerate(plans['it'].tolist()):
|
||||
n = min(3, 8 - start)
|
||||
if n <= 0:
|
||||
continue
|
||||
torch.testing.assert_close(plans['qe'][:, i], per['traj'][:, start], rtol=0, atol=0)
|
||||
before = per['traj'][:, start:start + n]
|
||||
after = per['traj'][:, start + 1:start + n + 1]
|
||||
torch.testing.assert_close(plans['points'][:, i, :n], after, rtol=0, atol=0)
|
||||
torch.testing.assert_close(plans['midpoints'][:, i, :n], before + 0.5 * (after - before),
|
||||
rtol=0, atol=0)
|
||||
checked += n
|
||||
self.assertEqual(checked, 8)
|
||||
|
||||
def rollout_fallback(self, safety_partial, midpoint_collision=False):
|
||||
class ObstacleKin:
|
||||
def clamp_box(self, q):
|
||||
return q
|
||||
|
||||
def gaps(self, q, cap):
|
||||
collision = q[:, :1] > 0.3
|
||||
if midpoint_collision:
|
||||
collision |= (q[:, :1] > 0.09) & (q[:, :1] < 0.11)
|
||||
return {'gm': torch.where(collision, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
|
||||
|
||||
teacher = torch.zeros(1, 4, 6)
|
||||
teacher[0, :, 0] = torch.arange(4)
|
||||
ref = torch.zeros(1, 1, 6)
|
||||
ref[0, 0, 0] = 0.2
|
||||
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
|
||||
patches = {
|
||||
'features': lambda *args: torch.zeros(1, 83),
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
|
||||
'dls_steps': lambda *args, **kwargs: ref,
|
||||
}
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
|
||||
max_sec=0.06, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(teacher), replan='empty', commit_n=3,
|
||||
safety_partial=safety_partial, fb_gain=1.0, fb_fallback=1.0)
|
||||
return per
|
||||
|
||||
def test_fallback_executes_one_safe_dls_step_then_certified_zeros(self):
|
||||
for safety_partial in (0, 1):
|
||||
with self.subTest(safety_partial=safety_partial):
|
||||
per = self.rollout_fallback(safety_partial)
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
self.assertEqual(per['brakes'].item(), 1.0)
|
||||
expected = torch.zeros(1, 4, 6)
|
||||
expected[0, 1:, 0] = 0.2
|
||||
torch.testing.assert_close(per['traj'], expected)
|
||||
commit = torch.zeros(1, 1, 3, 6)
|
||||
commit[0, 0, 0, 0] = 0.2
|
||||
plans = per['plans']
|
||||
self.assertEqual(plans['it'].tolist(), [0])
|
||||
torch.testing.assert_close(plans['commit'], commit, rtol=0, atol=0)
|
||||
self.assertTrue(plans['certified'].all().item())
|
||||
self.assertTrue((plans['gaps'] > 0).all().item())
|
||||
torch.testing.assert_close(plans['points'][:, 0], expected[:, 1:])
|
||||
torch.testing.assert_close(plans['midpoints'][:, 0],
|
||||
expected[:, :-1] + 0.5 * (expected[:, 1:] - expected[:, :-1]))
|
||||
|
||||
def test_fallback_rejects_safe_endpoint_with_colliding_midpoint(self):
|
||||
for safety_partial in (0, 1):
|
||||
with self.subTest(safety_partial=safety_partial):
|
||||
per = self.rollout_fallback(safety_partial, midpoint_collision=True)
|
||||
self.assertEqual(per['col'].item(), 0.0)
|
||||
self.assertEqual(per['brakes'].item(), 1.0)
|
||||
torch.testing.assert_close(per['traj'], torch.zeros(1, 4, 6), rtol=0, atol=0)
|
||||
torch.testing.assert_close(per['plans']['commit'], torch.zeros(1, 1, 3, 6), rtol=0, atol=0)
|
||||
self.assertFalse(per['plans']['certified'].any().item())
|
||||
|
||||
|
||||
class SegmentCertificateTest(unittest.TestCase):
|
||||
class ThinObstacle(Kin):
|
||||
def __init__(self):
|
||||
self.dtype = torch.float64
|
||||
self.lo, self.hi = torch.full((6,), -5.0), torch.full((6,), 5.0)
|
||||
self.cont = torch.zeros(6, dtype=torch.bool)
|
||||
self.cert_groups = torch.tensor([0])
|
||||
self.cert_lever = torch.tensor([[1.0, 0, 0, 0, 0, 0]], dtype=self.dtype)
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': ((q[:, :1] - 0.25).abs() - 0.05).clamp(max=cap)}
|
||||
|
||||
def test_bound_detects_hidden_collision_and_accepts_clear_segment(self):
|
||||
a, b = torch.zeros(3, 6, dtype=torch.float64), torch.zeros(3, 6, dtype=torch.float64)
|
||||
a[:, 0] = torch.tensor([0.0, -1.0, 0.25])
|
||||
b[:, 0] = torch.tensor([1.0, 0.0, 0.25])
|
||||
result = self.ThinObstacle().certify_segments(a, b)
|
||||
self.assertEqual(result['safe'].tolist(), [False, True, False])
|
||||
self.assertEqual(result['collision'].tolist(), [True, False, True])
|
||||
self.assertEqual(result['queries'].tolist(), [4, 2, 2])
|
||||
|
||||
def test_budget_and_depth_exhaustion_do_not_accept_unknown_segment(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
b[:, 0] = 1.0
|
||||
kin = self.ThinObstacle()
|
||||
for kwargs in ({'max_queries': 2}, {'max_depth': 0}):
|
||||
with self.subTest(**kwargs):
|
||||
result = kin.certify_segments(a, b, **kwargs)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['unresolved'].item())
|
||||
self.assertEqual(result['queries'].item(), 2)
|
||||
|
||||
def test_shape_mismatch_between_endpoints_is_rejected(self):
|
||||
a = torch.zeros(1, 6, dtype=torch.float64)
|
||||
b = torch.zeros(2, 6, dtype=torch.float64)
|
||||
kin = self.ThinObstacle()
|
||||
for method in (kin.certify_segments, kin.certify_uniform):
|
||||
with self.subTest(method=method.__name__):
|
||||
with self.assertRaisesRegex(ValueError, 'identical shape'):
|
||||
method(a, b)
|
||||
|
||||
def test_budget_exhaustion_scales_with_gap_and_bound(self):
|
||||
class ConstantGap(self.ThinObstacle):
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.full((q.shape[0], 1), 0.1, dtype=q.dtype, device=q.device)}
|
||||
|
||||
# 界取 1 mm/度:1° 段要证明无碰撞,必须细分到位移界 < 两端间隙之和(0.2 mm)。
|
||||
kin = ConstantGap()
|
||||
kin.cert_lever = torch.ones(1, 6, dtype=torch.float64)
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
b[:, 0] = 1.0
|
||||
for budget, safe, queries in ((4, False, 3), (8, False, 5), (9, True, 9)):
|
||||
with self.subTest(budget=budget):
|
||||
result = kin.certify_segments(a, b, max_queries=budget)
|
||||
self.assertEqual(bool(result['safe'].item()), safe)
|
||||
self.assertEqual(int(result['queries'].item()), queries)
|
||||
|
||||
def test_tensor_budget_cannot_wrap_depth(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
a[:, 0] = b[:, 0] = 0.25
|
||||
for tensor in (torch.tensor(255, dtype=torch.uint8), torch.tensor(255)):
|
||||
with self.subTest(dtype=tensor.dtype):
|
||||
with self.assertRaisesRegex(ValueError, 'plain ints'):
|
||||
self.ThinObstacle().certify_segments(a, b, max_depth=tensor)
|
||||
|
||||
def test_batch_matches_per_root_and_ignores_order(self):
|
||||
kin = self.ThinObstacle()
|
||||
a = torch.tensor([[0.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
b = torch.tensor([[1.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [0.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
batch = kin.certify_segments(a, b, max_queries=16)
|
||||
self.assertEqual(batch['collision'].tolist(), [True, True, False])
|
||||
self.assertEqual(batch['safe'].tolist(), [False, False, True])
|
||||
for perm in ([0, 1, 2], [2, 1, 0], [1, 0, 2]):
|
||||
index = torch.tensor(perm)
|
||||
moved = kin.certify_segments(a[index], b[index], max_queries=16)
|
||||
for key in ('safe', 'collision', 'unresolved', 'queries'):
|
||||
torch.testing.assert_close(moved[key], batch[key][index], rtol=0, atol=0)
|
||||
|
||||
def test_empty_batch_and_all_rejected_frontier(self):
|
||||
kin = self.ThinObstacle()
|
||||
empty = kin.certify_segments(torch.zeros(0, 6, dtype=torch.float64), torch.zeros(0, 6, dtype=torch.float64))
|
||||
self.assertEqual(empty['safe'].shape, (0,))
|
||||
a = torch.tensor([[0.25, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
result = kin.certify_segments(a, a.clone(), max_queries=2)
|
||||
self.assertEqual(result['safe'].tolist(), [False, False])
|
||||
self.assertEqual(result['collision'].tolist(), [True, False])
|
||||
self.assertEqual(result['outside_box'].tolist(), [False, True])
|
||||
|
||||
def test_uniform_matches_bound_formula_and_needs_no_round_trips(self):
|
||||
class ConstantGap(self.ThinObstacle):
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.full((q.shape[0], 1), 0.1, dtype=q.dtype, device=q.device)}
|
||||
|
||||
# 界 1 mm/度、间隙 0.1 mm:子段判据 0.2 > 1/n 需要 n ≥ 6,n=5 必须落 unproven。
|
||||
kin = ConstantGap()
|
||||
kin.cert_lever = torch.ones(1, 6, dtype=torch.float64)
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
b[:, 0] = 1.0
|
||||
for n, safe in ((5, False), (6, True)):
|
||||
with self.subTest(n=n):
|
||||
result = kin.certify_uniform(a, b, n=n)
|
||||
self.assertEqual(bool(result['safe'].item()), safe)
|
||||
self.assertEqual(result['points'], n + 1)
|
||||
self.assertFalse(bool(result['collision'].item()))
|
||||
self.assertEqual(bool(result['unproven'].item()), not safe)
|
||||
self.assertAlmostEqual(result['cap_required_mm'], 1.0 / n + 2e-4, places=9)
|
||||
|
||||
def test_uniform_catches_hidden_collision_and_rejects_short_grids(self):
|
||||
a = torch.tensor([[0.0, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
b = torch.tensor([[1.0, 0, 0, 0, 0, 0], [0.0, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
kin = self.ThinObstacle()
|
||||
short = kin.certify_uniform(a, b, n=2)
|
||||
self.assertEqual(short['collision'].tolist(), [False, False, False])
|
||||
self.assertEqual(short['unproven'].tolist(), [True, False, False])
|
||||
self.assertEqual(short['safe'].tolist(), [False, True, False])
|
||||
for n in (4, 8, 64):
|
||||
with self.subTest(n=n):
|
||||
result = kin.certify_uniform(a, b, n=n)
|
||||
self.assertEqual(result['collision'].tolist(), [True, False, False])
|
||||
self.assertEqual(result['safe'].tolist(), [False, True, False])
|
||||
self.assertEqual(result['outside_box'].tolist(), [False, False, True])
|
||||
|
||||
def test_uniform_requires_plain_int_count(self):
|
||||
a = torch.zeros(1, 6, dtype=torch.float64)
|
||||
for tensor in (torch.tensor(8, dtype=torch.uint8), torch.tensor(8)):
|
||||
with self.subTest(dtype=tensor.dtype):
|
||||
with self.assertRaisesRegex(ValueError, 'subdivision count'):
|
||||
self.ThinObstacle().certify_uniform(a, a.clone(), n=tensor)
|
||||
|
||||
def test_nonfinite_and_outside_box_are_rejected(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
kin = self.ThinObstacle()
|
||||
with patch.object(kin, 'gaps', side_effect=lambda q, cap: {'gm': torch.full((q.shape[0], 1), float('nan'))}):
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['unresolved'].item())
|
||||
for value in (float('inf'), -float('inf')):
|
||||
with self.subTest(value=value):
|
||||
with patch.object(kin, 'gaps',
|
||||
side_effect=lambda q, cap, v=value: {'gm': torch.full((q.shape[0], 1), v)}):
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
a[:, 0] = b[:, 0] = 6.0
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['outside_box'].item())
|
||||
|
||||
def test_point_segment_distance_is_symmetric_and_differentiable(self):
|
||||
p = torch.tensor([[0.0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], dtype=torch.float64, requires_grad=True)
|
||||
q = torch.tensor([[2.0, 0, 0]] * 4, dtype=torch.float64)
|
||||
point = torch.tensor([[1.0, 0, 0], [1, 3, 0], [4, 0, 0], [-1, 0, 0]], dtype=torch.float64)
|
||||
forward = seg_seg_dist(p, q, point, point)
|
||||
reverse = seg_seg_dist(point, point, p, q)
|
||||
torch.testing.assert_close(forward, torch.tensor([0.0, 3, 2, 1], dtype=torch.float64), atol=1e-8, rtol=0)
|
||||
torch.testing.assert_close(forward, reverse, atol=1e-8, rtol=0)
|
||||
forward.sum().backward()
|
||||
self.assertTrue(torch.isfinite(p.grad).all())
|
||||
torch.testing.assert_close(seg_seg_dist(p.detach(), p.detach(), point, point),
|
||||
(p.detach() - point).norm(dim=-1), atol=1e-8, rtol=0)
|
||||
|
||||
|
||||
class SpeedAndMarginTest(unittest.TestCase):
|
||||
"""轮次33:反馈叠加后的每轴速度限幅,以及安全层的正余量门限(含 cap 截顶坑)。"""
|
||||
STEP = [1.2, 1.2, 1.8, 1.8, 2.4, 3.6]
|
||||
|
||||
def rollout_one(self, teacher_step, gap_value, fb=None, **kwargs):
|
||||
class FlatKin:
|
||||
def clamp_box(self, q):
|
||||
return q
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.full((q.shape[0], 49), float(gap_value))}
|
||||
|
||||
commit_n = 2
|
||||
traj = torch.zeros(1, commit_n + 1, 6)
|
||||
traj[0, 1:] = torch.tensor(teacher_step)[None].repeat(commit_n, 1).cumsum(0) # teacher_step = 每步各轴的度数增量
|
||||
patches = {
|
||||
'features': lambda *args: torch.zeros(1, 83),
|
||||
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
|
||||
}
|
||||
with patch.multiple('ml.sim.closed_loop', **patches):
|
||||
with patch('ml.sim.closed_loop.dls_steps', return_value=torch.tensor([[[1.0, 0, 0, 0, 0, 0]]])):
|
||||
_, per = rollout(None, FlatKin(), [{'q0': [0] * 6, 'T': [10, 0, 0]}], 'cpu', K=1, safety=True,
|
||||
max_sec=0.02 * commit_n, return_traj=True, use_sub=False, select='first',
|
||||
oracle=TeacherOracle(traj), replan='empty', commit_n=commit_n,
|
||||
**kwargs)
|
||||
return per
|
||||
|
||||
def test_feedback_over_speed_is_measured_not_assumed(self):
|
||||
# 策略给满 STEP(J1 +1.2°)+ 2.2×1.0° 反馈 = 3.4° > VMAX·DT
|
||||
per = self.rollout_one([1.2, 0, 0, 0, 0, 0], 5.0, fb_gain=2.2)
|
||||
self.assertAlmostEqual(per['traj'][0, 1, 0].item(), 3.4, places=5)
|
||||
self.assertAlmostEqual(per['speed_ratio'][0].item(), 3.4 / 1.2, places=5)
|
||||
|
||||
def test_inc_cap_limits_actual_step_to_vmax(self):
|
||||
per = self.rollout_one([1.2, 0, 0, 0, 0, 0], 5.0, fb_gain=2.2, inc_cap=1)
|
||||
self.assertAlmostEqual(per['traj'][0, 1, 0].item(), 1.2, places=5)
|
||||
self.assertLessEqual(per['speed_ratio'][0].item(), 1.0 + 1e-6)
|
||||
|
||||
def test_margin_rejects_near_obstacle_step_that_passes_at_zero(self):
|
||||
moved = self.rollout_one([0.5, 0, 0, 0, 0, 0], 0.3, safety_partial=1)
|
||||
self.assertAlmostEqual(moved['traj'][0, -1, 0].item(), 1.0, places=5)
|
||||
self.assertEqual(moved['brakes'].item(), 0.0)
|
||||
braked = self.rollout_one([0.5, 0, 0, 0, 0, 0], 0.3, gap_margin=0.5, safety_partial=1)
|
||||
self.assertEqual(braked['brakes'].item(), 1.0)
|
||||
torch.testing.assert_close(braked['traj'], torch.zeros(1, 3, 6), rtol=0, atol=0)
|
||||
|
||||
def test_margin_below_cap_still_accepts_clear_step(self):
|
||||
# 余量 0.5 必须仍能通过 5 mm 间隙:若宽相位 cap 没抬到 margin+0.5,截顶值 0.5 会让门限恒假
|
||||
per = self.rollout_one([0.5, 0, 0, 0, 0, 0], 5.0, gap_margin=0.5, safety_partial=1)
|
||||
self.assertEqual(per['brakes'].item(), 0.0)
|
||||
self.assertAlmostEqual(per['traj'][0, -1, 0].item(), 1.0, places=5)
|
||||
self.assertTrue((per['plans']['gaps'] > 0.5).all().item())
|
||||
|
||||
|
||||
class FeatureGeometryReuseTest(unittest.TestCase):
|
||||
"""#16 第①条:gm_groups 复用调用者已算好的 cum_xf,省一次 FK,但结果必须与原口径逐位一致。"""
|
||||
def test_gm_groups_with_supplied_xf_is_bitwise_identical(self):
|
||||
from ml.common.features import features, GP, GAP_CAP
|
||||
kin = Kin()
|
||||
q = torch.tensor([[-129.9, 46.68, 43.49, -97.46, -85.1, -139.7],
|
||||
[10.0, -20.0, 30.0, -40.0, 50.0, -60.0]])
|
||||
xf = kin.cum_xf(q)
|
||||
torch.testing.assert_close(kin.gm_groups(q, GP, GAP_CAP, xf=xf), kin.gm_groups(q, GP, GAP_CAP),
|
||||
rtol=0, atol=0)
|
||||
idx = [a * 7 + b for a, b in GP]
|
||||
table = kin.gaps(q, GAP_CAP, xf)['gm'][:, idx]
|
||||
torch.testing.assert_close(kin.gm_groups(q, GP, GAP_CAP), table, rtol=0, atol=0)
|
||||
z = torch.zeros(len(q), 6)
|
||||
f = features(kin, q, z, q[:, :3], torch.zeros(len(q), 9), z[:, None, :].expand(len(q), 3, 6),
|
||||
torch.zeros(len(q)), z, torch.zeros(len(q)))
|
||||
cols = 12 + 3 + 6 + 3 + 6 + 3 + 6 + 3 + 3 + 2 + 18 + 3 + 6 + 1 # 间隙块之前的特征数 = 75
|
||||
self.assertEqual(cols + 8, 83)
|
||||
torch.testing.assert_close(f[:, cols:cols + 8], table / GAP_CAP, rtol=0, atol=0)
|
||||
|
||||
|
||||
class CertBatchEquivalenceTest(unittest.TestCase):
|
||||
"""#16 第②条:把 K 条候选的认证并成一次 gaps 调用,选择结果必须与串行循环逐位一致。"""
|
||||
class StubModel:
|
||||
act_scale = 1.0
|
||||
|
||||
def __init__(self, act):
|
||||
self.act = act
|
||||
|
||||
def sample_k(self, feat, K, z, steps=1):
|
||||
B = feat.shape[0]
|
||||
return (self.act.expand(B, K, 16, 6).clone(), torch.zeros(B, device=feat.device),
|
||||
torch.zeros(B, 13, device=feat.device))
|
||||
|
||||
def run_both(self, acts, reject_first):
|
||||
class StepKin:
|
||||
def clamp_box(self, q):
|
||||
return q
|
||||
|
||||
def gaps(self, q, cap):
|
||||
bad = (q[:, 0] > 0.5) if reject_first else torch.zeros(len(q), dtype=torch.bool, device=q.device)
|
||||
return {'gm': torch.where(bad[:, None], -torch.ones(len(q), 49, device=q.device),
|
||||
torch.full((len(q), 49), 1.0, device=q.device))}
|
||||
|
||||
acts = torch.tensor(acts, dtype=torch.float32) # [K,16,6] 动作(±1 归一化)
|
||||
model = self.StubModel(acts)
|
||||
tasks = [{'q0': [0] * 6, 'T': [10, 0, 0]}] * 2 # B=2:批量索引形状必须在多行下也成立
|
||||
out = []
|
||||
for batched in (0, 1):
|
||||
with patch('ml.sim.closed_loop.features', return_value=torch.zeros(len(tasks), 83)), \
|
||||
patch('ml.sim.closed_loop.goal_err',
|
||||
side_effect=lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0]))):
|
||||
_, per = rollout(model, StepKin(), tasks, 'cpu', K=acts.shape[0], safety=True, max_sec=0.04,
|
||||
return_traj=True, use_sub=False, select='first', replan='empty', commit_n=2,
|
||||
safety_partial=1, cert_batch=batched)
|
||||
out.append(per)
|
||||
return out
|
||||
|
||||
def test_batched_certification_selects_the_same_candidate(self):
|
||||
# 候选 0 第 2 步撞上 q0>0.5 的障碍 ⇒ 只有前缀 1 步;候选 1 全程安全 ⇒ 应被选中
|
||||
acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14,
|
||||
[[0.1, 0, 0, 0, 0, 0], [0.1, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
|
||||
seq, batched = self.run_both(acts, reject_first=True)
|
||||
torch.testing.assert_close(batched['traj'], seq['traj'], rtol=0, atol=0)
|
||||
torch.testing.assert_close(batched['plans']['commit'], seq['plans']['commit'], rtol=0, atol=0)
|
||||
torch.testing.assert_close(batched['plans']['certified'], seq['plans']['certified'], rtol=0, atol=0)
|
||||
self.assertEqual(batched['brakes'].tolist(), seq['brakes'].tolist())
|
||||
self.assertAlmostEqual(seq['traj'][0, -1, 0].item(), 0.24, places=5) # 候选 1 的两步 0.1×STEP=0.12°
|
||||
|
||||
def test_batched_certification_keeps_partial_prefix_choice(self):
|
||||
# 所有候选都只走得了第 1 步 ⇒ 两条路径都取第 1 条候选、前缀长度 1
|
||||
acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14,
|
||||
[[0.3, 0, 0, 0, 0, 0], [0.6, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
|
||||
seq, batched = self.run_both(acts, reject_first=True)
|
||||
torch.testing.assert_close(batched['traj'], seq['traj'], rtol=0, atol=0)
|
||||
torch.testing.assert_close(batched['plans']['commit'], seq['plans']['commit'], rtol=0, atol=0)
|
||||
self.assertAlmostEqual(seq['traj'][0, 1, 0].item(), 0.24, places=5)
|
||||
self.assertAlmostEqual(seq['traj'][0, 2, 0].item(), 0.24, places=5) # 第 2 步被拒后停在原地
|
||||
|
||||
|
||||
def test_mismatched_xf_row_count_is_rejected(self):
|
||||
kin = Kin()
|
||||
q1 = torch.zeros(1, 6); q5 = torch.zeros(5, 6)
|
||||
xf5 = kin.cum_xf(q5)
|
||||
for name, fn in (('gaps', lambda: kin.gaps(q1, 40.0, xf5)),
|
||||
('gm_groups', lambda: kin.gm_groups(q1, [(0, 2)], 40.0, xf=xf5))):
|
||||
with self.subTest(fn=name):
|
||||
with self.assertRaisesRegex(ValueError, '行数'):
|
||||
fn()
|
||||
self.assertEqual(kin.gaps(q5, 40.0, xf5)['gm'].shape, (5, 49))
|
||||
self.assertEqual(kin.gm_groups(q5, [(0, 2)], 40.0, xf=xf5).shape, (5, 1))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -55,7 +55,9 @@ async function boot() {
|
||||
}
|
||||
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, {});
|
||||
const FB = qs.get('fb') === null ? 1.8 : +qs.get('fb'); // 2026-10-20:全程目标反馈(fb=0 关闭)
|
||||
const COMMIT = qs.get('commit') === null ? 3 : Math.max(1, +qs.get('commit')); // 部署节奏:commit=1 每步规划
|
||||
runner = new ArmPolicy.Runner(S, infer, { actScale: meta.act_scale || 1, fbGain: FB, commit: COMMIT }); // 2026-10-19:动作尺度随模型(旧模型无此字段 = 1)
|
||||
/* 预热:首次推理有编译开销 */
|
||||
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' }));
|
||||
|
||||
+118
-36
@@ -138,7 +138,19 @@ function refineStep(S, q, T, Rt, maxScale) {
|
||||
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 = [];
|
||||
if (S.jac) {
|
||||
/* 解析几何雅可比(与 ml/sim/closed_loop.py 的 dls_steps(analytic=True) 同口径):
|
||||
有限差分在 float32/双精度下都会给反馈方向注入 ~1–4% 噪声,闭环里会被慢变误差放大。
|
||||
J[j][i] = −∂err_i/∂q_j:位置 err=T−p → +Jv;姿态 err=100·rotvec → +100·Jw。 */
|
||||
const jv = S.jac(q);
|
||||
for (let j = 0; j < 6; j++) {
|
||||
const col = [jv.Jv[0][j], jv.Jv[1][j], jv.Jv[2][j]];
|
||||
if (m === 6) col.push(100 * jv.Jw[0][j], 100 * jv.Jw[1][j], 100 * jv.Jw[2][j]);
|
||||
J.push(col);
|
||||
}
|
||||
} else {
|
||||
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); } }
|
||||
@@ -178,19 +190,21 @@ class Runner {
|
||||
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(超出按刹停处理)
|
||||
stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, subRule: 'cross', subLeadSec: 0.02, seed: 12345, lookahead: true,
|
||||
actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45, fbGain: 0, segCert: true, select: 'consist' }, opts || {});
|
||||
/* 2026-10-19:actScale = 模型 meta.act_scale(动作标签按 act_scale·STEP 归一化,旧模型 1);增量 = clamp(act, ±1)·actScale·STEP,
|
||||
与 ml/sim/closed_loop.py 同口径。subRule:'cross'(默认,越过顶点即切,与训练标签同口径;见 _subAdvance)/ 'timeout'(旧:< subReachDeg 或 subTimeout 超时)。 */ // 每条候选最多 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.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0, partial: 0, starveTicks: 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;
|
||||
this.subs = []; this.subT = 0; this.subPrev = null; 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);
|
||||
@@ -211,7 +225,10 @@ class Runner {
|
||||
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]);
|
||||
if (d) this.q = this.q.map((v, j) => { // 与 ml/sim/closed_loop.py 的 clamp_box 同口径:限位轴执行时夹到 LIM
|
||||
const id = this.S.ORDER[j], L = this.S.LIM[id], x = v + d[j];
|
||||
return L.cont ? x : Math.max(L.lo, Math.min(L.hi, x));
|
||||
});
|
||||
this.t += DT;
|
||||
if (this.goal && !this.queue.length) {
|
||||
const { e, oe } = this.err(this.q);
|
||||
@@ -220,29 +237,79 @@ class Runner {
|
||||
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); }
|
||||
const inc = [], sc = this.o.actScale || 1;
|
||||
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])) * sc * STEP[j]); inc.push(row); }
|
||||
return inc;
|
||||
}
|
||||
_certCommit(qe, steps) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
|
||||
/* 子目标推进(与 ml/sim/closed_loop.py sub_advance 同一规则,三处同步改)。
|
||||
2026-10-19(审查 C5 / P0-1):训练标签里 sub = 老师「下一个尚未经过的顶点」,老师 τ 一过顶点(−0.02 s)就换;
|
||||
旧规则要 < 5° 或 1.5 s 超时,策略在顶点附近进不去时 sub 一直指着身后的点(训练里从未出现的特征序列)。
|
||||
'cross':进入方向 a = 顶点 − 上一点,离开方向 b = 下一顶点 − 顶点(最后一个取 a),各轴 /VMAX 折成秒;
|
||||
法向 n = â + b̂,(qe − 顶点)·n̂ ≥ −subLeadSec 即越过。没有超时(标签也没有)。 */
|
||||
_subAdvance(qe) {
|
||||
if (!this.subs.length) return;
|
||||
const S = this.S, o = this.o, sg = this.subs[0];
|
||||
const df = (a, b, j) => (S.LIM[S.ORDER[j]].cont ? wrap180(a - b) : a - b);
|
||||
let adv = false;
|
||||
if (o.subRule === 'timeout') {
|
||||
let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(df(sg[j], qe[j], j)));
|
||||
adv = m < o.subReachDeg || this.t - this.subT > o.subTimeout;
|
||||
} else {
|
||||
const unit = v => { const n = Math.hypot.apply(null, v); return n > 1e-9 ? v.map(x => x / n) : v.map(() => 0); };
|
||||
const prev = this.subPrev || this.q;
|
||||
const a = [0, 1, 2, 3, 4, 5].map(j => df(sg[j], prev[j], j) / VMAX[j]);
|
||||
const b = this.subs.length > 1 ? [0, 1, 2, 3, 4, 5].map(j => df(this.subs[1][j], sg[j], j) / VMAX[j]) : a;
|
||||
const ua = unit(a), ub = unit(b), n = unit(ua.map((x, j) => x + ub[j]));
|
||||
let proj = 0; for (let j = 0; j < 6; j++) proj += df(qe[j], sg[j], j) / VMAX[j] * n[j];
|
||||
adv = proj >= -o.subLeadSec;
|
||||
}
|
||||
if (adv) { this.subPrev = this.subs.shift(); this.subT = this.t; }
|
||||
}
|
||||
_certCommit(qe, steps, partial) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
|
||||
/* partial:与 ml/sim/closed_loop.py 的 safety_partial 同口径——整段不过时依次缩短前缀,
|
||||
执行到第一个不安全步为止(而不是整段拒绝 → 刹停)。2026-10-20:浏览器 bench 刹车率 47% 的主因是整段拒绝。 */
|
||||
const tryPath = n => {
|
||||
const use = steps.slice(0, n);
|
||||
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]);
|
||||
for (const d of use) { q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
|
||||
if (!this.o.segCert) {
|
||||
/* 离散判定(步点 + 中点,与 ml/sim/closed_loop.py 安全层同口径):capsule 模型余量已含在 collideFree。
|
||||
连续区间认证(segCert)~40 ms/次,是 50 Hz 部署的主要瓶颈;关闭后 planP50 预计 <30 ms。 */
|
||||
let prev = qe.slice();
|
||||
for (const p of pts.slice(1)) {
|
||||
const mid = prev.map((v, j) => 0.5 * (v + p[j]));
|
||||
if (!this.S.collideFree(mid) || !this.S.collideFree(p)) return null;
|
||||
prev = p;
|
||||
}
|
||||
const tail = brakeSteps(use.length ? use[use.length - 1] : [0, 0, 0, 0, 0, 0]);
|
||||
let qq = q;
|
||||
if (tail.length) { for (const d of tail) qq = qq.map((v, j) => v + d[j]); if (!this.S.collideFree(qq)) return null; }
|
||||
return { ok: true, tail: tail, steps: use };
|
||||
}
|
||||
for (let i = 1; i < pts.length; i++) if (!this.S.collideFree(pts[i])) return null;
|
||||
const tail = brakeSteps(use.length ? use[use.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()); }
|
||||
let qq = q;
|
||||
if (tail.length) { for (const d of tail) qq = qq.map((v, j) => v + d[j]); pts.push(qq.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 };
|
||||
return { ok: c.ok, tail: tail, steps: use };
|
||||
};
|
||||
if (!partial) { const r = tryPath(steps.length); return r || { ok: false, tail: [] }; }
|
||||
for (let n = steps.length; n >= 1; n--) { const r = tryPath(n); if (r && r.ok) return r; }
|
||||
return { ok: false, tail: [] };
|
||||
}
|
||||
async plan() {
|
||||
if (!this.needPlan()) return;
|
||||
this.planning = true;
|
||||
const qlen0 = this.queue.length; // 规划开始时队列里可执行步数
|
||||
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;
|
||||
/* 50 Hz 饥饿估计:规划耗时 dt 期间每 20 ms 消耗一步;超过 qlen0 的步数即队列空转 tick */
|
||||
this.stats.starveTicks += Math.max(0, Math.ceil(dt / (DT * 1000)) - qlen0);
|
||||
}
|
||||
}
|
||||
async _plan() {
|
||||
@@ -257,13 +324,11 @@ class Runner {
|
||||
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; }
|
||||
/* 2026-10-19(审查 F5):DLS 接管时没有「已提交的模型计划」,lastPlan 置空(原来保留上一条,下次一致性去对齐一条没执行的轨迹;仿真同步) */
|
||||
if (c.ok) { this.queue.push(...steps); this.brakeTail = c.tail; this.stats.refine++; this.lastPlan = null; 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; }
|
||||
}
|
||||
this._subAdvance(qe);
|
||||
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);
|
||||
@@ -279,28 +344,28 @@ class Runner {
|
||||
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));
|
||||
if (sb[12] > 0.5) { this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180)); this.subPrev = this.q.slice(); }
|
||||
}
|
||||
/* 候选:去掉前缀后的提交步,投影,打分 */
|
||||
const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64);
|
||||
const cands = [], near = o.projMax > 0 ? S.criticalPairs(qe, o.margin + 10).slice(0, 64) : [];
|
||||
// 2026-10-20:全程目标反馈(与 ml/sim/closed_loop.py 的 fb_gain 一致)——一次 DLS,加到所有候选所有步
|
||||
const fbStep = (o.fbGain > 0 && this.goal) ? refineStep(S, qe, this.goal.T, this.goal.R, 0.3).dq : null;
|
||||
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;
|
||||
if (fbStep) for (let s = 0; s < H; s++) for (let j = 0; j < 6; j++) inc[s][j] += o.fbGain * fbStep[j];
|
||||
const steps = inc.slice(r, r + o.commit); // 先不投影;只对进入认证的候选投影(2026-10-20:投影 8 份候选占安全层 ~13 ms)
|
||||
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 步)
|
||||
if (o.select === 'endpoint') {
|
||||
/* 2026-10-20:端点进度选择(与 ml/sim/closed_loop.py select='endpoint' 同口径)——n=512 时间效率 29→35%、ratio2s 0.343→0.285 */
|
||||
const qa = fut[fut.length - 1], pe = S.fk(qa, 0);
|
||||
score = -Math.hypot(pe[0] - this.goal.T[0], pe[1] - this.goal.T[1], pe[2] - this.goal.T[2]);
|
||||
} else if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐:lastPlan[i] = 旧 qe 走 i 步,新 qe = lastPlan[commit],fut[s] 对应 lastPlan[s+commit+1]
|
||||
// 2026-10-19 修错位(原对齐 lastPlan[s+commit],奖励比上一计划慢一步的候选;Python closed_loop.py 同步修正)
|
||||
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);
|
||||
for (let s = 0; s < fut.length && s + o.commit + 1 < 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 + 1][j]) / STEP[j], 2);
|
||||
c += w * d2;
|
||||
}
|
||||
score -= 0.02 * c;
|
||||
@@ -319,7 +384,7 @@ class Runner {
|
||||
/* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。
|
||||
2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐;
|
||||
前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */
|
||||
if (o.lookahead && this.cold) {
|
||||
if (o.lookahead && this.cold && o.select !== 'endpoint') {
|
||||
this.stats.lookahead++;
|
||||
const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6);
|
||||
pool.forEach((c, i) => {
|
||||
@@ -338,16 +403,33 @@ class Runner {
|
||||
collideFree 1.5 ms、criticalPairs 3.1 ms,推理本身 1.5 ms——耗时在几何查询,不在模型。此前截止只在候选之间检查,
|
||||
单条候选最多 40 次查询(≈150 ms)不受限。现在墙钟截止下沉到认证内部:第 1 条候选截止 t1+firstMs,其余共享到 t1+safetyMs;
|
||||
到点判不过 → 沿上次已认证的刹停段停,安全性不变,只是可能多刹停。 */
|
||||
let picked = null;
|
||||
let picked = null, pickedSteps = 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; }
|
||||
let steps = c.steps;
|
||||
if (o.projMax > 0) { // 投影只做给要认证的候选(按需),不再对全部 K 条预算
|
||||
const adj = []; let q = qe.slice();
|
||||
for (const d of c.steps) {
|
||||
const qn = q.map((v, j) => v + d[j]);
|
||||
let qp = project(S, qn, o.margin, o.projMax, near);
|
||||
qp = qp.map((v, j) => { const L = S.LIM[S.ORDER[j]]; return L.cont ? v : Math.max(L.lo, Math.min(L.hi, v)); });
|
||||
if (qp !== qn) this.stats.projected++;
|
||||
adj.push(qp.map((v, j) => v - q[j])); q = qp;
|
||||
}
|
||||
steps = adj;
|
||||
}
|
||||
const v = this._certCommit(qe, steps, true);
|
||||
if (v.ok) { picked = c; pickedSteps = v.steps; 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;
|
||||
if (pickedSteps.length < picked.steps.length) this.stats.partial++;
|
||||
this.queue.push(...pickedSteps);
|
||||
/* 部分前缀时实际执行步数 < commit,一致性对齐的偏移不再成立 → 置空(Python 侧同轮次 14 的部分前缀满分对齐留待后续统一) */
|
||||
this.lastPlan = pickedSteps.length === picked.steps.length ? [qe.slice()].concat(picked.fut) : null;
|
||||
this.brakeRun = 0;
|
||||
} else {
|
||||
this.stats.brakes++; this.brakeRun++;
|
||||
if (this.brakeTail.length) { this.queue.push(...this.brakeTail); this.brakeTail = []; }
|
||||
|
||||
+18
-1
@@ -731,6 +731,23 @@ function make(cfg) {
|
||||
for (let i = 0; i < n; i++) M = mul3(M, rotAxis(D[ORDER[i]], q[i] * D2R));
|
||||
return M;
|
||||
}
|
||||
/* 几何雅可比(对度):Jv[i][j] = d p_i/d q_j (mm/deg),Jw[i][j] = 世界角速度分量/deg。
|
||||
J4/J5 同轴不影响正确性:两者各贡献同一世界轴。 */
|
||||
function jac(q) {
|
||||
const { R: Rs, T: Ts } = cumXf(q);
|
||||
const p = fk(q, 0);
|
||||
const Jv = [[], [], []], Jw = [[], [], []];
|
||||
for (let j = 0; j < 6; j++) {
|
||||
const id = ORDER[j], Rj = Rs[j], tj = Ts[j], pj = P[id];
|
||||
const a = mv3(Rj, D[id]);
|
||||
const lever = [p[0] - (Rj[0] * pj[0] + Rj[1] * pj[1] + Rj[2] * pj[2] + tj[0]),
|
||||
p[1] - (Rj[3] * pj[0] + Rj[4] * pj[1] + Rj[5] * pj[2] + tj[1]),
|
||||
p[2] - (Rj[6] * pj[0] + Rj[7] * pj[1] + Rj[8] * pj[2] + tj[2])];
|
||||
const v = cross3(a, lever);
|
||||
for (let i = 0; i < 3; i++) { Jv[i][j] = v[i] * D2R; Jw[i][j] = a[i] * D2R; }
|
||||
}
|
||||
return { Jv: Jv, Jw: Jw };
|
||||
}
|
||||
/* M5 第 3 列(工具轴)列向量传播:J4/J5 同轴 ⇒ R4·R5=R(θ4+θ5) 合并为一次旋转,
|
||||
只乘单位向量、不构造 3×3、免 T3/mul3/rotErrDeg/acos。与 orient(q,5) 列 2 一致(≤1e-15)。 */
|
||||
const SAMEAX = D.J4[0] === D.J5[0] && D.J4[1] === D.J5[1] && D.J4[2] === D.J5[2];
|
||||
@@ -3233,7 +3250,7 @@ function make(cfg) {
|
||||
return { poseHit: pHit, poseTried: pTried, poseErr: pErr, poseMs: pTried ? pMs / pTried : 0 };
|
||||
}
|
||||
|
||||
return { fk: fk, fkChain: fkChain, solve: solve, solveTo: solveTo, solveWarm: solveWarm, solveDyn: solveDyn,
|
||||
return { fk: fk, fkChain: fkChain, jac: jac, solve: solve, solveTo: solveTo, solveWarm: solveWarm, solveDyn: solveDyn,
|
||||
solveRoute: solveRoute, routeFrom: routeFrom, transitPt: transitPt, planAround: planAround, edgeFree: edgeFree, PST: PST, unwrapTo: unwrapTo, rtWarm: rtWarm, rtReset: () => { RT = null; }, rmPlan: rmPlan, rmSmooth: rmSmooth, modelFingerprint: modelFingerprint, RM: RM, wd: wd, classGap: classGap, classFingerprint: classFingerprint, P2D: P2D,
|
||||
mixSolve: mixSolve, closest: closest, closest6: closest6, closestOri: closestOri, orient: orient,
|
||||
branch: branch, selfTest: selfTest, poseSelfTest: poseSelfTest,
|
||||
|
||||
Reference in New Issue
Block a user