diff --git a/admin/app/services/benchmark_service.ts b/admin/app/services/benchmark_service.ts index 68f69b7f..a2d4e23a 100644 --- a/admin/app/services/benchmark_service.ts +++ b/admin/app/services/benchmark_service.ts @@ -30,6 +30,7 @@ import type { } from '../../types/benchmark.js' import KVStore from '#models/kv_store' import { normalizeArchitecture, deriveOsName } from '../utils/platform_metadata.js' +import { isUnresolvedGpuModel } from '../utils/gpu_model.js' import { getFreeBytes } from '../utils/image_disk_preflight.js' import { readFile } from 'node:fs/promises' import { randomUUID, createHmac } from 'node:crypto' @@ -186,35 +187,8 @@ function isSelfHostedOllamaUrl(rawUrl: string): boolean { return false } -/** - * Is this GPU "model" a placeholder rather than a real name? - * - * systeminformation resolves PCI ids against the container's pci.ids database. - * When a card is newer than that file it reports the raw id verbatim — an RTX - * 5060 comes back as "Device 2d05". Vendor detection still succeeds, so these - * strings otherwise pass as legitimate model names and reach the leaderboard. - * - * Matches the "Device " shape plus the empty/unknown cases. Deliberately - * narrow: it must never reject a real product name, and no shipping GPU is - * called "Device" followed by four hex digits. - */ -function isUnresolvedGpuModel(model: string): boolean { - const s = model.trim() - if (s === '') return true - if (/^device\s+[0-9a-f]{4}$/i.test(s)) return true - if (/^unknown$/i.test(s)) return true - // WSL2 exposes the GPU through /dev/dxg rather than the real adapter, so - // si.graphics() reports Microsoft's generic placeholder even while CUDA work - // is running on a physical card. Left unhandled, an RTX 3090 reaches the - // public leaderboard labelled "Microsoft Basic Render Driver", which is worse - // than no label: it is wrong, and it fragments per-hardware grouping (#1218). - // - // These are Microsoft's own placeholder adapter names and appear nowhere - // outside a Windows/WSL graphics stack, so this cannot reject a real product - // name on a native Linux host. - if (/^microsoft basic (render driver|display adapter)$/i.test(s)) return true - return false -} +// Moved to app/utils/gpu_model.ts so the Settings > System display path can +// share the same definition — see the note there (#1196). // Minimum free disk required before pulling the AI model on first run. llama3.1:8b // (Q4) is ~4.9 GB; this covers the model plus headroom for the transient sysbench diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index d9ab50d7..846bd0f7 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -21,6 +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 { invalidateAssistantNameCache } from '../../config/inertia.js' import { invalidateMinRelevanceCache } from '../utils/rag_relevance.js' import { KiwixLibraryService } from '#services/kiwix_library_service' @@ -501,12 +502,31 @@ export class SystemService { } } - // Run the probes when controllers are empty (common inside Docker) or - // when lspci gave us bogus discrete-GPU BAR0 values that need replacing. + // The same pci.ids staleness that produces bogus VRAM also produces a + // placeholder model name — a card newer than the container's pci.ids is + // reported as its raw id, e.g. "Device 2d05" for an RTX 5060 (#1165). + // + // These are independent symptoms, not one. lspci can hand back a + // perfectly plausible BAR0 reading alongside an unresolved name, and in + // that case nothing above fires, no probe runs, and Settings > System + // renders the raw PCI id as the GPU model (#1196). NVIDIA usually hides + // this because the nvidia-smi path tends to be reached for other + // reasons; AMD has no equivalent, so it shows the raw id. + // + // The probes below resolve a real name from Ollama's own startup log, + // so trigger them on an unresolved name too rather than only on VRAM. + const hasUnresolvedGpuName = (graphics.controllers || []).some((c) => + isUnresolvedGpuModel(c.model || '') + ) + + // Run the probes when controllers are empty (common inside Docker), + // when lspci gave us bogus discrete-GPU BAR0 values that need replacing, + // or when it named a card it couldn't resolve. if ( !graphics.controllers || graphics.controllers.length === 0 || - hasLspciBogusDgpuVram + hasLspciBogusDgpuVram || + hasUnresolvedGpuName ) { const runtimes = dockerInfo.Runtimes || {} gpuHealth.hasNvidiaRuntime = 'nvidia' in runtimes diff --git a/admin/app/utils/gpu_model.ts b/admin/app/utils/gpu_model.ts new file mode 100644 index 00000000..27bc93c8 --- /dev/null +++ b/admin/app/utils/gpu_model.ts @@ -0,0 +1,36 @@ +/** + * Is this GPU "model" a placeholder rather than a real name? + * + * systeminformation resolves PCI ids against the container's pci.ids database. + * When a card is newer than that file it reports the raw id verbatim — an RTX + * 5060 comes back as "Device 2d05". Vendor detection still succeeds, so these + * strings otherwise pass as legitimate model names. + * + * Deliberately narrow: it must never reject a real product name. No shipping + * GPU is called "Device" followed by four hex digits, and the Microsoft entries + * below are placeholder adapter names that appear nowhere outside a Windows or + * WSL graphics stack. + * + * Lives in its own module rather than beside its first caller because both the + * leaderboard submission path and the Settings > System display path need the + * same answer, and they diverged once already: #1165 fixed the submission and + * left the System page reporting the raw id (#1196). One definition, two + * callers, so the next placeholder shape only has to be added once. + */ +export function isUnresolvedGpuModel(model: string): boolean { + const s = model.trim() + if (s === '') return true + if (/^device\s+[0-9a-f]{4}$/i.test(s)) return true + if (/^unknown$/i.test(s)) return true + // WSL2 exposes the GPU through /dev/dxg rather than the real adapter, so + // si.graphics() reports Microsoft's generic placeholder even while CUDA work + // is running on a physical card. Left unhandled, an RTX 3090 reaches the + // public leaderboard labelled "Microsoft Basic Render Driver", which is worse + // than no label: it is wrong, and it fragments per-hardware grouping (#1218). + // + // These are Microsoft's own placeholder adapter names and appear nowhere + // outside a Windows/WSL graphics stack, so this cannot reject a real product + // name on a native Linux host. + if (/^microsoft basic (render driver|display adapter)$/i.test(s)) return true + return false +} diff --git a/admin/tests/unit/gpu_model.spec.ts b/admin/tests/unit/gpu_model.spec.ts new file mode 100644 index 00000000..651dd45f --- /dev/null +++ b/admin/tests/unit/gpu_model.spec.ts @@ -0,0 +1,57 @@ +import * as assert from 'node:assert/strict' +import { test } from 'node:test' + +import { isUnresolvedGpuModel } from '../../app/utils/gpu_model.js' + +test('unresolved PCI ids are rejected', () => { + // The #1165 shape: a card newer than the container's pci.ids database comes + // back as its raw id. "Device 2d05" is a real RTX 5060 that reached the + // public leaderboard under that name. + for (const s of ['Device 2d05', 'device 2d05', 'DEVICE 1002', 'Device 2d05']) { + assert.equal(isUnresolvedGpuModel(s), true, s) + } +}) + +test('empty and unknown are rejected', () => { + for (const s of ['', ' ', 'Unknown', 'unknown', 'UNKNOWN']) { + assert.equal(isUnresolvedGpuModel(s), true, JSON.stringify(s)) + } +}) + +test('WSL placeholder adapter names are rejected', () => { + // #1218: WSL reaches the GPU via /dev/dxg, so si.graphics() reports + // Microsoft's placeholder while CUDA runs on a physical card. + for (const s of [ + 'Microsoft Basic Render Driver', + 'microsoft basic render driver', + 'Microsoft Basic Display Adapter', + ]) { + assert.equal(isUnresolvedGpuModel(s), true, s) + } +}) + +test('real product names are never rejected', () => { + // The one thing this function must not do. A false positive here discards a + // correct GPU name and sends "unknown" to the leaderboard instead. + for (const s of [ + 'NVIDIA GeForce RTX 5090', + 'NVIDIA GeForce RTX 3090 Ti', + 'AMD Radeon RX 6800', + 'Navi 48 [Radeon AI PRO R9700]', + 'Intel Iris Xe Graphics', + 'Raphael', + 'Phoenix1', + 'Device Manager Graphics 5000', + 'GeForce GTX 1080', + 'Microsoft Basic Render Driver Pro', + ]) { + assert.equal(isUnresolvedGpuModel(s), false, s) + } +}) + +test('a hex-like id that is not the placeholder shape is kept', () => { + // Guard the regex against over-reach: only a bare "Device <4 hex>" qualifies. + for (const s of ['Device 2d05x', 'Device 2d0', 'Device 2d055', 'My Device 2d05']) { + assert.equal(isUnresolvedGpuModel(s), false, s) + } +})