fix: parse quality data

This commit is contained in:
jialin
2026-05-31 22:23:32 +08:00
committed by jialin
parent 16a97772d3
commit 55028cf3a3
8 changed files with 214 additions and 122 deletions
@@ -268,6 +268,7 @@ const AddModal: React.FC<AddModalProps> = ({
await onOk({
...values
});
console.log('submit form values', values);
} finally {
setLoading(false);
}
@@ -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<InstanceTypeItemProps> = ({ 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<InstanceTypeItemProps> = ({ item }) => {
},
{ count: '' }
)}
value={`${item.maxAccelerator || '-'}`}
value={`${item.maxAccelerator || 0}`}
/>
</>
)}
@@ -169,14 +166,14 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
showDot={false}
icon="icon-ram-02"
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
value={toDisplayUnit(convertKiToGi(ramRaw)) ?? '-'}
value={ramUnit ? `${ramUnit}GB` : '-'}
/>
<MetaItem
show={!!cpuRaw}
show={!!cpuUnitCores}
showDot={true}
icon="icon-cpu"
label="CPU"
value={ceilMilliToCore(cpuRaw) ?? '-'}
value={cpuUnitCores || '-'}
/>
</span>
</Meta>
@@ -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 {
+76 -53
View File
@@ -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<InstanceFormProps> = 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<InstanceFormProps> = 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<InstanceFormProps> = 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<InstanceFormProps> = 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<InstanceFormProps> = 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<InstanceFormProps> = forwardRef(
getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType: InstanceTypeItem) => {
const count = instanceType.spec?.acceleratable ? 1 : 0;
resolveAndApply(instanceType, count, { writeAccelerator: true });
resolveAndApply(instanceType, count);
}
}));
@@ -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<InstanceTypeFormItemProps> = ({
};
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<InstanceTypeFormItemProps> = ({
<div style={{ flex: 1 }}>
<Form.Item<FormData>
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$/, '')
})}
>
<InputNumber
disabled={isGPU || disabled}
disabled={isGPU || disabled || action === PageAction.EDIT}
label={renderMemoryLabel()}
max={onceMaxRequest?.memory ?? undefined}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
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
};
}}
>
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
<InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled || isGPU}
disabled={disabled || isGPU || action === PageAction.EDIT}
/>
</Form.Item>
</div>
+1 -1
View File
@@ -88,7 +88,7 @@ const GPUService: React.FC = () => {
} = useViewEvents();
const {
fetchClusterList,
cancelRequest: cancelClusterRequest,
cancelRequest,
clusterList,
loading: clusterLoading
} = useQueryClusterList();
@@ -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;
};
+81 -26
View File
@@ -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<string, number> = {
Ki: 1 / (1024 * 1024),
Mi: 1 / 1024,
Gi: 1,
Ti: 1024
const UNIT_FACTOR: Record<string, number> = {
'': 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
};
};