Commit b40e3db7 authored by quzha's avatar quzha
Browse files

Merge branch 'master' of github.com:Microsoft/nni into dev-retiarii

parents efa4e31c 95f731e4
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import { ExperimentManager } from '../../common/experimentManager';
import { Provider } from 'typescript-ioc';
export const testExperimentManagerProvider: Provider = {
get: (): ExperimentManager => { return new mockedeExperimentManager(); }
};
export class mockedeExperimentManager extends ExperimentManager {
public getExperimentsInfo(): Promise<JSON> {
const expInfo = JSON.parse(JSON.stringify({
"test": {
"port": 8080,
"startTime": 1605246730756,
"endTime": "N/A",
"status": "RUNNING",
"platform": "local",
"experimentName": "testExp",
"tag": [], "pid": 11111,
"webuiUrl": [],
"logDir": null
}
}));
return new Promise<JSON>((resolve, reject) => {
resolve(expInfo);
});
}
public setExperimentPath(newPath: string): void {
return
}
public setExperimentInfo(experimentId: string, key: string, value: any): void {
return
}
public stop(): Promise<void> {
return new Promise<void>(()=>{});
}
}
......@@ -101,7 +101,7 @@ export class MockedNNIManager extends Manager {
public getTrialJob(trialJobId: string): Promise<TrialJobInfo> {
const deferred: Deferred<TrialJobInfo> = new Deferred<TrialJobInfo>();
const jobInfo: TrialJobInfo = {
id: '1234',
trialJobId: '1234',
status: 'SUCCEEDED',
startTime: Date.now(),
endTime: Date.now()
......@@ -110,6 +110,11 @@ export class MockedNNIManager extends Manager {
return deferred.promise;
}
public getTrialJobMessage(trialJobId: string): string | undefined {
return "TEST-MESSAGE"
}
public stopExperiment(): Promise<void> {
throw new MethodNotImplementedError();
}
......@@ -152,7 +157,7 @@ export class MockedNNIManager extends Manager {
}
public listTrialJobs(status?: TrialJobStatus): Promise<TrialJobInfo[]> {
const job1: TrialJobInfo = {
id: '1234',
trialJobId: '1234',
status: 'SUCCEEDED',
startTime: Date.now(),
endTime: Date.now(),
......@@ -166,7 +171,7 @@ export class MockedNNIManager extends Manager {
}]
};
const job2: TrialJobInfo = {
id: '3456',
trialJobId: '3456',
status: 'FAILED',
startTime: Date.now(),
endTime: Date.now(),
......
......@@ -10,12 +10,14 @@ import { Container } from 'typescript-ioc';
import * as component from '../../common/component';
import { DataStore } from '../../common/datastore';
import { ExperimentProfile, Manager } from '../../common/manager';
import { ExperimentManager } from '../../common/experimentManager'
import { TrainingService } from '../../common/trainingService';
import { cleanupUnitTest, prepareUnitTest } from '../../common/utils';
import { MockedDataStore } from '../../core/test/mockedDatastore';
import { MockedTrainingService } from '../../core/test/mockedTrainingService';
import { NNIRestServer } from '../nniRestServer';
import { testManagerProvider } from './mockedNNIManager';
import { testExperimentManagerProvider } from './mockedExperimentManager';
describe('Unit test for rest server', () => {
......@@ -26,6 +28,7 @@ describe('Unit test for rest server', () => {
Container.bind(Manager).provider(testManagerProvider);
Container.bind(DataStore).to(MockedDataStore);
Container.bind(TrainingService).to(MockedTrainingService);
Container.bind(ExperimentManager).provider(testExperimentManagerProvider)
const restServer: NNIRestServer = component.get(NNIRestServer);
restServer.start().then(() => {
ROOT_URL = `${restServer.endPoint}/api/v1/nni`;
......@@ -57,7 +60,7 @@ describe('Unit test for rest server', () => {
assert.fail(err.message);
} else {
expect(res.statusCode).to.equal(200);
expect(JSON.parse(body).id).to.equal('1234');
expect(JSON.parse(body).trialJobId).to.equal('1234');
}
done();
});
......@@ -84,6 +87,16 @@ describe('Unit test for rest server', () => {
});
});
it('Test GET experiments-info', (done: Mocha.Done) => {
request.get(`${ROOT_URL}/experiments-info`, (err: Error, res: request.Response) => {
expect(res.statusCode).to.equal(200);
if (err) {
assert.fail(err.message);
}
done();
});
});
it('Test change concurrent-trial-jobs', (done: Mocha.Done) => {
request.get(`${ROOT_URL}/experiment`, (err: Error, res: request.Response, body: any) => {
if (err) {
......
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import * as fs from 'fs';
import { GeneralK8sClient, KubernetesCRDClient } from '../kubernetesApiClient';
/**
* Adl ClientV1
*/
class AdlClientV1 extends KubernetesCRDClient {
/**
* constructor, to initialize adl CRD definition
*/
public constructor() {
super();
this.crdSchema = JSON.parse(fs.readFileSync('./config/adl/adaptdl-crd-v1.json', 'utf8'));
this.client.addCustomResourceDefinition(this.crdSchema);
}
protected get operator(): any {
return this.client.apis['adaptdl.petuum.com'].v1.namespaces('default').adaptdljobs;
}
public get containerName(): string {
return 'main';
}
public async getKubernetesPods(jobName: string): Promise<any> {
let result: Promise<any>;
const response = await this.client.api.v1.namespaces('default').pods
.get({ qs: { labelSelector: `adaptdl/job=${jobName}` } });
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(response.body);
} else {
result = Promise.reject(`AdlClient getKubernetesPods failed, statusCode is ${response.statusCode}`);
}
return result;
}
}
/**
* Adl Client
*/
class AdlClientFactory {
/**
* Factory method to generate operator client
*/
public static createClient(): KubernetesCRDClient {
return new AdlClientV1();
}
}
export { AdlClientFactory, GeneralK8sClient };
export { AdlClientV1 }
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import {KubernetesTrialConfig} from "../kubernetesConfig";
/**
* Checkpoint Config
*/
export class CheckpointConfig {
public readonly storageClass: string;
public readonly storageSize: string;
constructor(storageClass: string, storageSize: string) {
this.storageClass = storageClass;
this.storageSize = storageSize;
}
}
/**
* imagePullSecret Config
*/
export class ImagePullSecretConfig{
public readonly name: string;
constructor(name: string) {
this.name = name
}
}
/**
* NFS Config
*/
export class NFSConfig {
public readonly server: string;
public readonly path: string;
public readonly containerMountPath: string;
constructor(server: string, path: string, containerMountPath: string) {
this.server = server;
this.path = path;
this.containerMountPath = containerMountPath;
}
}
/**
* Trial job configuration for Adl
*/
export class AdlTrialConfig extends KubernetesTrialConfig {
public readonly command: string;
public readonly gpuNum: number;
public readonly image: string;
public readonly imagePullSecrets?: ImagePullSecretConfig[];
public readonly nfs?: NFSConfig;
public readonly checkpoint?: CheckpointConfig;
public readonly cpuNum?: number;
public readonly memorySize?: string;
public readonly adaptive?: boolean; // adaptive == preemptible
constructor(codeDir: string,
command: string, gpuNum: number,
image: string, imagePullSecrets?: ImagePullSecretConfig[],
nfs?: NFSConfig, checkpoint?: CheckpointConfig,
cpuNum?: number, memorySize?: string,
adaptive?: boolean
) {
super(codeDir);
this.command = command;
this.gpuNum = gpuNum;
this.image = image;
this.imagePullSecrets = imagePullSecrets;
this.nfs = nfs;
this.checkpoint = checkpoint;
this.cpuNum = cpuNum;
this.memorySize = memorySize;
this.adaptive = adaptive;
}
}
export type AdlJobStatus = "Pending" | "Running" | "Starting" | "Stopping" | "Failed" | "Succeeded";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import { AdlClientV1 } from './adlApiClient';
import { KubernetesTrialJobDetail} from '../kubernetesData';
import { KubernetesJobInfoCollector } from '../kubernetesJobInfoCollector';
import { AdlJobStatus } from './adlConfig';
/**
* Collector Adl jobs info from Kubernetes cluster, and update adl job status locally
*/
export class AdlJobInfoCollector extends KubernetesJobInfoCollector {
constructor(jobMap: Map<string, KubernetesTrialJobDetail>) {
super(jobMap);
}
protected async retrieveSingleTrialJobInfo(kubernetesCRDClient: AdlClientV1 | undefined,
kubernetesTrialJob: KubernetesTrialJobDetail): Promise<void> {
if (!this.statusesNeedToCheck.includes(kubernetesTrialJob.status)) {
return Promise.resolve();
}
if (kubernetesCRDClient === undefined) {
return Promise.reject('kubernetesCRDClient is undefined');
}
let kubernetesJobInfo: any;
let kubernetesPodsInfo: any;
try {
kubernetesJobInfo = await kubernetesCRDClient.getKubernetesJob(kubernetesTrialJob.kubernetesJobName);
kubernetesPodsInfo = await kubernetesCRDClient.getKubernetesPods(kubernetesTrialJob.kubernetesJobName);
} catch (error) {
// Notice: it maynot be a 'real' error since cancel trial job can also cause getKubernetesJob failed.
this.log.error(`Get job ${kubernetesTrialJob.kubernetesJobName} info failed, error is ${error}`);
//This is not treat as a error status
return Promise.resolve();
}
/* eslint-disable require-atomic-updates */
if (kubernetesJobInfo.status) {
const phase: AdlJobStatus = <AdlJobStatus>kubernetesJobInfo.status.phase
switch (phase) {
case 'Pending':
case 'Starting':
kubernetesTrialJob.status = 'WAITING';
if (kubernetesPodsInfo.items.length > 0){
if (kubernetesPodsInfo.items[0].status.containerStatuses != undefined) {
const currState: any = kubernetesPodsInfo.items[0].status.containerStatuses[0].state
if (currState.waiting != undefined) {
const msg: string = currState.waiting.reason
if (msg == "ImagePullBackOff" || msg == "ErrImagePull") {
kubernetesTrialJob.status = 'FAILED';
}
}
}
kubernetesTrialJob.message = kubernetesPodsInfo.items
.map((pod: any) => JSON.stringify(pod.status.containerStatuses))
.join('\n');
}
kubernetesTrialJob.startTime = Date.parse(<string>kubernetesJobInfo.metadata.creationTimestamp);
break;
case 'Running':
case 'Stopping':
kubernetesTrialJob.status = 'RUNNING';
kubernetesTrialJob.message = `Use 'nnictl log trial --trial_id ${kubernetesTrialJob.id}' to check the log stream.`;
if (kubernetesTrialJob.startTime === undefined) {
kubernetesTrialJob.startTime = Date.parse(<string>kubernetesJobInfo.metadata.creationTimestamp);
}
break;
case 'Failed':
kubernetesTrialJob.status = 'FAILED';
kubernetesTrialJob.message = kubernetesJobInfo.status.message;
if (kubernetesPodsInfo.items.length > 0) {
kubernetesTrialJob.message += " ; ";
kubernetesTrialJob.message += `Use 'nnictl log trial --trial_id ${kubernetesTrialJob.id}' for the path of the collected logs.`;
}
// undefined => NaN as endTime here
kubernetesTrialJob.endTime = Date.parse(<string>kubernetesJobInfo.status.completionTimestamp);
break;
case 'Succeeded':
kubernetesTrialJob.status = 'SUCCEEDED';
kubernetesTrialJob.endTime = Date.parse(<string>kubernetesJobInfo.status.completionTimestamp);
kubernetesTrialJob.message = `Succeeded at ${kubernetesJobInfo.status.completionTimestamp}`
break;
default:
}
}
/* eslint-enable require-atomic-updates */
return Promise.resolve();
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import * as component from '../../../common/component';
import { KubernetesJobRestServer } from '../kubernetesJobRestServer';
import { AdlTrainingService } from './adlTrainingService';
/**
* Adl Training service Rest server, provides rest API to support adl job metrics update
*
*/
@component.Singleton
export class AdlJobRestServer extends KubernetesJobRestServer {
/**
* constructor to provide NNIRestServer's own rest property, e.g. port
*/
constructor() {
super(component.get(AdlTrainingService));
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import * as fs from 'fs';
import * as component from '../../../common/component';
import { String } from 'typescript-string-operations';
import { getExperimentId } from '../../../common/experimentStartupInfo';
import {
NNIManagerIpConfig, TrialJobApplicationForm, TrialJobDetail, TrialJobStatus
} from '../../../common/trainingService';
import { delay, generateParamFileName, getVersion, uniqueString } from '../../../common/utils';
import { TrialConfigMetadataKey } from '../../common/trialConfigMetadataKey';
import { KubernetesTrialJobDetail } from '../kubernetesData';
import { KubernetesTrainingService } from '../kubernetesTrainingService';
import { AdlClientFactory } from './adlApiClient'
import { AdlJobInfoCollector } from './adlJobInfoCollector';
import { AdlJobRestServer } from './adlJobRestServer';
import { AdlTrialConfig } from './adlConfig'
/**
* Training Service implementation for Adl
*/
@component.Singleton
class AdlTrainingService extends KubernetesTrainingService implements KubernetesTrainingService {
private adlTrialConfig?: AdlTrialConfig;
private readonly adlJobInfoCollector: AdlJobInfoCollector;
private configmapTemplateStr: string;
private jobTemplateStr: string;
private pvcTemplateStr: string;
private tensorboardPvcTemplate: any;
private tensorboardDeploymentTemplate: any;
//TODO: change the logic here when we want to support multiple tensorboard
private tensorboardName: string = "adaptdl-tensorboard-" + getExperimentId().toLowerCase();
constructor() {
super();
this.adlJobInfoCollector = new AdlJobInfoCollector(this.trialJobsMap);
this.experimentId = getExperimentId();
this.kubernetesCRDClient = AdlClientFactory.createClient();
this.configmapTemplateStr = fs.readFileSync(
'./config/adl/adaptdl-nni-configmap-template.json', 'utf8');
this.jobTemplateStr = fs.readFileSync('./config/adl/adaptdljob-template.json', 'utf8');
this.pvcTemplateStr = fs.readFileSync('./config/adl/adaptdl-pvc-template.json', 'utf8');
this.tensorboardPvcTemplate = JSON.parse(
fs.readFileSync('./config/adl/adaptdl-tensorboard-pvc-template.json', 'utf8'));
this.tensorboardDeploymentTemplate = JSON.parse(
fs.readFileSync('./config/adl/adaptdl-tensorboard-deployment-template.json', 'utf8'));
this.log.info('Construct Adl training service.');
}
public async run(): Promise<void> {
this.log.info(this.tensorboardName);
this.log.info('Start tensorboard deployment.');
await this.launchTensorboard()
this.log.info('Run Adl training service.');
this.kubernetesJobRestServer = component.get(AdlJobRestServer);
if (this.kubernetesJobRestServer === undefined) {
throw new Error('kubernetesJobRestServer not initialized!');
}
await this.kubernetesJobRestServer.start();
this.kubernetesJobRestServer.setEnableVersionCheck = this.versionCheck;
this.log.info(`Adl Training service rest server listening on: ${this.kubernetesJobRestServer.endPoint}`);
while (!this.stopping) {
// collect metrics for Adl jobs by interacting with Kubernetes API server
await delay(3000);
await this.adlJobInfoCollector.retrieveTrialStatus(this.kubernetesCRDClient);
if (this.kubernetesJobRestServer.getErrorMessage !== undefined) {
throw new Error(this.kubernetesJobRestServer.getErrorMessage);
}
}
this.log.info('Adl training service exit.');
}
private async launchTensorboard(): Promise<void> {
// Start the tensorboard at the beginning of the experiment.
if (this.adlTrialConfig === undefined) {
throw new Error('Adl trial config is undefined');
}
// Create tensorboard deployment
this.tensorboardDeploymentTemplate.metadata.name = this.tensorboardName
this.tensorboardDeploymentTemplate.metadata.labels.expId = this.experimentId
this.tensorboardDeploymentTemplate.spec.selector.matchLabels.app = this.tensorboardName
this.tensorboardDeploymentTemplate.spec.template.metadata.labels.app = this.tensorboardName
this.tensorboardDeploymentTemplate.spec.template.spec.volumes[0]
.persistentVolumeClaim.claimName = this.tensorboardName
const deploymentUid: string = await this.genericK8sClient.createDeployment(this.tensorboardDeploymentTemplate);
// Create pvc
this.tensorboardPvcTemplate.metadata.name = this.tensorboardName;
this.tensorboardPvcTemplate.metadata.ownerReferences[0].name = this.tensorboardName;
this.tensorboardPvcTemplate.metadata.ownerReferences[0].uid = deploymentUid
if (this.adlTrialConfig.checkpoint != undefined) {
this.tensorboardPvcTemplate.spec.resources.requests.storage = this.adlTrialConfig.checkpoint.storageSize;
this.tensorboardPvcTemplate.spec.storageClassName = this.adlTrialConfig.checkpoint.storageClass;
}
else {
this.tensorboardPvcTemplate.spec.resources.requests.storage = "1Gi"
this.tensorboardPvcTemplate.spec.storageClassName = await this.genericK8sClient.getStorageClass();
}
await this.genericK8sClient.createPersistentVolumeClaim(this.tensorboardPvcTemplate);
return Promise.resolve()
}
public async submitTrialJob(form: TrialJobApplicationForm): Promise<TrialJobDetail> {
if (this.kubernetesCRDClient === undefined) {
throw new Error('Adl job operator client is undefined');
}
if (this.adlTrialConfig === undefined) {
throw new Error('Adl trial config is undefined');
}
if (this.kubernetesRestServerPort === undefined) {
const restServer: AdlJobRestServer = component.get(AdlJobRestServer);
this.kubernetesRestServerPort = restServer.clusterRestServerPort;
}
const trialJobId: string = uniqueString(5);
const adlJobName: string = `nni-exp-${this.experimentId}-trial-${trialJobId}`.toLowerCase();
const initStatus: TrialJobStatus = 'WAITING';
const codeDir = this.adlTrialConfig.codeDir;
const outputDir = "output"
const trialJobDetail: KubernetesTrialJobDetail = new KubernetesTrialJobDetail(
trialJobId,
initStatus,
Date.now(),
codeDir,
form,
adlJobName,
outputDir
);
// Create adljob
const job: any = JSON.parse(this.jobTemplateStr);
job.metadata.name = adlJobName
job.metadata.labels.app = this.NNI_KUBERNETES_TRIAL_LABEL
job.metadata.labels.expId = this.experimentId
job.metadata.labels.trialId = trialJobId
if (this.adlTrialConfig.adaptive !== undefined){
job.spec.preemptible = this.adlTrialConfig.adaptive
}
job.spec.template.spec.containers[0]
.image = this.adlTrialConfig.image;
job.spec.template.spec.volumes[0]
.persistentVolumeClaim.claimName = adlJobName
job.spec.template.spec.volumes[1]
.persistentVolumeClaim.claimName = this.tensorboardName
job.spec.template.spec.volumes[2]
.configMap.name = adlJobName
// Handle Pod Resource
let cpu: number = 1;
let memory: string = "1Gi";
if (this.adlTrialConfig.cpuNum !== undefined) {
cpu = this.adlTrialConfig.cpuNum;
}
if (this.adlTrialConfig.memorySize !== undefined) {
memory = this.adlTrialConfig.memorySize;
}
job.spec.template.spec.containers[0]
.resources.requests.memory = memory;
job.spec.template.spec.containers[0]
.resources.requests.cpu = cpu;
job.spec.template.spec.containers[0]
.resources.limits["nvidia.com/gpu"] = this.adlTrialConfig.gpuNum;
// Handle imagePullSecrets
if (this.adlTrialConfig.imagePullSecrets !== undefined) {
job.spec.template.spec.imagePullSecrets = job.spec.template.spec
.imagePullSecrets.concat(this.adlTrialConfig.imagePullSecrets);
}
// Handle NFS
if (this.adlTrialConfig.nfs !== undefined) {
job.spec.template.spec.volumes.push({
"name": "nfs",
"nfs": {
"server": this.adlTrialConfig.nfs.server,
"path": this.adlTrialConfig.nfs.path,
"readOnly": false
}
});
job.spec.template.spec.containers[0].volumeMounts.push({
"name": "nfs",
"mountPath": this.adlTrialConfig.nfs.containerMountPath
});
}
await this.kubernetesCRDClient.createKubernetesJob(job);
const k8sadlJob: any = await this.kubernetesCRDClient.getKubernetesJob(adlJobName);
// Create pvc
const pvc: any = JSON.parse(this.pvcTemplateStr);
pvc.metadata.name = adlJobName;
pvc.metadata.ownerReferences[0].name = adlJobName;
pvc.metadata.ownerReferences[0].uid = k8sadlJob.metadata.uid;
if (this.adlTrialConfig.checkpoint != undefined) {
pvc.spec.resources.requests.storage = this.adlTrialConfig
.checkpoint.storageSize;
pvc.spec.storageClassName = this.adlTrialConfig.checkpoint.storageClass;
}
else {
pvc.spec.resources.requests.storage = "1Gi"
pvc.spec.storageClassName = await this.genericK8sClient.getStorageClass();
}
await this.genericK8sClient.createPersistentVolumeClaim(pvc);
// prepare the runscript and convert it to configmap and mount it
const configmap: any = JSON.parse(this.configmapTemplateStr);
configmap.metadata.name = adlJobName;
configmap.metadata.ownerReferences[0].name = adlJobName;
configmap.metadata.ownerReferences[0].uid = k8sadlJob.metadata.uid;
configmap.data["run.sh"] = await this.prepareRunScript(
trialJobId, form, codeDir, outputDir)
const cleanupScriptTemplate: string =
`#!/bin/bash
ps aux | grep "python3 -m nni_trial_tool.trial_keeper" | awk '{print $2}' | xargs kill -2
while true;
do
proc=\`ps aux | grep "python3 -m nni_trial_tool.trial_keeper" | awk '{print $2}' | grep "" -c\`
if (( $proc == 1 )); then
exit 0
else
echo "waiting"
fi
sleep 1
done
`;
configmap.data["cleanup.sh"] = cleanupScriptTemplate
await this.genericK8sClient.createConfigMap(configmap)
// Set trial job detail until create Adl job successfully
this.trialJobsMap.set(trialJobId, trialJobDetail);
return Promise.resolve(trialJobDetail);
}
private async prepareRunScript(jobId: string,
form: TrialJobApplicationForm,
codeDir: string,
outputDir: string): Promise<string> {
if (this.adlTrialConfig === undefined) {
throw new Error('Adl trial config is undefined');
}
if (this.kubernetesRestServerPort === undefined) {
throw new Error('Adl rest server port is undefined');
}
if (this.nniManagerIpConfig === undefined) {
throw new Error('Adl nniManager ip config is undefined');
}
const expId: string = this.experimentId;
const seqId: string = form.sequenceId.toString();
const command: string = this.adlTrialConfig.command;
const hyperParameters: string = form.hyperParameters.value;
const hyperParametersFile: string = generateParamFileName(form.hyperParameters);
const nniManagerPort: string = this.kubernetesRestServerPort.toString();
const nniManagerIp: string = this.nniManagerIpConfig.nniManagerIp;
let nniManagerVersion: string = '';
if (this.versionCheck) {
nniManagerVersion = await getVersion();
}
let nvidiaScript: string = '';
if (this.adlTrialConfig.gpuNum == 0) {
nvidiaScript = 'export CUDA_VISIBLE_DEVICES=';
}
const runScriptTemplate: string =
`#!/bin/bash
export NNI_PLATFORM=adl
export MULTI_PHASE=false
export NNI_SYS_DIR={0}
export NNI_CODE_DIR={0}
export NNI_OUTPUT_DIR={1}
export NNI_TRIAL_JOB_ID={2}
export NNI_EXP_ID={3}
export NNI_TRIAL_SEQ_ID={4}
mkdir -p $NNI_OUTPUT_DIR
{5}
echo '{6}' > $NNI_CODE_DIR/{7}
python3 -m nni_trial_tool.trial_keeper --trial_command '{8}' \
--nnimanager_ip {9} --nnimanager_port {10} \
--nni_manager_version '{11}' --log_collection '{12}'
`;
const runScript = String.Format(
runScriptTemplate, codeDir, outputDir,
jobId, expId, seqId, nvidiaScript,
hyperParameters, hyperParametersFile, command,
nniManagerIp, nniManagerPort, nniManagerVersion,
this.logCollection);
return Promise.resolve(runScript);
}
public async setClusterMetadata(key: string, value: string): Promise<void> {
this.log.info('SetCluster ' + key + ', ' +value);
switch (key) {
case TrialConfigMetadataKey.NNI_MANAGER_IP:
this.nniManagerIpConfig = <NNIManagerIpConfig>JSON.parse(value);
break;
case TrialConfigMetadataKey.TRIAL_CONFIG:
this.adlTrialConfig = <AdlTrialConfig>JSON.parse(value);
break;
case TrialConfigMetadataKey.VERSION_CHECK:
this.versionCheck = (value === 'true' || value === 'True');
break;
case TrialConfigMetadataKey.LOG_COLLECTION:
this.logCollection = value;
break;
default:
}
return Promise.resolve();
}
public getClusterMetadata(key: string): Promise<string> {
let result: string;
switch (key) {
case TrialConfigMetadataKey.TRIAL_CONFIG:
if (this.adlTrialConfig === undefined) {
return Promise.reject(`${key} is not set yet`);
}
result = JSON.stringify(this.adlTrialConfig);
break;
case TrialConfigMetadataKey.NNI_MANAGER_IP:
if (this.nniManagerIpConfig === undefined) {
return Promise.reject(`${key} is not set yet`);
}
result = JSON.stringify(this.nniManagerIpConfig);
break;
default:
return Promise.reject(`${key} not set`);
}
return Promise.resolve(result);
}
}
export { AdlTrainingService };
......@@ -19,6 +19,94 @@ class GeneralK8sClient {
this.client.loadSpec();
}
private matchStorageClass(response: any): string {
const adlSupportedProvisioners: RegExp[] = [
new RegExp("microk8s.io/hostpath"),
new RegExp(".*cephfs.csi.ceph.com"),
new RegExp(".*azure.*"),
new RegExp("\\b" + "efs" + "\\b")
]
const templateLen = adlSupportedProvisioners.length,
responseLen = response.items.length
let i = 0,
j = 0;
for (; i < responseLen; i++) {
const provisioner: string = response.items[i].provisioner
for (; j < templateLen; j++) {
if (provisioner.match(adlSupportedProvisioners[j])) {
return response.items[i].metadata.name;
}
}
}
return "Not Found!";
}
public async getStorageClass(): Promise<string> {
let result: Promise<string>;
const response: any = await this.client.apis["storage.k8s.io"].v1beta1.storageclasses.get()
const storageClassType: string = this.matchStorageClass(response.body)
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
if (storageClassType != "Not Found!") {
result = Promise.resolve(storageClassType);
}
else {
result = Promise.reject("No StorageClasses are supported!")
}
} else {
result = Promise.reject(`List storageclasses failed, statusCode is ${response.statusCode}`);
}
return result;
}
public async createDeployment(deploymentManifest: any): Promise<string> {
let result: Promise<string>;
const response: any = await this.client.apis.apps.v1.namespaces('default').deployments.post({ body: deploymentManifest })
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(response.body.metadata.uid);
} else {
result = Promise.reject(`Create deployment failed, statusCode is ${response.statusCode}`);
}
return result;
}
public async deleteDeployment(deploymentName: string): Promise<boolean> {
let result: Promise<boolean>;
// TODO: change this hard coded deployment name after demo
const response: any = await this.client.apis.apps.v1.namespaces('default')
.deployment(deploymentName).delete();
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
} else {
result = Promise.reject(`Delete deployment failed, statusCode is ${response.statusCode}`);
}
return result;
}
public async createConfigMap(configMapManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default')
.configmaps.post({body: configMapManifest});
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
} else {
result = Promise.reject(`Create configMap failed, statusCode is ${response.statusCode}`);
}
return result;
}
public async createPersistentVolumeClaim(pvcManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default')
.persistentvolumeclaims.post({body: pvcManifest});
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
} else {
result = Promise.reject(`Create pvc failed, statusCode is ${response.statusCode}`);
}
return result;
}
public async createSecret(secretManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default').secrets
......@@ -77,7 +165,7 @@ abstract class KubernetesCRDClient {
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
} else {
result = Promise.reject(`Create kubernetes job failed, statusCode is ${response.statusCode}`);
result = Promise.reject(`KubernetesApiClient createKubernetesJob failed, statusCode is ${response.statusCode}`);
}
return result;
......@@ -91,7 +179,7 @@ abstract class KubernetesCRDClient {
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(response.body);
} else {
result = Promise.reject(`KubeflowOperatorClient get tfjobs failed, statusCode is ${response.statusCode}`);
result = Promise.reject(`KubernetesApiClient getKubernetesJob failed, statusCode is ${response.statusCode}`);
}
return result;
......@@ -115,7 +203,7 @@ abstract class KubernetesCRDClient {
result = Promise.resolve(true);
} else {
result = Promise.reject(
`KubeflowOperatorClient, delete labels ${matchQuery} get wrong statusCode ${deleteResult.statusCode}`);
`KubernetesApiClient, delete labels ${matchQuery} get wrong statusCode ${deleteResult.statusCode}`);
}
} catch (err) {
result = Promise.reject(err);
......
......@@ -11,6 +11,7 @@ import { TrialJobApplicationForm, TrialJobDetail, TrialJobStatus } from '../../
export class KubernetesTrialJobDetail implements TrialJobDetail {
public id: string;
public status: TrialJobStatus;
public message?: string;
public submitTime: number;
public startTime?: number;
public endTime?: number;
......@@ -26,6 +27,7 @@ export class KubernetesTrialJobDetail implements TrialJobDetail {
kubernetesJobName: string, url: string) {
this.id = id;
this.status = status;
this.message = 'Pending for creating the trial job.';
this.submitTime = submitTime;
this.workingDirectory = workingDirectory;
this.form = form;
......
......@@ -23,21 +23,16 @@ export class KubernetesJobInfoCollector {
this.statusesNeedToCheck = ['RUNNING', 'WAITING'];
}
public async retrieveTrialStatus(kubernetesCRDClient: KubernetesCRDClient | undefined): Promise<void> {
public async retrieveTrialStatus(kubernetesCRDClient: KubernetesCRDClient | undefined): Promise<void[]> {
assert(kubernetesCRDClient !== undefined);
const updateKubernetesTrialJobs: Promise<void>[] = [];
for (const [trialJobId, kubernetesTrialJob] of this.trialJobsMap) {
if (kubernetesTrialJob === undefined) {
throw new NNIError(NNIErrorNames.NOT_FOUND, `trial job id ${trialJobId} not found`);
}
// Since Kubeflow needs some delay to schedule jobs, we provide 20 seconds buffer time to check kubeflow job's status
if (Date.now() - kubernetesTrialJob.submitTime < 20 * 1000) {
return Promise.resolve();
}
updateKubernetesTrialJobs.push(this.retrieveSingleTrialJobInfo(kubernetesCRDClient, kubernetesTrialJob));
}
await Promise.all(updateKubernetesTrialJobs);
return Promise.all(updateKubernetesTrialJobs);
}
protected async retrieveSingleTrialJobInfo(_kubernetesCRDClient: KubernetesCRDClient | undefined,
......
......@@ -209,6 +209,13 @@ abstract class KubernetesTrainingService {
return Promise.reject(error);
}
try {
await this.genericK8sClient.deleteDeployment("adaptdl-tensorboard-" + getExperimentId().toLowerCase())
this.log.info('tensorboard deployment deleted')
} catch (error) {
this.log.error(`tensorboard deployment deletion failed: ${error.message}`)
}
return Promise.resolve();
}
......@@ -377,6 +384,5 @@ abstract class KubernetesTrainingService {
}
return Promise.resolve(folderUriInAzure);
}
}
export { KubernetesTrainingService };
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as fs from 'fs';
import * as tmp from 'tmp';
import * as component from '../../common/component';
import { TrialJobApplicationForm, TrialJobDetail, TrainingService } from '../../common/trainingService';
import { cleanupUnitTest, prepareUnitTest } from '../../common/utils';
import { TrialConfigMetadataKey } from '../common/trialConfigMetadataKey';
import { AdlTrainingService } from '../kubernetes/adl/adlTrainingService';
const localCodeDir: string = tmp.dirSync().name
describe('Unit Test for AdlTrainingService', () => {
let skip: boolean = false;
try {
const testKubeflowConfig = fs.readFileSync('/home/vsts/.kube/config', 'utf8');
} catch (err) {
console.log('Please have kubernetes cluster to enable its training service unit test.');
skip = true;
}
let testAdlTrialConfig: any = JSON.stringify({
"command": "python3 /root/apps/nni_linear_regression/main.py",
"codeDir": ".",
"gpuNum": 0,
"image": "test.image:latest",
"imagePullSecrets": [
{
"name": "stagingsecrets"
}
],
"nfs": {
"server": "172.20.188.236",
"path": "/exports",
"containerMountPath": "/nfs"
},
"memorySize": "1Gi",
"cpuNum": 1
});
let testAdlTrialConfig2: any = JSON.stringify({
"command": "python3 /root/apps/nni_linear_regression/main.py",
"codeDir": ".",
"gpuNum": 0,
"image": "test.image:latest",
"imagePullSecrets": [
{
"name": "stagingsecrets"
}
],
"adaptive": true,
"checkpoint": {
"storageClass": "aws-efs",
"storageSize": "1Gi"
},
"nfs": {
"server": "172.20.188.236",
"path": "/exports",
"containerMountPath": "/nfs"
}
});
let testNniManagerIp: any = JSON.stringify({
"nniManagerIp": "0.0.0.0"
});
let adlTrainingService: AdlTrainingService;
console.log(tmp.dirSync().name);
before(() => {
chai.should();
chai.use(chaiAsPromised);
prepareUnitTest();
});
after(() => {
cleanupUnitTest();
});
beforeEach(() => {
if (skip) {
return;
}
adlTrainingService = component.get(AdlTrainingService);
adlTrainingService.run()
});
afterEach(() => {
if (skip) {
return;
}
adlTrainingService.cleanUp();
});
it('Set and get cluster metadata', async () => {
if (skip) {
return;
}
await adlTrainingService.setClusterMetadata(TrialConfigMetadataKey.TRIAL_CONFIG, testAdlTrialConfig2);
await adlTrainingService.setClusterMetadata(TrialConfigMetadataKey.NNI_MANAGER_IP, testNniManagerIp);
let data:string = await adlTrainingService.getClusterMetadata(TrialConfigMetadataKey.TRIAL_CONFIG);
chai.expect(data).to.be.equals(testAdlTrialConfig2);
});
it('Submit job', async () => {
if (skip) {
return;
}
// job without given checkpoint, with resource config
await adlTrainingService.setClusterMetadata(TrialConfigMetadataKey.TRIAL_CONFIG, testAdlTrialConfig);
let form: TrialJobApplicationForm = {
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
}
};
let jobDetail: TrialJobDetail = await adlTrainingService.submitTrialJob(form);
chai.expect(jobDetail.status).to.be.equals('WAITING');
await adlTrainingService.cancelTrialJob(jobDetail.id);
chai.expect(jobDetail.status).to.be.equals('USER_CANCELED');
// job with given checkpoint
await adlTrainingService.setClusterMetadata(TrialConfigMetadataKey.TRIAL_CONFIG, testAdlTrialConfig2);
form = {
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
}
};
jobDetail = await adlTrainingService.submitTrialJob(form);
chai.expect(jobDetail.status).to.be.equals('WAITING');
await adlTrainingService.cancelTrialJob(jobDetail.id);
chai.expect(jobDetail.status).to.be.equals('USER_CANCELED');
}).timeout(3000000);
});
......@@ -29,6 +29,8 @@
width: 87%;
margin: 0 auto;
min-width: 1200px;
/* nav bar: 56 + marginTop: 18 */
margin-top: 74px;
margin-bottom: 30px;
}
......
......@@ -4,8 +4,9 @@ import { COLUMN } from './static/const';
import { EXPERIMENT, TRIALS } from './static/datamodel';
import NavCon from './components/NavCon';
import MessageInfo from './components/modals/MessageInfo';
import { TrialConfigButton } from './components/public-child/config/TrialConfigButton';
import { SlideNavBtns } from './components/slideNav/SlideNavBtns';
import './App.scss';
import './static/style/common.scss';
interface AppState {
interval: number;
......@@ -164,7 +165,7 @@ class App extends React.Component<{}, AppState> {
updateOverviewPage: this.updateOverviewPage
}}
>
<TrialConfigButton />
<SlideNavBtns />
</AppContext.Provider>
{/* if api has error field, show error message */}
{errorList.map(
......
......@@ -10,9 +10,8 @@ import {
IStackTokens,
IStackStyles
} from '@fluentui/react';
import LogPanel from './modals/LogPanel';
import ExperimentPanel from './modals/ExperimentPanel';
import { downLoadIcon, infoIconAbout, timeIcon, disableUpdates, requency, closeTimer } from './buttons/Icon';
import ExperimentSummaryPanel from './modals/ExperimentSummaryPanel';
import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer } from './buttons/Icon';
import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs';
import { EXPERIMENT } from '../static/datamodel';
import '../static/style/nav/nav.scss';
......@@ -36,7 +35,6 @@ interface NavState {
menuVisible: boolean;
navBarVisible: boolean;
isdisabledFresh: boolean;
isvisibleLogDrawer: boolean;
isvisibleExperimentDrawer: boolean;
refreshText: string;
refreshFrequency: number | string;
......@@ -55,7 +53,6 @@ class NavCon extends React.Component<NavProps, NavState> {
menuVisible: false,
navBarVisible: false,
isdisabledFresh: false,
isvisibleLogDrawer: false, // download button (nnimanager·dispatcher) click -> drawer
isvisibleExperimentDrawer: false,
refreshText: 'Auto refresh',
refreshFrequency: 10
......@@ -67,16 +64,6 @@ class NavCon extends React.Component<NavProps, NavState> {
this.setState({ isvisibleExperimentDrawer: true });
};
// to see & download dispatcher | nnimanager log
showDispatcherLog = (): void => {
this.setState({ isvisibleLogDrawer: true });
};
// close log drawer (nnimanager.dispatcher)
closeLogDrawer = (): void => {
this.setState({ isvisibleLogDrawer: false });
};
// close download experiment parameters drawer
closeExpDrawer = (): void => {
this.setState({ isvisibleExperimentDrawer: false });
......@@ -121,7 +108,7 @@ class NavCon extends React.Component<NavProps, NavState> {
}
render(): React.ReactNode {
const { isvisibleLogDrawer, isvisibleExperimentDrawer, version, refreshText, refreshFrequency } = this.state;
const { isvisibleExperimentDrawer, version, refreshText, refreshFrequency } = this.state;
const aboutProps: IContextualMenuProps = {
items: [
{
......@@ -168,38 +155,24 @@ class NavCon extends React.Component<NavProps, NavState> {
/>
<div className='nav-refresh-num'>{refreshFrequency}</div>
</div>
<CommandBarButton iconProps={downLoadIcon} text='Download' menuProps={this.menuProps} />
<CommandBarButton
iconProps={{ iconName: 'ShowResults' }}
text='Experiment summary'
onClick={this.showExpcontent}
/>
<CommandBarButton iconProps={infoIconAbout} text='About' menuProps={aboutProps} />
</Stack>
</StackItem>
{/* the drawer for dispatcher & nnimanager log message */}
{isvisibleLogDrawer && <LogPanel closeDrawer={this.closeLogDrawer} />}
{isvisibleExperimentDrawer && (
<ExperimentPanel closeExpDrawer={this.closeExpDrawer} experimentProfile={EXPERIMENT.profile} />
<ExperimentSummaryPanel
closeExpDrawer={this.closeExpDrawer}
experimentProfile={EXPERIMENT.profile}
/>
)}
</Stack>
);
}
// view and download experiment [log & experiment result]
private menuProps: IContextualMenuProps = {
items: [
{
key: 'experiment',
text: 'Experiment summary',
iconProps: { iconName: 'ShowResults' },
onClick: this.showExpcontent
},
{
key: 'logfiles',
text: 'Log files',
iconProps: { iconName: 'FilePDB' },
onClick: this.showDispatcherLog
}
],
directionalHintFixed: true
};
private refreshProps: IContextualMenuProps = {
items: [
{
......
......@@ -13,8 +13,9 @@ import { TrialCount } from './overview/count/TrialCount';
import { Command1 } from './overview/command/Command1';
import { Command2 } from './overview/command/Command2';
import { TitleContext } from './overview/TitleContext';
import { itemStyle1, itemStyleSucceed, itemStyle2, entriesOption } from './overview/overviewConst';
import { itemStyleSucceed, entriesOption } from './overview/overviewConst';
import '../static/style/overview/overview.scss';
import '../static/style/overview/topTrial.scss';
import '../static/style/logPath.scss';
interface OverviewState {
......@@ -89,42 +90,40 @@ class Overview extends React.Component<{}, OverviewState> {
</BestMetricContext.Provider>
</div>
{/* duration & trial numbers */}
<div className='overviewProgress'>
<div className='duration'>
<TitleContext.Provider value={{ text: 'Duration', icon: 'Timer' }}>
<Title />
</TitleContext.Provider>
<ExpDurationContext.Provider
value={{
maxExecDuration,
execDuration,
updateOverviewPage,
maxDurationUnit,
changeMaxDurationUnit
}}
>
<ExpDuration />
</ExpDurationContext.Provider>
</div>
<div className='trialCount'>
<TitleContext.Provider value={{ text: 'Trial numbers', icon: 'NumberSymbol' }}>
<Title />
</TitleContext.Provider>
<ExpDurationContext.Provider
value={{
maxExecDuration,
execDuration,
updateOverviewPage,
maxDurationUnit,
changeMaxDurationUnit
}}
>
<TrialCount />
</ExpDurationContext.Provider>
</div>
<div className='duration'>
<TitleContext.Provider value={{ text: 'Duration', icon: 'Timer' }}>
<Title />
</TitleContext.Provider>
<ExpDurationContext.Provider
value={{
maxExecDuration,
execDuration,
updateOverviewPage,
maxDurationUnit,
changeMaxDurationUnit
}}
>
<ExpDuration />
</ExpDurationContext.Provider>
</div>
<div className='trialCount'>
<TitleContext.Provider value={{ text: 'Trial numbers', icon: 'NumberSymbol' }}>
<Title />
</TitleContext.Provider>
<ExpDurationContext.Provider
value={{
maxExecDuration,
execDuration,
updateOverviewPage,
maxDurationUnit,
changeMaxDurationUnit
}}
>
<TrialCount />
</ExpDurationContext.Provider>
</div>
{/* table */}
<div className='overviewTable'>
<div className='overviewBestMetric'>
<Stack horizontal>
<div style={itemStyleSucceed}>
<TitleContext.Provider value={{ text: 'Top trials', icon: 'BulletedList' }}>
......@@ -167,7 +166,13 @@ class Overview extends React.Component<{}, OverviewState> {
</Stack>
</div>
</Stack>
<SuccessTable trialIds={bestTrials.map(trial => trial.info.id)} />
<div className='overviewChart'>
<Accuracy accuracyData={accuracyGraphData} accNodata={noDataMessage} />
<SuccessTable
trialIds={bestTrials.map(trial => trial.info.trialJobId)}
updateOverviewPage={updateOverviewPage}
/>
</div>
</div>
<div className='overviewCommand1'>
<Command1 />
......@@ -175,24 +180,6 @@ class Overview extends React.Component<{}, OverviewState> {
<div className='overviewCommand2'>
<Command2 />
</div>
<div className='overviewChart'>
<Stack horizontal>
<div style={itemStyle1}>
<TitleContext.Provider
value={{ text: 'Trial metric chart', icon: 'HomeGroup' }}
>
<Title />
</TitleContext.Provider>
</div>
<div style={itemStyle2}>
<Stack className='maxmin' horizontal>
<div className='circle' />
<div>{`Top ${this.context.metricGraphMode}imal trials`}</div>
</Stack>
</div>
</Stack>
<Accuracy accuracyData={accuracyGraphData} accNodata={noDataMessage} height={380} />
</div>
</div>
</div>
);
......@@ -219,8 +206,8 @@ class Overview extends React.Component<{}, OverviewState> {
return {
// support max show 0.0000000
grid: {
left: 67,
right: 40
x: 60,
y: 40
},
tooltip: {
trigger: 'item'
......
import * as React from 'react';
import { downFile } from '../../static/function';
import { Stack, PrimaryButton, DefaultButton, Panel, StackItem, Pivot, PivotItem } from '@fluentui/react';
import { Stack, PrimaryButton, DefaultButton, Panel, StackItem } from '@fluentui/react';
import { DRAWEROPTION } from '../../static/const';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { caclMonacoEditorHeight } from '../../static/function';
import MonacoEditor from 'react-monaco-editor';
import '../../static/style/logDrawer.scss';
......@@ -16,7 +17,7 @@ interface ExpDrawerState {
expDrawerHeight: number;
}
class ExperimentDrawer extends React.Component<ExpDrawerProps, ExpDrawerState> {
class ExperimentSummaryPanel extends React.Component<ExpDrawerProps, ExpDrawerState> {
public _isExperimentMount!: boolean;
private refreshId!: number | undefined;
......@@ -88,41 +89,31 @@ class ExperimentDrawer extends React.Component<ExpDrawerProps, ExpDrawerState> {
render(): React.ReactNode {
const { closeExpDrawer } = this.props;
const { experiment, expDrawerHeight } = this.state;
const monacoEditorHeight = caclMonacoEditorHeight(expDrawerHeight);
return (
<Stack className='logDrawer'>
<Panel
isOpen={true}
hasCloseButton={false}
isLightDismiss={true}
onLightDismissClick={closeExpDrawer}
styles={{ root: { height: expDrawerHeight, paddingTop: 15 } }}
>
<Pivot style={{ minHeight: 190 }} className='log-tab-body'>
<PivotItem headerText='Experiment parameters'>
<div className='just-for-log'>
<MonacoEditor
width='100%'
// 92 + marginTop[16]
height={expDrawerHeight - 108}
language='json'
value={experiment}
options={DRAWEROPTION}
/>
</div>
<Stack horizontal className='buttons'>
<StackItem grow={50} className='download'>
<PrimaryButton text='Download' onClick={this.downExperimentParameters} />
</StackItem>
<StackItem grow={50} className='close'>
<DefaultButton text='Close' onClick={closeExpDrawer} />
</StackItem>
</Stack>
</PivotItem>
</Pivot>
</Panel>
</Stack>
<Panel isOpen={true} hasCloseButton={false} isLightDismiss={true} onLightDismissClick={closeExpDrawer}>
<div className='panel'>
<div className='panelName'>Experiment summary</div>
<MonacoEditor
width='100%'
height={monacoEditorHeight}
language='json'
value={experiment}
options={DRAWEROPTION}
/>
<Stack horizontal className='buttons'>
<StackItem grow={50} className='download'>
<PrimaryButton text='Download' onClick={this.downExperimentParameters} />
</StackItem>
<StackItem grow={50} className='close'>
<DefaultButton text='Close' onClick={closeExpDrawer} />
</StackItem>
</Stack>
</div>
</Panel>
);
}
}
export default ExperimentDrawer;
export default ExperimentSummaryPanel;
......@@ -29,7 +29,7 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
nniManagerLogStr: null,
dispatcherLogStr: null,
isLoading: true,
logDrawerHeight: window.innerHeight - 48
logDrawerHeight: window.innerHeight
};
}
......@@ -64,7 +64,7 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
);
setLogDrawerHeight = (): void => {
this.setState(() => ({ logDrawerHeight: window.innerHeight - 48 }));
this.setState(() => ({ logDrawerHeight: window.innerHeight }));
};
async componentDidMount(): Promise<void> {
......@@ -80,7 +80,8 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
render(): React.ReactNode {
const { closeDrawer, activeTab } = this.props;
const { nniManagerLogStr, dispatcherLogStr, isLoading, logDrawerHeight } = this.state;
// tab[height: 56] + tab[margin-bottom: 20] + button[32] + button[margin-top: 45, -bottom: 7] + fluent-panel own paddingBottom[20] + title-border[2]
const monacoHeight = logDrawerHeight - 182;
return (
<Stack>
<Panel
......@@ -90,15 +91,13 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
isLightDismiss={true}
onLightDismissClick={closeDrawer}
>
<div className='log-tab-body'>
<Pivot selectedKey={activeTab} style={{ minHeight: 190, paddingTop: '16px' }}>
{/* <PivotItem headerText={this.dispatcherHTML()} key="dispatcher" onLinkClick> */}
<PivotItem headerText='Dispatcher log' key='dispatcher'>
<Pivot selectedKey={activeTab} style={{ minHeight: 190 }}>
<PivotItem headerText='Dispatcher log' key='dispatcher'>
<div className='panel logMargin'>
<MonacoHTML
content={dispatcherLogStr || 'Loading...'}
loading={isLoading}
// paddingTop[16] + tab[44] + button[32]
height={logDrawerHeight - 92}
height={monacoHeight}
/>
<Stack horizontal className='buttons'>
<StackItem grow={12} className='download'>
......@@ -108,13 +107,14 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
<DefaultButton text='Close' onClick={closeDrawer} />
</StackItem>
</Stack>
</PivotItem>
<PivotItem headerText='NNIManager log' key='nnimanager'>
{/* <TabPane tab="NNImanager Log" key="nnimanager"> */}
</div>
</PivotItem>
<PivotItem headerText='NNIManager log' key='nnimanager'>
<div className='panel logMargin'>
<MonacoHTML
content={nniManagerLogStr || 'Loading...'}
loading={isLoading}
height={logDrawerHeight - 92}
height={monacoHeight}
/>
<Stack horizontal className='buttons'>
<StackItem grow={12} className='download'>
......@@ -124,9 +124,9 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
<DefaultButton text='Close' onClick={closeDrawer} />
</StackItem>
</Stack>
</PivotItem>
</Pivot>
</div>
</div>
</PivotItem>
</Pivot>
</Panel>
</Stack>
);
......
......@@ -11,7 +11,6 @@ import 'echarts/lib/component/title';
interface AccuracyProps {
accuracyData: object;
accNodata: string;
height: number;
}
class Accuracy extends React.Component<AccuracyProps, {}> {
......@@ -20,17 +19,10 @@ class Accuracy extends React.Component<AccuracyProps, {}> {
}
render(): React.ReactNode {
const { accNodata, accuracyData, height } = this.props;
const { accNodata, accuracyData } = this.props;
return (
<div style={{ position: 'relative' }}>
<ReactEcharts
option={accuracyData}
style={{
height: height,
margin: '0 auto'
}}
theme='my_theme'
/>
<div className='defaultMetricContainer'>
<ReactEcharts option={accuracyData} theme='my_theme' />
<div className='showMess'>{accNodata}</div>
</div>
);
......
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