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.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. 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