refactor vulkan detection

This commit is contained in:
Egor 2025-09-26 11:25:15 -07:00
parent 3ef91d1198
commit 3652ce4686
5 changed files with 209 additions and 205 deletions

View file

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

View file

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

View file

@ -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'],
};
}

163
src/utils/node/vulkan.ts Normal file
View file

@ -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'],
};
}
}

View file

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