nnimanager.ts 39.7 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Deshui Yu's avatar
Deshui Yu committed
3

4
import assert from 'assert';
chicm-ms's avatar
chicm-ms committed
5
import { ChildProcess, StdioOptions } from 'child_process';
Deshui Yu's avatar
Deshui Yu committed
6
7
8
import { Deferred } from 'ts-deferred';
import * as component from '../common/component';
import { DataStore, MetricDataRecord, MetricType, TrialJobInfo } from '../common/datastore';
9
import { NNIError } from '../common/errors';
10
import { getExperimentId } from '../common/experimentStartupInfo';
11
12
import globals from 'common/globals';
import { Logger, getLogger } from '../common/log';
Deshui Yu's avatar
Deshui Yu committed
13
import {
14
    ExperimentProfile, Manager, ExperimentStatus,
15
    NNIManagerStatus, ProfileUpdateType, TrialJobStatistics
Deshui Yu's avatar
Deshui Yu committed
16
} from '../common/manager';
liuzhe-lz's avatar
liuzhe-lz committed
17
import { ExperimentConfig, LocalConfig, toSeconds, toCudaVisibleDevices } from '../common/experimentConfig';
18
import { getExperimentsManager } from 'extensions/experiments_manager';
J-shang's avatar
J-shang committed
19
import { TensorboardManager } from '../common/tensorboardManager';
Deshui Yu's avatar
Deshui Yu committed
20
import {
QuanluZhang's avatar
QuanluZhang committed
21
    TrainingService, TrialJobApplicationForm, TrialJobDetail, TrialJobMetric, TrialJobStatus, TrialCommandContent, PlacementConstraint
Deshui Yu's avatar
Deshui Yu committed
22
} from '../common/trainingService';
23
import { delay, getCheckpointDir, getExperimentRootDir, getLogDir, getMsgDispatcherCommand, mkDirP, getTunerProc, getLogLevel, isAlive, killPid } from '../common/utils';
Deshui Yu's avatar
Deshui Yu committed
24
import {
chicm-ms's avatar
chicm-ms committed
25
    INITIALIZE, INITIALIZED, KILL_TRIAL_JOB, NEW_TRIAL_JOB, NO_MORE_TRIAL_JOBS, PING,
26
    REPORT_METRIC_DATA, REQUEST_TRIAL_JOBS, SEND_TRIAL_JOB_PARAMETER, TERMINATE, TRIAL_END, UPDATE_SEARCH_SPACE, IMPORT_DATA
Deshui Yu's avatar
Deshui Yu committed
27
} from './commands';
28
import { createDispatcherInterface, IpcInterface } from './ipcInterface';
Deshui Yu's avatar
Deshui Yu committed
29
30

/**
chicm-ms's avatar
chicm-ms committed
31
 * NNIManager which implements Manager interface
Deshui Yu's avatar
Deshui Yu committed
32
33
 */
class NNIManager implements Manager {
34
    private trainingService!: TrainingService;
35
    private dispatcher: IpcInterface | undefined;
36
    private currSubmittedTrialNum: number;  // need to be recovered
QuanluZhang's avatar
QuanluZhang committed
37
    private trialConcurrencyChange: number; // >0: increase, <0: decrease
Deshui Yu's avatar
Deshui Yu committed
38
39
    private log: Logger;
    private dataStore: DataStore;
40
    private experimentProfile!: ExperimentProfile;
41
    private dispatcherPid: number;
42
    private status: NNIManagerStatus;
43
    private waitingTrials: TrialJobApplicationForm[];
QuanluZhang's avatar
QuanluZhang committed
44
    private trialJobs: Map<string, TrialJobDetail>;
45
    private trialDataForTuner: string;
SparkSnail's avatar
SparkSnail committed
46
    private readonly: boolean;
47
    private config!: ExperimentConfig;
48

49
    private trialJobMetricListener: (metric: TrialJobMetric) => void;
50

Deshui Yu's avatar
Deshui Yu committed
51
52
    constructor() {
        this.currSubmittedTrialNum = 0;
QuanluZhang's avatar
QuanluZhang committed
53
        this.trialConcurrencyChange = 0;
54
        this.dispatcherPid = 0;
QuanluZhang's avatar
QuanluZhang committed
55
56
        this.waitingTrials = [];
        this.trialJobs = new Map<string, TrialJobDetail>();
57
        this.trialDataForTuner = '';
SparkSnail's avatar
SparkSnail committed
58
        this.readonly = false;
Deshui Yu's avatar
Deshui Yu committed
59

liuzhe-lz's avatar
liuzhe-lz committed
60
        this.log = getLogger('NNIManager');
Deshui Yu's avatar
Deshui Yu committed
61
        this.dataStore = component.get(DataStore);
62
63
64
        this.status = {
            status: 'INITIALIZED',
            errors: []
Deshui Yu's avatar
Deshui Yu committed
65
        };
chicm-ms's avatar
chicm-ms committed
66
        this.trialJobMetricListener = (metric: TrialJobMetric): void => {
67
68
69
70
            this.onTrialJobMetrics(metric).catch((err: Error) => {
                this.criticalError(NNIError.FromError(err, 'Job metrics error: '));
            });
        };
71

72
        globals.shutdown.register('NniManager', this.stopExperiment.bind(this));
Deshui Yu's avatar
Deshui Yu committed
73
74
75
    }

