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

This commit is contained in:
Egor 2025-08-23 12:19:11 -07:00
parent 0410e199f1
commit c1b7c7f73e
15 changed files with 272 additions and 68 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { checkBackendWarnings } from '@/utils/backendWarnings';
interface BackendSelectorProps {
onWarningsChange?: (
@ -89,41 +90,26 @@ export const BackendSelector = ({
useEffect(() => {
if (!onWarningsChange) return;
if (backend !== 'cpu' || !cpuCapabilities) {
onWarningsChange([]);
return;
}
const updateWarnings = async () => {
try {
const warnings = await checkBackendWarnings({
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
});
onWarningsChange(warnings);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
onWarningsChange([]);
}
};
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
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:
"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);
updateWarnings();
}, [
backend,
cpuCapabilities,

View file

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

View file

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

View file

@ -33,7 +33,9 @@ export class BinaryService {
this.hardwareService = hardwareService;
}
detectBackendSupport(koboldBinaryPath: string): BackendSupport {
private detectBackendSupportFromPath(
koboldBinaryPath: string
): BackendSupport {
if (this.backendSupportCache.has(koboldBinaryPath)) {
return this.backendSupportCache.get(koboldBinaryPath)!;
}
@ -87,6 +89,24 @@ export class BinaryService {
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<
Array<{ value: string; label: string; devices?: string[] }>
> {
@ -106,7 +126,12 @@ export class BinaryService {
return this.availableBackendsCache.get(cacheKey)!;
}
const backendSupport = this.detectBackendSupport(currentBinaryInfo.path);
const backendSupport = await this.detectBackendSupport();
if (!backendSupport) {
return [];
}
const backends: Array<{
value: string;
label: string;

View file

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

View file

@ -1,17 +1,25 @@
/* eslint-disable no-comments/disallowComments */
import si from 'systeminformation';
import { shortenDeviceName } from '@/utils';
import { LogManager } from '@/main/managers/LogManager';
import type {
CPUCapabilities,
GPUCapabilities,
BasicGPUInfo,
HardwareInfo,
GPUMemoryInfo,
} from '@/types/hardware';
export class HardwareService {
private cpuCapabilitiesCache: CPUCapabilities | null = null;
private basicGPUInfoCache: BasicGPUInfo | 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> {
if (this.cpuCapabilitiesCache) {
@ -37,8 +45,7 @@ export class HardwareService {
return this.cpuCapabilitiesCache;
} catch (error) {
// eslint-disable-next-line no-console
console.warn('CPU detection failed:', error);
this.logManager.logError('CPU detection failed:', error as Error);
const fallbackCapabilities = {
avx: false,
avx2: false,
@ -98,8 +105,7 @@ export class HardwareService {
return this.basicGPUInfoCache;
} catch (error) {
// eslint-disable-next-line no-console
console.warn('GPU detection failed:', error);
this.logManager.logError('GPU detection failed:', error as Error);
const fallbackGPUInfo = {
hasAMD: false,
hasNVIDIA: false,
@ -443,4 +449,37 @@ export class HardwareService {
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()
);
ipcMain.handle('kobold:detectGPUMemory', () =>
this.hardwareService.detectGPUMemory()
);
ipcMain.handle('kobold:detectROCm', () =>
this.hardwareService.detectROCm()
);
@ -120,8 +124,8 @@ export class IPCHandlers {
this.hardwareService.detectAllWithCapabilities()
);
ipcMain.handle('kobold:detectBackendSupport', (_, binaryPath: string) =>
this.binaryService.detectBackendSupport(binaryPath)
ipcMain.handle('kobold:detectBackendSupport', () =>
this.binaryService.detectBackendSupport()
);
ipcMain.handle('kobold:getAvailableBackends', () =>

View file

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

View file

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

View file

@ -4,6 +4,11 @@ export interface CPUCapabilities {
devices: string[];
}
export interface GPUMemoryInfo {
deviceName: string;
totalMemoryMB: number | null;
}
export interface GPUCapabilities {
cuda: {
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;
}
};