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 @@
-

GPU

-

CPU

+

GPU / CPU

@@ -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) => `
${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 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)} / ${(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 `
-
${tot}%总占用
-
核心${CP.n} 个
-
${bars}
+
${tot}%CPU
+
${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)