Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add: a draft for updated sliderInputItem with selectable AI accelerator #2017

Merged
18 changes: 13 additions & 5 deletions react/src/components/ImageEnvironmentSelectFormItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,279 +69,284 @@
}
const ImageEnvironmentSelectFormItems: React.FC<
ImageEnvironmentSelectFormItemsProps
> = ({ filter, showPrivate }) => {
// TODO: fix below without useSuspendedBackendaiClient
// Before fetching on relay environment, BAI client should be ready
useSuspendedBackendaiClient();

const form = Form.useFormInstance<ImageEnvironmentFormInput>();
Form.useWatch('environments', form);

const [environmentSearch, setEnvironmentSearch] = useState('');
const [versionSearch, setVersionSearch] = useState('');
const { t } = useTranslation();
const [metadata, { getImageMeta }] = useBackendaiImageMetaData();
const { token } = theme.useToken();

const envSelectRef = useRef<RefSelectProps>(null);
const versionSelectRef = useRef<RefSelectProps>(null);

const { images } = useLazyLoadQuery<ImageEnvironmentSelectFormItemsQuery>(
graphql`
query ImageEnvironmentSelectFormItemsQuery($installed: Boolean) {
images(is_installed: $installed) {
name
humanized_name
tag
registry
architecture
digest
installed
resource_limits {
key
min
max
}
labels {
key
value
}
}
}
`,
{
installed: true,
},
{
fetchPolicy: 'store-and-network',
},
);

// console.log('nextEnvironmentName form', form.getFieldValue('environments'));
// console.log('nextEnvironmentName form', currentEnvironmentsFormData);
// If not initial value, select first value
// auto select when relative field is changed
useEffect(() => {
// if not initial value, select first value
const nextEnvironmentName =
form.getFieldValue('environments')?.environment ||
imageGroups[0]?.environmentGroups[0]?.environmentName;

let nextEnvironmentGroup: ImageGroup['environmentGroups'][0] | undefined;
_.find(imageGroups, (group) => {
return _.find(group.environmentGroups, (environment) => {
if (environment.environmentName === nextEnvironmentName) {
nextEnvironmentGroup = environment;
return true;
} else {
return false;
}
});
});

// if current version does'nt exist in next environment group, select a version of the first image of next environment group
if (
!_.find(
nextEnvironmentGroup?.images,
(image) =>
form.getFieldValue('environments')?.version ===
getImageFullName(image),
)
) {
const nextNewImage = nextEnvironmentGroup?.images[0];
if (nextNewImage) {
form.setFieldsValue({
environments: {
environment: nextEnvironmentName,
version: getImageFullName(nextNewImage),
image: nextNewImage,
},
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.getFieldValue('environments')?.environment]);

const isPrivateImage = (image: Image) => {
return _.some(image?.labels, (label) => {
return (
label?.key === 'ai.backend.features' &&
label?.value?.split(' ').includes('private')
);
});
};
const imageGroups: ImageGroup[] = useMemo(
() =>
_.chain(images)
.filter((image) => {
return (
(showPrivate ? true : !isPrivateImage(image)) &&
(filter ? filter(image) : true)
);
})
.groupBy((image) => {
// group by using `group` property of image info
return (
metadata?.imageInfo[getImageMeta(getImageFullName(image) || '').key]
?.group || 'Custom Environments'
);
})
.map((images, groupName) => {
return {
groupName,
environmentGroups: _.chain(images)
// sub group by using (environment) `name` property of image info
.groupBy((image) => {
return (
// metadata?.imageInfo[
// getImageMeta(getImageFullName(image) || "").key
// ]?.name || image?.name
image?.name
);
})
.map((images, environmentName) => ({
environmentName,
displayName:
metadata?.imageInfo[environmentName.split('/')?.[1]]?.name ||
environmentName,
prefix: environmentName.split('/')?.[0],
images: images.sort((a, b) =>
compareVersions(
// latest version comes first
b?.tag?.split('-')?.[0] ?? '',
a?.tag?.split('-')?.[0] ?? '',
),
),
}))
.sortBy((item) => item.displayName)
.value(),
};
})
.sortBy((item) => item.groupName)
.value(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[images, metadata, filter, showPrivate],
);

// support search image by full name
const { fullNameMatchedImage } = useMemo(() => {
let fullNameMatchedImage: Image | undefined;
let fullNameMatchedImageGroup:
| ImageGroup['environmentGroups'][0]
| undefined;
if (environmentSearch.length) {
_.chain(
imageGroups
.flatMap((group) => group.environmentGroups)
.find((envGroup) => {
fullNameMatchedImageGroup = envGroup;
fullNameMatchedImage = _.find(envGroup.images, (image) => {
return getImageFullName(image) === environmentSearch;
});
return !!fullNameMatchedImage;
}),
).value();
}
return {
fullNameMatchedImage,
fullNameMatchedImageGroup,
};
}, [environmentSearch, imageGroups]);

return (
<>
<Form.Item
name={['environments', 'environment']}
label={`${t('session.launcher.Environments')} / ${t(
'session.launcher.Version',
)}`}
rules={[{ required: true }]}
style={{ marginBottom: 10 }}
>
<Select
ref={envSelectRef}
showSearch
// autoClearSearchValue
labelInValue={false}
searchValue={environmentSearch}
onSearch={setEnvironmentSearch}
defaultActiveFirstOption={true}
optionLabelProp="label"
optionFilterProp="filterValue"
onChange={(value) => {
if (fullNameMatchedImage) {
form.setFieldsValue({
environments: {
environment: fullNameMatchedImage?.name || '',
version: getImageFullName(fullNameMatchedImage),
image: fullNameMatchedImage,
},
});
}
}}
>
{fullNameMatchedImage ? (
<Select.Option
value={fullNameMatchedImage?.name}
filterValue={getImageFullName(fullNameMatchedImage)}
>
<Flex
direction="row"
align="center"
gap="xs"
style={{ display: 'inline-flex' }}
>
<ImageMetaIcon
image={getImageFullName(fullNameMatchedImage) || ''}
style={{
width: 15,
height: 15,
}}
/>
{getImageFullName(fullNameMatchedImage)}
</Flex>
</Select.Option>
) : (
_.map(imageGroups, (group) => {
return (
<Select.OptGroup key={group.groupName} label={group.groupName}>
{_.map(group.environmentGroups, (environmentGroup) => {
const firstImage = environmentGroup.images[0];
const currentMetaImageInfo =
metadata?.imageInfo[
environmentGroup.environmentName.split('/')?.[1]
];

const extraFilterValues: string[] = [];
let environmentPrefixTag = null;
if (
environmentGroup.prefix &&
!['lablup', 'cloud', 'stable'].includes(
environmentGroup.prefix,
)
) {
extraFilterValues.push(environmentGroup.prefix);
environmentPrefixTag = (
<Tag color="purple">{environmentGroup.prefix}</Tag>
<Tag color="purple" key={environmentGroup.prefix}>
{environmentGroup.prefix}
</Tag>
);

Check warning on line 328 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 329 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 329 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

const tagsFromMetaImageInfoLabel = _.map(
currentMetaImageInfo?.label,
(label) => {
if (
_.isUndefined(label.category) &&
label.tag &&
label.color
) {
extraFilterValues.push(label.tag);
return (
<Tag color={label.color}>
<TextHighlighter keyword={environmentSearch}>
<Tag color={label.color} key={label.tag}>
<TextHighlighter
keyword={environmentSearch}
key={label.tag}
>
{label.tag}
</TextHighlighter>
</Tag>
);

Check warning on line 349 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 350 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 350 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
return null;
},
Expand Down Expand Up @@ -401,7 +406,7 @@
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.environments?.environments !== cur.environments?.environment
prev.environments?.environment !== cur.environments?.environment
}
>
{({ getFieldValue }) => {
Expand All @@ -421,88 +426,91 @@
}
});
});
return (
<Form.Item
name={['environments', 'version']}
rules={[{ required: true }]}
>
<Select
ref={versionSelectRef}
onChange={() => {}}
showSearch
searchValue={versionSearch}
onSearch={setVersionSearch}
// autoClearSearchValue
optionFilterProp="filterValue"
optionLabelProp="label"
dropdownRender={(menu) => (
<>
<Flex
style={{
fontWeight: token.fontWeightStrong,
paddingLeft: token.paddingSM,
}}
>
{t('session.launcher.Version')}
<Divider type="vertical" />
{t('session.launcher.Base')}
<Divider type="vertical" />
{t('session.launcher.Architecture')}
<Divider type="vertical" />
{t('session.launcher.Requirements')}
</Flex>
<Divider style={{ margin: '8px 0' }} />
{menu}
</>
)}
>
{_.map(
_.uniqBy(selectedEnvironmentGroup?.images, 'digest'),

(image) => {
const [version, tag, ...requirements] = image?.tag?.split(
'-',
) || ['', '', ''];

let tagAlias = metadata?.tagAlias[tag];
if (!tagAlias) {
for (const [key, replaceString] of Object.entries(
metadata?.tagReplace || {},
)) {
const pattern = new RegExp(key);
if (pattern.test(tag)) {
tagAlias = tag?.replace(pattern, replaceString);
}
}
if (!tagAlias) {
tagAlias = tag;
}
}

const extraFilterValues: string[] = [];
const requirementTags =
requirements.length > 0 ? (
<Flex
direction="row"
wrap="wrap"
style={{
flex: 1,
}}
gap={'xxs'}
>
{_.map(requirements, (requirement, idx) => (
<DoubleTag
key={idx}
values={
metadata?.tagAlias[requirement]
?.split(':')
.map((str) => {
extraFilterValues.push(str);
return (
<TextHighlighter keyword={versionSearch}>
<TextHighlighter
keyword={versionSearch}
key={str}
>
{str}
</TextHighlighter>
);

Check warning on line 513 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}) || requirements

Check warning on line 514 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
/>

Check warning on line 516 in react/src/components/ImageEnvironmentSelectFormItems.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
Expand Down
244 changes: 206 additions & 38 deletions react/src/components/ServiceLauncherModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
ImageEnvironmentFormInput,
} from './ImageEnvironmentSelectFormItems';
import ResourceGroupSelect from './ResourceGroupSelect';
import { ACCELERATOR_UNIT_MAP } from './ResourceNumber';
import SliderInputItem from './SliderInputFormItem';
import VFolderSelect from './VFolderSelect';
import { Card, Form, Input, theme, Switch, message } from 'antd';
import { Card, Form, Input, theme, Select, Switch, message } from 'antd';
import _ from 'lodash';
import React, { Suspense } from 'react';
import React, { Suspense, useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';

type ClusterMode = 'single-node' | 'multi-node';
Expand All @@ -27,6 +28,11 @@
mem: string;
'cuda.device'?: number | string;
'cuda.shares'?: number | string;
'rocm.device'?: number | string;
'tpu.device'?: number | string;
'ipu.device'?: number | string;
'atom.device'?: number | string;
'warboy.device'?: number | string;
}

interface ServiceCreateConfigType {
Expand Down Expand Up @@ -62,7 +68,8 @@
}
interface ServiceLauncherFormInput extends ImageEnvironmentFormInput {
serviceName: string;
gpu: number;
// gpu: number;
resource: AIAccelerator;
cpu: number;
mem: number;
shmem: number;
Expand All @@ -72,352 +79,513 @@
openToPublic: boolean;
}

interface AIAccelerator {
accelerator: number;
acceleratorType: SelectUIType;
}

interface SelectUIType {
value: string;
label: string;
}

const ServiceLauncherModal: React.FC<ServiceLauncherProps> = ({
extraP,
onRequestClose,
...modalProps
}) => {
const { t } = useTranslation();
const { token } = theme.useToken();
const baiClient = useSuspendedBackendaiClient();
// const [modalText, setModalText] = useState("Content of the modal");
const currentDomain = useCurrentDomainValue();
const [form] = Form.useForm<ServiceLauncherFormInput>();
const [resourceSlots] = useResourceSlots();
const currentImage = Form.useWatch(['environments', 'image'], form); //TODO: type // form.getFieldValue(['environments', 'image']);
const currentAcceleratorType = form.getFieldValue([
'resource',
'acceleratorType',
]);
const currentImageAcceleratorLimits = _.filter(
currentImage?.resource_limits,
(limit) =>
limit ? !_.includes(['cpu', 'mem', 'shmem'], limit.key) : false,
);
const currentImageAcceleratorTypeName: string =
// NOTE:
// filter from resourceSlots since resourceSlots and supported image could be non-identical.
// resourceSlots returns "all resources enable to allocate(including AI accelerator)"
// imageAcceleratorLimit returns "all resources that is supported in the selected image"
_.filter(currentImageAcceleratorLimits, (acceleratorInfo: any) =>
_.keys(resourceSlots).includes(acceleratorInfo?.key),
)[0]?.key || '';
const acceleratorSlots = _.omit(resourceSlots, ['cpu', 'mem', 'shmem']);

// change selected accelerator type according to currentImageAcceleratorTypeName
useEffect(() => {
form.setFieldValue(
['resource', 'accelerator'],
getLimitByAccelerator(currentImageAcceleratorTypeName).min || 0,
);
form.setFieldValue(['resource', 'acceleratorType'], {
value: currentImageAcceleratorTypeName,
label: ACCELERATOR_UNIT_MAP[currentImageAcceleratorTypeName] || 'UNIT',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentImage]);

const getLimitByAccelerator = (acceleratorName: string) => {
// FIXME: temporally add hard-coded number when config is undefined
let maxLimit = 8;
let minLimit = 0;

// get max
switch (acceleratorName) {
case 'cuda.device':
default:
maxLimit = baiClient._config.maxCUDADevicesPerContainer || maxLimit;
break;
case 'cuda.shares':
maxLimit = baiClient._config.maxCUDASharesPerContainer || maxLimit;
break;
case 'rocm.device':
maxLimit = baiClient._config.maxROCMDevicesPerContainer || maxLimit;
break;
case 'tpu.device':
maxLimit = baiClient._config.maxTPUDevicesPerContainer || maxLimit;
break;
case 'ipu.device':
maxLimit = baiClient._config.maxIPUDevicesPerContainer || maxLimit;
break;
case 'atom.device':
maxLimit = baiClient._config.maxATOMDevicesPerContainer || maxLimit;
break;
case 'warboy.device':
maxLimit = baiClient._config.maxWarboyDevicesPerContainer || maxLimit;
break;
}
// get min
minLimit = parseInt(
_.filter(
currentImageAcceleratorLimits,
(supportedAcceleratorInfo) =>
supportedAcceleratorInfo?.key === currentImageAcceleratorTypeName,
)[0]?.min as string,
);
return {
min: minLimit,
max: maxLimit,
};
};

const mutationToCreateService = useTanMutation<
unknown,
{
message?: string;
},
ServiceLauncherFormInput
>({
mutationFn: (values) => {
const image: string = `${values.environments.image?.registry}/${values.environments.image?.name}:${values.environments.image?.tag}`;
const body: ServiceCreateType = {
name: values.serviceName,
desired_session_count: values.desiredRoutingCount,
image: image,
arch: values.environments.image?.architecture as string,
group: baiClient.current_group, // current Project Group,
domain: currentDomain, // current Domain Group,
cluster_size: 1, // FIXME: hardcoded. change it with option later
cluster_mode: 'single-node', // FIXME: hardcoded. change it with option later
open_to_public: values.openToPublic,
config: {
model: values.vFolderName,
model_mount_destination: '/models', // FIXME: hardcoded. change it with option later
environ: {}, // FIXME: hardcoded. change it with option later
scaling_group: values.resourceGroup,
resources: {
cpu: values.cpu,
mem: values.mem + 'G',
},
},
};
if (resourceSlots?.['cuda.shares']) {
body['config'].resources['cuda.shares'] = values.gpu;
}
if (resourceSlots?.['cuda.device']) {
body['config'].resources['cuda.device'] = values.gpu;
// Set AI accelerator value if set
// Currently, we only support one AI accelerator per session
if (values.resource.acceleratorType) {
const acceleratorTypeName: string =
values.resource.acceleratorType?.value;
// FIXME: temporally add switch-case
switch (acceleratorTypeName) {
case 'cuda.shares':
body['config'].resources['cuda.shares'] =
values.resource.accelerator;
break;
case 'cuda.device':
body['config'].resources['cuda.device'] =
values.resource.accelerator;
break;
case 'rocm.device':
body['config'].resources['rocm.device'] =
values.resource.accelerator;
break;
case 'tpu.device':
body['config'].resources['tpu.device'] =
values.resource.accelerator;
break;
case 'ipu.device':
body['config'].resources['ipu.device'] =
values.resource.accelerator;
break;
case 'warboy.device':
body['config'].resources['warboy.device'] =
values.resource.accelerator;
break;
}
}
if (values.shmem && values.shmem > 0) {
body['config'].resource_opts = {
shmem: values.shmem + 'G',
};
}
return baiSignedRequestWithPromise({
method: 'POST',
url: '/services',
body,
client: baiClient,
});
},
});
// const scalingGroupList = use;
// modelStorageList: Record<string, any>[];
// environmentList: Record<string, any>[];
// name?: string;
// cpu: number | string;
// mem: number | string;
// npu?: number | string;
// shmem?: number | string;

// Apply any operation after clicking OK button
const handleOk = () => {
// setModalText("Lorem Ipsum");
// setConfirmLoading(true);
// // TODO: send request to start service to manager server
// setTimeout(() => {
// setConfirmLoading(false);
// }, 2000);
form
.validateFields()
.then((values) => {
mutationToCreateService.mutate(values, {
onSuccess: () => {
onRequestClose(true);
},
onError: (error) => {
if (error?.message) {
message.error(
_.truncate(error?.message, {
length: 200,
}),
);
} else {
message.error(t('modelService.FailedToStartService'));
}
},
});
})
.catch((err) => {
if (err.errorFields?.[0].errors?.[0]) {
message.error(err.errorFields?.[0].errors?.[0]);
} else {
message.error(t('modelService.FormValidationFailed'));
}
});
};

// Apply any operation after clicking Cancel button
const handleCancel = () => {
// console.log("Clicked cancel button");
onRequestClose();
};

return (
<BAIModal
title={t('modelService.StartNewServing')}
onOk={handleOk}
onCancel={handleCancel}
destroyOnClose={true}
maskClosable={false}
confirmLoading={mutationToCreateService.isLoading}
{...modalProps}
>
<Suspense fallback={<FlexActivityIndicator />}>
<Form
disabled={mutationToCreateService.isLoading}
form={form}
preserve={false}
layout="vertical"
labelCol={{ span: 12 }}
initialValues={
{
cpu: 1,
gpu: 0,
// gpu: 0,
resource: {
accelerator: 0,
},
mem: 0.25,
shmem: 0,
desiredRoutingCount: 1,
} as ServiceLauncherFormInput
}
>
<Form.Item
label={t('modelService.ServiceName')}
name="serviceName"
rules={[
{
pattern: /^(?=.{4,24}$)\w[\w.-]*\w$/,
message: t('modelService.ServiceNameRule'),
},
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="resourceGroup"
label={t('session.ResourceGroup')}
rules={[
{
required: true,
},
]}
>
<ResourceGroupSelect autoSelectDefault />
</Form.Item>
<Form.Item
name="openToPublic"
label={t('modelService.OpenToPublic')}
valuePropName="checked"
>
<Switch></Switch>
</Form.Item>
<Form.Item
name={'vFolderName'}
label={t('session.launcher.ModelStorageToMount')}
rules={[
{
required: true,
},
]}
>
<VFolderSelect
filter={(vf) => vf.usage_mode === 'model'}
autoSelectDefault
/>
</Form.Item>
<SliderInputItem
label={t('modelService.DesiredRoutingCount')}
name="desiredRoutingCount"
rules={[
{
required: true,
},
]}
inputNumberProps={{
//TODO: change unit based on resource limit
addonAfter: '#',
}}
required
/>
<Card
style={{
marginBottom: token.margin,
}}
>
<ImageEnvironmentSelectFormItems
// //TODO: test with real inference images
// filter={(image) => {
// return !!_.find(image?.labels, (label) => {
// return (
// label?.key === "ai.backend.role" &&
// label.value === "INFERENCE" //['COMPUTE', 'INFERENCE', 'SYSTEM']
// );
// });
// }}
/>
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.environments?.image?.digest !==
cur.environments?.image?.digest
}
>
{({ getFieldValue }) => {
// TODO: change min/max based on selected images resource limit and current user limit
const currentImage: Image =
getFieldValue('environments')?.image;

return (
<>
<SliderInputItem
name={'cpu'}
label={t('session.launcher.CPU')}
tooltip={<Trans i18nKey={'session.launcher.DescCPU'} />}
min={parseInt(
_.find(
currentImage?.resource_limits,
(i) => i?.key === 'cpu',
)?.min || '0',
)}
max={baiClient._config.maxCPUCoresPerContainer || 128}
inputNumberProps={{
addonAfter: t('session.launcher.Core'),
}}
required
rules={[
{
required: true,
},
]}
/>
<SliderInputItem
name={'mem'}
label={t('session.launcher.Memory')}
tooltip={
<Trans i18nKey={'session.launcher.DescMemory'} />
}
max={baiClient._config.maxMemoryPerContainer || 1536}
min={0}
inputNumberProps={{
addonAfter: 'GiB',
}}
step={0.25}
required
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_form, value) {
const sizeGInfo = iSizeToSize(
_.find(
currentImage?.resource_limits,
(i) => i?.key === 'mem',
)?.min || '0B',
'G',
);

if (sizeGInfo.number > value) {
return Promise.reject(
new Error(
t('session.launcher.MinMemory', {
size: sizeGInfo.numberUnit,
}),
),
);
}
return Promise.resolve();
},
}),
]}
/>
<SliderInputItem
name={'shmem'}
label={t('session.launcher.SharedMemory')}
tooltip={
<Trans i18nKey={'session.launcher.DescSharedMemory'} />
}
max={baiClient._config.maxShmPerContainer || 8}
min={0}
step={0.25}
inputNumberProps={{
addonAfter: 'GiB',
}}
required
rules={[
{
required: true,
},
]}
/>
{(resourceSlots?.['cuda.device'] ||
resourceSlots?.['cuda.shares']) && (
<SliderInputItem
style={{ marginBottom: 0 }}
name={'gpu'}
label={t('session.launcher.AIAccelerator')}
tooltip={
<Trans
i18nKey={'session.launcher.DescAIAccelerator'}
/>
}
max={
resourceSlots['cuda.shares']
? baiClient._config.maxCUDASharesPerContainer
: baiClient._config.maxCUDADevicesPerContainer
}
step={resourceSlots['cuda.shares'] ? 0.1 : 1}
inputNumberProps={{
//TODO: change unit based on resource limit
addonAfter: 'GPU',
}}
required
rules={[
{
required: true,
},
]}
/>
)}
</>
);
}}
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.environments?.environment !== cur.environments?.environment
}
>
{() => {
return (
<SliderInputItem
name={['resource', 'accelerator']}
label={t(`session.launcher.AIAccelerator`)}
tooltip={
<Trans i18nKey={'session.launcher.DescAIAccelerator'} />
}
sliderProps={
{
// FIXME: temporally comment out min value
// marks: {
// 0: 0,
// },
}
}
min={0}
max={
getLimitByAccelerator(currentImageAcceleratorTypeName).max
}
step={
_.endsWith(currentAcceleratorType, 'shares') ? 0.1 : 1
}
disabled={currentImageAcceleratorLimits.length <= 0}
inputNumberProps={{
addonAfter: (
<Form.Item
noStyle
name={['resource', 'acceleratorType']}
initialValue={currentImageAcceleratorTypeName}
>
<Select
disabled={currentImageAcceleratorLimits.length <= 0}
suffixIcon={
_.size(acceleratorSlots) > 1 ? undefined : null
}
open={
_.size(acceleratorSlots) > 1 ? undefined : false
}
popupMatchSelectWidth={false}
options={_.map(acceleratorSlots, (value, name) => {
return {
value: name,
label: ACCELERATOR_UNIT_MAP[name] || 'UNIT',
disabled:
currentImageAcceleratorLimits.length > 0 &&
!_.find(
currentImageAcceleratorLimits,
(limit) => {
return limit?.key === name;
},
),
};
})}
/>
</Form.Item>
),
}}
required
rules={[
{
required: true,
},
]}
/>
);

Check warning on line 586 in react/src/components/ServiceLauncherModal.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}}
</Form.Item>
</Card>
</Form>
</Suspense>
Expand Down
11 changes: 10 additions & 1 deletion react/src/components/SliderInputFormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,76 +18,85 @@
name: NamePath;
inputNumberProps?: InputNumberProps;
sliderProps?: SliderSingleProps | SliderRangeProps;
disabled?: boolean;
}
const SliderInputItem: React.FC<SliderInputProps> = ({
name,
min,
max,
step,
rules,
required,
inputNumberProps,
sliderProps,
initialValue,
disabled,
...formItemProps
}) => {
return (
<Form.Item required={required} {...formItemProps}>
<Flex direction="row" gap={'md'}>
<Flex direction="column" align="stretch" style={{ flex: 3 }}>
<Form.Item
name={name}
noStyle
rules={rules}
initialValue={initialValue}
>
<Slider max={max} min={min} step={step} {...sliderProps} />
<Slider
max={max}
min={min}
step={step}
disabled={disabled}
{...sliderProps}
/>
</Form.Item>
</Flex>
<Flex style={{ flex: 2 }}>
<Form.Item name={name} noStyle initialValue={initialValue}>
<InputNumber
max={max}
min={min}
step={step}
onStep={(value, info) => {
console.log(value, info);
}}
disabled={disabled}
{...inputNumberProps}
/>
</Form.Item>
</Flex>
</Flex>
</Form.Item>
// <Row justify="space-around" align="middle" gutter={20}>
// <Col span={6}>
// <p>Resource</p>
// </Col>
// <Col span={8}>
// <Slider
// min={minValue}
// max={maxValue}
// onChange={onChange}
// value={typeof inputValue === "number" ? inputValue : 0}
// step={0.01}
// />
// </Col>
// <Col span={6}>
// <InputNumber
// min={minValue}
// max={maxValue}
// style={
// {
// /* use theme config */
// }
// }
// step={0.01}
// value={inputValue}
// onChange={onChange}
// />
// </Col>
// </Row>
);

Check warning on line 99 in react/src/components/SliderInputFormItem.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
};

Check warning on line 100 in react/src/components/SliderInputFormItem.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

export default SliderInputItem;
Loading