From b1cf4126fde751042142c1c93df2d5bd68d71603 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sun, 18 Oct 2026 17:23:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(vram):=20=E6=98=BE=E5=AD=98=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E6=94=B9=E4=B8=BA=E6=B5=AE=E5=8A=A8=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=9C=AC=E8=BF=9B=E7=A8=8B=E4=B8=8A=E9=99=90=20=3D=20min(?= =?UTF-8?q?=E6=95=B4=E5=8D=A1=20=E2=88=92=20nvidia-smi=20=E5=B7=B2?= =?UTF-8?q?=E7=94=A8=20=E2=88=92=201=20GB=20=E4=BD=99=E9=87=8F,=20CUDA=20?= =?UTF-8?q?=E7=A9=BA=E9=97=B2=20=E2=88=92=20=E4=BD=99=E9=87=8F)=EF=BC=8C?= =?UTF-8?q?=E5=88=AB=E4=BA=BA=E5=8D=A0=E5=A4=9A=E5=B0=91=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=89=A3=E6=8E=89=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=86=99=E6=AD=BB?= =?UTF-8?q?=200.78=EF=BC=88=E5=AE=9E=E6=B5=8B=E5=BD=93=E5=89=8D=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=2014.1=20GiB=EF=BC=9BVRAM=5FFRAC=20=E4=BB=8D=E5=8F=AF?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E5=AF=B9=E6=8B=8D=E6=97=A7=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E7=BD=91=E9=A1=B5=E6=98=BE=E5=AD=98=E8=A1=8C?= =?UTF-8?q?=E6=94=B9=E6=88=90"8.7=20/=2023=20GB=20=C2=B7=20=E7=A9=BA=2014.?= =?UTF-8?q?3"=E5=B9=B6=E6=8A=8A=E8=A7=A3=E9=87=8A=E6=94=BE=E6=82=AC?= =?UTF-8?q?=E5=81=9C=EF=BC=88=E9=95=BF=E6=96=87=E6=9C=AC=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E6=8A=8A=E8=A1=8C=E6=8C=A4=E6=88=90=E4=B8=A4=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?.gr=20=E8=A1=8C=E5=8A=A0=E4=BA=86=E7=9C=81=E7=95=A5=E5=8F=B7?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=EF=BC=89=EF=BC=9BREADME/=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ML运行手册审查.md | 2 +- ml/README.md | 2 +- ml/common/vram.py | 47 ++++++++++++++++++++++++++++++++++-------- ml/monitor.html | 10 +++++---- ml/monitor.py | 3 ++- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/docs/ML运行手册审查.md b/docs/ML运行手册审查.md index 7b16b9c..e05d1ea 100644 --- a/docs/ML运行手册审查.md +++ b/docs/ML运行手册审查.md @@ -25,7 +25,7 @@ |---|---| | R9(真查看器、训练好的模型、30 道贴柱题) | 稳态 `planWarmP99` < 45 ms、运动中饥饿 < 1%、认证超时占比 < 5%;冷启动 P99、刹停率、`tickGapMax` 只报 | | R9 不过 | 几何查询(特征、投影、认证)搬进 Worker,主线程只执行 | -| APG | batch 48、steps 8000(总任务量同原 128×3000),进程显存硬帽 `VRAM_FRAC=0.78` | +| APG | batch 48、steps 8000(总任务量同原 128×3000),本进程显存上限浮动(整卡 − 已用 − 1 GB 余量,见 `ml/common/vram.py`) | | 离线 ort | 只用入库本地副本,缺失即报错,不退回 CDN | | 训练余量 3 mm | `stats.json` 间隙 p10 ≥ 3 mm 保持,否则降到 p10 | | `R2_STRICT` | `pool_check.independent=true` 且截距 < 0.10 时打开 | diff --git a/ml/README.md b/ml/README.md index 5a8b822..7ca36ea 100644 --- a/ml/README.md +++ b/ml/README.md @@ -131,7 +131,7 @@ bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/b - **通过线(R10):`samples_per_s` ≥ 5000,即 60000 步 ≤ 3.5 小时**。参考:3090、batch 1024 实测 6.2 k 样本/s ≈ 2.8 小时。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48,仍不够再把 `steps` 降到 40000 - 训练中每 2000 步打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`time_ok_1p3`、`start_fail`。时限 = 老师时长 × 2(至少 3 s) -- 显存预算按 **19–20 GB** 算(Windows + 4K 桌面常驻占 4–5 GB),训练与 APG 进程启动时自设硬帽 `VRAM_FRAC=0.78`(≈18.7 GiB),日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。 +- 显存预算**浮动**:整卡留 1 GB 给系统(`VRAM_RESERVE_GIB`,默认 1),训练与 APG 进程启动时按"整卡 − nvidia-smi 已用 − 余量"和"CUDA 空闲 − 余量"两者取小自设本进程上限(`ml/common/vram.py`)。占显存的应用多,上限就自动变小,不用改配置;想按定值对拍给 `VRAM_FRAC`(旧口径 0.78)。日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB、APG `batch=48` 13.08 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。 ## 7. M5 门禁:选定动作头 diff --git a/ml/common/vram.py b/ml/common/vram.py index 2009601..e1ca3b0 100644 --- a/ml/common/vram.py +++ b/ml/common/vram.py @@ -1,19 +1,48 @@ -"""显存预算(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。""" +"""显存预算(2026-10-18 本机实测后加;同日按用户要求改成浮动)。 +口径:整卡留 1 GB 给系统,其余"能给应用用多少"按启动那一刻的实际情况算——桌面 / 浏览器 / OBS / +直播软件占了多少都自动扣掉,不再写死一个比例(写死有两个毛病:别人占得多时上限仍很高 → 顶满整卡、 +桌面卡顿;占得少时又白白少用)。 +两个口径取较小(2026-10-18 实测差的不是一点): + nvidia-smi:整卡 24.0 − 已用 8.6 − 余量 1.0 → 14.4 GiB(别人手里的显存先扣掉,别把它们挤去系统内存) + CUDA free:mem_get_info 报空闲 22.8 GiB —— WDDM 下这部分含"驱动能换出"的量,单独信它会顶满整卡 + (APG batch=128 那次顶到 23.6 GB 就是整机卡顿的根源)。 + limit() 本进程上限 = min(上面两个) ;显式给 VRAM_FRAC 时再取较小值,用来跟旧口径(0.78)对拍。 + peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对,不用再去猜。 +实测参考(d=192 同结构、1 步即到位):预训练 batch=1024 → 2.76 GiB;APG batch=48/plans=150 → 13.08 GiB。 +要跑大 batch 的步骤(APG)之前先把 OBS / 浏览器这类占显存的收一收——上限会跟着启动时的占用变小。""" import os +import subprocess import torch +def _smi(): + """(整卡已用 GiB, 整卡总量 GiB);拿不到就 (None, None)。""" + try: + q = 'memory.used,memory.total' + r = subprocess.run(['nvidia-smi', f'--query-gpu={q}', '--format=csv,noheader,nounits'], + capture_output=True, text=True, timeout=3) + u, t = [float(x) for x in r.stdout.strip().splitlines()[0].split(',')[:2]] + return u / 1024, t / 1024 + except Exception: + return None, None + + 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) + torch.empty(1, device='cuda') # 让本进程的 CUDA 上下文先落地,再读空闲(否则会把自己的上下文算进空闲) + free = torch.cuda.mem_get_info()[0] / 2**30 + res = float(os.environ.get('VRAM_RESERVE_GIB', '1')) + cap, why = free - res, 'CUDA 空闲 %.1f − 余量 %.1f' % (free, res) + used, _ = _smi() + if used is not None: + head = tot - used - res + if head < cap: cap, why = head, '整卡 %.1f − 已用 %.1f − 余量 %.1f' % (tot, used, res) + if os.environ.get('VRAM_FRAC'): # 显式硬帽(对拍旧口径用) + cap = min(cap, tot * float(os.environ['VRAM_FRAC'])) + cap = max(1.0, min(cap, tot - res)) + torch.cuda.set_per_process_memory_fraction(cap / tot) + print('显存上限:本进程 %.1f GiB(%s;= 整卡的 %.0f%%)' % (cap, why, 100 * cap / tot), flush=True) return True diff --git a/ml/monitor.html b/ml/monitor.html index 458ebf5..99fd25a 100644 --- a/ml/monitor.html +++ b/ml/monitor.html @@ -86,7 +86,8 @@ .ring .c b { font: 700 24px/1 var(--mono); display: block; } .ring .c span { font-size: 11px; color: var(--dim); } .gpurows { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; } .cores { display: flex; gap: 2px; height: 26px; margin-top: 2px; background: #1b2433; border-radius: 3px; padding: 2px; } - .gr .l { display: flex; justify-content: space-between; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); } + .gr .l { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); } + .gr .l span { flex: none; white-space: nowrap; } .gr .l b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* 值太长就省略号,别把整行挤成两行 */ .mini { height: 6px; background: #0d131d; border-radius: 99px; overflow: hidden; } .mini > div { height: 100%; border-radius: 99px; transition: width .6s; } .gname { font-size: 12px; color: var(--dim2); margin-top: 12px; } @@ -539,17 +540,18 @@ function renderStatus(s) { // GPU + CPU(同一个框:上面是 GPU 环 + 显存/温度/功耗,下面接 CPU 总占用与每核条) const G = s.gpu || [], CP = s.cpu || {}; - const BUD = (s.vram_budget_gb || 24) * 1024; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看 + const RESV = s.vram_reserve_gb ?? 1; // 给系统留的余量(GB):训练进程启动时按"整卡 − 已用 − 余量"自设上限(浮动) const hr = '
'; - const gr = (k, v, p, c) => `
${k}${v}
`; + const gr = (k, v, p, c, t) => `
${k}${v}
`; $('gputime').textContent = (G.length || CP.n) ? '实时' : ''; const gpuBlk = 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 BUD = (s.vram_budget_gb || (g.mem_total / 1024 - RESV)) * 1024, avail = BUD / 1024 - g.mem_used / 1024; const memP = 100 * g.mem_used / BUD, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6'); return `
${u}%利用率
-
${gr('显存', `${(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'))} +
${gr('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB · 空 ${avail.toFixed(1)}`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'), `可用 ${(BUD / 1024).toFixed(0)} GB = 整卡 ${(g.mem_total / 1024).toFixed(0)} GB − 给系统的余量 ${RESV} GB;训练进程启动时按「可用 − 那一刻已用」再自设本进程上限,别人占多少自动扣掉(ml/common/vram.py)`)} ${gr('温度', `${g.temp} °C`, g.temp, tc)}${gr('功耗', g.power != null ? `${Math.round(g.power)} W` : '—', g.power ? g.power / 3.5 : 0, css('--c7'))}
${g.name}
`; }).join(hr) : ''; diff --git a/ml/monitor.py b/ml/monitor.py index 6d33af9..0097ab8 100644 --- a/ml/monitor.py +++ b/ml/monitor.py @@ -263,7 +263,8 @@ def status(): 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(), 'cpu': cpu(), - 'vram_budget_gb': float(os.environ.get('VRAM_BUDGET_GB', 20)), # 本机桌面常驻占 4–5 GB,给训练的预算 19–20 GB + 'vram_reserve_gb': float(os.environ.get('VRAM_RESERVE_GIB', 1)), # 给系统留的余量(GB):训练进程上限 = 整卡 − 已用 − 这个余量(浮动,见 ml/common/vram.py) + 'vram_budget_gb': float(os.environ['VRAM_BUDGET_GB']) if os.environ.get('VRAM_BUDGET_GB') else None, # 只有显式指定才覆盖上面的口径 'pipeline': pl, 'check': CHECK.get(), 'lock': lock_info(), # 2026-10-18 第八轮修:上一版这三项被行尾注释吞掉,网页步骤条/锁状态拿不到 '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')),