Files

134 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
* ik-policy-glue.js —— 查看器接入 Transformer 策略(URL 加 ?policy=1 启用)
* ?policy=1 用 models/traj_policy.onnx
* ?policy=models/xxx.onnx 指定模型
* &ep=webgpu 改用 WebGPU 后端(默认 wasm-SIMD 多线程)
* &threads=N wasm 线程数(默认 min(16, ⌊0.6×逻辑核⌋);2026-10-18 R4 实测 28 核机 16 最优、24 反而变慢、4 线程 P99 37 ms)
* &proxy=0 关闭推理代理 Worker(默认开:wasm 推理不占主线程,见下)
* &probe=1 允许加载 --random 导出的延迟探针模型(默认拒绝,防把随机策略当真模型跑)
* &autotest=N 自动跑 N 道贴立柱题(与 tests/policy_bench.cjs 同种子同题),结果 POST 到 &report=(默认 /r4/result);
* 由 python -m ml.eval.r4_probe --viewer 驱动,测部署形态的整次规划耗时与节拍(方案 R9)
* 2026-10-18 第五轮:ort.env.wasm.proxy=true 把 wasm 推理挪到 ort 自带的 Worker。此前推理在主线程同步算,
* R4 实测时 CDP 15 s 拿不到执行槽,50 Hz tick 也被它拖住——丢步不只来自推理时长本身。
* 目标变化(松手求解 / 实时追踪)都交给 ArmPolicy.Runner:50 Hz 执行时钟放在 Web Worker(不受 rAF 60 Hz 与后台节流影响),
* 每 3 步推理一次,安全层认证后才执行;ik-glue 的逐帧碰撞守卫不参与(策略执行不走 animTo),由 Runner 安全层保证。
* onnxruntime-web 默认从 jsDelivr 加载;离线部署把 ort.min.js 与 *.wasm 放到 vendor/ort/ 并加 &ortLocal=1。
* 多线程 wasm 需要页面跨源隔离(COOP/COEP 头,见 ml/README.md);没有时自动退回单线程。
* ==========================================================================*/
(function () {
'use strict';
const qs = new URLSearchParams(location.search);
if (!qs.has('policy')) return;
const MODEL = qs.get('policy') && qs.get('policy') !== '1' ? qs.get('policy') : 'models/traj_policy.onnx';
/* 2026-10-18 第六轮(审查 A-3):本地路径必须是绝对 URL——ORT 用 import() 加载 .mjs,相对前缀 'vendor/ort/' 会被当成裸模块名而失败 */
const ORT_BASE = qs.get('ortLocal') === '1' ? new URL('vendor/ort/', location.href).href : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/';
/* WebGPU 后端只在 ort.webgpu.min.js 里(ort.min.js 只有 wasm,第六轮实测 "[webgpu] backend not found" 即此因) */
const ORT_JS = qs.get('ep') === 'webgpu' ? 'ort.webgpu.min.js' : 'ort.min.js';
const badge = () => document.getElementById('ikBadge');
const say = (t, cls) => { const b = badge(); if (b) { b.textContent = t; b.className = 'badge ' + (cls || 'wait'); } console.log('[policy]', t); };
let ready = false, runner = null, running = false, worker = null;
window.__POLICY = { active: () => ready, setGoal: () => false, stats: () => runner && runner.stats };
function loadScript(src) { return new Promise((res, rej) => { const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = () => rej(new Error('load ' + src)); document.head.appendChild(s); }); }
async function waitIK() { for (let i = 0; i < 200 && !(window.IK && window.IK.solver && window.ArmPolicy); i++) await new Promise(r => setTimeout(r, 50)); if (!window.IK) throw new Error('IK 未就绪'); }
async function boot() {
try {
await waitIK();
const S = window.IK.solver;
const meta = await (await fetch(MODEL.replace('.onnx', '.meta.json'))).json();
if (meta.fp !== S.modelFingerprint()) throw new Error('模型碰撞指纹 ' + meta.fp + ' ≠ 当前 ' + S.modelFingerprint() + ':collide.json 已变,须重训');
if (meta.feat_ver !== ArmPolicy.FEAT_VER) throw new Error('特征版本不符:模型 ' + meta.feat_ver + ' / 运行时 ' + ArmPolicy.FEAT_VER);
if (meta.tcp0 && meta.tcp0.some((v, i) => Math.abs(v - S.TCP0[i]) > 0.01)) throw new Error('TCP 与训练时不同:' + S.TCP0 + ' vs ' + meta.tcp0);
const probe = !!meta.random_init;
if (probe && qs.get('probe') !== '1') throw new Error('这是随机初始化的延迟探针模型(meta.random_init),不是训练产物;仅测延迟时加 &probe=1');
if (!window.ort) await loadScript(ORT_BASE + ORT_JS);
ort.env.wasm.wasmPaths = ORT_BASE;
const hc = navigator.hardwareConcurrency || 1;
ort.env.wasm.numThreads = !self.crossOriginIsolated ? 1 : (+qs.get('threads') || Math.max(1, Math.min(16, Math.floor(hc * 0.6))));
const ep = qs.get('ep') === 'webgpu' ? ['webgpu', 'wasm'] : ['wasm'];
let sess = null, proxy = ep[0] === 'wasm' && qs.get('proxy') !== '0'; // proxy 只支持 wasm EP
if (proxy) {
ort.env.wasm.proxy = true;
try { sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); }
catch (e) { console.warn('[policy] 代理 Worker 不可用,退回主线程推理:', e); proxy = false; ort.env.wasm.proxy = false; sess = null; }
}
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, {});
/* 预热:首次推理有编译开销 */
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' }));
worker = new Worker(tick);
worker.onmessage = onTick;
window.__POLICY = {
active: () => ready,
setGoal: (T, R, tool) => {
if (tool) { say('策略模式暂不支持工具偏移 ≠ 0,已交回规划器', 'badb'); return false; }
if (!running || !runner.q) runner.reset(window.IK.curQ());
runner.setGoal(T, R); start(); return true;
},
stop, stats: () => runner.stats, runner: () => runner,
latency: () => { const p = (a, x) => { const s = a.slice().sort((u, v) => u - v); return s.length ? s[Math.min(s.length - 1, Math.floor(s.length * x))] : null; };
const st = runner.stats;
return { inferP50: p(st.inferMs, .5), inferP99: p(st.inferMs, .99), safetyP50: p(st.safetyMs, .5), safetyP99: p(st.safetyMs, .99),
lookaheadP99: p(st.lookaheadMs, .99), planP50: p(st.planMs, .5), planP99: p(st.planMs, .99), planWarmP99: p(st.planWarmMs, .99), planColdP99: p(st.planColdMs, .99), coldPlans: st.planColdMs.length, certQP99: p(st.certQ, .99),
tickGapP99: p(TICKS.gaps, .99), tickGapMax: TICKS.gaps.length ? Math.max(...TICKS.gaps) : null, starve: TICKS.starve, ticks: TICKS.n,
threads: ort.env.wasm.numThreads, ep: ep[0], proxy, encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn, probe }; }
};
ready = true;
const how = ep[0] + ',' + ort.env.wasm.numThreads + ' 线程' + (proxy ? ',推理在 Worker' : ',推理在主线程');
if (probe) say('随机探针模型(仅测延迟,动作无意义):' + how, 'badb');
else say('策略模式就绪(' + how + '):拖动目标即由策略执行', 'ok');
if (qs.get('autotest')) autotest(+qs.get('autotest') || 30, meta);
} catch (e) { say('策略模式不可用:' + e.message, 'badb'); console.error(e); }
}
/* 节拍统计(R9):tick 间隔、饥饿次数(执行时钟到了但队列为空、又没到达 = 实际丢步) */
const TICKS = { gaps: [], last: 0, starve: 0, n: 0 };
/* autotest:同一批贴立柱题(PolicyCases.ncCases,与 Node bench 同种子 20261017)逐题跑,每题限时 10 s,结果 POST 回驱动方 */
async function autotest(N, meta) {
const S = window.IK.solver;
if (!window.PolicyCases) await loadScript(new URL('js/policy-cases.js', location.href).href);
const cases = PolicyCases.ncCases(S, N, 20261017), per = [];
const t0 = performance.now();
for (let i = 0; i < cases.length; i++) {
const c = cases[i]; stop(); await new Promise(r => setTimeout(r, 50));
window.IK.setJoints(c.q0, false); runner.reset(c.q0); runner.setGoal(c.T, null);
const s0 = { plans: runner.stats.plans, brakes: runner.stats.brakes }, ts = performance.now(); let col = 0;
TICKS.last = 0; start();
while (running && performance.now() - ts < 10000) {
await new Promise(r => setTimeout(r, 100));
if (runner.q && !S.collideFree(runner.q)) col++;
}
stop();
const { e } = runner.err(runner.q);
per.push({ i, reached: runner.done && e < 1, err: +e.toFixed(1), colSamples: col, plans: runner.stats.plans - s0.plans, brakes: runner.stats.brakes - s0.brakes, s: +((performance.now() - ts) / 1000).toFixed(2) });
}
const lat = window.__POLICY.latency();
const res = { kind: 'viewer_autotest', model: MODEL, probe: !!meta.random_init, n: cases.length, reached: per.filter(x => x.reached).length,
collisionTasks: per.filter(x => x.colSamples).length, brakes: runner.stats.brakes, plans: runner.stats.plans, certMemoHits: runner.stats.certHit,
certFailWhy: runner.stats.certWhy, wallS: +((performance.now() - t0) / 1000).toFixed(1), coi: self.crossOriginIsolated, ua: navigator.userAgent, ...lat, per };
say('autotest 完成:到达 ' + res.reached + '/' + res.n + ',整次规划 P99 ' + res.planP99 + ' ms', 'ok');
try { await fetch(qs.get('report') || '/r4/result', { method: 'POST', body: JSON.stringify(res) }); } catch (e) { console.warn(e); }
window.__POLICY.autotest = res;
}
function start() { if (running) return; running = true; worker.postMessage(ArmPolicy.DT * 1000); }
function stop() { running = false; if (worker) worker.postMessage(0); }
function onTick() {
if (!running) return;
const tn = performance.now(); if (TICKS.last) TICKS.gaps.push(tn - TICKS.last); TICKS.last = tn; TICKS.n++;
if (TICKS.gaps.length > 20000) TICKS.gaps.splice(0, 10000);
/* 第八轮:冷启动(静止起步、首次规划含前瞻)时队列本就为空、机械臂没在动,不算丢步;只统计运动中断粮 */
if (!runner.queue.length && !runner.done && runner.goal && !runner.cold) TICKS.starve++;
if (runner.needPlan()) runner.plan().catch(e => { console.error(e); stop(); say('策略推理出错:' + e.message, 'badb'); });
const q = runner.tick();
if (q) window.IK.setJoints(q, false);
if (runner.done && !runner.queue.length) {
stop();
const { e, oe } = runner.err(runner.q);
say('策略到达 · 误差 ' + e.toFixed(2) + ' mm' + (runner.goal.R ? ' / ' + oe.toFixed(2) + '°' : '') + ' · 刹停 ' + runner.stats.brakes + ' 次', 'ok');
} else if (runner.brakeRun >= 2) say('策略:安全层连续拦截,减速重规划中…', 'wait');
}
boot();
})();