Skip to content

Commit

Permalink
feat(frontend): Runtime DAG in RunDetailsV2. Fix #6673 (#6694)
Browse files Browse the repository at this point in the history
* feat(frontend): Runtime DAG in RunDetailsV2

* remove debug log
  • Loading branch information
zijianjoy committed Oct 7, 2021
1 parent ea205cf commit 74c7773
Show file tree
Hide file tree
Showing 15 changed files with 946 additions and 39 deletions.
6 changes: 6 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ development.
Run `npm run mock:api` to start a mock backend api server handler so it can
serve basic api calls with mock data.

If you want to port real MLMD store to be used for mock backend scenario, you can run the following command. Note that a mock MLMD store doesn't exist yet.

```
kubectl port-forward svc/metadata-envoy-service 9090:9090
```

### Proxy to a real cluster

This requires you already have a real KFP cluster, you can proxy requests to it.
Expand Down
93 changes: 91 additions & 2 deletions frontend/mock-backend/fixed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ import { ApiExperiment } from '../src/apis/experiment';
import { ApiJob } from '../src/apis/job';
import { ApiPipeline, ApiPipelineVersion } from '../src/apis/pipeline';
import { ApiRelationship, ApiResourceType, ApiRunDetail, RunMetricFormat } from '../src/apis/run';
import v2_lightweight_python_pipeline from './data/v2/pipeline/mock_lightweight_python_functions_v2_pipeline.json';
import xgboost_sample_pipeline from './data/v2/pipeline/xgboost_sample_pipeline.json';
import helloWorldRun from './hello-world-runtime';
import helloWorldWithStepsRun from './hello-world-with-steps-runtime';
import jsonRun from './json-runtime';
import largeGraph from './large-graph-runtime';
import coinflipRun from './mock-coinflip-runtime';
import errorRun from './mock-error-runtime';
import xgboostRun from './mock-xgboost-runtime';
import largeGraph from './large-graph-runtime';
import retryRun from './mock-retry-runtime';
import xgboostRun from './mock-xgboost-runtime';

