diff --git a/docs/本机执行清单.md b/docs/本机执行清单.md index 423424e..683ec0a 100644 --- a/docs/本机执行清单.md +++ b/docs/本机执行清单.md @@ -3,90 +3,37 @@ 本文件是 Hark 对本机计算的唯一要求清单。按顺序执行,每一步把「回传」里的文件或输出原样交给用户,不要跳步,不要自行改代码。 所有命令在仓库根目录(G:\Robot_v2\机器人手臂)的 Git Bash 里运行。完成的条目由 Hark 在下一次提交里删掉。 -## 已知结论(截至 2026-10-19 02:43 回传) +## 已知结论(截至 2026-10-19 03:07 回传) -- 特征口径 Python 与 JS 一致(最大差 3.6e-6):排除「闭环输入没见过」。 -- 子目标排除:不用子目标、用老师真实途经点,与基线完全一样(0% 到达 / 99.6% 碰撞)。 -- 闭环方向对但极慢:2 s 只把目标误差缩小 18.5%,首撞中位 1.46 s,主碰撞组对 2-4。开环(老师状态上)几乎精确。 -- 现在的嫌疑:模型「抄上一帧速度」。第 2 步的 E 段专门验证。 -- gaps 显存:粗筛分块已生效(1.05 / 1.40 GiB),4096×40 的 4.46 GiB 全在拼接胶囊对。本版默认只返回 gm,不再拼胶囊对(只有 APG 要)。 +- 特征口径 Python 与 JS 一致;子目标、抄上一帧速度、起步都已排除(老师轨迹中段带速度起步也 0% 到达、96% 碰撞)。 +- gaps 显存已过:三条都 1.05 GiB,与点数无关。 +- 稳定现象:策略动作幅度在开环、起点、闭环三处都是老师的 1.4–1.6 倍,开环 16 步末端却准(~6 mm)。幅度大、终点准 = 每步叠着来回抵消的抖动;闭环只执行前 3 步,抖动不抵消,表现为「动得多、走得少」。 +- 现在的嫌疑:MeanFlow 一步生成没把噪声去干净。第 1 步的 F 段专门验证。 ## 第 0 步:同步代码 ```bash git status --short # 有未提交改动就先 commit,不要丢 git pull --rebase gitea main -git log --oneline -1 # 应是 Hark 的「fix(kin/diag): gaps 默认不拼胶囊对 …」 +git log --oneline -1 # 应是 Hark 的「diag: F 段 …」 ``` - 训练控制台若开着先点「停止」,确认没有 python 训练进程在跑。 -- 不要启动 DAgger / M5,等 Hark 看完第 1、2 步结果再定。 +- 不要启动训练 / DAgger / M5,等 Hark 看完第 1 步结果再定。 -## 第 1 步:gaps 显存峰值(不到 1 分钟) - -```bash -PYTHONUTF8=1 python - <<'EOF' -import torch -from ml.common.kin_torch import Kin -kin = Kin(device='cuda') -for n, cap in [(512, 40.0), (4096, 4.0), (4096, 40.0)]: - q = kin.lo + (kin.hi - kin.lo) * torch.rand(n, 6, device='cuda') - torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats() - with torch.no_grad(): kin.gaps(q, cap) - print(f'n={n:5d} cap={cap:4.0f} peak {torch.cuda.max_memory_allocated() / 2**30:6.2f} GiB', flush=True) -EOF -``` - -- 通过:三行峰值都 ≤ 2 GiB。 -- 专用显存接近 22 GB 或共享显存开始上涨:立刻 Ctrl+C,回传已打印的行,不要继续第 2 步。 -- 回传:终端 3 行。 - -## 第 2 步:闭环失败定位(含新加的 E 段,约 5–8 分钟) +## 第 1 步:闭环失败定位(含新加的 F 段,约 6–10 分钟) ```bash PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json ``` - 专用显存超过 22 GB 就 Ctrl+C 并回传终端输出。 -- 新增输出:`E0_openloop`(开环动作幅度 ÷ 老师幅度;把 dq 抹成 0 后的幅度;起点行幅度);`E1_mid_teacher_vel` / `E1_mid_zero_vel`(从老师轨迹 30% 处起步,带老师速度 vs 静止起步:到达、碰撞、1 s 目标误差比、前 1 s 速度 ÷ 老师速度)。D 段已删。 +- 新增输出:`F0_openloop_sampler`(开环 1/2/4/8 段采样与零噪声:幅度比、抖动比、残差与噪声相关、前 3 步与 16 步末关节误差);`F1_4step`、`F1_z0`(闭环用 4 段采样、零噪声:到达、碰撞、2 s 目标误差比)。E 段已删。 - 回传:`ml/runs/diag_cl.json`。 ## 回传汇总 | 步骤 | 回传内容 | |---|---| -| 1 | 终端 3 行峰值 | -| 2 | ml/runs/diag_cl.json | +| 1 | ml/runs/diag_cl.json | | 任一步出错 | 终端最后 30 行输出 | - ---- - -## 执行回传(2026-10-19 04:05,本地 AI) - -### 第 0 步:同步代码 - -pull 前无未提交的 tracked 改动;两个 untracked 文件原位保留未丢(`ml/eval/diag_collision.py` —— 早前已被 diag_cl 取代的旧诊断脚本;根目录「视频轮播-绿幕间隔.html」—— 用户个人文件,两者都未入仓)。log 顶部 = `527bc25`「fix(kin/diag): gaps 默认不拼胶囊对 …」✓。无训练进程。 - -### 第 1 步:gaps 显存峰值(实测) - -``` -n= 512 cap= 40 peak 1.05 GiB -n= 4096 cap= 4 peak 1.05 GiB -n= 4096 cap= 40 peak 1.05 GiB -``` - -**通过**:三行都 ≤ 2 GiB,且都等于单粗块(446 点)的固有水平——默认不拼胶囊对后,峰值与总点数完全无关。 - -### 第 2 步:闭环失败定位(E 段;完整 JSON 已更新到 ml/runs/diag_cl.json) - -A/B/C 与前两轮一致(A/B 全过;C:0% 到达 / 99.6% 碰撞、2 s 误差比 0.815、主碰撞对 J3-J5)。E 段新增: - -- **E0_openloop**(开环 val 随机 4096 行):老师幅度 0.319;策略开环幅度比 **1.438**;**把 dq 抹成 0 后幅度比 1.399(只降 2.7%)**;老师正在动的行上抹 dq 也 1.385(不降);抹 dq 前后 16 步末端动作差 p50 7.8°;起点行(静止起步)幅度比 **1.521**。 - → **排除「抄上一帧速度」**:若幅度靠 dq 撑着,抹 dq 应暴降;实测几乎不动(1.438→1.399)。反过来,开环/起点幅度本身就系统性偏大(1.44~1.52×)。 -- **E1_mid_teacher_vel**(老师轨迹 30% 处起步、带老师上一帧速度):到达 **0%** / 碰撞 **96.1%** / 1 s 误差比 **0.627** / 前 1 s 速度比 **1.565** -- **E1_mid_zero_vel**(同起点、静止起步):到达 **0%** / 碰撞 **97.7%** / 1 s 误差比 **0.829** / 速度比 **1.442** - → 带老师速度只把 1 s 误差比从 0.829 改善到 0.627,但两者都 0% 到达、~96–98% 碰撞(老师剩余时长中位 1.5 s):**不是起步问题**,中段带速度起步也全灭。 - -**本轮观察(供定位)**:策略幅度在开环(1.44)、起点行(1.52)、闭环前 1 s(1.44~1.57)三处一致地是老师的 **1.4~1.6 倍**——幅度系统性偏大是稳定的模型级现象,不是起步或 dq 造成的。 - -### 第 1、2 步都没出错,无终端报错段。 diff --git a/ml/eval/diag_cl.py b/ml/eval/diag_cl.py index 95e18c5..13d92a3 100644 --- a/ml/eval/diag_cl.py +++ b/ml/eval/diag_cl.py @@ -8,8 +8,7 @@ B. 同一噪声下,用两份特征得到的动作差(16 步末端关节差,度)——A 有差但 B 很小说明差异无害; C. 闭环失败形态:首次碰撞时刻分布、碰撞步数、首次碰撞的组对、全程最小目标误差、目标误差进度比; 以及同一批题开安全层(safety=True,近似部署)的到达率 / 刹停次数。 - E. 速度:开环动作幅度 / 老师幅度、抹掉 dq 后的幅度(是否「抄上一帧速度」); - 从老师轨迹中段起步的闭环,带老师速度 vs 静止起步(是否只坏在起步)。 + F. 一步采样残留噪声:开环比较 1/2/4/8 段采样与零噪声的幅度、抖动、残差与噪声相关;闭环用 4 段采样、零噪声各跑一遍。 用法(仓库根目录): PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json """ @@ -124,65 +123,43 @@ def main(): 'safety_on': {'reach': reach_s / n, 'collided': col_s / n, 'brakes_p50': pct(brakes_s, .5)}} out['C']['goal_err_ratio_p50'] = {'%.1fs' % ts: pct(v, .5) for ts, v in prog.items()} # <1 = 在靠近目标 - # ---------- E:速度与「抄速度」(2026-10-19 加) ---------- - # 上一轮 D 段:不用子目标 / 用老师真实途经点,结果与基线完全一样(0% / 99.6%),子目标排除。 - # C 段 2 s 只把目标误差缩小 18.5%:方向对、但走得极慢。开环(数据集里的老师状态)却几乎精确。 - # 嫌疑:特征里的 dq(上一帧速度)在老师数据里总是满速,模型学会「照上一帧速度往下走」; - # 闭环从静止起步 dq=0,模型就给小动作,小动作又喂回 dq,一直慢,慢慢挤进自碰撞。 + # ---------- F:一步采样残留噪声(2026-10-19 加) ---------- + # 前几轮:子目标、抄速度、起步都已排除;但动作幅度在开环、起点、闭环三处一致是老师的 1.4–1.6 倍, + # 而开环 16 步末端 FK 误差只有 ~6 mm。幅度大、终点却准 = 每步动作里叠着来回抵消的抖动。 + # 嫌疑:MeanFlow 一步生成 x = z − u(z,0,1) 没把噪声 z 去干净(只有 25% 样本学区间平均速度、14k 步), + # 闭环每次只执行前 3 步,抖动不抵消,表现为「动得多、走得少」,再挤进碰撞。 + # F0:开环 val 随机 4096 行,比较 1/2/4/8 段采样和零噪声(z=0):幅度比、抖动比、残差与 z 的相关、前 3 步与 16 步末关节误差 + # F1:同一批闭环题,用 4 段采样、零噪声各跑一遍,对比 C 的基线 stepv = torch.tensor(STEP, device=dev) - gen = torch.Generator(device=dev).manual_seed(11) - # E0:开环,val 全部时刻随机 4096 行。动作幅度 / 老师幅度;把 dq 抹成 0 后幅度与 16 步末端差 ri = torch.randperm(vd.X.shape[0], generator=torch.Generator().manual_seed(3))[:4096].to(dev) Xr, Yr = vd.X[ri].float(), vd.Y[ri].float() - zr = torch.randn(len(ri), 1, H, 6, device=dev, generator=gen) - def amp(X): - with torch.no_grad(): ac, _, _ = model.sample_k(X, 1, zr) - return ac[:, 0].clamp(-1, 1) - a_full = amp(Xr); X0dq = Xr.clone(); X0dq[:, 15:21] = 0; a_0dq = amp(X0dq) - ymag = Yr.abs().mean().item() - moving = Xr[:, 15:21].abs().max(-1).values > 0.3 # 老师此刻在动(|dq| > 30% 满速) - out['E0_openloop'] = {'n': len(ri), 'teacher_amp': ymag, 'policy_amp_ratio': a_full.abs().mean().item() / ymag, - 'dq_zeroed_amp_ratio': a_0dq.abs().mean().item() / ymag, - 'dq_zeroed_amp_ratio_on_moving_rows': a_0dq[moving].abs().mean().item() / max(1e-6, Yr[moving].abs().mean().item()), - 'dq_zeroed_end_diff_deg_p50': pct(((a_full - a_0dq) * stepv).sum(1).abs().max(-1).values.tolist(), .5), - 'start_rows_amp_ratio': None} - st = first.nonzero().flatten()[:4096] - with torch.no_grad(): a_st, _, _ = model.sample_k(vd.X[st].float(), 1, torch.randn(len(st), 1, H, 6, device=dev, generator=gen)) - out['E0_openloop']['start_rows_amp_ratio'] = a_st.clamp(-1, 1).abs().mean().item() / max(1e-6, vd.Y[st].float().abs().mean().item()) - # E1:从老师轨迹中段(每题第 30% 行)起步闭环,带老师上一帧速度 vs 静止起步,同一批题同一噪声 - tid_all = vd.TID.long(); starts = first.nonzero().flatten().tolist() + [len(tid_all)] - mid_rows = [] - for s0, s1 in zip(starts[:-1], starts[1:]): - if int(tid_all[s0]) in by_tid and s1 - s0 >= 4: mid_rows.append(s0 + int(0.3 * (s1 - s0))) - mid_rows = mid_rows[:a.n] - Xm = vd.X[mid_rows].float() - qm = torch.atan2(Xm[:, 0:12:2], Xm[:, 1:12:2]) * 180 / torch.pi - qm = torch.where(kin.cont, qm, kin.clamp_box(qm)) - qpm = qm - Xm[:, 15:21] * stepv - mtasks = [] - for r_, q_ in zip(mid_rows, qm.tolist()): - t = dict(by_tid[int(tid_all[r_])]); t['q0'] = q_; t['exec_s'] = float(vd.V[r_]) * 5; mtasks.append(t) - rem = [t['exec_s'] for t in mtasks] - ym = vd.Y[mid_rows].float().abs().mean().item() - for tag, use_v in (('E1_mid_teacher_vel', True), ('E1_mid_zero_vel', False)): - rc = cc_ = 0; ratio1 = []; spd = [] - for i in range(0, len(mtasks), a.bs): - ch = mtasks[i:i + a.bs] + zr = torch.randn(len(ri), 1, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(11)) + ymag = Yr.abs().mean().item(); yjit = (Yr[:, 1:] - Yr[:, :-1]).abs().mean().item() + f0 = {'teacher_amp': ymag, 'teacher_jitter': yjit} + for tag, steps, zs in (('1step', 1, 1.0), ('2step', 2, 1.0), ('4step', 4, 1.0), ('8step', 8, 1.0), ('1step_z0', 1, 0.0)): + with torch.no_grad(): ac, _, _ = model.sample_k(Xr, 1, zr * zs, steps=steps) + ac = ac[:, 0]; acc = ac.clamp(-1, 1); res = ac - Yr + zc = zr[:, 0] - zr[:, 0].mean(); rc_ = res - res.mean() + corr = ((zc * rc_).sum() / (zc.pow(2).sum().sqrt() * rc_.pow(2).sum().sqrt()).clamp(min=1e-9)).item() if zs > 0 else None + e3 = ((acc[:, :3] - Yr[:, :3]) * stepv).sum(1).abs().max(-1).values + e16 = ((acc - Yr) * stepv).sum(1).abs().max(-1).values + f0[tag] = {'amp_ratio': acc.abs().mean().item() / ymag, 'jitter_ratio': (acc[:, 1:] - acc[:, :-1]).abs().mean().item() / yjit, + 'resid_corr_z': corr, 'first3_err_deg_p50': pct(e3.tolist(), .5), 'end16_err_deg_p50': pct(e16.tolist(), .5)} + out['F0_openloop_sampler'] = f0 + for tag, kw in (('F1_4step', {'steps': 4}), ('F1_z0', {'z_scale': 0.0})): + rc = cc_ = 0; me = []; r2 = [] + for i in range(0, len(tasks), a.bs): + ch = tasks[i:i + a.bs] with torch.no_grad(): - _, pp = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True, - qp0=qpm[i:i + a.bs] if use_v else None) + _, pp = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True, **kw) tr = pp['traj'].to(dev); B, Tn, _ = tr.shape - k1 = min(Tn - 1, int(round(1.0 / DT))) + k2 = min(Tn - 1, int(round(2.0 / DT))) Tt = torch.tensor([t['T'] for t in ch], dtype=torch.float32, device=dev) Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in ch], dtype=torch.float32, device=dev) - e0, _ = goal_err(kin, tr[:, 0], Tt, Rt); e1, _ = goal_err(kin, tr[:, k1], Tt, Rt) - ratio1 += (e1 / e0.clamp(min=1e-3)).tolist() - dd = (tr[:, 1:k1 + 1] - tr[:, :k1]) / stepv - spd.append(dd.abs().mean().item()) - rc += int(pp['done'].sum()); cc_ += int((pp['col'] > 0).sum()) - nm = len(mtasks) - out[tag] = {'n': nm, 'teacher_remaining_s_p50': pct(rem, .5), 'reach': rc / nm, 'collided': cc_ / nm, - 'goal_err_ratio_1s_p50': pct(ratio1, .5), 'speed_ratio_first_1s': sum(spd) / len(spd) / max(1e-6, ym)} + e0, _ = goal_err(kin, tr[:, 0], Tt, Rt); e2, _ = goal_err(kin, tr[:, k2], Tt, Rt) + r2 += (e2 / e0.clamp(min=1e-3)).tolist() + rc += int(pp['done'].sum()); cc_ += int((pp['col'] > 0).sum()); me += pp['err'].tolist() + out[tag] = {'reach': rc / n, 'collided': cc_ / n, 'goal_err_ratio_2s_p50': pct(r2, .5), 'end_goal_err_mm_p50': pct(me, .5)} s = json.dumps(out, indent=1, ensure_ascii=False); print(s) os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True) open(a.out, 'w', encoding='utf-8').write(s) diff --git a/ml/model/policy.py b/ml/model/policy.py index 3166f01..8e419d6 100644 --- a/ml/model/policy.py +++ b/ml/model/policy.py @@ -151,8 +151,9 @@ class Policy(nn.Module): x = z - self.decode(m, z, torch.zeros_like(one), one) return x, v.expand(K), s.expand(K, -1) - def sample_k(self, feat, K, z=None): - """仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13]""" + def sample_k(self, feat, K, z=None, steps=1): + """仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13] + steps>1(诊断用,2026-10-19):把 [0,1] 均分成 steps 段,x ← x − (t−r)·u(x, r, t) 逐段走;steps=1 即一步生成。""" mem = self.encode(feat) v, s = self.heads(mem) B = feat.shape[0] @@ -165,7 +166,10 @@ class Policy(nn.Module): z = torch.randn(B * K, self.h, 6, device=feat.device, dtype=feat.dtype) if z is None else z.reshape(B * K, self.h, 6) m = mem.repeat_interleave(K, 0) one = torch.ones(B * K, device=feat.device, dtype=feat.dtype) - x = z - self.decode(m, z, torch.zeros_like(one), one) + x = z + for i in range(steps): + t, r = 1 - i / steps, 1 - (i + 1) / steps + x = x - (t - r) * self.decode(m, x, one * r, one * t) return x.view(B, K, self.h, 6), v, s diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index 3c0c353..75a3185 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -65,8 +65,8 @@ def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5): @torch.no_grad() -def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None, qp0=None): - """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,2,6] 绝对关节角, need [B] bool):冷启动时用给定途经点代替子目标头;qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。均为诊断用""" +def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None, qp0=None, steps=1, z_scale=1.0): + """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,2,6] 绝对关节角, need [B] bool):冷启动时用给定途经点代替子目标头;qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放(0 = 只取「均值」样本)。均为诊断用""" g = torch.Generator(device=device); g.manual_seed(seed) q0, T, R, ex, tr = tasks_to_tensors(tasks, device) B = q0.shape[0] @@ -102,8 +102,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t) sf = (nsub > 0).float() feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=device), subs[:, 0], sf) - z = torch.randn(B * K, H, 6, device=device, generator=g) - act, val, sub = model.sample_k(feat, K, z) + z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale + act, val, sub = model.sample_k(feat, K, z, steps=steps) inc = act.clamp(-1, 1) * step if cold: sub0 = sub