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/wrap.py ADDED
@@ -0,0 +1,671 @@
1
+ import copy
2
+ import fnmatch
3
+ import functools
4
+ import inspect
5
+ import math
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+ from .errors import BuildError, RunError
11
+ from .protocols import ContextMetric, Objective
12
+
13
+
14
+ DEFAULT_ON = ("train", "valid", "test")
15
+
16
+
17
+ def as_tuple(on):
18
+ return (on,) if isinstance(on, str) else tuple(on)
19
+
20
+
21
+ def fields(mapping):
22
+ return sorted(str(key) for key in mapping)
23
+
24
+
25
+ def name_of(obj):
26
+ name = getattr(obj, "name", None)
27
+ return name if isinstance(name, str) and name else type(obj).__name__
28
+
29
+
30
+ def unwrap(obj):
31
+ if obj is None:
32
+ return None
33
+ if callable(getattr(obj, "named_parameters", None)):
34
+ return obj
35
+ inner = getattr(obj, "module", None)
36
+ if inner is not None and callable(getattr(inner, "named_parameters", None)):
37
+ return inner
38
+ raise BuildError(f"{type(obj).__name__} {name_of(obj)!r} has no parameters")
39
+
40
+
41
+ def construct(cls, path):
42
+ try:
43
+ return cls()
44
+ except TypeError as problem:
45
+ raise BuildError(f"{path}: {cls.__name__} needs arguments; write it as a call with use") from problem
46
+
47
+
48
+ def grad_norm(parameters):
49
+ grads = [parameter.grad.detach() for parameter in parameters if parameter.grad is not None]
50
+ if not grads:
51
+ return 0.0
52
+ norms = torch.stack([torch.linalg.vector_norm(grad.float()) for grad in grads])
53
+ return float(torch.linalg.vector_norm(norms))
54
+
55
+
56
+ def frozen_parts(module):
57
+ found = []
58
+ for part in module.modules():
59
+ parameters = list(part.parameters())
60
+ if parameters and not any(parameter.requires_grad for parameter in parameters):
61
+ found.append(part)
62
+ return found
63
+
64
+
65
+ class EmaCopy(nn.Module):
66
+ def __init__(self, module, decay=0.999, every=1):
67
+ super().__init__()
68
+ self.decay = float(decay)
69
+ self.every = int(every)
70
+ self.module = copy.deepcopy(module)
71
+ for parameter in self.module.parameters():
72
+ parameter.requires_grad_(False)
73
+ nn.Module.train(self, False)
74
+ self.updates = 0
75
+ object.__setattr__(self, "source", module)
76
+
77
+ def shift(self, module):
78
+ self.updates += 1
79
+ if self.every > 1 and self.updates % self.every:
80
+ return
81
+ module = unwrap(module)
82
+ with torch.no_grad():
83
+ own = dict(self.module.named_parameters())
84
+ for name, parameter in module.named_parameters():
85
+ kept = own.get(name)
86
+ if kept is None:
87
+ raise RunError(f"the EMA copy has no parameter {name!r}; the module changed after it was copied")
88
+ kept.mul_(self.decay).add_(parameter.detach().to(kept.device), alpha=1.0 - self.decay)
89
+ buffers = dict(self.module.named_buffers())
90
+ for name, buffer in module.named_buffers():
91
+ kept = buffers.get(name)
92
+ if kept is not None:
93
+ kept.copy_(buffer)
94
+
95
+ def forward(self, *inputs):
96
+ return self.module(*inputs)
97
+
98
+ def train(self, mode=True):
99
+ return self
100
+
101
+ def get_extra_state(self):
102
+ return {"updates": self.updates}
103
+
104
+ def set_extra_state(self, state):
105
+ self.updates = int((state or {}).get("updates", 0))
106
+
107
+
108
+ class ModelEntry:
109
+ def __init__(self, name, module, inputs, outputs, ema=None, freeze=False, weights=None, init=None):
110
+ self.name = name
111
+ self.module = module
112
+ self.inputs = list(inputs)
113
+ self.outputs = list(outputs)
114
+ self.ema = ema
115
+ self.freeze = freeze
116
+ self.weights = weights
117
+ self.init = init
118
+ self.summary = None
119
+ self.path = f"models.{name}"
120
+ self.locked = False
121
+ self.frozen_parts = []
122
+ if freeze:
123
+ self.set_freeze(freeze)
124
+
125
+ def call(self, batch):
126
+ inputs = []
127
+ for name in self.inputs:
128
+ if name not in batch:
129
+ raise RunError(f"model {self.name!r} needs the batch field {name!r}; the batch has {fields(batch)}")
130
+ inputs.append(batch[name])
131
+ return self.name_outputs(self.module(*inputs))
132
+
133
+ def name_outputs(self, result):
134
+ names = self.outputs or [self.name]
135
+ if isinstance(result, torch.Tensor):
136
+ return {names[0]: result}
137
+ if isinstance(result, dict):
138
+ missing = [name for name in names if name not in result]
139
+ if missing:
140
+ raise RunError(f"model {self.name!r} returned a dict without {missing}; its keys are {fields(result)}")
141
+ return result
142
+ if isinstance(result, (tuple, list)):
143
+ if len(result) != len(names):
144
+ raise RunError(f"model {self.name!r} returned {len(result)} values for the outputs {names}")
145
+ return dict(zip(names, result))
146
+ raise RunError(f"model {self.name!r} returned {type(result).__name__}, not a tensor, a tuple or a dict")
147
+
148
+ def set_freeze(self, value):
149
+ freeze_parameters(self.module, value)
150
+ self.freeze = value
151
+ self.locked = value is True
152
+ self.frozen_parts = [] if self.locked or not value else frozen_parts(self.module)
153
+ if self.locked:
154
+ self.module.eval()
155
+
156
+ @property
157
+ def frozen(self):
158
+ return self.freeze
159
+
160
+ def set_mode(self, train):
161
+ self.module.train(bool(train) and not self.locked)
162
+ for part in self.frozen_parts:
163
+ part.eval()
164
+ return self
165
+
166
+ def parameter_count(self):
167
+ return sum(parameter.numel() for parameter in self.module.parameters())
168
+
169
+ def state_dict(self):
170
+ return self.module.state_dict()
171
+
172
+ def load_state_dict(self, state):
173
+ self.module.load_state_dict(state)
174
+
175
+
176
+ def align(guess, truth):
177
+ if isinstance(guess, torch.Tensor) and isinstance(truth, torch.Tensor):
178
+ if truth.ndim == guess.ndim - 1 and guess.ndim > 0 and guess.shape[-1] == 1:
179
+ guess = guess.squeeze(-1)
180
+ elif guess.ndim == truth.ndim - 1 and truth.ndim > 0 and truth.shape[-1] == 1:
181
+ truth = truth.squeeze(-1)
182
+ return guess, truth
183
+
184
+
185
+ def pair_values(owner, outputs, batch, ctx, kind):
186
+ label = f"{kind} {owner.name!r}" if getattr(owner, "name", "") else kind
187
+ if owner.pred is None:
188
+ raise RunError(f"{label}: pred is not set")
189
+ if owner.target is None:
190
+ raise RunError(f"{label}: target is not set")
191
+ outputs = {} if outputs is None else outputs
192
+ if owner.pred not in outputs:
193
+ raise RunError(f"{label}: output {owner.pred!r} is not among the step outputs {fields(outputs)}")
194
+ if owner.target not in batch:
195
+ raise RunError(f"{label}: field {owner.target!r} is not in the batch; its fields are {fields(batch)}")
196
+ guess, truth = align(outputs[owner.pred], batch[owner.target])
197
+ if owner.rescale:
198
+ inverse = getattr(getattr(ctx, "data", None), "inverse", None)
199
+ if callable(inverse):
200
+ guess = inverse(owner.target, guess)
201
+ truth = inverse(owner.target, truth)
202
+ return guess, truth
203
+
204
+
205
+ def apply_keys(obj, keys, path):
206
+ if keys.get("sample_weight") is not None:
207
+ raise BuildError(f"{path}: sample_weight belongs to a criterion, a loss of (pred, target); "
208
+ f"{type(obj).__name__} is an objective and reads the batch field itself")
209
+ for key in ("pred", "target", "rescale"):
210
+ if key not in keys:
211
+ continue
212
+ if not hasattr(obj, key):
213
+ raise BuildError(f"{path}: {key} belongs to a criterion or a metric, not to {type(obj).__name__}")
214
+ value = keys[key]
215
+ setattr(obj, key, bool(value) if key == "rescale" and value is not None else value)
216
+ if "on" in keys:
217
+ obj.on = as_tuple(keys["on"])
218
+ if "every" in keys:
219
+ obj.every = int(keys["every"])
220
+
221
+
222
+ def takes_reduction(function):
223
+ try:
224
+ return "reduction" in inspect.signature(function).parameters
225
+ except (TypeError, ValueError):
226
+ return False
227
+
228
+
229
+ def function_name(function):
230
+ inner = function.func if isinstance(function, functools.partial) else function
231
+ return getattr(inner, "__name__", None) or type(inner).__name__
232
+
233
+
234
+ def reduction_of(function):
235
+ reduction = getattr(function, "reduction", None)
236
+ if isinstance(reduction, str):
237
+ return reduction
238
+ if not takes_reduction(function):
239
+ return None
240
+ bound = function.keywords.get("reduction") if isinstance(function, functools.partial) else None
241
+ return "mean" if bound is None else bound
242
+
243
+
244
+ def element_losses(function, guess, truth):
245
+ kept = getattr(function, "reduction", None)
246
+ if not isinstance(kept, str):
247
+ return function(guess, truth, reduction="none")
248
+ function.reduction = "none"
249
+ try:
250
+ return function(guess, truth)
251
+ finally:
252
+ function.reduction = kept
253
+
254
+
255
+ def weighted(values, weight, reduction, label):
256
+ if reduction not in ("mean", "sum"):
257
+ raise RunError(f"{label}: sample_weight takes the reduction mean or sum, not {reduction!r}")
258
+ if not isinstance(values, torch.Tensor) or values.ndim == 0:
259
+ raise RunError(f"{label}: sample_weight needs one loss value per element, but the loss returned one value")
260
+ weight = weight.to(values.dtype)
261
+ while weight.ndim > 1 and weight.shape[-1] == 1:
262
+ weight = weight.squeeze(-1)
263
+ if weight.ndim > values.ndim or tuple(weight.shape) != tuple(values.shape[:weight.ndim]):
264
+ raise RunError(f"{label}: the weights of shape {tuple(weight.shape)} do not match the element losses of shape "
265
+ f"{tuple(values.shape)}; a weight has one value per row")
266
+ scaled = values * weight.reshape(weight.shape + (1,) * (values.ndim - weight.ndim))
267
+ return scaled.sum() if reduction == "sum" else scaled.mean()
268
+
269
+
270
+ class Criterion(Objective):
271
+ def __init__(self, function, pred=None, target=None, rescale=False, on=("train", "valid", "test"), every=1,
272
+ sample_weight=None, path=""):
273
+ super().__init__()
274
+ self.pred = pred
275
+ self.target = target
276
+ self.rescale = bool(rescale)
277
+ self.on = (on,) if isinstance(on, str) else tuple(on)
278
+ self.every = int(every)
279
+ self.sample_weight = sample_weight
280
+ self.function = function
281
+ if sample_weight is not None:
282
+ self.check_weight(path or "loss")
283
+
284
+ def check_weight(self, path):
285
+ if not isinstance(self.sample_weight, str) or not self.sample_weight:
286
+ raise BuildError(f"{path}: sample_weight names a batch field such as w, got {self.sample_weight!r}")
287
+ reduction = reduction_of(self.function)
288
+ if reduction is None:
289
+ raise BuildError(f"{path}: sample_weight needs a loss that can return one value per element, a loss "
290
+ "with a reduction param such as mse, l1, huber, bce_logits or cross_entropy; "
291
+ f"{function_name(self.function)} has none")
292
+ if reduction not in ("mean", "sum"):
293
+ raise BuildError(f"{path}: sample_weight takes the reduction mean or sum, not {reduction!r}")
294
+
295
+ def pair(self, outputs, batch, ctx):
296
+ return pair_values(self, outputs, batch, ctx, "loss")
297
+
298
+ def forward(self, outputs, batch, ctx):
299
+ guess, truth = self.pair(outputs, batch, ctx)
300
+ if self.sample_weight is None:
301
+ return self.function(guess, truth)
302
+ label = f"loss {self.name!r}" if getattr(self, "name", "") else "loss"
303
+ if self.sample_weight not in batch:
304
+ raise RunError(f"{label}: sample_weight names the field {self.sample_weight!r}, which is not in the "
305
+ f"batch; its fields are {fields(batch)}")
306
+ values = element_losses(self.function, guess, truth)
307
+ return weighted(values, batch[self.sample_weight], reduction_of(self.function), label)
308
+
309
+
310
+ def wrap_loss(obj, keys, name, path):
311
+ keys = dict(keys or {})
312
+ if isinstance(obj, type):
313
+ obj = construct(obj, path)
314
+ if isinstance(obj, Objective):
315
+ apply_keys(obj, keys, path)
316
+ result = obj
317
+ elif callable(obj):
318
+ result = Criterion(obj, pred=keys.get("pred"), target=keys.get("target"), rescale=bool(keys.get("rescale")),
319
+ on=keys.get("on", DEFAULT_ON), every=keys.get("every", 1),
320
+ sample_weight=keys.get("sample_weight"), path=path)
321
+ else:
322
+ raise BuildError(f"{path}: {type(obj).__name__} is not a loss; a loss is an Objective or a callable "
323
+ "taking (pred, target)")
324
+ result.name = name
325
+ result.path = path
326
+ return result
327
+
328
+
329
+ def as_float(value):
330
+ if isinstance(value, dict):
331
+ return {str(key): as_float(inner) for key, inner in value.items()}
332
+ if isinstance(value, torch.Tensor):
333
+ if value.numel() == 1:
334
+ return float(value.item())
335
+ return {str(index): float(item) for index, item in enumerate(value.detach().flatten().tolist())}
336
+ if isinstance(value, (list, tuple)):
337
+ return {str(index): as_float(item) for index, item in enumerate(value)}
338
+ return float(value)
339
+
340
+
341
+ class MetricEntry:
342
+ def __init__(self, name, metric, pred=None, target=None, rescale=None, on=("train", "valid", "test"), every=1):
343
+ self.name = name
344
+ self.metric = metric
345
+ self.pred = pred
346
+ self.target = target
347
+ self.rescale = rescale
348
+ self.on = (on,) if isinstance(on, str) else tuple(on)
349
+ self.every = int(every)
350
+ self.path = f"metrics.{name}"
351
+ self.placed = False
352
+ self.seen = 0
353
+
354
+ def reset(self):
355
+ self.metric.reset()
356
+ self.seen = 0
357
+
358
+ def update(self, outputs, batch, ctx):
359
+ if isinstance(self.metric, ContextMetric):
360
+ self.metric.update(outputs, batch, ctx)
361
+ self.seen += 1
362
+ return
363
+ guess, truth = pair_values(self, outputs, batch, ctx, "metric")
364
+ if isinstance(guess, torch.Tensor):
365
+ guess = guess.detach()
366
+ if not self.placed:
367
+ self.to(guess.device)
368
+ if isinstance(truth, torch.Tensor):
369
+ truth = truth.detach()
370
+ if not truth.is_floating_point() and not truth.is_complex():
371
+ truth = truth.long()
372
+ self.metric.update(guess, truth)
373
+ self.seen += 1
374
+
375
+ def compute(self):
376
+ if not self.seen:
377
+ return math.nan
378
+ return as_float(self.metric.compute())
379
+
380
+ def to(self, device):
381
+ mover = getattr(self.metric, "to", None)
382
+ if callable(mover):
383
+ mover(device)
384
+ self.placed = True
385
+ return self
386
+
387
+ def state_dict(self):
388
+ return {}
389
+
390
+ def load_state_dict(self, state):
391
+ pass
392
+
393
+
394
+ def wrap_metric(obj, keys, name, path):
395
+ keys = dict(keys or {})
396
+ if isinstance(obj, type):
397
+ obj = construct(obj, path)
398
+ if isinstance(obj, ContextMetric):
399
+ apply_keys(obj, keys, path)
400
+ obj.name = name
401
+ obj.path = path
402
+ return obj
403
+ if not all(callable(getattr(obj, method, None)) for method in ("update", "compute", "reset")):
404
+ raise BuildError(f"{path}: {type(obj).__name__} is not a metric; a metric has update(pred, target), "
405
+ "compute() and reset(), or it is a ContextMetric")
406
+ entry = MetricEntry(name, obj, pred=keys.get("pred"), target=keys.get("target"), rescale=keys.get("rescale"),
407
+ on=keys.get("on", DEFAULT_ON), every=keys.get("every", 1))
408
+ entry.path = path
409
+ return entry
410
+
411
+
412
+ class Group:
413
+ def __init__(self, of, match=None, **params):
414
+ self.of = of
415
+ self.match = match
416
+ self.params = dict(params)
417
+
418
+
419
+ class OptimizerEntry:
420
+ def __init__(self, name, target, params, over, minimizes=None, schedule=None):
421
+ self.name = name
422
+ self.target = target
423
+ self.params = dict(params)
424
+ self.over = list(over)
425
+ self.minimizes = minimizes
426
+ self.schedule = schedule
427
+ self.real = None
428
+ self.base = None
429
+ self.scaler = None
430
+ self.updates = 0
431
+ self.pending = None
432
+ self.emas = []
433
+ self.origins = []
434
+ self.path = f"optimizers.{name}"
435
+
436
+ def bind(self, ctx):
437
+ self.build()
438
+
439
+ def build(self):
440
+ if self.real is not None:
441
+ return self.real
442
+ claimed = set()
443
+ groups = []
444
+ origins = []
445
+ for position, item in enumerate(self.over):
446
+ if not isinstance(item, Group):
447
+ continue
448
+ module = unwrap(item.of)
449
+ if module is None:
450
+ continue
451
+ picked = [parameter for name, parameter in module.named_parameters()
452
+ if (item.match is None or fnmatch.fnmatchcase(name, item.match)) and id(parameter) not in claimed]
453
+ if not picked:
454
+ wanted = f" matching {item.match!r}" if item.match else ""
455
+ raise BuildError(f"{self.path}.over[{position}]: no parameter of {name_of(module)} is left for the "
456
+ f"group{wanted}")
457
+ claimed.update(id(parameter) for parameter in picked)
458
+ groups.append({"params": picked, **item.params})
459
+ origins.append(item)
460
+ rest = []
461
+ for item in self.over:
462
+ if isinstance(item, Group):
463
+ continue
464
+ module = unwrap(item)
465
+ if module is None:
466
+ continue
467
+ for parameter in module.parameters():
468
+ if id(parameter) not in claimed:
469
+ claimed.add(id(parameter))
470
+ rest.append(parameter)
471
+ if rest:
472
+ groups.insert(0, {"params": rest})
473
+ origins.insert(0, None)
474
+ if not groups:
475
+ raise BuildError(f"{self.path} has no parameters to optimize")
476
+ self.real = self.target(groups, **self.params)
477
+ self.origins = origins
478
+ self.base = [float(group.get("lr", math.nan)) for group in self.real.param_groups]
479
+ if self.pending is not None:
480
+ self.real.load_state_dict(self.pending)
481
+ self.pending = None
482
+ self.restore_params()
483
+ self.reschedule()
484
+ return self.real
485
+
486
+ def restore_params(self):
487
+ for index, (group, origin) in enumerate(zip(self.real.param_groups, self.origins)):
488
+ for name, value in self.params.items():
489
+ if origin is None or name not in origin.params:
490
+ group[name] = value
491
+ if origin is not None:
492
+ group.update(origin.params)
493
+ self.base[index] = float(group.get("lr", math.nan))
494
+
495
+ def owned(self):
496
+ self.build()
497
+ found = []
498
+ seen = set()
499
+ for group in self.real.param_groups:
500
+ for parameter in group["params"]:
501
+ if id(parameter) not in seen:
502
+ seen.add(id(parameter))
503
+ found.append(parameter)
504
+ return found
505
+
506
+ def modules(self):
507
+ found = []
508
+ for item in self.over:
509
+ module = unwrap(item.of if isinstance(item, Group) else item)
510
+ if module is not None and all(module is not other for other in found):
511
+ found.append(module)
512
+ return found
513
+
514
+ def parameters(self):
515
+ found = []
516
+ seen = set()
517
+ for module in self.modules():
518
+ for parameter in module.parameters():
519
+ if id(parameter) not in seen:
520
+ seen.add(id(parameter))
521
+ found.append(parameter)
522
+ return found
523
+
524
+ def scaling(self):
525
+ if self.scaler is None:
526
+ return False
527
+ enabled = getattr(self.scaler, "is_enabled", None)
528
+ return bool(enabled()) if callable(enabled) else True
529
+
530
+ def reschedule(self):
531
+ if self.schedule is None or self.real is None:
532
+ return
533
+ factor = float(self.schedule(self.updates))
534
+ for group, base in zip(self.real.param_groups, self.base):
535
+ group["lr"] = base * factor
536
+
537
+ def zero_grad(self):
538
+ for parameter in self.owned():
539
+ parameter.grad = None
540
+
541
+ def backward(self, loss):
542
+ if self.scaling():
543
+ self.scaler.scale(loss).backward()
544
+ else:
545
+ loss.backward()
546
+
547
+ def clip(self, limit):
548
+ parameters = self.owned()
549
+ if self.scaling():
550
+ self.scaler.unscale_(self.real)
551
+ if limit is None:
552
+ return grad_norm(parameters)
553
+ return float(torch.nn.utils.clip_grad_norm_(parameters, float(limit)))
554
+
555
+ def step(self):
556
+ self.build()
557
+ if self.scaling():
558
+ self.scaler.step(self.real)
559
+ self.scaler.update()
560
+ else:
561
+ self.real.step()
562
+ self.updates += 1
563
+ self.reschedule()
564
+ self.shift_emas()
565
+
566
+ def shift_emas(self):
567
+ for ema in self.emas:
568
+ ema.shift(ema.source)
569
+
570
+ def minimize(self, loss, clip=None):
571
+ self.zero_grad()
572
+ self.backward(loss)
573
+ norm = self.clip(clip)
574
+ self.step()
575
+ return norm
576
+
577
+ def rate(self):
578
+ if self.real is None:
579
+ return float(self.params.get("lr", math.nan))
580
+ return float(self.real.param_groups[0]["lr"])
581
+
582
+ def group_label(self, group):
583
+ label = getattr(group, "label", None) or name_of(unwrap(group.of) or group.of)
584
+ return f"{label}[{group.match}]" if group.match else label
585
+
586
+ def rates(self):
587
+ found = {}
588
+ if self.real is None:
589
+ for item in self.over:
590
+ if isinstance(item, Group):
591
+ found[self.group_label(item)] = float(item.params.get("lr", self.params.get("lr", math.nan)))
592
+ return found
593
+ for index, origin in enumerate(self.origins):
594
+ if origin is not None:
595
+ found[self.group_label(origin)] = float(self.real.param_groups[index]["lr"])
596
+ return found
597
+
598
+ def set_param(self, name, value):
599
+ if name in ("minimizes", "schedule", "over"):
600
+ setattr(self, name, value)
601
+ if name == "schedule":
602
+ self.reschedule()
603
+ return
604
+ self.params[name] = value
605
+ if self.real is None:
606
+ return
607
+ for index, (group, origin) in enumerate(zip(self.real.param_groups, self.origins)):
608
+ if origin is not None and name in origin.params:
609
+ continue
610
+ group[name] = value
611
+ if name == "lr":
612
+ self.base[index] = float(value)
613
+ self.reschedule()
614
+
615
+ def state_dict(self):
616
+ inner = self.pending if self.real is None else self.real.state_dict()
617
+ state = {"torch": inner, "updates": self.updates, "params": dict(self.params),
618
+ "base": None if self.base is None else list(self.base)}
619
+ saver = getattr(self.scaler, "state_dict", None)
620
+ if self.scaling() and callable(saver):
621
+ state["scaler"] = saver()
622
+ return state
623
+
624
+ def load_state_dict(self, state):
625
+ if not state:
626
+ return
627
+ self.updates = int(state.get("updates", 0))
628
+ loader = getattr(self.scaler, "load_state_dict", None)
629
+ if state.get("scaler") is not None and callable(loader):
630
+ loader(state["scaler"])
631
+ inner = state.get("torch")
632
+ if inner is None:
633
+ return
634
+ if self.real is None:
635
+ self.pending = inner
636
+ return
637
+ self.real.load_state_dict(inner)
638
+ self.restore_params()
639
+ self.reschedule()
640
+
641
+
642
+ def freeze_parameters(module, spec):
643
+ module = unwrap(module)
644
+ if spec is True:
645
+ for parameter in module.parameters():
646
+ parameter.requires_grad_(False)
647
+ return
648
+ patterns = [] if not spec else ([spec] if isinstance(spec, str) else [str(pattern) for pattern in spec])
649
+ for name, parameter in module.named_parameters():
650
+ if any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns):
651
+ parameter.requires_grad_(False)
652
+ elif parameter.is_floating_point() or parameter.is_complex():
653
+ parameter.requires_grad_(True)
654
+
655
+
656
+ def active(entry, set_name, turn):
657
+ on = getattr(entry, "on", None)
658
+ if on is not None and set_name not in on:
659
+ return False
660
+ every = int(getattr(entry, "every", 1) or 1)
661
+ return every <= 1 or turn % every == 0
662
+
663
+
664
+ def loss_scalar(value, name):
665
+ if isinstance(value, dict):
666
+ if "loss" not in value:
667
+ raise ValueError(f"loss {name!r} returned a mapping without a 'loss' key; its keys are {sorted(value)}")
668
+ return value["loss"]
669
+ if not isinstance(value, torch.Tensor):
670
+ raise ValueError(f"loss {name!r} returned {type(value).__name__}, not a tensor")
671
+ return value
@@ -0,0 +1,24 @@
1
+ from .compare import aligned_series, config_diff, config_of, differing_params # noqa: F401
2
+ from .reader import ( # noqa: F401
3
+ best_of,
4
+ data_report_of,
5
+ events_of,
6
+ fields_of,
7
+ files_of,
8
+ is_record,
9
+ model_summary_of,
10
+ plots_of,
11
+ progress_of,
12
+ read_events,
13
+ read_history,
14
+ read_manifest,
15
+ read_plan,
16
+ read_updates,
17
+ records_under,
18
+ series_of,
19
+ state_of,
20
+ summary_of,
21
+ updates_of,
22
+ )
23
+ from .stats import prediction_pairs, prediction_stats, prediction_tables, read_predictions # noqa: F401
24
+ from .sweep import best_point, group_folds, is_sweep, rank_points, read_sweep, sweep_points # noqa: F401