Commit 62e2f079608671976fdbfaf5352d832e236b22f6

Authored by yibo
1 parent 27392c5d

console 左下角加实时 GPU 显存/利用率显示

之前撞过好几次 CUDA OOM,事后查 nvidia-smi 才发现是别的工作负载(比如旧 minimax-h3 四卡
worker)在偷偷抢显存,提交任务前完全看不出来。raylight/server.py 的 /health 新增 gpus 字段
(直接读 nvidia-smi,不缓存),console 的 /v1/backends 是纯透传所以不用改,前端在原来的
"后端: ready/排队/运行中"下面加每张卡的显存占用(GB)/百分比/利用率,超过18GB标⚠。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
h3-console/static/index.html
... ... @@ -124,6 +124,7 @@
124 124 <textarea id="text-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea>
125 125 </details>
126 126 <button class="submit" onclick="submitJob('text')">提交</button>
  127 + <button type="button" style="margin-top:12px;margin-left:8px;padding:10px 20px" onclick="clearForm('text')">清空</button>
127 128 <div class="err" id="text-err"></div>
128 129 </fieldset>
129 130  
... ... @@ -161,6 +162,7 @@
161 162 <textarea id="fl2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea>
162 163 </details>
163 164 <button class="submit" onclick="submitJob('fl2va')">提交</button>
  165 + <button type="button" style="margin-top:12px;margin-left:8px;padding:10px 20px" onclick="clearForm('fl2va')">清空</button>
164 166 <div class="err" id="fl2va-err"></div>
165 167 </fieldset>
166 168  
... ... @@ -202,6 +204,7 @@
202 204 <textarea id="ref2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea>
203 205 </details>
204 206 <button class="submit" onclick="submitJob('ref2va')">提交</button>
  207 + <button type="button" style="margin-top:12px;margin-left:8px;padding:10px 20px" onclick="clearForm('ref2va')">清空</button>
205 208 <div class="err" id="ref2va-err"></div>
206 209 </fieldset>
207 210 </div>
... ... @@ -468,6 +471,40 @@ async function submitJob(kind) {
468 471 } catch (e) { errBox.textContent = '提交失败: ' + e.message; }
469 472 }
470 473  
  474 +// 一键清空——2026-08-24 用户要求:重跑会把整个表单灌满,之前只能一个个手动清,没有回退路径。
  475 +// 直接恢复成 initParamSelects 那套默认值(分辨率第一项=1344×768、时长15s、Turbo关/20步/
  476 +// seed=0),不是清空成空值——分辨率/时长/Turbo/步数这几个下拉框本来就不允许空选项。
  477 +function clearForm(kind) {
  478 + if (!confirm('确定清空这个表单的所有内容?')) return;
  479 + document.getElementById(kind + '-prompt').value = '';
  480 + document.getElementById(kind + '-cfg').value = '';
  481 + document.getElementById(kind + '-negprompt').value = '';
  482 + document.getElementById(kind + '-res').selectedIndex = 0;
  483 + syncDurOptions(kind);
  484 + const durSel = document.getElementById(kind + '-dur');
  485 + if ([...durSel.options].some(o => o.value === '362')) durSel.value = '362';
  486 + const turboSel = document.getElementById(kind + '-turbo');
  487 + turboSel.value = 'false';
  488 + turboSel.onchange(); // 联动刷新步数选项
  489 + document.getElementById(kind + '-seed').value = '0';
  490 + document.getElementById(kind + '-err').textContent = '';
  491 +
  492 + if (kind === 'fl2va') {
  493 + uploaded['fl2va-first'] = null; uploaded['fl2va-last'] = null;
  494 + document.getElementById('fl2va-first').value = '';
  495 + document.getElementById('fl2va-last').value = '';
  496 + renderFl2vaThumbs();
  497 + } else if (kind === 'ref2va') {
  498 + uploaded['ref2va-refs'] = []; uploaded['ref2va-videos'] = []; uploaded['ref2va-audios'] = [];
  499 + document.getElementById('ref2va-refs').value = '';
  500 + document.getElementById('ref2va-videos-input').value = '';
  501 + document.getElementById('ref2va-audios-input').value = '';
  502 + renderRefThumbs();
  503 + renderTags('ref2va-video-tags', 'ref2va-videos');
  504 + renderTags('ref2va-audio-tags', 'ref2va-audios');
  505 + }
  506 +}
  507 +
