565 lines
15 KiB
TypeScript
565 lines
15 KiB
TypeScript
import { clusterListAtom, workerListAtom } 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';
|
|
import { modelSourceMap, modelTaskMap } from '../config';
|
|
import { EvaluateResult, FormData } from '../config/types';
|
|
import {
|
|
backendOptionsMap,
|
|
BuiltInBackendOptions
|
|
} from '../constants/backend-parameters';
|
|
import { generateGPUIds } from '../utils';
|
|
import useCheckBackend from './use-check-backend';
|
|
import useRecognizeAudio from './use-recognize-audio';
|
|
|
|
export type MessageStatus = {
|
|
show: boolean;
|
|
title?: string;
|
|
type?: Global.MessageType;
|
|
isHtml?: boolean;
|
|
isDefault?: boolean;
|
|
message: string | string[];
|
|
evaluateResult?: EvaluateResult;
|
|
};
|
|
|
|
export interface ModelFileOption {
|
|
label: string;
|
|
value: number;
|
|
labels?: Record<string, string>;
|
|
parent: boolean;
|
|
repoId?: string;
|
|
fileName?: string;
|
|
[key: string]: any;
|
|
children?: ModelFileOption[];
|
|
}
|
|
|
|
export type WarningStausOptions = {
|
|
lockAfterUpdate?: boolean;
|
|
override?: boolean;
|
|
};
|
|
|
|
export const useGenerateWorkersModelFileOptions = () => {
|
|
const [modelFileOptions, setModelFileOptions] = useState<ModelFileOption[]>(
|
|
[]
|
|
);
|
|
|
|
const getModelFileList = async () => {
|
|
try {
|
|
const res = await queryModelFilesList({ page: -1 });
|
|
const list = res.items || [];
|
|
return list;
|
|
} catch (error) {
|
|
console.error('Error fetching model file list:', error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const generateWorkersModelFileOptions = (list: any[], workerList: any[]) => {
|
|
const workerFields = new Set(['name', 'id', 'ip', 'status']);
|
|
const workersMap = new Map<number, WorkerListItem>();
|
|
|
|
for (const item of workerList) {
|
|
if (!workersMap.has(item.id)) {
|
|
workersMap.set(item.id, item);
|
|
}
|
|
}
|
|
|
|
const result = Array.from(workersMap.values()).map((worker) => ({
|
|
label: worker.name,
|
|
value: worker.id,
|
|
labels: worker.labels,
|
|
parent: true,
|
|
children: list
|
|
.filter(
|
|
(item) =>
|
|
item.worker_id === worker.id && !!item.resolved_paths?.length
|
|
)
|
|
.map((item) => {
|
|
return {
|
|
label: item.resolved_paths[0] || '',
|
|
value: item.resolved_paths[0] || '',
|
|
parent: false,
|
|
repoId: item.huggingface_repo_id || item.model_scope_model_id || '',
|
|
fileName:
|
|
item.huggingface_filename ||
|
|
item.model_scope_file_path ||
|
|
item.local_path ||
|
|
'',
|
|
...item
|
|
};
|
|
}),
|
|
...Object.fromEntries(
|
|
Object.entries(worker).filter(([key]) => workerFields.has(key))
|
|
)
|
|
}));
|
|
|
|
const childrenList = result.reduce((acc: any[], cur) => {
|
|
if (cur.children) {
|
|
const list = cur.children.map((child: any) => ({
|
|
...child,
|
|
label: `${cur.label}${child.label}`,
|
|
value: child.value
|
|
}));
|
|
acc.push(...list);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
|
|
setModelFileOptions(result);
|
|
|
|
return childrenList;
|
|
};
|
|
|
|
return {
|
|
getModelFileList,
|
|
modelFileOptions,
|
|
generateWorkersModelFileOptions
|
|
};
|
|
};
|
|
|
|
export const useCheckCompatibility = () => {
|
|
const intl = useIntl();
|
|
const cacheFormValuesRef = useRef<any>({});
|
|
const checkTokenRef = useRef<any>(null);
|
|
const submitAnyway = useRef<boolean>(false);
|
|
const requestIdRef = useRef(0);
|
|
const updateStatusTimer = useRef<any>(null);
|
|
const isLockWarningStatus = useRef<boolean>(false);
|
|
const clusterList = useAtomValue(clusterListAtom);
|
|
const workerList = useAtomValue(workerListAtom);
|
|
const [warningStatus, setWarningStatus] = useState<MessageStatus>({
|
|
show: false,
|
|
title: '',
|
|
message: []
|
|
});
|
|
|
|
const updateRequestId = () => {
|
|
requestIdRef.current += 1;
|
|
return requestIdRef.current;
|
|
};
|
|
|
|
const lockWarningStatus = () => {
|
|
isLockWarningStatus.current = true;
|
|
};
|
|
|
|
const unlockWarningStatus = () => {
|
|
isLockWarningStatus.current = false;
|
|
};
|
|
|
|
const updateWarningStatus = (
|
|
params: MessageStatus,
|
|
options?: WarningStausOptions
|
|
) => {
|
|
const { lockAfterUpdate = false, override = false } = options || {};
|
|
console.log('updateWarningStatus', params, options);
|
|
|
|
setWarningStatus((prev: MessageStatus) => {
|
|
if (isLockWarningStatus.current && !override) {
|
|
return prev;
|
|
}
|
|
|
|
if (lockAfterUpdate) {
|
|
lockWarningStatus();
|
|
}
|
|
|
|
return params;
|
|
});
|
|
};
|
|
|
|
const handleEvaluate = async (data: any) => {
|
|
try {
|
|
// when no cluster selected, show warning and prompt user to add cluster first
|
|
console.log('handleEvaluate', data);
|
|
|
|
if (!data.cluster_id || workerList.length === 0) {
|
|
setWarningStatus({
|
|
show: true,
|
|
title: '',
|
|
type: 'warning',
|
|
message: !data.cluster_id
|
|
? intl.formatMessage({ id: 'noresult.resources.cluster' })
|
|
: intl.formatMessage({ id: 'noresult.resources.worker' })
|
|
});
|
|
return;
|
|
}
|
|
checkTokenRef.current?.cancel();
|
|
checkTokenRef.current = createAxiosToken();
|
|
setWarningStatus({
|
|
show: true,
|
|
title: '',
|
|
type: 'transition',
|
|
message: intl.formatMessage({ id: 'models.form.evaluating' })
|
|
});
|
|
const evalution = await evaluationsModelSpec(
|
|
{
|
|
cluster_id: data.cluster_id,
|
|
model_specs: [
|
|
{
|
|
..._.omit(data, ['scheduleType']),
|
|
categories: Array.isArray(data.categories)
|
|
? data.categories
|
|
: data.categories
|
|
? [data.categories]
|
|
: []
|
|
}
|
|
]
|
|
},
|
|
{
|
|
token: checkTokenRef.current.token
|
|
}
|
|
);
|
|
return evalution.results?.[0];
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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 handleCheckCompatibility = (
|
|
evaluateResult: EvaluateResult | null
|
|
): MessageStatus => {
|
|
console.log('handleCheckCompatibility', clusterList, evaluateResult);
|
|
if (!evaluateResult) {
|
|
return {
|
|
show: false,
|
|
message: ''
|
|
};
|
|
}
|
|
|
|
const {
|
|
compatible,
|
|
compatibility_messages = [],
|
|
scheduling_messages = [],
|
|
resource_claim_by_cluster_id,
|
|
cluster_id,
|
|
error,
|
|
error_message
|
|
} = evaluateResult || {};
|
|
|
|
// error message
|
|
if (error) {
|
|
return {
|
|
show: true,
|
|
type: 'danger',
|
|
message: `${intl.formatMessage({ id: 'models.search.evaluate.error' })}${error_message}`
|
|
};
|
|
}
|
|
|
|
const resourceClaimMap = new Map(
|
|
Object.entries(resource_claim_by_cluster_id || {})
|
|
);
|
|
|
|
// current cluster resource claim: {ram: number, vram: number}
|
|
const resource_claim = resourceClaimMap.get(`${cluster_id}`);
|
|
|
|
const hasClaim = resourceClaimMap.has(`${cluster_id}`);
|
|
|
|
let compatibilityMessage = compatibility_messages.join(' ');
|
|
|
|
compatibilityMessage = compatibilityMessage.startsWith(
|
|
`The model file path you specified does not exist on the GPUStack server. It's recommended`
|
|
)
|
|
? intl.formatMessage({ id: 'models.form.modelfile.notfound' })
|
|
: compatibilityMessage;
|
|
|
|
let msgData = {
|
|
title: scheduling_messages?.length > 0 ? compatibilityMessage : '',
|
|
message:
|
|
scheduling_messages?.length > 0
|
|
? scheduling_messages
|
|
: compatibilityMessage
|
|
};
|
|
|
|
let noResourceClaim = false;
|
|
|
|
if (hasClaim) {
|
|
const ram = convertFileSize(resource_claim?.ram || 0, 2);
|
|
const vram = convertFileSize(resource_claim?.vram || 0, 2);
|
|
let messageId = 'models.form.check.claims';
|
|
// when no ram and no vram
|
|
noResourceClaim = !ram && !vram;
|
|
|
|
if (!ram) {
|
|
messageId = 'models.form.check.claims2';
|
|
}
|
|
if (!vram) {
|
|
messageId = 'models.form.check.claims3';
|
|
}
|
|
msgData = {
|
|
title: intl.formatMessage({ id: 'models.form.check.passed' }),
|
|
message: intl.formatMessage({ id: messageId }, { ram, vram })
|
|
};
|
|
}
|
|
|
|
return {
|
|
show: noResourceClaim ? false : !compatible || hasClaim,
|
|
type: !compatible ? 'warning' : 'success',
|
|
...msgData
|
|
};
|
|
};
|
|
|
|
const handleShowCompatibleAlert = (
|
|
evaluateResult: EvaluateResult | null,
|
|
options?: WarningStausOptions
|
|
) => {
|
|
const result = handleCheckCompatibility(evaluateResult);
|
|
if (updateStatusTimer.current) {
|
|
clearTimeout(updateStatusTimer.current);
|
|
}
|
|
updateStatusTimer.current = setTimeout(() => {
|
|
updateWarningStatus(result, options);
|
|
}, 300);
|
|
};
|
|
|
|
const checkIsGGUFFileOrNotSupport = (localPath: string, allValues?: any) => {
|
|
const isBlobFile = localPath?.split('/').pop()?.includes('sha256');
|
|
const isOllamaModel = localPath?.includes('ollama');
|
|
const isGGUFFile = localPath?.endsWith?.('.gguf');
|
|
|
|
const isOllamaModelFile = isBlobFile || isOllamaModel;
|
|
return (
|
|
isGGUFFile ||
|
|
isOllamaModelFile ||
|
|
allValues?.huggingface_filename?.endsWith?.('.gguf') ||
|
|
allValues?.model_scope_file_path?.endsWith?.('.gguf')
|
|
);
|
|
};
|
|
|
|
const clearCacheFormValues = () => {
|
|
cacheFormValuesRef.current = {};
|
|
};
|
|
|
|
const updateShowWarning = (params: {
|
|
backend: string;
|
|
localPath: string;
|
|
source: string;
|
|
}) => {
|
|
const { backend, localPath, source } = params;
|
|
if (source !== modelSourceMap.local_path_value || !localPath) {
|
|
return {
|
|
show: false,
|
|
message: ''
|
|
};
|
|
}
|
|
|
|
const isGGUFFile = checkIsGGUFFileOrNotSupport(localPath);
|
|
|
|
let warningMessage = '';
|
|
if (isGGUFFile && BuiltInBackendOptions.includes(backend)) {
|
|
warningMessage = intl.formatMessage({
|
|
id: 'models.form.backend.warning'
|
|
});
|
|
} else if (isGGUFFile && !BuiltInBackendOptions.includes(backend)) {
|
|
warningMessage = '';
|
|
}
|
|
clearCacheFormValues();
|
|
return {
|
|
show: !!warningMessage,
|
|
isHtml: true,
|
|
message: warningMessage
|
|
};
|
|
};
|
|
|
|
const handleUpdateWarning = (params: {
|
|
backend: string;
|
|
localPath: string;
|
|
source: string;
|
|
}) => {
|
|
const warningMessage = updateShowWarning(params);
|
|
return warningMessage;
|
|
};
|
|
|
|
const handleDoEvalute = async (formData: FormData) => {
|
|
const currentRequestId = updateRequestId();
|
|
const evalutionData = await handleEvaluate(formData);
|
|
if (currentRequestId === requestIdRef.current && evalutionData) {
|
|
handleShowCompatibleAlert?.(evalutionData);
|
|
return evalutionData;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const noLocalPathValue = (allValues: any) => {
|
|
return (
|
|
allValues.source === modelSourceMap.local_path_value &&
|
|
!allValues.local_path
|
|
);
|
|
};
|
|
|
|
const handleOnValuesChange = async (params: {
|
|
changedValues: any;
|
|
allValues: any;
|
|
source: string;
|
|
}) => {
|
|
const { allValues, source } = params;
|
|
if (
|
|
_.isEqual(cacheFormValuesRef.current, allValues) ||
|
|
noLocalPathValue(allValues) ||
|
|
!allValues.replicas
|
|
) {
|
|
console.log('No changes detected, skipping evaluation.');
|
|
return;
|
|
}
|
|
|
|
// when custom backend, and no run_command or image_name, skip evaluate
|
|
if (
|
|
backendOptionsMap.custom === allValues.backend &&
|
|
(!allValues.run_command || !allValues.image_name)
|
|
) {
|
|
setWarningStatus({
|
|
show: false,
|
|
type: 'warning',
|
|
isHtml: true,
|
|
message: ''
|
|
});
|
|
return;
|
|
}
|
|
|
|
// check gguf: for online and local path
|
|
if (
|
|
checkIsGGUFFileOrNotSupport(allValues.local_path, allValues) &&
|
|
BuiltInBackendOptions.includes(allValues.backend)
|
|
) {
|
|
clearCacheFormValues();
|
|
setWarningStatus({
|
|
show: true,
|
|
type: 'warning',
|
|
isHtml: true,
|
|
message: intl.formatMessage({
|
|
id: 'models.form.backend.warning'
|
|
})
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
cacheFormValuesRef.current = _.cloneDeep(allValues);
|
|
const gpuSelector = generateGPUIds(allValues);
|
|
return await handleDoEvalute({
|
|
...allValues,
|
|
...gpuSelector,
|
|
replicas: allValues.replicas || 0
|
|
});
|
|
};
|
|
|
|
// trigger from local_path change or backend change
|
|
const handleBackendChangeBefore = (params: {
|
|
local_path: string;
|
|
backend: string;
|
|
source: string;
|
|
}) => {
|
|
const { local_path, backend, source } = params;
|
|
|
|
const res = handleUpdateWarning?.({
|
|
backend,
|
|
localPath: local_path,
|
|
source: source
|
|
});
|
|
|
|
setWarningStatus?.(res);
|
|
return res;
|
|
};
|
|
|
|
const cancelEvaluate = () => {
|
|
// update the requestId to cancel the current evaluation
|
|
updateRequestId();
|
|
checkTokenRef.current?.cancel();
|
|
checkTokenRef.current = null;
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
cancelEvaluate();
|
|
clearCacheFormValues();
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
handleShowCompatibleAlert,
|
|
handleUpdateWarning,
|
|
handleDoEvalute,
|
|
handleEvaluate,
|
|
setWarningStatus: updateWarningStatus,
|
|
unlockWarningStatus,
|
|
cancelEvaluate,
|
|
handleBackendChangeBefore,
|
|
handleOnValuesChange: handleOnValuesChange,
|
|
handleEvaluateOnChange: handleOnValuesChange,
|
|
clearCacheFormValues,
|
|
warningStatus,
|
|
checkTokenRef,
|
|
submitAnyway
|
|
};
|
|
};
|
|
|
|
export const useSelectModel = (data: { gpuOptions: any[] }) => {
|
|
const { checkCurrentbackend } = useCheckBackend();
|
|
const { recognizeAudioModel } = useRecognizeAudio();
|
|
|
|
// just for setting the model name or repo_id, and the backend, Since the model type is fixed.
|
|
const { gpuOptions } = data;
|
|
|
|
const onSelectModel = (
|
|
selectModel: any,
|
|
options: {
|
|
source: string;
|
|
defaultBackend?: string;
|
|
flatBackendOptions?: any[];
|
|
}
|
|
) => {
|
|
const { source, defaultBackend, flatBackendOptions } = options;
|
|
let name = _.split(selectModel.name, '/').slice(-1)[0];
|
|
const reg = /(-gguf)$/i;
|
|
name = _.toLower(name).replace(reg, '');
|
|
|
|
const modelTaskData = recognizeAudioModel(selectModel, source);
|
|
|
|
const backend = checkCurrentbackend({
|
|
defaultBackend: defaultBackend || backendOptionsMap.vllm,
|
|
isAudio: modelTaskData.type === modelTaskMap.audio,
|
|
isGGUF: selectModel.isGGUF,
|
|
gpuOptions: gpuOptions
|
|
});
|
|
|
|
const selectedBackend = flatBackendOptions?.find(
|
|
(item) => item.value === backend
|
|
);
|
|
console.log('selectedBackend=========', selectedBackend);
|
|
return {
|
|
...(source === modelSourceMap.huggingface_value
|
|
? { huggingface_repo_id: selectModel.name }
|
|
: {}),
|
|
...(source === modelSourceMap.modelscope_value
|
|
? { model_scope_model_id: selectModel.name }
|
|
: {}),
|
|
...modelTaskData,
|
|
env: {
|
|
...(selectedBackend?.default_env || {})
|
|
},
|
|
backend_parameters: [...(selectedBackend?.default_backend_param || [])],
|
|
name: name,
|
|
source: source,
|
|
backend: backend
|
|
};
|
|
};
|
|
|
|
return {
|
|
onSelectModel
|
|
};
|
|
};
|