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 type { BackendOption } from '@/types';
import { GPUDevice } from '@/types/hardware';
type BackendSelectItemProps = Omit<BackendOption, 'value'>;
@ -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)}...`

View file

@ -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<typeof option> => option !== null
);
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
}
return [
{ value: 'all', label: 'All GPUs' },
...selectedBackend.devices.map((device, index) => {

View file

@ -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<Warning[]> => {
}) => {
const warnings: Warning[] = [];
const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([

View file

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

View file

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

View file

@ -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[];

View file

@ -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;
}

View file

@ -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) {

View file

@ -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(),