471 508 // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、
472 509 // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交——
473 510 // 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。
... ... @@ -765,7 +802,7 @@ async function refreshJobs() {
765 802 async function refreshBackends() {
766 803 try {
767 804 const r = await fetch(H3_BASE + '/v1/backends'); const d = await r.json();
768   - document.getElementById('backends').textContent =
  805 + let text =
769 806 `后端: ${d.ready}/${d.total} ready · 排队 ${d.queued} · 运行中 ${d.running}\n` +
770 807 d.backends.map(b => {
771 808 // b.status 只是模型健康态(ok/down),不代表这张卡是否正在跑任务——
... ... @@ -774,6 +811,17 @@ async function refreshBackends() {
774 811 const busy = b.running > 0 ? ` running×${b.running}` : (b.queued > 0 ? ` queued×${b.queued}` : '');
775 812 return `${b.backend.replace('http://127.0.0.1:','')} gpu${b.gpu} [${b.partition||'?'}] ${b.status}${busy}`;
776 813 }).join('\n');
  814 + // 2026-08-24:实时显存/利用率——之前撞过好几次OOM,事后查才发现是别的工作负载(比如旧
  815 + // minimax-h3 四卡worker)在偷偷占显存,提交前完全看不出来。>18GB(跟22.5GB总量比,剩不到
  816 + // 4GB)标红提醒可能会OOM。
  817 + if ((d.gpus || []).length) {
  818 + text += '\n' + d.gpus.map(g => {
  819 + const pct = Math.round(g.memory_used_mb / g.memory_total_mb * 100);
  820 + const warn = g.memory_used_mb > 18000 ? ' ⚠' : '';
  821 + 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}`;
  822 + }).join('\n');
  823 + }
  824 + document.getElementById('backends').textContent = text;
777 825 } catch (e) {}
778 826 }
779 827  
... ...
raylight/scripts/server.py
... ... @@ -509,6 +509,29 @@ def _start():
509 509 threading.Thread(target=worker, daemon=True).start()
510 510  
511 511  
  512 +def _gpu_status() -> list:
  513 + # 2026-08-24:给 console 首页显示实时显存/利用率——之前撞过好几次OOM,后来发现是别的
  514 + # 工作负载(比如旧 minimax-h3 四卡worker)在偷偷抢显存,提交任务前完全看不出来。
  515 + # 直接读 nvidia-smi,不缓存,每次 /health 请求都是当下的真实数字。
  516 + try:
  517 + out = subprocess.run(
  518 + ["nvidia-smi", "--query-gpu=index,memory.used,memory.total,utilization.gpu",
  519 + "--format=csv,noheader,nounits"],
  520 + capture_output=True, text=True, timeout=3, check=True,
  521 + ).stdout
  522 + except Exception:
  523 + return []
  524 + gpus = []
  525 + for line in out.strip().splitlines():
  526 + parts = [x.strip() for x in line.split(",")]
  527 + if len(parts) != 4:
  528 + continue
  529 + idx, used, total, util = parts
  530 + gpus.append({"index": int(idx), "memory_used_mb": int(used),
  531 + "memory_total_mb": int(total), "utilization_pct": int(util)})
  532 + return gpus
  533 +
  534 +
512 535 @app.get("/health")
513 536 def health():
514 537 running = sum(1 for j in JOBS.values() if j.status == "running")
... ... @@ -528,6 +551,7 @@ def health():
528 551 "ready": ready, "total": 1,
529 552 "queued": Q.qsize(), "running": running,
530 553 "backends": [backend],
  554 + "gpus": _gpu_status(),
531 555 }
532 556  
533 557  
... ...