test(ui): keep VRAM renderer dependency-light

This commit is contained in:
Joseph Magly
2026-08-23 20:13:16 -04:00
parent 5ce65e8193
commit 3aaa7e563c
4 changed files with 132 additions and 127 deletions
+2 -58
View File
@@ -59,6 +59,7 @@ import gradio as gr
import torch
from obliteratus import device as dev
from obliteratus.credential_sources import resolve_first, resolve_secret, secret_available
from obliteratus.ui_vram import render_vram_html
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
# ── ZeroGPU support ─────────────────────────────────────────────────
@@ -936,64 +937,7 @@ def _cleanup_disk(
def _get_vram_html() -> str:
"""Return per-device GPU/accelerator memory usage as styled bars."""
if not dev.is_gpu_available():
return (
'<div style="text-align:center;color:#4a5568;font-size:0.72rem;'
'letter-spacing:1px;margin-top:6px;">CPU ONLY — NO GPU DETECTED</div>'
)
try:
cuda_count = dev.device_count() if dev.is_cuda() else 0
device_indices = range(cuda_count) if cuda_count else (None,)
rows = []
for device_index in device_indices:
query_index = 0 if device_index is None else device_index
device_key = "single" if device_index is None else device_index
mem = dev.get_memory_info(query_index)
used = mem.used_gb
total = mem.total_gb
pct = (used / total * 100) if total > 0 else 0
# Color shifts from green → yellow → red
if pct < 50:
bar_color = "#00ff41"
elif pct < 80:
bar_color = "#ffcc00"
else:
bar_color = "#ff003c"
device_name = (
f"GPU {device_index} · {mem.device_name}"
if device_index is not None
else mem.device_name
)
reserved_html = (
f'<span style="color:#4a5568;">reserved: {mem.reserved_gb:.1f} GB</span>'
if mem.reserved_gb > 0
else '<span style="color:#4a5568;">unified memory</span>'
)
rows.append(
f'<div data-device-index="{device_key}" '
f'style="margin:6px auto 0;max-width:480px;">'
f'<div style="display:flex;justify-content:space-between;font-size:0.68rem;'
f'color:#4a5568;letter-spacing:1px;margin-bottom:2px;">'
f'<span>{device_name}</span>'
f'<span>{used:.1f} / {total:.1f} GB ({pct:.0f}%)</span></div>'
f'<div style="background:#0a0a0f;border:1px solid #1a1f2e;border-radius:3px;'
f'height:10px;overflow:hidden;">'
f'<div style="width:{min(pct, 100):.1f}%;height:100%;background:{bar_color};'
f'box-shadow:0 0 6px {bar_color};transition:width 0.5s ease;"></div></div>'
f'<div style="display:flex;justify-content:space-between;font-size:0.6rem;'
f'color:#333;margin-top:1px;">'
f'{reserved_html}</div>'
f'</div>'
)
if cuda_count > 1:
rows.append(
'<div style="text-align:center;color:#4a5568;font-size:0.6rem;'
'margin-top:4px;">Automatic sharding uses additional GPUs as model size '
'requires; smaller models may remain on GPU 0.</div>'
)
return "".join(rows)
except Exception:
return '<div style="text-align:center;color:#4a5568;font-size:0.72rem;">Memory: unavailable</div>'
return render_vram_html(dev)
# ---------------------------------------------------------------------------
+1
View File
@@ -28,6 +28,7 @@
"obliteratus/evaluation/__init__.py",
"obliteratus/models/__init__.py",
"obliteratus/reporting/__init__.py",
"obliteratus/ui_vram.py",
"obliteratus/strategies/__init__.py"
],
"required_tests": [
+66
View File
@@ -0,0 +1,66 @@
"""HTML rendering for accelerator-memory status."""
from __future__ import annotations
from typing import Any
def render_vram_html(device: Any) -> str:
"""Return per-device GPU/accelerator memory usage as styled bars."""
if not device.is_gpu_available():
return (
'<div style="text-align:center;color:#4a5568;font-size:0.72rem;'
'letter-spacing:1px;margin-top:6px;">CPU ONLY — NO GPU DETECTED</div>'
)
try:
cuda_count = device.device_count() if device.is_cuda() else 0
device_indices = range(cuda_count) if cuda_count else (None,)
rows = []
for device_index in device_indices:
query_index = 0 if device_index is None else device_index
device_key = "single" if device_index is None else device_index
mem = device.get_memory_info(query_index)
used = mem.used_gb
total = mem.total_gb
pct = (used / total * 100) if total > 0 else 0
if pct < 50:
bar_color = "#00ff41"
elif pct < 80:
bar_color = "#ffcc00"
else:
bar_color = "#ff003c"
device_name = (
f"GPU {device_index} · {mem.device_name}"
if device_index is not None
else mem.device_name
)
reserved_html = (
f'<span style="color:#4a5568;">reserved: {mem.reserved_gb:.1f} GB</span>'
if mem.reserved_gb > 0
else '<span style="color:#4a5568;">unified memory</span>'
)
rows.append(
f'<div data-device-index="{device_key}" '
f'style="margin:6px auto 0;max-width:480px;">'
f'<div style="display:flex;justify-content:space-between;font-size:0.68rem;'
f'color:#4a5568;letter-spacing:1px;margin-bottom:2px;">'
f'<span>{device_name}</span>'
f'<span>{used:.1f} / {total:.1f} GB ({pct:.0f}%)</span></div>'
f'<div style="background:#0a0a0f;border:1px solid #1a1f2e;border-radius:3px;'
f'height:10px;overflow:hidden;">'
f'<div style="width:{min(pct, 100):.1f}%;height:100%;background:{bar_color};'
f'box-shadow:0 0 6px {bar_color};transition:width 0.5s ease;"></div></div>'
f'<div style="display:flex;justify-content:space-between;font-size:0.6rem;'
f'color:#333;margin-top:1px;">'
f'{reserved_html}</div>'
f'</div>'
)
if cuda_count > 1:
rows.append(
'<div style="text-align:center;color:#4a5568;font-size:0.6rem;'
'margin-top:4px;">Automatic sharding uses additional GPUs as model size '
'requires; smaller models may remain on GPU 0.</div>'
)
return "".join(rows)
except Exception:
return '<div style="text-align:center;color:#4a5568;font-size:0.72rem;">Memory: unavailable</div>'
+63 -69
View File
@@ -1,20 +1,13 @@
"""Deterministic contracts for the Gradio accelerator-memory display."""
"""Deterministic contracts for the accelerator-memory display."""
from __future__ import annotations
import subprocess
import sys
def test_vram_html_covers_cpu_single_and_multi_accelerator_topologies():
"""Exercise app state away from Gradio's import-time worker sockets."""
script = r'''
from types import SimpleNamespace
import app
from obliteratus.ui_vram import render_vram_html
def memory(used, reserved, total, name):
def _memory(used, reserved, total, name):
return SimpleNamespace(
used_gb=used,
reserved_gb=reserved,
@@ -23,65 +16,66 @@ def memory(used, reserved, total, name):
)
# Zero accelerators preserves the CPU-only message.
app.dev.is_gpu_available = lambda: False
html = app._get_vram_html()
assert "CPU ONLY — NO GPU DETECTED" in html
assert "data-device-index" not in html
def test_vram_html_covers_cpu_single_and_multi_accelerator_topologies():
device = SimpleNamespace(is_gpu_available=lambda: False)
html = render_vram_html(device)
assert "CPU ONLY — NO GPU DETECTED" in html
assert "data-device-index" not in html
# A single non-CUDA accelerator preserves one unified-memory row.
app.dev.is_gpu_available = lambda: True
app.dev.is_cuda = lambda: False
app.dev.get_memory_info = lambda _index=0: memory(4, 0, 16, "Apple M3 (MPS)")
html = app._get_vram_html()
assert html.count("data-device-index=") == 1
assert 'data-device-index="single"' in html
assert "Apple M3 (MPS)" in html
assert "4.0 / 16.0 GB (25%)" in html
assert "Automatic sharding" not in html
# Homogeneous CUDA devices each receive an indexed row and query.
memories = [
memory(4, 5, 80, "NVIDIA A100"),
memory(16, 20, 80, "NVIDIA A100"),
memory(72, 74, 80, "NVIDIA A100"),
]
queried = []
app.dev.is_cuda = lambda: True
app.dev.device_count = lambda: len(memories)
def memory_info(index):
queried.append(index)
return memories[index]
app.dev.get_memory_info = memory_info
html = app._get_vram_html()
assert queried == [0, 1, 2]
assert html.count("data-device-index=") == 3
assert "GPU 0 · NVIDIA A100" in html
assert "GPU 1 · NVIDIA A100" in html
assert "GPU 2 · NVIDIA A100" in html
assert "Automatic sharding uses additional GPUs" in html
# Heterogeneous device names stay aligned with their CUDA indices.
memories = [
memory(2, 3, 24, "NVIDIA RTX 4090"),
memory(8, 9, 80, "NVIDIA A100"),
]
app.dev.device_count = lambda: len(memories)
app.dev.get_memory_info = memories.__getitem__
html = app._get_vram_html()
assert "GPU 0 · NVIDIA RTX 4090" in html
assert "GPU 1 · NVIDIA A100" in html
assert html.index("GPU 0 · NVIDIA RTX 4090") < html.index("GPU 1 · NVIDIA A100")
'''
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=120,
check=False,
device = SimpleNamespace(
is_gpu_available=lambda: True,
is_cuda=lambda: False,
get_memory_info=lambda _index=0: _memory(4, 0, 16, "Apple M3 (MPS)"),
)
html = render_vram_html(device)
assert html.count("data-device-index=") == 1
assert 'data-device-index="single"' in html
assert "Apple M3 (MPS)" in html
assert "4.0 / 16.0 GB (25%)" in html
assert "Automatic sharding" not in html
assert result.returncode == 0, result.stdout + result.stderr
memories = [
_memory(4, 5, 80, "NVIDIA A100"),
_memory(16, 20, 80, "NVIDIA A100"),
_memory(72, 74, 80, "NVIDIA A100"),
]
queried = []
def memory_info(index):
queried.append(index)
return memories[index]
device = SimpleNamespace(
is_gpu_available=lambda: True,
is_cuda=lambda: True,
device_count=lambda: len(memories),
get_memory_info=memory_info,
)
html = render_vram_html(device)
assert queried == [0, 1, 2]
assert html.count("data-device-index=") == 3
assert "GPU 0 · NVIDIA A100" in html
assert "GPU 1 · NVIDIA A100" in html
assert "GPU 2 · NVIDIA A100" in html
assert "Automatic sharding uses additional GPUs" in html
memories = [
_memory(2, 3, 24, "NVIDIA RTX 4090"),
_memory(8, 9, 80, "NVIDIA A100"),
]
device.device_count = lambda: len(memories)
device.get_memory_info = memories.__getitem__
html = render_vram_html(device)
assert "GPU 0 · NVIDIA RTX 4090" in html
assert "GPU 1 · NVIDIA A100" in html
assert html.index("GPU 0 · NVIDIA RTX 4090") < html.index("GPU 1 · NVIDIA A100")
def test_vram_html_handles_memory_errors():
device = SimpleNamespace(
is_gpu_available=lambda: True,
is_cuda=lambda: True,
device_count=lambda: 1,
get_memory_info=lambda _index: (_ for _ in ()).throw(RuntimeError("unavailable")),
)
assert "Memory: unavailable" in render_vram_html(device)