exp: 轮次45——tie(等长安全前缀按沿路最小间隙定夺)判掉

两臂 n=256 同 seed 背靠背(配方=轮次42 K8+cert_batch=1+inc_cap=1):到达 0.5234→0.5039
(-1.95pp,逐题配对丢6赢1净-5,McNemar 精确 p=0.125 与零不可分),时限内 +1.17pp、
刹停 -9%、行程比 -0.076、执行碰撞仍为 0(判据未放松)。丢的 6 题 5 题在 A 类(开阔区)
=买余量卖进度的签名。

仪表与离线数交叉验证:改选率 5.58% 对上轮次44 独立测得的"执行点间隙落在 0.2–0.5mm 占
5.6%",并列率 79.0% 对上"满前缀 79.4%" ⇒ 分辨率上界被宽相位截顶 gcap=max(0.5,margin+0.5)
锁死,5.6% 是这个家族在 cap=0.5 下的天花板,不是实现没调好。

结构性发现:#24 作用在错的人群——tie 只在 maxlen>0 触发,而失败主人群是全拒(maxlen=0,
未到达题 22% 的规划);全拒那条已由 backoff(轮次43)判掉。前置条件记为 #28(gcap 与
gap_margin 解耦)。

实现账:新增 tie 与触发计数器(生产端 per 数组+dump 键、消费端汇总 tie_* 同批改完,
tie=0 时 22/22 老键逐位复现);修掉 tie 分支局部名 nmax 顶掉主循环总步数的遮蔽 bug,
守卫单测做过变异检查;计数器正对照可手算(tie_opp=tie_changed=n_plans)。47 项测试绿。
This commit is contained in:
cbb
2026-10-21 00:33:44 +08:00
parent 350f386c02
commit 72c8835229
8 changed files with 564 additions and 5 deletions
+8 -2
View File
@@ -68,6 +68,10 @@ def run(model, kin, tasks, device, bs=256, **kw):
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()
if 'tie_opp' in flat and flat['tie_opp'].any(): # tie=1 的自检:钩子可用率/真的改选率,分母与 brake_rate 同为规划次数
out.update({'tie_opp_rate_mean': flat['tie_opp_rate'].mean().item(),
'tie_changed_rate_mean': flat['tie_changed_rate'].mean().item(),
'tie_opp_task_share': (flat['tie_opp'] > 0).float().mean().item()})
return out, per
@@ -115,6 +119,7 @@ def main():
ap.add_argument('--steps', type=int, default=1, help='MeanFlow 采样段数;1=线上一口径(历史发布数字全是 1)')
ap.add_argument('--z_scale', type=float, default=1.0, help='候选噪声缩放:>1 让 K 条候选更分散(测"全拒是没有备选"还是"真的没路")')
ap.add_argument('--len_w', type=float, default=0.0, help="ep_len=罚累计归一行程;ep_waste=罚(1−净/总)×当前误差;0 与 endpoint 逐位相同")
ap.add_argument('--tie', type=int, default=0, help='1=等长安全前缀时按沿路最小间隙定夺(需 --cert_batch 1)')
ap.add_argument('--backoff', type=int, default=0, help='全拒时把首选候选按 1/2、1/4…缩步重试的档数(需 --cert_batch 1)')
ap.add_argument('--max_sec', type=float, default=None)
ap.add_argument('--seed', type=int, default=0)
@@ -142,7 +147,7 @@ def main():
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, backoff=a.backoff, len_w=a.len_w, return_traj=bool(a.trace))
inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, backoff=a.backoff, len_w=a.len_w, tie=a.tie, 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)
@@ -150,7 +155,8 @@ 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', 'brake_rate', 'n_plans']
keys = ['done', 'ok', 't_done', 'limit', 'col', 'brakes', 'err', 'first_hit', 'dls_it', 'goal_ratio_2s', 'brake_rate', 'n_plans',
'tie_opp', 'tie_changed', 'tie_opp_rate', 'tie_changed_rate']
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}})
+17 -2
View File
@@ -163,7 +163,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=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, backoff=0, len_w=0.0, progress=None):
inc_cap=0, gap_margin=0.0, cert_batch=0, backoff=0, len_w=0.0, tie=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 条)。
@@ -174,10 +174,11 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
gap_margin>0:安全层要求间隙大于该毫米余量而不是 >0;宽相位 cap 自动抬到 margin+0.5,否则 0.5 的截顶值会让余量门限恒假。
backoff>0:全拒(连第 1 步都不安全)时不再站住,把首选候选按 1/2、1/4…(共 backoff 档)缩步一起送进同一次认证,
取能通过的第 1 档执行;缩步候选不参与"前缀更长者优先"的比较,只在原候选全被拒时顶上。需 cert_batch=1。
tie=1:等长安全前缀之间改用「沿路最小间隙更大」定夺(默认 0=沿用选择器分数顺序)。需 cert_batch=1。
select='ep_len' + len_w>0:在末态误差打分上再减 len_w·Σ|Δ|/STEP(每轴归一的候选累计行程)。§9.2 要行程≤规划器×1.10,
而实测行程比 2.4~2.6 没有任何机制在管;len_w=0 与 select='endpoint' 逐位相同(有回归断言)。
两者默认值下行为与第32轮逐位相同。"""
if backoff and not cert_batch: raise ValueError('backoff 目前只实现在 cert_batch=1 口径')
if (backoff or tie) and not cert_batch: raise ValueError('backoff/tie 目前只实现在 cert_batch=1 口径')
g = torch.Generator(device=device); g.manual_seed(seed)
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
B = q0.shape[0]
@@ -199,6 +200,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
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)
bo = torch.zeros(B, device=device) # 缩步顶上的次数(backoff>0 才会计)
to = torch.zeros(B, device=device) # tie=1 才计:出现「等长安全前缀并列」的规划次数(钩子可用)
tx = torch.zeros(B, device=device) # tie=1 才计:并列里真的由余量改选了那条的次数(钩子有效)
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)
@@ -363,6 +366,15 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
plen = pl_all[:, :ntry]
maxlen = plen.max(-1).values
best = plen.argmax(-1) # 与串行同口径:严格更大者胜 ⇒ 首个最大值
if tie: # 等长前缀时不再只看选择器分数:取"沿路最小间隙更大"的那条(零额外几何调用,判据仍是 gap>0)
ci = torch.arange(2 * commit_n, device=device)
keep = (ci < maxlen[:, None]) | ((ci >= commit_n) & (ci < commit_n + maxlen[:, None])) # [B,2cn] 只看会被执行的那些点
clr = gaps6[:, :ntry].where(keep[:, None, :], torch.full((), float('inf'), device=device)).amin(-1)
cand = torch.where(plen == maxlen[:, None], clr, torch.full_like(clr, -float('inf'))).argmax(-1)
ntile = (plen == maxlen[:, None]).sum(-1) # 并列最长的候选条数(勿用 nmax:那是主循环的总步数)
to += (ntile > 1) & (maxlen > 0)
tx += (cand != best)
best = cand
if backoff: # 缩步候选只在全拒时顶上:不参与「前缀更长者优先」的比较,所以正常路径逐位不变
full0 = maxlen == 0
bb = pl_all[:, ntry:].argmax(-1) # 平手取缩得最少的(档 0 = 1/2)
@@ -516,6 +528,9 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
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(),
'tie_opp': to.cpu(), 'tie_changed': tx.cpu(), # tie=1 才有意义:并列次数/改选次数(每题)
'tie_opp_rate': (to / nplan.clamp_min(1.0)).cpu(), # 分母与 brake_rate 同口径 = 规划次数
'tie_changed_rate': (tx / 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(),