mirror of
https://github.com/wassname/ray.git
synced 2026-07-07 04:30:30 +08:00
[tune] Add AutoMLBoard: Monitoring UI (experimental) (#2574)
This commit is contained in:
committed by
Richard Liaw
parent
b6260003cb
commit
3813ae34b3
@@ -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
|
||||
@@ -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()
|
||||
Executable
+13
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user