diff --git a/h3-console/static/index.html b/h3-console/static/index.html index af84735..9f8f609 100644 --- a/h3-console/static/index.html +++ b/h3-console/static/index.html @@ -124,6 +124,7 @@ +
@@ -161,6 +162,7 @@ + @@ -202,6 +204,7 @@ + @@ -468,6 +471,40 @@ async function submitJob(kind) { } catch (e) { errBox.textContent = '提交失败: ' + e.message; } } +// 一键清空——2026-08-24 用户要求:重跑会把整个表单灌满,之前只能一个个手动清,没有回退路径。 +// 直接恢复成 initParamSelects 那套默认值(分辨率第一项=1344×768、时长15s、Turbo关/20步/ +// seed=0),不是清空成空值——分辨率/时长/Turbo/步数这几个下拉框本来就不允许空选项。 +function clearForm(kind) { + if (!confirm('确定清空这个表单的所有内容?')) return; + document.getElementById(kind + '-prompt').value = ''; + document.getElementById(kind + '-cfg').value = ''; + document.getElementById(kind + '-negprompt').value = ''; + document.getElementById(kind + '-res').selectedIndex = 0; + syncDurOptions(kind); + const durSel = document.getElementById(kind + '-dur'); + if ([...durSel.options].some(o => o.value === '362')) durSel.value = '362'; + const turboSel = document.getElementById(kind + '-turbo'); + turboSel.value = 'false'; + turboSel.onchange(); // 联动刷新步数选项 + document.getElementById(kind + '-seed').value = '0'; + document.getElementById(kind + '-err').textContent = ''; + + if (kind === 'fl2va') { + uploaded['fl2va-first'] = null; uploaded['fl2va-last'] = null; + document.getElementById('fl2va-first').value = ''; + document.getElementById('fl2va-last').value = ''; + renderFl2vaThumbs(); + } else if (kind === 'ref2va') { + uploaded['ref2va-refs'] = []; uploaded['ref2va-videos'] = []; uploaded['ref2va-audios'] = []; + document.getElementById('ref2va-refs').value = ''; + document.getElementById('ref2va-videos-input').value = ''; + document.getElementById('ref2va-audios-input').value = ''; + renderRefThumbs(); + renderTags('ref2va-video-tags', 'ref2va-videos'); + renderTags('ref2va-audio-tags', 'ref2va-audios'); + } +} + // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交—— // 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。 @@ -765,7 +802,7 @@ async function refreshJobs() { async function refreshBackends() { try { const r = await fetch(H3_BASE + '/v1/backends'); const d = await r.json(); - document.getElementById('backends').textContent = + let text = `后端: ${d.ready}/${d.total} ready · 排队 ${d.queued} · 运行中 ${d.running}\n` + d.backends.map(b => { // b.status 只是模型健康态(ok/down),不代表这张卡是否正在跑任务—— @@ -774,6 +811,17 @@ async function refreshBackends() { const busy = b.running > 0 ? ` running×${b.running}` : (b.queued > 0 ? ` queued×${b.queued}` : ''); return `${b.backend.replace('http://127.0.0.1:','')} gpu${b.gpu} [${b.partition||'?'}] ${b.status}${busy}`; }).join('\n'); + // 2026-08-24:实时显存/利用率——之前撞过好几次OOM,事后查才发现是别的工作负载(比如旧 + // minimax-h3 四卡worker)在偷偷占显存,提交前完全看不出来。>18GB(跟22.5GB总量比,剩不到 + // 4GB)标红提醒可能会OOM。 + if ((d.gpus || []).length) { + text += '\n' + d.gpus.map(g => { + const pct = Math.round(g.memory_used_mb / g.memory_total_mb * 100); + const warn = g.memory_used_mb > 18000 ? ' ⚠' : ''; + return `GPU${g.index}: ${(g.memory_used_mb/1024).toFixed(1)}/${(g.memory_total_mb/1024).toFixed(1)}GB (${pct}%) ${g.utilization_pct}%util${warn}`; + }).join('\n'); + } + document.getElementById('backends').textContent = text; } catch (e) {} } diff --git a/raylight/scripts/server.py b/raylight/scripts/server.py index 0ca11a7..0dce30d 100644 --- a/raylight/scripts/server.py +++ b/raylight/scripts/server.py @@ -509,6 +509,29 @@ def _start(): threading.Thread(target=worker, daemon=True).start() +def _gpu_status() -> list: + # 2026-08-24:给 console 首页显示实时显存/利用率——之前撞过好几次OOM,后来发现是别的 + # 工作负载(比如旧 minimax-h3 四卡worker)在偷偷抢显存,提交任务前完全看不出来。 + # 直接读 nvidia-smi,不缓存,每次 /health 请求都是当下的真实数字。 + try: + out = subprocess.run( + ["nvidia-smi", "--query-gpu=index,memory.used,memory.total,utilization.gpu", + "--format=csv,noheader,nounits"], + capture_output=True, text=True, timeout=3, check=True, + ).stdout + except Exception: + return [] + gpus = [] + for line in out.strip().splitlines(): + parts = [x.strip() for x in line.split(",")] + if len(parts) != 4: + continue + idx, used, total, util = parts + gpus.append({"index": int(idx), "memory_used_mb": int(used), + "memory_total_mb": int(total), "utilization_pct": int(util)}) + return gpus + + @app.get("/health") def health(): running = sum(1 for j in JOBS.values() if j.status == "running") @@ -528,6 +551,7 @@ def health(): "ready": ready, "total": 1, "queued": Q.qsize(), "running": running, "backends": [backend], + "gpus": _gpu_status(), }