fix(monitor): CPU 占用读数改为后台每秒独立采样(此前在请求里算差值,网页 1 秒轮询会把窗口压成几毫秒 → 恒为 0);CPU 并进 GPU 同一个卡(上 GPU 环+显存/温度/功耗,下 CPU 环+28 核逐核条)

实测:空闲 16.8% → 14 个满载循环 71.7% → 结束 19.1%,随负载单调;截图复核合并卡排版正常。
This commit is contained in:
cbb
2026-10-18 15:13:37 +08:00
parent 9ed213417b
commit 83dcc1c3ee
2 changed files with 38 additions and 32 deletions
+18 -20
View File
@@ -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 @@
<section class="hero">
<div class="card" id="pipe"></div>
<div class="card"><h2><span>GPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
<div class="card"><h2><span>CPU</span><span class="r" id="cputime"></span></h2><div id="cpu"></div></div>
<div class="card"><h2><span>GPU / CPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
</section>
<section class="card" id="prog"></section>
@@ -534,35 +534,33 @@ function renderStatus(s) {
$('steps').innerHTML = L.map(([nm, st, ds], i) => `<div class="step ${st}"><div class="ic">${st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1}</div><div class="nm">${nm}</div><div class="ds">${ds}</div></div>`).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 = '<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">';
const gr = (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>`;
$('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) => `<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)} / ${(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="gpurows">${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'))}</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');
// 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) => `<div title="核 ${i + 1}: ${v}%" style="flex:1;min-width:0;height:100%;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.25 + 0.75 * Math.min(1, v / 100)}"></div>`).join('');
const bars = (CP.per || []).map((v, i) => `<div title="核 ${i + 1}: ${v}%" style="flex:1;min-width:0;height:100%;border-radius:2px;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.18 + 0.82 * Math.min(1, v / 100)}"></div>`).join('');
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 - tot / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b>${tot}%</b><span>总占用</span></div></div></div>
<div class="gpurows"><div class="gr"><div class="l"><span>核心</span><b>${CP.n} 个</b></div><div class="mini"><div style="width:${Math.min(100, tot)}%;background:${col}"></div></div></div>
<div style="display:flex;gap:2px;height:26px;margin-top:6px">${bars}</div></div></div>
<div class="c"><div><b>${tot}%</b><span>CPU</span></div></div></div>
<div class="gpurows">${gr('占用', `${tot}% · ${CP.n} 核`, tot, col)}
<div class="cores">${bars}</div></div></div>
<div class="gname">每核占用(深色=忙)</div>`;
})() : empty('没装 psutil,无法读 CPU');
})() : '';
$('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil');
// 数据
const D = s.data || {}, st = D.stats, ix = D.index;
+20 -12
View File
@@ -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)