perf+exp: gm_groups 只算特征 8 组(数值一致,features 10.4→7.1ms);混合节奏负结论(执行反馈无提升);浏览器部署 dag2 ONNX + fbGain/commit 参数(bench 刹车 47% 待调);实时口径改为异步规划+tick

This commit is contained in:
cbb
2026-10-20 12:38:56 +08:00
parent 3b748c811c
commit 79519b042c
7 changed files with 65 additions and 7 deletions
+15
View File
@@ -384,6 +384,21 @@
- **结论**:**当前方案不满足 50 Hz 实时**(单次 40–80 ms,超预算 2–4 倍);42.2% 的成绩依赖每步规划(20 ms 节奏)。实时可行节奏下:每 3 步 → 36.3%(p50 有余量、p99 需继续优化),每 6 步 → 14.8%。**实时性是当前方案的硬约束,不是仿真耗时问题**。 - **结论**:**当前方案不满足 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%)。 - **下一步(需确认)**:① 浏览器 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) ## 分支清理(2026-10-20)
- `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。
+4 -2
View File
@@ -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(sd)
out.append(sub_flag[:, None].to(dt)) out.append(sub_flag[:, None].to(dt))
if gm is None: if gm is None:
gm = kin.gaps(q, GAP_CAP, xf)['gm'] out.append(kin.gm_groups(q, GP, GAP_CAP) / GAP_CAP) # 2026-10-20:只算特征用到的 8 组
out.append(torch.stack([gm[:, a * 7 + b].clamp(max=GAP_CAP) / GAP_CAP for a, b in GP], -1)) 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) f = torch.cat(out, -1)
assert f.shape[1] == F_DIM, f.shape assert f.shape[1] == F_DIM, f.shape
return f return f
+35
View File
@@ -202,6 +202,41 @@ class Kin:
if not pairs: return {'gm': gm.view(B, 49)} if not pairs: return {'gm': gm.view(B, 49)}
return {'gm': gm.view(B, 49), 'b': rep_b, 'i': pi, 'j': pj, 'gap': gap} 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): def min_gap(self, q, cap=40.0):
return self.gaps(q, cap)['gm'].min(-1).values return self.gaps(q, cap)['gm'].min(-1).values
+2 -1
View File
@@ -145,7 +145,8 @@ class TeacherOracle:
@torch.no_grad() @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, 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', 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):冷启动时用给定途经点代替子目标头; """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头;
qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放; qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放;
select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。 select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。
+2 -2
View File
@@ -14,7 +14,7 @@ const fs = require('fs'), path = require('path');
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const flag = k => argv.includes(k); const flag = k => argv.includes(k);
const opt = k => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; }; 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 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 MODEL = pos[0] || path.join(__dirname, '../webapp/models/traj_policy.onnx');
const SETS = (pos[1] || 'nc,route').split(',').concat(flag('--nc1000') ? ['nc1000'] : []); 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++) { for (let ci = 0; ci < cases.length; ci++) {
const c = cases[ci]; const c = cases[ci];
const R = mode === 'pose' ? c.R : null; 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); r.reset(c.q0); r.setGoal(c.T, R);
let prev = c.q0.slice(), cf = 0, tLock = null; let prev = c.q0.slice(), cf = 0, tLock = null;
for (let k = 0; k < 500 && !r.done; k++) { for (let k = 0; k < 500 && !r.done; k++) {
+3 -1
View File
@@ -55,7 +55,9 @@ async function boot() {
} }
if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); 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 }); 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); 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' })); 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' }));
+4 -1
View File
@@ -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 */ 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, 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, 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, /* 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(超出按刹停处理) 与 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 }); 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); 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++) { for (let k = 0; k < K; k++) {
const inc = this._cand(out.act, k, r); 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(); let steps = inc.slice(r, r + o.commit), q = qe.slice();
const adj = []; const adj = [];
for (const d of steps) { for (const d of steps) {