Files
gpustack-ui/src/pages/llmodels/components/deploy-modal.tsx
T

543 lines
15 KiB
TypeScript

import ModalFooter from '@/components/modal-footer';
import { PageActionType } from '@/config/types';
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd';
import _ from 'lodash';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import {
backendOptionsMap,
getSourceRepoConfigValue,
modelSourceMap
} from '../config';
import { FormContext } from '../config/form-context';
import { FormData, SourceType } from '../config/types';
import {
checkOnlyAscendNPU,
useCheckCompatibility,
useSelectModel
} from '../hooks';
import ColumnWrapper from './column-wrapper';
import CompatibilityAlert from './compatible-alert';
import DataForm from './data-form';
import HFModelFile from './hf-model-file';
import ModelCard from './model-card';
import OllamaTips from './ollama-tips';
import SearchModel from './search-model';
import Separator from './separator';
import TitleWrapper from './title-wrapper';
const resetFieldsByModel = [
'cpu_offloading',
'distributed_inference_across_workers',
'backend_version',
'backend_parameters',
'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;
gpuOptions: any[];
modelFileOptions: any[];
initialValues?: any;
deploymentType?: 'modelList' | 'modelFiles';
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
} = props || {};
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const {
handleShowCompatibleAlert,
setWarningStatus,
handleBackendChangeBefore,
cancelEvaluate,
handleOnValuesChange: handleOnValuesChangeBefore,
handleEvaluateOnChange,
warningStatus,
submitAnyway
} = useCheckCompatibility();
const { onSelectModel } = useSelectModel({ gpuOptions: props.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 isHolderRef = useRef<{
model: boolean;
file: boolean;
}>({
model: false,
file: false
});
const evaluateStateRef = useRef<{ state: EvaluateProccessType }>({
state: 'form'
});
const requestModelIdRef = useRef<number>(0);
/**
* Update the request model id to distinguish
* the evaluate request.
*/
const updateRequestModelId = () => {
requestModelIdRef.current += 1;
};
/**
*
* @param state target to distinguish the evaluate state
*/
const setEvaluteState = (state: EvaluateProccessType) => {
evaluateStateRef.current.state = state;
};
/**
*
* @param flag set the evaluate status of the model or file
*/
const setIsHolderRef = (flag: Record<string, boolean>) => {
isHolderRef.current = {
...isHolderRef.current,
...flag
};
};
const handleOnValuesChange = (data: {
changedValues: any;
allValues: any;
source: SourceType;
}) => {
setEvaluteState(EvaluateProccess.form);
handleOnValuesChangeBefore(data);
};
const getDefaultSpec = (item: any) => {
const defaultSpec = item.evaluateResult?.default_spec || {};
return _.omit(defaultSpec, [
'cpu_offloading',
'distributed_inference_across_workers'
]);
};
const getCategory = (item: any) => {
const categories = item.evaluateResult?.default_spec?.categories || [];
if (Array.isArray(categories)) {
return categories?.[0] || null;
}
return categories || null;
};
const handleSelectModelFile = async (item: any, evaluate?: boolean) => {
form.current?.form?.resetFields(resetFieldsByFile);
const modelInfo = onSelectModel(selectedModel, props.source);
/** display the selected model file information, but not
* unitl the evaluate result is ready
*/
form.current?.setFieldsValue?.({
file_name: item.fakeName,
...modelInfo,
categories: getCategory(item)
});
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 0);
});
if (item.fakeName) {
const currentModelId = requestModelIdRef.current;
setEvaluteState(EvaluateProccess.file);
const evaluateRes = await handleEvaluateOnChange?.({
changedValues: {},
allValues: form.current?.form?.getFieldsValue?.(),
source: props.source
});
if (currentModelId !== requestModelIdRef.current) {
// if the request model id has changed, do not update the form
return;
}
const defaultSpec = getDefaultSpec({
evaluateResult: evaluateRes
});
/**
* do not reset backend_parameters when select a model file
*/
const formBackendParameters =
form.current?.getFieldValue?.('backend_parameters') || [];
form.current?.setFieldsValue?.({
file_name: item.fakeName,
...defaultSpec,
...modelInfo,
backend_parameters:
formBackendParameters.length > 0
? formBackendParameters
: defaultSpec.backend_parameters || [],
categories: getCategory(item)
});
}
};
const handleOnSelectModel = (item: any, evaluate?: boolean) => {
/**
* evaluate: false means select a new model
* evaluate: true means select a model file from the evaluate result
*/
updateRequestModelId();
if (!evaluate) {
setEvaluteState(EvaluateProccess.model);
setSelectedModel(item);
form.current?.form?.resetFields(resetFieldsByModel);
const modelInfo = onSelectModel(item, props.source);
form.current?.setFieldsValue?.({
...modelInfo,
categories: getCategory(item)
});
}
if (!item.isGGUF) {
setIsGGUF(false);
const modelInfo = onSelectModel(item, props.source);
if (
!isHolderRef.current.model &&
evaluateStateRef.current.state === EvaluateProccess.model
) {
handleShowCompatibleAlert(item.evaluateResult);
form.current?.setFieldsValue?.({
...getDefaultSpec(item),
...modelInfo,
categories: getCategory(item)
});
}
}
};
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) => {
setIsGGUF(flag);
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 0);
});
if (flag) {
modelFileRef.current?.fetchModelFiles?.();
}
};
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 handleOnOpen = () => {
if (props.deploymentType === 'modelFiles') {
form.current?.form?.setFieldsValue({
...props.initialValues
});
handleOnValuesChange?.({
changedValues: {},
allValues: props.initialValues,
source: source
});
} else {
let backend = checkOnlyAscendNPU(props.gpuOptions)
? backendOptionsMap.ascendMindie
: backendOptionsMap.vllm;
if (source === modelSourceMap.ollama_library_value) {
backend = backendOptionsMap.llamaBox;
}
form.current?.setFieldValue?.('backend', backend);
setIsGGUF(source === modelSourceMap.ollama_library_value);
}
};
const showExtraButton = useMemo(() => {
return warningStatus.show && warningStatus.type !== 'success';
}, [warningStatus.show, warningStatus.type]);
const displayEvaluateStatus = (data: {
show?: boolean;
flag: Record<string, boolean>;
}) => {
setIsHolderRef(data.flag);
setWarningStatus({
show: isHolderRef.current.model || isHolderRef.current.file,
title: '',
type: 'transition',
message: intl.formatMessage({ id: 'models.form.evaluating' })
});
};
useEffect(() => {
if (open) {
handleOnOpen();
} else {
cancelEvaluate();
}
return () => {
setSelectedModel({});
setWarningStatus({
show: false,
title: '',
message: []
});
};
}, [open, props.gpuOptions.length]);
return (
<Drawer
title={
<div className="flex-between flex-center">
<span>{title}</span>
<Button type="text" size="small" onClick={handleCancel}>
<CloseOutlined></CloseOutlined>
</Button>
</div>
}
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}
displayEvaluateStatus={displayEvaluateStatus}
gpuOptions={props.gpuOptions}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
</ColWrapper>
<ColWrapper>
<ColumnWrapper>
<ModelCard
selectedModel={selectedModel}
onCollapse={setCollapsed}
collapsed={collapsed}
modelSource={props.source}
setIsGGUF={handleSetIsGGUF}
></ModelCard>
{isGGUF && (
<HFModelFile
ref={modelFileRef}
selectedModel={selectedModel}
modelSource={props.source}
onSelectFile={handleSelectModelFile}
collapsed={collapsed}
gpuOptions={props.gpuOptions}
></HFModelFile>
)}
</ColumnWrapper>
<Separator></Separator>
</ColWrapper>
</>
)}
<FormContext.Provider
value={{
isGGUF: isGGUF,
pageAction: action,
modelFileOptions: props.modelFileOptions,
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>
</>
}
>
<>
{source === modelSourceMap.ollama_library_value && (
<OllamaTips></OllamaTips>
)}
{SEARCH_SOURCE.includes(source) &&
deploymentType === 'modelList' && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
</TitleWrapper>
)}
<DataForm
initialValues={initialValues}
source={source}
action={action}
selectedModel={selectedModel}
onOk={handleOnOk}
ref={form}
isGGUF={isGGUF}
gpuOptions={props.gpuOptions}
modelFileOptions={props.modelFileOptions}
onBackendChange={handleBackendChange}
onValuesChange={onValuesChange}
></DataForm>
</>
</ColumnWrapper>
</FormWrapper>
</FormContext.Provider>
</div>
</Drawer>
);
};
export default AddModal;