diff --git a/Dockerfile b/Dockerfile index 03acaa9..8850e23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,14 @@ FROM node:22-slim AS base # Install bash & curl for entrypoint script compatibility, graphicsmagick for pdf2pic, and vips-dev & build-base for sharp -RUN apt-get update && apt-get install -y bash curl graphicsmagick libvips-dev build-essential +RUN apt-get update && apt-get install -y \ + bash \ + curl \ + graphicsmagick \ + libvips-dev \ + build-essential \ + pciutils \ + && rm -rf /var/lib/apt/lists/* # All deps stage FROM base AS deps diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 714cd11..7501e78 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -10,7 +10,7 @@ import { KiwixLibraryService } from './kiwix_library_service.js' import { SERVICE_NAMES } from '../../constants/service_names.js' import { exec } from 'child_process' import { promisify } from 'util' -// import { readdir } from 'fs/promises' +import { readFile } from 'node:fs/promises' import KVStore from '#models/kv_store' import { BROADCAST_CHANNELS } from '../../constants/broadcast.js' import { KIWIX_LIBRARY_CMD } from '../../constants/kiwix.js' @@ -500,6 +500,7 @@ export class DockerService { // GPU-aware configuration for Ollama let finalImage = service.container_image let gpuHostConfig = containerConfig?.HostConfig || {} + let amdGpuConfigured = false if (service.service_name === SERVICE_NAMES.OLLAMA) { const gpuResult = await this._detectGPUType() @@ -523,16 +524,51 @@ export class DockerService { ], } } else if (gpuResult.type === 'amd') { - this._broadcast( - service.service_name, - 'gpu-config', - `AMD GPU detected. ROCm GPU acceleration is not yet supported in this version — proceeding with CPU-only configuration. GPU support for AMD will be available in a future update.` - ) - logger.warn('[DockerService] AMD GPU detected but ROCm support is not yet enabled. Using CPU-only configuration.') - // TODO: Re-enable AMD GPU support once ROCm image and device discovery are validated. - // When re-enabling: - // 1. Switch image to 'ollama/ollama:rocm' - // 2. Restore _discoverAMDDevices() to map /dev/kfd and /dev/dri/* into the container + // AMD acceleration is opt-out via the 'ai.amdGpuAcceleration' KV key (default-on). + // Per memory feedback: KV values can be string or boolean — coerce explicitly. + const amdEnabledRaw = await KVStore.getValue('ai.amdGpuAcceleration') + const amdAccelerationEnabled = String(amdEnabledRaw) !== 'false' + + if (amdAccelerationEnabled) { + this._broadcast( + service.service_name, + 'gpu-config', + `AMD GPU detected. Using ROCm image with /dev/kfd and /dev/dri passthrough...` + ) + + finalImage = 'ollama/ollama:rocm' + + // The pull-if-missing earlier in this function used service.container_image + // (the DB-pinned tag, e.g. ollama/ollama:0.18.2). The AMD branch overrides + // to a different tag — so we need to pull :rocm separately if it's not local. + const rocmImageExists = await this._checkImageExists(finalImage) + if (!rocmImageExists) { + this._broadcast( + service.service_name, + 'pulling', + `Pulling Docker image ${finalImage}...` + ) + const rocmPullStream = await this.docker.pull(finalImage) + await new Promise((res) => this.docker.modem.followProgress(rocmPullStream, res)) + } + + const amdDevices = await this._discoverAMDDevices() + gpuHostConfig = { + ...gpuHostConfig, + Devices: amdDevices, + } + amdGpuConfigured = true + logger.info( + `[DockerService] Configured ROCm image and ${amdDevices.length} AMD device entries for Ollama` + ) + } else { + this._broadcast( + service.service_name, + 'gpu-config', + `AMD GPU detected but acceleration is disabled via ai.amdGpuAcceleration. Using CPU-only configuration.` + ) + logger.info('[DockerService] AMD GPU acceleration disabled by KV opt-out; using CPU-only configuration.') + } } else if (gpuResult.toolkitMissing) { this._broadcast( service.service_name, @@ -555,6 +591,12 @@ export class DockerService { if (flashAttentionEnabled !== false) { ollamaEnv.push('OLLAMA_FLASH_ATTENTION=1') } + if (amdGpuConfigured) { + // RDNA3 iGPUs (gfx1103: 780M, 880M, 890M, ...) aren't on AMD's official ROCm + // allowlist but work when forced to identify as gfx1100 via HSA_OVERRIDE_GFX_VERSION. + // Harmless on supported discrete cards (gfx1030 RX 6800, etc.) — they ignore the override. + ollamaEnv.push('HSA_OVERRIDE_GFX_VERSION=11.0.0') + } } this._broadcast( @@ -857,7 +899,10 @@ export class DockerService { /** * Detect GPU type and toolkit availability. * Primary: Check Docker runtimes via docker.info() (works from inside containers). - * Fallback: lspci for host-based installs and AMD detection. + * Secondary: Read /app/storage/.nomad-gpu-type written by install_nomad.sh — needed + * for AMD detection because lspci isn't available inside the admin container and + * AMD has no Docker runtime registration to query. + * Fallback: lspci for host-based installs. */ private async _detectGPUType(): Promise<{ type: 'nvidia' | 'amd' | 'none'; toolkitMissing?: boolean }> { try { @@ -874,6 +919,24 @@ export class DockerService { logger.warn(`[DockerService] Could not query Docker info for GPU runtimes: ${error.message}`) } + // Secondary: install_nomad.sh writes the host-detected GPU type to a marker file in + // the storage volume so the admin container (which lacks lspci) can read it. + try { + const marker = (await readFile('/app/storage/.nomad-gpu-type', 'utf8')).trim() + if (marker === 'nvidia') { + // Hardware present but Docker doesn't have nvidia runtime → toolkit missing + logger.warn('[DockerService] NVIDIA GPU recorded in marker file but NVIDIA Container Toolkit is not installed') + return { type: 'none', toolkitMissing: true } + } + if (marker === 'amd') { + logger.info('[DockerService] AMD GPU detected via install-time marker file') + await this._persistGPUType('amd') + return { type: 'amd' } + } + } catch { + // No marker file — fall through to lspci attempt for host-based installs + } + // Fallback: lspci for host-based installs (not available inside Docker) const execAsync = promisify(exec) @@ -937,60 +1000,23 @@ export class DockerService { } /** - * Discover AMD GPU DRI devices dynamically. - * Returns an array of device configurations for Docker. + * Build the Docker Devices array for AMD GPU passthrough. + * + * Returns /dev/kfd (Kernel Fusion Driver, required by ROCm) and /dev/dri (the DRM + * device tree). Passing /dev/dri as a single directory entry mirrors Docker CLI + * --device behavior — the daemon expands it to all child devices (card*, renderD*) + * regardless of how the host enumerates them. This avoids the brittle hardcoded + * fallback (card0/renderD128) the prior implementation used, which was wrong on + * systems where the AMD GPU enumerates as card1+ (e.g. UM890 Pro 780M iGPU). */ - // private async _discoverAMDDevices(): Promise< - // Array<{ PathOnHost: string; PathInContainer: string; CgroupPermissions: string }> - // > { - // try { - // const devices: Array<{ - // PathOnHost: string - // PathInContainer: string - // CgroupPermissions: string - // }> = [] - - // // Always add /dev/kfd (Kernel Fusion Driver) - // devices.push({ - // PathOnHost: '/dev/kfd', - // PathInContainer: '/dev/kfd', - // CgroupPermissions: 'rwm', - // }) - - // // Discover DRI devices in /dev/dri/ - // try { - // const driDevices = await readdir('/dev/dri') - // for (const device of driDevices) { - // const devicePath = `/dev/dri/${device}` - // devices.push({ - // PathOnHost: devicePath, - // PathInContainer: devicePath, - // CgroupPermissions: 'rwm', - // }) - // } - // logger.info( - // `[DockerService] Discovered ${driDevices.length} DRI devices: ${driDevices.join(', ')}` - // ) - // } catch (error) { - // logger.warn(`[DockerService] Could not read /dev/dri directory: ${error.message}`) - // // Fallback to common device names if directory read fails - // const fallbackDevices = ['card0', 'renderD128'] - // for (const device of fallbackDevices) { - // devices.push({ - // PathOnHost: `/dev/dri/${device}`, - // PathInContainer: `/dev/dri/${device}`, - // CgroupPermissions: 'rwm', - // }) - // } - // logger.info(`[DockerService] Using fallback DRI devices: ${fallbackDevices.join(', ')}`) - // } - - // return devices - // } catch (error) { - // logger.error(`[DockerService] Error discovering AMD devices: ${error.message}`) - // return [] - // } - // } + private async _discoverAMDDevices(): Promise< + Array<{ PathOnHost: string; PathInContainer: string; CgroupPermissions: string }> + > { + return [ + { PathOnHost: '/dev/kfd', PathInContainer: '/dev/kfd', CgroupPermissions: 'rwm' }, + { PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }, + ] + } /** * Update a service container to a new image version while preserving volumes and data. @@ -1014,12 +1040,60 @@ export class DockerService { this.activeInstallations.add(serviceName) - // Compute new image string + // Compute new image string. AMD-on-Ollama overrides this to the rolling :rocm tag + // (set during GPU detection below) since per-version ROCm tags aren't always published. const currentImage = service.container_image const imageBase = currentImage.includes(':') ? currentImage.substring(0, currentImage.lastIndexOf(':')) : currentImage - const newImage = `${imageBase}:${targetVersion}` + let newImage = `${imageBase}:${targetVersion}` + + // GPU detection runs before the pull so AMD updates pull ollama/ollama:rocm rather + // than the standard tag. Detection result is reused below when building the new + // container config (devices, env). Non-Ollama services skip this entirely. + let updatedDeviceRequests: any[] | undefined = undefined + let updatedAmdDevices: any[] | undefined = undefined + let updatedAmdGpuConfigured = false + if (serviceName === SERVICE_NAMES.OLLAMA) { + const gpuResult = await this._detectGPUType() + if (gpuResult.type === 'nvidia') { + this._broadcast( + serviceName, + 'update-gpu-config', + `NVIDIA container runtime detected. Configuring updated container with GPU support...` + ) + updatedDeviceRequests = [ + { Driver: 'nvidia', Count: -1, Capabilities: [['gpu']] }, + ] + } else if (gpuResult.type === 'amd') { + const amdEnabledRaw = await KVStore.getValue('ai.amdGpuAcceleration') + const amdAccelerationEnabled = String(amdEnabledRaw) !== 'false' + if (amdAccelerationEnabled) { + this._broadcast( + serviceName, + 'update-gpu-config', + `AMD GPU detected. Using ROCm image with /dev/kfd and /dev/dri passthrough...` + ) + newImage = 'ollama/ollama:rocm' + updatedAmdDevices = await this._discoverAMDDevices() + updatedAmdGpuConfigured = true + } else { + this._broadcast( + serviceName, + 'update-gpu-config', + `AMD GPU detected but acceleration is disabled via ai.amdGpuAcceleration. Using CPU-only configuration.` + ) + } + } else if (gpuResult.toolkitMissing) { + this._broadcast( + serviceName, + 'update-gpu-config', + `NVIDIA GPU detected but NVIDIA Container Toolkit is not installed. Using CPU-only configuration. Install the toolkit and reinstall AI Assistant for GPU acceleration: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html` + ) + } else { + this._broadcast(serviceName, 'update-gpu-config', `No GPU detected. Using CPU-only configuration.`) + } + } // Step 1: Pull new image this._broadcast(serviceName, 'update-pulling', `Pulling image ${newImage}...`) @@ -1054,48 +1128,21 @@ export class DockerService { const hostConfig = inspectData.HostConfig || {} - // Re-run GPU detection for Ollama so updates always reflect the current GPU environment. - // This handles cases where the NVIDIA Container Toolkit was installed after the initial - // Ollama setup, and ensures DeviceRequests are always built fresh rather than relying on - // round-tripping the Docker inspect format back into the create API. - let updatedDeviceRequests: any[] | undefined = undefined - if (serviceName === SERVICE_NAMES.OLLAMA) { - const gpuResult = await this._detectGPUType() - - if (gpuResult.type === 'nvidia') { - this._broadcast( - serviceName, - 'update-gpu-config', - `NVIDIA container runtime detected. Configuring updated container with GPU support...` - ) - updatedDeviceRequests = [ - { - Driver: 'nvidia', - Count: -1, - Capabilities: [['gpu']], - }, + // GPU detection already ran above (before the pull) so we know the right image, devices, + // and whether HSA_OVERRIDE needs injection. For AMD, replace any prior HSA_OVERRIDE in + // the inspect-captured env so updates from older containers pick up the current value. + const baseEnv = inspectData.Config?.Env || [] + const finalEnv = updatedAmdGpuConfigured + ? [ + ...baseEnv.filter((e: string) => !e.startsWith('HSA_OVERRIDE_GFX_VERSION=')), + 'HSA_OVERRIDE_GFX_VERSION=11.0.0', ] - } else if (gpuResult.type === 'amd') { - this._broadcast( - serviceName, - 'update-gpu-config', - `AMD GPU detected. ROCm GPU acceleration is not yet supported — using CPU-only configuration.` - ) - } else if (gpuResult.toolkitMissing) { - this._broadcast( - serviceName, - 'update-gpu-config', - `NVIDIA GPU detected but NVIDIA Container Toolkit is not installed. Using CPU-only configuration. Install the toolkit and reinstall AI Assistant for GPU acceleration: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html` - ) - } else { - this._broadcast(serviceName, 'update-gpu-config', `No GPU detected. Using CPU-only configuration.`) - } - } + : baseEnv const newContainerConfig: any = { Image: newImage, name: serviceName, - Env: inspectData.Config?.Env || undefined, + Env: finalEnv.length > 0 ? finalEnv : undefined, Cmd: inspectData.Config?.Cmd || undefined, ExposedPorts: inspectData.Config?.ExposedPorts || undefined, WorkingDir: inspectData.Config?.WorkingDir || undefined, @@ -1105,7 +1152,7 @@ export class DockerService { PortBindings: hostConfig.PortBindings || undefined, RestartPolicy: hostConfig.RestartPolicy || undefined, DeviceRequests: serviceName === SERVICE_NAMES.OLLAMA ? updatedDeviceRequests : (hostConfig.DeviceRequests || undefined), - Devices: hostConfig.Devices || undefined, + Devices: serviceName === SERVICE_NAMES.OLLAMA && updatedAmdDevices ? updatedAmdDevices : (hostConfig.Devices || undefined), }, NetworkingConfig: inspectData.NetworkSettings?.Networks ? { diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 5701de3..1a55cfb 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -12,6 +12,7 @@ import { } from '../../types/system.js' import { SERVICE_NAMES } from '../../constants/service_names.js' import { readFileSync } from 'node:fs' +import { readFile } from 'node:fs/promises' import path, { join } from 'node:path' import { getAllFilesystems, getFile } from '../utils/fs.js' import axios from 'axios' @@ -72,6 +73,61 @@ export class SystemService { return false } + /** + * Probe Ollama startup logs for the canonical "inference compute" line that records + * which compute backend was selected. This catches silent CPU fallback (e.g. when + * /dev/kfd is mounted but ROCm initialization fails, or NVML dies after an update) + * which the older nvidia-smi exec probe could not detect. + * + * Returns the parsed library, GPU model name, and VRAM in MiB, or null when: + * - the Ollama container is not running + * - the line has not been emitted (Ollama still starting up) + * - logs show CPU-only operation (no GPU detected) + */ + async getOllamaInferenceComputeFromLogs(): Promise<{ + library: 'CUDA' | 'ROCm' + name: string + vramMiB: number + } | null> { + try { + const containers = await this.dockerService.docker.listContainers({ all: false }) + const ollamaContainer = containers.find((c) => c.Names.includes(`/${SERVICE_NAMES.OLLAMA}`)) + if (!ollamaContainer) return null + + const container = this.dockerService.docker.getContainer(ollamaContainer.Id) + const buf = (await container.logs({ + stdout: true, + stderr: true, + tail: 500, + follow: false, + })) as unknown as Buffer + const logs = buf.toString('utf8') + + const lines = logs.split('\n').filter((l) => l.includes('msg="inference compute"')) + if (lines.length === 0) return null + + const lastLine = lines[lines.length - 1] + const libraryMatch = lastLine.match(/library=(CUDA|ROCm)/) + if (!libraryMatch) return null + + const descMatch = lastLine.match(/description="([^"]+)"/) + const totalMatch = lastLine.match(/total="([0-9.]+)\s*GiB"/) + + return { + library: libraryMatch[1] as 'CUDA' | 'ROCm', + name: + descMatch?.[1] || + (libraryMatch[1] === 'CUDA' ? 'NVIDIA GPU' : 'AMD GPU'), + vramMiB: totalMatch ? Math.round(Number.parseFloat(totalMatch[1]) * 1024) : 0, + } + } catch (error) { + logger.warn( + `[SystemService] Failed to probe Ollama logs for inference compute line: ${error instanceof Error ? error.message : error}` + ) + return null + } + } + async getNvidiaSmiInfo(): Promise< | Array<{ vendor: string; model: string; vram: number }> | { error: string } @@ -317,10 +373,14 @@ export class SystemService { logger.error('Error reading disk info file:', error) } - // GPU health tracking — detect when host has NVIDIA GPU but Ollama can't access it + // GPU health tracking — detect when host has a GPU runtime but Ollama can't access it. + // Primary probe: parse Ollama's "inference compute" startup log line for both NVIDIA + // and AMD. Secondary probe (NVIDIA only): nvidia-smi exec, retained as a fallback for + // hardware enrichment when log parsing has not yet captured a startup line. let gpuHealth: GpuHealthStatus = { status: 'no_gpu', hasNvidiaRuntime: false, + hasRocmRuntime: false, ollamaGpuAccessible: false, } @@ -340,27 +400,51 @@ export class SystemService { } // If si.graphics() returned no controllers (common inside Docker), - // fall back to nvidia runtime + nvidia-smi detection + // fall back to runtime + Ollama log probe to figure out what's accessible. if (!graphics.controllers || graphics.controllers.length === 0) { const runtimes = dockerInfo.Runtimes || {} - if ('nvidia' in runtimes) { - gpuHealth.hasNvidiaRuntime = true - const nvidiaInfo = await this.getNvidiaSmiInfo() - if (Array.isArray(nvidiaInfo)) { - graphics.controllers = nvidiaInfo.map((gpu) => ({ - model: gpu.model, - vendor: gpu.vendor, - bus: '', - vram: gpu.vram, - vramDynamic: false, // assume false here, we don't actually use this field for our purposes. - })) + gpuHealth.hasNvidiaRuntime = 'nvidia' in runtimes + + // AMD doesn't register a Docker runtime. Detection sources, in priority order: + // 1. KV 'gpu.type' (set by DockerService._detectGPUType after first Ollama install) + // 2. Marker file at /app/storage/.nomad-gpu-type (written by install_nomad.sh) + // The marker file matters because the System page should reflect AMD presence + // even before AI Assistant has been installed for the first time. + let savedGpuType: string | null | undefined = await KVStore.getValue('gpu.type') as string | undefined + if (!savedGpuType) { + try { + savedGpuType = (await readFile('/app/storage/.nomad-gpu-type', 'utf8')).trim() + } catch {} + } + const amdEnabledRaw = await KVStore.getValue('ai.amdGpuAcceleration') + const amdAccelerationEnabled = String(amdEnabledRaw) !== 'false' + gpuHealth.hasRocmRuntime = savedGpuType === 'amd' && amdAccelerationEnabled + + if (gpuHealth.hasNvidiaRuntime || gpuHealth.hasRocmRuntime) { + gpuHealth.gpuVendor = gpuHealth.hasNvidiaRuntime ? 'nvidia' : 'amd' + + // Primary probe: Ollama log parsing — works for both vendors and catches silent fallback + const logInfo = await this.getOllamaInferenceComputeFromLogs() + if (logInfo) { + graphics.controllers = [ + { + model: logInfo.name, + vendor: logInfo.library === 'CUDA' ? 'NVIDIA' : 'AMD', + bus: '', + vram: logInfo.vramMiB, + vramDynamic: false, + }, + ] gpuHealth.status = 'ok' gpuHealth.ollamaGpuAccessible = true - } else if (nvidiaInfo === 'OLLAMA_NOT_FOUND') { - // No local Ollama container — check if a remote Ollama URL is configured - const externalOllamaGpu = await this.getExternalOllamaGpuInfo() - if (externalOllamaGpu) { - graphics.controllers = externalOllamaGpu.map((gpu) => ({ + } else if (gpuHealth.hasNvidiaRuntime) { + // NVIDIA secondary path: nvidia-smi exec preserves prior behavior when + // the log parser hasn't seen a startup line yet (e.g. log rotation, + // very fresh container). Distinguishes "no Ollama container" from + // "container exists but GPU broken". + const nvidiaInfo = await this.getNvidiaSmiInfo() + if (Array.isArray(nvidiaInfo)) { + graphics.controllers = nvidiaInfo.map((gpu) => ({ model: gpu.model, vendor: gpu.vendor, bus: '', @@ -369,25 +453,66 @@ export class SystemService { })) gpuHealth.status = 'ok' gpuHealth.ollamaGpuAccessible = true + } else if (nvidiaInfo === 'OLLAMA_NOT_FOUND') { + const externalOllamaGpu = await this.getExternalOllamaGpuInfo() + if (externalOllamaGpu) { + graphics.controllers = externalOllamaGpu.map((gpu) => ({ + model: gpu.model, + vendor: gpu.vendor, + bus: '', + vram: gpu.vram, + vramDynamic: false, + })) + gpuHealth.status = 'ok' + gpuHealth.ollamaGpuAccessible = true + } else { + gpuHealth.status = 'ollama_not_installed' + } } else { - gpuHealth.status = 'ollama_not_installed' + const externalOllamaGpu = await this.getExternalOllamaGpuInfo() + if (externalOllamaGpu) { + graphics.controllers = externalOllamaGpu.map((gpu) => ({ + model: gpu.model, + vendor: gpu.vendor, + bus: '', + vram: gpu.vram, + vramDynamic: false, + })) + gpuHealth.status = 'ok' + gpuHealth.ollamaGpuAccessible = true + } else { + gpuHealth.status = 'passthrough_failed' + logger.warn( + `NVIDIA runtime detected but GPU passthrough failed: ${typeof nvidiaInfo === 'string' ? nvidiaInfo : JSON.stringify(nvidiaInfo)}` + ) + } } } else { - const externalOllamaGpu = await this.getExternalOllamaGpuInfo() - if (externalOllamaGpu) { - graphics.controllers = externalOllamaGpu.map((gpu) => ({ - model: gpu.model, - vendor: gpu.vendor, - bus: '', - vram: gpu.vram, - vramDynamic: false, - })) - gpuHealth.status = 'ok' - gpuHealth.ollamaGpuAccessible = true + // AMD path: no nvidia-smi equivalent worth running — log parser is authoritative. + // Distinguish "Ollama not running" from "Ollama running but no GPU log line". + const containers = await this.dockerService.docker.listContainers({ all: false }) + const ollamaRunning = containers.some((c) => + c.Names.includes(`/${SERVICE_NAMES.OLLAMA}`) + ) + if (!ollamaRunning) { + const externalOllamaGpu = await this.getExternalOllamaGpuInfo() + if (externalOllamaGpu) { + graphics.controllers = externalOllamaGpu.map((gpu) => ({ + model: gpu.model, + vendor: gpu.vendor, + bus: '', + vram: gpu.vram, + vramDynamic: false, + })) + gpuHealth.status = 'ok' + gpuHealth.ollamaGpuAccessible = true + } else { + gpuHealth.status = 'ollama_not_installed' + } } else { gpuHealth.status = 'passthrough_failed' logger.warn( - `NVIDIA runtime detected but GPU passthrough failed: ${typeof nvidiaInfo === 'string' ? nvidiaInfo : JSON.stringify(nvidiaInfo)}` + 'AMD GPU detected but Ollama logs show no ROCm initialization — passthrough or HSA override may have failed' ) } } diff --git a/admin/inertia/pages/settings/models.tsx b/admin/inertia/pages/settings/models.tsx index fc2b1dc..fe119d8 100644 --- a/admin/inertia/pages/settings/models.tsx +++ b/admin/inertia/pages/settings/models.tsx @@ -283,7 +283,7 @@ export default function ModelsPage(props: { type="warning" variant="bordered" title="GPU Not Accessible" - message={`Your system has an NVIDIA GPU, but ${aiAssistantName} can't access it. AI is running on CPU only, which is significantly slower.`} + message={`Your system has ${systemInfo?.gpuHealth?.gpuVendor === 'amd' ? 'an AMD' : 'an NVIDIA'} GPU, but ${aiAssistantName} can't access it. AI is running on CPU only, which is significantly slower.`} className="!mt-6" dismissible={true} onDismiss={handleDismissGpuBanner} diff --git a/admin/inertia/pages/settings/system.tsx b/admin/inertia/pages/settings/system.tsx index 7b40088..1a13b52 100644 --- a/admin/inertia/pages/settings/system.tsx +++ b/admin/inertia/pages/settings/system.tsx @@ -209,7 +209,7 @@ export default function SettingsPage(props: { type="warning" variant="bordered" title="GPU Not Accessible to AI Assistant" - message="Your system has an NVIDIA GPU, but the AI Assistant can't access it. AI is running on CPU only, which is significantly slower." + message={`Your system has ${info?.gpuHealth?.gpuVendor === 'amd' ? 'an AMD' : 'an NVIDIA'} GPU, but the AI Assistant can't access it. AI is running on CPU only, which is significantly slower.`} dismissible={true} onDismiss={handleDismissGpuBanner} buttonProps={{ diff --git a/admin/types/kv_store.ts b/admin/types/kv_store.ts index 8fb2686..a3632ab 100644 --- a/admin/types/kv_store.ts +++ b/admin/types/kv_store.ts @@ -12,6 +12,7 @@ export const KV_STORE_SCHEMA = { 'gpu.type': 'string', 'ai.remoteOllamaUrl': 'string', 'ai.ollamaFlashAttention': 'boolean', + 'ai.amdGpuAcceleration': 'boolean', } as const type KVTagToType = T extends 'boolean' ? boolean : string diff --git a/admin/types/system.ts b/admin/types/system.ts index 7c4e6d7..c8ed4ab 100644 --- a/admin/types/system.ts +++ b/admin/types/system.ts @@ -3,7 +3,9 @@ import { Systeminformation } from 'systeminformation' export type GpuHealthStatus = { status: 'ok' | 'passthrough_failed' | 'no_gpu' | 'ollama_not_installed' hasNvidiaRuntime: boolean + hasRocmRuntime: boolean ollamaGpuAccessible: boolean + gpuVendor?: 'nvidia' | 'amd' } export type SystemInformationResponse = { diff --git a/install/install_nomad.sh b/install/install_nomad.sh index ced178f..ef501a0 100644 --- a/install/install_nomad.sh +++ b/install/install_nomad.sh @@ -517,18 +517,35 @@ verify_gpu_setup() { echo -e "${YELLOW}○${RESET} Docker NVIDIA runtime not detected\\n" fi - # Check for AMD GPU + # Check for AMD GPU — restrict to display controller classes to avoid false positives + # from AMD CPU host bridges, PCI bridges, and chipset devices. + local has_amd_gpu='false' if command -v lspci &> /dev/null; then - if lspci 2>/dev/null | grep -iE "amd|radeon" &> /dev/null; then - echo -e "${YELLOW}○${RESET} AMD GPU detected (ROCm support not currently available)\\n" + if lspci 2>/dev/null | grep -iE "VGA|3D controller|Display" | grep -iE "amd|radeon" &> /dev/null; then + has_amd_gpu='true' + echo -e "${GREEN}✓${RESET} AMD GPU detected — ROCm acceleration will be configured automatically when AI Assistant is installed.\\n" fi fi - + + # Write detected GPU type to a marker file the admin container can read. The admin + # container lacks lspci and AMD GPUs don't register a Docker runtime, so this is the + # only reliable way for the admin to know an AMD GPU is present at install time. + local gpu_marker_path="${NOMAD_DIR}/storage/.nomad-gpu-type" + if command -v nvidia-smi &> /dev/null; then + echo 'nvidia' | sudo tee "${gpu_marker_path}" > /dev/null 2>&1 || true + elif [[ "${has_amd_gpu}" == 'true' ]]; then + echo 'amd' | sudo tee "${gpu_marker_path}" > /dev/null 2>&1 || true + else + sudo rm -f "${gpu_marker_path}" 2>/dev/null || true + fi + echo -e "${YELLOW}===========================================${RESET}\\n" - + # Summary if command -v nvidia-smi &> /dev/null && docker info 2>/dev/null | grep -q "nvidia"; then echo -e "${GREEN}#${RESET} GPU acceleration is properly configured! The AI Assistant will use your GPU.\\n" + elif [[ "${has_amd_gpu}" == 'true' ]]; then + echo -e "${GREEN}#${RESET} GPU acceleration will be enabled (AMD/ROCm) when AI Assistant is installed from the dashboard.\\n" else echo -e "${YELLOW}#${RESET} GPU acceleration not detected. The AI Assistant will run in CPU-only mode.\\n" if command -v nvidia-smi &> /dev/null && ! docker info 2>/dev/null | grep -q "nvidia"; then