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/record.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from io import StringIO
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
from ruamel.yaml import YAML
|
|
18
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
19
|
+
|
|
20
|
+
from .errors import RunError
|
|
21
|
+
from .refs import tokenize
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
FILES = ("manifest.json", "resolved.yaml", "plan.json", "history.jsonl", "updates.jsonl", "events.jsonl")
|
|
25
|
+
STOP_FILE = "stop.json"
|
|
26
|
+
LOG_FILE = "log.txt"
|
|
27
|
+
FINAL = ("done", "failed", "stopped")
|
|
28
|
+
BEAT = 10.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def file_mode():
|
|
32
|
+
mask = os.umask(0)
|
|
33
|
+
os.umask(mask)
|
|
34
|
+
return 0o666 & ~mask
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
MODE = file_mode()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def atomic(path):
|
|
42
|
+
path = Path(path)
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
handle, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
45
|
+
os.close(handle)
|
|
46
|
+
try:
|
|
47
|
+
yield Path(temporary)
|
|
48
|
+
os.chmod(temporary, MODE)
|
|
49
|
+
os.replace(temporary, path)
|
|
50
|
+
finally:
|
|
51
|
+
if os.path.exists(temporary):
|
|
52
|
+
os.unlink(temporary)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def now():
|
|
56
|
+
return datetime.now().isoformat(timespec="seconds")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def version():
|
|
60
|
+
from kalfa import __version__
|
|
61
|
+
|
|
62
|
+
return __version__
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RecordWriter:
|
|
66
|
+
def __init__(self, directory):
|
|
67
|
+
self.directory = Path(directory)
|
|
68
|
+
self.lock = threading.RLock()
|
|
69
|
+
self.within = None
|
|
70
|
+
|
|
71
|
+
def path(self, name):
|
|
72
|
+
return self.directory / name
|
|
73
|
+
|
|
74
|
+
def write_json(self, name, value):
|
|
75
|
+
with atomic(self.path(name)) as temporary:
|
|
76
|
+
temporary.write_text(json.dumps(value, indent=1, default=str), encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
def write_text(self, name, text):
|
|
79
|
+
with atomic(self.path(name)) as temporary:
|
|
80
|
+
temporary.write_text(text, encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
def read_json(self, name):
|
|
83
|
+
path = self.path(name)
|
|
84
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
|
|
85
|
+
|
|
86
|
+
def append(self, name, line):
|
|
87
|
+
path = self.path(name)
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
90
|
+
handle.write(json.dumps(line, default=str) + "\n")
|
|
91
|
+
|
|
92
|
+
def event(self, kind, turn=None, **fields):
|
|
93
|
+
self.append("events.jsonl", {"at": now(), "turn": turn, "kind": kind, **fields})
|
|
94
|
+
|
|
95
|
+
def manifest(self, **changes):
|
|
96
|
+
with self.lock:
|
|
97
|
+
note = self.read_json("manifest.json") or {}
|
|
98
|
+
note.update(changes)
|
|
99
|
+
note["updated"] = now()
|
|
100
|
+
if note.get("status") in FINAL:
|
|
101
|
+
note.pop("within", None)
|
|
102
|
+
elif self.within is not None:
|
|
103
|
+
note["within"] = dict(self.within)
|
|
104
|
+
self.write_json("manifest.json", note)
|
|
105
|
+
return note
|
|
106
|
+
|
|
107
|
+
def note(self, turn, phase, done=None, total=None, unit=None):
|
|
108
|
+
before = self.within
|
|
109
|
+
started = before["started"] if before is not None and before.get("turn") == turn else now()
|
|
110
|
+
self.within = {"turn": turn, "phase": phase, "done": done, "total": total, "unit": unit, "started": started}
|
|
111
|
+
|
|
112
|
+
def beat(self):
|
|
113
|
+
with self.lock:
|
|
114
|
+
try:
|
|
115
|
+
note = self.read_json("manifest.json")
|
|
116
|
+
except (OSError, ValueError):
|
|
117
|
+
return False
|
|
118
|
+
if not isinstance(note, dict) or note.get("status") != "running":
|
|
119
|
+
return False
|
|
120
|
+
note["updated"] = now()
|
|
121
|
+
if self.within is not None:
|
|
122
|
+
note["within"] = dict(self.within)
|
|
123
|
+
try:
|
|
124
|
+
self.write_json("manifest.json", note)
|
|
125
|
+
except OSError:
|
|
126
|
+
return False
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def log(self, text):
|
|
130
|
+
path = self.path(LOG_FILE)
|
|
131
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
stamp = now()
|
|
133
|
+
with self.lock, path.open("a", encoding="utf-8") as handle:
|
|
134
|
+
for line in str(text).splitlines() or [""]:
|
|
135
|
+
handle.write(f"{stamp} {line}\n")
|
|
136
|
+
|
|
137
|
+
def stop_requested(self):
|
|
138
|
+
return self.path(STOP_FILE).exists()
|
|
139
|
+
|
|
140
|
+
def stop_note(self):
|
|
141
|
+
return self.read_json(STOP_FILE)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Pulse:
|
|
145
|
+
def __init__(self, writer, every=None):
|
|
146
|
+
self.writer = writer
|
|
147
|
+
self.every = float(BEAT if every is None else every)
|
|
148
|
+
self.ended = threading.Event()
|
|
149
|
+
self.thread = None
|
|
150
|
+
|
|
151
|
+
def __enter__(self):
|
|
152
|
+
self.ended.clear()
|
|
153
|
+
self.thread = threading.Thread(target=self.run, name="kalfa-pulse", daemon=True)
|
|
154
|
+
self.thread.start()
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def __exit__(self, kind, error, trace):
|
|
158
|
+
self.ended.set()
|
|
159
|
+
thread, self.thread = self.thread, None
|
|
160
|
+
if thread is not None:
|
|
161
|
+
thread.join()
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def run(self):
|
|
165
|
+
while not self.ended.wait(self.every):
|
|
166
|
+
self.writer.beat()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def commented(value):
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
out = CommentedMap()
|
|
172
|
+
for key, item in value.items():
|
|
173
|
+
out[key] = commented(item)
|
|
174
|
+
return out
|
|
175
|
+
if isinstance(value, (list, tuple)):
|
|
176
|
+
out = CommentedSeq()
|
|
177
|
+
for item in value:
|
|
178
|
+
out.append(commented(item))
|
|
179
|
+
return out
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def locate(document, tokens):
|
|
184
|
+
holder = document
|
|
185
|
+
for token in tokens[:-1]:
|
|
186
|
+
try:
|
|
187
|
+
holder = holder[token]
|
|
188
|
+
except (KeyError, IndexError, TypeError):
|
|
189
|
+
return None, None
|
|
190
|
+
if not tokens or not isinstance(holder, (CommentedMap, CommentedSeq)):
|
|
191
|
+
return None, None
|
|
192
|
+
try:
|
|
193
|
+
holder[tokens[-1]]
|
|
194
|
+
except (KeyError, IndexError, TypeError):
|
|
195
|
+
return None, None
|
|
196
|
+
return holder, tokens[-1]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def annotate(document, overrides):
|
|
200
|
+
for path, label in (overrides or {}).items():
|
|
201
|
+
try:
|
|
202
|
+
tokens = tokenize(path)
|
|
203
|
+
except RunError:
|
|
204
|
+
continue
|
|
205
|
+
holder, key = locate(document, tokens)
|
|
206
|
+
if holder is None:
|
|
207
|
+
continue
|
|
208
|
+
try:
|
|
209
|
+
holder.yaml_add_eol_comment(str(label), key=key)
|
|
210
|
+
except (KeyError, IndexError, TypeError, AttributeError):
|
|
211
|
+
try:
|
|
212
|
+
holder.yaml_add_eol_comment(str(label), key=key, column=0)
|
|
213
|
+
except (KeyError, IndexError, TypeError, AttributeError):
|
|
214
|
+
continue
|
|
215
|
+
return document
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def resolved_mapping(config, record=None):
|
|
219
|
+
data = {key: value for key, value in (config.data or {}).items() if key != "sweep"}
|
|
220
|
+
if record is not None:
|
|
221
|
+
data["record"] = str(record)
|
|
222
|
+
plugins = [str(name) for name in (getattr(config, "plugins", None) or [])]
|
|
223
|
+
if plugins and "plugins" not in data:
|
|
224
|
+
return {"plugins": plugins, **data}
|
|
225
|
+
return data
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def resolved_text(config, record=None):
|
|
229
|
+
yaml = YAML()
|
|
230
|
+
yaml.width = 120
|
|
231
|
+
document = annotate(commented(resolved_mapping(config, record)), getattr(config, "overrides", None))
|
|
232
|
+
stream = StringIO()
|
|
233
|
+
yaml.dump(document, stream)
|
|
234
|
+
return stream.getvalue()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def host_note():
|
|
238
|
+
return {"hostname": socket.gethostname(), "platform": platform.platform(), "python": platform.python_version(),
|
|
239
|
+
"torch": torch.__version__, "cuda": torch.version.cuda or None, "cwd": str(Path.cwd())}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def git_note(root):
|
|
243
|
+
try:
|
|
244
|
+
commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(root), capture_output=True, text=True,
|
|
245
|
+
check=True, timeout=5)
|
|
246
|
+
status = subprocess.run(["git", "status", "--porcelain"], cwd=str(root), capture_output=True, text=True,
|
|
247
|
+
check=True, timeout=5)
|
|
248
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
249
|
+
return None
|
|
250
|
+
return {"commit": commit.stdout.strip(), "dirty": bool(status.stdout.strip())}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def start_record(ctx, directory, paths=(), plan=None, parent=None, sweep=None):
|
|
254
|
+
directory = Path(directory)
|
|
255
|
+
if directory.exists() and any(directory.iterdir()) and parent is None:
|
|
256
|
+
raise RunError(f"record directory {directory} exists and is not empty; name another record or resume")
|
|
257
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
writer = RecordWriter(directory)
|
|
259
|
+
config = ctx.config
|
|
260
|
+
train = getattr(ctx, "train", None)
|
|
261
|
+
device = getattr(ctx, "device", None)
|
|
262
|
+
writer.manifest(format=1, kalfa=version(), name=directory.resolve().name, started=now(), finished=None,
|
|
263
|
+
status="running", config_files=[str(Path(path).resolve()) for path in paths],
|
|
264
|
+
params=dict(getattr(config, "params", None) or {}),
|
|
265
|
+
unit=train.unit if train is not None else "epochs",
|
|
266
|
+
turns_planned=train.total_turns() if train is not None else 0, host=host_note(),
|
|
267
|
+
device=str(device) if device is not None else None, git=git_note(Path.cwd()), sweep=sweep,
|
|
268
|
+
resumed_from=str(parent) if parent is not None else None, failure=None)
|
|
269
|
+
writer.write_text("resolved.yaml", resolved_text(config, record=directory))
|
|
270
|
+
if plan is not None:
|
|
271
|
+
writer.write_json("plan.json", plan)
|
|
272
|
+
copy_plugins(config, directory, imports=getattr(getattr(ctx, "tree", None), "imports", None) or ())
|
|
273
|
+
ctx.record = writer
|
|
274
|
+
return writer
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def module_file(name, search=False):
|
|
278
|
+
module = sys.modules.get(name)
|
|
279
|
+
file = getattr(module, "__file__", None) if module is not None else None
|
|
280
|
+
if file is None and search and module is None:
|
|
281
|
+
try:
|
|
282
|
+
spec = importlib.util.find_spec(name)
|
|
283
|
+
except (ImportError, ValueError, AttributeError):
|
|
284
|
+
return None
|
|
285
|
+
file = spec.origin if spec is not None and spec.has_location else None
|
|
286
|
+
if not file or not str(file).endswith(".py"):
|
|
287
|
+
return None
|
|
288
|
+
return Path(file).resolve()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def plugin_source(name, roots, search=False):
|
|
292
|
+
parts = str(name).split(".")
|
|
293
|
+
for cut in range(1, len(parts) + 1):
|
|
294
|
+
file = module_file(".".join(parts[:cut]), search=search)
|
|
295
|
+
if file is None:
|
|
296
|
+
continue
|
|
297
|
+
home = next((root for root in roots if file.is_relative_to(root)), None)
|
|
298
|
+
if home is not None:
|
|
299
|
+
return file, home
|
|
300
|
+
return (module_file(name, search=search) if search else None), None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def copy_module(file, home, destination):
|
|
304
|
+
relative = file.relative_to(home) if home is not None else Path(file.name)
|
|
305
|
+
if file.name == "__init__.py":
|
|
306
|
+
target = destination / (relative.parent if home is not None else Path(file.parent.name))
|
|
307
|
+
shutil.copytree(file.parent, target, ignore=shutil.ignore_patterns("__pycache__"), dirs_exist_ok=True)
|
|
308
|
+
return target
|
|
309
|
+
target = destination / relative
|
|
310
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
shutil.copyfile(file, target)
|
|
312
|
+
return target
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def manifest_files(directory):
|
|
316
|
+
try:
|
|
317
|
+
files = json.loads((Path(directory) / "manifest.json").read_text(encoding="utf-8")).get("config_files") or []
|
|
318
|
+
except (OSError, ValueError, AttributeError):
|
|
319
|
+
return []
|
|
320
|
+
return [str(file) for file in files if isinstance(file, str)]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def plugin_roots(config):
|
|
324
|
+
roots = []
|
|
325
|
+
for item in getattr(config, "directories", None) or []:
|
|
326
|
+
root = Path(item).resolve()
|
|
327
|
+
if (root / "resolved.yaml").is_file():
|
|
328
|
+
if (root / "plugins").is_dir():
|
|
329
|
+
roots.append(root / "plugins")
|
|
330
|
+
roots.extend(Path(file).resolve().parent for file in manifest_files(root))
|
|
331
|
+
roots.append(root)
|
|
332
|
+
return list(dict.fromkeys(roots))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def copy_plugins(config, directory, imports=()):
|
|
336
|
+
roots = plugin_roots(config)
|
|
337
|
+
named = [str(name) for name in (getattr(config, "plugins", None) or [])]
|
|
338
|
+
destination = Path(directory) / "plugins"
|
|
339
|
+
copied = []
|
|
340
|
+
seen = set()
|
|
341
|
+
for name, search in [(name, True) for name in named] + [(str(text), False) for text in imports or ()]:
|
|
342
|
+
file, home = plugin_source(name, roots, search=search)
|
|
343
|
+
if file is None or file in seen:
|
|
344
|
+
continue
|
|
345
|
+
seen.add(file)
|
|
346
|
+
copied.append(copy_module(file, home, destination))
|
|
347
|
+
return copied
|
kalfa/core/refs.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from .errors import RunError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
TABLES = ("models", "losses", "metrics", "optimizers")
|
|
7
|
+
SINGULAR = {"models": "model", "losses": "loss", "metrics": "metric", "optimizers": "optimizer"}
|
|
8
|
+
SEGMENT = re.compile(r"(?P<name>[^.\[\]\s]+)(?:\[\d+\])*")
|
|
9
|
+
INDEX = re.compile(r"\[(\d+)\]")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tokenize(path):
|
|
13
|
+
if not isinstance(path, str) or not path.strip():
|
|
14
|
+
raise RunError(f"a path is dotted like optimizers.main.lr, got {path!r}")
|
|
15
|
+
tokens = []
|
|
16
|
+
for piece in path.strip().split("."):
|
|
17
|
+
match = SEGMENT.fullmatch(piece)
|
|
18
|
+
if match is None:
|
|
19
|
+
raise RunError(f"malformed path {path!r}: {piece!r} is not a name with an optional [index]")
|
|
20
|
+
tokens.append(match.group("name"))
|
|
21
|
+
tokens.extend(int(index) for index in INDEX.findall(piece))
|
|
22
|
+
return tokens
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def join_tokens(tokens):
|
|
26
|
+
text = ""
|
|
27
|
+
for token in tokens:
|
|
28
|
+
if isinstance(token, int):
|
|
29
|
+
text += f"[{token}]"
|
|
30
|
+
else:
|
|
31
|
+
text += f".{token}" if text else token
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ref_parts(asked):
|
|
36
|
+
if isinstance(asked, str) and asked.startswith("@"):
|
|
37
|
+
return asked[1:]
|
|
38
|
+
if isinstance(asked, dict) and "ref" in asked:
|
|
39
|
+
attr = asked.get("attr")
|
|
40
|
+
return f"{asked['ref']}.{attr}" if attr else str(asked["ref"])
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ref_json(asked):
|
|
45
|
+
tokens = tokenize(ref_parts(asked))
|
|
46
|
+
split = 2 if tokens[0] in TABLES else 1
|
|
47
|
+
out = {"ref": join_tokens(tokens[:split])}
|
|
48
|
+
if tokens[split:]:
|
|
49
|
+
out["attr"] = join_tokens(tokens[split:])
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ref_json_of(ctx, value):
|
|
54
|
+
for section in TABLES:
|
|
55
|
+
for name, entry in getattr(ctx, section).items():
|
|
56
|
+
if entry is value or getattr(entry, "module", None) is value:
|
|
57
|
+
return {"ref": f"{section}.{name}"}
|
|
58
|
+
if section == "models" and getattr(entry, "ema", None) is value:
|
|
59
|
+
return {"ref": f"{section}.{name}", "attr": "ema"}
|
|
60
|
+
for section in ("data", "generate"):
|
|
61
|
+
if getattr(ctx, section, None) is value:
|
|
62
|
+
return {"ref": section}
|
|
63
|
+
for node in ctx.tree.nodes.values():
|
|
64
|
+
if node.obj is value:
|
|
65
|
+
return {"ref": node.path}
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def jsonable(value, ctx=None):
|
|
70
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
71
|
+
return value
|
|
72
|
+
from .build import Node, Ref
|
|
73
|
+
|
|
74
|
+
if isinstance(value, Ref):
|
|
75
|
+
return {"ref": value.path} if value.attr is None else {"ref": value.path, "attr": value.attr}
|
|
76
|
+
if isinstance(value, Node):
|
|
77
|
+
return {"node": value.path}
|
|
78
|
+
if isinstance(value, dict):
|
|
79
|
+
return {str(key): jsonable(item, ctx) for key, item in value.items()}
|
|
80
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
81
|
+
return [jsonable(item, ctx) for item in value]
|
|
82
|
+
if ctx is not None:
|
|
83
|
+
found = ref_json_of(ctx, value)
|
|
84
|
+
if found is not None:
|
|
85
|
+
return found
|
|
86
|
+
if hasattr(value, "tolist"):
|
|
87
|
+
return value.tolist()
|
|
88
|
+
return str(value)
|
kalfa/core/rng.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Rng:
|
|
9
|
+
def __init__(self, seed=0):
|
|
10
|
+
self.seed = int(seed)
|
|
11
|
+
self.streams = {}
|
|
12
|
+
self.pending = {}
|
|
13
|
+
|
|
14
|
+
def seed_everything(self):
|
|
15
|
+
random.seed(self.seed)
|
|
16
|
+
numpy.random.seed(self.seed % (2**32))
|
|
17
|
+
torch.manual_seed(self.seed)
|
|
18
|
+
|
|
19
|
+
def seed_for(self, name):
|
|
20
|
+
digest = hashlib.sha256(f"{self.seed}:{name}".encode()).digest()
|
|
21
|
+
return int.from_bytes(digest[:8], "little") % (2**63 - 1)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def place(device=None, like=None):
|
|
25
|
+
if device is None and like is not None:
|
|
26
|
+
device = like.device
|
|
27
|
+
if device is None:
|
|
28
|
+
return torch.device("cpu")
|
|
29
|
+
if isinstance(device, torch.device):
|
|
30
|
+
return device
|
|
31
|
+
found = getattr(device, "torch", None)
|
|
32
|
+
if isinstance(found, torch.device):
|
|
33
|
+
return found
|
|
34
|
+
return torch.device(str(device))
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def size(shape, like=None):
|
|
38
|
+
if shape is None:
|
|
39
|
+
return tuple(like.shape)
|
|
40
|
+
if isinstance(shape, int):
|
|
41
|
+
return (shape,)
|
|
42
|
+
return tuple(shape)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def float_dtype(dtype, like):
|
|
46
|
+
if dtype is not None:
|
|
47
|
+
return dtype
|
|
48
|
+
if like is not None and like.dtype.is_floating_point:
|
|
49
|
+
return like.dtype
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def stream(self, name, device=None):
|
|
53
|
+
where = self.place(device)
|
|
54
|
+
key = f"{name}@{where.type}"
|
|
55
|
+
generator = self.streams.get(key)
|
|
56
|
+
if generator is None:
|
|
57
|
+
generator = torch.Generator(device=where)
|
|
58
|
+
generator.manual_seed(self.seed_for(name))
|
|
59
|
+
state = self.pending.pop(key, None)
|
|
60
|
+
if state is not None:
|
|
61
|
+
generator.set_state(state)
|
|
62
|
+
self.streams[key] = generator
|
|
63
|
+
return generator
|
|
64
|
+
|
|
65
|
+
def normal(self, name, shape, like=None, device=None, dtype=None):
|
|
66
|
+
where = self.place(device, like)
|
|
67
|
+
return torch.randn(self.size(shape, like), generator=self.stream(name, where), device=where,
|
|
68
|
+
dtype=self.float_dtype(dtype, like))
|
|
69
|
+
|
|
70
|
+
def uniform(self, name, shape, like=None, device=None, dtype=None):
|
|
71
|
+
where = self.place(device, like)
|
|
72
|
+
return torch.rand(self.size(shape, like), generator=self.stream(name, where), device=where,
|
|
73
|
+
dtype=self.float_dtype(dtype, like))
|
|
74
|
+
|
|
75
|
+
def integers(self, name, low, high, shape, device=None):
|
|
76
|
+
where = self.place(device)
|
|
77
|
+
return torch.randint(int(low), int(high), self.size(shape), generator=self.stream(name, where), device=where)
|
|
78
|
+
|
|
79
|
+
def permutation(self, name, count, device=None):
|
|
80
|
+
where = self.place(device)
|
|
81
|
+
return torch.randperm(int(count), generator=self.stream(name, where), device=where)
|
|
82
|
+
|
|
83
|
+
def state_dict(self):
|
|
84
|
+
streams = {key: generator.get_state().detach().cpu().clone() for key, generator in self.streams.items()}
|
|
85
|
+
for key, state in self.pending.items():
|
|
86
|
+
streams.setdefault(key, state)
|
|
87
|
+
return {"seed": self.seed, "streams": streams}
|
|
88
|
+
|
|
89
|
+
def load_state_dict(self, state):
|
|
90
|
+
if not state:
|
|
91
|
+
return
|
|
92
|
+
if state.get("seed") is not None:
|
|
93
|
+
self.seed = int(state["seed"])
|
|
94
|
+
for key, saved in (state.get("streams") or {}).items():
|
|
95
|
+
tensor = (saved if isinstance(saved, torch.Tensor) else torch.as_tensor(saved)).cpu()
|
|
96
|
+
generator = self.streams.get(key)
|
|
97
|
+
if generator is not None:
|
|
98
|
+
generator.set_state(tensor)
|
|
99
|
+
else:
|
|
100
|
+
self.pending[key] = tensor
|