diff --git a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx index 5a41052..446b4aa 100644 --- a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx +++ b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx @@ -1,5 +1,6 @@ import { Text, Group, Badge, Box } from '@mantine/core'; import type { BackendOption } from '@/types'; +import { GPUDevice } from '@/types/hardware'; type BackendSelectItemProps = Omit; @@ -8,9 +9,7 @@ export const BackendSelectItem = ({ devices, disabled = false, }: BackendSelectItemProps) => { - const renderDeviceName = ( - device: string | { name: string; isIntegrated: boolean } - ) => { + const renderDeviceName = (device: string | GPUDevice) => { const deviceName = typeof device === 'string' ? device : device.name; return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` diff --git a/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx b/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx index b478e2b..0212cfa 100644 --- a/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx +++ b/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx @@ -28,7 +28,7 @@ export const GpuDeviceSelector = ({ const getDiscreteDeviceCount = () => { if (!selectedBackend?.devices) return 0; - if (backend === 'clblast') { + if (backend === 'clblast' || backend === 'vulkan' || backend === 'rocm') { return selectedBackend.devices.filter( (device) => typeof device === 'string' || !device.isIntegrated ).length; @@ -66,6 +66,25 @@ export const GpuDeviceSelector = ({ ); } + if (backend === 'vulkan' || backend === 'rocm') { + const discreteDeviceOptions = selectedBackend.devices + .map((device, index) => { + if (typeof device === 'object' && device.isIntegrated) { + return null; + } + const deviceName = typeof device === 'string' ? device : device.name; + return { + value: index.toString(), + label: `GPU ${index}: ${deviceName}`, + }; + }) + .filter( + (option): option is NonNullable => option !== null + ); + + return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions]; + } + return [ { value: 'all', label: 'All GPUs' }, ...selectedBackend.devices.map((device, index) => { diff --git a/src/hooks/useWarnings.ts b/src/hooks/useWarnings.ts index 2b52e15..2275af4 100644 --- a/src/hooks/useWarnings.ts +++ b/src/hooks/useWarnings.ts @@ -1,5 +1,6 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; import type { BackendOption, BackendSupport } from '@/types'; +import { CPUCapabilities, GPUDevice } from '@/types/hardware'; export interface Warning { type: 'warning' | 'info'; @@ -46,7 +47,9 @@ const checkModelWarnings = ( interface GpuCapabilities { cuda: { devices: readonly string[] }; - rocm: { devices: readonly string[] }; + rocm: { devices: readonly GPUDevice[] }; + vulkan: { devices: readonly GPUDevice[] }; + clblast: { devices: readonly GPUDevice[] }; } interface GpuInfo { @@ -155,11 +158,9 @@ const checkCpuWarnings = ( const checkBackendWarnings = async (params?: { backend: string; - cpuCapabilities: { - devices: string[]; - } | null; + cpuCapabilities: CPUCapabilities | null; availableBackends: BackendOption[]; -}): Promise => { +}) => { const warnings: Warning[] = []; const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([ diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index a80064d..f4f3eef 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -11,11 +11,12 @@ import type { GPUCapabilities, BasicGPUInfo, GPUMemoryInfo, + GPUDevice, } from '@/types/hardware'; import { execa } from 'execa'; import { formatDeviceName } from '@/utils/format'; import { platform } from 'process'; -import { getVulkanInfo, detectLinuxGPUViaVulkan } from '@/utils/node/vulkan'; +import { getVulkanInfo, detectGPUViaVulkan } from '@/utils/node/vulkan'; const COMMON_EXEC_OPTIONS = { timeout: 3000, @@ -35,11 +36,14 @@ export async function detectCPU() { const result = await safeExecute(async () => { const cpu = await siCpu(); - const devices: string[] = []; + const devices: { name: string; detailedName: string }[] = []; if (cpu.brand) { - devices.push( - `${formatDeviceName(cpu.brand)} (${cpu.cores} cores) @ ${cpu.speed} GHz` - ); + const name = formatDeviceName(cpu.brand); + + devices.push({ + name, + detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`, + }); } const capabilities = { @@ -50,11 +54,10 @@ export async function detectCPU() { return capabilities; }, 'CPU detection failed'); - const fallbackCapabilities = { + cpuCapabilitiesCache = result || { devices: [], }; - cpuCapabilitiesCache = result || fallbackCapabilities; return cpuCapabilitiesCache; } @@ -64,7 +67,7 @@ export async function detectGPU() { } const result = await safeExecute( - () => detectLinuxGPUViaVulkan(), + () => detectGPUViaVulkan(), 'GPU detection failed' ); @@ -101,10 +104,15 @@ async function detectVulkan() { try { const vulkanInfo = await getVulkanInfo(); - const devices: string[] = []; + const devices: GPUDevice[] = []; - for (const gpu of vulkanInfo.discreteGPUs) { - devices.push(formatDeviceName(gpu.deviceName)); + for (const gpu of vulkanInfo.allGPUs) { + const isIntegrated = gpu.isIntegrated; + + devices.push({ + name: isIntegrated ? gpu.deviceName : formatDeviceName(gpu.deviceName), + isIntegrated, + }); } return { @@ -187,21 +195,56 @@ export async function detectROCm() { const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS); if (stdout.trim()) { - const devices: string[] = []; + const devices: GPUDevice[] = []; if (platform === 'win32') { const lines = stdout.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith('Name:')) { - const name = trimmedLine.split('Name:')[1]?.trim(); - if ( - name && - !name.toLowerCase().includes('cpu') && - !devices.includes(formatDeviceName(name)) - ) { - devices.push(formatDeviceName(name)); + if (line.includes('Marketing Name:')) { + const name = line.split('Marketing Name:')[1]?.trim(); + if (name && !name.toLowerCase().includes('cpu')) { + let deviceType = ''; + + const searchRangeLines = 20; + const searchStartIndex = Math.max(0, i - searchRangeLines); + const searchEndIndex = Math.min( + lines.length, + i + searchRangeLines + ); + + for ( + let searchIndex = searchStartIndex; + searchIndex < searchEndIndex; + searchIndex++ + ) { + if (lines[searchIndex].includes('Device Type:')) { + deviceType = + lines[searchIndex].split('Device Type:')[1]?.trim() || ''; + break; + } + } + + if (deviceType !== 'CPU') { + let isIntegrated = true; + try { + const vulkanInfo = await getVulkanInfo(); + const matchingGPU = vulkanInfo.allGPUs.find( + (gpu) => + gpu.deviceName.includes(name) || + name.includes(gpu.deviceName) + ); + isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false; + } catch { + isIntegrated = false; + } + + devices.push({ + name: isIntegrated ? name : formatDeviceName(name), + isIntegrated, + }); + } } } } @@ -235,7 +278,24 @@ export async function detectROCm() { } if (deviceType !== 'CPU') { - devices.push(formatDeviceName(name)); + // Check if integrated by cross-referencing with vulkan GPU list + let isIntegrated = true; + try { + const vulkanInfo = await getVulkanInfo(); + const matchingGPU = vulkanInfo.allGPUs.find( + (gpu) => + gpu.deviceName.includes(name) || + name.includes(gpu.deviceName) + ); + isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false; + } catch { + isIntegrated = false; + } + + devices.push({ + name: isIntegrated ? name : formatDeviceName(name), + isIntegrated, + }); } } } @@ -301,8 +361,8 @@ export async function detectROCm() { try { const vulkanInfo = await getVulkanInfo(); - for (const gpu of vulkanInfo.discreteGPUs) { - if (gpu.driverInfo) { + for (const gpu of vulkanInfo.allGPUs) { + if (gpu.driverInfo && !gpu.isIntegrated) { driverVersion = gpu.driverInfo; break; } @@ -324,7 +384,7 @@ export async function detectROCm() { } function parseClInfoOutput(output: string) { - const devices: { name: string; isIntegrated: boolean }[] = []; + const devices: GPUDevice[] = []; const lines = output.split('\n'); let currentPlatform = ''; @@ -342,9 +402,11 @@ function parseClInfoOutput(output: string) { const computeUnits = findComputeUnitsInClInfo(lines, i); if (deviceName && currentPlatform) { + const isIntegrated = !isDiscreteGPU(computeUnits); + devices.push({ - name: formatDeviceName(deviceName), - isIntegrated: !isDiscreteGPU(computeUnits), + name: isIntegrated ? deviceName : formatDeviceName(deviceName), + isIntegrated, }); } } diff --git a/src/main/modules/koboldcpp/backend.ts b/src/main/modules/koboldcpp/backend.ts index fb38659..b7cb8df 100644 --- a/src/main/modules/koboldcpp/backend.ts +++ b/src/main/modules/koboldcpp/backend.ts @@ -159,7 +159,7 @@ export async function getAvailableBackends(includeDisabled = false) { backends.push({ value: 'cpu', label: 'CPU', - devices: cpuCapabilities?.devices, + devices: cpuCapabilities?.devices.map((device) => device.name) || [], disabled: false, }); diff --git a/src/types/hardware.d.ts b/src/types/hardware.d.ts index 95d71e3..0f3f2cc 100644 --- a/src/types/hardware.d.ts +++ b/src/types/hardware.d.ts @@ -1,5 +1,5 @@ export interface CPUCapabilities { - devices: string[]; + devices: { name: string; detailedName: string }[]; } export interface GPUMemoryInfo { @@ -12,6 +12,11 @@ export interface SystemMemoryInfo { type?: string; } +export interface GPUDevice { + readonly name: string; + readonly isIntegrated: boolean; +} + export interface GPUCapabilities { cuda: { readonly devices: readonly string[]; @@ -19,16 +24,16 @@ export interface GPUCapabilities { readonly driverVersion?: string; }; rocm: { - readonly devices: readonly string[]; + readonly devices: readonly GPUDevice[]; readonly version?: string; readonly driverVersion?: string; }; vulkan: { - readonly devices: readonly string[]; + readonly devices: readonly GPUDevice[]; readonly version?: string; }; clblast: { - readonly devices: readonly CLBlastDevice[]; + readonly devices: readonly GPUDevice[]; readonly version?: string; }; } @@ -39,11 +44,6 @@ export interface BasicGPUInfo { gpuInfo: string[]; } -export interface CLBlastDevice { - readonly name: string; - readonly isIntegrated: boolean; -} - export interface HardwareDetectionResult { readonly supported: boolean; readonly devices: readonly string[]; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 117e74a..0a1621f 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,3 +1,5 @@ +import { GPUDevice } from './hardware'; + export interface ConfigFile { name: string; path: string; @@ -72,10 +74,7 @@ export interface SelectOption { } export interface BackendOption extends SelectOption { - readonly devices?: readonly ( - | string - | { name: string; isIntegrated: boolean } - )[]; + readonly devices?: readonly (string | GPUDevice)[]; readonly disabled?: boolean; } diff --git a/src/utils/node/vulkan.ts b/src/utils/node/vulkan.ts index 65e41f7..85abf4e 100644 --- a/src/utils/node/vulkan.ts +++ b/src/utils/node/vulkan.ts @@ -3,12 +3,13 @@ import { execa } from 'execa'; import { formatDeviceName } from '@/utils/format'; let vulkanInfoCache: { - discreteGPUs: { + allGPUs: { deviceName: string; driverInfo?: string; apiVersion?: string; hasAMD: boolean; hasNVIDIA: boolean; + isIntegrated: boolean; }[]; apiVersion?: string; } | null = null; @@ -25,19 +26,20 @@ export async function getVulkanInfo() { reject: false, }); - const discreteGPUs: { + const allGPUs: { deviceName: string; driverInfo?: string; apiVersion?: string; hasAMD: boolean; hasNVIDIA: boolean; + isIntegrated: boolean; }[] = []; let globalApiVersion: string | undefined; if (stdout.trim()) { const lines = stdout.split('\n'); - let foundDiscreteGPU = false; - let currentGPU: (typeof discreteGPUs)[0] | null = null; + let foundGPU = false; + let currentGPU: (typeof allGPUs)[0] | null = null; for (const line of lines) { if ( @@ -51,15 +53,22 @@ export async function getVulkanInfo() { } } - if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) { - foundDiscreteGPU = true; + if ( + line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU') || + line.includes('PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU') + ) { + foundGPU = true; + const isIntegrated = line.includes( + 'PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU' + ); currentGPU = { deviceName: '', hasAMD: false, hasNVIDIA: false, + isIntegrated, }; } else if ( - foundDiscreteGPU && + foundGPU && currentGPU && line.includes('deviceName') && line.includes('=') @@ -79,17 +88,13 @@ export async function getVulkanInfo() { name.toLowerCase().includes('gtx'); } } - } else if ( - foundDiscreteGPU && - currentGPU && - line.includes('driverInfo') - ) { + } else if (foundGPU && currentGPU && line.includes('driverInfo')) { const mesaMatch = line.match(/Mesa\s+(.+)/); if (mesaMatch) { currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`; } } else if ( - foundDiscreteGPU && + foundGPU && currentGPU && line.includes('apiVersion') && line.includes('=') @@ -101,35 +106,35 @@ export async function getVulkanInfo() { globalApiVersion = match[1]; } } - } else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) { + } else if (foundGPU && currentGPU && line.includes('GPU')) { if (currentGPU.deviceName) { - discreteGPUs.push(currentGPU); + allGPUs.push(currentGPU); } - foundDiscreteGPU = false; + foundGPU = false; currentGPU = null; } } - if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) { - discreteGPUs.push(currentGPU); + if (foundGPU && currentGPU && currentGPU.deviceName) { + allGPUs.push(currentGPU); } } vulkanInfoCache = { - discreteGPUs, + allGPUs, apiVersion: globalApiVersion, }; return vulkanInfoCache; } catch { vulkanInfoCache = { - discreteGPUs: [], + allGPUs: [], }; return vulkanInfoCache; } } -export async function detectLinuxGPUViaVulkan() { +export async function detectGPUViaVulkan() { try { const vulkanInfo = await getVulkanInfo(); @@ -137,7 +142,7 @@ export async function detectLinuxGPUViaVulkan() { let hasNVIDIA = false; const gpuInfo: string[] = []; - for (const gpu of vulkanInfo.discreteGPUs) { + for (const gpu of vulkanInfo.allGPUs.filter((g) => !g.isIntegrated)) { gpuInfo.push(formatDeviceName(gpu.deviceName)); if (gpu.hasAMD) { diff --git a/src/utils/systemInfo.ts b/src/utils/systemInfo.ts index 5a19ad6..662235e 100644 --- a/src/utils/systemInfo.ts +++ b/src/utils/systemInfo.ts @@ -130,7 +130,7 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [ label: 'CPU', value: hardwareInfo.cpu.devices.length > 0 - ? hardwareInfo.cpu.devices[0] + ? hardwareInfo.cpu.devices[0].detailedName : 'Unknown', }, { @@ -147,12 +147,42 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [ }` : 'Detecting...', }, - ...(hardwareInfo.gpu.gpuInfo.length > 0 - ? hardwareInfo.gpu.gpuInfo.map((gpu, index) => ({ - label: `GPU ${index > 0 ? index + 1 : ''}`.trim(), - value: gpu, - })) - : [{ label: 'GPU', value: 'No GPU detected' }]), + ...(() => { + const discreteGPUs = []; + + if (hardwareInfo.gpuCapabilities.vulkan.devices.length > 0) { + discreteGPUs.push( + ...hardwareInfo.gpuCapabilities.vulkan.devices.filter( + (gpu) => !gpu.isIntegrated + ) + ); + } + if (hardwareInfo.gpuCapabilities.rocm.devices.length > 0) { + discreteGPUs.push( + ...hardwareInfo.gpuCapabilities.rocm.devices.filter( + (gpu) => !gpu.isIntegrated + ) + ); + } + if (hardwareInfo.gpuCapabilities.clblast.devices.length > 0) { + discreteGPUs.push( + ...hardwareInfo.gpuCapabilities.clblast.devices.filter( + (gpu) => !gpu.isIntegrated + ) + ); + } + + const uniqueDiscreteGPUs = discreteGPUs.filter( + (gpu, index, arr) => arr.findIndex((g) => g.name === gpu.name) === index + ); + + return uniqueDiscreteGPUs.length > 0 + ? uniqueDiscreteGPUs.map((gpu, index) => ({ + label: `GPU ${index > 0 ? index + 1 : ''}`.trim(), + value: gpu.name, + })) + : [{ label: 'GPU', value: 'No discrete GPU detected' }]; + })(), ...(hardwareInfo.gpuMemory && hardwareInfo.gpuMemory.length > 0 ? hardwareInfo.gpuMemory.map((mem, index) => ({ label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(),