    public updateExperimentProfile(experimentProfile: ExperimentProfile, updateType: ProfileUpdateType): Promise<void> {
SparkSnail's avatar
SparkSnail committed
76
77
78
        if (this.readonly) {
            return Promise.reject(new Error('Error: can not update experiment profile in readonly mode!'));
        }
Deshui Yu's avatar
Deshui Yu committed
79
80
81
82
83
        switch (updateType) {
            case 'TRIAL_CONCURRENCY':
                this.updateTrialConcurrency(experimentProfile.params.trialConcurrency);
                break;
            case 'MAX_EXEC_DURATION':
84
                this.experimentProfile.params.maxExperimentDuration = experimentProfile.params.maxExperimentDuration;
Deshui Yu's avatar
Deshui Yu committed
85
86
87
88
                break;
            case 'SEARCH_SPACE':
                this.updateSearchSpace(experimentProfile.params.searchSpace);
                break;
QuanluZhang's avatar
QuanluZhang committed
89
            case 'MAX_TRIAL_NUM':
90
                this.experimentProfile.params.maxTrialNumber = experimentProfile.params.maxTrialNumber;
QuanluZhang's avatar
QuanluZhang committed
91
                break;
Deshui Yu's avatar
Deshui Yu committed
92
93
94
95
96
97
98
            default:
                throw new Error('Error: unrecognized updateType');
        }

        return this.storeExperimentProfile();
    }

99
    public importData(data: string): Promise<void> {
SparkSnail's avatar
SparkSnail committed
100
101
102
        if (this.readonly) {
            return Promise.reject(new Error('Error: can not import data in readonly mode!'));
        }
103
104
105
106
107
108
109
110
111
112
        if (this.dispatcher === undefined) {
            return Promise.reject(
                new Error('tuner has not been setup')
            );
        }
        this.dispatcher.sendCommand(IMPORT_DATA, data);

        return this.dataStore.storeTrialJobEvent('IMPORT_DATA', '', data);
    }

113
114
115
116
    public getImportedData(): Promise<string[]> {
        return this.dataStore.getImportedData();
    }

117
118
119
120
    public async exportData(): Promise<string> {
        return this.dataStore.exportTrialHpConfigs();
    }

121
    public addCustomizedTrialJob(hyperParams: string): Promise<number> {
SparkSnail's avatar
SparkSnail committed
122
123
124
        if (this.readonly) {
            return Promise.reject(new Error('Error: can not add customized trial job in readonly mode!'));
        }
125
        if (this.currSubmittedTrialNum >= this.maxTrialNum) {
126
            return Promise.reject(new Error('reach maxTrialNum'));
Deshui Yu's avatar
Deshui Yu committed
127
        }
128
129
130

        // TODO: NNI manager should not peek tuner's internal protocol, let's refactor this later
        const packedParameter = {
chicm-ms's avatar
chicm-ms committed
131
132
            parameter_id: null, // eslint-disable-line @typescript-eslint/camelcase
            parameter_source: 'customized', // eslint-disable-line @typescript-eslint/camelcase
133
134
135
136
137
138
139
140
141
142
143
            parameters: JSON.parse(hyperParams)
        }

        const form: TrialJobApplicationForm = {
            sequenceId: this.experimentProfile.nextSequenceId++,
            hyperParameters: {
                value: JSON.stringify(packedParameter),
                index: 0
            }
        };
        this.waitingTrials.push(form);
Deshui Yu's avatar
Deshui Yu committed
144
145

        // trial id has not been generated yet, thus use '' instead
146
147
148
        this.dataStore.storeTrialJobEvent('ADD_CUSTOMIZED', '', hyperParams);

        return Promise.resolve(form.sequenceId);
Deshui Yu's avatar
Deshui Yu committed
149
150
151
    }

    public async cancelTrialJobByUser(trialJobId: string): Promise<void> {
SparkSnail's avatar
SparkSnail committed
152
153
154
        if (this.readonly) {
            return Promise.reject(new Error('Error: can not cancel trial job in readonly mode!'));
        }
chicm-ms's avatar
chicm-ms committed
155
        this.log.info(`User cancelTrialJob: ${trialJobId}`);
Deshui Yu's avatar
Deshui Yu committed
156
157
158
159
        await this.trainingService.cancelTrialJob(trialJobId);
        await this.dataStore.storeTrialJobEvent('USER_TO_CANCEL', trialJobId, '');
    }

160
161
162
163
164
165
166
167
168
169
170
    public async startExperiment(config: ExperimentConfig): Promise<string> {
        this.experimentProfile = {
            params: config,
            id: getExperimentId(),
            execDuration: 0,
            logDir: getExperimentRootDir(),
            startTime: Date.now(),
            endTime: undefined,
            nextSequenceId: 0,
            revision: 0
        };
171
        this.config = config;
chicm-ms's avatar
chicm-ms committed
172
        this.log.info(`Starting experiment: ${this.experimentProfile.id}`);
Deshui Yu's avatar
Deshui Yu committed
173
        await this.storeExperimentProfile();
174

175
176
177
178
        if (this.trainingService === undefined) {
            this.log.info('Setup training service...');
            this.trainingService = await this.initTrainingService(config);
        }
179

180
181
        this.log.info('Setup tuner...');
        const dispatcherCommand: string = getMsgDispatcherCommand(config);
182
        this.log.debug(`dispatcher command: ${dispatcherCommand}`);
QuanluZhang's avatar
QuanluZhang committed
183
        const checkpointDir: string = await this.createCheckpointDir();
184
        await this.setupTuner(dispatcherCommand, undefined, 'start', checkpointDir);
chicm-ms's avatar
chicm-ms committed
185
        this.setStatus('RUNNING');
Deshui Yu's avatar
Deshui Yu committed
186
        await this.storeExperimentProfile();
187
188
        this.run().catch((err: Error) => {
            this.criticalError(err);
Deshui Yu's avatar
Deshui Yu committed
189
        });
190

Deshui Yu's avatar
Deshui Yu committed
191
192
193
        return this.experimentProfile.id;
    }

SparkSnail's avatar
SparkSnail committed
194
    public async resumeExperiment(readonly: boolean): Promise<void> {
Deshui Yu's avatar
Deshui Yu committed
195
196
        //Fetch back the experiment profile
        const experimentId: string = getExperimentId();
197
        this.log.info(`Resuming experiment: ${experimentId}`);
Deshui Yu's avatar
Deshui Yu committed
198
        this.experimentProfile = await this.dataStore.getExperimentProfile(experimentId);
199

200
        const config: ExperimentConfig = this.experimentProfile.params;
SparkSnail's avatar
SparkSnail committed
201
202
203
204
205
        this.config = config;
        if (this.trainingService === undefined) {
            this.log.info('Setup training service...');
            this.trainingService = await this.initTrainingService(config);
        }
206

207
208
209
210
211
212
        this.readonly = readonly;
        if (readonly) {
            this.setStatus('VIEWED');
            return;
        }

213
214
        this.log.info('Setup tuner...');
        const dispatcherCommand: string = getMsgDispatcherCommand(config);
215
        this.log.debug(`dispatcher command: ${dispatcherCommand}`);
QuanluZhang's avatar
QuanluZhang committed
216
        const checkpointDir: string = await this.createCheckpointDir();
217
        await this.setupTuner(dispatcherCommand, undefined, 'resume', checkpointDir);
Deshui Yu's avatar
Deshui Yu committed
218
219
220
221
222
223
224
225
226

        const allTrialJobs: TrialJobInfo[] = await this.dataStore.listTrialJobs();

        // Resume currSubmittedTrialNum
        this.currSubmittedTrialNum = allTrialJobs.length;

        // Check the final status for WAITING and RUNNING jobs
        await Promise.all(allTrialJobs
            .filter((job: TrialJobInfo) => job.status === 'WAITING' || job.status === 'RUNNING')
J-shang's avatar
J-shang committed
227
            .map((job: TrialJobInfo) => this.dataStore.storeTrialJobEvent('FAILED', job.trialJobId)));
Deshui Yu's avatar
Deshui Yu committed
228

229
230
231
        // Collect generated trials and imported trials
        const finishedTrialData: string = await this.exportData();
        const importedData: string[] = await this.dataStore.getImportedData();
chicm-ms's avatar
chicm-ms committed
232
        let trialData: Record<string, any>[] = JSON.parse(finishedTrialData);
233
234
        for (const oneImportedData of importedData) {
            // do not deduplicate
chicm-ms's avatar
chicm-ms committed
235
            trialData = trialData.concat(<Record<string, any>[]>JSON.parse(oneImportedData));
236
237
238
        }
        this.trialDataForTuner = JSON.stringify(trialData);

239
240
        if (this.experimentProfile.execDuration < this.maxDuration &&
            this.currSubmittedTrialNum < this.maxTrialNum &&
chicm-ms's avatar
chicm-ms committed
241
242
243
            this.experimentProfile.endTime) {
            delete this.experimentProfile.endTime;
        }
chicm-ms's avatar
chicm-ms committed
244
        this.setStatus('RUNNING');
245

Deshui Yu's avatar
Deshui Yu committed
246
        // TO DO: update database record for resume event
247
248
249
        this.run().catch((err: Error) => {
            this.criticalError(err);
        });
Deshui Yu's avatar
Deshui Yu committed
250
251
    }

252
253
    public getTrialJob(trialJobId: string): Promise<TrialJobInfo> {
        return this.dataStore.getTrialJob(trialJobId);
Deshui Yu's avatar
Deshui Yu committed
254
255
    }

liuzhe-lz's avatar
liuzhe-lz committed
256
    public async setClusterMetadata(key: string, value: string): Promise<void> {
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
        // Hack for supporting v2 config, need refactor
        if (this.trainingService === undefined) {
            this.log.info('Setup training service...');
            switch (key) {
                case 'kubeflow_config': {
                    const kubeflowModule = await import('../training_service/kubernetes/kubeflow/kubeflowTrainingService');
                    this.trainingService = new kubeflowModule.KubeflowTrainingService();
                    break;
                }
                case 'frameworkcontroller_config': {
                    const fcModule = await import('../training_service/kubernetes/frameworkcontroller/frameworkcontrollerTrainingService');
                    this.trainingService = new fcModule.FrameworkControllerTrainingService();
                    break;
                }
                case 'adl_config': {
                    const adlModule = await import('../training_service/kubernetes/adl/adlTrainingService');
                    this.trainingService = new adlModule.AdlTrainingService();
                    break;
                }
                default:
                    throw new Error("Setup training service failed.");
            }
liuzhe-lz's avatar
liuzhe-lz committed
279
        }
280
        await this.trainingService.setClusterMetadata(key, value);
Deshui Yu's avatar
Deshui Yu committed
281
282
    }

liuzhe-lz's avatar
liuzhe-lz committed
283
284
    public getClusterMetadata(key: string): Promise<string> {
        return this.trainingService.getClusterMetadata(key);
Deshui Yu's avatar
Deshui Yu committed
285
286
287
288
289
290
    }

