refactor: model instance item
This commit is contained in:
Vendored
+3
-1
@@ -69,7 +69,7 @@ declare namespace Global {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface ActionItem {
|
||||
interface ActionItem<T> {
|
||||
label: string;
|
||||
key: string;
|
||||
icon: React.ReactNode;
|
||||
@@ -77,6 +77,8 @@ declare namespace Global {
|
||||
props?: {
|
||||
danger?: boolean;
|
||||
};
|
||||
visible?: (record: T) => boolean;
|
||||
disabled?: (record: T) => boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function useActions<T>(actions: Global.ActionItem<T>[], ctx: T) {
|
||||
return actions
|
||||
.filter((action) => {
|
||||
return action.visible ? action.visible(ctx) : true;
|
||||
})
|
||||
.map((action) => ({
|
||||
...action,
|
||||
disabled: action.disabled?.(ctx)
|
||||
}));
|
||||
}
|
||||
+8
-8
@@ -5,14 +5,14 @@ import { useIntl } from '@umijs/max';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ColumnWrapper from '../../_components/column-wrapper';
|
||||
import HFModelFile from '../components/model-source/hf-model-file';
|
||||
import ModelCard from '../components/model-source/model-card';
|
||||
import SearchModel from '../components/model-source/search-model';
|
||||
import Separator from '../components/separator';
|
||||
import TitleWrapper from '../components/title-wrapper';
|
||||
import { modelSourceMap } from '../config';
|
||||
import { FormData } from '../config/types';
|
||||
import ColumnWrapper from '../../../_components/column-wrapper';
|
||||
import { modelSourceMap } from '../../config';
|
||||
import { FormData } from '../../config/types';
|
||||
import HFModelFile from '../model-source/hf-model-file';
|
||||
import ModelCard from '../model-source/model-card';
|
||||
import SearchModel from '../model-source/search-model';
|
||||
import Separator from '../separator';
|
||||
import TitleWrapper from '../title-wrapper';
|
||||
import TargetForm from './target-form';
|
||||
|
||||
type AddModalProps = {
|
||||
+2
-2
@@ -14,8 +14,8 @@ import React, {
|
||||
useImperativeHandle,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import { localPathTipsList, modelSourceMap, sourceOptions } from '../config';
|
||||
import { useGenerateWorkersModelFileOptions } from '../hooks';
|
||||
import { localPathTipsList, modelSourceMap, sourceOptions } from '../../config';
|
||||
import { useGenerateWorkersModelFileOptions } from '../../hooks';
|
||||
|
||||
type EmptyObject = Record<never, never>;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||
import useDownloadStream from '@/hooks/use-download-stream';
|
||||
import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark';
|
||||
import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Progress, notification } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { MODEL_INSTANCE_API } from '../../apis';
|
||||
import { InstanceStatusMap, modelCategoriesMap } from '../../config';
|
||||
import { ListItem, ModelInstanceListItem } from '../../config/types';
|
||||
|
||||
const childActionList = [
|
||||
{
|
||||
label: 'common.button.viewlog',
|
||||
key: 'viewlog',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <IconFont type="icon-logs" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.downloadLog',
|
||||
key: 'download',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <DownloadOutlined />
|
||||
},
|
||||
{
|
||||
label: 'models.table.instance.benchmark',
|
||||
key: 'benchmark',
|
||||
status: [InstanceStatusMap.Running],
|
||||
icon: <IconFont type="icon-speed" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.delrecreate',
|
||||
key: 'delete',
|
||||
props: {
|
||||
danger: true
|
||||
},
|
||||
icon: <DeleteOutlined />
|
||||
}
|
||||
];
|
||||
|
||||
interface ActionsCellProps {
|
||||
record: ModelInstanceListItem;
|
||||
modelData: ListItem;
|
||||
onSelect: (val: string, record: ModelInstanceListItem) => void;
|
||||
}
|
||||
|
||||
const ActionsCell: React.FC<ActionsCellProps> = ({
|
||||
record,
|
||||
modelData,
|
||||
onSelect
|
||||
}) => {
|
||||
const { runBenchmarkOnInstance } = useBenchmarkTargetInstance();
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
stack: { threshold: 1 }
|
||||
});
|
||||
const { downloadStream } = useDownloadStream();
|
||||
const intl = useIntl();
|
||||
|
||||
const createFileName = (name: string) => {
|
||||
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
const fileName = `${name}_${timestamp}.txt`;
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const renderMessage = (title: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 280,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const downloadNotification = (
|
||||
data: HandlerOptions & {
|
||||
filename: string;
|
||||
duration?: number;
|
||||
chunkRequestRef: any;
|
||||
}
|
||||
) => {
|
||||
api.open({
|
||||
duration: data.duration,
|
||||
message: renderMessage(data.filename),
|
||||
key: data.filename,
|
||||
closeIcon: (
|
||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||
),
|
||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||
onClose() {
|
||||
data.chunkRequestRef?.current?.abort();
|
||||
notification.destroy?.(data.filename);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnSelect = (val: string) => {
|
||||
if (val === 'benchmark') {
|
||||
runBenchmarkOnInstance(record);
|
||||
} else if (val === 'download') {
|
||||
downloadStream({
|
||||
url: `${MODEL_INSTANCE_API}/${record.id}/logs`,
|
||||
filename: createFileName(record.name),
|
||||
downloadNotification
|
||||
});
|
||||
} else {
|
||||
onSelect(val, record);
|
||||
}
|
||||
};
|
||||
|
||||
const actionItems = childActionList.filter((action: any) => {
|
||||
if (action.key === 'benchmark') {
|
||||
return (
|
||||
action.status.includes(record.state) &&
|
||||
modelData?.categories?.includes(modelCategoriesMap.llm)
|
||||
);
|
||||
}
|
||||
if (action.status && action.status.length > 0) {
|
||||
return action.status.includes(record.state);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<DropdownButtons
|
||||
items={actionItems}
|
||||
onSelect={handleOnSelect}
|
||||
></DropdownButtons>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
||||
@@ -0,0 +1,80 @@
|
||||
import InfoColumn from '@/components/simple-table/info-column';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { ModelInstanceListItem } from '../../config/types';
|
||||
|
||||
const fieldList = [
|
||||
{
|
||||
label: 'CPU',
|
||||
key: 'cpuoffload',
|
||||
locale: false
|
||||
},
|
||||
{
|
||||
label: 'GPU',
|
||||
key: 'gpuoffload',
|
||||
locale: false
|
||||
}
|
||||
];
|
||||
|
||||
interface CPUOffloadingCellProps {
|
||||
record: ModelInstanceListItem;
|
||||
}
|
||||
const CPUOffloadingCell: React.FC<CPUOffloadingCellProps> = ({ record }) => {
|
||||
const intl = useIntl();
|
||||
const { total_layers, offload_layers } =
|
||||
record?.computed_resource_claim || {};
|
||||
|
||||
if (total_layers === offload_layers || !total_layers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const offloadData = {
|
||||
cpuoffload: `${
|
||||
_.subtract(total_layers, offload_layers) || 0
|
||||
} ${intl.formatMessage({
|
||||
id: 'models.table.layers'
|
||||
})}`,
|
||||
gpuoffload: `${offload_layers} ${intl.formatMessage({
|
||||
id: 'models.table.layers'
|
||||
})}`
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
styles={{
|
||||
container: {
|
||||
paddingInline: 12
|
||||
}
|
||||
}}
|
||||
title={<InfoColumn fieldList={fieldList} data={offloadData}></InfoColumn>}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="cyan"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.cpuoffload'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CPUOffloadingCell;
|
||||
@@ -0,0 +1,184 @@
|
||||
import { TooltipOverlayScroller } from '@/components/overlay-scroller';
|
||||
import SimpleTabel, { ColumnProps } from '@/components/simple-table';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
DistributedServerItem,
|
||||
DistributedServers,
|
||||
ModelInstanceListItem
|
||||
} from '../../config/types';
|
||||
|
||||
const GPUIndexWrapper = styled.span`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
interface DistributeInfoCellProps {
|
||||
record: ModelInstanceListItem;
|
||||
workerList: WorkerListItem[];
|
||||
}
|
||||
|
||||
const renderGpuIndexs = (gpuIndexes: number[]) => {
|
||||
return (
|
||||
<GPUIndexWrapper>
|
||||
{_.chunk(gpuIndexes, 8).map((item: number[], index: number) => {
|
||||
return <span key={index}>{item.join(',')}</span>;
|
||||
})}
|
||||
</GPUIndexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const distributeCols: ColumnProps[] = [
|
||||
{
|
||||
title: 'Worker',
|
||||
key: 'worker_name',
|
||||
style: {
|
||||
wordBreak: 'break-word'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
key: 'worker_ip',
|
||||
render: ({ row }) => {
|
||||
return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'models.table.gpuindex',
|
||||
locale: true,
|
||||
key: 'gpu_index',
|
||||
render: ({ row }) => {
|
||||
const list = row.gpu_index?.sort((a: number, b: number) => a - b) || [];
|
||||
return row.is_main ? (
|
||||
<>
|
||||
{renderGpuIndexs(list)}
|
||||
<span>(main)</span>
|
||||
</>
|
||||
) : (
|
||||
renderGpuIndexs(list)
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'models.table.vram.allocated',
|
||||
locale: true,
|
||||
key: 'vram',
|
||||
render: ({ rowIndex, row, dataList }) => {
|
||||
return convertFileSize(row.vram, 1);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const calcTotalVram = (vram: Record<string, number>) => {
|
||||
return _.sum(_.values(vram));
|
||||
};
|
||||
|
||||
const DistributedServerList: React.FC<DistributeInfoCellProps> = ({
|
||||
record,
|
||||
workerList
|
||||
}) => {
|
||||
const severList: DistributedServerItem[] =
|
||||
record?.distributed_servers?.subordinate_workers || [];
|
||||
|
||||
const list = _.map(severList, (item: any) => {
|
||||
const data = _.find(workerList, { id: item.worker_id });
|
||||
return {
|
||||
worker_name: data?.name,
|
||||
worker_ip: data?.ip,
|
||||
port: '',
|
||||
is_main: false,
|
||||
vram: calcTotalVram(item.computed_resource_claim?.vram || {}),
|
||||
gpu_index: _.keys(item.computed_resource_claim?.vram)
|
||||
.map((i: string) => Number(i))
|
||||
.sort((a: number, b: number) => a - b)
|
||||
};
|
||||
});
|
||||
|
||||
const mainWorker = [
|
||||
{
|
||||
worker_name: `${record.worker_name}`,
|
||||
worker_ip: `${record.worker_ip}`,
|
||||
port: '',
|
||||
vram: calcTotalVram(record.computed_resource_claim?.vram || {}),
|
||||
is_main: true,
|
||||
gpu_index: record.gpu_indexes?.sort((a: number, b: number) => a - b)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SimpleTabel
|
||||
rowKey="worker_name"
|
||||
columns={distributeCols}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
></SimpleTabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DistributeInfoCell: React.FC<{
|
||||
record: ModelInstanceListItem;
|
||||
workerList: WorkerListItem[];
|
||||
}> = ({ record, workerList }) => {
|
||||
const intl = useIntl();
|
||||
const distributed_servers: DistributedServers | undefined =
|
||||
record?.distributed_servers;
|
||||
|
||||
const severList: DistributedServerItem[] =
|
||||
distributed_servers?.subordinate_workers || [];
|
||||
|
||||
if (!severList.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TooltipOverlayScroller
|
||||
toolTipProps={{
|
||||
styles: {
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '520px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<DistributedServerList
|
||||
record={record}
|
||||
workerList={workerList}
|
||||
></DistributedServerList>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="processing"
|
||||
style={{
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.acrossworker'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</TooltipOverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributeInfoCell;
|
||||
@@ -0,0 +1,179 @@
|
||||
import SimpleTabel, { ColumnProps } from '@/components/simple-table';
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
||||
import { Progress, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { InstanceStatusMap, status } from '../../config';
|
||||
import { generateSource } from '../../config/button-actions';
|
||||
import {
|
||||
DistributedServerItem,
|
||||
DistributedServers,
|
||||
ModelInstanceListItem
|
||||
} from '../../config/types';
|
||||
import { backendOptionsMap } from '../../constants/backend-parameters';
|
||||
|
||||
interface DownloadingStatusProps {
|
||||
distributed_servers?: DistributedServers;
|
||||
workerList: WorkerListItem[];
|
||||
record: ModelInstanceListItem;
|
||||
backend?: string;
|
||||
}
|
||||
|
||||
const statusColumn: ColumnProps[] = [
|
||||
{
|
||||
title: 'models.table.download.progress',
|
||||
locale: true,
|
||||
key: 'download_progress',
|
||||
render: ({ row }) => {
|
||||
return (
|
||||
<StatusTag
|
||||
download={{
|
||||
percent: row.download_progress
|
||||
}}
|
||||
statusValue={{
|
||||
status: row.download_progress
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[InstanceStatusMap.Initializing],
|
||||
text: row.download_progress,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
const downloadList: ColumnProps[] = [
|
||||
{
|
||||
title: 'resources.worker',
|
||||
locale: true,
|
||||
key: 'worker_name',
|
||||
width: 280
|
||||
},
|
||||
...statusColumn
|
||||
];
|
||||
|
||||
const draftModelDownloadList: ColumnProps[] = [
|
||||
{
|
||||
title: 'models.form.draftModel',
|
||||
locale: true,
|
||||
key: 'draft_model',
|
||||
style: {
|
||||
wordBreak: 'break-word'
|
||||
},
|
||||
width: 280
|
||||
},
|
||||
...statusColumn
|
||||
];
|
||||
|
||||
const DownloadingTips = (props: {
|
||||
severList: any[];
|
||||
record: ModelInstanceListItem;
|
||||
workerList: WorkerListItem[];
|
||||
}) => {
|
||||
const { severList, record, workerList } = props;
|
||||
if (!severList.length && !record.draft_model_download_progress) {
|
||||
return null;
|
||||
}
|
||||
const list = _.map(severList, (item: any) => {
|
||||
const data = _.find(workerList, { id: item.worker_id });
|
||||
return {
|
||||
worker_name: data?.name,
|
||||
worker_ip: data?.ip,
|
||||
download_progress: _.round(item.download_progress, 2)
|
||||
};
|
||||
});
|
||||
|
||||
const mainWorker = [
|
||||
{
|
||||
worker_name: `${record.worker_name}`,
|
||||
worker_ip: `${record.worker_ip}`,
|
||||
download_progress: _.round(record.download_progress, 2)
|
||||
}
|
||||
];
|
||||
|
||||
const draftModelList = [];
|
||||
if (record.draft_model_download_progress > 0) {
|
||||
draftModelList.push({
|
||||
draft_model: generateSource(record.draft_model_source),
|
||||
download_progress: _.round(record.draft_model_download_progress, 2)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{severList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={downloadList}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
|
||||
{draftModelList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={draftModelDownloadList}
|
||||
dataSource={[...draftModelList]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DownloadingStatus: React.FC<DownloadingStatusProps> = (props) => {
|
||||
const { distributed_servers, workerList, record, backend } = props;
|
||||
|
||||
const severList: DistributedServerItem[] =
|
||||
distributed_servers?.subordinate_workers || [];
|
||||
|
||||
const isWorkerNotDownloading =
|
||||
record.state !== InstanceStatusMap.Downloading ||
|
||||
!severList.length ||
|
||||
backend === backendOptionsMap.llamaBox;
|
||||
|
||||
const isDraftModeNotDownloading =
|
||||
!record.draft_model_download_progress ||
|
||||
record.draft_model_download_progress >= 100;
|
||||
|
||||
if (isWorkerNotDownloading && isDraftModeNotDownloading) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
arrow={true}
|
||||
styles={{
|
||||
container: {
|
||||
width: 360,
|
||||
backgroundColor: 'var(--color-spotlight-bg)'
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
root: 'light-downloading-tooltip'
|
||||
}}
|
||||
title={
|
||||
<DownloadingTips
|
||||
severList={severList}
|
||||
workerList={workerList}
|
||||
record={record}
|
||||
></DownloadingTips>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
showInfo={false}
|
||||
type="circle"
|
||||
size={16}
|
||||
strokeColor="var(--ant-color-success)"
|
||||
percent={
|
||||
_.find(severList, (item: any) => item.download_progress < 100)
|
||||
?.download_progress ||
|
||||
record.draft_model_download_progress ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadingStatus;
|
||||
@@ -0,0 +1,61 @@
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import {
|
||||
InstanceStatusMap,
|
||||
InstanceStatusMapValue,
|
||||
status
|
||||
} from '../../config';
|
||||
import { ModelInstanceListItem } from '../../config/types';
|
||||
|
||||
interface InstanceStatusProps {
|
||||
record: ModelInstanceListItem;
|
||||
onSelect: (val: string, record: ModelInstanceListItem) => void;
|
||||
}
|
||||
|
||||
const InstanceStatusTag: React.FC<InstanceStatusProps> = ({
|
||||
record,
|
||||
onSelect
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
if (!record.state) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<StatusTag
|
||||
download={
|
||||
record.state === InstanceStatusMap.Downloading
|
||||
? { percent: record.download_progress }
|
||||
: undefined
|
||||
}
|
||||
extra={
|
||||
record.state === InstanceStatusMap.Error && record.worker_id ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
onClick={() => onSelect('viewlog', record)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'models.list.more.logs' })}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
statusValue={{
|
||||
status:
|
||||
record.state === InstanceStatusMap.Downloading &&
|
||||
record.download_progress === 100
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[record.state],
|
||||
text: InstanceStatusMapValue[record.state],
|
||||
message:
|
||||
record.state === InstanceStatusMap.Downloading &&
|
||||
record.download_progress === 100
|
||||
? ''
|
||||
: record.state_message
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceStatusTag;
|
||||
@@ -0,0 +1,133 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import {
|
||||
HddFilled,
|
||||
InfoCircleOutlined,
|
||||
PieChartFilled,
|
||||
ThunderboltFilled
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import { ModelInstanceListItem } from '../../config/types';
|
||||
import '../../style/instance-item.less';
|
||||
|
||||
interface NameCellProps {
|
||||
record: ModelInstanceListItem;
|
||||
modelData: any;
|
||||
defaultOpenId?: string;
|
||||
}
|
||||
|
||||
const calcTotalVram = (vram: Record<string, number>) => {
|
||||
return _.sum(_.values(vram));
|
||||
};
|
||||
|
||||
const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
|
||||
const intl = useIntl();
|
||||
let workerIp = '-';
|
||||
if (record.worker_ip) {
|
||||
workerIp = record.port
|
||||
? `${record.worker_ip}:${record.port}`
|
||||
: record.worker_ip;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>{record.worker_name}</div>
|
||||
<div className="flex-center">
|
||||
<HddFilled className="m-r-5" />
|
||||
{workerIp}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<IconFont type="icon-filled-gpu" className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.gpuindex' })}: [
|
||||
{_.join(
|
||||
record.gpu_indexes?.sort?.((a, b) => a - b),
|
||||
','
|
||||
)}
|
||||
]
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<ThunderboltFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.form.backend' })}:{' '}
|
||||
{record?.backend || modelData?.backend || ''}
|
||||
{record.backend_version || modelData?.backend_version
|
||||
? `(${record.backend_version || modelData?.backend_version})`
|
||||
: ''}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<PieChartFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '}
|
||||
{convertFileSize(
|
||||
record.computed_resource_claim?.vram
|
||||
? calcTotalVram(record.computed_resource_claim?.vram)
|
||||
: 0,
|
||||
1
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkerInfo = (props: {
|
||||
title: React.ReactNode;
|
||||
defaultOpen: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(props.defaultOpen);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultOpen) {
|
||||
setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [props.defaultOpen]);
|
||||
|
||||
return (
|
||||
<span className="server-info-wrapper">
|
||||
<Tooltip
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={props.title}
|
||||
styles={{
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="server-info">
|
||||
<InfoCircleOutlined />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NameCell: React.FC<NameCellProps> = ({
|
||||
record,
|
||||
modelData,
|
||||
defaultOpenId
|
||||
}) => {
|
||||
return (
|
||||
<span className="flex-center instance-name">
|
||||
<AutoTooltip title={record.name} ghost>
|
||||
<span className="m-r-5">{record.name}</span>
|
||||
</AutoTooltip>
|
||||
{!!record.worker_id && (
|
||||
<WorkerInfo
|
||||
title={
|
||||
<WorkerInfoContent
|
||||
record={record}
|
||||
modelData={modelData}
|
||||
></WorkerInfoContent>
|
||||
}
|
||||
defaultOpen={defaultOpenId === record.name}
|
||||
></WorkerInfo>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameCell;
|
||||
@@ -1,48 +1,17 @@
|
||||
import { systemConfigAtom } from '@/atoms/system';
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { TooltipOverlayScroller } from '@/components/overlay-scroller';
|
||||
import RowChildren from '@/components/seal-table/components/row-children';
|
||||
import SimpleTabel, { ColumnProps } from '@/components/simple-table';
|
||||
import InfoColumn from '@/components/simple-table/info-column';
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
||||
import useDownloadStream from '@/hooks/use-download-stream';
|
||||
import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark';
|
||||
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
HddFilled,
|
||||
InfoCircleOutlined,
|
||||
PieChartFilled,
|
||||
ThunderboltFilled
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Col, Progress, Row, Tooltip, notification } from 'antd';
|
||||
import { Col, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { MODEL_INSTANCE_API } from '../../apis';
|
||||
import {
|
||||
InstanceStatusMap,
|
||||
InstanceStatusMapValue,
|
||||
modelCategoriesMap,
|
||||
status
|
||||
} from '../../config';
|
||||
import { generateSource } from '../../config/button-actions';
|
||||
import {
|
||||
DistributedServerItem,
|
||||
DistributedServers,
|
||||
ModelInstanceListItem
|
||||
} from '../../config/types';
|
||||
import { backendOptionsMap } from '../../constants/backend-parameters';
|
||||
import React from 'react';
|
||||
import { ModelInstanceListItem } from '../../config/types';
|
||||
import '../../style/instance-item.less';
|
||||
import ActionsCell from '../instance-cells/actions-cell';
|
||||
import CPUOffloadingCell from '../instance-cells/cpu-offloading-cell';
|
||||
import DistributeInfoCell from '../instance-cells/distribute-info-cell';
|
||||
import DownloadingStatusCell from '../instance-cells/downloading-status-cell';
|
||||
import InstanceStatusCell from '../instance-cells/instance-status-cell';
|
||||
import NameCell from '../instance-cells/name-cell';
|
||||
|
||||
interface InstanceItemProps {
|
||||
instanceData: ModelInstanceListItem;
|
||||
@@ -52,377 +21,6 @@ interface InstanceItemProps {
|
||||
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
|
||||
}
|
||||
|
||||
const fieldList = [
|
||||
{
|
||||
label: 'CPU',
|
||||
key: 'cpuoffload',
|
||||
locale: false
|
||||
},
|
||||
{
|
||||
label: 'GPU',
|
||||
key: 'gpuoffload',
|
||||
locale: false
|
||||
}
|
||||
];
|
||||
|
||||
const statusColumn: ColumnProps[] = [
|
||||
{
|
||||
title: 'models.table.download.progress',
|
||||
locale: true,
|
||||
key: 'download_progress',
|
||||
render: ({ row }) => {
|
||||
return (
|
||||
<StatusTag
|
||||
download={{
|
||||
percent: row.download_progress
|
||||
}}
|
||||
statusValue={{
|
||||
status: row.download_progress
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[InstanceStatusMap.Initializing],
|
||||
text: row.download_progress,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
const downloadList: ColumnProps[] = [
|
||||
{
|
||||
title: 'resources.worker',
|
||||
locale: true,
|
||||
key: 'worker_name',
|
||||
width: 280
|
||||
},
|
||||
...statusColumn
|
||||
];
|
||||
|
||||
const draftModelDownloadList: ColumnProps[] = [
|
||||
{
|
||||
title: 'models.form.draftModel',
|
||||
locale: true,
|
||||
key: 'draft_model',
|
||||
style: {
|
||||
wordBreak: 'break-word'
|
||||
},
|
||||
width: 280
|
||||
},
|
||||
...statusColumn
|
||||
];
|
||||
|
||||
const calcTotalVram = (vram: Record<string, number>) => {
|
||||
return _.sum(_.values(vram));
|
||||
};
|
||||
|
||||
const WorkerInfo = (props: {
|
||||
title: React.ReactNode;
|
||||
defaultOpen: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(props.defaultOpen);
|
||||
useEffect(() => {
|
||||
if (props.defaultOpen) {
|
||||
setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [props.defaultOpen]);
|
||||
return (
|
||||
<span className="server-info-wrapper">
|
||||
<Tooltip
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={props.title}
|
||||
styles={{
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="server-info">
|
||||
<InfoCircleOutlined />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const GPUIndexWrapper = styled.span`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`;
|
||||
const RenderRayactorDownloading = (props: {
|
||||
severList: any[];
|
||||
instanceData: any;
|
||||
workerList: WorkerListItem[];
|
||||
}) => {
|
||||
const { severList, instanceData, workerList } = props;
|
||||
if (!severList.length && !instanceData.draft_model_download_progress) {
|
||||
return null;
|
||||
}
|
||||
const list = _.map(severList, (item: any) => {
|
||||
const data = _.find(workerList, { id: item.worker_id });
|
||||
return {
|
||||
worker_name: data?.name,
|
||||
worker_ip: data?.ip,
|
||||
download_progress: _.round(item.download_progress, 2)
|
||||
};
|
||||
});
|
||||
|
||||
const mainWorker = [
|
||||
{
|
||||
worker_name: `${instanceData.worker_name}`,
|
||||
worker_ip: `${instanceData.worker_ip}`,
|
||||
download_progress: _.round(instanceData.download_progress, 2)
|
||||
}
|
||||
];
|
||||
|
||||
const draftModelList = [];
|
||||
if (instanceData.draft_model_download_progress > 0) {
|
||||
draftModelList.push({
|
||||
draft_model: generateSource(instanceData.draft_model_source),
|
||||
download_progress: _.round(instanceData.draft_model_download_progress, 2)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{severList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={downloadList}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
|
||||
{draftModelList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={draftModelDownloadList}
|
||||
dataSource={[...draftModelList]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderWorkerDownloading = (props: {
|
||||
distributed_servers?: DistributedServers;
|
||||
workerList: WorkerListItem[];
|
||||
instanceData: ModelInstanceListItem;
|
||||
backend?: string;
|
||||
}) => {
|
||||
const { distributed_servers, workerList, instanceData, backend } = props;
|
||||
|
||||
const severList: DistributedServerItem[] =
|
||||
distributed_servers?.subordinate_workers || [];
|
||||
|
||||
const isWorkerNotDownloading =
|
||||
instanceData.state !== InstanceStatusMap.Downloading ||
|
||||
!severList.length ||
|
||||
backend === backendOptionsMap.llamaBox;
|
||||
|
||||
const isDraftModeNotDownloading =
|
||||
!instanceData.draft_model_download_progress ||
|
||||
instanceData.draft_model_download_progress >= 100;
|
||||
|
||||
if (isWorkerNotDownloading && isDraftModeNotDownloading) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
arrow={true}
|
||||
styles={{
|
||||
container: {
|
||||
width: 360,
|
||||
backgroundColor: 'var(--color-spotlight-bg)'
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
root: 'light-downloading-tooltip'
|
||||
}}
|
||||
title={
|
||||
<RenderRayactorDownloading
|
||||
severList={severList}
|
||||
workerList={workerList}
|
||||
instanceData={instanceData}
|
||||
></RenderRayactorDownloading>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
showInfo={false}
|
||||
type="circle"
|
||||
size={16}
|
||||
strokeColor="var(--ant-color-success)"
|
||||
percent={
|
||||
_.find(severList, (item: any) => item.download_progress < 100)
|
||||
?.download_progress ||
|
||||
instanceData.draft_model_download_progress ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceStatusTag = (
|
||||
props: Pick<InstanceItemProps, 'instanceData' | 'handleChildSelect'>
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { instanceData, handleChildSelect } = props;
|
||||
if (!instanceData.state) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StatusTag
|
||||
download={
|
||||
instanceData.state === InstanceStatusMap.Downloading
|
||||
? { percent: instanceData.download_progress }
|
||||
: undefined
|
||||
}
|
||||
extra={
|
||||
instanceData.state === InstanceStatusMap.Error &&
|
||||
instanceData.worker_id ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
onClick={() => handleChildSelect('viewlog', instanceData)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'models.list.more.logs' })}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
statusValue={{
|
||||
status:
|
||||
instanceData.state === InstanceStatusMap.Downloading &&
|
||||
instanceData.download_progress === 100
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[instanceData.state],
|
||||
text: InstanceStatusMapValue[instanceData.state],
|
||||
message:
|
||||
instanceData.state === InstanceStatusMap.Downloading &&
|
||||
instanceData.download_progress === 100
|
||||
? ''
|
||||
: instanceData.state_message
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const childActionList = [
|
||||
{
|
||||
label: 'common.button.viewlog',
|
||||
key: 'viewlog',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <IconFont type="icon-logs" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.downloadLog',
|
||||
key: 'download',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <DownloadOutlined />
|
||||
},
|
||||
{
|
||||
label: 'models.table.instance.benchmark',
|
||||
key: 'benchmark',
|
||||
status: [InstanceStatusMap.Running],
|
||||
icon: <IconFont type="icon-speed" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.delrecreate',
|
||||
key: 'delete',
|
||||
props: {
|
||||
danger: true
|
||||
},
|
||||
icon: <DeleteOutlined />
|
||||
}
|
||||
];
|
||||
|
||||
const renderGpuIndexs = (gpuIndexes: number[]) => {
|
||||
return (
|
||||
<GPUIndexWrapper>
|
||||
{_.chunk(gpuIndexes, 8).map((item: number[], index: number) => {
|
||||
return <span key={index}>{item.join(',')}</span>;
|
||||
})}
|
||||
</GPUIndexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const distributeCols: ColumnProps[] = [
|
||||
{
|
||||
title: 'Worker',
|
||||
key: 'worker_name',
|
||||
style: {
|
||||
wordBreak: 'break-word'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
key: 'worker_ip',
|
||||
render: ({ row }) => {
|
||||
return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'models.table.gpuindex',
|
||||
locale: true,
|
||||
key: 'gpu_index',
|
||||
render: ({ row }) => {
|
||||
const list = row.gpu_index?.sort((a: number, b: number) => a - b) || [];
|
||||
return row.is_main ? (
|
||||
<>
|
||||
{renderGpuIndexs(list)}
|
||||
<span>(main)</span>
|
||||
</>
|
||||
) : (
|
||||
renderGpuIndexs(list)
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'models.table.vram.allocated',
|
||||
locale: true,
|
||||
key: 'vram',
|
||||
render: ({ rowIndex, row, dataList }) => {
|
||||
return convertFileSize(row.vram, 1);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const renderMessage = (title: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 280,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
instanceData,
|
||||
workerList,
|
||||
@@ -430,350 +28,74 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
defaultOpenId,
|
||||
handleChildSelect
|
||||
}) => {
|
||||
const systemConfig = useAtomValue(systemConfigAtom);
|
||||
const { runBenchmarkOnInstance } = useBenchmarkTargetInstance();
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
stack: { threshold: 1 }
|
||||
});
|
||||
const { downloadStream } = useDownloadStream();
|
||||
const intl = useIntl();
|
||||
const actionItems = useMemo(() => {
|
||||
return _.filter(childActionList, (action: any) => {
|
||||
if (action.key === 'benchmark') {
|
||||
return (
|
||||
action.status.includes(instanceData.state) &&
|
||||
modelData?.categories?.includes(modelCategoriesMap.llm)
|
||||
);
|
||||
}
|
||||
if (action.status && action.status.length > 0) {
|
||||
return action.status.includes(instanceData.state);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [instanceData.state, modelData, systemConfig.showMonitoring]);
|
||||
|
||||
const createFileName = (name: string) => {
|
||||
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
const fileName = `${name}_${timestamp}.txt`;
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const downloadNotification = useCallback(
|
||||
(
|
||||
data: HandlerOptions & {
|
||||
filename: string;
|
||||
duration?: number;
|
||||
chunkRequestRef: any;
|
||||
}
|
||||
) => {
|
||||
api.open({
|
||||
duration: data.duration,
|
||||
message: renderMessage(data.filename),
|
||||
key: data.filename,
|
||||
closeIcon: (
|
||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||
),
|
||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||
onClose() {
|
||||
data.chunkRequestRef?.current?.abort();
|
||||
notification.destroy?.(data.filename);
|
||||
}
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderWorkerInfo = useMemo(() => {
|
||||
let workerIp = '-';
|
||||
if (instanceData.worker_ip) {
|
||||
workerIp = instanceData.port
|
||||
? `${instanceData.worker_ip}:${instanceData.port}`
|
||||
: instanceData.worker_ip;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>{instanceData.worker_name}</div>
|
||||
<div className="flex-center">
|
||||
<HddFilled className="m-r-5" />
|
||||
{workerIp}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<IconFont type="icon-filled-gpu" className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.gpuindex' })}: [
|
||||
{_.join(
|
||||
instanceData.gpu_indexes?.sort?.((a, b) => a - b),
|
||||
','
|
||||
)}
|
||||
]
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<ThunderboltFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.form.backend' })}:{' '}
|
||||
{instanceData?.backend || modelData?.backend || ''}
|
||||
{instanceData.backend_version || modelData?.backend_version
|
||||
? `(${instanceData.backend_version || modelData?.backend_version})`
|
||||
: ''}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<PieChartFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '}
|
||||
{convertFileSize(
|
||||
instanceData.computed_resource_claim?.vram
|
||||
? calcTotalVram(instanceData.computed_resource_claim?.vram)
|
||||
: 0,
|
||||
1
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
instanceData.worker_name,
|
||||
instanceData.worker_ip,
|
||||
instanceData.port,
|
||||
instanceData.gpu_indexes,
|
||||
instanceData?.backend,
|
||||
instanceData?.backend_version,
|
||||
modelData?.backend,
|
||||
modelData?.backend_version,
|
||||
intl
|
||||
]);
|
||||
|
||||
const renderDistributedServer = (severList: any[]) => {
|
||||
const list = _.map(severList, (item: any) => {
|
||||
const data = _.find(workerList, { id: item.worker_id });
|
||||
return {
|
||||
worker_name: data?.name,
|
||||
worker_ip: data?.ip,
|
||||
port: '',
|
||||
is_main: false,
|
||||
vram: calcTotalVram(item.computed_resource_claim?.vram || {}),
|
||||
gpu_index: _.keys(item.computed_resource_claim?.vram)
|
||||
.map((i: string) => Number(i))
|
||||
.sort((a: number, b: number) => a - b)
|
||||
};
|
||||
});
|
||||
|
||||
const mainWorker = [
|
||||
{
|
||||
worker_name: `${instanceData.worker_name}`,
|
||||
worker_ip: `${instanceData.worker_ip}`,
|
||||
port: '',
|
||||
vram: calcTotalVram(instanceData.computed_resource_claim?.vram || {}),
|
||||
is_main: true,
|
||||
gpu_index: instanceData.gpu_indexes?.sort(
|
||||
(a: number, b: number) => a - b
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SimpleTabel
|
||||
rowKey="worker_name"
|
||||
columns={distributeCols}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
></SimpleTabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDistributionInfo = (distributed_servers: DistributedServers) => {
|
||||
const severList: DistributedServerItem[] =
|
||||
distributed_servers?.subordinate_workers || [];
|
||||
|
||||
if (!severList.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TooltipOverlayScroller
|
||||
toolTipProps={{
|
||||
styles: {
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '520px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={renderDistributedServer(severList)}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="processing"
|
||||
style={{
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.acrossworker'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</TooltipOverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOffloadInfo = useMemo(() => {
|
||||
const total_layers = instanceData.computed_resource_claim?.total_layers;
|
||||
const offload_layers = instanceData.computed_resource_claim?.offload_layers;
|
||||
|
||||
if (total_layers === offload_layers || !total_layers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const offloadData = {
|
||||
cpuoffload: `${
|
||||
_.subtract(
|
||||
instanceData.computed_resource_claim?.total_layers,
|
||||
instanceData.computed_resource_claim?.offload_layers
|
||||
) || 0
|
||||
} ${intl.formatMessage({
|
||||
id: 'models.table.layers'
|
||||
})}`,
|
||||
gpuoffload: `${instanceData.computed_resource_claim?.offload_layers} ${intl.formatMessage(
|
||||
{
|
||||
id: 'models.table.layers'
|
||||
}
|
||||
)}`
|
||||
};
|
||||
return (
|
||||
<Tooltip
|
||||
styles={{
|
||||
container: {
|
||||
paddingInline: 12
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<InfoColumn fieldList={fieldList} data={offloadData}></InfoColumn>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="cyan"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.cpuoffload'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}, [
|
||||
instanceData.computed_resource_claim?.total_layers,
|
||||
instanceData.computed_resource_claim?.offload_layers
|
||||
]);
|
||||
|
||||
const handleOnSelect = (val: string) => {
|
||||
if (val === 'benchmark') {
|
||||
runBenchmarkOnInstance(instanceData);
|
||||
} else if (val === 'download') {
|
||||
downloadStream({
|
||||
url: `${MODEL_INSTANCE_API}/${instanceData.id}/logs`,
|
||||
filename: createFileName(instanceData.name),
|
||||
downloadNotification
|
||||
});
|
||||
} else {
|
||||
handleChildSelect(val, instanceData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||
<RowChildren>
|
||||
<Row style={{ width: '100%' }} align="middle">
|
||||
<Col
|
||||
span={6}
|
||||
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||
<RowChildren>
|
||||
<Row style={{ width: '100%' }} align="middle">
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||
}}
|
||||
>
|
||||
<NameCell
|
||||
record={instanceData}
|
||||
modelData={modelData}
|
||||
defaultOpenId={defaultOpenId}
|
||||
></NameCell>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<span
|
||||
style={{
|
||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||
paddingLeft: '58px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}
|
||||
className="flex align-center"
|
||||
>
|
||||
<span className="flex-center instance-name">
|
||||
<AutoTooltip title={instanceData.name} ghost>
|
||||
<span className="m-r-5">{instanceData.name}</span>
|
||||
</AutoTooltip>
|
||||
{!!instanceData.worker_id && (
|
||||
<WorkerInfo
|
||||
title={renderWorkerInfo}
|
||||
defaultOpen={defaultOpenId === instanceData.name}
|
||||
></WorkerInfo>
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<span
|
||||
style={{
|
||||
paddingLeft: '58px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}
|
||||
className="flex align-center"
|
||||
>
|
||||
{renderOffloadInfo}
|
||||
{renderDistributionInfo(
|
||||
instanceData.distributed_servers || ({} as DistributedServers)
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span
|
||||
style={{ paddingLeft: '40px', gap: 4 }}
|
||||
className="flex-center"
|
||||
>
|
||||
<InstanceStatusTag
|
||||
instanceData={instanceData}
|
||||
handleChildSelect={handleChildSelect}
|
||||
/>
|
||||
<RenderWorkerDownloading
|
||||
backend={modelData?.backend}
|
||||
distributed_servers={instanceData.distributed_servers}
|
||||
workerList={workerList}
|
||||
instanceData={instanceData}
|
||||
></RenderWorkerDownloading>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span style={{ paddingLeft: 43 }} className="flex">
|
||||
<AutoTooltip ghost>
|
||||
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div style={{ paddingLeft: 36 }}>
|
||||
<DropdownButtons
|
||||
items={actionItems}
|
||||
onSelect={handleOnSelect}
|
||||
></DropdownButtons>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</RowChildren>
|
||||
</div>
|
||||
</>
|
||||
<CPUOffloadingCell record={instanceData}></CPUOffloadingCell>
|
||||
<DistributeInfoCell
|
||||
record={instanceData}
|
||||
workerList={workerList}
|
||||
></DistributeInfoCell>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span
|
||||
style={{ paddingLeft: '40px', gap: 4 }}
|
||||
className="flex-center"
|
||||
>
|
||||
<InstanceStatusCell
|
||||
record={instanceData}
|
||||
onSelect={handleChildSelect}
|
||||
/>
|
||||
<DownloadingStatusCell
|
||||
backend={modelData?.backend}
|
||||
distributed_servers={instanceData.distributed_servers}
|
||||
workerList={workerList}
|
||||
record={instanceData}
|
||||
></DownloadingStatusCell>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span style={{ paddingLeft: 43 }} className="flex">
|
||||
<AutoTooltip ghost>
|
||||
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div style={{ paddingLeft: 36 }}>
|
||||
<ActionsCell
|
||||
record={instanceData}
|
||||
modelData={modelData}
|
||||
onSelect={handleChildSelect}
|
||||
></ActionsCell>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</RowChildren>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default InstanceItem;
|
||||
|
||||
@@ -1,7 +1,111 @@
|
||||
import React from 'react';
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { FilterBar } from '@/components/page-tools';
|
||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import NoResult from '@/pages/_components/no-result';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
deleteModelInstance,
|
||||
MODEL_INSTANCE_API,
|
||||
queryModelsInstances
|
||||
} from '../apis';
|
||||
import { ModelInstanceListItem as ListItem } from '../config/types';
|
||||
import useInstanceColumns from './use-instance-columns';
|
||||
|
||||
const InstanceView: React.FC = () => {
|
||||
return <div>InstanceView</div>;
|
||||
const {
|
||||
dataSource,
|
||||
rowSelection,
|
||||
queryParams,
|
||||
modalRef,
|
||||
handleTableChange,
|
||||
handleDelete,
|
||||
handleDeleteBatch,
|
||||
fetchData,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
handleNameChange
|
||||
} = useTableFetch<ListItem>({
|
||||
key: PaginationKey.Providers,
|
||||
fetchAPI: queryModelsInstances,
|
||||
deleteAPI: deleteModelInstance,
|
||||
watch: false,
|
||||
API: MODEL_INSTANCE_API,
|
||||
contentForDelete: 'menu.models.providers'
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {});
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={dataSource.loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-extension-outline" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.providers.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.providers.title' })}
|
||||
subTitle={intl.formatMessage({
|
||||
id: 'noresult.providers.subTitle'
|
||||
})}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = useInstanceColumns(handleSelect);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageBox>
|
||||
<FilterBar
|
||||
showSelect={false}
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
widths={{ input: 300 }}
|
||||
rowSelection={rowSelection}
|
||||
handleInputChange={handleNameChange}
|
||||
handleSearch={handleSearch}
|
||||
handleDeleteByBatch={handleDeleteBatch}
|
||||
></FilterBar>
|
||||
<ConfigProvider renderEmpty={renderEmpty}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
tableLayout="fixed"
|
||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||
showSorterTooltip={false}
|
||||
dataSource={dataSource.dataList}
|
||||
loading={{
|
||||
spinning: dataSource.loading,
|
||||
size: 'middle'
|
||||
}}
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
scroll={{ x: 900 }}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSize: queryParams.perPage,
|
||||
current: queryParams.page,
|
||||
total: dataSource.total,
|
||||
hideOnSinglePage: queryParams.perPage === 10,
|
||||
onChange: handlePageChange
|
||||
}}
|
||||
></Table>
|
||||
</ConfigProvider>
|
||||
</PageBox>
|
||||
<DeleteModal ref={modalRef}></DeleteModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceView;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// columns.ts
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import { tableSorter } from '@/config/settings';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { useMemo } from 'react';
|
||||
import { ModelInstanceListItem as ListItem } from '../config/types';
|
||||
|
||||
const useProviderColumns = (
|
||||
handleSelect: (val: string, record: ListItem) => void,
|
||||
onCellClick?: (record: ListItem, dataIndex: string) => void
|
||||
): ColumnsType<ListItem> => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: 'name',
|
||||
sorter: tableSorter(1),
|
||||
minWidth: 160,
|
||||
span: 5,
|
||||
render: () => '--'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'providers.table.providerName' }),
|
||||
dataIndex: ['config', 'type'],
|
||||
sorter: tableSorter(2),
|
||||
span: 4,
|
||||
minWidth: 160,
|
||||
render: () => '--'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'providers.table.models' }),
|
||||
dataIndex: 'models',
|
||||
span: 3,
|
||||
minWidth: 200,
|
||||
render: () => '--'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: 'created_at',
|
||||
sorter: tableSorter(6),
|
||||
span: 3,
|
||||
render: () => '--'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.operation' }),
|
||||
dataIndex: 'operations',
|
||||
span: 3,
|
||||
minWidth: 120,
|
||||
render: (value: string, record: ListItem) => (
|
||||
<DropdownButtons
|
||||
items={[]}
|
||||
onSelect={(val) => handleSelect(val, record)}
|
||||
></DropdownButtons>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [handleSelect, onCellClick]);
|
||||
};
|
||||
|
||||
export default useProviderColumns;
|
||||
@@ -11,6 +11,7 @@ import NoResult from '@/pages/_components/no-result';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
import { createModel } from '@/pages/llmodels/apis';
|
||||
import DeployModal from '@/pages/llmodels/components/deployment/deploy-modal';
|
||||
import DownloadModal from '@/pages/llmodels/components/download';
|
||||
import { modelSourceMap } from '@/pages/llmodels/config';
|
||||
import {
|
||||
modalConfig,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from '@/pages/llmodels/config/button-actions';
|
||||
import { SourceType } from '@/pages/llmodels/config/types';
|
||||
import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters';
|
||||
import DownloadModal from '@/pages/llmodels/download';
|
||||
import useCheckBackend from '@/pages/llmodels/hooks/use-check-backend';
|
||||
import { useGenerateWorkerOptions } from '@/pages/llmodels/hooks/use-form-initial-values';
|
||||
import useRecognizeAudio from '@/pages/llmodels/hooks/use-recognize-audio';
|
||||
|
||||
Reference in New Issue
Block a user