feat(gpu-service): add sliced compute ratio, confirm activate/deactivate, watch instance types
- add configurable compute (cores) percentage in sliced mode (cores >= memory) - confirm instance-type activate/deactivate via DeleteModal - live-sync instance types list via chunked watch
This commit is contained in:
@@ -222,6 +222,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Confirm deletion',
|
||||
'common.title.stop.confirm': 'Confirm stop',
|
||||
'common.title.start.confirm': 'Confirm start',
|
||||
'common.title.activate.confirm': 'Confirm activate',
|
||||
'common.title.deactivate.confirm': 'Confirm deactivate',
|
||||
'common.activate.single.confirm':
|
||||
'Are you sure you want to activate? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'Are you sure you want to deactivate? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.title.recreate.confirm': 'Confirm recreate',
|
||||
'common.button.addLabel': 'Add Label',
|
||||
'common.button.addSelector': 'Add Selector',
|
||||
|
||||
@@ -122,6 +122,9 @@ export default {
|
||||
'gpuservice.instance.mode.whole': 'Full GPU',
|
||||
'gpuservice.instance.mode.sliced': 'By Ratio',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM Percentage (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'Compute Percentage (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'The compute ratio must be no less than the VRAM ratio ({count}%)',
|
||||
'gpuservice.instance.slice.fullCores': '100% Compute',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Please select or enter a percentage',
|
||||
|
||||
@@ -221,6 +221,12 @@ export default {
|
||||
'common.title.delete.confirm': '削除を確認',
|
||||
'common.title.stop.confirm': '停止を確認',
|
||||
'common.title.start.confirm': '開始を確認',
|
||||
'common.title.activate.confirm': '有効化を確認',
|
||||
'common.title.deactivate.confirm': '無効化を確認',
|
||||
'common.activate.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を有効化してもよろしいですか?',
|
||||
'common.deactivate.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を無効化してもよろしいですか?',
|
||||
'common.title.recreate.confirm': '再作成を確認',
|
||||
'common.button.addLabel': 'ラベルを追加',
|
||||
'common.button.addSelector': 'セレクターを追加',
|
||||
|
||||
@@ -121,6 +121,9 @@ export default {
|
||||
'gpuservice.instance.mode.whole': 'GPU 全体',
|
||||
'gpuservice.instance.mode.sliced': '比率で',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM の割合(%)',
|
||||
'gpuservice.instance.slice.coresPercentage': '演算能力の割合(%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'演算能力の割合は VRAM の割合({count}%)以上である必要があります',
|
||||
'gpuservice.instance.slice.fullCores': '100% コンピュート',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'パーセンテージを選択または入力してください',
|
||||
|
||||
@@ -219,6 +219,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Подтверждение удаления',
|
||||
'common.title.stop.confirm': 'Подтверждение остановки',
|
||||
'common.title.start.confirm': 'Подтверждение запуска',
|
||||
'common.title.activate.confirm': 'Подтверждение активации',
|
||||
'common.title.deactivate.confirm': 'Подтверждение деактивации',
|
||||
'common.activate.single.confirm':
|
||||
'Вы уверены, что хотите активировать <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
'common.deactivate.single.confirm':
|
||||
'Вы уверены, что хотите деактивировать <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
'common.title.recreate.confirm': 'Подтверждение пересоздания',
|
||||
'common.button.addLabel': 'Добавить метку',
|
||||
'common.button.addSelector': 'Добавить селектор',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
'gpuservice.instance.mode.whole': 'Весь GPU',
|
||||
'gpuservice.instance.mode.sliced': 'По доле',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'Доля VRAM (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'Доля вычислений (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'Доля вычислений должна быть не меньше доли VRAM ({count}%)',
|
||||
'gpuservice.instance.slice.fullCores': '100% вычислений',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Выберите или введите процент',
|
||||
|
||||
@@ -224,6 +224,12 @@ export default {
|
||||
'common.title.delete.confirm': 'Silme onayı',
|
||||
'common.title.stop.confirm': 'Durdurma onayı',
|
||||
'common.title.start.confirm': 'Başlatma onayı',
|
||||
'common.title.activate.confirm': 'Etkinleştirme onayı',
|
||||
'common.title.deactivate.confirm': 'Devre dışı bırakma onayı',
|
||||
'common.activate.single.confirm':
|
||||
'Etkinleştirmek istediğinizden emin misiniz? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'Devre dışı bırakmak istediğinizden emin misiniz? \n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
'common.title.recreate.confirm': 'Yeniden oluşturma onayı',
|
||||
'common.button.addLabel': 'Etiket Ekle',
|
||||
'common.button.addSelector': 'Seçici Ekle',
|
||||
|
||||
@@ -116,6 +116,9 @@ export default {
|
||||
'gpuservice.instance.mode.whole': 'Tam GPU',
|
||||
'gpuservice.instance.mode.sliced': 'Orana Göre',
|
||||
'gpuservice.instance.slice.memoryPercentage': 'VRAM Yüzdesi (%)',
|
||||
'gpuservice.instance.slice.coresPercentage': 'İşlem Gücü Yüzdesi (%)',
|
||||
'gpuservice.instance.slice.cores.min':
|
||||
'İşlem gücü oranı VRAM oranından ({count}%) küçük olamaz',
|
||||
'gpuservice.instance.slice.fullCores': '%100 İşlem Gücü',
|
||||
'gpuservice.instance.slice.percentage.required':
|
||||
'Lütfen bir yüzde seçin veya girin',
|
||||
|
||||
@@ -212,6 +212,12 @@ export default {
|
||||
'common.title.delete.confirm': '确认删除',
|
||||
'common.title.stop.confirm': '确认停止',
|
||||
'common.title.start.confirm': '确认启动',
|
||||
'common.title.activate.confirm': '确认启用',
|
||||
'common.title.deactivate.confirm': '确认停用',
|
||||
'common.activate.single.confirm':
|
||||
'确定启用 <span style="font-size: 13px;font-weight: 700">{name}?</span>',
|
||||
'common.deactivate.single.confirm':
|
||||
'确定停用 <span style="font-size: 13px;font-weight: 700">{name}?</span>',
|
||||
'common.title.recreate.confirm': '确认重新创建',
|
||||
'common.button.addLabel': '添加标签',
|
||||
'common.button.addSelector': '添加选择器',
|
||||
|
||||
@@ -111,6 +111,8 @@ export default {
|
||||
'gpuservice.instance.mode.whole': '整卡',
|
||||
'gpuservice.instance.mode.sliced': '按比例',
|
||||
'gpuservice.instance.slice.memoryPercentage': '显存占比(%)',
|
||||
'gpuservice.instance.slice.coresPercentage': '算力占比(%)',
|
||||
'gpuservice.instance.slice.cores.min': '算力占比需不小于显存占比 {count}%',
|
||||
'gpuservice.instance.slice.fullCores': '100% 算力',
|
||||
'gpuservice.instance.slice.percentage.required': '请选择或输入百分比',
|
||||
'gpuservice.instance.slice.percentage.max': '比例需在 1% 到 {count}% 之间',
|
||||
|
||||
@@ -39,7 +39,8 @@ const GPUServiceInstanceTypes: React.FC = () => {
|
||||
const {
|
||||
dataList,
|
||||
loading: instanceTypesLoading,
|
||||
fetchInstanceTypes
|
||||
fetchInstanceTypes,
|
||||
startWatch
|
||||
} = useQueryInstanceTypes();
|
||||
const { fetchData: createInstanceType } = useCreateInstanceType();
|
||||
const {
|
||||
@@ -66,16 +67,18 @@ const GPUServiceInstanceTypes: React.FC = () => {
|
||||
if (firstK8s?.id != null) {
|
||||
setClusterId(firstK8s.id);
|
||||
await fetchInstanceTypes(firstK8s.id);
|
||||
startWatch(firstK8s.id);
|
||||
}
|
||||
setLoaded(true);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleClusterChange = useMemoizedFn((value: number) => {
|
||||
const handleClusterChange = useMemoizedFn(async (value: number) => {
|
||||
setClusterId(value);
|
||||
setKeyword('');
|
||||
fetchInstanceTypes(value);
|
||||
await fetchInstanceTypes(value);
|
||||
startWatch(value);
|
||||
});
|
||||
|
||||
const handleRefresh = useMemoizedFn(() => {
|
||||
@@ -125,18 +128,29 @@ const GPUServiceInstanceTypes: React.FC = () => {
|
||||
});
|
||||
|
||||
const handleToggleActive = useMemoizedFn(
|
||||
async (record: ListItem, activate: boolean) => {
|
||||
(record: ListItem, activate: boolean) => {
|
||||
if (clusterId == null) return;
|
||||
try {
|
||||
const action = activate
|
||||
? activateGPUInstanceType
|
||||
: deactivateGPUInstanceType;
|
||||
await action({ name: record.name, cluster_id: clusterId });
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
fetchInstanceTypes(clusterId);
|
||||
} catch (error) {
|
||||
// handled by the request interceptor
|
||||
}
|
||||
const action = activate
|
||||
? activateGPUInstanceType
|
||||
: deactivateGPUInstanceType;
|
||||
deleteModalRef.current?.show({
|
||||
content: intl.formatMessage({ id: 'gpuservice.instanceType' }),
|
||||
title: activate
|
||||
? 'common.title.activate.confirm'
|
||||
: 'common.title.deactivate.confirm',
|
||||
okText: activate
|
||||
? 'gpuservice.instanceType.activate'
|
||||
: 'gpuservice.instanceType.deactivate',
|
||||
operation: activate
|
||||
? 'common.activate.single.confirm'
|
||||
: 'common.deactivate.single.confirm',
|
||||
name: record.spec?.displayName || record.name,
|
||||
async onOk() {
|
||||
await action({ name: record.name, cluster_id: clusterId });
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
fetchInstanceTypes(clusterId);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||
import { WatchEventType } from '@/config';
|
||||
import useSetChunkRequest, {
|
||||
createAxiosToken
|
||||
} from '@/hooks/use-chunk-request';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { CancelTokenSource } from 'axios';
|
||||
import qs from 'query-string';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { queryGPUInstanceTypes } from '../apis';
|
||||
import { GPU_INSTANCE_TYPES_API, queryGPUInstanceTypes } from '../apis';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
// Merge a batch of watch events into the current name-keyed list. Instance
|
||||
// types have no numeric id, so we upsert / remove by `name` rather than reuse
|
||||
// the shared id-based chunked-list helper.
|
||||
const mergeWatchEvents = (current: ListItem[], events: any[]) => {
|
||||
let list = [...current];
|
||||
events.forEach((event: any) => {
|
||||
const collection: ListItem[] = event?.collection || [];
|
||||
if (event?.type === WatchEventType.DELETE) {
|
||||
const names = collection.map((item) => item.name);
|
||||
list = list.filter((item) => !names.includes(item.name));
|
||||
} else if (
|
||||
event?.type === WatchEventType.CREATE ||
|
||||
event?.type === WatchEventType.UPDATE
|
||||
) {
|
||||
collection.forEach((item) => {
|
||||
const index = list.findIndex((it) => it.name === item.name);
|
||||
if (index > -1) {
|
||||
list[index] = item;
|
||||
} else {
|
||||
list = [item, ...list];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// Cluster-scoped instance types for the management list. Action-driven:
|
||||
// call fetchInstanceTypes(clusterId) from the cluster picker / refresh, not
|
||||
// via an effect dependency.
|
||||
// via an effect dependency. startWatch(clusterId) opens a chunked watch that
|
||||
// keeps the list in sync with live create/update/delete events.
|
||||
export default function useQueryInstanceTypes() {
|
||||
const tokenRef = useRef<CancelTokenSource | null>(null);
|
||||
const chunkRequestRef = useRef<any>(null);
|
||||
const { setChunkRequest } = useSetChunkRequest();
|
||||
const [dataList, setDataList] = useState<ListItem[]>([]);
|
||||
|
||||
const {
|
||||
@@ -43,10 +77,27 @@ export default function useQueryInstanceTypes() {
|
||||
tokenRef.current?.cancel('CANCEL_PREVIOUS_REQUEST');
|
||||
};
|
||||
|
||||
const stopWatch = () => {
|
||||
chunkRequestRef.current?.current?.cancel?.();
|
||||
};
|
||||
|
||||
const startWatch = (clusterId: number) => {
|
||||
stopWatch();
|
||||
chunkRequestRef.current = setChunkRequest({
|
||||
url: `${GPU_INSTANCE_TYPES_API}?${qs.stringify({
|
||||
cluster_id: clusterId
|
||||
})}`,
|
||||
handler: (events: any[]) => {
|
||||
setDataList((pre) => mergeWatchEvents(pre, events));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancel();
|
||||
tokenRef.current?.cancel();
|
||||
stopWatch();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -55,6 +106,8 @@ export default function useQueryInstanceTypes() {
|
||||
loading,
|
||||
fetchInstanceTypes,
|
||||
cancelRequest,
|
||||
startWatch,
|
||||
stopWatch,
|
||||
setDataList
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,21 +350,39 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
} as any);
|
||||
};
|
||||
|
||||
// Single entry point for the sliced memory ratio: write the ratio (compute
|
||||
// stays pinned at 100%) and rescale CPU / RAM off it. Reused by the slider
|
||||
// onChange and by the sliced-mode defaults so both share one path.
|
||||
// Single entry point for the sliced memory ratio: write the ratio and
|
||||
// rescale CPU / RAM off it. The compute (cores) ratio must stay >= memory,
|
||||
// so bump it up when memory overtakes it. Reused by the slider onChange.
|
||||
const applySliceMemoryPercentage = (value: number) => {
|
||||
const currentCores = _.toNumber(
|
||||
form.getFieldValue([
|
||||
'spec',
|
||||
'resources',
|
||||
'acceleratorSlicedCoresPercentage'
|
||||
])
|
||||
);
|
||||
const coresPercentage = currentCores >= value ? currentCores : value;
|
||||
form.setFieldsValue({
|
||||
spec: {
|
||||
resources: {
|
||||
acceleratorSlicedMemoryPercentage: value,
|
||||
acceleratorSlicedCoresPercentage: 100
|
||||
acceleratorSlicedCoresPercentage: coresPercentage
|
||||
}
|
||||
}
|
||||
} as any);
|
||||
applySlicedResourceScaling();
|
||||
};
|
||||
|
||||
// Compute (cores) ratio — a GPU-slice-only parameter that rides along on
|
||||
// submit. It does not scale CPU / RAM (those track the memory ratio), so
|
||||
// just write the field.
|
||||
const applySliceCoresPercentage = (value: number) => {
|
||||
form.setFieldValue(
|
||||
['spec', 'resources', 'acceleratorSlicedCoresPercentage'],
|
||||
value
|
||||
);
|
||||
};
|
||||
|
||||
const resolveAndApply = (
|
||||
instanceType: InstanceTypeItem | undefined,
|
||||
count: number,
|
||||
@@ -436,14 +454,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
resolveAndApply(selectedInstanceType, count, false);
|
||||
};
|
||||
|
||||
// Seed the sliced-mode default ratio for an instance type: 10% but never
|
||||
// above the type's max sliceable ratio (status.onceMaxRequest
|
||||
// .acceleratorSliced). Shares applySliceMemoryPercentage with the slider.
|
||||
// Seed the sliced-mode defaults for an instance type: memory ratio at 10%
|
||||
// (never above the type's max sliceable ratio, status.onceMaxRequest
|
||||
// .acceleratorSliced), and the cores ratio defaulting to the same value
|
||||
// (cores >= memory). Set both together so a fresh selection doesn't carry a
|
||||
// stale cores value from a previous type.
|
||||
const applySlicedDefaults = (instanceType?: InstanceTypeItem) => {
|
||||
const slicedMax =
|
||||
_.toNumber(instanceType?.status?.onceMaxRequest?.acceleratorSliced) ||
|
||||
0;
|
||||
applySliceMemoryPercentage(slicedMax ? Math.min(10, slicedMax) : 10);
|
||||
const memoryPercentage = slicedMax ? Math.min(10, slicedMax) : 10;
|
||||
form.setFieldsValue({
|
||||
spec: {
|
||||
resources: {
|
||||
acceleratorSlicedMemoryPercentage: memoryPercentage,
|
||||
acceleratorSlicedCoresPercentage: memoryPercentage
|
||||
}
|
||||
}
|
||||
} as any);
|
||||
applySlicedResourceScaling();
|
||||
};
|
||||
|
||||
// Toggle between whole-card and sliced mode. Sliced fixes the accelerator
|
||||
@@ -757,6 +786,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
sliceMode={sliceMode}
|
||||
onSliceModeChange={handleSliceModeChange}
|
||||
onSliceMemoryPercentageChange={applySliceMemoryPercentage}
|
||||
onSliceCoresPercentageChange={applySliceCoresPercentage}
|
||||
onGPUCountChange={handleAcceleratorChange}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import NumberSelection from '@/pages/_components/number-selection';
|
||||
import { InputNumber } from '@gpustack/core-ui';
|
||||
import { InputNumber, LabelInfo } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Form, Segmented } from 'antd';
|
||||
import { Divider, Flex, Form, Segmented } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -76,6 +76,8 @@ interface InstanceTypeFormItemProps {
|
||||
onSliceModeChange?: (mode: 'whole' | 'sliced') => void;
|
||||
// Commit a new sliced memory ratio (writes the field + rescales CPU / RAM).
|
||||
onSliceMemoryPercentageChange?: (value: number) => void;
|
||||
// Commit a new sliced compute (cores) ratio (writes the field only).
|
||||
onSliceCoresPercentageChange?: (value: number) => void;
|
||||
onGPUCountChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
@@ -89,6 +91,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
sliceMode = 'whole',
|
||||
onSliceModeChange,
|
||||
onSliceMemoryPercentageChange,
|
||||
onSliceCoresPercentageChange,
|
||||
onGPUCountChange
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -139,6 +142,21 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
onSliceMemoryPercentageChange?.(value);
|
||||
};
|
||||
|
||||
// Compute (cores) percentage changed — forward the new value.
|
||||
const handleCoresPercentageChange = (value: number) => {
|
||||
onSliceCoresPercentageChange?.(value);
|
||||
};
|
||||
|
||||
// The cores ratio must be >= the memory ratio, so ticks below the current
|
||||
// memory percentage are disabled (min). Cores range is a fixed 10..100.
|
||||
const slicedMemoryPercentage =
|
||||
_.toNumber(
|
||||
Form.useWatch(
|
||||
['spec', 'resources', 'acceleratorSlicedMemoryPercentage'],
|
||||
form
|
||||
)
|
||||
) || 1;
|
||||
|
||||
// Max selectable ratio in sliced mode: status.onceMaxRequest.acceleratorSliced
|
||||
// (a percentage). Ticks above it stay visible but disabled.
|
||||
const slicedMaxPercentage =
|
||||
@@ -272,6 +290,13 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
)}
|
||||
</Form.Item>
|
||||
</FieldBlock>
|
||||
{showModeSwitch && (
|
||||
<div>
|
||||
<LabelInfo label={intl.formatMessage({ id: 'models.form.mode' })} />
|
||||
<div style={{ marginTop: 8 }}>{modeSegmented}</div>
|
||||
<Divider />
|
||||
</div>
|
||||
)}
|
||||
{!noAvailableTypes && (
|
||||
<Form.Item<FormData>
|
||||
key={isGPUType ? 'accelerator' : 'cpu'}
|
||||
@@ -322,7 +347,6 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
max={maxComputeUnitCount}
|
||||
step={1}
|
||||
required
|
||||
labelExtra={sliceMode === 'whole' ? modeSegmented : undefined}
|
||||
disabled={disabled || readonlyType}
|
||||
label={`${intl.formatMessage({ id: 'common.max.count' }, { label: numberSelectionLabel.label })} (${intl.formatMessage(
|
||||
{
|
||||
@@ -335,66 +359,113 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
)}
|
||||
{!noAvailableTypes && isSliced && (
|
||||
<FieldBlock>
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'acceleratorSlicedMemoryPercentage']}
|
||||
getValueProps={(value) => ({
|
||||
value: value != null ? _.toNumber(value) : undefined
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
const num = Number(value);
|
||||
if (value == null || value === '' || Number.isNaN(num)) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.percentage.required'
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
if (num > slicedMaxPercentage || num <= 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'gpuservice.instance.slice.percentage.max'
|
||||
},
|
||||
{ count: slicedMaxPercentage }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<NumberSelection
|
||||
min={1}
|
||||
max={slicedMaxPercentage}
|
||||
step={1}
|
||||
maxCount={sliceTicks.length}
|
||||
presetValues={sliceTicks}
|
||||
alwaysShowInput
|
||||
required
|
||||
disabled={disabled}
|
||||
onChange={handleMemoryPercentageChange}
|
||||
labelExtra={modeSegmented}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.memoryPercentage'
|
||||
<SelectedCard>
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'acceleratorSlicedMemoryPercentage']}
|
||||
getValueProps={(value) => ({
|
||||
value: value != null ? _.toNumber(value) : undefined
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* Compute (cores) percentage is fixed at 100. Kept in the form via a
|
||||
hidden item so it rides along on submit. */}
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'acceleratorSlicedCoresPercentage']}
|
||||
hidden
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
const num = Number(value);
|
||||
if (value == null || value === '' || Number.isNaN(num)) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.percentage.required'
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
if (num > slicedMaxPercentage || num <= 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'gpuservice.instance.slice.percentage.max'
|
||||
},
|
||||
{ count: slicedMaxPercentage }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<NumberSelection
|
||||
min={1}
|
||||
max={slicedMaxPercentage}
|
||||
step={1}
|
||||
maxCount={sliceTicks.length}
|
||||
presetValues={sliceTicks}
|
||||
alwaysShowInput
|
||||
required
|
||||
disabled={disabled}
|
||||
style={{ border: 'none' }}
|
||||
onChange={handleMemoryPercentageChange}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.memoryPercentage'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* Compute (cores) percentage. Fixed 10..100 ticks; ticks below the
|
||||
chosen memory ratio are disabled (cores must be >= memory). */}
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'acceleratorSlicedCoresPercentage']}
|
||||
style={{ marginBottom: 0 }}
|
||||
getValueProps={(value) => ({
|
||||
value: value != null ? _.toNumber(value) : undefined
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
const num = Number(value);
|
||||
if (value == null || value === '' || Number.isNaN(num)) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.percentage.required'
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
if (num < slicedMemoryPercentage || num > 100) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage(
|
||||
{ id: 'gpuservice.instance.slice.cores.min' },
|
||||
{ count: slicedMemoryPercentage }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<NumberSelection
|
||||
min={slicedMemoryPercentage}
|
||||
max={100}
|
||||
step={10}
|
||||
maxCount={SLICE_PERCENT_TICKS.length}
|
||||
presetValues={SLICE_PERCENT_TICKS}
|
||||
alwaysShowInput
|
||||
required
|
||||
disabled={disabled}
|
||||
onChange={handleCoresPercentageChange}
|
||||
style={{ border: 'none' }}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.instance.slice.coresPercentage'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</SelectedCard>
|
||||
</FieldBlock>
|
||||
)}
|
||||
{/* A not-yet-re-typed edit renders a readonly card (no sliced UI), so
|
||||
|
||||
Reference in New Issue
Block a user