    public async getTrialJobStatistics(): Promise<TrialJobStatistics[]> {
        return this.dataStore.getTrialJobStatistics();
    }

291
    private async stopExperiment(): Promise<void> {
292
293
294
295
        await this.stopExperimentTopHalf();
        await this.stopExperimentBottomHalf();
    }

296
    private async stopExperimentTopHalf(): Promise<void> {
chicm-ms's avatar
chicm-ms committed
297
298
        this.setStatus('STOPPING');
        this.log.info('Stopping experiment, cleaning up ...');
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319

        if (this.dispatcher === undefined) {
            this.log.error('Tuner has not been setup');
            return;
        }

        this.trainingService.removeTrialJobMetricListener(this.trialJobMetricListener);
        if (this.dispatcherPid > 0) {
            this.dispatcher.sendCommand(TERMINATE);
            // gracefully terminate tuner and assessor here, wait at most 30 seconds.
            for (let i: number = 0; i < 30; i++) {
                if (!await isAlive(this.dispatcherPid)) {
                    break;
                }
                await delay(1000);
            }
            await killPid(this.dispatcherPid);
        }
        this.dispatcher = undefined;
    }

320
    private async stopExperimentBottomHalf(): Promise<void> {
321
322
323
324
325
326
327
328
329
330
331
332
333
334
        try {
            const trialJobList: TrialJobDetail[] = await this.trainingService.listTrialJobs();

            // DON'T try to make it in parallel, the training service may not handle it well.
            // If there is performance concern, consider to support batch cancellation on training service.
            for (const trialJob of trialJobList) {
                if (trialJob.status === 'RUNNING' ||
                    trialJob.status === 'WAITING') {
                    try {
                        this.log.info(`cancelTrialJob: ${trialJob.id}`);
                        await this.trainingService.cancelTrialJob(trialJob.id);
                    } catch (error) {
                        this.log.debug(`ignorable error on canceling trial ${trialJob.id}. ${error}`);
                    }
335
336
                }
            }
337
338
339
            await this.trainingService.cleanUp();
        } catch (err) {
            this.log.error(`${err.stack}`);
340
341
342
343
344
345
        }
        if (this.experimentProfile.endTime === undefined) {
            this.setEndtime();
        }
        await this.storeExperimentProfile();
        this.setStatus('STOPPED');
chicm-ms's avatar
chicm-ms committed
346
        this.log.info('Experiment stopped.');
347

348
349
        await component.get<TensorboardManager>(TensorboardManager).stop();
        await this.dataStore.close();
Deshui Yu's avatar
Deshui Yu committed
350
351
    }

352
    public async getMetricData(trialJobId?: string, metricType?: MetricType): Promise<MetricDataRecord[]> {
Deshui Yu's avatar
Deshui Yu committed
353
354
355
        return this.dataStore.getMetricData(trialJobId, metricType);
    }

356
357
358
359
360
361
    public async getMetricDataByRange(minSeqId: number, maxSeqId: number): Promise<MetricDataRecord[]> {
        const trialJobs = await this.dataStore.listTrialJobs();
        const targetTrials = trialJobs.filter(trial => (
            // FIXME: can this be undefined?
            trial.sequenceId !== undefined && minSeqId <= trial.sequenceId && trial.sequenceId <= maxSeqId
        ));
J-shang's avatar
J-shang committed
362
        const targetTrialIds = new Set(targetTrials.map(trial => trial.trialJobId));
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386

        const allMetrics = await this.dataStore.getMetricData();
        return allMetrics.filter(metric => targetTrialIds.has(metric.trialJobId));
    }

