warnings for users that don't have cuda/rocm installed but use binaries that support them

This commit is contained in:
lone-cloud 2025-08-23 12:19:11 -07:00
parent dd3b1c2593
commit f681129aae
15 changed files with 272 additions and 68 deletions

View file

@ -1,3 +1,4 @@
alsa
AMDGPU AMDGPU
APPIMAGE APPIMAGE
asar asar
@ -29,6 +30,7 @@ KOBOLDAI
koboldcpp koboldcpp
KoboldCpp KoboldCpp
KOBOLDCPP KOBOLDCPP
libxss
lora lora
lowvram lowvram
Lowvram Lowvram
@ -50,8 +52,16 @@ nvidia
oldpc oldpc
OLDPC OLDPC
opencl opencl
optdepends
paru paru
pkexec pkexec
pkgbase
PKGBUILD
pkgdesc
pkgdir
pkgname
pkgrel
pkgver
quantmatmul quantmatmul
radeon radeon
remotetunnel remotetunnel
@ -70,6 +80,8 @@ SDXL
Segoe Segoe
sonarjs sonarjs
SPACEBAR SPACEBAR
squashfs
SRCINFO
taskkill taskkill
Tauri Tauri
togglefullscreen togglefullscreen

View file

@ -100,7 +100,7 @@ jobs:
# Maintainer: ${{ steps.release_info.outputs.author_name }} <${{ steps.release_info.outputs.author_email }}> # Maintainer: ${{ steps.release_info.outputs.author_name }} <${{ steps.release_info.outputs.author_email }}>
pkgname=friendly-kobold pkgname=friendly-kobold
pkgver=${{ steps.release_info.outputs.version }} pkgver=${{ steps.release_info.outputs.version }}
pkgrel=2 pkgrel=3
pkgdesc="A desktop app for running Large Language Models locally" pkgdesc="A desktop app for running Large Language Models locally"
arch=('x86_64') arch=('x86_64')
url="https://github.com/lone-cloud/friendly-kobold" url="https://github.com/lone-cloud/friendly-kobold"
@ -167,7 +167,7 @@ jobs:
pkgbase = friendly-kobold pkgbase = friendly-kobold
pkgdesc = A desktop app for running Large Language Models locally pkgdesc = A desktop app for running Large Language Models locally
pkgver = ${{ steps.release_info.outputs.version }} pkgver = ${{ steps.release_info.outputs.version }}
pkgrel = 2 pkgrel = 3
url = https://github.com/lone-cloud/friendly-kobold url = https://github.com/lone-cloud/friendly-kobold
arch = x86_64 arch = x86_64
license = AGPL-3.0-or-later license = AGPL-3.0-or-later

View file

