From 9ff6dcf9e3b173e28538f3b4f6aa2ed8cb59b675 Mon Sep 17 00:00:00 2001 From: Jong Eun Lee Date: Tue, 12 Sep 2023 11:24:01 +0800 Subject: [PATCH] fix: set the max limit and display ui of resource numbers (#1918) * refactoring resource number UI of routing list page * set the max value of the resource slider in the service launcher using config.toml * fix typo * display resource type icon in the service detail * fix eslint * display fixed number for memory * fix: condition to determine whether to display the GPU slider * change `somthing.slot` to `somthing.device`, improve type definitions * use `toFixed(2)` to display FGPU number * add test code for iSizeToSize and update test snapshot * display shmem on Endpoint detail * fix: add shmem unit to the body of `/services` POST request --- react/src/components/ResourceNumber.tsx | 154 ++++++++++++++++++ react/src/components/ResourcesNumbers.tsx | 25 --- react/src/components/ServiceLauncherModal.tsx | 67 ++++---- react/src/helper/index.test.tsx | 68 ++++++++ react/src/helper/index.tsx | 22 ++- react/src/pages/RoutingListPage.tsx | 27 ++- resources/i18n/ko.json | 2 +- 7 files changed, 295 insertions(+), 70 deletions(-) create mode 100644 react/src/components/ResourceNumber.tsx delete mode 100644 react/src/components/ResourcesNumbers.tsx create mode 100644 react/src/helper/index.test.tsx diff --git a/react/src/components/ResourceNumber.tsx b/react/src/components/ResourceNumber.tsx new file mode 100644 index 0000000000..753b7fbe9d --- /dev/null +++ b/react/src/components/ResourceNumber.tsx @@ -0,0 +1,154 @@ +import { iSizeToSize } from '../helper'; +import Flex from './Flex'; +import { Tooltip, Typography, theme } from 'antd'; +import React, { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +export type ResourceTypeKey = + | 'cpu' + | 'mem' + | 'cuda.device' + | 'cuda.shares' + | 'rocm.device' + | 'tpu.device' + | 'ipu.device' + | 'atom.device' + | 'warboy.device'; + +export type ResourceOpts = { + shmem: number; +}; +interface Props { + type: ResourceTypeKey; + extra?: ReactElement; + opts?: ResourceOpts; + value: string; +} + +type ResourceTypeInfo = { + [key in ResourceTypeKey]: V; +}; +const ResourceNumber: React.FC = ({ + type, + value: amount, + extra, + opts, +}) => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const units: ResourceTypeInfo = { + cpu: t('session.core'), + mem: 'GiB', + 'cuda.device': 'GPU', + 'cuda.shares': 'FGPU', + 'rocm.device': 'GPU', + 'tpu.device': 'TPU', + 'ipu.device': 'IPU', + 'atom.device': 'ATOM', + 'warboy.device': 'Warboy', + }; + + return ( + + + + {units[type] === 'GiB' + ? iSizeToSize(amount + 'b', 'g', 2).numberFixed + : units[type] === 'FGPU' + ? parseFloat(amount).toFixed(2) + : amount} + + {units[type]} + {type === 'mem' && opts?.shmem && ( + + (SHM: {iSizeToSize(opts.shmem + 'b', 'g', 2).numberFixed}GiB) + + )} + {extra} + + ); +}; + +const MWCIconWrap: React.FC<{ size?: number; children: string }> = ({ + size = 16, + children, +}) => { + return ( + // @ts-ignore + + {children} + {/* @ts-ignore */} + + ); +}; +interface AccTypeIconProps + extends Omit, 'src'> { + type: ResourceTypeKey; + showIcon?: boolean; + showUnit?: boolean; + showTooltip?: boolean; + size?: number; +} +export const ResourceTypeIcon: React.FC = ({ + type, + size = 16, + showIcon = true, + showUnit = true, + showTooltip = true, + ...props +}) => { + const { t } = useTranslation(); + + const resourceTypeIconSrcMap: ResourceTypeInfo< + [ReactElement | string, string] + > = { + cpu: [ + developer_board, + t('session.core'), + ], + mem: [memory, 'GiB'], + 'cuda.device': ['/resources/icons/file_type_cuda.svg', 'GPU'], + 'cuda.shares': ['/resources/icons/file_type_cuda.svg', 'FGPU'], + 'rocm.device': ['/resources/icons/ROCm.png', 'GPU'], + 'tpu.device': [view_module, 'TPU'], + 'ipu.device': [view_module, 'IPU'], + 'atom.device': ['/resources/icons/rebel.svg', 'ATOM'], + 'warboy.device': ['/resources/icons/furiosa.svg', 'Warboy'], + }; + + return ( + + {typeof resourceTypeIconSrcMap[type]?.[0] === 'string' ? ( + {type} + ) : ( +
+ {resourceTypeIconSrcMap[type]?.[0]} +
+ )} +
+ ); +}; + +export default ResourceNumber; diff --git a/react/src/components/ResourcesNumbers.tsx b/react/src/components/ResourcesNumbers.tsx deleted file mode 100644 index 0579a2aad3..0000000000 --- a/react/src/components/ResourcesNumbers.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { humanReadableBinarySize } from '../helper'; -import { Typography } from 'antd'; -import _ from 'lodash'; -import React from 'react'; - -interface Props { - cpu: number | string; - mem: number | string; - gpu: number | string; -} -const ResourcesNumbers: React.FC = ({ cpu, mem, gpu }) => { - return ( - <> - {!_.isUndefined(cpu) && CPU: {cpu}} - {!_.isUndefined(mem) && ( - - Mem: {humanReadableBinarySize(parseInt(mem + ''))} - - )} - {!_.isUndefined(gpu) && GPU: {gpu}} - - ); -}; - -export default ResourcesNumbers; diff --git a/react/src/components/ServiceLauncherModal.tsx b/react/src/components/ServiceLauncherModal.tsx index ad9e9dc15a..d6617ae020 100644 --- a/react/src/components/ServiceLauncherModal.tsx +++ b/react/src/components/ServiceLauncherModal.tsx @@ -123,7 +123,7 @@ const ServiceLauncherModal: React.FC = ({ } if (values.shmem && values.shmem > 0) { body['config'].resource_opts = { - shmem: values.shmem, + shmem: values.shmem + 'G', }; } return baiSignedRequestWithPromise({ @@ -314,12 +314,7 @@ const ServiceLauncherModal: React.FC = ({ (i) => i?.key === 'cpu', )?.min || '0', )} - max={parseInt( - _.find( - currentImage?.resource_limits, - (i) => i?.key === 'cpu', - )?.max || '100', - )} + max={baiClient._config.maxCPUCoresPerContainer || 128} inputNumberProps={{ addonAfter: t('session.launcher.Core'), }} @@ -336,7 +331,7 @@ const ServiceLauncherModal: React.FC = ({ tooltip={ } - max={64} + max={baiClient._config.maxMemoryPerContainer || 1536} min={0} inputNumberProps={{ addonAfter: 'GiB', @@ -377,7 +372,7 @@ const ServiceLauncherModal: React.FC = ({ tooltip={ } - max={64} + max={baiClient._config.maxShmPerContainer || 8} min={0} step={0.25} inputNumberProps={{ @@ -390,31 +385,35 @@ const ServiceLauncherModal: React.FC = ({ }, ]} /> - {resourceSlots?.['cuda.device'] || - (resourceSlots?.['cuda.shares'] && ( - - } - max={30} - step={resourceSlots['cuda.shares'] ? 0.1 : 1} - inputNumberProps={{ - //TODO: change unit based on resource limit - addonAfter: 'GPU', - }} - required - rules={[ - { - required: true, - }, - ]} - /> - ))} + {(resourceSlots?.['cuda.device'] || + resourceSlots?.['cuda.shares']) && ( + + } + 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, + }, + ]} + /> + )} ); }} diff --git a/react/src/helper/index.test.tsx b/react/src/helper/index.test.tsx new file mode 100644 index 0000000000..a6fbfa9ee7 --- /dev/null +++ b/react/src/helper/index.test.tsx @@ -0,0 +1,68 @@ +import { iSizeToSize } from './index'; + +describe('iSizeToSize', () => { + it('should convert iSize to Size with default fixed value', () => { + const sizeWithUnit = '1K'; + const targetSizeUnit = 'B'; + const result = iSizeToSize(sizeWithUnit, targetSizeUnit); + expect(result).toEqual({ + number: 1024, + numberFixed: '1024.00', + unit: 'B', + numberUnit: '1024.00B', + }); + }); + + it('should convert iSize to Size with fixed value of 0', () => { + const sizeWithUnit = '1K'; + const targetSizeUnit = 'B'; + const fixed = 0; + const result = iSizeToSize(sizeWithUnit, targetSizeUnit, fixed); + expect(result).toEqual({ + number: 1024, + numberFixed: '1024', + unit: 'B', + numberUnit: '1024B', + }); + }); + + it('should convert iSize to Size with targetSizeUnit of "k"', () => { + const sizeWithUnit = '1M'; + const targetSizeUnit = 'k'; + const result = iSizeToSize(sizeWithUnit, targetSizeUnit); + expect(result).toEqual({ + number: 1024, + numberFixed: '1024.00', + unit: 'k', + numberUnit: '1024.00k', + }); + }); + + it('should convert iSize to Size with targetSizeUnit of "t"', () => { + const sizeWithUnit = '1P'; + const targetSizeUnit = 't'; + const result = iSizeToSize(sizeWithUnit, targetSizeUnit); + expect(result).toEqual({ + number: 0.0009765625, + numberFixed: '0.00', + unit: 't', + numberUnit: '0.00t', + }); + }); + + it('should throw an error if size format is invalid', () => { + const sizeWithUnit = 'invalid'; + expect(() => iSizeToSize(sizeWithUnit)).toThrow('Invalid size format'); + }); + + it('should use default targetSizeUnit and fixed values if not provided', () => { + const sizeWithUnit = '1K'; + const result = iSizeToSize(sizeWithUnit); + expect(result).toEqual({ + number: 1, + numberFixed: '1.00', + unit: 'K', + numberUnit: '1.00K', + }); + }); +}); diff --git a/react/src/helper/index.tsx b/react/src/helper/index.tsx index a4eb0afa31..79dee3cb67 100644 --- a/react/src/helper/index.tsx +++ b/react/src/helper/index.tsx @@ -133,8 +133,22 @@ export const bytesToGB = ( }; export function iSizeToSize( - size: string, - targetSizeUnit?: string, + sizeWithUnit: string, + targetSizeUnit?: + | 'B' + | 'K' + | 'M' + | 'G' + | 'T' + | 'P' + | 'E' + | 'b' + | 'k' + | 'm' + | 'g' + | 't' + | 'p' + | 'e', fixed: number = 2, ): { number: number; @@ -143,8 +157,8 @@ export function iSizeToSize( numberUnit: string; } { const sizes = ['B', 'K', 'M', 'G', 'T', 'P', 'E']; - const sizeUnit = size.slice(-1).toUpperCase(); - const sizeValue = parseFloat(size.slice(0, -1)); + const sizeUnit = sizeWithUnit.slice(-1).toUpperCase(); + const sizeValue = parseFloat(sizeWithUnit.slice(0, -1)); const sizeIndex = sizes.indexOf(sizeUnit); if (sizeIndex === -1 || isNaN(sizeValue)) { throw new Error('Invalid size format'); diff --git a/react/src/pages/RoutingListPage.tsx b/react/src/pages/RoutingListPage.tsx index b75dc67326..948565a6de 100644 --- a/react/src/pages/RoutingListPage.tsx +++ b/react/src/pages/RoutingListPage.tsx @@ -4,7 +4,7 @@ import EndpointTokenGenerationModal from '../components/EndpointTokenGenerationM import Flex from '../components/Flex'; import ImageMetaIcon from '../components/ImageMetaIcon'; import ModelServiceSettingModal from '../components/ModelServiceSettingModal'; -import ResourcesNumbers from '../components/ResourcesNumbers'; +import ResourceNumber, { ResourceTypeKey } from '../components/ResourceNumber'; import ServingRouteErrorModal from '../components/ServingRouteErrorModal'; import { ServingRouteErrorModalFragment$key } from '../components/__generated__/ServingRouteErrorModalFragment.graphql'; import { baiSignedRequestWithPromise } from '../helper'; @@ -37,6 +37,7 @@ import { } from 'antd'; import graphql from 'babel-plugin-relay/macro'; import { default as dayjs } from 'dayjs'; +import _ from 'lodash'; import React, { useState, useTransition } from 'react'; import { useTranslation } from 'react-i18next'; import { useLazyLoadQuery } from 'react-relay'; @@ -124,6 +125,7 @@ const RoutingListPage: React.FC = () => { retries resource_group resource_slots + resource_opts routings { routing_id session @@ -201,6 +203,7 @@ const RoutingListPage: React.FC = () => { return color; }; + const resource_opts = JSON.parse(endpoint?.resource_opts || '{}'); return ( = () => { {endpoint?.open_to_public ? : } - - {endpoint?.resource_group} - + + + {endpoint?.resource_group} + + {_.map( + JSON.parse(endpoint?.resource_slots || '{}'), + (value: string, type: ResourceTypeKey) => { + return ( + + ); + }, + )} diff --git a/resources/i18n/ko.json b/resources/i18n/ko.json index aeab029b4f..5541300efc 100644 --- a/resources/i18n/ko.json +++ b/resources/i18n/ko.json @@ -266,7 +266,7 @@ "PreOpenPortConfigurationDone": "사전 개방 포트값이 설정되었습니다.", "PreOpenPortPanelTitle": "추가될 사전 개방 포트값 (옵션)", "PreOpenPortRange": "사전 개방 포트는 1024 ~ 65535 사이만 설정할 수 있습니다.", - "MinMemory": "재 선택한 실행환경의 최소 메모리 용량은 {{size}}iB입니다." + "MinMemory": "현재 선택한 실행환경의 최소 메모리 용량은 {{size}}iB입니다." }, "Preparing": "준비중...", "PreparingSession": "세션 준비중...",