    public async getLatestMetricData(): Promise<MetricDataRecord[]> {
        // FIXME: this can take a long time
        const allMetrics: MetricDataRecord[] = await this.dataStore.getMetricData();
        const finals: MetricDataRecord[] = [];
        const latestIntermediates: Map<string, MetricDataRecord> = new Map<string, MetricDataRecord>();
        for (const metric of allMetrics) {
            if (metric.type !== 'PERIODICAL') {
                finals.push(metric);
            } else {
                const old: MetricDataRecord | undefined = latestIntermediates.get(metric.trialJobId);
                if (old === undefined || old.sequence <= metric.sequence) {
                    latestIntermediates.set(metric.trialJobId, metric);
                }
            }
        }
        return finals.concat(Array.from(latestIntermediates.values()));
        // FIXME: unit test
    }

Yuge Zhang's avatar
Yuge Zhang committed
387
388
    public async getTrialFile(trialJobId: string, fileName: string): Promise<Buffer | string> {
        return this.trainingService.getTrialFile(trialJobId, fileName);
389
390
    }

Deshui Yu's avatar
Deshui Yu committed
391
392
393
394
395
396
397
398
    public getExperimentProfile(): Promise<ExperimentProfile> {
        // TO DO: using Promise.resolve()
        const deferred: Deferred<ExperimentProfile> = new Deferred<ExperimentProfile>();
        deferred.resolve(this.experimentProfile);

        return deferred.promise;
    }

399
400
401
402
    public getStatus(): NNIManagerStatus {
        return this.status;
    }

Deshui Yu's avatar
Deshui Yu committed
403
404
405
406
    public async listTrialJobs(status?: TrialJobStatus): Promise<TrialJobInfo[]> {
        return this.dataStore.listTrialJobs(status);
    }

407
408
409
410
411
412
413
414
415
416
    private get maxDuration(): number {
        const value = this.experimentProfile.params.maxExperimentDuration;
        return (value === undefined ? Infinity : toSeconds(value));
    }

    private get maxTrialNum(): number {
        const value = this.experimentProfile.params.maxTrialNumber;
        return (value === undefined ? Infinity : value);
    }

Ni Hao's avatar
Ni Hao committed
417
418
419
420
421
    private get maxTrialDuration(): number {
        const value = this.experimentProfile.params.maxTrialDuration;
        return (value === undefined ? Infinity : toSeconds(value));
    }

422
    private async initTrainingService(config: ExperimentConfig): Promise<TrainingService> {
liuzhe-lz's avatar
liuzhe-lz committed
423
424
425
426
427
428
429
430
431
432
433
        let platform: string;
        if (Array.isArray(config.trainingService)) {
            platform = 'hybrid';
        } else if (config.trainingService.platform) {
            platform = config.trainingService.platform;
        } else {
            platform = (config as any).trainingServicePlatform;
        }
        if (!platform) {
            throw new Error('Cannot detect training service platform');
        }
434
        const reuseMode = Array.isArray(config.trainingService) || (config.trainingService as any).reuseMode;
435

436
437
438
439
        if (reuseMode) {
            const module_ = await import('../training_service/reusable/routerTrainingService');
            return await module_.RouterTrainingService.construct(config);
        } else if (platform === 'local') {
440
            const module_ = await import('../training_service/local/localTrainingService');
liuzhe-lz's avatar
liuzhe-lz committed
441
            return new module_.LocalTrainingService(<LocalConfig>config.trainingService);
442
443
444
        } else if (platform === 'kubeflow') {
            const module_ = await import('../training_service/kubernetes/kubeflow/kubeflowTrainingService');
            return new module_.KubeflowTrainingService();
445
446
447
448
449
450
        } else if (platform === 'frameworkcontroller') {
            const module_ = await import('../training_service/kubernetes/frameworkcontroller/frameworkcontrollerTrainingService');
            return new module_.FrameworkControllerTrainingService();
        } else if (platform === 'adl') {
            const module_ = await import('../training_service/kubernetes/adl/adlTrainingService');
            return new module_.AdlTrainingService();
451
452
453
        } else {
            const module_ = await import('../training_service/reusable/routerTrainingService');
            return await module_.RouterTrainingService.construct(config);
454
455
456
        }
    }

457
    private async setupTuner(command: string, cwd: string | undefined, mode: 'start' | 'resume', dataDirectory: string): Promise<void> {
458
        if (this.dispatcher !== undefined) {
Deshui Yu's avatar
Deshui Yu committed
459
460
            return;
        }
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476

        let tunerWs: string;
        if (globals.args.urlPrefix) {
            tunerWs = `ws://localhost:${globals.args.port}/${globals.args.urlPrefix}/tuner`;
        } else {
            tunerWs = `ws://localhost:${globals.args.port}/tuner`;
        }

        if (globals.args.tunerCommandChannel) {
            // TODO: this will become configurable after refactoring rest handler interface
            assert.equal(tunerWs, globals.args.tunerCommandChannel);
            this.dispatcher = await createDispatcherInterface();
            return;
        }

        const stdio: StdioOptions = ['ignore', process.stdout, process.stderr];
Deshui Yu's avatar
Deshui Yu committed
477
478
479
480
481
482
483
        let newCwd: string;
        if (cwd === undefined || cwd === '') {
            newCwd = getLogDir();
        } else {
            newCwd = cwd;
        }
        // TO DO: add CUDA_VISIBLE_DEVICES
484
        const includeIntermediateResultsEnv = !!(this.config.deprecated && this.config.deprecated.includeIntermediateResults);
485

chicm-ms's avatar
chicm-ms committed
486
        const nniEnv = {
chicm-ms's avatar
chicm-ms committed
487
            SDK_PROCESS: 'dispatcher',
Zejun Lin's avatar
Zejun Lin committed
488
489
            NNI_MODE: mode,
            NNI_CHECKPOINT_DIRECTORY: dataDirectory,
490
            NNI_LOG_DIRECTORY: getLogDir(),
491
            NNI_LOG_LEVEL: getLogLevel(),
492
            NNI_INCLUDE_INTERMEDIATE_RESULTS: includeIntermediateResultsEnv,
493
            NNI_TUNER_COMMAND_CHANNEL: tunerWs,
494
            CUDA_VISIBLE_DEVICES: toCudaVisibleDevices(this.experimentProfile.params.tunerGpuIndices)
Zejun Lin's avatar
Zejun Lin committed
495
        };
chicm-ms's avatar
chicm-ms committed
496
        const newEnv = Object.assign({}, process.env, nniEnv);
497
        const tunerProc: ChildProcess = getTunerProc(command, stdio, newCwd, newEnv);
498
        this.dispatcherPid = tunerProc.pid!;
499
        this.dispatcher = await createDispatcherInterface();
Deshui Yu's avatar
Deshui Yu committed
500
501
502
503
504

        return;
    }

    private updateTrialConcurrency(trialConcurrency: number): void {
QuanluZhang's avatar
QuanluZhang committed
505
506
        // we assume trialConcurrency >= 0, which is checked by restserver
        this.trialConcurrencyChange += (trialConcurrency - this.experimentProfile.params.trialConcurrency);
Deshui Yu's avatar
Deshui Yu committed
507
508
509
510
511
        this.experimentProfile.params.trialConcurrency = trialConcurrency;

        return;
    }

512
    private updateSearchSpace(searchSpace: object): void {
513
        if (this.dispatcher === undefined) {
Deshui Yu's avatar
Deshui Yu committed
514
515
            throw new Error('Error: tuner has not been setup');
        }
516
        this.log.info(`Updated search space ${searchSpace}`);
517
518
        this.dispatcher.sendCommand(UPDATE_SEARCH_SPACE, JSON.stringify(searchSpace));
        this.experimentProfile.params.searchSpace = searchSpace;
Deshui Yu's avatar
Deshui Yu committed
519
520
521
522
523

        return;
    }

    private async periodicallyUpdateExecDuration(): Promise<void> {
524
        let count: number = 1;
525
        while (!['ERROR', 'STOPPING', 'STOPPED'].includes(this.status.status)) {
526
            await delay(1000 * 1); // 1 seconds
527
            if (['RUNNING', 'NO_MORE_TRIAL', 'TUNER_NO_MORE_TRIAL'].includes(this.status.status)) {
528
529
530
531
532
533
                this.experimentProfile.execDuration += 1;
                if (count % 10 === 0) {
                    await this.storeExperimentProfile();
                }
            }
            count += 1;
Deshui Yu's avatar
Deshui Yu committed
534
535
536
        }
    }

chicm-ms's avatar
chicm-ms committed
537
538
539
540
541
542
    private async pingDispatcher(): Promise<void> {
        if (this.dispatcher === undefined) {
            throw new Error('Error: tuner has not been setup');
        }
        while (!['ERROR', 'STOPPING', 'STOPPED'].includes(this.status.status)) {
            this.dispatcher.sendCommand(PING);
chicm-ms's avatar
chicm-ms committed
543
            await delay(1000 * 5);
chicm-ms's avatar
chicm-ms committed
544
545
546
        }
    }

547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
    private async stopTrialIfOverMaxDurationLimit(): Promise<void> {
        if(this.maxTrialDuration === Infinity){
            return;
        }

        for (const trialJobId of Array.from(this.trialJobs.keys())) {
            const trialJobDetail: TrialJobDetail | undefined = this.trialJobs.get(trialJobId);
            if(undefined !== trialJobDetail &&
                trialJobDetail.status === 'RUNNING' &&
                trialJobDetail.startTime !== undefined){
                const currentTrialDuration = (new Date().getTime() - trialJobDetail.startTime) / 1000;
                if(currentTrialDuration>this.maxTrialDuration) {
                    const isEarlyStopped = true;
                    await this.trainingService.cancelTrialJob(trialJobId, isEarlyStopped);
                    this.log.info(`Trial job ${trialJobDetail.id} has been canceled because it is over max trial duration.`);
                }
            }
564
        }
Ni Hao's avatar
Ni Hao committed
565
566
    }

QuanluZhang's avatar
QuanluZhang committed
567
568
    private async requestTrialJobsStatus(): Promise<number> {
        let finishedTrialJobNum: number = 0;
QuanluZhang's avatar
QuanluZhang committed
569
570
571
        if (this.dispatcher === undefined) {
            throw new Error('Error: tuner has not been setup');
        }
QuanluZhang's avatar
QuanluZhang committed
572
573
574
575
        for (const trialJobId of Array.from(this.trialJobs.keys())) {
            const trialJobDetail: TrialJobDetail = await this.trainingService.getTrialJob(trialJobId);
            const oldTrialJobDetail: TrialJobDetail | undefined = this.trialJobs.get(trialJobId);
            if (oldTrialJobDetail !== undefined && oldTrialJobDetail.status !== trialJobDetail.status) {
chicm-ms's avatar
chicm-ms committed
576
                this.log.info(`Trial job ${trialJobDetail.id} status changed from ${oldTrialJobDetail.status} to ${trialJobDetail.status}`);
QuanluZhang's avatar
QuanluZhang committed
577
                this.trialJobs.set(trialJobId, Object.assign({}, trialJobDetail));
578
                await this.dataStore.storeTrialJobEvent(trialJobDetail.status, trialJobDetail.id, undefined, trialJobDetail);
QuanluZhang's avatar
QuanluZhang committed
579
            }
580
581
582
583
            const newTrialJobDetail: TrialJobDetail | undefined = this.trialJobs.get(trialJobId);
            if (newTrialJobDetail !== undefined) {
                newTrialJobDetail.message = trialJobDetail.message;
            }
QuanluZhang's avatar
QuanluZhang committed
584
            let hyperParams: string | undefined = undefined;
QuanluZhang's avatar
QuanluZhang committed
585
586
587
            switch (trialJobDetail.status) {
                case 'SUCCEEDED':
                case 'USER_CANCELED':
QuanluZhang's avatar
QuanluZhang committed
588
                case 'EARLY_STOPPED':
QuanluZhang's avatar
QuanluZhang committed
589
590
                    this.trialJobs.delete(trialJobId);
                    finishedTrialJobNum++;
591
                    hyperParams = trialJobDetail.form.hyperParameters.value;
QuanluZhang's avatar
QuanluZhang committed
592
                    this.dispatcher.sendCommand(TRIAL_END, JSON.stringify({
chicm-ms's avatar
chicm-ms committed
593
                        trial_job_id: trialJobDetail.id, // eslint-disable-line @typescript-eslint/camelcase
QuanluZhang's avatar
QuanluZhang committed
594
                        event: trialJobDetail.status,
chicm-ms's avatar
chicm-ms committed
595
                        hyper_params: hyperParams // eslint-disable-line @typescript-eslint/camelcase
goooxu's avatar
goooxu committed
596
                    }));
QuanluZhang's avatar
QuanluZhang committed
597
598
599
600
601
602
603
                    break;
                case 'FAILED':
                case 'SYS_CANCELED':
                    // In the current version, we do not retry
                    // TO DO: push this job to queue for retry
                    this.trialJobs.delete(trialJobId);
                    finishedTrialJobNum++;
604
                    hyperParams = trialJobDetail.form.hyperParameters.value;
QuanluZhang's avatar
QuanluZhang committed
605
                    this.dispatcher.sendCommand(TRIAL_END, JSON.stringify({
chicm-ms's avatar
chicm-ms committed
606
                        trial_job_id: trialJobDetail.id, // eslint-disable-line @typescript-eslint/camelcase
QuanluZhang's avatar
QuanluZhang committed
607
                        event: trialJobDetail.status,
chicm-ms's avatar
chicm-ms committed
608
                        hyper_params: hyperParams // eslint-disable-line @typescript-eslint/camelcase
goooxu's avatar
goooxu committed
609
                    }));
QuanluZhang's avatar
QuanluZhang committed
610
611
612
613
614
615
616
617
618
619
                    break;
                case 'WAITING':
                case 'RUNNING':
                case 'UNKNOWN':
                    // Do nothing
                    break;
                default:
                // TO DO: add warning in log
            }
        }
goooxu's avatar
goooxu committed
620

Gems Guo's avatar
Gems Guo committed
621
        return finishedTrialJobNum;
QuanluZhang's avatar
QuanluZhang committed
622
623
624
625
626
627
    }

