kalfa 3.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from kalfa.commands.sweep.space import SPACE_HINT, space_values
|
|
2
|
+
from kalfa.commands.sweep.strategies import STRATEGY_HINT, Grid, strategy_of
|
|
3
|
+
from kalfa.core.check import SERIES_PREFIXES
|
|
4
|
+
from kalfa.core.context import train_of
|
|
5
|
+
from kalfa.core.errors import BuildError, error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SWEEP_KEYS = ("strategy", "space", "objective", "record")
|
|
9
|
+
OBJECTIVE_KEYS = ("watch", "mode", "at")
|
|
10
|
+
MODES = ("min", "max")
|
|
11
|
+
MOMENTS = ("best", "last")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def series_problem(tree, watch):
|
|
15
|
+
names = set(tree.section("losses")) | set(tree.section("metrics"))
|
|
16
|
+
prefixes = set(SERIES_PREFIXES)
|
|
17
|
+
evaluate = train_of(tree).get("evaluate")
|
|
18
|
+
if isinstance(evaluate, list):
|
|
19
|
+
prefixes.update(str(name) for name in evaluate)
|
|
20
|
+
prefix, _, rest = watch.partition("/")
|
|
21
|
+
if not rest or prefix not in prefixes:
|
|
22
|
+
return f"{watch!r} does not look like a series such as val/<metric>"
|
|
23
|
+
if prefix in ("train", "val", "test") and rest.split("/")[0] not in names:
|
|
24
|
+
known = ", ".join(sorted(names)) or "none"
|
|
25
|
+
return f"{watch!r} names no loss or metric; the losses and metrics are {known}"
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_space(tree, space, problems):
|
|
30
|
+
try:
|
|
31
|
+
dimensions = space_values(space)
|
|
32
|
+
except BuildError as exc:
|
|
33
|
+
problems.append(error("sweep_space", str(exc), "sweep.space", hint=SPACE_HINT))
|
|
34
|
+
return None
|
|
35
|
+
params = getattr(getattr(tree, "config", None), "params", None) or {}
|
|
36
|
+
known = ", ".join(sorted(str(name) for name in params))
|
|
37
|
+
for name in dimensions:
|
|
38
|
+
if name not in params:
|
|
39
|
+
problems.append(error("sweep_param", f"{name!r} is not in the params section; a swept name is a param the "
|
|
40
|
+
f"config reads as ${name}$", "sweep.space",
|
|
41
|
+
hint=f"the params are {known}" if known else "define it under params"))
|
|
42
|
+
return dimensions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_strategy(raw, space, dimensions, problems):
|
|
46
|
+
try:
|
|
47
|
+
strategy = strategy_of(raw)
|
|
48
|
+
except BuildError as exc:
|
|
49
|
+
problems.append(error("sweep_strategy", str(exc), "sweep.strategy", hint=STRATEGY_HINT))
|
|
50
|
+
return
|
|
51
|
+
if dimensions is not None and isinstance(strategy, Grid):
|
|
52
|
+
try:
|
|
53
|
+
strategy.axes(space)
|
|
54
|
+
except BuildError as exc:
|
|
55
|
+
problems.append(error("sweep_space", str(exc), "sweep.space"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_objective(tree, objective, problems):
|
|
59
|
+
where = "sweep.objective"
|
|
60
|
+
if not isinstance(objective, dict):
|
|
61
|
+
problems.append(error("sweep_objective", "sweep.objective is a mapping {watch, mode, at}", where,
|
|
62
|
+
hint="write objective: {watch: val/<metric>, mode: min}"))
|
|
63
|
+
return
|
|
64
|
+
for key in objective:
|
|
65
|
+
if key not in OBJECTIVE_KEYS:
|
|
66
|
+
problems.append(error("sweep_objective", f"{key!r} is not an objective key; the keys are "
|
|
67
|
+
+ ", ".join(OBJECTIVE_KEYS), where))
|
|
68
|
+
watch = objective.get("watch")
|
|
69
|
+
if not isinstance(watch, str) or not watch:
|
|
70
|
+
problems.append(error("sweep_objective", "the objective needs watch, the series that ranks the points", where,
|
|
71
|
+
hint="such as watch: val/rmse"))
|
|
72
|
+
else:
|
|
73
|
+
message = series_problem(tree, watch)
|
|
74
|
+
if message:
|
|
75
|
+
problems.append(error("sweep_objective", message, where))
|
|
76
|
+
mode = objective.get("mode")
|
|
77
|
+
if mode is not None and mode not in MODES:
|
|
78
|
+
problems.append(error("sweep_objective", f"mode is min or max, got {mode!r}", where))
|
|
79
|
+
at = objective.get("at")
|
|
80
|
+
if at is not None and at not in MOMENTS:
|
|
81
|
+
problems.append(error("sweep_objective", f"at is best or last, got {at!r}", where))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_sweep(tree, raw, problems):
|
|
85
|
+
if not isinstance(raw, dict):
|
|
86
|
+
problems.append(error("sweep_key", "sweep is a mapping with strategy, space, objective and record", "sweep"))
|
|
87
|
+
return
|
|
88
|
+
for key in raw:
|
|
89
|
+
if key not in SWEEP_KEYS:
|
|
90
|
+
problems.append(error("sweep_key", f"{key!r} is not a sweep key; the keys are " + ", ".join(SWEEP_KEYS),
|
|
91
|
+
f"sweep.{key}"))
|
|
92
|
+
dimensions = check_space(tree, raw.get("space"), problems)
|
|
93
|
+
check_strategy(raw.get("strategy"), raw.get("space"), dimensions, problems)
|
|
94
|
+
check_objective(tree, raw.get("objective"), problems)
|
|
95
|
+
record = raw.get("record")
|
|
96
|
+
if not isinstance(record, str) or not record.strip():
|
|
97
|
+
problems.append(error("sweep_record", "the sweep needs record, the root directory its points go under",
|
|
98
|
+
"sweep.record", hint="write record: runs/<name>, or give --record"))
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ruamel.yaml.error import YAMLError
|
|
11
|
+
|
|
12
|
+
from kalfa.commands.sweep.points import LOGS, POINTS_FILE, SWEEP_FILE
|
|
13
|
+
from kalfa.commands.sweep.space import is_int
|
|
14
|
+
from kalfa.commands.sweep.strategies import strategy_note, strategy_of
|
|
15
|
+
from kalfa.core import run as running
|
|
16
|
+
from kalfa.core.check import check
|
|
17
|
+
from kalfa.core.errors import KalfaError, ProblemsError
|
|
18
|
+
from kalfa.core.record import atomic, now
|
|
19
|
+
from kalfa.core.spec import parse_value
|
|
20
|
+
from kalfa.records import reader
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MAPPING_NOTE = "# mapping config: run through the python api"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Plan:
|
|
28
|
+
paths: tuple
|
|
29
|
+
sets: tuple
|
|
30
|
+
params: tuple
|
|
31
|
+
root: str
|
|
32
|
+
strategy: object
|
|
33
|
+
space: dict
|
|
34
|
+
objective: dict
|
|
35
|
+
points: list | None = None
|
|
36
|
+
config_name: str = "sweep"
|
|
37
|
+
extra: dict = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def count(self):
|
|
41
|
+
return None if self.points is None else len(self.points)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def adaptive(self):
|
|
45
|
+
return self.points is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def objective_note(raw):
|
|
49
|
+
raw = raw if isinstance(raw, dict) else {}
|
|
50
|
+
return {"watch": raw.get("watch"), "mode": raw.get("mode") or "min", "at": raw.get("at") or "best"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def plan_seed(seed):
|
|
54
|
+
return int(seed) if is_int(seed) else 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plan_of(paths=(), sets=(), params=(), mappings=(), record=None):
|
|
58
|
+
paths = tuple(str(path) for path in paths)
|
|
59
|
+
sets = tuple(running.pairs(sets))
|
|
60
|
+
params = tuple(running.pairs(params, "param"))
|
|
61
|
+
mappings = list(mappings)
|
|
62
|
+
checked = check(paths=paths, sets=list(sets), params=list(params), mappings=mappings)
|
|
63
|
+
problems = [problem for problem in checked.problems if record is None or problem.code != "sweep_record"]
|
|
64
|
+
if any(problem.severity == "error" for problem in problems):
|
|
65
|
+
raise ProblemsError(problems)
|
|
66
|
+
raw = checked.tree.settings.get("sweep") if checked.tree is not None else None
|
|
67
|
+
if not isinstance(raw, dict):
|
|
68
|
+
raise KalfaError("the config has no sweep section; write sweep: {strategy, space, objective, record}")
|
|
69
|
+
seed = plan_seed(checked.config.data.get("seed"))
|
|
70
|
+
strategy = strategy_of(raw.get("strategy"))
|
|
71
|
+
if getattr(strategy, "seed", 0) is None:
|
|
72
|
+
strategy.seed = seed
|
|
73
|
+
space = copy.deepcopy(raw["space"])
|
|
74
|
+
plan = Plan(paths, sets, params, str(record) if record is not None else str(raw["record"]), strategy, space,
|
|
75
|
+
objective_note(raw.get("objective")), config_name=Path(paths[0]).stem if paths else "sweep",
|
|
76
|
+
extra={"mappings": mappings, "seed": seed, "strategy": strategy_note(raw.get("strategy"))})
|
|
77
|
+
if callable(getattr(strategy, "points", None)):
|
|
78
|
+
plan.points = list(strategy.points(space, seed))
|
|
79
|
+
return plan
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def value_text(value):
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
try:
|
|
85
|
+
if parse_value(value) == value:
|
|
86
|
+
return value
|
|
87
|
+
except YAMLError:
|
|
88
|
+
pass
|
|
89
|
+
return json.dumps(value, default=str)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def command_args(plan, index=None, absolute=False, repair=False):
|
|
93
|
+
paths = [os.path.abspath(path) if absolute else path for path in plan.paths] or ["CONFIG"]
|
|
94
|
+
args = ["sweep", *paths]
|
|
95
|
+
for path, value in plan.sets:
|
|
96
|
+
args += ["--set", f"{path}={value_text(value)}"]
|
|
97
|
+
for name, value in plan.params:
|
|
98
|
+
args += ["-p", f"{name}={value_text(value)}"]
|
|
99
|
+
args += ["--record", os.path.abspath(plan.root) if absolute else plan.root]
|
|
100
|
+
if index is not None:
|
|
101
|
+
args += ["--point", str(index)]
|
|
102
|
+
if repair:
|
|
103
|
+
args.append("--repair")
|
|
104
|
+
args.append("--quiet")
|
|
105
|
+
return args
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def script_text(plan):
|
|
109
|
+
indices = [None] if plan.adaptive else list(range(plan.count))
|
|
110
|
+
lines = [shlex.join(["kalfa", *command_args(plan, index, absolute=True)]) for index in indices]
|
|
111
|
+
if not plan.paths:
|
|
112
|
+
return "\n".join(["#!/usr/bin/env bash", MAPPING_NOTE, *(f"# {line}" for line in lines)]) + "\n"
|
|
113
|
+
repair = shlex.join(["kalfa", *command_args(plan, absolute=True, repair=True)])
|
|
114
|
+
return "\n".join(["#!/usr/bin/env bash", "set -uo pipefail", "status=0", *(f"{line} || status=1" for line in lines),
|
|
115
|
+
"# a point that fails leaves the others running; this reruns the failed and missing points:",
|
|
116
|
+
f"# {repair}", 'exit "$status"']) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def condor_arg(text):
|
|
120
|
+
text = str(text).replace('"', '""')
|
|
121
|
+
if any(character.isspace() or character == "'" for character in text):
|
|
122
|
+
return "'" + text.replace("'", "''") + "'"
|
|
123
|
+
return text
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def executable():
|
|
127
|
+
found = shutil.which("kalfa")
|
|
128
|
+
return (found, []) if found else (sys.executable, ["-m", "kalfa.cli"])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def submit_text(plan):
|
|
132
|
+
index = None if plan.adaptive else "$(Process)"
|
|
133
|
+
program, prefix = executable()
|
|
134
|
+
arguments = " ".join(condor_arg(arg) for arg in [*prefix, *command_args(plan, index, absolute=True)])
|
|
135
|
+
logs = os.path.join(os.path.abspath(plan.root), LOGS)
|
|
136
|
+
lines = [f"executable = {program}", f'arguments = "{arguments}"', "getenv = True",
|
|
137
|
+
f"output = {logs}/$(Process).out", f"error = {logs}/$(Process).err", f"log = {logs}/sweep.log",
|
|
138
|
+
f"queue {1 if plan.adaptive else plan.count}"]
|
|
139
|
+
if not plan.paths:
|
|
140
|
+
lines = [MAPPING_NOTE, *(f"# {line}" for line in lines)]
|
|
141
|
+
return "\n".join(lines) + "\n"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def write_text(path, text):
|
|
145
|
+
with atomic(path) as temporary:
|
|
146
|
+
temporary.write_text(text, encoding="utf-8")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def points_text(points):
|
|
150
|
+
return "".join(json.dumps({"id": index, "values": values}, default=str) + "\n"
|
|
151
|
+
for index, values in enumerate(points or []))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def previous_note(root):
|
|
155
|
+
try:
|
|
156
|
+
found = reader.read_json(root, SWEEP_FILE)
|
|
157
|
+
except (OSError, ValueError):
|
|
158
|
+
return {}
|
|
159
|
+
return found if isinstance(found, dict) else {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def write_plan(plan, log=print):
|
|
163
|
+
root = Path(plan.root)
|
|
164
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
(root / LOGS).mkdir(exist_ok=True)
|
|
166
|
+
previous = previous_note(root)
|
|
167
|
+
note = {"format": 1, "strategy": plan.extra.get("strategy") or {"use": type(plan.strategy).__name__.lower()},
|
|
168
|
+
"space": plan.space, "objective": plan.objective, "points": plan.count}
|
|
169
|
+
if plan.adaptive:
|
|
170
|
+
note["trials"] = getattr(plan.strategy, "trials", None)
|
|
171
|
+
note["started"] = previous.get("started") or now()
|
|
172
|
+
note["config_files"] = [os.path.abspath(path) for path in plan.paths]
|
|
173
|
+
note["record"] = plan.root
|
|
174
|
+
written = [root / SWEEP_FILE, root / POINTS_FILE, root / "sweep.sh", root / "sweep.sub"]
|
|
175
|
+
write_text(written[0], json.dumps(note, indent=1, default=str) + "\n")
|
|
176
|
+
if not plan.adaptive or not written[1].exists():
|
|
177
|
+
write_text(written[1], points_text(plan.points))
|
|
178
|
+
write_text(written[2], script_text(plan))
|
|
179
|
+
written[2].chmod(0o755)
|
|
180
|
+
write_text(written[3], submit_text(plan))
|
|
181
|
+
size = f"{plan.count} points" if not plan.adaptive else f"adaptive, {note['trials']} trials"
|
|
182
|
+
log(f"{plan.root}: {size}; " + ", ".join(path.name for path in written))
|
|
183
|
+
return [str(path) for path in written]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from kalfa.commands.sweep.space import is_int
|
|
6
|
+
from kalfa.core.errors import KalfaError
|
|
7
|
+
from kalfa.core.record import STOP_FILE
|
|
8
|
+
from kalfa.records import reader
|
|
9
|
+
from kalfa.records.sweep import objective_of, point_dirs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPAIRED = ("failed", "stopped")
|
|
13
|
+
OUTSIDE_STOPS = ("keyboard", "stop.json")
|
|
14
|
+
SWEEP_FILE = "sweep.json"
|
|
15
|
+
POINTS_FILE = "points.jsonl"
|
|
16
|
+
LOGS = "logs"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def point_dir(root, index):
|
|
20
|
+
return f"{root}/{index:04d}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def manifest_of(directory):
|
|
24
|
+
try:
|
|
25
|
+
found = reader.read_manifest(directory)
|
|
26
|
+
except (OSError, ValueError):
|
|
27
|
+
return None
|
|
28
|
+
return found if isinstance(found, dict) else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stopped_by_rule(directory):
|
|
32
|
+
ends = [line for line in reader.read_lines(Path(directory) / "events.jsonl") if line.get("kind") == "run_end"]
|
|
33
|
+
stopping = ends[-1].get("stopping") if ends else None
|
|
34
|
+
return isinstance(stopping, str) and not stopping.startswith(OUTSIDE_STOPS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def complete(directory, manifest=None):
|
|
38
|
+
manifest = manifest if manifest is not None else manifest_of(directory)
|
|
39
|
+
if manifest is None:
|
|
40
|
+
return False
|
|
41
|
+
status = manifest.get("status")
|
|
42
|
+
return status == "done" or (status == "stopped" and stopped_by_rule(directory))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def abandoned(directory, manifest):
|
|
46
|
+
return reader.state_of(manifest) == "stale" and (Path(directory) / STOP_FILE).is_file()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def repairable(directory):
|
|
50
|
+
manifest = manifest_of(directory)
|
|
51
|
+
if manifest is None:
|
|
52
|
+
return True
|
|
53
|
+
if manifest.get("status") == "running":
|
|
54
|
+
return abandoned(directory, manifest)
|
|
55
|
+
return manifest.get("status") in REPAIRED and not complete(directory, manifest)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_point(directory, index):
|
|
59
|
+
if not directory.is_dir() or not any(directory.iterdir()):
|
|
60
|
+
return
|
|
61
|
+
manifest = manifest_of(directory)
|
|
62
|
+
if manifest is not None and complete(directory, manifest):
|
|
63
|
+
raise KalfaError(f"point {index} is {manifest.get('status')} ({directory}); --repair runs only the failed "
|
|
64
|
+
f"and missing points")
|
|
65
|
+
if manifest is not None and manifest.get("status") == "running" and not abandoned(directory, manifest):
|
|
66
|
+
if reader.state_of(manifest) == "stale":
|
|
67
|
+
raise KalfaError(f"point {index} was running and its heartbeat is stale ({directory}); if its process is "
|
|
68
|
+
f"dead, kalfa stop {directory} marks it and --repair runs it again, or remove the "
|
|
69
|
+
f"directory")
|
|
70
|
+
raise KalfaError(f"point {index} is running ({directory}); kalfa stop {directory} asks it to stop after its "
|
|
71
|
+
f"turn")
|
|
72
|
+
shutil.rmtree(directory)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def recorded_values(plan):
|
|
76
|
+
found = {}
|
|
77
|
+
for directory in point_dirs(plan.root):
|
|
78
|
+
manifest = manifest_of(directory)
|
|
79
|
+
sweep = manifest.get("sweep") if manifest is not None else None
|
|
80
|
+
if isinstance(sweep, dict) and isinstance(sweep.get("values"), dict):
|
|
81
|
+
found[int(directory.name)] = dict(sweep["values"])
|
|
82
|
+
for line in reader.read_lines(Path(plan.root) / POINTS_FILE):
|
|
83
|
+
if is_int(line.get("id")) and isinstance(line.get("values"), dict):
|
|
84
|
+
found[line["id"]] = dict(line["values"])
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def planned_values(plan, index):
|
|
89
|
+
if plan.points is not None:
|
|
90
|
+
if not 0 <= index < len(plan.points):
|
|
91
|
+
raise KalfaError(f"point {index} is not in the plan of {plan.root}, which has {len(plan.points)} points")
|
|
92
|
+
return plan.points[index]
|
|
93
|
+
found = recorded_values(plan).get(index)
|
|
94
|
+
if found is None:
|
|
95
|
+
raise KalfaError(f"{plan.root}: the strategy is adaptive and point {index} was never asked; run the sweep "
|
|
96
|
+
f"without --point")
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def next_index(plan):
|
|
101
|
+
known = [int(directory.name) for directory in point_dirs(plan.root)]
|
|
102
|
+
known += [line["id"] for line in reader.read_lines(Path(plan.root) / POINTS_FILE) if is_int(line.get("id"))]
|
|
103
|
+
return max(known) + 1 if known else 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def append_point(root, index, values):
|
|
107
|
+
with open(Path(root) / POINTS_FILE, "a", encoding="utf-8") as handle:
|
|
108
|
+
handle.write(json.dumps({"id": index, "values": values}, default=str) + "\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def recorded_points(plan):
|
|
112
|
+
points = []
|
|
113
|
+
for index, values in sorted(recorded_values(plan).items()):
|
|
114
|
+
directory = Path(point_dir(plan.root, index))
|
|
115
|
+
summary = reader.summary_of(directory) if directory.is_dir() else None
|
|
116
|
+
failed = not isinstance(summary, dict) or summary.get("status") == "failed"
|
|
117
|
+
points.append((values, None if failed else objective_of(summary, plan.objective)))
|
|
118
|
+
return points
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def failed_points(plan):
|
|
122
|
+
indices = range(len(plan.points)) if plan.points is not None else sorted(recorded_values(plan))
|
|
123
|
+
return [index for index in indices if repairable(Path(point_dir(plan.root, index)))]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from kalfa.commands.sweep.plan import command_args, write_plan
|
|
9
|
+
from kalfa.commands.sweep.points import (
|
|
10
|
+
LOGS,
|
|
11
|
+
OUTSIDE_STOPS,
|
|
12
|
+
SWEEP_FILE,
|
|
13
|
+
clear_point,
|
|
14
|
+
manifest_of,
|
|
15
|
+
planned_values,
|
|
16
|
+
point_dir,
|
|
17
|
+
recorded_points,
|
|
18
|
+
stopped_by_rule,
|
|
19
|
+
)
|
|
20
|
+
from kalfa.commands.sweep.space import is_int
|
|
21
|
+
from kalfa.core import run as running
|
|
22
|
+
from kalfa.records import reader
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
LOGGER = logging.getLogger("kalfa.commands")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def silent(text):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def say_of(monitor):
|
|
33
|
+
say = getattr(monitor, "say", None)
|
|
34
|
+
return say if callable(say) else silent
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_point(plan, index, values=None, monitor=None):
|
|
38
|
+
index = int(index)
|
|
39
|
+
if values is None:
|
|
40
|
+
values = planned_values(plan, index)
|
|
41
|
+
if not (Path(plan.root) / SWEEP_FILE).exists():
|
|
42
|
+
write_plan(plan, log=say_of(monitor))
|
|
43
|
+
directory = Path(point_dir(plan.root, index))
|
|
44
|
+
clear_point(directory, index)
|
|
45
|
+
return running.run(paths=plan.paths, sets=[*plan.sets, ("sweep.record", plan.root)],
|
|
46
|
+
params=[*plan.params, *values.items()], mappings=plan.extra.get("mappings", ()),
|
|
47
|
+
record=str(directory), monitor=monitor,
|
|
48
|
+
sweep={"root": plan.root, "id": index, "values": values})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def values_text(values):
|
|
52
|
+
return " ".join(f"{key}={value}" for key, value in values.items())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def interrupted(result):
|
|
56
|
+
stopping = getattr(getattr(result, "context", None), "stopping", None)
|
|
57
|
+
return isinstance(stopping, str) and stopping.startswith(OUTSIDE_STOPS)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def child(plan, index, logs, halt):
|
|
61
|
+
if halt.is_set():
|
|
62
|
+
return index, None
|
|
63
|
+
command = [sys.executable, "-m", "kalfa.cli", *command_args(plan, index)]
|
|
64
|
+
with open(logs / f"{index:04d}.log", "w", encoding="utf-8") as out:
|
|
65
|
+
code = subprocess.run(command, stdout=out, stderr=subprocess.STDOUT, check=False).returncode
|
|
66
|
+
directory = Path(point_dir(plan.root, index))
|
|
67
|
+
manifest = manifest_of(directory)
|
|
68
|
+
if code == 0 and manifest is not None and manifest.get("status") == "stopped" and not stopped_by_rule(directory):
|
|
69
|
+
halt.set()
|
|
70
|
+
return index, code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def in_children(plan, indices, jobs, log):
|
|
74
|
+
logs = Path(plan.root) / LOGS
|
|
75
|
+
logs.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
halt = threading.Event()
|
|
77
|
+
results, skipped = [], 0
|
|
78
|
+
with ThreadPoolExecutor(max_workers=jobs) as pool:
|
|
79
|
+
for index, code in pool.map(lambda index: child(plan, index, logs, halt), indices):
|
|
80
|
+
directory = point_dir(plan.root, index)
|
|
81
|
+
output = logs / f"{index:04d}.log"
|
|
82
|
+
if code is None:
|
|
83
|
+
skipped += 1
|
|
84
|
+
elif code == 0:
|
|
85
|
+
results.append(running.Result(record=directory, history=reader.read_history(directory), context=None))
|
|
86
|
+
else:
|
|
87
|
+
LOGGER.warning("point %04d failed (exit %s); its output is in %s", index, code, output)
|
|
88
|
+
results.append(None)
|
|
89
|
+
if skipped:
|
|
90
|
+
log(f"a point was stopped from outside; {skipped} points did not start and --repair runs them")
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resumed(plan):
|
|
95
|
+
resume = getattr(plan.strategy, "resume", None)
|
|
96
|
+
if not callable(resume):
|
|
97
|
+
return None
|
|
98
|
+
points = recorded_points(plan)
|
|
99
|
+
told = resume(plan.space, plan.objective, points)
|
|
100
|
+
return told if is_int(told) else len(points)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
|
|
6
|
+
from kalfa.core.errors import BuildError
|
|
7
|
+
from kalfa.records import reader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RANGES = ("log", "float", "int")
|
|
11
|
+
SPACE_HINT = "a param maps to a list of values or to {log: [a, b]}, {float: [a, b]} or {int: [a, b]}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_int(value):
|
|
15
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def spread(kind, bounds, steps):
|
|
19
|
+
low, high = bounds
|
|
20
|
+
grid = numpy.geomspace(low, high, steps) if kind == "log" else numpy.linspace(low, high, steps)
|
|
21
|
+
if kind == "int":
|
|
22
|
+
return list(dict.fromkeys(round(float(value)) for value in grid))
|
|
23
|
+
return [float(f"{float(value):.12g}") for value in grid]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def draw(generator, kind, values):
|
|
27
|
+
if kind == "list":
|
|
28
|
+
return copy.deepcopy(values[int(generator.integers(len(values)))])
|
|
29
|
+
low, high = values
|
|
30
|
+
if kind == "int":
|
|
31
|
+
return int(generator.integers(low, high + 1))
|
|
32
|
+
if kind == "log":
|
|
33
|
+
return float(min(max(math.exp(generator.uniform(math.log(low), math.log(high))), low), high))
|
|
34
|
+
return float(generator.uniform(low, high))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def space_values(space):
|
|
38
|
+
if not isinstance(space, dict) or not space:
|
|
39
|
+
raise BuildError(f"sweep.space must be a mapping of params to values; {SPACE_HINT}")
|
|
40
|
+
return {str(name): dimension(name, entry) for name, entry in space.items()}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def dimension(name, entry):
|
|
44
|
+
if isinstance(entry, (list, tuple)):
|
|
45
|
+
if not entry:
|
|
46
|
+
raise BuildError(f"sweep.space.{name}: the list of values is empty")
|
|
47
|
+
return "list", list(entry)
|
|
48
|
+
if isinstance(entry, dict) and len(entry) == 1:
|
|
49
|
+
kind, bounds = next(iter(entry.items()))
|
|
50
|
+
if kind in RANGES:
|
|
51
|
+
return kind, bounds_of(name, kind, bounds)
|
|
52
|
+
raise BuildError(f"sweep.space.{name}: {entry!r} is neither a list of values nor a range; {SPACE_HINT}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bounds_of(name, kind, bounds):
|
|
56
|
+
if not isinstance(bounds, (list, tuple)) or len(bounds) != 2 or not all(map(reader.is_finite, bounds)):
|
|
57
|
+
raise BuildError(f"sweep.space.{name}: {kind} takes two numbers [low, high], got {bounds!r}")
|
|
58
|
+
low, high = bounds
|
|
59
|
+
if kind == "int" and not (is_int(low) and is_int(high)):
|
|
60
|
+
raise BuildError(f"sweep.space.{name}: int takes two whole numbers [low, high], got {list(bounds)!r}")
|
|
61
|
+
if low > high:
|
|
62
|
+
raise BuildError(f"sweep.space.{name}: {kind} [{low}, {high}] has its low end above its high end")
|
|
63
|
+
if kind == "log" and low <= 0:
|
|
64
|
+
raise BuildError(f"sweep.space.{name}: log [{low}, {high}] needs a low end above zero")
|
|
65
|
+
if kind == "int":
|
|
66
|
+
return [int(low), int(high)]
|
|
67
|
+
return [float(low), float(high)]
|