第六、七轮:离线 ort/WebGPU、R9 查看器整周期、安全层截止下沉、enc_once 前瞻 bug(feat 动态轴)、UTF-8、断点 RNG 续跑与 --stop-at、R2 池无关性自检、R8 出题缓存、本机自检 selftest、冒烟 smoke;遗留清单并入审查文档第六节
This commit is contained in:
+14
-6
@@ -29,7 +29,7 @@
|
||||
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 未实测:展开长度 60–150 次推理随批变化,以监控页前 50 步换算的 ETA 为准 | `ml/runs/apg/best.pt` |
|
||||
| 9 挖错循环 | `bash ml/pipeline.sh mine` | 未实测:每轮 = 5 万题作答 + APG,以第 1 轮实际耗时 × 轮数为准 | `ml/runs/current.pt` |
|
||||
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) |
|
||||
| 11 部署评测 | `bash ml/pipeline.sh bench` | 估算约 1 小时(Node CPU,1120 个任务;R8 出题约 2 秒/题) | `ml/runs/policy_bench.json` |
|
||||
| 11 部署评测 | `bash ml/pipeline.sh bench` | 估算约 1 小时(Node CPU,1120 个任务;R8 出题耗时未实测)+ 查看器 R9 约 5 分钟 | `ml/runs/policy_bench.json`、`ml/runs/r9_browser.json` |
|
||||
| 12 验收汇总 | `bash ml/pipeline.sh accept` | 10–20 分钟 | `ml/runs/acceptance.json` |
|
||||
| 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
|
||||
|
||||
@@ -61,6 +61,8 @@ bash ml/pipeline.sh kin
|
||||
检查:最后打印的 JSON 里 `"PASS": true`(fk、姿态、组对间隙、83 维特征、间隙梯度与 JS 一致)。
|
||||
`docs/collide.json` 以后只要重新拟合或重生成 `pair2d`/`roadmap`,这一步、数据、模型都要重来(指纹不符时各脚本会拒绝运行)。
|
||||
|
||||
**真跑之前(第七轮新增,5–10 分钟)**:`bash ml/pipeline.sh selftest`(进程生命周期与断点续跑自检 → `ml/runs/selftest.json`,不碰本仓库数据)。通过后正式流程按「数据 → 训练 → 后训练与评测」三段跑,中途出错从该阶段 `FROM=` 续跑。`bash ml/pipeline.sh smoke`(副本里极小模型全流程冒烟,40–70 分钟)备用,不必跑。待办与已定做法见 `docs/ML运行手册审查.md` 第六节。
|
||||
|
||||
**R4 延迟(训练前做,自动)**:推理延迟只取决于模型结构,与权重无关,所以用随机初始化模型先测,不过就在训练前改结构,不必等训完。
|
||||
|
||||
```bash
|
||||
@@ -71,7 +73,10 @@ bash ml/pipeline.sh r4 # 随机初始化模型导出到 ml/runs/r4/traj_pol
|
||||
探针测 4 种配置:默认线程 + Worker(部署口径)、4 线程(对照旧默认)、默认线程不开 Worker(看 `tickGapMax`:推理是否卡住 20 ms 节拍)、WebGPU。
|
||||
**通过线:部署口径 batch=8 P99 < 15 ms**;wasm 不过而 WebGPU 过也算通过,`r4.json` 记 `deploy_ep: webgpu`,查看器用 `&ep=webgpu`。
|
||||
不通过时流程停在这里(返回码 5):改用 `CFG=ml/configs/small.json`(d=128、ff=512,约 1.8 M 参数)重跑。确认要继续设 `R4_STRICT=0`。
|
||||
找不到浏览器时 `pass` 为 null、流程继续、验收标红;可用 `R4_BROWSER=浏览器路径` 指定。离线时设 `ORT_LOCAL=1` 并把 onnxruntime-web 的 dist 文件放到 `webapp/vendor/ort/`。
|
||||
找不到浏览器时 `pass` 为 null、流程继续、验收标红;可用 `R4_BROWSER=浏览器路径` 指定。
|
||||
onnxruntime-web 默认用本地副本 `webapp/vendor/ort/`(`setup` 与 `r4` 会自动执行 `bash ml/prep/vendor_ort.sh` 放好,约 33 MB、不入库),不依赖 CDN;`ORT_LOCAL=0` 强制走 CDN。
|
||||
WebGPU 配置和查看器参考测试会弹出一个有界面的浏览器窗口(无头模式拿不到真 GPU),跑完自动关闭,期间别动它。
|
||||
`r4.json` 还带一项 `viewer_ref`:随机模型在真查看器里跑 30 道贴柱题的整次规划耗时,**只作参考**(随机策略几乎每次都要认证多条候选,安全层路径不代表真模型),R9 以第 11 步为准。
|
||||
手动看:`python ml/serve.py` 后打开 `http://localhost:8000/?policy=../ml/runs/r4/traj_policy_r4.onnx&probe=1`(随机模型默认会被查看器拒绝,`&probe=1` 才放行,徽标标红「随机探针模型」)。
|
||||
|
||||
## 3. 老师一致性门禁
|
||||
@@ -191,7 +196,9 @@ bash ml/pipeline.sh bench
|
||||
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、R8 对抗 50 例(J1 起终差 ≥ 140°、终点贴立柱、两侧都要绕远)、一次性贴立柱扩展集 1000 例。各集合数量可用 `--nc-n` / `--route-n` / `--r8-n` 单独调。
|
||||
|
||||
看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 为 0;`near_column_*` 与 `route40` 全部 `ok`;`near_column_1000` 0 失败;`brakeRate` < 0.02;`r8_adversarial` 的 `recoveryRate` ≥ 0.95、`livelock` = 0。
|
||||
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 13 步实测。
|
||||
同一步还会在查看器里跑**同一批** 30 道贴柱题(与 Node 同种子),写 `ml/runs/r9_browser.json`(会弹出浏览器窗口几分钟):
|
||||
- **R9 通过线:整次规划 `planP99` < 45 ms、饥饿节拍 `starve_rate` < 1%**(整次规划 = 推理 + 前瞻 + 安全层,60 ms 周期留 15 ms 给渲染与执行时钟;饥饿 = 执行时钟到点时队列空了,即实际丢步)。
|
||||
- 两边对照:`policy_bench.json` 的 `near_column_pos.safetyMsP99` 与浏览器 `safetyP99` 差 > 3× 时验收标红,用 `certQueriesP99`(每次规划新增距离查询)、`certMemoHitRate`、`certFailWhy`(`cert` / `budget` / `time` / `box`)定位。
|
||||
|
||||
## 12. 验收汇总
|
||||
|
||||
@@ -210,10 +217,11 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
|
||||
|
||||
浏览器打开 `http://localhost:8000/?policy=1`:右上角徽标显示「策略模式就绪」后,拖动黄球或松手,手臂由策略闭环执行(不调用任何规划器)。
|
||||
- `&ep=webgpu` 改用 WebGPU 后端对比延迟
|
||||
- 控制台 `__POLICY.latency()` 查看推理与安全层的 P50/P99;`__POLICY.stats()` 查看刹停、换向、精修次数
|
||||
- 控制台 `__POLICY.latency()` 查看推理、前瞻、安全层、整次规划的 P50/P99 与节拍;`__POLICY.stats()` 查看刹停、换向、精修次数
|
||||
- `&autotest=30` 自动跑与 bench 同一批贴柱题,结果在 `__POLICY.autotest`
|
||||
- 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守)
|
||||
- 工具偏移 ≠ 0 时自动交回原规划器
|
||||
- 离线环境:把 onnxruntime-web 的 `ort.min.js` 与 wasm 文件放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`
|
||||
- 离线环境:`bash ml/prep/vendor_ort.sh` 把 onnxruntime-web 放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`(WebGPU 同样可用)
|
||||
|
||||
## 14. 回传给我
|
||||
|
||||
@@ -224,7 +232,7 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
|
||||
| 6 | `ml/runs/bench_base/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
|
||||
| 7 | `ml/runs/m5_gate_*.json` |
|
||||
| 8–9 | `ml/runs/apg/log.jsonl`、`ml/runs/mine/r*/report.json` 与 `gate.json` |
|
||||
| 11–13 | `ml/runs/policy_bench.json`、`ml/runs/acceptance.json` |
|
||||
| 11–13 | `ml/runs/policy_bench.json`、`ml/runs/r9_browser.json`、`ml/runs/acceptance.json` |
|
||||
| 任一步出错 | `ml/runs/pipeline.json`、`ml/runs/console.log`(控制台启动时)或终端输出 |
|
||||
|
||||
## 训练控制台
|
||||
|
||||
@@ -16,7 +16,7 @@ from ml.data import Samples, load_tasks
|
||||
from ml.model.policy import build, wta_loss
|
||||
from ml.model.meanflow import meanflow_loss
|
||||
from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err
|
||||
from ml.common.ckpt import Saver, atomic_save
|
||||
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
|
||||
|
||||
|
||||
def load_pool(paths):
|
||||
@@ -33,7 +33,7 @@ def main():
|
||||
ap.add_argument('--pool', nargs='*', default=[]); ap.add_argument('--out', default='ml/runs/apg')
|
||||
ap.add_argument('--config', default='ml/configs/apg.json'); ap.add_argument('--steps', type=int, default=None)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
|
||||
ap.add_argument('--resume', default=None, help='从 <out>/last.pt 续跑(2026-10-18 第五轮,审查 N-11:几小时的 APG 断了不再从头来)')
|
||||
ap.add_argument('--stop-at', type=int, default=0, help='自检用:跑到第 N 次按停止请求处理'); ap.add_argument('--resume', default=None, help='从 <out>/last.pt 续跑(2026-10-18 第五轮,审查 N-11:几小时的 APG 断了不再从头来)')
|
||||
a = ap.parse_args()
|
||||
cfg = json.load(open(a.config)); C = cfg['apg']
|
||||
if a.steps: C['steps'] = a.steps
|
||||
@@ -56,6 +56,7 @@ def main():
|
||||
rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model'])
|
||||
if 'opt' in rk: opt.load_state_dict(rk['opt'])
|
||||
it0 = rk.get('step', 0)
|
||||
if set_rng_state(rk.get('rng')): print('续跑:随机数状态已恢复', flush=True)
|
||||
bp = os.path.join(a.out, 'best.pt')
|
||||
best = torch.load(bp, map_location='cpu').get('eval', {}).get('success', -1.0) if os.path.exists(bp) else -1.0
|
||||
log = open(os.path.join(a.out, 'log.jsonl'), 'a')
|
||||
@@ -69,7 +70,7 @@ def main():
|
||||
M = C['margin_train_mm']; t0 = time.time()
|
||||
saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存)
|
||||
def save_last(n, ev=None):
|
||||
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev}, os.path.join(a.out, 'last.pt')); saver.mark()
|
||||
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev, 'rng': rng_state()}, os.path.join(a.out, 'last.pt')); saver.mark()
|
||||
for it in range(it0, C['steps']):
|
||||
model.train()
|
||||
batch = random.choices(train_tasks, weights=w, k=C['batch'])
|
||||
@@ -149,7 +150,7 @@ def main():
|
||||
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60}
|
||||
if it % 10 == 0: print(json.dumps(rec), flush=True)
|
||||
log.write(json.dumps(rec) + '\n'); log.flush()
|
||||
if saver.stop():
|
||||
if saver.stop() or (a.stop_at and it + 1 >= a.stop_at):
|
||||
save_last(it + 1); log.close()
|
||||
print(f'收到停止请求:已存档 last.pt(第 {it + 1} 次),下次运行自动续跑', flush=True); sys.exit(143)
|
||||
if saver.due(): save_last(it + 1)
|
||||
|
||||
+19
-1
@@ -6,7 +6,7 @@
|
||||
训练循环据此先存 last.pt 再以 143 退出。Windows 上 taskkill /F 是硬杀、收不到信号,
|
||||
所以控制台「停止」先写 STOP 文件、等进程自己存档退出,超时才硬杀(见 monitor.Job.stop)。
|
||||
"""
|
||||
import os, signal, time
|
||||
import os, random, signal, time
|
||||
import torch
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
@@ -42,3 +42,21 @@ class Saver:
|
||||
return False
|
||||
|
||||
def mark(self): self.last = time.time()
|
||||
|
||||
|
||||
def rng_state():
|
||||
"""2026-10-18 第七轮(遗留:续跑后数值轨迹与不断点不一致):随机数状态一起存档。
|
||||
训练取 batch 用 torch.randint(全局生成器,CUDA 上取 CUDA 生成器)、噪声用 torch.randn、APG 选题用 random.choices,
|
||||
三者状态都恢复后,续跑的 batch 顺序与噪声与一口气跑完一致(同一设备、同一版本;cuDNN 非确定算子另计)。"""
|
||||
d = {'torch': torch.get_rng_state(), 'py': random.getstate()}
|
||||
if torch.cuda.is_available(): d['cuda'] = torch.cuda.get_rng_state_all()
|
||||
return d # 不存 numpy 状态:训练不用 np.random,且 ndarray 过不了 torch.load 的 weights_only 默认
|
||||
|
||||
|
||||
def set_rng_state(d):
|
||||
if not d: return False
|
||||
torch.set_rng_state(d['torch'].cpu() if hasattr(d['torch'], 'cpu') else d['torch']); random.setstate(d['py'])
|
||||
if 'cuda' in d and torch.cuda.is_available():
|
||||
try: torch.cuda.set_rng_state_all([t.cpu() for t in d['cuda']])
|
||||
except Exception: pass
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"model": {
|
||||
"d": 32,
|
||||
"heads": 4,
|
||||
"ff": 64,
|
||||
"enc": 1,
|
||||
"dec": 1
|
||||
},
|
||||
"train": {
|
||||
"batch": 64,
|
||||
"lr": 0.001,
|
||||
"warmup": 10,
|
||||
"steps": 300,
|
||||
"wd": 0.01,
|
||||
"betas": [
|
||||
0.9,
|
||||
0.95
|
||||
],
|
||||
"amp": "bf16",
|
||||
"grad_clip": 1.0,
|
||||
"w_mf": 1.0,
|
||||
"w_fk": 0.5,
|
||||
"w_smooth": 0.05,
|
||||
"w_value": 0.1,
|
||||
"w_sub": 0.1,
|
||||
"p_neq": 0.25,
|
||||
"fk_caps": 96,
|
||||
"ckpt_min": 1,
|
||||
"eval_every": 100,
|
||||
"eval_tasks": 16,
|
||||
"log_every": 10,
|
||||
"patience_evals": 5
|
||||
},
|
||||
"_note": "冒烟专用(bash ml/smoke.sh):只验证流程能跑通,数值无意义"
|
||||
}
|
||||
@@ -7,6 +7,8 @@ rem 关闭本窗口只关控制台;已开始的训练继续跑,重新双击
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0\.."
|
||||
if "%PYTHON%"=="" set "PYTHON=python"
|
||||
rem 2026-10-18 第六轮:Python 按 UTF-8 读写文件(Windows 默认 GBK 读中文 json 会报错)
|
||||
set "PYTHONUTF8=1"
|
||||
"%PYTHON%" -c "import torch" >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo 当前 Python(%PYTHON%)不能 import torch。
|
||||
|
||||
@@ -8,4 +8,5 @@ for c in "${PYTHON:-}" python python3; do
|
||||
done
|
||||
[ -n "${PY:-}" ] || { echo "找不到能 import torch 的 Python:先激活训练环境,或设 PYTHON=..."; exit 1; }
|
||||
export PYTHON=$PY
|
||||
export PYTHONUTF8=1 # 2026-10-18 第六轮:文件读写统一 UTF-8
|
||||
exec "$PY" -m ml.monitor --open "$@"
|
||||
|
||||
+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()
|
||||
|
||||
+11
-3
@@ -1,8 +1,10 @@
|
||||
"""导出部署包(方案 §8.4):
|
||||
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx [--fp16]
|
||||
输出 traj_policy.onnx(输入 feat[1,83]、z[K,16,6];输出 act[K,16,6] 归一化增量、value[K]、sub[K,13])
|
||||
输出 traj_policy.onnx(输入 feat[1 或 K,83]、z[K,16,6];输出 act[K,16,6] 归一化增量、value[K]、sub[K,13])
|
||||
与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once)。导出后用 onnxruntime 与 PyTorch 对拍。
|
||||
2026-10-18 第五轮:改为"编码一次、解码 K 次"(Policy.sample_shared),meta.enc_once=true;运行时据此只送 1 行特征。
|
||||
2026-10-18 第六轮:feat 轴改为动态(1 行 = 主推理编码一次;K 行 = 前瞻逐行编码),meta.feat_dyn=true。
|
||||
第五轮的 enc_once 模型 feat 固定 1 行,运行时前瞻只能逐行推(ik-policy.js ortInfer 已兼容),重新导出即可。
|
||||
--random 导出的是延迟探针(meta.random_init=true):查看器 / bench / 验收都会拒绝把它当策略用。"""
|
||||
import argparse, json, os
|
||||
import numpy as np
|
||||
@@ -38,7 +40,7 @@ def main():
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
feat, z = torch.randn(1, F_DIM), torch.randn(8, H, 6)
|
||||
torch.onnx.export(Wrap(m), (feat, z), a.out, input_names=['feat', 'z'], output_names=['act', 'value', 'sub'],
|
||||
dynamic_axes={'z': {0: 'K'}, 'act': {0: 'K'}, 'value': {0: 'K'}, 'sub': {0: 'K'}},
|
||||
dynamic_axes={'feat': {0: 'B'}, 'z': {0: 'K'}, 'act': {0: 'K'}, 'value': {0: 'K'}, 'sub': {0: 'K'}},
|
||||
opset_version=a.opset, dynamo=False)
|
||||
if a.fp16:
|
||||
from onnxconverter_common import float16
|
||||
@@ -54,8 +56,14 @@ def main():
|
||||
err8 = max(float((t[i] - t8[i]).abs().max()) for i in range(3))
|
||||
assert err8 < 1e-4, f'sample_shared 与 sample 不一致:{err8}'
|
||||
err = max(float(np.abs(o[i] - t[i].numpy()).max()) for i in range(3))
|
||||
f8 = torch.randn(8, F_DIM) # 前瞻口径:8 行不同特征,须与 sample() 逐行一致
|
||||
o8 = s.run(None, {'feat': f8.numpy(), 'z': z.numpy()})
|
||||
with torch.no_grad(): r8 = m.sample(f8, z)
|
||||
err_rows = max(float(np.abs(o8[i] - r8[i].numpy()).max()) for i in range(3))
|
||||
assert err_rows < 1e-3, f'feat 为 K 行时与 sample 不一致:{err_rows}'
|
||||
err = max(err, err_rows)
|
||||
meta = {'fp': kin.fp, 'tcp0': kin.tcp0.tolist(), 'head': m.head, 'feat_ver': FEAT_VER, 'f_dim': F_DIM, 'h': H, 'step': STEP, 'fp16': a.fp16,
|
||||
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err}
|
||||
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'feat_dyn': True, 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err}
|
||||
json.dump(meta, open(a.out.replace('.onnx', '.meta.json'), 'w'), indent=1)
|
||||
print(json.dumps({'onnx': a.out, 'MB': os.path.getsize(a.out) / 1e6, 'max_abs_err_vs_torch': err}))
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ cd "$(dirname "$0")/../.."
|
||||
PY=${PYTHON:-python}
|
||||
ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000}
|
||||
mkdir -p ml/runs/mine
|
||||
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
|
||||
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate ${MINE_GATE_N:-2000} --seed 777 --out ml/runs
|
||||
[ -f ml/runs/current.pt ] || cp ml/runs/apg/best.pt ml/runs/current.pt
|
||||
SAMPLES="ml/data/samples"; FIXED=""; small=0
|
||||
for ((r=1; r<=ROUNDS; r++)); do
|
||||
|
||||
+4
-2
@@ -129,12 +129,14 @@ class Policy(nn.Module):
|
||||
def sample_shared(self, feat1, z):
|
||||
"""部署接口(ONNX,2026-10-18 第五轮,R4 实测后改):一次观测、K 条候选。
|
||||
feat1 [1,F],z [K,16,6] → act [K,16,6],value [K],sub [K,13](value/sub 对 K 行相同,展开只为保持运行时读法)。
|
||||
此前部署把同一观测复制 K=8 行喂 sample(),encoder 被重复算 8 次(约占总计算 29%);这里只编码一次再展开给解码器。"""
|
||||
此前部署把同一观测复制 K=8 行喂 sample(),encoder 被重复算 8 次(约占总计算 29%);这里只编码一次再展开给解码器。
|
||||
2026-10-18 第六轮:feat 行数也可以等于 K(前瞻:8 个候选末态各一行特征),此时逐行编码,结果与 sample(feat, z) 一致;
|
||||
expand 对 1→K 与 K→K 都成立,所以 ONNX 里 feat 轴是动态的(meta.feat_dyn)。"""
|
||||
mem = self.encode(feat1)
|
||||
v, s = self.heads(mem)
|
||||
K = z.shape[0]
|
||||
if self.head == 'wta':
|
||||
act, _ = self.wta_modes(mem)
|
||||
act, _ = self.wta_modes(mem[:1])
|
||||
idx = torch.arange(K, device=z.device) % self.K
|
||||
return act[0, idx] + 0.0 * z, v.expand(K), s.expand(K, -1)
|
||||
m = mem.expand(K, -1, -1)
|
||||
|
||||
+1
-1
@@ -298,7 +298,7 @@ class Job:
|
||||
if not ck.get('ok'): return {'error': '环境自检未通过:' + ';'.join(ck['problems'])}
|
||||
if frm not in STAGES or to not in STAGES or STAGES.index(frm) > STAGES.index(to): return {'error': '起止阶段无效'}
|
||||
bash = find_bash()
|
||||
e = dict(os.environ, FROM=frm, TO=to, MONITOR='0', PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8', PIPE_OWNER='网页')
|
||||
e = dict(os.environ, FROM=frm, TO=to, MONITOR='0', PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PIPE_OWNER='网页')
|
||||
py = train_python()
|
||||
if ' ' not in py: e['PYTHON'] = py.replace('\\', '/')
|
||||
for k, v in (env or {}).items():
|
||||
|
||||
+21
-10
@@ -11,9 +11,11 @@
|
||||
# apg APG 首轮微调(从选定模型开始)
|
||||
# mine 挖错循环(3–6 轮,含门禁回滚)
|
||||
# export 导出 ONNX(fp32)到 webapp/models/
|
||||
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)
|
||||
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)+ 查看器里同一批贴柱题的整次规划耗时(R9 → ml/runs/r9_browser.json)
|
||||
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
|
||||
# all 一键无人值守 kin → accept:已完成阶段自动跳过(断了再跑一次即续跑)、阶段超时、门槛自动分支,并在后台打开训练监控
|
||||
# selftest 本机自检约 5 分钟(Windows 进程生命周期 + 断点续跑),写 ml/runs/selftest.json(bash ml/selftest.sh)
|
||||
# smoke 全流程冒烟约 40–70 分钟:在仓库旁的副本里用极小模型把 kin → accept 跑通,并出 2000 条任务的数据统计,写 ml/runs/smoke.json(bash ml/smoke.sh)
|
||||
# monitor 训练控制台网页 http://localhost:8100:点「开始训练」一键跑全流程、看进度、可停止(也可双击 ml/console.bat / ml/console.sh)
|
||||
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
|
||||
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
|
||||
@@ -26,6 +28,9 @@
|
||||
# r4 自动探针(独立文件,不碰 webapp/models)、启动前自检 torch。
|
||||
set -e -o pipefail # pipefail:`node ... | tee` 里 node 失败也算失败
|
||||
cd "$(dirname "$0")/.."
|
||||
# 2026-10-18 第六轮:所有 Python 子进程按 UTF-8 读写文件(Windows 默认 GBK,读 ensure_ascii=False 写出的中文 json 会 UnicodeDecodeError,
|
||||
# 第六轮本机实测 :110 即此因;逐处补 encoding 容易漏,统一用 UTF-8 模式兜住)
|
||||
export PYTHONUTF8=1 PYTHONIOENCODING=utf-8
|
||||
PY=${PYTHON:-python}
|
||||
command -v "$PY" >/dev/null 2>&1 && "$PY" -c "import sys" 2>/dev/null || { echo "找不到可用的 Python($PY),请设 PYTHON=..."; exit 1; }
|
||||
export PYTHON=$PY
|
||||
@@ -74,7 +79,7 @@ trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返
|
||||
train_one() { # $1 = 配置文件
|
||||
local cfg=$1 tag; tag=$(basename "$1" .json)
|
||||
if [ -f ml/runs/bench_$tag/last.pt ]; then echo "吞吐摸底 ml/runs/bench_$tag 已有,跳过"
|
||||
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps 2000 --no-eval --config $cfg; fi
|
||||
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps ${BENCH_STEPS:-2000} --no-eval --config $cfg; fi
|
||||
local res=""; [ -f ml/runs/$tag/last.pt ] && { res="--resume ml/runs/$tag/last.pt"; echo "发现 ml/runs/$tag/last.pt → 续训"; }
|
||||
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$tag --config $cfg $res
|
||||
}
|
||||
@@ -82,8 +87,8 @@ m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过
|
||||
local tag=$1 ck=${CKPT:-ml/runs/$1/best.pt} o
|
||||
[ -f "$ck" ] || { echo "M5:$ck 不存在(训练发散或没训完)→ 按不通过处理"; return 3; }
|
||||
# 断点续跑:结果文件比检查点新 → 这一半已经评过,跳过
|
||||
o=ml/runs/m5_${tag}_policy.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --out $o
|
||||
o=ml/runs/m5_${tag}_safety.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --safety --out $o
|
||||
o=ml/runs/m5_${tag}_policy.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${M5_N:-1000} --out $o
|
||||
o=ml/runs/m5_${tag}_safety.json; [ -f $o ] && [ $o -nt "$ck" ] && echo "M5:$o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${M5_N:-1000} --safety --out $o
|
||||
$PY - "$tag" "$ck" <<'PY'
|
||||
import json, sys
|
||||
tag, ck = sys.argv[1], sys.argv[2]
|
||||
@@ -101,12 +106,13 @@ sys.exit(0 if ok else 3)
|
||||
PY
|
||||
}
|
||||
case $stage in
|
||||
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
|
||||
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund); run bash ml/prep/vendor_ort.sh ;;
|
||||
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;;
|
||||
r4) # 浏览器推理延迟(方案 R4):随机初始化模型导出到独立文件(不碰 webapp/models,审查 N-5),无头浏览器自动测
|
||||
mkdir -p ml/runs/r4
|
||||
bash ml/prep/vendor_ort.sh || echo "本地 ort 没放好(npm 不通?),探针改用 CDN"
|
||||
run $PY -m ml.export_onnx --random $CFG --out ml/runs/r4/traj_policy_r4.onnx
|
||||
run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json
|
||||
run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json ${R4_ARGS:-}
|
||||
r=$($PY -c "import json;p=json.load(open('ml/runs/r4.json', encoding='utf-8')).get('pass');print({True:'pass',False:'fail'}.get(p,'null'))")
|
||||
if [ "$r" = fail ] && [ "${R4_STRICT:-1}" = 1 ]; then
|
||||
echo "R4 不通过(见 ml/runs/r4.json):部署延迟超标,先别花几十小时训练这个规模的模型。按方案改 CFG=ml/configs/small.json(d=128)重跑;确认要继续设 R4_STRICT=0"; exit 5
|
||||
@@ -139,14 +145,14 @@ case $stage in
|
||||
|| { echo "MIN_GAP_MM=$MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep"; exit 8; }
|
||||
# 已打包完的 shard 跳过(断点续跑,见 run_prep.sh);参数变了由 pstate reset 把旧 samples 移到 ml/archive/
|
||||
run bash ml/prep/run_prep.sh $NPROC
|
||||
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json
|
||||
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ${R2_ARGS:-}
|
||||
# 第五轮(审查 N-2):R2 判据与搜索池大小有关、阈值未经实测校准 → 默认只警告继续,R2_STRICT=1 才拦
|
||||
if ! $PY -c "import json,sys;sys.exit(4 if json.load(open('ml/data/r2.json', encoding='utf-8')).get('pass') is False else 0)"; then
|
||||
if [ "${R2_STRICT:-0}" = 1 ]; then echo "R2 不通过(R2_STRICT=1):停在这里,把 ml/data/r2.json 发我"; exit 4; fi
|
||||
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
|
||||
fi ;;
|
||||
train) train_one $CFG ;;
|
||||
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
|
||||
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate ${M5_N:-1000} --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
|
||||
if m5_one $TAG; then :
|
||||
elif [ "$TAG" != wta ] && [ "${AUTO_WTA:-1}" = 1 ] && [ -z "${CKPT:-}" ]; then
|
||||
# 第五轮(缺口 3):base 头不过 → 流程内自动训 WTA 头再门禁一次,两种头都不过才停
|
||||
@@ -154,14 +160,19 @@ case $stage in
|
||||
$PY -m ml.pstate set m5 branch=wta
|
||||
train_one ml/configs/wta.json
|
||||
m5_one wta || { echo "M5:两种头都未通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; }
|
||||
elif [ "${M5_STRICT:-1}" = 0 ]; then echo "M5:不通过,M5_STRICT=0(冒烟)→ 照常继续"; echo ml/runs/$TAG/best.pt > $SEL
|
||||
else echo "M5:不通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; fi ;;
|
||||
apg) ck=$(ckpt); res=""; [ -f ml/runs/apg/last.pt ] && { res="--resume ml/runs/apg/last.pt"; echo "发现 ml/runs/apg/last.pt → 续跑"; }
|
||||
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
|
||||
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg ${APG_FIRST_STEPS:+--steps $APG_FIRST_STEPS} $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
|
||||
mine) run bash ml/mining/loop.sh $ROUNDS $APG_STEPS $MINE_N ;;
|
||||
monitor) run $PY -m ml.monitor ${MONITOR_ARGS:-} ;;
|
||||
selftest) run bash ml/selftest.sh ;;
|
||||
smoke) run bash ml/smoke.sh ;;
|
||||
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
|
||||
run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;;
|
||||
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 --nc1000 --out ml/runs/policy_bench.json ;;
|
||||
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 ${BENCH_ARGS:---nc1000} --out ml/runs/policy_bench.json
|
||||
# R9(第六轮,审查 B-1/B-2):查看器里跑同一批 30 道贴柱题,整次规划 P99 < 45 ms、饥饿节拍 < 1%;结果由 accept 判定,这里不拦
|
||||
run $PY -m ml.eval.r4_probe --viewer --model webapp/models/traj_policy.onnx --out ml/runs/r9_browser.json --viewer-n ${VIEWER_N:-30} ;;
|
||||
accept) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
|
||||
run $PY -m ml.eval.acceptance --ckpt $ck --samples ml/data/samples --out ml/runs/acceptance.json ;;
|
||||
all) # 默认 kin → accept;已完成(rc=0)的阶段自动跳过,从第一个没完成的继续。FRESH=1 从头来;FROM/TO 可只跑一段
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# 把 onnxruntime-web 1.20.1 放到 webapp/vendor/ort/(离线部署 &ortLocal=1、R4/R9 探针不依赖 CDN)。
|
||||
# 2026-10-18 第六轮(审查 A-3 / B-3):第六轮实测 jsdelivr 间歇不可达;WebGPU 需要 ort.webgpu.min.js 与 jsep 版 wasm。
|
||||
# 已存在且版本对就跳过。用法:bash ml/prep/vendor_ort.sh
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
V=1.20.1; D=webapp/vendor/ort
|
||||
if [ -f $D/VERSION ] && [ "$(cat $D/VERSION)" = $V ]; then echo "[ort] $D 已是 $V,跳过"; exit 0; fi
|
||||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||
(cd "$T" && npm pack onnxruntime-web@$V --silent >/dev/null && tar xzf onnxruntime-web-$V.tgz)
|
||||
mkdir -p $D
|
||||
for f in ort.min.js ort.webgpu.min.js ort-wasm-simd-threaded.wasm ort-wasm-simd-threaded.mjs ort-wasm-simd-threaded.jsep.wasm ort-wasm-simd-threaded.jsep.mjs; do
|
||||
cp "$T/package/dist/$f" $D/
|
||||
done
|
||||
echo $V > $D/VERSION
|
||||
echo "[ort] 已放到 $D($(du -sh $D | cut -f1))"
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# 本机自检(2026-10-18 第七轮,遗留 C-5 / C-6:Windows 生命周期与断点续跑只在沙箱验证过)。约 5–10 分钟。
|
||||
# 在仓库旁的副本(默认 ../<仓库名>_selftest)里做,不碰本仓库数据。结果写本仓库 ml/runs/selftest.json,每项 pass=true/false:
|
||||
# gen_resume 生成 6 条一口气 vs 先生成 3 条、把最后一行截掉一半、再续到 6 条 → 两份文件逐字节相同
|
||||
# prep_rerun 打包两次,第二次「待打包 0 个」
|
||||
# train_stop 训练 --stop-at 20 → rc 143、last.pt 第 20 步;续训到 40 步 rc 0
|
||||
# train_stopfile 训练中途写 ml/runs/STOP → 60 s 内 rc 143 且 last.pt 已存(控制台「停止」走的就是这条路)
|
||||
# train_term Git Bash 的 timeout 送 TERM 给训练 → 是否来得及存 last.pt(Windows 原生 python 未必收得到,结果决定控制台是否只能靠 STOP 文件)
|
||||
# apg_resume APG --stop-at 5 → rc 143;--resume 续跑 rc 0
|
||||
# tree_killed / survive_parent_exit 控制台的进程树停止、关窗口后子树存活(ml/selftest_proc.py)
|
||||
set -o pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
SRC=$(pwd); PY=${PYTHON:-python}; export PYTHONUTF8=1
|
||||
D=${SELFTEST_DIR:-$(dirname "$SRC")/$(basename "$SRC")_selftest}
|
||||
rm -rf "$D"; mkdir -p "$D"
|
||||
git ls-files -z | tar --null -T - -cf - | (cd "$D" && tar xf -)
|
||||
[ -d ml/node_modules ] && cp -r ml/node_modules "$D/ml/node_modules"
|
||||
cd "$D"; mkdir -p ml/runs ml/data
|
||||
R=ml/runs/selftest_items.jsonl; : > $R
|
||||
rec() { $PY -c "import json,sys;print(json.dumps({'item':sys.argv[1],'pass':json.loads(sys.argv[2]),'detail':sys.argv[3]},ensure_ascii=False))" "$1" "$2" "$3" >> $R; echo "[selftest] $1 → $2 $3"; }
|
||||
CFGS=ml/configs/smoke.json
|
||||
# 1 生成续写
|
||||
node ml/gen/gen_dataset.cjs --n 6 --seed 11 --out ml/data/a.jsonl 2>/dev/null
|
||||
node ml/gen/gen_dataset.cjs --n 3 --seed 11 --out ml/data/shard_0011.jsonl.part 2>/dev/null
|
||||
sz=$(wc -c < ml/data/shard_0011.jsonl.part); head -c $((sz - 40)) ml/data/shard_0011.jsonl.part > t && mv t ml/data/shard_0011.jsonl.part
|
||||
node ml/gen/gen_dataset.cjs --n 6 --seed 11 --out ml/data/shard_0011.jsonl.part 2>ml/runs/st_gen.log; mv ml/data/shard_0011.jsonl.part ml/data/shard_0011.jsonl
|
||||
cmp -s ml/data/a.jsonl ml/data/shard_0011.jsonl && rec gen_resume true "$(grep -o '续写[^,]*' ml/runs/st_gen.log | head -1)" || rec gen_resume false "续写结果与一口气生成不同"
|
||||
rm -f ml/data/a.jsonl
|
||||
node ml/gen/gen_dataset.cjs --n 40 --seed 12 --out ml/data/shard_0012.jsonl 2>/dev/null # 训练自检用的小数据
|
||||
# 2 打包二次运行
|
||||
bash ml/prep/run_prep.sh 4 >/dev/null 2>&1; bash ml/prep/run_prep.sh 4 > ml/runs/st_prep.log 2>&1
|
||||
grep -q "待打包 0 个" ml/runs/st_prep.log && rec prep_rerun true "" || rec prep_rerun false "$(head -c 200 ml/runs/st_prep.log)"
|
||||
T="--samples ml/data/samples --no-eval --config $CFGS --steps 40"
|
||||
step_of() { $PY -c "import torch;print(torch.load('$1',map_location='cpu',weights_only=False)['step'])" 2>/dev/null; }
|
||||
# 3 --stop-at
|
||||
$PY -m ml.train $T --out ml/runs/st_a --stop-at 20 > ml/runs/st_a.log 2>&1; rc=$?
|
||||
s=$(step_of ml/runs/st_a/last.pt)
|
||||
$PY -m ml.train $T --out ml/runs/st_a --resume ml/runs/st_a/last.pt >> ml/runs/st_a.log 2>&1; rc2=$?
|
||||
[ $rc = 143 ] && [ "$s" = 20 ] && [ $rc2 = 0 ] && rec train_stop true "rc 143 / 第 $s 步 / 续训 rc 0" || rec train_stop false "rc=$rc step=$s 续训rc=$rc2"
|
||||
# 4 STOP 文件
|
||||
rm -f ml/runs/STOP
|
||||
$PY -m ml.train --samples ml/data/samples --no-eval --config $CFGS --steps 1000000 --out ml/runs/st_b > ml/runs/st_b.log 2>&1 & pid=$!
|
||||
sleep 25; touch ml/runs/STOP; t0=$(date +%s)
|
||||
wait $pid; rc=$?; dt=$(( $(date +%s) - t0 )); rm -f ml/runs/STOP
|
||||
[ $rc = 143 ] && [ -f ml/runs/st_b/last.pt ] && rec train_stopfile true "写 STOP 后 ${dt}s 退出,第 $(step_of ml/runs/st_b/last.pt) 步" || rec train_stopfile false "rc=$rc ${dt}s"
|
||||
# 5 timeout TERM
|
||||
if command -v timeout >/dev/null; then
|
||||
timeout -k 30 25 $PY -m ml.train --samples ml/data/samples --no-eval --config $CFGS --steps 1000000 --out ml/runs/st_c > ml/runs/st_c.log 2>&1; rc=$?
|
||||
grep -q "收到停止请求" ml/runs/st_c.log && rec train_term true "rc=$rc,TERM 送达并存档" || rec train_term false "rc=$rc,TERM 没送到 python(Windows 原生进程常见)→ 阶段超时只能硬杀,进度靠每 10 分钟存档"
|
||||
else rec train_term false "没有 timeout 命令"; fi
|
||||
# 6 APG 续跑
|
||||
$PY -m ml.apg.apg_finetune --init ml/runs/st_a/last.pt --samples ml/data/samples --out ml/runs/st_apg --steps 10 --stop-at 5 > ml/runs/st_apg.log 2>&1; rc=$?
|
||||
$PY -m ml.apg.apg_finetune --init ml/runs/st_a/last.pt --samples ml/data/samples --out ml/runs/st_apg --steps 10 --resume ml/runs/st_apg/last.pt >> ml/runs/st_apg.log 2>&1; rc2=$?
|
||||
[ $rc = 143 ] && [ $rc2 = 0 ] && rec apg_resume true "" || rec apg_resume false "rc=$rc 续跑rc=$rc2(日志 $D/ml/runs/st_apg.log)"
|
||||
# 7 进程树 / 关窗口
|
||||
pj=$($PY ml/selftest_proc.py 2>ml/runs/st_proc.err | tail -1)
|
||||
echo "$pj" > ml/runs/st_proc.json
|
||||
k=$($PY -c "import json,sys;d=json.loads(sys.argv[1]);print(str(d.get('tree_killed')).lower(), str(d.get('survive_parent_exit')).lower())" "$pj" 2>/dev/null || echo "null null")
|
||||
rec tree_killed ${k% *} "$pj" ; rec survive_parent_exit ${k#* } ""
|
||||
mkdir -p "$SRC/ml/runs"
|
||||
$PY - "$R" "$SRC/ml/runs/selftest.json" "$D" <<'PY'
|
||||
import json, sys
|
||||
items = []
|
||||
for l in open(sys.argv[1], encoding='utf-8'):
|
||||
try: items.append(json.loads(l))
|
||||
except Exception: items.append({'item': 'parse', 'pass': False, 'detail': l.strip()[:200]})
|
||||
out = {'all_pass': all(i['pass'] is True for i in items), 'items': items, 'dir': sys.argv[3]}
|
||||
json.dump(out, open(sys.argv[2], 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
||||
print('[selftest] 全部通过' if out['all_pass'] else '[selftest] 有未通过项,见 ' + sys.argv[2])
|
||||
PY
|
||||
@@ -0,0 +1,71 @@
|
||||
"""本机自检的进程部分(2026-10-18 第七轮,遗留 C-5:Windows 生命周期从未实测)。由 ml/selftest.sh 调用。
|
||||
tree :按控制台同一套标志(Windows: CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW;POSIX: 新会话)起 bash → python + node 两个孙进程,
|
||||
再用控制台同一套停止方法(Windows: taskkill /T /F;POSIX: 按会话 TERM)结束,检查孙进程是否全部消失。
|
||||
detach :起一个"启动器"进程,它按同一标志拉起子树后立刻退出,检查子树是否还活着(等价于关掉控制台黑窗口后训练是否继续)。
|
||||
输出一行 JSON:{"tree_killed": bool, "survive_parent_exit": bool, ...}"""
|
||||
import json, os, subprocess, sys, time, tempfile, shutil
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from ml.monitor import find_bash, kill_session # noqa: E402
|
||||
|
||||
FLAGS = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP | 0x08000000} if os.name == 'nt' else {'start_new_session': True}
|
||||
|
||||
|
||||
def alive(pid):
|
||||
if os.name == 'nt':
|
||||
r = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True)
|
||||
return str(pid) in r.stdout
|
||||
try: os.kill(pid, 0); return True
|
||||
except OSError: return False
|
||||
|
||||
|
||||
def tree_cmd(d):
|
||||
py = sys.executable.replace('\\', '/'); dd = d.replace('\\', '/')
|
||||
return (f'"{py}" -c "import os,time;open(\'{dd}/p.pid\',\'w\').write(str(os.getpid()));time.sleep(300)" & '
|
||||
f'node -e "require(\'fs\').writeFileSync(\'{dd}/n.pid\',String(process.pid));setInterval(()=>{{}},1000)" & wait')
|
||||
|
||||
|
||||
def wait_pids(d, t=20):
|
||||
for _ in range(t * 4):
|
||||
try: return int(open(os.path.join(d, 'p.pid')).read()), int(open(os.path.join(d, 'n.pid')).read())
|
||||
except Exception: time.sleep(0.25)
|
||||
return None, None
|
||||
|
||||
|
||||
def main():
|
||||
bash = find_bash(); out = {'os': os.name, 'bash': bash}
|
||||
if not bash: out['error'] = '找不到 bash'; print(json.dumps(out, ensure_ascii=False)); return
|
||||
# tree
|
||||
d = tempfile.mkdtemp(prefix='st_tree_')
|
||||
p = subprocess.Popen([bash, '-c', tree_cmd(d)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **FLAGS)
|
||||
pp, pn = wait_pids(d)
|
||||
out['tree_started'] = pp is not None and pn is not None
|
||||
if out['tree_started']:
|
||||
if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(p.pid)], capture_output=True)
|
||||
else: kill_session(p.pid)
|
||||
time.sleep(3)
|
||||
out['tree_left'] = {'python': alive(pp), 'node': alive(pn)}
|
||||
out['tree_killed'] = not any(out['tree_left'].values())
|
||||
for x in (pp, pn): # 清场
|
||||
if alive(x):
|
||||
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
|
||||
else: os.kill(x, 9)
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
# detach:启动器拉起子树后立即退出
|
||||
d = tempfile.mkdtemp(prefix='st_det_')
|
||||
launcher = ('import subprocess,sys;' f'f={FLAGS!r};' 'subprocess.Popen([sys.argv[1],"-c",sys.argv[2]],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**f)')
|
||||
subprocess.run([sys.executable, '-c', launcher, bash, tree_cmd(d)], timeout=30)
|
||||
pp, pn = wait_pids(d)
|
||||
time.sleep(3)
|
||||
out['survive_parent_exit'] = bool(pp and pn and alive(pp) and alive(pn))
|
||||
for x in (pp, pn):
|
||||
if x and alive(x):
|
||||
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
|
||||
else: os.kill(x, 9)
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# 全流程冒烟(2026-10-18 第七轮,遗留 C-1:第 6–13 步从没跑过,第一次真跑要 15–28 h,中途才崩代价太大)。
|
||||
# 在仓库旁边建一份副本(默认 ../<仓库名>_smoke,可设 SMOKE_DIR),用极小参数把 kin → accept 整条 `all` 跑一遍:
|
||||
# 2000 条老师任务(够出真实的 keepRate / gapDrop / 间隙分位 / R2,作数据侧可行性依据)、极小模型、M5/APG/挖错各缩到几十题、bench 每集几题、查看器 3 题。
|
||||
# 训练侧只验证"每个阶段都能跑完、产物格式对、阶段之间衔接对",模型数值没有意义(门槛按冒烟放行:M5_STRICT=0 R4_STRICT=0 R2_STRICT=0);
|
||||
# 数据侧(stats / r2)数值有意义,汇总进 smoke.json。
|
||||
# 不碰本仓库的 ml/data、ml/runs、webapp/models。结果:本仓库 ml/runs/smoke.json(每阶段返回码与耗时)+ 副本里的完整日志。
|
||||
# 用法:bash ml/smoke.sh 预计 40–70 分钟(28 核 + 3090;生成 2000 条约 4 分钟,若门 1 触发共识约 20 分钟;其余阶段多数几分钟)
|
||||
set -e -o pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
SRC=$(pwd); PY=${PYTHON:-python}
|
||||
D=${SMOKE_DIR:-$(dirname "$SRC")/$(basename "$SRC")_smoke}
|
||||
echo "[smoke] 副本目录:$D"
|
||||
rm -rf "$D"; mkdir -p "$D"
|
||||
# 只拷受版本管理的文件(含未提交改动)+ 运行需要的本地依赖;数据与产物不拷
|
||||
git ls-files -z | tar --null -T - -cf - | (cd "$D" && tar xf -)
|
||||
for x in ml/node_modules webapp/vendor/ort webapp/generated; do [ -d "$x" ] && { mkdir -p "$D/$(dirname $x)"; cp -r "$x" "$D/$x"; }; done
|
||||
cd "$D"
|
||||
export N_TASKS=2000 NPROC=${SMOKE_NPROC:-$(( $(nproc) > 24 ? 24 : $(nproc) ))} TARGET_SAMPLES=1 CFG=ml/configs/smoke.json BENCH_STEPS=50 \
|
||||
M5_N=32 M5_STRICT=0 AUTO_WTA=0 R4_STRICT=0 R2_STRICT=0 R2_ARGS="--queries 2000 --pool 15000" \
|
||||
R4_ARGS="--viewer-n 3" APG_FIRST_STEPS=20 ROUNDS=1 APG_STEPS=20 MINE_N=128 MINE_GATE_N=64 \
|
||||
BENCH_ARGS="--nc-n 3 --route-n 3 --r8-n 2" VIEWER_N=3 MONITOR=0 FRESH=1 PIPE_OWNER=冒烟
|
||||
t0=$(date +%s); rc=0
|
||||
bash ml/pipeline.sh all 2>&1 | tee ml/runs_smoke.log || rc=$?
|
||||
mkdir -p "$SRC/ml/runs"
|
||||
$PY - "$D" "$rc" "$(( $(date +%s) - t0 ))" "$SRC/ml/runs/smoke.json" <<'PY'
|
||||
import json, os, sys
|
||||
d, rc, sec, out = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
|
||||
p = os.path.join(d, 'ml', 'runs', 'pipeline.json')
|
||||
st = json.load(open(p, encoding='utf-8')) if os.path.exists(p) else {}
|
||||
stages = {k: {'rc': v.get('rc'), 'min': round((v['end'] - v['start']) / 60, 1) if v.get('end') and v.get('start') else None} for k, v in (st.get('stages') or {}).items()}
|
||||
acc = os.path.join(d, 'ml', 'runs', 'acceptance.json')
|
||||
def rd(*p):
|
||||
try: return json.load(open(os.path.join(d, *p), encoding='utf-8'))
|
||||
except Exception: return None
|
||||
stx = rd('ml', 'data', 'stats.json') or {}
|
||||
data = {k: stx.get(k) for k in ('tasks', 'keepRate', 'gapDrop', 'vertexMinGapMm', 'planMs', 'samplesPerTask', 'consensusDrop')}
|
||||
data['r2'] = rd('ml', 'data', 'r2.json'); data['gate1'] = rd('ml', 'runs', 'consistency.json')
|
||||
data['r4'] = rd('ml', 'runs', 'r4.json'); data['r9'] = rd('ml', 'runs', 'r9_browser.json')
|
||||
pb = rd('ml', 'runs', 'policy_bench.json') or {}
|
||||
data['r8_genS'] = (pb.get('r8_adversarial') or {}).get('genS')
|
||||
res = {'rc': rc, 'wall_s': sec, 'dir': d, 'stages': stages, 'acceptance_written': os.path.exists(acc),
|
||||
'data': data, 'log': os.path.join(d, 'ml', 'runs_smoke.log'),
|
||||
'note': '训练侧只看每阶段 rc=0 与 acceptance_written;data 段(2000 条任务的统计、R2、门 1、R4、R9 路径、R8 出题耗时)是可行性依据'}
|
||||
json.dump(res, open(out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
||||
print('[smoke] rc=%d,%d 分钟;结果 %s' % (rc, sec // 60, out))
|
||||
PY
|
||||
exit $rc
|
||||
+5
-2
@@ -8,7 +8,7 @@ import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.common.features import STEP, D2R
|
||||
from ml.common.ckpt import Saver, atomic_save
|
||||
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
|
||||
from ml.data import Samples, load_tasks
|
||||
from ml.model.policy import build, wta_loss
|
||||
from ml.model.meanflow import meanflow_loss, one_step
|
||||
@@ -32,6 +32,7 @@ def main():
|
||||
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/runs/base')
|
||||
ap.add_argument('--config', default='ml/configs/base.json'); ap.add_argument('--steps', type=int, default=None)
|
||||
ap.add_argument('--resume', default=None); ap.add_argument('--no-eval', action='store_true')
|
||||
ap.add_argument('--stop-at', type=int, default=0, help='自检用:跑到第 N 步按停止请求处理(存 last.pt、rc 143)')
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
|
||||
a = ap.parse_args()
|
||||
cfg = json.load(open(a.config)); tc = cfg['train']
|
||||
@@ -51,6 +52,7 @@ def main():
|
||||
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
|
||||
if 'opt' in ck: opt.load_state_dict(ck['opt'])
|
||||
step0, best, bad_evals = ck.get('step', 0), ck.get('best', -1.0), ck.get('bad_evals', 0)
|
||||
if set_rng_state(ck.get('rng')): print('续训:随机数状态已恢复(batch 顺序与不断点一致)', flush=True)
|
||||
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好 success {best:.4f}', flush=True)
|
||||
total = tc['steps']
|
||||
lr_at = lambda s: tc['lr'] * (s / tc['warmup'] if s < tc['warmup'] else 0.5 * (1 + math.cos(math.pi * (s - tc['warmup']) / max(1, total - tc['warmup']))))
|
||||
@@ -64,6 +66,7 @@ def main():
|
||||
saver = Saver(tc.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(原子写)
|
||||
def save(name, step, extra=None):
|
||||
d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'bad_evals': bad_evals, 'fp': kin.fp}
|
||||
if name == 'last.pt': d['rng'] = rng_state()
|
||||
if extra: d.update(extra)
|
||||
atomic_save(d, os.path.join(a.out, name))
|
||||
if name == 'last.pt': saver.mark()
|
||||
@@ -100,7 +103,7 @@ def main():
|
||||
if not torch.isfinite(loss):
|
||||
print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break
|
||||
opt.step(); seen += X.shape[0]
|
||||
if saver.stop():
|
||||
if saver.stop() or (a.stop_at and step + 1 >= a.stop_at):
|
||||
save('last.pt', step + 1); log.close()
|
||||
print(f'收到停止请求:已存档 last.pt(第 {step + 1} 步),下次运行自动续训', flush=True); sys.exit(143)
|
||||
if saver.due(): save('last.pt', step + 1)
|
||||
|
||||
Reference in New Issue
Block a user