    private async manageTrials(): Promise<void> {
        if (this.dispatcher === undefined) {
            throw new Error('Error: tuner has not been setup');
        }
QuanluZhang's avatar
QuanluZhang committed
628
        let allFinishedTrialJobNum: number = this.currSubmittedTrialNum;
QuanluZhang's avatar
QuanluZhang committed
629
        let waitSubmittedToFinish: number;
630
        while (!['ERROR', 'STOPPING', 'STOPPED'].includes(this.status.status)) {
631
632
            await this.stopTrialIfOverMaxDurationLimit();

QuanluZhang's avatar
QuanluZhang committed
633
634
635
636
637
638
639
            const finishedTrialJobNum: number = await this.requestTrialJobsStatus();
            allFinishedTrialJobNum += finishedTrialJobNum;

            // requestTrialNum is the number of trials that will be requested from tuner.
            // If trialConcurrency does not change, requestTrialNum equals finishedTrialJobNum.
            // If trialConcurrency changes, for example, trialConcurrency increases by 2 (trialConcurrencyChange=2), then
            // requestTrialNum equals 2 + finishedTrialJobNum and trialConcurrencyChange becomes 0.
640
            // If trialConcurrency changes, for example, trialConcurrency decreases by 4 (trialConcurrencyChange=-4) and
QuanluZhang's avatar
QuanluZhang committed
641
642
643
644
645
646
647
648
            // finishedTrialJobNum is 2, then requestTrialNum becomes -2. No trial will be requested from tuner,
            // and trialConcurrencyChange becomes -2.
            const requestTrialNum: number = this.trialConcurrencyChange + finishedTrialJobNum;
            if (requestTrialNum >= 0) {
                this.trialConcurrencyChange = 0;
            } else {
                this.trialConcurrencyChange = requestTrialNum;
            }
chicm-ms's avatar
chicm-ms committed
649

QuanluZhang's avatar
QuanluZhang committed
650
            // check maxtrialnum and maxduration here
651
            // NO_MORE_TRIAL is more like a subset of RUNNING, because during RUNNING tuner
652
            // might tell nnimanager that this is no more trials. In NO_MORE_TRIAL state, the experiment is viewed
653
654
            // as still running. DONE could be transfered from RUNNING or NO_MORE_TRIAL.
            assert(this.status.status === 'RUNNING' ||
655
                this.status.status === 'DONE' ||
QuanluZhang's avatar
QuanluZhang committed
656
                this.status.status === 'NO_MORE_TRIAL' ||
657
                this.status.status === 'TUNER_NO_MORE_TRIAL', `Actual status: ${this.status.status}`);
658
659
            if (this.experimentProfile.execDuration > this.maxDuration ||
                this.currSubmittedTrialNum >= this.maxTrialNum) {
QuanluZhang's avatar
QuanluZhang committed
660
                if (this.status.status !== 'DONE') {
chicm-ms's avatar
chicm-ms committed
661
                    this.setStatus('NO_MORE_TRIAL');
QuanluZhang's avatar
QuanluZhang committed
662
663
664
665
                    waitSubmittedToFinish = this.currSubmittedTrialNum;

                    assert(allFinishedTrialJobNum <= waitSubmittedToFinish);
                    if (allFinishedTrialJobNum >= waitSubmittedToFinish) {
chicm-ms's avatar
chicm-ms committed
666
                        this.setStatus('DONE');
667
                        this.setEndtime();
QuanluZhang's avatar
QuanluZhang committed
668
669
670
671
                        await this.storeExperimentProfile();
                        // write this log for travis CI
                        this.log.info('Experiment done.');
                    }
QuanluZhang's avatar
QuanluZhang committed
672
673
                }
            } else {
QuanluZhang's avatar
QuanluZhang committed
674
675
                this.requestTrialJobs(requestTrialNum);

QuanluZhang's avatar
QuanluZhang committed
676
                if (this.status.status === 'DONE') {
677
678
                    delete this.experimentProfile.endTime;
                    await this.storeExperimentProfile();
QuanluZhang's avatar
QuanluZhang committed
679
                }
QuanluZhang's avatar
QuanluZhang committed
680
                if (this.status.status !== 'TUNER_NO_MORE_TRIAL') {
chicm-ms's avatar
chicm-ms committed
681
                    this.setStatus('RUNNING');
682
                }
QuanluZhang's avatar
QuanluZhang committed
683
684
                for (let i: number = this.trialJobs.size; i < this.experimentProfile.params.trialConcurrency; i++) {
                    if (this.waitingTrials.length === 0 ||
685
                        this.currSubmittedTrialNum >= this.maxTrialNum) {
QuanluZhang's avatar
QuanluZhang committed
686
687
                        break;
                    }
688
                    const form = this.waitingTrials.shift() as TrialJobApplicationForm;
QuanluZhang's avatar
QuanluZhang committed
689
                    this.currSubmittedTrialNum++;
liuzhe-lz's avatar
liuzhe-lz committed
690
                    this.log.info('submitTrialJob: form:', form);
691
                    const trialJobDetail: TrialJobDetail = await this.trainingService.submitTrialJob(form);
692
                    const Snapshot: TrialJobDetail = Object.assign({}, trialJobDetail);
693
                    await this.storeExperimentProfile();
694
                    this.trialJobs.set(trialJobDetail.id, Snapshot);
QuanluZhang's avatar
QuanluZhang committed
695
696
697
                    const trialJobDetailSnapshot: TrialJobDetail | undefined = this.trialJobs.get(trialJobDetail.id);
                    if (trialJobDetailSnapshot != undefined) {
                        await this.dataStore.storeTrialJobEvent(
698
                            trialJobDetailSnapshot.status, trialJobDetailSnapshot.id, form.hyperParameters.value, trialJobDetailSnapshot);
QuanluZhang's avatar
QuanluZhang committed
699
700
701
702
703
704
705
706
707
                    } else {
                        assert(false, `undefined trialJobDetail in trialJobs: ${trialJobDetail.id}`);
                    }
                }
            }
            await delay(1000 * 5); // 5 seconds
        }
    }

Deshui Yu's avatar
Deshui Yu committed
708
709
710
711
712
713
    private storeExperimentProfile(): Promise<void> {
        this.experimentProfile.revision += 1;

        return this.dataStore.storeExperimentProfile(this.experimentProfile);
    }

714
    private async run(): Promise<void> {
QuanluZhang's avatar
QuanluZhang committed
715
        assert(this.dispatcher !== undefined);
716
        await this.dispatcher.init();
717
718
719
720
721
722
723

        this.addEventListeners();

        this.sendInitTunerCommands();

        await Promise.all([
            this.periodicallyUpdateExecDuration(),
chicm-ms's avatar
chicm-ms committed
724
            this.pingDispatcher().catch((err: Error) => {
chicm-ms's avatar
chicm-ms committed
725
                throw NNIError.FromError(err, 'Dispatcher error: ');
chicm-ms's avatar
chicm-ms committed
726
            }),
727
            this.trainingService.run().catch((err: Error) => {
chicm-ms's avatar
chicm-ms committed
728
                throw NNIError.FromError(err, 'Training service error: ');
729
            }),
QuanluZhang's avatar
QuanluZhang committed
730
            this.manageTrials().catch((err: Error) => {
chicm-ms's avatar
chicm-ms committed
731
                throw NNIError.FromError(err, 'Job management error: ');
732
            })]);
733
734
    }

QuanluZhang's avatar
QuanluZhang committed
735
    private addEventListeners(): void {
chicm-ms's avatar
chicm-ms committed
736
        this.log.info('Add event listeners');
737
        // TO DO: cannot run this method more than once in one NNIManager instance
QuanluZhang's avatar
QuanluZhang committed
738
        if (this.dispatcher === undefined) {
739
740
            throw new Error('Error: tuner or job maintainer have not been setup');
        }
741
        this.trainingService.addTrialJobMetricListener(this.trialJobMetricListener);
742
743
744

        this.dispatcher.onCommand((commandType: string, content: string) => {
            this.onTunerCommand(commandType, content).catch((err: Error) => {
chicm-ms's avatar
chicm-ms committed
745
                this.criticalError(NNIError.FromError(err, 'Tuner command event error: '));
746
747
            });
        });
748
749
750
751
        this.dispatcher.onError((error: Error) => {
            this.log.error(`Dispatcher error: ${error.message}`);
            this.criticalError(new Error('Dispatcher stream error, tuner may have crashed.'));
        });
752
753
754
755
    }

