fix(system): resolve a placeholder GPU name on the System page too

#1165 taught the benchmark path to reject an unresolved PCI id as a GPU
model, but isUnresolvedGpuModel lived inside benchmark_service.ts and the
Settings > System display path never learned the same thing. It still shows
whatever lspci handed back.

The two symptoms of a stale pci.ids database are independent, which is why
the existing guard misses this. system_service already re-probes when a
discrete-GPU controller reports an implausible BAR0 reading as VRAM, and
that happens to fix the name as a side effect. But lspci can return a
perfectly plausible VRAM figure alongside a name it could not resolve, and
then nothing fires.

Confirmed live on an AMD box. The Radeon 780M reports:

  model: "Device 1900", vram: 512

512 MiB clears the 256 MiB bogus-VRAM threshold, so no probe runs and the
System page renders "Device 1900". The same box's Ollama startup log carries
description="AMD Radeon 780M Graphics", so the probe the guard declined to
run had the real name available the whole time. NVIDIA hides this because
its path tends to be reached for other reasons; AMD has no equivalent.

Adds an unresolved-name trigger alongside the VRAM one, and moves
isUnresolvedGpuModel to app/utils/gpu_model.ts so the submission path and the
display path share one definition instead of diverging again. Tests cover
both placeholder shapes and, more importantly, a set of real product names
that must never be rejected.

Stacked on #1277, so the shared helper carries the WSL placeholders too.

Closes #1196

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-08-29 15:36:13 -07:00 committed by Jake Turner
parent b0be0dec43
commit bf7e4b3b48
4 changed files with 119 additions and 32 deletions

View File

@ -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 <hex>" 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

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
})