diff --git a/src/assets/logo/jupyter_logo.png b/src/assets/logo/jupyter_logo.png new file mode 100644 index 00000000..6ba9f963 Binary files /dev/null and b/src/assets/logo/jupyter_logo.png differ diff --git a/src/assets/logo/ubuntu.png b/src/assets/logo/ubuntu.png deleted file mode 100644 index 22875fcb..00000000 Binary files a/src/assets/logo/ubuntu.png and /dev/null differ diff --git a/src/assets/logo/ubuntu_logo.png b/src/assets/logo/ubuntu_logo.png new file mode 100644 index 00000000..c9e577d0 Binary files /dev/null and b/src/assets/logo/ubuntu_logo.png differ diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 8fe2206c..2f751d9d 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -105,6 +105,8 @@ export default { 'Please select at least {count} GPU card(s)', 'gpuservice.instance.gpuCount.noAvailable': 'No available GPU resources, please choose another instance type.', + 'gpuservice.instance.gpuCount.zero': + 'CPU-only setup for environment preparation.', 'gpuservice.instance.stock': 'Stock', 'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.memory': 'Memory', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index e518a4b8..9c16979d 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -104,6 +104,7 @@ export default { '少なくとも {count} 枚の GPU カードを選択してください', 'gpuservice.instance.gpuCount.noAvailable': '利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。', + 'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。', 'gpuservice.instance.stock': '在庫', 'gpuservice.instance.sliced': '分割', 'gpuservice.instance.memory': 'Memory', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index bfc70f4d..d0afbc70 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -106,6 +106,7 @@ export default { 'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт', 'gpuservice.instance.gpuCount.noAvailable': 'Нет доступных ресурсов GPU, выберите другой тип экземпляра.', + 'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.', 'gpuservice.instance.stock': 'Остаток', 'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.memory': 'Память', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index 6c3af077..ab60763c 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -103,6 +103,7 @@ export default { 'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin', 'gpuservice.instance.gpuCount.noAvailable': 'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.', + 'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.', 'gpuservice.instance.stock': 'Stok', 'gpuservice.instance.sliced': 'Bölünmüş', 'gpuservice.instance.memory': 'Bellek', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index bd7a6f2b..c382f332 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -98,6 +98,7 @@ export default { 'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡', 'gpuservice.instance.gpuCount.noAvailable': '没有可用的 GPU 资源,请选择其他实例类型。', + 'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。', 'gpuservice.instance.stock': '库存', 'gpuservice.instance.sliced': '切分', 'gpuservice.instance.memory': '显存', diff --git a/src/pages/_components/number-selection/index.tsx b/src/pages/_components/number-selection/index.tsx index d2dc0b22..573fe088 100644 --- a/src/pages/_components/number-selection/index.tsx +++ b/src/pages/_components/number-selection/index.tsx @@ -1,6 +1,7 @@ +import { QuestionCircleOutlined } from '@ant-design/icons'; import { LabelInfo } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Flex, InputNumber } from 'antd'; +import { Flex, InputNumber, Tooltip } from 'antd'; import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; import styles from './styles.less'; @@ -21,6 +22,7 @@ interface NumberSelectionProps { }; labelExtra?: React.ReactNode; maxCount?: number; + tips?: string; onChange?: (value: number) => void; } @@ -36,6 +38,7 @@ const NumberSelection: React.FC = ({ labelExtra, className, maxCount = 8, + tips, style, onChange }) => { @@ -108,25 +111,38 @@ const NumberSelection: React.FC = ({ {items.map((num) => { const itemDisabled = isItemDisabled(num); return ( -
handleSelect(num)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleSelect(num); - } - }} - > - {num} +
+ +
handleSelect(num)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSelect(num); + } + }} + > + {num} + {num === 0 && ( + + )} +
+
); })} diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 95486358..ee8815ab 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -91,14 +91,13 @@ const AddModal: React.FC = ({ const [loading, setLoading] = useState(false); const { - detailData, + detailData: instanceTypeList, loading: instanceTypesLoading, fetchData } = useQueryInstanceTypes(); const { detailData: templatesData, fetchData: fetchTemplates } = useQueryTemplates(); - const instanceTypeList = detailData?.items || []; const templateList = templatesData?.items || []; // const readonly = action === PageAction.VIEW; const readonly = false; @@ -180,7 +179,7 @@ const AddModal: React.FC = ({ ); }; - // initial + // initial for first const applyAutoSelection = ( instanceTypes: InstanceTypeItem[], templates: TemplateItem[] @@ -230,13 +229,12 @@ const AddModal: React.FC = ({ if (action === PageAction.CREATE) { const session = ++sessionRef.current; - Promise.all([ - fetchData({ page: 1, perPage: 100 }), - fetchTemplates({ page: -1 }) - ]).then(([instanceRes, templatesRes]) => { - if (sessionRef.current !== session) return; - applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []); - }); + Promise.all([fetchData({ page: -1 }), fetchTemplates({ page: -1 })]).then( + ([instanceResItems, templatesRes]) => { + if (sessionRef.current !== session) return; + applyAutoSelection(instanceResItems || [], templatesRes?.items || []); + } + ); } }, [open, shouldAutoSelectResource, action]); diff --git a/src/pages/gpu-service/instances/components/instance-type-list.tsx b/src/pages/gpu-service/instances/components/instance-type-list.tsx index 4b290b38..565b3f50 100644 --- a/src/pages/gpu-service/instances/components/instance-type-list.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-list.tsx @@ -3,7 +3,6 @@ import { TemplateCard } from '@gpustack/core-ui'; import { Empty, Spin } from 'antd'; import _ from 'lodash'; import styled from 'styled-components'; -import { getAcceleratorMax } from '../config'; import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types'; import InstanceTypeItem from './instance-type-item'; @@ -20,34 +19,17 @@ interface InstanceTypeListProps { loading?: boolean; } -// CPU types always have stock (no accelerator). Accelerated types are -// considered available when at least one tier offers onceMaxRequest > 0. -const isAvailable = (item: InstanceTypeItemModel) => { - if (!item.spec?.acceleratable) return true; - return getAcceleratorMax(item.status?.acceleratorTiers) > 0; -}; - const InstanceTypeList: React.FC = ({ value, onChange, dataList = [], loading }) => { - const handleSelect = ( - item: InstanceTypeItemModel & { disabled?: boolean } - ) => { + const handleSelect = (item: InstanceTypeItemModel) => { if (item.disabled || value === item.name) return; onChange?.(item); }; - const filterList = dataList.map((item) => { - const available = isAvailable(item); - return { - ...item, - disabled: !available - }; - }); - if (!dataList.length) { if (loading) { return ( @@ -65,7 +47,7 @@ const InstanceTypeList: React.FC = ({ return ( - {filterList.map((item) => { + {dataList.map((item) => { const name = item.name; return ( = forwardRef( updateActiveKey } = useScrollActiveChange({ initalActiveKeys: [TABKeysMap.BASIC], - initialCollapseKeys: [ - TABKeysMap.INSTANCE_TYPE, - TABKeysMap.TEMPLATE, - TABKeysMap.STORAGE - ] + initialCollapseKeys: + action === PageAction.EDIT + ? [] + : [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE] }); useEffect(() => { @@ -166,10 +165,14 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ); useEffect(() => { - if (hasSSHPort && !form.getFieldValue('enable_ssh')) { + if ( + hasSSHPort && + !form.getFieldValue('enable_ssh') && + action === PageAction.CREATE + ) { form.setFieldValue('enable_ssh', true); } - }, [hasSSHPort, form]); + }, [hasSSHPort, form, action]); const segmentOptions = useMemo( () => [ @@ -272,6 +275,12 @@ const GPUServiceInstanceForm: React.FC = forwardRef( })); rawHandleOnFinishFailed({ ...errorInfo, errorFields }); }; + const detectMode = (volume?: FormData['spec']['volume']) => { + if (volume?.persistent?.name || volume?.persistentTemplate?.name) { + return StorageModeValueMap.Persistent; + } + return StorageModeValueMap.Temporary; + }; useEffect(() => { if (!open) { @@ -286,7 +295,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ) { form.setFieldsValue({ ...currentData, - enable_ssh: !!currentData?.spec?.sshPublicKeys?.length + enable_ssh: !!currentData?.spec?.sshPublicKeys?.length, + storageMode: detectMode(currentData?.spec?.volume) }); } }, [action, currentData, form, open, realAction, instanceTypeList]); @@ -297,14 +307,13 @@ const GPUServiceInstanceForm: React.FC = forwardRef( (item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT ); - if (values.enable_ssh && !submittedHasSSHPort) { + if (!submittedHasSSHPort) { submittedPorts.push({ protocol: 'TCP', port: SSH_PORT, name: 'SSH' }); } - console.log('submit values', values, submittedPorts); await onFinish({ ..._.omit(values, ['enable_ssh']), spec: { @@ -348,6 +357,14 @@ const GPUServiceInstanceForm: React.FC = forwardRef( navigate('/gpu-service/public-keys'); }; + const handleOnEnableSSHChange = (e: any) => { + const checked = e.target.checked; + console.log('enable ssh change', checked); + if (!checked) { + form.setFieldValue(['spec', 'sshPublicKeys'], []); + } + }; + return ( = forwardRef( } } }, - enable_ssh: false + enable_ssh: false, + storageMode: StorageModeValueMap.Temporary }} > @@ -451,19 +469,21 @@ const GPUServiceInstanceForm: React.FC = forwardRef( style={{ marginBottom: 8 }} > - {sshEnabled && ( + {
name={['spec', 'sshPublicKeys']} style={{ marginBottom: 12 }} + hidden={!sshEnabled} normalize={(value) => Array.isArray(value) ? value?.map((item) => ({ name: item })) @@ -476,7 +496,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( })} rules={[ { - required: true, + required: sshEnabled, message: getRuleMessage('select', 'gpuservice.publicKey') } ]} @@ -525,7 +545,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( >
- )} + }
); diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index c548b382..3b1d7f37 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -82,11 +82,15 @@ const InstanceTypeFormItem: React.FC = ({ const renderInstanceType = () => { const description = JSON.parse(currentData?.description || '{}').spec || {}; return ( - + @@ -123,9 +127,9 @@ const InstanceTypeFormItem: React.FC = ({ name={['spec', 'resources', 'accelerator']} hidden={action === PageAction.EDIT} - normalize={(value) => (value ? _.toString(value) : undefined)} + normalize={(value) => (value != null ? _.toString(value) : undefined)} getValueProps={(value) => ({ - value: value ? _.toNumber(value) : undefined + value: value != null ? _.toNumber(value) : undefined })} rules={[ { @@ -142,12 +146,12 @@ const InstanceTypeFormItem: React.FC = ({ ) ); } - if (num < 1) { + if (num < 0) { return Promise.reject( new Error( intl.formatMessage( { id: 'gpuservice.instance.gpuCount.min' }, - { count: 1 } + { count: 0 } ) ) ); @@ -158,11 +162,14 @@ const InstanceTypeFormItem: React.FC = ({ ]} > { - if (volume?.persistent || volume?.persistentTemplate) { - return StorageModeValueMap.Persistent; - } - return StorageModeValueMap.Temporary; -}; - const StorageVolume = ({ disabled, action @@ -36,16 +28,7 @@ const StorageVolume = ({ const intl = useIntl(); const { getRuleMessage } = useAppUtils(); const form = Form.useFormInstance(); - const currentVolume = Form.useWatch(['spec', 'volume'], form); - const [storageMode, setStorageMode] = useState( - StorageModeValueMap.Temporary - ); - - useEffect(() => { - if (!currentVolume) return; - setStorageMode(detectMode(currentVolume)); - }, [currentVolume]); - + const storageMode = Form.useWatch('storageMode', form); const { fetchData: createStorage } = useCreateStorage(); const { detailData: storageData, fetchData: fetchStorage } = useQueryStorage(); @@ -81,7 +64,6 @@ const StorageVolume = ({ }; const handleModeChange = (mode: string) => { - setStorageMode(mode); applyMode(mode); }; @@ -99,49 +81,55 @@ const StorageVolume = ({ return ( <>
- - handleModeChange(e.target.value)} - options={[ - { - label: ( - - {intl.formatMessage({ - id: 'gpuservice.storage.temporary' - })} -
- } - /> - ), - value: StorageModeValueMap.Temporary - }, - { - label: ( - - {intl.formatMessage({ - id: 'gpuservice.storage.persistentVolume' - })} - - } - /> - ), - value: StorageModeValueMap.Persistent - } - ]} - /> + + + handleModeChange(e.target.value)} + options={[ + { + label: ( + + {intl.formatMessage({ + id: 'gpuservice.storage.temporary' + })} + + } + /> + ), + value: StorageModeValueMap.Temporary + }, + { + label: ( + + {intl.formatMessage({ + id: 'gpuservice.storage.persistentVolume' + })} + + } + /> + ), + value: StorageModeValueMap.Persistent + } + ]} + /> + {action === PageAction.CREATE && storageMode === StorageModeValueMap.Persistent && ( - 0 && ( - ({rowSelection.selectedRowKeys.length}) - ) - } - size="large" - showText={true} - disabled={!rowSelection.selectedRowKeys.length} - onSelect={handleBatchActionSelect} - /> - - } + handleClickPrimary={openCreateInstanceModal} + buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })} + handleDeleteByBatch={handleDeleteBatch} /> - queryGPUServiceInstanceTypes(params, options), - [] - ); + const fetchDetail = ( + params: Global.SearchParams = { page: 1, perPage: 100 }, + options?: any + ) => queryGPUServiceInstanceTypes(params, options); const { detailData, loading, cancelRequest, fetchData } = useQueryData< Global.PageResponse, @@ -18,10 +22,31 @@ export default function useQueryInstanceTypes() { key: 'instanceTypes' }); + const [dataList, setDataList] = React.useState([]); + + const isAvailable = (item: InstanceTypeItem) => { + if (!item.spec?.acceleratable) return true; + return getAcceleratorMax(item.status?.acceleratorTiers) > 0; + }; + + const queryInstanceTypes = async ( + params: Global.SearchParams = { page: -1 } + ) => { + const res = await fetchData(params); + + const list = (res?.items || []).map((item) => ({ + ...item, + disabled: !isAvailable(item) + })); + console.log('queryInstanceTypes', list); + setDataList(list); + return list; + }; + return { - detailData, + detailData: dataList, loading, cancelRequest, - fetchData + fetchData: queryInstanceTypes }; } diff --git a/src/pages/gpu-service/public-keys/index.tsx b/src/pages/gpu-service/public-keys/index.tsx index 8aa51f54..76575890 100644 --- a/src/pages/gpu-service/public-keys/index.tsx +++ b/src/pages/gpu-service/public-keys/index.tsx @@ -98,7 +98,7 @@ const GPUServicePublicKeys: React.FC = () => { loading={dataSource.loading} loadend={dataSource.loadend} dataSource={dataSource.dataList} - image={} + image={} filters={_.pick(queryParams, ['search'])} noFoundText={intl.formatMessage({ id: 'noresult.gpuservice.sshkey.nofound' diff --git a/src/pages/gpu-service/storage/index.tsx b/src/pages/gpu-service/storage/index.tsx index c3c3f429..887ee4e0 100644 --- a/src/pages/gpu-service/storage/index.tsx +++ b/src/pages/gpu-service/storage/index.tsx @@ -112,7 +112,7 @@ const GPUServiceStorage: React.FC = () => { loading={dataSource.loading} loadend={dataSource.loadend} dataSource={dataSource.dataList} - image={} + image={} filters={_.pick(queryParams, ['search'])} noFoundText={intl.formatMessage({ id: 'noresult.gpuservice.storage.nofound' diff --git a/src/pages/gpu-service/templates/components/template-card.tsx b/src/pages/gpu-service/templates/components/template-card.tsx index ee4587c1..9b89376f 100644 --- a/src/pages/gpu-service/templates/components/template-card.tsx +++ b/src/pages/gpu-service/templates/components/template-card.tsx @@ -2,7 +2,7 @@ import ascendLogo from '@/assets/logo/ascend.png'; import CambriconPNG from '@/assets/logo/cambricon.png'; import hyponPNG from '@/assets/logo/hygon.png'; import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; -import jupyterLogo from '@/assets/logo/jupyter.png'; +import jupyterLogo from '@/assets/logo/jupyter_logo.png'; import metaxLogo from '@/assets/logo/metax.png'; import mooreLogo from '@/assets/logo/moore-logo.png'; import nvidiaLogo from '@/assets/logo/nvidia.png'; @@ -11,7 +11,7 @@ import sgLangLogo from '@/assets/logo/sglang.png'; import theadLogoEN from '@/assets/logo/t-head-en.png'; import theadLogoZH from '@/assets/logo/t-head-zh.png'; import tensorflowkLogo from '@/assets/logo/tensorflow.svg'; -import ubuntuLogo from '@/assets/logo/ubuntu.png'; +import ubuntuLogo from '@/assets/logo/ubuntu_logo.png'; import vllmLogo from '@/assets/logo/vllm.png'; import { GPUsConfigs, @@ -50,7 +50,9 @@ const manufacturerLabelMap: Record = Object.values( { cpu: 'CPU' } as Record ); -const matchImageLogo = (image: string | undefined): string | null => { +const matchImageLogo = ( + image: string | undefined +): { logo: string; type: string } | null => { if (!image) return null; const lower = image.toLowerCase(); let matched: keyof typeof imageLogoMap | null = null; @@ -64,7 +66,12 @@ const matchImageLogo = (image: string | undefined): string | null => { } } ); - return matched ? imageLogoMap[matched] : null; + return matched + ? { + logo: imageLogoMap[matched], + type: matched + } + : null; }; const StyledCard = styled(TemplateCard)` @@ -147,8 +154,13 @@ const TemplateCardItem: React.FC = ({ data, onSelect }) => { const renderLogo = () => { const imageLogo = matchImageLogo(data.spec?.image); - if (imageLogo) { - return ; + if (imageLogo?.logo) { + return ( + + ); } switch (data.manufacturer) { case manfacturerValueMap.NVIDIA: