Unverified Commit 3e62e60b authored by liuzhe-lz's avatar liuzhe-lz Committed by GitHub
Browse files

Refactor web UI to support incremental metric loading (#1557)

* Refactor web UI to support incremental metric loading

* refactor

* Remove host job

* Move sequence ID to NNI manager

* implement incremental loading
parent 99f7d79c
......@@ -22,8 +22,6 @@ interface DurationState {
class Duration extends React.Component<DurationProps, DurationState> {
public _isMounted = false;
constructor(props: DurationProps) {
super(props);
......@@ -142,15 +140,12 @@ class Duration extends React.Component<DurationProps, DurationState> {
trialId: trialId,
trialTime: trialTime
});
if (this._isMounted) {
this.setState({
durationSource: this.getOption(trialRun[0])
});
}
this.setState({
durationSource: this.getOption(trialRun[0])
});
}
componentDidMount() {
this._isMounted = true;
const { source } = this.props;
this.drawDurationGraph(source);
}
......@@ -187,10 +182,6 @@ class Duration extends React.Component<DurationProps, DurationState> {
return false;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { durationSource } = this.state;
return (
......@@ -206,4 +197,4 @@ class Duration extends React.Component<DurationProps, DurationState> {
}
}
export default Duration;
\ No newline at end of file
export default Duration;
......@@ -24,7 +24,6 @@ interface IntermediateProps {
class Intermediate extends React.Component<IntermediateProps, IntermediateState> {
static intervalMediate = 1;
public _isMounted = false;
public pointInput: HTMLInputElement | null;
public minValInput: HTMLInputElement | null;
public maxValInput: HTMLInputElement | null;
......@@ -45,12 +44,10 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState>
drawIntermediate = (source: Array<TableObj>) => {
if (source.length > 0) {
if (this._isMounted) {
this.setState(() => ({
length: source.length,
detailSource: source
}));
}
this.setState({
length: source.length,
detailSource: source
});
const trialIntermediate: Array<Intermedia> = [];
Object.keys(source).map(item => {
const temp = source[item];
......@@ -118,11 +115,9 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState>
},
series: trialIntermediate
};
if (this._isMounted) {
this.setState(() => ({
interSource: option
}));
}
this.setState({
interSource: option
});
} else {
const nullData = {
grid: {
......@@ -139,71 +134,60 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState>
name: 'Metric'
}
};
if (this._isMounted) {
this.setState(() => ({ interSource: nullData }));
}
this.setState({ interSource: nullData });
}
}
// confirm btn function [filter data]
filterLines = () => {
if (this._isMounted) {
const filterSource: Array<TableObj> = [];
this.setState({ isLoadconfirmBtn: true }, () => {
const { source } = this.props;
// get input value
const pointVal = this.pointInput !== null ? this.pointInput.value : '';
const minVal = this.minValInput !== null ? this.minValInput.value : '';
const maxVal = this.maxValInput !== null ? this.maxValInput.value : '';
// user not input message
if (pointVal === '' || minVal === '') {
alert('Please input filter message');
const filterSource: Array<TableObj> = [];
this.setState({ isLoadconfirmBtn: true }, () => {
const { source } = this.props;
// get input value
const pointVal = this.pointInput !== null ? this.pointInput.value : '';
const minVal = this.minValInput !== null ? this.minValInput.value : '';
const maxVal = this.maxValInput !== null ? this.maxValInput.value : '';
// user not input message
if (pointVal === '' || minVal === '') {
alert('Please input filter message');
} else {
// user not input max value
const position = JSON.parse(pointVal);
const min = JSON.parse(minVal);
if (maxVal === '') {
Object.keys(source).map(item => {
const temp = source[item];
const val = temp.description.intermediate[position - 1];
if (val >= min) {
filterSource.push(temp);
}
});
} else {
// user not input max value
const position = JSON.parse(pointVal);
const min = JSON.parse(minVal);
if (maxVal === '') {
Object.keys(source).map(item => {
const temp = source[item];
const val = temp.description.intermediate[position - 1];
if (val >= min) {
filterSource.push(temp);
}
});
} else {
const max = JSON.parse(maxVal);
Object.keys(source).map(item => {
const temp = source[item];
const val = temp.description.intermediate[position - 1];
if (val >= min && val <= max) {
filterSource.push(temp);
}
});
}
if (this._isMounted) {
this.setState({ filterSource: filterSource });
}
this.drawIntermediate(filterSource);
}
const counts = this.state.clickCounts + 1;
if (this._isMounted) {
this.setState({ isLoadconfirmBtn: false, clickCounts: counts });
const max = JSON.parse(maxVal);
Object.keys(source).map(item => {
const temp = source[item];
const val = temp.description.intermediate[position - 1];
if (val >= min && val <= max) {
filterSource.push(temp);
}
});
}
});
}
this.setState({ filterSource: filterSource });
this.drawIntermediate(filterSource);
}
const counts = this.state.clickCounts + 1;
this.setState({ isLoadconfirmBtn: false, clickCounts: counts });
});
}
switchTurn = (checked: boolean) => {
if (this._isMounted) {
this.setState({ isFilter: checked });
}
this.setState({ isFilter: checked });
if (checked === false) {
this.drawIntermediate(this.props.source);
}
}
componentDidMount() {
this._isMounted = true;
const { source } = this.props;
this.drawIntermediate(source);
}
......@@ -272,10 +256,6 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState>
return false;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { interSource, isLoadconfirmBtn, isFilter } = this.state;
return (
......
......@@ -40,8 +40,6 @@ message.config({
class Para extends React.Component<ParaProps, ParaState> {
public _isMounted = false;
private chartMulineStyle = {
width: '100%',
height: 392,
......@@ -121,15 +119,12 @@ class Para extends React.Component<ParaProps, ParaState> {
this.swapGraph(paraData, swapAxisArr);
}
this.getOption(paraData, lengthofTrials);
if (this._isMounted === true) {
this.setState(() => ({ paraBack: paraData }));
}
this.setState({ paraBack: paraData });
}
hyperParaPic = (source: Array<TableObj>, searchSpace: string) => {
// filter succeed trials [{}, {}, {}]
const origin = source.filter(filterByStatus);
const dataSource: Array<TableObj> = JSON.parse(JSON.stringify(origin));
const dataSource = source.filter(filterByStatus);
const lenOfDataSource: number = dataSource.length;
const accPara: Array<number> = [];
// specific value array
......@@ -139,15 +134,13 @@ class Para extends React.Component<ParaProps, ParaState> {
// nest search space
let isNested: boolean = false;
Object.keys(searchRange).map(item => {
if (typeof searchRange[item]._value[0] === 'object') {
if (searchRange[item]._value && typeof searchRange[item]._value[0] === 'object') {
isNested = true;
return;
}
});
const dimName = Object.keys(searchRange);
if (this._isMounted === true) {
this.setState(() => ({ dimName: dimName }));
}
this.setState({ dimName: dimName });
const parallelAxis: Array<Dimobj> = [];
// search space range and specific value [only number]
......@@ -324,23 +317,21 @@ class Para extends React.Component<ParaProps, ParaState> {
color: ['#CA0000', '#FFC400', '#90EE90']
}
};
if (this._isMounted === true) {
this.setState({
paraNodata: 'No data',
option: optionOfNull,
sutrialCount: 0,
succeedRenderCount: 0
});
}
this.setState({
paraNodata: 'No data',
option: optionOfNull,
sutrialCount: 0,
succeedRenderCount: 0
});
} else {
Object.keys(dataSource).map(item => {
const temp = dataSource[item];
eachTrialParams.push(temp.description.parameters);
const trial = dataSource[item];
eachTrialParams.push(trial.description.parameters.error || '');
// may be a succeed trial hasn't final result
// all detail page may be break down if havn't if
if (temp.acc !== undefined) {
if (temp.acc.default !== undefined) {
accPara.push(temp.acc.default);
if (trial.acc !== undefined) {
if (trial.acc.default !== undefined) {
accPara.push(JSON.parse(trial.acc.default));
}
}
});
......@@ -361,14 +352,12 @@ class Para extends React.Component<ParaProps, ParaState> {
});
});
}
if (this._isMounted) {
// if not return final result
const maxVal = accPara.length === 0 ? 1 : Math.max(...accPara);
const minVal = accPara.length === 0 ? 1 : Math.min(...accPara);
this.setState({ max: maxVal, min: minVal }, () => {
this.getParallelAxis(dimName, parallelAxis, accPara, eachTrialParams, lenOfDataSource);
});
}
// if not return final result
const maxVal = accPara.length === 0 ? 1 : Math.max(...accPara);
const minVal = accPara.length === 0 ? 1 : Math.min(...accPara);
this.setState({ max: maxVal, min: minVal }, () => {
this.getParallelAxis(dimName, parallelAxis, accPara, eachTrialParams, lenOfDataSource);
});
}
}
......@@ -376,11 +365,9 @@ class Para extends React.Component<ParaProps, ParaState> {
percentNum = (value: string) => {
let vals = parseFloat(value);
if (this._isMounted) {
this.setState({ percent: vals }, () => {
this.reInit();
});
}
this.setState({ percent: vals }, () => {
this.reInit();
});
}
// deal with response data into pic data
......@@ -445,22 +432,17 @@ class Para extends React.Component<ParaProps, ParaState> {
}
};
// please wait the data
if (this._isMounted) {
this.setState(() => ({
option: optionown,
paraNodata: '',
succeedRenderCount: lengthofTrials,
sutrialCount: paralleData.length
}));
}
this.setState({
option: optionown,
paraNodata: '',
succeedRenderCount: lengthofTrials,
sutrialCount: paralleData.length
});
}
// get swap parallel axis
getSwapArr = (value: Array<string>) => {
if (this._isMounted) {
this.setState(() => ({ swapAxisArr: value }));
}
this.setState({ swapAxisArr: value });
}
reInit = () => {
......@@ -471,9 +453,7 @@ class Para extends React.Component<ParaProps, ParaState> {
swapReInit = () => {
const { clickCounts, succeedRenderCount } = this.state;
const val = clickCounts + 1;
if (this._isMounted) {
this.setState({ isLoadConfirm: true, clickCounts: val, });
}
this.setState({ isLoadConfirm: true, clickCounts: val, });
const { paraBack, swapAxisArr } = this.state;
const paralDim = paraBack.parallelAxis;
const paraData = paraBack.data;
......@@ -523,11 +503,9 @@ class Para extends React.Component<ParaProps, ParaState> {
});
this.getOption(paraBack, succeedRenderCount);
// please wait the data
if (this._isMounted) {
this.setState(() => ({
isLoadConfirm: false
}));
}
this.setState({
isLoadConfirm: false
});
}
sortDimY = (a: Dimobj, b: Dimobj) => {
......@@ -585,7 +563,6 @@ class Para extends React.Component<ParaProps, ParaState> {
}
componentDidMount() {
this._isMounted = true;
this.reInit();
}
......@@ -623,10 +600,6 @@ class Para extends React.Component<ParaProps, ParaState> {
return false;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { option, paraNodata, dimName, isLoadConfirm } = this.state;
return (
......@@ -687,4 +660,4 @@ class Para extends React.Component<ParaProps, ParaState> {
}
}
export default Para;
\ No newline at end of file
export default Para;
// when there are more trials than this threshold, metrics will be updated in group of this size to avoid freezing
const METRIC_GROUP_UPDATE_THRESHOLD = 100;
const METRIC_GROUP_UPDATE_SIZE = 20;
const MANAGER_IP = `/api/v1/nni`;
const DOWNLOAD_IP = `/logs`;
const trialJobStatus = [
......@@ -65,9 +69,10 @@ const COLUMN_INDEX = [
// defatult selected column
const COLUMN = ['Trial No.', 'ID', 'Duration', 'Status', 'Default', 'Operation'];
// all choice column !dictory final
const COLUMNPro = ['Trial No.', 'ID', 'StartTime', 'EndTime', 'Duration', 'Status',
const COLUMNPro = ['Trial No.', 'ID', 'Start Time', 'End Time', 'Duration', 'Status',
'Intermediate count', 'Default', 'Operation'];
export {
MANAGER_IP, DOWNLOAD_IP, trialJobStatus, COLUMNPro,
CONTROLTYPE, MONACO, COLUMN, COLUMN_INDEX, DRAWEROPTION
CONTROLTYPE, MONACO, COLUMN, COLUMN_INDEX, DRAWEROPTION,
METRIC_GROUP_UPDATE_THRESHOLD, METRIC_GROUP_UPDATE_SIZE,
};
import { Experiment } from './model/experiment';
import { TrialManager } from './model/trialmanager';
const EXPERIMENT = new Experiment();
const TRIALS = new TrialManager();
export { EXPERIMENT, TRIALS };
import axios from 'axios';
import { message } from 'antd';
import { MANAGER_IP } from './const';
import { FinalResult, FinalType, TableObj } from './interface';
import { MetricDataRecord, FinalType, TableObj } from './interface';
const convertTime = (num: number) => {
if (num <= 0) {
return '0';
}
if (num % 3600 === 0) {
return num / 3600 + 'h';
} else {
......@@ -15,24 +18,28 @@ const convertTime = (num: number) => {
// trial's duration, accurate to seconds for example 10min 30s
const convertDuration = (num: number) => {
if (num < 1) {
return '0s';
}
const hour = Math.floor(num / 3600);
const min = Math.floor(num / 60 % 60);
const minute = Math.floor(num / 60 % 60);
const second = Math.floor(num % 60);
const result = hour > 0 ? `${hour} h ${min} min ${second}s` : `${min} min ${second}s`;
if (hour <= 0 && min === 0 && second !== 0) {
return `${second}s`;
} else if (hour === 0 && min !== 0 && second === 0) {
return `${min}min`;
} else if (hour === 0 && min !== 0 && second !== 0) {
return `${min}min ${second}s`;
} else {
return result;
let result = [ ];
if (hour > 0) {
result.push(`${hour}h`);
}
if (minute > 0) {
result.push(`${minute}min`);
}
if (second > 0) {
result.push(`${second}s`);
}
return result.join(' ');
};
// get final result value
// draw Accuracy point graph
const getFinalResult = (final: Array<FinalResult>) => {
const getFinalResult = (final?: MetricDataRecord[]) => {
let acc;
let showDefault = 0;
if (final) {
......@@ -51,7 +58,7 @@ const getFinalResult = (final: Array<FinalResult>) => {
};
// get final result value // acc obj
const getFinal = (final: Array<FinalResult>) => {
const getFinal = (final?: MetricDataRecord[]) => {
let showDefault: FinalType;
if (final) {
showDefault = JSON.parse(final[final.length - 1].data);
......@@ -101,7 +108,7 @@ const intermediateGraphOption = (intermediateArr: number[], id: string) => {
};
// kill job
const killJob = (key: number, id: string, status: string, updateList: Function) => {
const killJob = (key: number, id: string, status: string, updateList?: Function) => {
axios(`${MANAGER_IP}/trial-jobs/${id}`, {
method: 'DELETE',
headers: {
......@@ -113,7 +120,9 @@ const killJob = (key: number, id: string, status: string, updateList: Function)
message.destroy();
message.success('Cancel the job successfully');
// render the table
updateList();
if (updateList) {
updateList(); // FIXME
}
} else {
message.error('fail to cancel the job');
}
......@@ -160,7 +169,22 @@ const downFile = (content: string, fileName: string) => {
}
};
function formatTimestamp(timestamp?: number, placeholder?: string = 'N/A'): string {
return timestamp ? new Date(timestamp).toLocaleString('en-US') : placeholder;
}
function metricAccuracy(metric: MetricDataRecord): number {
const data = JSON.parse(metric.data);
return typeof data === 'number' ? data : NaN;
}
function formatAccuracy(accuracy: number): string {
// TODO: how to format NaN?
return accuracy.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
}
export {
convertTime, convertDuration, getFinalResult, getFinal, downFile,
intermediateGraphOption, killJob, filterByStatus, filterDuration
intermediateGraphOption, killJob, filterByStatus, filterDuration,
formatAccuracy, formatTimestamp, metricAccuracy,
};
// tslint:disable:no-any
// draw accuracy graph data interface
interface TableObj {
key: number;
......@@ -12,6 +14,19 @@ interface TableObj {
endTime?: number;
}
interface TableRecord {
key: string;
sequenceId: number;
startTime: number;
endTime?: number;
id: string;
duration: number;
status: string;
intermediateCount: number;
accuracy?: number;
latestAccuracy: string; // formatted string
}
interface SearchSpace {
_value: Array<number | string>;
_type: string;
......@@ -32,26 +47,6 @@ interface Parameters {
multiProgress?: number;
}
interface Experiment {
id: string;
author: string;
revision?: number;
experName: string;
logDir?: string;
runConcurren: number;
maxDuration: number;
execDuration: number;
MaxTrialNum: number;
startTime: number;
endTime?: number;
trainingServicePlatform: string;
tuner: object;
assessor?: object;
advisor?: object;
clusterMetaData?: object;
logCollection?: string;
}
// trial accuracy
interface AccurPoint {
acc: number;
......@@ -74,21 +69,6 @@ interface TooltipForAccuracy {
data: Array<number | object>;
}
interface TrialNumber {
succTrial: number;
failTrial: number;
stopTrial: number;
waitTrial: number;
runTrial: number;
unknowTrial: number;
totalCurrentTrial: number;
}
interface TrialJob {
text: string;
value: string;
}
interface Dimobj {
dim: number;
name: string;
......@@ -108,10 +88,6 @@ interface ParaObj {
parallelAxis: Array<Dimobj>;
}
interface FinalResult {
data: string;
}
interface Intermedia {
name: string; // id
type: string;
......@@ -119,13 +95,93 @@ interface Intermedia {
hyperPara: object; // each trial hyperpara value
}
interface ExperimentInfo {
platform: string;
optimizeMode: string;
interface MetricDataRecord {
timestamp: number;
trialJobId: string;
parameterId: string;
type: string;
sequence: number;
data: string;
}
interface TrialJobInfo {
id: string;
sequenceId: number;
status: string;
startTime?: number;
endTime?: number;
hyperParameters?: string[];
logPath?: string;
finalMetricData?: MetricDataRecord[];
stderrPath?: string;
}
interface ExperimentParams {
authorName: string;
experimentName: string;
description?: string;
trialConcurrency: number;
maxExecDuration: number; // seconds
maxTrialNum: number;
searchSpace: string;
trainingServicePlatform: string;
multiPhase?: boolean;
multiThread?: boolean;
versionCheck?: boolean;
logCollection?: string;
tuner?: {
className: string;
builtinTunerName?: string;
codeDir?: string;
classArgs?: any;
classFileName?: string;
checkpointDir: string;
gpuNum?: number;
includeIntermediateResults?: boolean;
};
assessor?: {
className: string;
builtinAssessorName?: string;
codeDir?: string;
classArgs?: any;
classFileName?: string;
checkpointDir: string;
gpuNum?: number;
};
advisor?: {
className: string;
builtinAdvisorName?: string;
codeDir?: string;
classArgs?: any;
classFileName?: string;
checkpointDir: string;
gpuNum?: number;
};
clusterMetaData?: {
key: string;
value: string;
}[];
}
interface ExperimentProfile {
params: ExperimentParams;
id: string;
execDuration: number;
logDir?: string;
startTime?: number;
endTime?: number;
maxSequenceId: number;
revision: number;
}
interface NNIManagerStatus {
status: string;
errors: string[];
}
export {
TableObj, Parameters, Experiment, AccurPoint, TrialNumber, TrialJob,
DetailAccurPoint, TooltipForAccuracy, ParaObj, Dimobj, FinalResult, FinalType,
TooltipForIntermediate, SearchSpace, Intermedia, ExperimentInfo
TableObj, TableRecord, Parameters, ExperimentProfile, AccurPoint,
DetailAccurPoint, TooltipForAccuracy, ParaObj, Dimobj, FinalType,
TooltipForIntermediate, SearchSpace, Intermedia, MetricDataRecord, TrialJobInfo,
NNIManagerStatus,
};
import axios from 'axios';
import { MANAGER_IP } from '../const';
import { ExperimentProfile, NNIManagerStatus } from '../interface';
function compareProfiles(profile1?: ExperimentProfile, profile2?: ExperimentProfile): boolean {
if (!profile1 || !profile2) {
return false;
}
const copy1 = Object.assign({}, profile1, { execDuration: undefined });
const copy2 = Object.assign({}, profile2, { execDuration: undefined });
return JSON.stringify(copy1) === JSON.stringify(copy2);
}
class Experiment {
private profileField?: ExperimentProfile = undefined;
private statusField?: NNIManagerStatus = undefined;
public async init(): Promise<void> {
while (!this.profileField || !this.statusField) {
await this.update();
}
}
public async update(): Promise<boolean> {
const profilePromise = axios.get(`${MANAGER_IP}/experiment`);
const statusPromise = axios.get(`${MANAGER_IP}/check-status`);
const [ profileResponse, statusResponse ] = await Promise.all([ profilePromise, statusPromise ]);
let updated = false;
if (statusResponse.status === 200) {
updated = JSON.stringify(this.statusField) === JSON.stringify(statusResponse.data);
this.statusField = statusResponse.data;
}
if (profileResponse.status === 200) {
updated = updated || compareProfiles(this.profileField, profileResponse.data);
this.profileField = profileResponse.data;
}
return updated;
}
get profile(): ExperimentProfile {
if (!this.profileField) {
throw Error('Experiment profile not initialized');
}
return this.profileField!;
}
get trialConcurrency(): number {
return this.profile.params.trialConcurrency;
}
get optimizeMode(): string {
const tuner = this.profile.params.tuner;
return (tuner && tuner.classArgs && tuner.classArgs.optimize_mode) ? tuner.classArgs.optimize_mode : 'unknown';
}
get trainingServicePlatform(): string {
return this.profile.params.trainingServicePlatform;
}
get searchSpace(): object {
return JSON.parse(this.profile.params.searchSpace);
}
get logCollectionEnabled(): boolean {
return !!(this.profile.params.logCollection && this.profile.params.logCollection !== 'none');
}
get multiPhase(): boolean {
return !!(this.profile.params.multiPhase);
}
get status(): string {
if (!this.statusField) {
throw Error('Experiment status not initialized');
}
return this.statusField!.status;
}
get error(): string {
if (!this.statusField) {
throw Error('Experiment status not initialized');
}
return this.statusField!.errors[0] || '';
}
}
export { Experiment };
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment