From 55028cf3a3d635cd73f239bd99c65309bbb1e393 Mon Sep 17 00:00:00 2001 From: jialin Date: Sun, 31 May 2026 19:45:18 +0800 Subject: [PATCH] fix: parse quality data --- .../instances/components/add-modal.tsx | 1 + .../components/instance-type-item.tsx | 21 ++- .../gpu-service/instances/config/types.ts | 20 ++- .../gpu-service/instances/forms/index.tsx | 129 +++++++++++------- .../instances/forms/instance-type.tsx | 33 ++--- src/pages/gpu-service/instances/index.tsx | 2 +- .../services/use-query-instance-types.ts | 23 +++- src/pages/gpu-service/utils.ts | 107 +++++++++++---- 8 files changed, 214 insertions(+), 122 deletions(-) diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 50344150..e648f19a 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -268,6 +268,7 @@ const AddModal: React.FC = ({ await onOk({ ...values }); + console.log('submit form values', values); } finally { setLoading(false); } diff --git a/src/pages/gpu-service/instances/components/instance-type-item.tsx b/src/pages/gpu-service/instances/components/instance-type-item.tsx index 6dbad5d0..5808220a 100644 --- a/src/pages/gpu-service/instances/components/instance-type-item.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-item.tsx @@ -1,4 +1,3 @@ -import { ceilMilliToCore } from '@/pages/gpu-service/utils'; import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Flex } from 'antd'; @@ -62,7 +61,7 @@ interface InstanceTypeItemProps { const MetaItem: React.FC<{ icon: string; label?: string; - value?: string | null; + value?: string | null | number; showDot?: boolean; show?: boolean; }> = ({ icon, label, value, showDot = true, show = true }) => { @@ -91,15 +90,13 @@ const InstanceTypeItem: React.FC = ({ item }) => { const onceMaxRequestData = item.status?.onceMaxRequest || {}; // RAM resource - const ramRaw = acceleratable - ? specData.unitResources?.ram + const ramUnit = acceleratable + ? specData.unitResourcesParsed?.ram?.value : onceMaxRequestData.ram; - console.log('InstanceTypeItem', item.name, 'ramRaw', ramRaw); - // CPU resource - const cpuRaw = acceleratable - ? specData.unitResources?.cpu + const cpuUnitCores = acceleratable + ? specData.unitResourcesParsed?.cpu?.cores : onceMaxRequestData.cpu; const renderName = () => { @@ -159,7 +156,7 @@ const InstanceTypeItem: React.FC = ({ item }) => { }, { count: '' } )} - value={`${item.maxAccelerator || '-'}`} + value={`${item.maxAccelerator || 0}`} /> )} @@ -169,14 +166,14 @@ const InstanceTypeItem: React.FC = ({ item }) => { showDot={false} icon="icon-ram-02" label={intl.formatMessage({ id: 'gpuservice.instance.ram' })} - value={toDisplayUnit(convertKiToGi(ramRaw)) ?? '-'} + value={ramUnit ? `${ramUnit}GB` : '-'} /> diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index 988d61b1..93347b85 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -41,10 +41,10 @@ export interface FormData { }[]; volumeMount: string; resources: { - cpu?: string; - ram?: string; - localStorage?: string; - accelerator?: number | string; + cpu: string | null | number; + ram: string | null | number; + localStorage: string | null | number; + accelerator: number | string | null; }; volume: { ephemeral?: { @@ -162,6 +162,18 @@ export interface InstanceTypeSpec { cpu: QuanityCPU; ram: QuanityMemory; }; + unitResourcesParsed?: { + cpu: { + cores?: number; + unit: string; + num: number; + } | null; + ram: { + value: number; + unit: string; + num: number; + } | null; + }; } export interface InstanceTypeStatus { diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 12a32ee7..ae91edf0 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -1,10 +1,6 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import { - ceilMilliToCore, - parseQuantityToGi, - parseQuantityToNumber -} from '@/pages/gpu-service/utils'; +import { ceilMilliToCore, parseQuantityToGi } from '@/pages/gpu-service/utils'; import { PlusOutlined } from '@ant-design/icons'; import { CheckboxField, @@ -198,10 +194,52 @@ const GPUServiceInstanceForm: React.FC = forwardRef( localStorage: null }); + const buildResourcesData = ( + instanceType: InstanceTypeItem | undefined, + options: { + count: number; + } + ) => { + const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed; + const acceleratable = instanceType?.spec?.acceleratable; + const { count = 0 } = options; + + if (acceleratable) { + return { + accelerator: _.toString(count), + cpu: unitResourcesParsed?.cpu?.cores + ? count * unitResourcesParsed?.cpu?.cores + : undefined, + ram: unitResourcesParsed?.ram?.value + ? count * unitResourcesParsed?.ram?.value + : undefined + }; + } + return {}; + }; + + const buildResourcesDataForSubmit = (values: FormData) => { + const unitResourcesParsed = getUnitResources(); + const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0; + + const cpuNum = unitResourcesParsed?.cpu?.num; + const ramNum = unitResourcesParsed?.ram?.num; + + return { + cpu: + accelerator > 0 && cpuNum + ? `${accelerator * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}` + : `${values.spec?.resources?.cpu}`, + ram: + accelerator > 0 && ramNum + ? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}` + : values.spec?.resources?.ram + }; + }; + const resolveAndApply = ( instanceType: InstanceTypeItem | undefined, - count: number, - options: { writeAccelerator?: boolean } = {} + count: number ) => { if (!instanceType) { setSelectedInstanceType(undefined); @@ -214,40 +252,22 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ); setSelectedInstanceType(instanceType); - setOnceMaxRequest({ - cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest), - memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest), + cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores, + memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value, localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest) + ?.value }); - const unitResources = instanceType.spec?.unitResources; - const unitCpu = parseQuantityToNumber(unitResources?.cpu); - const unitRam = parseQuantityToNumber(unitResources?.ram); - const acceleratable = instanceType.spec?.acceleratable; - const cpuCores = - unitCpu != null ? ceilMilliToCore(`${count * unitCpu}m`) : null; - const ramGi = - unitRam != null ? parseQuantityToGi(`${count * unitRam}Mi`) : null; - form.setFieldsValue({ clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null, spec: { type: candidate?.name || '', - ...(options.writeAccelerator - ? { resources: { accelerator: _.toString(count) } } - : {}), - ...(acceleratable - ? { - resources: { - ...(options.writeAccelerator - ? { accelerator: _.toString(count) } - : {}), - ...(cpuCores != null ? { cpu: cpuCores } : {}), - ...(ramGi != null ? { ram: `${ramGi}Gi` } : {}) - } - } - : {}) + resources: { + ...buildResourcesData(instanceType, { + count + }) + } } as any }); }; @@ -293,8 +313,22 @@ const GPUServiceInstanceForm: React.FC = forwardRef( action === PageAction.VIEW || realAction === PageAction.CREATE ) { + console.log('currentData', currentData); form.setFieldsValue({ ...currentData, + spec: { + ...currentData?.spec, + resources: { + ...currentData?.spec?.resources, + ...buildResourcesData( + JSON.parse(currentData?.description || '{}'), + { + count: + _.toNumber(currentData?.spec?.resources?.accelerator) || 0 + } + ) + } + }, enable_ssh: !!currentData?.spec?.sshPublicKeys?.length, storageMode: detectMode(currentData?.spec?.volume) }); @@ -302,13 +336,13 @@ const GPUServiceInstanceForm: React.FC = forwardRef( }, [action, currentData, form, open, realAction, instanceTypeList]); const getUnitResources = () => { - if (selectedInstanceType?.spec?.unitResources) { - return selectedInstanceType.spec.unitResources; + if (selectedInstanceType?.spec?.unitResourcesParsed) { + return selectedInstanceType.spec.unitResourcesParsed; } try { return ( - JSON.parse(currentData?.description || '{}')?.spec?.unitResources ?? - undefined + JSON.parse(currentData?.description || '{}')?.spec + ?.unitResourcesParsed ?? undefined ); } catch { return undefined; @@ -329,26 +363,15 @@ const GPUServiceInstanceForm: React.FC = forwardRef( }); } - const accelerator = _.toNumber(values.spec?.resources?.accelerator); - const submittedResources = { ...(values.spec?.resources ?? {}) }; - if (accelerator > 0) { - const unitResources = getUnitResources(); - const unitCpu = parseQuantityToNumber(unitResources?.cpu); - const unitRam = parseQuantityToNumber(unitResources?.ram); - if (unitCpu != null) { - submittedResources.cpu = `${accelerator * unitCpu}m`; - } - if (unitRam != null) { - submittedResources.ram = `${accelerator * unitRam}Mi`; - } - } - await onFinish({ ..._.omit(values, ['enable_ssh']), spec: { ...values.spec, ports: submittedPorts, - resources: submittedResources + resources: { + ...values.spec?.resources, + ...buildResourcesDataForSubmit(values) + } } }); }; @@ -377,7 +400,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( getFieldsValue: () => form.getFieldsValue(), applyInstanceType: (instanceType: InstanceTypeItem) => { const count = instanceType.spec?.acceleratable ? 1 : 0; - resolveAndApply(instanceType, count, { writeAccelerator: true }); + resolveAndApply(instanceType, count); } })); diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 5dba59ca..4a2d4e82 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -8,7 +8,6 @@ import _ from 'lodash'; import { useMemo } from 'react'; import styled from 'styled-components'; import { BasicResourceMax } from '../../templates/forms/basic'; -import { ceilMilliToCore, parseQuantityToGi } from '../../utils'; import InstanceTypeItem from '../components/instance-type-item'; import { getAcceleratorMax } from '../config'; import { @@ -102,7 +101,7 @@ const InstanceTypeFormItem: React.FC = ({ }; const renderMemoryLabel = (): React.ReactNode => { - if (isGPU) { + if (isGPU || action === PageAction.EDIT || !onceMaxRequest?.memory) { return intl.formatMessage({ id: 'gpuservice.template.memory' }); } @@ -235,40 +234,24 @@ const InstanceTypeFormItem: React.FC = ({
name={['spec', 'resources', 'ram']} - normalize={(value) => (value ? `${value}Gi` : undefined)} - getValueProps={(value) => { - if (!value) return { value: '' }; - const str = String(value); - if (/Gi$/.test(str)) return { value: str.replace(/Gi$/, '') }; - if (/(Ki|Mi|Ti)$/.test(str)) { - return { value: parseQuantityToGi(str) ?? '' }; - } - return { value: str }; - }} + normalize={(value) => (value ? `${value}Gi` : null)} + getValueProps={(value) => ({ + value: _.toString(value).replace(/Gi$/, '') + })} >
- - name={['spec', 'resources', 'cpu']} - normalize={(value) => (value ? `${value}` : '')} - getValueProps={(value) => { - if (!value) return { value: '' }; - const str = String(value); - return { - value: /m$/.test(str) ? (ceilMilliToCore(str) ?? '') : str - }; - }} - > + name={['spec', 'resources', 'cpu']}>
diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index c7e3af71..b7557414 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -88,7 +88,7 @@ const GPUService: React.FC = () => { } = useViewEvents(); const { fetchClusterList, - cancelRequest: cancelClusterRequest, + cancelRequest, clusterList, loading: clusterLoading } = useQueryClusterList(); 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 31351a5e..786e6a32 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 @@ -1,5 +1,6 @@ import { useQueryData } from '@gpustack/core-ui'; import React from 'react'; +import { ceilMilliToCore, parseQuantityToGi } from '../../utils'; import { queryGPUServiceInstanceTypes } from '../apis'; import { getAcceleratorMax } from '../config'; import { InstanceTypeItem } from '../config/types'; @@ -44,13 +45,33 @@ export default function useQueryInstanceTypes() { const list = (res?.items || []).map((item) => { const remainingData = isAvailable(item); + const rawMax = item.status?.onceMaxRequest; + return { ...item, + spec: { + ...item.spec, + unitResourcesParsed: { + cpu: ceilMilliToCore(item.spec?.unitResources?.cpu ?? null), + ram: parseQuantityToGi(item.spec?.unitResources?.ram ?? null) + } + }, + status: { + ...item.status, + onceMaxRequest: { + ...rawMax, + cpu: rawMax?.cpu ? `${ceilMilliToCore(rawMax.cpu)?.cores}` : '', + ram: rawMax?.ram ? `${parseQuantityToGi(rawMax.ram)?.value}` : '', + localStorage: rawMax?.localStorage + ? `${parseQuantityToGi(rawMax.localStorage)?.value}` + : '' + } + }, maxAccelerator: remainingData.maxAccelerator, disabled: !remainingData.available }; }); - console.log('queryInstanceTypes', list); + setDataList(list); return list; }; diff --git a/src/pages/gpu-service/utils.ts b/src/pages/gpu-service/utils.ts index 1ad6cc9b..d96e3dfc 100644 --- a/src/pages/gpu-service/utils.ts +++ b/src/pages/gpu-service/utils.ts @@ -20,34 +20,89 @@ export const parseQuantityToNumber = (value?: string | null): number | null => { return Number.isFinite(num) && num > 0 ? num : null; }; -const GI_FROM_UNIT: Record = { - Ki: 1 / (1024 * 1024), - Mi: 1 / 1024, - Gi: 1, - Ti: 1024 +const UNIT_FACTOR: Record = { + '': 1, + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6 }; -// Converts a K8s-style quantity ("24314504Ki", "2Ti", "10Gi", or a bare -// number assumed to be bytes) to an integer GiB. Unit suffix is -// case-insensitive. -export const parseQuantityToGi = (value?: string | null): number | null => { - if (!value) return null; - const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)?$/i.exec(String(value)); - if (!match) return null; - const num = Number(match[1]); - if (!Number.isFinite(num) || num <= 0) return null; - const unit = match[2] - ? match[2][0].toUpperCase() + match[2].slice(1).toLowerCase() - : null; - const factor = unit ? GI_FROM_UNIT[unit] : 1 / (1024 * 1024 * 1024); - return Math.floor(num * factor); -}; +export const parseQuantity = ( + value?: string | null +): { num: number; unit: string; base: number } | null => { + if (value == null) return null; + const str = String(value).trim(); + const len = str.length; + if (len === 0) return null; -export const ceilMilliToCore = (value?: string | null): string | null => { - if (!value) return null; - const match = /^(-?\d+(?:\.\d+)?)m?$/.exec(value); - if (!match) return null; - const num = Number(match[1]); + let unit = ''; + let numEnd = len; + if (len >= 2 && str[len - 1] === 'i') { + unit = str.slice(len - 2); + if (UNIT_FACTOR[unit] === undefined) return null; + numEnd = len - 2; + } else { + const last = str[len - 1]; + const code = last.charCodeAt(0); + const isDigit = code >= 48 && code <= 57; + if (!isDigit && UNIT_FACTOR[last] !== undefined) { + unit = last; + numEnd = len - 1; + } + } + + if (numEnd === 0) return null; + const num = Number(str.slice(0, numEnd)); if (!Number.isFinite(num)) return null; - return String(Math.ceil(num / 1000)); + const base = num * UNIT_FACTOR[unit]; + if (!Number.isFinite(base)) return null; + + return { num, unit, base }; +}; + +export const parseQuantityToGi = ( + value?: string | null +): { value: number; unit: string; num: number } | null => { + if (!value) return null; + const normalized = String(value).replace( + /(ki|mi|gi|ti|pi|ei)$/i, + (m) => m[0].toUpperCase() + m[1].toLowerCase() + ); + const parsed = parseQuantity(normalized); + + if (!parsed || parsed.base <= 0) return null; + + return { + value: _.floor(parsed.base / UNIT_FACTOR.Gi, 0), + unit: parsed.unit, + num: parsed.num + }; +}; + +export const ceilMilliToCore = ( + value?: string | null, + unit?: string +): { cores: number; unit: string; num: number } | null => { + const parsed = parseQuantity(value); + if (!parsed) return null; + const outUnit = unit ?? parsed.unit; + const outFactor = UNIT_FACTOR[outUnit]; + if (outFactor === undefined) return null; + return { + cores: Math.ceil(parsed.base), + unit: outUnit, + num: parsed.base / outFactor + }; };