From f5c4749954d5c877391e068aae61c863fa22df18 Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 1 Apr 2025 15:59:20 +0800 Subject: [PATCH] chore: check compatibility in edit --- src/pages/llmodels/apis/index.ts | 18 +- .../llmodels/components/compatible-alert.tsx | 1 + .../llmodels/components/deploy-modal.tsx | 45 +--- .../llmodels/components/search-model.tsx | 48 +---- src/pages/llmodels/components/table-list.tsx | 9 +- .../llmodels/components/update-modal.tsx | 202 +++++++++--------- src/pages/llmodels/hooks/index.ts | 75 ++++++- src/pages/resources/config/types.ts | 2 +- 8 files changed, 212 insertions(+), 188 deletions(-) diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 58e3b057..b254553f 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -5,6 +5,7 @@ import qs from 'query-string'; import { CatalogItem, CatalogSpec, + EvaluateResult, EvaluateSpec, FormData, GPUListItem, @@ -373,18 +374,11 @@ export async function evaluationsModelSpec( }, options: { token: any } ) { - return { - results: [ - { - compatible: true - } - ] - }; - // return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, { - // method: 'POST', - // data, - // cancelToken: options?.token - // }); + return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, { + method: 'POST', + data, + cancelToken: options?.token + }); } // export const evaluationsModelSpec = async ( diff --git a/src/pages/llmodels/components/compatible-alert.tsx b/src/pages/llmodels/components/compatible-alert.tsx index 16bfb8fc..9b83ea06 100644 --- a/src/pages/llmodels/components/compatible-alert.tsx +++ b/src/pages/llmodels/components/compatible-alert.tsx @@ -26,6 +26,7 @@ const CloseWrapper = styled.div` top: 6px; right: 18px; cursor: pointer; + background-color: var(--ant-color-warning-bg); `; const MessageWrapper = styled.div` diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 68d6ae2c..8d71290a 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -8,7 +8,6 @@ import { FC, useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { backendOptionsMap, - excludeFields, getSourceRepoConfigValue, modelSourceMap } from '../config'; @@ -44,6 +43,7 @@ const FormWrapper = styled.div` type AddModalProps = { title: string; + hasLinuxWorker?: boolean; action: PageActionType; open: boolean; source: SourceType; @@ -63,6 +63,7 @@ const AddModal: FC = (props) => { open, onOk, onCancel, + hasLinuxWorker, source, action, width = 600, @@ -79,6 +80,7 @@ const AddModal: FC = (props) => { handleUpdateWarning, setWarningStatus, handleEvaluate, + handleOnValuesChange, checkTokenRef, warningStatus } = useCheckCompatibility(); @@ -103,14 +105,12 @@ const AddModal: FC = (props) => { const handleOnSelectModel = (item: any, isgguf?: boolean) => { setSelectedModel(item); form.current?.handleOnSelectModel?.(item); - console.log('isgguf+++++++', isgguf, item); if (!isgguf) { handleShowCompatibleAlert(item.evaluateResult); } }; const handleOnOk = async (allValues: FormData) => { - console.log('allValues---------', allValues); if (submitAnyway.current) { onOk(allValues); return; @@ -181,38 +181,12 @@ const AddModal: FC = (props) => { } }; - const handleOnValuesChange = async (changedValues: any, allValues: any) => { - const keys = Object.keys(changedValues); - const isExcludeField = keys.some((key) => excludeFields.includes(key)); - const hasValue = keys.every((key) => { - return !!changedValues[key]; + const onValuesChange = async (changedValues: any, allValues: any) => { + handleOnValuesChange?.({ + changedValues, + allValues, + source: props.source }); - - // let hasExcludeField = false; - // let allFieldsHaveValue = true; - - // for (const key of keys) { - // if (excludeFields.includes(key)) { - // hasExcludeField = true; - // break; - // } - // if (!changedValues[key]) { - // allFieldsHaveValue = false; - // } - // } - - if ( - !isExcludeField && - hasValue && - !_.has(changedValues, 'backend') && - !_.has(changedValues, 'local_path') - ) { - const values = form.current?.form.getFieldsValue?.(); - const data = getSourceRepoConfigValue(props.source, values); - - const evalutionData = await handleEvaluate(data.values); - handleShowCompatibleAlert?.(evalutionData); - } }; const handleCancel = useCallback(() => { @@ -285,6 +259,7 @@ const AddModal: FC = (props) => { @@ -382,7 +357,7 @@ const AddModal: FC = (props) => { gpuOptions={props.gpuOptions} modelFileOptions={props.modelFileOptions} onBackendChange={handleBackendChange} - onValuesChange={handleOnValuesChange} + onValuesChange={onValuesChange} > diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index 048ad02d..cf89d680 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -27,6 +27,7 @@ import SearchInput from './search-input'; import SearchResult from './search-result'; interface SearchInputProps { + hasLinuxWorker?: boolean; modelSource: string; isDownload?: boolean; setLoadingModel?: (flag: boolean) => void; @@ -36,7 +37,13 @@ interface SearchInputProps { const SearchModel: React.FC = (props) => { const intl = useIntl(); - const { modelSource, isDownload, setLoadingModel, onSelectModel } = props; + const { + modelSource, + isDownload, + hasLinuxWorker, + setLoadingModel, + onSelectModel + } = props; const [dataSource, setDataSource] = useState<{ repoOptions: any[]; loading: boolean; @@ -57,12 +64,10 @@ const SearchModel: React.FC = (props) => { const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); const checkTokenRef = useRef(null); - const evaluateTokenRef = useRef(null); const searchInputRef = useRef(''); - const filterGGUFRef = useRef(); + const filterGGUFRef = useRef(!hasLinuxWorker); const filterTaskRef = useRef(''); const timer = useRef(null); - const workerRef = useRef(null); const modelFilesSortOptions = useRef([ { label: intl.formatMessage({ id: 'models.sort.trending' }), @@ -219,41 +224,6 @@ const SearchModel: React.FC = (props) => { } }; - const handleEvaluateWorker = (params: { - list: any[]; - modelSource: string; - modelSourceMap: any; - }) => { - console.log('handleEvaluateWorker======='); - const { list, modelSource, modelSourceMap } = params; - workerRef.current?.terminate(); - setIsEvaluating(true); - workerRef.current = new Worker( - // @ts-ignore - new URL('../apis/evaluateWorker.ts', import.meta.url) - ); - workerRef.current.postMessage({ - list, - modelSource, - modelSourceMap - }); - workerRef.current.onmessage = function (event: any) { - const { success, resultList } = event.data; - if (success) { - setDataSource((pre) => { - return { - ...pre, - repoOptions: resultList - }; - }); - } - setIsEvaluating(false); - handleOnSelectModel(resultList[0]); - workerRef.current.terminate(); - workerRef.current = null; - }; - }; - const handleOnSearchRepo = async (sortType?: string) => { if (!SUPPORTEDSOURCE.includes(modelSource)) { return; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 7ce6e0b1..c81b95c3 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -180,11 +180,13 @@ const Models: React.FC = ({ const [openDeployModal, setOpenDeployModal] = useState<{ show: boolean; width: number | string; + hasLinuxWorker?: boolean; source: SourceType; gpuOptions: any[]; modelFileOptions?: any[]; }>({ show: false, + hasLinuxWorker: false, width: 600, source: modelSourceMap.huggingface_value as SourceType, gpuOptions: [], @@ -516,10 +518,14 @@ const Models: React.FC = ({ } const config = modalConfig[item.key]; - console.log('modelFileOptions:', modelFileOptions); + const hasLinuxWorker = workerList.some( + (worker) => worker.labels?.os === 'linux' + ); + if (config) { setOpenDeployModal({ ...config, + hasLinuxWorker: hasLinuxWorker, gpuOptions: gpuDeviceList.current, modelFileOptions: modelFileOptions }); @@ -835,6 +841,7 @@ const Models: React.FC = ({ title={intl.formatMessage({ id: 'models.button.deploy' })} source={openDeployModal.source} width={openDeployModal.width} + hasLinuxWorker={openDeployModal.hasLinuxWorker} gpuOptions={openDeployModal.gpuOptions} modelFileOptions={openDeployModal.modelFileOptions || []} onCancel={handleDeployModalCancel} diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index 838b1436..e3cd3ef4 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -1,4 +1,3 @@ -import AlertBlockInfo from '@/components/alert-info/block'; import IconFont from '@/components/icon-font'; import ModalFooter from '@/components/modal-footer'; import SealAutoComplete from '@/components/seal-form/auto-complete'; @@ -9,20 +8,23 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; -import { Form, Modal, Typography } from 'antd'; +import { Button, Form, Modal, Typography } from 'antd'; import _ from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { backendOptionsMap, backendTipsList, + getSourceRepoConfigValue, localPathTipsList, modelSourceMap, ollamaModelOptions, sourceOptions } from '../config'; import { FormData, ListItem } from '../config/types'; +import { useCheckCompatibility } from '../hooks'; import AdvanceConfig from './advance-config'; import ColumnWrapper from './column-wrapper'; +import CompatibilityAlert from './compatible-alert'; type AddModalProps = { title: string; @@ -51,17 +53,21 @@ const UpdateModal: React.FC = (props) => { onCancel, updateFormInitials: { gpuOptions, isGGUF, data: formData } } = props || {}; + const { + handleShowCompatibleAlert, + handleUpdateWarning, + setWarningStatus, + handleEvaluate, + generateGPUIds, + handleOnValuesChange, + checkTokenRef, + warningStatus + } = useCheckCompatibility(); const { getRuleMessage } = useAppUtils(); const [form] = Form.useForm(); const intl = useIntl(); const localPathCache = useRef(''); - const [warningStatus, setWarningStatus] = useState<{ - show: boolean; - message: string; - }>({ - show: false, - message: '' - }); + const submitAnyway = useRef(false); const handleSetGPUIds = (backend: string) => { if (backend === backendOptionsMap.llamaBox) { @@ -77,37 +83,30 @@ const UpdateModal: React.FC = (props) => { } }; - const updateShowWarning = (backend: string) => { + // trigger from local_path change or backend change + const handleBackendChangeHook = async () => { const localPath = form.getFieldValue?.('local_path'); + const backend = form.getFieldValue?.('backend'); - if (formData?.source !== modelSourceMap.local_path_value || !localPath) { - return; - } - - const isBlobFile = localPath?.split('/').pop()?.includes('sha256'); - const isOllamaModel = localPath?.includes('ollama'); - const isGGUFFile = localPath.endsWith('.gguf'); - - let warningMessage = ''; - - if (isBlobFile && isOllamaModel && backend === backendOptionsMap.llamaBox) { - warningMessage = ''; - } else if ( - isBlobFile && - isOllamaModel && - backend !== backendOptionsMap.llamaBox - ) { - warningMessage = 'models.form.ollama.warning'; - } else if (isGGUFFile && backend !== backendOptionsMap.llamaBox) { - warningMessage = 'models.form.backend.warning'; - } else if (!isGGUFFile && backend === backendOptionsMap.llamaBox) { - warningMessage = 'models.form.backend.warning.llamabox'; - } - - setWarningStatus({ - show: !!warningMessage, - message: warningMessage + const res = handleUpdateWarning?.({ + backend, + localPath: localPath, + source: formData?.source as string }); + + if (!res.show) { + const values = form.getFieldsValue?.(); + const data = getSourceRepoConfigValue(formData?.source as string, values); + const evalutionData = await handleEvaluate( + _.omit(data.values, [ + 'cpu_offloading', + 'distributed_inference_across_workers' + ]) + ); + handleShowCompatibleAlert?.(evalutionData); + } else { + setWarningStatus?.(res); + } }; const handleBackendChange = (val: string) => { @@ -119,7 +118,7 @@ const UpdateModal: React.FC = (props) => { } form.setFieldValue('backend_version', ''); handleSetGPUIds(val); - updateShowWarning(val); + handleBackendChangeHook(); }; const handleOnFocus = () => { @@ -289,37 +288,14 @@ const UpdateModal: React.FC = (props) => { form.submit(); }; - const generateGPUIds = (data: FormData) => { - const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); - if (!gpu_ids.length) { - return {}; - } - - const result = _.reduce( - gpu_ids, - (acc: string[], item: string | string[], index: number) => { - if (Array.isArray(item)) { - acc.push(item[1]); - } else if (index === 1) { - acc.push(item); - } - return acc; - }, - [] - ); - - if (result.length) { - return { - gpu_selector: { - gpu_ids: result - } - }; - } - return {}; + const handleSubmitAnyway = async () => { + submitAnyway.current = true; + form.submit?.(); }; - const handleOk = (formdata: FormData) => { + const handleOk = async (formdata: FormData) => { let obj = {}; + let submitData = {} as FormData; if ( [backendOptionsMap.vllm, backendOptionsMap.voxBox].includes( formdata.backend @@ -332,26 +308,40 @@ const UpdateModal: React.FC = (props) => { } if (formdata.scheduleType === 'manual') { const gpuSelector = generateGPUIds(formdata); - onOk({ + submitData = { ..._.omit(formdata, ['scheduleType']), categories: formdata.categories ? [formdata.categories] : [], worker_selector: null, - gpu_selector: formdata.gpu_selector?.gpu_ids?.length - ? { - gpu_ids: formdata.gpu_selector.gpu_ids - } - : null, ...obj, ...gpuSelector - }); + }; } else { - onOk({ + submitData = { ..._.omit(formdata, ['scheduleType']), categories: formdata.categories ? [formdata.categories] : [], gpu_selector: null, ...obj - }); + }; } + + if (submitAnyway.current) { + onOk(submitData); + return; + } + + const evalutionData = await handleEvaluate(submitData); + handleShowCompatibleAlert?.(evalutionData); + if (evalutionData?.compatible) { + onOk(submitData); + } + }; + + const onValuesChange = (changedValues: any, allValues: any) => { + handleOnValuesChange({ + changedValues, + allValues, + source: formData?.source as string + }); }; const handleOnClose = () => { @@ -363,6 +353,7 @@ const UpdateModal: React.FC = (props) => { form.setFieldsValue(formData); } if (!open) { + checkTokenRef.current?.cancel?.(); setWarningStatus({ show: false, message: '' @@ -401,40 +392,55 @@ const UpdateModal: React.FC = (props) => { }} footer={ <> - + + {intl.formatMessage({ + id: 'models.form.submit.anyway' + })} + + ) + } + > } > - {warningStatus.show && ( - - } - title={intl.formatMessage({ - id: 'common.text.tips' - })} - type="warning" - > - )} - + { + setWarningStatus({ + show: false, + message: '' + }); + }} + warningStatus={warningStatus} + contentStyle={{ paddingInline: 0 }} + > } >
{ const gpuDeviceList = useRef([]); + const workerList = useRef([]); const generateCascaderOptions = ( list: GPUListItem[], @@ -75,6 +83,7 @@ export const useGenerateFormEditInitialValues = () => { ]); const gpuList = generateCascaderOptions(gpuData.items, workerData.items); gpuDeviceList.current = gpuList; + workerList.current = workerData.items; return gpuList; }; @@ -115,7 +124,8 @@ export const useGenerateFormEditInitialValues = () => { return { getGPUList, generateFormValues, - gpuDeviceList + gpuDeviceList, + workerList }; }; @@ -331,6 +341,65 @@ export const useCheckCompatibility = () => { return warningMessage; }; + const generateGPUIds = (data: FormData) => { + const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); + if (!gpu_ids.length) { + return { + gpu_selector: null + }; + } + + const result = _.reduce( + gpu_ids, + (acc: string[], item: string | string[], index: number) => { + if (Array.isArray(item)) { + acc.push(item[1]); + } else if (index === 1) { + acc.push(item); + } + return acc; + }, + [] + ); + + return { + gpu_selector: { + gpu_ids: result + } + }; + }; + + const handleOnValuesChange = async (params: { + changedValues: any; + allValues: any; + source: string; + }) => { + const { changedValues, allValues, source } = params; + const keys = Object.keys(changedValues); + const isExcludeField = keys.some((key) => excludeFields.includes(key)); + const hasValue = keys.every((key) => { + return !!changedValues[key]; + }); + + if ( + !isExcludeField && + hasValue && + !_.has(changedValues, 'backend') && + !_.has(changedValues, 'local_path') + ) { + const data = getSourceRepoConfigValue(source, allValues); + const gpuSelector = generateGPUIds(data.values); + + const evalutionData = await handleEvaluate({ + ...data.values, + ...gpuSelector + }); + handleShowCompatibleAlert?.(evalutionData); + } + }; + + const debounceHandleValuesChange = _.debounce(handleOnValuesChange, 300); + useEffect(() => { return () => { checkTokenRef.current?.cancel(); @@ -341,8 +410,10 @@ export const useCheckCompatibility = () => { return { handleShowCompatibleAlert, handleUpdateWarning, + handleOnValuesChange: debounceHandleValuesChange, warningStatus, checkTokenRef, + generateGPUIds, handleEvaluate, setWarningStatus }; diff --git a/src/pages/resources/config/types.ts b/src/pages/resources/config/types.ts index 8781f0d6..fcb748de 100644 --- a/src/pages/resources/config/types.ts +++ b/src/pages/resources/config/types.ts @@ -61,7 +61,7 @@ export interface ListItem { name: string; hostname: string; address: string; - labels: object; + labels: Record; state: string; ip: string; state_message: string;