chore: worker downloading tooltip

This commit is contained in:
jialin
2025-03-25 12:33:29 +08:00
parent 626e29a001
commit b25644c590
14 changed files with 299 additions and 77 deletions
+13 -1
View File
@@ -31,11 +31,23 @@
.cell-span { .cell-span {
display: flex; display: flex;
padding: 4px 6px; padding: 6px;
min-height: 32px; min-height: 32px;
} }
.cell-header { .cell-header {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
&.light {
th {
color: var(--ant-color-text);
border-bottom: 1px solid var(--ant-color-split);
}
td {
color: var(--ant-color-text);
border-bottom: 1px solid var(--ant-color-split);
}
}
} }
+10 -2
View File
@@ -9,6 +9,7 @@ import TableRow from './row';
export interface ColumnProps { export interface ColumnProps {
title: string; title: string;
key: string; key: string;
width?: string | number;
render?: (data: { render?: (data: {
dataIndex: string; dataIndex: string;
dataList?: any[]; dataList?: any[];
@@ -34,13 +35,20 @@ export interface ColumnProps {
} }
interface SimpleTableProps { interface SimpleTableProps {
theme?: 'dark' | 'light';
columns: ColumnProps[]; columns: ColumnProps[];
dataSource: any[]; dataSource: any[];
bordered?: boolean; bordered?: boolean;
rowKey?: string; rowKey?: string;
} }
const SimpleTabel: React.FC<SimpleTableProps> = (props) => { const SimpleTabel: React.FC<SimpleTableProps> = (props) => {
const { columns, dataSource, rowKey, bordered = true } = props; const {
columns,
dataSource,
rowKey,
bordered = true,
theme = 'dark'
} = props;
const scroller = React.useRef<any>(null); const scroller = React.useRef<any>(null);
const { initialize } = useOverlayScroller(); const { initialize } = useOverlayScroller();
@@ -50,7 +58,7 @@ const SimpleTabel: React.FC<SimpleTableProps> = (props) => {
return ( return (
<div style={{ maxHeight: 200 }} ref={scroller}> <div style={{ maxHeight: 200 }} ref={scroller}>
<table <table
className={classNames('simple-table', { className={classNames('simple-table', theme, {
'simple-table-bordered': bordered 'simple-table-bordered': bordered
})} })}
> >
+1
View File
@@ -35,6 +35,7 @@ const TableCell: React.FC<TableCellProps> = (props: TableCellProps) => {
return ( return (
<td <td
width={column.width}
key={colIndex} key={colIndex}
rowSpan={column.rowSpan?.({ rowSpan={column.rowSpan?.({
row, row,
+6
View File
@@ -875,6 +875,12 @@ body {
} }
} }
.light-downloading-tooltip {
&.ant-tooltip .ant-tooltip-arrow::before {
background: var(--color-white-1);
}
}
.tooltip-wrapper { .tooltip-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+2 -1
View File
@@ -33,7 +33,7 @@ export default function useTableFetch<ListItem>(options: {
loadend: false, loadend: false,
total: 0 total: 0
}); });
const [queryParams, setQueryParams] = useState({ const [queryParams, setQueryParams] = useState<any>({
page: 1, page: 1,
perPage: 10, perPage: 10,
search: '' search: ''
@@ -191,6 +191,7 @@ export default function useTableFetch<ListItem>(options: {
sortOrder, sortOrder,
queryParams, queryParams,
modalRef, modalRef,
setQueryParams,
handleDelete, handleDelete,
handleDeleteBatch, handleDeleteBatch,
fetchData, fetchData,
+2 -1
View File
@@ -119,5 +119,6 @@ export default {
'models.form.backend.warning.llamabox': 'models.form.backend.warning.llamabox':
'To use the llama-box backend, specify the full path to the model file (e.g.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).', 'To use the llama-box backend, specify the full path to the model file (e.g.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).',
'models.form.keyvalue.paste': 'models.form.keyvalue.paste':
'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.' 'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.',
'models.form.files': 'files'
}; };
+3 -1
View File
@@ -66,5 +66,7 @@ export default {
'The default storage directory is /var/lib/gpustack/cache.', 'The default storage directory is /var/lib/gpustack/cache.',
'resources.modelfiles.retry.download': 'Retry Download', 'resources.modelfiles.retry.download': 'Retry Download',
'resources.modelfiles.storagePath.holder': 'resources.modelfiles.storagePath.holder':
'Waiting for download to complete...' 'Waiting for download to complete...',
'resources.filter.worker': 'Filter by Worker',
'resources.filter.file': 'Filter by File'
}; };
+3 -1
View File
@@ -119,9 +119,11 @@ export default {
'models.form.backend.warning.llamabox': 'models.form.backend.warning.llamabox':
'Чтобы использовать бэкенд llama-box , укажите полный путь к файлу модели (например,<span style="font-weight: 700">/data/models/model.gguf</span>). Для шардированных моделей укажите путь к первому шарду (например,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).', 'Чтобы использовать бэкенд llama-box , укажите полный путь к файлу модели (например,<span style="font-weight: 700">/data/models/model.gguf</span>). Для шардированных моделей укажите путь к первому шарду (например,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).',
'models.form.keyvalue.paste': 'models.form.keyvalue.paste':
'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.' 'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.',
'models.form.files': 'files'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
//1. 'models.form.keyvalue.paste' //1. 'models.form.keyvalue.paste'
//2. 'models.form.files'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+5 -1
View File
@@ -66,7 +66,9 @@ export default {
'The default storage directory is /var/lib/gpustack/cache.', 'The default storage directory is /var/lib/gpustack/cache.',
'resources.modelfiles.retry.download': 'Retry Download', 'resources.modelfiles.retry.download': 'Retry Download',
'resources.modelfiles.storagePath.holder': 'resources.modelfiles.storagePath.holder':
'Waiting for download to complete...' 'Waiting for download to complete...',
'resources.filter.worker': 'Filter by Worker',
'resources.filter.file': 'Filter by File'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -79,4 +81,6 @@ export default {
//7. 'resources.modelfiles.retry.download', //7. 'resources.modelfiles.retry.download',
//8. 'resources.modelfiles.form.localdir.tips', //8. 'resources.modelfiles.form.localdir.tips',
//9. 'resources.modelfiles.storagePath.holder', //9. 'resources.modelfiles.storagePath.holder',
//10. 'resources.filter.worker',
//11. 'resources.filter.file',
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -1
View File
@@ -112,5 +112,6 @@ export default {
'models.form.backend.warning.llamabox': 'models.form.backend.warning.llamabox':
'要使用 llama-box 后端,请指定模型文件的完整路径(例如:<span style="font-weight: 700">/data/models/model.gguf</span>)。对于分片模型,请提供第一个分片的路径(例如:<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>)。', '要使用 llama-box 后端,请指定模型文件的完整路径(例如:<span style="font-weight: 700">/data/models/model.gguf</span>)。对于分片模型,请提供第一个分片的路径(例如:<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>)。',
'models.form.keyvalue.paste': 'models.form.keyvalue.paste':
'粘贴多行文本,每行包含一个键值对,键和值之间用 = 号分隔,不同的键值对之间用换行符分隔。' '粘贴多行文本,每行包含一个键值对,键和值之间用 = 号分隔,不同的键值对之间用换行符分隔。',
'models.form.files': '文件'
}; };
+3 -1
View File
@@ -64,5 +64,7 @@ export default {
'resources.modelfiles.form.localdir.tips': 'resources.modelfiles.form.localdir.tips':
'默认存储目录为 /var/lib/gpustack/cache', '默认存储目录为 /var/lib/gpustack/cache',
'resources.modelfiles.retry.download': '重新下载', 'resources.modelfiles.retry.download': '重新下载',
'resources.modelfiles.storagePath.holder': '等待下载完成...' 'resources.modelfiles.storagePath.holder': '等待下载完成...',
'resources.filter.worker': '按 worker 筛选',
'resources.filter.file': '按文件筛选'
}; };
+147 -35
View File
@@ -39,6 +39,34 @@ const fieldList = [
} }
]; ];
const downloadList: ColumnProps[] = [
{
title: 'Worker',
key: 'worker_name',
width: 200
},
{
title: 'Status',
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 WorkerInfo = (props: { const WorkerInfo = (props: {
title: React.ReactNode; title: React.ReactNode;
defaultOpen: boolean; defaultOpen: boolean;
@@ -70,6 +98,82 @@ const WorkerInfo = (props: {
); );
}; };
const RenderRayactorDownloading = (props: {
severList: any[];
instanceData: any;
workerList: WorkerListItem[];
}) => {
const { severList, instanceData, workerList } = props;
if (!severList.length) {
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)
}
];
return (
<div>
<SimpleTabel
columns={downloadList}
dataSource={[...mainWorker, ...list]}
theme="light"
></SimpleTabel>
</div>
);
};
const RenderWorkerDownloading = (props: {
rayActors: any[];
workerList: WorkerListItem[];
instanceData: ModelInstanceListItem;
}) => {
const { rayActors, workerList, instanceData } = props;
if (instanceData.state === InstanceStatusMap.Error || !rayActors.length) {
return null;
}
return (
<Tooltip
arrow={true}
overlayInnerStyle={{
width: 300,
backgroundColor: 'var(--color-white-1)'
}}
overlayClassName="light-downloading-tooltip"
title={
<RenderRayactorDownloading
severList={rayActors}
workerList={workerList}
instanceData={instanceData}
></RenderRayactorDownloading>
}
>
<Progress
showInfo={false}
type="circle"
size={20}
strokeColor="var(--color-progress-green)"
percent={
_.find(rayActors, (item: any) => item.download_progress < 100)
?.download_progress || 100
}
/>
</Tooltip>
);
};
const InstanceStatusTag = ( const InstanceStatusTag = (
props: Pick<InstanceItemProps, 'instanceData' | 'handleChildSelect'> props: Pick<InstanceItemProps, 'instanceData' | 'handleChildSelect'>
) => { ) => {
@@ -79,39 +183,41 @@ const InstanceStatusTag = (
return null; return null;
} }
return ( return (
<StatusTag <>
download={ <StatusTag
instanceData.state === InstanceStatusMap.Downloading download={
? { percent: instanceData.download_progress } instanceData.state === InstanceStatusMap.Downloading
: undefined ? { percent: instanceData.download_progress }
} : undefined
extra={ }
instanceData.state === InstanceStatusMap.Error && extra={
instanceData.worker_id ? ( instanceData.state === InstanceStatusMap.Error &&
<Button instanceData.worker_id ? (
type="link" <Button
size="small" type="link"
style={{ paddingLeft: 0 }} size="small"
onClick={() => handleChildSelect('viewlog', instanceData)} style={{ paddingLeft: 0 }}
> onClick={() => handleChildSelect('viewlog', instanceData)}
{intl.formatMessage({ id: 'models.list.more.logs' })} >
</Button> {intl.formatMessage({ id: 'models.list.more.logs' })}
) : null </Button>
} ) : null
statusValue={{ }
status: statusValue={{
instanceData.state === InstanceStatusMap.Downloading && status:
instanceData.download_progress === 100 instanceData.state === InstanceStatusMap.Downloading &&
? status[InstanceStatusMap.Running] instanceData.download_progress === 100
: status[instanceData.state], ? status[InstanceStatusMap.Running]
text: InstanceStatusMapValue[instanceData.state], : status[instanceData.state],
message: text: InstanceStatusMapValue[instanceData.state],
instanceData.state === InstanceStatusMap.Downloading && message:
instanceData.download_progress === 100 instanceData.state === InstanceStatusMap.Downloading &&
? '' instanceData.download_progress === 100
: instanceData.state_message ? ''
}} : instanceData.state_message
/> }}
/>
</>
); );
}; };
@@ -341,6 +447,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
<Tag <Tag
color="processing" color="processing"
style={{ style={{
marginRight: 0,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
maxWidth: '100%', maxWidth: '100%',
@@ -471,13 +578,18 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
</Col> </Col>
<Col span={4}> <Col span={4}>
<span <span
style={{ paddingLeft: '62px' }} style={{ paddingLeft: '62px', gap: 4 }}
className="flex justify-center" className="flex-center justify-center"
> >
<InstanceStatusTag <InstanceStatusTag
instanceData={instanceData} instanceData={instanceData}
handleChildSelect={handleChildSelect} handleChildSelect={handleChildSelect}
/> />
<RenderWorkerDownloading
rayActors={instanceData.distributed_servers?.ray_actors || []}
workerList={workerList}
instanceData={instanceData}
></RenderWorkerDownloading>
</span> </span>
</Col> </Col>
<Col span={5}> <Col span={5}>
@@ -6,6 +6,64 @@ import { ModelInstanceListItem } from '../config/types';
import '../style/instance-item.less'; import '../style/instance-item.less';
import InstanceItem from './instance-item'; import InstanceItem from './instance-item';
const testInstanceData = {
source: 'huggingface',
huggingface_repo_id: 'Qwen/Qwen2.5-3B-Instruct',
huggingface_filename: null,
ollama_library_model_name: null,
model_scope_model_id: null,
model_scope_file_path: null,
local_path: null,
name: 'qwen2.5-XVCXa',
worker_id: 1,
worker_name: 'sealgpuhost4080',
worker_ip: '192.168.50.12',
pid: 208852,
port: 40063,
download_progress: 72.11,
resolved_path: null,
state: 'downloading',
state_message: '',
computed_resource_claim: {
is_unified_memory: false,
offload_layers: null,
total_layers: null,
ram: null,
vram: {
'0': 1717148058
},
tensor_split: null
},
gpu_indexes: [0],
model_id: 1,
model_name: 'qwen2.5',
distributed_servers: {
rpc_servers: null,
ray_actors: [
{
worker_id: 2,
worker_ip: '192.168.50.13',
total_gpus: 1,
gpu_indexes: [0],
computed_resource_claim: {
is_unified_memory: false,
offload_layers: null,
total_layers: null,
ram: null,
vram: {
'0': 23181498777
},
tensor_split: null
},
download_progress: 27.49
}
]
},
id: 1,
created_at: '2025-03-24T12:06:30Z',
updated_at: '2025-03-24T12:09:01Z'
};
interface InstanceItemProps { interface InstanceItemProps {
list: ModelInstanceListItem[]; list: ModelInstanceListItem[];
workerList: WorkerListItem[]; workerList: WorkerListItem[];
+44 -32
View File
@@ -12,7 +12,6 @@ import useBodyScroll from '@/hooks/use-body-scroll';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { createModel } from '@/pages/llmodels/apis'; import { createModel } from '@/pages/llmodels/apis';
import DeployModal from '@/pages/llmodels/components/deploy-modal'; import DeployModal from '@/pages/llmodels/components/deploy-modal';
import FileParts from '@/pages/llmodels/components/file-parts';
import { import {
backendOptionsMap, backendOptionsMap,
getSourceRepoConfigValue, getSourceRepoConfigValue,
@@ -27,22 +26,17 @@ import {
} from '@/pages/llmodels/config/button-actions'; } from '@/pages/llmodels/config/button-actions';
import DownloadModal from '@/pages/llmodels/download'; import DownloadModal from '@/pages/llmodels/download';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { import { DeleteOutlined, DownOutlined, SyncOutlined } from '@ant-design/icons';
DeleteOutlined,
DownOutlined,
InfoCircleOutlined,
SyncOutlined
} from '@ant-design/icons';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { import {
Button, Button,
ConfigProvider, ConfigProvider,
Empty, Empty,
Input, Input,
Select,
Space, Space,
Table, Table,
Tag, Tag,
Tooltip,
message message
} from 'antd'; } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -112,7 +106,7 @@ const InstanceStatusTag = (props: { data: ListItem }) => {
}; };
const ModelFiles = () => { const ModelFiles = () => {
const { getGPUList, generateFormValues } = useGenerateFormEditInitialValues(); const { getGPUList } = useGenerateFormEditInitialValues();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [modelsExpandKeys, setModelsExpandKeys] = useAtom(modelsExpandKeysAtom); const [modelsExpandKeys, setModelsExpandKeys] = useAtom(modelsExpandKeysAtom);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -127,7 +121,8 @@ const ModelFiles = () => {
handlePageChange, handlePageChange,
handleTableChange, handleTableChange,
handleSearch, handleSearch,
handleNameChange handleNameChange,
setQueryParams
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
fetchAPI: queryModelFilesList, fetchAPI: queryModelFilesList,
deleteAPI: deleteModelFile, deleteAPI: deleteModelFile,
@@ -202,6 +197,20 @@ const ModelFiles = () => {
return name.replace(filterPattern, '$1'); return name.replace(filterPattern, '$1');
}; };
const handleWorkerChange = (value: number) => {
setQueryParams({
...queryParams,
page: 1,
worker_id: value
});
fetchData({
query: {
...queryParams,
page: 1,
worker_id: value
}
});
};
const generateInitialValues = (record: ListItem) => { const generateInitialValues = (record: ListItem) => {
const isGGUF = _.includes(record.resolved_paths?.[0], 'gguf'); const isGGUF = _.includes(record.resolved_paths?.[0], 'gguf');
const isOllama = !!record.ollama_library_model_name; const isOllama = !!record.ollama_library_model_name;
@@ -251,28 +260,20 @@ const ModelFiles = () => {
}; };
}); });
return ( return (
<Tooltip <Tag
overlayInnerStyle={{ className="flex-center"
width: 120, color="purple"
padding: 0 style={{
marginRight: 0,
height: 22,
borderRadius: 'var(--border-radius-base)'
}} }}
title={<FileParts fileList={partsList} showSize={false}></FileParts>}
> >
<Tag <span style={{ opacity: 1 }}>
className="tag-item" {record.resolved_paths?.length}{' '}
color="purple" {intl.formatMessage({ id: 'models.form.files' })}
style={{ </span>
marginRight: 0, </Tag>
height: 22,
borderRadius: 'var(--border-radius-base)'
}}
>
<span style={{ opacity: 1 }}>
<InfoCircleOutlined className="m-r-5" />
{partsList.length} parts
</span>
</Tag>
</Tooltip>
); );
}; };
@@ -492,12 +493,23 @@ const ModelFiles = () => {
<Space> <Space>
<Input <Input
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'common.filter.name' id: 'resources.filter.file'
})} })}
style={{ width: 300 }} style={{ width: 230 }}
allowClear allowClear
onChange={handleNameChange} onChange={handleNameChange}
></Input> ></Input>
<Select
allowClear
showSearch={false}
placeholder={intl.formatMessage({
id: 'resources.filter.worker'
})}
style={{ width: 230 }}
size="large"
onChange={handleWorkerChange}
options={workersList}
></Select>
<Button <Button
type="text" type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }} style={{ color: 'var(--ant-color-text-tertiary)' }}