Commit 62e2f079608671976fdbfaf5352d832e236b22f6
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>
Showing
2 changed files
with
73 additions
and
1 deletions
h3-console/static/index.html
| @@ -124,6 +124,7 @@ | @@ -124,6 +124,7 @@ | ||
| 124 | <textarea id="text-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> | 124 | <textarea id="text-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> |
| 125 | </details> | 125 | </details> |
| 126 | <button class="submit" onclick="submitJob('text')">提交</button> | 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 | <div class="err" id="text-err"></div> | 128 | <div class="err" id="text-err"></div> |
| 128 | </fieldset> | 129 | </fieldset> |
| 129 | 130 | ||
| @@ -161,6 +162,7 @@ | @@ -161,6 +162,7 @@ | ||
| 161 | <textarea id="fl2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> | 162 | <textarea id="fl2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> |
| 162 | </details> | 163 | </details> |
| 163 | <button class="submit" onclick="submitJob('fl2va')">提交</button> | 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 | <div class="err" id="fl2va-err"></div> | 166 | <div class="err" id="fl2va-err"></div> |
| 165 | </fieldset> | 167 | </fieldset> |
| 166 | 168 | ||
| @@ -202,6 +204,7 @@ | @@ -202,6 +204,7 @@ | ||
| 202 | <textarea id="ref2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> | 204 | <textarea id="ref2va-negprompt" rows="2" placeholder="例如:模糊,抖动,拖影,五官变形"></textarea> |
| 203 | </details> | 205 | </details> |
| 204 | <button class="submit" onclick="submitJob('ref2va')">提交</button> | 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 | <div class="err" id="ref2va-err"></div> | 208 | <div class="err" id="ref2va-err"></div> |
| 206 | </fieldset> | 209 | </fieldset> |
| 207 | </div> | 210 | </div> |
| @@ -468,6 +471,40 @@ async function submitJob(kind) { | @@ -468,6 +471,40 @@ async function submitJob(kind) { | ||
| 468 | } catch (e) { errBox.textContent = '提交失败: ' + e.message; } | 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 | // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 | 508 | // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 |
| 472 | // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交—— | 509 | // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交—— |
| 473 | // 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。 | 510 | // 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。 |
| @@ -765,7 +802,7 @@ async function refreshJobs() { | @@ -765,7 +802,7 @@ async function refreshJobs() { | ||
| 765 | async function refreshBackends() { | 802 | async function refreshBackends() { |
| 766 | try { | 803 | try { |
| 767 | const r = await fetch(H3_BASE + '/v1/backends'); const d = await r.json(); | 804 | const r = await fetch(H3_BASE + '/v1/backends'); const d = await r.json(); |
| 768 | - document.getElementById('backends').textContent = | 805 | + let text = |
| 769 | `后端: ${d.ready}/${d.total} ready · 排队 ${d.queued} · 运行中 ${d.running}\n` + | 806 | `后端: ${d.ready}/${d.total} ready · 排队 ${d.queued} · 运行中 ${d.running}\n` + |
| 770 | d.backends.map(b => { | 807 | d.backends.map(b => { |
| 771 | // b.status 只是模型健康态(ok/down),不代表这张卡是否正在跑任务—— | 808 | // b.status 只是模型健康态(ok/down),不代表这张卡是否正在跑任务—— |
| @@ -774,6 +811,17 @@ async function refreshBackends() { | @@ -774,6 +811,17 @@ async function refreshBackends() { | ||
| 774 | const busy = b.running > 0 ? ` running×${b.running}` : (b.queued > 0 ? ` queued×${b.queued}` : ''); | 811 | const busy = b.running > 0 ? ` running×${b.running}` : (b.queued > 0 ? ` queued×${b.queued}` : ''); |
| 775 | return `${b.backend.replace('http://127.0.0.1:','')} gpu${b.gpu} [${b.partition||'?'}] ${b.status}${busy}`; | 812 | return `${b.backend.replace('http://127.0.0.1:','')} gpu${b.gpu} [${b.partition||'?'}] ${b.status}${busy}`; |
| 776 | }).join('\n'); | 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 | } catch (e) {} | 825 | } catch (e) {} |
| 778 | } | 826 | } |
| 779 | 827 |
raylight/scripts/server.py
| @@ -509,6 +509,29 @@ def _start(): | @@ -509,6 +509,29 @@ def _start(): | ||
| 509 | threading.Thread(target=worker, daemon=True).start() | 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 | @app.get("/health") | 535 | @app.get("/health") |
| 513 | def health(): | 536 | def health(): |
| 514 | running = sum(1 for j in JOBS.values() if j.status == "running") | 537 | running = sum(1 for j in JOBS.values() if j.status == "running") |
| @@ -528,6 +551,7 @@ def health(): | @@ -528,6 +551,7 @@ def health(): | ||
| 528 | "ready": ready, "total": 1, | 551 | "ready": ready, "total": 1, |
| 529 | "queued": Q.qsize(), "running": running, | 552 | "queued": Q.qsize(), "running": running, |
| 530 | "backends": [backend], | 553 | "backends": [backend], |
| 554 | + "gpus": _gpu_status(), | ||
| 531 | } | 555 | } |
| 532 | 556 | ||
| 533 | 557 |