feat: dynamic backends

This commit is contained in:
jialin
2025-10-13 14:40:26 +08:00
parent ee95fabc47
commit bca1e8140e
19 changed files with 325 additions and 177 deletions
+10
View File
@@ -23,3 +23,13 @@ export const clusterListAtom = atom<
value: number;
}[]
>([]);
export const backendOptionsAtom = atom<
{
value: string;
label: string;
default_backend_param: string[];
default_version: string;
versions: { label: string; value: string }[];
}[]
>([]);
+7 -2
View File
@@ -12,7 +12,12 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import InputWrapper from './wrapper/input';
const LabelWrapper = styled.div`
const LabelWrapper = styled.div.attrs({
className: 'seal-textarea-label'
})`
display: flex;
align-items: center;
justify-content: flex-start;
background-color: var(--ant-color-bg-container);
`;
@@ -110,7 +115,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
<InputWrapper>
<Wrapper
status={status}
label={<LabelWrapper>{label}</LabelWrapper>}
label={label}
isFocus={isFocus}
required={required}
description={description}
+3 -1
View File
@@ -154,7 +154,9 @@ export const Label = styled.div.attrs<{
`;
// inner
const Inner = styled.div`
const Inner = styled.div.attrs({
className: '__inner__'
})`
width: 100%;
display: flex;
`;
@@ -163,6 +163,9 @@ const InputWrapper = styled.div`
height: auto;
padding-right: 10px;
}
.ant-input-textarea-allow-clear.ant-input-affix-wrapper {
padding: 0;
}
`;
export default InputWrapper;
+3 -3
View File
@@ -97,10 +97,10 @@ const SelectWrapper = styled.div`
align-items: center;
height: 54px;
.ant-select-selection-search {
// top: 20px !important;
// inset-inline-start: ${INPUT_INNER_PADDING}px;
.ant-select-selection-wrap {
height: 100%;
}
&.ant-select-auto-complete {
.ant-select-selection-search {
padding-inline-start: ${INPUT_INNER_PADDING}px;
-52
View File
@@ -1,52 +0,0 @@
const MODEL_EVALUATIONS = '/model-evaluations';
let controller = new AbortController();
let signal = controller.signal;
self.onmessage = async (event) => {
const { list, modelSource, modelSourceMap } = event.data;
const repoList = list.map((item: any) => ({
source: modelSource,
...(modelSource === modelSourceMap.huggingface_value
? { huggingface_repo_id: item.name }
: { model_scope_model_id: item.name })
}));
try {
controller?.abort();
controller = new AbortController();
signal = controller.signal;
const response = await fetch(`v1/${MODEL_EVALUATIONS}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal,
body: JSON.stringify({ model_specs: repoList })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const evaluations = await response.json();
const { results } = evaluations;
const resultList = list.map((item: any, index: number) => {
return {
...item,
evaluateResult: results[index] || null
};
});
self.postMessage({ success: true, resultList });
} catch (error) {
self.postMessage({ success: false, resultList: list });
}
};
self.onmessage = (event) => {
if (event.data === 'abort') {
controller.abort();
}
};
+53
View File
@@ -20,6 +20,8 @@ export const MODEL_INSTANCE_API = '/model-instances';
export const MODEL_EVALUATIONS = '/model-evaluations';
export const BACKEND_LIST_API = '/inference-backend/list';
const setProxyUrl = (url: string) => {
return `/proxy?url=${encodeURIComponent(url)}`;
};
@@ -396,3 +398,54 @@ export async function evaluationsModelSpec(
})
};
}
export async function queryBackendList() {
// return request<{
// items: {
// backend_name: string;
// backend_show_name: string;
// from_config: boolean;
// default_version: string;
// default_backend_param: string[];
// versions: string[];
// }[];
// }>(BACKEND_LIST_API, {
// method: 'GET'
// });
return {
items: [
{
backend_name: 'vllm',
backend_show_name: 'vLLM',
from_config: false,
default_version: '0.10.1.1',
default_backend_param: null,
versions: ['0.10.1.1', '0.10.0', '0.9.2', '0.8.5', '0.8.3']
},
{
backend_name: 'ascend-mindie',
backend_show_name: 'Ascend MindIE',
from_config: false,
default_version: null,
default_backend_param: null,
versions: null
},
{
backend_name: 'custom',
backend_show_name: 'Custom',
from_config: false,
default_version: null,
default_backend_param: null,
versions: null
},
{
backend_name: 'test',
backend_show_name: null,
from_config: true,
default_version: 'v1',
default_backend_param: ['--host=0.0.0.0'],
versions: ['v1']
}
]
};
}
@@ -2,22 +2,17 @@ import IconFont from '@/components/icon-font';
import LabelSelector from '@/components/label-selector';
import ListInput from '@/components/list-input';
import CheckboxField from '@/components/seal-form/checkbox-field';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { Collapse, Form, FormInstance, Typography } from 'antd';
import _ from 'lodash';
import React, { useCallback, useMemo } from 'react';
import {
backendLabelMap,
backendParamsHolderTips,
backendTipsList,
getBackendParamsTips,
modelCategories,
modelSourceMap,
placementStrategyOptions,
ScheduleValueMap
} from '../config';
@@ -26,6 +21,7 @@ import BackendParameters, {
} from '../config/backend-parameters';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
import BackendFields from '../forms/backend-fields';
import dataformStyles from '../style/data-form.less';
import Performance from './performance';
@@ -35,8 +31,6 @@ interface AdvanceConfigProps {
gpuOptions: Array<any>;
action: PageActionType;
source: string;
backendOptions?: Global.BaseOption<string>[];
handleBackendChange?: (value: string) => void;
}
const placementStrategyTips = [
@@ -51,7 +45,7 @@ const placementStrategyTips = [
];
const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
const { form, isGGUF, gpuOptions, source, backendOptions, action } = props;
const { form, isGGUF, gpuOptions, source, action } = props;
const intl = useIntl();
const wokerSelector = Form.useWatch('worker_selector', form);
const EnviromentVars = Form.useWatch('env', form);
@@ -63,7 +57,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
const placement_strategy = Form.useWatch('placement_strategy', form);
const gpuSelectorIds = Form.useWatch(['gpu_selector', 'gpu_ids'], form);
const worker_selector = Form.useWatch('worker_selector', form);
const { onValuesChange } = useFormContext();
const { onValuesChange, onBackendChange, backendOptions } = useFormContext();
const paramsConfig = useMemo(() => {
return _.get(BackendParameters, backend, []);
@@ -154,47 +148,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
options={modelCategories}
></SealSelect>
</Form.Item>
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={props.handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
description={<TooltipList list={backendTipsList}></TooltipList>}
options={
backendOptions ?? [
{
label: backendLabelMap[backendOptionsMap.vllm],
value: backendOptionsMap.vllm,
disabled:
props.source === modelSourceMap.local_path_value
? false
: isGGUF
},
{
label: backendLabelMap[backendOptionsMap.ascendMindie],
value: backendOptionsMap.ascendMindie,
disabled:
props.source === modelSourceMap.local_path_value
? false
: isGGUF
},
{
label: backendLabelMap[backendOptionsMap.voxBox],
value: backendOptionsMap.voxBox,
disabled:
props.source === modelSourceMap.local_path_value
? false
: props.source === modelSourceMap.ollama_library_value ||
isGGUF
}
]
}
disabled={
action === PageAction.EDIT &&
props.source !== modelSourceMap.local_path_value
}
></SealSelect>
</Form.Item>
<BackendFields></BackendFields>
{scheduleType === ScheduleValueMap.Auto && (
<>
<Form.Item<FormData> name="placement_strategy">
@@ -257,51 +211,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
</>
)}
<Form.Item name="backend_version">
<SealInput.Input
placeholder={
backendParamsTips?.version
? `${intl.formatMessage({ id: 'common.help.eg' })} ${backendParamsTips?.version}`
: ''
}
onBlur={handleBackendVersionOnBlur}
label={intl.formatMessage({ id: 'models.form.backendVersion' })}
description={intl.formatMessage(
{
id: 'models.form.backendVersion.tips'
},
{
backend: backendLabelMap[backend],
version: backendParamsTips?.version
? `(${intl.formatMessage({ id: 'common.help.eg' })} ${backendParamsTips?.version})`
: '',
link: backendParamsTips?.releases && (
<span
style={{
marginLeft: 5
}}
>
<Typography.Link
className="flex-center"
style={{ display: 'inline' }}
href={backendParamsTips?.releases}
target="_blank"
>
<span>
{intl.formatMessage({ id: 'models.form.releases' })}
</span>
<IconFont
type="icon-external-link"
className="font-size-14 m-l-4"
></IconFont>
</Typography.Link>
</span>
)
}
)}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData> name="backend_parameters">
<ListInput
placeholder={
+10 -5
View File
@@ -9,7 +9,12 @@ import React, { forwardRef, useImperativeHandle } from 'react';
import { excludeFields, ScheduleValueMap, sourceOptions } from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import { FormContext } from '../config/form-context';
import { DeployFormKey, FormData, SourceType } from '../config/types';
import {
BackendOption,
DeployFormKey,
FormData,
SourceType
} from '../config/types';
import CatalogFrom from '../forms/catalog';
import HuggingFaceForm from '../forms/hugging-face';
import LocalPathForm from '../forms/local-path';
@@ -24,7 +29,7 @@ interface DataFormProps {
isGGUF: boolean;
formKey: DeployFormKey;
sourceDisable?: boolean;
backendOptions?: Global.BaseOption<string>[];
backendOptions: BackendOption[];
sourceList?: Global.BaseOption<string>[];
clusterList: Global.BaseOption<number>[];
fields?: string[];
@@ -41,7 +46,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
formKey,
initialValues,
sourceDisable = true,
backendOptions,
backendOptions = [],
sourceList,
clusterList = [],
fields = ['source'],
@@ -174,8 +179,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
value={{
isGGUF: isGGUF,
formKey: formKey,
source: props.source,
pageAction: action,
gpuOptions: gpuOptions,
backendOptions: backendOptions,
onValuesChange: onValuesChange,
onBackendChange: handleBackendChange
}}
@@ -276,8 +283,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
isGGUF={isGGUF}
action={action}
source={props.source}
backendOptions={backendOptions}
handleBackendChange={handleBackendChange}
></AdvanceConfig>
</Form>
</FormContext.Provider>
@@ -9,7 +9,7 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { defaultFormValues, deployFormKeyMap, modelSourceMap } from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import { FormData, SourceType } from '../config/types';
import { BackendOption, FormData, SourceType } from '../config/types';
import {
MessageStatus,
WarningStausOptions,
@@ -58,6 +58,7 @@ type AddModalProps = {
width?: string | number;
initialValues?: any;
deploymentType?: 'modelList' | 'modelFiles';
backendOptions: BackendOption[];
clusterList: Global.BaseOption<
number,
{ provider: string; state: string | number }
@@ -541,6 +542,7 @@ const AddModal: FC<AddModalProps> = (props) => {
onOk={handleOnOk}
ref={form}
isGGUF={isGGUF}
backendOptions={props.backendOptions}
onBackendChange={handleBackendChange}
onValuesChange={onValuesChange}
></DataForm>
@@ -57,6 +57,7 @@ import {
} from '../config/types';
import useFormInitialValues from '../hooks/use-form-initial-values';
import useModelsColumns from '../hooks/use-models-columns';
import useQueryBackends from '../hooks/use-query-backends';
import APIAccessInfoModal from './api-access-info';
import DeployModal from './deploy-modal';
import Instances from './instances';
@@ -123,6 +124,7 @@ const Models: React.FC<ModelsProps> = ({
loadend,
total
}) => {
const { backendOptions } = useQueryBackends();
const { generateFormValues, clusterList, getClusterList } =
useFormInitialValues();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
@@ -686,6 +688,7 @@ const Models: React.FC<ModelsProps> = ({
clusterList={clusterList}
onCancel={handleModalCancel}
onOk={handleModalOk}
backendOptions={backendOptions}
></UpdateModel>
<DeployModal
open={openDeployModal.show}
@@ -696,6 +699,7 @@ const Models: React.FC<ModelsProps> = ({
isGGUF={openDeployModal.isGGUF}
hasLinuxWorker={openDeployModal.hasLinuxWorker}
clusterList={clusterList}
backendOptions={backendOptions}
onCancel={handleDeployModalCancel}
onOk={handleCreateModel}
></DeployModal>
@@ -11,7 +11,7 @@ import {
updateIgnoreFields
} from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import { FormData } from '../config/types';
import { BackendOption, FormData } from '../config/types';
import { generateGPUSelector } from '../config/utils';
import { useCheckCompatibility } from '../hooks';
import ColumnWrapper from './column-wrapper';
@@ -26,6 +26,7 @@ type AddModalProps = {
data: FormData;
isGGUF: boolean;
};
backendOptions: BackendOption[];
clusterList: Global.BaseOption<
number,
{ provider: string; state: string | number }
@@ -325,6 +326,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
onOk={handleOk}
ref={formRef}
isGGUF={isGGUF}
backendOptions={props.backendOptions}
onBackendChange={handleAsyncBackendChange}
onValuesChange={handleManulOnValuesChange}
></DataForm>
+22 -3
View File
@@ -1,15 +1,16 @@
import { PageActionType } from '@/config/types';
import React from 'react';
import { DeployFormKey } from './types';
import { BackendOption, DeployFormKey } from './types';
interface FormContextProps {
isGGUF?: boolean;
formKey: DeployFormKey;
source?: string;
source: string;
pageAction: PageActionType;
gpuOptions?: any[];
backendOptions: BackendOption[];
onValuesChange?: (changedValues: any, allValues: any) => void;
onBackendChange?: (backend: string) => void;
onBackendChange: (backend: string) => void;
}
interface CatalogFormContextProps {
@@ -19,6 +20,10 @@ interface CatalogFormContextProps {
onQuantizationChange: (val: string) => void;
}
interface FormOuterContextProps {
sourceList?: Global.BaseOption<string>[];
}
export const FormContext = React.createContext<FormContextProps>(
{} as FormContextProps
);
@@ -27,6 +32,10 @@ export const CatalogFormContext = React.createContext<CatalogFormContextProps>(
{} as CatalogFormContextProps
);
export const FormOuterContext = React.createContext<FormOuterContextProps>(
{} as FormOuterContextProps
);
export const useFormContext = () => {
const context = React.useContext(FormContext);
if (!context) {
@@ -44,3 +53,13 @@ export const useCatalogFormContext = () => {
}
return context;
};
export const useFormOuterContext = () => {
const context = React.useContext(FormOuterContext);
if (!context) {
throw new Error(
'useFormOuterContext must be used within a FormOuterProvider'
);
}
return context;
};
+3 -1
View File
@@ -324,7 +324,9 @@ export const excludeFields = [
'scheduleType',
'placement_strategy',
'backend',
'gpu_selector'
'gpu_selector',
'run_command',
'image_name'
];
// ingore fields when compare old and new data
+10
View File
@@ -39,6 +39,8 @@ export type SourceType =
| 'ollama_library';
export interface FormData {
image_name?: string;
run_command?: string;
backend: string;
restart_on_error?: boolean;
env?: Record<string, any>;
@@ -242,3 +244,11 @@ export interface EvaluateResult {
};
};
}
export interface BackendOption {
value: string;
label: string;
default_backend_param: string[];
default_version: string;
versions: { label: string; value: string }[];
}
+134
View File
@@ -0,0 +1,134 @@
import IconFont from '@/components/icon-font';
import AutoComplete from '@/components/seal-form/auto-complete';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config';
import { useIntl } from '@umijs/max';
import { Form, Typography } from 'antd';
import React, { useMemo } from 'react';
import {
backendLabelMap,
backendTipsList,
getBackendParamsTips,
modelSourceMap
} from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const BackendFields: React.FC = () => {
const intl = useIntl();
const form = Form.useFormInstance();
const {
isGGUF,
formKey,
pageAction: action,
source,
gpuOptions,
onValuesChange,
backendOptions,
onBackendChange
} = useFormContext();
const backend = Form.useWatch('backend', form);
const handleBackendVersionOnBlur = () => {
onValuesChange?.({}, form.getFieldsValue());
};
const backendParamsTips = useMemo(() => {
return getBackendParamsTips(backend);
}, [backend]);
const backendVersions = useMemo(() => {
if (!backend || backend === backendOptionsMap.custom) {
return [];
}
return (
backendOptions.find((item) => item.value === backend)?.versions || []
);
}, [backend]);
return (
<>
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={onBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
description={<TooltipList list={backendTipsList}></TooltipList>}
options={backendOptions}
disabled={
action === PageAction.EDIT &&
source !== modelSourceMap.local_path_value
}
></SealSelect>
</Form.Item>
{backendOptionsMap.custom !== backend && (
<Form.Item name="backend_version">
<AutoComplete
options={backendVersions}
placeholder="enter or select a version"
onBlur={handleBackendVersionOnBlur}
label={intl.formatMessage({ id: 'models.form.backendVersion' })}
description={intl.formatMessage(
{
id: 'models.form.backendVersion.tips'
},
{
backend: backendLabelMap[backend],
version: backendParamsTips?.version
? `(${intl.formatMessage({ id: 'common.help.eg' })} ${backendParamsTips?.version})`
: '',
link: backendParamsTips?.releases && (
<span
style={{
marginLeft: 5
}}
>
<Typography.Link
className="flex-center"
style={{ display: 'inline' }}
href={backendParamsTips?.releases}
target="_blank"
>
<span>
{intl.formatMessage({ id: 'models.form.releases' })}
</span>
<IconFont
type="icon-external-link"
className="font-size-14 m-l-4"
></IconFont>
</Typography.Link>
</span>
)
}
)}
></AutoComplete>
</Form.Item>
)}
{backend === backendOptionsMap.custom && (
<>
<Form.Item<FormData> name="image_name" rules={[{ required: true }]}>
<SealInput.Input
required
allowClear
scaleSize={true}
label="Image Name"
></SealInput.Input>
</Form.Item>
<Form.Item<FormData> name="run_command" rules={[{ required: true }]}>
<SealInput.TextArea
required
scaleSize={true}
allowClear
label="Execution Command"
></SealInput.TextArea>
</Form.Item>
</>
)}
</>
);
};
export default BackendFields;
-9
View File
@@ -128,10 +128,6 @@ export const checkCurrentbackend = (data: {
return backendOptionsMap.voxBox;
}
if (isGGUF) {
return backendOptionsMap.llamaBox;
}
if (checkOnlyAscendNPU(gpuOptions)) {
return backendOptionsMap.ascendMindie;
}
@@ -229,11 +225,6 @@ export const useCheckCompatibility = () => {
return clusterNames.join(', ');
};
const getCurrentCluster = (id: number) => {
const cluster = clusterList.find?.((item) => item.value === id);
return cluster?.label || '';
};
const handleCheckCompatibility = (
evaluateResult: EvaluateResult | null
): MessageStatus => {
@@ -0,0 +1,41 @@
import { backendOptionsAtom } from '@/atoms/models';
import { useAtom } from 'jotai';
import { useEffect } from 'react';
import { queryBackendList } from '../apis';
export default function useQueryBackends() {
const [backendOptions, setBackendOptions] = useAtom(backendOptionsAtom);
const getBackendOptions = async () => {
try {
const res = await queryBackendList();
const list = res?.items?.map((item) => {
return {
value: item.backend_name,
label: item.backend_show_name,
default_backend_param: item.default_backend_param || [],
default_version: item.default_version,
versions: (item.versions || []).map((version) => ({
label: version,
value: version
}))
};
});
if (res?.items) {
setBackendOptions(list || []);
}
} catch (error) {
// ignore
setBackendOptions([]);
}
};
useEffect(() => {
getBackendOptions();
}, []);
return {
backendOptions,
getBackendOptions
};
}
+12 -4
View File
@@ -17,6 +17,7 @@ import {
import { SourceType } from '@/pages/llmodels/config/types';
import DownloadModal from '@/pages/llmodels/download';
import { useGenerateWorkerOptions } from '@/pages/llmodels/hooks/use-form-initial-values';
import useQueryBackends from '@/pages/llmodels/hooks/use-query-backends';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
@@ -64,7 +65,7 @@ const ModelFiles = () => {
watch: true,
contentForDelete: 'resources.modelfiles.modelfile'
});
const { backendOptions } = useQueryBackends();
const intl = useIntl();
const { showSuccess } = useAppUtils();
const [downloadModalStatus, setDownlaodMoalStatus] = useState<{
@@ -106,9 +107,15 @@ const ModelFiles = () => {
worker_id: value
});
};
const generateInitialValues = (record: ListItem, gpuOptions: any[]) => {
const checkIsGGUF = (record: ListItem) => {
const isGGUF = _.includes(record.resolved_paths?.[0], 'gguf');
const isOllama = !!record.ollama_library_model_name;
return isGGUF || isOllama;
};
const generateInitialValues = (record: ListItem, gpuOptions: any[]) => {
const isGGUF = checkIsGGUF(record);
const audioModelTag = identifyModelTask(
record.source,
record.resolved_paths?.[0]
@@ -140,12 +147,12 @@ const ModelFiles = () => {
: {},
name: extractFileName(name),
backend: checkCurrentbackend({
isGGUF: !audioModelTag && (isGGUF || isOllama),
isGGUF: !audioModelTag && isGGUF,
isAudio: !!audioModelTag,
gpuOptions: gpuOptions,
defaultBackend: backendOptionsMap.vllm
}),
isGGUF: !audioModelTag && (isGGUF || isOllama)
isGGUF: !audioModelTag && isGGUF
};
};
@@ -338,6 +345,7 @@ const ModelFiles = () => {
workerOptions={readyWorkers}
></DownloadModal>
<DeployModal
backendOptions={backendOptions}
deploymentType="modelFiles"
title={intl.formatMessage({ id: 'models.button.deploy' })}
onCancel={handleDeployModalCancel}