allow users to launch with support for multiple GPUs, image gen UI bug fixes, adding the new 1.98.0 sdflashattention and sdconvdirect image gen options, remove cspell

This commit is contained in:
Egor 2025-08-25 17:16:13 -07:00
parent 8c6f3a308d
commit 2ef23888df
26 changed files with 509 additions and 1391 deletions

View file

@ -1,102 +0,0 @@
alsa
AMDGPU
ansi
ANSI
APPIMAGE
stripansi
asar
Autoencoder
BLAS
clblast
clinfo
Consolas
contextsize
Cooldown
cublas
cuda
CUDA
Dolfino
finetuned
flashattention
Flashattention
friendlykobold
geforce
ggml
gguf
GGUF
gpulayers
hipblas
kcpps
kcppt
koboldai
KOBOLDAI
koboldcpp
KoboldCpp
KOBOLDCPP
libxss
lora
lowvram
Lowvram
makepkg
maximizable
minimizable
MMAP
mmproj
mmq
multiuser
noavx
nocertify
nocuda
noheader
nommq
noshift
nsis
nvidia
oldpc
OLDPC
opencl
optdepends
paru
pkexec
pkgbase
PKGBUILD
pkgdesc
pkgdir
pkgname
pkgrel
pkgver
quantmatmul
radeon
remotetunnel
rocm
ROCM
rocminfo
safetensors
sdclipg
sdclipl
sdlora
sdmodel
sdphotomaker
sdui
sdvae
SDXL
Segoe
sonarjs
SPACEBAR
squashfs
SRCINFO
taskkill
Tauri
togglefullscreen
treemap
trycloudflare
unquantized
useclblast
usecuda
usemmap
usevulkan
vram
vulkan
vulkaninfo
wayland
websearch

View file

@ -1,7 +1,3 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker"
]
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

14
.vscode/tasks.json vendored
View file

