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:
Gmer4Lfe
2026-08-21 08:23:14 -04:00
parent 4a29e7bc99
commit 00fdee5649
2 changed files with 135 additions and 9 deletions
+106 -1
View File
@@ -196,9 +196,114 @@ function vv_gpu_stats_all(): array {
'power_w' => is_numeric($p[7]) ? round((float)$p[7], 1) : null,
'enc_pct' => (int)$p[8],
'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.