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
kalfa/core/run.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import signal
|
|
6
|
+
import socket
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import traceback
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pandas
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from ..records.reader import read_lines
|
|
18
|
+
from . import build, loop, predict
|
|
19
|
+
from .aliases import add_search_dir
|
|
20
|
+
from .check import check
|
|
21
|
+
from .context import Context
|
|
22
|
+
from .device import Device
|
|
23
|
+
from .errors import KalfaError, ProblemsError, RunError
|
|
24
|
+
from .history import progress_text
|
|
25
|
+
from .record import Pulse, RecordWriter, atomic, manifest_files, now, start_record
|
|
26
|
+
from .rng import Rng
|
|
27
|
+
from .rules import Rule, replay
|
|
28
|
+
from .spec import parse_param, parse_set
|
|
29
|
+
from .state import checkpoint_path, load_checkpoint
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
CHECKPOINTS = ("best", "last", "final")
|
|
33
|
+
TABLES = (".parquet", ".pq", ".csv", ".tsv")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Result:
|
|
38
|
+
record: str
|
|
39
|
+
history: list
|
|
40
|
+
context: object
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Monitor:
|
|
44
|
+
def __init__(self, stream=None, quiet=False):
|
|
45
|
+
self.stream = stream
|
|
46
|
+
self.quiet = quiet
|
|
47
|
+
self.total = None
|
|
48
|
+
|
|
49
|
+
def expect(self, total):
|
|
50
|
+
self.total = total
|
|
51
|
+
|
|
52
|
+
def say(self, text):
|
|
53
|
+
if not self.quiet:
|
|
54
|
+
print(text, file=self.stream or sys.stderr, flush=True)
|
|
55
|
+
|
|
56
|
+
def turn(self, line):
|
|
57
|
+
self.say(progress_text(line, self.total))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def stamp():
|
|
61
|
+
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def default_record(paths):
|
|
65
|
+
files = [str(path) for path in paths if not str(path).startswith("<")]
|
|
66
|
+
stem = Path(files[0]).stem if files else "run"
|
|
67
|
+
return f"runs/{stem}_{stamp()}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def pairs(items, kind="set"):
|
|
71
|
+
parser = parse_param if kind == "param" else parse_set
|
|
72
|
+
flag = "-p" if kind == "param" else "--set"
|
|
73
|
+
found = []
|
|
74
|
+
if isinstance(items, dict):
|
|
75
|
+
items = [items]
|
|
76
|
+
for item in items or ():
|
|
77
|
+
if isinstance(item, str):
|
|
78
|
+
found.append(tuple(parser(item)))
|
|
79
|
+
elif isinstance(item, (tuple, list)) and len(item) == 2:
|
|
80
|
+
found.append((str(item[0]), item[1]))
|
|
81
|
+
elif isinstance(item, dict):
|
|
82
|
+
found.extend((str(key), value) for key, value in item.items())
|
|
83
|
+
else:
|
|
84
|
+
raise KalfaError(f"{flag} takes 'NAME=VALUE' or a (name, value) pair, got {item!r}")
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def config_files(checked, paths=()):
|
|
89
|
+
given = [str(path) for path in paths]
|
|
90
|
+
layers = getattr(checked.config, "layers", None) or []
|
|
91
|
+
given.extend(str(file) for file in (getattr(layer, "file", None) for layer in layers) if file)
|
|
92
|
+
for file in list(given):
|
|
93
|
+
if Path(file).name == "resolved.yaml":
|
|
94
|
+
given.extend(manifest_files(Path(file).parent))
|
|
95
|
+
files, seen = [], set()
|
|
96
|
+
for file in given:
|
|
97
|
+
key = str(Path(file).resolve())
|
|
98
|
+
if key not in seen:
|
|
99
|
+
seen.add(key)
|
|
100
|
+
files.append(file)
|
|
101
|
+
return files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def note_default(ctx, path, key, value):
|
|
105
|
+
nodes = getattr(ctx.tree, "nodes", None) or {}
|
|
106
|
+
node = nodes.get(path)
|
|
107
|
+
if node is not None and isinstance(node.keys, dict) and node.keys.get(key) is None:
|
|
108
|
+
node.keys[key] = value
|
|
109
|
+
planned = ctx.plan.get("nodes") if isinstance(ctx.plan, dict) else None
|
|
110
|
+
entry = planned.get(path) if isinstance(planned, dict) else None
|
|
111
|
+
if isinstance(entry, dict) and isinstance(entry.get("keys"), dict) and entry["keys"].get(key) is None:
|
|
112
|
+
entry["keys"][key] = value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fill_defaults(ctx):
|
|
116
|
+
predicts = ctx.predicts()
|
|
117
|
+
targets = predict.target_names(ctx.data)
|
|
118
|
+
for section, entries in (("losses", ctx.losses), ("metrics", ctx.metrics)):
|
|
119
|
+
for name, entry in entries.items():
|
|
120
|
+
if not hasattr(entry, "pred") or not hasattr(entry, "target"):
|
|
121
|
+
continue
|
|
122
|
+
path = f"{section}.{name}"
|
|
123
|
+
if entry.pred is None:
|
|
124
|
+
if predicts is None or not getattr(predicts, "outputs", None):
|
|
125
|
+
raise RunError(f"{path}: pred_required: there is no predicts model whose first output could be "
|
|
126
|
+
f"pred; name pred")
|
|
127
|
+
entry.pred = predicts.outputs[0]
|
|
128
|
+
note_default(ctx, path, "pred", entry.pred)
|
|
129
|
+
if entry.target is None:
|
|
130
|
+
if len(targets) != 1:
|
|
131
|
+
raise RunError(f"{path}: target_required: the data has {len(targets)} targets, not one; name "
|
|
132
|
+
f"target")
|
|
133
|
+
entry.target = targets[0]
|
|
134
|
+
note_default(ctx, path, "target", entry.target)
|
|
135
|
+
if section == "metrics" and getattr(entry, "rescale", None) is None:
|
|
136
|
+
entry.rescale = predict.rescales(ctx.data, entry.target)
|
|
137
|
+
note_default(ctx, path, "rescale", entry.rescale)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def node_object(tree, path):
|
|
141
|
+
node = tree.nodes.get(path)
|
|
142
|
+
return None if node is None else node.obj
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_rules(ctx, tree, config):
|
|
146
|
+
ctx.rules = []
|
|
147
|
+
ctx.rules_by_name = {}
|
|
148
|
+
for spec in tree.rules:
|
|
149
|
+
rule = Rule(spec, when=node_object(tree, f"rules.{spec.name}.when"),
|
|
150
|
+
run=node_object(tree, f"rules.{spec.name}.run"))
|
|
151
|
+
ctx.rules.append(rule)
|
|
152
|
+
ctx.rules_by_name[spec.name] = rule
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def place(ctx):
|
|
156
|
+
device = ctx.device.torch
|
|
157
|
+
for entry in ctx.models.values():
|
|
158
|
+
module = getattr(entry, "module", entry)
|
|
159
|
+
module.to(device)
|
|
160
|
+
ema = getattr(entry, "ema", None)
|
|
161
|
+
if isinstance(ema, torch.nn.Module):
|
|
162
|
+
ema.to(device)
|
|
163
|
+
for objective in ctx.losses.values():
|
|
164
|
+
if isinstance(objective, torch.nn.Module):
|
|
165
|
+
objective.to(device)
|
|
166
|
+
for metric in ctx.metrics.values():
|
|
167
|
+
mover = getattr(metric, "to", None)
|
|
168
|
+
if callable(mover):
|
|
169
|
+
mover(device)
|
|
170
|
+
amp = bool(getattr(ctx.step, "amp", False))
|
|
171
|
+
for entry in ctx.optimizers.values():
|
|
172
|
+
if amp:
|
|
173
|
+
scaler = ctx.device.scaler(True)
|
|
174
|
+
if scaler is not None:
|
|
175
|
+
entry.scaler = scaler
|
|
176
|
+
if getattr(entry, "real", None) is None:
|
|
177
|
+
entry.build()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def save_fitted(ctx, directory):
|
|
181
|
+
saver = getattr(ctx.data, "save", None)
|
|
182
|
+
if callable(saver) and not (Path(directory) / "fitted").exists():
|
|
183
|
+
saver(str(directory))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def data_document(ctx):
|
|
187
|
+
from kalfa.std.data import flow
|
|
188
|
+
|
|
189
|
+
data = ctx.data
|
|
190
|
+
report = None
|
|
191
|
+
reporter = getattr(data, "report", None)
|
|
192
|
+
if callable(reporter):
|
|
193
|
+
try:
|
|
194
|
+
report = reporter()
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
ctx.logger.warning(f"data.json: the data report failed ({type(exc).__name__}: {exc})")
|
|
197
|
+
try:
|
|
198
|
+
pipeline = flow.pipeline_of(data)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
pipeline = None
|
|
201
|
+
ctx.logger.warning(f"data.json: the data flowchart could not be drawn ({type(exc).__name__}: {exc}); the "
|
|
202
|
+
f"board shows the data tables without it")
|
|
203
|
+
return flow.jsonable({"format": 1, "kind": flow.kind_of(data), "report": report, "pipeline": pipeline})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def save_data(ctx):
|
|
207
|
+
if ctx.record is not None and getattr(ctx, "data", None) is not None:
|
|
208
|
+
ctx.record.write_json("data.json", data_document(ctx))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def unused_directory(directory):
|
|
212
|
+
directory = Path(directory)
|
|
213
|
+
if not directory.exists() or not any(directory.iterdir()):
|
|
214
|
+
return directory
|
|
215
|
+
base = f"{str(directory).rstrip('/')}_{stamp()}"
|
|
216
|
+
chosen = Path(base)
|
|
217
|
+
count = 1
|
|
218
|
+
while chosen.exists():
|
|
219
|
+
count += 1
|
|
220
|
+
chosen = Path(f"{base}_{count}")
|
|
221
|
+
logging.getLogger("kalfa.run").warning(f"record {directory} already holds a run; writing {chosen}")
|
|
222
|
+
return chosen
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def prepare(checked, record=None, monitor=None, parent=None, paths=(), fitted=None, sets=True, sweep=None):
|
|
226
|
+
if checked.errors:
|
|
227
|
+
raise ProblemsError(checked.problems)
|
|
228
|
+
tree = checked.tree
|
|
229
|
+
config = checked.config.data
|
|
230
|
+
files = config_files(checked, paths)
|
|
231
|
+
ctx = Context(tree, checked.plan)
|
|
232
|
+
ctx.monitor = monitor if monitor is not None else Monitor(quiet=True)
|
|
233
|
+
ctx.seed = int(config.get("seed") or 0)
|
|
234
|
+
ctx.rng = Rng(ctx.seed)
|
|
235
|
+
ctx.rng.seed_everything()
|
|
236
|
+
ctx.device = Device(config.get("device") or "auto")
|
|
237
|
+
ctx.fitted_from = fitted
|
|
238
|
+
ctx.want_sets = sets
|
|
239
|
+
directory = None if record is False else Path(record or config.get("record") or default_record(files))
|
|
240
|
+
if directory is not None and parent is None and sweep is None:
|
|
241
|
+
directory = unused_directory(directory)
|
|
242
|
+
build.instantiate(tree, ctx)
|
|
243
|
+
fill_defaults(ctx)
|
|
244
|
+
build_rules(ctx, tree, config)
|
|
245
|
+
place(ctx)
|
|
246
|
+
expect = getattr(ctx.monitor, "expect", None)
|
|
247
|
+
if callable(expect):
|
|
248
|
+
expect(ctx.train.total_turns())
|
|
249
|
+
if directory is not None:
|
|
250
|
+
writer = start_record(ctx, directory, files, checked.plan, parent=parent, sweep=sweep)
|
|
251
|
+
if ctx.record is None:
|
|
252
|
+
ctx.record = writer if isinstance(writer, RecordWriter) else RecordWriter(directory)
|
|
253
|
+
save_fitted(ctx, directory)
|
|
254
|
+
save_data(ctx)
|
|
255
|
+
say = getattr(ctx.monitor, "say", None)
|
|
256
|
+
if callable(say):
|
|
257
|
+
say(f"record {directory}")
|
|
258
|
+
return ctx
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@contextlib.contextmanager
|
|
262
|
+
def interruptible(ctx):
|
|
263
|
+
if threading.current_thread() is not threading.main_thread():
|
|
264
|
+
yield
|
|
265
|
+
return
|
|
266
|
+
asked = []
|
|
267
|
+
|
|
268
|
+
def handle(number, frame):
|
|
269
|
+
if asked:
|
|
270
|
+
raise KeyboardInterrupt
|
|
271
|
+
asked.append(number)
|
|
272
|
+
ctx.stop("keyboard")
|
|
273
|
+
ctx.stopped_by = "keyboard"
|
|
274
|
+
say = getattr(ctx.monitor, "say", None)
|
|
275
|
+
if callable(say):
|
|
276
|
+
say("stop requested: the run ends after this turn with its final state and predictions; ctrl-c again "
|
|
277
|
+
"aborts it now")
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
previous = signal.signal(signal.SIGINT, handle)
|
|
281
|
+
except ValueError:
|
|
282
|
+
yield
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
yield
|
|
286
|
+
finally:
|
|
287
|
+
signal.signal(signal.SIGINT, previous)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def finish(ctx, status):
|
|
291
|
+
if ctx.record is not None:
|
|
292
|
+
extra = {}
|
|
293
|
+
if status == "stopped" and getattr(ctx, "stopping", None):
|
|
294
|
+
extra = {"stop_reason": ctx.stopping, "stopped_by": getattr(ctx, "stopped_by", None)}
|
|
295
|
+
ctx.record.manifest(status=status, finished=now(), **extra)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def note_record(ctx, exc):
|
|
299
|
+
if ctx.record is not None:
|
|
300
|
+
exc.add_note(f"record {ctx.record.directory}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class RecordLog(logging.Handler):
|
|
304
|
+
def __init__(self, record):
|
|
305
|
+
super().__init__(level=logging.INFO)
|
|
306
|
+
self.record = record
|
|
307
|
+
|
|
308
|
+
def emit(self, entry):
|
|
309
|
+
try:
|
|
310
|
+
self.record.log(f"{entry.levelname.lower()}: {entry.getMessage()}")
|
|
311
|
+
except Exception:
|
|
312
|
+
self.handleError(entry)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@contextlib.contextmanager
|
|
316
|
+
def followed(ctx):
|
|
317
|
+
record = ctx.record
|
|
318
|
+
if record is None:
|
|
319
|
+
yield
|
|
320
|
+
return
|
|
321
|
+
logger = logging.getLogger("kalfa")
|
|
322
|
+
handler = RecordLog(record)
|
|
323
|
+
logger.addHandler(handler)
|
|
324
|
+
record.log(f"run on {socket.gethostname()}, pid {os.getpid()}, device {getattr(ctx, 'device', None)}")
|
|
325
|
+
try:
|
|
326
|
+
with Pulse(record):
|
|
327
|
+
yield
|
|
328
|
+
finally:
|
|
329
|
+
logger.removeHandler(handler)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def train_into(ctx):
|
|
333
|
+
with followed(ctx):
|
|
334
|
+
return trained(ctx)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def trained(ctx):
|
|
338
|
+
history = None
|
|
339
|
+
try:
|
|
340
|
+
with interruptible(ctx):
|
|
341
|
+
history = loop.train(ctx)
|
|
342
|
+
loop.after(ctx)
|
|
343
|
+
finish(ctx, "done" if ctx.stopping is None else "stopped")
|
|
344
|
+
except KeyboardInterrupt as exc:
|
|
345
|
+
if ctx.record is not None:
|
|
346
|
+
ctx.record.event("stop", turn=ctx.turn, by="keyboard", aborted=True)
|
|
347
|
+
ctx.record.log(f"aborted from the keyboard in turn {ctx.turn}")
|
|
348
|
+
finish(ctx, "stopped")
|
|
349
|
+
note_record(ctx, exc)
|
|
350
|
+
raise
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
if ctx.record is not None:
|
|
353
|
+
failure = {"type": type(exc).__name__, "message": str(exc), "traceback": traceback.format_exc()}
|
|
354
|
+
ctx.record.event("error", turn=ctx.turn, type=failure["type"], message=failure["message"])
|
|
355
|
+
ctx.record.manifest(status="failed", finished=now(), failure=failure)
|
|
356
|
+
ctx.record.log(f"failed in turn {ctx.turn}: {failure['type']}: {failure['message']}")
|
|
357
|
+
ctx.record.log(failure["traceback"].rstrip())
|
|
358
|
+
note_record(ctx, exc)
|
|
359
|
+
raise
|
|
360
|
+
if ctx.record is not None:
|
|
361
|
+
ctx.record.log(f"{'done' if ctx.stopping is None else 'stopped: ' + str(ctx.stopping)} after turn {ctx.turn}")
|
|
362
|
+
if history is None:
|
|
363
|
+
history = ctx.history
|
|
364
|
+
record = str(ctx.record.directory) if ctx.record is not None else None
|
|
365
|
+
return Result(record=record, history=list(history), context=ctx)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def run(paths=(), sets=(), params=(), mappings=(), record=None, monitor=None, resume_from=None, sweep=None):
|
|
369
|
+
if resume_from is not None:
|
|
370
|
+
return resume(resume_from, sets=sets, params=params, target=record, monitor=monitor, paths=paths)
|
|
371
|
+
files = tuple(str(path) for path in paths)
|
|
372
|
+
checked = check(paths=files, sets=pairs(sets), params=pairs(params, "param"), mappings=list(mappings))
|
|
373
|
+
ctx = prepare(checked, record, monitor, paths=files, sweep=sweep)
|
|
374
|
+
return train_into(ctx)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def config_directories(source):
|
|
378
|
+
found = []
|
|
379
|
+
for file in manifest_files(source):
|
|
380
|
+
directory = Path(file).parent
|
|
381
|
+
if directory.is_dir() and directory not in found:
|
|
382
|
+
found.append(directory)
|
|
383
|
+
return found
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def record_root(record):
|
|
387
|
+
source = Path(record)
|
|
388
|
+
if not (source / "resolved.yaml").exists():
|
|
389
|
+
raise KalfaError(f"{record} has no resolved.yaml; it is no record directory")
|
|
390
|
+
plugins = source / "plugins"
|
|
391
|
+
if plugins.is_dir():
|
|
392
|
+
add_search_dir(plugins)
|
|
393
|
+
for directory in config_directories(source):
|
|
394
|
+
add_search_dir(directory)
|
|
395
|
+
return source
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def carry_lines(source, ctx, upto):
|
|
399
|
+
for line in read_lines(source / "history.jsonl"):
|
|
400
|
+
turn = line.get("turn")
|
|
401
|
+
if isinstance(turn, (int, float)) and turn <= upto:
|
|
402
|
+
ctx.history.append(line)
|
|
403
|
+
ctx.record.append("history.jsonl", line)
|
|
404
|
+
for line in read_lines(source / "updates.jsonl"):
|
|
405
|
+
turn = line.get("turn")
|
|
406
|
+
if isinstance(turn, (int, float)) and turn <= upto:
|
|
407
|
+
ctx.record.append("updates.jsonl", line)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def fresh_directory(source, target):
|
|
411
|
+
directory = Path(target) if target else Path(f"{str(source).rstrip('/')}_{stamp()}")
|
|
412
|
+
if directory.exists() and any(directory.iterdir()):
|
|
413
|
+
same = directory.resolve() == source.resolve()
|
|
414
|
+
what = "the record itself" if same else "a directory that is not empty"
|
|
415
|
+
raise RunError(f"a resumed run continues into a new directory, not into {what} ({directory}); name an empty "
|
|
416
|
+
f"one with --record or leave it out for {source}_<stamp>")
|
|
417
|
+
return directory
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def carry_best(source, ctx):
|
|
421
|
+
best = source / "checkpoints" / "best.pt"
|
|
422
|
+
if best.exists():
|
|
423
|
+
target = ctx.record.path("checkpoints/best.pt")
|
|
424
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
shutil.copyfile(best, target)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def carry_plugins(source, ctx):
|
|
429
|
+
plugins = source / "plugins"
|
|
430
|
+
if plugins.is_dir():
|
|
431
|
+
shutil.copytree(plugins, ctx.record.path("plugins"), ignore=shutil.ignore_patterns("__pycache__"),
|
|
432
|
+
dirs_exist_ok=True)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def carry_params(source, ctx):
|
|
436
|
+
before = (RecordWriter(source).read_json("manifest.json") or {}).get("params")
|
|
437
|
+
asked = dict(getattr(ctx.config, "params", None) or {})
|
|
438
|
+
ctx.record.manifest(params={**(before if isinstance(before, dict) else {}), **asked})
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def resume(record, sets=(), params=(), target=None, monitor=None, paths=()):
|
|
442
|
+
source = record_root(record)
|
|
443
|
+
checkpoint = checkpoint_path(source, None)
|
|
444
|
+
directory = fresh_directory(source, target)
|
|
445
|
+
files = tuple(str(path) for path in paths) or (str(source / "resolved.yaml"),)
|
|
446
|
+
checked = check(paths=files, sets=pairs(sets), params=pairs(params, "param"))
|
|
447
|
+
ctx = prepare(checked, directory, monitor, parent=str(source), paths=files)
|
|
448
|
+
carry_params(source, ctx)
|
|
449
|
+
carry_plugins(source, ctx)
|
|
450
|
+
load_checkpoint(ctx, checkpoint, counters=True, rng=True)
|
|
451
|
+
upto = int(ctx.turn)
|
|
452
|
+
replay(ctx, read_lines(source / "events.jsonl"), upto)
|
|
453
|
+
carry_lines(source, ctx, upto)
|
|
454
|
+
carry_best(source, ctx)
|
|
455
|
+
loop.replan(ctx, None)
|
|
456
|
+
ctx.record.event("resume", turn=upto, source=str(source), checkpoint=checkpoint.name)
|
|
457
|
+
return train_into(ctx)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def report_choice(ctx, source):
|
|
461
|
+
if ctx.train.report == "best" and (source / "checkpoints" / "best.pt").exists():
|
|
462
|
+
return "best"
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def open_record(record, which=None, sets=(), params=(), device=None, data=True):
|
|
467
|
+
source = record_root(record)
|
|
468
|
+
if which is not None and which not in CHECKPOINTS:
|
|
469
|
+
raise KalfaError(f"which must be best, last or final, got {which!r}")
|
|
470
|
+
layer = pairs(sets)
|
|
471
|
+
if device is not None:
|
|
472
|
+
layer.append(("device", device))
|
|
473
|
+
files = (str(source / "resolved.yaml"),)
|
|
474
|
+
checked = check(paths=files, sets=layer, params=pairs(params, "param"))
|
|
475
|
+
if checked.errors:
|
|
476
|
+
raise ProblemsError(checked.problems)
|
|
477
|
+
ctx = prepare(checked, False, None, paths=files, fitted=source, sets=data)
|
|
478
|
+
path = checkpoint_path(source, which or report_choice(ctx, source))
|
|
479
|
+
load_checkpoint(ctx, path, only=["models"], counters=False, rng=False)
|
|
480
|
+
ctx.set_modes(False)
|
|
481
|
+
return ctx
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def model_entry(ctx, model=None):
|
|
485
|
+
if model is None:
|
|
486
|
+
entry = ctx.predicts()
|
|
487
|
+
if entry is None:
|
|
488
|
+
raise KalfaError("the run has several models and no train.predicts; name one with model")
|
|
489
|
+
return entry
|
|
490
|
+
if model not in ctx.models:
|
|
491
|
+
raise KalfaError(f"model {model!r} is not in the models section, which has {sorted(ctx.models)}")
|
|
492
|
+
return ctx.models[model]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def predict_frame(ctx, loader, frame=None, entry=None):
|
|
496
|
+
return predict.predictions_table(ctx, "test", loader=loader, frame=frame, entry=entry)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def read_frame(path):
|
|
500
|
+
suffix = Path(path).suffix.lower()
|
|
501
|
+
if suffix in (".parquet", ".pq"):
|
|
502
|
+
return pandas.read_parquet(path)
|
|
503
|
+
if suffix == ".tsv":
|
|
504
|
+
return pandas.read_csv(path, sep="\t")
|
|
505
|
+
return pandas.read_csv(path)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def source_of(data):
|
|
509
|
+
if isinstance(data, pandas.DataFrame):
|
|
510
|
+
return data, data, "_frame"
|
|
511
|
+
path = Path(str(data)).expanduser()
|
|
512
|
+
if not path.exists():
|
|
513
|
+
raise KalfaError(f"{data} does not exist; predict takes a .parquet or .csv table, or what the data class "
|
|
514
|
+
f"reads, such as a folder of images or a text file")
|
|
515
|
+
if path.is_file() and path.suffix.lower() in TABLES:
|
|
516
|
+
frame = read_frame(path)
|
|
517
|
+
return frame, frame, f"_{path.stem}"
|
|
518
|
+
return None, str(path), f"_{path.stem or path.name}"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def predict_record(record, data=None, model=None, which=None, device=None):
|
|
522
|
+
ctx = open_record(record, which=which, device=device, data=data is None)
|
|
523
|
+
if not getattr(ctx.data, "predictions", True):
|
|
524
|
+
raise KalfaError(f"{record}: the data class {type(ctx.data).__name__} writes no predictions table; kalfa "
|
|
525
|
+
f"generate draws samples from a record whose config has a generate section")
|
|
526
|
+
source = Path(record)
|
|
527
|
+
entry = None if model is None else model_entry(ctx, model)
|
|
528
|
+
if data is None:
|
|
529
|
+
if "test" not in ctx.loaders():
|
|
530
|
+
raise KalfaError(f"{record}: the run has no test set to predict on; predict on a file with data")
|
|
531
|
+
table = predict.predictions_table(ctx, "test", entry=entry)
|
|
532
|
+
tag = f"_{checkpoint_path(source, which or report_choice(ctx, source)).stem}"
|
|
533
|
+
else:
|
|
534
|
+
frame, given, tag = source_of(data)
|
|
535
|
+
loader = ctx.data.loader_for(given)
|
|
536
|
+
if frame is None:
|
|
537
|
+
frame = getattr(getattr(loader, "dataset", None), "frame", None)
|
|
538
|
+
table = predict_frame(ctx, loader, frame, entry)
|
|
539
|
+
if model is not None:
|
|
540
|
+
tag += f"_{model}"
|
|
541
|
+
if not len(table):
|
|
542
|
+
raise KalfaError(f"{record}: there is nothing to predict; the model gave no outputs")
|
|
543
|
+
path = source / f"predictions{tag}.parquet"
|
|
544
|
+
with atomic(path) as temporary:
|
|
545
|
+
table.to_parquet(temporary, index=False)
|
|
546
|
+
return path, table
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def chosen_plots(ctx, only):
|
|
550
|
+
if only is None or only == "all":
|
|
551
|
+
return list(ctx.plots)
|
|
552
|
+
names = [name for name in only.split(",") if name] if isinstance(only, str) else list(only)
|
|
553
|
+
unknown = [name for name in names if name not in ctx.plots]
|
|
554
|
+
if unknown:
|
|
555
|
+
raise KalfaError(f"plots {unknown} are not in the plots section, which has {sorted(ctx.plots)}")
|
|
556
|
+
return names
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def draw_plots(ctx, record, only=None):
|
|
560
|
+
names = chosen_plots(ctx, only)
|
|
561
|
+
plots, writer = ctx.plots, ctx.record
|
|
562
|
+
ctx.plots = {name: plots[name] for name in names}
|
|
563
|
+
if ctx.record is None:
|
|
564
|
+
ctx.record = RecordWriter(record)
|
|
565
|
+
try:
|
|
566
|
+
return loop.draw_plots(ctx)
|
|
567
|
+
finally:
|
|
568
|
+
ctx.plots, ctx.record = plots, writer
|