fix: correct default engine variant setup on first launch (#4747)

* fix: there is a case where app selects incorrect engine variant first launch

* refactor: clean up legacy settings hook
This commit is contained in:
Louis 2025-02-27 09:03:01 +07:00 committed by GitHub
parent ecb14b3621
commit 6a0fb09610
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 66 additions and 137 deletions

View File

@ -21,7 +21,7 @@ describe('engineVariant', () => {
it('should return mac-amd64 when platform is darwin and arch is not arm64', async () => {
vi.stubGlobal('PLATFORM', 'darwin')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: '' },
cpu: { arch: 'x64', instructions: [] },
gpus: [],
vulkan: false,
})
@ -31,7 +31,7 @@ describe('engineVariant', () => {
it('should return windows-amd64-noavx-cuda-12-0 when platform is win32, cuda is enabled, and cuda version is 12', async () => {
vi.stubGlobal('PLATFORM', 'win32')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: 'avx2' },
cpu: { arch: 'x64', instructions: ['avx2'] },
gpus: [
{
activated: true,
@ -47,7 +47,7 @@ describe('engineVariant', () => {
it('should return linux-amd64-noavx-cuda-11-7 when platform is linux, cuda is enabled, and cuda version is 11', async () => {
vi.stubGlobal('PLATFORM', 'linux')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: 'avx2' },
cpu: { arch: 'x64', instructions: [] },
gpus: [
{
activated: true,
@ -57,16 +57,34 @@ describe('engineVariant', () => {
],
vulkan: false,
})
expect(result).toBe('linux-amd64-avx2-cuda-11-7')
expect(result).toBe('linux-amd64-noavx-cuda-11-7')
})
it('should return windows-amd64-vulkan when platform is win32 and vulkan is enabled', async () => {
vi.stubGlobal('PLATFORM', 'win32')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: '' },
cpu: { arch: 'x64', instructions: [] },
gpus: [{ activated: true, version: '12' }],
vulkan: true,
})
expect(result).toBe('windows-amd64-vulkan')
})
it('should return windows-amd64-avx512 when platform is win32, no gpu detected and avx512 cpu instruction is supported', async () => {
vi.stubGlobal('PLATFORM', 'win32')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: ['avx512'] },
gpus: [{ activated: true, version: '12' }],
})
expect(result).toBe('windows-amd64-avx512')
})
it('should return windows-amd64-avx512 when platform is win32, no gpu detected and no accelerated cpu instructions are supported', async () => {
vi.stubGlobal('PLATFORM', 'win32')
const result = await engineVariant({
cpu: { arch: 'x64', instructions: [''] },
gpus: [{ activated: true, version: '12' }],
})
expect(result).toBe('windows-amd64-noavx')
})
})

View File

@ -1,20 +1,29 @@
import { GpuSetting, log } from '@janhq/core'
// Supported run modes
enum RunMode {
Cuda = 'cuda',
CPU = 'cpu',
}
// Supported instruction sets
const instructionBinaryNames = ['noavx', 'avx', 'avx2', 'avx512']
/**
* The GPU runMode that will be set - either 'vulkan', 'cuda', or empty for cpu.
* @param settings
* @returns
*/
const gpuRunMode = (settings?: GpuSetting): string => {
const gpuRunMode = (settings?: GpuSetting): RunMode => {
return settings.gpus?.some(
(gpu) =>
gpu.activated === true &&
gpu.activated &&
gpu.additional_information &&
gpu.additional_information.driver_version
)
? 'cuda'
: ''
? RunMode.Cuda
: RunMode.CPU
}
/**
@ -37,13 +46,6 @@ const os = (settings?: GpuSetting): string => {
* @returns
*/
const cudaVersion = (settings?: GpuSetting): '12-0' | '11-7' | undefined => {
const isUsingCuda =
settings?.vulkan !== true &&
settings?.gpus?.some((gpu) => (gpu.activated === true ? 'gpu' : 'cpu')) &&
!os().includes('mac')
if (!isUsingCuda) return undefined
// return settings?.cuda?.version === '11' ? '11-7' : '12-0'
return settings.gpus?.some((gpu) => gpu.version.includes('12'))
? '12-0'
: '11-7'
@ -65,6 +67,7 @@ export const engineVariant = async (
// There is no need to append the variant extension for mac
if (platform.startsWith('mac')) return platform
const runMode = gpuRunMode(gpuSetting)
// Only Nvidia GPUs have addition_information set and activated by default
let engineVariant =
!gpuSetting?.vulkan ||
@ -72,14 +75,23 @@ export const engineVariant = async (
gpuSetting.gpus.some((e) => e.additional_information && e.activated)
? [
platform,
gpuRunMode(gpuSetting) === 'cuda' &&
(gpuSetting.cpu.instructions.includes('avx2') ||
gpuSetting.cpu.instructions.includes('avx512'))
...(runMode === RunMode.Cuda
? // For cuda we only need to check if the cpu supports avx2 or noavx - since other binaries are not shipped with the extension
[
gpuSetting.cpu?.instructions.includes('avx2') ||
gpuSetting.cpu?.instructions.includes('avx512')
? 'avx2'
: 'noavx',
gpuRunMode(gpuSetting),
runMode,
cudaVersion(gpuSetting),
].filter(Boolean) // Remove any falsy values
]
: // For cpu only we need to check all available supported instructions
[
(gpuSetting.cpu?.instructions ?? ['noavx']).find((e) =>
instructionBinaryNames.includes(e.toLowerCase())
) ?? 'noavx',
]),
].filter(Boolean)
: [platform, 'vulkan']
let engineVariantString = engineVariant.join('-')

View File

@ -1,8 +1,7 @@
import React from 'react'
import { render, waitFor, screen } from '@testing-library/react'
import { render } from '@testing-library/react'
import { useAtomValue } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useSettings } from '@/hooks/useSettings'
import ModelLabel from '@/containers/ModelLabel'
jest.mock('jotai', () => ({
@ -14,14 +13,9 @@ jest.mock('@/hooks/useActiveModel', () => ({
useActiveModel: jest.fn(),
}))
jest.mock('@/hooks/useSettings', () => ({
useSettings: jest.fn(),
}))
describe('ModelLabel', () => {
const mockUseAtomValue = useAtomValue as jest.Mock
const mockUseActiveModel = useActiveModel as jest.Mock
const mockUseSettings = useSettings as jest.Mock
const defaultProps: any = {
metadata: {
@ -44,7 +38,6 @@ describe('ModelLabel', () => {
mockUseActiveModel.mockReturnValue({
activeModel: { metadata: { size: 0 } },
})
mockUseSettings.mockReturnValue({ settings: { run_mode: 'cpu' } })
const props = {
...defaultProps,

View File

@ -4,8 +4,6 @@ import { useAtomValue } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useSettings } from '@/hooks/useSettings'
import NotEnoughMemoryLabel from './NotEnoughMemoryLabel'
import SlowOnYourDeviceLabel from './SlowOnYourDeviceLabel'
@ -26,12 +24,12 @@ const ModelLabel = ({ size, compact }: Props) => {
const totalRam = useAtomValue(totalRamAtom)
const usedRam = useAtomValue(usedRamAtom)
const availableVram = useAtomValue(availableVramAtom)
const { settings } = useSettings()
const getLabel = (size: number) => {
const minimumRamModel = (size * 1.25) / (1024 * 1024)
const availableRam = settings?.gpus?.some((gpu) => gpu.activated)
const availableRam =
availableVram > 0
? availableVram * 1000000 // MB to bytes
: totalRam -
(usedRam +
@ -42,7 +40,7 @@ const ModelLabel = ({ size, compact }: Props) => {
if (minimumRamModel > totalRam) {
return (
<NotEnoughMemoryLabel
unit={settings?.gpus?.some((gpu) => gpu.activated) ? 'VRAM' : 'RAM'}
unit={availableVram > 0 ? 'VRAM' : 'RAM'}
compact={compact}
/>
)

View File

@ -1,51 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import { fs, GpuSettingInfo, joinPath } from '@janhq/core'
export type AppSettings = {
vulkan: boolean
gpus: GpuSettingInfo[]
}
export const useSettings = () => {
const [settings, setSettings] = useState<AppSettings>()
useEffect(() => {
readSettings().then((settings) => setSettings(settings as AppSettings))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const readSettings = useCallback(async () => {
if (!window?.core?.api) {
return
}
const settingsFile = await joinPath(['file://settings', 'settings.json'])
if (await fs.existsSync(settingsFile)) {
const settings = await fs.readFileSync(settingsFile, 'utf-8')
return typeof settings === 'object' ? settings : JSON.parse(settings)
}
return {}
}, [])
const saveSettings = async ({ vulkan }: { vulkan?: boolean | undefined }) => {
const settingsFile = await joinPath(['file://settings', 'settings.json'])
const settings = await readSettings()
if (vulkan != null) {
settings.vulkan = vulkan
// GPU enabled, set run_mode to 'gpu'
if (settings.vulkan === true) {
settings?.gpus?.some((gpu: { activated: boolean }) =>
gpu.activated === true ? 'gpu' : 'cpu'
)
}
}
await fs.writeFileSync(settingsFile, JSON.stringify(settings))
}
return {
readSettings,
saveSettings,
settings,
}
}

View File

@ -18,8 +18,6 @@ import { MainViewState } from '@/constants/screens'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useDownloadModel from '@/hooks/useDownloadModel'
import { useSettings } from '@/hooks/useSettings'
import { toGigabytes } from '@/utils/converter'
import { getLogoEngine } from '@/utils/modelEngine'
@ -53,16 +51,13 @@ const ModelItemHeader = ({ model, onSelectedModel }: Props) => {
const setSelectedSetting = useSetAtom(selectedSettingAtom)
const { requestCreateNewThread } = useCreateNewThread()
const totalRam = useAtomValue(totalRamAtom)
const { settings } = useSettings()
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
// Default nvidia returns vram in MB, need to convert to bytes to match the unit of totalRamW
let ram = nvidiaTotalVram * 1024 * 1024
if (ram === 0 || settings?.gpus?.some((gpu) => gpu.activated !== true)) {
ram = totalRam
}
const ram = nvidiaTotalVram > 0 ? nvidiaTotalVram * 1024 * 1024 : totalRam
const serverEnabled = useAtomValue(serverEnabledAtom)
const assistants = useAtomValue(assistantsAtom)

View File

@ -22,25 +22,6 @@ global.window.core = {
},
}
const setSettingsMock = jest.fn()
// Mock useSettings hook
jest.mock('@/hooks/useSettings', () => ({
__esModule: true,
useSettings: () => ({
readSettings: () => ({
run_mode: 'gpu',
experimental: false,
proxy: false,
gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }],
gpus_in_use: ['0'],
quick_ask: false,
}),
setSettings: setSettingsMock,
}),
}))
import * as toast from '@/containers/Toast'
jest.mock('@/containers/Toast')

View File

@ -22,23 +22,6 @@ global.window.core = {
},
}
const setSettingsMock = jest.fn()
// Mock useSettings hook
jest.mock('@/hooks/useSettings', () => ({
__esModule: true,
useSettings: () => ({
readSettings: () => ({
run_mode: 'gpu',
experimental: false,
proxy: false,
gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }],
gpus_in_use: ['0'],
quick_ask: false,
}),
setSettings: setSettingsMock,
}),
}))
import * as toast from '@/containers/Toast'