better discrete device filtering

This commit is contained in:
Egor 2025-09-26 16:01:39 -07:00
parent ddf42c41db
commit b2533659c2
9 changed files with 195 additions and 80 deletions

View file

@ -1,5 +1,6 @@
import { Text, Group, Badge, Box } from '@mantine/core'; import { Text, Group, Badge, Box } from '@mantine/core';
import type { BackendOption } from '@/types'; import type { BackendOption } from '@/types';
import { GPUDevice } from '@/types/hardware';
type BackendSelectItemProps = Omit<BackendOption, 'value'>; type BackendSelectItemProps = Omit<BackendOption, 'value'>;
@ -8,9 +9,7 @@ export const BackendSelectItem = ({
devices, devices,
disabled = false, disabled = false,
}: BackendSelectItemProps) => { }: BackendSelectItemProps) => {
const renderDeviceName = ( const renderDeviceName = (device: string | GPUDevice) => {
device: string | { name: string; isIntegrated: boolean }
) => {
const deviceName = typeof device === 'string' ? device : device.name; const deviceName = typeof device === 'string' ? device : device.name;
return deviceName.length > 25 return deviceName.length > 25
? `${deviceName.slice(0, 25)}...` ? `${deviceName.slice(0, 25)}...`

View file

@ -28,7 +28,7 @@ export const GpuDeviceSelector = ({
const getDiscreteDeviceCount = () => { const getDiscreteDeviceCount = () => {
if (!selectedBackend?.devices) return 0; if (!selectedBackend?.devices) return 0;
if (backend === 'clblast') { if (backend === 'clblast' || backend === 'vulkan' || backend === 'rocm') {
return selectedBackend.devices.filter( return selectedBackend.devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated (device) => typeof device === 'string' || !device.isIntegrated
).length; ).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<typeof option> => option !== null
);
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
}
return [ return [
{ value: 'all', label: 'All GPUs' }, { value: 'all', label: 'All GPUs' },
...selectedBackend.devices.map((device, index) => { ...selectedBackend.devices.map((device, index) => {

View file

@ -1,5 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import type { BackendOption, BackendSupport } from '@/types'; import type { BackendOption, BackendSupport } from '@/types';
import { CPUCapabilities, GPUDevice } from '@/types/hardware';
export interface Warning { export interface Warning {
type: 'warning' | 'info'; type: 'warning' | 'info';
@ -46,7 +47,9 @@ const checkModelWarnings = (
interface GpuCapabilities { interface GpuCapabilities {
cuda: { devices: readonly string[] }; cuda: { devices: readonly string[] };
rocm: { devices: readonly string[] }; rocm: { devices: readonly GPUDevice[] };
vulkan: { devices: readonly GPUDevice[] };
clblast: { devices: readonly GPUDevice[] };
} }
interface GpuInfo { interface GpuInfo {
@ -155,11 +158,9 @@ const checkCpuWarnings = (
const checkBackendWarnings = async (params?: { const checkBackendWarnings = async (params?: {
backend: string; backend: string;
cpuCapabilities: { cpuCapabilities: CPUCapabilities | null;
devices: string[];
} | null;
availableBackends: BackendOption[]; availableBackends: BackendOption[];
}): Promise<Warning[]> => { }) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([ const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([

View file

@ -11,11 +11,12 @@ import type {
GPUCapabilities, GPUCapabilities,
BasicGPUInfo, BasicGPUInfo,
GPUMemoryInfo, GPUMemoryInfo,
GPUDevice,
} from '@/types/hardware'; } from '@/types/hardware';
import { execa } from 'execa'; import { execa } from 'execa';
import { formatDeviceName } from '@/utils/format'; import { formatDeviceName } from '@/utils/format';
import { platform } from 'process'; import { platform } from 'process';
import { getVulkanInfo, detectLinuxGPUViaVulkan } from '@/utils/node/vulkan'; import { getVulkanInfo, detectGPUViaVulkan } from '@/utils/node/vulkan';
const COMMON_EXEC_OPTIONS = { const COMMON_EXEC_OPTIONS = {
timeout: 3000, timeout: 3000,
@ -35,11 +36,14 @@ export async function detectCPU() {
const result = await safeExecute(async () => { const result = await safeExecute(async () => {
const cpu = await siCpu(); const cpu = await siCpu();
const devices: string[] = []; const devices: { name: string; detailedName: string }[] = [];
if (cpu.brand) { if (cpu.brand) {
devices.push( const name = formatDeviceName(cpu.brand);
`${formatDeviceName(cpu.brand)} (${cpu.cores} cores) @ ${cpu.speed} GHz`
); devices.push({
name,
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`,
});
} }
const capabilities = { const capabilities = {
@ -50,11 +54,10 @@ export async function detectCPU() {
return capabilities; return capabilities;
}, 'CPU detection failed'); }, 'CPU detection failed');
const fallbackCapabilities = { cpuCapabilitiesCache = result || {
devices: [], devices: [],
}; };
cpuCapabilitiesCache = result || fallbackCapabilities;
return cpuCapabilitiesCache; return cpuCapabilitiesCache;
} }
@ -64,7 +67,7 @@ export async function detectGPU() {
} }
const result = await safeExecute( const result = await safeExecute(
() => detectLinuxGPUViaVulkan(), () => detectGPUViaVulkan(),
'GPU detection failed' 'GPU detection failed'
); );
@ -101,10 +104,15 @@ async function detectVulkan() {
try { try {
const vulkanInfo = await getVulkanInfo(); const vulkanInfo = await getVulkanInfo();
const devices: string[] = []; const devices: GPUDevice[] = [];
for (const gpu of vulkanInfo.discreteGPUs) { for (const gpu of vulkanInfo.allGPUs) {
devices.push(formatDeviceName(gpu.deviceName)); const isIntegrated = gpu.isIntegrated;
devices.push({
name: isIntegrated ? gpu.deviceName : formatDeviceName(gpu.deviceName),
isIntegrated,
});
} }
return { return {
@ -187,21 +195,56 @@ export async function detectROCm() {
const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS); const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS);
if (stdout.trim()) { if (stdout.trim()) {
const devices: string[] = []; const devices: GPUDevice[] = [];
if (platform === 'win32') { if (platform === 'win32') {
const lines = stdout.split('\n'); const lines = stdout.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const line of lines) { if (line.includes('Marketing Name:')) {
const trimmedLine = line.trim(); const name = line.split('Marketing Name:')[1]?.trim();
if (trimmedLine.startsWith('Name:')) { if (name && !name.toLowerCase().includes('cpu')) {
const name = trimmedLine.split('Name:')[1]?.trim(); let deviceType = '';
if (
name && const searchRangeLines = 20;
!name.toLowerCase().includes('cpu') && const searchStartIndex = Math.max(0, i - searchRangeLines);
!devices.includes(formatDeviceName(name)) const searchEndIndex = Math.min(
) { lines.length,
devices.push(formatDeviceName(name)); 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') { 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 { try {
const vulkanInfo = await getVulkanInfo(); const vulkanInfo = await getVulkanInfo();
for (const gpu of vulkanInfo.discreteGPUs) { for (const gpu of vulkanInfo.allGPUs) {
if (gpu.driverInfo) { if (gpu.driverInfo && !gpu.isIntegrated) {
driverVersion = gpu.driverInfo; driverVersion = gpu.driverInfo;
break; break;
} }
@ -324,7 +384,7 @@ export async function detectROCm() {
} }
function parseClInfoOutput(output: string) { function parseClInfoOutput(output: string) {
const devices: { name: string; isIntegrated: boolean }[] = []; const devices: GPUDevice[] = [];
const lines = output.split('\n'); const lines = output.split('\n');
let currentPlatform = ''; let currentPlatform = '';
@ -342,9 +402,11 @@ function parseClInfoOutput(output: string) {
const computeUnits = findComputeUnitsInClInfo(lines, i); const computeUnits = findComputeUnitsInClInfo(lines, i);
if (deviceName && currentPlatform) { if (deviceName && currentPlatform) {
const isIntegrated = !isDiscreteGPU(computeUnits);
devices.push({ devices.push({
name: formatDeviceName(deviceName), name: isIntegrated ? deviceName : formatDeviceName(deviceName),
isIntegrated: !isDiscreteGPU(computeUnits), isIntegrated,
}); });
} }
} }

View file

@ -159,7 +159,7 @@ export async function getAvailableBackends(includeDisabled = false) {
backends.push({ backends.push({
value: 'cpu', value: 'cpu',
label: 'CPU', label: 'CPU',
devices: cpuCapabilities?.devices, devices: cpuCapabilities?.devices.map((device) => device.name) || [],
disabled: false, disabled: false,
}); });

View file

@ -1,5 +1,5 @@
export interface CPUCapabilities { export interface CPUCapabilities {
devices: string[]; devices: { name: string; detailedName: string }[];
} }
export interface GPUMemoryInfo { export interface GPUMemoryInfo {
@ -12,6 +12,11 @@ export interface SystemMemoryInfo {
type?: string; type?: string;
} }
export interface GPUDevice {
readonly name: string;
readonly isIntegrated: boolean;
}
export interface GPUCapabilities { export interface GPUCapabilities {
cuda: { cuda: {
readonly devices: readonly string[]; readonly devices: readonly string[];
@ -19,16 +24,16 @@ export interface GPUCapabilities {
readonly driverVersion?: string; readonly driverVersion?: string;
}; };
rocm: { rocm: {
readonly devices: readonly string[]; readonly devices: readonly GPUDevice[];
readonly version?: string; readonly version?: string;
readonly driverVersion?: string; readonly driverVersion?: string;
}; };
vulkan: { vulkan: {
readonly devices: readonly string[]; readonly devices: readonly GPUDevice[];
readonly version?: string; readonly version?: string;
}; };
clblast: { clblast: {
readonly devices: readonly CLBlastDevice[]; readonly devices: readonly GPUDevice[];
readonly version?: string; readonly version?: string;
}; };
} }
@ -39,11 +44,6 @@ export interface BasicGPUInfo {
gpuInfo: string[]; gpuInfo: string[];
} }
export interface CLBlastDevice {
readonly name: string;
readonly isIntegrated: boolean;
}
export interface HardwareDetectionResult { export interface HardwareDetectionResult {
readonly supported: boolean; readonly supported: boolean;
readonly devices: readonly string[]; readonly devices: readonly string[];

View file

@ -1,3 +1,5 @@
import { GPUDevice } from './hardware';
export interface ConfigFile { export interface ConfigFile {
name: string; name: string;
path: string; path: string;
@ -72,10 +74,7 @@ export interface SelectOption {
} }
export interface BackendOption extends SelectOption { export interface BackendOption extends SelectOption {
readonly devices?: readonly ( readonly devices?: readonly (string | GPUDevice)[];
| string
| { name: string; isIntegrated: boolean }
)[];
readonly disabled?: boolean; readonly disabled?: boolean;
} }

View file

@ -3,12 +3,13 @@ import { execa } from 'execa';
import { formatDeviceName } from '@/utils/format'; import { formatDeviceName } from '@/utils/format';
let vulkanInfoCache: { let vulkanInfoCache: {
discreteGPUs: { allGPUs: {
deviceName: string; deviceName: string;
driverInfo?: string; driverInfo?: string;
apiVersion?: string; apiVersion?: string;
hasAMD: boolean; hasAMD: boolean;
hasNVIDIA: boolean; hasNVIDIA: boolean;
isIntegrated: boolean;
}[]; }[];
apiVersion?: string; apiVersion?: string;
} | null = null; } | null = null;
@ -25,19 +26,20 @@ export async function getVulkanInfo() {
reject: false, reject: false,
}); });
const discreteGPUs: { const allGPUs: {
deviceName: string; deviceName: string;
driverInfo?: string; driverInfo?: string;
apiVersion?: string; apiVersion?: string;
hasAMD: boolean; hasAMD: boolean;
hasNVIDIA: boolean; hasNVIDIA: boolean;
isIntegrated: boolean;
}[] = []; }[] = [];
let globalApiVersion: string | undefined; let globalApiVersion: string | undefined;
if (stdout.trim()) { if (stdout.trim()) {
const lines = stdout.split('\n'); const lines = stdout.split('\n');
let foundDiscreteGPU = false; let foundGPU = false;
let currentGPU: (typeof discreteGPUs)[0] | null = null; let currentGPU: (typeof allGPUs)[0] | null = null;
for (const line of lines) { for (const line of lines) {
if ( if (
@ -51,15 +53,22 @@ export async function getVulkanInfo() {
} }
} }
if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) { if (
foundDiscreteGPU = true; 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 = { currentGPU = {
deviceName: '', deviceName: '',
hasAMD: false, hasAMD: false,
hasNVIDIA: false, hasNVIDIA: false,
isIntegrated,
}; };
} else if ( } else if (
foundDiscreteGPU && foundGPU &&
currentGPU && currentGPU &&
line.includes('deviceName') && line.includes('deviceName') &&
line.includes('=') line.includes('=')
@ -79,17 +88,13 @@ export async function getVulkanInfo() {
name.toLowerCase().includes('gtx'); name.toLowerCase().includes('gtx');
} }
} }
} else if ( } else if (foundGPU && currentGPU && line.includes('driverInfo')) {
foundDiscreteGPU &&
currentGPU &&
line.includes('driverInfo')
) {
const mesaMatch = line.match(/Mesa\s+(.+)/); const mesaMatch = line.match(/Mesa\s+(.+)/);
if (mesaMatch) { if (mesaMatch) {
currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`; currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`;
} }
} else if ( } else if (
foundDiscreteGPU && foundGPU &&
currentGPU && currentGPU &&
line.includes('apiVersion') && line.includes('apiVersion') &&
line.includes('=') line.includes('=')
@ -101,35 +106,35 @@ export async function getVulkanInfo() {
globalApiVersion = match[1]; globalApiVersion = match[1];
} }
} }
} else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) { } else if (foundGPU && currentGPU && line.includes('GPU')) {
if (currentGPU.deviceName) { if (currentGPU.deviceName) {
discreteGPUs.push(currentGPU); allGPUs.push(currentGPU);
} }
foundDiscreteGPU = false; foundGPU = false;
currentGPU = null; currentGPU = null;
} }
} }
if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) { if (foundGPU && currentGPU && currentGPU.deviceName) {
discreteGPUs.push(currentGPU); allGPUs.push(currentGPU);
} }
} }
vulkanInfoCache = { vulkanInfoCache = {
discreteGPUs, allGPUs,
apiVersion: globalApiVersion, apiVersion: globalApiVersion,
}; };
return vulkanInfoCache; return vulkanInfoCache;
} catch { } catch {
vulkanInfoCache = { vulkanInfoCache = {
discreteGPUs: [], allGPUs: [],
}; };
return vulkanInfoCache; return vulkanInfoCache;
} }
} }
export async function detectLinuxGPUViaVulkan() { export async function detectGPUViaVulkan() {
try { try {
const vulkanInfo = await getVulkanInfo(); const vulkanInfo = await getVulkanInfo();
@ -137,7 +142,7 @@ export async function detectLinuxGPUViaVulkan() {
let hasNVIDIA = false; let hasNVIDIA = false;
const gpuInfo: string[] = []; const gpuInfo: string[] = [];
for (const gpu of vulkanInfo.discreteGPUs) { for (const gpu of vulkanInfo.allGPUs.filter((g) => !g.isIntegrated)) {
gpuInfo.push(formatDeviceName(gpu.deviceName)); gpuInfo.push(formatDeviceName(gpu.deviceName));
if (gpu.hasAMD) { if (gpu.hasAMD) {

View file

@ -130,7 +130,7 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [
label: 'CPU', label: 'CPU',
value: value:
hardwareInfo.cpu.devices.length > 0 hardwareInfo.cpu.devices.length > 0
? hardwareInfo.cpu.devices[0] ? hardwareInfo.cpu.devices[0].detailedName
: 'Unknown', : 'Unknown',
}, },
{ {
@ -147,12 +147,42 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [
}` }`
: 'Detecting...', : 'Detecting...',
}, },
...(hardwareInfo.gpu.gpuInfo.length > 0 ...(() => {
? hardwareInfo.gpu.gpuInfo.map((gpu, index) => ({ const discreteGPUs = [];
label: `GPU ${index > 0 ? index + 1 : ''}`.trim(),
value: gpu, if (hardwareInfo.gpuCapabilities.vulkan.devices.length > 0) {
})) discreteGPUs.push(
: [{ label: 'GPU', value: 'No GPU detected' }]), ...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 && hardwareInfo.gpuMemory.length > 0
? hardwareInfo.gpuMemory.map((mem, index) => ({ ? hardwareInfo.gpuMemory.map((mem, index) => ({
label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(), label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(),