diff --git a/docs/本机执行清单.md b/docs/本机执行清单.md new file mode 100644 index 0000000..1155237 --- /dev/null +++ b/docs/本机执行清单.md @@ -0,0 +1,53 @@ +# 本机执行清单(给本地 AI 逐条执行) + +本文件是 Hark 对本机计算的唯一要求清单。按顺序执行,每一步把「回传」里的文件或输出原样交给用户,不要跳步,不要自行改代码。 +所有命令在仓库根目录(G:\Robot_v2\机器人手臂)的 Git Bash 里运行。完成的条目由 Hark 在下一次提交里删掉。 + +当前状态(2026-10-19 02:00):第二次预训练开环已正常(候选多样性 0.65、8 候选最好 2.9 mm),闭环仍 0% 到达 / 100% 碰撞; +DAgger 阶段曾把显存吃到约 30 GB,原因是 kin.gaps 一次几千个点、cap=40 mm,已在 e0e99ca 修复(自动分块),尚未实测。 + +## 第 0 步:同步代码 + +```bash +git fetch gitea && git reset --hard gitea/main +git log --oneline -1 # 应不早于 e0e99ca +``` + +- 训练控制台若开着,先点「停止」,确认没有 python 训练进程在跑。 +- 不要启动 DAgger / M5,等 Hark 看完第 1、2 步的结果再定。 + +## 第 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。 +- 任务管理器里专用显存接近 24 GB 或共享显存开始上涨:立刻 Ctrl+C,回传已打印的行,不要继续第 2 步。 +- 回传:终端输出的 3 行。 + +## 第 2 步:闭环失败定位(约 2–4 分钟) + +```bash +PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json +``` + +- 运行期间看一眼任务管理器,专用显存应远低于 24 GB;超过 22 GB 就 Ctrl+C 并回传终端输出。 +- 回传:`ml/runs/diag_cl.json`、`ml/runs/replay_teacher.json`、`ml/runs/base/log.jsonl`。 + +## 回传汇总 + +| 步骤 | 回传内容 | +|---|---| +| 1 | 终端 3 行峰值 | +| 2 | ml/runs/diag_cl.json、ml/runs/replay_teacher.json、ml/runs/base/log.jsonl | +| 任一步出错 | 终端最后 30 行输出 | diff --git a/ml/README.md b/ml/README.md index ff04097..7c480e6 100644 --- a/ml/README.md +++ b/ml/README.md @@ -129,6 +129,7 @@ bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/b ``` 开训前先跑老师轨迹回放自检(`ml/runs/replay_teacher.json`,约 1 分钟):老师轨迹放进 PyTorch 仿真口径,到达 ≥ 95%、碰撞 ≤ 1% 才开训,否则 rc 5 停——口径对不上时闭环指标不可信。 +闭环指标长期 0% 而开环(ol_fk_mm / ol_best_mm / ol_div)正常时,跑 `PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256`(约 2–4 分钟,可与训练并行)→ `ml/runs/diag_cl.json`:A 起点特征 Python 与 JS 逐组差、B 两份特征下动作差、C 首次碰撞时刻 / 组对 / 最小目标误差 / 开安全层到达率。 先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练到 60000 步或闭环到达率连续 5 次不升为止。 diff --git a/ml/common/kin_torch.py b/ml/common/kin_torch.py index 91fb176..31587a1 100644 --- a/ml/common/kin_torch.py +++ b/ml/common/kin_torch.py @@ -118,12 +118,41 @@ class Kin: return a, b # ---------------- 间隙 ---------------- + # 2026-10-19(用户实测 dagger/诊断吃满 24 GB 显存):粗筛后幸存胶囊对数随 点数 × cap 暴涨, + # 一次几千个点、cap=40 mm 时中间张量可达十几 GB(训练里 eval 峰值 15.9 GiB 也是它)。 + # 现在先按簇级粗筛估出每个点的幸存胶囊对数,按 PAIR_BUDGET 把 batch 切块逐块算,峰值与调用方的 batch 大小无关。 + PAIR_BUDGET = 2_000_000 + def gaps(self, q, cap=40.0, xf=None, max_pairs=None): """返回 dict: gm [B,49]:组对最小间隙 min(gap, cap)(gA*7+gB,gAbki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g] + d2 = (cc[:, self.cp_a] - cc[:, self.cp_b]).pow(2).sum(-1) + per = ((d2 <= (self.cp_lim + cap).pow(2)).long() * self.cp_cnt).sum(1) # [B] 每点幸存胶囊对数 + del cc, d2 + if B <= 1 or int(per.sum()) <= self.PAIR_BUDGET: + bounds = [0, B] + else: + cs = torch.cumsum(per, 0).tolist(); bounds = [0]; base = 0 + for k, c in enumerate(cs): + if c - base > self.PAIR_BUDGET and k > bounds[-1]: + bounds.append(k); base = cs[k - 1] + bounds.append(B) + if len(bounds) == 2: + return self._gaps_one(q, cap, xf) + parts = [self._gaps_one(q[s:e], cap, (Rs[s:e], Ts[s:e])) for s, e in zip(bounds[:-1], bounds[1:])] + return {'gm': torch.cat([p['gm'] for p in parts], 0), + 'b': torch.cat([p['b'] + s for p, s in zip(parts, bounds[:-1])]), + '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])} + + def _gaps_one(self, q, cap, xf): Rs, Ts = xf B = q.shape[0] with torch.no_grad(): diff --git a/ml/dagger/collect.py b/ml/dagger/collect.py index 49e8779..8ea3b3b 100644 --- a/ml/dagger/collect.py +++ b/ml/dagger/collect.py @@ -16,6 +16,8 @@ from ml.data import load_tasks from ml.model.policy import build from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors, DT +GAP_CHUNK = 1024 # 每次 kin.gaps 的查询点数上限 + def main(): ap = argparse.ArgumentParser() @@ -41,8 +43,14 @@ def main(): tr = per['traj'].to(a.device) # [B,T,6] B, Tn, _ = tr.shape _, T, R, _, _ = tasks_to_tensors(tb, a.device) + # 2026-10-19 修(用户实测):原来一次喂 64 题 × 64 步 = 4096 点、cap=40 mm 给 kin.gaps,40 mm 内幸存的胶囊对太多, + # 峰值把显存 / 内存吃满。这里只需判断"≤0(碰撞)"和"> gap_min(起点地板)",cap 取 gap_min+1 即可, + # 且按 GAP_CHUNK 个点分块,用完即释放。 + cap = gap_min + 1.0 + flat = tr.reshape(-1, 6) with torch.no_grad(): - g = torch.cat([kin.gaps(tr[:, s:s + 64].reshape(-1, 6), 40.0)['gm'].min(-1).values.view(B, -1) for s in range(0, Tn, 64)], 1) + g = torch.cat([kin.gaps(flat[s:s + GAP_CHUNK], cap)['gm'].min(-1).values for s in range(0, flat.shape[0], GAP_CHUNK)]).view(B, Tn) + del flat for b, t in enumerate(tb): hit = (g[b] <= 0).nonzero() c = int(hit[0]) if hit.numel() else Tn @@ -58,6 +66,8 @@ def main(): if e.item() < 5 and (not t.get('R') or oe.item() < 5): stat['rej_near'] += 1; continue out.append({'q0': [round(v, 3) for v in q[0].tolist()], 'T': t['T'], 'R': t.get('R'), 'cat': t.get('cat', 'A'), 'src': {'tid': t['tid'], 'k': k, 'col': bool(hit.numel())}}) + del tr, g, per + if a.device.startswith('cuda'): torch.cuda.empty_cache() print(f'[dagger] {min(i + a.bs, len(tasks))}/{len(tasks)} 题,起点 {len(out)}', flush=True) stat['states'] = len(out) rng.shuffle(out) diff --git a/ml/eval/diag_cl.py b/ml/eval/diag_cl.py new file mode 100644 index 0000000..c45e884 --- /dev/null +++ b/ml/eval/diag_cl.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +"""闭环失败定位(只读,手动运行,约 2–4 分钟;训练进行中也可以另开终端跑,显存约 2–3 GB)。 + +2026-10-19 新增:第二次预训练(979a581)开环已经正常(候选0 末端误差 ~6 mm、K 中最好 ~2.9 mm、多样性 0.65), +闭环却仍 100% 碰撞、0% 到达。开环用的是 JS 生成的特征 X,闭环用的是 Python features() 现算的特征, +两者之间只要有一处口径不同,模型在闭环里看到的就是没见过的输入。本脚本回答三件事: + A. 特征口径:val 任务起点(非增广、老师状态)上,Python features() 与数据集 X 的逐组最大差; + B. 同一噪声下,用两份特征得到的动作差(16 步末端关节差,度)——A 有差但 B 很小说明差异无害; + C. 闭环失败形态:首次碰撞时刻分布、碰撞步数、首次碰撞的组对、全程最小目标误差; + 以及同一批题开安全层(safety=True,近似部署)的到达率 / 刹停次数。 +用法(仓库根目录): + PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json +""" +import argparse, json, os + +import torch + +from ml.common.kin_torch import Kin +from ml.common.features import features, STEP, H +from ml.data import Samples, load_tasks +from ml.model.policy import build +from ml.sim.closed_loop import rollout, goal_err, DT + +# 83 维特征分组(与 features.py 拼接顺序一致) +GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 24), ('Rq', 24, 30), ('T', 30, 33), + ('R', 33, 39), ('T-p', 39, 42), ('T-p_clamp', 42, 45), ('mode', 45, 47), ('prefix', 47, 65), ('mask', 65, 68), + ('sub', 68, 74), ('sub_flag', 74, 75), ('gaps', 75, 83)] + + +def pct(v, p): + v = sorted(v) + return v[min(len(v) - 1, int(len(v) * p))] if v else None + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples') + ap.add_argument('--n', type=int, default=256); ap.add_argument('--bs', type=int, default=64) + ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default='ml/runs/diag_cl.json') + a = ap.parse_args() + dev = a.device + ck = torch.load(a.ckpt, map_location=dev) + model = build(ck['cfg']).to(dev); model.load_state_dict(ck['model']); model.eval() + kin = Kin(device=dev) + out = {'ckpt': a.ckpt, 'step': ck.get('step')} + tasks = [t for t in load_tasks([a.samples]) if t['split'] == 'val'][:a.n] + by_tid = {t['tid']: t for t in tasks} + + # ---------- A / B:起点特征口径 ---------- + vd = Samples(a.samples, device=dev, split='val') + tid = vd.TID.long() + first = torch.ones_like(tid, dtype=torch.bool); first[1:] = tid[1:] != tid[:-1] # 每题第一行 = t=0 样本 + idx = [i for i in first.nonzero().flatten().tolist() if int(tid[i]) in by_tid] + X0 = vd.X[idx].float() + tk = [by_tid[int(tid[i])] for i in idx] + q0 = torch.tensor([t['q0'] for t in tk], dtype=torch.float32, device=dev) + qx = torch.atan2(X0[:, 0:12:2], X0[:, 1:12:2]) * 180 / torch.pi + dqx = torch.remainder(qx - q0 + 180, 360) - 180 + clean = dqx.abs().max(-1).values < 1e-2 # 未被噪声增广的起点 + out['A_rows'] = int(clean.sum()); out['A_rows_aug_skipped'] = int((~clean).sum()) + if clean.any(): + Xc = X0[clean]; qc = q0[clean]; tc = [t for t, c in zip(tk, clean.tolist()) if c] + T = torch.tensor([t['T'] for t in tc], dtype=torch.float32, device=dev) + R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tc], dtype=torch.float32, device=dev) + B = qc.shape[0] + pre = Xc[:, 47:65].view(B, 3, 6) * torch.tensor(STEP, device=dev) # 前缀 / 子目标直接取数据集里的,只比状态相关部分 + pl = Xc[:, 65:68].sum(-1).round() + sf = Xc[:, 74] + sub = qc + Xc[:, 68:74] * 180 + Fp = features(kin, qc, qc, T, R, pre, pl, sub, sf) + out['A_maxdiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().max().item() for g, s, e in GROUPS} + out['A_meandiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().mean().item() for g, s, e in GROUPS} + with torch.no_grad(): + z = torch.randn(B * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(7)) + a1, _, _ = model.sample_k(Xc, 8, z); a2, _, _ = model.sample_k(Fp, 8, z) + stepv = torch.tensor(STEP, device=dev) + d = ((a1.clamp(-1, 1) - a2.clamp(-1, 1)) * stepv).sum(2).abs().max(-1).values # [B,K] 16 步末端关节差(度) + mag = (a1.clamp(-1, 1) * stepv).sum(2).abs().max(-1).values + out['B_act_diff_deg'] = {'mean': d.mean().item(), 'p90': d.flatten().kthvalue(max(1, int(0.9 * d.numel()))).values.item(), + 'ref_motion_deg_mean': mag.mean().item()} + + # ---------- C:闭环失败形态 ---------- + first_t, col_steps, pairs, min_err, reach = [], [], {}, [], 0 + reach_s, brakes_s, col_s = 0, [], 0 + for i in range(0, len(tasks), a.bs): + ch = tasks[i:i + a.bs] + with torch.no_grad(): + _, per = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True) + tr = per['traj'].to(dev) # [B,Tn,6] + B, Tn, _ = tr.shape + flat = tr.reshape(-1, 6) # 分块 ≤1024 点(同 dagger/collect.py 的显存修复) + gm = torch.cat([kin.gaps(flat[s:s + 1024], 0.5)['gm'] for s in range(0, flat.shape[0], 1024)]).view(B, Tn, 49) + hit = gm.min(-1).values <= 0 # [B,Tn] + 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) + e, _ = goal_err(kin, tr.reshape(-1, 6), Tt.repeat_interleave(Tn, 0), Rt.repeat_interleave(Tn, 0)) + e = e.view(B, Tn) + # 到达后轨迹被冻结(d=0),之后不再计 + for b in range(B): + if per['done'][b]: reach += 1 + min_err.append(e[b].min().item()); col_steps.append(int(per['col'][b])) + h = hit[b].nonzero().flatten() + if len(h): + k = int(h[0]); first_t.append(k * DT) + pr = int(gm[b, k].argmin()); key = '%d-%d' % (pr // 7, pr % 7); pairs[key] = pairs.get(key, 0) + 1 + rs, ps = rollout(model, kin, ch, dev, K=8, safety=True, seed=i) + reach_s += int(ps['done'].sum()); col_s += int((ps['col'] > 0).sum()); brakes_s += ps['brakes'].tolist() + n = len(tasks) + out['C'] = {'n': n, 'reach': reach / n, 'collided': len(first_t) / n, + 'first_hit_s': {'p10': pct(first_t, .1), 'p50': pct(first_t, .5), 'p90': pct(first_t, .9), + 'within_0.1s': sum(t <= 0.1 for t in first_t) / max(1, len(first_t))}, + 'col_steps': {'p50': pct(col_steps, .5), 'p90': pct(col_steps, .9)}, + 'first_hit_pair(group a-b)': dict(sorted(pairs.items(), key=lambda kv: -kv[1])[:8]), + 'min_goal_err_mm': {'p10': pct(min_err, .1), 'p50': pct(min_err, .5), 'p90': pct(min_err, .9)}, + 'safety_on': {'reach': reach_s / n, 'collided': col_s / n, 'brakes_p50': pct(brakes_s, .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) + + +if __name__ == '__main__': + main()