From f9fa3e67b6305808514a66591e5808b620138ac3 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sun, 18 Oct 2026 05:34:07 +0800 Subject: [PATCH] =?UTF-8?q?ml:=20=E6=98=BE=E5=AD=98=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E6=8C=89=2019=E2=80=9320=20GB=20=E8=90=BD=E5=9C=B0=EF=BC=88APG?= =?UTF-8?q?=20batch=20128=E2=86=9248=20+=20=E6=AF=8F=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E7=A1=AC=E5=B8=BD=20+=20vram=20=E4=B8=8A=E6=97=A5=E5=BF=97/?= =?UTF-8?q?=E7=BD=91=E9=A1=B5=EF=BC=89=EF=BC=9B=E8=87=AA=E6=A3=80=E5=9B=9B?= =?UTF-8?q?=E5=A4=84=E5=88=A4=E6=8D=AE=E4=B8=8E=E7=8E=AF=E5=A2=83=E5=9D=91?= =?UTF-8?q?=E4=BF=AE=E5=A5=BD=EF=BC=8C=E6=95=B4=E8=BD=AE=207/8=20=E9=80=9A?= =?UTF-8?q?=E8=BF=87=203.5=20=E5=88=86=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 组复核后关闭。 --- docs/ML运行手册审查.md | 81 ++++++++++++++++++++++++++++++++++++++---- ml/apg/apg_finetune.py | 4 ++- ml/common/vram.py | 21 +++++++++++ ml/configs/apg.json | 3 +- ml/configs/smoke.json | 10 +++++- ml/monitor.html | 9 +++-- ml/monitor.py | 12 +++++-- ml/selftest.sh | 20 ++++++++--- ml/selftest_proc.py | 12 ++++--- ml/train.py | 4 ++- 10 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 ml/common/vram.py diff --git a/docs/ML运行手册审查.md b/docs/ML运行手册审查.md index 7ab7f84..633cc19 100644 --- a/docs/ML运行手册审查.md +++ b/docs/ML运行手册审查.md @@ -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 用真模型才有意义。 diff --git a/ml/apg/apg_finetune.py b/ml/apg/apg_finetune.py index 421a3b8..ae20075 100644 --- a/ml/apg/apg_finetune.py +++ b/ml/apg/apg_finetune.py @@ -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): diff --git a/ml/common/vram.py b/ml/common/vram.py new file mode 100644 index 0000000..2009601 --- /dev/null +++ b/ml/common/vram.py @@ -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 diff --git a/ml/configs/apg.json b/ml/configs/apg.json index 209510d..474f3e3 100644 --- a/ml/configs/apg.json +++ b/ml/configs/apg.json @@ -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}, diff --git a/ml/configs/smoke.json b/ml/configs/smoke.json index 5cc09f2..7fea377 100644 --- a/ml/configs/smoke.json +++ b/ml/configs/smoke.json @@ -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,自检不必吃这么多。" } \ No newline at end of file diff --git a/ml/monitor.html b/ml/monitor.html index 250221b..04f88bd 100644 --- a/ml/monitor.html +++ b/ml/monitor.html @@ -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)}GiB` : '—', '预算 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)}GiB` : '—', '预算 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', `
这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。
`); } + if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', `
这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。
`); } 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) => `
${k}${v}
`; return `
${u}%利用率
-
${row('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(g.mem_total / 1024).toFixed(0)} GB`, memP, css('--accent2'))} +
${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'))}
${g.name}
`; }).join('
') : empty('未检测到 nvidia-smi'); diff --git a/ml/monitor.py b/ml/monitor.py index 17d543d..40f2e80 100644 --- a/ml/monitor.py +++ b/ml/monitor.py @@ -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') diff --git a/ml/selftest.sh b/ml/selftest.sh index 9e90000..432438a 100755 --- a/ml/selftest.sh +++ b/ml/selftest.sh @@ -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' diff --git a/ml/selftest_proc.py b/ml/selftest_proc.py index 0724988..0224fbd 100644 --- a/ml/selftest_proc.py +++ b/ml/selftest_proc.py @@ -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(): diff --git a/ml/train.py b/ml/train.py index fcfab9a..2706eab 100644 --- a/ml/train.py +++ b/ml/train.py @@ -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()