fix: gpu detected from backend version (#5882)
* fix: gpu detected from backend version * chore: remove readonly props from dynamic field
This commit is contained in:
parent
6599d91660
commit
399671488c
@ -131,11 +131,11 @@
|
|||||||
{
|
{
|
||||||
"key": "device",
|
"key": "device",
|
||||||
"title": "Devices for Offload",
|
"title": "Devices for Offload",
|
||||||
"description": "Comma-separated list of devices to use for offloading (e.g., 'cuda:0', 'cuda:0,cuda:1'). Leave empty to use default/CPU only.",
|
"description": "Comma-separated list of devices to use for offloading (e.g., 'CUDA0', 'CUDA0,CUDA1'). Leave empty to use default/CPU only.",
|
||||||
"controllerType": "input",
|
"controllerType": "input",
|
||||||
"controllerProps": {
|
"controllerProps": {
|
||||||
"value": "",
|
"value": "",
|
||||||
"placeholder": "cuda:0",
|
"placeholder": "CUDA0",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -526,6 +526,9 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
const valueStr = value as string
|
const valueStr = value as string
|
||||||
const [version, backend] = valueStr.split('/')
|
const [version, backend] = valueStr.split('/')
|
||||||
|
|
||||||
|
// Reset device setting when backend changes
|
||||||
|
this.config.device = ''
|
||||||
|
|
||||||
const closure = async () => {
|
const closure = async () => {
|
||||||
await this.ensureBackendReady(backend, version)
|
await this.ensureBackendReady(backend, version)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ type DynamicControllerProps = {
|
|||||||
title?: string
|
title?: string
|
||||||
className?: string
|
className?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
readonly?: boolean
|
||||||
controllerType:
|
controllerType:
|
||||||
| 'input'
|
| 'input'
|
||||||
| 'checkbox'
|
| 'checkbox'
|
||||||
|
|||||||
@ -17,26 +17,6 @@ vi.mock('@/constants/localStorage', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./useModelProvider', () => ({
|
|
||||||
useModelProvider: {
|
|
||||||
getState: () => ({
|
|
||||||
updateProvider: vi.fn(),
|
|
||||||
getProviderByName: vi.fn(() => ({
|
|
||||||
settings: [
|
|
||||||
{
|
|
||||||
key: 'version_backend',
|
|
||||||
controller_props: { value: 'cuda' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'device',
|
|
||||||
controller_props: { value: '' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock zustand persist
|
// Mock zustand persist
|
||||||
vi.mock('zustand/middleware', () => ({
|
vi.mock('zustand/middleware', () => ({
|
||||||
persist: (fn: any) => fn,
|
persist: (fn: any) => fn,
|
||||||
@ -253,50 +233,6 @@ describe('useHardware', () => {
|
|||||||
expect(result.current.pollingPaused).toBe(false)
|
expect(result.current.pollingPaused).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get activated device string', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const testHardwareData = {
|
|
||||||
cpu: {
|
|
||||||
arch: 'x86_64',
|
|
||||||
core_count: 8,
|
|
||||||
extensions: ['SSE', 'AVX'],
|
|
||||||
name: 'Intel Core i7',
|
|
||||||
usage: 25.5,
|
|
||||||
},
|
|
||||||
gpus: [
|
|
||||||
{
|
|
||||||
name: 'NVIDIA RTX 3080',
|
|
||||||
total_memory: 10737418240,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'GPU-12345',
|
|
||||||
driver_version: '470.57.02',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: {
|
|
||||||
index: 0,
|
|
||||||
compute_capability: '8.6',
|
|
||||||
},
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 8704,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.2.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
os_type: 'linux',
|
|
||||||
os_name: 'Ubuntu',
|
|
||||||
total_memory: 17179869184,
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setHardwareData(testHardwareData)
|
|
||||||
})
|
|
||||||
|
|
||||||
const deviceString = result.current.getActivatedDeviceString()
|
|
||||||
expect(typeof deviceString).toBe('string')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('setOS', () => {
|
describe('setOS', () => {
|
||||||
it('should update OS data', () => {
|
it('should update OS data', () => {
|
||||||
const { result } = renderHook(() => useHardware())
|
const { result } = renderHook(() => useHardware())
|
||||||
@ -331,202 +267,6 @@ describe('useHardware', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('updateHardwareDataPreservingGpuOrder', () => {
|
|
||||||
it('should preserve existing GPU order and activation states', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const initialData: HardwareData = {
|
|
||||||
cpu: {
|
|
||||||
arch: 'x86_64',
|
|
||||||
core_count: 4,
|
|
||||||
extensions: [],
|
|
||||||
name: 'CPU',
|
|
||||||
usage: 0,
|
|
||||||
},
|
|
||||||
gpus: [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPU 2',
|
|
||||||
total_memory: 4096,
|
|
||||||
vendor: 'AMD',
|
|
||||||
uuid: 'gpu-2',
|
|
||||||
driver_version: '2.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 1, compute_capability: '7.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 2,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
os_type: 'windows',
|
|
||||||
os_name: 'Windows 11',
|
|
||||||
total_memory: 16384,
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setHardwareData(initialData)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedData: HardwareData = {
|
|
||||||
...initialData,
|
|
||||||
gpus: [
|
|
||||||
{ ...initialData.gpus[1], name: 'GPU 2 Updated' },
|
|
||||||
{ ...initialData.gpus[0], name: 'GPU 1 Updated' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateHardwareDataPreservingGpuOrder(updatedData)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].uuid).toBe('gpu-1')
|
|
||||||
expect(result.current.hardwareData.gpus[0].name).toBe('GPU 1 Updated')
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(true)
|
|
||||||
expect(result.current.hardwareData.gpus[1].uuid).toBe('gpu-2')
|
|
||||||
expect(result.current.hardwareData.gpus[1].name).toBe('GPU 2 Updated')
|
|
||||||
expect(result.current.hardwareData.gpus[1].activated).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should add new GPUs at the end', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const initialData: HardwareData = {
|
|
||||||
cpu: {
|
|
||||||
arch: 'x86_64',
|
|
||||||
core_count: 4,
|
|
||||||
extensions: [],
|
|
||||||
name: 'CPU',
|
|
||||||
usage: 0,
|
|
||||||
},
|
|
||||||
gpus: [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
os_type: 'windows',
|
|
||||||
os_name: 'Windows 11',
|
|
||||||
total_memory: 16384,
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setHardwareData(initialData)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedData: HardwareData = {
|
|
||||||
...initialData,
|
|
||||||
gpus: [
|
|
||||||
...initialData.gpus,
|
|
||||||
{
|
|
||||||
name: 'New GPU',
|
|
||||||
total_memory: 4096,
|
|
||||||
vendor: 'AMD',
|
|
||||||
uuid: 'gpu-new',
|
|
||||||
driver_version: '3.0',
|
|
||||||
nvidia_info: { index: 1, compute_capability: '7.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 3,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateHardwareDataPreservingGpuOrder(updatedData)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus).toHaveLength(2)
|
|
||||||
expect(result.current.hardwareData.gpus[0].uuid).toBe('gpu-1')
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(true)
|
|
||||||
expect(result.current.hardwareData.gpus[1].uuid).toBe('gpu-new')
|
|
||||||
expect(result.current.hardwareData.gpus[1].activated).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should initialize all GPUs as inactive when no existing data', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
// First clear any existing data by setting empty hardware data
|
|
||||||
act(() => {
|
|
||||||
result.current.setHardwareData({
|
|
||||||
cpu: { arch: '', core_count: 0, extensions: [], name: '', usage: 0 },
|
|
||||||
gpus: [],
|
|
||||||
os_type: '',
|
|
||||||
os_name: '',
|
|
||||||
total_memory: 0,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Now we should have empty hardware state
|
|
||||||
expect(result.current.hardwareData.gpus.length).toBe(0)
|
|
||||||
|
|
||||||
const hardwareData: HardwareData = {
|
|
||||||
cpu: {
|
|
||||||
arch: 'x86_64',
|
|
||||||
core_count: 4,
|
|
||||||
extensions: [],
|
|
||||||
name: 'CPU',
|
|
||||||
usage: 0,
|
|
||||||
},
|
|
||||||
gpus: [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
os_type: 'windows',
|
|
||||||
os_name: 'Windows 11',
|
|
||||||
total_memory: 16384,
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateHardwareDataPreservingGpuOrder(hardwareData)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('updateGPU', () => {
|
describe('updateGPU', () => {
|
||||||
it('should update specific GPU at index', () => {
|
it('should update specific GPU at index', () => {
|
||||||
const { result } = renderHook(() => useHardware())
|
const { result } = renderHook(() => useHardware())
|
||||||
@ -621,485 +361,84 @@ describe('useHardware', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('reorderGPUs', () => {
|
describe('setHardwareData with GPU activation', () => {
|
||||||
it('should reorder GPUs correctly', () => {
|
it('should initialize GPUs as inactive when activated is not specified', () => {
|
||||||
const { result } = renderHook(() => useHardware())
|
const { result } = renderHook(() => useHardware())
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
const hardwareData: HardwareData = {
|
||||||
{
|
cpu: {
|
||||||
name: 'GPU 1',
|
arch: 'x86_64',
|
||||||
total_memory: 8192,
|
core_count: 4,
|
||||||
vendor: 'NVIDIA',
|
extensions: [],
|
||||||
uuid: 'gpu-1',
|
name: 'CPU',
|
||||||
driver_version: '1.0',
|
usage: 0,
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
gpus: [
|
||||||
name: 'GPU 2',
|
{
|
||||||
total_memory: 4096,
|
name: 'GPU 1',
|
||||||
vendor: 'AMD',
|
total_memory: 8192,
|
||||||
uuid: 'gpu-2',
|
vendor: 'NVIDIA',
|
||||||
driver_version: '2.0',
|
uuid: 'gpu-1',
|
||||||
activated: false,
|
driver_version: '1.0',
|
||||||
nvidia_info: { index: 1, compute_capability: '7.0' },
|
nvidia_info: { index: 0, compute_capability: '8.0' },
|
||||||
vulkan_info: {
|
vulkan_info: {
|
||||||
index: 1,
|
index: 0,
|
||||||
device_id: 2,
|
device_id: 1,
|
||||||
device_type: 'discrete',
|
device_type: 'discrete',
|
||||||
api_version: '1.0',
|
api_version: '1.0',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{
|
os_type: 'windows',
|
||||||
name: 'GPU 3',
|
os_name: 'Windows 11',
|
||||||
total_memory: 6144,
|
total_memory: 16384,
|
||||||
vendor: 'Intel',
|
|
||||||
uuid: 'gpu-3',
|
|
||||||
driver_version: '3.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 2, compute_capability: '6.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 2,
|
|
||||||
device_id: 3,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.reorderGPUs(0, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].uuid).toBe('gpu-2')
|
|
||||||
expect(result.current.hardwareData.gpus[1].uuid).toBe('gpu-3')
|
|
||||||
expect(result.current.hardwareData.gpus[2].uuid).toBe('gpu-1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle invalid indices gracefully', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const originalOrder = result.current.hardwareData.gpus
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.reorderGPUs(-1, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus).toEqual(originalOrder)
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.reorderGPUs(0, 5)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus).toEqual(originalOrder)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getActivatedDeviceString', () => {
|
|
||||||
it('should return empty string when no GPUs are activated', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const deviceString = result.current.getActivatedDeviceString()
|
|
||||||
expect(deviceString).toBe('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return CUDA device string for NVIDIA GPUs', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const deviceString = result.current.getActivatedDeviceString('cuda')
|
|
||||||
expect(deviceString).toBe('cuda:0')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return Vulkan device string for Vulkan backend', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'AMD',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 2,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const deviceString = result.current.getActivatedDeviceString('vulkan')
|
|
||||||
expect(deviceString).toBe('Vulkan1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle mixed backend correctly', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'NVIDIA GPU',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AMD GPU',
|
|
||||||
total_memory: 4096,
|
|
||||||
vendor: 'AMD',
|
|
||||||
uuid: 'gpu-2',
|
|
||||||
driver_version: '2.0',
|
|
||||||
activated: true,
|
|
||||||
// AMD GPU shouldn't have nvidia_info, just vulkan_info
|
|
||||||
nvidia_info: { index: 1, compute_capability: '7.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 2,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Based on the implementation, both GPUs will use CUDA since they both have nvidia_info
|
|
||||||
// The test should match the actual behavior
|
|
||||||
const deviceString =
|
|
||||||
result.current.getActivatedDeviceString('cuda+vulkan')
|
|
||||||
expect(deviceString).toBe('cuda:0,cuda:1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return multiple device strings comma-separated', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPU 2',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-2',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 1, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 2,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const deviceString = result.current.getActivatedDeviceString('cuda')
|
|
||||||
expect(deviceString).toBe('cuda:0,cuda:1')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('updateGPUActivationFromDeviceString', () => {
|
|
||||||
it('should activate GPUs based on device string', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPU 2',
|
|
||||||
total_memory: 4096,
|
|
||||||
vendor: 'AMD',
|
|
||||||
uuid: 'gpu-2',
|
|
||||||
driver_version: '2.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 1, compute_capability: '7.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 1,
|
|
||||||
device_id: 2,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateGPUActivationFromDeviceString('cuda:0,Vulkan1')
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(true)
|
|
||||||
expect(result.current.hardwareData.gpus[1].activated).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty device string', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: true,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateGPUActivationFromDeviceString('')
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle invalid device string format', () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateGPUActivationFromDeviceString('invalid:format,bad')
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('toggleGPUActivation', () => {
|
|
||||||
it('should toggle GPU activation and manage loading state', async () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(false)
|
|
||||||
expect(result.current.pollingPaused).toBe(false)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.toggleGPUActivation(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle invalid GPU index gracefully', async () => {
|
|
||||||
const { result } = renderHook(() => useHardware())
|
|
||||||
|
|
||||||
const gpus: GPU[] = [
|
|
||||||
{
|
|
||||||
name: 'GPU 1',
|
|
||||||
total_memory: 8192,
|
|
||||||
vendor: 'NVIDIA',
|
|
||||||
uuid: 'gpu-1',
|
|
||||||
driver_version: '1.0',
|
|
||||||
activated: false,
|
|
||||||
nvidia_info: { index: 0, compute_capability: '8.0' },
|
|
||||||
vulkan_info: {
|
|
||||||
index: 0,
|
|
||||||
device_id: 1,
|
|
||||||
device_type: 'discrete',
|
|
||||||
api_version: '1.0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setGPUs(gpus)
|
|
||||||
})
|
|
||||||
|
|
||||||
const originalState = result.current.hardwareData.gpus[0].activated
|
|
||||||
|
|
||||||
// Test with invalid index that doesn't throw an error
|
|
||||||
try {
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.toggleGPUActivation(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(
|
|
||||||
originalState
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
// If it throws an error due to index bounds, that's expected behavior
|
|
||||||
expect(result.current.hardwareData.gpus[0].activated).toBe(
|
|
||||||
originalState
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setHardwareData(hardwareData)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.hardwareData.gpus[0].activated).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve existing activation states when set', () => {
|
||||||
|
const { result } = renderHook(() => useHardware())
|
||||||
|
|
||||||
|
const hardwareData: HardwareData = {
|
||||||
|
cpu: {
|
||||||
|
arch: 'x86_64',
|
||||||
|
core_count: 4,
|
||||||
|
extensions: [],
|
||||||
|
name: 'CPU',
|
||||||
|
usage: 0,
|
||||||
|
},
|
||||||
|
gpus: [
|
||||||
|
{
|
||||||
|
name: 'GPU 1',
|
||||||
|
total_memory: 8192,
|
||||||
|
vendor: 'NVIDIA',
|
||||||
|
uuid: 'gpu-1',
|
||||||
|
driver_version: '1.0',
|
||||||
|
activated: true,
|
||||||
|
nvidia_info: { index: 0, compute_capability: '8.0' },
|
||||||
|
vulkan_info: {
|
||||||
|
index: 0,
|
||||||
|
device_id: 1,
|
||||||
|
device_type: 'discrete',
|
||||||
|
api_version: '1.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
os_type: 'windows',
|
||||||
|
os_name: 'Windows 11',
|
||||||
|
total_memory: 16384,
|
||||||
|
}
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setHardwareData(hardwareData)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.hardwareData.gpus[0].activated).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
132
web-app/src/hooks/__tests__/useLlamacppDevices.test.ts
Normal file
132
web-app/src/hooks/__tests__/useLlamacppDevices.test.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { useLlamacppDevices } from '../useLlamacppDevices'
|
||||||
|
import { getLlamacppDevices } from '../../services/hardware'
|
||||||
|
|
||||||
|
// Mock the hardware service
|
||||||
|
vi.mock('@/services/hardware', () => ({
|
||||||
|
getLlamacppDevices: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the window.core object
|
||||||
|
Object.defineProperty(window, 'core', {
|
||||||
|
value: {
|
||||||
|
extensionManager: {
|
||||||
|
getByName: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useLlamacppDevices', () => {
|
||||||
|
const mockGetLlamacppDevices = vi.mocked(getLlamacppDevices)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize with default state', () => {
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
expect(result.current.devices).toEqual([])
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.error).toBeNull()
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set())
|
||||||
|
expect(typeof result.current.fetchDevices).toBe('function')
|
||||||
|
expect(typeof result.current.clearError).toBe('function')
|
||||||
|
expect(typeof result.current.setDevices).toBe('function')
|
||||||
|
expect(typeof result.current.toggleDevice).toBe('function')
|
||||||
|
expect(typeof result.current.setActivatedDevices).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch devices successfully', async () => {
|
||||||
|
const mockDevices = [
|
||||||
|
{ id: 'CUDA0', name: 'NVIDIA GeForce RTX 4090', mem: 24576, free: 20480 },
|
||||||
|
{ id: 'CUDA1', name: 'NVIDIA GeForce RTX 3080', mem: 10240, free: 8192 },
|
||||||
|
]
|
||||||
|
|
||||||
|
mockGetLlamacppDevices.mockResolvedValue(mockDevices)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.fetchDevices()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.devices).toEqual(mockDevices)
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.error).toBeNull()
|
||||||
|
expect(mockGetLlamacppDevices).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear error', () => {
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
// Set an error first
|
||||||
|
act(() => {
|
||||||
|
result.current.setDevices([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear the error
|
||||||
|
act(() => {
|
||||||
|
result.current.clearError()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.error).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set devices directly', () => {
|
||||||
|
const mockDevices = [
|
||||||
|
{ id: 'CUDA0', name: 'NVIDIA GeForce RTX 4090', mem: 24576, free: 20480 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setDevices(mockDevices)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.devices).toEqual(mockDevices)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle device activation', () => {
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
// Initially no devices are activated
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set())
|
||||||
|
|
||||||
|
// Toggle a device on
|
||||||
|
act(() => {
|
||||||
|
result.current.toggleDevice('CUDA0')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set(['CUDA0']))
|
||||||
|
|
||||||
|
// Toggle the same device off
|
||||||
|
act(() => {
|
||||||
|
result.current.toggleDevice('CUDA0')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set())
|
||||||
|
|
||||||
|
// Toggle multiple devices
|
||||||
|
act(() => {
|
||||||
|
result.current.toggleDevice('CUDA0')
|
||||||
|
result.current.toggleDevice('CUDA1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set(['CUDA0', 'CUDA1']))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set activated devices', () => {
|
||||||
|
const { result } = renderHook(() => useLlamacppDevices())
|
||||||
|
|
||||||
|
const deviceIds = ['CUDA0', 'CUDA1', 'Vulkan0']
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setActivatedDevices(deviceIds)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.activatedDevices).toEqual(new Set(deviceIds))
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -102,18 +102,12 @@ interface HardwareStore {
|
|||||||
// Update entire hardware data at once
|
// Update entire hardware data at once
|
||||||
setHardwareData: (data: HardwareData) => void
|
setHardwareData: (data: HardwareData) => void
|
||||||
|
|
||||||
// Update hardware data while preserving GPU order
|
|
||||||
updateHardwareDataPreservingGpuOrder: (data: HardwareData) => void
|
|
||||||
|
|
||||||
// Update individual GPU
|
// Update individual GPU
|
||||||
updateGPU: (index: number, gpu: GPU) => void
|
updateGPU: (index: number, gpu: GPU) => void
|
||||||
|
|
||||||
// Update RAM available
|
// Update RAM available
|
||||||
updateSystemUsage: (usage: SystemUsage) => void
|
updateSystemUsage: (usage: SystemUsage) => void
|
||||||
|
|
||||||
// Toggle GPU activation (async, with loading)
|
|
||||||
toggleGPUActivation: (index: number) => Promise<void>
|
|
||||||
|
|
||||||
// GPU loading state
|
// GPU loading state
|
||||||
gpuLoading: { [index: number]: boolean }
|
gpuLoading: { [index: number]: boolean }
|
||||||
setGpuLoading: (index: number, loading: boolean) => void
|
setGpuLoading: (index: number, loading: boolean) => void
|
||||||
@ -122,20 +116,11 @@ interface HardwareStore {
|
|||||||
pollingPaused: boolean
|
pollingPaused: boolean
|
||||||
pausePolling: () => void
|
pausePolling: () => void
|
||||||
resumePolling: () => void
|
resumePolling: () => void
|
||||||
|
|
||||||
// Reorder GPUs
|
|
||||||
reorderGPUs: (oldIndex: number, newIndex: number) => void
|
|
||||||
|
|
||||||
// Get activated GPU device string
|
|
||||||
getActivatedDeviceString: (backendType?: string) => string
|
|
||||||
|
|
||||||
// Update GPU activation states from device string
|
|
||||||
updateGPUActivationFromDeviceString: (deviceString: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHardware = create<HardwareStore>()(
|
export const useHardware = create<HardwareStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set) => ({
|
||||||
hardwareData: defaultHardwareData,
|
hardwareData: defaultHardwareData,
|
||||||
systemUsage: defaultSystemUsage,
|
systemUsage: defaultSystemUsage,
|
||||||
gpuLoading: {},
|
gpuLoading: {},
|
||||||
@ -193,58 +178,6 @@ export const useHardware = create<HardwareStore>()(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateHardwareDataPreservingGpuOrder: (data) =>
|
|
||||||
set((state) => {
|
|
||||||
// If we have existing GPU data, preserve the order and activation state
|
|
||||||
if (state.hardwareData.gpus.length > 0) {
|
|
||||||
// Reorder fresh GPU data to match existing order, adding new GPUs at the end
|
|
||||||
const reorderedGpus: GPU[] = []
|
|
||||||
const processedUuids = new Set()
|
|
||||||
|
|
||||||
// First, add existing GPUs in their current order, preserving activation state
|
|
||||||
state.hardwareData.gpus.forEach((existingGpu) => {
|
|
||||||
const freshGpu = data.gpus.find(
|
|
||||||
(gpu) => gpu.uuid === existingGpu.uuid
|
|
||||||
)
|
|
||||||
if (freshGpu) {
|
|
||||||
reorderedGpus.push({
|
|
||||||
...freshGpu,
|
|
||||||
activated: existingGpu.activated ?? false,
|
|
||||||
})
|
|
||||||
processedUuids.add(freshGpu.uuid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Then, add any new GPUs that weren't in the existing order (default to inactive)
|
|
||||||
data.gpus.forEach((freshGpu) => {
|
|
||||||
if (!processedUuids.has(freshGpu.uuid)) {
|
|
||||||
reorderedGpus.push({
|
|
||||||
...freshGpu,
|
|
||||||
activated: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
hardwareData: {
|
|
||||||
...data,
|
|
||||||
gpus: reorderedGpus,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No existing GPU data, initialize all GPUs as inactive
|
|
||||||
return {
|
|
||||||
hardwareData: {
|
|
||||||
...data,
|
|
||||||
gpus: data.gpus.map((gpu) => ({
|
|
||||||
...gpu,
|
|
||||||
activated: false,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateGPU: (index, gpu) =>
|
updateGPU: (index, gpu) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newGPUs = [...state.hardwareData.gpus]
|
const newGPUs = [...state.hardwareData.gpus]
|
||||||
@ -263,190 +196,6 @@ export const useHardware = create<HardwareStore>()(
|
|||||||
set(() => ({
|
set(() => ({
|
||||||
systemUsage,
|
systemUsage,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
toggleGPUActivation: async (index) => {
|
|
||||||
const { pausePolling, resumePolling, setGpuLoading } = get()
|
|
||||||
pausePolling()
|
|
||||||
setGpuLoading(index, true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200)) // Simulate async operation
|
|
||||||
|
|
||||||
set((state) => {
|
|
||||||
const newGPUs = [...state.hardwareData.gpus]
|
|
||||||
if (index >= 0 && index < newGPUs.length) {
|
|
||||||
newGPUs[index] = {
|
|
||||||
...newGPUs[index],
|
|
||||||
activated: !newGPUs[index].activated,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hardwareData: {
|
|
||||||
...state.hardwareData,
|
|
||||||
gpus: newGPUs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the device setting after state change
|
|
||||||
const updatedState = get()
|
|
||||||
|
|
||||||
// Import and get backend type
|
|
||||||
const { useModelProvider } = await import('./useModelProvider')
|
|
||||||
const { updateProvider, getProviderByName } =
|
|
||||||
useModelProvider.getState()
|
|
||||||
|
|
||||||
const llamacppProvider = getProviderByName('llamacpp')
|
|
||||||
const backendType = llamacppProvider?.settings.find(
|
|
||||||
(s) => s.key === 'version_backend'
|
|
||||||
)?.controller_props.value as string
|
|
||||||
|
|
||||||
const deviceString =
|
|
||||||
updatedState.getActivatedDeviceString(backendType)
|
|
||||||
|
|
||||||
if (llamacppProvider) {
|
|
||||||
const updatedSettings = llamacppProvider.settings.map((setting) => {
|
|
||||||
if (setting.key === 'device') {
|
|
||||||
return {
|
|
||||||
...setting,
|
|
||||||
controller_props: {
|
|
||||||
...setting.controller_props,
|
|
||||||
value: deviceString,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return setting
|
|
||||||
})
|
|
||||||
|
|
||||||
updateProvider('llamacpp', {
|
|
||||||
settings: updatedSettings,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setGpuLoading(index, false)
|
|
||||||
setTimeout(resumePolling, 1000) // Resume polling after 1s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reorderGPUs: (oldIndex, newIndex) =>
|
|
||||||
set((state) => {
|
|
||||||
const newGPUs = [...state.hardwareData.gpus]
|
|
||||||
// Move the GPU from oldIndex to newIndex
|
|
||||||
if (
|
|
||||||
oldIndex >= 0 &&
|
|
||||||
oldIndex < newGPUs.length &&
|
|
||||||
newIndex >= 0 &&
|
|
||||||
newIndex < newGPUs.length
|
|
||||||
) {
|
|
||||||
const [removed] = newGPUs.splice(oldIndex, 1)
|
|
||||||
newGPUs.splice(newIndex, 0, removed)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hardwareData: {
|
|
||||||
...state.hardwareData,
|
|
||||||
gpus: newGPUs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
getActivatedDeviceString: (backendType?: string) => {
|
|
||||||
const { hardwareData } = get()
|
|
||||||
|
|
||||||
// Get activated GPUs and generate appropriate device format based on backend
|
|
||||||
const activatedDevices = hardwareData.gpus
|
|
||||||
.filter((gpu) => gpu.activated)
|
|
||||||
.map((gpu) => {
|
|
||||||
const isCudaBackend = backendType?.includes('cuda')
|
|
||||||
const isVulkanBackend = backendType?.includes('vulkan')
|
|
||||||
|
|
||||||
// Handle different backend scenarios
|
|
||||||
if (isCudaBackend && isVulkanBackend) {
|
|
||||||
// Mixed backend - prefer CUDA for NVIDIA GPUs, Vulkan for others
|
|
||||||
if (gpu.nvidia_info) {
|
|
||||||
return `cuda:${gpu.nvidia_info.index}`
|
|
||||||
} else if (gpu.vulkan_info) {
|
|
||||||
return `Vulkan${gpu.vulkan_info.index}`
|
|
||||||
}
|
|
||||||
} else if (isCudaBackend && gpu.nvidia_info) {
|
|
||||||
// CUDA backend - only use CUDA-compatible GPUs
|
|
||||||
return `cuda:${gpu.nvidia_info.index}`
|
|
||||||
} else if (isVulkanBackend && gpu.vulkan_info) {
|
|
||||||
// Vulkan backend - only use Vulkan-compatible GPUs
|
|
||||||
return `Vulkan${gpu.vulkan_info.index}`
|
|
||||||
} else if (!backendType) {
|
|
||||||
// No backend specified, use GPU's preferred type
|
|
||||||
if (gpu.nvidia_info) {
|
|
||||||
return `cuda:${gpu.nvidia_info.index}`
|
|
||||||
} else if (gpu.vulkan_info) {
|
|
||||||
return `Vulkan${gpu.vulkan_info.index}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.filter((device) => device !== null) as string[]
|
|
||||||
|
|
||||||
const deviceString = activatedDevices.join(',')
|
|
||||||
return deviceString
|
|
||||||
},
|
|
||||||
|
|
||||||
updateGPUActivationFromDeviceString: (deviceString: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const newGPUs = [...state.hardwareData.gpus]
|
|
||||||
|
|
||||||
// Parse device string to get active device indices
|
|
||||||
const activeDevices = deviceString
|
|
||||||
.split(',')
|
|
||||||
.map((device) => device.trim())
|
|
||||||
.filter((device) => device.length > 0)
|
|
||||||
.map((device) => {
|
|
||||||
// Handle both formats: "cuda:0" and "Vulkan1"
|
|
||||||
const cudaMatch = device.match(/^cuda:(\d+)$/)
|
|
||||||
const vulkanMatch = device.match(/^Vulkan(\d+)$/)
|
|
||||||
|
|
||||||
if (cudaMatch) {
|
|
||||||
return {
|
|
||||||
type: 'cuda' as const,
|
|
||||||
index: parseInt(cudaMatch[1]),
|
|
||||||
}
|
|
||||||
} else if (vulkanMatch) {
|
|
||||||
return {
|
|
||||||
type: 'vulkan' as const,
|
|
||||||
index: parseInt(vulkanMatch[1]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.filter((device) => device !== null) as Array<{
|
|
||||||
type: 'cuda' | 'vulkan'
|
|
||||||
index: number
|
|
||||||
}>
|
|
||||||
|
|
||||||
// Update GPU activation states
|
|
||||||
newGPUs.forEach((gpu, gpuIndex) => {
|
|
||||||
const shouldBeActive = activeDevices.some((device) => {
|
|
||||||
if (device.type === 'cuda' && gpu.nvidia_info) {
|
|
||||||
return gpu.nvidia_info.index === device.index
|
|
||||||
} else if (device.type === 'vulkan' && gpu.vulkan_info) {
|
|
||||||
return gpu.vulkan_info.index === device.index
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
newGPUs[gpuIndex] = {
|
|
||||||
...gpu,
|
|
||||||
activated: shouldBeActive,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
hardwareData: {
|
|
||||||
...state.hardwareData,
|
|
||||||
gpus: newGPUs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: localStorageKey.settingHardware,
|
name: localStorageKey.settingHardware,
|
||||||
|
|||||||
84
web-app/src/hooks/useLlamacppDevices.ts
Normal file
84
web-app/src/hooks/useLlamacppDevices.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { getLlamacppDevices, DeviceList } from '@/services/hardware'
|
||||||
|
import { updateSettings } from '@/services/providers'
|
||||||
|
import { useModelProvider } from './useModelProvider'
|
||||||
|
|
||||||
|
interface LlamacppDevicesStore {
|
||||||
|
devices: DeviceList[]
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
activatedDevices: Set<string> // Track which devices are activated
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchDevices: () => Promise<void>
|
||||||
|
clearError: () => void
|
||||||
|
setDevices: (devices: DeviceList[]) => void
|
||||||
|
toggleDevice: (deviceId: string) => void
|
||||||
|
setActivatedDevices: (deviceIds: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLlamacppDevices = create<LlamacppDevicesStore>((set, get) => ({
|
||||||
|
devices: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
activatedDevices: new Set(),
|
||||||
|
|
||||||
|
fetchDevices: async () => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const devices = await getLlamacppDevices()
|
||||||
|
set({ devices, loading: false })
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Failed to fetch devices'
|
||||||
|
set({ error: errorMessage, loading: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
|
||||||
|
setDevices: (devices) => set({ devices }),
|
||||||
|
|
||||||
|
toggleDevice: async (deviceId: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const newActivatedDevices = new Set(state.activatedDevices)
|
||||||
|
if (newActivatedDevices.has(deviceId)) {
|
||||||
|
newActivatedDevices.delete(deviceId)
|
||||||
|
} else {
|
||||||
|
newActivatedDevices.add(deviceId)
|
||||||
|
}
|
||||||
|
return { activatedDevices: newActivatedDevices }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update llamacpp provider settings
|
||||||
|
const { getProviderByName, updateProvider } = useModelProvider.getState()
|
||||||
|
const llamacppProvider = getProviderByName('llamacpp')
|
||||||
|
|
||||||
|
if (llamacppProvider) {
|
||||||
|
const deviceString = Array.from(get().activatedDevices).join(',')
|
||||||
|
|
||||||
|
const updatedSettings = llamacppProvider.settings.map((setting) => {
|
||||||
|
if (setting.key === 'device') {
|
||||||
|
return {
|
||||||
|
...setting,
|
||||||
|
controller_props: {
|
||||||
|
...setting.controller_props,
|
||||||
|
value: deviceString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return setting
|
||||||
|
})
|
||||||
|
|
||||||
|
await updateSettings('llamacpp', updatedSettings)
|
||||||
|
updateProvider('llamacpp', {
|
||||||
|
settings: updatedSettings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setActivatedDevices: (deviceIds: string[]) => {
|
||||||
|
set({ activatedDevices: new Set(deviceIds) })
|
||||||
|
},
|
||||||
|
}))
|
||||||
@ -7,258 +7,87 @@ import { Switch } from '@/components/ui/switch'
|
|||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useHardware } from '@/hooks/useHardware'
|
import { useHardware } from '@/hooks/useHardware'
|
||||||
// import { useVulkan } from '@/hooks/useVulkan'
|
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'
|
||||||
import type { GPU, HardwareData } from '@/hooks/useHardware'
|
import { useEffect } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { IconDeviceDesktopAnalytics } from '@tabler/icons-react'
|
||||||
import {
|
import { getSystemUsage } from '@/services/hardware'
|
||||||
DndContext,
|
|
||||||
closestCenter,
|
|
||||||
KeyboardSensor,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
DragEndEvent,
|
|
||||||
} from '@dnd-kit/core'
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
useSortable,
|
|
||||||
} from '@dnd-kit/sortable'
|
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
|
||||||
import {
|
|
||||||
IconGripVertical,
|
|
||||||
IconDeviceDesktopAnalytics,
|
|
||||||
} from '@tabler/icons-react'
|
|
||||||
import { getHardwareInfo, getSystemUsage } from '@/services/hardware'
|
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { formatMegaBytes } from '@/lib/utils'
|
import { formatMegaBytes } from '@/lib/utils'
|
||||||
import { windowKey } from '@/constants/windows'
|
import { windowKey } from '@/constants/windows'
|
||||||
import { toNumber } from '@/utils/number'
|
import { toNumber } from '@/utils/number'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
import { stopAllModels } from '@/services/models'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.settings.hardware as any)({
|
export const Route = createFileRoute(route.settings.hardware as any)({
|
||||||
component: Hardware,
|
component: Hardware,
|
||||||
})
|
})
|
||||||
|
|
||||||
function SortableGPUItem({ gpu, index, isCompatible, isActivated }: { gpu: GPU; index: number; isCompatible: boolean; isActivated: boolean }) {
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({ id: index })
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { systemUsage, toggleGPUActivation, gpuLoading } = useHardware()
|
|
||||||
const usage = systemUsage.gpus[index]
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
position: 'relative' as const,
|
|
||||||
zIndex: isDragging ? 1 : 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={setNodeRef} style={style} className={`mb-4 last:mb-0 ${!isCompatible ? 'opacity-60' : ''}`}>
|
|
||||||
<CardItem
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
className="size-6 cursor-move flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
|
||||||
>
|
|
||||||
<IconGripVertical size={18} className="text-main-view-fg/60" />
|
|
||||||
</div>
|
|
||||||
<span className="text-main-view-fg/80">{gpu.name}</span>
|
|
||||||
{!isCompatible && (
|
|
||||||
<span className="text-xs bg-destructive/10 text-destructive px-2 py-1 rounded-sm">
|
|
||||||
Incompatible with current backend
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Switch
|
|
||||||
checked={isActivated}
|
|
||||||
disabled={!!gpuLoading[index] || !isCompatible}
|
|
||||||
onCheckedChange={() => toggleGPUActivation(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="ml-8 mt-3">
|
|
||||||
<CardItem
|
|
||||||
title={t('settings:hardware.vram')}
|
|
||||||
actions={
|
|
||||||
<span className="text-main-view-fg/80">
|
|
||||||
{formatMegaBytes(usage?.used_memory)}{' '}
|
|
||||||
{t('settings:hardware.freeOf')}{' '}
|
|
||||||
{formatMegaBytes(gpu.total_memory)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<CardItem
|
|
||||||
title={t('settings:hardware.driverVersion')}
|
|
||||||
actions={
|
|
||||||
<span className="text-main-view-fg/80">
|
|
||||||
{gpu.driver_version?.slice(0, 50) || '-'}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<CardItem
|
|
||||||
title={t('settings:hardware.computeCapability')}
|
|
||||||
actions={
|
|
||||||
<span className="text-main-view-fg/80">
|
|
||||||
{gpu.nvidia_info?.compute_capability ??
|
|
||||||
gpu.vulkan_info?.api_version}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Hardware() {
|
function Hardware() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
hardwareData,
|
hardwareData,
|
||||||
systemUsage,
|
systemUsage,
|
||||||
setHardwareData,
|
setHardwareData,
|
||||||
updateHardwareDataPreservingGpuOrder,
|
|
||||||
updateSystemUsage,
|
updateSystemUsage,
|
||||||
reorderGPUs,
|
|
||||||
pollingPaused,
|
pollingPaused,
|
||||||
} = useHardware()
|
} = useHardware()
|
||||||
// const { vulkanEnabled, setVulkanEnabled } = useVulkan()
|
|
||||||
|
|
||||||
const { providers } = useModelProvider()
|
const { providers } = useModelProvider()
|
||||||
const llamacpp = providers.find((p) => p.provider === 'llamacpp')
|
const llamacpp = providers.find((p) => p.provider === 'llamacpp')
|
||||||
const versionBackend = llamacpp?.settings.find((s) => s.key === "version_backend")?.controller_props.value
|
|
||||||
|
|
||||||
// Determine backend type and filter GPUs accordingly
|
// Llamacpp devices hook
|
||||||
const isCudaBackend = typeof versionBackend === 'string' && versionBackend.includes('cuda')
|
const {
|
||||||
const isVulkanBackend = typeof versionBackend === 'string' && versionBackend.includes('vulkan')
|
devices: llamacppDevices,
|
||||||
|
loading: llamacppDevicesLoading,
|
||||||
// Filter and prepare GPUs based on backend
|
error: llamacppDevicesError,
|
||||||
const getFilteredGPUs = () => {
|
activatedDevices,
|
||||||
// Always show all GPUs, but compatibility will be determined by isGPUActive
|
toggleDevice,
|
||||||
return hardwareData.gpus
|
fetchDevices,
|
||||||
}
|
} = useLlamacppDevices()
|
||||||
|
|
||||||
const filteredGPUs = getFilteredGPUs()
|
|
||||||
|
|
||||||
// Check if GPU should be active based on backend compatibility
|
|
||||||
const isGPUCompatible = (gpu: GPU) => {
|
|
||||||
if (isCudaBackend) {
|
|
||||||
return gpu.nvidia_info !== null
|
|
||||||
} else if (isVulkanBackend) {
|
|
||||||
return gpu.vulkan_info !== null
|
|
||||||
} else {
|
|
||||||
// No valid backend - all GPUs are inactive
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if GPU is actually activated
|
|
||||||
const isGPUActive = (gpu: GPU) => {
|
|
||||||
return isGPUCompatible(gpu) && (gpu.activated ?? false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Fetch llamacpp devices when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getHardwareInfo().then((freshData) => {
|
fetchDevices()
|
||||||
const data = freshData as unknown as HardwareData
|
}, [fetchDevices])
|
||||||
updateHardwareDataPreservingGpuOrder(data)
|
|
||||||
})
|
|
||||||
}, [updateHardwareDataPreservingGpuOrder])
|
|
||||||
|
|
||||||
// Hardware and provider sync logic
|
const { getProviderByName } = useModelProvider()
|
||||||
const { getActivatedDeviceString, updateGPUActivationFromDeviceString } = useHardware()
|
|
||||||
const { updateProvider, getProviderByName } = useModelProvider()
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
|
||||||
|
|
||||||
// Initialize GPU activations from device setting on first load
|
// Initialize llamacpp device activations from provider settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hardwareData.gpus.length > 0 && !isInitialized) {
|
if (llamacppDevices.length > 0 && activatedDevices.size === 0) {
|
||||||
const llamacppProvider = getProviderByName('llamacpp')
|
const llamacppProvider = getProviderByName('llamacpp')
|
||||||
const currentDeviceSetting = llamacppProvider?.settings.find(s => s.key === 'device')?.controller_props.value as string
|
const currentDeviceSetting = llamacppProvider?.settings.find(
|
||||||
|
(s) => s.key === 'device'
|
||||||
|
)?.controller_props.value as string
|
||||||
|
|
||||||
if (currentDeviceSetting) {
|
if (currentDeviceSetting) {
|
||||||
console.log(`Initializing GPU activations from device setting: "${currentDeviceSetting}"`)
|
const deviceIds = currentDeviceSetting
|
||||||
updateGPUActivationFromDeviceString(currentDeviceSetting)
|
.split(',')
|
||||||
}
|
.map((device) => device.trim())
|
||||||
|
.filter((device) => device.length > 0)
|
||||||
|
|
||||||
setIsInitialized(true)
|
// Find matching devices by ID
|
||||||
}
|
const matchingDeviceIds = deviceIds.filter((deviceId) =>
|
||||||
}, [hardwareData.gpus.length, isInitialized, getProviderByName, updateGPUActivationFromDeviceString])
|
llamacppDevices.some((device) => device.id === deviceId)
|
||||||
|
)
|
||||||
|
|
||||||
// Sync device setting when GPU activations change (only after initialization)
|
if (matchingDeviceIds.length > 0) {
|
||||||
const gpuActivationStates = hardwareData.gpus.map(gpu => gpu.activated)
|
console.log(
|
||||||
|
`Initializing llamacpp device activations from device setting: "${currentDeviceSetting}"`
|
||||||
useEffect(() => {
|
)
|
||||||
if (isInitialized && hardwareData.gpus.length > 0) {
|
// Update the activatedDevices in the hook
|
||||||
const llamacppProvider = getProviderByName('llamacpp')
|
const { setActivatedDevices } = useLlamacppDevices.getState()
|
||||||
const backendType = llamacppProvider?.settings.find(s => s.key === 'version_backend')?.controller_props.value as string
|
setActivatedDevices(matchingDeviceIds)
|
||||||
const deviceString = getActivatedDeviceString(backendType)
|
|
||||||
|
|
||||||
if (llamacppProvider) {
|
|
||||||
const currentDeviceSetting = llamacppProvider.settings.find(s => s.key === 'device')
|
|
||||||
|
|
||||||
// Sync device string when GPU activations change (only after initialization)
|
|
||||||
if (currentDeviceSetting && currentDeviceSetting.controller_props.value !== deviceString) {
|
|
||||||
console.log(`Syncing device string from "${currentDeviceSetting.controller_props.value}" to "${deviceString}"`)
|
|
||||||
|
|
||||||
const updatedSettings = llamacppProvider.settings.map(setting => {
|
|
||||||
if (setting.key === 'device') {
|
|
||||||
return {
|
|
||||||
...setting,
|
|
||||||
controller_props: {
|
|
||||||
...setting.controller_props,
|
|
||||||
value: deviceString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return setting
|
|
||||||
})
|
|
||||||
|
|
||||||
updateProvider('llamacpp', {
|
|
||||||
settings: updatedSettings
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isInitialized, gpuActivationStates, versionBackend, getActivatedDeviceString, updateProvider, getProviderByName, hardwareData.gpus.length])
|
}, [
|
||||||
|
llamacppDevices.length,
|
||||||
// Set up DnD sensors
|
activatedDevices.size,
|
||||||
const sensors = useSensors(
|
getProviderByName,
|
||||||
useSensor(PointerSensor),
|
llamacppDevices,
|
||||||
useSensor(KeyboardSensor)
|
])
|
||||||
)
|
|
||||||
|
|
||||||
// Handle drag end event
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event
|
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
|
||||||
// Find the actual indices in the original hardwareData.gpus array
|
|
||||||
const activeGpu = filteredGPUs[active.id as number]
|
|
||||||
const overGpu = filteredGPUs[over.id as number]
|
|
||||||
|
|
||||||
const oldIndex = hardwareData.gpus.findIndex(gpu => gpu.uuid === activeGpu.uuid)
|
|
||||||
const newIndex = hardwareData.gpus.findIndex(gpu => gpu.uuid === overGpu.uuid)
|
|
||||||
|
|
||||||
if (oldIndex !== -1 && newIndex !== -1) {
|
|
||||||
reorderGPUs(oldIndex, newIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pollingPaused) return
|
if (pollingPaused) return
|
||||||
@ -452,64 +281,64 @@ function Hardware() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Vulkan Settings */}
|
{/* Llamacpp Devices Information */}
|
||||||
{/* {hardwareData.gpus.length > 0 && (
|
{!IS_MACOS && llamacpp && (
|
||||||
<Card title={t('settings:hardware.vulkan')}>
|
<Card title="GPUs">
|
||||||
<CardItem
|
{llamacppDevicesLoading ? (
|
||||||
title={t('settings:hardware.enableVulkan')}
|
<CardItem title="Loading devices..." actions={<></>} />
|
||||||
description={t('settings:hardware.enableVulkanDesc')}
|
) : llamacppDevicesError ? (
|
||||||
actions={
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Switch
|
|
||||||
checked={vulkanEnabled}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setVulkanEnabled(checked)
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload()
|
|
||||||
}, 500) // Reload after 500ms to apply changes
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* GPU Information */}
|
|
||||||
{!IS_MACOS ? (
|
|
||||||
<Card title={t('settings:hardware.gpus')}>
|
|
||||||
|
|
||||||
|
|
||||||
{hardwareData.gpus.length > 0 ? (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredGPUs.map((_, index) => index)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
{filteredGPUs.map((gpu, index) => (
|
|
||||||
<SortableGPUItem
|
|
||||||
key={index}
|
|
||||||
gpu={gpu}
|
|
||||||
index={index}
|
|
||||||
isCompatible={isGPUCompatible(gpu)}
|
|
||||||
isActivated={isGPUActive(gpu)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
) : (
|
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:hardware.noGpus')}
|
title="Error loading devices"
|
||||||
actions={<></>}
|
actions={
|
||||||
|
<span className="text-destructive text-sm">
|
||||||
|
{llamacppDevicesError}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
) : llamacppDevices.length > 0 ? (
|
||||||
|
llamacppDevices.map((device, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardItem
|
||||||
|
title={device.name}
|
||||||
|
actions={
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* <div className="flex flex-col items-end gap-1">
|
||||||
|
<span className="text-main-view-fg/80 text-sm">
|
||||||
|
ID: {device.id}
|
||||||
|
</span>
|
||||||
|
<span className="text-main-view-fg/80 text-sm">
|
||||||
|
Memory: {formatMegaBytes(device.mem)} /{' '}
|
||||||
|
{formatMegaBytes(device.free)} free
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<Switch
|
||||||
|
checked={activatedDevices.has(device.id)}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
toggleDevice(device.id)
|
||||||
|
stopAllModels()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-3">
|
||||||
|
<CardItem
|
||||||
|
title={t('settings:hardware.vram')}
|
||||||
|
actions={
|
||||||
|
<span className="text-main-view-fg/80">
|
||||||
|
{formatMegaBytes(device.mem)}{' '}
|
||||||
|
{t('settings:hardware.freeOf')}{' '}
|
||||||
|
{formatMegaBytes(device.free)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<CardItem title="No devices found" actions={<></>} />
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { Card, CardItem } from '@/containers/Card'
|
|||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import SettingsMenu from '@/containers/SettingsMenu'
|
import SettingsMenu from '@/containers/SettingsMenu'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import { useHardware } from '@/hooks/useHardware'
|
|
||||||
import { cn, getProviderTitle } from '@/lib/utils'
|
import { cn, getProviderTitle } from '@/lib/utils'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import {
|
import {
|
||||||
@ -39,6 +38,7 @@ import { toast } from 'sonner'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { predefinedProviders } from '@/consts/providers'
|
import { predefinedProviders } from '@/consts/providers'
|
||||||
import { useModelLoad } from '@/hooks/useModelLoad'
|
import { useModelLoad } from '@/hooks/useModelLoad'
|
||||||
|
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/settings/providers/$providerName')({
|
export const Route = createFileRoute('/settings/providers/$providerName')({
|
||||||
@ -80,7 +80,6 @@ function ProviderDetail() {
|
|||||||
const [refreshingModels, setRefreshingModels] = useState(false)
|
const [refreshingModels, setRefreshingModels] = useState(false)
|
||||||
const { providerName } = useParams({ from: Route.id })
|
const { providerName } = useParams({ from: Route.id })
|
||||||
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
|
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
|
||||||
const { updateGPUActivationFromDeviceString } = useHardware()
|
|
||||||
const provider = getProviderByName(providerName)
|
const provider = getProviderByName(providerName)
|
||||||
const isSetup = step === 'setup_remote_provider'
|
const isSetup = step === 'setup_remote_provider'
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -256,7 +255,8 @@ function ProviderDetail() {
|
|||||||
controllerProps={setting.controller_props}
|
controllerProps={setting.controller_props}
|
||||||
className={cn(
|
className={cn(
|
||||||
setting.key === 'api-key' &&
|
setting.key === 'api-key' &&
|
||||||
'third-step-setup-remote-provider'
|
'third-step-setup-remote-provider',
|
||||||
|
setting.key === 'device' && 'hidden'
|
||||||
)}
|
)}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@ -288,16 +288,28 @@ function ProviderDetail() {
|
|||||||
updateObj.base_url = newValue
|
updateObj.base_url = newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for device setting changes
|
// Reset device setting to empty when backend version changes
|
||||||
if (
|
if (settingKey === 'version_backend') {
|
||||||
settingKey === 'device' &&
|
const deviceSettingIndex =
|
||||||
typeof newValue === 'string' &&
|
newSettings.findIndex(
|
||||||
provider.provider === 'llamacpp'
|
(s) => s.key === 'device'
|
||||||
) {
|
)
|
||||||
console.log(
|
|
||||||
`Device setting manually changed to: "${newValue}"`
|
if (deviceSettingIndex !== -1) {
|
||||||
)
|
;(
|
||||||
updateGPUActivationFromDeviceString(newValue)
|
newSettings[deviceSettingIndex]
|
||||||
|
.controller_props as {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
).value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset llamacpp device activations when backend version changes
|
||||||
|
if (providerName === 'llamacpp') {
|
||||||
|
const { setActivatedDevices } =
|
||||||
|
useLlamacppDevices.getState()
|
||||||
|
setActivatedDevices([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(
|
updateSettings(
|
||||||
|
|||||||
@ -2,16 +2,13 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useHardware } from '@/hooks/useHardware'
|
import { useHardware } from '@/hooks/useHardware'
|
||||||
import { getHardwareInfo, getSystemUsage } from '@/services/hardware'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import type { HardwareData } from '@/hooks/useHardware'
|
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
import { formatMegaBytes } from '@/lib/utils'
|
import { formatMegaBytes } from '@/lib/utils'
|
||||||
import { IconDeviceDesktopAnalytics } from '@tabler/icons-react'
|
import { IconDeviceDesktopAnalytics } from '@tabler/icons-react'
|
||||||
import { getActiveModels, stopModel } from '@/services/models'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { toNumber } from '@/utils/number'
|
import { toNumber } from '@/utils/number'
|
||||||
|
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
|
||||||
export const Route = createFileRoute(route.systemMonitor as any)({
|
export const Route = createFileRoute(route.systemMonitor as any)({
|
||||||
@ -20,126 +17,66 @@ export const Route = createFileRoute(route.systemMonitor as any)({
|
|||||||
|
|
||||||
function SystemMonitor() {
|
function SystemMonitor() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { hardwareData, systemUsage, updateSystemUsage } = useHardware()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hardwareData,
|
devices: llamacppDevices,
|
||||||
systemUsage,
|
activatedDevices,
|
||||||
updateHardwareDataPreservingGpuOrder,
|
fetchDevices,
|
||||||
updateSystemUsage,
|
setActivatedDevices,
|
||||||
updateGPUActivationFromDeviceString,
|
} = useLlamacppDevices()
|
||||||
} = useHardware()
|
const { getProviderByName } = useModelProvider()
|
||||||
const [activeModels, setActiveModels] = useState<string[]>([])
|
|
||||||
const { providers, getProviderByName } = useModelProvider()
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
// Determine backend type and filter GPUs accordingly (same logic as hardware.tsx)
|
|
||||||
const llamacpp = providers.find((p) => p.provider === 'llamacpp')
|
|
||||||
const versionBackend = llamacpp?.settings.find(
|
|
||||||
(s) => s.key === 'version_backend'
|
|
||||||
)?.controller_props.value
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initial data fetch - use updateHardwareDataPreservingGpuOrder like hardware.tsx
|
// Fetch llamacpp devices
|
||||||
getHardwareInfo().then((data) => {
|
fetchDevices()
|
||||||
updateHardwareDataPreservingGpuOrder(data as unknown as HardwareData)
|
}, [updateSystemUsage, fetchDevices])
|
||||||
})
|
|
||||||
getActiveModels().then((models) => setActiveModels(models || []))
|
|
||||||
|
|
||||||
// Set up interval for real-time updates
|
// Initialize when hardware data and llamacpp devices are available
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
getSystemUsage().then((data) => {
|
|
||||||
updateSystemUsage(data)
|
|
||||||
})
|
|
||||||
getActiveModels().then((models) => setActiveModels(models || []))
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId)
|
|
||||||
}, [updateHardwareDataPreservingGpuOrder, setActiveModels, updateSystemUsage])
|
|
||||||
|
|
||||||
// Initialize GPU activations from device setting on first load (same logic as hardware.tsx)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hardwareData.gpus.length > 0 && !isInitialized) {
|
if (hardwareData.gpus.length > 0 && !isInitialized) {
|
||||||
|
setIsInitialized(true)
|
||||||
|
}
|
||||||
|
}, [hardwareData.gpus.length, isInitialized])
|
||||||
|
|
||||||
|
// Initialize llamacpp device activations from provider settings
|
||||||
|
useEffect(() => {
|
||||||
|
if (llamacppDevices.length > 0 && activatedDevices.size === 0) {
|
||||||
const llamacppProvider = getProviderByName('llamacpp')
|
const llamacppProvider = getProviderByName('llamacpp')
|
||||||
const currentDeviceSetting = llamacppProvider?.settings.find(
|
const currentDeviceSetting = llamacppProvider?.settings.find(
|
||||||
(s) => s.key === 'device'
|
(s) => s.key === 'device'
|
||||||
)?.controller_props.value as string
|
)?.controller_props.value as string
|
||||||
|
|
||||||
if (currentDeviceSetting) {
|
if (currentDeviceSetting) {
|
||||||
updateGPUActivationFromDeviceString(currentDeviceSetting)
|
const deviceIds = currentDeviceSetting
|
||||||
}
|
.split(',')
|
||||||
|
.map((device) => device.trim())
|
||||||
|
.filter((device) => device.length > 0)
|
||||||
|
|
||||||
setIsInitialized(true)
|
// Find matching devices by ID
|
||||||
}
|
const matchingDeviceIds = deviceIds.filter((deviceId) =>
|
||||||
}, [
|
llamacppDevices.some((device) => device.id === deviceId)
|
||||||
hardwareData.gpus.length,
|
|
||||||
isInitialized,
|
|
||||||
getProviderByName,
|
|
||||||
updateGPUActivationFromDeviceString,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Sync device setting when GPU activations change (only after initialization) - same logic as hardware.tsx
|
|
||||||
const { getActivatedDeviceString } = useHardware()
|
|
||||||
const { updateProvider } = useModelProvider()
|
|
||||||
const gpuActivationStates = hardwareData.gpus.map((gpu) => gpu.activated)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialized && hardwareData.gpus.length > 0) {
|
|
||||||
const llamacppProvider = getProviderByName('llamacpp')
|
|
||||||
const backendType = llamacppProvider?.settings.find(
|
|
||||||
(s) => s.key === 'version_backend'
|
|
||||||
)?.controller_props.value as string
|
|
||||||
const deviceString = getActivatedDeviceString(backendType)
|
|
||||||
|
|
||||||
if (llamacppProvider) {
|
|
||||||
const currentDeviceSetting = llamacppProvider.settings.find(
|
|
||||||
(s) => s.key === 'device'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync device string when GPU activations change (only after initialization)
|
if (matchingDeviceIds.length > 0) {
|
||||||
if (
|
console.log(
|
||||||
currentDeviceSetting &&
|
`Initializing llamacpp device activations from device setting: "${currentDeviceSetting}"`
|
||||||
currentDeviceSetting.controller_props.value !== deviceString
|
)
|
||||||
) {
|
// Update the activatedDevices in the hook
|
||||||
const updatedSettings = llamacppProvider.settings.map((setting) => {
|
setActivatedDevices(matchingDeviceIds)
|
||||||
if (setting.key === 'device') {
|
|
||||||
return {
|
|
||||||
...setting,
|
|
||||||
controller_props: {
|
|
||||||
...setting.controller_props,
|
|
||||||
value: deviceString,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return setting
|
|
||||||
})
|
|
||||||
|
|
||||||
updateProvider('llamacpp', {
|
|
||||||
settings: updatedSettings,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isInitialized,
|
llamacppDevices.length,
|
||||||
gpuActivationStates,
|
activatedDevices.size,
|
||||||
versionBackend,
|
|
||||||
getActivatedDeviceString,
|
|
||||||
updateProvider,
|
|
||||||
getProviderByName,
|
getProviderByName,
|
||||||
hardwareData.gpus.length,
|
llamacppDevices,
|
||||||
|
setActivatedDevices,
|
||||||
])
|
])
|
||||||
|
|
||||||
const stopRunningModel = (modelId: string) => {
|
|
||||||
stopModel(modelId)
|
|
||||||
.then(() => {
|
|
||||||
setActiveModels((prevModels) =>
|
|
||||||
prevModels.filter((model) => model !== modelId)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error stopping model:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate RAM usage percentage
|
// Calculate RAM usage percentage
|
||||||
const ramUsagePercentage =
|
const ramUsagePercentage =
|
||||||
toNumber(
|
toNumber(
|
||||||
@ -147,35 +84,6 @@ function SystemMonitor() {
|
|||||||
hardwareData.total_memory
|
hardwareData.total_memory
|
||||||
) * 100
|
) * 100
|
||||||
|
|
||||||
// Determine backend type and filter GPUs accordingly
|
|
||||||
const isCudaBackend =
|
|
||||||
typeof versionBackend === 'string' && versionBackend.includes('cuda')
|
|
||||||
const isVulkanBackend =
|
|
||||||
typeof versionBackend === 'string' && versionBackend.includes('vulkan')
|
|
||||||
|
|
||||||
// Check if GPU should be active based on backend compatibility
|
|
||||||
const isGPUCompatible = (gpu: any) => {
|
|
||||||
if (isCudaBackend) {
|
|
||||||
return gpu.nvidia_info !== null
|
|
||||||
} else if (isVulkanBackend) {
|
|
||||||
return gpu.vulkan_info !== null
|
|
||||||
} else {
|
|
||||||
// No valid backend - all GPUs are inactive
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if GPU is actually activated
|
|
||||||
const isGPUActive = (gpu: any) => {
|
|
||||||
const compatible = isGPUCompatible(gpu)
|
|
||||||
const activated = gpu.activated ?? false
|
|
||||||
const result = compatible && activated
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to show only active GPUs
|
|
||||||
const activeGPUs = hardwareData.gpus.filter((gpu) => isGPUActive(gpu))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-main-view overflow-y-auto p-6">
|
<div className="flex flex-col h-full bg-main-view overflow-y-auto p-6">
|
||||||
<div className="flex items-center mb-4 gap-2">
|
<div className="flex items-center mb-4 gap-2">
|
||||||
@ -185,7 +93,7 @@ function SystemMonitor() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{/* CPU Usage Card */}
|
{/* CPU Usage Card */}
|
||||||
<div className="bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
|
<div className="bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
|
||||||
<h2 className="text-base font-semibold text-main-view-fg mb-4">
|
<h2 className="text-base font-semibold text-main-view-fg mb-4">
|
||||||
@ -273,150 +181,51 @@ function SystemMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Active Model Section */}
|
{/* GPU Usage Card */}
|
||||||
<div className="mt-6 bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
|
<div className="bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
|
||||||
<h2 className="text-base font-semibold text-main-view-fg mb-4">
|
<h2 className="text-base font-semibold text-main-view-fg mb-4">
|
||||||
{t('system-monitor:runningModels')}
|
{t('system-monitor:activeGpus')}
|
||||||
</h2>
|
</h2>
|
||||||
{activeModels.length === 0 && (
|
<div className="flex flex-col gap-2">
|
||||||
<div className="text-center text-main-view-fg/50 py-4">
|
{llamacppDevices.length > 0 ? (
|
||||||
{t('system-monitor:noRunningModels')}
|
llamacppDevices.map((device) => (
|
||||||
</div>
|
<div key={device.id} className="flex flex-col gap-1">
|
||||||
)}
|
|
||||||
{activeModels.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{activeModels.map((model) => (
|
|
||||||
<div className="bg-main-view-fg/3 rounded-lg p-4" key={model}>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="font-semibold text-main-view-fg">
|
|
||||||
{model}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 mt-3">
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-main-view-fg/70">
|
<span className="text-main-view-fg/70">{device.name}</span>
|
||||||
{t('system-monitor:provider')}
|
<span
|
||||||
|
className={`text-sm px-2 py-1 rounded-md ${
|
||||||
|
activatedDevices.has(device.id)
|
||||||
|
? 'bg-green-500/20 text-green-600 dark:text-green-400'
|
||||||
|
: 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activatedDevices.has(device.id)
|
||||||
|
? t('system-monitor:active')
|
||||||
|
: 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-main-view-fg">llama.cpp</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center text-sm">
|
||||||
<span className="text-main-view-fg/70">
|
<span className="text-main-view-fg/70">VRAM:</span>
|
||||||
{t('system-monitor:uptime')}
|
|
||||||
</span>
|
|
||||||
{/* <span className="text-main-view-fg">
|
|
||||||
{model.start_time && formatDuration(model.start_time)}
|
|
||||||
</span> */}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-main-view-fg/70">
|
|
||||||
{t('system-monitor:actions')}
|
|
||||||
</span>
|
|
||||||
<span className="text-main-view-fg">
|
<span className="text-main-view-fg">
|
||||||
<Button
|
{formatMegaBytes(device.mem)}
|
||||||
variant="destructive"
|
</span>
|
||||||
size="sm"
|
</div>
|
||||||
onClick={() => stopRunningModel(model)}
|
<div className="flex justify-between items-center text-sm">
|
||||||
>
|
<span className="text-main-view-fg/70">Free:</span>
|
||||||
{t('system-monitor:stop')}
|
<span className="text-main-view-fg">
|
||||||
</Button>
|
{formatMegaBytes(device.free)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-main-view-fg/70 text-center py-4">
|
||||||
|
{t('system-monitor:noGpus')}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active GPUs Section */}
|
|
||||||
<div className="mt-6 bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
|
|
||||||
<h2 className="text-base font-semibold text-main-view-fg mb-4">
|
|
||||||
{t('system-monitor:activeGpus')}
|
|
||||||
</h2>
|
|
||||||
{!isInitialized ? (
|
|
||||||
<div className="text-center text-main-view-fg/50 py-4">
|
|
||||||
Initializing GPU states...
|
|
||||||
</div>
|
|
||||||
) : activeGPUs.length > 0 ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{activeGPUs.map((gpu, index) => {
|
|
||||||
// Find the corresponding system usage data for this GPU
|
|
||||||
const gpuUsage = systemUsage.gpus.find(
|
|
||||||
(usage) => usage.uuid === gpu.uuid
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={gpu.uuid || index}
|
|
||||||
className="bg-main-view-fg/3 rounded-lg p-4"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="font-semibold text-main-view-fg">
|
|
||||||
{gpu.name}
|
|
||||||
</span>
|
|
||||||
<div className="bg-green-500/20 px-2 py-1 rounded-sm">
|
|
||||||
{t('system-monitor:active')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 mt-3">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-main-view-fg/70">
|
|
||||||
{t('system-monitor:vramUsage')}
|
|
||||||
</span>
|
|
||||||
<span className="text-main-view-fg">
|
|
||||||
{gpuUsage ? (
|
|
||||||
<>
|
|
||||||
{formatMegaBytes(gpuUsage.used_memory)} /{' '}
|
|
||||||
{formatMegaBytes(gpu.total_memory)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{formatMegaBytes(0)} /{' '}
|
|
||||||
{formatMegaBytes(gpu.total_memory)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-main-view-fg/70">
|
|
||||||
{t('system-monitor:driverVersion')}
|
|
||||||
</span>
|
|
||||||
<span className="text-main-view-fg">
|
|
||||||
{gpu.driver_version || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-main-view-fg/70">
|
|
||||||
{t('system-monitor:computeCapability')}
|
|
||||||
</span>
|
|
||||||
<span className="text-main-view-fg">
|
|
||||||
{gpu.nvidia_info?.compute_capability ||
|
|
||||||
gpu.vulkan_info?.api_version ||
|
|
||||||
'-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<Progress
|
|
||||||
value={
|
|
||||||
gpuUsage
|
|
||||||
? (gpuUsage.used_memory / gpu.total_memory) * 100
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
className="h-2 w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-main-view-fg/50 py-4">
|
|
||||||
{t('system-monitor:noGpus')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { HardwareData, SystemUsage } from '@/hooks/useHardware'
|
import { HardwareData, SystemUsage } from '@/hooks/useHardware'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
// Device list interface for llamacpp extension
|
||||||
|
export interface DeviceList {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
mem: number
|
||||||
|
free: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get hardware information from the HardwareManagementExtension.
|
* Get hardware information from the HardwareManagementExtension.
|
||||||
* @returns {Promise<HardwareInfo>} A promise that resolves to the hardware information.
|
* @returns {Promise<HardwareInfo>} A promise that resolves to the hardware information.
|
||||||
@ -17,6 +25,21 @@ export const getSystemUsage = async () => {
|
|||||||
return invoke('get_system_usage') as Promise<SystemUsage>
|
return invoke('get_system_usage') as Promise<SystemUsage>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get devices from the llamacpp extension.
|
||||||
|
* @returns {Promise<DeviceList[]>} A promise that resolves to the list of available devices.
|
||||||
|
*/
|
||||||
|
export const getLlamacppDevices = async (): Promise<DeviceList[]> => {
|
||||||
|
const extensionManager = window.core.extensionManager
|
||||||
|
const llamacppExtension = extensionManager.getByName('@janhq/llamacpp-extension')
|
||||||
|
|
||||||
|
if (!llamacppExtension) {
|
||||||
|
throw new Error('llamacpp extension not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return llamacppExtension.getDevices()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set gpus activate
|
* Set gpus activate
|
||||||
* @returns A Promise that resolves set gpus activate.
|
* @returns A Promise that resolves set gpus activate.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user