Browse Source

feat: 超参数寻优-trail列表

”
'
pull/174/head
cp3hnu 11 months ago
parent
commit
f0d1a13cad
13 changed files with 212 additions and 143 deletions
  1. +1
    -1
      react-ui/src/pages/AutoML/Instance/index.tsx
  2. +1
    -1
      react-ui/src/pages/AutoML/components/ExperimentResult/index.less
  3. +15
    -14
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  4. +9
    -2
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  5. +1
    -1
      react-ui/src/pages/HyperParameter/Instance/index.less
  6. +17
    -40
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  7. +23
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
  8. +64
    -8
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
  9. +16
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
  10. +55
    -26
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  11. +1
    -1
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
  12. +2
    -47
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
  13. +7
    -2
      react-ui/src/pages/HyperParameter/types.ts

+ 1
- 1
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -22,7 +22,7 @@ enum TabKeys {
History = 'history',
}

const NodePrefix = 'auto-hpo';
const NodePrefix = 'auto-ml';

function AutoMLInstance() {
const [activeTab, setActiveTab] = useState<string>(TabKeys.Params);


+ 1
- 1
react-ui/src/pages/AutoML/components/ExperimentResult/index.less View File

@@ -26,7 +26,7 @@

&__text {
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
white-space: pre;
}

&__images {


+ 15
- 14
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -17,6 +17,7 @@ import styles from './index.less';

export type LogGroupProps = ExperimentLog & {
status?: ExperimentStatus; // 实验状态
listId: string;
};

type Log = {
@@ -25,25 +26,13 @@ type Log = {
pod_name: string; // pod名称
};

// 滚动到底部
const scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementById('log-list');
if (element) {
const optons: ScrollToOptions = {
top: element.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
};

element.scrollTo(optons);
}
};

function LogGroup({
log_type = 'normal',
pod_name = '',
log_content = '',
start_time,
status,
listId,
}: LogGroupProps) {
const [collapse, setCollapse] = useState(true);
const [logList, setLogList, logListRef] = useStateRef<Log[]>([]);
@@ -135,7 +124,7 @@ function LogGroup({
const setupSockect = () => {
let { host } = location;
if (process.env.NODE_ENV === 'development') {
host = '172.20.32.197:31213';
host = '172.20.32.181:31213';
}
const socket = new WebSocket(
`ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`,
@@ -210,6 +199,18 @@ function LogGroup({
}
};

// 滚动到底部
const scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementById(listId);
if (element) {
const optons: ScrollToOptions = {
top: element.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
};

element.scrollTo(optons);
}
};
const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal';
const logText = log_content + logList.map((v) => v.log_content).join('');
const showMoreBtn =


+ 9
- 2
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -14,6 +14,7 @@ export type ExperimentLog = {
};

type LogListProps = {
idPrefix?: string; // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识
instanceName: string; // 实验实例 name
instanceNamespace: string; // 实验实例 namespace
pipelineNodeId: string; // 流水线节点 id
@@ -23,6 +24,7 @@ type LogListProps = {
};

function LogList({
idPrefix,
instanceName,
instanceNamespace,
pipelineNodeId,
@@ -86,10 +88,15 @@ function LogList({
}
};

// 当一个页面有多个日志组件时,使用这个变量作为唯一性标识
const listId = idPrefix ? `${idPrefix}-log-list` : 'log-list';

return (
<div className={styles['log-list']} id="log-list">
<div className={styles['log-list']} id={listId}>
{logList.length > 0 ? (
logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />)
logList.map((v) => (
<LogGroup key={v.pod_name} {...v} listId={listId} status={instanceNodeStatus} />
))
) : (
<div className={styles['log-list__empty']}>暂无日志</div>
)}


+ 1
- 1
react-ui/src/pages/HyperParameter/Instance/index.less View File

@@ -34,7 +34,7 @@
&__log {
height: calc(100% - 10px);
margin-top: 10px;
padding: 20px calc(@content-padding - 8px);
padding: 8px calc(@content-padding - 8px) 20px;
overflow-y: visible;
background-color: white;
border-radius: 10px;


+ 17
- 40
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -1,6 +1,5 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import LogList from '@/pages/Experiment/components/LogList';
import { getRayInsReq } from '@/services/hyperParameter';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
@@ -10,6 +9,7 @@ import { useParams } from '@umijs/max';
import { Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import ExperimentHistory from '../components/ExperimentHistory';
import ExperimentLog from '../components/ExperimentLog';
import ExperimentResult from '../components/ExperimentResult';
import HyperParameterBasic from '../components/HyperParameterBasic';
import { HyperParameterData, HyperParameterInstanceData } from '../types';
@@ -22,8 +22,6 @@ enum TabKeys {
History = 'history',
}

const NodePrefix = 'auto-hpo';

function HyperParameterInstance() {
const [activeTab, setActiveTab] = useState<string>(TabKeys.Params);
const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined);
@@ -32,6 +30,7 @@ function HyperParameterInstance() {
);
// 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态
const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined);
const [nodes, setNodes] = useState<Record<string, NodeStatus> | undefined>(undefined);
const params = useParams();
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef<EventSource | null>(null);
@@ -72,30 +71,27 @@ function HyperParameterInstance() {
setExperimentInfo(paramJson);
}

setInstanceInfo(info);

// 这个接口返回的状态有延时,SSE 返回的状态是最新的
// SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE
// SSE 调用时,不需要解析 node_status,也不要重新建立 SSE
if (isStatusDetermined) {
setInstanceInfo((prev) => ({
...info,
nodeStatus: prev!.nodeStatus,
}));
return;
}

// 进行节点状态
const nodeStatusJson = parseJsonText(node_status);
if (nodeStatusJson) {
Object.keys(nodeStatusJson).forEach((key) => {
if (key.startsWith(NodePrefix)) {
const nodeStatus = nodeStatusJson[key];
info.nodeStatus = nodeStatus;
} else if (key.startsWith('workflow')) {
setNodes(nodeStatusJson);
Object.keys(nodeStatusJson).some((key) => {
if (key.startsWith('workflow')) {
const workflowStatus = nodeStatusJson[key];
setWorkflowStatus(workflowStatus);
return true;
}
return false;
});
}
setInstanceInfo(info);

// 运行中或者等待中,开启 SSE
if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
@@ -106,9 +102,9 @@ function HyperParameterInstance() {

const setupSSE = (name: string, namespace: string) => {
let { origin } = location;
if (process.env.NODE_ENV === 'development') {
origin = 'http://172.20.32.197:31213';
}
// if (process.env.NODE_ENV === 'development') {
// origin = 'http://172.20.32.197:31213';
// }
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
@@ -123,20 +119,12 @@ function HyperParameterInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
const nodeStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith('workflow'),
) as NodeStatus;

// 节点状态
if (nodeStatus) {
setInstanceInfo((prev) => ({
...prev!,
nodeStatus: nodeStatus,
}));
}
// 节点
setNodes(nodes);

// 设置工作流状态
if (workflowStatus) {
@@ -188,16 +176,7 @@ function HyperParameterInstance() {
icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['hyper-parameter-instance__log']}>
{instanceInfo && instanceInfo.nodeStatus && (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={instanceInfo.nodeStatus.displayName}
workflowId={instanceInfo.nodeStatus.id}
instanceNodeStartTime={instanceInfo.nodeStatus.startedAt}
instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus}
></LogList>
)}
{instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />}
</div>
),
},
@@ -208,9 +187,7 @@ function HyperParameterInstance() {
key: TabKeys.Result,
label: '实验结果',
icon: <KFIcon type="icon-shiyanjieguo1" />,
children: (
<ExperimentResult fileUrl={instanceInfo?.result_txt} fileList={instanceInfo?.file_list} />
),
children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />,
},
{
key: TabKeys.History,


+ 23
- 0
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less View File

@@ -33,3 +33,26 @@
}
}
}

.cell-index {
position: relative;
width: 100%;
padding-left: 20px;
text-align: left;

&__best-tag {
margin-left: 8px;
padding: 1px 10px;
color: @primary-color;
font-weight: normal;
font-size: 13px;
background-color: .addAlpha(@primary-color, 0.1) [];
border: 1px solid .addAlpha(@primary-color, 0.5) [];
border-radius: 2px;
}
}

.table-best-row {
color: @primary-color;
font-weight: bold;
}

+ 64
- 8
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx View File

@@ -1,6 +1,8 @@
import { HyperParameterTrialList } from '@/pages/HyperParameter/types';
import KFIcon from '@/components/KFIcon';
import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types';
import { downLoadZip } from '@/utils/downloadfile';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { Table, Tooltip, type TableProps } from 'antd';
import { Button, Table, Tooltip, type TableProps } from 'antd';
import classNames from 'classnames';
import styles from './index.less';

@@ -15,14 +17,21 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
const paramsNames = Object.keys(config);
const metricNames = Object.keys(metricAnalysis);

const columns: TableProps<HyperParameterTrialList>['columns'] = [
const trialColumns: TableProps<HyperParameterTrialList>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 100,
width: 120,
align: 'center',
render: tableCellRender(false, TableCellValueType.Index),
render: (_text, record, index: number) => {
return (
<div className={styles['cell-index']}>
<span className={styles['cell-index__text']}>{index + 1}</span>
{record.is_best && <span className={styles['cell-index__best-tag']}>最佳</span>}
</div>
);
},
},
{
title: '运行次数',
@@ -51,7 +60,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
];

if (paramsNames.length) {
columns.push({
trialColumns.push({
title: '运行参数',
dataIndex: 'config',
key: 'config',
@@ -74,7 +83,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
}

if (metricNames.length) {
columns.push({
trialColumns.push({
title: `指标分析(${first.metric ?? ''})`,
dataIndex: 'metrics',
key: 'metrics',
@@ -96,6 +105,51 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
});
}

const fileColumns: TableProps<HyperParameterFileList>['columns'] = [
{
title: '文件名称',
dataIndex: 'name',
key: 'name',
render: tableCellRender(false),
},
{
title: '文件大小',
dataIndex: 'size',
key: 'size',
width: 200,
render: tableCellRender(false),
},
{
title: '操作',
dataIndex: 'option',
width: 160,
key: 'option',
render: (_: any, record: HyperParameterFileList) => {
return (
<Button
type="link"
size="small"
key="download"
icon={<KFIcon type="icon-xiazai" />}
onClick={() => {
if (record.isFile) {
downLoadZip(`/api/mmp/minioStorage/downloadFile`, { path: record.url });
} else {
downLoadZip(`/api/mmp/minioStorage/download`, { path: record.url });
}
}}
>
下载
</Button>
);
},
},
];

const expandedRowRender = (record: HyperParameterTrialList) => (
<Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" />
);

return (
<div className={styles['experiment-history']}>
<div className={styles['experiment-history__content']}>
@@ -106,12 +160,14 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
)}
>
<Table
rowClassName={(record) => (record.is_best ? styles['table-best-row'] : '')}
dataSource={trialList}
columns={columns}
columns={trialColumns}
pagination={false}
bordered={true}
scroll={{ y: 'calc(100% - 110px)', x: '100%' }}
rowKey="trial_id"
expandable={{ expandedRowRender }}
/>
</div>
</div>


+ 16
- 0
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less View File

@@ -0,0 +1,16 @@
.experiment-log {
height: 100%;
&__tabs {
height: 100%;
:global {
.ant-tabs-nav-list {
padding-left: 0 !important;
background: none !important;
}
}

&__log {
height: 100%;
}
}
}

+ 55
- 26
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx View File

@@ -1,50 +1,78 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import LogList from '@/pages/Experiment/components/LogList';
import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
import { NodeStatus } from '@/types';
import { Tabs } from 'antd';
import { useEffect } from 'react';
import styles from './index.less';

type ExperimentLogProps = {
instanceInfo: HyperParameterInstanceData;
nodes: Record<string, NodeStatus>;
};

function ExperimentLog({ instanceInfo }: ExperimentLogProps) {
function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
let hpoNodeStatus: NodeStatus | undefined;
let frameworkCloneNodeStatus: NodeStatus | undefined;
let trainCloneNodeStatus: NodeStatus | undefined;

Object.keys(nodes)
.sort((key1, key2) => {
const node1 = nodes[key1];
const node2 = nodes[key2];
return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime();
})
.forEach((key) => {
const node = nodes[key];
if (node.displayName.startsWith('auto-hpo')) {
hpoNodeStatus = node;
} else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) {
frameworkCloneNodeStatus = node;
} else if (
node.displayName.startsWith('git-clone') &&
frameworkCloneNodeStatus &&
node.displayName !== frameworkCloneNodeStatus?.displayName
) {
trainCloneNodeStatus = node;
}
});

const tabItems = [
{
key: 'git-clone-1',
key: 'git-clone-framework',
label: '框架代码日志',
icon: <KFIcon type="icon-jibenxinxi" />,
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['auto-ml-instance__log']}>
{instanceInfo && instanceInfo.nodeStatus && (
<div className={styles['experiment-log__tabs__log']}>
{frameworkCloneNodeStatus && (
<LogList
idPrefix="git-clone-framework"
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={instanceInfo.nodeStatus.displayName}
workflowId={instanceInfo.nodeStatus.id}
instanceNodeStartTime={instanceInfo.nodeStatus.startedAt}
instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus}
pipelineNodeId={frameworkCloneNodeStatus.displayName}
workflowId={frameworkCloneNodeStatus.id}
instanceNodeStartTime={frameworkCloneNodeStatus.startedAt}
instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus}
></LogList>
)}
</div>
),
},
{
key: 'git-clone-2',
key: 'git-clone-train',
label: '训练代码日志',
icon: <KFIcon type="icon-rizhi1" />,
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['auto-ml-instance__log']}>
{instanceInfo && instanceInfo.nodeStatus && (
<div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
<LogList
idPrefix="git-clone-train"
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={instanceInfo.nodeStatus.displayName}
workflowId={instanceInfo.nodeStatus.id}
instanceNodeStartTime={instanceInfo.nodeStatus.startedAt}
instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus}
pipelineNodeId={trainCloneNodeStatus.displayName}
workflowId={trainCloneNodeStatus.id}
instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList>
)}
</div>
@@ -53,17 +81,18 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) {
{
key: 'auto-hpo',
label: '超参寻优日志',
icon: <KFIcon type="icon-rizhi1" />,
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['auto-ml-instance__log']}>
{instanceInfo && instanceInfo.nodeStatus && (
<div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
<LogList
idPrefix="auto-hpo"
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={instanceInfo.nodeStatus.displayName}
workflowId={instanceInfo.nodeStatus.id}
instanceNodeStartTime={instanceInfo.nodeStatus.startedAt}
instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus}
pipelineNodeId={hpoNodeStatus.displayName}
workflowId={hpoNodeStatus.id}
instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList>
)}
</div>
@@ -75,7 +104,7 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) {

return (
<div className={styles['experiment-log']}>
<Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} />
<Tabs className={styles['experiment-log__tabs']} items={tabItems} />
</div>
);
}


+ 1
- 1
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less View File

@@ -12,6 +12,6 @@

&__text {
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
white-space: pre;
}
}

+ 2
- 47
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx View File

@@ -1,45 +1,16 @@
import InfoGroup from '@/components/InfoGroup';
import { HyperParameterFileList } from '@/pages/HyperParameter/types';
import { getFileReq } from '@/services/file';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { Table, type TableProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './index.less';

type ExperimentResultProps = {
fileList?: HyperParameterFileList[];
fileUrl?: string;
};

function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) {
function ExperimentResult({ fileUrl }: ExperimentResultProps) {
const [result, setResult] = useState<string | undefined>('');

const columns: TableProps<HyperParameterFileList>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 120,
align: 'center',
render: tableCellRender(false, TableCellValueType.Index),
},
{
title: '文件名称',
dataIndex: 'name',
key: 'name',
render: tableCellRender(false),
},
{
title: '文件大小',
dataIndex: 'size',
key: 'size',
width: 200,
render: tableCellRender(false),
},
];

useEffect(() => {
if (fileUrl) {
getResultFile();
@@ -56,23 +27,7 @@ function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) {

return (
<div className={styles['experiment-result']}>
<InfoGroup title="文件列表" style={{ margin: '16px 0' }}>
<div
className={classNames(
'vertical-scroll-table-no-page',
styles['experiment-result__table'],
)}
>
<Table
dataSource={fileList}
columns={columns}
pagination={false}
scroll={{ y: 'calc(100% - 55px)', x: '100%' }}
rowKey="name"
/>
</div>
</InfoGroup>
<InfoGroup title="实验结果" height={420} width="100%">
<InfoGroup title="最佳实验结果" width="100%">
<div className={styles['experiment-result__text']}>{result}</div>
</InfoGroup>
</div>


+ 7
- 2
react-ui/src/pages/HyperParameter/types.ts View File

@@ -71,9 +71,14 @@ export type HyperParameterTrialList = {
config?: Record<string, any>;
metric_analysis?: Record<string, any>;
metric: string;
file: HyperParameterFileList;
is_best?: boolean;
};

export type HyperParameterFileList = {
name?: string;
size?: string;
name: string;
size: string;
url: string;
isFile: boolean;
children?: HyperParameterFileList[];
};

Loading…
Cancel
Save