第五轮:锁/续跑/超时/自动分支/R4 探针与 encode-once/控制台加固;阶段内断点续跑(gen 按条、prep 按 shard、训练与 APG 定时原子存档+体面停止);分歧以实现方意见为准

This commit is contained in:
Hark
2026-10-18 06:40:25 +08:00
committed by cbb
parent 3675fbbd9b
commit f40a40c56b
36 changed files with 1199 additions and 285 deletions
+11 -2
View File
@@ -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
View File
@@ -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)
+119
View File
@@ -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()