exp+fix: 候选塌缩定案(steps=2 把K条候选抹平35倍)+z_scale反例;补 n_plans/brake_rate 核算§9.2刹停率;记录我在链式作业排队期间改库导致两臂未跑的事故

This commit is contained in:
cbb
2026-10-20 22:48:51 +08:00
parent 4597569dd8
commit b30c271896
5 changed files with 130 additions and 1 deletions
+22
View File
@@ -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
}
}
+79
View File
@@ -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
}
+22
View File
@@ -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`)不变,只改**安全层允许试几条候选**:
+4 -1
View File
@@ -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}})
+3
View File
@@ -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(),