From 3c250c7df3a4f61ed8f8a2bf9aacd358f57789b8 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 21 Oct 2026 00:57:08 +0800 Subject: [PATCH] =?UTF-8?q?exp:=20=E8=BD=AE=E6=AC=A146-47=E2=80=94?= =?UTF-8?q?=E2=80=94select=3Dpre(=E6=89=93=E5=88=86=E5=9C=B0=E5=B9=B3?= =?UTF-8?q?=E7=BA=BF=3D=E8=A2=AB=E6=89=A7=E8=A1=8C=E5=89=8D=E7=BC=80?= =?UTF-8?q?=E7=BB=88=E7=82=B9)=E6=AD=A3=E5=90=91=EF=BC=8C=E4=B8=A4=20seed?= =?UTF-8?q?=20=E5=A4=8D=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2 s 误差比逐题符号检验 s0 192好/64差 p=4.9e-16、s1 191/65 p=1.5e-15;时限内(≤1.3×老师) 0.2656→0.3320(+6.64pp)、0.2969→0.3398(+4.30pp);行程比 −0.171/−0.171;碰撞仍 0、超 VMAX 仍 0。 到达只 +5/+3 题(McNemar p=0.42/0.68 ⇒ 噪声),故不改默认口径、作可选口径入库。 代价:到达题末态误差 0.22→0.42 mm(判据壳 1 mm 内,余量 4.6×→2.4×),两 seed 都涨。 对照可比性先证:控制臂 tie0 与轮次42 round42_K8cb1 的 22/22 键逐位相同 ⇒ 该配方跨窗口确定性 复现,本轮起新杠杆可用单臂对已落盘控制。 实现:select='pre' 用 fut[:,:,commit_n-1] 的终点误差(同一次 goal_err 只换点集);两条可手算正对照 (前缀好/末端差 vs 末端好/前缀差 ⇒ 两口径必须选相反候选) + 不变式(commit_n=16 时与 endpoint 同选); 49 项测试绿;n=16 冒烟证非惰性。候选表同步修准:backoff/ep_waste 两行状态改判掉,#29/#30 登记。 --- docs/experiment_round46_ep16_smoke.json | 96 ++++++++++++++++++++ docs/experiment_round46_pre16_smoke.json | 96 ++++++++++++++++++++ docs/experiment_round46_pre256.json | 108 +++++++++++++++++++++++ docs/experiment_round47_ep_s1.json | 108 +++++++++++++++++++++++ docs/experiment_round47_pre_s1.json | 108 +++++++++++++++++++++++ docs/探索日志.md | 32 ++++++- ml/sim/closed_loop.py | 6 ++ tests/test_eval_cl.py | 50 +++++++++++ 8 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 docs/experiment_round46_ep16_smoke.json create mode 100644 docs/experiment_round46_pre16_smoke.json create mode 100644 docs/experiment_round46_pre256.json create mode 100644 docs/experiment_round47_ep_s1.json create mode 100644 docs/experiment_round47_pre_s1.json diff --git a/docs/experiment_round46_ep16_smoke.json b/docs/experiment_round46_ep16_smoke.json new file mode 100644 index 0000000..caf7fd6 --- /dev/null +++ b/docs/experiment_round46_ep16_smoke.json @@ -0,0 +1,96 @@ +{ + "config": { + "ckpt": "ml/runs/base_dag2_dag1/best.pt", + "samples": "ml/data/samples", + "tasks": null, + "split": "val", + "n": 16, + "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_round46_ep16_smoke.json", + "trace": null, + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 1, + "steps": 1, + "z_scale": 1.0, + "len_w": 0.0, + "tie": 0, + "backoff": 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": "7b7c219c70d15b280682fe0626192b267f0e4dfd843b1e70fb279e148eb6eaa3", + "kin_fp": "2ebcbf17", + "act_scale": 1.0, + "metrics": { + "n": 16, + "n_ref": 16, + "success": 0.375, + "collision_free_success": 0.375, + "time_ok_1p3": 0.25, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.426271438598633, + "time_mean_s": 2.680000066757202, + "brakes_mean": 24.9375, + "start_fail": 0.0, + "goal_ratio_2s": 0.43800246715545654, + "err_fail_med": 231.5127410888672, + "err_success_med": 0.2731371223926544, + "dls_share": 0.5, + "time_budget": "teacher×2(≥3 s)", + "speed_ratio_p50": 1.0000016689300537, + "speed_ratio_p95": 1.0000053644180298, + "speed_ratio_max": 1.0000101327896118, + "over_vmax_rate": 0.0, + "accel_ratio_p95": 1.0000016689300537, + "accel_ratio_max": 1.0000016689300537, + "reverse_p50": 0.22442243993282318, + "reverse_mean": 0.2425607144832611, + "reverse_moving_p50": 0.30572596192359924, + "moving_share_p50": 0.7567567825317383, + "brake_rate_mean": 0.14249999821186066, + "brake_rate_p50": 0.07428571581840515, + "brake_rate_p90": 0.3571428656578064, + "stuck_share": 0.3125, + "n_plans_mean": 175.0, + "success_reachable": 0.375, + "by_cat": { + "A": [ + 8, + 4 + ], + "B": [ + 8, + 2 + ] + } + }, + "simulation_wall_s": 35.48499080000329 +} diff --git a/docs/experiment_round46_pre16_smoke.json b/docs/experiment_round46_pre16_smoke.json new file mode 100644 index 0000000..bf176b2 --- /dev/null +++ b/docs/experiment_round46_pre16_smoke.json @@ -0,0 +1,96 @@ +{ + "config": { + "ckpt": "ml/runs/base_dag2_dag1/best.pt", + "samples": "ml/data/samples", + "tasks": null, + "split": "val", + "n": 16, + "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_round46_pre16_smoke.json", + "trace": null, + "select": "pre", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 1, + "steps": 1, + "z_scale": 1.0, + "len_w": 0.0, + "tie": 0, + "backoff": 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": "7b7c219c70d15b280682fe0626192b267f0e4dfd843b1e70fb279e148eb6eaa3", + "kin_fp": "2ebcbf17", + "act_scale": 1.0, + "metrics": { + "n": 16, + "n_ref": 16, + "success": 0.375, + "collision_free_success": 0.375, + "time_ok_1p3": 0.25, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.6735622882843018, + "time_mean_s": 2.763333320617676, + "brakes_mean": 21.9375, + "start_fail": 0.0, + "goal_ratio_2s": 0.4253818392753601, + "err_fail_med": 223.724365234375, + "err_success_med": 0.7986489534378052, + "dls_share": 0.5625, + "time_budget": "teacher×2(≥3 s)", + "speed_ratio_p50": 1.0000016689300537, + "speed_ratio_p95": 1.0000053644180298, + "speed_ratio_max": 1.0000101327896118, + "over_vmax_rate": 0.0, + "accel_ratio_p95": 1.0000016689300537, + "accel_ratio_max": 1.0000016689300537, + "reverse_p50": 0.23239436745643616, + "reverse_mean": 0.22883012890815735, + "reverse_moving_p50": 0.29054054617881775, + "moving_share_p50": 0.9583333134651184, + "brake_rate_mean": 0.12535713613033295, + "brake_rate_p50": 0.0057142856530845165, + "brake_rate_p90": 0.3257142901420593, + "stuck_share": 0.3125, + "n_plans_mean": 175.0, + "success_reachable": 0.375, + "by_cat": { + "A": [ + 8, + 4 + ], + "B": [ + 8, + 2 + ] + } + }, + "simulation_wall_s": 31.1417752999987 +} diff --git a/docs/experiment_round46_pre256.json b/docs/experiment_round46_pre256.json new file mode 100644 index 0000000..1316d4d --- /dev/null +++ b/docs/experiment_round46_pre256.json @@ -0,0 +1,108 @@ +{ + "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/round46_pre256_rows.json", + "out": "docs/experiment_round46_pre256.json", + "trace": null, + "select": "pre", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 1, + "steps": 1, + "z_scale": 1.0, + "len_w": 0.0, + "tie": 0, + "backoff": 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.54296875, + "collision_free_success": 0.54296875, + "time_ok_1p3": 0.33203125, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.4448752403259277, + "time_mean_s": 2.342589855194092, + "brakes_mean": 19.83984375, + "start_fail": 0.0, + "goal_ratio_2s": 0.17014972865581512, + "err_fail_med": 185.13986206054688, + "err_success_med": 0.4150978922843933, + "dls_share": 0.63671875, + "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, + "reverse_p50": 0.22891566157341003, + "reverse_mean": 0.23401273787021637, + "reverse_moving_p50": 0.28184282779693604, + "moving_share_p50": 0.9583333134651184, + "brake_rate_mean": 0.10174279659986496, + "brake_rate_p50": 0.0051282052882015705, + "brake_rate_p90": 0.35384616255760193, + "stuck_share": 0.24609375, + "n_plans_mean": 195.0, + "success_reachable": 0.5720164775848389, + "by_cat": { + "A": [ + 128, + 95 + ], + "B": [ + 74, + 22 + ], + "C": [ + 32, + 14 + ], + "D": [ + 13, + 0 + ], + "Dr": [ + 9, + 8 + ] + } + }, + "simulation_wall_s": 319.2378991000005 +} diff --git a/docs/experiment_round47_ep_s1.json b/docs/experiment_round47_ep_s1.json new file mode 100644 index 0000000..e2c7db9 --- /dev/null +++ b/docs/experiment_round47_ep_s1.json @@ -0,0 +1,108 @@ +{ + "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/round47_ep_s1_rows.json", + "out": "docs/experiment_round47_ep_s1.json", + "trace": null, + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 1, + "steps": 1, + "z_scale": 1.0, + "len_w": 0.0, + "tie": 0, + "backoff": 0, + "max_sec": null, + "seed": 1, + "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.53125, + "collision_free_success": 0.53125, + "time_ok_1p3": 0.296875, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.636941432952881, + "time_mean_s": 2.4600000381469727, + "brakes_mean": 19.265625, + "start_fail": 0.0, + "goal_ratio_2s": 0.2750342786312103, + "err_fail_med": 214.6193084716797, + "err_success_med": 0.22276456654071808, + "dls_share": 0.62890625, + "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, + "reverse_p50": 0.25339367985725403, + "reverse_mean": 0.2575649917125702, + "reverse_moving_p50": 0.3117443919181824, + "moving_share_p50": 0.9636363387107849, + "brake_rate_mean": 0.09879808127880096, + "brake_rate_p50": 0.0051282052882015705, + "brake_rate_p90": 0.3384615480899811, + "stuck_share": 0.25390625, + "n_plans_mean": 195.0, + "success_reachable": 0.5596708059310913, + "by_cat": { + "A": [ + 128, + 96 + ], + "B": [ + 74, + 21 + ], + "C": [ + 32, + 12 + ], + "D": [ + 13, + 0 + ], + "Dr": [ + 9, + 7 + ] + } + }, + "simulation_wall_s": 315.10028919999604 +} diff --git a/docs/experiment_round47_pre_s1.json b/docs/experiment_round47_pre_s1.json new file mode 100644 index 0000000..4ba8d85 --- /dev/null +++ b/docs/experiment_round47_pre_s1.json @@ -0,0 +1,108 @@ +{ + "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/round47_pre_s1_rows.json", + "out": "docs/experiment_round47_pre_s1.json", + "trace": null, + "select": "pre", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 1, + "steps": 1, + "z_scale": 1.0, + "len_w": 0.0, + "tie": 0, + "backoff": 0, + "max_sec": null, + "seed": 1, + "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.54296875, + "collision_free_success": 0.54296875, + "time_ok_1p3": 0.33984375, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.4660637378692627, + "time_mean_s": 2.4476258754730225, + "brakes_mean": 19.703125, + "start_fail": 0.0, + "goal_ratio_2s": 0.20109058916568756, + "err_fail_med": 187.68463134765625, + "err_success_med": 0.3424937129020691, + "dls_share": 0.6484375, + "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, + "reverse_p50": 0.22884614765644073, + "reverse_mean": 0.23487445712089539, + "reverse_moving_p50": 0.2777777910232544, + "moving_share_p50": 0.9679999947547913, + "brake_rate_mean": 0.10104167461395264, + "brake_rate_p50": 0.0051282052882015705, + "brake_rate_p90": 0.35641026496887207, + "stuck_share": 0.2421875, + "n_plans_mean": 195.0, + "success_reachable": 0.5720164775848389, + "by_cat": { + "A": [ + 128, + 95 + ], + "B": [ + 74, + 23 + ], + "C": [ + 32, + 14 + ], + "D": [ + 13, + 0 + ], + "Dr": [ + 9, + 7 + ] + } + }, + "simulation_wall_s": 314.3736673000094 +} diff --git a/docs/探索日志.md b/docs/探索日志.md index a88481d..dadd3ac 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -20,6 +20,7 @@ - **当前最好(n=256 val,同 seed 同批)**:`ml/runs/base_dag2_dag1/best.pt` + `K=8, select=endpoint, replan=empty, commit_n=2, fb=2.2, fb_block_gain=0.5, dls_safety=1, safety=1` → **到达 57.4%、无碰撞到达 57.4%、时限内(≤1.3×老师) 39.1%、刹停 19.25/题、行程比 2.445、2 s 误差比 0.210**(轮次 33 base)。 - **但这组数含超速**:`speed_ratio_p50=1.660`、`over_vmax_rate=1.000` ⇒ 每轴实际最大速度是 VMAX 的 1.66 倍(反馈叠在 ±1 限幅之后,轮次 30 定位)。按合法限速 `--inc_cap 1` 重测是 **到达 52.3% / 时限内 26.6%**(轮次 33 cap)。**验收要引用后者**,两者的差就是"以前的分数有一部分是超速换来的"。 - n=512 复核在轮次 28–30 给过 50.8–52.1%;当前配方(含 `inc_cap`)尚未在 n=512 重跑。 + - **合法限速口径下的新最好时限读数(轮次 46–47)**:同配方把打分地平线换成被执行前缀 `select=pre` → **到达 54.3%、时限内 33.2%(控制 26.6%)、2 s 误差比 0.170(控制 0.279)、行程比 2.445、碰撞 0、超 VMAX 0**;seed=1 复现 54.3%/34.0%。到达相对控制只 +5/+3 题(McNemar p=0.42/0.68 ⇒ 噪声),**显著的是时限内与进度**,代价是到达题末态误差 0.22→0.42 mm(判据壳 1 mm 内,余量 4.6×→2.4×)。所以它刷新的是"时限内"这一栏,不是"到达"这一栏。 - **实时性(GPU,B=1、K=8、一步)**:整周期 P50 33.5 ms、P99 64.7 ms;批量认证把最坏情况从 80.2 → 37.1 ms(轮次 34)。仍未进 §9.2 的 P99 < 20 ms,瓶颈是几何查询不是推理(推理 7.2 ms)。 - **安全层口径**:线上只做**端点 + 中点**点检(`gaps(...).min(-1) > 0`)。轮次 30 那条"认证通过仍碰撞"(−0.149 mm)已被连续段认证抓住(`certify_uniform` n≥8、二分 5 次查询),但它进不了实时预算(轮次 31–32)。 - **失败结构(轮次 38,147 到达 / 109 未到)**:**63 题是走一半被冻住**(离目标 >100 mm 且刹车 >30 次)、23 题方向错、位置差 20~100 mm 15 题、差 5~20 mm 5 题、姿态没到位 3 题、起步失败 0;未到组活动窗内 37% 的步是零运动。 @@ -34,8 +35,10 @@ | 0 | ~~把"沿路最小间隙"接进候选打分~~(极端版 = `--tie 1`:只在"前缀最长"的并列集合内按余量定夺,零额外几何) | 轮次 45 两臂:到达 −1.95 pp(净 −5 题,McNemar p=0.125 与零不可分)、时限 +1.17 pp、刹停 −9%、行程 −0.076、**碰撞仍 0**;且分辨率被 `gcap=0.5 mm` 截顶锁死(并列 79% 但只有 5.6% 可分辨,与轮次 44 离线 5.6% 对上) | **判掉**:买余量卖进度,且作用在 `maxlen>0` 的人群——失败主人群是全拒(`maxlen=0`)。前置条件见 #28 | | 0b | **#28 抬宽相位截顶 `gcap` 并与 `gap_margin` 解耦** | 现在 `--gap_margin 1.5` 会同时抬截顶值和验收门限,动一个必动另一个;`_coarse` 保留 `d ≤ cp_lim + cap` 的胶囊对 ⇒ 成本随壳层体积涨,而 GPU 周期 P50 已 33.5 ms | 未实现(要新增只改数值精度的参数,不许碰判据) | | 1 | **候选数翻倍 K=8→16** | 轮次 39 实测:到达 **+3.9 pp**、时限内 **+3.1 pp**、**刹车 −30%**(19.55→13.77)、碰撞仍 0、行程比不变 ⇒ 很多"全拒"是**没备选**而不是真没路 | **正向**;认证/打分成本要用独占微基准量完才能谈部署 | -| 2 | **全拒时缩步顶上**(`--backoff`:首选候选按 1/2、1/4、1/8 并进同一次认证,只在原候选连第 1 步都不安全时启用) | 失败分类 63/109 是"被冻住"、未到组 37% 步为零运动;但 4 题 CPU 冒烟提示转化率只 ~13% | 代码与单测已落,256 题四臂待补跑(判读规则已预注册) | -| 3 | **白费比例惩罚**(`select=ep_waste --len_w`,罚 `1−同轴净/总` × 当前误差) | 换向是**全局**现象(~1/4 的(步,轴)对在反向,到达/未到达两组差不多);净位移只占总行程 0.445 | 已实现+正对照单测(可手算 2/18);三臂排队 | +| 2 | ~~全拒时缩步顶上~~(`--backoff`) | 四臂实测:转化率 7.7%→37%(能救回停步),但到达 0.5234→0.5156 **不赚**;且它单独把 `brake_rate` 改善 16% 而行为没变好 ⇒ **可以绕过 §9.2 的刹停判据**,这是仪表漏洞不是收益 | **已判掉**(轮次 43);同时把 §9.2 的刹停口径标记为"不可由缩步单独满足" | +| 3 | ~~白费比例惩罚~~(`select=ep_waste --len_w`) | 三臂实测行程比不降反升,给出原因:**罚的是没被执行的那段地平线**(16 步打分地平线 vs 实际提交 2 步) | **已判掉**(轮次 42);其诊断直接引出 #29(把打分地平线换成真会执行到的位置) | +| 3c | **#29 打分地平线 = 被执行的前缀**(`select=pre`:用 `fut[:,:,commit_n]` 的终点误差代替 `fut[:,:,-1]`) | 两 seed 独立复现:2 s 误差比符号检验 192:64(p=5e−16)/ 191:65(p=1.5e−15),时限内 +6.64/+4.30 pp,行程比 −0.171/−0.171,**碰撞仍 0**;到达只 +5/+3 题(McNemar p=0.42/0.68 ⇒ 噪声);代价是到达题末态误差 0.22→0.42 mm(判据壳内但余量 4.6×→2.4×) | **正向(时限/进度显著、到达未显著)**,作可选口径入库,不改默认。下一试 #30 | +| 3d | **#30 前瞻长度曲线**(把打分步长做成 `pre_h`,扫 h=2/4/8/16,16=现 endpoint) | 若时限收益随 h 增大而塌、精度随 h 增大而恢复 ⇒ 形态应是"前瞻长度随剩余距离收缩",而不是二选一 | 待实现(`pre` 已落,只差把 `commit_n` 换成参数) | | 3b | ~~行程惩罚 `ep_len`(罚候选累计行程)~~ | 三臂实测行程比 2.6216/2.6548/2.6719 vs 对照 2.6158 ⇒ **不降反升**,只换来 +2 pp 时限 | **负结论**,被 3 取代 | | 4 | **一步角点的标定**(E1 那条线:`mf_target='mid'` 把自举导数移到中点) | 解析高斯场判据:`sq` 只线性收敛(结构性少修一半)、`mid/lin` 二次收敛;实测部署角偏差 MF 3.851 → `mid` 2.364(0.61×);但**线上一步输出比自身两段采样离老师远 10 倍**(4.326 vs 0.434) | 开关已落、默认关;要付训练,且受"一步与认证/DAgger 共同标定"约束 | | 5 | **认证进 20 ms**(#16/#18) | GPU 周期 P50 33.5 ms,几何宽相位占大头;f32 认证需先抬 eps 到 2e−4 mm | 未解,是验收硬门槛 | @@ -109,6 +112,31 @@ - **对 `backoff` 的预期与判据**:全拒意味着"连第 1 步的端点或中点已经穿进障碍",缩步能不能救取决于**穿得有多深**(穿 0.1 mm 救得回,穿 3 mm 救不回)。这个数转储里没记(只记被选中那条),所以直接由 `backoff_mean`/`reject_all_mean` 来量: `reject_all_mean ≈ 原全拒次数`(=backoff_mean+brakes_mean,与轮次 33 cap 的 19.55 可比),`backoff_mean/(backoff_mean+brakes_mean)` 就是"缩步能救回的比例"。这是补跑(#22)要读的第一个数。 +## 轮次 46–47:#29 把候选打分的地平线换成「这一轮真会执行到的位置」——链上第一个正向杠杆,两个 seed 独立复现 + +- **诊断(不是新想法,是旧负结论给出的原因)**:`select='endpoint'` 按**整条 16 步地平线末端**的终点误差给 K 条候选排序,而每轮只提交 `commit_n=2` 步。对被安全层截断的候选,那个终点**根本到不了**;对满前缀候选,它是 14 步开环外推。轮次 42 判掉 `ep_len/ep_waste` 时写的就是同一件事——"罚的是没被执行的那段地平线"。`select='pre'` 把打分点换成 `fut[:, :, commit_n-1]`(这一轮真正会走到的位置),**同一次 `goal_err` 只换点集,机制上不增加量级**。 +- **实现账**:分支 + 文档串 + `--select pre`;两条可手算的正对照(夹具造一对"前缀好但末端差"/"末端好但前缀差"的候选 ⇒ 两口径必须选**相反**的那条)+ 一条不变式(`commit_n=16` 时 pre 的索引落到地平线末端 ⇒ 必须与 endpoint 选同一条)。49 项测试绿。仪表灵敏度:n=16 冒烟上 `pre` 与 `endpoint` 已给出不同轨迹(刹停 24.94→21.94、行程比 2.43→2.67),不是惰性口径。 +- **对照臂的可比性先证再引用**:本轮控制臂 = `docs/experiment_round45_tie0.json`(`tie=0`、同配方),它与轮次 42 的 `round42_K8cb1` **22/22 键逐位相同** ⇒ 该配方跨窗口确定性复现,单臂对照成立(这是第一次在新杠杆上省掉控制臂,前提是复现证据已落盘)。 +- **seed=0(n=256,同 seed 同批)** 与 **seed=1 独立复现(两臂同窗口背靠背)**: + + | 读数 | s0 控制 | s0 `pre` | Δ | s1 控制 | s1 `pre` | Δ | + |---|---|---|---|---|---|---| + | 执行碰撞率 | 0.0 | **0.0** | — | 0.0 | **0.0** | — | + | 到达 | 0.5234 | 0.5430 | +1.95 pp | 0.5313 | 0.5430 | +1.17 pp | + | 时限内(≤1.3×老师) | 0.2656 | **0.3320** | **+6.64 pp** | 0.2969 | **0.3398** | **+4.30 pp** | + | 2 s 误差比(中位) | 0.2792 | **0.1702** | −39% | 0.2750 | **0.2011** | −27% | + | 行程比中位 | 2.6158 | 2.4449 | −0.171 | 2.6369 | 2.4661 | **−0.171** | + | 超 VMAX 题占比 | 0.0 | 0.0 | — | 0.0 | 0.0 | — | + | 到达题末态误差中位 | 0.2175 | 0.4151 | **+0.198 mm** | 0.2228 | 0.3425 | **+0.120 mm** | + +- **统计口径(这条线真正的证据在哪)**: + - `goal_ratio_2s` 逐题符号检验:s0 **192 好 / 64 差(p=4.9e−16)**,s1 **191 好 / 65 差(p=1.5e−15)**。⇒ 显著、且两个 seed 的计数几乎一样。 + - 到达:s0 逐题丢 10 赢 15(净 +5,McNemar **p=0.424**)、s1 丢 10 赢 13(净 +3,**p=0.678**)。⇒ **到达率的提升在两个 seed 上都只是噪声**。诚实的写法是"时限内到达显著变多、总到达未见显著变化",即它把路上的时间买回来了,没有把通路买回来。 + - 末态误差逐题:s0 130 好 / 126 差(p=0.85)、s1 121/135(p=0.42)⇒ 全程不显著;但**到达题组内**的中位误差两 seed 都涨(0.22→0.42、0.22→0.34 mm)。这是"打分只看 2 步"的必然代价:近目标处不再为最后那几毫米承诺路径。判据仍未违反(到达定义是 `e<1 mm 且 oe<0.5°`,最差的 0.42 mm 也在壳内),但**这条余量从 4.6 倍缩到 2.4 倍**。 +- **按类别(两个 seed 方向一致)**:A 类 −1,**B 类 +2、C 类 +3/+2**。单个切片在这个分母上都不显著,但方向与轮次 44 的"#27:候选容量救不了 B/C"**不矛盾**——那条测的是"多给同分布候选",这条改的是"用哪个终点评价候选"。真正的判据留给下一轮的地平线曲线。 +- **成本**:两臂墙钟 317.6/319.2 s(s0)与 315.1/314.4 s(s1)——同量级;但**墙钟差不构成成本结论**,要说"零额外开销"得用独占微基准把 `goal_err` 在 `B×K` 个点上的两次调用分开量(记为待办)。 +- **状态**:**不改为默认口径**(到达未显著 + 到达精度余量减半),把 `select='pre'` 作为已验证的可选口径入库。下一步 #30:把打分步长做成参数 `pre_h`,扫 h∈{2,4,8,16}(16=现在的 endpoint),量的是"前瞻长度 → 时限内到达 / 末态精度"这条**机理曲线**而不是挑系数;若精度代价随 h 恢复而时限收益不塌,正确的形态是"前瞻长度随剩余距离收缩"。 + ## 轮次 45:`tie`(等长安全前缀按沿路最小间隙定夺)判掉——它买的是余量,卖的是进度,而且作用在错的人群上 - **做法**:`cert_batch=1` 已经在同一次几何调用里算了 K 条候选各 `2·commit_n` 个点的间隙,但这些信息只用来判通过/拒绝。`--tie 1` 让"前缀最长"的并列集合改由**沿路最小间隙**定夺;零额外几何调用,判据一个字没动(仍是 `gap > gap_margin`)。 diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index 74fb413..707405a 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -167,6 +167,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=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 条)。 + select='pre':把打分地平线换成「这一轮真会执行到的位置」(fut 的第 commit_n 步);'endpoint' 用的是整条地平线末端。 oracle:TeacherOracle,代替 model 出动作(model 可为 None,须配 select='first'、use_sub=False)。 replan:'r3'(部署:队列 ≤ 3 步就规划,稳态 r=3)/ 'empty'(队列空了才规划,每次 r=0,前缀消融用)。 sub_rule:子目标推进规则,见 sub_advance。除默认值外均为诊断用。 @@ -291,6 +292,11 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6] if select == 'first': score = torch.zeros(B, K, device=device); score[:, 0] = 1 + elif select == 'pre': # #29:按「这一轮真会执行到的位置」打分,而不是 16 步开环外推的末端。 + # 诊断来自 ep_len/ep_waste 的失败:打分地平线远长于提交步数,被安全层截断的候选那个终点根本到不了。 + qa_pre = fut[:, :, min(commit_n, fut.shape[2]) - 1].reshape(B * K, 6) + e_pre, _ = goal_err(kin, qa_pre, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) + score = -e_pre.view(B, K) elif select in ('endpoint', 'adapt', 'ep_len', 'ep_waste'): qa = fut[:, :, -1].reshape(B * K, 6) e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) diff --git a/tests/test_eval_cl.py b/tests/test_eval_cl.py index 6e795dc..110615d 100644 --- a/tests/test_eval_cl.py +++ b/tests/test_eval_cl.py @@ -966,5 +966,55 @@ class TieByClearanceTest(unittest.TestCase): self.assertTrue(all(isinstance(s, int) and isinstance(t, int) for s, t in seen)) +class PreHorizonTest(unittest.TestCase): + """#29:`select='pre'` 按「这一轮真会执行到的位置」打分;`endpoint` 按整条地平线末端打分。 + 夹具是两条刻意做成「前缀好但末端差」/「末端好但前缀差」的候选,期望值可手算。""" + 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)) + + class NearKin: + def clamp_box(self, q): + return q + + def gaps(self, q, cap): + return {'gm': torch.ones(q.shape[0], 49)} + + def run1(self, select, commit_n=2): + # 候选0:前 2 步各 +0.2(前缀 0.4),随后 14 步 −0.4/14 ⇒ 地平线末端回到 0 + # 候选1:16 步各 +0.05 ⇒ 前缀只有 0.1,但地平线末端 0.8 + c0 = [0.2, 0.2] + [-(0.4 / 14.0)] * 14 + c1 = [0.05] * 16 + acts = torch.stack([torch.tensor([[v, 0, 0, 0, 0, 0] for v in c0], dtype=torch.float32), + torch.tensor([[v, 0, 0, 0, 0, 0] for v in c1], dtype=torch.float32)]) + self.assertEqual(tuple(acts.shape), (2, 16, 6)) # 夹具自证:第 0 轴才是唯一有内容的轴 + err = lambda kin, q, T, R: (((0.9 - q[:, 0]).clamp(min=0.0)) * 1000.0, torch.zeros(q.shape[0])) + with patch('ml.sim.closed_loop.features', return_value=torch.zeros(1, 83)), \ + patch('ml.sim.closed_loop.goal_err', side_effect=err): + _, per = rollout(self.StubModel(acts), self.NearKin(), [{'q0': [0] * 6, 'T': [1, 0, 0]}], 'cpu', + K=2, safety=False, max_sec=0.04, return_traj=True, use_sub=False, + select=select, replan='empty', commit_n=commit_n) + return per + + def test_pre_and_endpoint_pick_opposite(self): + p_end = self.run1('endpoint') + self.assertAlmostEqual(float(p_end['traj'][0, 1, 0]), 0.05 * 1.2, places=5) # 末端 0.8 ⇒ 选候选1 + p_pre = self.run1('pre') + self.assertAlmostEqual(float(p_pre['traj'][0, 1, 0]), 0.2 * 1.2, places=5) # 前缀 0.4 ⇒ 选候选0 + self.assertNotAlmostEqual(float(p_end['traj'][0, 1, 0]), float(p_pre['traj'][0, 1, 0]), places=3) + + def test_pre_equals_endpoint_when_commit_is_the_whole_horizon(self): + """不变式:commit_n=16 = 整条地平线 ⇒ pre 的索引落到末端,必须与 endpoint 选同一条。""" + p = self.run1('pre', commit_n=16) + self.assertAlmostEqual(float(p['traj'][0, 1, 0]), 0.05 * 1.2, places=5) + + if __name__ == '__main__': unittest.main()