"""APG 可微微调(方案 §7.1):按部署节奏可微展开策略,直接对"走得到、不碰、平滑"求梯度;不需要老师。 python -m ml.apg.apg_finetune --init ml/runs/base/best.pt --samples ml/data/samples --out ml/runs/apg [--pool ml/runs/mine/pool.jsonl] 每条展开固定一个噪声 z(确定性可微映射);每次推理提交 3 步(已提交前缀 3 步,与部署一致); 截断反传:每 seg_plans 次推理(=3·seg_plans 步)反传一次并截断状态梯度。 损失:段末到达项 + Σ碰撞铰链 ReLU(margin_train − 间隙)(逐胶囊对,端点+中点)+ 平滑(速度²+加速度²)+ 行程 + BC 锚定(MeanFlow 损失)。 2026-10-18:展开长度不再定长 60 次推理(3.6 s,会截掉老师时长 > 3.6 s 的绕远题),改为覆盖本批 exec_s p90×1.2,上限 plans_max; "到达"仍是每 seg_plans 段末的距离项(无到达即停),属有意设计:段末项给出持续的接近梯度。 margin_train 3 mm:与训练数据过滤线一致(认证最小间隙 < 3 mm 的老师轨迹不进训练集),碰撞项不与 BC 标签对抗; 运行时投影护栏独立保持 5 mm。端点与中点两个铰链取均值,与只查端点同量纲,w_col 无需重标。""" import argparse, json, math, os, random, sys, time import torch from ml.common.kin_torch import Kin from ml.common.features import features, STEP, H, wrap180 from ml.data import Samples, load_tasks from ml.model.policy import build, wta_loss from ml.model.meanflow import meanflow_loss from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err from ml.common.ckpt import Saver, atomic_save def load_pool(paths): out = [] for p in paths or []: if os.path.exists(p): out += [json.loads(l) for l in open(p, encoding='utf-8') if l.strip()] return out def main(): ap = argparse.ArgumentParser() ap.add_argument('--init', required=True); ap.add_argument('--samples', nargs='+', default=['ml/data/samples']) ap.add_argument('--pool', nargs='*', default=[]); ap.add_argument('--out', default='ml/runs/apg') ap.add_argument('--config', default='ml/configs/apg.json'); ap.add_argument('--steps', type=int, default=None) ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0) ap.add_argument('--resume', default=None, help='从 /last.pt 续跑(2026-10-18 第五轮,审查 N-11:几小时的 APG 断了不再从头来)') a = ap.parse_args() cfg = json.load(open(a.config)); C = cfg['apg'] if a.steps: C['steps'] = a.steps dev = a.device; torch.manual_seed(a.seed); random.seed(a.seed) os.makedirs(a.out, exist_ok=True) ck = torch.load(a.init, map_location=dev) mcfg = ck['cfg'] model = build(mcfg).to(dev); model.load_state_dict(ck['model']) kin = Kin(device=dev) data = Samples(a.samples, device=dev, split='train') all_tasks = load_tasks(a.samples) train_tasks = [t for t in all_tasks if t['split'] == 'train'] + load_pool(a.pool) val_tasks = [t for t in all_tasks if t['split'] == 'val'][:C['eval_tasks']] w = [C['cat_weights'].get(t.get('cat', 'A'), 1.0) * (2.0 if t.get('hard') else 1.0) for t in train_tasks] opt = torch.optim.AdamW(model.parameters(), lr=C['lr'], betas=(0.9, 0.95), weight_decay=0.0) amp = dev.startswith('cuda') and C.get('amp') == 'bf16' step = torch.tensor(STEP, device=dev) it0 = 0 if a.resume and os.path.exists(a.resume): rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model']) if 'opt' in rk: opt.load_state_dict(rk['opt']) it0 = rk.get('step', 0) bp = os.path.join(a.out, 'best.pt') best = torch.load(bp, map_location='cpu').get('eval', {}).get('success', -1.0) if os.path.exists(bp) else -1.0 log = open(os.path.join(a.out, 'log.jsonl'), 'a') print(f'续跑:从第 {it0} 步继续,当前最好 success {best:.4f}', flush=True) else: log = open(os.path.join(a.out, 'log.jsonl'), 'w') base, _ = evaluate(model.eval(), kin, val_tasks, dev) print('EVAL0', json.dumps(base), flush=True); log.write(json.dumps({'eval': base, 'step': 0}) + '\n') best = base['success'] atomic_save({'model': model.state_dict(), 'cfg': mcfg, 'step': 0, 'fp': kin.fp, 'eval': base}, os.path.join(a.out, 'best.pt')) M = C['margin_train_mm']; t0 = time.time() saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存) def save_last(n, ev=None): atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev}, os.path.join(a.out, 'last.pt')); saver.mark() for it in range(it0, C['steps']): model.train() batch = random.choices(train_tasks, weights=w, k=C['batch']) q0, T, R, ex, tr = tasks_to_tensors(batch, dev) B = q0.shape[0] z = torch.randn(B, H, 6, device=dev) q, qp = q0.clone(), q0.clone() queue = torch.zeros(B, 0, 6, device=dev) subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev) opt.zero_grad(set_to_none=True) stats = {'col': 0.0, 'tgt': 0.0, 'smooth': 0.0} # 展开长度随本批任务时长:覆盖本批 exec_s 的 p90 × 1.2(无老师参考题按 6 s),夹在 [plans, plans_max] exb = torch.nan_to_num(ex, nan=6.0) n_plans = int(min(C['plans_max'], max(C['plans'], math.ceil(1.2 * torch.quantile(exb, 0.9).item() / (3 * 0.02))))) nseg = math.ceil(n_plans / C['seg_plans']) seg_loss = torch.zeros((), device=dev); prev_d = torch.zeros(B, 6, device=dev) for p in range(n_plans): tnow = p * 3 * 0.02 r = queue.shape[1] pre = torch.zeros(B, 3, 6, device=dev); pre[:, :r] = queue[:, :r] qe = q + queue.sum(1) with torch.no_grad(): if (nsub > 0).any(): cur = subs[:, 0]; dd = torch.where(kin.cont, wrap180(cur - qe), cur - qe).abs().max(-1).values adv = (nsub > 0) & ((dd < 5) | (tnow - sub_t > 1.5)) subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, 1:]], 1), subs) nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t) feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=dev), subs[:, 0], (nsub > 0).float()) with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp): act, val, sub = model.sample_k(feat, 1, z) act = act[:, 0].float() if p == 0: with torch.no_grad(): need = sub[:, 12].float() > 0.5 subs = q.detach()[:, None] + sub[:, :12].float().view(B, 2, 6) * 180 nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub)) inc = act.clamp(-1, 1) * step # [B,16,6] commit = inc[:, r:r + 3] if r == 0: # 冷启动:先提交一块,下一轮才开始执行 queue = commit; continue # 执行队列中的 3 步(上一轮提交的) for k in range(3): d = queue[:, k] qp = q; q = kin.clamp_box(q + d) # 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致) G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M) 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) prev_d = d seg_loss = seg_loss + C['w_col'] * col.mean() + C['w_smooth'] * sm.mean() + C['w_len'] * d.abs().sum(-1).mean() stats['col'] += col.mean().item(); stats['smooth'] += sm.mean().item() queue = commit if (p + 1) % C['seg_plans'] == 0 or p == n_plans - 1: xf = kin.cum_xf(q) pe = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(B, 9) pose = (R.abs().sum(-1) > 0).float() lt = ((T - pe) / 100).pow(2).sum(-1) + C['w_rot'] * pose * (Rq - R).pow(2).sum(-1) seg_loss = seg_loss + C['w_tgt'] * lt.mean() stats['tgt'] += lt.mean().item() (seg_loss / nseg).backward() seg_loss = torch.zeros((), device=dev) q, qp, queue, prev_d = q.detach(), qp.detach(), queue.detach(), prev_d.detach() # BC 锚定 X, Y, V, SG = data.batch(C['batch'] * 4) with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp): mem = model.encode(X) l_bc = wta_loss(model, mem, Y)[0] if model.head == 'wta' else meanflow_loss(model, mem, Y)[0] v, s = model.heads(mem) l_bc = l_bc + 0.1 * (v.float() - V).abs().mean() + 0.1 * (s.float() - SG).abs().mean() (C['w_bc'] * l_bc).backward() gn = torch.nn.utils.clip_grad_norm_(model.parameters(), C['grad_clip']) opt.step() with torch.no_grad(): e, oe = goal_err(kin, q, T, R) rec = {'it': it, 'total': C['steps'], 't': time.time(), 'plans': n_plans, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(), 'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60} if it % 10 == 0: print(json.dumps(rec), flush=True) log.write(json.dumps(rec) + '\n'); log.flush() if saver.stop(): save_last(it + 1); log.close() print(f'收到停止请求:已存档 last.pt(第 {it + 1} 次),下次运行自动续跑', flush=True); sys.exit(143) if saver.due(): save_last(it + 1) if (it + 1) % C['eval_every'] == 0 or it == C['steps'] - 1: res, _ = evaluate(model.eval(), kin, val_tasks, dev) res['it'] = it + 1; res['t'] = time.time() print('EVAL', json.dumps(res), flush=True); log.write(json.dumps({'eval': res}) + '\n'); log.flush() sd = {'model': model.state_dict(), 'cfg': mcfg, 'step': it + 1, 'fp': kin.fp, 'eval': res} save_last(it + 1, res) # last 带优化器状态供续跑;best 不带(导出/门禁用) if res['success'] > best + 1e-4: best = res['success']; atomic_save(sd, os.path.join(a.out, 'best.pt')) print(f'done, best success {best:.4f}', flush=True) if __name__ == '__main__': main()