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/std/samplers.py ADDED
@@ -0,0 +1,261 @@
1
+ import itertools
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+ from kalfa.core.aliases import register
7
+ from kalfa.core.errors import RunError
8
+ from kalfa.core.rng import Rng
9
+ from kalfa.std.common import evaluating
10
+ from kalfa.std.data.text import size_of
11
+ from kalfa.std.objectives import NoiseTables
12
+
13
+
14
+ STREAM = "samples"
15
+
16
+
17
+ def module_of(model):
18
+ if isinstance(model, nn.Module):
19
+ return model
20
+ inner = getattr(model, "module", None)
21
+ if isinstance(inner, nn.Module):
22
+ return inner
23
+ if callable(model):
24
+ return model
25
+ raise RunError(f"a sampler needs a model to call, got {type(model).__name__}")
26
+
27
+
28
+ def device_of(ctx, model):
29
+ device = getattr(ctx, "device", None)
30
+ if device is not None:
31
+ return Rng.place(device)
32
+ if isinstance(model, nn.Module):
33
+ for tensor in itertools.chain(model.parameters(), model.buffers()):
34
+ return tensor.device
35
+ return torch.device("cpu")
36
+
37
+
38
+ def count_of(sampler, n):
39
+ count = int(sampler.n if n is None else n)
40
+ if count < 1:
41
+ raise RunError(f"{type(sampler).__name__} draws n samples; n must be positive, got {count}")
42
+ return count
43
+
44
+
45
+ def rng_of(ctx):
46
+ return getattr(ctx, "rng", None)
47
+
48
+
49
+ def normal(ctx, shape, device):
50
+ rng = rng_of(ctx)
51
+ if rng is None:
52
+ return torch.randn(shape, device=device)
53
+ return rng.normal(STREAM, shape, device=device)
54
+
55
+
56
+ def first(result):
57
+ if isinstance(result, dict):
58
+ result = next(iter(result.values()))
59
+ while isinstance(result, (tuple, list)):
60
+ result = result[0]
61
+ return result
62
+
63
+
64
+ def finished(result):
65
+ result = first(result)
66
+ if isinstance(result, torch.Tensor):
67
+ return result.detach().cpu()
68
+ return result
69
+
70
+
71
+ class GanSampler:
72
+ def __init__(self, model, latent, n_classes=None, n=64):
73
+ self.model = model
74
+ self.latent = int(latent)
75
+ self.n_classes = None if n_classes is None else int(n_classes)
76
+ self.n = int(n)
77
+
78
+ def sample(self, ctx, n=None):
79
+ count = count_of(self, n)
80
+ model = module_of(self.model)
81
+ device = device_of(ctx, model)
82
+ with evaluating(model):
83
+ z = normal(ctx, (count, self.latent), device)
84
+ if self.n_classes:
85
+ labels = torch.arange(count, device=device) % self.n_classes
86
+ return finished(model(z, labels))
87
+ return finished(model(z))
88
+
89
+
90
+ class LatentSampler:
91
+ def __init__(self, model, latent, n=64):
92
+ self.model = model
93
+ self.latent = int(latent)
94
+ self.n = int(n)
95
+
96
+ def sample(self, ctx, n=None):
97
+ count = count_of(self, n)
98
+ model = module_of(self.model)
99
+ device = device_of(ctx, model)
100
+ with evaluating(model):
101
+ return finished(model(normal(ctx, (count, self.latent), device)))
102
+
103
+
104
+ class DdpmSampler:
105
+ def __init__(self, model, schedule, shape, n=64, steps=None):
106
+ self.model = model
107
+ self.schedule = schedule
108
+ self.shape = tuple(int(part) for part in shape)
109
+ self.n = int(n)
110
+ self.steps = None if steps is None else int(steps)
111
+ self.noise = None
112
+
113
+ def tables(self, device):
114
+ source = self.schedule
115
+ if not isinstance(source, NoiseTables):
116
+ if self.noise is None or self.noise.schedule is not source:
117
+ self.noise = NoiseTables(source)
118
+ source = self.noise
119
+ placed = source.on(device)
120
+ return int(placed.steps), placed.betas, placed.alphas, placed.cumulative
121
+
122
+ def timesteps(self, total):
123
+ if self.steps is None:
124
+ return list(range(total - 1, -1, -1))
125
+ if self.steps < 1:
126
+ raise RunError(f"ddpm_sampler steps must be positive, got {self.steps}")
127
+ count = min(self.steps, total)
128
+ if count == 1:
129
+ return [total - 1]
130
+ spaced = torch.linspace(total - 1, 0, count, dtype=torch.float64).round().long().tolist()
131
+ return list(dict.fromkeys(int(step) for step in spaced))
132
+
133
+ def sample(self, ctx, n=None):
134
+ count = count_of(self, n)
135
+ model = module_of(self.model)
136
+ device = device_of(ctx, model)
137
+ total, betas, alphas, cumulative = self.tables(device)
138
+ size = (count, *self.shape)
139
+ schedule = self.timesteps(total)
140
+ with evaluating(model):
141
+ x = normal(ctx, size, device)
142
+ for position, step in enumerate(schedule):
143
+ t = torch.full((count,), step, dtype=torch.long, device=device)
144
+ predicted = first(model(x, t)).to(x.dtype)
145
+ if self.steps is None:
146
+ x = self.ancestral(ctx, x, predicted, step, betas, alphas, cumulative, size, device)
147
+ else:
148
+ following = schedule[position + 1] if position + 1 < len(schedule) else None
149
+ x = self.implicit(x, predicted, step, following, cumulative)
150
+ return x.clamp(-1.0, 1.0).detach().cpu()
151
+
152
+ @staticmethod
153
+ def ancestral(ctx, x, predicted, step, betas, alphas, cumulative, size, device):
154
+ coefficient = betas[step] / (1.0 - cumulative[step]).sqrt()
155
+ x = (x - coefficient * predicted) / alphas[step].sqrt()
156
+ if step > 0:
157
+ x = x + betas[step].sqrt() * normal(ctx, size, device)
158
+ return x
159
+
160
+ @staticmethod
161
+ def implicit(x, predicted, step, following, cumulative):
162
+ current = cumulative[step]
163
+ previous = cumulative[following] if following is not None else torch.ones_like(current)
164
+ start = ((x - (1.0 - current).sqrt() * predicted) / current.sqrt()).clamp(-1.0, 1.0)
165
+ return previous.sqrt() * start + (1.0 - previous).sqrt() * predicted
166
+
167
+
168
+ class LmSampler:
169
+ def __init__(self, model, tokenizer, prompt="", max_new_tokens=100, temperature=1.0, top_k=None, n=1):
170
+ self.model = model
171
+ self.tokenizer = tokenizer
172
+ self.prompt = str(prompt)
173
+ self.max_new_tokens = int(max_new_tokens)
174
+ self.temperature = float(temperature)
175
+ self.top_k = None if top_k is None else int(top_k)
176
+ self.n = int(n)
177
+
178
+ def tokenizer_of(self, ctx):
179
+ tokenizer = self.tokenizer
180
+ if tokenizer is None or isinstance(tokenizer, str):
181
+ tokenizer = getattr(getattr(ctx, "data", None), "tokenizer", None)
182
+ if tokenizer is None or isinstance(tokenizer, str) or not callable(getattr(tokenizer, "decode", None)):
183
+ raise RunError("lm_sampler needs a tokenizer with encode and decode; write tokenizer: \"@data.tokenizer\"")
184
+ return tokenizer
185
+
186
+ @staticmethod
187
+ def window_of(ctx):
188
+ limit = getattr(getattr(ctx, "data", None), "seq_len", None)
189
+ if isinstance(limit, int) and not isinstance(limit, bool) and limit > 0:
190
+ return limit
191
+ return None
192
+
193
+ @staticmethod
194
+ def logits_of(result, size=None, length=None):
195
+ if isinstance(result, dict) and "logits" in result:
196
+ result = result["logits"]
197
+ result = first(result)
198
+ if not isinstance(result, torch.Tensor) or result.ndim not in (2, 3):
199
+ shape = tuple(result.shape) if isinstance(result, torch.Tensor) else type(result).__name__
200
+ raise RunError(f"lm_sampler needs logits of shape (batch, tokens, vocabulary) from the model, got {shape}")
201
+ if result.ndim == 3:
202
+ result = result[:, -1, :] if LmSampler.classes_last(result.shape, size, length) else result[:, :, -1]
203
+ return result.float()
204
+
205
+ @staticmethod
206
+ def classes_last(shape, size, length):
207
+ tokens, classes = shape[1], shape[2]
208
+ if tokens == length:
209
+ return True
210
+ if classes == length:
211
+ return False
212
+ if size is None or classes == size:
213
+ return True
214
+ if tokens == size:
215
+ return False
216
+ raise RunError(f"lm_sampler cannot find the vocabulary axis of logits of shape {tuple(shape)}: the model "
217
+ f"read {length} tokens and the tokenizer has {size}; give (batch, tokens, vocabulary) or "
218
+ "(batch, vocabulary, tokens)")
219
+
220
+ def pick(self, logits, generator):
221
+ if self.temperature <= 0:
222
+ return logits.argmax(dim=-1, keepdim=True)
223
+ logits = logits / self.temperature
224
+ if self.top_k:
225
+ keep = min(self.top_k, logits.shape[-1])
226
+ least = torch.topk(logits, keep, dim=-1).values[:, -1:]
227
+ logits = logits.masked_fill(logits < least, float("-inf"))
228
+ return torch.multinomial(torch.softmax(logits, dim=-1), 1, generator=generator)
229
+
230
+ def sample(self, ctx, n=None):
231
+ count = count_of(self, n)
232
+ tokenizer = self.tokenizer_of(ctx)
233
+ size = size_of(tokenizer)
234
+ model = module_of(self.model)
235
+ device = device_of(ctx, model)
236
+ prompt = [int(token) for token in tokenizer.encode(self.prompt)] if self.prompt else []
237
+ skip = 0 if prompt else 1
238
+ ids = torch.tensor([prompt or [0]] * count, dtype=torch.long, device=device)
239
+ limit = self.window_of(ctx)
240
+ rng = rng_of(ctx)
241
+ generator = None if rng is None else rng.stream(STREAM, device)
242
+ with evaluating(model):
243
+ for _ in range(self.max_new_tokens):
244
+ window = ids[:, -limit:] if limit else ids
245
+ logits = self.logits_of(model(window), size, window.shape[1])
246
+ ids = torch.cat([ids, self.pick(logits, generator)], dim=1)
247
+ return [tokenizer.decode(row) for row in ids[:, skip:].cpu().tolist()]
248
+
249
+
250
+ REGISTRATIONS = (
251
+ ("gan_sampler", "kalfa.std.samplers.GanSampler",
252
+ "n samples of model(z) or model(z, label) with z from the samples stream and labels cycling over n_classes"),
253
+ ("latent_sampler", "kalfa.std.samplers.LatentSampler", "n samples of a decoder fed standard normal latents"),
254
+ ("ddpm_sampler", "kalfa.std.samplers.DdpmSampler",
255
+ "reverse diffusion from noise of the given shape with the beta schedule; steps limits the number of steps"),
256
+ ("lm_sampler", "kalfa.std.samplers.LmSampler",
257
+ "autoregressive text from the prompt: max_new_tokens draws at temperature, top_k limits the choices"),
258
+ )
259
+
260
+ for name, target, doc in REGISTRATIONS:
261
+ register(name, target, slot="sampler", doc=doc)
kalfa/std/samples.py ADDED
@@ -0,0 +1,72 @@
1
+ import math
2
+ from pathlib import Path
3
+
4
+ import numpy
5
+ import torch
6
+ from PIL import Image
7
+
8
+ from kalfa.core.record import atomic
9
+
10
+
11
+ def image_grid(images, columns=8, pad=2):
12
+ images = torch.nan_to_num(torch.as_tensor(images).detach().float().cpu())
13
+ if images.ndim == 3:
14
+ images = images.unsqueeze(1)
15
+ count, channels, height, width = images.shape
16
+ columns = max(1, min(columns, count))
17
+ rows = math.ceil(count / columns)
18
+ low, high = float(images.min()), float(images.max())
19
+ if high > low:
20
+ images = (images - low) / (high - low)
21
+ else:
22
+ images = images.clamp(0.0, 1.0)
23
+ canvas = torch.ones((channels, rows * (height + pad) + pad, columns * (width + pad) + pad))
24
+ for index in range(count):
25
+ row, column = divmod(index, columns)
26
+ top = pad + row * (height + pad)
27
+ left = pad + column * (width + pad)
28
+ canvas[:, top:top + height, left:left + width] = images[index]
29
+ if channels == 1:
30
+ canvas = canvas.expand(3, -1, -1)
31
+ return canvas.permute(1, 2, 0).numpy()
32
+
33
+
34
+ def write_png(grid, path, least=256, most=8):
35
+ pixels = numpy.clip(numpy.rint(numpy.asarray(grid) * 255.0), 0, 255).astype(numpy.uint8)
36
+ height, width = pixels.shape[:2]
37
+ scale = max(1, min(most, math.ceil(least / max(height, width, 1))))
38
+ image = Image.fromarray(numpy.ascontiguousarray(pixels))
39
+ if scale > 1:
40
+ image = image.resize((width * scale, height * scale), Image.Resampling.NEAREST)
41
+ with atomic(path) as temporary:
42
+ image.save(temporary, format="PNG")
43
+ return Path(path)
44
+
45
+
46
+ def is_text(samples):
47
+ return isinstance(samples, (list, tuple)) and bool(samples) and all(isinstance(item, str) for item in samples)
48
+
49
+
50
+ def is_images(samples):
51
+ return isinstance(samples, torch.Tensor) and samples.ndim == 4 and samples.shape[1] in (1, 3)
52
+
53
+
54
+ def save_samples(samples, directory, stem, figures=None):
55
+ directory = Path(directory)
56
+ directory.mkdir(parents=True, exist_ok=True)
57
+ if isinstance(samples, str):
58
+ samples = [samples]
59
+ if isinstance(samples, torch.Tensor):
60
+ samples = samples.detach().cpu()
61
+ path = directory / f"{stem}.pt"
62
+ with atomic(path) as temporary:
63
+ torch.save(samples, temporary)
64
+ written = [path]
65
+ if is_text(samples):
66
+ text = directory / f"{stem}.txt"
67
+ with atomic(text) as temporary:
68
+ temporary.write_text("\n\n".join(samples) + "\n", encoding="utf-8")
69
+ written.append(text)
70
+ elif is_images(samples):
71
+ written.append(write_png(image_grid(samples), directory / f"{stem}.png"))
72
+ return written
kalfa/std/schedules.py ADDED
@@ -0,0 +1,74 @@
1
+ import math
2
+
3
+ from kalfa.core.aliases import register
4
+
5
+
6
+ def fraction(update, steps):
7
+ return min(max(float(update) / float(steps), 0.0), 1.0)
8
+
9
+
10
+ class Warmup:
11
+ def __init__(self, steps):
12
+ self.steps = int(steps)
13
+
14
+ def __call__(self, update):
15
+ if self.steps <= 0:
16
+ return 1.0
17
+ return fraction(update, self.steps)
18
+
19
+
20
+ class Cosine:
21
+ def __init__(self, steps, warmup=0, floor=0.0):
22
+ self.steps = int(steps)
23
+ self.warmup = int(warmup)
24
+ self.floor = float(floor)
25
+
26
+ def __call__(self, update):
27
+ update = float(update)
28
+ if self.warmup > 0 and update < self.warmup:
29
+ return fraction(update, self.warmup)
30
+ if self.steps <= self.warmup:
31
+ return self.floor
32
+ progress = fraction(update - self.warmup, self.steps - self.warmup)
33
+ return self.floor + (1.0 - self.floor) * 0.5 * (1.0 + math.cos(math.pi * progress))
34
+
35
+
36
+ class LinearWarmup:
37
+ def __init__(self, start, end, steps):
38
+ self.start = float(start)
39
+ self.end = float(end)
40
+ self.steps = int(steps)
41
+
42
+ def __call__(self, update):
43
+ if self.steps <= 0:
44
+ return self.end
45
+ return self.start + (self.end - self.start) * fraction(update, self.steps)
46
+
47
+
48
+ class StepDecay:
49
+ def __init__(self, every, gamma=0.1):
50
+ self.every = int(every)
51
+ self.gamma = float(gamma)
52
+
53
+ def __call__(self, update):
54
+ if self.every <= 0:
55
+ return 1.0
56
+ return self.gamma ** (int(update) // self.every)
57
+
58
+
59
+ SCHEDULES = (
60
+ ("warmup", "kalfa.std.schedules.Warmup", "a factor rising linearly from 0 to 1 over steps updates, then 1"),
61
+ ("cosine", "kalfa.std.schedules.Cosine", "linear warmup over warmup updates, then cosine decay from 1 to "
62
+ "floor at steps updates"),
63
+ ("linear_warmup", "kalfa.std.schedules.LinearWarmup", "a value moving linearly from start to end over steps "
64
+ "updates, then end; a loss weight or an lr factor"),
65
+ ("step_decay", "kalfa.std.schedules.StepDecay", "a factor multiplied by gamma every every updates"),
66
+ )
67
+
68
+
69
+ def register_names():
70
+ for name, target, doc in SCHEDULES:
71
+ register(name, target, slot="schedule", doc=doc)
72
+
73
+
74
+ register_names()
kalfa/std/steps.py ADDED
@@ -0,0 +1,133 @@
1
+ import torch
2
+
3
+ from kalfa.core.aliases import register
4
+ from kalfa.core.errors import RunError
5
+ from kalfa.core.loop import TrainTurn, autocast, batch_size, detached
6
+ from kalfa.core.protocols import Step
7
+ from kalfa.core.wrap import loss_scalar
8
+
9
+
10
+ class Supervised(Step):
11
+ def __init__(self, order=None, amp=False, grad_clip=None, accumulate=1):
12
+ self.order = None if order is None else list(order)
13
+ self.amp = bool(amp)
14
+ self.grad_clip = None if grad_clip is None else float(grad_clip)
15
+ self.accumulate = int(accumulate or 1)
16
+ self.cached = (None, {})
17
+ self.pending = {}
18
+ self.stepped = True
19
+
20
+ def arrangement(self, ctx):
21
+ order = list(self.order or ctx.optimizers)
22
+ unknown = [name for name in order if name not in ctx.optimizers]
23
+ if unknown:
24
+ raise RunError(f"{self.path}.order names optimizers {unknown} that are not defined; the optimizers are "
25
+ f"{sorted(ctx.optimizers)}")
26
+ return order
27
+
28
+ def outputs_for(self, batch, ctx):
29
+ entry = ctx.predicts()
30
+ if entry is None:
31
+ return {}
32
+ return entry.call(batch)
33
+
34
+ def minimize(self, entry, scalar):
35
+ if self.accumulate <= 1:
36
+ self.stepped = True
37
+ return entry.minimize(scalar, clip=self.grad_clip)
38
+ count = self.pending.get(entry.name, 0) + 1
39
+ if count == 1:
40
+ entry.zero_grad()
41
+ entry.backward(scalar / self.accumulate)
42
+ if count < self.accumulate:
43
+ self.pending[entry.name] = count
44
+ return None
45
+ self.pending[entry.name] = 0
46
+ norm = entry.clip(self.grad_clip)
47
+ entry.step()
48
+ self.stepped = True
49
+ return norm
50
+
51
+ def update(self, entry, outputs, batch, ctx):
52
+ if entry.minimizes is None:
53
+ raise RunError(f"optimizer {entry.name!r} minimizes nothing; write minimizes: \"@losses.<name>\"")
54
+ value = entry.minimizes(outputs, batch, ctx)
55
+ scalar = loss_scalar(value, entry.minimizes.name)
56
+ if not isinstance(scalar, torch.Tensor) or not scalar.requires_grad:
57
+ raise RunError(f"the loss {entry.minimizes.name!r} of optimizer {entry.name!r} carries no gradient; its "
58
+ f"models are frozen or the loss does not depend on their parameters")
59
+ ctx.last_norms[entry.name] = self.minimize(entry, scalar)
60
+ return value
61
+
62
+ def train_batch(self, batch, ctx):
63
+ result = {}
64
+ self.stepped = False
65
+ for name in self.arrangement(ctx):
66
+ outputs = self.outputs_for(batch, ctx)
67
+ self.cached = (ctx.batches, detached(outputs))
68
+ result[name] = self.update(ctx.optimizers[name], outputs, batch, ctx)
69
+ return result
70
+
71
+ def eval_batch(self, batch, ctx):
72
+ stamp, outputs = self.cached
73
+ if stamp is not None and stamp == ctx.batches:
74
+ return outputs
75
+ return self.outputs_for(batch, ctx)
76
+
77
+ def predict(self, batch, ctx):
78
+ return self.outputs_for(batch, ctx)
79
+
80
+
81
+ class Alternating(Supervised):
82
+ def __init__(self, order=None, repeat=None, fresh_batch=False, amp=False, grad_clip=None, accumulate=1):
83
+ super().__init__(order, amp, grad_clip, accumulate)
84
+ self.repeat = dict(repeat or {})
85
+ self.fresh_batch = bool(fresh_batch)
86
+
87
+ def step_once(self, entry, batch, ctx):
88
+ with autocast(ctx, self.amp):
89
+ outputs = self.outputs_for(batch, ctx)
90
+ value = self.update(entry, outputs, batch, ctx)
91
+ self.cached = (ctx.batches, detached(outputs))
92
+ return value
93
+
94
+ def train_turn(self, ctx):
95
+ turn = TrainTurn(ctx, self)
96
+ order = self.arrangement(ctx)
97
+ repeat = {name: int(self.repeat.get(name, 1)) for name in order}
98
+ stepped_by = {name: 0 for name in order}
99
+ while turn.more():
100
+ shared = None if self.fresh_batch else turn.next_batch()
101
+ if shared is None and not self.fresh_batch:
102
+ break
103
+ losses = {}
104
+ last = None
105
+ self.stepped = False
106
+ for name in order:
107
+ entry = ctx.optimizers[name]
108
+ for _ in range(repeat[name]):
109
+ batch = turn.next_batch() if self.fresh_batch else shared
110
+ if batch is None:
111
+ break
112
+ value = self.step_once(entry, batch, ctx)
113
+ turn.means.record(entry.minimizes.name, value, batch_size(batch))
114
+ losses[name] = value
115
+ stepped_by[name] += 1
116
+ last = batch
117
+ if last is None:
118
+ break
119
+ if self.stepped:
120
+ turn.record_update(losses)
121
+ turn.observe(last)
122
+ idle = [name for name in order if stepped_by[name] == 0]
123
+ if idle and turn.taken:
124
+ ctx.logger.warning(f"turn {ctx.turn}: optimizers {idle} took no step; the train loader ran out of "
125
+ f"batches before their place in order {order}")
126
+ return turn.close()
127
+
128
+
129
+ register("supervised", "kalfa.std.steps.Supervised", slot="step",
130
+ doc="One forward pass of the predicts model per batch, then every optimizer in order minimizes its loss")
131
+ register("alternating", "kalfa.std.steps.Alternating", slot="step",
132
+ doc="Per update round each optimizer in order takes repeat[name] steps, on the shared batch or on a fresh "
133
+ "batch per step")
kalfa/std/triggers.py ADDED
@@ -0,0 +1,135 @@
1
+ import math
2
+ import time
3
+
4
+ from kalfa.core.aliases import register
5
+ from kalfa.core.protocols import Trigger
6
+
7
+
8
+ def latest(ctx, key):
9
+ value = (getattr(ctx, "latest", None) or {}).get(key)
10
+ if value is None:
11
+ return None
12
+ number = float(value)
13
+ return None if math.isnan(number) else number
14
+
15
+
16
+ class AfterEpoch(Trigger):
17
+ def __init__(self, at):
18
+ self.at = int(at)
19
+
20
+ def fires(self, ctx):
21
+ return ctx.turn >= self.at
22
+
23
+
24
+ class Every(Trigger):
25
+ def __init__(self, epochs):
26
+ self.epochs = int(epochs)
27
+
28
+ def fires(self, ctx):
29
+ return self.epochs > 0 and ctx.turn % self.epochs == 0
30
+
31
+
32
+ class Plateau(Trigger):
33
+ persist = ("best", "wait")
34
+
35
+ def __init__(self, watch, patience, mode="min", min_delta=0.0):
36
+ if mode not in ("min", "max"):
37
+ raise ValueError(f"mode must be min or max, got {mode!r}")
38
+ self.watch = str(watch)
39
+ self.patience = int(patience)
40
+ self.mode = mode
41
+ self.min_delta = float(min_delta)
42
+ self.best = None
43
+ self.wait = 0
44
+
45
+ def improved(self, value):
46
+ if self.best is None:
47
+ return True
48
+ if self.mode == "max":
49
+ return value > self.best + self.min_delta
50
+ return value < self.best - self.min_delta
51
+
52
+ def reset(self):
53
+ self.best = None
54
+ self.wait = 0
55
+
56
+ def fires(self, ctx):
57
+ value = latest(ctx, self.watch)
58
+ if value is None:
59
+ return False
60
+ if self.improved(value):
61
+ self.best = value
62
+ self.wait = 0
63
+ else:
64
+ self.wait += 1
65
+ return self.wait >= self.patience
66
+
67
+ def describe(self):
68
+ return f"{self.watch} plateau {self.patience}"
69
+
70
+
71
+ class Above(Trigger):
72
+ def __init__(self, watch, value):
73
+ self.watch = str(watch)
74
+ self.value = float(value)
75
+
76
+ def fires(self, ctx):
77
+ current = latest(ctx, self.watch)
78
+ return current is not None and current > self.value
79
+
80
+ def describe(self):
81
+ return f"{self.watch} > {self.value}"
82
+
83
+
84
+ class Below(Trigger):
85
+ def __init__(self, watch, value):
86
+ self.watch = str(watch)
87
+ self.value = float(value)
88
+
89
+ def fires(self, ctx):
90
+ current = latest(ctx, self.watch)
91
+ return current is not None and current < self.value
92
+
93
+ def describe(self):
94
+ return f"{self.watch} < {self.value}"
95
+
96
+
97
+ class TimeBudget(Trigger):
98
+ persist = ("started",)
99
+
100
+ def __init__(self, minutes):
101
+ self.minutes = float(minutes)
102
+ self.started = None
103
+
104
+ def reset(self):
105
+ self.started = None
106
+
107
+ def fires(self, ctx):
108
+ now = time.time()
109
+ if self.started is None:
110
+ self.started = now
111
+ return (now - self.started) >= self.minutes * 60.0
112
+
113
+ def describe(self):
114
+ return f"after {self.minutes} minutes"
115
+
116
+
117
+ TRIGGERS = (
118
+ ("after_epoch", "kalfa.std.triggers.AfterEpoch", "fires from the turn whose count reaches at (absolute, so a "
119
+ "resumed run counts on)"),
120
+ ("every", "kalfa.std.triggers.Every", "fires every epochs turns"),
121
+ ("plateau", "kalfa.std.triggers.Plateau", "fires after patience turns without the watched series improving "
122
+ "by min_delta; turns without a value are not counted"),
123
+ ("above", "kalfa.std.triggers.Above", "fires when the watched series is above value"),
124
+ ("below", "kalfa.std.triggers.Below", "fires when the watched series is below value"),
125
+ ("time_budget", "kalfa.std.triggers.TimeBudget", "fires once minutes have passed since the first turn that "
126
+ "asked it"),
127
+ )
128
+
129
+
130
+ def register_names():
131
+ for name, target, doc in TRIGGERS:
132
+ register(name, target, slot="trigger", doc=doc)
133
+
134
+
135
+ register_names()