@ -1,7 +1,7 @@
{ {
"name": "friendly-kobold", "name": "friendly-kobold",
"productName": "Friendly Kobold", "productName": "Friendly Kobold",
"version": "0.6.1", "version": "0.6.2",
"description": "A desktop app for running Large Language Models locally", "description": "A desktop app for running Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",

View file

@ -33,16 +33,14 @@ export const AdvancedTab = () => {
useEffect(() => { useEffect(() => {
const detectBackendSupport = async () => { const detectBackendSupport = async () => {
try { try {
const currentBinaryInfo = const support = await window.electronAPI.kobold.detectBackendSupport();
await window.electronAPI.kobold.getCurrentBinaryInfo(); if (support) {
if (currentBinaryInfo?.path) {
const support = await window.electronAPI.kobold.detectBackendSupport(
currentBinaryInfo.path
);
setBackendSupport({ setBackendSupport({
noavx2: support.noavx2, noavx2: support.noavx2,
failsafe: support.failsafe, failsafe: support.failsafe,
}); });
} else {
setBackendSupport({ noavx2: false, failsafe: false });
} }
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(

View file

@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem'; import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { checkBackendWarnings } from '@/utils/backendWarnings';
interface BackendSelectorProps { interface BackendSelectorProps {
onWarningsChange?: ( onWarningsChange?: (
@ -89,41 +90,26 @@ export const BackendSelector = ({
useEffect(() => { useEffect(() => {
if (!onWarningsChange) return; if (!onWarningsChange) return;
if (backend !== 'cpu' || !cpuCapabilities) { const updateWarnings = async () => {
onWarningsChange([]); try {
return; const warnings = await checkBackendWarnings({
} backend,
cpuCapabilities,
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = []; noavx2,
failsafe,
if (!cpuCapabilities.avx2 && !noavx2) { availableBackends,
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option on the Advanced tab to avoid crashes.',
}); });
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option on the Advanced tab to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"Performance Note: LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA or AMD's ROCm backends for optimal performance.",
});
}
onWarningsChange(warnings); onWarningsChange(warnings);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
onWarningsChange([]);
}
};
updateWarnings();
}, [ }, [
backend, backend,
cpuCapabilities, cpuCapabilities,

View file

@ -34,7 +34,7 @@ class FriendlyKoboldApp {
this.ensureInstallDirectory(); this.ensureInstallDirectory();
this.windowManager = new WindowManager(); this.windowManager = new WindowManager();
this.githubService = new GitHubService(this.logManager); this.githubService = new GitHubService(this.logManager);
this.hardwareService = new HardwareService(); this.hardwareService = new HardwareService(this.logManager);
this.koboldManager = new KoboldCppManager( this.koboldManager = new KoboldCppManager(
this.configManager, this.configManager,

View file

@ -142,8 +142,10 @@ export class KoboldCppManager {
try { try {
unlinkSync(tempPackedFilePath); unlinkSync(tempPackedFilePath);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console this.logManager.logError(
console.warn('Failed to cleanup packed file:', error); 'Failed to cleanup packed file:',
error as Error
);
} }
const launcherPath = this.getLauncherPath(unpackedDirPath); const launcherPath = this.getLauncherPath(unpackedDirPath);
@ -700,8 +702,10 @@ export class KoboldCppManager {
try { try {
unlinkSync(tempPackedFilePath); unlinkSync(tempPackedFilePath);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console this.logManager.logError(
console.warn('Failed to cleanup packed ROCm file:', error); 'Failed to cleanup packed ROCm file:',
error as Error
);
} }
const launcherPath = this.getLauncherPath(unpackedDirPath); const launcherPath = this.getLauncherPath(unpackedDirPath);

View file

@ -33,7 +33,9 @@ export class BinaryService {
this.hardwareService = hardwareService; this.hardwareService = hardwareService;
} }
detectBackendSupport(koboldBinaryPath: string): BackendSupport { private detectBackendSupportFromPath(
koboldBinaryPath: string
): BackendSupport {
if (this.backendSupportCache.has(koboldBinaryPath)) { if (this.backendSupportCache.has(koboldBinaryPath)) {
return this.backendSupportCache.get(koboldBinaryPath)!; return this.backendSupportCache.get(koboldBinaryPath)!;
} }
@ -87,6 +89,24 @@ export class BinaryService {
return support; return support;
} }
async detectBackendSupport(): Promise<BackendSupport | null> {
try {
const currentBinaryInfo = await this.koboldManager.getCurrentBinaryInfo();
if (!currentBinaryInfo?.path) {
return null;
}
return this.detectBackendSupportFromPath(currentBinaryInfo.path);
} catch (error) {
this.logManager.logError(
'Error detecting current binary backend support:',
error as Error
);
return null;
}
}
async getAvailableBackends(): Promise< async getAvailableBackends(): Promise<
Array<{ value: string; label: string; devices?: string[] }> Array<{ value: string; label: string; devices?: string[] }>
> { > {
@ -106,7 +126,12 @@ export class BinaryService {
return this.availableBackendsCache.get(cacheKey)!; return this.availableBackendsCache.get(cacheKey)!;
} }
const backendSupport = this.detectBackendSupport(currentBinaryInfo.path); const backendSupport = await this.detectBackendSupport();
if (!backendSupport) {
return [];
}
const backends: Array<{ const backends: Array<{
value: string; value: string;
label: string; label: string;

View file

@ -24,8 +24,7 @@ export class GitHubService {
if (!response.ok) { if (!response.ok) {
if (response.status === 403) { if (response.status === 403) {
// eslint-disable-next-line no-console this.logManager.logError(
console.warn(
'GitHub API rate limit reached, using cached data if available' 'GitHub API rate limit reached, using cached data if available'
); );
return this.cachedRelease return this.cachedRelease
@ -78,8 +77,7 @@ export class GitHubService {
if (!response.ok) { if (!response.ok) {
if (response.status === 403) { if (response.status === 403) {
// eslint-disable-next-line no-console this.logManager.logError(
console.warn(
'GitHub API rate limit reached, using cached data if available' 'GitHub API rate limit reached, using cached data if available'
); );
return this.cachedRelease; return this.cachedRelease;

View file

@ -1,17 +1,25 @@
/* eslint-disable no-comments/disallowComments */ /* eslint-disable no-comments/disallowComments */
import si from 'systeminformation'; import si from 'systeminformation';
import { shortenDeviceName } from '@/utils'; import { shortenDeviceName } from '@/utils';
import { LogManager } from '@/main/managers/LogManager';
import type { import type {
CPUCapabilities, CPUCapabilities,
GPUCapabilities, GPUCapabilities,
BasicGPUInfo, BasicGPUInfo,
HardwareInfo, HardwareInfo,
GPUMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
export class HardwareService { export class HardwareService {
private cpuCapabilitiesCache: CPUCapabilities | null = null; private cpuCapabilitiesCache: CPUCapabilities | null = null;
private basicGPUInfoCache: BasicGPUInfo | null = null; private basicGPUInfoCache: BasicGPUInfo | null = null;
private gpuCapabilitiesCache: GPUCapabilities | null = null; private gpuCapabilitiesCache: GPUCapabilities | null = null;
private gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
private logManager: LogManager;
constructor(logManager: LogManager) {
this.logManager = logManager;
}
async detectCPU(): Promise<CPUCapabilities> { async detectCPU(): Promise<CPUCapabilities> {
if (this.cpuCapabilitiesCache) { if (this.cpuCapabilitiesCache) {
@ -37,8 +45,7 @@ export class HardwareService {
return this.cpuCapabilitiesCache; return this.cpuCapabilitiesCache;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console this.logManager.logError('CPU detection failed:', error as Error);
console.warn('CPU detection failed:', error);
const fallbackCapabilities = { const fallbackCapabilities = {
avx: false, avx: false,
avx2: false, avx2: false,
@ -98,8 +105,7 @@ export class HardwareService {
return this.basicGPUInfoCache; return this.basicGPUInfoCache;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console this.logManager.logError('GPU detection failed:', error as Error);
console.warn('GPU detection failed:', error);
const fallbackGPUInfo = { const fallbackGPUInfo = {
hasAMD: false, hasAMD: false,
hasNVIDIA: false, hasNVIDIA: false,
@ -443,4 +449,37 @@ export class HardwareService {
return { supported: false, devices: [] }; return { supported: false, devices: [] };
} }
} }
async detectGPUMemory(): Promise<GPUMemoryInfo[]> {
if (this.gpuMemoryInfoCache) {
return this.gpuMemoryInfoCache;
}
const memoryInfo: GPUMemoryInfo[] = [];
try {
const graphics = await si.graphics();
for (const controller of graphics.controllers) {
if (controller.model) {
let vram = controller.vram;
// systeminformation returns 0 or 1 if unknown/invalid
if (!vram || vram === 1) {
vram = null;
}
memoryInfo.push({
deviceName: shortenDeviceName(controller.model),
totalMemoryMB: vram,
});
}
}
this.gpuMemoryInfoCache = memoryInfo;
} catch (error) {
this.logManager.logError('GPU memory detection failed:', error as Error);
this.gpuMemoryInfoCache = [];
}
return this.gpuMemoryInfoCache;
}
} }

View file

@ -112,6 +112,10 @@ export class IPCHandlers {
this.hardwareService.detectGPUCapabilities() this.hardwareService.detectGPUCapabilities()
); );
ipcMain.handle('kobold:detectGPUMemory', () =>
this.hardwareService.detectGPUMemory()
);
ipcMain.handle('kobold:detectROCm', () => ipcMain.handle('kobold:detectROCm', () =>
this.hardwareService.detectROCm() this.hardwareService.detectROCm()
); );
@ -120,8 +124,8 @@ export class IPCHandlers {
this.hardwareService.detectAllWithCapabilities() this.hardwareService.detectAllWithCapabilities()
); );
ipcMain.handle('kobold:detectBackendSupport', (_, binaryPath: string) => ipcMain.handle('kobold:detectBackendSupport', () =>
this.binaryService.detectBackendSupport(binaryPath) this.binaryService.detectBackendSupport()
); );
ipcMain.handle('kobold:getAvailableBackends', () => ipcMain.handle('kobold:getAvailableBackends', () =>

View file

@ -14,17 +14,19 @@ const koboldAPI: KoboldAPI = {
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'), getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'), detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
detectGPUCapabilities: () =>
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectBackendSupport: (binaryPath: string) => detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
ipcRenderer.invoke('kobold:detectBackendSupport', binaryPath),
getAvailableBackends: () => ipcRenderer.invoke('kobold:getAvailableBackends'), getAvailableBackends: () => ipcRenderer.invoke('kobold:getAvailableBackends'),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'), ipcRenderer.invoke('kobold:selectInstallDirectory'),
downloadRelease: (asset) => downloadRelease: (asset) =>
ipcRenderer.invoke('kobold:downloadRelease', asset), ipcRenderer.invoke('kobold:downloadRelease', asset),
launchKoboldCpp: (args?: string[], configFilePath?: string) => launchKoboldCpp: (args?: string[]) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args, configFilePath), ipcRenderer.invoke('kobold:launchKoboldCpp', args),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: ( saveConfigFile: (
configName: string, configName: string,

View file

@ -4,6 +4,7 @@ import type {
BasicGPUInfo, BasicGPUInfo,
HardwareInfo, HardwareInfo,
PlatformInfo, PlatformInfo,
GPUMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
interface GitHubAsset { interface GitHubAsset {
@ -67,15 +68,17 @@ export interface KoboldAPI {
getPlatform: () => Promise<PlatformInfo>; getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>; detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectBackendSupport: (binaryPath: string) => Promise<{ detectBackendSupport: () => Promise<{
rocm: boolean; rocm: boolean;
vulkan: boolean; vulkan: boolean;
clblast: boolean; clblast: boolean;
noavx2: boolean; noavx2: boolean;
failsafe: boolean; failsafe: boolean;
cuda: boolean; cuda: boolean;
}>; } | null>;
getAvailableBackends: () => Promise< getAvailableBackends: () => Promise<
Array<{ value: string; label: string; devices?: string[] }> Array<{ value: string; label: string; devices?: string[] }>
>; >;
@ -92,8 +95,7 @@ export interface KoboldAPI {
getROCmDownload: () => Promise<DownloadItem | null>; getROCmDownload: () => Promise<DownloadItem | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>; getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: ( launchKoboldCpp: (
args?: string[], args?: string[]
configFilePath?: string
) => Promise<{ success: boolean; pid?: number; error?: string }>; ) => Promise<{ success: boolean; pid?: number; error?: string }>;
getConfigFiles: () => Promise< getConfigFiles: () => Promise<
Array<{ name: string; path: string; size: number }> Array<{ name: string; path: string; size: number }>

View file

@ -4,6 +4,11 @@ export interface CPUCapabilities {
devices: string[]; devices: string[];
} }
export interface GPUMemoryInfo {
deviceName: string;
totalMemoryMB: number | null;
}
export interface GPUCapabilities { export interface GPUCapabilities {
cuda: { cuda: {
supported: boolean; supported: boolean;

View file

@ -0,0 +1,129 @@
export interface BackendWarning {
type: 'warning' | 'info';
message: string;
}
interface WarningParams {
backend: string;
cpuCapabilities: {
avx: boolean;
avx2: boolean;
} | null;
noavx2: boolean;
failsafe: boolean;
availableBackends: Array<{
value: string;
label: string;
devices?: string[];
}>;
}
export const checkBackendWarnings = async (
params?: WarningParams
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<BackendWarning[]> => {
const warnings: BackendWarning[] = [];
try {
const [backendSupport, gpuCapabilities] = await Promise.all([
window.electronAPI.kobold.detectBackendSupport(),
window.electronAPI.kobold.detectGPUCapabilities(),
]);
if (!backendSupport) {
return warnings;
}
if (backendSupport.cuda && !gpuCapabilities.cuda.supported) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports CUDA, but CUDA runtime is not detected on your system.',
});
}
if (backendSupport.rocm && !gpuCapabilities.rocm.supported) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports ROCm, but ROCm runtime is not detected on your system.',
});
}
if (params) {
const { backend, cpuCapabilities, noavx2, failsafe, availableBackends } =
params;
const isGpuBackend = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(
backend
);
if (isGpuBackend) {
try {
const gpuMemoryInfo =
await window.electronAPI.kobold.detectGPUMemory();
const lowVramGpus = gpuMemoryInfo.filter(
(gpu) =>
typeof gpu.totalMemoryMB === 'number' && gpu.totalMemoryMB < 8192
);
if (lowVramGpus.length > 0) {
warnings.push({
type: 'warning',
message: `Low VRAM detected (${lowVramGpus
.map(
(gpu) =>
`${gpu.deviceName}: ${(gpu.totalMemoryMB! / 1024).toFixed(1)}GB`
)
.join(
', '
)}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
});
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to detect GPU memory:',
error as Error
);
}
}
if (backend === 'cpu' && cpuCapabilities) {
if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option on the Advanced tab to avoid crashes.',
});
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option on the Advanced tab to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
});
}
}
}
return warnings;
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
return warnings;
}
};