From 527bc25c1197cf6069c9e4df9ac66ee3c46d4294 Mon Sep 17 00:00:00 2001 From: Hark Date: Sun, 18 Oct 2026 18:53:39 +0000 Subject: [PATCH] =?UTF-8?q?fix(kin/diag):=20gaps=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=8D=E6=8B=BC=E8=83=B6=E5=9B=8A=E5=AF=B9=EF=BC=88pairs=3DT?= =?UTF-8?q?rue=20=E4=BB=85=20APG=20=E7=94=A8=EF=BC=9B=E5=AE=9E=E6=B5=8B=20?= =?UTF-8?q?4096=C3=9740=20=E5=B3=B0=E5=80=BC=204.46=20GiB=20=E5=85=A8?= =?UTF-8?q?=E5=9C=A8=E6=8B=BC=E6=8E=A5=208.6e7=20=E5=AF=B9=EF=BC=89?= =?UTF-8?q?=EF=BC=9Bdiag=5Fcl=20=E5=88=A0=20D=20=E6=AE=B5=EF=BC=88?= =?UTF-8?q?=E5=AD=90=E7=9B=AE=E6=A0=87=E5=B7=B2=E6=8E=92=E9=99=A4=EF=BC=89?= =?UTF-8?q?=E3=80=81=E5=8A=A0=20E=20=E6=AE=B5=EF=BC=88=E5=BC=80=E7=8E=AF?= =?UTF-8?q?=E5=B9=85=E5=BA=A6=E4=B8=8E=E6=8A=B9=20dq=20=E6=95=8F=E6=84=9F?= =?UTF-8?q?=E5=BA=A6=E3=80=81=E8=80=81=E5=B8=88=E4=B8=AD=E6=AE=B5=E5=B8=A6?= =?UTF-8?q?=E9=80=9F=E5=BA=A6/=E9=9D=99=E6=AD=A2=E8=B5=B7=E6=AD=A5?= =?UTF-8?q?=E9=97=AD=E7=8E=AF=EF=BC=89=EF=BC=9Brollout=20=E5=8A=A0=20qp0?= =?UTF-8?q?=EF=BC=9B=E6=9C=AC=E6=9C=BA=E6=89=A7=E8=A1=8C=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/本机执行清单.md | 61 +++++---------------------- ml/apg/apg_finetune.py | 2 +- ml/common/kin_torch.py | 28 +++++++------ ml/eval/diag_cl.py | 93 ++++++++++++++++++++++++++++-------------- ml/sim/closed_loop.py | 6 +-- 5 files changed, 93 insertions(+), 97 deletions(-) diff --git a/docs/本机执行清单.md b/docs/本机执行清单.md index cbb426a..d2f2197 100644 --- a/docs/本机执行清单.md +++ b/docs/本机执行清单.md @@ -3,23 +3,20 @@ 本文件是 Hark 对本机计算的唯一要求清单。按顺序执行,每一步把「回传」里的文件或输出原样交给用户,不要跳步,不要自行改代码。 所有命令在仓库根目录(G:\Robot_v2\机器人手臂)的 Git Bash 里运行。完成的条目由 Hark 在下一次提交里删掉。 -## 已知结论(2026-10-19 02:35 回传) +## 已知结论(截至 2026-10-19 02:43 回传) -- 特征口径 Python 与 JS 一致(最大差 3.6e-6),同噪声动作差 ~1e-5°:排除「闭环输入没见过」。 -- 闭环关安全层 0% 到达 / 99.6% 碰撞,首撞中位 1.46 s,主碰撞组对 2-4;全程最小目标误差中位 254 mm:策略根本没朝目标走。开安全层碰撞 5.5%,到达仍 0.4%。 -- 开环正常:ol_fk_mm 5.98、ol_best_mm 2.92、ol_div 0.655。老师回放 pass。 -- 下一个嫌疑:子目标。训练时特征里的子目标是老师真实途经点,闭环里换成子目标头自己的预测。第 2 步的 D 段专门验证它。 -- gaps 显存:上一版只给精算段分块,粗筛段 4096 点仍 9.3 GiB。本版粗筛也按点分块(每块 ≤ 446 点)。 +- 特征口径 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 要)。 -## 第 0 步:同步代码(不要 reset --hard) - -本地有未提交的 `ml/monitor.html`、`ml/pstate.py` 改动和一个未推送的合并提交,`reset --hard` 会把它们丢掉。 +## 第 0 步:同步代码 ```bash -git add ml/monitor.html ml/pstate.py && git commit -m "monitor: 开环指标两张图;pstate: train 归档加 review_*" +git status --short # 有未提交改动就先 commit,不要丢 git pull --rebase gitea main -git log --oneline -3 # 最上面应是你的 monitor 提交,其下是 Hark 的「fix(kin/diag): …」 -git push gitea main +git log --oneline -1 # 应是 Hark 的「fix(kin/diag): gaps 默认不拼胶囊对 …」 ``` - 训练控制台若开着先点「停止」,确认没有 python 训练进程在跑。 @@ -44,14 +41,14 @@ EOF - 专用显存接近 22 GB 或共享显存开始上涨:立刻 Ctrl+C,回传已打印的行,不要继续第 2 步。 - 回传:终端 3 行。 -## 第 2 步:闭环失败定位(含新加的 D 段,约 5–8 分钟) +## 第 2 步:闭环失败定位(含新加的 E 段,约 5–8 分钟) ```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 并回传终端输出。 -- 新增输出:`C.goal_err_ratio_p50`(0.5/1/2 s 时目标误差 ÷ 起点误差,<1 表示在靠近);`D0_subhead`(冷启动子目标头精度);`D1_no_sub`(不用子目标);`D2_oracle_sub`(用老师真实途经点)。 +- 新增输出:`E0_openloop`(开环动作幅度 ÷ 老师幅度;把 dq 抹成 0 后的幅度;起点行幅度);`E1_mid_teacher_vel` / `E1_mid_zero_vel`(从老师轨迹 30% 处起步,带老师速度 vs 静止起步:到达、碰撞、1 s 目标误差比、前 1 s 速度 ÷ 老师速度)。D 段已删。 - 回传:`ml/runs/diag_cl.json`。 ## 回传汇总 @@ -63,39 +60,3 @@ PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out | 任一步出错 | 终端最后 30 行输出 | --- - -## 执行回传(2026-10-19 03:55,本地 AI) - -### 第 0 步:同步代码 - -monitor/pstate 按清单提交 `1f2f2a2`("monitor: 开环指标两张图;pstate: train 归档加 review_*"),pull --rebase 后已推送。log 与预期一致:`1f2f2a2`(monitor)→ `0653cf0`(Hark fix(kin/diag))。无训练进程在跑。 - -### 第 1 步:gaps 显存峰值(实测) - -``` -n= 512 cap= 40 peak 1.05 GiB -n= 4096 cap= 4 peak 1.40 GiB -n= 4096 cap= 40 peak 4.46 GiB -``` - -前两条过线;4096×40 仍 **4.46 GiB**(>2 GiB 判据)。 - -分层定位(独立测量): -- **粗筛分块工作正常**:单粗块(446 点,粗筛+块内全部精算)峰值 1.05 GiB。 -- **峰值全在顶层 `_cat_gaps` 拼接**:完整 gaps 4.54 GiB == 只做 `_cat_gaps`(parts 备好后单独调)4.54 GiB。 -- 机制:4096 点 cap=40 共 **8.63e7 细对**;顶层拼接时 10 个粗块的 parts(b/i/j int64 各 0.64 GiB、gap fp32 0.32 GiB,合计 2.25 GiB)与拼接输出(同样长大)同时存活,加上 `p['b']+s` 的临时副本,瞬时到 4.5 GiB 量级。 -- 三个可行的修法方向(未改代码):① 逐键消费——`torch.cat([p.pop('b')+s …])` 一类,每拼完一个键释放该键 parts,峰值降到 ~2.5 GiB;② b/i/j int32 化(细对数 8.6e7 < 2^31,索引完全够),内存直接减半到 ~2.3 GiB;③ 顶层不拼大数组,消费方(dagger/collect、dag 收集)按块消费。 -- 版本对比:上一版(`573082d`)同配置 9.30 GiB → 本版 4.54 GiB;旧版(`0b47905`)36.9 GiB。 - -### 第 2 步:闭环失败定位(含 D 段;完整 JSON 已更新到 ml/runs/diag_cl.json) - -A/B/C 与上轮一致(A/B 全过;C:0% 到达 / 99.6% 碰撞,主碰撞对 J3-J5)。新增输出: - -- **C.goal_err_ratio_p50**:0.5 s **0.967**、1.0 s **0.892**、2.0 s **0.815** —— 误差比值 <1,方向没错、在缓慢靠近,但 2 秒只缩 **18.5%**,从未接近。 -- **D0_subhead**:需子目标的题占 28.1%;need 二分类准确率 71.9%;冷启动预测的途经点 1 每关节最大误差 p50 **66.8°** / p90 **137.1°** —— 子目标头确实基本预测不对。 -- **D1_no_sub**(完全不用子目标):到达 **0%** / 碰撞 **99.6%** / 末端误差 p50 430 mm。 -- **D2_oracle_sub**(用老师真实途经点):到达 **0%** / 碰撞 **99.6%** / 末端误差 p50 435 mm。 - -**D 段结论:子目标不是元凶。** D2(最有利输入)与 D1、C 基线三者到达率/碰撞率完全相同(0% / 99.6%),末端误差也一致(430/435 mm)——拿掉子目标或换成老师真值都毫无区别。子目标头虽然预测很差(p50 66.8°),但不是闭环失败的原因;策略在闭环状态里本身就完全走不动(2 s 只靠近 18.5% 且 99.6% 撞)。 - -### 第 1、2 步都没出错,无终端报错段。 diff --git a/ml/apg/apg_finetune.py b/ml/apg/apg_finetune.py index ae20075..59cffe4 100644 --- a/ml/apg/apg_finetune.py +++ b/ml/apg/apg_finetune.py @@ -118,7 +118,7 @@ def main(): d = queue[:, k] qp = q; q = kin.clamp_box(q + d) # 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致) - G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M) + G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M, pairs=True) hinge = torch.relu(M - G['gap']) col = torch.zeros(2 * B, device=dev).index_add(0, G['b'], hinge).view(2, B).mean(0) # 端点与中点取均值:与只查端点时同量纲,w_col 无需重标 sm = ((d / step) ** 2).sum(-1) + (((d - prev_d) / step) ** 2).sum(-1) diff --git a/ml/common/kin_torch.py b/ml/common/kin_torch.py index 4545e13..9096a06 100644 --- a/ml/common/kin_torch.py +++ b/ml/common/kin_torch.py @@ -129,24 +129,27 @@ class Kin: @staticmethod def _cat_gaps(parts, starts): - return {'gm': torch.cat([p['gm'] for p in parts], 0), - 'b': torch.cat([p['b'] + s for p, s in zip(parts, starts)]), - 'i': torch.cat([p['i'] for p in parts]), 'j': torch.cat([p['j'] for p in parts]), - 'gap': torch.cat([p['gap'] for p in parts])} + out = {'gm': torch.cat([p.pop('gm') for p in parts], 0)} + if 'b' in parts[0]: + for p, s in zip(parts, starts): p['b'] += s + for k in ('b', 'i', 'j', 'gap'): out[k] = torch.cat([p.pop(k) for p in parts]) # 逐键拼、拼完即释放 + return out - def gaps(self, q, cap=40.0, xf=None, max_pairs=None): + def gaps(self, q, cap=40.0, xf=None, max_pairs=None, pairs=False): """返回 dict: gm [B,49]:组对最小间隙 min(gap, cap)(gA*7+gB,gA 0.5)) - # D0:冷启动子目标头精度(起点特征里抹掉数据集的真子目标,与闭环冷启动同口径) - Xs = X0.clone(); Xs[:, 68:75] = 0 - with torch.no_grad(): _, sp = model.heads(model.encode(Xs)) - lab = torch.stack([vd.SG[i].float() for i in idx]) - need_l = lab[:, 12] > 0.5; need_p = sp[:, 12] > 0.5 - via_err = ((sp[:, :6] - lab[:, :6]) * 180).abs().max(-1).values - out['D0_subhead'] = {'n': len(idx), 'need_label_rate': need_l.float().mean().item(), 'need_acc': (need_l == need_p).float().mean().item(), - 'via1_maxjoint_err_deg_p50': pct(via_err[need_l].tolist(), .5), 'via1_maxjoint_err_deg_p90': pct(via_err[need_l].tolist(), .9)} - # D1 不用子目标、D2 用老师真实途经点(oracle),同一批题、同一随机种子,对比 C 的基线 - def run(tag, **kw): - rc = cc_ = 0; me = [] - for i in range(0, len(tasks), a.bs): - ch = tasks[i:i + a.bs]; extra = dict(kw) - if kw.get('sub_override') == 'oracle': - subs = torch.stack([sg_by_tid[t['tid']][0] if t['tid'] in sg_by_tid else torch.zeros(2, 6, device=dev) for t in ch]) - need = torch.tensor([sg_by_tid[t['tid']][1] if t['tid'] in sg_by_tid else False for t in ch], device=dev) - extra['sub_override'] = (subs, need) + # ---------- E:速度与「抄速度」(2026-10-19 加) ---------- + # 上一轮 D 段:不用子目标 / 用老师真实途经点,结果与基线完全一样(0% / 99.6%),子目标排除。 + # C 段 2 s 只把目标误差缩小 18.5%:方向对、但走得极慢。开环(数据集里的老师状态)却几乎精确。 + # 嫌疑:特征里的 dq(上一帧速度)在老师数据里总是满速,模型学会「照上一帧速度往下走」; + # 闭环从静止起步 dq=0,模型就给小动作,小动作又喂回 dq,一直慢,慢慢挤进自碰撞。 + 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] with torch.no_grad(): - _, pp = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, **extra) - rc += int(pp['done'].sum()); cc_ += int((pp['col'] > 0).sum()); me += pp['err'].tolist() - out[tag] = {'reach': rc / n, 'collided': cc_ / n, 'end_goal_err_mm_p50': pct(me, .5)} - run('D1_no_sub', use_sub=False) - run('D2_oracle_sub', sub_override='oracle') + _, 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) + tr = pp['traj'].to(dev); B, Tn, _ = tr.shape + k1 = min(Tn - 1, int(round(1.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)} 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/sim/closed_loop.py b/ml/sim/closed_loop.py index 08a39fa..3c0c353 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): - """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,2,6] 绝对关节角, need [B] bool):冷启动时用给定途经点代替子目标头(诊断用)""" +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,即静止起步)。均为诊断用""" g = torch.Generator(device=device); g.manual_seed(seed) q0, T, R, ex, tr = tasks_to_tensors(tasks, device) B = q0.shape[0] @@ -74,7 +74,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, has_ref = torch.isfinite(ex) limit = torch.where(has_ref, torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC)) if max_sec is None else torch.full_like(ex, max_sec) nmax = int(math.ceil(limit.max().item() / DT)) - q, qp = q0.clone(), q0.clone() + q, qp = q0.clone(), (qp0.to(device) if qp0 is not None else q0.clone()) queue = torch.zeros(B, 0, 6, device=device) last = None subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device); sub_t = torch.zeros(B, device=device)