第六、七轮:离线 ort/WebGPU、R9 查看器整周期、安全层截止下沉、enc_once 前瞻 bug(feat 动态轴)、UTF-8、断点 RNG 续跑与 --stop-at、R2 池无关性自检、R8 出题缓存、本机自检 selftest、冒烟 smoke;遗留清单并入审查文档第六节
This commit is contained in:
+12
-2
@@ -8,9 +8,11 @@
|
||||
ml/runs/m5_gate_*.json M5 头门禁
|
||||
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
|
||||
ml/runs/r4.json R4 浏览器推理 batch=8 P99 < 15 ms(r4 探针)
|
||||
ml/runs/r9_browser.json R9 查看器整次规划(推理 + 前瞻 + 安全层)P99 < 45 ms、饥饿节拍 < 1%(训练好的模型,bench 阶段测)
|
||||
webapp/models/*.meta.json 被评测的模型不能是 --random 探针
|
||||
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。
|
||||
2026-10-18 第五轮:加 R4、随机探针模型拒绝(N-5)、R8 出题不足额判不通过(N-4);R2 改为参考项(N-2:阈值与池大小耦合、未校准)。"""
|
||||
2026-10-18 第五轮:加 R4、随机探针模型拒绝(N-5)、R8 出题不足额判不通过(N-4);R2 改为参考项(N-2:阈值与池大小耦合、未校准)。
|
||||
2026-10-18 第六轮:加 R9(审查 B-1:纯推理口径会放过整周期丢步的配置)与安全层两把尺对照(B-2:同一批题浏览器 / Node 差 > 3× 标红)。"""
|
||||
import argparse, glob, json, os, re
|
||||
import torch
|
||||
|
||||
@@ -59,6 +61,9 @@ def main():
|
||||
add('部署模型是训练产物(非随机探针)', None if mm is None else not mm.get('random_init'), mm is not None and not mm.get('random_init'), 'meta.random_init 不为真')
|
||||
r4 = jload('ml/runs/r4.json')
|
||||
add('R4 浏览器推理 batch=8 P99(ms)', (r4 or {}).get('deploy', {}).get('p99') if r4 else 'missing', bool(r4 and r4.get('pass')), '< 15(随机初始化同结构模型实测)')
|
||||
r9 = jload('ml/runs/r9_browser.json'); rr = (r9 or {}).get('result') or {}
|
||||
add('R9 查看器整次规划 P99(ms)/ 饥饿节拍占比', [rr.get('planP99'), rr.get('starve_rate')] if r9 else 'missing', bool(r9 and r9.get('pass')),
|
||||
'< 45 ms 且 < 1%(训练好的模型、30 道贴柱题,ml.eval.r4_probe --viewer)')
|
||||
pb = jload('ml/runs/policy_bench.json')
|
||||
if pb is None:
|
||||
add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench')
|
||||
@@ -78,7 +83,12 @@ def main():
|
||||
add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95')
|
||||
add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0')
|
||||
else: add('R8 对抗集', 'missing', False, 'bench 需含 r8')
|
||||
add('推理延迟 P99(Node CPU,参考)', max((v.get('inferMsP99') or 0) for v in sets.values()), None, '浏览器以 __POLICY.latency() 为准')
|
||||
add('推理延迟 P99(Node CPU,参考)', max((v.get('inferMsP99') or 0) for v in sets.values()), None, '浏览器以 r9_browser.json 为准')
|
||||
nc = sets.get('near_column_pos') or {}
|
||||
bs, ns = rr.get('safetyP99'), nc.get('safetyMsP99')
|
||||
ratio = (bs / ns) if (bs is not None and ns) else None
|
||||
add('安全层 P99 浏览器 / Node(同一批贴柱题)', [bs, ns, None if ratio is None else round(ratio, 2)], None if ratio is None else ratio <= 3,
|
||||
'≤ 3×;超了看 certQueriesP99 / certMemoHitRate / certFailWhy 定位')
|
||||
|
||||
# 3) 数据 / 训练 / 后训练门禁
|
||||
r2 = jload('ml/data/r2.json')
|
||||
|
||||
+31
-13
@@ -6,8 +6,8 @@
|
||||
2026-10-18 第五轮(审查 N-2:判据与搜索池耦合):最近 5% 的近邻距离随池大小变,label_std 随之系统偏大/偏小。
|
||||
改为按"近邻距离 → 标签差"做线性回归,取距离 → 0 的截距 label_std_norm 作判据(与池大小无关:池大只是近邻更近、点更多,
|
||||
截距估计的是"同一输入下的固有标签分歧");旧口径 label_std_mean 保留作参考。pipeline 默认只警告,R2_STRICT=1 才拦。
|
||||
自检:同一份数据 --pool 50000 与 --pool 200000 两跑,label_std_norm 应一致(差 < 0.02)。
|
||||
2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 峰值 < 0.5 GB,CPU 约 1–2 分钟;近邻对为空时输出 pass=null 而不报错。"""
|
||||
自检(第七轮起每次自动做):--pool 50000 与 4 倍池 200000 各测一次,label_std_norm 差 < 0.02 记 pool_check.independent=true。
|
||||
2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 峰值 < 0.5 GB,连同 4 倍池自检 CPU 约 5–10 分钟、GPU 约 1 分钟;近邻对为空时输出 pass=null 而不报错。"""
|
||||
import argparse, json, math
|
||||
import torch
|
||||
|
||||
@@ -18,13 +18,34 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/data/r2.json')
|
||||
ap.add_argument('--queries', type=int, default=5000); ap.add_argument('--pool', type=int, default=50000)
|
||||
ap.add_argument('--pool2', type=int, default=0, help='池无关性自检的大池(默认 4×pool)')
|
||||
ap.add_argument('--frac', type=float, default=0.05); ap.add_argument('--seed', type=int, default=0)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
a = ap.parse_args()
|
||||
d = Samples(a.samples, device=a.device, split='train')
|
||||
P2 = a.pool2 if a.pool2 else 4 * a.pool
|
||||
r1 = measure(d, a, a.pool)
|
||||
if r1.get('pairs', 0) == 0:
|
||||
out = r1
|
||||
else:
|
||||
# 2026-10-18 第七轮(遗留 C-3):池无关性自检并入每次运行——同一批查询、池放大 4 倍再测一次,
|
||||
# 截距差 < 0.02 才说明 label_std_norm 这把尺可信(independent=true);判定用大池的数(近邻更近、截距更稳)。
|
||||
r2 = measure(d, a, P2) if min(P2, d.n) > r1['pool'] else None
|
||||
out = dict(r2 or r1)
|
||||
out['pool_check'] = {'pool_small': r1['pool'], 'norm_small': r1['label_std_norm'],
|
||||
'pool_big': r2['pool'] if r2 else None, 'norm_big': r2['label_std_norm'] if r2 else None,
|
||||
'diff': abs(r2['label_std_norm'] - r1['label_std_norm']) if r2 else None,
|
||||
'independent': (abs(r2['label_std_norm'] - r1['label_std_norm']) < 0.02) if r2 else None,
|
||||
'note': '样本数不足 4 倍池时只测一次' if not r2 else '差 < 0.02 → 判据与池无关,R2_STRICT=1 可以放心打开'}
|
||||
print(json.dumps(out, indent=1))
|
||||
json.dump(out, open(a.out, 'w'), indent=1)
|
||||
|
||||
|
||||
def measure(d, a, pool):
|
||||
g = torch.Generator(device=a.device); g.manual_seed(a.seed)
|
||||
P = min(a.pool, d.n); Q = min(a.queries, d.n)
|
||||
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]; qi = torch.randperm(d.n, device=a.device, generator=g)[:Q]
|
||||
P = min(pool, d.n); Q = min(a.queries, d.n)
|
||||
qi = torch.randperm(d.n, device=a.device, generator=g)[:Q] # 查询集与池大小无关(先抽查询)
|
||||
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]
|
||||
X = d.X.float(); mu, sd = X[pi].mean(0), X[pi].std(0).clamp(min=1e-6)
|
||||
Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd
|
||||
tp, tq = d.TID[pi], d.TID[qi]
|
||||
@@ -35,8 +56,7 @@ def main():
|
||||
v, j = D.min(1); best_d[s:s + 256] = v; best_j[s:s + 256] = j
|
||||
fin = torch.isfinite(best_d)
|
||||
if fin.sum() < 20:
|
||||
out = {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'}
|
||||
print(json.dumps(out)); json.dump(out, open(a.out, 'w'), indent=1); return
|
||||
return {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'}
|
||||
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a
|
||||
dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1)
|
||||
half = dd <= torch.quantile(dd, 0.5)
|
||||
@@ -50,13 +70,11 @@ def main():
|
||||
Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float()
|
||||
rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均
|
||||
std = (rms / math.sqrt(2))
|
||||
out = {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(),
|
||||
'label_std_norm': std_norm, 'label_std_slope': coef[1].item(),
|
||||
'label_std_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(),
|
||||
'pass': std_norm < 0.10,
|
||||
'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'}
|
||||
print(json.dumps(out, indent=1))
|
||||
json.dump(out, open(a.out, 'w'), indent=1)
|
||||
return {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(),
|
||||
'label_std_norm': std_norm, 'label_std_slope': coef[1].item(),
|
||||
'label_std_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(),
|
||||
'pass': std_norm < 0.10,
|
||||
'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+61
-9
@@ -9,11 +9,24 @@
|
||||
判据:部署口径 P99 < 15 ms 即通过;wasm 不过而 WebGPU 过,也判通过并记 deploy_ep=webgpu(查看器用 &ep=webgpu)。
|
||||
找不到浏览器 / 页面没回报 → pass=null(流水线警告继续,验收标红)。浏览器路径可用 R4_BROWSER 指定。
|
||||
2026-10-18 第五轮新增(审查第四轮缺口 3:R4 需要人开浏览器看,导致 `all` 天生不完整)。
|
||||
范围说明:这里测的是纯推理;贴柱处安全层耗时(R9)由 bench(tests/policy_bench.cjs 的 safetyMsP99)给。"""
|
||||
范围说明:R4 判据只看纯推理(随机权重只能代表推理开销)。
|
||||
2026-10-18 第六轮(审查 B-1/B-2/B-3):
|
||||
--viewer:在真查看器里跑 autotest(与 Node bench 同一批 30 道贴立柱题),记录整次规划 planMs(推理 + 前瞻 + 安全层)、
|
||||
节拍间隔与饥饿(丢步)次数。r4 阶段用随机模型跑一次,只作参考(随机策略的安全层路径不代表真模型);
|
||||
bench 阶段用训练好的模型跑,写 ml/runs/r9_browser.json,判据 = 方案 R9:planMs P99 < 45 ms(60 ms 周期留 15 ms 余量)且饥饿节拍 < 1%。
|
||||
本地有 webapp/vendor/ort/(bash ml/pipeline.sh setup 会放好)就用本地 ort,不依赖 CDN(第六轮实测 jsdelivr 间歇不可达)。
|
||||
WebGPU 与查看器两种配置用有界面窗口打开(无头模式没有真 GPU 适配器,第六轮实测 backend not found),跑完自动关闭。"""
|
||||
import argparse, http.server, json, os, shutil, socket, subprocess, sys, tempfile, threading, time, urllib.parse
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
LIMIT_MS = 15.0
|
||||
PLAN_LIMIT_MS = 45.0 # R9:整次规划 P99(60 ms 周期留 15 ms 给渲染与 tick)
|
||||
STARVE_LIMIT = 0.01 # R9:饥饿节拍占比上限
|
||||
|
||||
|
||||
def ort_local():
|
||||
if os.environ.get('ORT_LOCAL') in ('0', '1'): return os.environ['ORT_LOCAL'] == '1'
|
||||
return os.path.exists(os.path.join(ROOT, 'webapp', 'vendor', 'ort', 'ort.min.js'))
|
||||
|
||||
|
||||
def find_browser():
|
||||
@@ -64,28 +77,47 @@ def kill_tree(p):
|
||||
except Exception: p.kill()
|
||||
|
||||
|
||||
def one(srv, browser, model_url, ep, threads, proxy, n, timeout_s):
|
||||
def open_page(srv, browser, url, timeout_s, headed=False):
|
||||
srv.result = None; srv.ev.clear()
|
||||
q = urllib.parse.urlencode({'model': model_url, 'ep': ep, 'threads': threads, 'proxy': proxy, 'n': n,
|
||||
**({'ortLocal': '1'} if os.environ.get('ORT_LOCAL') == '1' else {})})
|
||||
url = f'http://127.0.0.1:{srv.port}/webapp/r4_probe.html?{q}'
|
||||
prof = tempfile.mkdtemp(prefix='r4prof_')
|
||||
args = [browser, '--headless=new', f'--user-data-dir={prof}', '--no-first-run', '--no-default-browser-check',
|
||||
'--disable-extensions', '--enable-unsafe-webgpu', url]
|
||||
args = [browser] + ([] if headed else ['--headless=new']) + [f'--user-data-dir={prof}', '--no-first-run', '--no-default-browser-check',
|
||||
'--disable-extensions', '--enable-unsafe-webgpu', '--disable-background-timer-throttling', '--disable-renderer-backgrounding',
|
||||
'--window-size=1280,800', url]
|
||||
p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
got = srv.ev.wait(timeout_s)
|
||||
kill_tree(p); shutil.rmtree(prof, ignore_errors=True)
|
||||
r = srv.result if got else {'error': f'{timeout_s} s 内页面没有回报'}
|
||||
return srv.result if got else {'error': f'{timeout_s} s 内页面没有回报'}
|
||||
|
||||
|
||||
def one(srv, browser, model_url, ep, threads, proxy, n, timeout_s):
|
||||
q = urllib.parse.urlencode({'model': model_url, 'ep': ep, 'threads': threads, 'proxy': proxy, 'n': n, **({'ortLocal': '1'} if ort_local() else {})})
|
||||
r = open_page(srv, browser, f'http://127.0.0.1:{srv.port}/webapp/r4_probe.html?{q}', timeout_s, headed=(ep == 'webgpu'))
|
||||
r = dict(r or {}, config=f'{ep}·threads={threads}·proxy={proxy}')
|
||||
print(json.dumps(r, ensure_ascii=False), flush=True)
|
||||
return r
|
||||
|
||||
|
||||
def viewer(srv, browser, model_url, n, probe):
|
||||
"""真查看器 autotest:整次规划耗时 / 节拍 / 饥饿(R9 口径)"""
|
||||
q = urllib.parse.urlencode({'policy': model_url, 'autotest': n, **({'probe': '1'} if probe else {}), **({'ortLocal': '1'} if ort_local() else {})})
|
||||
r = open_page(srv, browser, f'http://127.0.0.1:{srv.port}/webapp/index.html?{q}', 120 + 12 * n, headed=True)
|
||||
r = dict(r or {})
|
||||
if r.get('planP99') is not None:
|
||||
sr = (r.get('starve') or 0) / max(1, r.get('ticks') or 1)
|
||||
r['starve_rate'] = round(sr, 4)
|
||||
r['r9_pass'] = r['planP99'] < PLAN_LIMIT_MS and sr < STARVE_LIMIT
|
||||
print('viewer', json.dumps({k: v for k, v in r.items() if k != 'per'}, ensure_ascii=False), flush=True)
|
||||
return r
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--model', default='ml/runs/r4/traj_policy_r4.onnx'); ap.add_argument('--out', default='ml/runs/r4.json')
|
||||
ap.add_argument('--n', type=int, default=150); ap.add_argument('--timeout', type=int, default=240)
|
||||
ap.add_argument('--viewer', action='store_true', help='只跑查看器 autotest(R9,bench 阶段用训练好的模型)')
|
||||
ap.add_argument('--viewer-n', type=int, default=30); ap.add_argument('--no-viewer-ref', action='store_true')
|
||||
a = ap.parse_args()
|
||||
if a.viewer: return main_viewer(a)
|
||||
out = {'rule': f'部署口径 batch=8 P99 < {LIMIT_MS:g} ms', 'model': a.model, 'pass': None}
|
||||
br = find_browser()
|
||||
if not br:
|
||||
@@ -96,8 +128,11 @@ def main():
|
||||
mu = '/' + os.path.relpath(os.path.abspath(a.model), ROOT).replace(os.sep, '/')
|
||||
runs = [one(srv, br, mu, 'wasm', 'auto', 1, a.n, a.timeout), one(srv, br, mu, 'wasm', '4', 1, a.n, a.timeout),
|
||||
one(srv, br, mu, 'wasm', 'auto', 0, a.n, a.timeout), one(srv, br, mu, 'webgpu', 'auto', 0, a.n, a.timeout)]
|
||||
srv.httpd.shutdown()
|
||||
out['runs'] = runs
|
||||
if not a.no_viewer_ref: # 随机模型在查看器里的整次规划耗时:仅参考,不参与 R4 判定
|
||||
out['viewer_ref'] = viewer(srv, br, mu, a.viewer_n, True)
|
||||
out['viewer_ref_note'] = '随机权重:安全层路径不代表真模型,R9 以 bench 阶段 r9_browser.json 为准'
|
||||
srv.httpd.shutdown()
|
||||
dep, gpu = runs[0], runs[3]
|
||||
ok = lambda r: r.get('p99') is not None and r['p99'] < LIMIT_MS
|
||||
if dep.get('p99') is None and gpu.get('p99') is None:
|
||||
@@ -115,5 +150,22 @@ def main():
|
||||
print(f"R4:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } 部署口径 P99 {d.get('p99')} ms({d.get('config')})")
|
||||
|
||||
|
||||
def main_viewer(a):
|
||||
meta = json.load(open(a.model.replace('.onnx', '.meta.json'), encoding='utf-8'))
|
||||
out = {'rule': f'R9:查看器整次规划 planMs P99 < {PLAN_LIMIT_MS:g} ms 且饥饿节拍 < {STARVE_LIMIT:.0%}', 'model': a.model, 'pass': None}
|
||||
br = find_browser()
|
||||
if not br: out['note'] = '没找到 Edge / Chrome(可设 R4_BROWSER=浏览器路径)'
|
||||
else:
|
||||
srv = Srv(); mu = '/' + os.path.relpath(os.path.abspath(a.model), ROOT).replace(os.sep, '/')
|
||||
r = viewer(srv, br, mu, a.viewer_n, bool(meta.get('random_init'))); srv.httpd.shutdown()
|
||||
out.update(browser=br, result=r)
|
||||
out['pass'] = r.get('r9_pass')
|
||||
if out['pass'] is None: out['note'] = r.get('error') or '页面没有给出整次规划耗时'
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
json.dump(out, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
||||
r = out.get('result') or {}
|
||||
print(f"R9:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } planMs P99 {r.get('planP99')} ms(推理 {r.get('inferP99')} / 安全层 {r.get('safetyP99')}),饥饿 {r.get('starve_rate')}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user