    private sendInitTunerCommands(): void {
        if (this.dispatcher === undefined) {
756
            throw new Error('Dispatcher error: tuner has not been setup');
757
        }
chicm-ms's avatar
chicm-ms committed
758
759
        this.log.debug(`Send tuner command: INITIALIZE: ${this.experimentProfile.params.searchSpace}`);
        // Tuner need to be initialized with search space before generating any hyper parameters
760
        this.dispatcher.sendCommand(INITIALIZE, JSON.stringify(this.experimentProfile.params.searchSpace));
761
762
763
    }

    private async onTrialJobMetrics(metric: TrialJobMetric): Promise<void> {
liuzhe-lz's avatar
liuzhe-lz committed
764
        this.log.debug('NNIManager received trial job metrics:', metric);
765
        if (this.trialJobs.has(metric.id)) {
766
767
768
769
770
771
            await this.dataStore.storeMetricData(metric.id, metric.data);
            if (this.dispatcher === undefined) {
                throw new Error('Error: tuner has not been setup');
            }
            this.dispatcher.sendCommand(REPORT_METRIC_DATA, metric.data);
        } else {
liuzhe-lz's avatar
liuzhe-lz committed
772
            this.log.warning('NNIManager received non-existent trial job metrics:', metric);
773
774
775
        }
    }

chicm-ms's avatar
chicm-ms committed
776
777
778
779
780
781
782
    private requestTrialJobs(jobNum: number): void {
        if (jobNum < 1) {
            return;
        }
        if (this.dispatcher === undefined) {
            throw new Error('Dispatcher error: tuner has not been setup');
        }
783
        if (this.config.deprecated && this.config.deprecated.multiThread) {
chicm-ms's avatar
chicm-ms committed
784
785
786
787
788
789
790
791
792
793
794
            // Send multiple requests to ensure multiple hyper parameters are generated in non-blocking way.
            // For a single REQUEST_TRIAL_JOBS request, hyper parameters are generated one by one
            // sequentially.
            for (let i: number = 0; i < jobNum; i++) {
                this.dispatcher.sendCommand(REQUEST_TRIAL_JOBS, '1');
            }
        } else {
            this.dispatcher.sendCommand(REQUEST_TRIAL_JOBS, String(jobNum));
        }
    }

795
    private async onTunerCommand(commandType: string, content: string): Promise<void> {
horizon365's avatar
horizon365 committed
796
        this.log.info(`NNIManager received command from dispatcher: ${commandType}, ${content}`);
797
        switch (commandType) {
chicm-ms's avatar
chicm-ms committed
798
            case INITIALIZED: {
chicm-ms's avatar
chicm-ms committed
799
                // Tuner is intialized, search space is set, request tuner to generate hyper parameters
800
801
802
803
804
805
                if (this.trialDataForTuner.length > 0) {
                    if (this.dispatcher === undefined) {
                        throw new Error('Dispatcher error: tuner has not been setup');
                    }
                    this.dispatcher.sendCommand(IMPORT_DATA, this.trialDataForTuner);
                }
chicm-ms's avatar
chicm-ms committed
806
807
                this.requestTrialJobs(this.experimentProfile.params.trialConcurrency);
                break;
chicm-ms's avatar
chicm-ms committed
808
809
            }
            case NEW_TRIAL_JOB: {
QuanluZhang's avatar
QuanluZhang committed
810
                if (this.status.status === 'TUNER_NO_MORE_TRIAL') {
811
                    this.log.warning('It is not supposed to receive more trials after NO_MORE_TRIAL is set');
chicm-ms's avatar
chicm-ms committed
812
                    this.setStatus('RUNNING');
813
                }
814
                const trialRequestContent: TrialCommandContent = JSON.parse(content);
QuanluZhang's avatar
QuanluZhang committed
815
                const noneConstraint: PlacementConstraint = {type: 'None', gpus: []};
816
817
818
819
820
                const form: TrialJobApplicationForm = {
                    sequenceId: this.experimentProfile.nextSequenceId++,
                    hyperParameters: {
                        value: content,
                        index: 0
821
                    },
QuanluZhang's avatar
QuanluZhang committed
822
                    placementConstraint: trialRequestContent.placement_constraint? trialRequestContent.placement_constraint : noneConstraint
823
824
                };
                this.waitingTrials.push(form);
825
                break;
chicm-ms's avatar
chicm-ms committed
826
827
            }
            case SEND_TRIAL_JOB_PARAMETER: {
chicm-ms's avatar
chicm-ms committed
828
829
830
831
832
                const tunerCommand: any = JSON.parse(content);
                assert(tunerCommand.parameter_index >= 0);
                assert(tunerCommand.trial_job_id !== undefined);

                const trialJobForm: TrialJobApplicationForm = {
833
                    sequenceId: -1,  // FIXME: multi-phase tuner should use sequence ID instead of trial job ID
chicm-ms's avatar
chicm-ms committed
834
835
836
837
838
                    hyperParameters: {
                        value: content,
                        index: tunerCommand.parameter_index
                    }
                };
liuzhe-lz's avatar
liuzhe-lz committed
839
                this.log.info('updateTrialJob: job id:', tunerCommand.trial_job_id, 'form:', trialJobForm);
chicm-ms's avatar
chicm-ms committed
840
                await this.trainingService.updateTrialJob(tunerCommand.trial_job_id, trialJobForm);
841
842
843
844
845
                if (tunerCommand['parameters'] !== null) {
                    // parameters field is set as empty string if no more hyper parameter can be generated by tuner.
                    await this.dataStore.storeTrialJobEvent(
                        'ADD_HYPERPARAMETER', tunerCommand.trial_job_id, content, undefined);
                }
chicm-ms's avatar
chicm-ms committed
846
                break;
chicm-ms's avatar
chicm-ms committed
847
848
            }
            case NO_MORE_TRIAL_JOBS: {
849
850
851
                if (!['ERROR', 'STOPPING', 'STOPPED'].includes(this.status.status)) {
                    this.setStatus('TUNER_NO_MORE_TRIAL');
                }
852
                break;
chicm-ms's avatar
chicm-ms committed
853
854
            }
            case KILL_TRIAL_JOB: {
liuzhe-lz's avatar
liuzhe-lz committed
855
                this.log.info('cancelTrialJob:', content);
QuanluZhang's avatar
QuanluZhang committed
856
                await this.trainingService.cancelTrialJob(JSON.parse(content), true);
857
                break;
chicm-ms's avatar
chicm-ms committed
858
            }
859
860
861
            default:
                throw new Error('Error: unsupported command type from tuner');
        }
Deshui Yu's avatar
Deshui Yu committed
862
863
    }

864
865
866
867
868
869
870
871
872
873
    private criticalError(err: Error): void {
        this.logError(err);
        console.error(err);
    }

