a11621b21f
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
138 lines
5.9 KiB
TypeScript
138 lines
5.9 KiB
TypeScript
import type { GpuStatus } from '../types'
|
||
|
||
type Props = {
|
||
gpu: GpuStatus | null
|
||
compact?: boolean
|
||
}
|
||
|
||
function memPct(used: number, total: number) {
|
||
if (!total) return 0
|
||
return Math.round((used / total) * 100)
|
||
}
|
||
|
||
export function GpuPanel({ gpu, compact }: Props) {
|
||
if (!gpu) {
|
||
return (
|
||
<div className="panel rounded-2xl p-5 animate-pulse">
|
||
<div className="h-4 w-32 rounded bg-[var(--surface-elevated)] mb-4" />
|
||
<div className="h-24 rounded-xl bg-[var(--surface-elevated)]" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const online = gpu.ok
|
||
const gpus = gpu.gpus || []
|
||
const avgUtil = gpus.length ? gpus.reduce((s, g) => s + g.util_gpu, 0) / gpus.length : 0
|
||
const avgVram = gpus.length ? gpus.reduce((s, g) => s + memPct(g.memory_used_mib, g.memory_total_mib), 0) / gpus.length : 0
|
||
|
||
if (compact) {
|
||
return (
|
||
<div className="panel rounded-2xl p-4 relative overflow-hidden">
|
||
<div className="absolute -top-8 -right-8 w-24 h-24 rounded-full bg-[var(--accent-gpu)] opacity-[0.1] blur-2xl pointer-events-none" />
|
||
<div className="flex items-center justify-between gap-2 mb-3 relative">
|
||
<div className="flex items-center gap-2">
|
||
<span>⚡</span>
|
||
<h2 className="font-bold text-sm text-[var(--accent-gpu)]">GPU Lab</h2>
|
||
</div>
|
||
<a href={gpu.ui_url} target="_blank" rel="noopener noreferrer" className="text-[10px] font-mono text-[var(--accent-gpu)] hover:underline">
|
||
Open ↗
|
||
</a>
|
||
</div>
|
||
{gpu.active_model && (
|
||
<div className="text-sm font-semibold text-[var(--text)] truncate">{gpu.active_model}</div>
|
||
)}
|
||
<div className="flex gap-3 mt-2 text-[10px] font-mono text-[var(--text-muted)]">
|
||
<span>{gpu.gpu_count ?? gpus.length}× V100</span>
|
||
<span>{Math.round(avgUtil)}% util</span>
|
||
<span>{Math.round(avgVram)}% VRAM</span>
|
||
<span className={online && gpu.inference_active ? 'text-[var(--status-ok)]' : 'text-[var(--status-warn)]'}>
|
||
{online ? (gpu.inference_active ? 'ON' : 'STBY') : 'OFF'}
|
||
</span>
|
||
</div>
|
||
<div className="h-1.5 rounded-full bg-[var(--surface-muted)] mt-3 overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-500"
|
||
style={{
|
||
width: `${Math.max(avgUtil, avgVram * 0.5)}%`,
|
||
background: 'linear-gradient(90deg, var(--accent-gpu), var(--accent))',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="panel rounded-2xl p-5 relative overflow-hidden">
|
||
<div className="absolute -top-12 -right-12 w-40 h-40 rounded-full bg-[var(--accent-gpu)] opacity-[0.08] blur-2xl pointer-events-none" />
|
||
|
||
<div className="flex flex-wrap items-start justify-between gap-3 mb-4 relative">
|
||
<div>
|
||
<p className="section-eyebrow">Inference cluster</p>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg" aria-hidden>⚡</span>
|
||
<h2 className="font-display font-bold text-[var(--accent-gpu)] tracking-tight">GPU Lab</h2>
|
||
<span
|
||
className={`text-[10px] font-mono px-2 py-0.5 rounded-full border ${
|
||
online && gpu.inference_active
|
||
? 'border-[var(--status-ok)] text-[var(--status-ok)] bg-[var(--status-ok-bg)]'
|
||
: 'border-[var(--status-warn)] text-[var(--status-warn)] bg-[var(--status-warn-bg)]'
|
||
}`}
|
||
>
|
||
{online ? (gpu.inference_active ? 'INFERENCE ON' : 'STANDBY') : 'OFFLINE'}
|
||
</span>
|
||
</div>
|
||
<p className="text-xs font-mono text-[var(--text-muted)] mt-1">
|
||
atc-gpu-dev · {gpu.host} · {gpu.gpu_count ?? gpus.length}× V100
|
||
</p>
|
||
</div>
|
||
<a href={gpu.ui_url} target="_blank" rel="noopener noreferrer" className="btn-secondary text-xs shrink-0">
|
||
Open GPU Manager ↗
|
||
</a>
|
||
</div>
|
||
|
||
{gpu.active_model && (
|
||
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface-elevated)] px-4 py-3 mb-4">
|
||
<div className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-faint)]">Active model</div>
|
||
<div className="font-semibold text-[var(--text)] mt-0.5">{gpu.active_model}</div>
|
||
{gpu.vllm_url && (
|
||
<div className="text-[11px] font-mono text-[var(--text-muted)] mt-1 truncate">{gpu.vllm_url}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{!online && (
|
||
<p className="text-sm text-[var(--status-down)]">{gpu.error || 'GPU manager unreachable'}</p>
|
||
)}
|
||
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
{gpus.map((g) => {
|
||
const pct = memPct(g.memory_used_mib, g.memory_total_mib)
|
||
return (
|
||
<div key={g.index} className="gpu-card rounded-xl p-3 border border-[var(--border)]">
|
||
<div className="flex justify-between items-center mb-2">
|
||
<span className="text-xs font-mono font-semibold text-[var(--accent-gpu)]">GPU {g.index}</span>
|
||
<span className="text-[10px] font-mono text-[var(--text-faint)]">{g.temperature_c}°C · {g.power_w}W</span>
|
||
</div>
|
||
<div className="text-[11px] text-[var(--text-muted)] truncate mb-2">{g.name}</div>
|
||
<div className="flex gap-3 text-[10px] font-mono mb-1.5">
|
||
<span>Util {Math.round(g.util_gpu)}%</span>
|
||
<span>VRAM {pct}%</span>
|
||
</div>
|
||
<div className="h-1.5 rounded-full bg-[var(--surface-muted)] overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-500"
|
||
style={{
|
||
width: `${Math.max(g.util_gpu, pct * 0.3)}%`,
|
||
background: 'linear-gradient(90deg, var(--accent-gpu), var(--accent))',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|