Detect Intel integrated graphics, which nvidia-smi cannot see
HOST2 has UHD Graphics 730 and reported no GPU at all. An iGPU has no VRAM, no sensor of its own and no encode/decode split, so it is drawn from the per-engine busy figures Intel actually publishes and the fields it lacks are null rather than a confident zero.
This commit is contained in:
@@ -196,9 +196,114 @@ function vv_gpu_stats_all(): array {
|
|||||||
'power_w' => is_numeric($p[7]) ? round((float)$p[7], 1) : null,
|
'power_w' => is_numeric($p[7]) ? round((float)$p[7], 1) : null,
|
||||||
'enc_pct' => (int)$p[8],
|
'enc_pct' => (int)$p[8],
|
||||||
'dec_pct' => (int)$p[9],
|
'dec_pct' => (int)$p[9],
|
||||||
|
'vendor' => 'nvidia',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $gpus;
|
// Integrated graphics after the discrete cards, numbered on from them. A box with both shows
|
||||||
|
// both; a box with only an iGPU stops reporting that it has no GPU at all.
|
||||||
|
return array_merge($gpus, vv_igpu_stats_all(count($gpus)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intel integrated graphics, which nvidia-smi cannot see and which therefore rendered as "no GPU
|
||||||
|
// detected" on any box without a discrete card — HOST2 has UHD Graphics 730 and showed nothing.
|
||||||
|
//
|
||||||
|
// An iGPU is not a small discrete card and the difference is not cosmetic. It has no VRAM (it
|
||||||
|
// shares system memory), no temperature sensor of its own (it is inside the CPU package, which
|
||||||
|
// vv_cpu_temp() already reports), and no encode/decode split — Intel publishes per-engine busy
|
||||||
|
// figures instead: Render/3D, Blitter, Video, VideoEnhance. Those fields are returned null rather
|
||||||
|
// than zero, because 0 MB of VRAM and 0°C are claims, and "this part does not have one" is not.
|
||||||
|
//
|
||||||
|
// Utilisation comes from rc6, the idle residency percentage: busy is what is left of it. That is
|
||||||
|
// the whole-GPU number; the engine breakdown is carried alongside because on a media server the
|
||||||
|
// Video engine is the one worth watching — it is what a hardware transcode actually uses.
|
||||||
|
//
|
||||||
|
// Sampled, not read: intel_gpu_top measures over an interval, so this costs roughly the sample
|
||||||
|
// window. Memoised per request and bounded by timeout, because the cache writer calls it once a
|
||||||
|
// minute and nothing should be able to hang that.
|
||||||
|
function vv_igpu_stats_all(int $startIndex = 0): array {
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
$cache = [];
|
||||||
|
|
||||||
|
foreach ((array)@glob('/sys/class/drm/card[0-9]*') as $card) {
|
||||||
|
if (trim((string)@file_get_contents("$card/device/vendor")) !== '0x8086') continue;
|
||||||
|
|
||||||
|
$addr = basename((string)@readlink("$card/device"));
|
||||||
|
if ($addr === '') continue;
|
||||||
|
|
||||||
|
// Marketing name where lspci publishes one: "Alder Lake-S GT1 [UHD Graphics 730]" is
|
||||||
|
// better known as UHD Graphics 730, and the bracketed half is the half people recognise.
|
||||||
|
$desc = trim((string)shell_exec('lspci -s ' . escapeshellarg($addr) . ' 2>/dev/null'));
|
||||||
|
$name = 'Intel integrated graphics';
|
||||||
|
if (preg_match('/\[([^\]]+)\]/', $desc, $m)) $name = 'Intel ' . trim($m[1]);
|
||||||
|
elseif (preg_match('/controller:\s*(.+?)(?:\s*\(rev|$)/', $desc, $m)) $name = trim($m[1]);
|
||||||
|
|
||||||
|
$entry = [
|
||||||
|
'available' => true,
|
||||||
|
'vendor' => 'intel',
|
||||||
|
'index' => $startIndex + count($cache),
|
||||||
|
// Prefixed so a discrete card's processes can never be attributed to this one on a
|
||||||
|
// box that has both.
|
||||||
|
'uuid' => 'intel:' . $addr,
|
||||||
|
'name' => $name,
|
||||||
|
'memory_used' => null, // shared system memory — there is no separate pool
|
||||||
|
'memory_total' => null,
|
||||||
|
'utilization' => null,
|
||||||
|
'temperature' => null, // no die sensor of its own; it is in the CPU package
|
||||||
|
'power_w' => null,
|
||||||
|
'package_w' => null,
|
||||||
|
'enc_pct' => null, // Intel reports engines, not an encode/decode split
|
||||||
|
'dec_pct' => null,
|
||||||
|
'engines' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// The card still renders without this — name and "no sample" beats no card at all, which
|
||||||
|
// is what the box showed before.
|
||||||
|
$sample = vv_igpu_sample();
|
||||||
|
if ($sample !== null) {
|
||||||
|
$rc6 = $sample['rc6']['value'] ?? null;
|
||||||
|
if (is_numeric($rc6)) $entry['utilization'] = max(0, min(100, (int)round(100 - (float)$rc6)));
|
||||||
|
|
||||||
|
$gpuW = $sample['power']['GPU'] ?? null;
|
||||||
|
$pkgW = $sample['power']['Package'] ?? null;
|
||||||
|
// Alder Lake reports 0.00 for the GPU rail. Reported as null rather than as a
|
||||||
|
// confident zero watts, with package power carried separately and labelled as such.
|
||||||
|
if (is_numeric($gpuW) && (float)$gpuW > 0) $entry['power_w'] = round((float)$gpuW, 1);
|
||||||
|
if (is_numeric($pkgW)) $entry['package_w'] = round((float)$pkgW, 1);
|
||||||
|
|
||||||
|
foreach ((array)($sample['engines'] ?? []) as $engine => $vals) {
|
||||||
|
if (!is_array($vals) || !isset($vals['busy'])) continue;
|
||||||
|
$entry['engines'][$engine] = round((float)$vals['busy'], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache[] = $entry;
|
||||||
|
}
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One sample from intel_gpu_top, decoded, or null.
|
||||||
|
//
|
||||||
|
// -J streams an unterminated JSON array: a bare "[" and then one object per interval, forever.
|
||||||
|
// Neither half is valid JSON on its own, so the first object is cut out by hand — from its opening
|
||||||
|
// brace to the first closing brace in column 1 — and decoded alone.
|
||||||
|
//
|
||||||
|
// timeout, and a short window: this runs inside the once-a-minute cache write and a sampler that
|
||||||
|
// never returns would take the whole payload with it.
|
||||||
|
function vv_igpu_sample(): ?array {
|
||||||
|
static $sample = null;
|
||||||
|
static $tried = false;
|
||||||
|
if ($tried) return $sample;
|
||||||
|
$tried = true;
|
||||||
|
|
||||||
|
if (trim((string)shell_exec('command -v intel_gpu_top 2>/dev/null')) === '') return null;
|
||||||
|
|
||||||
|
$raw = shell_exec('timeout 4 intel_gpu_top -J -s 600 2>/dev/null | sed -n "/^{/,/^}/p" | sed "/^}/q"');
|
||||||
|
if (!$raw) return null;
|
||||||
|
|
||||||
|
$decoded = json_decode(trim($raw), true);
|
||||||
|
$sample = is_array($decoded) ? $decoded : null;
|
||||||
|
return $sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept for the existing single-GPU consumers — same shape as before, always GPU 0.
|
// Kept for the existing single-GPU consumers — same shape as before, always GPU 0.
|
||||||
|
|||||||
@@ -1789,11 +1789,23 @@ function vvPollMonitor(live) {
|
|||||||
p.gpu_uuid ? p.gpu_uuid === gpu.uuid : (gpu.index ?? 0) === 0
|
p.gpu_uuid ? p.gpu_uuid === gpu.uuid : (gpu.index ?? 0) === 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isIntel = gpu.vendor === 'intel';
|
||||||
const vramPct = gpu.memory_total > 0 ? Math.round(gpu.memory_used / gpu.memory_total * 100) : 0;
|
const vramPct = gpu.memory_total > 0 ? Math.round(gpu.memory_used / gpu.memory_total * 100) : 0;
|
||||||
const utilPct = gpu.utilization ?? 0;
|
const utilPct = gpu.utilization ?? 0;
|
||||||
const temp = gpu.temperature ?? 0;
|
const temp = gpu.temperature ?? 0;
|
||||||
const tempColor = temp >= 85 ? '#f44336' : temp >= 70 ? '#ff9800' : '#4caf50';
|
const tempColor = temp >= 85 ? '#f44336' : temp >= 70 ? '#ff9800' : '#4caf50';
|
||||||
const powerStr = gpu.power_w != null ? gpu.power_w + ' W' : '—';
|
// Package power when the GPU rail reads nothing, which is what Alder Lake reports. Labelled
|
||||||
|
// "Pkg" in that case rather than passed off as the GPU's own draw.
|
||||||
|
const powerStr = gpu.power_w != null ? gpu.power_w + ' W'
|
||||||
|
: (isIntel && gpu.package_w != null) ? gpu.package_w + ' W' : '—';
|
||||||
|
|
||||||
|
// Video first — it is the engine a hardware transcode uses, and this is a media server.
|
||||||
|
const engineOrder = ['Video', 'VideoEnhance', 'Render/3D', 'Blitter'];
|
||||||
|
const engines = gpu.engines ?? {};
|
||||||
|
const engineMeters = engineOrder
|
||||||
|
.filter(n => engines[n] != null)
|
||||||
|
.map(n => vvMeter(n === 'VideoEnhance' ? 'VideoEnh' : n, engines[n], `${engines[n]}%`))
|
||||||
|
.join('');
|
||||||
const procCount = gpuProcs.length;
|
const procCount = gpuProcs.length;
|
||||||
const procColor = procCount > 0 ? '#4caf50' : '#555';
|
const procColor = procCount > 0 ? '#4caf50' : '#555';
|
||||||
|
|
||||||
@@ -1822,14 +1834,23 @@ function vvPollMonitor(live) {
|
|||||||
<div style="margin-left:8px;background:${procColor}22;border:1px solid ${procColor};color:${procColor};
|
<div style="margin-left:8px;background:${procColor}22;border:1px solid ${procColor};color:${procColor};
|
||||||
padding:1px 8px;border-radius:10px;font-size:11px;white-space:nowrap;">${procCount} proc${procCount !== 1 ? 's' : ''}</div>
|
padding:1px 8px;border-radius:10px;font-size:11px;white-space:nowrap;">${procCount} proc${procCount !== 1 ? 's' : ''}</div>
|
||||||
</div>` +
|
</div>` +
|
||||||
vvMeter('VRAM', vramPct, `${gpu.memory_used} / ${gpu.memory_total} MB`) +
|
// An integrated GPU has no VRAM pool, no sensor of its own and no encode/decode split,
|
||||||
vvMeter('GPU', utilPct, `${utilPct}%`) +
|
// so it is drawn from what Intel actually publishes — per-engine busy — rather than
|
||||||
vvMeter('Encode', gpu.enc_pct ?? 0, `${gpu.enc_pct ?? 0}%`) +
|
// from nvidia's fields with zeros standing in for the ones it lacks. Video is listed
|
||||||
vvMeter('Decode', gpu.dec_pct ?? 0, `${gpu.dec_pct ?? 0}%`) +
|
// first: on a media server it is the engine a hardware transcode uses.
|
||||||
|
(isIntel
|
||||||
|
? (gpu.utilization != null ? vvMeter('GPU', utilPct, `${utilPct}%`) : '')
|
||||||
|
+ engineMeters
|
||||||
|
: vvMeter('VRAM', vramPct, `${gpu.memory_used} / ${gpu.memory_total} MB`)
|
||||||
|
+ vvMeter('GPU', utilPct, `${utilPct}%`)
|
||||||
|
+ vvMeter('Encode', gpu.enc_pct ?? 0, `${gpu.enc_pct ?? 0}%`)
|
||||||
|
+ vvMeter('Decode', gpu.dec_pct ?? 0, `${gpu.dec_pct ?? 0}%`)
|
||||||
|
) +
|
||||||
`<div style="display:flex;justify-content:space-between;font-size:11px;margin-top:6px;margin-bottom:8px;">
|
`<div style="display:flex;justify-content:space-between;font-size:11px;margin-top:6px;margin-bottom:8px;">
|
||||||
<span style="color:#666;">Temp</span>
|
${gpu.temperature != null
|
||||||
<span style="color:${tempColor};font-weight:bold;">${temp}°C</span>
|
? `<span style="color:#666;">Temp</span><span style="color:${tempColor};font-weight:bold;">${temp}°C</span>`
|
||||||
<span style="color:#666;margin-left:12px;">Power</span>
|
: `<span style="color:#666;">Temp</span><span style="color:#555;">in CPU pkg</span>`}
|
||||||
|
<span style="color:#666;margin-left:12px;">${isIntel && gpu.power_w == null ? 'Pkg' : 'Power'}</span>
|
||||||
<span style="color:#aaa;font-weight:bold;">${powerStr}</span>
|
<span style="color:#aaa;font-weight:bold;">${powerStr}</span>
|
||||||
</div>` + gpuProcHtml;
|
</div>` + gpuProcHtml;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user