    private logError(err: Error): void {
        if (err.stack !== undefined) {
            this.log.error(err.stack);
        }
        this.status.errors.push(err.message);
874
        this.setEndtime();
chicm-ms's avatar
chicm-ms committed
875
876
877
878
879
880
881
        this.setStatus('ERROR');
    }

    private setStatus(status: ExperimentStatus): void {
        if (status !== this.status.status) {
            this.log.info(`Change NNIManager status from: ${this.status.status} to: ${status}`);
            this.status.status = status;
882
            getExperimentsManager().setExperimentInfo(this.experimentProfile.id, 'status', this.status.status);
chicm-ms's avatar
chicm-ms committed
883
        }
884
885
    }

886
887
    private setEndtime(): void {
        this.experimentProfile.endTime = Date.now();
888
        getExperimentsManager().setExperimentInfo(this.experimentProfile.id, 'endTime', this.experimentProfile.endTime);
889
890
    }

QuanluZhang's avatar
QuanluZhang committed
891
892
893
894
    private async createCheckpointDir(): Promise<string> {
        // TODO: test
        const chkpDir: string = getCheckpointDir();
        await mkDirP(chkpDir);
895
        return chkpDir;
QuanluZhang's avatar
QuanluZhang committed
896
    }
J-shang's avatar
J-shang committed
897
898
899
900
901
902
903
904

    public async getTrialOutputLocalPath(trialJobId: string): Promise<string> {
        return this.trainingService.getTrialOutputLocalPath(trialJobId);
    }

    public async fetchTrialOutput(trialJobId: string, subpath: string): Promise<void> {
        return this.trainingService.fetchTrialOutput(trialJobId, subpath);
    }
Deshui Yu's avatar
Deshui Yu committed
905
906
907
}

export { NNIManager };