From 39085789cb7dcdd75e3ac2dbb9daadab02909269 Mon Sep 17 00:00:00 2001 From: chicm-ms <38930155+chicm-ms@users.noreply.github.com> Date: Mon, 8 Oct 2018 19:21:51 +0800 Subject: [PATCH 1/9] Multi-phase training service (#148) * Dev enas - multi-phase hyper parameters support (#96) * Multi-phase support * Updates * Updates * updates * updates * updates * Merge master to dev-enas (#117) * Multi-phase support * update document (#92) * Edit readme.md * updated a word * Update GetStarted.md * Update GetStarted.md * refact readme, getstarted and write your trial md. * Update README.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Fix nnictl bugs and add new feature (#75) * fix nnictl bug * fix nnictl create bug * add experiment status logic * add more information for nnictl * fix Evolution Tuner bug * refactor code * fix code in updater.py * fix nnictl --help * fix classArgs bug * update check response.status_code logic * Updates * remove Buffer warning (#100) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * Updates * updates * updates * updates * Add support for debugging mode * fix setup.py (#115) * Add DAG model configuration format for SQuAD example. * Explain config format for SQuAD QA model. * Add more detailed introduction about the evolution algorithm. * Merge master to dev-enas (#118) * update document (#92) * Edit readme.md * updated a word * Update GetStarted.md * Update GetStarted.md * refact readme, getstarted and write your trial md. * Update README.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Update WriteYourTrial.md * Fix nnictl bugs and add new feature (#75) * fix nnictl bug * fix nnictl create bug * add experiment status logic * add more information for nnictl * fix Evolution Tuner bug * refactor code * fix code in updater.py * fix nnictl --help * fix classArgs bug * update check response.status_code logic * remove Buffer warning (#100) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * Add support for debugging mode * fix setup.py (#115) * Add DAG model configuration format for SQuAD example. * Explain config format for SQuAD QA model. * Add more detailed introduction about the evolution algorithm. * Fix install.sh add add trial log path (#109) * fix nnictl bug * fix nnictl create bug * add experiment status logic * add more information for nnictl * fix Evolution Tuner bug * refactor code * fix code in updater.py * fix nnictl --help * fix classArgs bug * update check response.status_code logic * show trial log path * update document * fix install.sh * set default vallue for maxTrialNum and maxExecDuration * fix nnictl * support multiPhase (#127) * fix nnictl bug * support multiPhase * Fix multiphase datastore problem (#125) * Fix multiphase datastore problem * updates * updates * updates * updates * Pull latest code (#2) * webui logpath and document (#135) * Add webui document and logpath as a href * fix tslint * fix comments by Chengmin * Pai training service bug fix and enhancement (#136) * Add NNI installation scripts * Update pai script, update NNI_out_dir * Update NNI dir in nni sdk local.py * Create .nni folder in nni sdk local.py * Add check before creating .nni folder * Fix typo for PAI_INSTALL_NNI_SHELL_FORMAT * Improve annotation (#138) * Improve annotation * Minor bugfix * Selectively install through pip (#139) Selectively install through pip * update setup.py * fix paiTrainingService bugs (#137) * fix nnictl bug * add hdfs host validation * fix bugs * fix dockerfile * fix install.sh * update install.sh * fix dockerfile * Set timeout for HDFSUtility exists function * remove unused TODO * fix sdk * add optional for outputDir and dataDir * refactor dockerfile.base * Remove unused import in hdfsclientUtility * Add documentation for NNI PAI mode experiment (#141) * Add documentation for NNI PAI mode * Fix typo based on PR comments * Exit with subprocess return code of trial keeper * Remove additional exit code * Fix typo based on PR comments * update doc for smac tuner (#140) * Revert "Selectively install through pip (#139)" due to potential pip install issue (#142) * Revert "Selectively install through pip (#139)" This reverts commit 1d174836d3146a0363e9c9c88094bf9cff865faa. * Add exit code of subprocess for trial_keeper * Update README, add link to PAImode doc * fix bug (#147) * Refactor nnictl and add config_pai.yml (#144) * fix nnictl bug * add hdfs host validation * fix bugs * fix dockerfile * fix install.sh * update install.sh * fix dockerfile * Set timeout for HDFSUtility exists function * remove unused TODO * fix sdk * add optional for outputDir and dataDir * refactor dockerfile.base * Remove unused import in hdfsclientUtility * add config_pai.yml * refactor nnictl create logic and add colorful print * fix nnictl stop logic * add annotation for config_pai.yml * add document for start experiment * fix config.yml * fix document * Fix trial keeper wrongly exit issue (#152) * Fix trial keeper bug, use actual exitcode to exit rather than 1 * Fix bug of table sort (#145) * Update doc for PAIMode and v0.2 release notes (#153) * Update v0.2 documentation regards to release note and PAI training service * Update document to describe NNI docker image * Bug fix for SQuAD example tuner. (#134) * Update Makefile (#151) * test * update setup.py * update Makefile and install.sh * rever setup.py * change color * update doc * update doc * fix auto-completion's extra space * update Makefile * update webui * Update doc image (#163) * update doc * trivial * trivial * trivial * trivial * trivial * trivial * update image * update image size * Update ga squad (#104) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * sklearn examples (#169) * fix nnictl bug * fix install.sh * add sklearn-regression example * add sklearn classification * update sklearn * update example * remove additional code * Update batch tuner (#158) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * update batch tuner * Quickly fix cascading search space bug in tuner (#156) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * quickly fix cascading searchspace bug in tuner * Add iterative search space example (#119) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * add iterative search space example * update * update readme * change name * updates * updates * Updates CI * updates --- .travis.yml | 2 +- src/nni_manager/common/datastore.ts | 6 +- src/nni_manager/common/manager.ts | 1 + src/nni_manager/common/trainingService.ts | 9 +- src/nni_manager/common/utils.ts | 5 +- src/nni_manager/core/commands.ts | 5 +- src/nni_manager/core/nniDataStore.ts | 62 +++++- src/nni_manager/core/nnimanager.ts | 27 ++- .../rest_server/restValidationSchemas.ts | 1 + .../local/localTrainingService.ts | 27 ++- .../remoteMachineTrainingService.ts | 44 ++++- .../test/remoteMachineTrainingService.test.ts | 10 +- src/sdk/pynni/nni/__main__.py | 8 +- src/sdk/pynni/nni/multi_phase/__init__.py | 0 .../nni/multi_phase/multi_phase_dispatcher.py | 178 ++++++++++++++++++ .../nni/multi_phase/multi_phase_tuner.py | 87 +++++++++ src/sdk/pynni/nni/platform/local.py | 29 ++- src/sdk/pynni/nni/protocol.py | 3 +- src/sdk/pynni/nni/trial.py | 6 +- src/sdk/pynni/tests/test_multi_phase_tuner.py | 89 +++++++++ test/naive/run.py | 2 +- tools/nnicmd/config_schema.py | 1 + tools/nnicmd/launcher.py | 4 + 23 files changed, 559 insertions(+), 47 deletions(-) create mode 100644 src/sdk/pynni/nni/multi_phase/__init__.py create mode 100644 src/sdk/pynni/nni/multi_phase/multi_phase_dispatcher.py create mode 100644 src/sdk/pynni/nni/multi_phase/multi_phase_tuner.py create mode 100644 src/sdk/pynni/tests/test_multi_phase_tuner.py diff --git a/.travis.yml b/.travis.yml index a9f4a1d734..04052b8b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ before_install: - sudo sh -c 'PATH=/usr/local/node/bin:$PATH yarn global add serve' install: - make - - make install + - make easy-install - export PATH=$HOME/.nni/bin:$PATH before_script: - cd test/naive diff --git a/src/nni_manager/common/datastore.ts b/src/nni_manager/common/datastore.ts index b86b0a95fe..d3cca0479b 100644 --- a/src/nni_manager/common/datastore.ts +++ b/src/nni_manager/common/datastore.ts @@ -22,8 +22,8 @@ import { ExperimentProfile, TrialJobStatistics } from './manager'; import { TrialJobDetail, TrialJobStatus } from './trainingService'; -type TrialJobEvent = TrialJobStatus | 'USER_TO_CANCEL' | 'ADD_CUSTOMIZED'; -type MetricType = 'PERIODICAL' | 'FINAL' | 'CUSTOM'; +type TrialJobEvent = TrialJobStatus | 'USER_TO_CANCEL' | 'ADD_CUSTOMIZED' | 'ADD_HYPERPARAMETER'; +type MetricType = 'PERIODICAL' | 'FINAL' | 'CUSTOM' | 'REQUEST_PARAMETER'; interface ExperimentProfileRecord { readonly timestamp: number; @@ -62,7 +62,7 @@ interface TrialJobInfo { status: TrialJobStatus; startTime?: number; endTime?: number; - hyperParameters?: string; + hyperParameters?: string[]; logPath?: string; finalMetricData?: string; stderrPath?: string; diff --git a/src/nni_manager/common/manager.ts b/src/nni_manager/common/manager.ts index 10fb9a4227..1d02a1775d 100644 --- a/src/nni_manager/common/manager.ts +++ b/src/nni_manager/common/manager.ts @@ -31,6 +31,7 @@ interface ExperimentParams { maxExecDuration: number; //seconds maxTrialNum: number; searchSpace: string; + multiPhase?: boolean; tuner: { className: string; builtinTunerName?: string; diff --git a/src/nni_manager/common/trainingService.ts b/src/nni_manager/common/trainingService.ts index 0b8708394c..7bcc575c34 100644 --- a/src/nni_manager/common/trainingService.ts +++ b/src/nni_manager/common/trainingService.ts @@ -37,11 +37,16 @@ interface JobApplicationForm { readonly jobType: JobType; } +interface HyperParameters { + readonly value: string; + readonly index: number; +} + /** * define TrialJobApplicationForm */ interface TrialJobApplicationForm extends JobApplicationForm { - readonly hyperParameters: string; + readonly hyperParameters: HyperParameters; } /** @@ -116,6 +121,6 @@ abstract class TrainingService { export { TrainingService, TrainingServiceError, TrialJobStatus, TrialJobApplicationForm, - TrainingServiceMetadata, TrialJobDetail, TrialJobMetric, + TrainingServiceMetadata, TrialJobDetail, TrialJobMetric, HyperParameters, HostJobApplicationForm, JobApplicationForm, JobType }; diff --git a/src/nni_manager/common/utils.ts b/src/nni_manager/common/utils.ts index e83e40e919..20609598a0 100644 --- a/src/nni_manager/common/utils.ts +++ b/src/nni_manager/common/utils.ts @@ -158,8 +158,11 @@ function parseArg(names: string[]): string { * @param assessor: similiar as tuner * */ -function getMsgDispatcherCommand(tuner: any, assessor: any): string { +function getMsgDispatcherCommand(tuner: any, assessor: any, multiPhase: boolean = false): string { let command: string = `python3 -m nni --tuner_class_name ${tuner.className}`; + if (multiPhase) { + command += ' --multi_phase'; + } if (tuner.classArgs !== undefined) { command += ` --tuner_args ${JSON.stringify(JSON.stringify(tuner.classArgs))}`; diff --git a/src/nni_manager/core/commands.ts b/src/nni_manager/core/commands.ts index 37bba31d9e..19204b2f31 100644 --- a/src/nni_manager/core/commands.ts +++ b/src/nni_manager/core/commands.ts @@ -27,6 +27,7 @@ const TRIAL_END = 'EN'; const TERMINATE = 'TE'; const NEW_TRIAL_JOB = 'TR'; +const SEND_TRIAL_JOB_PARAMETER = 'SP'; const NO_MORE_TRIAL_JOBS = 'NO'; const KILL_TRIAL_JOB = 'KI'; @@ -39,6 +40,7 @@ const TUNER_COMMANDS: Set = new Set([ TERMINATE, NEW_TRIAL_JOB, + SEND_TRIAL_JOB_PARAMETER, NO_MORE_TRIAL_JOBS ]); @@ -63,5 +65,6 @@ export { NO_MORE_TRIAL_JOBS, KILL_TRIAL_JOB, TUNER_COMMANDS, - ASSESSOR_COMMANDS + ASSESSOR_COMMANDS, + SEND_TRIAL_JOB_PARAMETER }; diff --git a/src/nni_manager/core/nniDataStore.ts b/src/nni_manager/core/nniDataStore.ts index 790a5680c0..81afeb3af3 100644 --- a/src/nni_manager/core/nniDataStore.ts +++ b/src/nni_manager/core/nniDataStore.ts @@ -26,6 +26,7 @@ import * as component from '../common/component'; import { Database, DataStore, MetricData, MetricDataRecord, MetricType, TrialJobEvent, TrialJobEventRecord, TrialJobInfo } from '../common/datastore'; import { isNewExperiment } from '../common/experimentStartupInfo'; +import { getExperimentId } from '../common/experimentStartupInfo'; import { getLogger, Logger } from '../common/log'; import { ExperimentProfile, TrialJobStatistics } from '../common/manager'; import { TrialJobStatus } from '../common/trainingService'; @@ -35,6 +36,7 @@ class NNIDataStore implements DataStore { private db: Database = component.get(Database); private log: Logger = getLogger(); private initTask!: Deferred; + private multiPhase: boolean | undefined; public init(): Promise { if (this.initTask !== undefined) { @@ -112,13 +114,19 @@ class NNIDataStore implements DataStore { } public async getTrialJob(trialJobId: string): Promise { - const trialJobs = await this.queryTrialJobs(undefined, trialJobId); + const trialJobs: TrialJobInfo[] = await this.queryTrialJobs(undefined, trialJobId); return trialJobs[0]; } public async storeMetricData(trialJobId: string, data: string): Promise { - const metrics = JSON.parse(data) as MetricData; + const metrics: MetricData = JSON.parse(data); + // REQUEST_PARAMETER is used to request new parameters for multiphase trial job, + // it is not metrics, so it is skipped here. + if (metrics.type === 'REQUEST_PARAMETER') { + + return; + } assert(trialJobId === metrics.trial_job_id); await this.db.storeMetricData(trialJobId, JSON.stringify({ trialJobId: metrics.trial_job_id, @@ -160,25 +168,56 @@ class NNIDataStore implements DataStore { private async getFinalMetricData(trialJobId: string): Promise { const metrics: MetricDataRecord[] = await this.getMetricData(trialJobId, 'FINAL'); - if (metrics.length > 1) { - this.log.error(`Found multiple final results for trial job: ${trialJobId}`); + + const multiPhase: boolean = await this.isMultiPhase(); + + if (metrics.length > 1 && !multiPhase) { + this.log.error(`Found multiple FINAL results for trial job ${trialJobId}`); + } + + return metrics[metrics.length - 1]; + } + + private async isMultiPhase(): Promise { + if (this.multiPhase === undefined) { + this.multiPhase = (await this.getExperimentProfile(getExperimentId())).params.multiPhase; } - return metrics[0]; + if (this.multiPhase !== undefined) { + return this.multiPhase; + } else { + return false; + } } - private getJobStatusByLatestEvent(event: TrialJobEvent): TrialJobStatus { + private getJobStatusByLatestEvent(oldStatus: TrialJobStatus, event: TrialJobEvent): TrialJobStatus { switch (event) { case 'USER_TO_CANCEL': return 'USER_CANCELED'; case 'ADD_CUSTOMIZED': return 'WAITING'; + case 'ADD_HYPERPARAMETER': + return oldStatus; default: } return event; } + private mergeHyperParameters(hyperParamList: string[], newParamStr: string): string[] { + const mergedHyperParams: any[] = []; + const newParam: any = JSON.parse(newParamStr); + for (const hyperParamStr of hyperParamList) { + const hyperParam: any = JSON.parse(hyperParamStr); + mergedHyperParams.push(hyperParam); + } + if (mergedHyperParams.filter((value: any) => value.parameter_index === newParam.parameter_index).length <= 0) { + mergedHyperParams.push(newParam); + } + + return mergedHyperParams.map((value: any) => { return JSON.stringify(value); }); + } + private getTrialJobsByReplayEvents(trialJobEvents: TrialJobEventRecord[]): Map { const map: Map = new Map(); // assume data is stored by time ASC order @@ -192,7 +231,8 @@ class NNIDataStore implements DataStore { } else { jobInfo = { id: record.trialJobId, - status: this.getJobStatusByLatestEvent(record.event) + status: this.getJobStatusByLatestEvent('UNKNOWN', record.event), + hyperParameters: [] }; } if (!jobInfo) { @@ -221,9 +261,13 @@ class NNIDataStore implements DataStore { } default: } - jobInfo.status = this.getJobStatusByLatestEvent(record.event); + jobInfo.status = this.getJobStatusByLatestEvent(jobInfo.status, record.event); if (record.data !== undefined && record.data.trim().length > 0) { - jobInfo.hyperParameters = record.data; + if (jobInfo.hyperParameters !== undefined) { + jobInfo.hyperParameters = this.mergeHyperParameters(jobInfo.hyperParameters, record.data); + } else { + assert(false, 'jobInfo.hyperParameters is undefined'); + } } map.set(record.trialJobId, jobInfo); } diff --git a/src/nni_manager/core/nnimanager.ts b/src/nni_manager/core/nnimanager.ts index f562598b11..c84688c038 100644 --- a/src/nni_manager/core/nnimanager.ts +++ b/src/nni_manager/core/nnimanager.ts @@ -37,7 +37,7 @@ import { import { delay , getLogDir, getMsgDispatcherCommand} from '../common/utils'; import { ADD_CUSTOMIZED_TRIAL_JOB, KILL_TRIAL_JOB, NEW_TRIAL_JOB, NO_MORE_TRIAL_JOBS, REPORT_METRIC_DATA, - REQUEST_TRIAL_JOBS, TERMINATE, TRIAL_END, UPDATE_SEARCH_SPACE + REQUEST_TRIAL_JOBS, SEND_TRIAL_JOB_PARAMETER, TERMINATE, TRIAL_END, UPDATE_SEARCH_SPACE } from './commands'; import { createDispatcherInterface, IpcInterface } from './ipcInterface'; import { TrialJobMaintainerEvent, TrialJobs } from './trialJobs'; @@ -116,7 +116,7 @@ class NNIManager implements Manager { await this.storeExperimentProfile(); this.log.debug('Setup tuner...'); - const dispatcherCommand: string = getMsgDispatcherCommand(expParams.tuner, expParams.assessor); + const dispatcherCommand: string = getMsgDispatcherCommand(expParams.tuner, expParams.assessor, expParams.multiPhase); console.log(`dispatcher command: ${dispatcherCommand}`); this.setupTuner( //expParams.tuner.tunerCommand, @@ -140,7 +140,7 @@ class NNIManager implements Manager { this.experimentProfile = await this.dataStore.getExperimentProfile(experimentId); const expParams: ExperimentParams = this.experimentProfile.params; - const dispatcherCommand: string = getMsgDispatcherCommand(expParams.tuner, expParams.assessor); + const dispatcherCommand: string = getMsgDispatcherCommand(expParams.tuner, expParams.assessor, expParams.multiPhase); console.log(`dispatcher command: ${dispatcherCommand}`); this.setupTuner( dispatcherCommand, @@ -462,7 +462,10 @@ class NNIManager implements Manager { this.currSubmittedTrialNum++; const trialJobAppForm: TrialJobApplicationForm = { jobType: 'TRIAL', - hyperParameters: content + hyperParameters: { + value: content, + index: 0 + } }; const trialJobDetail: TrialJobDetail = await this.trainingService.submitTrialJob(trialJobAppForm); this.trialJobsMaintainer.setTrialJob(trialJobDetail.id, Object.assign({}, trialJobDetail)); @@ -474,6 +477,22 @@ class NNIManager implements Manager { } } break; + case SEND_TRIAL_JOB_PARAMETER: + const tunerCommand: any = JSON.parse(content); + assert(tunerCommand.parameter_index >= 0); + assert(tunerCommand.trial_job_id !== undefined); + + const trialJobForm: TrialJobApplicationForm = { + jobType: 'TRIAL', + hyperParameters: { + value: content, + index: tunerCommand.parameter_index + } + }; + await this.trainingService.updateTrialJob(tunerCommand.trial_job_id, trialJobForm); + await this.dataStore.storeTrialJobEvent( + 'ADD_HYPERPARAMETER', tunerCommand.trial_job_id, content, undefined); + break; case NO_MORE_TRIAL_JOBS: this.trialJobsMaintainer.setNoMoreTrials(); break; diff --git a/src/nni_manager/rest_server/restValidationSchemas.ts b/src/nni_manager/rest_server/restValidationSchemas.ts index a94f9f11b8..01985cddb2 100644 --- a/src/nni_manager/rest_server/restValidationSchemas.ts +++ b/src/nni_manager/rest_server/restValidationSchemas.ts @@ -57,6 +57,7 @@ export namespace ValidationSchemas { trialConcurrency: joi.number().min(0).required(), searchSpace: joi.string().required(), maxExecDuration: joi.number().min(0).required(), + multiPhase: joi.boolean(), tuner: joi.object({ builtinTunerName: joi.string().valid('TPE', 'Random', 'Anneal', 'Evolution', 'SMAC', 'BatchTuner'), codeDir: joi.string(), diff --git a/src/nni_manager/training_service/local/localTrainingService.ts b/src/nni_manager/training_service/local/localTrainingService.ts index 50eb9b3846..b66dd8d68c 100644 --- a/src/nni_manager/training_service/local/localTrainingService.ts +++ b/src/nni_manager/training_service/local/localTrainingService.ts @@ -30,10 +30,11 @@ import { getLogger, Logger } from '../../common/log'; import { TrialConfig } from '../common/trialConfig'; import { TrialConfigMetadataKey } from '../common/trialConfigMetadataKey'; import { - HostJobApplicationForm, JobApplicationForm, TrainingService, TrialJobApplicationForm, + HostJobApplicationForm, JobApplicationForm, HyperParameters, TrainingService, TrialJobApplicationForm, TrialJobDetail, TrialJobMetric, TrialJobStatus } from '../../common/trainingService'; import { delay, getExperimentRootDir, uniqueString } from '../../common/utils'; +import { file } from 'tmp'; const tkill = require('tree-kill'); @@ -210,8 +211,18 @@ class LocalTrainingService implements TrainingService { * @param trialJobId trial job id * @param form job application form */ - public updateTrialJob(trialJobId: string, form: JobApplicationForm): Promise { - throw new MethodNotImplementedError(); + public async updateTrialJob(trialJobId: string, form: JobApplicationForm): Promise { + const trialJobDetail: undefined | TrialJobDetail = this.jobMap.get(trialJobId); + if (trialJobDetail === undefined) { + throw new Error(`updateTrialJob failed: ${trialJobId} not found`); + } + if (form.jobType === 'TRIAL') { + await this.writeParameterFile(trialJobDetail.workingDirectory, (form).hyperParameters); + } else { + throw new Error(`updateTrialJob failed: jobType ${form.jobType} not supported.`); + } + + return trialJobDetail; } /** @@ -332,10 +343,7 @@ class LocalTrainingService implements TrainingService { await cpp.exec(`mkdir -p ${path.join(trialJobDetail.workingDirectory, '.nni')}`); await cpp.exec(`touch ${path.join(trialJobDetail.workingDirectory, '.nni', 'metrics')}`); await fs.promises.writeFile(path.join(trialJobDetail.workingDirectory, 'run.sh'), runScriptLines.join('\n'), { encoding: 'utf8' }); - await fs.promises.writeFile( - path.join(trialJobDetail.workingDirectory, 'parameter.cfg'), - (trialJobDetail.form).hyperParameters, - { encoding: 'utf8' }); + await this.writeParameterFile(trialJobDetail.workingDirectory, (trialJobDetail.form).hyperParameters); const process: cp.ChildProcess = cp.exec(`bash ${path.join(trialJobDetail.workingDirectory, 'run.sh')}`); this.setTrialJobStatus(trialJobDetail, 'RUNNING'); @@ -402,6 +410,11 @@ class LocalTrainingService implements TrainingService { } } } + + private async writeParameterFile(directory: string, hyperParameters: HyperParameters): Promise { + const filepath: string = path.join(directory, `parameter_${hyperParameters.index}.cfg`); + await fs.promises.writeFile(filepath, hyperParameters.value, { encoding: 'utf8' }); + } } export { LocalTrainingService }; diff --git a/src/nni_manager/training_service/remote_machine/remoteMachineTrainingService.ts b/src/nni_manager/training_service/remote_machine/remoteMachineTrainingService.ts index e1cff16f22..a4be7a1b0d 100644 --- a/src/nni_manager/training_service/remote_machine/remoteMachineTrainingService.ts +++ b/src/nni_manager/training_service/remote_machine/remoteMachineTrainingService.ts @@ -34,7 +34,7 @@ import { getExperimentId } from '../../common/experimentStartupInfo'; import { getLogger, Logger } from '../../common/log'; import { ObservableTimer } from '../../common/observableTimer'; import { - HostJobApplicationForm, JobApplicationForm, TrainingService, TrialJobApplicationForm, TrialJobDetail, TrialJobMetric + HostJobApplicationForm, HyperParameters, JobApplicationForm, TrainingService, TrialJobApplicationForm, TrialJobDetail, TrialJobMetric } from '../../common/trainingService'; import { delay, getExperimentRootDir, uniqueString } from '../../common/utils'; import { GPUSummary } from '../common/gpuData'; @@ -198,8 +198,24 @@ class RemoteMachineTrainingService implements TrainingService { * @param trialJobId trial job id * @param form job application form */ - public updateTrialJob(trialJobId: string, form: JobApplicationForm): Promise { - throw new MethodNotImplementedError(); + public async updateTrialJob(trialJobId: string, form: JobApplicationForm): Promise { + this.log.info(`updateTrialJob: form: ${JSON.stringify(form)}`); + const trialJobDetail: undefined | TrialJobDetail = this.trialJobsMap.get(trialJobId); + if (trialJobDetail === undefined) { + throw new Error(`updateTrialJob failed: ${trialJobId} not found`); + } + if (form.jobType === 'TRIAL') { + const rmMeta: RemoteMachineMeta | undefined = (trialJobDetail).rmMeta; + if (rmMeta !== undefined) { + await this.writeParameterFile(trialJobId, (form).hyperParameters, rmMeta); + } else { + throw new Error(`updateTrialJob failed: ${trialJobId} rmMeta not found`); + } + } else { + throw new Error(`updateTrialJob failed: jobType ${form.jobType} not supported.`); + } + + return trialJobDetail; } /** @@ -442,15 +458,13 @@ class RemoteMachineTrainingService implements TrainingService { //create tmp trial working folder locally. await cpp.exec(`mkdir -p ${trialLocalTempFolder}`); - // Write file content ( run.sh and parameter.cfg ) to local tmp files + // Write file content ( run.sh and parameter_0.cfg ) to local tmp files await fs.promises.writeFile(path.join(trialLocalTempFolder, 'run.sh'), runScriptContent, { encoding: 'utf8' }); - await fs.promises.writeFile(path.join(trialLocalTempFolder, 'parameter.cfg'), form.hyperParameters, { encoding: 'utf8' }); // Copy local tmp files to remote machine await SSHClientUtility.copyFileToRemote( path.join(trialLocalTempFolder, 'run.sh'), path.join(trialWorkingFolder, 'run.sh'), sshClient); - await SSHClientUtility.copyFileToRemote( - path.join(trialLocalTempFolder, 'parameter.cfg'), path.join(trialWorkingFolder, 'parameter.cfg'), sshClient); + await this.writeParameterFile(trialJobId, form.hyperParameters, rmScheduleInfo.rmMeta); // Copy files in codeDir to remote working directory await SSHClientUtility.copyDirectoryToRemote(this.trialConfig.codeDir, trialWorkingFolder, sshClient); @@ -562,6 +576,22 @@ class RemoteMachineTrainingService implements TrainingService { return jobpidPath; } + + private async writeParameterFile(trialJobId: string, hyperParameters: HyperParameters, rmMeta: RemoteMachineMeta): Promise { + const sshClient: Client | undefined = this.machineSSHClientMap.get(rmMeta); + if (sshClient === undefined) { + throw new Error('sshClient is undefined.'); + } + + const trialWorkingFolder: string = path.join(this.remoteExpRootDir, 'trials', trialJobId); + const trialLocalTempFolder: string = path.join(this.expRootDir, 'trials-local', trialJobId); + + const fileName: string = `parameter_${hyperParameters.index}.cfg`; + const localFilepath: string = path.join(trialLocalTempFolder, fileName); + await fs.promises.writeFile(localFilepath, hyperParameters.value, { encoding: 'utf8' }); + + await SSHClientUtility.copyFileToRemote(localFilepath, path.join(trialWorkingFolder, fileName), sshClient); + } } export { RemoteMachineTrainingService }; diff --git a/src/nni_manager/training_service/test/remoteMachineTrainingService.test.ts b/src/nni_manager/training_service/test/remoteMachineTrainingService.test.ts index b55e041e1d..7509ea2ade 100644 --- a/src/nni_manager/training_service/test/remoteMachineTrainingService.test.ts +++ b/src/nni_manager/training_service/test/remoteMachineTrainingService.test.ts @@ -100,7 +100,10 @@ describe('Unit Test for RemoteMachineTrainingService', () => { TrialConfigMetadataKey.TRIAL_CONFIG, `{"command":"sleep 1h && echo ","codeDir":"${localCodeDir}","gpuNum":1}`); const form: TrialJobApplicationForm = { jobType: 'TRIAL', - hyperParameters: 'mock hyperparameters' + hyperParameters: { + value: 'mock hyperparameters', + index: 0 + } }; const trialJob = await remoteMachineTrainingService.submitTrialJob(form); @@ -135,7 +138,10 @@ describe('Unit Test for RemoteMachineTrainingService', () => { // submit job const form: TrialJobApplicationForm = { jobType: 'TRIAL', - hyperParameters: 'mock hyperparameters' + hyperParameters: { + value: 'mock hyperparameters', + index: 0 + } }; const jobDetail: TrialJobDetail = await remoteMachineTrainingService.submitTrialJob(form); // Add metrics listeners diff --git a/src/sdk/pynni/nni/__main__.py b/src/sdk/pynni/nni/__main__.py index 206cd1f5c1..5454e98343 100644 --- a/src/sdk/pynni/nni/__main__.py +++ b/src/sdk/pynni/nni/__main__.py @@ -29,7 +29,7 @@ from .constants import ModuleName, ClassName, ClassArgs from nni.msg_dispatcher import MsgDispatcher - +from nni.multi_phase.multi_phase_dispatcher import MultiPhaseMsgDispatcher logger = logging.getLogger('nni.main') logger.debug('START') @@ -90,6 +90,7 @@ def parse_args(): help='Assessor directory') parser.add_argument('--assessor_class_filename', type=str, required=False, help='Assessor class file path') + parser.add_argument('--multi_phase', action='store_true') flags, _ = parser.parse_known_args() return flags @@ -132,7 +133,10 @@ def main(): if assessor is None: raise AssertionError('Failed to create Assessor instance') - dispatcher = MsgDispatcher(tuner, assessor) + if args.multi_phase: + dispatcher = MultiPhaseMsgDispatcher(tuner, assessor) + else: + dispatcher = MsgDispatcher(tuner, assessor) try: dispatcher.run() diff --git a/src/sdk/pynni/nni/multi_phase/__init__.py b/src/sdk/pynni/nni/multi_phase/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sdk/pynni/nni/multi_phase/multi_phase_dispatcher.py b/src/sdk/pynni/nni/multi_phase/multi_phase_dispatcher.py new file mode 100644 index 0000000000..ec7d2be0f1 --- /dev/null +++ b/src/sdk/pynni/nni/multi_phase/multi_phase_dispatcher.py @@ -0,0 +1,178 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +# OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ================================================================================================== + +import logging +from collections import defaultdict +import json_tricks + +from nni.protocol import CommandType, send +from nni.msg_dispatcher_base import MsgDispatcherBase +from nni.assessor import AssessResult + +_logger = logging.getLogger(__name__) + +# Assessor global variables +_trial_history = defaultdict(dict) +'''key: trial job ID; value: intermediate results, mapping from sequence number to data''' + +_ended_trials = set() +'''trial_job_id of all ended trials. +We need this because NNI manager may send metrics after reporting a trial ended. +TODO: move this logic to NNI manager +''' + +def _sort_history(history): + ret = [ ] + for i, _ in enumerate(history): + if i in history: + ret.append(history[i]) + else: + break + return ret + +# Tuner global variables +_next_parameter_id = 0 +_trial_params = {} +'''key: trial job ID; value: parameters''' +_customized_parameter_ids = set() + +def _create_parameter_id(): + global _next_parameter_id # pylint: disable=global-statement + _next_parameter_id += 1 + return _next_parameter_id - 1 + +def _pack_parameter(parameter_id, params, customized=False, trial_job_id=None, parameter_index=None): + _trial_params[parameter_id] = params + ret = { + 'parameter_id': parameter_id, + 'parameter_source': 'customized' if customized else 'algorithm', + 'parameters': params + } + if trial_job_id is not None: + ret['trial_job_id'] = trial_job_id + if parameter_index is not None: + ret['parameter_index'] = parameter_index + else: + ret['parameter_index'] = 0 + return json_tricks.dumps(ret) + +class MultiPhaseMsgDispatcher(MsgDispatcherBase): + def __init__(self, tuner, assessor=None): + super() + self.tuner = tuner + self.assessor = assessor + if assessor is None: + _logger.debug('Assessor is not configured') + + def load_checkpoint(self): + self.tuner.load_checkpoint() + if self.assessor is not None: + self.assessor.load_checkpoint() + + def save_checkpoint(self): + self.tuner.save_checkpoint() + if self.assessor is not None: + self.assessor.save_checkpoint() + + def handle_request_trial_jobs(self, data): + # data: number or trial jobs + ids = [_create_parameter_id() for _ in range(data)] + params_list = self.tuner.generate_multiple_parameters(ids) + assert len(ids) == len(params_list) + for i, _ in enumerate(ids): + send(CommandType.NewTrialJob, _pack_parameter(ids[i], params_list[i])) + return True + + def handle_update_search_space(self, data): + self.tuner.update_search_space(data) + return True + + def handle_add_customized_trial(self, data): + # data: parameters + id_ = _create_parameter_id() + _customized_parameter_ids.add(id_) + send(CommandType.NewTrialJob, _pack_parameter(id_, data, customized=True)) + return True + + def handle_report_metric_data(self, data): + trial_job_id = data['trial_job_id'] + if data['type'] == 'FINAL': + id_ = data['parameter_id'] + if id_ in _customized_parameter_ids: + self.tuner.receive_customized_trial_result(id_, _trial_params[id_], data['value'], trial_job_id) + else: + self.tuner.receive_trial_result(id_, _trial_params[id_], data['value'], trial_job_id) + elif data['type'] == 'PERIODICAL': + if self.assessor is not None: + self._handle_intermediate_metric_data(data) + else: + pass + elif data['type'] == 'REQUEST_PARAMETER': + assert data['trial_job_id'] is not None + assert data['parameter_index'] is not None + param_id = _create_parameter_id() + param = self.tuner.generate_parameters(param_id, trial_job_id) + send(CommandType.SendTrialJobParameter, _pack_parameter(param_id, param, trial_job_id=data['trial_job_id'], parameter_index=data['parameter_index'])) + else: + raise ValueError('Data type not supported: {}'.format(data['type'])) + + return True + + def handle_trial_end(self, data): + trial_job_id = data['trial_job_id'] + _ended_trials.add(trial_job_id) + if trial_job_id in _trial_history: + _trial_history.pop(trial_job_id) + if self.assessor is not None: + self.assessor.trial_end(trial_job_id, data['event'] == 'SUCCEEDED') + return True + + def _handle_intermediate_metric_data(self, data): + if data['type'] != 'PERIODICAL': + return True + if self.assessor is None: + return True + + trial_job_id = data['trial_job_id'] + if trial_job_id in _ended_trials: + return True + + history = _trial_history[trial_job_id] + history[data['sequence']] = data['value'] + ordered_history = _sort_history(history) + if len(ordered_history) < data['sequence']: # no user-visible update since last time + return True + + try: + result = self.assessor.assess_trial(trial_job_id, ordered_history) + except Exception as e: + _logger.exception('Assessor error') + + if isinstance(result, bool): + result = AssessResult.Good if result else AssessResult.Bad + elif not isinstance(result, AssessResult): + msg = 'Result of Assessor.assess_trial must be an object of AssessResult, not %s' + raise RuntimeError(msg % type(result)) + + if result is AssessResult.Bad: + _logger.debug('BAD, kill %s', trial_job_id) + send(CommandType.KillTrialJob, json_tricks.dumps(trial_job_id)) + else: + _logger.debug('GOOD') diff --git a/src/sdk/pynni/nni/multi_phase/multi_phase_tuner.py b/src/sdk/pynni/nni/multi_phase/multi_phase_tuner.py new file mode 100644 index 0000000000..1fb10ab676 --- /dev/null +++ b/src/sdk/pynni/nni/multi_phase/multi_phase_tuner.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +# OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ================================================================================================== + + +import logging + +from nni.recoverable import Recoverable + +_logger = logging.getLogger(__name__) + + +class MultiPhaseTuner(Recoverable): + # pylint: disable=no-self-use,unused-argument + + def generate_parameters(self, parameter_id, trial_job_id=None): + """Returns a set of trial (hyper-)parameters, as a serializable object. + User code must override either this function or 'generate_multiple_parameters()'. + parameter_id: int + """ + raise NotImplementedError('Tuner: generate_parameters not implemented') + + def generate_multiple_parameters(self, parameter_id_list): + """Returns multiple sets of trial (hyper-)parameters, as iterable of serializable objects. + Call 'generate_parameters()' by 'count' times by default. + User code must override either this function or 'generate_parameters()'. + parameter_id_list: list of int + """ + return [self.generate_parameters(parameter_id) for parameter_id in parameter_id_list] + + def receive_trial_result(self, parameter_id, parameters, reward, trial_job_id): + """Invoked when a trial reports its final result. Must override. + parameter_id: int + parameters: object created by 'generate_parameters()' + reward: object reported by trial + """ + raise NotImplementedError('Tuner: receive_trial_result not implemented') + + def receive_customized_trial_result(self, parameter_id, parameters, reward, trial_job_id): + """Invoked when a trial added by WebUI reports its final result. Do nothing by default. + parameter_id: int + parameters: object created by user + reward: object reported by trial + """ + _logger.info('Customized trial job %s ignored by tuner', parameter_id) + + def update_search_space(self, search_space): + """Update the search space of tuner. Must override. + search_space: JSON object + """ + raise NotImplementedError('Tuner: update_search_space not implemented') + + def load_checkpoint(self): + """Load the checkpoint of tuner. + path: checkpoint directory for tuner + """ + checkpoin_path = self.get_checkpoint_path() + _logger.info('Load checkpoint ignored by tuner, checkpoint path: %s' % checkpoin_path) + + def save_checkpoint(self): + """Save the checkpoint of tuner. + path: checkpoint directory for tuner + """ + checkpoin_path = self.get_checkpoint_path() + _logger.info('Save checkpoint ignored by tuner, checkpoint path: %s' % checkpoin_path) + + def _on_exit(self): + pass + + def _on_error(self): + pass diff --git a/src/sdk/pynni/nni/platform/local.py b/src/sdk/pynni/nni/platform/local.py index 7a9df82971..1c3b196bf4 100644 --- a/src/sdk/pynni/nni/platform/local.py +++ b/src/sdk/pynni/nni/platform/local.py @@ -18,11 +18,12 @@ # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # ================================================================================================== - -import json import os +import json +import time +import json_tricks -from ..common import init_logger +from ..common import init_logger, env_args _sysdir = os.environ['NNI_SYS_DIR'] if not os.path.exists(os.path.join(_sysdir, '.nni')): @@ -35,10 +36,28 @@ _log_file_path = os.path.join(_outputdir, 'trial.log') init_logger(_log_file_path) +_param_index = 0 + +def request_next_parameter(): + metric = json_tricks.dumps({ + 'trial_job_id': env_args.trial_job_id, + 'type': 'REQUEST_PARAMETER', + 'sequence': 0, + 'parameter_index': _param_index + }) + send_metric(metric) def get_parameters(): - params_file = open(os.path.join(_sysdir, 'parameter.cfg'), 'r') - return json.load(params_file) + global _param_index + params_filepath = os.path.join(_sysdir, 'parameter_{}.cfg'.format(_param_index)) + if not os.path.isfile(params_filepath): + request_next_parameter() + while not os.path.isfile(params_filepath): + time.sleep(3) + params_file = open(params_filepath, 'r') + params = json.load(params_file) + _param_index += 1 + return params def send_metric(string): data = (string + '\n').encode('utf8') diff --git a/src/sdk/pynni/nni/protocol.py b/src/sdk/pynni/nni/protocol.py index 6c0e71506d..ada5527bfa 100644 --- a/src/sdk/pynni/nni/protocol.py +++ b/src/sdk/pynni/nni/protocol.py @@ -34,6 +34,7 @@ class CommandType(Enum): # out NewTrialJob = b'TR' + SendTrialJobParameter = b'SP' NoMoreTrialJobs = b'NO' KillTrialJob = b'KI' @@ -55,7 +56,7 @@ def send(command, data): data = data.encode('utf8') assert len(data) < 1000000, 'Command too long' msg = b'%b%06d%b' % (command.value, len(data), data) - logging.getLogger(__name__).debug('Sending command, data: [%s]' % data) + logging.getLogger(__name__).debug('Sending command, data: [%s]' % msg) _out_file.write(msg) _out_file.flush() diff --git a/src/sdk/pynni/nni/trial.py b/src/sdk/pynni/nni/trial.py index e5884f8dac..27dc864d23 100644 --- a/src/sdk/pynni/nni/trial.py +++ b/src/sdk/pynni/nni/trial.py @@ -32,11 +32,13 @@ ] -_params = platform.get_parameters() +_params = None def get_parameters(): """Returns a set of (hyper-)paremeters generated by Tuner.""" + global _params + _params = platform.get_parameters() return _params['parameters'] @@ -51,6 +53,7 @@ def report_intermediate_result(metric): metric: serializable object. """ global _intermediate_seq + assert _params is not None, 'nni.get_parameters() needs to be called before report_intermediate_result' metric = json_tricks.dumps({ 'parameter_id': _params['parameter_id'], 'trial_job_id': env_args.trial_job_id, @@ -66,6 +69,7 @@ def report_final_result(metric): """Reports final result to tuner. metric: serializable object. """ + assert _params is not None, 'nni.get_parameters() needs to be called before report_final_result' metric = json_tricks.dumps({ 'parameter_id': _params['parameter_id'], 'trial_job_id': env_args.trial_job_id, diff --git a/src/sdk/pynni/tests/test_multi_phase_tuner.py b/src/sdk/pynni/tests/test_multi_phase_tuner.py new file mode 100644 index 0000000000..72b477999e --- /dev/null +++ b/src/sdk/pynni/tests/test_multi_phase_tuner.py @@ -0,0 +1,89 @@ +import logging +import random +from io import BytesIO + +import nni +import nni.protocol +from nni.protocol import CommandType, send, receive +from nni.multi_phase.multi_phase_tuner import MultiPhaseTuner +from nni.multi_phase.multi_phase_dispatcher import MultiPhaseMsgDispatcher + +from unittest import TestCase, main + +class NaiveMultiPhaseTuner(MultiPhaseTuner): + ''' + supports only choices + ''' + def __init__(self): + self.search_space = None + + def generate_parameters(self, parameter_id, trial_job_id=None): + """Returns a set of trial (hyper-)parameters, as a serializable object. + User code must override either this function or 'generate_multiple_parameters()'. + parameter_id: int + """ + generated_parameters = {} + if self.search_space is None: + raise AssertionError('Search space not specified') + for k in self.search_space: + param = self.search_space[k] + if not param['_type'] == 'choice': + raise ValueError('Only choice type is supported') + param_values = param['_value'] + generated_parameters[k] = param_values[random.randint(0, len(param_values)-1)] + logging.getLogger(__name__).debug(generated_parameters) + return generated_parameters + + + def receive_trial_result(self, parameter_id, parameters, reward, trial_job_id): + logging.getLogger(__name__).debug('receive_trial_result: {},{},{},{}'.format(parameter_id, parameters, reward, trial_job_id)) + + def receive_customized_trial_result(self, parameter_id, parameters, reward, trial_job_id): + pass + + def update_search_space(self, search_space): + self.search_space = search_space + + +_in_buf = BytesIO() +_out_buf = BytesIO() + +def _reverse_io(): + _in_buf.seek(0) + _out_buf.seek(0) + nni.protocol._out_file = _in_buf + nni.protocol._in_file = _out_buf + +def _restore_io(): + _in_buf.seek(0) + _out_buf.seek(0) + nni.protocol._in_file = _in_buf + nni.protocol._out_file = _out_buf + +def _test_tuner(): + _reverse_io() # now we are sending to Tuner's incoming stream + send(CommandType.UpdateSearchSpace, "{\"learning_rate\": {\"_value\": [0.0001, 0.001, 0.002, 0.005, 0.01], \"_type\": \"choice\"}, \"optimizer\": {\"_value\": [\"Adam\", \"SGD\"], \"_type\": \"choice\"}}") + send(CommandType.RequestTrialJobs, '2') + send(CommandType.ReportMetricData, '{"parameter_id":0,"type":"PERIODICAL","value":10,"trial_job_id":"abc"}') + send(CommandType.ReportMetricData, '{"parameter_id":1,"type":"FINAL","value":11,"trial_job_id":"abc"}') + send(CommandType.AddCustomizedTrialJob, '{"param":-1}') + send(CommandType.ReportMetricData, '{"parameter_id":2,"type":"FINAL","value":22,"trial_job_id":"abc"}') + send(CommandType.RequestTrialJobs, '1') + send(CommandType.TrialEnd, '{"trial_job_id":"abc"}') + _restore_io() + + tuner = NaiveMultiPhaseTuner() + dispatcher = MultiPhaseMsgDispatcher(tuner) + dispatcher.run() + + _reverse_io() # now we are receiving from Tuner's outgoing stream + + command, data = receive() # this one is customized + print(command, data) + +class MultiPhaseTestCase(TestCase): + def test_tuner(self): + _test_tuner() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/test/naive/run.py b/test/naive/run.py index 239bedcd2c..f54fe7ab71 100644 --- a/test/naive/run.py +++ b/test/naive/run.py @@ -54,7 +54,7 @@ def run(): if trial > current_trial: current_trial = trial print('Trial #%d done' % trial) - + subprocess.run(['nnictl', 'log', 'stderr']) assert tuner_status == 'DONE' and assessor_status == 'DONE', 'Failed to finish in 1 min' ss1 = json.load(open('search_space.json')) diff --git a/tools/nnicmd/config_schema.py b/tools/nnicmd/config_schema.py index 9eff5bfa66..a4bb503291 100644 --- a/tools/nnicmd/config_schema.py +++ b/tools/nnicmd/config_schema.py @@ -29,6 +29,7 @@ Optional('maxTrialNum'): And(int, lambda x: 1 <= x <= 99999), 'trainingServicePlatform': And(str, lambda x: x in ['remote', 'local', 'pai']), Optional('searchSpacePath'): os.path.exists, +Optional('multiPhase'): bool, 'useAnnotation': bool, 'tuner': Or({ 'builtinTunerName': Or('TPE', 'Random', 'Anneal', 'Evolution', 'SMAC', 'BatchTuner'), diff --git a/tools/nnicmd/launcher.py b/tools/nnicmd/launcher.py index b223551bea..1e4bd25f88 100644 --- a/tools/nnicmd/launcher.py +++ b/tools/nnicmd/launcher.py @@ -114,6 +114,8 @@ def set_pai_config(experiment_config, port): if not response or not response.status_code == 200: if response is not None: err_message = response.text + with open(STDERR_FULL_PATH, 'a+') as fout: + fout.write(json.dumps(json.loads(err_message), indent=4, sort_keys=True, separators=(',', ':'))) return False, err_message #set trial_config @@ -128,6 +130,8 @@ def set_experiment(experiment_config, mode, port): request_data['maxExecDuration'] = experiment_config['maxExecDuration'] request_data['maxTrialNum'] = experiment_config['maxTrialNum'] request_data['searchSpace'] = experiment_config.get('searchSpace') + if experiment_config.get('multiPhase'): + request_data['multiPhase'] = experiment_config.get('multiPhase') request_data['tuner'] = experiment_config['tuner'] if 'assessor' in experiment_config: request_data['assessor'] = experiment_config['assessor'] From 935468543f117a636e8e3170f8db768e7e7c269b Mon Sep 17 00:00:00 2001 From: xuehui Date: Mon, 8 Oct 2018 21:41:40 +0800 Subject: [PATCH 2/9] Support more data structure in tuner (#157) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * support key-value pairs in final result for tuner * fix bug * update warning message * fix bug --- src/sdk/pynni/nni/msg_dispatcher.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/sdk/pynni/nni/msg_dispatcher.py b/src/sdk/pynni/nni/msg_dispatcher.py index e379ac5d8b..4f94f50f34 100644 --- a/src/sdk/pynni/nni/msg_dispatcher.py +++ b/src/sdk/pynni/nni/msg_dispatcher.py @@ -111,11 +111,24 @@ def handle_add_customized_trial(self, data): def handle_report_metric_data(self, data): if data['type'] == 'FINAL': + value = None id_ = data['parameter_id'] + + if isinstance(data['value'], float) or isinstance(data['value'], int): + value = data['value'] + elif isinstance(data['value'], dict) and 'default' in data['value']: + value = data['value']['default'] + if isinstance(value, float) or isinstance(value, int): + pass + else: + raise RuntimeError('Incorrect final result: the final result should be float/int, or a dict which has a key named "default" whose value is float/int.') + else: + raise RuntimeError('Incorrect final result: the final result should be float/int, or a dict which has a key named "default" whose value is float/int.') + if id_ in _customized_parameter_ids: - self.tuner.receive_customized_trial_result(id_, _trial_params[id_], data['value']) + self.tuner.receive_customized_trial_result(id_, _trial_params[id_], value) else: - self.tuner.receive_trial_result(id_, _trial_params[id_], data['value']) + self.tuner.receive_trial_result(id_, _trial_params[id_], value) elif data['type'] == 'PERIODICAL': if self.assessor is not None: self._handle_intermediate_metric_data(data) From 392c598e0207da60721446a260896af27e0dcd94 Mon Sep 17 00:00:00 2001 From: fishyds Date: Tue, 9 Oct 2018 09:56:38 +0800 Subject: [PATCH 3/9] Change hard-coded root directory to $PWD in PAI container (#182) --- .../training_service/pai/hdfsClientUtility.ts | 9 ++++++--- .../training_service/pai/paiTrainingService.ts | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/nni_manager/training_service/pai/hdfsClientUtility.ts b/src/nni_manager/training_service/pai/hdfsClientUtility.ts index 69fc383e6d..21271d2786 100644 --- a/src/nni_manager/training_service/pai/hdfsClientUtility.ts +++ b/src/nni_manager/training_service/pai/hdfsClientUtility.ts @@ -133,10 +133,13 @@ export namespace HDFSClientUtility { deferred.resolve(exist); }); - // Set timeout and reject the promise once reach timeout (5 seconds) - setTimeout(() => deferred.reject(`Check HDFS path ${hdfsPath} exists timeout`), 5000); + let timeoutId : NodeJS.Timer + const delayTimeout : Promise = new Promise((resolve : Function, reject : Function) : void => { + // Set timeout and reject the promise once reach timeout (5 seconds) + setTimeout(() => deferred.reject(`Check HDFS path ${hdfsPath} exists timeout`), 5000); + }); - return deferred.promise; + return Promise.race([deferred.promise, delayTimeout]).finally(() => clearTimeout(timeoutId)); } /** diff --git a/src/nni_manager/training_service/pai/paiTrainingService.ts b/src/nni_manager/training_service/pai/paiTrainingService.ts index f7f8b3c4e7..794e957413 100644 --- a/src/nni_manager/training_service/pai/paiTrainingService.ts +++ b/src/nni_manager/training_service/pai/paiTrainingService.ts @@ -186,8 +186,8 @@ class PAITrainingService implements TrainingService { const nniPaiTrialCommand : string = String.Format( PAI_TRIAL_COMMAND_FORMAT, // PAI will copy job's codeDir into /root directory - `/root/${trialJobId}`, - `/root/${trialJobId}/nnioutput`, + `$PWD/${trialJobId}`, + `$PWD/${trialJobId}/nnioutput`, trialJobId, this.experimentId, this.paiTrialConfig.command, @@ -343,7 +343,17 @@ class PAITrainingService implements TrainingService { deferred.resolve(); } }); - break; + + let timeoutId: NodeJS.Timer; + const timeoutDelay: Promise = new Promise((resolve: Function, reject: Function): void => { + // Set timeout and reject the promise once reach timeout (5 seconds) + timeoutId = setTimeout( + () => reject(new Error('Get PAI token timeout. Please check your PAI cluster.')), + 5000); + }); + + return Promise.race([timeoutDelay, deferred.promise]).finally(() => clearTimeout(timeoutId)); + case TrialConfigMetadataKey.TRIAL_CONFIG: if (!this.paiClusterConfig){ this.log.error('pai cluster config is not initialized'); From c9d1a385d0cfe2932acfd2aa72f27ef515f6b6c5 Mon Sep 17 00:00:00 2001 From: Vipul Gupta Date: Tue, 9 Oct 2018 11:23:08 +0530 Subject: [PATCH 4/9] Add CONTRIBUTING (#165) * Add CONTRIBUTING Signed-off-by: Vipul Gupta (@vipulgupta2048) * Changes made as requested Signed-off-by: Vipul Gupta (@vipulgupta2048) * Changes made as requested#2 Signed-off-by: Vipul Gupta (@vipulgupta2048) --- README.md | 5 ++++- docs/CONTRIBUTING.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/HowToContribute.md | 3 ++- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 docs/CONTRIBUTING.md diff --git a/README.md b/README.md index b77ff37418..0b4d0b385c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ To learn more about how this example was constructed and how to analyze the expe * [Tutorial of the command tool *nnictl*.](docs/NNICTLDOC.md) # Contributing -This project welcomes contributions and suggestions, we are constructing the contribution guidelines, stay tuned =). +This project welcomes contributions and suggestions, please refer to our [contributing](./docs/CONTRIBUTING.md) document for the same. We use [GitHub issues](https://github.com/Microsoft/nni/issues) for tracking requests and bugs. +# License +The entire codebase is under [MIT license](https://github.com/Microsoft/nni/blob/master/LICENSE) + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..80254c7af0 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to Neural Network Intelligence (NNI) + +Great!! We are always on the lookout for more contributors to our code base. + +Firstly, if you are unsure or afraid of anything, just ask or submit the issue or pull request anyways. You won't be yelled at for giving your best effort. The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contributions and don't want a wall of rules to get in the way of that. + +However, for those individuals who want a bit more guidance on the best way to contribute to the project, read on. This document will cover all the points we're looking for in your contributions, raising your chances of quickly merging or addressing your contributions. + +Looking for a quickstart, get acquainted with our [Get Started](./docs/GetStarted.md) guide. + +There are a few simple guidelines that you need to follow before providing your hacks. + +## Raising Issues + +When raising issues, please specify the following: +- Setup details needs to be filled as specified in the issue template clearly for the reviewer to check. +- A scenario where the issue occurred (with details on how to reproduce it). +- Errors and log messages that are displayed by the software. +- Any other details that might be useful. + +## Submit Proposals for New Features + +- There is always something more that is required, to make it easier to suit your use-cases. Feel free to join the discussion on new features or raise a PR with your proposed change. + +- Fork the repository under your own github handle. After cloning the repository. Add, commit, push and sqaush (if necessary) the changes with detailed commit messages to your fork. From where you can proceed to making a pull request. + +## Contributing to Source Code and Bug Fixes + +Provide PRs with appropriate tags for bug fixes or enhancements to the source code. Do follow the correct naming conventions and code styles when you work on and do try to implement all code reviews along the way. + +If you are looking for How to go about contributing and debugging the NNI source code, you can refer our [How to Contribute](./docs/HowToContribute.md) file in the `docs` folder. + +Similarly for [writing trials](./docs/WriteYourTrial.md) or [starting experiments](StartExperiment.md). For everything else, refer [here](https://github.com/Microsoft/nni/tree/master/docs). + +## Solve Existing Issues +Head over to [issues](https://github.com/Microsoft/nni/issues) to find issues where help is needed from contributors. You can find issues tagged with 'good-first-issue' or 'help-wanted' to contribute in. + +A person looking to contribute can take up an issue by claiming it as a comment/assign their Github ID to it. In case there is no PR or update in progress for a week on the said issue, then the issue reopens for anyone to take up again. We need to consider high priority issues/regressions where response time must be a day or so. + +## Code Styles & Naming Conventions +We follow [PEP8](https://www.python.org/dev/peps/pep-0008/) for Python code and naming conventions, do try to adhere to the same when making a pull request or making a change. One can also take the help of linters such as `flake8` or `pylint` diff --git a/docs/HowToContribute.md b/docs/HowToContribute.md index 87a570e182..c759e810d8 100644 --- a/docs/HowToContribute.md +++ b/docs/HowToContribute.md @@ -50,4 +50,5 @@ And open web ui to check if everything is OK After you change some code, just use **step 4** to rebuild your code, then the change will take effect immediately --- -At last, wish you have a wonderful day. \ No newline at end of file +At last, wish you have a wonderful day. +For more contribution guidelines on making PR's or issues to NNI source code, you can refer to our [CONTRIBUTING](./docs/CONTRIBUTING.md) document. \ No newline at end of file From fd8ee22e97bca223910b977c22a4ccba401f73c6 Mon Sep 17 00:00:00 2001 From: xuehui Date: Tue, 9 Oct 2018 16:09:02 +0800 Subject: [PATCH 5/9] Add enas from contributor (#172) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * update config.yml and README * add doc * add enas link * update picture of nni_overview * Update README.md --- README.md | 1 + docs/HowToDebug.md | 3 +++ docs/nni_overview.png | Bin 57734 -> 57236 bytes examples/trials/auto-gbdt/config.yml | 2 +- examples/trials/auto-gbdt/config_pai.yml | 2 +- examples/trials/enas/README.md | 6 ++++++ examples/trials/mnist-annotation/config.yml | 2 +- .../trials/mnist-annotation/config_pai.yml | 2 +- examples/trials/mnist-keras/config.yml | 2 +- examples/trials/mnist-keras/config_pai.yml | 2 +- examples/trials/mnist-smartparam/config.yml | 2 +- .../trials/mnist-smartparam/config_pai.yml | 2 +- examples/trials/mnist/config.yml | 2 +- examples/trials/mnist/config_assessor.yml | 2 +- examples/trials/mnist/config_pai.yml | 2 +- examples/trials/pytorch_cifar10/config.yml | 2 +- .../trials/pytorch_cifar10/config_pai.yml | 2 +- 17 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 docs/HowToDebug.md create mode 100644 examples/trials/enas/README.md diff --git a/README.md b/README.md index 0b4d0b385c..2b96cdf5f0 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ To learn more about how this example was constructed and how to analyze the expe * [How to write a customized assessor?](examples/assessors/README.md) * [How to resume an experiment?](docs/NNICTLDOC.md) * [Tutorial of the command tool *nnictl*.](docs/NNICTLDOC.md) +* [How to debug in NNI](docs/HowToDebug.md) # Contributing This project welcomes contributions and suggestions, please refer to our [contributing](./docs/CONTRIBUTING.md) document for the same. diff --git a/docs/HowToDebug.md b/docs/HowToDebug.md new file mode 100644 index 0000000000..0d62705512 --- /dev/null +++ b/docs/HowToDebug.md @@ -0,0 +1,3 @@ +**How to Debug in NNI** +=== + diff --git a/docs/nni_overview.png b/docs/nni_overview.png index fa30c2a0c146a7fb5e0cda5f3c039c04ebd21b34..7ed3ca96abac0de32061a4f96d0a5d6d9313fb15 100644 GIT binary patch literal 57236 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

-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 Date: Tue, 9 Oct 2018 17:34:44 +0800 Subject: [PATCH 6/9] Fix bug of hyper graph and add down experiment parameters (#185) * Fix bug of hyper graph and add down experiment parameters * Show loguniform elegantly * Change hyper graph color and numbers in bar show three decimal places --- src/webui/src/components/Para.tsx | 57 +++++++++++++-------- src/webui/src/components/Sessionpro.tsx | 68 +++++++++++++++++++++++-- src/webui/src/components/SlideBar.tsx | 2 +- src/webui/src/style/sessionpro.css | 4 ++ 4 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/webui/src/components/Para.tsx b/src/webui/src/components/Para.tsx index 2706db1d60..01506a5773 100644 --- a/src/webui/src/components/Para.tsx +++ b/src/webui/src/components/Para.tsx @@ -124,25 +124,38 @@ class Para extends React.Component<{}, ParaState> { const searchRange = JSON.parse(res1.data.params.searchSpace); for (let i = 0; i < dimName.length; i++) { const searchKey = searchRange[dimName[i]]; - if (searchKey._type === 'uniform') { - parallelAxis.push({ - dim: i, - name: dimName[i], - max: searchKey._value[1], - min: searchKey._value[0] - }); - } else { // choice - // data number ['0.2', '0.4', '0.6'] - const data: Array = []; - for (let j = 0; j < searchKey._value.length; j++) { - data.push(searchKey._value[j].toString()); - } - parallelAxis.push({ - dim: i, - name: dimName[i], - type: 'category', - data: data - }); + switch (searchKey._type) { + case 'uniform': + case 'quniform': + parallelAxis.push({ + dim: i, + name: dimName[i], + max: searchKey._value[1], + min: searchKey._value[0] + }); + break; + + case 'choice': + const data: Array = []; + for (let j = 0; j < searchKey._value.length; j++) { + data.push(searchKey._value[j].toString()); + } + parallelAxis.push({ + dim: i, + name: dimName[i], + type: 'category', + data: data + }); + break; + + case 'loguniform': + parallelAxis.push({ + dim: i, + name: dimName[i] + }); + break; + + default: } } // get data for every lines. if dim is choice type @@ -226,14 +239,16 @@ class Para extends React.Component<{}, ParaState> { if (maxAccuracy === minAccuracy) { visualMapObj = { type: 'continuous', - color: ['#fb7c7c', 'yellow', 'lightblue'] + precision: 3, + color: ['#CA0000', '#FFC400', '#90EE90'] }; } else { visualMapObj = { type: 'continuous', + precision: 3, min: visualValue.minAccuracy, max: visualValue.maxAccuracy, - color: ['#fb7c7c', 'yellow', 'lightblue'] + color: ['#CA0000', '#FFC400', '#90EE90'] }; } let optionown = { diff --git a/src/webui/src/components/Sessionpro.tsx b/src/webui/src/components/Sessionpro.tsx index 83b44b3f78..3457ca7e64 100644 --- a/src/webui/src/components/Sessionpro.tsx +++ b/src/webui/src/components/Sessionpro.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import axios from 'axios'; -import { Table, Select, Row, Col, Icon } from 'antd'; +import { Table, Select, Row, Col, Icon, Button } from 'antd'; import { MANAGER_IP, overviewItem } from '../const'; const Option = Select.Option; import JSONTree from 'react-json-tree'; @@ -120,10 +120,26 @@ class Sessionpro extends React.Component<{}, SessionState> { tuner: sessionData.params.tuner, assessor: sessionData.params.assessor }); + // search space format loguniform max and min + const searchSpace = JSON.parse(sessionData.params.searchSpace); + Object.keys(searchSpace).map(item => { + const key = searchSpace[item]._type; + if (key === 'loguniform') { + let value = searchSpace[item]._value; + const a = Math.pow(10, value[0]); + const b = Math.pow(10, value[1]); + if (a < b) { + value = [a, b]; + } else { + value = [b, a]; + } + searchSpace[item]._value = value; + } + }); if (this._isMounted) { this.setState({ trialProfile: trialPro[0], - searchSpace: JSON.parse(sessionData.params.searchSpace), + searchSpace: searchSpace, tunerAssessor: tunerAsstemp[0] }); } @@ -211,6 +227,43 @@ class Sessionpro extends React.Component<{}, SessionState> { } } + downExperimentContent = () => { + axios + .all([ + axios.get(`${MANAGER_IP}/experiment`), + axios.get(`${MANAGER_IP}/trial-jobs`) + ]) + .then(axios.spread((res, res1) => { + if (res.status === 200 && res1.status === 200) { + if (res.data.params.searchSpace) { + res.data.params.searchSpace = JSON.parse(res.data.params.searchSpace); + } + const contentOfExperiment = JSON.stringify(res.data, null, 2); + let trialMessagesArr = res1.data; + Object.keys(trialMessagesArr).map(item => { + trialMessagesArr[item].hyperParameters = JSON.parse(trialMessagesArr[item].hyperParameters); + }); + const trialMessages = JSON.stringify(trialMessagesArr, null, 2); + const aTag = document.createElement('a'); + const file = new Blob([contentOfExperiment, trialMessages], { type: 'application/json' }); + aTag.download = 'experiment.txt'; + aTag.href = URL.createObjectURL(file); + aTag.click(); + URL.revokeObjectURL(aTag.href); + if (navigator.userAgent.indexOf('Firefox') > -1) { + const downTag = document.createElement('a'); + downTag.addEventListener('click', function () { + downTag.download = 'experiment.txt'; + downTag.href = URL.createObjectURL(file); + }); + let eventMouse = document.createEvent('MouseEvents'); + eventMouse.initEvent('click', false, false); + downTag.dispatchEvent(eventMouse); + } + } + })); + } + componentDidMount() { this.showSessionPro(); this.showTrials(); @@ -285,7 +338,7 @@ class Sessionpro extends React.Component<{}, SessionState> { getItemString={() => ()} // remove the {} items data={openRowDataSource} /> - { + { isLogLink ?

@@ -433,6 +486,15 @@ class Sessionpro extends React.Component<{}, SessionState> { bordered={true} />
+
+ +
); } diff --git a/src/webui/src/components/SlideBar.tsx b/src/webui/src/components/SlideBar.tsx index 2219491971..fa44885123 100644 --- a/src/webui/src/components/SlideBar.tsx +++ b/src/webui/src/components/SlideBar.tsx @@ -11,7 +11,7 @@ class SlideBar extends React.Component<{}, {}> {
  • - Overview + Overview
  • diff --git a/src/webui/src/style/sessionpro.css b/src/webui/src/style/sessionpro.css index 4f8490ca5e..7a3faf3f8f 100644 --- a/src/webui/src/style/sessionpro.css +++ b/src/webui/src/style/sessionpro.css @@ -183,3 +183,7 @@ .experStatus{ line-height: 12px; } +.downExp{ + width: 154px; + margin: 0 auto; +} From 115028f2e7b59e7c369f4eeb88dbd53b4ea09dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Wed, 10 Oct 2018 03:46:08 -0300 Subject: [PATCH 7/9] Typos on paragraph #28. (#184) * Typos on paragraph #28. Typo on paragraph #34 * Update README.md --- src/sdk/pynni/nni/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sdk/pynni/nni/README.md b/src/sdk/pynni/nni/README.md index bfb752b7f1..224841e695 100644 --- a/src/sdk/pynni/nni/README.md +++ b/src/sdk/pynni/nni/README.md @@ -25,16 +25,16 @@ Comparing with other algorithm, TPE could be achieve better result when the numb **Random Search** -In [Random Search for Hyper-Parameter Optimization][2] show that Random Search might be surprsingly simple and effective. We suggests that we could use Random Search as basline when we have no knowledge about the prior distribution of hyper-parameters. +In [Random Search for Hyper-Parameter Optimization][2] show that Random Search might be surprisingly simple and effective. We suggests that we could use Random Search as baseline when we have no knowledge about the prior distribution of hyper-parameters. **Anneal** **Naive Evolution** -Naive Evolution comes from [Large-Scale Evolution of Image Classifiers][3]. Naive Evolution requir more experiments to works, but it's very simple and easily to expand new features. There are some tips for user: +Naive Evolution comes from [Large-Scale Evolution of Image Classifiers][3]. Naive Evolution require more experiments to works, but it's very simple and easily to expand new features. There are some tips for user: 1) large initial population could avoid to fall into local optimum -2) use some strategies to keep the deversity of population could be better. +2) use some strategies to keep the diversity of population could be better. **SMAC** From 28d3039e4622eecdc535fce83b69b6b3ff9cf5e3 Mon Sep 17 00:00:00 2001 From: chicm-ms <38930155+chicm-ms@users.noreply.github.com> Date: Wed, 10 Oct 2018 14:52:07 +0800 Subject: [PATCH 8/9] Add description and trainingServicePlatform field in experiment profile (#187) * Pull latest code (#2) * webui logpath and document (#135) * Add webui document and logpath as a href * fix tslint * fix comments by Chengmin * Pai training service bug fix and enhancement (#136) * Add NNI installation scripts * Update pai script, update NNI_out_dir * Update NNI dir in nni sdk local.py * Create .nni folder in nni sdk local.py * Add check before creating .nni folder * Fix typo for PAI_INSTALL_NNI_SHELL_FORMAT * Improve annotation (#138) * Improve annotation * Minor bugfix * Selectively install through pip (#139) Selectively install through pip * update setup.py * fix paiTrainingService bugs (#137) * fix nnictl bug * add hdfs host validation * fix bugs * fix dockerfile * fix install.sh * update install.sh * fix dockerfile * Set timeout for HDFSUtility exists function * remove unused TODO * fix sdk * add optional for outputDir and dataDir * refactor dockerfile.base * Remove unused import in hdfsclientUtility * Add documentation for NNI PAI mode experiment (#141) * Add documentation for NNI PAI mode * Fix typo based on PR comments * Exit with subprocess return code of trial keeper * Remove additional exit code * Fix typo based on PR comments * update doc for smac tuner (#140) * Revert "Selectively install through pip (#139)" due to potential pip install issue (#142) * Revert "Selectively install through pip (#139)" This reverts commit 1d174836d3146a0363e9c9c88094bf9cff865faa. * Add exit code of subprocess for trial_keeper * Update README, add link to PAImode doc * fix bug (#147) * Refactor nnictl and add config_pai.yml (#144) * fix nnictl bug * add hdfs host validation * fix bugs * fix dockerfile * fix install.sh * update install.sh * fix dockerfile * Set timeout for HDFSUtility exists function * remove unused TODO * fix sdk * add optional for outputDir and dataDir * refactor dockerfile.base * Remove unused import in hdfsclientUtility * add config_pai.yml * refactor nnictl create logic and add colorful print * fix nnictl stop logic * add annotation for config_pai.yml * add document for start experiment * fix config.yml * fix document * Fix trial keeper wrongly exit issue (#152) * Fix trial keeper bug, use actual exitcode to exit rather than 1 * Fix bug of table sort (#145) * Update doc for PAIMode and v0.2 release notes (#153) * Update v0.2 documentation regards to release note and PAI training service * Update document to describe NNI docker image * Bug fix for SQuAD example tuner. (#134) * Update Makefile (#151) * test * update setup.py * update Makefile and install.sh * rever setup.py * change color * update doc * update doc * fix auto-completion's extra space * update Makefile * update webui * Update doc image (#163) * update doc * trivial * trivial * trivial * trivial * trivial * trivial * update image * update image size * Update ga squad (#104) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * sklearn examples (#169) * fix nnictl bug * fix install.sh * add sklearn-regression example * add sklearn classification * update sklearn * update example * remove additional code * Update batch tuner (#158) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * update batch tuner * Quickly fix cascading search space bug in tuner (#156) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * quickly fix cascading searchspace bug in tuner * Add iterative search space example (#119) * update readme in ga_squad * update readme * fix typo * Update README.md * Update README.md * Update README.md * update readme * add iterative search space example * update * update readme * change name * Add description and trainingServicePlatform field in experiment profile --- src/nni_manager/common/manager.ts | 2 ++ src/nni_manager/core/nnimanager.ts | 1 + src/nni_manager/core/test/dataStore.test.ts | 1 + src/nni_manager/core/test/nnimanager.test.ts | 1 + src/nni_manager/core/test/sqlDatabase.test.ts | 2 ++ src/nni_manager/rest_server/restValidationSchemas.ts | 2 ++ src/nni_manager/rest_server/test/mockedNNIManager.ts | 1 + tools/nnicmd/config_schema.py | 1 + tools/nnicmd/launcher.py | 4 ++++ 9 files changed, 15 insertions(+) diff --git a/src/nni_manager/common/manager.ts b/src/nni_manager/common/manager.ts index 1d02a1775d..4deac47387 100644 --- a/src/nni_manager/common/manager.ts +++ b/src/nni_manager/common/manager.ts @@ -27,10 +27,12 @@ type ProfileUpdateType = 'TRIAL_CONCURRENCY' | 'MAX_EXEC_DURATION' | 'SEARCH_SPA interface ExperimentParams { authorName: string; experimentName: string; + description?: string; trialConcurrency: number; maxExecDuration: number; //seconds maxTrialNum: number; searchSpace: string; + trainingServicePlatform: string; multiPhase?: boolean; tuner: { className: string; diff --git a/src/nni_manager/core/nnimanager.ts b/src/nni_manager/core/nnimanager.ts index c84688c038..23364d5448 100644 --- a/src/nni_manager/core/nnimanager.ts +++ b/src/nni_manager/core/nnimanager.ts @@ -528,6 +528,7 @@ class NNIManager implements Manager { trialConcurrency: 0, maxExecDuration: 0, // unit: second maxTrialNum: 0, // maxTrialNum includes all the submitted trial jobs + trainingServicePlatform: '', searchSpace: '', tuner: { className: '', diff --git a/src/nni_manager/core/test/dataStore.test.ts b/src/nni_manager/core/test/dataStore.test.ts index 603daa1ac4..d4f5deb394 100644 --- a/src/nni_manager/core/test/dataStore.test.ts +++ b/src/nni_manager/core/test/dataStore.test.ts @@ -58,6 +58,7 @@ describe('Unit test for dataStore', () => { trialConcurrency: 2, maxExecDuration: 10, maxTrialNum: 5, + trainingServicePlatform: 'local', searchSpace: `{ "dropout_rate": { "_type": "uniform", diff --git a/src/nni_manager/core/test/nnimanager.test.ts b/src/nni_manager/core/test/nnimanager.test.ts index 1ca11f69f0..e73e269c2d 100644 --- a/src/nni_manager/core/test/nnimanager.test.ts +++ b/src/nni_manager/core/test/nnimanager.test.ts @@ -54,6 +54,7 @@ describe('Unit test for nnimanager', function () { trialConcurrency: 2, maxExecDuration: 5, maxTrialNum: 2, + trainingServicePlatform: 'local', searchSpace: '{"x":1}', tuner: { className: 'EvolutionTuner', diff --git a/src/nni_manager/core/test/sqlDatabase.test.ts b/src/nni_manager/core/test/sqlDatabase.test.ts index 7a67e98e96..03d1ab5e2a 100644 --- a/src/nni_manager/core/test/sqlDatabase.test.ts +++ b/src/nni_manager/core/test/sqlDatabase.test.ts @@ -36,6 +36,7 @@ const expParams1: ExperimentParams = { trialConcurrency: 3, maxExecDuration: 100, maxTrialNum: 5, + trainingServicePlatform: 'local', searchSpace: 'SS', tuner: { className: 'testTuner', @@ -50,6 +51,7 @@ const expParams2: ExperimentParams = { trialConcurrency: 5, maxExecDuration: 1000, maxTrialNum: 5, + trainingServicePlatform: 'local', searchSpace: '', tuner: { className: 'testTuner', diff --git a/src/nni_manager/rest_server/restValidationSchemas.ts b/src/nni_manager/rest_server/restValidationSchemas.ts index 01985cddb2..000ddee6a0 100644 --- a/src/nni_manager/rest_server/restValidationSchemas.ts +++ b/src/nni_manager/rest_server/restValidationSchemas.ts @@ -52,9 +52,11 @@ export namespace ValidationSchemas { export const STARTEXPERIMENT = { body: { experimentName: joi.string().required(), + description: joi.string(), authorName: joi.string(), maxTrialNum: joi.number().min(0).required(), trialConcurrency: joi.number().min(0).required(), + trainingServicePlatform: joi.string(), searchSpace: joi.string().required(), maxExecDuration: joi.number().min(0).required(), multiPhase: joi.boolean(), diff --git a/src/nni_manager/rest_server/test/mockedNNIManager.ts b/src/nni_manager/rest_server/test/mockedNNIManager.ts index 5acdd072ce..f93e0165ca 100644 --- a/src/nni_manager/rest_server/test/mockedNNIManager.ts +++ b/src/nni_manager/rest_server/test/mockedNNIManager.ts @@ -135,6 +135,7 @@ export class MockedNNIManager extends Manager { trialConcurrency: 2, maxExecDuration: 30, maxTrialNum: 3, + trainingServicePlatform: 'local', searchSpace: '{lr: 0.01}', tuner: { className: 'testTuner', diff --git a/tools/nnicmd/config_schema.py b/tools/nnicmd/config_schema.py index a4bb503291..ace9621a5a 100644 --- a/tools/nnicmd/config_schema.py +++ b/tools/nnicmd/config_schema.py @@ -24,6 +24,7 @@ common_schema = { 'authorName': str, 'experimentName': str, +Optional('description'): str, 'trialConcurrency': And(int, lambda n: 1 <=n <= 999999), Optional('maxExecDuration'): Regex(r'^[1-9][0-9]*[s|m|h|d]$'), Optional('maxTrialNum'): And(int, lambda x: 1 <= x <= 99999), diff --git a/tools/nnicmd/launcher.py b/tools/nnicmd/launcher.py index 1e4bd25f88..a44c263e90 100644 --- a/tools/nnicmd/launcher.py +++ b/tools/nnicmd/launcher.py @@ -130,6 +130,10 @@ def set_experiment(experiment_config, mode, port): request_data['maxExecDuration'] = experiment_config['maxExecDuration'] request_data['maxTrialNum'] = experiment_config['maxTrialNum'] request_data['searchSpace'] = experiment_config.get('searchSpace') + request_data['trainingServicePlatform'] = experiment_config.get('trainingServicePlatform') + + if experiment_config.get('description'): + request_data['description'] = experiment_config['description'] if experiment_config.get('multiPhase'): request_data['multiPhase'] = experiment_config.get('multiPhase') request_data['tuner'] = experiment_config['tuner'] From 334cad507b09f5844cd04acd3685b4f68b1e05c7 Mon Sep 17 00:00:00 2001 From: Zejun Lin <871886504@qq.com> Date: Wed, 10 Oct 2018 16:42:00 +0800 Subject: [PATCH 9/9] quick fix for Makefile (#191) --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 8532cc17e7..32c5ff6132 100644 --- a/Makefile +++ b/Makefile @@ -211,14 +211,14 @@ install-python-modules: .PHONY: install-node-modules install-node-modules: mkdir -p $(INSTALL_PREFIX)/nni - rm -rf ${PWD}/src/nni_manager/dist/node_modules + rm -rf src/nni_manager/dist/node_modules #$(_INFO) Installing NNI Manager $(_END) - cp -rT ${PWD}/src/nni_manager/dist $(INSTALL_PREFIX)/nni/nni_manager - cp -rT ${PWD}/src/nni_manager/node_modules $(INSTALL_PREFIX)/nni/nni_manager/node_modules + cp -rT src/nni_manager/dist $(INSTALL_PREFIX)/nni/nni_manager + cp -rT src/nni_manager/node_modules $(INSTALL_PREFIX)/nni/nni_manager/node_modules #$(_INFO) Installing Web UI $(_END) - cp -rT ${PWD}/src/webui/build $(INSTALL_PREFIX)/nni/webui + cp -rT src/webui/build $(INSTALL_PREFIX)/nni/webui .PHONY: install-dev-modules