From ddf723525a77d2d5b13beb0ade0ec77afdac7b06 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 12 Jun 2026 12:04:13 +0800 Subject: [PATCH] fix: instance types switch between gpu and cpu --- src/locales/en-US/gpuservice.ts | 2 + src/locales/ja-JP/gpuservice.ts | 2 + src/locales/ru-RU/gpuservice.ts | 2 + src/locales/tr-TR/gpuservice.ts | 2 + src/locales/zh-CN/gpuservice.ts | 2 + .../instances/components/add-modal.tsx | 164 +++++++++++++----- .../components/instance-type-item.tsx | 125 +++++++++++-- .../gpu-service/instances/config/types.ts | 25 +++ .../services/use-query-instance-types.ts | 1 + .../instances/styles/instances.module.less | 5 + 10 files changed, 270 insertions(+), 60 deletions(-) diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 51555bd2..47735599 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -120,6 +120,8 @@ export default { 'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.ram': 'RAM', + 'gpuservice.instance.os': 'OS', + 'gpuservice.instance.arch': 'Arch', 'gpuservice.instance.disk': 'Disk', 'gpuservice.table.count': 'Count', 'gpuservice.instance.disk.system': 'System Disk', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index fce356d3..50febcde 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -119,6 +119,8 @@ export default { 'gpuservice.instance.sliced': '分割', 'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.ram': 'RAM', + 'gpuservice.instance.os': 'OS', + 'gpuservice.instance.arch': 'アーキテクチャ', 'gpuservice.instance.disk': 'ディスク', 'gpuservice.table.count': '数量', 'gpuservice.instance.disk.system': 'システムディスク', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 3d09c1c6..3d01ed84 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -118,6 +118,8 @@ export default { 'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.ram': 'RAM', + 'gpuservice.instance.os': 'ОС', + 'gpuservice.instance.arch': 'Архитектура', 'gpuservice.instance.disk': 'Диск', 'gpuservice.table.count': 'Количество', 'gpuservice.instance.disk.system': 'Системный диск', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index b11a4132..9aee124b 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -114,6 +114,8 @@ export default { 'gpuservice.instance.sliced': 'Bölünmüş', 'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.ram': 'RAM', + 'gpuservice.instance.os': 'OS', + 'gpuservice.instance.arch': 'Mimari', 'gpuservice.instance.disk': 'Disk', 'gpuservice.table.count': 'Sayı', 'gpuservice.instance.disk.system': 'Sistem Diski', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index 9da31bca..d53e14fe 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -109,6 +109,8 @@ export default { 'gpuservice.instance.sliced': '切分', 'gpuservice.instance.memory': '显存', 'gpuservice.instance.ram': '内存', + 'gpuservice.instance.os': '系统', + 'gpuservice.instance.arch': '架构', 'gpuservice.instance.disk': '磁盘', 'gpuservice.table.count': '数量', 'gpuservice.instance.disk.system': '系统盘', diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 990adb30..fa18d25c 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -8,10 +8,12 @@ import { AlertBlockInfo, ColumnWrapper, GSDrawer, + IconFont, ModalFooter } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Empty, Input, Typography } from 'antd'; +import { Empty, Flex, Input, Segmented, Typography } from 'antd'; +import _ from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import { ListItem as TemplateItem } from '../../templates/config/types'; import useQueryTemplates from '../../templates/services/use-query-templates'; @@ -88,6 +90,7 @@ const AddModal: React.FC = ({ manufacturer: undefined }); const [templateId, setTemplateId] = useState(); + const [resourceType, setResourceType] = useState<'gpu' | 'cpu'>('gpu'); const [instanceKeyword, setInstanceKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState(''); const { loading, guard, run, release } = useSubmitLock(); @@ -171,17 +174,35 @@ const AddModal: React.FC = ({ return JSON.stringify({ name: instanceType.name, spec: { - ...instanceType.spec + ..._.omit(instanceType.spec, ['cache', 'cpu']), + cpu: _.pick(instanceType.spec?.cpu, [ + 'manufacturer', + 'product', + 'family' + ]) } }); }; + // GPU types carry their accelerator vendor; non-acceleratable (CPU) types + // all map to the single 'cpu' bucket used to match templates. + const manufacturerOf = (instanceType: InstanceTypeItem) => + instanceType.spec.acceleratable ? instanceType.spec?.manufacturer : 'cpu'; + + const matchesResourceType = ( + instanceType: InstanceTypeItem, + type: 'gpu' | 'cpu' + ) => + type === 'gpu' + ? !!instanceType.spec.acceleratable + : !instanceType.spec.acceleratable; + // apply the selection of instance type and template const applySelection = ( instanceType: InstanceTypeItem, template: TemplateItem | undefined ) => { - const manufacturer = instanceType.spec?.manufacturer; + const manufacturer = manufacturerOf(instanceType); setInstanceTypeSelection({ instanceType: instanceType.name, @@ -212,6 +233,35 @@ const AddModal: React.FC = ({ form.current?.applyInstanceType?.(instanceType); }; + // Drop the instance-type-derived selection + form state. Used when no + // candidate is available (empty segment / org with no clusters) so a stale + // type / cluster never survives a switch or reload. + const clearSelection = () => { + setInstanceTypeSelection({ + instanceType: undefined, + manufacturer: undefined + }); + setTemplateId(undefined); + form.current?.applyInstanceType?.(undefined); + form.current?.setFieldValue?.('clusterId', null); + form.current?.setFieldValue?.(['spec', 'type'], undefined); + }; + + const autoSelectFirst = ( + types: InstanceTypeItem[], + templates: TemplateItem[] + ) => { + const first = types.find((item) => !item.disabled); + if (!first) { + clearSelection(); + return; + } + applySelection( + first, + findTemplateByManufacturer(manufacturerOf(first), templates) + ); + }; + const findAggregateOf = ( candidateName: string | undefined, clusterId: number | null | undefined, @@ -248,41 +298,34 @@ const AddModal: React.FC = ({ if (aggregate) { setInstanceTypeSelection({ instanceType: aggregate.name, - manufacturer: aggregate.spec?.manufacturer + manufacturer: manufacturerOf(aggregate) }); + // Surface the persisted pick under the matching segment. + setResourceType(aggregate.spec.acceleratable ? 'gpu' : 'cpu'); } return; } // Scope to clusters the chosen org owns (admin "All" view). const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId); - const first = owned.find((item) => !item.disabled); - if (!first) { - // The chosen org has no clusters (hence no instance types). Clear any - // prior pick so a stale instance type / cross-org cluster isn't left - // on the form. - setInstanceTypeSelection({ - instanceType: undefined, - manufacturer: undefined - }); - setTemplateId(undefined); - form.current?.applyInstanceType?.(undefined); - form.current?.setFieldValue?.('clusterId', null); - form.current?.setFieldValue?.(['spec', 'type'], undefined); - return; - } + // Prefer the active segment, but fall back to the other kind when it has + // no enabled candidate so the drawer never opens on an empty list. + const hasEnabled = (type: 'gpu' | 'cpu') => + owned.some((it) => matchesResourceType(it, type) && !it.disabled); + const other = resourceType === 'gpu' ? 'cpu' : 'gpu'; + const nextType = hasEnabled(resourceType) + ? resourceType + : hasEnabled(other) + ? other + : resourceType; + setResourceType(nextType); - // On create, auto-select the first instance type in the list - - const template = findTemplateByManufacturer( - first.spec?.manufacturer, + // On create, auto-select the first instance type of the chosen kind. + autoSelectFirst( + owned.filter((it) => matchesResourceType(it, nextType)), templates ); - - applySelection(first, template); - - // initially finise }; // Fetch the (tenant-scoped) instance types + templates and auto-select. @@ -319,21 +362,14 @@ const AddModal: React.FC = ({ const handleScopeChange = (orgId?: number | null) => { if (!open || action !== PageAction.CREATE) return; setScopeOrgId(orgId); - setInstanceTypeSelection({ - instanceType: undefined, - manufacturer: undefined - }); - setTemplateId(undefined); - // Also clear the instance-type-derived form state (the selected type + // Drop the instance-type-derived selection + form state (the selected type // card + its limits, the cluster, and spec.type). The cluster decides // where the instance is scheduled, so a stale pick from the previous // scope must not survive — otherwise an instance owned by the newly // chosen org could land on the old org's cluster. The reload's // owner-scoped auto-selection re-fills them from the new org, or leaves // them empty (blocking submit) when the chosen org has no clusters. - form.current?.applyInstanceType?.(undefined); - form.current?.setFieldValue?.('clusterId', null); - form.current?.setFieldValue?.(['spec', 'type'], undefined); + clearSelection(); initializedRef.current = false; loadCreateResources(orgId); }; @@ -347,6 +383,7 @@ const AddModal: React.FC = ({ manufacturer: undefined }); setTemplateId(undefined); + setResourceType('gpu'); setInstanceKeyword(''); setTemplateKeyword(''); setScopeOrgId(undefined); @@ -358,10 +395,20 @@ const AddModal: React.FC = ({ } }, [open, shouldAutoSelectResource, action]); - // filter instance types (already scoped to the chosen org's clusters) - const filteredInstanceTypes = ownedInstanceTypes.filter((item) => - matchKeyword([item.name], instanceKeyword) + // Which kinds the chosen org actually offers — drives the GPU/CPU segment + // availability so a user can't switch to an empty list. + const hasGPUTypes = ownedInstanceTypes.some( + (item) => item.spec.acceleratable ); + const hasCPUTypes = ownedInstanceTypes.some( + (item) => !item.spec.acceleratable + ); + + // filter instance types (already scoped to the chosen org's clusters) by the + // active GPU/CPU segment, then by the search keyword. + const filteredInstanceTypes = ownedInstanceTypes + .filter((item) => matchesResourceType(item, resourceType)) + .filter((item) => matchKeyword([item.name], instanceKeyword)); // No instance types for the chosen org (e.g. it owns no clusters), and not // mid-fetch — drives the "no available instance type" message in the form. @@ -405,7 +452,7 @@ const AddModal: React.FC = ({ const handleInstanceTypeChange = (item: InstanceTypeItem) => { const template = findTemplateByManufacturer( - item.spec?.manufacturer, + manufacturerOf(item), templateList ); applySelection(item, template); @@ -430,6 +477,14 @@ const AddModal: React.FC = ({ }); }; + const handleOnTypeChange = (next: 'gpu' | 'cpu') => { + setResourceType(next); + autoSelectFirst( + ownedInstanceTypes.filter((item) => matchesResourceType(item, next)), + templateList + ); + }; + return ( = ({ }} > - {intl.formatMessage({ id: 'gpuservice.instance.types' })} + + + {intl.formatMessage({ + id: 'gpuservice.instance.types' + })} + + , + disabled: !hasGPUTypes + }, + { + label: 'CPU', + value: 'cpu', + icon: , + disabled: !hasCPUTypes + } + ]} + > + ` display: grid; - grid-template-columns: repeat(7, auto); + grid-template-columns: repeat(${(props) => props.$columns ?? 7}, auto); grid-auto-rows: minmax(15px, auto); justify-content: start; column-gap: 4px; @@ -32,10 +35,17 @@ const Meta = styled.div` height: 3px; border-radius: 50%; background-color: var(--ant-color-text-quaternary); - margin: 0 6px; + margin: 0 4px; justify-self: center; } + .meta-label { + font-size: 12px; + } + .meta-value { + font-size: 12px; + } + .meta-icon { font-size: 14px; color: var(--ant-color-text-quaternary); @@ -53,7 +63,7 @@ interface MetadataSectionProps { const MetaItem: React.FC<{ icon: string; label?: string; - value?: string | null | number; + value?: React.ReactNode; showDot?: boolean; show?: boolean; }> = ({ icon, label, value, showDot = true, show = true }) => { @@ -68,16 +78,47 @@ const MetaItem: React.FC<{ ); }; +const CPUManufacturerTag: React.FC<{ manufacturer?: string }> = ({ + manufacturer +}) => { + return ( + + {manufacturer} + + ); +}; + function getInstanceDerived(item: InstanceTypeItemModel) { const spec = item.spec || {}; const acceleratable = spec.acceleratable; + const cpuManufacturer = acceleratable + ? spec.cpu?.manufacturer + : spec.manufacturer; + return { acceleratable, isGPU: acceleratable, - manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', - displayName: acceleratable ? spec.product || item.name : 'CPU', + manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types + displayName: spec.product || item.name, ramUnit: spec.unitResourcesParsed?.ram?.value, + os: _.capitalize(spec.os) || '', + arch: spec.arch, + cpuManufacturer: Vendors.includes(cpuManufacturer as any) + ? _.capitalize(cpuManufacturer) + : _.toUpper(cpuManufacturer), cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores }; } @@ -87,14 +128,15 @@ export const InstanceMetadataSection: React.FC = ({ }) => { const intl = useIntl(); - const { ramUnit, cpuUnitCores, isGPU } = getInstanceDerived({ + const { ramUnit, cpuUnitCores, isGPU, os, arch } = getInstanceDerived({ spec } as InstanceTypeItemModel); return ( - + {isGPU && ( <> + {/* row 1: Memory | Max | RAM */} = ({ label={intl.formatMessage({ id: 'gpuservice.instance.memory' })} value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'} /> + = ({ )} value={`${spec.maxComputeUnitCount || 0}`} /> + {/* row 2: OS | Arch | CPU */} + + {cpuUnitCores || '-'} + + } /> )} {!isGPU && ( <> + {/* row 1: RAM | Max */} = ({ )} value={`${spec.maxComputeUnitCount || 0}`} /> + {/* row 2: OS | Arch */} + + )} @@ -155,10 +226,12 @@ export const InstanceMetadataSection: React.FC = ({ const InstanceTypeItem: React.FC = ({ item }) => { const specData = item.spec || {}; - const { acceleratable, manufacturer, displayName } = getInstanceDerived(item); + const { acceleratable, manufacturer, displayName, cpuManufacturer } = + getInstanceDerived(item); const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple'; - const showManufacturerTag = acceleratable && manufacturer; + const showManufacturerTag = acceleratable && !!manufacturer; + const showCPUManufacturerTag = !acceleratable && !!cpuManufacturer; return ( = ({ item }) => { style={{ height: '100%' }} > - <Flex gap={8} align="center"> - <AutoTooltip ghost minWidth={20} maxWidth={200}> - {displayName || '-'} - </AutoTooltip> + <Flex gap={8} align="center" style={{ width: '100%', minWidth: 0 }}> + <div + className="instance-type-name" + style={{ + flex: 1, + minWidth: 0 + }} + > + <AutoTooltip ghost minWidth={20} maxWidth={'100%'}> + {displayName || '-'} + </AutoTooltip> + </div> + {showManufacturerTag && ( <ThemeTag color={manufacturerColor} @@ -180,6 +262,11 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => { {manufacturer?.toUpperCase()} </ThemeTag> )} + {showCPUManufacturerTag && ( + <CPUManufacturerTag + manufacturer={`${cpuManufacturer}`} + ></CPUManufacturerTag> + )} </Flex> diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index 0e7ab6d9..367d3cac 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -149,6 +149,27 @@ export interface InstanceTypeOnceMaxRequestResource { localStorage: QuanityLocalStorage; } +export interface CPUCache { + l1i: string; + l1d: string; + l2: string; + l3: string; +} + +export interface CPUInfo { + physicalCores: string; + threadsPerPhysicalCore: string; + logicalCores: string; + stepping: string | null; + clockSpeed: string | null; + maxClockSpeed: string | null; + cacheLine: string; + cache: CPUCache; + manufacturer: string; + product: string; + family: string; +} + export interface InstanceTypeSpec { group: string; acceleratable: boolean; @@ -163,6 +184,10 @@ export interface InstanceTypeSpec { cpu: QuanityCPU; ram: QuanityMemory; }; + os?: string; + arch?: string; + cpu?: CPUInfo; + cache?: Record; unitResourcesParsed?: { cpu: { cores?: number; diff --git a/src/pages/gpu-service/instances/services/use-query-instance-types.ts b/src/pages/gpu-service/instances/services/use-query-instance-types.ts index 7dfdbab0..04367365 100644 --- a/src/pages/gpu-service/instances/services/use-query-instance-types.ts +++ b/src/pages/gpu-service/instances/services/use-query-instance-types.ts @@ -87,6 +87,7 @@ export default function useQueryInstanceTypes() { return { detailData: dataList, + setDataList, loading, cancelRequest, fetchData: queryInstanceTypes diff --git a/src/pages/gpu-service/instances/styles/instances.module.less b/src/pages/gpu-service/instances/styles/instances.module.less index a0a10bbe..fc12e25c 100644 --- a/src/pages/gpu-service/instances/styles/instances.module.less +++ b/src/pages/gpu-service/instances/styles/instances.module.less @@ -8,6 +8,7 @@ display: flex; flex: 1; max-width: 33%; + min-width: 0; min-height: 0; } @@ -40,3 +41,7 @@ } } } +.segmented { + font-weight: 400; + font-size: 12px; +}