diff --git a/docs/experiment_round38_cand_div.json b/docs/experiment_round38_cand_div.json new file mode 100644 index 0000000..eea8ea6 --- /dev/null +++ b/docs/experiment_round38_cand_div.json @@ -0,0 +1,22 @@ +{ + "K": 8, + "n": 256, + "steps1": { + "cand_spread": 0.8518, + "first_step_norm": 1.1565, + "clamp_saturated_rate": 0.0516, + "pairwise_mean": 5.3647 + }, + "steps2": { + "cand_spread": 0.0245, + "first_step_norm": 1.0956, + "clamp_saturated_rate": 0.0385, + "pairwise_mean": 0.3024 + }, + "steps4": { + "cand_spread": 0.0211, + "first_step_norm": 1.097, + "clamp_saturated_rate": 0.0689, + "pairwise_mean": 0.2861 + } +} \ No newline at end of file diff --git a/docs/experiment_round39_z15.json b/docs/experiment_round39_z15.json new file mode 100644 index 0000000..2e0c203 --- /dev/null +++ b/docs/experiment_round39_z15.json @@ -0,0 +1,79 @@ +{ + "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_round39_z15.json", + "trace": null, + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "cert_batch": 0, + "steps": 1, + "z_scale": 1.5, + "len_w": 0.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.49609375, + "collision_free_success": 0.49609375, + "time_ok_1p3": 0.18359375, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 3.52425217628479, + "time_mean_s": 2.805511713027954, + "brakes_mean": 10.51171875, + "start_fail": 0.0, + "goal_ratio_2s": 0.27530819177627563, + "err_fail_med": 105.35015869140625, + "err_success_med": 0.23657508194446564, + "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.32083332538604736, + "reverse_mean": 0.3213125765323639, + "reverse_moving_p50": 0.35158151388168335, + "moving_share_p50": 0.9866666793823242 + }, + "simulation_wall_s": 311.9796243000019 +} diff --git a/docs/探索日志.md b/docs/探索日志.md index 7480218..8063bcc 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -107,6 +107,28 @@ - **对 `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)要读的第一个数。 +## 候选为什么会"塌":两段积分把 K 条候选吸成一条(轮次 39 探针,机理定案) + +- `ml/runs/_cand_div.py`(256 题特征、同一批噪声 z、K=8,只改段数): + + | steps | 候选间一阶步的标准差 spread | 两两平均距离 pairwise | 第 1 步模长 | ±1 饱和率 | + |---|---|---|---|---| + | 1 | **0.8518** | 5.3647 | 1.1565 | 5.2% | + | 2 | **0.0245** | 0.3024 | 1.0956 | 3.9% | + | 4 | 0.0211 | 0.2861 | 1.0970 | 6.9% | + + ⇒ **多段积分把 K 条候选的差别抹掉了 35 倍**(0.85 → 0.024)。这不是"动作更大更危险"那么简单:安全层是在 K 条**几乎相同**的候选里挑一条能走的前缀,挑无可挑就刹停。 +- 这条与轮次 38 的实测严丝合缝:`steps=2/4/8` 到达掉 13–15 pp、刹车 ×4.5,但**开环误差与行程比都更好**——动作质量提升是真的,到达率下降是"没有备选"造成的。 +- **但"人为制造多样性"不是解法**(`z_scale=1.5`,同 K=8):刹车确实砍一半(19.55→10.51),可到到达掉到 0.4961、时限内从 0.2656 崩到 **0.1836**、行程比恶化到 3.5243、换向升到 0.32。⇒ 放大噪声换来的"能过的候选"是**分布外**的候选:安全层放行它,但它走的是绕、抖、慢的路。**K=16 的收益来自"同分布内更多候选",不是"多样性本身"**——这两个数放在一起才把这条轴的边界说清楚。 +- 判据状态:#21 的两个分支现在这样分:**"动作更危险"不是主因,"候选塌缩"是主因**(spread 35 倍 + try/K 单调曲线 + z_scale 反例三条独立证据)。剩下要量的只是成本口径(K=16 在批量认证下贵多少)。 + +## 事故:我在链式作业排队期间编辑了被 import 的库,两个臂直接没跑 + +- 时间线:`z15` 于 14:46:38 结束,链接着拉起 `safety=0 steps=1`(14:46:38)——而我为了让 `brake_rate` 有分母,正在 14:45:5x 用脚本改 `ml/sim/closed_loop.py`,一次替换把 `per = {...}` 的字典提前闭合,留下 `IndentationError: unexpected indent`。**新起的 python 进程 import 到的是半成品文件**,两个 safety-off 臂各 2 秒就退出(日志里 `=== safety=0 steps=1 done 14:46:40 ===` 的"done"只是 bash 循环走到下一行,不代表成功)。修好在 14:46:52,48 项测试全绿。 +- **教训(比我原先记的"别改正在跑的脚本"更进一层)**:链式作业会在**未来某个时刻**新起解释器,所以"只要不改正在跑的那一个进程"是不够的。规则改为:**链没结束就不编辑被 import 的库;要编辑,先等通知或先让链跑完**。并且 `=== done ===` 这种循环日志不能当成功凭据——**判成功要看产物文件在不在**(这次 `docs/experiment_round38_safetyoff_steps{1,2}.json` 根本没生成,一看便知)。 +- 两个 safety-off 臂与 `ep_waste`/`backoff`/K 代价一起并入下一批重跑(`ml/runs/_next_batch.sh`)。 +- 顺带补了验收判据缺的分母:`rollout` 现在记 `n_plans`(每题规划次数)并给 `brake_rate = brakes/n_plans`、`eval_cl` 报 `brake_rate_p50/mean` 与 `n_plans_mean`——§9.2 要的是"刹停 <2% **推理周期**",以前只有绝对次数,**这条判据实际上从来没被真正核算过**。命名上躲开一个坑:`per` 里原本有个 `'plans'` 键是转储用的逐规划字典,新键必须叫 `n_plans`,否则 `flat` 的过滤 `if k not in ('traj','plans')` 会把它静默吃掉。 + ## 试候选数的成本-收益曲线(轮次 39,四档齐全,#15 主结果) - 同一 256 题、同 seed、`--inc_cap 1`、其余配方(`select=endpoint, replan=empty, commit_n=2, K=8, fb=2.2`)不变,只改**安全层允许试几条候选**: diff --git a/ml/eval/eval_cl.py b/ml/eval/eval_cl.py index a7d9c2f..69133fc 100644 --- a/ml/eval/eval_cl.py +++ b/ml/eval/eval_cl.py @@ -54,6 +54,9 @@ def run(model, kin, tasks, device, bs=256, **kw): out.update({'reverse_p50': median(rr), 'reverse_mean': rr.mean().item(), 'reverse_moving_p50': median(flat['reverse_rate_moving']), # 只在动了的步上算,不被刹停稀释 'moving_share_p50': median(flat['moving_share'])}) # 活动窗内动了的步占比(=1−站住比例) + if 'brake_rate' in flat: # §9.2 判据是"刹停 <2% 推理周期",所以要按规划次数归一,不能只报绝对次数 + out.update({'brake_rate_mean': flat['brake_rate'].mean().item(), 'brake_rate_p50': median(flat['brake_rate']), + 'n_plans_mean': flat['n_plans'].mean().item()}) if 'backoff' in flat and flat['backoff'].any(): # backoff=缩步顶上的次数;brakes 现在只记连最小档都不安全的 out['backoff_mean'] = flat['backoff'].mean().item() # ⇒ 原来的"全拒"事件数 = 两者之和 out['reject_all_mean'] = (flat['backoff'] + flat['brakes']).mean().item() @@ -139,7 +142,7 @@ def main(): 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'] + keys = ['done', 'ok', 't_done', 'limit', 'col', 'brakes', 'err', 'first_hit', 'dls_it', 'goal_ratio_2s', 'brake_rate', 'n_plans'] 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}}) diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index c1b751a..263f702 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -204,6 +204,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device) rev = torch.zeros(B, device=device); nact = torch.zeros(B, device=device); prev_sgn = torch.zeros(B, 6, device=device) nmov = torch.zeros(B, device=device) + nplan = torch.zeros(B, device=device) # 每题规划次数 = 推理周期数(§9.2 的"刹停 <2% 推理周期"要这个分母) traj = [q.clone()] if return_traj else None plans = {k: [] for k in ('it', 'r', '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° @@ -216,6 +217,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, for it in range(nmax): tnow = it * DT if (queue.shape[1] <= 3) if replan == 'r3' else (queue.shape[1] == 0): + nplan += 1.0 # 验收判据要"刹停 / 推理周期"的比值,分母就是这个(批内所有行同步规划) r = queue.shape[1] pre = torch.zeros(B, 3, 6, device=device) qe = q @@ -513,6 +515,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, 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(), '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(), 'backoff': bo.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(), + 'n_plans': nplan.cpu(), 'brake_rate': (brakes / nplan.clamp_min(1.0)).cpu(), 'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu(), # 口径与离线分解一致:换向的 (步,轴) 对数 / 活动的 (步,轴) 对数 ∈ [0,1](除以 6 个轴) 'reverse_rate': (rev / (6.0 * nact.clamp_min(1.0))).cpu(),