[tune] Add AutoMLBoard: Monitoring UI (experimental) (#2574)

This commit is contained in:
wangyiguang
2018-08-31 15:26:44 +08:00
committed by Richard Liaw
parent b6260003cb
commit 3813ae34b3
29 changed files with 2249 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
## About AutoMLBoard
AutoMLBoard can be used as a monitor who collect information about Tune jobs and
show the trial information to the frontend.
### Install
Install django before start AutoMLBoard. run:
```bash
pip install django==1.11.14
```
### Usage
Please refer to `run.py -h` for more help
@@ -0,0 +1,367 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import os
import time
from threading import Thread
from ray.tune.automlboard.common.exception import CollectorError
from ray.tune.automlboard.common.utils import parse_json, \
parse_multiple_json, timestamp2date
from ray.tune.automlboard.models.models import JobRecord, \
TrialRecord, ResultRecord
from ray.tune.result import DEFAULT_RESULTS_DIR, JOB_META_FILE, \
EXPR_PARARM_FILE, EXPR_RESULT_FILE, EXPR_META_FILE
class CollectorService(object):
"""Server implementation to monitor the log directory.
The service will save the information of job and
trials information in db.
"""
def __init__(self,
log_dir=DEFAULT_RESULTS_DIR,
reload_interval=30,
standalone=True,
log_level="INFO"):
"""Initialize the collector service.
Args:
log_dir (str): Directory of the logs about trials' information.
reload_interval (int): Sleep time period after each polling round.
standalone (boolean): The service will not stop and if True.
log_level (str): Level of logging.
"""
self.logger = self.init_logger(log_level)
self.standalone = standalone
self.collector = Collector(
reload_interval=reload_interval,
logdir=log_dir,
logger=self.logger)
def run(self):
"""Start the collector worker thread.
If running in standalone mode, the current thread will wait
until the collector thread ends.
"""
self.collector.start()
if self.standalone:
self.collector.join()
def stop(self):
"""Stop the collector worker thread."""
self.collector.stop()
@classmethod
def init_logger(cls, log_level):
"""Initialize logger settings."""
logger = logging.getLogger("AutoMLBoard")
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(levelname)s %(asctime)s] '
'%(filename)s: %(lineno)d '
'%(message)s')
handler.setFormatter(formatter)
logger.setLevel(log_level)
logger.addHandler(handler)
return logger
class Collector(Thread):
"""Worker thread for collector service."""
def __init__(self, reload_interval, logdir, logger):
"""Initialize collector worker thread.
Args
reload_interval (int): Time period to sleep after each round
of polling.
logdir (str): Directory path to save the status information of
jobs and trials.
logger (Logger): Logger for collector thread.
"""
super(Collector, self).__init__()
self._is_finished = False
self._reload_interval = reload_interval
self._logdir = logdir
self._monitored_jobs = set()
self._monitored_trials = set()
self._result_offsets = {}
self.logger = logger
self.daemon = True
def run(self):
"""Run the main event loop for collector thread.
In each round the collector traverse the results log directory
and reload trial information from the status files.
"""
self._initialize()
self._do_collect()
while not self._is_finished:
time.sleep(self._reload_interval)
self._do_collect()
self.logger.info("Collector stopped.")
def stop(self):
"""Stop the main polling loop."""
self._is_finished = True
def _initialize(self):
"""Initialize collector worker thread, Log path will be checked first.
Records in DB backend will be cleared.
"""
if not os.path.exists(self._logdir):
raise CollectorError("Log directory %s not exists" % self._logdir)
self.logger.info("Collector started, taking %s as parent directory"
"for all job logs." % self._logdir)
# clear old records
JobRecord.objects.filter().delete()
TrialRecord.objects.filter().delete()
ResultRecord.objects.filter().delete()
def _do_collect(self):
sub_dirs = os.listdir(self._logdir)
job_names = filter(
lambda d: os.path.isdir(os.path.join(self._logdir, d)), sub_dirs)
for job_name in job_names:
self.sync_job_info(job_name)
def sync_job_info(self, job_name):
"""Load information of the job with the given job name.
1. Traverse each experiment sub-directory and sync information
for each trial.
2. Create or update the job information, together with the job
meta file.
Args:
job_name (str) name of the Tune experiment
"""
job_path = os.path.join(self._logdir, job_name)
if job_name not in self._monitored_jobs:
self._create_job_info(job_path)
self._monitored_jobs.add(job_name)
else:
self._update_job_info(job_path)
expr_dirs = filter(lambda d: os.path.isdir(os.path.join(job_path, d)),
os.listdir(job_path))
for expr_dir_name in expr_dirs:
self.sync_trial_info(job_path, expr_dir_name)
self._update_job_info(job_path)
def sync_trial_info(self, job_path, expr_dir_name):
"""Load information of the trial from the given experiment directory.
Create or update the trial information, together with the trial
meta file.
Args:
job_path(str)
expr_dir_name(str)
"""
expr_name = expr_dir_name[-8:]
expr_path = os.path.join(job_path, expr_dir_name)
if expr_name not in self._monitored_trials:
self._create_trial_info(expr_path)
self._monitored_trials.add(expr_name)
else:
self._update_trial_info(expr_path)
def _create_job_info(self, job_dir):
"""Create information for given job.
Meta file will be loaded if exists, and the job information will
be saved in db backend.
Args:
job_dir (str): Directory path of the job.
"""
meta = self._build_job_meta(job_dir)
self.logger.debug("Create job: %s" % meta)
job_record = JobRecord.from_json(meta)
job_record.save()
@classmethod
def _update_job_info(cls, job_dir):
"""Update information for given job.
Meta file will be loaded if exists, and the job information in
in db backend will be updated.
Args:
job_dir (str): Directory path of the job.
Return:
Updated dict of job meta info
"""
meta_file = os.path.join(job_dir, JOB_META_FILE)
meta = parse_json(meta_file)
if meta:
logging.debug("Update job info for %s" % meta["job_id"])
JobRecord.objects \
.filter(job_id=meta["job_id"]) \
.update(end_time=timestamp2date(meta["end_time"]))
def _create_trial_info(self, expr_dir):
"""Create information for given trial.
Meta file will be loaded if exists, and the trial information
will be saved in db backend.
Args:
expr_dir (str): Directory path of the experiment.
"""
meta = self._build_trial_meta(expr_dir)
self.logger.debug("Create trial for %s" % meta)
trial_record = TrialRecord.from_json(meta)
trial_record.save()
def _update_trial_info(self, expr_dir):
"""Update information for given trial.
Meta file will be loaded if exists, and the trial information
in db backend will be updated.
Args:
expr_dir(str)
"""
trial_id = expr_dir[-8:]
meta_file = os.path.join(expr_dir, EXPR_META_FILE)
meta = parse_json(meta_file)
result_file = os.path.join(expr_dir, EXPR_RESULT_FILE)
offset = self._result_offsets.get(trial_id, 0)
results, new_offset = parse_multiple_json(result_file, offset)
self._add_results(results, trial_id)
self._result_offsets[trial_id] = new_offset
if meta:
TrialRecord.objects \
.filter(trial_id=trial_id) \
.update(trial_status=meta["status"],
end_time=timestamp2date(meta.get("end_time", None)))
elif len(results) > 0:
metrics = {
"episode_reward": results[-1].get("episode_reward_mean", None),
"accuracy": results[-1].get("mean_accuracy", None),
"loss": results[-1].get("loss", None)
}
if results[-1].get("done"):
TrialRecord.objects \
.filter(trial_id=trial_id) \
.update(trial_status="TERMINATED",
end_time=results[-1].get("date", None),
metrics=str(metrics))
else:
TrialRecord.objects \
.filter(trial_id=trial_id) \
.update(metrics=str(metrics))
@classmethod
def _build_job_meta(cls, job_dir):
"""Build meta file for job.
Args:
job_dir (str): Directory path of the job.
Return:
A dict of job meta info.
"""
meta_file = os.path.join(job_dir, JOB_META_FILE)
meta = parse_json(meta_file)
if not meta:
job_name = job_dir.split('/')[-1]
user = os.environ.get("USER", None)
meta = {
"job_id": job_name,
"job_name": job_name,
"user": user,
"type": "ray",
"start_time": os.path.getctime(job_dir),
"end_time": None,
"best_trial_id": None,
}
if meta.get("start_time", None):
meta["start_time"] = timestamp2date(meta["start_time"])
return meta
@classmethod
def _build_trial_meta(cls, expr_dir):
"""Build meta file for trial.
Args:
expr_dir (str): Directory path of the experiment.
Return:
A dict of trial meta info.
"""
meta_file = os.path.join(expr_dir, EXPR_META_FILE)
meta = parse_json(meta_file)
if not meta:
job_id = expr_dir.split('/')[-2]
trial_id = expr_dir[-8:]
params = parse_json(os.path.join(expr_dir, EXPR_PARARM_FILE))
meta = {
"trial_id": trial_id,
"job_id": job_id,
"status": "RUNNING",
"type": "TUNE",
"start_time": os.path.getctime(expr_dir),
"end_time": None,
"progress_offset": 0,
"result_offset": 0,
"params": params
}
if not meta.get("start_time", None):
meta["start_time"] = os.path.getctime(expr_dir)
if isinstance(meta["start_time"], float):
meta["start_time"] = timestamp2date(meta["start_time"])
if meta.get("end_time", None):
meta["end_time"] = timestamp2date(meta["end_time"])
meta["params"] = parse_json(os.path.join(expr_dir, EXPR_PARARM_FILE))
return meta
def _add_results(self, results, trial_id):
"""Add a list of results into db.
Args:
results (list): A list of json results.
trial_id (str): Id of the trial.
"""
for result in results:
self.logger.debug("Appending result: %s" % result)
result["trial_id"] = trial_id
result_record = ResultRecord.from_json(result)
result_record.save()
@@ -0,0 +1,15 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
class CollectorError(Exception):
"""Error raised from the collector service."""
pass
class DatabaseError(Exception):
"""Error raised from the database manager."""
pass
+112
View File
@@ -0,0 +1,112 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import json
import os
import time
def dump_json(json_info, json_file, overwrite=True):
"""Dump a whole json record into the given file.
Overwrite the file if the overwrite flag set.
Args:
json_info (dict): Information dict to be dumped.
json_file (str): File path to be dumped to.
overwrite(boolean)
"""
if overwrite:
mode = 'w'
else:
mode = 'w+'
try:
with open(json_file, mode) as f:
f.write(json.dumps(json_info))
except BaseException as e:
logging.error(e.message)
def parse_json(json_file):
"""Parse a whole json record from the given file.
Return None if the json file does not exists or exception occurs.
Args:
json_file (str): File path to be parsed.
Returns:
A dict of json info.
"""
if not os.path.exists(json_file):
return None
try:
with open(json_file, 'r') as f:
info_str = f.readlines()
info_str = "".join(info_str)
json_info = json.loads(info_str)
return unicode2str(json_info)
except BaseException as e:
logging.error(e.message)
return None
def parse_multiple_json(json_file, offset=None):
"""Parse multiple json records from the given file.
Seek to the offset as the start point before parsing
if offset set. return empty list if the json file does
not exists or exception occurs.
Args:
json_file (str): File path to be parsed.
offset (int): Initial seek position of the file.
Returns:
A dict of json info.
New offset after parsing.
"""
json_info_list = []
if not os.path.exists(json_file):
return json_info_list
try:
with open(json_file, 'r') as f:
if offset:
f.seek(offset)
for line in f:
if line[-1] != '\n':
# Incomplete line
break
json_info = json.loads(line)
json_info_list.append(json_info)
offset += len(line)
except BaseException as e:
logging.error(e.message)
return json_info_list, offset
def timestamp2date(timestamp):
"""Convert a timestamp to date."""
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))
def unicode2str(content):
"""Convert the unicode element of the content to str recursively."""
if isinstance(content, dict):
result = {}
for key in content.keys():
result[unicode2str(key)] = unicode2str(content[key])
return result
elif isinstance(content, list):
return [unicode2str(element) for element in content]
elif isinstance(content, int) or isinstance(content, float):
return content
else:
return content.encode('utf-8')
@@ -0,0 +1,110 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.shortcuts import HttpResponse
from ray.tune.automlboard.models.models import JobRecord, TrialRecord
from ray.tune.trial import Trial
import json
def query_job(request):
"""Rest API to query the job info, with the given job_id.
The url pattern should be like this:
curl http://<server>:<port>/query_job?job_id=<job_id>
The response may be:
{
"running_trials": 0,
"start_time": "2018-07-19 20:49:40",
"current_round": 1,
"failed_trials": 0,
"best_trial_id": "2067R2ZD",
"name": "asynchyperband_test",
"job_id": "asynchyperband_test",
"user": "Grady",
"type": "RAY TUNE",
"total_trials": 4,
"end_time": "2018-07-19 20:50:10",
"progress": 100,
"success_trials": 4
}
"""
job_id = request.GET.get('job_id')
jobs = JobRecord.objects.filter(job_id=job_id)
trials = TrialRecord.objects.filter(job_id=job_id)
total_num = len(trials)
running_num = sum(t.trial_status == Trial.RUNNING for t in trials)
success_num = sum(t.trial_status == Trial.TERMINATED for t in trials)
failed_num = sum(t.trial_status == Trial.ERROR for t in trials)
if total_num == 0:
progress = 0
else:
progress = int(float(success_num) / total_num * 100)
if len(jobs) == 0:
resp = "Unkonwn job id %s.\n" % job_id
else:
job = jobs[0]
result = {
"job_id": job.job_id,
"name": job.name,
"user": job.user,
"type": job.type,
"start_time": job.start_time,
"end_time": job.end_time,
"success_trials": success_num,
"failed_trials": failed_num,
"running_trials": running_num,
"total_trials": total_num,
"best_trial_id": job.best_trial_id,
"progress": progress
}
resp = json.dumps(result)
return HttpResponse(resp, content_type='application/json;charset=utf-8')
def query_trial(request):
"""Rest API to query the trial info, with the given trial_id.
The url pattern should be like this:
curl http://<server>:<port>/query_trial?trial_id=<trial_id>
The response may be:
{
"app_url": "None",
"trial_status": "TERMINATED",
"params": {'a': 1, 'b': 2},
"job_id": "asynchyperband_test",
"end_time": "2018-07-19 20:49:44",
"start_time": "2018-07-19 20:49:40",
"trial_id": "2067R2ZD",
}
"""
trial_id = request.GET.get('trial_id')
trials = TrialRecord.objects \
.filter(trial_id=trial_id) \
.order_by('-start_time')
if len(trials) == 0:
resp = "Unkonwn trial id %s.\n" % trials
else:
trial = trials[0]
result = {
"trial_id": trial.trial_id,
"job_id": trial.job_id,
"trial_status": trial.trial_status,
"start_time": trial.start_time,
"end_time": trial.end_time,
"params": trial.params
}
resp = json.dumps(result)
return HttpResponse(resp, content_type='application/json;charset=utf-8')
@@ -0,0 +1,38 @@
"""
Monitor URL Configuration.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from django.conf.urls import url
from django.contrib import admin
import ray.tune.automlboard.frontend.view as view
import ray.tune.automlboard.frontend.query as query
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', view.index),
url(r'^job$', view.job),
url(r'^trial$', view.trial),
url(r'^query_job', query.query_job),
url(r'^query_trial', query.query_trial)
]
@@ -0,0 +1,182 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.shortcuts import render
from ray.tune.automlboard.settings import AUTOMLBOARD_RELOAD_INTERVAL, \
AUTOMLBOARD_LOG_DIR
from ray.tune.automlboard.models.models import JobRecord, \
TrialRecord, ResultRecord
from ray.tune.trial import Trial
import datetime
def index(request):
"""View for the home page."""
recent_jobs = JobRecord.objects.order_by('-start_time')[0:100]
recent_trials = TrialRecord.objects.order_by('-start_time')[0:500]
total_num = len(recent_trials)
running_num = sum(t.trial_status == Trial.RUNNING for t in recent_trials)
success_num = sum(
t.trial_status == Trial.TERMINATED for t in recent_trials)
failed_num = sum(t.trial_status == Trial.ERROR for t in recent_trials)
job_records = []
for recent_job in recent_jobs:
job_records.append(get_job_info(recent_job))
context = {
'log_dir': AUTOMLBOARD_LOG_DIR,
'reload_interval': AUTOMLBOARD_RELOAD_INTERVAL,
'recent_jobs': job_records,
'job_num': len(job_records),
'trial_num': total_num,
'running_num': running_num,
'success_num': success_num,
'failed_num': failed_num
}
return render(request, 'index.html', context)
def job(request):
"""View for a single job."""
job_id = request.GET.get('job_id')
recent_jobs = JobRecord.objects.order_by('-start_time')[0:100]
recent_trials = TrialRecord.objects \
.filter(job_id=job_id) \
.order_by('-start_time')
trial_records = []
for recent_trial in recent_trials:
trial_records.append(get_trial_info(recent_trial))
current_job = JobRecord.objects \
.filter(job_id=job_id) \
.order_by('-start_time')[0]
if len(trial_records) > 0:
param_keys = trial_records[0]["params"].keys()
else:
param_keys = []
# TODO: support custom metrics here
metric_keys = ["episode_reward", "accuracy", "loss"]
context = {
'current_job': get_job_info(current_job),
'recent_jobs': recent_jobs,
'recent_trials': trial_records,
'param_keys': param_keys,
'param_num': len(param_keys),
'metric_keys': metric_keys,
'metric_num': len(metric_keys)
}
return render(request, 'job.html', context)
def trial(request):
"""View for a single trial."""
job_id = request.GET.get('job_id')
trial_id = request.GET.get('trial_id')
recent_trials = TrialRecord.objects \
.filter(job_id=job_id) \
.order_by('-start_time')
recent_results = ResultRecord.objects \
.filter(trial_id=trial_id) \
.order_by('-date')[0:2000]
current_trial = TrialRecord.objects \
.filter(trial_id=trial_id) \
.order_by('-start_time')[0]
context = {
'job_id': job_id,
'trial_id': trial_id,
'current_trial': current_trial,
'recent_results': recent_results,
'recent_trials': recent_trials
}
return render(request, 'trial.html', context)
def get_job_info(current_job):
"""Get job information for current job."""
trials = TrialRecord.objects.filter(job_id=current_job.job_id)
total_num = len(trials)
running_num = sum(t.trial_status == Trial.RUNNING for t in trials)
success_num = sum(t.trial_status == Trial.TERMINATED for t in trials)
failed_num = sum(t.trial_status == Trial.ERROR for t in trials)
if total_num == 0:
progress = 0
else:
progress = int(float(success_num) / total_num * 100)
winner = get_winner(trials)
job_info = {
"job_id": current_job.job_id,
"job_name": current_job.name,
"user": current_job.user,
"type": current_job.type,
"start_time": current_job.start_time,
"end_time": current_job.end_time,
"total_num": total_num,
"running_num": running_num,
"success_num": success_num,
"failed_num": failed_num,
"best_trial_id": current_job.best_trial_id,
"progress": progress,
"winner": winner
}
return job_info
def get_trial_info(current_trial):
"""Get job information for current trial."""
if current_trial.end_time and ('_' in current_trial.end_time):
# end time is parsed from result.json and the format
# is like: yyyy-mm-dd_hh-MM-ss, which will be converted
# to yyyy-mm-dd hh:MM:ss here
time_obj = datetime.datetime.strptime(current_trial.end_time,
"%Y-%m-%d_%H-%M-%S")
end_time = time_obj.strftime("%Y-%m-%d %H:%M:%S")
else:
end_time = current_trial.end_time
if current_trial.metrics:
metrics = eval(current_trial.metrics)
else:
metrics = None
trial_info = {
"trial_id": current_trial.trial_id,
"job_id": current_trial.job_id,
"trial_status": current_trial.trial_status,
"start_time": current_trial.start_time,
"end_time": end_time,
"params": eval(current_trial.params.encode("utf-8")),
"metrics": metrics
}
return trial_info
def get_winner(trials):
"""Get winner trial of a job."""
winner = {}
# TODO: sort_key should be customized here
sort_key = "accuracy"
if trials and len(trials) > 0:
first_metrics = get_trial_info(trials[0])["metrics"]
if first_metrics and not first_metrics.get("accuracy", None):
sort_key = "episode_reward"
max_metric = float('-Inf')
for t in trials:
metrics = get_trial_info(t).get("metrics", None)
if metrics and metrics.get(sort_key, None):
current_metric = float(metrics[sort_key])
if current_metric > max_metric:
winner["trial_id"] = t.trial_id
winner["metric"] = sort_key + ": " + str(current_metric)
max_metric = current_metric
return winner
@@ -0,0 +1,20 @@
"""
WSGI config for monitor project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from django.core.wsgi import get_wsgi_application
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"ray.tune.automlboard.settings")
application = get_wsgi_application()
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from django.core.management import execute_from_command_line
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"ray.tune.automlboard.settings")
execute_from_command_line(sys.argv)
@@ -0,0 +1 @@
default_app_config = "ray.tune.automlboard.models.apps.ModelConfig"
@@ -0,0 +1,12 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.apps import AppConfig
class ModelConfig(AppConfig):
"""Model Congig for models."""
name = 'ray.tune.automlboard.models'
@@ -0,0 +1,98 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from django.db import models
class JobRecord(models.Model):
"""Information of an AutoML Job."""
job_id = models.CharField(max_length=50)
name = models.CharField(max_length=20)
user = models.CharField(max_length=20)
type = models.CharField(max_length=20)
start_time = models.CharField(max_length=50)
end_time = models.CharField(max_length=50)
best_trial_id = models.CharField(max_length=50)
@classmethod
def from_json(cls, json_info):
"""Build a Job instance from a json string."""
if json_info is None:
return None
return JobRecord(
job_id=json_info["job_id"],
name=json_info["job_name"],
user=json_info["user"],
type=json_info["type"],
start_time=json_info["start_time"])
def is_finished(self):
"""Judge whether this is a record for a finished job."""
return self.end_time is not None
class TrialRecord(models.Model):
"""Information of a single AutoML trial of the job."""
trial_id = models.CharField(max_length=50)
job_id = models.CharField(max_length=50)
trial_status = models.CharField(max_length=20)
start_time = models.CharField(max_length=50)
end_time = models.CharField(max_length=50)
params = models.CharField(max_length=50, blank=True, null=True)
metrics = models.CharField(max_length=256, null=True, blank=True)
@classmethod
def from_json(cls, json_info):
"""Build a Trial instance from a json string."""
if json_info is None:
return None
return TrialRecord(
trial_id=json_info["trial_id"],
job_id=json_info["job_id"],
trial_status=json_info["status"],
start_time=json_info["start_time"],
params=json_info["params"])
class ResultRecord(models.Model):
"""Information of a single result of a trial."""
trial_id = models.CharField(max_length=50)
timesteps_total = models.BigIntegerField(blank=True, null=True)
done = models.CharField(max_length=30, blank=True, null=True)
episode_reward_mean = models.CharField(
max_length=30, blank=True, null=True)
mean_accuracy = models.FloatField(blank=True, null=True)
mean_loss = models.FloatField(blank=True, null=True)
trainning_iteration = models.BigIntegerField(blank=True, null=True)
timesteps_this_iter = models.BigIntegerField(blank=True, null=True)
time_this_iter_s = models.BigIntegerField(blank=True, null=True)
time_total_s = models.BigIntegerField(blank=True, null=True)
date = models.CharField(max_length=30, blank=True, null=True)
hostname = models.CharField(max_length=50, blank=True, null=True)
node_ip = models.CharField(max_length=50, blank=True, null=True)
config = models.CharField(max_length=256, blank=True, null=True)
@classmethod
def from_json(cls, json_info):
"""Build a Result instance from a json string."""
if json_info is None:
return None
return ResultRecord(
trial_id=json_info["trial_id"],
timesteps_total=json_info["timesteps_total"],
done=json_info.get("done", None),
episode_reward_mean=json_info.get("episode_reward_mean", None),
mean_accuracy=json_info.get("mean_accuracy", None),
mean_loss=json_info.get("mean_loss", None),
trainning_iteration=json_info.get("training_iteration", None),
timesteps_this_iter=json_info.get("timesteps_this_iter", None),
time_this_iter_s=json_info.get("time_this_iter_s", None),
time_total_s=json_info.get("time_total_s", None),
date=json_info.get("date", None),
hostname=json_info.get("hostname", None),
node_ip=json_info.get("node_ip", None),
config=json_info.get("config", None))
+120
View File
@@ -0,0 +1,120 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import re
import django
import argparse
from django.core.management import execute_from_command_line
from common.exception import DatabaseError
root_path = os.path.dirname(os.path.abspath(__file__))
def run_board(args):
"""
Run main entry for AutoMLBoard.
Args:
args: args parsed from command line
"""
init_config(args)
# backend service, should import after django settings initialized
from backend.collector import CollectorService
service = CollectorService(
args.logdir,
args.reload_interval,
standalone=False,
log_level=args.log_level)
service.run()
# frontend service
print("Try to start automlboard on port %s\n" % args.port)
command = [
os.path.join(root_path, 'manage.py'), 'runserver',
'0.0.0.0:%s' % args.port, '--noreload'
]
execute_from_command_line(command)
def init_config(args):
"""
Initialize configs of the service.
Do the following things:
1. automl board settings
2. database settings
3. django settings
"""
os.environ["AUTOMLBOARD_LOGDIR"] = args.logdir
os.environ["AUTOMLBOARD_LOGLEVEL"] = args.log_level
os.environ["AUTOMLBOARD_RELOAD_INTERVAL"] = str(args.reload_interval)
if args.db:
try:
db_address_reg = re.compile(r"(.*)://(.*):(.*)@(.*):(.*)/(.*)")
match = re.match(db_address_reg, args.db_address)
os.environ["AUTOMLBOARD_DB_ENGINE"] = match.group(1)
os.environ["AUTOMLBOARD_DB_USER"] = match.group(2)
os.environ["AUTOMLBOARD_DB_PASSWORD"] = match.group(3)
os.environ["AUTOMLBOARD_DB_HOST"] = match.group(4)
os.environ["AUTOMLBOARD_DB_PORT"] = match.group(5)
os.environ["AUTOMLBOARD_DB_NAME"] = match.group(6)
print("Using %s as the database backend." % match.group(1))
except BaseException as e:
raise DatabaseError(e)
else:
print("Using sqlite3 as the database backend, "
"information will be stored in automlboard.db")
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"ray.tune.automlboard.settings")
django.setup()
command = [os.path.join(root_path, 'manage.py'), 'migrate', '--run-syncdb']
execute_from_command_line(command)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--logdir",
type=str,
required=True,
help="Directory where AutoML Board will "
"look to find tuning logs it can display")
parser.add_argument(
"--port",
type=int,
default=8008,
help="What port to serve AutoMLBoard on, "
"(default: %(default)s)")
parser.add_argument(
"--db",
type=str,
default=None,
help="Set SQL database URI in "
"schema://user:password@host:port/database, "
"(default: sqlite3)"),
parser.add_argument(
"--reload_interval",
type=int,
default=5,
help="How often the backend should load more data, "
"(default: %(default)s)")
parser.add_argument(
"--log_level",
type=str,
default="INFO",
help="Set the logging level, "
"(default: %(default)s)")
cmd_args = parser.parse_args()
run_board(cmd_args)
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
"""
Django settings for monitor project.
Generated by 'django-admin startproject' using Django 1.11.14.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
# Build paths inside the project like this:
# os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# You can specify your own secret key, here we just pick one randomly.
SECRET_KEY = 'tktks103=$7a#5axn)52&b87!#w_qm(%*72^@hsq!nur%dtk4b'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ray.tune.automlboard.models',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'ray.tune.automlboard.frontend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR + "/templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'ray.tune.automlboard.frontend.wsgi.application'
DB_ENGINE_NAME_MAP = {
"mysql": "django.db.backends.mysql",
"sqllite": "django.db.backends.sqlite3"
}
def lookup_db_engine(name):
"""Lookup db engine class name for engine name."""
return DB_ENGINE_NAME_MAP.get(name, DB_ENGINE_NAME_MAP["sqllite"])
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
if not os.environ.get("AUTOMLBOARD_DB_ENGINE", None):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'automlboard.db',
}
}
else:
DATABASES = {
'default': {
'ENGINE': lookup_db_engine(os.environ["AUTOMLBOARD_DB_ENGINE"]),
'NAME': os.environ["AUTOMLBOARD_DB_NAME"],
'USER': os.environ["AUTOMLBOARD_DB_USER"],
"PASSWORD": os.environ["AUTOMLBOARD_DB_PASSWORD"],
"HOST": os.environ["AUTOMLBOARD_DB_HOST"],
"PORT": os.environ["AUTOMLBOARD_DB_PORT"]
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
VALIDATION_PREFIX = "django.contrib.auth.password_validation."
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': VALIDATION_PREFIX + "UserAttributeSimilarityValidator",
},
{
'NAME': VALIDATION_PREFIX + "MinimumLengthValidator",
},
{
'NAME': VALIDATION_PREFIX + "CommonPasswordValidator",
},
{
'NAME': VALIDATION_PREFIX + "NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static').replace('\\', '/'), )
# automlboard settings
AUTOMLBOARD_LOG_DIR = os.environ.get("AUTOMLBOARD_LOGDIR", None)
AUTOMLBOARD_RELOAD_INTERVAL = os.environ.get("AUTOMLBOARD_RELOAD_INTERVAL",
None)
AUTOMLBOARD_LOG_LEVEL = os.environ.get("AUTOMLBOARD_LOGLEVEL", None)
@@ -0,0 +1,158 @@
body {
font-size: 14px;
}
input[type=text], textarea {
font-size: 14px;
padding: 5px 10px;
border-radius: 4px;
border: 1px solid #ccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
}
::-webkit-input-placeholder {
opacity: 0.6;
}
:-ms-input-placeholder {
opacity: 0.6;
}
::-ms-input-placeholder {
opacity: 0.6;
}
::placeholder {
opacity: 0.6;
}
button, .btn {
font-size: 14px;
background-color: #f5f5f5;
border-color: #cccccc;
}
button:hover, .btn:hover {
border-color: #c0c0c0;
}
a {
color: #017fcb;
}
a:hover, a:focus {
color: #015693;
}
.btn-primary {
background-color: #0193e1;
border-color: #0193e1;
}
.btn-primary:hover {
background-color: #017fcb;
border-color: #017fcb;
}
.btn-primary[disabled], .btn-primary[disabled]:hover {
background-color: #0193e1;
border-color: #0193e1;
opacity: 0.4;
}
.App-header {
background-color: #082142;
height: 60px;
color: white;
display: block;
}
.App-header-text {
position: relative;
top: 40%;
}
.App-experiments {
width: 200px;
}
.App-content {
width: 80%;
margin-right: auto;
margin-left: auto;
}
div.mlflow-logo {
display: inline-block;
}
img.mlflow-logo {
height: 40px;
margin-left: 64px;
margin-top: 10px;
margin-bottom: 10px;
}
div.github {
display: inline-block;
padding-right: 24px;
color: #ffffff;
}
div.docs {
display: inline-block;
color: #ffffff;
}
div.header-links {
display: inline-block;
float: right;
padding-top: 21px;
padding-right: 10%;
font-size: 16px;
}
h1 {
margin-top: 32px;
font-size: 24px;
font-weight: bold;
color: #333;
}
h2 {
font-size: 18px;
font-weight: normal;
color: #333;
}
label {
font-size: 14px;
font-weight: normal;
color: #333;
margin: 0;
}
div.metadata {
margin-top: 32px;
}
span.metadata {
font-size: 16px;
font-weight: normal;
white-space: nowrap;
margin-right: 100px;
}
span.metadata-header {
font-size: 16px;
font-weight: normal;
color: #888;
margin-right: 4px;
}
table th {
background-color: #fafafa;
color: #888888;
font-weight: 500;
}
@@ -0,0 +1,104 @@
.experiment-list-outer-container {
padding-left: 64px;
}
.experiment-list-container {
overflow-y: scroll;
overflow-x: hidden;
width: 236px;
min-height: 100%;
}
.active-experiment-list-item {
background: rgba(67, 199, 234, 0.1);
font-weight: bold;
}
.experiment-list-item {
overflow:hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16px;
height: 40px;
width: 220px;
line-height: 40px;
padding-left: 12px;
}
.experiments-header {
font-weight: normal;
display: inline-block;
padding-bottom: 6px;
}
.collapser-container {
display: inline-block;
position: relative;
top: -2px;
}
.collapser {
display: inline-block;
background-color: #082142d6;
color: #FFFFFF;
font-size: 16px;
line-height: 24px;
width: 24px;
height: 24px;
text-align: center;
margin-left: 68px;
}
.login-icon {
background-color: #E95420;
}
.login-icon:hover {
background-color: #AEA79F;
}
.fa, .fas {
font-weight: 900;
padding-top: 3px;
}
.collapsed {
display: none; /* hide it for small displays */
}
@media (min-width: 992px) {
.collapsed {
display: block;
margin-left: -18%; /* same width as sidebar */
}
}
#row-main {
overflow-x: hidden; /* necessary to hide collapsed sidebar */
}
#sidebar {
-webkit-transition: margin 0.3s ease;
-moz-transition: margin 0.3s ease;
-o-transition: margin 0.3s ease;
transition: margin 0.3s ease;
}
#content {
-webkit-transition: width 0.3s ease;
-moz-transition: width 0.3s ease;
-o-transition: width 0.3s ease;
transition: width 0.3s ease;
}
.experiment-list-container .nav .nav-item .nav-link:hover {
padding-left: 25px;
margin-right: 25px;
color: #0193e1;
background-color: #e9ecef;
}
.nav-item .nav-link {
color: #888888;
}
@@ -0,0 +1,175 @@
.ExperimentView input[type=checkbox] {
width: auto;
}
.ExperimentView th {
background-color: #fafafa;
color: #888888;
font-weight: 500;
}
.ExperimentView td, .ExperimentView th {
border-top: 1px solid #e2e2e2;
border-bottom: 1px solid #e2e2e2;
}
.ExperimentView th.top-row {
text-align: center;
border-bottom: none;
border-top: none;
}
.ExperimentView th.bottom-row {
text-align: left;
border-top: none;
}
.ExperimentView .left-border {
border-left: 1px solid #e2e2e2;
}
.ExperimentView-run-buttons .btn {
margin-left: 16px;
}
.ExperimentView-run-buttons .run-count {
font-size: 14px;
color: #888888;
}
.ExperimentView-evenRow {
background-color: #bbbbbb;
}
.ExperimentView-evenRow:hover {
background-color: #acacac;
}
.ExperimentView-oddRow:hover {
background-color: #e1e1e1;
}
.ExperimentView-downloadCsv {
float: right;
}
.ExperimentView-search-controls {
margin-top: 30px;
}
.ExperimentView-run-buttons{
margin-top: 30px;
margin-bottom: 30px;
}
.ExperimentView-paramKeyFilter, .ExperimentView-metricKeyFilter {
display: inline-block;
width: 100%;
min-width: 210px;
margin-top: 16px;
}
.ExperimentView-paramKeyFilter, .ExperimentView-metricKeyFilter, .ExperimentView-search {
padding-right: 16px;
}
.ExperimentView-search-buttons {
float: right;
width: 100px;
}
.ExperimentView-search-buttons .btn {
display: block;
width: 100%;
margin-bottom: 12px;
}
.ExperimentView-search-inputs {
margin-right: 100px;
}
.ExperimentView-search-controls .filter-label {
width: 110px;
float: left;
margin-top: 6px;
}
.ExperimentView-search-controls .filter-wrapper {
margin-left: 110px;
}
.ExperimentView-search-controls input {
width: 100%;
}
.search-button {
margin-right: 30px;
}
div.error-message {
margin-left: 100px;
/*width: auto;*/
}
span.error-message {
color: red;
}
.metric-filler-bg {
position: relative;
}
.metric-filler-fg {
background-color: #def1ff;
position: absolute;
left: -3px;
top: -1px;
height: 22px;
}
.metric-text {
position: relative;
}
.fixed-table-container {
border: none;
}
.fixed-table-toolbar .btn-group .keep-open .btn-secondary {
color: #868e96;
}
.fixed-table-toolbar .btn-group .show .btn-secondary {
color: #fff;
}
.fixed-table-toolbar .btn-group .keep-open .btn-secondary:hover {
background-color: #9c948a;
color: #fff;
}
.fixed-table-container .fixed-table-body {
overflow-x: auto;
overflow-y: auto;
height: 50%;
}
.page-list .btn-group .btn-secondary {
color: #868e96;
}
.page-list .btn-group .btn-secondary:hover {
background-color: #9c948a;
color: #fff;
}
hr.divider {
-moz-border-bottom-colors: none;
-moz-border-image: none;
-moz-border-left-colors: none;
-moz-border-right-colors: none;
-moz-border-top-colors: none;
border-color: #EEEEEE -moz-use-text-color #FFFFFF;
border-style: solid none;
border-width: 1px 0;
margin: 18px 0;
}
@@ -0,0 +1,36 @@
.outer-container {
display: -ms-flexbox;
display: flex;
}
.HomePage-experiment-list-container {
width: 10%;
min-width: 333px;
}
.experiment-view-container {
width: 80%;
}
.experiment-view-right {
width: 10%;
}
/* BEGIN css for when experiment list collapsed */
.experiment-page-container {
width: 80%;
margin: 0 auto;
}
.collapsed-expander-container {
float: left;
}
.expander {
display: inline-block;
background-color: #082142d6;
color: #FFFFFF;
font-size: 16px;
line-height: 24px;
width: 24px;
height: 24px;
text-align: center;
vertical-align: bottom;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.fa-chevron-left:before {
content: "\f053";
}
@@ -0,0 +1,11 @@
function collapse_experiment_list() {
$("#sidebar").toggleClass("collapsed");
$("#content").toggleClass("col-md-8");
$(".collapser").toggleClass("fa-chevron-left fa-chevron-right");
var over_flow_attr = $(".experiment-list-container").css("overflow-y");
if (over_flow_attr == "scroll") {
$(".experiment-list-container").css("overflow-y", "visible")
} else {
$(".experiment-list-container").css("overflow-y", "scroll")
}
}
@@ -0,0 +1,157 @@
<html>
<head>
<title>AutoMLBoard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load staticfiles %}
<!-- jquery and bootstrap dependency -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
<!-- bootstrap table dependency -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.js"></script>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/App.css">
<link rel="stylesheet" href="/static/css/HomePage.css">
<link rel="stylesheet" href="/static/css/ExperimentView.css">
<script src="/static/js/ExperimentList.js"></script>
<link rel="stylesheet" href="/static/css/ExperimentList.css">
<!-- awesome dependency -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" style="margin-right: auto;margin-left: auto;">
<a class="navbar-brand" href="#" style="padding-left: 50px">AutoMLBoard</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/ray-project/ray">Github <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://ray.readthedocs.io/">Document</a>
</li>
</ul>
</div>
</nav>
<div class="container" style="max-width: none">
<div class="outer-container row" id = "row-main">
<div class="HomePage-experiment-list-container col-md-2 collapsed" id="sidebar">
<div>
<div class="collapsed-expander-container">
<div class="experiment-list-outer-container">
<div><h1 class="experiments-header">Experiments</h1>
<div class="collapser-container" onclick="collapse_experiment_list()">
<i class="collapser fa fa-chevron-right login-icon"></i>
</div>
<div class="experiment-list-container" style="height: 800px; overflow-y: visible">
<ul class="nav nav-pills flex-column">
{% for job in recent_jobs %}
<tr>
<li class="nav-item">
<a class="nav-link" href="job?job_id={{ job.job_id }}">{{ job.job_id }}</a>
</li>
</tr>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-container" id="content">
<div class="ExperimentPage">
<div>
<div class="ExperimentView">
<h1>Summary</h1>
<hr class="divider"/>
<span class="metadata" style="line-height: 40px">
<span class="metadata-header">Log Path: </span>
{{ log_dir }}
</span>
<br>
<span class="metadata" style="line-height: 40px">
<span class="metadata-header">Reload Interval: </span>
{{ reload_interval }} s
</span>
</div>
<div class="ExperimentView-runs">
<div class="metadata" style="max-width: 900px; margin-top: 20px">
<span class="metadata">
<span class="metadata-header">Jobs:</span>
{{ job_num }}
</span>
<span class="metadata" style="margin-right: 0px">
<span class="metadata-header">Trials:
</span>
<span>{{ trial_num }} </span>
<span class="badge badge-pill badge-info"
style="margin-left: 10px; border-radius: 0.3em">{{ running_num }} Running</span>
<span class="badge badge-pill badge-success"
style="border-radius: 0.3em">{{ success_num }} Success</span>
<span class="badge badge-pill badge-danger"
style="border-radius: 0.3em">{{ failed_num }} Failed</span>
</span>
</div>
<table class="table table-hover"
id="job_table"
data-toggle="table"
data-show-columns="true"
data-minimum-count-columns="2"
data-id-field="id"
data-page-list="[10, 25, 50, 100, ALL]"
style="border: none; max-height: 800px">
<thead>
<tr>
<th class="bottom-row" data-field="Job ID" data-sortable="true">Job ID</th>
<th class="bottom-row" data-field="User" data-sortable="true">User</th>
<th class="bottom-row" data-field="Start Time" data-sortable="true">Start Time</th>
<th class="bottom-row" data-field="Status" data-sortable="true">Status (Succ / Run / Fail / Total)</th>
<th class="bottom-row" data-field="Progress" data-sortable="true">Progress</th>
<th class="bottom-row" data-field="Winner Trial" data-sortable="true">Winner Trial</th>
<th class="bottom-row" data-field="Winner Metric" data-sortable="true">Winner Metric</th>
</tr>
</thead>
<tbody>
{% for job in recent_jobs %}
<tr>
<td><a href="job?job_id={{ job.job_id }}">{{ job.job_id }}</a></td>
<td>{{ job.user }}</td>
<td>{{ job.start_time }}</td>
<td>{{ job.success_num }} / {{ job.running_num }}/ {{ job.failed_num }} / {{ job.total_num }}</td>
<td>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ job.progress }}%;">
<span class="sr-only">{{ job.progress }}</span>
</div>
</div>
</td>
<td><a href="trial?trial_id={{ job.winner.trial_id }}&job_id={{ job.job_id }}">{{ job.winner.trial_id }}</td>
<td>{{ job.winner.metric}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-right"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,161 @@
<html>
<head>
<title>AutoMLBoard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load staticfiles %}
<!-- jquery and bootstrap dependency -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
<!-- bootstrap table dependency -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.js"></script>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/App.css">
<link rel="stylesheet" href="/static/css/HomePage.css">
<link rel="stylesheet" href="/static/css/ExperimentView.css">
<script src="/static/js/ExperimentList.js"></script>
<link rel="stylesheet" href="/static/css/ExperimentList.css">
<!-- awesome dependency -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" style="margin-right: auto;margin-left: auto;">
<a class="navbar-brand" href="#" style="padding-left: 50px">AutoMLBoard</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/ray-project/ray">Github</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://ray.readthedocs.io/">Document</a>
</li>
</ul>
</div>
</nav>
<div class="container" style="max-width: none">
<div class="outer-container row" id = "row-main">
<div class="HomePage-experiment-list-container col-md-2" id="sidebar">
<div>
<div class="collapsed-expander-container">
<div class="experiment-list-outer-container">
<div><h1 class="experiments-header">Experiments</h1>
<div class="collapser-container" onclick="collapse_experiment_list()">
<i class="collapser fa fa-chevron-left login-icon"></i>
</div>
<div class="experiment-list-container" style="height: 800px;">
<ul class="nav nav-pills flex-column">
{% for job in recent_jobs %}
<tr>
<li class="nav-item">
<a class="nav-link" href="job?job_id={{ job.job_id }}">{{ job.job_id }}</a>
</li>
</tr>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-container col-md-8" id="content">
<div class="ExperimentPage">
<div>
<div class="ExperimentView">
<h1>{{ current_job.job_id }}</h1>
<hr class="divider"/>
<div class="metadata" style="max-width: 900px;">
<span class="metadata">
<span class="metadata-header">User:</span>
{{ current_job.user }}
</span>
<span class="metadata" style="margin-right: 0px">
<span class="metadata-header">Progress:
</span>
<span>{{ current_job.total_num }} Trials</span>
<span class="badge badge-pill badge-info" style="margin-left: 10px; border-radius: 0.3em">{{ current_job.running_num }} Running</span>
<span class="badge badge-pill badge-success" style="border-radius: 0.3em">{{ current_job.success_num }} Success</span>
<span class="badge badge-pill badge-danger" style="border-radius: 0.3em">{{ current_job.failed_num }} Failed</span>
<span class="progress"
style="width: 150px; float: right; margin-right: 120px; margin-top: 5px">
<span class="progress-bar bg-success" role="progressbar" style="width: {{ current_job.progress }}%;"></span>
</span>
</span>
<span class="metadata" style="line-height: 40px">
<span class="metadata-header">Start Time:</span>
{{ current_job.start_time }}
</span>
</div>
<div class="ExperimentView-runs">
<hr class="divider"/>
<table class="table table-hover"
id="trial_table"
data-toggle="table"
data-show-columns="true"
data-show-export="true"
data-minimum-count-columns="2"
data-id-field="id"
data-show-pagination-switch="true"
data-page-list="[10, 25, 50, 100, ALL]"
data-pagination="true"
style="border: none; max-height: 800px">
<thead>
<tr>
<th class="top-row" scope="colgroup" colspan="4">Trials</th>
<th class="top-row left-border" scope="colgroup" colspan="{{ param_num }}">Parameters</th>
<th class="top-row left-border" scope="colgroup" colspan="{{ metric_num }}">Metrics</th>
</tr>
<tr>
<th class="bottom-row" data-field="Trial-ID" data-sortable="true">Trial-ID</th>
<th class="bottom-row" data-field="Status" data-sortable="true">Status</th>
<th class="bottom-row" data-field="Start Time" data-sortable="true">Start Time</th>
<th class="bottom-row" data-field="End Time" data-sortable="true">End Time</th>
{% for param in param_keys %}
<th class="bottom-row" data-field="{{ param }}" data-sortable="true">{{ param }}</th>
{% endfor %}
{% for metric in metric_keys %}
<th class="bottom-row" data-field="{{ metric }}"
data-sortable="true">{{ metric }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for trial in recent_trials %}
<tr>
<td><a href="/trial?job_id={{ trial.job_id }}&trial_id={{ trial.trial_id }}">{{ trial.trial_id }}</a></td>
<td>{{ trial.trial_status}} <!--a href="#">(Kill)</a--></td>
<td>{{ trial.start_time }}</td>
<td>{{ trial.end_time }}</td>
{% for param in trial.params.items %}
<td>{{ param.1 }}</td>
{% endfor %}
<td>{{ trial.metrics.episode_reward }}</td>
<td>{{ trial.metrics.loss }}</td>
<td>{{ trial.metrics.accuracy }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-right"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,157 @@
<html>
<head>
<title>AutoMLBoard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load staticfiles %}
<!-- jquery and bootstrap dependency -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
<!-- bootstrap table dependency -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.12.1/bootstrap-table.min.js"></script>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/App.css">
<link rel="stylesheet" href="/static/css/HomePage.css">
<link rel="stylesheet" href="/static/css/ExperimentView.css">
<script src="/static/js/ExperimentList.js"></script>
<link rel="stylesheet" href="/static/css/ExperimentList.css">
<!-- awesome dependency -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" style="margin-right: auto;margin-left: auto;">
<a class="navbar-brand" href="#" style="padding-left: 50px">AutoMLBoard</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/ray-project/ray">Github</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://ray.readthedocs.io/">Document</a>
</li>
</ul>
</div>
</nav>
<div class="container" style="max-width: none">
<div class="outer-container row" id = "row-main">
<div class="HomePage-experiment-list-container col-md-2" id="sidebar">
<div>
<div class="collapsed-expander-container">
<div class="experiment-list-outer-container">
<div><h1 class="experiments-header" style="padding-right: 80px">Trials</h1>
<div class="collapser-container" onclick="collapse_experiment_list()">
<i class="collapser fa fa-chevron-left login-icon"></i>
</div>
<div class="experiment-list-container" style="height: 800px;">
<ul class="nav nav-pills flex-column">
{% for trial in recent_trials %}
<tr>
<li class="nav-item">
<a class="nav-link" href="trial?job_id={{ trial.job_id }}&trial_id={{ trial.trial_id }}">Trial-{{ trial.trial_id }}</a>
</li>
</tr>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-container col-md-8" id="content">
<div class="ExperimentPage">
<div>
<div class="ExperimentView">
<h1>Trial-{{ current_trial.trial_id }}</h1>
<hr class="divider"/>
<div class="metadata" style="max-width: 900px;">
<span class="metadata">
<span class="metadata-header">Status:</span>
{{ current_trial.trial_status }}
</span>
<br>
<span class="metadata" style="line-height: 40px">
<span class="metadata-header">Start Time:</span>
{{ current_trial.start_time }}
</span>
<span class="metadata" style="line-height: 40px">
<span class="metadata-header">End Time:</span>
{{ current_trial.end_time }}
</span>
<br>
</div>
<div class="metadata" style="margin-top: 20px">
<button disabled="" type="button" class="btn btn-default"
style="width: 80px; max-height: 35px; margin-right: 10px">Kill</button>
<a href="/job?job_id={{ job_id }}">
<button type="button" class="btn-primary btn btn-default"
style="width: 80px; max-height: 35px; margin-right: 20px">Return</button>
</a>
</div>
<hr class="divider"/>
<div class="ExperimentView-runs">
<table class="table table-hover"
id="trial_table"
data-toggle="table"
data-show-columns="true"
data-show-export="true"
data-minimum-count-columns="2"
data-id-field="id"
data-show-pagination-switch="true"
data-page-list="[10, 25, 50, 100, ALL]"
data-pagination="true"
style="border: none; max-height: 800px">
<thead>
<tr class="active">
<th class="bottom-row" data-field="Trial ID" data-sortable="true">Trial Id</th>
<th class="bottom-row" data-field="Timesteps" data-sortable="true">Timesteps</th>
<th class="bottom-row" data-field="Train Iteration" data-sortable="true">Train Iteration</th>
<th class="bottom-row" data-field="Episode Reward Mean" data-sortable="true">Episode Reward Mean</th>
<th class="bottom-row" data-field="Episodes Total" data-sortable="true">Episodes Total</th>
<th class="bottom-row" data-field="Mean Accuracy" data-sortable="true">Mean Accuracy</th>
<th class="bottom-row" data-field="Mean Loss" data-sortable="true">Mean Loss</th>
<th class="bottom-row" data-field="Time Total" data-sortable="true">Time Total</th>
<th class="bottom-row" data-field="Date" data-sortable="true">Date</th>
<th class="bottom-row" data-field="Hostname" data-sortable="true">Hostname</th>
</tr>
</thead>
<tbody>
{% for result in recent_results %}
<tr>
<td>{{ result.trial_id }}</td>
<td>{{ result.timesteps_total }}</td>
<td>{{ result.trainning_iteration }}</td>
<td>{{ result.episode_reward_mean }}</td>
<td>{{ result.episodes_total }}</td>
<td>{{ result.mean_accuracy }}</td>
<td>{{ result.loss }}</td>
<td>{{ result.time_total_s }}</td>
<td>{{ result.date }}</td>
<td>{{ result.hostname }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="experiment-view-right"></div>
</div>
</div>
</body>
</html>
+17
View File
@@ -34,3 +34,20 @@ TRAINING_ITERATION = "training_iteration"
# Where Tune writes result files by default
DEFAULT_RESULTS_DIR = os.path.expanduser("~/ray_results")
# Meta file about status under each experiment directory, can be
# parsed by automlboard if exists.
JOB_META_FILE = "job_status.json"
# Meta file about status under each trial directory, can be parsed
# by automlboard if exists.
EXPR_META_FILE = "trial_status.json"
# File that stores parameters of the trial.
EXPR_PARARM_FILE = "params.json"
# File that stores the progress of the trial.
EXPR_PROGRESS_FILE = "progress.csv"
# File that stores results of the trial.
EXPR_RESULT_FILE = "result.json"