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/spec.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ruamel.yaml import YAML
|
|
7
|
+
from ruamel.yaml.constructor import DuplicateKeyError
|
|
8
|
+
from ruamel.yaml.error import YAMLError
|
|
9
|
+
|
|
10
|
+
from .errors import ProblemsError, error, warning
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
LOADER_KEYS = ("include", "params", "plugins")
|
|
14
|
+
PARAM_FLAG = "-p"
|
|
15
|
+
TOKEN = re.compile(r"\$\$|\$([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\$")
|
|
16
|
+
BUILTINS = ("datetime",)
|
|
17
|
+
DATETIME_FORMAT = "%Y%m%d_%H%M%S"
|
|
18
|
+
MISSING = object()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Layer:
|
|
23
|
+
label: str
|
|
24
|
+
data: dict
|
|
25
|
+
file: str | None = None
|
|
26
|
+
includes: list = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Config:
|
|
31
|
+
data: dict
|
|
32
|
+
params: dict
|
|
33
|
+
plugins: list
|
|
34
|
+
layers: list
|
|
35
|
+
directories: list
|
|
36
|
+
overrides: dict
|
|
37
|
+
problems: list = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def errors(self):
|
|
41
|
+
return [problem for problem in self.problems if problem.severity == "error"]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def ok(self):
|
|
45
|
+
return not self.errors
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_call(value):
|
|
49
|
+
return isinstance(value, dict) and "use" in value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def join(path, key):
|
|
53
|
+
if isinstance(key, int) and not isinstance(key, bool):
|
|
54
|
+
return f"{path}[{key}]"
|
|
55
|
+
return f"{path}.{key}" if path else str(key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def kind(value):
|
|
59
|
+
return type(value).__name__
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def scalar(value):
|
|
63
|
+
if isinstance(value, bool):
|
|
64
|
+
return bool(value)
|
|
65
|
+
if isinstance(value, int):
|
|
66
|
+
return int(value)
|
|
67
|
+
if isinstance(value, float):
|
|
68
|
+
return float(value)
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
return str(value)
|
|
71
|
+
if isinstance(value, date):
|
|
72
|
+
return value.isoformat()
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def plain(value):
|
|
77
|
+
if isinstance(value, dict):
|
|
78
|
+
return {scalar(key): plain(item) for key, item in value.items()}
|
|
79
|
+
if isinstance(value, (list, tuple)):
|
|
80
|
+
return [plain(item) for item in value]
|
|
81
|
+
return scalar(value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def clone(value):
|
|
85
|
+
if isinstance(value, dict):
|
|
86
|
+
return {key: clone(item) for key, item in value.items()}
|
|
87
|
+
if isinstance(value, list):
|
|
88
|
+
return [clone(item) for item in value]
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def listed(value):
|
|
93
|
+
if value is None:
|
|
94
|
+
return []
|
|
95
|
+
if isinstance(value, (str, Path, dict)):
|
|
96
|
+
return [value]
|
|
97
|
+
return list(value)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def describe(exc):
|
|
101
|
+
parts = [str(part).strip() for part in (getattr(exc, "context", None), getattr(exc, "problem", None)) if part]
|
|
102
|
+
mark = getattr(exc, "problem_mark", None) or getattr(exc, "context_mark", None)
|
|
103
|
+
line = mark.line + 1 if mark is not None else None
|
|
104
|
+
return ": ".join(parts) or str(exc).strip(), line
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def located(path, exc):
|
|
108
|
+
message, line = describe(exc)
|
|
109
|
+
where = f"{path}:{line}" if line else str(path)
|
|
110
|
+
return f"{where}: {message}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_value(text):
|
|
114
|
+
if isinstance(text, str) and text.lstrip().startswith("@"):
|
|
115
|
+
return text.strip()
|
|
116
|
+
return plain(YAML(typ="safe").load(text))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def assignment(text, code, flag, what):
|
|
120
|
+
expected = f"{flag} expects {what}=VALUE, got {text!r}"
|
|
121
|
+
if not isinstance(text, str):
|
|
122
|
+
raise ProblemsError([error(code, expected)])
|
|
123
|
+
path, separator, raw = text.partition("=")
|
|
124
|
+
path = path.strip()
|
|
125
|
+
if not separator or not path:
|
|
126
|
+
raise ProblemsError([error(code, expected)])
|
|
127
|
+
try:
|
|
128
|
+
value = parse_value(raw.strip())
|
|
129
|
+
except YAMLError as exc:
|
|
130
|
+
message, _ = describe(exc)
|
|
131
|
+
raise ProblemsError([error(code, f"{flag} {path}: cannot parse value {raw!r}: {message}", path)]) from None
|
|
132
|
+
return path, value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def parse_set(text):
|
|
136
|
+
return assignment(text, "bad_set", "--set", "PATH")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_param(text):
|
|
140
|
+
return assignment(text, "bad_param", "-p", "NAME")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def read_yaml(path):
|
|
144
|
+
path = Path(path)
|
|
145
|
+
try:
|
|
146
|
+
text = path.read_text(encoding="utf-8")
|
|
147
|
+
except OSError as exc:
|
|
148
|
+
raise ProblemsError([error("file_not_found", f"cannot read {path}: {exc.strerror or exc}")]) from None
|
|
149
|
+
except UnicodeDecodeError as exc:
|
|
150
|
+
raise ProblemsError([error("yaml_error", f"{path}: not utf-8 text ({exc.reason})")]) from None
|
|
151
|
+
try:
|
|
152
|
+
loaded = YAML(typ="safe").load(text)
|
|
153
|
+
except DuplicateKeyError as exc:
|
|
154
|
+
raise ProblemsError([error("duplicate_key", located(path, exc))]) from None
|
|
155
|
+
except YAMLError as exc:
|
|
156
|
+
raise ProblemsError([error("yaml_error", located(path, exc))]) from None
|
|
157
|
+
return plain(loaded)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def merge(base, incoming, path, problems, overrides, label):
|
|
161
|
+
for key, value in incoming.items():
|
|
162
|
+
child = join(path, key)
|
|
163
|
+
if key not in base:
|
|
164
|
+
base[key] = clone(value)
|
|
165
|
+
continue
|
|
166
|
+
existing = base[key]
|
|
167
|
+
if isinstance(existing, dict) and isinstance(value, dict) and not is_call(value):
|
|
168
|
+
merge(existing, value, child, problems, overrides, label)
|
|
169
|
+
continue
|
|
170
|
+
if existing != value or type(existing) is not type(value):
|
|
171
|
+
overrides[child] = label
|
|
172
|
+
base[key] = clone(value)
|
|
173
|
+
return base
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def nested(pairs):
|
|
177
|
+
data = {}
|
|
178
|
+
for path, value in pairs:
|
|
179
|
+
parts = path.split(".")
|
|
180
|
+
target = data
|
|
181
|
+
for part in parts[:-1]:
|
|
182
|
+
if not isinstance(target.get(part), dict):
|
|
183
|
+
target[part] = {}
|
|
184
|
+
target = target[part]
|
|
185
|
+
target[parts[-1]] = value
|
|
186
|
+
return data
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Substitution:
|
|
190
|
+
def __init__(self, params, problems):
|
|
191
|
+
self.raw = params if isinstance(params, dict) else {}
|
|
192
|
+
self.problems = problems
|
|
193
|
+
self.builtins = {"datetime": datetime.now().strftime(DATETIME_FORMAT)}
|
|
194
|
+
self.done = {}
|
|
195
|
+
self.stack = []
|
|
196
|
+
self.used = set()
|
|
197
|
+
|
|
198
|
+
def resolved(self):
|
|
199
|
+
return {name: self.param(name, join("params", name)) for name in self.raw}
|
|
200
|
+
|
|
201
|
+
def param(self, name, path):
|
|
202
|
+
if name in self.done:
|
|
203
|
+
return self.done[name]
|
|
204
|
+
if name in self.stack:
|
|
205
|
+
chain = " -> ".join([*self.stack[self.stack.index(name):], name])
|
|
206
|
+
self.problems.append(error("param_cycle", f"param {name!r} refers to itself: {chain}", path))
|
|
207
|
+
return MISSING
|
|
208
|
+
self.stack.append(name)
|
|
209
|
+
value = self.walk(self.raw[name], join("params", name))
|
|
210
|
+
self.stack.pop()
|
|
211
|
+
self.done[name] = value
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
def lookup(self, token, path):
|
|
215
|
+
base, _, member = token.partition(".")
|
|
216
|
+
if base in self.raw:
|
|
217
|
+
self.used.add(base)
|
|
218
|
+
holder = self.param(base, path)
|
|
219
|
+
if holder is MISSING:
|
|
220
|
+
return MISSING
|
|
221
|
+
elif base in self.builtins:
|
|
222
|
+
holder = self.builtins[base]
|
|
223
|
+
else:
|
|
224
|
+
self.problems.append(error("unknown_param", f"unknown param ${token}$", path, "define it under params"))
|
|
225
|
+
return MISSING
|
|
226
|
+
if not member:
|
|
227
|
+
return holder
|
|
228
|
+
if isinstance(holder, dict) and member in holder:
|
|
229
|
+
return holder[member]
|
|
230
|
+
self.problems.append(error("unknown_param", f"param {base!r} has no field {member!r} for ${token}$", path))
|
|
231
|
+
return MISSING
|
|
232
|
+
|
|
233
|
+
def text(self, value, path):
|
|
234
|
+
if "$" not in value:
|
|
235
|
+
return value
|
|
236
|
+
whole = TOKEN.fullmatch(value)
|
|
237
|
+
if whole and whole.group(1):
|
|
238
|
+
found = self.lookup(whole.group(1), path)
|
|
239
|
+
return value if found is MISSING else clone(found)
|
|
240
|
+
|
|
241
|
+
def piece(match):
|
|
242
|
+
if match.group(1) is None:
|
|
243
|
+
return "$"
|
|
244
|
+
found = self.lookup(match.group(1), path)
|
|
245
|
+
return match.group(0) if found is MISSING else str(found)
|
|
246
|
+
|
|
247
|
+
return TOKEN.sub(piece, value)
|
|
248
|
+
|
|
249
|
+
def walk(self, value, path):
|
|
250
|
+
if isinstance(value, dict):
|
|
251
|
+
return {key: self.walk(item, join(path, key)) for key, item in value.items()}
|
|
252
|
+
if isinstance(value, list):
|
|
253
|
+
return [self.walk(item, join(path, index)) for index, item in enumerate(value)]
|
|
254
|
+
if isinstance(value, str):
|
|
255
|
+
return self.text(value, path)
|
|
256
|
+
return value
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def substitute(data, params, problems):
|
|
260
|
+
substitution = Substitution(params, problems)
|
|
261
|
+
substitution.resolved()
|
|
262
|
+
return substitution.walk(data, "")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Loader:
|
|
266
|
+
def __init__(self):
|
|
267
|
+
self.problems = []
|
|
268
|
+
self.layers = []
|
|
269
|
+
self.directories = []
|
|
270
|
+
self.data = {}
|
|
271
|
+
self.params = {}
|
|
272
|
+
self.plugins = []
|
|
273
|
+
self.overrides = {}
|
|
274
|
+
self.done = set()
|
|
275
|
+
self.param_layers = {}
|
|
276
|
+
|
|
277
|
+
def file(self, raw, label, stack, includer=None):
|
|
278
|
+
path = Path(raw).expanduser()
|
|
279
|
+
resolved = str(path.resolve())
|
|
280
|
+
if resolved in stack:
|
|
281
|
+
chain = " -> ".join(Path(entry).name for entry in [*stack[stack.index(resolved):], resolved])
|
|
282
|
+
hint = f"included by {includer}" if includer else None
|
|
283
|
+
self.problems.append(error("include_cycle", f"include cycle: {chain}", hint=hint))
|
|
284
|
+
return
|
|
285
|
+
if resolved in self.done:
|
|
286
|
+
return
|
|
287
|
+
try:
|
|
288
|
+
loaded = read_yaml(path)
|
|
289
|
+
except ProblemsError as exc:
|
|
290
|
+
hint = f"included by {includer}" if includer else None
|
|
291
|
+
self.problems.extend(error(problem.code, problem.message, problem.path, hint) for problem in exc.problems)
|
|
292
|
+
return
|
|
293
|
+
self.done.add(resolved)
|
|
294
|
+
if loaded is None:
|
|
295
|
+
loaded = {}
|
|
296
|
+
if not isinstance(loaded, dict):
|
|
297
|
+
message = f"{path}: the top level must be a mapping, not {kind(loaded)}"
|
|
298
|
+
self.problems.append(error("not_a_mapping", message))
|
|
299
|
+
return
|
|
300
|
+
directory = str(Path(resolved).parent)
|
|
301
|
+
if directory not in self.directories:
|
|
302
|
+
self.directories.append(directory)
|
|
303
|
+
layer = Layer(label, loaded, resolved)
|
|
304
|
+
self.includes(loaded.get("include"), Path(resolved).parent, [*stack, resolved], layer, label)
|
|
305
|
+
self.layers.append(layer)
|
|
306
|
+
self.apply(layer)
|
|
307
|
+
|
|
308
|
+
def includes(self, entries, directory, stack, layer, includer):
|
|
309
|
+
if entries is None:
|
|
310
|
+
return
|
|
311
|
+
if isinstance(entries, str):
|
|
312
|
+
entries = [entries]
|
|
313
|
+
if not isinstance(entries, list):
|
|
314
|
+
self.problems.append(error("bad_include", f"{includer}: include must be a list of file paths", "include"))
|
|
315
|
+
return
|
|
316
|
+
for index, entry in enumerate(entries):
|
|
317
|
+
if not isinstance(entry, str):
|
|
318
|
+
message = f"{includer}: include entries must be file paths, got {entry!r}"
|
|
319
|
+
self.problems.append(error("bad_include", message, join("include", index)))
|
|
320
|
+
continue
|
|
321
|
+
target = Path(entry).expanduser()
|
|
322
|
+
if not target.is_absolute():
|
|
323
|
+
target = directory / target
|
|
324
|
+
layer.includes.append(str(target.resolve()))
|
|
325
|
+
self.file(target, entry, stack, includer)
|
|
326
|
+
|
|
327
|
+
def mapping(self, mapping, number):
|
|
328
|
+
if not isinstance(mapping, dict):
|
|
329
|
+
message = f"mapping {number} must be a mapping, not {kind(mapping)}"
|
|
330
|
+
self.problems.append(error("not_a_mapping", message))
|
|
331
|
+
return
|
|
332
|
+
layer = Layer("mapping", plain(mapping))
|
|
333
|
+
self.includes(layer.data.get("include"), Path.cwd(), [], layer, "mapping")
|
|
334
|
+
self.layers.append(layer)
|
|
335
|
+
self.apply(layer)
|
|
336
|
+
|
|
337
|
+
def assignments(self, items, parser, code, label, section=None):
|
|
338
|
+
pairs = []
|
|
339
|
+
for item in items:
|
|
340
|
+
if isinstance(item, str):
|
|
341
|
+
try:
|
|
342
|
+
pairs.append(parser(item))
|
|
343
|
+
except ProblemsError as exc:
|
|
344
|
+
self.problems.extend(exc.problems)
|
|
345
|
+
elif isinstance(item, (tuple, list)) and len(item) == 2 and isinstance(item[0], str):
|
|
346
|
+
pairs.append((item[0], item[1]))
|
|
347
|
+
else:
|
|
348
|
+
message = f"{label} expects NAME=VALUE text or a (name, value) pair, got {item!r}"
|
|
349
|
+
self.problems.append(error(code, message))
|
|
350
|
+
if not pairs:
|
|
351
|
+
return
|
|
352
|
+
data = nested(pairs)
|
|
353
|
+
layer = Layer(label, {section: data} if section else data)
|
|
354
|
+
self.layers.append(layer)
|
|
355
|
+
self.apply(layer)
|
|
356
|
+
|
|
357
|
+
def apply(self, layer):
|
|
358
|
+
label = layer.label
|
|
359
|
+
params = layer.data.get("params")
|
|
360
|
+
if params is not None:
|
|
361
|
+
if isinstance(params, dict):
|
|
362
|
+
merge(self.params, params, "params", self.problems, self.overrides, label)
|
|
363
|
+
self.param_layers.update(dict.fromkeys(params, label))
|
|
364
|
+
else:
|
|
365
|
+
self.problems.append(error("bad_params", f"{label}: params must be a mapping", "params"))
|
|
366
|
+
plugins = layer.data.get("plugins")
|
|
367
|
+
if isinstance(plugins, str):
|
|
368
|
+
plugins = [plugins]
|
|
369
|
+
if plugins is not None:
|
|
370
|
+
if not isinstance(plugins, list):
|
|
371
|
+
self.problems.append(error("bad_plugins", f"{label}: plugins must be a list of modules", "plugins"))
|
|
372
|
+
else:
|
|
373
|
+
self.pool(plugins, label)
|
|
374
|
+
own = {key: value for key, value in layer.data.items() if key not in LOADER_KEYS}
|
|
375
|
+
merge(self.data, own, "", self.problems, self.overrides, label)
|
|
376
|
+
|
|
377
|
+
def pool(self, plugins, label):
|
|
378
|
+
for index, entry in enumerate(plugins):
|
|
379
|
+
if not isinstance(entry, str):
|
|
380
|
+
message = f"{label}: plugin entries must be module names, got {entry!r}"
|
|
381
|
+
self.problems.append(error("bad_plugins", message, join("plugins", index)))
|
|
382
|
+
elif entry not in self.plugins:
|
|
383
|
+
self.plugins.append(entry)
|
|
384
|
+
|
|
385
|
+
def finish(self):
|
|
386
|
+
substitution = Substitution(self.params, self.problems)
|
|
387
|
+
params = substitution.resolved()
|
|
388
|
+
data = substitution.walk(self.data, "")
|
|
389
|
+
self.unused(substitution.used)
|
|
390
|
+
return Config(data, params, list(self.plugins), self.layers, self.directories, self.overrides, self.problems)
|
|
391
|
+
|
|
392
|
+
def unused(self, used):
|
|
393
|
+
for name in self.params:
|
|
394
|
+
if name in used:
|
|
395
|
+
continue
|
|
396
|
+
path = join("params", name)
|
|
397
|
+
if self.param_layers.get(name) == PARAM_FLAG:
|
|
398
|
+
message = f"{PARAM_FLAG} {name}: nothing in the config reads ${name}$"
|
|
399
|
+
hint = "a param is read as $name$; a leaf is overridden with --set path=value"
|
|
400
|
+
self.problems.append(error("unused_param", message, path, hint))
|
|
401
|
+
else:
|
|
402
|
+
self.problems.append(warning("unused_param", f"param {name!r} is not read by any $...$", path))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def load(paths=(), sets=(), params=(), mappings=()):
|
|
406
|
+
loader = Loader()
|
|
407
|
+
for raw in listed(paths):
|
|
408
|
+
loader.file(raw, str(raw), [])
|
|
409
|
+
for number, mapping in enumerate(listed(mappings), 1):
|
|
410
|
+
loader.mapping(mapping, number)
|
|
411
|
+
loader.assignments(listed(sets), parse_set, "bad_set", "--set")
|
|
412
|
+
loader.assignments(listed(params), parse_param, "bad_param", PARAM_FLAG, "params")
|
|
413
|
+
return loader.finish()
|
kalfa/core/state.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from .errors import KalfaError
|
|
7
|
+
from .record import atomic
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
FORMAT = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def has_state(obj):
|
|
14
|
+
getter = getattr(obj, "state_dict", None)
|
|
15
|
+
return callable(getter) and bool(getter())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def stateful_nodes(ctx):
|
|
19
|
+
for name, entry in ctx.models.items():
|
|
20
|
+
yield f"models.{name}", entry
|
|
21
|
+
if entry.ema is not None:
|
|
22
|
+
yield f"models.{name}.ema", entry.ema
|
|
23
|
+
for name, loss in ctx.losses.items():
|
|
24
|
+
if has_state(loss):
|
|
25
|
+
yield f"losses.{name}", loss
|
|
26
|
+
for name, metric in ctx.metrics.items():
|
|
27
|
+
if has_state(metric):
|
|
28
|
+
yield f"metrics.{name}", metric
|
|
29
|
+
for name, optimizer in ctx.optimizers.items():
|
|
30
|
+
yield f"optimizers.{name}", optimizer
|
|
31
|
+
if ctx.step is not None and has_state(ctx.step):
|
|
32
|
+
yield "train.step", ctx.step
|
|
33
|
+
for rule in ctx.rules:
|
|
34
|
+
yield f"rules.{rule.name}", rule
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def payload(ctx):
|
|
38
|
+
nodes = {path: obj.state_dict() for path, obj in stateful_nodes(ctx)}
|
|
39
|
+
return {"format": FORMAT, "turn": int(ctx.turn), "update": int(ctx.updates), "nodes": nodes,
|
|
40
|
+
"rng": None if ctx.rng is None else ctx.rng.state_dict(), "patches": len(ctx.patches),
|
|
41
|
+
"best": None if ctx.best is None else float(ctx.best)}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_checkpoint(ctx, path):
|
|
45
|
+
data = payload(ctx)
|
|
46
|
+
with atomic(path) as temporary:
|
|
47
|
+
torch.save(data, temporary)
|
|
48
|
+
return Path(path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_checkpoint(path):
|
|
52
|
+
return torch.load(path, map_location="cpu", weights_only=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def wanted(path, only):
|
|
56
|
+
if only is None:
|
|
57
|
+
return True
|
|
58
|
+
prefixes = (only,) if isinstance(only, str) else tuple(only)
|
|
59
|
+
return any(path == prefix or path.startswith(prefix.rstrip(".") + ".") for prefix in prefixes)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_checkpoint(ctx, path, only=None, counters=True, rng=True):
|
|
63
|
+
data = read_checkpoint(path)
|
|
64
|
+
if not isinstance(data, dict) or data.get("format") != FORMAT or "nodes" not in data:
|
|
65
|
+
raise KalfaError(f"{path} is not a kalfa checkpoint of format {FORMAT}")
|
|
66
|
+
objects = dict(stateful_nodes(ctx))
|
|
67
|
+
logger = getattr(ctx, "logger", None) or logging.getLogger("kalfa.run")
|
|
68
|
+
for node_path, state in (data.get("nodes") or {}).items():
|
|
69
|
+
if not wanted(node_path, only):
|
|
70
|
+
continue
|
|
71
|
+
obj = objects.get(node_path)
|
|
72
|
+
if obj is None:
|
|
73
|
+
logger.debug("checkpoint node %s has no object in this run and is skipped", node_path)
|
|
74
|
+
continue
|
|
75
|
+
obj.load_state_dict(state)
|
|
76
|
+
if counters:
|
|
77
|
+
ctx.turn = int(data.get("turn", ctx.turn))
|
|
78
|
+
ctx.updates = int(data.get("update", ctx.updates))
|
|
79
|
+
if "best" in data:
|
|
80
|
+
ctx.best = data["best"]
|
|
81
|
+
if rng and data.get("rng") is not None and getattr(ctx, "rng", None) is not None:
|
|
82
|
+
ctx.rng.load_state_dict(data["rng"])
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def checkpoint_path(directory, which=None):
|
|
87
|
+
base = Path(directory) / "checkpoints"
|
|
88
|
+
if which == "final":
|
|
89
|
+
path = base / "final.pt"
|
|
90
|
+
elif which == "best":
|
|
91
|
+
path = base / "best.pt"
|
|
92
|
+
elif which in (None, "last"):
|
|
93
|
+
path = base / "last.pt"
|
|
94
|
+
if which is None and not path.exists() and (base / "final.pt").exists():
|
|
95
|
+
path = base / "final.pt"
|
|
96
|
+
else:
|
|
97
|
+
raise KalfaError(f"which must be best, last or final, got {which!r}")
|
|
98
|
+
if not path.exists():
|
|
99
|
+
raise KalfaError(f"{directory} has no {path.relative_to(directory)}")
|
|
100
|
+
return path
|