From 1f34d175618a49422b47064e05e11e254a4d1ec1 Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:53:19 +0800 Subject: [PATCH] fix(gpu): report an Intel GPU as unsupported instead of healthy The health probe's fallback branch set status 'ok' and ollamaGpuAccessible: true purely because si.graphics() returned a controller. On an Intel-only box that is the Core Ultra case in #1325: the controller is there, but NOMAD only ships CUDA and ROCm paths and Ollama has no Intel backend, so inference runs CPU-only while Settings > System reports a healthy GPU. Adds an 'unsupported' status, set only when no reported controller belongs to a vendor NOMAD can accelerate. NVIDIA and AMD boxes keep the existing behaviour, and an unrecognised vendor string is deliberately not flagged so a healthy card behind a newer pci.ids is never turned into a false alarm. --- admin/app/services/system_service.ts | 22 +++++++-- admin/app/utils/gpu_model.ts | 48 +++++++++++++++++++ admin/inertia/pages/settings/system.tsx | 12 +++++ admin/tests/unit/gpu_model.spec.ts | 62 ++++++++++++++++++++++++- admin/types/system.ts | 8 +++- 5 files changed, 145 insertions(+), 7 deletions(-) diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 2e620c7f..93f154a4 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -21,7 +21,7 @@ import env from '#start/env' import KVStore from '#models/kv_store' import { KV_STORE_SCHEMA, KVStoreKey } from '../../types/kv_store.js' import { isNewerVersion } from '../utils/version.js' -import { isUnresolvedGpuModel } from '../utils/gpu_model.js' +import { findUnacceleratedGpuVendor, isUnresolvedGpuModel } from '../utils/gpu_model.js' import { classifyOllamaComputeBackend, diagnoseAmdCpuFallback, @@ -635,9 +635,23 @@ export class SystemService { } } else { // si.graphics() returned usable controllers and no GPU runtime is expected - // (host install, not Docker). Still unprobed, see #1325. - gpuHealth.status = 'ok' - gpuHealth.ollamaGpuAccessible = true + // (host install, not Docker). A controller here says a GPU exists, not + // that anything can use it: NOMAD only has CUDA and ROCm paths, so an + // Intel-only box (Core Ultra iGPU, for example) can never accelerate + // Ollama. Reporting 'ok' from the bare presence of a controller is what + // let such a box show a healthy GPU while inference ran CPU-only (#1325). + const unacceleratedVendor = findUnacceleratedGpuVendor(graphics.controllers) + if (unacceleratedVendor) { + gpuHealth.status = 'unsupported' + gpuHealth.gpuVendor = unacceleratedVendor + gpuHealth.ollamaGpuAccessible = false + logger.warn( + `GPU present (${unacceleratedVendor}) but NOMAD has no acceleration path for it; the AI Assistant will run on CPU` + ) + } else { + gpuHealth.status = 'ok' + gpuHealth.ollamaGpuAccessible = true + } } } catch { // Docker info query failed, skip host-level enrichment diff --git a/admin/app/utils/gpu_model.ts b/admin/app/utils/gpu_model.ts index 27bc93c8..df7652e7 100644 --- a/admin/app/utils/gpu_model.ts +++ b/admin/app/utils/gpu_model.ts @@ -1,3 +1,51 @@ +/** GPU vendors named by the acceleration paths NOMAD actually ships. */ +export type GpuVendorKey = 'nvidia' | 'amd' | 'intel' + +/** + * Which of the known GPU vendors is this `si.graphics()` vendor string? + * + * The AMD arm is deliberately loose: pci.ids reports "Advanced Micro Devices, + * Inc." on some cards and "ATI Technologies Inc." on older ones, and lspci output + * reaches us with whatever casing the host had. + * + * Returns null for a vendor that is none of the three. Callers must treat that as + * "no opinion" rather than "unsupported" — an unidentified card may well be a + * future NVIDIA part behind a newer pci.ids. + */ +export function classifyGpuVendor(vendor: string | null | undefined): GpuVendorKey | null { + const v = (vendor ?? '').trim() + if (/intel/i.test(v)) return 'intel' + if (/nvidia/i.test(v)) return 'nvidia' + if (/advanced micro devices|\bamd\b|\bati\b/i.test(v)) return 'amd' + return null +} + +/** + * The GPU vendor on this box that NOMAD has no acceleration path for, if any. + * + * NOMAD accelerates Ollama over CUDA (NVIDIA) or ROCm (AMD) and nothing else, so + * a box whose only visible controller is an Intel iGPU can never run inference + * on it — Ollama has no Intel backend. Returning null when an NVIDIA or AMD + * controller is present keeps hybrid laptops (Intel iGPU + NVIDIA dGPU) on the + * existing path: those boxes do have a working route, so the runtime probe above + * stays authoritative for them and this must not pre-empt it. + * + * An unidentified vendor is deliberately not reported. This runs on the branch + * where nothing was probed, so guessing "unsupported" from an unrecognised vendor + * string would turn a healthy NVIDIA box into a false alarm. + */ +export function findUnacceleratedGpuVendor( + controllers: ReadonlyArray<{ vendor?: string | null }> | null | undefined +): GpuVendorKey | null { + let unaccelerated: GpuVendorKey | null = null + for (const controller of controllers ?? []) { + const vendor = classifyGpuVendor(controller?.vendor) + if (vendor === 'nvidia' || vendor === 'amd') return null + if (vendor === 'intel' && !unaccelerated) unaccelerated = 'intel' + } + return unaccelerated +} + /** * Is this GPU "model" a placeholder rather than a real name? * diff --git a/admin/inertia/pages/settings/system.tsx b/admin/inertia/pages/settings/system.tsx index 895c9f88..b7a48a31 100644 --- a/admin/inertia/pages/settings/system.tsx +++ b/admin/inertia/pages/settings/system.tsx @@ -140,6 +140,18 @@ export default function SettingsPage(props: { }, ]} /> + {info?.gpuHealth?.status === 'unsupported' && ( +