From 83dcc1c3ee0a9a2d6ab649cb78718ceb06e9ba78 Mon Sep 17 00:00:00 2001
From: cbb <2625274797@qq.com>
Date: Sun, 18 Oct 2026 15:13:37 +0800
Subject: [PATCH] =?UTF-8?q?fix(monitor):=20CPU=20=E5=8D=A0=E7=94=A8?=
=?UTF-8?q?=E8=AF=BB=E6=95=B0=E6=94=B9=E4=B8=BA=E5=90=8E=E5=8F=B0=E6=AF=8F?=
=?UTF-8?q?=E7=A7=92=E7=8B=AC=E7=AB=8B=E9=87=87=E6=A0=B7=EF=BC=88=E6=AD=A4?=
=?UTF-8?q?=E5=89=8D=E5=9C=A8=E8=AF=B7=E6=B1=82=E9=87=8C=E7=AE=97=E5=B7=AE?=
=?UTF-8?q?=E5=80=BC=EF=BC=8C=E7=BD=91=E9=A1=B5=201=20=E7=A7=92=E8=BD=AE?=
=?UTF-8?q?=E8=AF=A2=E4=BC=9A=E6=8A=8A=E7=AA=97=E5=8F=A3=E5=8E=8B=E6=88=90?=
=?UTF-8?q?=E5=87=A0=E6=AF=AB=E7=A7=92=20=E2=86=92=20=E6=81=92=E4=B8=BA=20?=
=?UTF-8?q?0=EF=BC=89=EF=BC=9BCPU=20=E5=B9=B6=E8=BF=9B=20GPU=20=E5=90=8C?=
=?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=8D=A1=EF=BC=88=E4=B8=8A=20GPU=20=E7=8E=AF?=
=?UTF-8?q?+=E6=98=BE=E5=AD=98/=E6=B8=A9=E5=BA=A6/=E5=8A=9F=E8=80=97?=
=?UTF-8?q?=EF=BC=8C=E4=B8=8B=20CPU=20=E7=8E=AF+28=20=E6=A0=B8=E9=80=90?=
=?UTF-8?q?=E6=A0=B8=E6=9D=A1=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
实测:空闲 16.8% → 14 个满载循环 71.7% → 结束 19.1%,随负载单调;截图复核合并卡排版正常。
---
ml/monitor.html | 38 ++++++++++++++++++--------------------
ml/monitor.py | 32 ++++++++++++++++++++------------
2 files changed, 38 insertions(+), 32 deletions(-)
diff --git a/ml/monitor.html b/ml/monitor.html
index 14ab6d8..2cfea17 100644
--- a/ml/monitor.html
+++ b/ml/monitor.html
@@ -84,6 +84,7 @@
.ring .c { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
.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); }
.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; }
@@ -171,8 +172,7 @@
@@ -534,35 +534,33 @@ function renderStatus(s) {
$('steps').innerHTML = L.map(([nm, st, ds], i) => `
${st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1}
${nm}
${ds}
`).join('');
const nDone = L.filter(x => x[1] === 'done').length; $('stepnote').textContent = `${nDone} / ${L.length} 完成`;
- // GPU
- const G = s.gpu || [];
+ // 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 看
- $('gputime').textContent = G.length ? '实时' : '';
- $('gpu').innerHTML = G.length ? G.map(g => {
+ const hr = '
';
+ const gr = (k, v, p, c) => ``;
+ $('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 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) => ``;
return `
-
${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'))}
+ ${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.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('
') : empty('未检测到 nvidia-smi');
-
- // CPU(psutil;没装就整块不显示)
- const CP = s.cpu || {};
- $('cputime').textContent = CP.n ? '实时' : '';
- $('cpu').innerHTML = CP.n ? (() => {
+ }).join(hr) : '';
+ const cpuBlk = CP.n ? (() => {
const tot = CP.total ?? 0, C = 2 * Math.PI * 50, col = tot > 85 ? css('--bad') : tot > 60 ? css('--warn') : css('--ok');
- const bars = (CP.per || []).map((v, i) => ``).join('');
+ const bars = (CP.per || []).map((v, i) => ``).join('');
return `
+
+ ${gr('占用', `${tot}% · ${CP.n} 核`, tot, col)}
+
${bars}
每核占用(深色=忙)
`;
- })() : empty('没装 psutil,无法读 CPU');
+ })() : '';
+ $('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil');
// 数据
const D = s.data || {}, st = D.stats, ix = D.index;
diff --git a/ml/monitor.py b/ml/monitor.py
index b021b4b..c56a21a 100644
--- a/ml/monitor.py
+++ b/ml/monitor.py
@@ -68,21 +68,28 @@ def read_log(name, offset):
return {'records': recs, 'offset': offset + end, 'size': size, 'reset': offset == 0, 'mtime': os.path.getmtime(f)}
-_CPU_CACHE = {'t': 0.0, 'v': {}}
+_CPU = {'v': {}}
+
+
+class CpuSampler(threading.Thread):
+ """后台每秒阻塞采样一次 CPU。cpu_percent 是"距上次调用"的差值:放在请求里算,窗口取决于两次
+ HTTP 请求的间隔(网页 1 秒轮询 + 手动 curl 会把它压成几毫秒,读数恒为 0)——所以单独一个线程按固定
+ 1 秒窗口取,status 只读最近一次。"""
+ def __init__(self): super().__init__(daemon=True); self.start()
+
+ def run(self):
+ try:
+ import psutil
+ psutil.cpu_percent(interval=None, percpu=True) # 打桩:第一次恒为 0,丢掉
+ while True:
+ per = psutil.cpu_percent(interval=1.0, percpu=True)
+ _CPU['v'] = {'total': round(sum(per) / max(1, len(per)), 1), 'per': [round(x) for x in per], 'n': len(per)}
+ except Exception:
+ _CPU['v'] = {}
def cpu():
- """CPU 占用:总百分比 + 每核(psutil 已装;没装就返回空,页面不显示这块)。"""
- if time.time() - _CPU_CACHE['t'] < 1.0: return _CPU_CACHE['v']
- try:
- import psutil
- per = psutil.cpu_percent(interval=None, percpu=True)
- out = {'total': round(sum(per) / max(1, len(per)), 1), 'per': [round(x, 0) for x in per], 'n': len(per),
- 'load1': round(os.getloadavg()[0], 2) if hasattr(os, 'getloadavg') else None}
- except Exception:
- out = {}
- _CPU_CACHE.update(t=time.time(), v=out)
- return out
+ return _CPU['v']
_GPU_CACHE = {'t': 0.0, 'v': []}
@@ -465,6 +472,7 @@ def main():
a = ap.parse_args()
global CHECK
CHECK = SelfCheck()
+ CPUS = CpuSampler() # 每秒采一次 CPU(线程常驻,status 只读最近一次)
srv = ThreadingHTTPServer((a.host, a.port), H); srv.remote_control = a.remote_control
url = f'http://localhost:{a.port}/'
print(f'训练控制台:{url} (关闭本窗口只关控制台,已开始的训练继续跑;重新打开控制台即可接着看、可停止)', flush=True)