diff --git a/docs/探索日志.md b/docs/探索日志.md index 4b2f5ae..f114404 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -384,6 +384,21 @@ - **结论**:**当前方案不满足 50 Hz 实时**(单次 40–80 ms,超预算 2–4 倍);42.2% 的成绩依赖每步规划(20 ms 节奏)。实时可行节奏下:每 3 步 → 36.3%(p50 有余量、p99 需继续优化),每 6 步 → 14.8%。**实时性是当前方案的硬约束,不是仿真耗时问题**。 - **下一步(需确认)**:① 浏览器 ONNX 路径实测(推理 P99 历史 10–12 ms;特征/安全层在 JS 端需 `tests/policy_bench.cjs` 实测);② 特征缓存 + 安全层粗筛,目标单次 P99 < 20 ms;③ 或接受「每 3 步规划」的 60 ms 周期(36.3%)。 +## 轮次 24:实时性优化与新方法(exp: `exp/dagger-deploy`) + +- **目标**:准确率更高 + 效率满足 50 Hz(20 ms 控制步)。 +- **① 混合节奏(规划每 3 步 + 执行时每步反馈)——负结论**:plan fb1.8 + exec fb1.8 → 36.3%(与纯规划反馈的 commit_n=3 完全相同);plan fb0 + exec fb1.8 → 仅 1.6%。执行时反馈不带来额外收益(规划时的反馈已是主要贡献),此捷径不成立。 +- **② 特征几何优化(有效)**:特征只用 8 组间隙(GP),但原实现算全部 49 组(67256 簇对)。新增 `Kin.gm_groups(q, GP, cap)`:按 7 连杆组对过滤簇对(簇索引 → `cl_g` → 组对 min/max),只算 8 组。 + - 数值验证:与 `gaps()['gm'][:, a*7+b]` 逐项一致(diff 0)。 + - 延迟:gm 计算 10.4→**7.1 ms**(p50),单次规划省约 3 ms。 +- **③ 浏览器部署**:导出 `webapp/models/traj_policy.onnx`(dag2 best,19.8 MB,对拍误差 3.1e-6,act_scale 1.0);`ik-policy.js` 加全程目标反馈(`fbGain`)+ `commit` 可配;`ik-policy-glue.js` 支持 `?fb=&commit=`(默认 fb=1.8、commit=3)。 + - JS bench(Node,10 题,fb1.8+commit1):到达 0、**刹车率 47.3%**(每 2 次规划刹停一次)、planP50 43.8 / P99 70.5 ms。JS 安全层(5 mm 护栏 + 距离认证)比 Python(0.5 mm gap)保守,反馈+每步规划被大量拒绝。 +- **④ 实时口径的关键认识**:部署形态是**异步规划**(tick 20 ms 只执行队列;`plan()` 后台跑),所以 50 Hz 的约束是「tick < 20 ms + 规划在队列耗尽前完成」,不是「单次规划 < 20 ms」。 + - JS 的 tick 只做队列执行 + 投影/碰撞检查(~1.5–3 ms)→ 50 Hz 可行; + - 规划 P99 70 ms → 需要 commit ≥ 4(80 ms 队列)才不饥饿;commit=3(60 ms)会偶发饥饿。 + - 准确率与规划频率的 trade-off:commit1 42.2%(不可 50Hz)、commit3 36.3%、commit6 14.8%。 +- **下一步**:① 在 commit=3/4 的实时节奏下找准确率提升(候选平均、dag3_fe 冻结编码器、更高 gain);② 把 gm_groups 同步到 JS(gapMatrix 只算 8 组);③ 降低 JS 安全层拒绝率(margin/认证预算调参)。 + ## 分支清理(2026-10-20) - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 diff --git a/ml/common/features.py b/ml/common/features.py index 4ce8734..25e58d8 100644 --- a/ml/common/features.py +++ b/ml/common/features.py @@ -52,8 +52,10 @@ def features(kin, q, q_prev, T, R, prefix, prefix_len, sub, sub_flag, gm=None, x out.append(sd) out.append(sub_flag[:, None].to(dt)) if gm is None: - gm = kin.gaps(q, GAP_CAP, xf)['gm'] - out.append(torch.stack([gm[:, a * 7 + b].clamp(max=GAP_CAP) / GAP_CAP for a, b in GP], -1)) + out.append(kin.gm_groups(q, GP, GAP_CAP) / GAP_CAP) # 2026-10-20:只算特征用到的 8 组 + gm = None + if gm is not None: + out.append(torch.stack([gm[:, a * 7 + b].clamp(max=GAP_CAP) / GAP_CAP for a, b in GP], -1)) f = torch.cat(out, -1) assert f.shape[1] == F_DIM, f.shape return f diff --git a/ml/common/kin_torch.py b/ml/common/kin_torch.py index ce911a7..37a28e0 100644 --- a/ml/common/kin_torch.py +++ b/ml/common/kin_torch.py @@ -202,6 +202,41 @@ class Kin: if not pairs: return {'gm': gm.view(B, 49)} return {'gm': gm.view(B, 49), 'b': rep_b, 'i': pi, 'j': pj, 'gap': gap} + def gm_groups(self, q, gp, cap=40.0): + """只算指定组对 gp=[(a,b),...] 的最小间隙 [B,len(gp)](特征用,避免 49 组全量)。 + 数值与 gaps(q,cap)['gm'][:, a*7+b] 一致;组对在碰撞粗对里不存在时返回 cap。""" + key = tuple(gp) + if getattr(self, '_gp_key', None) != key: + m = torch.full((49,), -1, dtype=torch.long, device=q.device) + for i, (a_, b_) in enumerate(gp): m[min(a_, b_) * 7 + max(a_, b_)] = i + ga, gb = self.cl_g[self.cp_a], self.cl_g[self.cp_b] + cg = torch.minimum(ga, gb) * 7 + torch.maximum(ga, gb) # 簇对 → 7 连杆组对 + sel = torch.isin(cg, torch.tensor([min(a, b) * 7 + max(a, b) for a, b in gp], device=q.device)) + self._gp = (m, sel, sel.nonzero(as_tuple=True)[0]); self._gp_key = key + self._gp49, sel, idx = self._gp + xf = self.cum_xf(q); Rs, Ts = xf; B = q.shape[0] + with torch.no_grad(): + cc = torch.einsum('bkij,kj->bki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g] + d2 = (cc[:, self.cp_a[idx]] - cc[:, self.cp_b[idx]]).pow(2).sum(-1) + keep = d2 <= (self.cp_lim[idx] + cap).pow(2) + bb, mm = keep.nonzero(as_tuple=True) + del keep, d2, cc + cnt = self.cp_cnt[idx][mm] + rep_b = torch.repeat_interleave(bb, cnt) + off = torch.repeat_interleave(self.cp_start[idx][mm] - torch.cumsum(cnt, 0) + cnt, cnt) + torch.arange(int(cnt.sum()), device=q.device) + pidx = off + a, b = self.caps_world(q, xf) + with torch.no_grad(): + pi, pj = self.pi[pidx], self.pj[pidx] + mid = 0.5 * (a[rep_b, pi] + b[rep_b, pi] - a[rep_b, pj] - b[rep_b, pj]) + k2 = mid.pow(2).sum(-1) <= (self.pair_bl[pidx] + cap).pow(2) + rep_b, pidx, pi, pj = rep_b[k2], pidx[k2], pi[k2], pj[k2] + d = seg_seg_dist(a[rep_b, pi], b[rep_b, pi], a[rep_b, pj], b[rep_b, pj]) + gap = d - self.pair_need[pidx].to(q.dtype) + gm = torch.full((B * len(gp),), float(cap), dtype=q.dtype, device=q.device) + gm = gm.scatter_reduce(0, rep_b * len(gp) + self._gp49[self.pair_gp[pidx]], gap.clamp(max=cap), reduce='amin', include_self=True) + return gm.view(B, len(gp)) + def min_gap(self, q, cap=40.0): return self.gaps(q, cap)['gm'].min(-1).values diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index 4828bdb..721b870 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -145,7 +145,8 @@ class TeacherOracle: @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, 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_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): """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 条)。 diff --git a/tests/policy_bench.cjs b/tests/policy_bench.cjs index 10612dc..4aebc73 100644 --- a/tests/policy_bench.cjs +++ b/tests/policy_bench.cjs @@ -14,7 +14,7 @@ const fs = require('fs'), path = require('path'); const argv = process.argv.slice(2); const flag = k => argv.includes(k); const opt = k => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; }; -const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n']); +const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n', '--fb', '--commit']); const pos = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VALOPT.has(argv[i - 1]))); const MODEL = pos[0] || path.join(__dirname, '../webapp/models/traj_policy.onnx'); const SETS = (pos[1] || 'nc,route').split(',').concat(flag('--nc1000') ? ['nc1000'] : []); @@ -71,7 +71,7 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x, for (let ci = 0; ci < cases.length; ci++) { const c = cases[ci]; const R = mode === 'pose' ? c.R : null; - const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1 }); // 2026-10-19:动作尺度随模型 + const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1, fbGain: +opt('--fb') || 0, commit: Math.max(1, +opt('--commit') || 3) }); // 2026-10-19:动作尺度随模型 r.reset(c.q0); r.setGoal(c.T, R); let prev = c.q0.slice(), cf = 0, tLock = null; for (let k = 0; k < 500 && !r.done; k++) { diff --git a/webapp/js/ik-policy-glue.js b/webapp/js/ik-policy-glue.js index d4ebb66..c46815a 100644 --- a/webapp/js/ik-policy-glue.js +++ b/webapp/js/ik-policy-glue.js @@ -55,7 +55,9 @@ async function boot() { } if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn }); - runner = new ArmPolicy.Runner(S, infer, { actScale: meta.act_scale || 1 }); // 2026-10-19:动作尺度随模型(旧模型无此字段 = 1) + const FB = qs.get('fb') === null ? 1.8 : +qs.get('fb'); // 2026-10-20:全程目标反馈(fb=0 关闭) + const COMMIT = qs.get('commit') === null ? 3 : Math.max(1, +qs.get('commit')); // 部署节奏:commit=1 每步规划 + runner = new ArmPolicy.Runner(S, infer, { actScale: meta.act_scale || 1, fbGain: FB, commit: COMMIT }); // 2026-10-19:动作尺度随模型(旧模型无此字段 = 1) /* 预热:首次推理有编译开销 */ await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8); const tick = URL.createObjectURL(new Blob(['let h=null;onmessage=e=>{clearInterval(h);if(e.data>0)h=setInterval(()=>postMessage(0),e.data)}'], { type: 'text/javascript' })); diff --git a/webapp/js/ik-policy.js b/webapp/js/ik-policy.js index af6bcd0..880efb1 100644 --- a/webapp/js/ik-policy.js +++ b/webapp/js/ik-policy.js @@ -179,7 +179,7 @@ class Runner { this.o = Object.assign({ K: 8, commit: 3, margin: 5, /* 2026-10-18:运行时投影余量保持 5 mm(真机护栏,不迁就数据分布);数据侧另行过滤认证间隙 < 3 mm 的老师轨迹,APG 训练余量 3 mm */ projMax: 0.5, refineMm: 5, arriveMm: 1, arriveDeg: 0.5, stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, subRule: 'cross', subLeadSec: 0.02, seed: 12345, lookahead: true, - actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45 }, opts || {}); + actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45, fbGain: 0 }, opts || {}); /* 2026-10-19:actScale = 模型 meta.act_scale(动作标签按 act_scale·STEP 归一化,旧模型 1);增量 = clamp(act, ±1)·actScale·STEP, 与 ml/sim/closed_loop.py 同口径。subRule:'cross'(默认,越过顶点即切,与训练标签同口径;见 _subAdvance)/ 'timeout'(旧:< subReachDeg 或 subTimeout 超时)。 */ // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理) this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 }); @@ -307,8 +307,11 @@ class Runner { } /* 候选:去掉前缀后的提交步,投影,打分 */ const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64); + // 2026-10-20:全程目标反馈(与 ml/sim/closed_loop.py 的 fb_gain 一致)——一次 DLS,加到所有候选所有步 + const fbStep = (o.fbGain > 0 && this.goal) ? refineStep(S, qe, this.goal.T, this.goal.R, 0.3).dq : null; for (let k = 0; k < K; k++) { const inc = this._cand(out.act, k, r); + if (fbStep) for (let s = 0; s < H; s++) for (let j = 0; j < 6; j++) inc[s][j] += o.fbGain * fbStep[j]; let steps = inc.slice(r, r + o.commit), q = qe.slice(); const adj = []; for (const d of steps) {