diff --git a/Plugin/unraid/Tools/api_cache_writer.php b/Plugin/unraid/Tools/api_cache_writer.php index 29bf704..1827856 100644 --- a/Plugin/unraid/Tools/api_cache_writer.php +++ b/Plugin/unraid/Tools/api_cache_writer.php @@ -27,6 +27,7 @@ $monitor = [ 'mem' => vv_memory_breakdown(), 'net' => vv_network_stats(), 'gpu' => vv_gpu_stats(), + 'gpus' => vv_gpu_stats_all(), 'gpu_procs' => vv_gpu_processes(), 'containers' => vv_docker_containers(), 'stopped' => vv_docker_stopped(), diff --git a/Plugin/unraid/api/monitor.php b/Plugin/unraid/api/monitor.php index 16406b0..bdd8c4c 100644 --- a/Plugin/unraid/api/monitor.php +++ b/Plugin/unraid/api/monitor.php @@ -25,6 +25,7 @@ echo json_encode([ 'mem' => vv_memory_breakdown(), 'net' => vv_network_stats(), 'gpu' => vv_gpu_stats(), + 'gpus' => vv_gpu_stats_all(), 'gpu_procs' => vv_gpu_processes(), 'containers' => vv_docker_containers(), 'stopped' => vv_docker_stopped(), diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index 4ecc5cf..deaed51 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -95,35 +95,54 @@ function vv_docker_stopped(): array { return $containers; } -function vv_gpu_stats(): array { - $out = shell_exec('nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu,power.draw,utilization.encoder,utilization.decoder --format=csv,noheader,nounits 2>/dev/null'); - if (!$out) return ['available' => false]; +// Every installed GPU, one entry per card. Parsed line by line — nvidia-smi emits one row +// per GPU, so splitting the whole output on commas (as this once did) runs the rows together +// and only ever describes GPU 0. +function vv_gpu_stats_all(): array { + $out = shell_exec('nvidia-smi --query-gpu=index,uuid,name,memory.used,memory.total,utilization.gpu,temperature.gpu,power.draw,utilization.encoder,utilization.decoder --format=csv,noheader,nounits 2>/dev/null'); + if (!$out) return []; - $parts = array_map('trim', explode(',', $out)); - $power = is_numeric($parts[5] ?? '') ? round((float)$parts[5], 1) : null; - return [ - 'available' => true, - 'name' => $parts[0] ?? '', - 'memory_used' => (int)($parts[1] ?? 0), - 'memory_total' => (int)($parts[2] ?? 0), - 'utilization' => (int)($parts[3] ?? 0), - 'temperature' => (int)($parts[4] ?? 0), - 'power_w' => $power, - 'enc_pct' => (int)($parts[6] ?? 0), - 'dec_pct' => (int)($parts[7] ?? 0), - ]; + $gpus = []; + foreach (explode("\n", trim($out)) as $line) { + if (!$line) continue; + $p = array_map('trim', explode(',', $line)); + if (count($p) < 10) continue; + $gpus[] = [ + 'available' => true, + 'index' => (int)$p[0], + 'uuid' => $p[1], + 'name' => $p[2], + 'memory_used' => (int)$p[3], + 'memory_total' => (int)$p[4], + 'utilization' => (int)$p[5], + 'temperature' => (int)$p[6], + 'power_w' => is_numeric($p[7]) ? round((float)$p[7], 1) : null, + 'enc_pct' => (int)$p[8], + 'dec_pct' => (int)$p[9], + ]; + } + return $gpus; } +// Kept for the existing single-GPU consumers — same shape as before, always GPU 0. +function vv_gpu_stats(): array { + $gpus = vv_gpu_stats_all(); + return $gpus[0] ?? ['available' => false]; +} + +// gpu_uuid is included so each process can be attributed to the card it is actually running +// on. Without it a two-GPU box shows every process under every card. function vv_gpu_processes(): array { - $out = shell_exec('nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv,noheader,nounits 2>/dev/null'); + $out = shell_exec('nvidia-smi --query-compute-apps=gpu_uuid,pid,used_gpu_memory,name --format=csv,noheader,nounits 2>/dev/null'); $procs = []; foreach (explode("\n", trim($out ?? '')) as $line) { if (!$line) continue; $parts = array_map('trim', explode(',', $line)); $procs[] = [ - 'pid' => $parts[0] ?? '', - 'memory_mb' => $parts[1] ?? '', - 'name' => $parts[2] ?? '', + 'gpu_uuid' => $parts[0] ?? '', + 'pid' => $parts[1] ?? '', + 'memory_mb' => $parts[2] ?? '', + 'name' => $parts[3] ?? '', ]; } return $procs; diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 6adcd55..633452e 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -104,7 +104,7 @@
Loading...
- +

@@ -119,14 +119,25 @@

- GPU + GPU

Loading...

-
+
+

+ + + GPU 1 + + +

+
Loading...
+
+ +

@@ -1494,9 +1505,30 @@ function vvPollMonitor() { })(); // ── GPU ───────────────────────────────────────────────────────────────── - const gpu = d.gpu ?? {}; - const gpuProcs = d.gpu_procs ?? []; - if (gpu.available) { + // One card per installed GPU. Falls back to the legacy single-GPU payload so the + // page still renders against a cached response written before 'gpus' existed. + const gpuList = (d.gpus && d.gpus.length) ? d.gpus + : ((d.gpu && d.gpu.available) ? [d.gpu] : []); + const allProcs = d.gpu_procs ?? []; + + const renderGpuCard = (gpu, bodyId, labelId, fallbackLabel) => { + const bodyEl = document.getElementById(bodyId); + const labelEl = document.getElementById(labelId); + if (!bodyEl) return; + + if (!gpu || !gpu.available) { + if (labelEl) labelEl.textContent = fallbackLabel; + bodyEl.innerHTML = '

No GPU detected

'; + return; + } + + // Attribute processes to this card by UUID. Older payloads have no gpu_uuid on the + // process rows — in that case only the first card claims them, rather than every + // card showing the same list. + const gpuProcs = allProcs.filter(p => + p.gpu_uuid ? p.gpu_uuid === gpu.uuid : (gpu.index ?? 0) === 0 + ); + const vramPct = gpu.memory_total > 0 ? Math.round(gpu.memory_used / gpu.memory_total * 100) : 0; const utilPct = gpu.utilization ?? 0; const temp = gpu.temperature ?? 0; @@ -1505,7 +1537,9 @@ function vvPollMonitor() { const procCount = gpuProcs.length; const procColor = procCount > 0 ? '#4caf50' : '#555'; - // Process list — which apps are actually using the GPU (name + VRAM) + if (labelEl) labelEl.textContent = 'GPU ' + (gpu.index ?? 0); + + // Process list — which apps are actually using this GPU (name + VRAM) let gpuProcHtml = ''; if (procCount > 0) { const rows = gpuProcs.map(p => { @@ -1521,7 +1555,7 @@ function vvPollMonitor() {

`; } - document.getElementById('vv-gpu-body').innerHTML = + bodyEl.innerHTML = // header row: name + process count pill `
${gpu.name}
@@ -1538,9 +1572,19 @@ function vvPollMonitor() { Power ${powerStr}
` + gpuProcHtml; - } else { - document.getElementById('vv-gpu-body').innerHTML = '

No GPU detected

'; - } + }; + + renderGpuCard(gpuList[0], 'vv-gpu-body', 'vv-gpu-label', 'GPU'); + renderGpuCard(gpuList[1], 'vv-gpu1-body', 'vv-gpu1-label', 'GPU 1'); + + // Row 3 is 8 columns wide: Rsync(1) + GPU0(1) + GPU1(1) + Transcode(1) + Streams(4). + // On a single-GPU host the second card is hidden and Transcode widens back to 2 so the + // row still fills exactly — otherwise it would leave a one-column hole. + const gpu1Card = document.getElementById('vv-gpu1-card'); + const transcodeCard = document.getElementById('vv-transcode'); + const twoGpus = gpuList.length > 1; + if (gpu1Card) gpu1Card.style.display = twoGpus ? '' : 'none'; + if (transcodeCard) transcodeCard.style.gridColumn = twoGpus ? 'span 1' : 'span 2'; // ── Scripts ───────────────────────────────────────────────────────────── vvLastScripts = d.scripts ?? {};