@ -46,19 +46,7 @@
}
},
{
"label": "Spell Check",
"type": "shell",
"command": "yarn spell-check",
"group": "test",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "Check All (Lint + Type + Spell)",
"label": "Check All (Lint + Type)",
"type": "shell",
"command": "yarn check-all",
"group": "test",

View file

@ -22,8 +22,8 @@ A desktop app for running Large Language Models locally. <!-- markdownlint-disab
Download the latest release for your platform from the [GitHub Releases page](https://github.com/lone-cloud/friendly-kobold/releases/latest):
- **Windows**: `Friendly.Kobold.Portable X.X.X.exe` (portable executable)
- **Windows**: `Friendly.Kobold.Setup X.X.X.exe` (installer executable)
- **Windows**: `Friendly.Kobold-Portable-X.X.X.exe` (portable executable)
- **Windows**: `Friendly.Kobold-Setup-X.X.X.exe` (installer executable)
- **macOS**: `Friendly.Kobold-X.X.X.dmg` (disk image)
- **Linux**: `Friendly.Kobold-X.X.X.AppImage` (portable application)

View file

@ -1,66 +0,0 @@
{
"version": "0.2",
"language": "en",
"dictionaryDefinitions": [
{
"name": "project-terms",
"path": "./.cspell/project-terms.txt",
"addWords": true
}
],
"dictionaries": [
"typescript",
"node",
"npm",
"html",
"css",
"bash",
"en-gb",
"companies",
"softwareTerms",
"misc",
"project-terms"
],
"ignorePaths": [
"node_modules/**",
"dist/**",
"dist-electron/**",
"out/**",
"release/**",
"build/**",
"*.min.js",
"*.min.css",
"package-lock.json",
"**/*.log",
"coverage/**",
".git/**",
".vscode/**",
"*.map",
".cspell/**"
],
"languageSettings": [
{
"languageId": "typescript,javascript",
"caseSensitive": true,
"dictionaries": ["typescript", "node", "npm", "softwareTerms"]
},
{
"languageId": "css,scss,less",
"dictionaries": ["css", "fonts"]
},
{
"languageId": "html",
"dictionaries": ["html", "css", "typescript"]
}
],
"overrides": [
{
"filename": "**/*.md",
"dictionaries": ["en-gb", "typescript", "node", "npm", "companies"]
},
{
"filename": "**/README.md",
"words": ["github", "screenshot", "screenshots", "workflow", "workflows"]
}
]
}

View file

@ -7,7 +7,6 @@ import reactRefresh from 'eslint-plugin-react-refresh';
import react from 'eslint-plugin-react';
import importPlugin from 'eslint-plugin-import';
import sonarjs from 'eslint-plugin-sonarjs';
import cspell from '@cspell/eslint-plugin';
import noComments from 'eslint-plugin-no-comments';
const config = [
@ -47,7 +46,6 @@ const config = [
react: react,
import: importPlugin,
sonarjs: sonarjs,
'@cspell': cspell,
'no-comments': noComments,
},
settings: {
@ -116,8 +114,6 @@ const config = [
'sonarjs/cognitive-complexity': ['warn', 25],
'@cspell/spellchecker': ['warn'],
'no-comments/disallowComments': 'error',
},
},

View file

@ -1,7 +1,7 @@
{
"name": "friendly-kobold",
"productName": "Friendly Kobold",
"version": "0.7.0",
"version": "0.8.0",
"description": "A desktop app for running Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@ -23,20 +23,16 @@
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"compile": "tsc --noEmit",
"spell-check": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress",
"spell-check:fix": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress --show-suggestions",
"check-all": "yarn lint && yarn compile && yarn spell-check",
"check-all": "yarn lint && yarn compile",
"release": "yarn dlx tsx scripts/release.ts",
"prepare": "husky"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress",
"prettier --write --ignore-path .gitignore"
],
"*.{json,css,md}": [
"cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress",
"prettier --write --ignore-path .gitignore"
]
},
@ -56,17 +52,15 @@
},
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@cspell/eslint-plugin": "^9.2.0",
"@eslint/js": "^9.34.0",
"@types/node": "^24.3.0",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@types/strip-ansi": "^5.2.1",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.41.0",
"@vitejs/plugin-react": "^5.0.1",
"cross-env": "^10.0.0",
"cspell": "^9.2.0",
"electron": "^37.3.1",
"electron-builder": "^26.0.12",
"electron-vite": "^4.0.0",
@ -76,7 +70,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-sonarjs": "^3.0.4",
"eslint-plugin-sonarjs": "^3.0.5",
"globals": "^16.3.0",
"husky": "^9.1.7",
"jiti": "^2.5.1",
@ -95,7 +89,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"strip-ansi": "^7.1.0",
"systeminformation": "^5.27.7",
"systeminformation": "^5.27.8",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"zustand": "^5.0.8"
@ -168,7 +162,7 @@
]
},
"nsis": {
"artifactName": "${productName} Setup ${version}.${ext}",
"artifactName": "${productName}-Setup-${version}.${ext}",
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
@ -176,11 +170,11 @@
"shortcutName": "Friendly Kobold"
},
"portable": {
"artifactName": "${productName} Portable ${version}.${ext}"
"artifactName": "${productName}-Portable-${version}.${ext}"
},
"linux": {
"compression": "store",
"category": "Development",
"category": "Utility",
"desktop": {
"entry": {
"Name": "Friendly Kobold",

View file

@ -13,6 +13,7 @@ import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { UI } from '@/constants';
import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab } from '@/types';
type Screen = 'welcome' | 'download' | 'launch' | 'interface';
@ -21,11 +22,9 @@ export const App = () => {
const [settingsOpened, setSettingsOpened] = useState(false);
const [hasInitialized, setHasInitialized] = useState(false);
const [showEjectModal, setShowEjectModal] = useState(false);
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
'terminal'
);
const [isImageGenerationMode, setIsImageGenerationMode] =
useState<boolean>(false);
const [activeInterfaceTab, setActiveInterfaceTab] =
useState<InterfaceTab>('terminal');
const [isImageGenerationMode, setIsImageGenerationMode] = useState(false);
const {
updateInfo: binaryUpdateInfo,

View file

@ -12,14 +12,15 @@ import {
} from '@mantine/core';
import { Settings, ArrowLeft } from 'lucide-react';
import { soundAssets, playSound, initializeAudio } from '@/utils';
import type { InterfaceTab } from '@/types';
import iconUrl from '/icon.png';
type Screen = 'welcome' | 'download' | 'launch' | 'interface';
interface AppHeaderProps {
currentScreen: Screen | null;
activeInterfaceTab: string | null;
setActiveInterfaceTab: (tab: string | null) => void;
activeInterfaceTab: InterfaceTab;
setActiveInterfaceTab: (tab: InterfaceTab) => void;
isImageGenerationMode: boolean;
onEject: () => void;
onSettingsOpen: () => void;
@ -109,8 +110,10 @@ export const AppHeader = ({
{currentScreen === 'interface' && (
<Select
value={activeInterfaceTab || 'terminal'}
onChange={(value) => setActiveInterfaceTab(value || 'terminal')}
value={activeInterfaceTab}
onChange={(value) =>
setActiveInterfaceTab((value || 'terminal') as InterfaceTab)
}
data={[
{
value: 'chat',

View file

@ -0,0 +1,23 @@
import { Group, Text } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
interface LabelWithTooltipProps {
label: string;
tooltip?: string;
fontWeight?: number;
marginBottom?: string;
}
export const LabelWithTooltip = ({
label,
tooltip,
fontWeight = 500,
marginBottom = 'xs',
}: LabelWithTooltipProps) => (
<Group gap="xs" align="center" mb={marginBottom}>
<Text size="sm" fw={fontWeight}>
{label}
</Text>
{tooltip && <InfoTooltip label={tooltip} />}
</Group>
);

View file

@ -1,6 +1,6 @@
import { Group, TextInput, Button } from '@mantine/core';
import { File, Search } from 'lucide-react';
import { SectionHeader } from '@/components/SectionHeader';
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import { getInputValidationState } from '@/utils';
import styles from '@/styles/layout.module.css';
@ -39,12 +39,7 @@ export const ModelFileField = ({
return (
<div>
<SectionHeader
title={label}
tooltip={tooltip}
fontWeight={500}
marginBottom="xs"
/>
<LabelWithTooltip label={label} tooltip={tooltip} />
<Group gap="xs" align="flex-start">
<div className={styles.flex1}>
<TextInput

View file

@ -0,0 +1,40 @@
import type { CSSProperties } from 'react';
import { Select } from '@mantine/core';
import type { ComboboxItem } from '@mantine/core';
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
interface SelectWithTooltipProps {
label: string;
tooltip: string;
value: string;
onChange: (value: string | null, option: ComboboxItem) => void;
data: { value: string; label: string }[];
placeholder?: string;
disabled?: boolean;
clearable?: boolean;
style?: CSSProperties;
}
export const SelectWithTooltip = ({
label,
tooltip,
value,
onChange,
data,
placeholder,
disabled = false,
clearable = false,
style,
}: SelectWithTooltipProps) => (
<div style={style}>
<LabelWithTooltip label={label} tooltip={tooltip} />
<Select
placeholder={placeholder}
value={value}
onChange={onChange}
data={data}
disabled={disabled}
clearable={clearable}
/>
</div>
);

View file

@ -1,11 +1,12 @@
import { useRef } from 'react';
import { Box, Text, Stack } from '@mantine/core';
import { UI } from '@/constants';
import type { ServerTabMode } from '@/types';
interface ServerTabProps {
serverUrl?: string;
isServerReady?: boolean;
mode: 'chat' | 'image-generation';
mode: ServerTabMode;
}
export const ServerTab = ({

View file

@ -1,10 +1,11 @@
import { useState, useCallback } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab';
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
import type { InterfaceTab } from '@/types';
interface InterfaceScreenProps {
activeTab?: string | null;
onTabChange?: (tab: string | null) => void;
activeTab?: InterfaceTab | null;
onTabChange?: (tab: InterfaceTab) => void;
isImageGenerationMode?: boolean;
}
@ -21,10 +22,10 @@ export const InterfaceScreen = ({
setServerUrl(url);
setIsServerReady(true);
if (onTabChange) {
onTabChange(isImageGenerationMode ? 'image' : 'chat');
onTabChange('chat');
}
},
[onTabChange, isImageGenerationMode]
[onTabChange]
);
return (
@ -48,18 +49,6 @@ export const InterfaceScreen = ({
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
/>
</div>
<div
style={{
flex: 1,
display: activeTab === 'image' ? 'block' : 'none',
}}
>
<ServerTab
serverUrl={serverUrl}
isServerReady={isServerReady}
mode="image-generation"
/>
</div>
<div
style={{
flex: 1,

View file

@ -2,6 +2,7 @@ import { Text, Group, Select, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
interface BackendSelectorProps {
@ -11,11 +12,9 @@ interface BackendSelectorProps {
export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
const {
backend,
gpuDevice,
gpuLayers,
autoGpuLayers,
handleBackendChange,
handleGpuDeviceChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
} = useLaunchConfig();
@ -72,7 +71,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
return (
<div>
<Group justify="space-between" align="flex-start" mb="xs">
<div style={{ flex: 1, marginRight: '2rem' }}>
<div style={{ flex: 1, marginRight: '1rem' }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
Backend
@ -104,33 +103,6 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
);
}}
/>
{(() => {
const selectedBackend = availableBackends.find(
(b) => b.value === backend
);
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
const hasMultipleDevices =
selectedBackend?.devices && selectedBackend.devices.length > 1;
return (
isGpuBackend &&
hasMultipleDevices && (
<Select
label="GPU Device"
placeholder="Select GPU device"
value={gpuDevice.toString()}
onChange={(value) =>
value && handleGpuDeviceChange(parseInt(value, 10))
}
data={selectedBackend.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
}))}
mt="xs"
/>
)
);
})()}
</div>
<div style={{ flex: 1 }}>
@ -169,6 +141,8 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
</Group>
</div>
</Group>
<GpuDeviceSelector availableBackends={availableBackends} />
</div>
);
};

View file

@ -0,0 +1,100 @@
import { Text, Group, Select, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
interface GpuDeviceSelectorProps {
availableBackends: Array<{
value: string;
label: string;
devices?: string[];
}>;
}
export const GpuDeviceSelector = ({
availableBackends,
}: GpuDeviceSelectorProps) => {
const {
backend,
gpuDeviceSelection,
tensorSplit,
handleGpuDeviceSelectionChange,
handleTensorSplitChange,
} = useLaunchConfig();
const selectedBackend = availableBackends.find((b) => b.value === backend);
const isGpuBackend =
backend === 'cuda' ||
backend === 'rocm' ||
backend === 'vulkan' ||
backend === 'clblast';
const hasMultipleDevices =
selectedBackend?.devices && selectedBackend.devices.length > 1;
const showTensorSplit =
(backend === 'cuda' || backend === 'rocm' || backend === 'vulkan') &&
hasMultipleDevices &&
gpuDeviceSelection === 'all';
if (!isGpuBackend || !hasMultipleDevices) {
return null;
}
const deviceOptions =
backend === 'clblast'
? selectedBackend.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
}))
: [
{ value: 'all', label: 'All GPUs' },
...selectedBackend.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
})),
];
return (
<div>
<Group align="flex-start" gap="md">
<div style={{ flex: 1, marginRight: '1rem' }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
GPU Device
</Text>
<InfoTooltip label="Select which GPU device(s) to use. Choose 'All GPUs' to use multiple devices with tensor splitting." />
</Group>
<Select
placeholder="Select GPU device"
value={gpuDeviceSelection}
onChange={(value) => {
if (value) {
handleGpuDeviceSelectionChange(value);
}
}}
data={deviceOptions}
/>
</div>
<div style={{ flex: 1 }}>
{showTensorSplit && (
<>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
Tensor Split
</Text>
<InfoTooltip label='When using multiple GPUs this option controls how large tensors should be split across all GPUs. Uses a comma-separated list of non-negative values that assigns the proportion of data that each GPU should get in order. For example, "3,2" will assign 60% of the data to GPU 0 and 40% to GPU 1.' />
</Group>
<TextInput
placeholder="e.g., 3,2 or 1,1,1"
value={tensorSplit}
onChange={(event) =>
handleTensorSplitChange(event.target.value)
}
size="sm"
/>
</>
)}
</div>
</Group>
</div>
);
};

View file

@ -1,10 +1,8 @@
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
import { File, Search } from 'lucide-react';
import { Stack, Text, Group, TextInput, Slider } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
import { getInputValidationState } from '@/utils';
import { ModelFileField } from '@/components/ModelFileField';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps {
onBackendsReady?: () => void;
@ -19,57 +17,20 @@ export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => {
handleContextSizeChangeWithStep,
} = useLaunchConfig();
const validationState = getInputValidationState(modelPath);
const getHelperText = () => {
if (!modelPath.trim()) return undefined;
if (validationState === 'invalid') {
return 'Enter a valid URL or file path to the .gguf';
}
return undefined;
};
return (
<Stack gap="md">
<BackendSelector onBackendsReady={onBackendsReady} />
<div>
<Text size="sm" fw={500} mb="xs">
Text Model File
</Text>
<Group gap="xs" align="flex-start">
<div className={styles.flex1}>
<TextInput
placeholder="Select a .gguf model file or enter a direct URL to file"
value={modelPath}
onChange={(e) => handleModelPathChange(e.target.value)}
error={
validationState === 'invalid' ? getHelperText() : undefined
}
/>
</div>
<Button
onClick={handleSelectModelFile}
variant="light"
leftSection={<File size={16} />}
>
Browse
</Button>
<Button
onClick={() => {
window.electronAPI.app.openExternal(
'https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending'
);
}}
variant="outline"
leftSection={<Search size={16} />}
>
Search HF
</Button>
</Group>
</div>
<ModelFileField
label="Text Model File"
value={modelPath}
placeholder="Select a .gguf model file or enter a direct URL to file"
tooltip="Select a GGUF text generation model file for chat and completion tasks."
onChange={handleModelPathChange}
onSelectFile={handleSelectModelFile}
showSearchHF
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
/>
<div>
<Group justify="space-between" align="center" mb="xs">

View file

@ -1,7 +1,7 @@
import { Stack, Select } from '@mantine/core';
import { Stack } from '@mantine/core';
import { useState } from 'react';
import { SectionHeader } from '@/components/SectionHeader';
import { ModelFileField } from '@/components/ModelFileField';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { IMAGE_MODEL_PRESETS } from '@/utils';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
@ -14,50 +14,54 @@ export const ImageGenerationTab = () => {
sdphotomaker,
sdvae,
sdlora,
sdconvdirect,
handleSdmodelChange,
handleSelectSdmodelFile,
handleSdt5xxlChange,
handleSelectSdt5xxlFile,
handleSdcliplChange,
handleSelectSdcliplFile,
handleSdclipgChange,
handleSelectSdclipgFile,
handleSdphotomakerChange,
handleSelectSdphotomakerFile,
handleSdvaeChange,
handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
handleSdconvdirectChange,
handleApplyPreset,
handleSelectSdmodelFile,
handleSelectSdt5xxlFile,
handleSelectSdcliplFile,
handleSelectSdclipgFile,
handleSelectSdphotomakerFile,
handleSelectSdvaeFile,
handleSelectSdloraFile,
} = useLaunchConfig();
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
return (
<Stack gap="md">
<div>
<SectionHeader
title="Model Preset"
tooltip="Quick presets for popular image generation models with pre-configured encoders."
fontWeight={500}
marginBottom="xs"
/>
<Select
placeholder="Choose a preset..."
data={IMAGE_MODEL_PRESETS.map((preset) => ({
value: preset.name,
label: preset.name,
}))}
value={selectedPreset}
onChange={(value) => {
setSelectedPreset(value);
if (value) {
handleApplyPreset(value);
}
}}
clearable
/>
</div>
<SelectWithTooltip
label="Model Preset"
tooltip="Quick presets for popular image generation models with pre-configured encoders."
placeholder="Choose a preset..."
data={IMAGE_MODEL_PRESETS.map((preset) => ({
value: preset.name,
label: preset.name,
}))}
value={selectedPreset ?? ''}
onChange={(value) => {
setSelectedPreset(value);
if (value) {
handleApplyPreset(value);
} else {
handleSdmodelChange('');
handleSdt5xxlChange('');
handleSdcliplChange('');
handleSdclipgChange('');
handleSdphotomakerChange('');
handleSdvaeChange('');
handleSdloraChange('');
}
}}
clearable
/>
<ModelFileField
label="Image Gen. Model File"
@ -123,6 +127,22 @@ export const ImageGenerationTab = () => {
onChange={handleSdloraChange}
onSelectFile={handleSelectSdloraFile}
/>
<SelectWithTooltip
label="Conv2D Direct"
tooltip="May improve performance or reduce memory usage. WARNING: Might crash if not supported by your backend! Only enable if you're sure your GPU and drivers support it."
value={sdconvdirect}
onChange={(value) => {
if (value === 'off' || value === 'vaeonly' || value === 'full') {
handleSdconvdirectChange(value);
}
}}
data={[
{ value: 'off', label: 'Off' },
{ value: 'vaeonly', label: 'VAE Only' },
{ value: 'full', label: 'Full' },
]}
/>
</Stack>
);
};

View file

@ -49,7 +49,8 @@ export const LaunchScreen = ({
quantmatmul,
usemmap,
backend,
gpuDevice,
gpuDeviceSelection,
tensorSplit,
gpuPlatform,
sdmodel,
sdt5xxl,
@ -58,6 +59,7 @@ export const LaunchScreen = ({
sdphotomaker,
sdvae,
sdlora,
sdconvdirect,
parseAndApplyConfigFile,
loadConfigFromFile,
handleModelPathChange,
@ -180,10 +182,8 @@ export const LaunchScreen = ({
usecuda: backend === 'cuda' || backend === 'rocm',
usevulkan: backend === 'vulkan',
useclblast: backend === 'clblast',
clBlastInfo:
backend === 'clblast'
? ([gpuDevice, gpuPlatform] as [number, number])
: undefined,
gpuDeviceSelection,
tensorSplit,
sdmodel,
sdt5xxl,
sdclipl,
@ -285,7 +285,9 @@ export const LaunchScreen = ({
failsafe,
backend,
lowvram,
gpuDevice,
gpuDeviceSelection,
gpuPlatform,
tensorSplit,
quantmatmul,
usemmap,
additionalArguments,
@ -295,6 +297,7 @@ export const LaunchScreen = ({
sdphotomaker,
sdvae,
sdlora,
sdconvdirect,
});
};

View file

@ -28,7 +28,8 @@ export const useLaunchConfig = () => {
quantmatmul: state.quantmatmul,
usemmap: state.usemmap,
backend: state.backend,
gpuDevice: state.gpuDevice,
gpuDeviceSelection: state.gpuDeviceSelection,
tensorSplit: state.tensorSplit,
gpuPlatform: state.gpuPlatform,
sdmodel: state.sdmodel,
sdt5xxl: state.sdt5xxl,
@ -37,6 +38,7 @@ export const useLaunchConfig = () => {
sdphotomaker: state.sdphotomaker,
sdvae: state.sdvae,
sdlora: state.sdlora,
sdconvdirect: state.sdconvdirect,
handleGpuLayersChange: state.setGpuLayers,
handleAutoGpuLayersChange: state.setAutoGpuLayers,
@ -58,7 +60,8 @@ export const useLaunchConfig = () => {
handleQuantmatmulChange: state.setQuantmatmul,
handleUsemmapChange: state.setUsemmap,
handleBackendChange: state.setBackend,
handleGpuDeviceChange: state.setGpuDevice,
handleGpuDeviceSelectionChange: state.setGpuDeviceSelection,
handleTensorSplitChange: state.setTensorSplit,
handleGpuPlatformChange: state.setGpuPlatform,
handleSdmodelChange: state.setSdmodel,
handleSdt5xxlChange: state.setSdt5xxl,
@ -67,6 +70,7 @@ export const useLaunchConfig = () => {
handleSdphotomakerChange: state.setSdphotomaker,
handleSdvaeChange: state.setSdvae,
handleSdloraChange: state.setSdlora,
handleSdconvdirectChange: state.setSdconvdirect,
parseAndApplyConfigFile: state.parseAndApplyConfigFile,
loadConfigFromFile: state.loadConfigFromFile,

View file

@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { parseCLBlastDevice } from '@/utils';
import type { SdConvDirectMode } from '@/types';
interface UseLaunchLogicProps {
modelPath: string;
@ -25,7 +26,9 @@ interface LaunchArgs {
failsafe: boolean;
backend: string;
lowvram: boolean;
gpuDevice: number | string;
gpuDeviceSelection: string;
gpuPlatform: number;
tensorSplit: string;
quantmatmul: boolean;
usemmap: boolean;
additionalArguments: string;
@ -35,20 +38,18 @@ interface LaunchArgs {
sdphotomaker: string;
sdvae: string;
sdlora: string;
sdconvdirect: SdConvDirectMode;
}
const buildModelArgs = (
isImageMode: boolean,
isTextMode: boolean,
modelPath: string,
sdmodel: string,
launchArgs: LaunchArgs
): string[] => {
const args: string[] = [];
if (isImageMode && isTextMode) {
args.push('--sdmodel', sdmodel);
} else if (isImageMode) {
if (isImageMode) {
args.push('--sdmodel', sdmodel);
const imageModels = [
@ -65,6 +66,14 @@ const buildModelArgs = (
args.push(flag, value);
}
});
if (launchArgs.flashattention) {
args.push('--sdflashattention');
}
if (launchArgs.sdconvdirect !== 'off') {
args.push('--sdconvdirect', launchArgs.sdconvdirect);
}
} else {
args.push('--model', modelPath);
}
@ -72,7 +81,10 @@ const buildModelArgs = (
return args;
};
const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
const buildConfigArgs = (
isImageMode: boolean,
launchArgs: LaunchArgs
): string[] => {
const args: string[] = [];
if (launchArgs.autoGpuLayers) {
@ -100,7 +112,7 @@ const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
[launchArgs.nocertify, '--nocertify'],
[launchArgs.websearch, '--websearch'],
[launchArgs.noshift, '--noshift'],
[launchArgs.flashattention, '--flashattention'],
[!isImageMode && launchArgs.flashattention, '--flashattention'],
[launchArgs.noavx2, '--noavx2'],
[launchArgs.failsafe, '--failsafe'],
[launchArgs.usemmap, '--usemmap'],
@ -116,41 +128,87 @@ const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
return args;
};
const buildCudaArgs = (launchArgs: LaunchArgs): string[] => {
const cudaArgs = ['--usecuda'];
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal');
if (launchArgs.gpuDeviceSelection === 'all') {
cudaArgs.push('0');
} else {
cudaArgs.push(launchArgs.gpuDeviceSelection || '0');
}
cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
return cudaArgs;
};
const buildVulkanArgs = (): string[] => ['--usevulkan'];
const buildClblastArgs = (launchArgs: LaunchArgs): string[] => {
const clblastArgs = ['--useclblast'];
if (
typeof launchArgs.gpuDeviceSelection === 'string' &&
launchArgs.gpuDeviceSelection.includes(':')
) {
const parsed = parseCLBlastDevice(launchArgs.gpuDeviceSelection);
if (parsed) {
clblastArgs.push(
parsed.platformIndex.toString(),
parsed.deviceIndex.toString()
);
} else {
clblastArgs.push(launchArgs.gpuPlatform.toString(), '0');
}
} else {
clblastArgs.push(
launchArgs.gpuPlatform.toString(),
launchArgs.gpuDeviceSelection || '0'
);
}
return clblastArgs;
};
const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs): void => {
if (launchArgs.tensorSplit && launchArgs.tensorSplit.trim()) {
const tensorValues = launchArgs.tensorSplit
.split(',')
.map((value) => value.trim())
.filter((value) => value !== '' && !isNaN(Number(value)));
if (tensorValues.length > 0) {
args.push('--tensorsplit', ...tensorValues);
}
}
};
const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
const args: string[] = [];
if (launchArgs.backend && launchArgs.backend !== 'cpu') {
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
const cudaArgs = ['--usecuda'];
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal');
cudaArgs.push(
typeof launchArgs.gpuDevice === 'string'
? '0'
: launchArgs.gpuDevice.toString()
);
cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs);
} else if (launchArgs.backend === 'vulkan') {
args.push('--usevulkan');
} else if (launchArgs.backend === 'clblast') {
const clblastArgs = ['--useclblast'];
if (!launchArgs.backend || launchArgs.backend === 'cpu') {
return args;
}
if (typeof launchArgs.gpuDevice === 'string') {
const parsed = parseCLBlastDevice(launchArgs.gpuDevice);
if (parsed) {
clblastArgs.push(
parsed.deviceIndex.toString(),
parsed.platformIndex.toString()
);
} else {
clblastArgs.push('0', '0');
}
} else {
clblastArgs.push(launchArgs.gpuDevice.toString(), '0');
}
const isTensorSplitSupported =
launchArgs.backend === 'cuda' ||
launchArgs.backend === 'rocm' ||
launchArgs.backend === 'vulkan';
args.push(...clblastArgs);
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
args.push(...buildCudaArgs(launchArgs));
if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) {
addTensorSplitArgs(args, launchArgs);
}
} else if (launchArgs.backend === 'vulkan') {
args.push(...buildVulkanArgs());
if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) {
addTensorSplitArgs(args, launchArgs);
}
} else if (launchArgs.backend === 'clblast') {
args.push(...buildClblastArgs(launchArgs));
}
return args;
@ -177,14 +235,8 @@ export const useLaunchLogic = ({
try {
const args: string[] = [
...buildModelArgs(
isImageMode,
isTextMode,
modelPath,
sdmodel,
launchArgs
),
...buildConfigArgs(launchArgs),
...buildModelArgs(isImageMode, modelPath, sdmodel, launchArgs),
...buildConfigArgs(isImageMode, launchArgs),
...buildBackendArgs(launchArgs),
];

View file

@ -1,5 +1,5 @@
import { create } from 'zustand';
import type { ConfigFile } from '@/types';
import type { ConfigFile, SdConvDirectMode } from '@/types';
import type { ImageModelPreset } from '@/utils/imageModelPresets';
import { DEFAULT_CONTEXT_SIZE } from '@/constants';
@ -24,7 +24,8 @@ interface LaunchConfigState {
quantmatmul: boolean;
usemmap: boolean;
backend: string;
gpuDevice: number;
gpuDeviceSelection: string;
tensorSplit: string;
gpuPlatform: number;
sdmodel: string;
sdt5xxl: string;
@ -33,6 +34,7 @@ interface LaunchConfigState {
sdphotomaker: string;
sdvae: string;
sdlora: string;
sdconvdirect: SdConvDirectMode;
setGpuLayers: (layers: number) => void;
setAutoGpuLayers: (auto: boolean) => void;
@ -54,7 +56,8 @@ interface LaunchConfigState {
setQuantmatmul: (quantmatmul: boolean) => void;
setUsemmap: (usemmap: boolean) => void;
setBackend: (backend: string) => void;
setGpuDevice: (device: number) => void;
setGpuDeviceSelection: (selection: string) => void;
setTensorSplit: (split: string) => void;
setGpuPlatform: (platform: number) => void;
setSdmodel: (model: string) => void;
setSdt5xxl: (model: string) => void;
@ -63,6 +66,7 @@ interface LaunchConfigState {
setSdphotomaker: (model: string) => void;
setSdvae: (vae: string) => void;
setSdlora: (loraModel: string) => void;
setSdconvdirect: (mode: SdConvDirectMode) => void;
parseAndApplyConfigFile: (configPath: string) => Promise<void>;
loadConfigFromFile: (
@ -102,7 +106,8 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
quantmatmul: true,
usemmap: true,
backend: '',
gpuDevice: 0,
gpuDeviceSelection: '0',
tensorSplit: '',
gpuPlatform: 0,
sdmodel: '',
sdt5xxl: '',
@ -111,6 +116,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
sdphotomaker: '',
sdvae: '',
sdlora: '',
sdconvdirect: 'off' as const,
setGpuLayers: (layers) => set({ gpuLayers: layers }),
setAutoGpuLayers: (auto) => set({ autoGpuLayers: auto }),
@ -131,8 +137,14 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
setLowvram: (lowvram) => set({ lowvram }),
setQuantmatmul: (quantmatmul) => set({ quantmatmul }),
setUsemmap: (usemmap) => set({ usemmap }),
setBackend: (backend) => set({ backend }),
setGpuDevice: (device) => set({ gpuDevice: device }),
setBackend: (backend) =>
set({
backend,
gpuDeviceSelection: '0',
tensorSplit: '',
}),
setGpuDeviceSelection: (selection) => set({ gpuDeviceSelection: selection }),
setTensorSplit: (split) => set({ tensorSplit: split }),
setGpuPlatform: (platform) => set({ gpuPlatform: platform }),
setSdmodel: (model) => set({ sdmodel: model }),
setSdt5xxl: (model) => set({ sdt5xxl: model }),
@ -141,6 +153,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
setSdphotomaker: (model) => set({ sdphotomaker: model }),
setSdvae: (vae) => set({ sdvae: vae }),
setSdlora: (loraModel) => set({ sdlora: loraModel }),
setSdconvdirect: (mode) => set({ sdconvdirect: mode }),
// eslint-disable-next-line sonarjs/cognitive-complexity
parseAndApplyConfigFile: async (configPath: string) => {
@ -262,7 +275,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
) {
const [vramMode, deviceId, mmqMode] = configData.usecuda;
updates.lowvram = vramMode === 'lowvram';
updates.gpuDevice = parseInt(deviceId, 10) || 0;
updates.gpuDeviceSelection = deviceId || '0';
updates.quantmatmul = mmqMode === 'mmq';
}
} else if (configData.usevulkan === true) {
@ -273,12 +286,20 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
) {
updates.backend = 'clblast';
const [deviceIndex, platformIndex] = configData.useclblast;
updates.gpuDevice = deviceIndex;
updates.gpuDeviceSelection = deviceIndex.toString();
updates.gpuPlatform = platformIndex;
} else {
updates.backend = 'cpu';
}
if (typeof configData.gpuDeviceSelection === 'string') {
updates.gpuDeviceSelection = configData.gpuDeviceSelection;
}
if (typeof configData.tensorSplit === 'string') {
updates.tensorSplit = configData.tensorSplit;
}
if (typeof configData.sdmodel === 'string') {
updates.sdmodel = configData.sdmodel;
}
@ -306,6 +327,12 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
if (typeof configData.sdlora === 'string') {
updates.sdlora = configData.sdlora;
}
if (
typeof configData.sdconvdirect === 'string' &&
['off', 'vaeonly', 'full'].includes(configData.sdconvdirect)
) {
updates.sdconvdirect = configData.sdconvdirect as SdConvDirectMode;
}
set(updates);
}
@ -396,6 +423,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
applyImageModelPreset: (preset: ImageModelPreset) => {
set({
sdmodel: preset.sdmodel,
sdt5xxl: preset.sdt5xxl,
sdclipl: preset.sdclipl,
sdclipg: preset.sdclipg || '',

View file

@ -121,7 +121,6 @@ export interface KoboldAPI {
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
clBlastInfo?: [number, number];
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;

View file

@ -4,6 +4,12 @@ export interface ConfigFile {
size: number;
}
export type InterfaceTab = 'terminal' | 'chat';
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
export type ServerTabMode = 'chat' | 'image-generation';
export interface GitHubAsset {
name: string;
browser_download_url: string;

View file

@ -38,14 +38,4 @@ export const IMAGE_MODEL_PRESETS: ImageModelPreset[] = [
sdvae:
'https://huggingface.co/lodestones/Chroma/resolve/main/ae.safetensors?download=true',
},
{
name: 'Custom',
description: 'Start with empty fields for custom configuration',
sdmodel: '',
sdt5xxl: '',
sdclipl: '',
sdclipg: '',
sdphotomaker: '',
sdvae: '',
},
];

1059
yarn.lock

File diff suppressed because it is too large Load diff