diff --git a/src/atoms/models.ts b/src/atoms/models.ts index 436f3b43..5437f91b 100644 --- a/src/atoms/models.ts +++ b/src/atoms/models.ts @@ -16,3 +16,10 @@ export const getRequestId = () => { const store = getDefaultStore(); return store.get(requestIdAtom); }; + +export const clusterListAtom = atom< + { + label: string; + value: number; + }[] +>([]); diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index da51318e..728bed19 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -167,5 +167,8 @@ export default { 'models.form.gpuCount': 'GPUs per Replica', 'models.form.gpuType': 'GPU Type', 'models.form.optimizeLongPrompt': 'Optimize Long Prompt', - 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' + 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding', + 'models.form.check.clusterUnavailable': 'Current cluster is unavailable', + 'models.form.check.otherClustersAvailable': + 'Available clusters: {clusters}. Please switch cluster.' }; diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index 5b336a86..2d6dba78 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -164,11 +164,16 @@ export default { 'models.form.gpuCount': '各レプリカのGPU数', 'models.form.gpuType': 'GPU タイプ', 'models.form.optimizeLongPrompt': '長いプロンプトを最適化', - 'models.form.enableSpeculativeDecoding': '推測デコーディングを有効にする' + 'models.form.enableSpeculativeDecoding': '推測デコーディングを有効にする', + 'models.form.check.clusterUnavailable': 'Current cluster is unavailable', + 'models.form.check.otherClustersAvailable': + 'Available clusters: {clusters}. Please switch cluster.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== // 1. 'models.ollama.deprecated.title': 'Deprecation Notice', // 2. 'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the related GitHub issue.` -// 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.' +// 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.', +// 4. 'models.form.check.clusterUnavailable': 'Current cluster is unavailable', +// 5. 'models.form.check.otherClustersAvailable': 'Available clusters: {clusters}. Please switch cluster.' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index 50b10179..c0d610d5 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -166,7 +166,10 @@ export default { 'models.form.gpuCount': 'GPUs per Replica', 'models.form.gpuType': 'GPU Type', 'models.form.optimizeLongPrompt': 'Optimize Long Prompt', - 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' + 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding', + 'models.form.check.clusterUnavailable': 'Current cluster is unavailable', + 'models.form.check.otherClustersAvailable': + 'Available clusters: {clusters}. Please switch cluster.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -176,5 +179,7 @@ export default { // 4. 'models.form.gpuCount': 'GPUs per Replica', // 5. 'models.form.gpuType': 'GPU Type', // 6. 'models.form.optimizeLongPrompt': 'Optimize Long Prompt', -// 7. 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' +// 7. 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding', +// 8. 'models.form.check.clusterUnavailable': 'Current cluster is unavailable', +// 9. 'models.form.check.otherClustersAvailable': 'Available clusters: {clusters}. Please switch cluster.' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index c50751ea..778a5b24 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -157,5 +157,8 @@ export default { 'models.form.gpuCount': '每副本 GPU 数量', 'models.form.gpuType': 'GPU 类型', 'models.form.optimizeLongPrompt': '优化长提示', - 'models.form.enableSpeculativeDecoding': '启用推测解码' + 'models.form.enableSpeculativeDecoding': '启用推测解码', + 'models.form.check.clusterUnavailable': '当前集群不可用', + 'models.form.check.otherClustersAvailable': + '可用的集群有: {clusters}。请切换集群。' }; diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 03562d1a..ca23f84a 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -376,23 +376,23 @@ export async function evaluationsModelSpec( }, options: { token: any } ) { - return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, { - method: 'POST', - data, - cancelToken: options?.token - }); -} + const result = await request<{ results: EvaluateResult[] }>( + `${MODEL_EVALUATIONS}`, + { + method: 'POST', + data, + cancelToken: options?.token + } + ); -// export const evaluationsModelSpec = async ( -// data: { -// model_specs: EvaluateSpec[]; -// }, -// options: { token: any } -// ) => { -// const response = await fetch(`v1/${MODEL_EVALUATIONS}`, { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify(data) -// }); -// return response.json(); -// }; + const resultList = result?.results || []; + + return { + results: resultList.map((item) => { + return { + ...item, + cluster_id: data.model_specs?.[0]?.cluster_id || undefined + }; + }) + }; +} diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx index 02d9c25d..53eff857 100644 --- a/src/pages/llmodels/components/data-form.tsx +++ b/src/pages/llmodels/components/data-form.tsx @@ -169,7 +169,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { getGPUOptionList(params); } }; - }, []); + }); return ( = forwardRef((props, ref) => { - name="cluster_id" rules={[ diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index 82631c24..9598aa87 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -486,6 +486,9 @@ const AddModal: React.FC = (props) => { if (open) { setTimeout(() => { fetchSpecData(); + form.current?.getGPUOptionList?.({ + clusterId: initClusterId() + }); }, 100); } return () => { diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 4f56b44d..57258f32 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -477,6 +477,10 @@ const AddModal: FC = (props) => { onSelectModelAfterEvaluate={ handleOnSelectModelAfterEvaluate } + clusterId={ + form.current?.getFieldValue?.('cluster_id') || + initClusterId() + } displayEvaluateStatus={displayEvaluateStatus} gpuOptions={[]} > diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index ed2b94f3..99ede4b1 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -40,19 +40,12 @@ const PaginationMain = styled(Pagination)` } `; -const IconFontWrapper = styled.div` - .ant-pagination-item-link { - display: flex !important; - align-items: center; - justify-content: center; - } -`; - interface SearchInputProps { hasLinuxWorker?: boolean; modelSource: string; isDownload?: boolean; gpuOptions?: any[]; + clusterId: number; setLoadingModel?: (flag: boolean) => void; onSourceChange?: (source: string) => void; onSelectModel: (model: any, manul?: boolean) => void; @@ -70,6 +63,7 @@ const SearchModel: React.FC = (props) => { isDownload, hasLinuxWorker, gpuOptions, + clusterId, setLoadingModel, onSelectModel, onSelectModelAfterEvaluate, @@ -295,6 +289,7 @@ const SearchModel: React.FC = (props) => { return { ...backendObj, + cluster_id: clusterId, source: modelSource, ...(modelSource === modelSourceMap.huggingface_value ? { diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index 90effb07..50623391 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -256,6 +256,7 @@ const UpdateModal: React.FC = (props) => { useEffect(() => { if (open && formData) { setOriginalFormData(); + getGPUOptionList({ clusterId: formData.cluster_id }); } if (!open) { checkTokenRef.current?.cancel?.(); diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index beb084b2..0663e016 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -192,6 +192,7 @@ export interface CatalogSpec { export interface EvaluateSpec { source?: string; + cluster_id?: number; huggingface_repo_id?: string; huggingface_filename?: string; ollama_library_model_name?: string; @@ -231,4 +232,11 @@ export interface EvaluateResult { ram: number; vram: number; }; + cluster_id?: number; + resource_claim_by_cluster_id?: { + [key: number]: { + ram: number; + vram: number; + }; + }; } diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index e6b7a684..083121b9 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -1,8 +1,10 @@ +import { clusterListAtom } from '@/atoms/models'; import { createAxiosToken } from '@/hooks/use-chunk-request'; import { queryModelFilesList } from '@/pages/resources/apis'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { convertFileSize } from '@/utils'; import { useIntl } from '@umijs/max'; +import { useAtomValue } from 'jotai'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { evaluationsModelSpec } from '../apis'; @@ -148,6 +150,7 @@ export const useCheckCompatibility = () => { const requestIdRef = useRef(0); const updateStatusTimer = useRef(null); const isLockWarningStatus = useRef(false); + const clusterList = useAtomValue(clusterListAtom); const [warningStatus, setWarningStatus] = useState({ show: false, title: '', @@ -220,10 +223,25 @@ export const useCheckCompatibility = () => { } }; + const getAvailableClusters = (ids: string[]) => { + const clusterNames: string[] = []; + clusterList.forEach?.((item: { value: number; label: string }) => { + if (ids.includes(item.value.toString())) { + clusterNames.push(item.label); + } + }); + return clusterNames.join(', '); + }; + + const getCurrentCluster = (id: number) => { + const cluster = clusterList.find?.((item) => item.value === id); + return cluster?.label || ''; + }; + const handleCheckCompatibility = ( evaluateResult: EvaluateResult | null ): MessageStatus => { - console.log('handleCheckCompatibility', evaluateResult); + console.log('handleCheckCompatibility', clusterList, evaluateResult); if (!evaluateResult) { return { show: false, @@ -234,7 +252,8 @@ export const useCheckCompatibility = () => { compatible, compatibility_messages = [], scheduling_messages = [], - resource_claim, + resource_claim_by_cluster_id, + cluster_id, error, error_message } = evaluateResult || {}; @@ -248,8 +267,18 @@ export const useCheckCompatibility = () => { }; } + const resourceClaimMap = new Map( + Object.entries(resource_claim_by_cluster_id || {}) + ); + + // current cluster resource claim + const resource_claim = resourceClaimMap.get(`${cluster_id}`); + const hasClaim = !!resource_claim?.ram || !!resource_claim?.vram; + // current cluster is not available, but other clusters are available + const othersAvailable = !hasClaim && resourceClaimMap.size > 0; + let msgData = { title: scheduling_messages?.length > 0 @@ -260,6 +289,7 @@ export const useCheckCompatibility = () => { ? scheduling_messages : compatibility_messages?.join(' ') }; + if (hasClaim) { const ram = convertFileSize(resource_claim.ram, 2); const vram = convertFileSize(resource_claim.vram, 2); @@ -274,11 +304,25 @@ export const useCheckCompatibility = () => { title: intl.formatMessage({ id: 'models.form.check.passed' }), message: intl.formatMessage({ id: messageId }, { ram, vram }) }; + } else if (othersAvailable) { + msgData = { + title: intl.formatMessage({ + id: 'models.form.check.clusterUnavailable' + }), + message: intl.formatMessage( + { + id: 'models.form.check.otherClustersAvailable' + }, + { + clusters: getAvailableClusters(Array.from(resourceClaimMap.keys())) + } + ) + }; } return { - show: !compatible || hasClaim, - type: !compatible ? 'warning' : 'success', + show: !compatible || hasClaim || othersAvailable, + type: !compatible || othersAvailable ? 'warning' : 'success', ...msgData }; }; diff --git a/src/pages/llmodels/hooks/use-form-initial-values.ts b/src/pages/llmodels/hooks/use-form-initial-values.ts index 8ef4e3ac..b399ca64 100644 --- a/src/pages/llmodels/hooks/use-form-initial-values.ts +++ b/src/pages/llmodels/hooks/use-form-initial-values.ts @@ -1,3 +1,4 @@ +import { clusterListAtom } from '@/atoms/models'; import { queryClusterList } from '@/pages/cluster-management/apis'; import { ClusterListItem } from '@/pages/cluster-management/config/types'; import { queryWorkersList } from '@/pages/resources/apis'; @@ -6,6 +7,7 @@ import { WorkerStatusMapValue } from '@/pages/resources/config'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; +import { useAtom } from 'jotai'; import _ from 'lodash'; import { useState } from 'react'; import { queryGPUList } from '../apis'; @@ -188,6 +190,7 @@ export const useGenerateWorkerOptions = () => { export default function useFormInitialValues() { const { getGPUOptionList } = useGenerateGPUOptions(); + const [, setClusterListAtom] = useAtom(clusterListAtom); const [clusterList, setClusterList] = useState< Global.BaseOption[] @@ -206,10 +209,12 @@ export default function useFormInitialValues() { state: item.state })); setClusterList(list); + setClusterListAtom(list); return list; } catch (error) { console.error('Failed to fetch cluster list:', error); setClusterList([]); + setClusterListAtom([]); return []; } };