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/loop.py ADDED
@@ -0,0 +1,710 @@
1
+ import contextlib
2
+ import copy
3
+ import inspect
4
+ import itertools
5
+ import math
6
+ import random
7
+ import time
8
+
9
+ import numpy
10
+ import torch
11
+ from torch.nn.parameter import is_lazy
12
+
13
+ from kalfa.core import graph, state
14
+ from kalfa.core.errors import RunError
15
+ from kalfa.core.history import Means, history_line, prefixed, progress_text
16
+ from kalfa.core.predict import batch_size, empty, enter, write_predictions
17
+ from kalfa.core.protocols import Step
18
+ from kalfa.core.record import now
19
+ from kalfa.core.rules import read_path
20
+ from kalfa.core.wrap import active, loss_scalar
21
+
22
+
23
+ def autocast(ctx, enabled):
24
+ if ctx.device is None or not enabled:
25
+ return contextlib.nullcontext()
26
+ return ctx.device.autocast(enabled)
27
+
28
+
29
+ def as_number(value):
30
+ if isinstance(value, bool):
31
+ return None
32
+ if isinstance(value, torch.Tensor):
33
+ return float(value.detach()) if value.numel() == 1 else None
34
+ if isinstance(value, (int, float, numpy.integer, numpy.floating)):
35
+ return float(value)
36
+ return None
37
+
38
+
39
+ def detached(outputs):
40
+ return {name: value.detach() if isinstance(value, torch.Tensor) else value for name, value in outputs.items()}
41
+
42
+
43
+ def size_of(loader):
44
+ try:
45
+ return len(loader)
46
+ except (TypeError, NotImplementedError):
47
+ return None
48
+
49
+
50
+ def tell(ctx, phase, done=None, total=None, unit=None):
51
+ if ctx.record is not None:
52
+ ctx.record.note(ctx.turn, phase, done, total, unit)
53
+
54
+
55
+ def observers_of(ctx, set_name):
56
+ skip = set()
57
+ if set_name == "train":
58
+ skip = {id(entry.minimizes) for entry in ctx.optimizers.values() if entry.minimizes is not None}
59
+ return [(name, entry) for name, entry in ctx.losses.items()
60
+ if id(entry) not in skip and active(entry, set_name, ctx.turn)]
61
+
62
+
63
+ def metrics_of(ctx, set_name):
64
+ return [(name, entry) for name, entry in ctx.metrics.items() if active(entry, set_name, ctx.turn)]
65
+
66
+
67
+ def computed(metrics):
68
+ found = {}
69
+ for name, metric in metrics:
70
+ value = metric.compute()
71
+ if isinstance(value, dict):
72
+ for term, item in value.items():
73
+ number = as_number(item)
74
+ if number is not None:
75
+ found[f"{name}/{term}"] = number
76
+ continue
77
+ number = as_number(value)
78
+ if number is not None:
79
+ found[name] = number
80
+ return found
81
+
82
+
83
+ def observe(ctx, outputs, batch, set_name, means, metrics, losses=None):
84
+ size = batch_size(batch)
85
+ if losses is None:
86
+ losses = observers_of(ctx, set_name)
87
+ for name, entry in losses:
88
+ means.record(name, entry(outputs, batch, ctx), size)
89
+ for _, metric in metrics:
90
+ metric.update(outputs, batch, ctx)
91
+
92
+
93
+ class Cursor:
94
+ def __init__(self, loader, endless):
95
+ self.loader = loader
96
+ self.endless = endless
97
+ self.iterator = iter(loader)
98
+ self.exhausted = False
99
+
100
+ def take(self):
101
+ if self.exhausted:
102
+ return None
103
+ try:
104
+ return next(self.iterator)
105
+ except StopIteration:
106
+ if not self.endless:
107
+ self.exhausted = True
108
+ return None
109
+ self.iterator = iter(self.loader)
110
+ try:
111
+ return next(self.iterator)
112
+ except StopIteration:
113
+ raise RunError("the train loader has no batches") from None
114
+
115
+
116
+ class TrainTurn:
117
+ def __init__(self, ctx, step):
118
+ self.ctx = ctx
119
+ self.step = step
120
+ ctx.set_modes(True)
121
+ ctx.set_name = "train"
122
+ self.means = Means()
123
+ self.observers = observers_of(ctx, "train")
124
+ self.metrics = metrics_of(ctx, "train")
125
+ for _, metric in self.metrics:
126
+ metric.reset()
127
+ loaders = ctx.loaders()
128
+ if "train" not in loaders:
129
+ raise RunError(f"there is no train set to train on; the sets are {sorted(loaders)}")
130
+ self.loader = loaders["train"]
131
+ self.limit = None
132
+ if ctx.train.updates:
133
+ total = int(ctx.train.updates["total"])
134
+ per_turn = int(ctx.train.updates.get("turn", total))
135
+ self.limit = max(1, min(per_turn, total - ctx.updates))
136
+ if ctx.cursor is None:
137
+ ctx.cursor = Cursor(self.loader, endless=True)
138
+ self.cursor = ctx.cursor
139
+ else:
140
+ self.cursor = Cursor(self.loader, endless=False)
141
+ self.total = self.limit if self.limit is not None else size_of(self.loader)
142
+ self.taken = 0
143
+ self.seen = 0
144
+ self.note()
145
+
146
+ def note(self):
147
+ if self.limit is not None:
148
+ tell(self.ctx, "train", self.taken, self.total, "updates")
149
+ else:
150
+ tell(self.ctx, "train", self.seen, self.total, "batches")
151
+
152
+ @property
153
+ def exhausted(self):
154
+ return self.cursor.exhausted
155
+
156
+ def more(self):
157
+ return not self.exhausted and (self.limit is None or self.taken < self.limit)
158
+
159
+ def next_batch(self):
160
+ batch = self.cursor.take()
161
+ if batch is None:
162
+ return None
163
+ self.seen += 1
164
+ self.note()
165
+ return enter(self.ctx, batch)
166
+
167
+ def record_losses(self, losses, size):
168
+ for name, value in losses.items():
169
+ self.means.record(self.ctx.optimizers[name].minimizes.name, value, size)
170
+
171
+ def record_update(self, losses):
172
+ ctx = self.ctx
173
+ ctx.updates += 1
174
+ self.taken += 1
175
+ line = {"update": ctx.updates, "turn": ctx.turn}
176
+ for name, value in losses.items():
177
+ line[f"loss/{name}"] = float(loss_scalar(value, name).detach())
178
+ line[f"lr/{name}"] = ctx.optimizers[name].rate()
179
+ norm = ctx.last_norms.get(name)
180
+ if norm is not None:
181
+ line[f"grad_norm/{name}"] = float(norm)
182
+ if ctx.record is not None:
183
+ ctx.record.append("updates.jsonl", line)
184
+ self.note()
185
+ for logger in ctx.loggers:
186
+ logger.update(ctx, line)
187
+ return line
188
+
189
+ def observe(self, batch):
190
+ if not self.observers and not self.metrics:
191
+ return
192
+ with torch.no_grad():
193
+ outputs = self.step.eval_batch(batch, self.ctx)
194
+ observe(self.ctx, outputs, batch, "train", self.means, self.metrics, self.observers)
195
+
196
+ def close(self):
197
+ if not self.seen:
198
+ raise RunError("the train loader has no batches")
199
+ found = self.means.result()
200
+ found.update(computed(self.metrics))
201
+ return found
202
+
203
+
204
+ def default_train_turn(step, ctx):
205
+ turn = TrainTurn(ctx, step)
206
+ while turn.more():
207
+ batch = turn.next_batch()
208
+ if batch is None:
209
+ break
210
+ with autocast(ctx, step.amp):
211
+ losses = step.train_batch(batch, ctx)
212
+ turn.record_losses(losses, batch_size(batch))
213
+ if getattr(step, "stepped", True):
214
+ turn.record_update(losses)
215
+ turn.observe(batch)
216
+ return turn.close()
217
+
218
+
219
+ def train_turn(ctx):
220
+ step = ctx.step
221
+ if step is None:
222
+ raise RunError("train.step is not set; the default is supervised")
223
+ if type(step).train_turn is Step.train_turn:
224
+ return default_train_turn(step, ctx)
225
+ return step.train_turn(ctx)
226
+
227
+
228
+ def evaluate(ctx, set_name):
229
+ loader = ctx.loaders().get(set_name)
230
+ if loader is None or empty(loader):
231
+ return {}
232
+ losses = observers_of(ctx, set_name)
233
+ metrics = metrics_of(ctx, set_name)
234
+ if not losses and not metrics:
235
+ return {}
236
+ ctx.set_modes(False)
237
+ ctx.set_name = set_name
238
+ means = Means()
239
+ for _, metric in metrics:
240
+ metric.reset()
241
+ seen = 0
242
+ total = size_of(loader)
243
+ tell(ctx, set_name, seen, total, "batches")
244
+ with torch.no_grad():
245
+ for batch in loader:
246
+ seen += 1
247
+ tell(ctx, set_name, seen, total, "batches")
248
+ batch = enter(ctx, batch)
249
+ outputs = ctx.step.eval_batch(batch, ctx)
250
+ observe(ctx, outputs, batch, set_name, means, metrics, losses)
251
+ if not seen:
252
+ return {}
253
+ found = means.result()
254
+ found.update(computed(metrics))
255
+ return found
256
+
257
+
258
+ def tracked(ctx):
259
+ found = {}
260
+ for path in ctx.train.track:
261
+ value = read_path(ctx, path)
262
+ number = as_number(value)
263
+ if number is not None:
264
+ found[f"track/{path}"] = number
265
+ continue
266
+ if isinstance(value, torch.Tensor):
267
+ value = value.detach().flatten().tolist()
268
+ if isinstance(value, (list, tuple)):
269
+ for position, item in enumerate(value):
270
+ number = as_number(item)
271
+ if number is not None:
272
+ found[f"track/{path}[{position}]"] = number
273
+ return found
274
+
275
+
276
+ def checkpoint_tags(ctx):
277
+ settings = ctx.train.checkpoint
278
+ if settings is False:
279
+ return []
280
+ settings = settings if isinstance(settings, dict) else {}
281
+ tags = []
282
+ watch = settings.get("watch")
283
+ if watch is not None:
284
+ value = as_number(ctx.latest.get(watch))
285
+ if value is None:
286
+ if ctx.turn == 1:
287
+ ctx.logger.warning(f"checkpoint watch {watch!r} is not among the series of turn 1: "
288
+ f"{sorted(ctx.latest)}")
289
+ elif not math.isnan(value):
290
+ mode = settings.get("mode", "min")
291
+ if mode not in ("min", "max"):
292
+ raise RunError(f"train.checkpoint.mode must be min or max, got {mode!r}")
293
+ improved = ctx.best is None or (value > ctx.best if mode == "max" else value < ctx.best)
294
+ if improved:
295
+ ctx.best = value
296
+ tags.append("best")
297
+ if settings.get("last", True) is not False:
298
+ tags.append("last")
299
+ every = settings.get("every")
300
+ if every and ctx.turn % int(every) == 0:
301
+ tags.append(f"turn_{ctx.turn}")
302
+ return tags
303
+
304
+
305
+ def write_checkpoints(ctx):
306
+ tags = checkpoint_tags(ctx)
307
+ if ctx.record is None or not tags:
308
+ return tags
309
+ for tag in tags:
310
+ state.save_checkpoint(ctx, ctx.record.path(f"checkpoints/{tag}.pt"))
311
+ ctx.record.event("checkpoint", turn=ctx.turn, tags=tags)
312
+ return tags
313
+
314
+
315
+ def fire_rules(ctx):
316
+ for rule in ctx.rules:
317
+ if rule.fire(ctx) and rule.name not in ctx.fired:
318
+ ctx.fired.append(rule.name)
319
+
320
+
321
+ def replan(ctx, total):
322
+ current = ctx.train.total_turns()
323
+ if current == total:
324
+ return total
325
+ expect = getattr(ctx.monitor, "expect", None)
326
+ if callable(expect):
327
+ expect(current)
328
+ if ctx.record is not None:
329
+ ctx.record.manifest(turns_planned=current)
330
+ ctx.logger.info(f"the run now plans {current} turns of {ctx.train.unit}")
331
+ return current
332
+
333
+
334
+ def train(ctx):
335
+ total = ctx.train.total_turns()
336
+ ctx.logger.info(f"{total} turns of {ctx.train.unit}, {ctx.turn} done")
337
+ written, calls = None, None
338
+ if ctx.record is not None:
339
+ ctx.record.event("run_start", turn=ctx.turn, total=total)
340
+ written = write_summary(ctx)
341
+ calls = FirstCalls(ctx)
342
+ ctx.first_calls = calls
343
+ try:
344
+ first = True
345
+ while ctx.turn < total and ctx.stopping is None:
346
+ total = one_turn(ctx, total)
347
+ if first and calls is not None and calls.helps(written):
348
+ write_summary(ctx)
349
+ first = False
350
+ ctx.logger.info(f"turn {ctx.turn}: {ctx.stopping or 'done'}")
351
+ finally:
352
+ if calls is not None:
353
+ calls.close()
354
+ for logger in ctx.loggers:
355
+ logger.close(ctx)
356
+ return ctx.history
357
+
358
+
359
+ def one_turn(ctx, total):
360
+ ctx.turn += 1
361
+ started = time.perf_counter()
362
+ ctx.fired = []
363
+ tell(ctx, "train")
364
+ results = {"train": train_turn(ctx)}
365
+ loaders = ctx.loaders()
366
+ for set_name in ctx.train.evaluate:
367
+ loader = loaders.get(set_name)
368
+ if loader is None or empty(loader):
369
+ continue
370
+ results[set_name] = evaluate(ctx, set_name)
371
+ metrics = prefixed(results)
372
+ metrics.update(tracked(ctx))
373
+ ctx.latest = metrics
374
+ fire_rules(ctx)
375
+ total = replan(ctx, total)
376
+ write_checkpoints(ctx)
377
+ seconds = time.perf_counter() - started
378
+ line = history_line(ctx, metrics, seconds)
379
+ ctx.history.append(line)
380
+ for logger in ctx.loggers:
381
+ logger.turn(ctx, line)
382
+ if ctx.record is not None:
383
+ ctx.record.append("history.jsonl", line)
384
+ ctx.record.event("turn", turn=ctx.turn, seconds=round(seconds, 3), rules=list(ctx.fired))
385
+ ctx.record.manifest(updated=now(), turn=ctx.turn)
386
+ fired = f" rules {', '.join(ctx.fired)}" if ctx.fired else ""
387
+ ctx.record.log(progress_text(line, total) + fired)
388
+ if ctx.monitor is not None:
389
+ ctx.monitor.turn(line)
390
+ if ctx.record is not None and ctx.stopping is None and ctx.record.stop_requested():
391
+ note = ctx.record.stop_note() or {}
392
+ ctx.stop(f"stop.json by {note.get('by') or 'unknown'}")
393
+ ctx.stopped_by = note.get("by") or "stop.json"
394
+ note_stop(ctx)
395
+ return total
396
+
397
+
398
+ def note_stop(ctx):
399
+ if ctx.stopping is None or ctx.record is None or getattr(ctx, "stop_noted", False):
400
+ return
401
+ ctx.record.event("stop", turn=ctx.turn, by=getattr(ctx, "stopped_by", None), reason=ctx.stopping)
402
+ ctx.record.log(f"stop asked in turn {ctx.turn}: {ctx.stopping}")
403
+ ctx.stop_noted = True
404
+
405
+
406
+ def moved(ctx, batch):
407
+ return batch if ctx.device is None else ctx.device.move(batch)
408
+
409
+
410
+ def first_batch(ctx):
411
+ loaders = ctx.loaders()
412
+ for set_name in ("valid", "test", "train", *loaders):
413
+ loader = loaders.get(set_name)
414
+ if loader is None or empty(loader):
415
+ continue
416
+ for batch in loader:
417
+ return moved(ctx, batch)
418
+ return None
419
+
420
+
421
+ def device_streams(ctx):
422
+ where = getattr(ctx.device, "torch", None)
423
+ if not isinstance(where, torch.device) or where.type == "cpu":
424
+ return torch.random.fork_rng(devices=[], device_type="cpu")
425
+ index = where.index if where.index is not None else torch.accelerator.current_device_index()
426
+ return torch.random.fork_rng(devices=[index], device_type=where.type)
427
+
428
+
429
+ @contextlib.contextmanager
430
+ def unchanged(ctx):
431
+ rng = ctx.rng
432
+ streams = {} if rng is None else {key: generator.get_state() for key, generator in rng.streams.items()}
433
+ pending = {} if rng is None else dict(rng.pending)
434
+ python = random.getstate()
435
+ legacy = numpy.random.get_state()
436
+ try:
437
+ with device_streams(ctx):
438
+ yield
439
+ finally:
440
+ random.setstate(python)
441
+ numpy.random.set_state(legacy)
442
+ if rng is not None:
443
+ for key, generator in rng.streams.items():
444
+ if key in streams:
445
+ generator.set_state(streams[key])
446
+ continue
447
+ generator.manual_seed(rng.seed_for(key.rsplit("@", 1)[0]))
448
+ if key in pending:
449
+ generator.set_state(pending[key])
450
+
451
+
452
+ def counted(parameters):
453
+ return sum(parameter.numel() for parameter in parameters if not is_lazy(parameter))
454
+
455
+
456
+ def module_tree(module):
457
+ return [{"name": name, "class": type(child).__name__, "parameters": counted(child.parameters()),
458
+ "children": module_tree(child)} for name, child in module.named_children()]
459
+
460
+
461
+ def tensors_of(module):
462
+ return itertools.chain(module.parameters(), module.buffers())
463
+
464
+
465
+ def stand_in(entry):
466
+ module = entry.module
467
+ if not any(is_lazy(tensor) for tensor in tensors_of(module)):
468
+ return entry
469
+ kept = {id(tensor): tensor for tensor in tensors_of(module) if not is_lazy(tensor)}
470
+ double = copy.copy(entry)
471
+ double.module = copy.deepcopy(module, kept)
472
+ return double
473
+
474
+
475
+ def kept_copy(value):
476
+ if isinstance(value, torch.Tensor):
477
+ return value.detach().clone()
478
+ if type(value) in (list, tuple):
479
+ return type(value)(kept_copy(item) for item in value)
480
+ if type(value) is dict:
481
+ return {key: kept_copy(item) for key, item in value.items()}
482
+ return value
483
+
484
+
485
+ class FirstCalls:
486
+ def __init__(self, ctx):
487
+ self.found = {}
488
+ self.handles = {}
489
+ for name, entry in ctx.models.items():
490
+ module = getattr(entry, "module", None)
491
+ if isinstance(module, torch.nn.Module):
492
+ self.handles[name] = module.register_forward_pre_hook(self.keeper(name), with_kwargs=True)
493
+
494
+ def keeper(self, name):
495
+ def keep(module, args, kwargs):
496
+ if name not in self.found:
497
+ self.found[name] = (kept_copy(tuple(args)), kept_copy(dict(kwargs or {})))
498
+ handle = self.handles.pop(name, None)
499
+ if handle is not None:
500
+ handle.remove()
501
+ return keep
502
+
503
+ def helps(self, summary):
504
+ if not isinstance(summary, dict):
505
+ return False
506
+ return any(isinstance(entry, dict) and entry.get("graph") is None and name in self.found
507
+ for name, entry in summary.items())
508
+
509
+ def close(self):
510
+ for handle in self.handles.values():
511
+ handle.remove()
512
+ self.handles = {}
513
+
514
+
515
+ def shapes_of(entry, result):
516
+ return {label: list(value.shape) for label, value in entry.name_outputs(result).items()
517
+ if isinstance(value, torch.Tensor)}
518
+
519
+
520
+ def traced_outputs(entry, batch):
521
+ with torch.no_grad():
522
+ outputs = entry.call(batch)
523
+ return {label: list(value.shape) for label, value in outputs.items() if isinstance(value, torch.Tensor)}
524
+
525
+
526
+ def module_dtype(module):
527
+ for tensor in tensors_of(module):
528
+ if tensor.is_floating_point() and not is_lazy(tensor):
529
+ return tensor.dtype
530
+ return None
531
+
532
+
533
+ def aligned(value, dtype):
534
+ if isinstance(value, torch.Tensor):
535
+ return value.to(dtype) if dtype is not None and value.is_floating_point() and value.dtype != dtype else value
536
+ if type(value) in (list, tuple):
537
+ return type(value)(aligned(item, dtype) for item in value)
538
+ if type(value) is dict:
539
+ return {key: aligned(item, dtype) for key, item in value.items()}
540
+ return value
541
+
542
+
543
+ def call_for(entry, call):
544
+ dtype = module_dtype(entry.module)
545
+ args, kwargs = call
546
+ return aligned(args, dtype), aligned(kwargs, dtype)
547
+
548
+
549
+ def called_outputs(entry, call):
550
+ args, kwargs = call_for(entry, call)
551
+ with torch.no_grad():
552
+ return shapes_of(entry, entry.module(*args, **kwargs))
553
+
554
+
555
+ def called_graph(name, entry, call):
556
+ args, kwargs = call_for(entry, call)
557
+ try:
558
+ return graph.trace(entry.module, args, graph.argument_names(entry.module, len(args)),
559
+ list(getattr(entry, "outputs", None) or [name]), keywords=kwargs)
560
+ except Exception:
561
+ return None
562
+
563
+
564
+ def traced_graph(name, entry, batch):
565
+ if batch is None:
566
+ return None
567
+ fields = list(getattr(entry, "inputs", None) or [])
568
+ if not isinstance(batch, dict) or any(field not in batch for field in fields):
569
+ return None
570
+ try:
571
+ return graph.trace(entry.module, [batch[field] for field in fields], fields,
572
+ list(getattr(entry, "outputs", None) or [name]))
573
+ except Exception:
574
+ return None
575
+
576
+
577
+ def summary_of(name, entry, batch, call=None):
578
+ outputs, found, note, source = {}, None, None, "first batch"
579
+ try:
580
+ used = stand_in(entry)
581
+ except Exception as problem:
582
+ used = entry
583
+ note = f"the model has lazy layers and could not be copied to run it: {type(problem).__name__}: {problem}"
584
+ if note is None and batch is not None:
585
+ try:
586
+ outputs = traced_outputs(used, batch)
587
+ except Exception as problem:
588
+ note = f"the model could not run on the first batch: {type(problem).__name__}: {problem}"
589
+ found = traced_graph(name, used, batch)
590
+ if found is None and call is not None and used is entry:
591
+ found = called_graph(name, entry, call)
592
+ if found is not None:
593
+ source = "first training call"
594
+ if not outputs:
595
+ try:
596
+ outputs = called_outputs(entry, call)
597
+ except Exception:
598
+ outputs = {}
599
+ if note is None and batch is None:
600
+ note = "there was no batch to run the model on"
601
+ elif note is None:
602
+ note = "the model ran on the first batch but could not be traced"
603
+ module = used.module
604
+ summary = {"class": graph.qualified(module), "parameters": counted(module.parameters()),
605
+ "trainable": counted(parameter for parameter in module.parameters() if parameter.requires_grad),
606
+ "tree": module_tree(module), "outputs": outputs, "graph": found}
607
+ if found is None:
608
+ summary["graph_note"] = note
609
+ else:
610
+ summary["graph_from"] = source
611
+ return summary
612
+
613
+
614
+ def failed_summary(entry, problem):
615
+ return {"class": graph.qualified(entry.module), "parameters": None, "trainable": None, "tree": [], "outputs": {},
616
+ "graph": None, "graph_note": f"the summary of this model failed: {type(problem).__name__}: {problem}"}
617
+
618
+
619
+ def model_summary(ctx):
620
+ ctx.loaders()
621
+ summary = {}
622
+ with unchanged(ctx):
623
+ batch = first_batch(ctx)
624
+ ctx.set_modes(False)
625
+ calls = getattr(getattr(ctx, "first_calls", None), "found", None) or {}
626
+ for name, entry in ctx.models.items():
627
+ try:
628
+ summary[name] = summary_of(name, entry, batch, calls.get(name))
629
+ except Exception as problem:
630
+ summary[name] = failed_summary(entry, problem)
631
+ return summary
632
+
633
+
634
+ def write_summary(ctx):
635
+ try:
636
+ summary = model_summary(ctx)
637
+ except Exception as problem:
638
+ ctx.logger.warning(f"the model summary could not be written: {type(problem).__name__}: {problem}")
639
+ return None
640
+ ctx.record.write_json("model_summary.json", summary)
641
+ return summary
642
+
643
+
644
+ def write_samples(ctx):
645
+ sampler = ctx.generate
646
+ if sampler is None or ctx.record is None:
647
+ return None
648
+ from kalfa.std.samples import save_samples
649
+
650
+ count = int(getattr(sampler, "n", 16) or 16)
651
+ with torch.no_grad():
652
+ samples = sampler.sample(ctx, count)
653
+ return save_samples(samples, ctx.record.path("samples"), "samples")
654
+
655
+
656
+ def takes_context(function):
657
+ try:
658
+ return "ctx" in inspect.signature(function).parameters
659
+ except (TypeError, ValueError):
660
+ return False
661
+
662
+
663
+ def draw_plots(ctx):
664
+ if ctx.record is None:
665
+ return []
666
+ from kalfa.std.figure import Figures
667
+
668
+ figures = Figures(ctx.record.path("plots"))
669
+ drawn = []
670
+ for name, plot in ctx.plots.items():
671
+ function, params = plot if isinstance(plot, (tuple, list)) else (plot, {})
672
+ params = dict(params or {})
673
+ if takes_context(function):
674
+ params["ctx"] = ctx
675
+ figure = function(ctx.record.directory, figures, **params)
676
+ if figure is not None:
677
+ figures.save(figure, name)
678
+ drawn.append(name)
679
+ return drawn
680
+
681
+
682
+ def after(ctx):
683
+ record = ctx.record
684
+ tell(ctx, "after")
685
+ if record is not None:
686
+ state.save_checkpoint(ctx, record.path("checkpoints/final.pt"))
687
+ write_summary(ctx)
688
+ if ctx.train.report == "best":
689
+ path = None if record is None else record.path("checkpoints/best.pt")
690
+ if path is None or not path.exists():
691
+ raise RunError("report: best needs checkpoints/best.pt, which is written when train.checkpoint names a "
692
+ "watch series that improves; write checkpoint: {watch: <series>} or report: last")
693
+ state.load_checkpoint(ctx, path, only=["models"], counters=False, rng=False)
694
+ ctx.set_modes(False)
695
+ tables = {}
696
+ loaders = ctx.loaders() if getattr(ctx.data, "predictions", True) else {}
697
+ tell(ctx, "predictions")
698
+ if "test" in loaders:
699
+ tables["test"] = write_predictions(ctx, "test")
700
+ for set_name in loaders:
701
+ if set_name not in ("train", "valid", "test"):
702
+ tables[set_name] = write_predictions(ctx, set_name)
703
+ tell(ctx, "samples")
704
+ write_samples(ctx)
705
+ tell(ctx, "plots")
706
+ draw_plots(ctx)
707
+ if record is not None:
708
+ record.event("run_end", turn=ctx.turn, stopping=ctx.stopping)
709
+ record.manifest(status="done", finished=now(), turn=ctx.turn)
710
+ return tables