function padStartTwoZeroes(str: string): string {
let padded = str || '';
Expand Down Expand Up @@ -359,6 +361,11 @@ const jobs: ApiJob[] = [
jobs.push(...generateNJobs());

const experiments: ApiExperiment[] = [
{
description: 'This experiment includes KFP v2 runs',
id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b',
name: 'KFP v2 Runs',
},
{
description: 'This experiment has no runs',
id: '7fc01714-4a13-4c05-5902-a8a72c14253b',
Expand Down Expand Up @@ -408,6 +415,88 @@ const versions: ApiPipelineVersion[] = [
];

const runs: ApiRunDetail[] = [
{
pipeline_runtime: {
// workflow_manifest: JSON.stringify(coinflipRun),
},
run: {
created_at: new Date('2021-05-17T20:58:23.000Z'),
description: 'V2 xgboost',
finished_at: new Date('2021-05-18T21:01:23.000Z'),
id: 'e0115ac1-0479-4194-a22d-01e65e09a32b',
name: 'v2-xgboost-ilbo',
pipeline_spec: {
pipeline_id: PIPELINE_V2_XGBOOST.id,
pipeline_name: PIPELINE_V2_XGBOOST_DEFAULT.name,
workflow_manifest: JSON.stringify(xgboost_sample_pipeline),
},
resource_references: [
{
key: {
id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b',
type: ApiResourceType.EXPERIMENT,
},
relationship: ApiRelationship.OWNER,
},
],
scheduled_at: new Date('2021-05-17T20:58:23.000Z'),
status: 'Succeeded',
},
},
{
pipeline_runtime: {
// workflow_manifest: JSON.stringify(coinflipRun),
},
run: {
created_at: new Date('2021-04-17T20:58:23.000Z'),
description: 'V2 two steps run from pipeline template',
finished_at: new Date('2021-04-18T21:01:23.000Z'),
id: 'c1e11ff7-e1af-4a8d-a9e4-718f32934ae0',
name: 'v2-lightweight-two-steps-i5jk',
pipeline_spec: {
pipeline_id: PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT.id,
pipeline_name: PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT.name,
workflow_manifest: JSON.stringify(v2_lightweight_python_pipeline),
},
resource_references: [
{
key: {
id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b',
type: ApiResourceType.EXPERIMENT,
},
relationship: ApiRelationship.OWNER,
},
],
scheduled_at: new Date('2021-04-17T20:58:23.000Z'),
status: 'Succeeded',
},
},
{
pipeline_runtime: {
// workflow_manifest: JSON.stringify(v2_lightweight_python_pipeline),
},
run: {
created_at: new Date('2021-03-17T20:58:23.000Z'),
description: 'V2 two steps run from SDK',
finished_at: new Date('2021-03-18T21:01:23.000Z'),
id: '3308d0ec-f1b3-4488-a2d3-8ad0f91e88f8',
name: 'v2-lightweight-two-steps-jk4u',
pipeline_spec: {
workflow_manifest: JSON.stringify(v2_lightweight_python_pipeline),
},
resource_references: [
{
key: {
id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b',
type: ApiResourceType.EXPERIMENT,
},
relationship: ApiRelationship.OWNER,
},
],
scheduled_at: new Date('2021-03-17T20:58:23.000Z'),
status: 'Succeeded',
},
},
{
pipeline_runtime: {
workflow_manifest: JSON.stringify(coinflipRun),
Expand Down
36 changes: 36 additions & 0 deletions frontend/mock-backend/mock-api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import express from 'express';
import proxy from 'http-proxy-middleware';
import mockApiMiddleware from './mock-api-middleware';

const app = express();
Expand All @@ -28,9 +29,44 @@ app.use((_: any, res: any, next: any) => {
next();
});

export const HACK_FIX_HPM_PARTIAL_RESPONSE_HEADERS = {
Connection: 'keep-alive',
};

// To enable porting MLMD to mock backend, run following command:
// kubectl port-forward svc/metadata-envoy-service 9090:9090
/** Proxy metadata requests to the Envoy instance which will handle routing to the metadata gRPC server */
app.all(
'/ml_metadata.*',
proxy({
changeOrigin: true,
onProxyReq: proxyReq => {
console.log('Metadata proxied request: ', (proxyReq as any).path);
},
headers: HACK_FIX_HPM_PARTIAL_RESPONSE_HEADERS,
target: getAddress({ host: 'localhost', port: '9090' }),
}),
);

mockApiMiddleware(app as any);

app.listen(port, () => {
// tslint:disable-next-line:no-console
console.log('Server listening at http://localhost:' + port);
});

export function getAddress({
host,
port,
namespace,
schema = 'http',
}: {
host: string;
port?: string | number;
namespace?: string;
schema?: string;
}) {
namespace = namespace ? `.${namespace}` : '';
port = port ? `:${port}` : '';
return `${schema}://${host}${namespace}${port}`;
}
42 changes: 21 additions & 21 deletions frontend/src/components/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,41 @@
* limitations under the License.
*/

import * as React from 'react';
import ArtifactList from '../pages/ArtifactList';
import ArtifactDetails from '../pages/ArtifactDetails';
import Banner, { BannerProps } from '../components/Banner';
import Button from '@material-ui/core/Button';
import Compare from '../pages/Compare';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import ExecutionList from '../pages/ExecutionList';
import ExecutionDetails from '../pages/ExecutionDetails';
import ExperimentDetails from '../pages/ExperimentDetails';
import Snackbar, { SnackbarProps } from '@material-ui/core/Snackbar';
import * as React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import FrontendFeatures from 'src/pages/FrontendFeatures';
import RunDetailsRouter from 'src/pages/RunDetailsRouter';
import { classes, stylesheet } from 'typestyle';
import Banner, { BannerProps } from '../components/Banner';
import { commonCss } from '../Css';
import { Deployments, KFP_FLAGS } from '../lib/Flags';
import Page404 from '../pages/404';
import AllExperimentsAndArchive, {
AllExperimentsAndArchiveTab,
} from '../pages/AllExperimentsAndArchive';
import AllRunsAndArchive, { AllRunsAndArchiveTab } from '../pages/AllRunsAndArchive';
import AllRecurringRunsList from '../pages/AllRecurringRunsList';
import AllRunsAndArchive, { AllRunsAndArchiveTab } from '../pages/AllRunsAndArchive';
import ArtifactDetails from '../pages/ArtifactDetails';
import ArtifactList from '../pages/ArtifactList';
import Compare from '../pages/Compare';
import ExecutionDetails from '../pages/ExecutionDetails';
import ExecutionList from '../pages/ExecutionList';
import ExperimentDetails from '../pages/ExperimentDetails';
import { GettingStarted } from '../pages/GettingStarted';
import NewExperiment from '../pages/NewExperiment';
import NewPipelineVersion from '../pages/NewPipelineVersion';
import NewRun from '../pages/NewRun';
import Page404 from '../pages/404';
import PipelineDetails from '../pages/PipelineDetails';
import PipelineList from '../pages/PipelineList';
import RecurringRunDetails from '../pages/RecurringRunDetails';
import RunDetails from '../pages/RunDetails';
import SideNav from './SideNav';
import Snackbar, { SnackbarProps } from '@material-ui/core/Snackbar';
import Toolbar, { ToolbarProps } from './Toolbar';
import { Route, Switch, Redirect } from 'react-router-dom';
import { classes, stylesheet } from 'typestyle';
import { commonCss } from '../Css';
import NewPipelineVersion from '../pages/NewPipelineVersion';
import { GettingStarted } from '../pages/GettingStarted';
import { KFP_FLAGS, Deployments } from '../lib/Flags';
import FrontendFeatures from 'src/pages/FrontendFeatures';

export type RouteConfig = {
path: string;
Expand Down Expand Up @@ -193,8 +193,8 @@ const Router: React.FC<RouterProps> = ({ configs }) => {
{ path: RoutePage.RUNS, Component: AllRunsAndArchive, view: AllRunsAndArchiveTab.RUNS },
{ path: RoutePage.RECURRING_RUNS, Component: AllRecurringRunsList },
{ path: RoutePage.RECURRING_RUN_DETAILS, Component: RecurringRunDetails },
{ path: RoutePage.RUN_DETAILS, Component: RunDetails },
{ path: RoutePage.RUN_DETAILS_WITH_EXECUTION, Component: RunDetails },
{ path: RoutePage.RUN_DETAILS, Component: RunDetailsRouter },
{ path: RoutePage.RUN_DETAILS_WITH_EXECUTION, Component: RunDetailsRouter },
{ path: RoutePage.COMPARE, Component: Compare },
{ path: RoutePage.FRONTEND_FEATURES, Component: FrontendFeatures },
];
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/lib/v2/DynamicFlow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2021 The Kubeflow Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as TWO_STEP_PIPELINE from 'src/data/test/mock_lightweight_python_functions_v2_pipeline.json';
import { PipelineSpec } from 'src/generated/pipeline_spec';
import { ml_pipelines } from 'src/generated/pipeline_spec/pbjs_ml_pipelines';
import { Artifact, Event, Execution, Value } from 'src/third_party/mlmd';
import { TASK_NAME_KEY, updateFlowElementsState } from './DynamicFlow';
import { convertFlowElements } from './StaticFlow';

describe('DynamicFlow', () => {
it('update node status based on MLMD', () => {
// Prepare MLMD objects.
const EXECUTION_PREPROCESS = new Execution()
.setId(3)
.setLastKnownState(Execution.State.COMPLETE);
EXECUTION_PREPROCESS.getCustomPropertiesMap().set(
TASK_NAME_KEY,
new Value().setStringValue('preprocess'),
);
const EXECUTION_TRAIN = new Execution().setId(4).setLastKnownState(Execution.State.FAILED);
EXECUTION_TRAIN.getCustomPropertiesMap().set(
TASK_NAME_KEY,
new Value().setStringValue('train'),
);

const ARTIFACT_OUTPUT_DATA_ONE = new Artifact().setId(1).setState(Artifact.State.LIVE);
const ARTIFACT_OUTPUT_DATA_TWO = new Artifact().setId(2).setState(Artifact.State.PENDING);
const ARTIFACT_MODEL = new Artifact().setId(3).setState(Artifact.State.DELETED);

const EVENT_PREPROCESS_OUTPUT_DATA_ONE = new Event()
.setExecutionId(3)
.setArtifactId(1)
.setType(Event.Type.OUTPUT)
.setPath(new Event.Path().setStepsList([new Event.Path.Step().setKey('output_dataset_one')]));
const EVENT_PREPROCESS_OUTPUT_DATA_TWO = new Event()
.setExecutionId(3)
.setArtifactId(2)
.setType(Event.Type.OUTPUT)
.setPath(
new Event.Path().setStepsList([new Event.Path.Step().setKey('output_dataset_two_path')]),
);
const EVENT_OUTPUT_DATA_ONE_TRAIN = new Event().setExecutionId(4).setArtifactId(1);
const EVENT_OUTPUT_DATA_TWO_TRAIN = new Event().setExecutionId(4).setArtifactId(2);
const EVENT_TRAIN_MODEL = new Event()
.setExecutionId(4)
.setArtifactId(3)
.setType(Event.Type.OUTPUT)
.setPath(new Event.Path().setStepsList([new Event.Path.Step().setKey('model')]));

// Converts to static graph first, its type is Elements<any>.
const jsonObject = TWO_STEP_PIPELINE;
const message = ml_pipelines.PipelineSpec.fromObject(jsonObject['pipelineSpec']);
const buffer = ml_pipelines.PipelineSpec.encode(message).finish();
const pipelineSpec = PipelineSpec.deserializeBinary(buffer);
const graph = convertFlowElements(pipelineSpec);

// MLMD objects to provide node states.
const executions: Execution[] = [EXECUTION_PREPROCESS, EXECUTION_TRAIN];
const events: Event[] = [
EVENT_PREPROCESS_OUTPUT_DATA_ONE,
EVENT_PREPROCESS_OUTPUT_DATA_TWO,
EVENT_OUTPUT_DATA_ONE_TRAIN,
EVENT_OUTPUT_DATA_TWO_TRAIN,
EVENT_TRAIN_MODEL,
];
const artifacts: Artifact[] = [
ARTIFACT_OUTPUT_DATA_ONE,
ARTIFACT_OUTPUT_DATA_TWO,
ARTIFACT_MODEL,
];

updateFlowElementsState(graph, executions, events, artifacts);
for (let element of graph) {
graph
.filter(e => e.id === element.id)
.forEach(e => {
if (e.id === 'task.preprocess') {
expect(e.data.state).toEqual(EXECUTION_PREPROCESS.getLastKnownState());
} else if (e.id === 'task.train') {
expect(e.data.state).toEqual(EXECUTION_TRAIN.getLastKnownState());
} else if (e.id === 'artifact.preprocess.output_dataset_one') {
expect(e.data.state).toEqual(ARTIFACT_OUTPUT_DATA_ONE.getState());
} else if (e.id === 'artifact.preprocess.output_dataset_two_path') {
expect(e.data.state).toEqual(ARTIFACT_OUTPUT_DATA_TWO.getState());
} else if (e.id === 'artifact.train.model') {
expect(e.data.state).toEqual(ARTIFACT_MODEL.getState());
}
});
}
});
});

0 comments on commit 74c7773

Please sign in to comment.