From 54fb50a9f1a3fbc408cb88cff124e41182e57ddd Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 21 Oct 2026 01:55:57 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=8E=92=E5=BA=8F=E4=BF=AE=E6=AD=A3+?= =?UTF-8?q?=E8=BD=AE=E6=AC=A153=E2=80=94=E2=80=94=E6=8A=8A=E5=8E=8B?= =?UTF-8?q?=E5=9C=A8=E6=9C=80=E6=96=B0=E8=BD=AE=E6=AC=A1=E4=B9=8B=E4=B8=8A?= =?UTF-8?q?=E7=9A=84"=E8=BD=AE=E6=AC=A139"=E5=9D=97=E7=A7=BB=E5=9B=9E?= =?UTF-8?q?=E5=AE=83=E7=9A=84=E4=BD=8D=E7=BD=AE=EF=BC=9B=C2=A79.2=20?= =?UTF-8?q?=E8=AE=B0=E5=88=86=E6=9D=BF=E6=8D=A2=E5=88=B0=20K16+pre=20?= =?UTF-8?q?=E7=8E=B0=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排序:倒序约定此前只在各轮之间成立,旧的"轮次 39"及其 4 个续篇块卡在候选表下面、 轮次 52 上面 ⇒ 打开日志第一眼是 39,看起来像"没再更新"。整块 67 行移到 轮次38 之前, 纯移动不改内容(字节数 188223 不变,LF 保持)。 轮次53(seed=1 逐题配对):pre 在 §9.2 自己的 1.3x 口径上复现——s0 22新增/5失去 p=0.0015, s1 17/6 净+11 p=0.035,合并 39/11 p=9.0e-5 ⇒ "打分地平线=执行地平线"确实买到时限这一栏。 门控20mm 在 s1 只有 12/11 净+1 (p=1.0),合并 +17 p=0.024 ⇒ 修正轮次50 说过头的"塌了", 准确说法是"门控吃掉约四成时限收益,损失集中在 seed=1"。 记分板更新(最好合法口径 = inc_cap+K16+pre,轮次51):碰撞 0 / 超VMAX 0 达标; 可达到达 0.5967(差 38.3pp,相对控制净+11 题 p=0.061 未确立);时限内 0.3594(唯一被配对证明); 行程比 2.414(超 1.3 倍);brake_rate mean 7.28% 但 p50=0.0%、p90 27.4%(长尾, stuck_share 17.2%)。 --- docs/experiment_round53_ctl_ex_s1.json | 110 ++++++++++++++++++ docs/探索日志.md | 147 ++++++++++++++----------- 2 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 docs/experiment_round53_ctl_ex_s1.json diff --git a/docs/experiment_round53_ctl_ex_s1.json b/docs/experiment_round53_ctl_ex_s1.json new file mode 100644 index 0000000..b8be480 --- /dev/null +++ b/docs/experiment_round53_ctl_ex_s1.json @@ -0,0 +1,110 @@ +{ + "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/round53_ctl_ex_s1_rows.json", + "out": "docs/experiment_round53_ctl_ex_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, + "pre_h": 0, + "pre_near": 0.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": 290.3290144000057 +} diff --git a/docs/探索日志.md b/docs/探索日志.md index 7a48b42..b46a889 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -47,72 +47,18 @@ | 6 | 速度/余量口径(#11/#14) | `--inc_cap 1` 代价 −5.1 pp 到达、−12.5 pp 时限;`gap_margin 0.5 mm` 只付 3 题 | 已量化,投影式余量未移植 | | 7 | 循环内共享 FK(#19)、FP16/BF16、流水线复用 | 实测分别只有 0.03 ms/次、负收益、−3.1 ms 且扰动特征 | **已判掉** | -## 轮次 39:两条没人碰过的算法杠杆——候选档位与行程惩罚(机制已单测,A/B 排队) +## 轮次 53:seed=1 的逐题配对——`pre` 在 §9.2 自己的 1.3× 口径上复现(p=0.035),门控 20 mm 那臂只有 +1 题 -- **为什么是这两条**:轮次 38 的失败分类说清楚了两件事——① 到达率的缺口在"K 条候选全被拒 ⇒ 原地冻住"(63/109),② §9.2 要"关节行程 ≤ 规划器×1.10",实测行程比中位 **2.4~2.6**,而现在**没有任何机制在管行程**(选择器只看末态误差,安全层只看间隙)。另外 `--steps` 那组数顺带给了一条线索:多段推理把行程比从 2.62 拉到 1.87,说明行程主要由候选选择/动作形状决定,不是物理必然。 -- **杠杆 1:候选档位与多样性(#15)**。`ml/eval/eval_cl.py` 补 `--z_scale`(rollout 早就有这个参数,只是评估没接)。四臂对照轮次 33 cap:`K=16`、`K=8 --safety_try 3`、`K=8 --safety_try 1`、`K=8 --z_scale 1.5`。要回答的是:**全拒是"真没路"还是"没备选"**——若 K=16 或 z_scale 拉大能把到达/刹车买回来,那是候选池的问题;若 `safety_try 1` 几乎不掉到达,那说明第 1 条候选就够,后面试 7 条只是在花几何查询的钱(这对 20 ms 预算直接相关)。 -- **杠杆 2:行程惩罚(`select='ep_len'` + `--len_w`)**。分数从 `-末态误差` 改成 `-末态误差 − len_w·Σ_t Σ_j |Δ_j|/STEP_j`(每轴按自己的满速步长归一,所以 6 轴可比)。 - - 机制单测 2 条(`tests/test_eval_cl.py::TravelPenaltySelectTest`,40 项全过):造两条**末态相同、累计位移相同**的候选,一条来回抖(归一行程 10.8)一条匀速(3.6),`goal_err` 打成常数 ⇒ 只有行程能把它们分开。实测 `len_w=0` 选到抖动那条(第 1 步 1.08°)、`len_w=1.0` 选到匀速那条(第 1 步 0.27°);另有一条**同分零权重必须与 `select='endpoint'` 逐位相同**的回归断言(rtol=atol=0),保证默认行为没被这次改动碰到。 - - 待测:`len_w ∈ {0.1, 0.3, 1.0}` 三臂,看行程比能压到多少、到达与时限各付多少。这条杠杆与"到达率"是相互冲突的目标(惩罚行程=主动走慢),所以必须成对汇报,不能只报行程变好。 - - 单位口径要说清:`len_w` 的量纲是"mm 误差 / 一个归一 STEP 的累计行程",16 步满速在 3 条轴上大约是 48,所以 0.1~1.0 对应的惩罚是 5~50 mm 当量——这是扫这三个值的原因,不是随手取的。 -- 测试 `tests/test_eval_cl.py` 38→40 项、全套 43→45 项(`python -m unittest discover -s tests`,2.2 s 全绿)。GPU 队列顺序:backoff A/B → `--safety 0` 判别 → 候选档位 → 行程惩罚。 -- **等价闸门先过了**:`--backoff 0 --cert_batch 1 --inc_cap 1` 的 256 题与轮次 33 cap(`--cert_batch 0`)**逐项完全相同**(`docs/experiment_round38_backoff0.json` 对 `docs/experiment_round33_cap.json`,把两个 metrics 字典做键并集比较,差异集合为空,含 success 0.523438、time_ok 0.265625、brakes 19.55…)⇒ 轮次 36 我把批量认证分支重写(加缩步档那一段)没有改动任何行为。 -- **两个我自己的操作失误,如实记在这里**: - 1. 新加的 `backoff` 指标一度在产物里根本没有——`ml/eval/eval_cl.py:run()` 的 `out` 是**自己显式列键**的,不是从 `rollout` 返回的字典通抄,所以我往 `res` 加的 `backoff_mean` 被无声丢掉。已改成从逐题 `per['backoff']` 汇总(`backoff_mean` + `reject_all_mean = backoff+brakes`,后者才是与"原来全拒次数"可比的量)。**教训:加指标要顺着输出链路走到底,别只改生产端。** - 2. 停后台 GPU 作业时,`TaskStop` 之后 `bash` 脚本链会活下来并继续拉起下一个 python;而 `nvidia-smi --query-compute-apps` 在这台机器上列不出这些 python(只列 ShellHost/wallpaper32 之类),据此判断"卡已空"会误判。我第一次按 PID 杀子进程后,链条 30 秒后又起了一个新的 eval。第二次我按进程树杀,但**把父子关系读反了**(Windows 的 PPID 在父进程已死时是陈旧值),结果误杀了新链的第一臂。正确做法:重启前用命令行正则列出全量候选、数 python 个数确认单租户,重启后再数一次;杀之前先按 `StartTime` 判新旧。backoff 那一臂因此要补跑。 +- **只跑一条控制臂**(同配方同 seed=1,带 `t_exp`)就能给轮次 47/50 已有的两臂补上配对 ⇒ 先自证确定性:这条控制臂与轮次 47 的控制 **22/22 老键逐位相同**。 +- **1.3× 口径逐题配对(vs 各自 seed 的控制)**: -## 行程惩罚 ep_len 三臂读数:时限变好,但行程比没降(轮次 39 续,负结论) - -- 同一 256 题、同 seed、都带 `--inc_cap 1`,只改 `select=ep_len --len_w`: - - | 臂 | 到达 | 时限内(≤1.3×老师) | 行程比(中位) | 刹车/题 | 成功题末态误差 | - |---|---|---|---|---|---| - | 对照(endpoint) | 0.5234 | 0.2656 | 2.6158 | 19.55 | 0.218 mm | - | len_w=0.1 | 0.5117 | 0.2695 | 2.6216 | 19.49 | 0.244 mm | - | len_w=0.3 | 0.5195 | 0.2852 | 2.6548 | 20.00 | 0.219 mm | - | len_w=1.0 | 0.5273 | **0.2891** | 2.6719 | 19.35 | — | - - (`docs/experiment_round39_lenw{0.1,0.3,1.0}.json`) -- **两条相反的结论**:① 时限内**单调变好**(0.2656→0.2891,+2.4 pp = 6 题),到达在噪声内(0.5117/0.5195/0.5273,最大偏离对照 −1.2/+1.2 pp,n=256 下这就是噪声量级);② **行程比一点没降**(2.6216/2.6548/2.6719 vs 对照 2.6158,+0.006/+0.039/+0.056,只有最后一个勉强出噪声、且方向是"变差")。 - ⇒ "候选地平线内的累计行程"不是"实际执行行程"的好代理:罚它选出的候选在**近端少过冲**(所以收得快、时限内变好),但整段任务要更多次规划才能走完,累计行程不降反升。这条是我原先的假设被打掉,不是被确认;`ep_len` 只值 +2 pp 时限,且对 §9.2 的行程判据无用。 -- 顺着这个读数,真正该罚的是**同一轴来回**而不是总长:已实现 `select='ep_waste'`,罚 `(1 − 同轴净位移/累计行程) × 当前误差`(乘当前误差是为了近端不压死进度项——上一版的形状问题正在这里)。三臂 `len_w=0.1/0.3/1.0` 排在 `ml/runs/_waste_sweep.sh`。 -- 换向率指标 `reverse_p50` 从这一批之后才会出现在产物里(前面的臂是在改动前加载的模块跑的),口径已与离线分解同分母,预期对照臂应复现 **23.5%** 量级——**对不上就要查口径,不能各自报各自的**。 - -## 换向率进了评估指标(轮次 39 续) - -- 分解既然指到"来回抖",每个臂都要直接报这个数,不能每次回头读转储:`rollout` 里按轴记方向,`reverse_rate` = 同轴方向翻转的步数 / 活动步数(站住的步不重置方向,所以"停了再同向走"不算换向);`eval_cl` 报 `reverse_p50`/`reverse_mean`。 -- **正对照**(`tests/test_eval_cl.py`,42 项全绿):抖动轨迹报 >0.4、匀速轨迹报 ≈0——指标本身要证明有灵敏度,不然等于没测。 -- 待交叉验证:新指标在对照臂上应复现转储离线算的 **23.5%**(p50);对不上就是两个口径有一个错。 -- 生产端改了、输出端没改=没改:上一次 `backoff_mean` 就是这么丢的,这次加指标同时改了 `closed_loop.per` 与 `eval_cl` 的显式键列表。 - -## 行程超出的成因分解(轮次 39 续,纯 CPU 读轮次 33 base 转储) - -- 口径先对齐:只在"有老师参考且到达"的题上算,自测 **2.425** 与 eval 报的 2.445 同量级(差别在我按 `t_done/DT` 取窗端,eval 逐步累加),后面的数都在这个口径上。 -- **超出主要是同一根轴来回抖,不是绕远路**:净位移/总行程中位 **0.445**(完美单调应为 1.0)、各轴最大 excursion/行程 0.507、旧口径换向中位 23.5%(p25 15.7%、p75 30.3%)。 -- **撤回一句我自己的假象(并且这条也要再撤回一次,见下一条)**:上面原来写"到达组抖得更厉害(26.1% vs 未到达 14.7%)",**错在口径**。旧定义 `(dd[t+1]·dd[t] < 0).mean()` 要求相邻两步都非零,遇到"前进→停住→后退"就漏计;而未到达题恰恰大量站住,于是被系统性地少计。 -- **用与定义无关的量重算(净位移/总行程,同一转储、同一窗口)**:**到达组 0.478、未到达组 0.352** ⇒ 未到达组白费比例更高(走完 738° 只前进 255°;到达组是 569°/267°)。这条稳。 -- **但换向率的组间比较我第二次算错,现纠正**:上一次我用"未到达 24.0% vs 到达 14.4%"来说组间差异,那个算法把**到达题到达之后的冻结步**也算进了分母(`act` 只按 `tn<=limit`,没排除已到达),把到达组稀释了。按与 `rollout` 完全同语义的窗口(到达题截到 `t_done`)重算,得到的是**两组差不多**: - - | 转储(口径) | reverse_p50 全体 | 到达组 | 未到达组 | + | 口径 | seed=0 | seed=1 | 合并(各自配对的不一致对相加) | |---|---|---|---| - | `round33_cap`(=轮次33 cap,限幅) | **0.2474** | 0.265 | 0.233 | - | `round33_base`(不限幅) | 0.257 | 0.275 | 0.240 | + | K8 `pre` | 22 新增 / 5 失去,净 **+17**,p=0.0015 | 17 / 6,净 **+11**,**p=0.035** | 39 / 11,净 +28,**p=9.0e−5** | + | 门控 20 mm | 22 / 6,净 +16,p=0.0037 | 12 / 11,**净 +1,p=1.0** | 34 / 17,净 +17,p=0.024 | - (`docs/experiment_round39_reverse_offline.json`)⇒ **换向是全局现象(约 1/4 的(步,轴)对在反向),不是失败题的特征**;失败题的特征是"白费行程比例高 + 大量站住"。这两件事先前被我混成一句,现在分开。 -- **可证伪预测(等 backoff 对照臂回来对表)**:同一配方下 in-eval 的 `reverse_p50` 应当 ≈ **0.247**。差得多就说明线上实现与离线同语义重算不一致,得先查仪表再谈算法。 -- **口径敏感要常驻**:分组/比较类断言之前,先换一种定义把同一份数据再算一遍;两种定义都给同方向才写进日志。分母必须与指标定义一致(`act_ = 未 done 且 在时限内`),否则"到达题窗短"会系统性稀释任何按步数归一的指标。 -- **对得上训练侧的事实**:预训练本来就有 `w_smooth=0.05` 的二阶差分正则,但闭环仍大量换向 ⇒ 训练期的平滑项没管住执行期的抖动。 -- **第一条 A/B 读数(`len_w=0.1`,256 题、`--inc_cap 1`)**:到达 0.5117(对照 0.5234,−1.2 pp)、时限内 0.2695(+0.4 pp)、**行程比 2.6216 几乎没动**(对照 2.6158)、成功题末态误差 0.2438(对照 0.2175,变差)。 - 原因是量级不匹配,不是方向错:`0.1 × 累计归一行程`(16 步满速在 3 条轴上约 48)只当量 ~5 mm,而早段末态误差是几百 mm ⇒ 罚不动。等 `len_w=0.3/1.0` 两臂看行程比是否真的开始下降;若仍压不住,惩罚对象应从"总行程"换成**换向/二阶差分**(直接打这个 23.5%)。 - -## 执行余量与前缀完整性审计(轮次 39 续,纯 CPU 读轮次 33 转储) - -- 256 题 × 195 次同步规划、commit_n=2 ⇒ 169,216 个**真正被执行过的**检查点(每步的端点+中点,间隙在 0.5 mm 截顶)。 -- **执行点的间隙分布**:≥0.5 mm(=宽裕)占 **91.0%**;0.2–0.5 mm 占 5.6%;<0.1 mm 占 0.84%;<0.02 mm 占 0.25%。每题"最险的那个执行点"的中位数只有 **0.029 mm**。 - ⇒ 两件事:① 大多数执行是宽裕的,所以"加 0.5 mm 余量门限"只花 3 题(轮次 33 margin 已实测)是自洽的;② 但每题都会至少一次贴到 **30 µm** 级——这是胶囊模型内部的"贴",真机的运动学与装配误差远大于这个数,**搬到实机时这条是实质风险**,不能当成"我们有余量"。 -- **前缀完整性**:满前缀 79.4%、部分前缀 10.8%、全拒 9.9%。分组看全拒率——**到达题 0.88%,未到达题 22.0%**(未到达组因此 44% 的步是零运动,与时间账量的 37% 同量级:那 37% 是"活动窗内",这里是"×commit_n"的换算)。 -- **对 `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)要读的第一个数。 +- **要修正轮次 50 的一处措辞**:当时写的是门控的时限收益"塌了"。按配对检验的准确说法是**门控把收益吃掉约四成**(合并 +17 对 `pre` 的 +28),而且损失集中在 seed=1 那一臂(+1 题,p=1.0)。⇒ 原判断("门控不是免费午餐,是用时限换末态余量")不变,但强度被我说过了一层。合并检验只是近似:同一批题、不同噪声抽样,两臂并非完全独立。 +- **`pre` 这条线到此定性**:**"打分地平线 = 执行地平线"能在不放松任何判据(碰撞 0、超 VMAX 0、n_plans 不变)的前提下,把 §9.2 的"到达用时 ≤ 老师×1.3"这一栏显著往上抬,两个 seed 都复现**;它对"到达率"(自带 2× 预算那道门)没有可用效应。⇒ 下一轮的取舍不该再是"怎么调这条轴"(#30/#31 已把形状量尽),而是**末态余量**要不要用轮次 52 的门控换、以及把预算转到 B/C 类的数据侧(#27 的结论仍未被任何后续轮次推翻)。 ## 轮次 52:补上逐题老师用时这把尺子之后,整条"打分地平线"线的结论要翻面——1.3× 口径上的时限收益是显著的 @@ -453,18 +399,18 @@ - **对"把一步角点训准"这条训练路线的直接警告**:一步候选的多样性,恰恰来自角点的**偏差/欠缩放**(每条 ε 被削到不同的衰减幅度)。如果训练把一步映射成两段那样的收敛解,候选可能一起塌缩 ⇒ **按当前安全层口径,到达率可能不升反降**(实测就是这个方向:steps=2 开闸 −12.9 pp)。所以任何"修角点"的训练都必须**同时**处理候选多样性(更大 K、或给候选注入受控多样性),否则就是拿开环收益去换闭环到达。 - 顺带修一处历史口径:§9.2 的"策略单独碰撞率 <3%"这一格,现在才有合法读数 **58.6%**(一步)/ **55.9%**(两段)——差距巨大,且以前从没在同一配方下报过。 -## §9.2 记分板(2026-10-20 现状,最好的合法口径 = `--inc_cap 1` + K=16) +## §9.2 记分板(2026-10-21 现状,最好的合法口径 = `--inc_cap 1` + K=16 + `select=pre`,轮次 51) | 判据(§9.2) | 要求 | 当前 | 差距 | |---|---|---|---| | 实际执行碰撞 | = 0 | **0**(`collision_rate 0`、`collision_free_success` = `success`) | **达标** | | 超 VMAX 的题占比 | = 0(本轮新增的硬口径) | **0**(`over_vmax_rate 0.000`,开 `--inc_cap 1` 后;不开则 1.000、峰值 1.660×VMAX) | **达标**(代价:到达 −5.1 pp) | -| held-out 可达任务到达率 | ≥ 98% | **0.5625**(K=16)/ 0.5234(K=8) | 差 **41.8 pp** | -| 到达用时 ≤ 老师×1.3 | 全部到达题 | 达标比例 **0.2969**(K=16)/ 0.2656(K=8) | 到达题里也只有约三成按时 | -| 关节行程 ≤ 规划器×1.10 | ≤1.10 | 行程比中位 **2.62** | 超 **1.4 倍**,且罚候选行程是负结论(见上) | +| held-out 可达任务到达率 | ≥ 98% | **0.5967**(可达分母,K16+`pre`)/ 0.5664(含 D 的整批)/ 0.5234(K8 控制) | 差 **38.3 pp**;且相对控制净 +11 题只到 **p=0.061 ⇒ 未确立** | +| 到达用时 ≤ 老师×1.3 | 全部到达题 | 达标比例 **0.3594**(K16+`pre`)/ 0.2969(K16)/ 0.2656(K8 控制) | **这一栏是唯一被逐题配对证明买到的**:`pre` 净 +17(s0 p=0.0015)/+11(s1 p=0.035),合并 p=9.0e−5 | +| 关节行程 ≤ 规划器×1.10 | ≤1.10 | 行程比中位 **2.414**(K16+`pre`;控制 2.616) | 超 **1.3 倍**;罚候选行程/白费比例两条都是负结论,缩短打分地平线顺带降了 0.2 | | 冷启动 | <10 ms | 未单独测(周期 P50 9.0 ms 是稳态推理,含首段编码) | 待测 | | 推理 + 安全认证 P99 | <20 ms | 现口径周期 P50 33.5 / P99 64.7 ms;批量认证把最坏 80.2→37.1 ms | 差 **2~3 倍** | -| 刹停 < 2% 推理周期 | <2% | 估算 **≈9.9%**(19.25 次全拒 ÷ 195 次规划;K=16 时 13.77/195 ≈ 7.1%) | 差 **3.5~5 倍** | +| 刹停 < 2% 推理周期 | <2% | `brake_rate_mean` **7.28%**(K16+`pre`,14.20 次/195 规划);p50 **0.0%**、p90 27.4% | 均值差 **3.6 倍**,但中位已达标 ⇒ 极端长尾(`stuck_share` 17.2%) | - 刹停率这一格现在是**手算估算**:`eval` 产物里以前没存每题规划次数。已补 `n_plans` 与 `brake_rate`(并因此踩了 `per['plans']` 键名冲突的坑),当前批次之后的臂会直接给这个数,届时替换估算。 - 记分板脚本 `ml/runs/_scoreboard.py`(读所有产物 + 轮次 33/38 的 GPU 微基准)。**注意它的输出在 GBK 控制台下会因 `⇒` 崩**,要带 `PYTHONIOENCODING=utf-8` 跑——这是我自己的老坑第 N 次复现,脚本以后只打 ASCII。 @@ -561,6 +507,73 @@ - 若转化率到 60% 以上:`backoff` 直接吃到失败分类里 63/109 那组,值得继续投预算(并与 `ep_waste` 组合再测)。 - 两个分支都要同时报 `brakes_mean` 与到达/时限,不能只报转化率。 +## 轮次 39:两条没人碰过的算法杠杆——候选档位与行程惩罚(机制已单测,A/B 排队) + +- **为什么是这两条**:轮次 38 的失败分类说清楚了两件事——① 到达率的缺口在"K 条候选全被拒 ⇒ 原地冻住"(63/109),② §9.2 要"关节行程 ≤ 规划器×1.10",实测行程比中位 **2.4~2.6**,而现在**没有任何机制在管行程**(选择器只看末态误差,安全层只看间隙)。另外 `--steps` 那组数顺带给了一条线索:多段推理把行程比从 2.62 拉到 1.87,说明行程主要由候选选择/动作形状决定,不是物理必然。 +- **杠杆 1:候选档位与多样性(#15)**。`ml/eval/eval_cl.py` 补 `--z_scale`(rollout 早就有这个参数,只是评估没接)。四臂对照轮次 33 cap:`K=16`、`K=8 --safety_try 3`、`K=8 --safety_try 1`、`K=8 --z_scale 1.5`。要回答的是:**全拒是"真没路"还是"没备选"**——若 K=16 或 z_scale 拉大能把到达/刹车买回来,那是候选池的问题;若 `safety_try 1` 几乎不掉到达,那说明第 1 条候选就够,后面试 7 条只是在花几何查询的钱(这对 20 ms 预算直接相关)。 +- **杠杆 2:行程惩罚(`select='ep_len'` + `--len_w`)**。分数从 `-末态误差` 改成 `-末态误差 − len_w·Σ_t Σ_j |Δ_j|/STEP_j`(每轴按自己的满速步长归一,所以 6 轴可比)。 + - 机制单测 2 条(`tests/test_eval_cl.py::TravelPenaltySelectTest`,40 项全过):造两条**末态相同、累计位移相同**的候选,一条来回抖(归一行程 10.8)一条匀速(3.6),`goal_err` 打成常数 ⇒ 只有行程能把它们分开。实测 `len_w=0` 选到抖动那条(第 1 步 1.08°)、`len_w=1.0` 选到匀速那条(第 1 步 0.27°);另有一条**同分零权重必须与 `select='endpoint'` 逐位相同**的回归断言(rtol=atol=0),保证默认行为没被这次改动碰到。 + - 待测:`len_w ∈ {0.1, 0.3, 1.0}` 三臂,看行程比能压到多少、到达与时限各付多少。这条杠杆与"到达率"是相互冲突的目标(惩罚行程=主动走慢),所以必须成对汇报,不能只报行程变好。 + - 单位口径要说清:`len_w` 的量纲是"mm 误差 / 一个归一 STEP 的累计行程",16 步满速在 3 条轴上大约是 48,所以 0.1~1.0 对应的惩罚是 5~50 mm 当量——这是扫这三个值的原因,不是随手取的。 +- 测试 `tests/test_eval_cl.py` 38→40 项、全套 43→45 项(`python -m unittest discover -s tests`,2.2 s 全绿)。GPU 队列顺序:backoff A/B → `--safety 0` 判别 → 候选档位 → 行程惩罚。 +- **等价闸门先过了**:`--backoff 0 --cert_batch 1 --inc_cap 1` 的 256 题与轮次 33 cap(`--cert_batch 0`)**逐项完全相同**(`docs/experiment_round38_backoff0.json` 对 `docs/experiment_round33_cap.json`,把两个 metrics 字典做键并集比较,差异集合为空,含 success 0.523438、time_ok 0.265625、brakes 19.55…)⇒ 轮次 36 我把批量认证分支重写(加缩步档那一段)没有改动任何行为。 +- **两个我自己的操作失误,如实记在这里**: + 1. 新加的 `backoff` 指标一度在产物里根本没有——`ml/eval/eval_cl.py:run()` 的 `out` 是**自己显式列键**的,不是从 `rollout` 返回的字典通抄,所以我往 `res` 加的 `backoff_mean` 被无声丢掉。已改成从逐题 `per['backoff']` 汇总(`backoff_mean` + `reject_all_mean = backoff+brakes`,后者才是与"原来全拒次数"可比的量)。**教训:加指标要顺着输出链路走到底,别只改生产端。** + 2. 停后台 GPU 作业时,`TaskStop` 之后 `bash` 脚本链会活下来并继续拉起下一个 python;而 `nvidia-smi --query-compute-apps` 在这台机器上列不出这些 python(只列 ShellHost/wallpaper32 之类),据此判断"卡已空"会误判。我第一次按 PID 杀子进程后,链条 30 秒后又起了一个新的 eval。第二次我按进程树杀,但**把父子关系读反了**(Windows 的 PPID 在父进程已死时是陈旧值),结果误杀了新链的第一臂。正确做法:重启前用命令行正则列出全量候选、数 python 个数确认单租户,重启后再数一次;杀之前先按 `StartTime` 判新旧。backoff 那一臂因此要补跑。 + +## 行程惩罚 ep_len 三臂读数:时限变好,但行程比没降(轮次 39 续,负结论) + +- 同一 256 题、同 seed、都带 `--inc_cap 1`,只改 `select=ep_len --len_w`: + + | 臂 | 到达 | 时限内(≤1.3×老师) | 行程比(中位) | 刹车/题 | 成功题末态误差 | + |---|---|---|---|---|---| + | 对照(endpoint) | 0.5234 | 0.2656 | 2.6158 | 19.55 | 0.218 mm | + | len_w=0.1 | 0.5117 | 0.2695 | 2.6216 | 19.49 | 0.244 mm | + | len_w=0.3 | 0.5195 | 0.2852 | 2.6548 | 20.00 | 0.219 mm | + | len_w=1.0 | 0.5273 | **0.2891** | 2.6719 | 19.35 | — | + + (`docs/experiment_round39_lenw{0.1,0.3,1.0}.json`) +- **两条相反的结论**:① 时限内**单调变好**(0.2656→0.2891,+2.4 pp = 6 题),到达在噪声内(0.5117/0.5195/0.5273,最大偏离对照 −1.2/+1.2 pp,n=256 下这就是噪声量级);② **行程比一点没降**(2.6216/2.6548/2.6719 vs 对照 2.6158,+0.006/+0.039/+0.056,只有最后一个勉强出噪声、且方向是"变差")。 + ⇒ "候选地平线内的累计行程"不是"实际执行行程"的好代理:罚它选出的候选在**近端少过冲**(所以收得快、时限内变好),但整段任务要更多次规划才能走完,累计行程不降反升。这条是我原先的假设被打掉,不是被确认;`ep_len` 只值 +2 pp 时限,且对 §9.2 的行程判据无用。 +- 顺着这个读数,真正该罚的是**同一轴来回**而不是总长:已实现 `select='ep_waste'`,罚 `(1 − 同轴净位移/累计行程) × 当前误差`(乘当前误差是为了近端不压死进度项——上一版的形状问题正在这里)。三臂 `len_w=0.1/0.3/1.0` 排在 `ml/runs/_waste_sweep.sh`。 +- 换向率指标 `reverse_p50` 从这一批之后才会出现在产物里(前面的臂是在改动前加载的模块跑的),口径已与离线分解同分母,预期对照臂应复现 **23.5%** 量级——**对不上就要查口径,不能各自报各自的**。 + +## 换向率进了评估指标(轮次 39 续) + +- 分解既然指到"来回抖",每个臂都要直接报这个数,不能每次回头读转储:`rollout` 里按轴记方向,`reverse_rate` = 同轴方向翻转的步数 / 活动步数(站住的步不重置方向,所以"停了再同向走"不算换向);`eval_cl` 报 `reverse_p50`/`reverse_mean`。 +- **正对照**(`tests/test_eval_cl.py`,42 项全绿):抖动轨迹报 >0.4、匀速轨迹报 ≈0——指标本身要证明有灵敏度,不然等于没测。 +- 待交叉验证:新指标在对照臂上应复现转储离线算的 **23.5%**(p50);对不上就是两个口径有一个错。 +- 生产端改了、输出端没改=没改:上一次 `backoff_mean` 就是这么丢的,这次加指标同时改了 `closed_loop.per` 与 `eval_cl` 的显式键列表。 + +## 行程超出的成因分解(轮次 39 续,纯 CPU 读轮次 33 base 转储) + +- 口径先对齐:只在"有老师参考且到达"的题上算,自测 **2.425** 与 eval 报的 2.445 同量级(差别在我按 `t_done/DT` 取窗端,eval 逐步累加),后面的数都在这个口径上。 +- **超出主要是同一根轴来回抖,不是绕远路**:净位移/总行程中位 **0.445**(完美单调应为 1.0)、各轴最大 excursion/行程 0.507、旧口径换向中位 23.5%(p25 15.7%、p75 30.3%)。 +- **撤回一句我自己的假象(并且这条也要再撤回一次,见下一条)**:上面原来写"到达组抖得更厉害(26.1% vs 未到达 14.7%)",**错在口径**。旧定义 `(dd[t+1]·dd[t] < 0).mean()` 要求相邻两步都非零,遇到"前进→停住→后退"就漏计;而未到达题恰恰大量站住,于是被系统性地少计。 +- **用与定义无关的量重算(净位移/总行程,同一转储、同一窗口)**:**到达组 0.478、未到达组 0.352** ⇒ 未到达组白费比例更高(走完 738° 只前进 255°;到达组是 569°/267°)。这条稳。 +- **但换向率的组间比较我第二次算错,现纠正**:上一次我用"未到达 24.0% vs 到达 14.4%"来说组间差异,那个算法把**到达题到达之后的冻结步**也算进了分母(`act` 只按 `tn<=limit`,没排除已到达),把到达组稀释了。按与 `rollout` 完全同语义的窗口(到达题截到 `t_done`)重算,得到的是**两组差不多**: + + | 转储(口径) | reverse_p50 全体 | 到达组 | 未到达组 | + |---|---|---|---| + | `round33_cap`(=轮次33 cap,限幅) | **0.2474** | 0.265 | 0.233 | + | `round33_base`(不限幅) | 0.257 | 0.275 | 0.240 | + + (`docs/experiment_round39_reverse_offline.json`)⇒ **换向是全局现象(约 1/4 的(步,轴)对在反向),不是失败题的特征**;失败题的特征是"白费行程比例高 + 大量站住"。这两件事先前被我混成一句,现在分开。 +- **可证伪预测(等 backoff 对照臂回来对表)**:同一配方下 in-eval 的 `reverse_p50` 应当 ≈ **0.247**。差得多就说明线上实现与离线同语义重算不一致,得先查仪表再谈算法。 +- **口径敏感要常驻**:分组/比较类断言之前,先换一种定义把同一份数据再算一遍;两种定义都给同方向才写进日志。分母必须与指标定义一致(`act_ = 未 done 且 在时限内`),否则"到达题窗短"会系统性稀释任何按步数归一的指标。 +- **对得上训练侧的事实**:预训练本来就有 `w_smooth=0.05` 的二阶差分正则,但闭环仍大量换向 ⇒ 训练期的平滑项没管住执行期的抖动。 +- **第一条 A/B 读数(`len_w=0.1`,256 题、`--inc_cap 1`)**:到达 0.5117(对照 0.5234,−1.2 pp)、时限内 0.2695(+0.4 pp)、**行程比 2.6216 几乎没动**(对照 2.6158)、成功题末态误差 0.2438(对照 0.2175,变差)。 + 原因是量级不匹配,不是方向错:`0.1 × 累计归一行程`(16 步满速在 3 条轴上约 48)只当量 ~5 mm,而早段末态误差是几百 mm ⇒ 罚不动。等 `len_w=0.3/1.0` 两臂看行程比是否真的开始下降;若仍压不住,惩罚对象应从"总行程"换成**换向/二阶差分**(直接打这个 23.5%)。 + +## 执行余量与前缀完整性审计(轮次 39 续,纯 CPU 读轮次 33 转储) + +- 256 题 × 195 次同步规划、commit_n=2 ⇒ 169,216 个**真正被执行过的**检查点(每步的端点+中点,间隙在 0.5 mm 截顶)。 +- **执行点的间隙分布**:≥0.5 mm(=宽裕)占 **91.0%**;0.2–0.5 mm 占 5.6%;<0.1 mm 占 0.84%;<0.02 mm 占 0.25%。每题"最险的那个执行点"的中位数只有 **0.029 mm**。 + ⇒ 两件事:① 大多数执行是宽裕的,所以"加 0.5 mm 余量门限"只花 3 题(轮次 33 margin 已实测)是自洽的;② 但每题都会至少一次贴到 **30 µm** 级——这是胶囊模型内部的"贴",真机的运动学与装配误差远大于这个数,**搬到实机时这条是实质风险**,不能当成"我们有余量"。 +- **前缀完整性**:满前缀 79.4%、部分前缀 10.8%、全拒 9.9%。分组看全拒率——**到达题 0.88%,未到达题 22.0%**(未到达组因此 44% 的步是零运动,与时间账量的 37% 同量级:那 37% 是"活动窗内",这里是"×commit_n"的换算)。 +- **对 `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)要读的第一个数。 + ## 轮次 38:把 `--steps` 打通之后,闭环反而变差——瓶颈从"动作质量"移到了"安全层拒绝率"