第五轮:锁/续跑/超时/自动分支/R4 探针与 encode-once/控制台加固;阶段内断点续跑(gen 按条、prep 按 shard、训练与 APG 定时原子存档+体面停止);分歧以实现方意见为准
This commit is contained in:
+11
-2
@@ -7,7 +7,10 @@
|
||||
ml/runs/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(≈ 60000 步 ≤ 3.5 h)
|
||||
ml/runs/m5_gate_*.json M5 头门禁
|
||||
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
|
||||
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。"""
|
||||
ml/runs/r4.json R4 浏览器推理 batch=8 P99 < 15 ms(r4 探针)
|
||||
webapp/models/*.meta.json 被评测的模型不能是 --random 探针
|
||||
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。
|
||||
2026-10-18 第五轮:加 R4、随机探针模型拒绝(N-5)、R8 出题不足额判不通过(N-4);R2 改为参考项(N-2:阈值与池大小耦合、未校准)。"""
|
||||
import argparse, glob, json, os, re
|
||||
import torch
|
||||
|
||||
@@ -52,6 +55,10 @@ def main():
|
||||
add('D 类停靠距离 ≤ 老师 + 5 mm 的比例', rate, rate >= 0.98, '≥ 0.98')
|
||||
|
||||
# 2) 部署形态
|
||||
mm = jload('webapp/models/traj_policy.meta.json')
|
||||
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(随机初始化同结构模型实测)')
|
||||
pb = jload('ml/runs/policy_bench.json')
|
||||
if pb is None:
|
||||
add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench')
|
||||
@@ -65,6 +72,8 @@ def main():
|
||||
plans = sum(v.get('plans', 0) for v in sets.values()); br = sum(v.get('brakes', 0) for v in sets.values())
|
||||
add('刹停发生率(刹停 / 推理)', br / plans if plans else None, (br / plans < 0.02) if plans else False, '< 0.02')
|
||||
r8 = sets.get('r8_adversarial')
|
||||
if r8 and r8.get('requested') and r8['n'] < r8['requested']:
|
||||
add('R8 出题数', f"{r8['n']}/{r8['requested']}", False, '出满要求题数(不足则恢复率分母缩水)')
|
||||
if r8:
|
||||
add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95')
|
||||
add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0')
|
||||
@@ -73,7 +82,7 @@ def main():
|
||||
|
||||
# 3) 数据 / 训练 / 后训练门禁
|
||||
r2 = jload('ml/data/r2.json')
|
||||
add('R2 近邻标签 std(步长比例)', r2['label_std_mean'] if r2 else 'missing', bool(r2 and r2['pass']), '< 0.10')
|
||||
add('R2 近邻标签 std(步长比例)', [r2.get('label_std_mean'), r2.get('label_std_norm')] if r2 else 'missing', None, '参考:< 0.10;按近邻距离归一后看 label_std_norm(见 check_r2)')
|
||||
sps = None
|
||||
for f in sorted(glob.glob('ml/runs/bench_*/log.jsonl')):
|
||||
recs = [json.loads(l) for l in open(f) if '"samples_per_s"' in l]
|
||||
|
||||
+18
-4
@@ -3,6 +3,10 @@
|
||||
做法:特征逐维标准化;随机取 Q 个查询样本,在 P 个样本池里找最近邻(排除同一任务,避免同轨迹相邻帧冒充"近邻");
|
||||
取距离最近的 5% 近邻对,比较前 3 步(实际提交的动作)标签:std ≈ RMS(ΔY)/√2,单位为每步最大步长 STEP 的比例。
|
||||
判据:label_std < 0.10(即 < 10% 步长)。起步失败率在闭环仿真结果的 start_fail 字段(acceptance 汇总)。
|
||||
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 而不报错。"""
|
||||
import argparse, json, math
|
||||
import torch
|
||||
@@ -31,16 +35,26 @@ 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, 'rule': 'label_std_mean < 0.10', 'note': '近邻对不足(样本太少),未判定'}
|
||||
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
|
||||
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 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)
|
||||
Ya, Yb = d.Y[qi][qf[half], :3].float(), d.Y[pi][jj[half], :3].float()
|
||||
sa = (Ya - Yb).pow(2).mean(-1).mean(-1).sqrt() / math.sqrt(2)
|
||||
A = torch.stack([torch.ones_like(dd[half]), dd[half]], 1)
|
||||
coef = torch.linalg.lstsq(A.cpu(), sa.cpu()[:, None]).solution.squeeze(1)
|
||||
std_norm = max(0.0, coef[0].item())
|
||||
thr = torch.quantile(best_d[fin], a.frac)
|
||||
m = fin & (best_d <= thr)
|
||||
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()), 'nn_dist_threshold': thr.item(), 'label_std_mean': std.mean().item(),
|
||||
'label_std_p90': torch.quantile(std, 0.9).item(), 'pass': std.mean().item() < 0.10,
|
||||
'rule': 'label_std_mean < 0.10(10% 步长)'}
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""方案 R4 自动探针:无头 Edge / Chrome 里测浏览器推理延迟(部署形态:1 行特征 + K=8 条候选)。
|
||||
python -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json
|
||||
做法:本进程起一个带 COOP/COEP 头的静态服务(根目录 = 仓库),逐个配置用无头浏览器打开 webapp/r4_probe.html,
|
||||
页面把 P50/P95/P99 与 20 ms 节拍最大间隔 POST 回来。配置:
|
||||
wasm·默认线程(与查看器同一默认 min(16, ⌊0.6×逻辑核⌋),推理在 Worker)——部署默认口径
|
||||
wasm·4 线程(第四轮实测的旧默认,作对照)
|
||||
wasm·默认线程·不开 Worker(看推理是否卡主线程:tickGapMax)
|
||||
webgpu
|
||||
判据:部署口径 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)给。"""
|
||||
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
|
||||
|
||||
|
||||
def find_browser():
|
||||
if os.environ.get('R4_BROWSER'): return os.environ['R4_BROWSER']
|
||||
cands = []
|
||||
for base in (os.environ.get('ProgramFiles(x86)'), os.environ.get('ProgramFiles'), os.environ.get('LOCALAPPDATA')):
|
||||
if base:
|
||||
cands += [os.path.join(base, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
||||
os.path.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe')]
|
||||
for n in ('microsoft-edge', 'microsoft-edge-stable', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'):
|
||||
p = shutil.which(n)
|
||||
if p: cands.append(p)
|
||||
return next((c for c in cands if c and os.path.exists(c)), None)
|
||||
|
||||
|
||||
class Srv:
|
||||
def __init__(self):
|
||||
self.result, self.ev = None, threading.Event()
|
||||
me = self
|
||||
|
||||
class H(http.server.SimpleHTTPRequestHandler):
|
||||
extensions_map = {**http.server.SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript'}
|
||||
|
||||
def __init__(s, *a, **k): super().__init__(*a, directory=ROOT, **k)
|
||||
|
||||
def end_headers(s):
|
||||
s.send_header('Cross-Origin-Opener-Policy', 'same-origin'); s.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
s.send_header('Cross-Origin-Resource-Policy', 'cross-origin'); s.send_header('Cache-Control', 'no-store')
|
||||
super().end_headers()
|
||||
|
||||
def do_POST(s):
|
||||
n = int(s.headers.get('Content-Length') or 0)
|
||||
try: me.result = json.loads(s.rfile.read(n) or b'{}')
|
||||
except Exception as e: me.result = {'error': 'bad json: %s' % e}
|
||||
me.ev.set(); s.send_response(200); s.end_headers(); s.wfile.write(b'ok')
|
||||
|
||||
def log_message(s, *a): pass
|
||||
|
||||
sk = socket.socket(); sk.bind(('127.0.0.1', 0)); self.port = sk.getsockname()[1]; sk.close()
|
||||
self.httpd = http.server.ThreadingHTTPServer(('127.0.0.1', self.port), H)
|
||||
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
||||
|
||||
|
||||
def kill_tree(p):
|
||||
if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(p.pid)], capture_output=True)
|
||||
else:
|
||||
try: p.terminate(); p.wait(5)
|
||||
except Exception: p.kill()
|
||||
|
||||
|
||||
def one(srv, browser, model_url, ep, threads, proxy, n, timeout_s):
|
||||
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]
|
||||
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 内页面没有回报'}
|
||||
r = dict(r or {}, config=f'{ep}·threads={threads}·proxy={proxy}')
|
||||
print(json.dumps(r, 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)
|
||||
a = ap.parse_args()
|
||||
out = {'rule': f'部署口径 batch=8 P99 < {LIMIT_MS:g} ms', 'model': a.model, 'pass': None}
|
||||
br = find_browser()
|
||||
if not br:
|
||||
out['note'] = '没找到 Edge / Chrome(可设 R4_BROWSER=浏览器路径)'; print(out['note'])
|
||||
else:
|
||||
out['browser'] = br
|
||||
srv = Srv()
|
||||
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
|
||||
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:
|
||||
out['note'] = '页面没有给出数字(见 runs[].error)'
|
||||
elif ok(dep):
|
||||
out['pass'] = True; out['deploy'] = dep; out['deploy_ep'] = 'wasm'
|
||||
elif ok(gpu):
|
||||
out['pass'] = True; out['deploy'] = gpu; out['deploy_ep'] = 'webgpu'
|
||||
out['note'] = 'wasm 不达标而 WebGPU 达标:查看器部署用 &ep=webgpu'
|
||||
else:
|
||||
out['pass'] = False; out['deploy'] = dep if dep.get('p99') is not None else gpu
|
||||
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)
|
||||
d = out.get('deploy') or {}
|
||||
print(f"R4:{ {True: '通过', False: '未通过', None: '未判定'}[out['pass']] } 部署口径 P99 {d.get('p99')} ms({d.get('config')})")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user