ml: 显存预算按 19–20 GB 落地(APG batch 128→48 + 每进程硬帽 + vram 上日志/网页);自检四处判据与环境坑修好,整轮 7/8 通过 3.5 分钟

- ml/configs/apg.json:batch 128→48。本机实测(d=192 同结构、每进程 19.2 GB 硬帽、1 步即到位)batch=128 无论 plans 取 60 还是 150 都 OOM(崩前已分配 17.85 GiB;d=32 小模型也 17.54 GiB,说明是展开图吃显存而非模型大小);48/plans=150 为 13.08、48/60 为 7.67。展开长度 plans 没动,降 plans 会把长任务绕出段截掉。
- ml/common/vram.py(新):limit() 设每进程帽 VRAM_FRAC 默认 0.78≈18.7 GiB 并打印上限;peak_gib() 供日志。train.py 与 apg_finetune.py 调用,每条记录新增 vram 字段(实测 train batch=1024 → 2.71,APG 自检档 → 0.33)。没帽时 APG 会把整卡顶到 23.6 GB、连桌面一起挤掉。
- ml/monitor.py / monitor.html:状态带 vram_budget_gb(默认 20,VRAM_BUDGET_GB 可改),GPU 显存条按预算算并超线变色,指标卡加「显存峰值」(>14 黄 >17 红)。headless 加载渲染 DOM 复核过。
- ml/configs/smoke.json 补 apg 段(batch=8)+ selftest.sh 的 APG 两项带 --config:原先不带 → 自检第 6 项按正式 batch=128 跑,本机必 OOM。
- ml/selftest.sh:gen_resume 判据由 cmp -s 改为剔除 plan_ms/cert.ms 两个墙钟字段后逐条比(旧判据恒 false);rec() 值非合法 JSON 时记 false 不再静默丢项。
- ml/selftest_proc.py:alive() 读 tasklist 改 text=True+errors='replace' 并防 None(中文 Windows 表头 GBK 会让读线程抛 UnicodeDecodeError→stdout=None→TypeError);wait_pids 带上最后一次异常。
- ml/monitor.py find_bash():便携版 git 的 mingw64 布局要再上一层找 bash,并兜 which('bash')(排除 System32 的 WSL)。本机原先返回 None → 网页「开始训练」起不动、自检进程两项没跑。
- 整轮自检重跑:gen_resume/prep_rerun/train_stop/train_stopfile/apg_resume/tree_killed/survive_parent_exit 全 true,仅 train_term false(timeout 的 TERM 送不到原生 python,rc=124 硬杀);用时 3.5 分钟。
- 文档:新增第七节(显存实测表、自检 8 项结果、四条判据/环境坑、本机改动清单);A-3 改为「实现方已改、待复跑验证」,C-5/C-6 标为已实测,D 组复核后关闭。
This commit is contained in:
cbb
2026-10-18 05:34:07 +08:00
parent fd2f2db991
commit f9fa3e67b6
10 changed files with 150 additions and 26 deletions
+74 -7
View File
@@ -3,7 +3,7 @@
六轮历史(`0218d7f` → `a5e779c`):第一轮 13 条、第二轮可行性、第三~五轮处置、第六轮本机实跑。**已解决的条目按用户要求删去,完整原文在 git 历史里**。本文件只剩三部分:仍有效的实测数字 / 仍未解决的问题 / 已复核正常的项,供实现方下一轮处置。
环境:本机 CBB-003(RTX 3090、28 核、Python 3.12 + torch 2.10 + onnxruntime 1.22、Edge + onnxruntime-web 1.20.1)。
当前结论:第 1–6 步已实测可跑;**第 7–13 步零证据**;R4 部署口径(纯推理)已判通过,`bash ml/pipeline.sh r4` 端到端 rc=0(2026-10-18 本机修复后复跑)。A 组原三处中两处已修,只剩离线部署 A-3。
当前结论:第 1–6 步已实测可跑;**第 7–13 步零证据**;R4 部署口径(纯推理)已判通过,`bash ml/pipeline.sh r4` 端到端 rc=0(2026-10-18 本机修复后复跑)。A 组原三处中两处已修,A-3 由实现方第七轮改掉、待本机复跑验证。**第七轮本机复核与显存预算(19–20 GB)见第七节。**
---
@@ -43,8 +43,8 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮
- `pipeline.sh` 7 处 `json.load(open(...))` 全部补 `encoding='utf-8'`(`:90 :110 :118 :127 :136 :138 :144`)。
- 附带发现并修掉:r4 分支末行 `[ "$r" = null ] && echo` 在 `r=pass` 时整体返回 1,`set -e` + EXIT trap 把**通过的 r4 记成 rc=1**(`all` 会因此停)。已改为 `if ... then ... fi`。
**A-3 仍未修:`&ortLocal=1` + 默认 proxy 必然失败。**
`ik-policy-glue.js` 把 `ort.env.wasm.wasmPaths` 设为相对前缀 `vendor/ort/`,ORT 对 `.mjs` 走 `import()` 需要绝对说明符 → `no available backend found. ERR: [wasm] TypeError: Failed to resolve module specifier 'vendor/ort/ort-wasm-simd-threaded.mjs'`。README 明确支持的离线部署路径在默认配置下不可用。(`ortLocal=1` + `proxy=0` 未测;`wasmPaths` 与 proxy 无关,大概率同样失败。)
**A-3 离线 ort 加载:实现方第七轮已改(绝对 URL + `vendor_ort.sh`),本机待复跑验证。**
`ik-policy-glue.js` 把 `ort.env.wasm.wasmPaths` 设为相对前缀 `vendor/ort/`,ORT 对 `.mjs` 走 `import()` 需要绝对说明符 → `no available backend found. ERR: [wasm] TypeError: Failed to resolve module specifier 'vendor/ort/ort-wasm-simd-threaded.mjs'`。README 明确支持的离线部署路径在默认配置下不可用。(`ortLocal=1` + `proxy=0` 未测;`wasmPaths` 与 proxy 无关,大概率同样失败。)**复跑判据**:断网/CDN 不可达时 `bash ml/pipeline.sh r4` 仍能出数(探针默认走本地 `webapp/vendor/ort/`),且 `&ortLocal=1` 的查看器徽标显示「策略模式就绪」。
### B. 待实现方判断(数据已摆在上面;分歧按第五轮第〇节规则,以实现方为准)
@@ -60,12 +60,13 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮
2. **3 mm 间隙过滤的真实剔除比例(`gapDrop`)没测过**:只测过沙箱 30 条(剔 9/29,不具代表性);上表 p50 4.4 mm 是未过滤口径。生成后看 `stats.json` 的 `gapDrop`。
3. **R2 新判据(`label_std_norm`)的池无关性**:需在 1.7 M 样本上 `--pool 50000` 与 `--pool 200000` 两跑、差 <0.02 才成立;没做。默认 `R2_STRICT=0` 只警告,开着就是一道未校准的闸。
4. **R8 出题耗时**:沙箱 2 题 9 分钟没出完;本机未测(`policy_bench.json` 会记 `requested` / `genS`,不足额时看警告)。
5. **Windows 侧生命周期全部未实测**:关控制台黑窗口训练是否继续(`CREATE_NO_WINDOW`)、`/proc/$$/winpid` + `taskkill /T` 能否结束整树、Git Bash 自带 `timeout` 能否把 TERM 送到 python / node。
6. **断点续跑的沙箱自测未在本机复核**:gen 截断续写(「续写:已有 N 条」)、train STOP 存档(rc 143、`last.pt` 第 N 步)、apg `--resume`、prep 二次运行「待打包 0 个」。
5. ~~**Windows 侧生命周期未实测**~~ → **已实测**(见 7.2):整树杀净 ✅、关黑窗口训练继续 ✅、`timeout` 的 TERM 送不到 python ❌(超时只能硬杀,靠 STOP 文件与定时存档)。
6. ~~**断点续跑未在本机复核**~~ → **已实测**(见 7.2):gen 按条续写逐条一致、train STOP 2 s 存档续训、apg `--resume`、prep 二次运行「待打包 0 个」全部通过。
7. **APG 的机时仍然零实测**(显存已量:`batch=48/plans=150` → 13.08 GiB)。`batch` 从 128 降到 48 后每步代价与收敛都要重算一遍才知道。
### D. 小遗留
`webapp/models/README.md` 过时:还写"由 `--fp16` 生成"、指向"第 8 步"(现为第 10 步、默认 fp32)。
~~`webapp/models/README.md` 过时~~ → 实现方第七轮已改,本机复核过:现在写的是 `bash ml/pipeline.sh export`(默认 fp32)、第 10 步、新输入签名 `feat[1 或 K,83]` 与 `feat_dyn`,并说明离线要先跑 `ml/prep/vendor_ort.sh`。
---
@@ -81,7 +82,7 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮
1. ~~A 组修复并重跑 `r4`~~:已完成(本机 2026-10-18,rc=0、P99 11.25)。
2. 接着 `FROM=gen TO=prep` 跑一次(数小时),拿 `gapDrop` 与 R2 两个口径的数——C-2 / C-3 随之消掉,B-2 的 Node 侧安全层数字可顺手从 m5 前的 bench 拿。
3. B 组等上面的数字齐了再定;C-1 / C-5 / C-6 随第一次真跑逐项核对;A-3(离线 ort 加载)交给实现方。
3. B 组等上面的数字齐了再定;C-1/C-7 随第一次真跑逐项核对;A-3 由实现方已改,本机复跑 `r4` 时顺带验(会弹有界面窗口)。
---
@@ -179,3 +180,69 @@ V1 通过后再跑,分三段,每段跑完把回传文件发我:
| 后训练与评测 | `FROM=apg TO=accept bash ml/pipeline.sh all` | 约 10–20 小时 | `ml/runs/acceptance.json`、`policy_bench.json`、`r9_browser.json` |
任何一段中途出错:把出错信息发我,我修完后用 `FROM=<出错阶段>` 接着跑,前面的不重做。
---
## 七、本机复核第六/七轮处置(2026-10-18,含显存预算)
**先说约束(用户定的,之前所有估算都按整卡 24 GB 写了)**:本机 Windows + 4K 桌面**空闲就占 4.4 GB**(`nvidia-smi` 空载 23604→4394 MiB 实测),所以 **ML 只能用 19–20 GB**。
### 7.1 APG 会直接把第 8 步打崩(实测,非推算)
用 d=192(与正式训练同结构)、`--steps 1`(峰值在第一次反向就到位)、每进程硬帽 19.2 GB,逐档实测 `max_memory_allocated`(GiB):
| batch | plans=60 | 90 | 120 | 150 |
|---|---|---|---|---|
| 32 | 6.14 | | | 9.53 |
| **48(已定为新值)** | 7.67 | | | **13.08** |
| 56 | | | 13.61 | |
| 64 | 10.81 | 15.41 | | 17.66 |
| **128(原值)** | **OOM** | | | **OOM**(崩前已分配 17.85 GiB) |
- **原 `batch=128` 无论 `plans` 取 60 还是 150 都 OOM**(d=32 的小模型也一样,崩前 17.54 GiB)→ 显存是被展开图吃掉的,与模型大小、数据量无关。
- **已把 `apg.json` 的 batch 定为 48,`plans 60→150` 不动**:最坏 13.08 GiB,加桌面 4.4 GB ≈ 17.5 GB,在 19–20 GB 预算内。没选"降 plans"是因为降展开长度会把长任务的绕出段截掉(就是口径-3 那个问题)。`batch=64 + plans=150` 要 17.66 GiB,加桌面就到 22 GB 了,不取。
- **加了每进程显存硬帽**:`ml/common/vram.py`,`VRAM_FRAC` 默认 0.78 ≈ 18.7 GiB,训练与 APG 启动时打印上限。没帽时它会把整卡顶到 23.6 GB、连桌面一起挤掉(就是这次报显存的原因)。
- **显存用量现在看得见**:`train.py` / `apg_finetune.py` 每条日志带 `vram`(峰值 GiB);监控页指标卡加「显存峰值」(>14 黄、>17 红),GPU 卡的显存条改成按 **20 GB 预算**算而不是整卡 24。实测:预训练 `batch=1024` 记到 `vram: 2.71`,APG 自检档 `vram: 0.33`。
- 对照:预训练 `batch=1024` 峰值 **2.76 GiB**(够);`check_r2` 每样本常驻 **776 B** → 1.7 M 样本 **1.32 GB** + 分块 0.05(4× 池自检 0.2)≈ **1.6 GB**,脚本里"GPU 峰值 <0.5 GB"错了约 3 倍,但不会崩。
- 顺带:N-6 的"按空闲显存 1/4 自动定块"、出题分块等估算都要按 19 GB 重算。
- **`ml/configs/smoke.json` 原来没有 `apg` 段**,而 `selftest.sh` 调 APG 时不带 `--config` → 自检第 6 项实际按正式 `batch=128` 跑,在本机必然 OOM、自检永远跑不完。已补 `apg` 段(`batch=8`、实测 `vram` 0.33 GiB)并让自检带 `--config`。
### 7.2 生命周期与断点续跑:本机整轮自检 **7/8 通过**,用时 3.5 分钟
| 项 | 结果 |
|---|---|
| `gen_resume` | ✅ true(判据修正后)——「续写:已有 2/6 条,从第 3 条继续」,与一口气生成**逐条一致** |
| `prep_rerun` | ✅ true——二次运行「待打包 0 个」 |
| `train_stop` | ✅ true——`--stop-at 20` → rc 143、`last.pt` 第 20 步、续训到 40 步 rc 0 |
| `train_stopfile` | ✅ true——写 `ml/runs/STOP` 后 **2 s** 退出并存档(第 351 步)。控制台「停止」走的就是这条路 |
| `train_term` | ❌ **false(Windows 的真实限制)**——`timeout` 的 TERM 送不到原生 python,rc=124 是硬杀。结论:阶段超时不能指望体面存档,靠 STOP 文件 + 每 10 分钟定时存档 |
| `apg_resume` | ✅ true(自检档 `batch=8`,`vram` 0.33 GiB) |
| `tree_killed` | ✅ true(实测)——`taskkill /T /F` 把 bash→python+node 整树杀净,没留孤儿 |
| `survive_parent_exit` | ✅ true(实测)——启动器退出后子树仍活着 = **关掉控制台黑窗口不影响训练** |
用时 3.5 分钟(APG 用自检档;上一轮 APG 走正式 `batch=128` 时是 13.2 分钟,而且 OOM)。
之前测不到的原因:① `monitor.find_bash()` 在本机返回 `None`(Qoder 自带 git 在 `mingw64\bin\git.exe`,"上上级 + bin/bash.exe"推出一个不存在的路径);② `selftest_proc.alive()` 用 `text=True` 读 `tasklist`,中文 Windows 的表头是 GBK → 读线程 `UnicodeDecodeError`、`stdout` 变 `None` → `TypeError`,异常又被 `wait_pids` 的 `except Exception` 吞掉。
### 7.3 两条判据自身的问题(会让"通过/失败"失真)
1. **`selftest.sh:27` 用 `cmp -s` 逐字节比生成结果**,但记录里含 `plan_ms` 与 `cert.ms` 两个**墙钟耗时字段** → 两次运行不可能相同,**这项永远是 false**。我在副本里重跑基线、只把这两个字段剔掉后逐行比:**6 条完全一致** → 续写逻辑正确,是判据错。
2. **`rec()` 用 `json.loads` 解析传入值**,而 `str(None).lower()`=`none` 不是合法 JSON → 抛异常、整项**静默不进结果文件**,`all_pass` 照样算出来。所以"第七轮说自检已修完"的两项进程测试,实际上是没跑过。
3. **自检副本只复制 `git ls-files` 里的文件**(`selftest.sh:16`)→ 任何**未跟踪的新文件**在副本里不存在,调用它的项会以 `ModuleNotFoundError` 变成 rc=1,看起来像功能坏了。本轮新加 `ml/common/vram.py` 就踩了:`train_stop`/`train_stopfile`/`apg_resume` 三项同时 rc=1,`git add` 之后才恢复。判据建议:自检开头先 `git ls-files --others --exclude-standard` 非空就报出来(或改用 `git ls-files -o --exclude-standard -t` 一起复制)。
### 7.4 本机已改的(都按实测落地,不改判据含义)
| 位置 | 改动 | 验证 |
|---|---|---|
| `ml/monitor.py` `find_bash()` | 便携版 git 的 `mingw64` 布局要再上一层;再兜 `which('bash')`(排除 System32 的 WSL bash) | 返回 `...\.qoder\bin\git\bin\bash.exe`(bash 5.2.37);网页「开始训练」在本机的拦路石解除 |
| `ml/selftest_proc.py` `alive()` / `wait_pids()` | `tasklist` 用 `text=True, errors='replace'` 并防 `None`;起不来时把最后一次异常带进结果 | `tree_killed`、`survive_parent_exit` 两项实测 true |
| `ml/selftest.sh` | `rec` 解析失败记 false 不再丢项;`gen_resume` 判据剔除 `plan_ms`/`cert.ms`;APG 两项带 `--config` | 重跑第 1 项已变 **true**(「续写:已有 2/6 条 → 逐条一致」) |
| `ml/configs/apg.json` | `batch 128 → 48`,加 `_vram` 记录实测表 | 最坏 13.08 GiB(实测),在 19–20 GB 预算内 |
| `ml/configs/smoke.json` | 补 `apg` 段(`batch=8`) | 自检 APG 档 `vram: 0.33` |
| `ml/common/vram.py`(新)+ `train.py` + `apg/apg_finetune.py` | 每进程硬帽 `VRAM_FRAC=0.78`(启动打印上限);日志每条记录加 `vram` 峰值 | `显存上限:本进程 18.7 GiB(整卡 24.0 × 0.78)`;train `vram: 2.71` |
| `ml/monitor.py` + `ml/monitor.html` | 状态带 `vram_budget_gb`(默认 20,`VRAM_BUDGET_GB` 可改);GPU 显存条按预算算并超线变色;指标卡加「显存峰值」 | headless 加载渲染出的 DOM 里有「显存峰值 / 预算 19–20 GB(整卡 24,桌面占 4–5)」 |
### 7.5 仍需实现方处理的(我没动)
- **APG 的 `steps` 与机时**:`batch` 从 128 降到 48,每步看到的任务少 2.7 倍。`steps=3000` 要不要加、加多少是收敛判断,我只有显存实测、没有这个依据。`R2/R4/N-6` 各处按整卡写的显存估算文案也要按 19–20 GB 重写。
- `R4 复跑`:`feat_dyn` 要重导模型,探针现在会**弹有界面窗口**跑 `viewer_ref`(30 题×≤10 s)与 WebGPU——本机跑之前请知悉会有窗口,别动它。
- B-2 的几何查询搬 Worker(若本机 R9 仍不过);R9 用真模型才有意义。
+3 -1
View File
@@ -17,6 +17,7 @@ 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, rng_state, set_rng_state
from ml.common import vram
def load_pool(paths):
@@ -38,6 +39,7 @@ def main():
cfg = json.load(open(a.config)); C = cfg['apg']
if a.steps: C['steps'] = a.steps
dev = a.device; torch.manual_seed(a.seed); random.seed(a.seed)
vram.limit()
os.makedirs(a.out, exist_ok=True)
ck = torch.load(a.init, map_location=dev)
mcfg = ck['cfg']
@@ -147,7 +149,7 @@ def main():
with torch.no_grad():
e, oe = goal_err(kin, q, T, R)
rec = {'it': it, 'total': C['steps'], 't': time.time(), 'plans': n_plans, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(),
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60}
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60, 'vram': vram.peak_gib()}
if it % 10 == 0: print(json.dumps(rec), flush=True)
log.write(json.dumps(rec) + '\n'); log.flush()
if saver.stop() or (a.stop_at and it + 1 >= a.stop_at):
+21
View File
@@ -0,0 +1,21 @@
"""显存预算(2026-10-18 本机实测后加)。
用户定的预算:这台 3090 只有 19–20 GB 能给训练用——Windows + 4K 桌面常驻就占掉 4–5 GB(空载 nvidia-smi 4.4 GB)。
limit() 给本进程设硬帽(VRAM_FRAC,默认 0.78 ≈ 18.7 GiB)。超限时在自己的反向里早失败,
而不是把桌面挤到没有显存(整机卡顿、别的应用崩),这也是 APG batch=128 原先 OOM 时把显存顶到 23.6 GB 的原因。
peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对预算,不用再去猜。
实测参考(d=192 同结构、1 步即到位):预训练 batch=1024 → 2.76;APG batch=48/plans=150 → 13.08、batch=128 → OOM。"""
import os
import torch
def limit():
if not torch.cuda.is_available(): return False
f = float(os.environ.get('VRAM_FRAC', '0.78'))
tot = torch.cuda.get_device_properties(0).total_memory / 2**30
torch.cuda.set_per_process_memory_fraction(f)
print('显存上限:本进程 %.1f GiB(整卡 %.1f GiB × %.2f,其余留给桌面)' % (f * tot, tot, f), flush=True)
return True
def peak_gib():
return round(torch.cuda.max_memory_allocated() / 2**30, 2) if torch.cuda.is_available() else None
+2 -1
View File
@@ -1,7 +1,8 @@
{
"apg": {
"batch": 128, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16",
"batch": 48, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16",
"plans": 60, "plans_max": 150, "seg_plans": 10,
"_vram": "batch 由 128 降到 48:2026-10-18 本机实测(d=192 同结构、每进程 19.2 GB 硬帽)batch=128 无论 plans 取 60 还是 150 都 OOM(崩前已分配 17.85 GiB);batch=48/plans=150 峰值 13.08 GiB、plans=60 为 7.67 GiB,加 Windows+4K 桌面常驻 4.4 GB 仍在 19–20 GB 预算内。batch=64/plans=150 要 17.66 GiB(不取)。展开长度 plans 没动——降 plans 会把长任务的绕出段截掉。",
"margin_train_mm": 3.0,
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
+9 -1
View File
@@ -31,5 +31,13 @@
"log_every": 10,
"patience_evals": 5
},
"_note": "冒烟专用(bash ml/smoke.sh):只验证流程能跑通,数值无意义"
"apg": {
"batch": 8, "lr": 0.0001, "steps": 10, "grad_clip": 1.0, "amp": "bf16",
"plans": 20, "plans_max": 30, "seg_plans": 5,
"margin_train_mm": 3.0,
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
"ckpt_min": 10, "eval_every": 1000, "eval_tasks": 4
},
"_note": "冒烟与自检专用(bash ml/pipeline.sh selftest / smoke):只验证流程能跑通,数值无意义。apg 段必须有:selftest.sh 第 6 项原先不带 --config,走的是正式 apg.json 的 batch=128,本机实测第一次反向就 OOM;改成正式值 48 也要约 13 GiB,自检不必吃这么多。"
}
+6 -3
View File
@@ -436,6 +436,7 @@ function renderKpis() {
tile('展开内到达比例', pct(r.reach_frac), '误差 < 1 mm 且姿态 < 0.5°', '', last('reach_frac'), COLORS[2]);
tile('闭环到达率', pct(e.success) + dlt('success'), `最好 ${pct(best)}`, '', evs('success'), COLORS[2]);
tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]);
tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}<small>GiB</small>` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]);
} else {
const sps = r.samples_per_s;
tile('总损失', fmt(r.loss, 4), `MF ${fmt(r.mf)} · FK ${fmt(r.fk_cm)} cm`, '', ema(last('loss'), 0.9), COLORS[0]);
@@ -443,6 +444,7 @@ function renderKpis() {
tile('闭环到达率', pct(e.success) + dlt('success'), evals.length ? `最好 ${pct(best)} · @${e.x.toLocaleString()} 步` : '尚未评估', '', evs('success'), COLORS[2]);
tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层,部署另有护栏', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]);
tile('梯度范数', fmt(r.gnorm), '裁剪阈值 1.0', r.gnorm > 5 ? 'warn' : '', ema(last('gnorm'), 0.9), COLORS[4]);
tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}<small>GiB</small>` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]);
}
$('kpis').innerHTML = tiles.map(t => t.html).join('');
[...$('kpis').querySelectorAll('canvas')].forEach((cv, i) => spark(cv, tiles[i].sp, tiles[i].color));
@@ -477,7 +479,7 @@ async function poll() {
render();
}
const P = lastStatus.pipeline, busy = (job && job.running) || (P && P.current);
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`);
else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`);
else setPill('', '空闲');
@@ -531,15 +533,16 @@ function renderStatus(s) {
// GPU
const G = s.gpu || [];
const BUD = (s.vram_budget_gb || 24) * 1024; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看
$('gputime').textContent = G.length ? '实时' : '';
$('gpu').innerHTML = G.length ? G.map(g => {
const u = g.util ?? 0, C = 2 * Math.PI * 50, col = u > 85 ? css('--ok') : u > 50 ? css('--accent') : css('--warn');
const memP = 100 * g.mem_used / g.mem_total, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6');
const memP = 100 * g.mem_used / BUD, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6');
const row = (k, v, p, c) => `<div class="gr"><div class="l"><span>${k}</span><b>${v}</b></div><div class="mini"><div style="width:${Math.min(100, p)}%;background:${c}"></div></div></div>`;
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
<circle cx="58" cy="58" r="50" stroke="${col}" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="${C}" stroke-dashoffset="${C * (1 - u / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b>${u}%</b><span>利用率</span></div></div></div>
<div class="gpurows">${row('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(g.mem_total / 1024).toFixed(0)} GB`, memP, css('--accent2'))}
<div class="gpurows">${row('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB 预算(整卡 ${(g.mem_total / 1024).toFixed(0)})`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'))}
${row('温度', `${g.temp} °C`, g.temp, tc)}${row('功耗', g.power != null ? `${Math.round(g.power)} W` : '—', g.power ? g.power / 3.5 : 0, css('--c7'))}</div></div>
<div class="gname">${g.name}</div>`;
}).join('<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">') : empty('未检测到 nvidia-smi');
+9 -3
View File
@@ -192,7 +192,8 @@ def status():
pb = jload(P(RUNS, 'policy_bench.json'))
if pb: pb = {k: {kk: v.get(kk) for kk in ('n', 'ok', 'reached', 'collisionFrames', 'brakeRate', 'recoveryRate', 'livelock', 'inferMsP99', 'safetyMsP99')}
for k, v in pb.items() if isinstance(v, dict) and 'n' in v}
return {'now': time.time(), 'gpu': gpu(), 'pipeline': pl, 'check': CHECK.get(), 'lock': lock_info(),
return {'now': time.time(), 'gpu': gpu(),
'vram_budget_gb': float(os.environ.get('VRAM_BUDGET_GB', 20)), # 本机桌面常驻占 4–5 GB,给训练的预算 19–20 GB 'pipeline': pl, 'check': CHECK.get(), 'lock': lock_info(),
'stage_meta': [{'id': k, 'name': n, 'est_h': h, 'timeout_h': pstate.TIMEOUT_H[k]} for k, n, h in pstate.STAGES],
'r4': jload(P(RUNS, 'r4.json')),
'data': {'consistency': (jload(P(DATA, 'consistency.json')) or {}).get('consistentByGateRule'),
@@ -218,8 +219,13 @@ def find_bash():
if p and os.path.exists(p): return p
g = shutil.which('git')
if g:
p = os.path.join(os.path.dirname(os.path.dirname(g)), 'bin', 'bash.exe')
if os.path.exists(p): return p
root = os.path.dirname(os.path.dirname(g)) # 便携版 git 常放在 <根>\mingw64\bin\git.exe,要再上一层才找到 bash
for base in dict.fromkeys([root, os.path.dirname(root)]):
for sub in ('bin', os.path.join('usr', 'bin')):
p = os.path.join(base, sub, 'bash.exe')
if os.path.exists(p): return p
b = shutil.which('bash')
if b and 'windows\\system32' not in b.lower(): return b
return None
return shutil.which('bash')
+15 -5
View File
@@ -17,14 +17,21 @@ 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"; }
rec() { $PY -c "import json,sys
try: v=json.loads(sys.argv[2])
except Exception: v=False # 值不是合法 JSON 时记 false,不能让整项静默消失(第七轮实测:'none' 抛异常 → 两项没进结果)
print(json.dumps({'item':sys.argv[1],'pass':v,'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 "续写结果与一口气生成不同"
gd=$($PY -c "import json,sys
f=lambda p:[json.dumps({**r,'plan_ms':0,'cert':{**r.get('cert',{}),'ms':0}},sort_keys=True) for r in (json.loads(l) for l in open(p,encoding='utf-8') if l.strip())]
a,b=f(sys.argv[1]),f(sys.argv[2]);print('OK' if a==b else '不同 %d/%d 条'%(sum(x!=y for x,y in zip(a,b))+abs(len(a)-len(b)),max(len(a),len(b))))" ml/data/a.jsonl ml/data/shard_0011.jsonl)
if [ "$gd" = OK ]; then rec gen_resume true "$(grep -o '续写[^,]*' ml/runs/st_gen.log | head -1)(逐条一致;不含 plan_ms/cert.ms 两个墙钟字段,第七轮实测 cmp -s 恒不等)"
else rec gen_resume false "续写与一口气生成不同:$gd"; fi
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 打包二次运行
@@ -49,13 +56,16 @@ if command -v timeout >/dev/null; then
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=$?
$PY -m ml.apg.apg_finetune --config $CFGS --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 --config $CFGS --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")
k=$($PY -c "import json,sys
try: d=json.loads(sys.argv[1])
except Exception: d={}
print(json.dumps(d.get('tree_killed')), json.dumps(d.get('survive_parent_exit')))" "$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'
+8 -4
View File
@@ -14,8 +14,9 @@ FLAGS = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP | 0x08000000} if o
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
# 中文 Windows 的 tasklist 表头是 GBK,text=True 会让读线程解码崩掉(stdout 变 None)→ 按字节读再容错解码
r = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True, errors='replace')
return str(pid) in (r.stdout or '')
try: os.kill(pid, 0); return True
except OSError: return False
@@ -26,11 +27,14 @@ def tree_cmd(d):
f'node -e "require(\'fs\').writeFileSync(\'{dd}/n.pid\',String(process.pid));setInterval(()=>{{}},1000)" & wait')
WAITERR = ['']
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
except Exception as e: WAITERR[0] = type(e).__name__ + ': ' + str(e)[:90]; time.sleep(0.25)
return None, None # 起不来时把最后一次异常带进结果,别让 tree_started=false 变成没有原因的失败
def main():
+3 -1
View File
@@ -9,6 +9,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, rng_state, set_rng_state
from ml.common import vram
from ml.data import Samples, load_tasks
from ml.model.policy import build, wta_loss
from ml.model.meanflow import meanflow_loss, one_step
@@ -40,6 +41,7 @@ def main():
os.makedirs(a.out, exist_ok=True)
torch.manual_seed(a.seed)
dev = a.device
vram.limit()
kin = Kin(device=dev)
data = Samples(a.samples, device=dev, split='train')
val_tasks = [t for t in load_tasks(a.samples) if t['split'] == 'val'][:tc['eval_tasks']]
@@ -111,7 +113,7 @@ def main():
el = time.time() - t0
rec = {'step': step, 'total': total, 't': time.time(), 'loss': loss.item(), 'mf': mf_raw.item(), 'fk_cm': l_fk.item(), 'smooth': l_sm.item(),
'value': l_val.item(), 'sub': l_sub.item(), 'gnorm': gn.item(), 'lr': lr_at(step), 'samples_per_s': seen / max(el, 1e-6),
'eta_h': (total - step - 1) * tc['batch'] / max(seen / max(el, 1e-6), 1e-6) / 3600}
'eta_h': (total - step - 1) * tc['batch'] / max(seen / max(el, 1e-6), 1e-6) / 3600, 'vram': vram.peak_gib()}
print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + '\n'); log.flush()
if not a.no_eval and (step + 1) % tc['eval_every'] == 0:
model.eval()