fix: gpu hardware state (#4650)

* fix: gpu hardware state

* chore: cleanup state

* chore: clear state
This commit is contained in:
Faisal Amir 2025-02-14 11:21:18 +07:00 committed by GitHub
parent 7a6890bd7f
commit dde260e723
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 186 additions and 142 deletions

View File

@ -509,61 +509,61 @@ __metadata:
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=2e50a2&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=429572&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/92904b534f4f82c31effd9eade0f8685b87165e28f0c3ba3a0c18a6a0a21bf21ee769a98c562c4ba7488ceb1238e2dacce2bb2f729c1bdd84ad44054fb985da0 checksum: 10c0/18b8ca8795ccf2c89e2225668ce2824e4ce7b32872764a739155d2b68a773676188186d721e70dbcf2605bb28697b97d6c01348d34bc440e829087ebd6a3158c
languageName: node languageName: node
linkType: hard linkType: hard

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd' import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { Gpu } from '@janhq/core'
import { Progress, ScrollArea, Switch } from '@janhq/joi' import { Progress, ScrollArea, Switch } from '@janhq/joi'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from 'jotai/utils'
@ -20,19 +20,24 @@ import {
import { toGibibytes } from '@/utils/converter' import { toGibibytes } from '@/utils/converter'
import { utilizedMemory } from '@/utils/memory'
import { import {
cpuUsageAtom, cpuUsageAtom,
ramUtilitizedAtom, ramUtilitizedAtom,
totalRamAtom, totalRamAtom,
usedRamAtom, usedRamAtom,
gpusAtom,
} from '@/helpers/atoms/SystemBar.atom' } from '@/helpers/atoms/SystemBar.atom'
const gpusAtom = atomWithStorage<Gpu[]>('gpus', [], undefined, { const orderGpusAtom = atomWithStorage<any>('orderGpus', [], undefined, {
getOnInit: true, getOnInit: true,
}) })
const Hardware = () => { const Hardware = () => {
const { hardware } = useGetHardwareInfo() const { hardware, mutate } = useGetHardwareInfo()
const [isActivatingGpu, setIsActivatingGpu] = useState<Set<string>>(new Set())
const [openPanels, setOpenPanels] = useState<Record<number, boolean>>({}) const [openPanels, setOpenPanels] = useState<Record<number, boolean>>({})
const cpuUsage = useAtomValue(cpuUsageAtom) const cpuUsage = useAtomValue(cpuUsageAtom)
@ -40,7 +45,9 @@ const Hardware = () => {
const usedRam = useAtomValue(usedRamAtom) const usedRam = useAtomValue(usedRamAtom)
const ramUtilitized = useAtomValue(ramUtilitizedAtom) const ramUtilitized = useAtomValue(ramUtilitizedAtom)
const [gpus, setGpus] = useAtom<Gpu[]>(gpusAtom) const [gpus, setGpus] = useAtom(gpusAtom)
const [orderGpus, setOrderGpus] = useAtom(orderGpusAtom)
const togglePanel = (index: number) => { const togglePanel = (index: number) => {
setOpenPanels((prev) => ({ setOpenPanels((prev) => ({
@ -51,16 +58,18 @@ const Hardware = () => {
// Handle switch toggle for GPU activation // Handle switch toggle for GPU activation
const handleSwitchChange = async (id: string, isActive: boolean) => { const handleSwitchChange = async (id: string, isActive: boolean) => {
setIsActivatingGpu((prev) => new Set(prev).add(id))
const updatedGpus = gpus.map((gpu) => const updatedGpus = gpus.map((gpu) =>
gpu.id === id ? { ...gpu, activated: isActive } : gpu gpu.id === id ? { ...gpu, activated: isActive } : gpu
) )
setGpus(updatedGpus)
// Call the API to update the active GPUs // Call the API to update the active GPUs
try { try {
const activeGpuIds = updatedGpus const activeGpuIds = updatedGpus
.filter((gpu) => gpu.activated) .filter((gpu: any) => gpu.activated)
.map((gpu) => Number(gpu.id)) .map((gpu: any) => Number(gpu.id))
await setActiveGpus({ gpus: activeGpuIds }) await setActiveGpus({ gpus: activeGpuIds })
mutate()
} catch (error) { } catch (error) {
console.error('Failed to update active GPUs:', error) console.error('Failed to update active GPUs:', error)
} }
@ -71,23 +80,36 @@ const Hardware = () => {
const reorderedGpus = Array.from(gpus) const reorderedGpus = Array.from(gpus)
const [movedGpu] = reorderedGpus.splice(result.source.index, 1) const [movedGpu] = reorderedGpus.splice(result.source.index, 1)
reorderedGpus.splice(result.destination.index, 0, movedGpu) reorderedGpus.splice(result.destination.index, 0, movedGpu)
setGpus(reorderedGpus) // Update the atom, which persists to localStorage
setGpus(reorderedGpus)
setOrderGpus(reorderedGpus.map((gpu) => gpu.id))
} }
React.useEffect(() => { React.useEffect(() => {
if (hardware?.gpus) { if (hardware?.gpus) {
setGpus((prevGpus) => { setGpus((prevGpus: any) => {
// Create a map of existing GPUs by UUID for quick lookup // Create a map of existing GPUs by UUID for quick lookup
const gpuMap = new Map(prevGpus.map((gpu) => [gpu.uuid, gpu])) const gpuMap = new Map(prevGpus.map((gpu: any) => [gpu.uuid, gpu]))
// Update existing GPUs or add new ones // Update existing GPUs or add new ones
const updatedGpus = hardware.gpus.map((newGpu) => { const updatedGpus = hardware.gpus.map((newGpu) => {
const existingGpu = gpuMap.get(newGpu.uuid) const existingGpu: any = gpuMap.get(newGpu.uuid)
if (existingGpu) { if (existingGpu) {
// Update the GPU properties while keeping the original order // Update the GPU properties while keeping the original order
if (existingGpu.activated !== newGpu.activated) {
setIsActivatingGpu((prev) => {
const updated = new Set(prev)
updated.delete(existingGpu.id)
updated.clear()
return updated
})
}
return { return {
...existingGpu, ...existingGpu,
activated: newGpu.activated,
free_vram: newGpu.free_vram, free_vram: newGpu.free_vram,
total_vram: newGpu.total_vram, total_vram: newGpu.total_vram,
} }
@ -100,7 +122,8 @@ const Hardware = () => {
// Append GPUs from the previous state that are not in the hardware.gpus // Append GPUs from the previous state that are not in the hardware.gpus
// This preserves user-reordered GPUs that aren't present in the new data // This preserves user-reordered GPUs that aren't present in the new data
const remainingGpus = prevGpus.filter( const remainingGpus = prevGpus.filter(
(prevGpu) => !hardware.gpus?.some((gpu) => gpu.uuid === prevGpu.uuid) (prevGpu: any) =>
!hardware.gpus?.some((gpu) => gpu.uuid === prevGpu.uuid)
) )
return [...updatedGpus, ...remainingGpus] return [...updatedGpus, ...remainingGpus]
@ -202,133 +225,154 @@ const Hardware = () => {
ref={provided.innerRef} ref={provided.innerRef}
className="mt-4" className="mt-4"
> >
{gpus.map((item, i) => ( {gpus
<Draggable key={i} draggableId={String(i)} index={i}> .sort((a, b) => {
{(provided, snapshot) => ( const orderA = orderGpus.indexOf(a.id)
<div const orderB = orderGpus.indexOf(b.id)
ref={provided.innerRef} return orderA - orderB
{...provided.draggableProps} })
{...provided.dragHandleProps} .map((item: any, i) => {
className={twMerge( const gpuUtilization = utilizedMemory(
'cursor-pointer border border-[hsla(var(--app-border))] bg-[hsla(var(--tertiary-bg))] p-4 first:rounded-t-lg last:rounded-b-lg', item.free_vram,
gpus.length > 1 && 'last:rounded-t-none', item.total_vram
snapshot.isDragging )
? 'border-b' const isLoading = isActivatingGpu.has(item.id)
: 'border-b-0 last:border-b'
)} return (
onClick={() => togglePanel(i)} <Draggable
key={i}
draggableId={String(i)}
index={i}
> >
<div className="flex flex-col items-start justify-start gap-4 sm:flex-row sm:items-center sm:justify-between"> {(provided, snapshot) => (
<div className="flex w-full items-center justify-between"> <div
<div className="flex h-full flex-shrink-0 items-center gap-2"> ref={provided.innerRef}
<GripVerticalIcon {...provided.draggableProps}
size={14} {...provided.dragHandleProps}
className="text-[hsla(var(--text-tertiary))]" className={twMerge(
/> 'cursor-pointer border border-[hsla(var(--app-border))] bg-[hsla(var(--tertiary-bg))] p-4 first:rounded-t-lg last:rounded-b-lg',
<div gpus.length > 1 && 'last:rounded-t-none',
className={twMerge( snapshot.isDragging
'h-2 w-2 rounded-full', ? 'border-b'
item.activated : 'border-b-0 last:border-b'
? 'bg-green-400' )}
: 'bg-neutral-300' onClick={() => togglePanel(i)}
)} >
/> <div className="flex flex-col items-start justify-start gap-4 sm:flex-row sm:items-center sm:justify-between">
<h6 title={item.name}>{item.name}</h6> <div className="flex w-full items-center justify-between">
</div> <div className="flex h-full flex-shrink-0 items-center gap-2">
<div className="flex flex-shrink-0 items-end gap-4"> <GripVerticalIcon
{item.activated && ( size={14}
<div className="flex w-40 items-center gap-3"> className="text-[hsla(var(--text-tertiary))]"
<Progress
value={Math.round(
((Number(item.total_vram) -
Number(item.free_vram)) /
Number(item.total_vram)) *
100
)}
size="small"
className="w-full"
/> />
<span className="font-medium"> <div
{Math.round( className={twMerge(
((Number(item.total_vram) - 'h-2 w-2 rounded-full',
Number(item.free_vram)) / item.activated
Number(item.total_vram)) * ? 'bg-green-400'
100 : 'bg-neutral-300'
).toFixed()} )}
% />
<h6 title={item.name}>{item.name}</h6>
</div>
<div className="flex flex-shrink-0 items-end gap-4">
{item.activated && (
<div className="flex w-40 items-center gap-3">
<Progress
value={gpuUtilization}
size="small"
className="w-full"
/>
<span className="font-medium">
{gpuUtilization}%
</span>
</div>
)}
<div className="flex justify-end gap-2 text-xs text-[hsla(var(--text-secondary))]">
{item.activated && (
<span>
{(
(Number(item.total_vram) -
Number(item.free_vram)) /
1024
).toFixed(2)}
GB /{' '}
</span>
)}
<span>
{(
Number(item.total_vram) / 1024
).toFixed(2)}
GB
</span>
</div>
<Switch
checked={item.activated}
className={twMerge(
isLoading && 'pointer-events-none'
)}
disabled={
Boolean(isActivatingGpu.size) &&
!isLoading
}
onChange={(e) =>
handleSwitchChange(
item.id,
e.target.checked
)
}
/>
{isLoading && (
<div className="ml-2 h-4 w-4 animate-spin rounded-full border-t-2 border-solid border-blue-500" />
)}
<ChevronDownIcon
size={14}
className={twMerge(
'relative z-10 transform cursor-pointer transition-transform',
openPanels[i]
? 'rotate-180'
: 'rotate-0'
)}
/>
</div>
</div>
</div>
{openPanels[i] && (
<div className="space-y-4 p-4 pb-0 text-[hsla(var(--text-secondary))]">
<div className="flex">
<div className="w-[200px]">
Driver Version
</div>
<span>
{
item.additional_information
?.driver_version
}
</span> </span>
</div> </div>
)} <div className="flex">
<div className="w-[200px]">
<div className="flex justify-end gap-2 text-xs text-[hsla(var(--text-secondary))]"> Compute Capability
{item.activated && ( </div>
<span> <span>
{( {
(Number(item.total_vram) - item.additional_information
Number(item.free_vram)) / ?.compute_cap
1024 }
).toFixed(2)}
GB /{' '}
</span> </span>
)} </div>
<span>
{(
Number(item.total_vram) / 1024
).toFixed(2)}
GB
</span>
</div> </div>
)}
<Switch
checked={item.activated}
onChange={(e) =>
handleSwitchChange(
item.id,
e.target.checked
)
}
/>
<ChevronDownIcon
size={14}
className={twMerge(
'relative z-10 transform cursor-pointer transition-transform',
openPanels[i]
? 'rotate-180'
: 'rotate-0'
)}
/>
</div>
</div>
</div>
{openPanels[i] && (
<div className="space-y-4 p-4 pb-0 text-[hsla(var(--text-secondary))]">
<div className="flex">
<div className="w-[200px]">
Driver Version
</div>
<span>
{
item.additional_information
?.driver_version
}
</span>
</div>
<div className="flex">
<div className="w-[200px]">
Compute Capability
</div>
<span>
{item.additional_information?.compute_cap}
</span>
</div>
</div> </div>
)} )}
</div> </Draggable>
)} )
</Draggable> })}
))}
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}