Files
gpustack-ui/src/pages/llmodels/components/deploy-modal.tsx
T
2025-09-16 11:26:17 +08:00

575 lines
15 KiB
TypeScript

import ModalFooter from '@/components/modal-footer';
import GSDrawer from '@/components/scroller-modal/gs-drawer';
import { PageActionType } from '@/config/types';
import useDeferredRequest from '@/hooks/use-deferred-request';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import {
backendOptionsMap,
defaultFormValues,
getSourceRepoConfigValue,
modelSourceMap
} from '../config';
import { FormContext } from '../config/form-context';
import { FormData, SourceType } from '../config/types';
import {
MessageStatus,
WarningStausOptions,
checkOnlyAscendNPU,
useCheckCompatibility,
useSelectModel
} from '../hooks';
import ColumnWrapper from './column-wrapper';
import CompatibilityAlert from './compatible-alert';
import DataForm from './data-form';
import GGUFResult from './gguf-result';
import ModelCard from './model-card';
import SearchModel from './search-model';
import Separator from './separator';
import TitleWrapper from './title-wrapper';
const resetFieldsByModel = ['backend_version', 'backend_parameters', 'env'];
const pickFieldsFromSpec = ['backend_version', 'backend_parameters', 'env'];
const dropFieldsFromForm = ['name', 'file_name', 'repo_id', 'backend'];
const resetFields = ['worker_selector', 'env'];
const resetFieldsByFile = [
'cpu_offloading',
'distributed_inference_across_workers'
];
const ModalFooterStyle = {
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
};
const ColWrapper = styled.div`
display: flex;
flex: 1;
maxwidth: 33.33%;
`;
const FormWrapper = styled.div`
display: flex;
flex: 1;
maxwidth: 100%;
`;
type AddModalProps = {
title: string;
hasLinuxWorker?: boolean;
action: PageActionType;
open: boolean;
source: SourceType;
isGGUF?: boolean;
width?: string | number;
initialValues?: any;
deploymentType?: 'modelList' | 'modelFiles';
clusterList: Global.BaseOption<
number,
{ provider: string; state: string | number }
>[];
onOk: (values: FormData) => void;
onCancel: () => void;
};
type EvaluateProccessType = 'model' | 'file' | 'form';
const EvaluateProccess: Record<string, EvaluateProccessType> = {
model: 'model',
file: 'file',
form: 'form'
};
const AddModal: FC<AddModalProps> = (props) => {
const {
title,
open,
onOk,
onCancel,
hasLinuxWorker,
source,
action,
width = 600,
deploymentType = 'modelList',
initialValues,
clusterList
} = props || {};
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const {
handleShowCompatibleAlert,
setWarningStatus,
handleBackendChangeBefore,
cancelEvaluate,
unlockWarningStatus,
handleOnValuesChange: handleOnValuesChangeBefore,
clearCahceFormValues,
warningStatus,
submitAnyway
} = useCheckCompatibility();
const { onSelectModel } = useSelectModel({ gpuOptions: [] });
const form = useRef<any>({});
const intl = useIntl();
const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false);
const [isGGUF, setIsGGUF] = useState<boolean>(false);
const modelFileRef = useRef<any>(null);
const evaluateStateRef = useRef<{
state: EvaluateProccessType;
requestModelId: number;
}>({
state: 'form',
requestModelId: 0
});
const requestModelIdRef = useRef<number>(0);
const currentSelectedModel = useRef<any>({});
const { run: fetchModelFiles } = useDeferredRequest(
() => modelFileRef.current?.fetchModelFiles?.(),
100
);
const updateSelectedModel = (model: any) => {
currentSelectedModel.current = model;
setSelectedModel(model);
};
/**
* Update the request model id to distinguish
* the evaluate request.
*/
const updateRequestModelId = () => {
requestModelIdRef.current += 1;
return requestModelIdRef.current;
};
/**
*
* @param state target to distinguish the evaluate state, current evaluate state
* can be 'model', 'file' or 'form'.
*/
const setEvaluteState = (state: {
state: EvaluateProccessType;
requestModelId: number;
}) => {
evaluateStateRef.current = state;
};
const handleOnValuesChange = (data: {
changedValues: any;
allValues: any;
source: SourceType;
}) => {
setEvaluteState({
state: EvaluateProccess.form,
requestModelId: updateRequestModelId()
});
handleOnValuesChangeBefore(data);
};
const getDefaultSpec = (item: any) => {
const defaultSpec = _.pick(
item.evaluateResult?.default_spec,
pickFieldsFromSpec
);
return defaultSpec;
};
const getCategory = (item: any) => {
const categories = item.evaluateResult?.default_spec?.categories || [];
if (Array.isArray(categories)) {
return categories?.[0] || null;
}
return categories || null;
};
const handleCancelFiles = () => {
cancelEvaluate();
modelFileRef.current?.cancelRequest();
};
const generateNameValue = (
item: any,
modelName: string,
manual?: boolean
) => {
if (item.name === currentSelectedModel.current.name) {
return manual ? modelName : form.current?.getFieldValue?.('name');
}
return modelName;
};
const currentModelDuringEvaluate = (item: any) => {
return (
evaluateStateRef.current.state === EvaluateProccess.form &&
item.name === currentSelectedModel.current.name
);
};
const handleOnSelectModel = async (item: any, manual?: boolean) => {
// If the item is empty or the same as the selected model, do nothing
handleCancelFiles();
if (
_.isEmpty(item) ||
(item.isGGUF === selectedModel.isGGUF && item.name === selectedModel.name)
) {
return;
}
console.log('isgguf==================> select 1', item.isGGUF);
console.log('handleOnSelectModel:', item, selectedModel);
setIsGGUF(item.isGGUF);
clearCahceFormValues();
unlockWarningStatus();
setEvaluteState({
state: EvaluateProccess.model,
requestModelId: updateRequestModelId()
});
updateSelectedModel(item);
// TODO
form.current?.resetFields(resetFields);
const modelInfo = onSelectModel(item, props.source);
form.current?.setFieldsValue?.({
...defaultFormValues,
...modelInfo,
categories: getCategory(item)
});
setWarningStatus(
{
show: true,
title: '',
type: 'transition',
message: intl.formatMessage({ id: 'models.form.evaluating' })
},
{
override: true
}
);
if (item.isGGUF) {
fetchModelFiles();
}
};
const handleOnSelectModelAfterEvaluate = (item: any, manual?: boolean) => {
if (currentModelDuringEvaluate(item)) {
return;
}
if (manual) {
form.current?.resetFields(resetFields);
}
console.log('isgguf==================> select 2', item.isGGUF);
// If the item is empty
setIsGGUF(item.isGGUF);
updateSelectedModel(item);
setEvaluteState({
state: EvaluateProccess.model,
requestModelId: updateRequestModelId()
});
handleCancelFiles();
const modelInfo = onSelectModel(item, props.source);
if (
evaluateStateRef.current.state === EvaluateProccess.model &&
item.evaluated
) {
handleShowCompatibleAlert(item.evaluateResult);
const newFormValues = {
...(manual
? { ...defaultFormValues }
: _.omit(form.current?.form?.getFieldsValue?.(), [
...dropFieldsFromForm
])),
...getDefaultSpec(item),
...modelInfo,
name: generateNameValue(item, modelInfo.name, manual),
categories: getCategory(item)
};
console.log('newFormValues:', newFormValues);
form.current?.form?.setFieldsValue?.(newFormValues);
handleOnValuesChangeBefore({
changedValues: {},
allValues: newFormValues,
source: props.source
});
}
};
const handleOnOk = async (allValues: FormData) => {
const result = getSourceRepoConfigValue(props.source, allValues).values;
onOk(result);
};
const handleSubmitAnyway = async () => {
submitAnyway.current = true;
form.current?.submit?.();
};
const handleSumit = () => {
form.current?.submit?.();
};
const handleSetIsGGUF = async (flag: boolean) => {
console.log('isgguf==================>', flag);
setIsGGUF(flag);
};
const handleBackendChange = async (backend: string) => {
if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true);
} else {
setIsGGUF(false);
}
const data = form.current.form.getFieldsValue?.();
const res = handleBackendChangeBefore(data);
if (res.show) {
return;
}
if (data.local_path || props.source !== modelSourceMap.local_path_value) {
handleOnValuesChange?.({
changedValues: {},
allValues:
backend === backendOptionsMap.llamaBox
? data
: _.omit(data, [
'cpu_offloading',
'distributed_inference_across_workers'
]),
source: props.source
});
}
};
const onValuesChange = async (changedValues: any, allValues: any) => {
handleOnValuesChange?.({
changedValues,
allValues,
source: props.source
});
};
const handleCancel = useCallback(() => {
onCancel?.();
}, [onCancel]);
const initClusterId = () => {
const cluster_id =
clusterList?.find((item) => item.provider === ProviderValueMap.Custom)
?.value || clusterList?.[0]?.value;
return cluster_id;
};
const handleOnOpen = () => {
if (props.deploymentType === 'modelFiles') {
form.current?.form?.setFieldsValue({
...props.initialValues,
cluster_id: initClusterId()
});
handleOnValuesChange?.({
changedValues: {},
allValues: {
...props.initialValues,
cluster_id: initClusterId()
},
source: source
});
} else {
let backend = checkOnlyAscendNPU([])
? backendOptionsMap.ascendMindie
: backendOptionsMap.vllm;
form.current?.setFieldsValue?.({
backend,
cluster_id: initClusterId()
});
}
};
const showExtraButton = useMemo(() => {
return warningStatus.show && warningStatus.type !== 'success';
}, [warningStatus.show, warningStatus.type]);
// This is only a placeholder for querying the model or file during the transition period.
const displayEvaluateStatus = (
params: MessageStatus,
options?: WarningStausOptions
) => {
setWarningStatus(
{
show: params.show,
title: '',
type: 'transition',
message: intl.formatMessage({ id: 'models.form.evaluating' })
},
options
);
};
useEffect(() => {
if (open) {
handleOnOpen();
form.current?.getGPUOptionList?.({
clusterId: initClusterId()
});
} else {
cancelEvaluate();
clearCahceFormValues();
}
return () => {
setSelectedModel({});
setWarningStatus({
show: false,
title: '',
message: []
});
};
}, [open, clusterList]);
return (
<GSDrawer
title={title}
open={open}
onClose={handleCancel}
destroyOnClose={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
zIndex={2000}
styles={{
body: {
height: 'calc(100vh - 57px)',
padding: '16px 0',
overflowX: 'hidden'
},
content: {
borderRadius: '6px 0 0 6px'
}
}}
width={width}
footer={false}
>
<div style={{ display: 'flex', height: '100%' }}>
{SEARCH_SOURCE.includes(props.source) &&
deploymentType === 'modelList' && (
<>
<ColWrapper>
<ColumnWrapper>
<SearchModel
hasLinuxWorker={hasLinuxWorker}
modelSource={props.source}
onSelectModel={handleOnSelectModel}
onSelectModelAfterEvaluate={
handleOnSelectModelAfterEvaluate
}
displayEvaluateStatus={displayEvaluateStatus}
gpuOptions={[]}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
</ColWrapper>
<ColWrapper>
<ColumnWrapper>
<ModelCard
selectedModel={selectedModel}
onCollapse={setCollapsed}
collapsed={collapsed}
modelSource={props.source}
setIsGGUF={handleSetIsGGUF}
></ModelCard>
{isGGUF && <GGUFResult></GGUFResult>}
</ColumnWrapper>
<Separator></Separator>
</ColWrapper>
</>
)}
<FormContext.Provider
value={{
isGGUF: isGGUF,
pageAction: action,
onValuesChange: onValuesChange
}}
>
<FormWrapper>
<ColumnWrapper
paddingBottom={warningStatus.show ? 170 : 50}
footer={
<>
<CompatibilityAlert
showClose={true}
onClose={() => {
setWarningStatus({
show: false,
message: ''
});
}}
warningStatus={warningStatus}
contentStyle={{ paddingInline: 0 }}
></CompatibilityAlert>
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
showOkBtn={!showExtraButton}
extra={
showExtraButton && (
<Button type="primary" onClick={handleSubmitAnyway}>
{intl.formatMessage({
id: 'models.form.submit.anyway'
})}
</Button>
)
}
style={ModalFooterStyle}
></ModalFooter>
</>
}
>
<>
{SEARCH_SOURCE.includes(source) &&
deploymentType === 'modelList' && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
</TitleWrapper>
)}
<DataForm
initialValues={initialValues}
source={source}
action={action}
clusterList={clusterList}
selectedModel={selectedModel}
onOk={handleOnOk}
ref={form}
isGGUF={isGGUF}
onBackendChange={handleBackendChange}
onValuesChange={onValuesChange}
></DataForm>
</>
</ColumnWrapper>
</FormWrapper>
</FormContext.Provider>
</div>
</GSDrawer>
);
};
export default AddModal;