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/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
|