Add a second GPU card to the monitor page
The 3080 was invisible because the stats parser only ever described GPU 0.
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<div id="vv-docker-folders-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Rsync | GPU | Transcode | Streams -->
|
||||
<!-- Row 3: Rsync | GPU 0 | GPU 1 | Transcode | Streams -->
|
||||
<div class="vv-card" id="vv-rsync-card" style="grid-column:span 1;">
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
@@ -119,14 +119,25 @@
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
|
||||
GPU
|
||||
<span id="vv-gpu-label">GPU</span>
|
||||
</span>
|
||||
<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-gpu-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-transcode" style="grid-column:span 2;">
|
||||
<div class="vv-card" id="vv-gpu1-card" style="grid-column:span 1;">
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
|
||||
<span id="vv-gpu1-label">GPU 1</span>
|
||||
</span>
|
||||
<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-gpu1-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-transcode" style="grid-column:span 1;">
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.5" y="1" width="4" height="10" rx="0.8"/><line x1="0.5" y1="3.5" x2="2.3" y2="3.5"/><line x1="0.5" y1="8.5" x2="2.3" y2="8.5"/><line x1="2.7" y1="3.5" x2="4.5" y2="3.5"/><line x1="2.7" y1="8.5" x2="4.5" y2="8.5"/><line x1="6" y1="6" x2="9.5" y2="6"/><polyline points="8.2,4.3 10.2,6 8.2,7.7"/><rect x="11" y="1" width="2.5" height="10" rx="0.5"/></svg></span>
|
||||
@@ -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 = '<p style="color:#555;font-style:italic">No GPU detected</p>';
|
||||
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() {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.getElementById('vv-gpu-body').innerHTML =
|
||||
bodyEl.innerHTML =
|
||||
// header row: name + process count pill
|
||||
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||||
<div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${gpu.name}</div>
|
||||
@@ -1538,9 +1572,19 @@ function vvPollMonitor() {
|
||||
<span style="color:#666;margin-left:12px;">Power</span>
|
||||
<span style="color:#aaa;font-weight:bold;">${powerStr}</span>
|
||||
</div>` + gpuProcHtml;
|
||||
} else {
|
||||
document.getElementById('vv-gpu-body').innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
|
||||
}
|
||||
};
|
||||
|
||||
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 ?? {};
|
||||
|
||||
Reference in New Issue
Block a user