| @@ -561,6 +561,18 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '知识图谱', | |||||
| path: '/knowledge', | |||||
| routes: [ | |||||
| { | |||||
| name: '知识图谱', | |||||
| path: '', | |||||
| key: 'knowledge', | |||||
| component: './Knowledge/index', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| { | { | ||||
| path: '*', | path: '*', | ||||
| layout: false, | layout: false, | ||||
| @@ -25,7 +25,7 @@ export { requestConfig as request } from './requestConfig'; | |||||
| /** | /** | ||||
| * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | ||||
| * */ | |||||
| */ | |||||
| export async function getInitialState(): Promise<GlobalInitialState> { | export async function getInitialState(): Promise<GlobalInitialState> { | ||||
| const fetchUserInfo = async () => { | const fetchUserInfo = async () => { | ||||
| try { | try { | ||||
| @@ -15,6 +15,7 @@ export enum IframePageType { | |||||
| DevEnv = 'DevEnv', // 开发环境 | DevEnv = 'DevEnv', // 开发环境 | ||||
| GitLink = 'GitLink', // git link | GitLink = 'GitLink', // git link | ||||
| Aim = 'Aim', // 实验对比 | Aim = 'Aim', // 实验对比 | ||||
| Knowledge = 'Knowledge', // 知识图谱 | |||||
| } | } | ||||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | ||||
| @@ -37,6 +38,8 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||||
| code: 200, | code: 200, | ||||
| data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '', | data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '', | ||||
| }); | }); | ||||
| case IframePageType.Knowledge: // git link | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.168.15.235:32701' }); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -9,7 +9,7 @@ import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useParams } from '@umijs/max'; | import { useParams } from '@umijs/max'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import BasicInfo from '../components/BasicInfo'; | |||||
| import ActiveLearnBasic from '../components/ActiveLearnBasic'; | |||||
| import { ActiveLearnData } from '../types'; | import { ActiveLearnData } from '../types'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -24,7 +24,7 @@ function ActiveLearnInfo() { | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| // 获取主动学习详情 | |||||
| // 获取详情 | |||||
| const getActiveLearnInfo = async () => { | const getActiveLearnInfo = async () => { | ||||
| const [res] = await to(getActiveLearnInfoReq({ id: id })); | const [res] = await to(getActiveLearnInfoReq({ id: id })); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| @@ -36,7 +36,7 @@ function ActiveLearnInfo() { | |||||
| <div className={styles['auto-ml-info']}> | <div className={styles['auto-ml-info']}> | ||||
| <PageTitle title="实验详情"></PageTitle> | <PageTitle title="实验详情"></PageTitle> | ||||
| <div className={styles['auto-ml-info__content']}> | <div className={styles['auto-ml-info__content']}> | ||||
| <BasicInfo info={info} /> | |||||
| <ActiveLearnBasic info={info} /> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -1,4 +1,4 @@ | |||||
| .auto-ml-instance { | |||||
| .active-learn-instance { | |||||
| height: 100%; | height: 100%; | ||||
| &__tabs { | &__tabs { | ||||
| @@ -1,7 +1,6 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | |||||
| import { getExperimentInsReq } from '@/services/autoML'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { getActiveLearnInsReq } from '@/services/activeLearn'; | |||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| import { safeInvoke } from '@/utils/functional'; | import { safeInvoke } from '@/utils/functional'; | ||||
| @@ -9,10 +8,11 @@ import { to } from '@/utils/promise'; | |||||
| import { useParams } from '@umijs/max'; | import { useParams } from '@umijs/max'; | ||||
| import { Tabs } from 'antd'; | import { Tabs } from 'antd'; | ||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||
| import BasicInfo from '../components/BasicInfo'; | |||||
| import ActiveLearnBasic from '../components/ActiveLearnBasic'; | |||||
| import ExperimentHistory from '../components/ExperimentHistory'; | import ExperimentHistory from '../components/ExperimentHistory'; | ||||
| import ExperimentLog from '../components/ExperimentLog'; | |||||
| import ExperimentResult from '../components/ExperimentResult'; | import ExperimentResult from '../components/ExperimentResult'; | ||||
| import { AutoMLInstanceData, HyperparameterData } from '../types'; | |||||
| import { ActiveLearnData, ActiveLearnInstanceData } from '../types'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| enum TabKeys { | enum TabKeys { | ||||
| @@ -22,12 +22,15 @@ enum TabKeys { | |||||
| History = 'history', | History = 'history', | ||||
| } | } | ||||
| function AutoMLInstance() { | |||||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | |||||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperparameterData | undefined>(undefined); | |||||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||||
| const NodePrefix = 'workflow'; | |||||
| function ActiveLearnInstance() { | |||||
| const [experimentInfo, setExperimentInfo] = useState<ActiveLearnData | undefined>(undefined); | |||||
| const [instanceInfo, setInstanceInfo] = useState<ActiveLearnInstanceData | undefined>(undefined); | |||||
| // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 | |||||
| const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); | |||||
| const [nodes, setNodes] = useState<Record<string, NodeStatus> | undefined>(undefined); | |||||
| const params = useParams(); | const params = useParams(); | ||||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||||
| const instanceId = safeInvoke(Number)(params.id); | const instanceId = safeInvoke(Number)(params.id); | ||||
| const evtSourceRef = useRef<EventSource | null>(null); | const evtSourceRef = useRef<EventSource | null>(null); | ||||
| @@ -38,41 +41,43 @@ function AutoMLInstance() { | |||||
| return () => { | return () => { | ||||
| closeSSE(); | closeSSE(); | ||||
| }; | }; | ||||
| }, []); | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, [instanceId]); | |||||
| // 获取实验实例详情 | // 获取实验实例详情 | ||||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | ||||
| const [res] = await to(getExperimentInsReq(instanceId)); | |||||
| const [res] = await to(getActiveLearnInsReq(instanceId)); | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const info = res.data as AutoMLInstanceData; | |||||
| const info = res.data as ActiveLearnInstanceData; | |||||
| const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | ||||
| // 解析配置参数 | // 解析配置参数 | ||||
| const paramJson = parseJsonText(param); | const paramJson = parseJsonText(param); | ||||
| if (paramJson) { | if (paramJson) { | ||||
| setAutoMLInfo(paramJson); | |||||
| setExperimentInfo(paramJson.data); | |||||
| } | } | ||||
| setInstanceInfo(info); | |||||
| // 这个接口返回的状态有延时,SSE 返回的状态是最新的 | // 这个接口返回的状态有延时,SSE 返回的状态是最新的 | ||||
| // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE | |||||
| // SSE 调用时,不需要解析 node_status,也不要重新建立 SSE | |||||
| if (isStatusDetermined) { | if (isStatusDetermined) { | ||||
| setInstanceInfo((prev) => ({ | |||||
| ...info, | |||||
| nodeStatus: prev!.nodeStatus, | |||||
| })); | |||||
| return; | return; | ||||
| } | } | ||||
| // 进行节点状态 | // 进行节点状态 | ||||
| const nodeStatusJson = parseJsonText(node_status); | const nodeStatusJson = parseJsonText(node_status); | ||||
| if (nodeStatusJson) { | if (nodeStatusJson) { | ||||
| Object.keys(nodeStatusJson).forEach((key) => { | |||||
| if (key.startsWith('auto-ml')) { | |||||
| const value = nodeStatusJson[key]; | |||||
| info.nodeStatus = value; | |||||
| setNodes(nodeStatusJson); | |||||
| Object.keys(nodeStatusJson).some((key) => { | |||||
| if (key.startsWith(NodePrefix)) { | |||||
| const workflowStatus = nodeStatusJson[key]; | |||||
| setWorkflowStatus(workflowStatus); | |||||
| return true; | |||||
| } | } | ||||
| return false; | |||||
| }); | }); | ||||
| } | } | ||||
| setInstanceInfo(info); | |||||
| // 运行中或者等待中,开启 SSE | // 运行中或者等待中,开启 SSE | ||||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | ||||
| setupSSE(argo_ins_name, argo_ins_ns); | setupSSE(argo_ins_name, argo_ins_ns); | ||||
| @@ -81,10 +86,7 @@ function AutoMLInstance() { | |||||
| }; | }; | ||||
| const setupSSE = (name: string, namespace: string) => { | const setupSSE = (name: string, namespace: string) => { | ||||
| let { origin } = location; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| origin = 'http://172.20.32.181:31213'; | |||||
| } | |||||
| const { origin } = location; | |||||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | ||||
| const evtSource = new EventSource( | const evtSource = new EventSource( | ||||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | ||||
| @@ -99,19 +101,21 @@ function AutoMLInstance() { | |||||
| if (dataJson) { | if (dataJson) { | ||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (nodes) { | if (nodes) { | ||||
| const statusData = Object.values(nodes).find((node: any) => | |||||
| node.displayName.startsWith('auto-ml'), | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||||
| node.displayName.startsWith(NodePrefix), | |||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| if (statusData) { | |||||
| setInstanceInfo((prev) => ({ | |||||
| ...prev!, | |||||
| nodeStatus: statusData, | |||||
| })); | |||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| // 设置工作流状态 | |||||
| if (workflowStatus) { | |||||
| setWorkflowStatus(workflowStatus); | |||||
| // 实验结束,关闭 SSE | // 实验结束,关闭 SSE | ||||
| if ( | if ( | ||||
| statusData.phase !== ExperimentStatus.Pending && | |||||
| statusData.phase !== ExperimentStatus.Running | |||||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||||
| workflowStatus.phase !== ExperimentStatus.Running | |||||
| ) { | ) { | ||||
| closeSSE(); | closeSSE(); | ||||
| getExperimentInsInfo(true); | getExperimentInsInfo(true); | ||||
| @@ -140,10 +144,10 @@ function AutoMLInstance() { | |||||
| label: '基本信息', | label: '基本信息', | ||||
| icon: <KFIcon type="icon-jibenxinxi" />, | icon: <KFIcon type="icon-jibenxinxi" />, | ||||
| children: ( | children: ( | ||||
| <BasicInfo | |||||
| className={styles['auto-ml-instance__basic']} | |||||
| info={autoMLInfo} | |||||
| runStatus={instanceInfo?.nodeStatus} | |||||
| <ActiveLearnBasic | |||||
| className={styles['active-learn-instance__basic']} | |||||
| info={experimentInfo} | |||||
| runStatus={workflowStatus} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -153,17 +157,8 @@ function AutoMLInstance() { | |||||
| label: '日志', | label: '日志', | ||||
| icon: <KFIcon type="icon-rizhi1" />, | icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['auto-ml-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> | |||||
| )} | |||||
| <div className={styles['active-learn-instance__log']}> | |||||
| {instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />} | |||||
| </div> | </div> | ||||
| ), | ), | ||||
| }, | }, | ||||
| @@ -174,24 +169,13 @@ function AutoMLInstance() { | |||||
| key: TabKeys.Result, | key: TabKeys.Result, | ||||
| label: '实验结果', | label: '实验结果', | ||||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | icon: <KFIcon type="icon-shiyanjieguo1" />, | ||||
| children: ( | |||||
| <ExperimentResult | |||||
| fileUrl={instanceInfo?.result_path} | |||||
| imageUrl={instanceInfo?.img_path} | |||||
| modelPath={instanceInfo?.model_path} | |||||
| /> | |||||
| ), | |||||
| children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />, | |||||
| }, | }, | ||||
| { | { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: 'Trial 列表', | |||||
| label: '寻优列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: ( | |||||
| <ExperimentHistory | |||||
| fileUrl={instanceInfo?.run_history_path} | |||||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||||
| /> | |||||
| ), | |||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -201,15 +185,10 @@ function AutoMLInstance() { | |||||
| : basicTabItems; | : basicTabItems; | ||||
| return ( | return ( | ||||
| <div className={styles['auto-ml-instance']}> | |||||
| <Tabs | |||||
| className={styles['auto-ml-instance__tabs']} | |||||
| items={tabItems} | |||||
| activeKey={activeTab} | |||||
| onChange={setActiveTab} | |||||
| /> | |||||
| <div className={styles['active-learn-instance']}> | |||||
| <Tabs className={styles['active-learn-instance__tabs']} items={tabItems} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| export default AutoMLInstance; | |||||
| export default ActiveLearnInstance; | |||||
| @@ -1,4 +1,4 @@ | |||||
| .hyper-parameter-basic { | |||||
| .active-learn-basic { | |||||
| height: 100%; | height: 100%; | ||||
| padding: 20px @content-padding; | padding: 20px @content-padding; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| @@ -73,7 +73,7 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| const modelInfo = [ | const modelInfo = [ | ||||
| { | { | ||||
| label: '模型', | |||||
| label: '预训练模型', | |||||
| value: info.model, | value: info.model, | ||||
| format: formatModel, | format: formatModel, | ||||
| }, | }, | ||||
| @@ -85,6 +85,10 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| label: '模型类名称', | label: '模型类名称', | ||||
| value: info.model_class_name, | value: info.model_class_name, | ||||
| }, | }, | ||||
| { | |||||
| label: 'epochs', | |||||
| value: info.epochs, | |||||
| }, | |||||
| ]; | ]; | ||||
| const lossInfo = [ | const lossInfo = [ | ||||
| @@ -96,6 +100,10 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| label: 'loss类名', | label: 'loss类名', | ||||
| value: info.loss_class_name, | value: info.loss_class_name, | ||||
| }, | }, | ||||
| { | |||||
| label: '学习率', | |||||
| value: info.lr, | |||||
| }, | |||||
| ]; | ]; | ||||
| const algorithmInfo = [ | const algorithmInfo = [ | ||||
| @@ -171,15 +179,15 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| }, | }, | ||||
| { | { | ||||
| label: '初始训练数据量', | label: '初始训练数据量', | ||||
| value: info.ninitial, | |||||
| value: info.initial_num, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '查询次数', | label: '查询次数', | ||||
| value: info.nqueries, | |||||
| value: info.queries_num, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '每次查询数据量', | label: '每次查询数据量', | ||||
| value: info.ninstances, | |||||
| value: info.instances_num, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '查询策略', | label: '查询策略', | ||||
| @@ -187,21 +195,13 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| format: formatEnum(queryStrategies), | format: formatEnum(queryStrategies), | ||||
| }, | }, | ||||
| { | { | ||||
| label: '轮数', | |||||
| value: info.ncheckpoint, | |||||
| label: '检查点轮数', | |||||
| value: info.checkpoint_num, | |||||
| }, | }, | ||||
| { | { | ||||
| label: 'batch_size', | label: 'batch_size', | ||||
| value: info.batch_size, | value: info.batch_size, | ||||
| }, | }, | ||||
| { | |||||
| label: 'epochs', | |||||
| value: info.epochs, | |||||
| }, | |||||
| { | |||||
| label: '学习率', | |||||
| value: info.lr, | |||||
| }, | |||||
| ]; | ]; | ||||
| }, [info, getResourceDescription]); | }, [info, getResourceDescription]); | ||||
| @@ -248,7 +248,7 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| }, [runStatus]); | }, [runStatus]); | ||||
| return ( | return ( | ||||
| <div className={classNames(styles['hyper-parameter-basic'], className)}> | |||||
| <div className={classNames(styles['active-learn-basic'], className)}> | |||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="运行信息" | title="运行信息" | ||||
| @@ -71,17 +71,7 @@ function ExecuteConfig() { | |||||
| <> | <> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | |||||
| label="模型" | |||||
| name="model" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择模型', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <Form.Item label="预训练模型" name="model"> | |||||
| <ResourceSelect | <ResourceSelect | ||||
| type={ResourceSelectorType.Model} | type={ResourceSelectorType.Model} | ||||
| placeholder="请选择模型" | placeholder="请选择模型" | ||||
| @@ -123,6 +113,22 @@ function ExecuteConfig() { | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="epochs" | |||||
| name="epochs" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入epochs', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入epochs" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| {frameworkType === FrameworkType.Pytorch ? ( | {frameworkType === FrameworkType.Pytorch ? ( | ||||
| <> | <> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| @@ -149,7 +155,7 @@ function ExecuteConfig() { | |||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="loss类名" | |||||
| label="loss 类名" | |||||
| name="loss_class_name" | name="loss_class_name" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| @@ -167,6 +173,22 @@ function ExecuteConfig() { | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="学习率" | |||||
| name="lr" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入学习率', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入学习率" min={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | </> | ||||
| ) : null} | ) : null} | ||||
| </> | </> | ||||
| @@ -391,7 +413,7 @@ function ExecuteConfig() { | |||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="初始训练数据量" | label="初始训练数据量" | ||||
| name="ninitial" | |||||
| name="initial_num" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| @@ -408,7 +430,7 @@ function ExecuteConfig() { | |||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="查询次数" | label="查询次数" | ||||
| name="nqueries" | |||||
| name="queries_num" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| @@ -425,7 +447,7 @@ function ExecuteConfig() { | |||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="每次查询数据量" | label="每次查询数据量" | ||||
| name="ninstances" | |||||
| name="instances_num" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| @@ -458,17 +480,17 @@ function ExecuteConfig() { | |||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="轮数" | |||||
| name="ncheckpoint" | |||||
| label="检查点轮数" | |||||
| name="checkpoint_num" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| message: '请输入轮数', | |||||
| message: '请输入检查点轮数', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| tooltip="多少轮查询保存一次模型参数" | tooltip="多少轮查询保存一次模型参数" | ||||
| > | > | ||||
| <InputNumber placeholder="请输入轮数" min={0} precision={0} /> | |||||
| <InputNumber placeholder="请输入检查点轮数" min={0} precision={0} /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -489,40 +511,6 @@ function ExecuteConfig() { | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="epochs" | |||||
| name="epochs" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入epochs', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入epochs" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="学习率" | |||||
| name="lr" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入学习率', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入学习率" min={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | </> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,145 +0,0 @@ | |||||
| .metrics-weight { | |||||
| margin-bottom: 20px; | |||||
| &:last-child { | |||||
| margin-bottom: 0; | |||||
| } | |||||
| } | |||||
| .add-weight { | |||||
| margin-bottom: 0 !important; | |||||
| // 增加样式权重 | |||||
| & &__button { | |||||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||||
| box-shadow: none !important; | |||||
| &:hover { | |||||
| border-style: solid; | |||||
| } | |||||
| } | |||||
| } | |||||
| .hyper-parameter { | |||||
| width: 83.33%; | |||||
| margin-bottom: 20px; | |||||
| border: 1px solid rgba(234, 234, 234, 0.8); | |||||
| border-radius: 4px; | |||||
| &__header { | |||||
| height: 50px; | |||||
| padding-left: 8px; | |||||
| color: @text-color; | |||||
| font-size: @font-size; | |||||
| background: #f8f8f9; | |||||
| border-radius: 4px 4px 0px 0px; | |||||
| &__name, | |||||
| &__type, | |||||
| &__space { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| margin-right: 15px; | |||||
| &::before { | |||||
| display: inline-block; | |||||
| color: @error-color; | |||||
| font-size: 14px; | |||||
| font-family: SimSun, sans-serif; | |||||
| line-height: 1; | |||||
| content: '*'; | |||||
| margin-inline-end: 4px; | |||||
| } | |||||
| :global { | |||||
| .anticon-question-circle { | |||||
| vertical-align: middle; | |||||
| cursor: help; | |||||
| } | |||||
| } | |||||
| } | |||||
| &__tooltip { | |||||
| max-width: 600px; | |||||
| :global { | |||||
| .ant-tooltip-inner { | |||||
| max-height: 400px; | |||||
| overflow-y: auto; | |||||
| white-space: pre-line; | |||||
| &::-webkit-scrollbar-thumb { | |||||
| background: rgba(255, 255, 255, 0.5); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| &__operation { | |||||
| flex: none; | |||||
| width: 100px; | |||||
| } | |||||
| } | |||||
| &__body { | |||||
| padding: 8px; | |||||
| border-bottom: 1px solid rgba(234, 234, 234, 0.8); | |||||
| &:last-child { | |||||
| border-bottom: none; | |||||
| } | |||||
| &__name, | |||||
| &__type, | |||||
| &__space { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| margin-right: 15px; | |||||
| margin-bottom: 0 !important; | |||||
| } | |||||
| &__operation { | |||||
| display: flex; | |||||
| flex: none; | |||||
| align-items: center; | |||||
| width: 100px; | |||||
| height: 46px; | |||||
| } | |||||
| } | |||||
| &__add { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| padding: 15px 0; | |||||
| } | |||||
| } | |||||
| .run-parameter { | |||||
| width: calc(41.66% + 126px); | |||||
| margin-bottom: 20px; | |||||
| &__body { | |||||
| flex: 1; | |||||
| margin-right: 10px; | |||||
| padding: 20px 20px 0; | |||||
| border: 1px dashed #e0e0e0; | |||||
| border-radius: 8px; | |||||
| :global { | |||||
| .ant-form-item-label { | |||||
| label { | |||||
| width: calc(100% - 10px); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| &__operation { | |||||
| display: flex; | |||||
| flex: none; | |||||
| align-items: center; | |||||
| width: 100px; | |||||
| } | |||||
| &__error { | |||||
| margin-top: -20px; | |||||
| color: @error-color; | |||||
| } | |||||
| } | |||||
| @@ -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%; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,106 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | |||||
| import { NodeStatus } from '@/types'; | |||||
| import { Tabs } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentLogProps = { | |||||
| instanceInfo: ActiveLearnInstanceData; | |||||
| nodes: Record<string, NodeStatus>; | |||||
| }; | |||||
| 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('active-learn')) { | |||||
| 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-framework', | |||||
| // label: '框架代码日志', | |||||
| // // icon: <KFIcon type="icon-rizhi1" />, | |||||
| // children: ( | |||||
| // <div className={styles['experiment-log__tabs__log']}> | |||||
| // {frameworkCloneNodeStatus && ( | |||||
| // <LogList | |||||
| // instanceName={instanceInfo.argo_ins_name} | |||||
| // instanceNamespace={instanceInfo.argo_ins_ns} | |||||
| // pipelineNodeId={frameworkCloneNodeStatus.displayName} | |||||
| // workflowId={frameworkCloneNodeStatus.id} | |||||
| // instanceNodeStartTime={frameworkCloneNodeStatus.startedAt} | |||||
| // instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus} | |||||
| // ></LogList> | |||||
| // )} | |||||
| // </div> | |||||
| // ), | |||||
| // }, | |||||
| { | |||||
| key: 'git-clone-train', | |||||
| label: '系统日志', | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | |||||
| children: ( | |||||
| <div className={styles['experiment-log__tabs__log']}> | |||||
| {trainCloneNodeStatus && ( | |||||
| <LogList | |||||
| instanceName={instanceInfo.argo_ins_name} | |||||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||||
| pipelineNodeId={trainCloneNodeStatus.displayName} | |||||
| workflowId={trainCloneNodeStatus.id} | |||||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | |||||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | |||||
| ></LogList> | |||||
| )} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: 'active-learn', | |||||
| label: '主动学习日志', | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | |||||
| children: ( | |||||
| <div className={styles['experiment-log__tabs__log']}> | |||||
| {hpoNodeStatus && ( | |||||
| <LogList | |||||
| instanceName={instanceInfo.argo_ins_name} | |||||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||||
| pipelineNodeId={hpoNodeStatus.displayName} | |||||
| workflowId={hpoNodeStatus.id} | |||||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | |||||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | |||||
| ></LogList> | |||||
| )} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['experiment-log']}> | |||||
| <Tabs className={styles['experiment-log__tabs']} items={tabItems} /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentLog; | |||||
| @@ -0,0 +1,120 @@ | |||||
| { | |||||
| "workflow-xwnb8": { | |||||
| "id": "workflow-xwnb8", | |||||
| "name": "workflow-xwnb8", | |||||
| "type": "DAG", | |||||
| "phase": "Failed", | |||||
| "children": [ | |||||
| "workflow-xwnb8-1083129199" | |||||
| ], | |||||
| "progress": "2/3", | |||||
| "startedAt": "2025-04-18T06:56:18Z", | |||||
| "finishedAt": "2025-04-18T06:57:32Z", | |||||
| "displayName": "workflow-xwnb8", | |||||
| "templateName": "ml-workflow", | |||||
| "outboundNodes": [ | |||||
| "workflow-xwnb8-1355608520" | |||||
| ], | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 42, | |||||
| "memory": 851, | |||||
| "nvidia.com/gpu": 10 | |||||
| } | |||||
| }, | |||||
| "git-clone-9d0c5965": { | |||||
| "id": "workflow-xwnb8-514970004", | |||||
| "name": "workflow-xwnb8.git-clone-9d0c5965", | |||||
| "type": "Pod", | |||||
| "phase": "Succeeded", | |||||
| "outputs": { | |||||
| "exitCode": "0", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-git-clone-9d0c5965-514970004/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "children": [ | |||||
| "workflow-xwnb8-1355608520" | |||||
| ], | |||||
| "progress": "1/1", | |||||
| "startedAt": "2025-04-18T06:56:38Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:56:49Z", | |||||
| "displayName": "git-clone-9d0c5965", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "git-clone-9d0c5965", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 1, | |||||
| "memory": 11 | |||||
| } | |||||
| }, | |||||
| "git-clone-e28c560c": { | |||||
| "id": "workflow-xwnb8-1083129199", | |||||
| "name": "workflow-xwnb8.git-clone-e28c560c", | |||||
| "type": "Pod", | |||||
| "phase": "Succeeded", | |||||
| "outputs": { | |||||
| "exitCode": "0", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-git-clone-e28c560c-1083129199/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "children": [ | |||||
| "workflow-xwnb8-514970004" | |||||
| ], | |||||
| "progress": "1/1", | |||||
| "startedAt": "2025-04-18T06:56:18Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:56:27Z", | |||||
| "displayName": "git-clone-e28c560c", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "git-clone-e28c560c", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 1, | |||||
| "memory": 11 | |||||
| } | |||||
| }, | |||||
| "active-learn-b708ed0b": { | |||||
| "id": "workflow-xwnb8-1355608520", | |||||
| "name": "workflow-xwnb8.active-learn-b708ed0b", | |||||
| "type": "Pod", | |||||
| "phase": "Failed", | |||||
| "message": "Error (exit code 1)", | |||||
| "outputs": { | |||||
| "exitCode": "1", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-active-learn-b708ed0b-1355608520/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "progress": "0/1", | |||||
| "startedAt": "2025-04-18T06:57:00Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:57:27Z", | |||||
| "displayName": "active-learn-b708ed0b", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "active-learn-b708ed0b", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 40, | |||||
| "memory": 829, | |||||
| "nvidia.com/gpu": 10 | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -35,14 +35,14 @@ export type FormData = { | |||||
| dataset_class_name: string; // dataset类名 | dataset_class_name: string; // dataset类名 | ||||
| data_size: number; // 数据量 | data_size: number; // 数据量 | ||||
| train_size: number; // 训练集数据量 | train_size: number; // 训练集数据量 | ||||
| ninitial: number; // 初始训练数据量 | |||||
| nqueries: number; // 查询次数 | |||||
| ninstances: number; // 每次查询数据量 | |||||
| initial_num: number; // 初始训练数据量 | |||||
| queries_num: number; // 查询次数 | |||||
| instances_num: number; // 每次查询数据量 | |||||
| computing_resource_id: number; // 资源规格 | computing_resource_id: number; // 资源规格 | ||||
| image: ParameterInputObject; // 镜像 | image: ParameterInputObject; // 镜像 | ||||
| shuffle: boolean; // 是否随机打乱 | shuffle: boolean; // 是否随机打乱 | ||||
| query_strategy: string; // 查询策略 | query_strategy: string; // 查询策略 | ||||
| ncheckpoint: number; // 多少轮查询保存一次模型参数 | |||||
| checkpoint_num: number; // 多少轮查询保存一次模型参数 | |||||
| batch_size: number; // batch_size | batch_size: number; // batch_size | ||||
| epochs: number; // epochs | epochs: number; // epochs | ||||
| lr: number; // 学习率 | lr: number; // 学习率 | ||||
| @@ -77,6 +77,7 @@ function EditorList() { | |||||
| content.forEach((item: EditorData) => { | content.forEach((item: EditorData) => { | ||||
| item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; | item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; | ||||
| item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; | item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; | ||||
| item.image = typeof item.image === 'string' ? parseJsonText(item.image) : null; | |||||
| }); | }); | ||||
| setTableData(content); | setTableData(content); | ||||
| setTotal(totalElements); | setTotal(totalElements); | ||||
| @@ -224,7 +225,7 @@ function EditorList() { | |||||
| }, | }, | ||||
| { | { | ||||
| title: '镜像', | title: '镜像', | ||||
| dataIndex: ['image'], | |||||
| dataIndex: ['image', 'showValue'], | |||||
| key: 'image', | key: 'image', | ||||
| width: '15%', | width: '15%', | ||||
| render: tableCellRender(true), | render: tableCellRender(true), | ||||
| @@ -54,10 +54,7 @@ function LogGroup({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 建立 socket 连接 | // 建立 socket 连接 | ||||
| const setupSockect = () => { | const setupSockect = () => { | ||||
| let { host } = location; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| host = '172.20.32.197:31213'; | |||||
| } | |||||
| const { host } = location; | |||||
| const socket = new WebSocket( | const socket = new WebSocket( | ||||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | ||||
| ); | ); | ||||
| @@ -20,7 +20,7 @@ function HyperparameterInfo() { | |||||
| undefined, | undefined, | ||||
| ); | ); | ||||
| // 获取自动机器学习详情 | |||||
| // 获取详情 | |||||
| const getHyperparameterInfo = useCallback(async () => { | const getHyperparameterInfo = useCallback(async () => { | ||||
| const [res] = await to(getRayInfoReq({ id: hyperparameterId })); | const [res] = await to(getRayInfoReq({ id: hyperparameterId })); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| @@ -3,7 +3,6 @@ import LogList from '@/pages/Experiment/components/LogList'; | |||||
| import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import { Tabs } from 'antd'; | import { Tabs } from 'antd'; | ||||
| import { useEffect } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentLogProps = { | type ExperimentLogProps = { | ||||
| @@ -97,8 +96,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| useEffect(() => {}, []); | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-log']}> | <div className={styles['experiment-log']}> | ||||
| <Tabs className={styles['experiment-log__tabs']} items={tabItems} /> | <Tabs className={styles['experiment-log__tabs']} items={tabItems} /> | ||||
| @@ -41,7 +41,7 @@ export type HyperParameterData = { | |||||
| status_list: string; // 最近五次运行状态 | status_list: string; // 最近五次运行状态 | ||||
| } & FormData; | } & FormData; | ||||
| // 自动机器学习实验实例 | |||||
| // 实验实例 | |||||
| export type HyperParameterInstanceData = { | export type HyperParameterInstanceData = { | ||||
| id: number; | id: number; | ||||
| ray_id: number; | ray_id: number; | ||||
| @@ -0,0 +1,12 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-04-21 16:38:59 | |||||
| * @Description: 知识图谱 | |||||
| */ | |||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||||
| function KnowledgePage() { | |||||
| return <IframePage type={IframePageType.Knowledge}></IframePage>; | |||||
| } | |||||
| export default KnowledgePage; | |||||
| @@ -292,7 +292,7 @@ function ServiceInfo() { | |||||
| }, | }, | ||||
| { | { | ||||
| title: '版本镜像', | title: '版本镜像', | ||||
| dataIndex: 'image', | |||||
| dataIndex: ['image', 'showValue'], | |||||
| key: 'image', | key: 'image', | ||||
| width: '20%', | width: '20%', | ||||
| render: tableCellRender(true), | render: tableCellRender(true), | ||||
| @@ -20,7 +20,7 @@ export const elapsedTime = (begin?: string | null, end?: string | null): string | |||||
| const timestamp = endDate.valueOf() - beginDate.valueOf(); | const timestamp = endDate.valueOf() - beginDate.valueOf(); | ||||
| if (timestamp < 0) { | if (timestamp < 0) { | ||||
| return '时间有误'; | |||||
| return '0秒'; | |||||
| } | } | ||||
| const duration = dayjs.duration(timestamp); | const duration = dayjs.duration(timestamp); | ||||
| const years = duration.years(); | const years = duration.years(); | ||||