diff --git a/package.json b/package.json index 2f0415c..a00442c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.6.0-beta-1", + "version": "1.6.0", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/js": "^9.36.0", "@types/node": "^24.5.2", - "@types/react": "^19.1.13", + "@types/react": "^19.1.14", "@types/react-dom": "^19.1.9", "@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/parser": "^8.44.1", diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index 07d9a48..66fcf8f 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -2,12 +2,11 @@ import { cpu as siCpu, cpuFlags, - graphics as siGraphics, mem as siMem, memLayout as siMemLayout, } from 'systeminformation'; import { safeExecute } from '@/utils/node/logging'; -import { getGPUData, isDiscreteBusAddress } from '@/utils/node/gpu'; +import { getGPUData, detectGPUViaSI } from '@/utils/node/gpu'; import type { CPUCapabilities, GPUCapabilities, @@ -17,6 +16,7 @@ import type { import { execa } from 'execa'; import { formatDeviceName } from '@/utils/format'; import { platform } from 'process'; +import { getVulkanInfo, detectLinuxGPUViaVulkan } from '@/utils/node/vulkan'; const COMMON_EXEC_OPTIONS = { timeout: 3000, @@ -27,16 +27,6 @@ let cpuCapabilitiesCache: CPUCapabilities | null = null; let basicGPUInfoCache: BasicGPUInfo | null = null; let gpuCapabilitiesCache: GPUCapabilities | null = null; let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null; -let vulkanInfoCache: { - discreteGPUs: { - deviceName: string; - driverInfo?: string; - apiVersion?: string; - hasAMD: boolean; - hasNVIDIA: boolean; - }[]; - apiVersion?: string; -} | null = null; export async function detectCPU() { if (cpuCapabilitiesCache) { @@ -99,192 +89,6 @@ export async function detectGPU() { return basicGPUInfoCache; } -// eslint-disable-next-line sonarjs/cognitive-complexity -async function getVulkanInfo() { - if (vulkanInfoCache) { - return vulkanInfoCache; - } - - try { - const { stdout } = await execa( - 'vulkaninfo', - ['--summary'], - COMMON_EXEC_OPTIONS - ); - - const discreteGPUs: { - deviceName: string; - driverInfo?: string; - apiVersion?: string; - hasAMD: boolean; - hasNVIDIA: boolean; - }[] = []; - let globalApiVersion: string | undefined; - - if (stdout.trim()) { - const lines = stdout.split('\n'); - let foundDiscreteGPU = false; - let currentGPU: (typeof discreteGPUs)[0] | null = null; - - for (const line of lines) { - if ( - !globalApiVersion && - line.includes('apiVersion') && - line.includes('=') - ) { - const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/); - if (match) { - globalApiVersion = match[1]; - } - } - - if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) { - foundDiscreteGPU = true; - currentGPU = { - deviceName: '', - hasAMD: false, - hasNVIDIA: false, - }; - } else if ( - foundDiscreteGPU && - currentGPU && - line.includes('deviceName') && - line.includes('=') - ) { - const parts = line.split('='); - if (parts.length >= 2) { - const name = parts[1]?.trim(); - if (name) { - currentGPU.deviceName = name; - currentGPU.hasAMD = - name.toLowerCase().includes('amd') || - name.toLowerCase().includes('radeon'); - currentGPU.hasNVIDIA = - name.toLowerCase().includes('nvidia') || - name.toLowerCase().includes('geforce') || - name.toLowerCase().includes('rtx') || - name.toLowerCase().includes('gtx'); - } - } - } else if ( - foundDiscreteGPU && - currentGPU && - line.includes('driverInfo') - ) { - const mesaMatch = line.match(/Mesa\s+(.+)/); - if (mesaMatch) { - currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`; - } - } else if ( - foundDiscreteGPU && - currentGPU && - line.includes('apiVersion') && - line.includes('=') - ) { - const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/); - if (match) { - currentGPU.apiVersion = match[1]; - if (!globalApiVersion) { - globalApiVersion = match[1]; - } - } - } else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) { - if (currentGPU.deviceName) { - discreteGPUs.push(currentGPU); - } - foundDiscreteGPU = false; - currentGPU = null; - } - } - - if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) { - discreteGPUs.push(currentGPU); - } - } - - vulkanInfoCache = { - discreteGPUs, - apiVersion: globalApiVersion, - }; - - return vulkanInfoCache; - } catch { - vulkanInfoCache = { - discreteGPUs: [], - }; - return vulkanInfoCache; - } -} - -async function detectLinuxGPUViaVulkan() { - try { - const vulkanInfo = await getVulkanInfo(); - - let hasAMD = false; - let hasNVIDIA = false; - const gpuInfo: string[] = []; - - for (const gpu of vulkanInfo.discreteGPUs) { - gpuInfo.push(formatDeviceName(gpu.deviceName)); - - if (gpu.hasAMD) { - hasAMD = true; - } - if (gpu.hasNVIDIA) { - hasNVIDIA = true; - } - } - - return { - hasAMD, - hasNVIDIA, - gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], - }; - } catch { - return { - hasAMD: false, - hasNVIDIA: false, - gpuInfo: ['GPU detection failed'], - }; - } -} - -async function detectGPUViaSI() { - const graphics = await siGraphics(); - - let hasAMD = false; - let hasNVIDIA = false; - const gpuInfo: string[] = []; - - const discreteControllers = graphics.controllers.filter( - (controller) => - controller.busAddress && isDiscreteBusAddress(controller.busAddress) - ); - - for (const controller of discreteControllers) { - if ( - controller.vendor?.toLowerCase().includes('amd') || - controller.vendor?.toLowerCase().includes('ati') - ) { - hasAMD = true; - } - - if (controller.vendor?.toLowerCase().includes('nvidia')) { - hasNVIDIA = true; - } - - if (controller.model) { - gpuInfo.push(controller.model); - } - } - - return { - hasAMD, - hasNVIDIA, - gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], - }; -} - export async function detectGPUCapabilities() { // WARNING: we're not worrying about the users that update their system // during runtime and not restart. Should we be though? diff --git a/src/utils/node/gpu.ts b/src/utils/node/gpu.ts index 55f2232..4ffb486 100644 --- a/src/utils/node/gpu.ts +++ b/src/utils/node/gpu.ts @@ -2,6 +2,7 @@ import { readFile, readdir } from 'fs/promises'; import { join } from 'path'; import { platform } from 'process'; import { execa } from 'execa'; +import { graphics as siGraphics } from 'systeminformation'; interface CachedGPUInfo { devicePath: string; @@ -241,3 +242,39 @@ export function isDiscreteBusAddress(busAddress?: string) { return false; } + +export async function detectGPUViaSI() { + const graphics = await siGraphics(); + + let hasAMD = false; + let hasNVIDIA = false; + const gpuInfo: string[] = []; + + const discreteControllers = graphics.controllers.filter( + (controller) => + controller.busAddress && isDiscreteBusAddress(controller.busAddress) + ); + + for (const controller of discreteControllers) { + if ( + controller.vendor?.toLowerCase().includes('amd') || + controller.vendor?.toLowerCase().includes('ati') + ) { + hasAMD = true; + } + + if (controller.vendor?.toLowerCase().includes('nvidia')) { + hasNVIDIA = true; + } + + if (controller.model) { + gpuInfo.push(controller.model); + } + } + + return { + hasAMD, + hasNVIDIA, + gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], + }; +} diff --git a/src/utils/node/vulkan.ts b/src/utils/node/vulkan.ts new file mode 100644 index 0000000..8744364 --- /dev/null +++ b/src/utils/node/vulkan.ts @@ -0,0 +1,163 @@ +import { execa } from 'execa'; + +import { formatDeviceName } from '@/utils/format'; + +let vulkanInfoCache: { + discreteGPUs: { + deviceName: string; + driverInfo?: string; + apiVersion?: string; + hasAMD: boolean; + hasNVIDIA: boolean; + }[]; + apiVersion?: string; +} | null = null; + +// eslint-disable-next-line sonarjs/cognitive-complexity +export async function getVulkanInfo() { + if (vulkanInfoCache) { + return vulkanInfoCache; + } + + try { + const { stdout } = await execa('vulkaninfo', ['--summary'], { + timeout: 3000, + reject: false, + }); + + const discreteGPUs: { + deviceName: string; + driverInfo?: string; + apiVersion?: string; + hasAMD: boolean; + hasNVIDIA: boolean; + }[] = []; + let globalApiVersion: string | undefined; + + if (stdout.trim()) { + const lines = stdout.split('\n'); + let foundDiscreteGPU = false; + let currentGPU: (typeof discreteGPUs)[0] | null = null; + + for (const line of lines) { + if ( + !globalApiVersion && + line.includes('apiVersion') && + line.includes('=') + ) { + const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/); + if (match) { + globalApiVersion = match[1]; + } + } + + if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) { + foundDiscreteGPU = true; + currentGPU = { + deviceName: '', + hasAMD: false, + hasNVIDIA: false, + }; + } else if ( + foundDiscreteGPU && + currentGPU && + line.includes('deviceName') && + line.includes('=') + ) { + const parts = line.split('='); + if (parts.length >= 2) { + const name = parts[1]?.trim(); + if (name) { + currentGPU.deviceName = name; + currentGPU.hasAMD = + name.toLowerCase().includes('amd') || + name.toLowerCase().includes('radeon'); + currentGPU.hasNVIDIA = + name.toLowerCase().includes('nvidia') || + name.toLowerCase().includes('geforce') || + name.toLowerCase().includes('rtx') || + name.toLowerCase().includes('gtx'); + } + } + } else if ( + foundDiscreteGPU && + currentGPU && + line.includes('driverInfo') + ) { + const mesaMatch = line.match(/Mesa\s+(.+)/); + if (mesaMatch) { + currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`; + } + } else if ( + foundDiscreteGPU && + currentGPU && + line.includes('apiVersion') && + line.includes('=') + ) { + const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/); + if (match) { + currentGPU.apiVersion = match[1]; + if (!globalApiVersion) { + globalApiVersion = match[1]; + } + } + } else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) { + if (currentGPU.deviceName) { + discreteGPUs.push(currentGPU); + } + foundDiscreteGPU = false; + currentGPU = null; + } + } + + if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) { + discreteGPUs.push(currentGPU); + } + } + + vulkanInfoCache = { + discreteGPUs, + apiVersion: globalApiVersion, + }; + + return vulkanInfoCache; + } catch { + vulkanInfoCache = { + discreteGPUs: [], + }; + return vulkanInfoCache; + } +} + +export async function detectLinuxGPUViaVulkan() { + try { + const vulkanInfo = await getVulkanInfo(); + + let hasAMD = false; + let hasNVIDIA = false; + const gpuInfo: string[] = []; + + for (const gpu of vulkanInfo.discreteGPUs) { + gpuInfo.push(formatDeviceName(gpu.deviceName)); + + if (gpu.hasAMD) { + hasAMD = true; + } + if (gpu.hasNVIDIA) { + hasNVIDIA = true; + } + } + + return { + hasAMD, + hasNVIDIA, + gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], + }; + } catch { + return { + hasAMD: false, + hasNVIDIA: false, + gpuInfo: ['GPU detection failed'], + }; + } +} diff --git a/yarn.lock b/yarn.lock index 09261e7..7129ef7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1456,12 +1456,12 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^19.1.13": - version: 19.1.13 - resolution: "@types/react@npm:19.1.13" +"@types/react@npm:^19.1.14": + version: 19.1.14 + resolution: "@types/react@npm:19.1.14" dependencies: csstype: "npm:^3.0.2" - checksum: 10c0/75e35b54883f5ed07d3b5cb16a4711b6dbb7ec6b74301bcb9bfa697c9d9fff022ec508e1719e7b2c69e2e8b042faac1125be7717b5e5e084f816a2c88e136920 + checksum: 10c0/0d1629e065413be79c949f011845836e7f515c8f97dbfef057719be2f312b404405d2d064cb28e03363c62c153c100e84268c09dd9aeda488858d45033b520f0 languageName: node linkType: hard @@ -3866,7 +3866,7 @@ __metadata: "@mantine/core": "npm:8.3.1" "@mantine/hooks": "npm:8.3.1" "@types/node": "npm:^24.5.2" - "@types/react": "npm:^19.1.13" + "@types/react": "npm:^19.1.14" "@types/react-dom": "npm:^19.1.9" "@types/yauzl": "npm:^2.10.3" "@typescript-eslint/eslint-plugin": "npm:^8.44.1"