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/rules.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from .context import TrainSettings
|
|
5
|
+
from .errors import RunError, error
|
|
6
|
+
from .refs import SINGULAR, TABLES, jsonable, join_tokens, ref_json, ref_parts, tokenize
|
|
7
|
+
from .spec import is_call
|
|
8
|
+
from .spec import kind as type_name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RULE_KEYS = ("when", "after", "once", "set", "stop", "run")
|
|
12
|
+
ROOTS = TABLES + ("train", "rules")
|
|
13
|
+
INNER = ("module", "metric", "function")
|
|
14
|
+
OPERATIONS = ("times", "plus")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RuleSpec:
|
|
19
|
+
name: str
|
|
20
|
+
when: object = None
|
|
21
|
+
after: str | None = None
|
|
22
|
+
once: bool = True
|
|
23
|
+
set: dict = field(default_factory=dict)
|
|
24
|
+
stop: bool = False
|
|
25
|
+
run: object = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Patch:
|
|
30
|
+
path: str
|
|
31
|
+
value: object
|
|
32
|
+
asked: object
|
|
33
|
+
by: str
|
|
34
|
+
turn: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def rules_by_name(ctx):
|
|
38
|
+
table = getattr(ctx, "rules_by_name", None)
|
|
39
|
+
if not table:
|
|
40
|
+
table = {rule.name: rule for rule in (getattr(ctx, "rules", None) or [])}
|
|
41
|
+
ctx.rules_by_name = table
|
|
42
|
+
return table
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def describe(trigger):
|
|
46
|
+
if trigger is None:
|
|
47
|
+
return "rule"
|
|
48
|
+
method = getattr(trigger, "describe", None)
|
|
49
|
+
return method() if callable(method) else type(trigger).__name__
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def logger_of(ctx):
|
|
53
|
+
return getattr(ctx, "logger", None) or logging.getLogger("kalfa.run")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Rule:
|
|
57
|
+
def __init__(self, spec, when=None, run=None):
|
|
58
|
+
self.spec = spec
|
|
59
|
+
self.name = spec.name
|
|
60
|
+
self.when = when
|
|
61
|
+
self.run = run
|
|
62
|
+
self.after = spec.after
|
|
63
|
+
self.once = spec.once
|
|
64
|
+
self.fired_turn = None
|
|
65
|
+
self.done = False
|
|
66
|
+
|
|
67
|
+
def ready(self, ctx):
|
|
68
|
+
if self.after is None:
|
|
69
|
+
return True
|
|
70
|
+
table = rules_by_name(ctx)
|
|
71
|
+
other = table.get(self.after)
|
|
72
|
+
if other is None:
|
|
73
|
+
raise RunError(f"rule {self.name!r} waits for {self.after!r}, which is no rule; "
|
|
74
|
+
f"the rules are {sorted(table)}")
|
|
75
|
+
return other.fired_turn is not None and other.fired_turn < ctx.turn
|
|
76
|
+
|
|
77
|
+
def fire(self, ctx):
|
|
78
|
+
if self.done and self.once:
|
|
79
|
+
return False
|
|
80
|
+
if not self.ready(ctx):
|
|
81
|
+
return False
|
|
82
|
+
if self.when is not None and not self.when.fires(ctx):
|
|
83
|
+
return False
|
|
84
|
+
for path, value in self.spec.set.items():
|
|
85
|
+
apply_patch(ctx, path, value, by=self.name, turn=ctx.turn)
|
|
86
|
+
if self.spec.stop:
|
|
87
|
+
ctx.stop(f"{self.name}: {describe(self.when)}")
|
|
88
|
+
ctx.stopped_by = self.name
|
|
89
|
+
if self.run is not None:
|
|
90
|
+
self.run(ctx)
|
|
91
|
+
self.fired_turn = ctx.turn
|
|
92
|
+
self.done = self.once
|
|
93
|
+
ctx.fired.append(self.name)
|
|
94
|
+
logger_of(ctx).info("rule %s fired at turn %s%s", self.name, ctx.turn, self.effects())
|
|
95
|
+
if not self.once and self.when is not None:
|
|
96
|
+
reset = getattr(self.when, "reset", None)
|
|
97
|
+
if callable(reset):
|
|
98
|
+
reset()
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
def effects(self):
|
|
102
|
+
parts = [f"{path}={value}" for path, value in self.spec.set.items()]
|
|
103
|
+
if self.spec.stop:
|
|
104
|
+
parts.append("stop")
|
|
105
|
+
if self.run is not None:
|
|
106
|
+
parts.append(f"run {type(self.run).__name__}")
|
|
107
|
+
return ": " + ", ".join(parts) if parts else ""
|
|
108
|
+
|
|
109
|
+
def state_dict(self):
|
|
110
|
+
state = {"fired_turn": self.fired_turn, "done": self.done}
|
|
111
|
+
if self.when is not None:
|
|
112
|
+
state["when"] = self.when.state_dict()
|
|
113
|
+
return state
|
|
114
|
+
|
|
115
|
+
def load_state_dict(self, state):
|
|
116
|
+
self.fired_turn = state.get("fired_turn")
|
|
117
|
+
self.done = bool(state.get("done", False))
|
|
118
|
+
if self.when is not None and "when" in state:
|
|
119
|
+
self.when.load_state_dict(state["when"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def patch_value_problem(value):
|
|
123
|
+
if not isinstance(value, dict):
|
|
124
|
+
return None
|
|
125
|
+
if is_call(value):
|
|
126
|
+
return "a call cannot be a patch value; set a scalar, {times: x}, {plus: x} or an @reference"
|
|
127
|
+
if len(value) == 1 and next(iter(value)) in OPERATIONS:
|
|
128
|
+
amount = next(iter(value.values()))
|
|
129
|
+
if isinstance(amount, (int, float)) and not isinstance(amount, bool):
|
|
130
|
+
return None
|
|
131
|
+
return f"{next(iter(value))} takes a number, got {amount!r}"
|
|
132
|
+
if set(value) in ({"ref"}, {"ref", "attr"}):
|
|
133
|
+
return None
|
|
134
|
+
return "a patch value is a scalar, {times: x}, {plus: x} or an @reference"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_rules(raw, problems):
|
|
138
|
+
if raw is None:
|
|
139
|
+
return []
|
|
140
|
+
if not isinstance(raw, dict):
|
|
141
|
+
problems.append(error("rules_type", f"rules is a mapping of rule name to rule, got {type_name(raw)}", "rules"))
|
|
142
|
+
return []
|
|
143
|
+
keys_hint = "the rule keys are " + ", ".join(RULE_KEYS)
|
|
144
|
+
specs = []
|
|
145
|
+
for raw_name, body in raw.items():
|
|
146
|
+
name = str(raw_name)
|
|
147
|
+
path = f"rules.{name}"
|
|
148
|
+
if not isinstance(body, dict):
|
|
149
|
+
problems.append(error("rule_type", f"a rule is a mapping, got {type_name(body)}", path, hint=keys_hint))
|
|
150
|
+
continue
|
|
151
|
+
spec = RuleSpec(name)
|
|
152
|
+
for key in body:
|
|
153
|
+
if key not in RULE_KEYS:
|
|
154
|
+
problems.append(error("rule_key", f"unknown rule key {key!r}", f"{path}.{key}", hint=keys_hint))
|
|
155
|
+
when = body.get("when")
|
|
156
|
+
if when is not None and not is_call(when) and not isinstance(when, str):
|
|
157
|
+
problems.append(error("rule_value", f"when is a trigger call, got {type_name(when)}", f"{path}.when",
|
|
158
|
+
hint="for example when: {use: plateau, watch: val/rmse, patience: 6}"))
|
|
159
|
+
else:
|
|
160
|
+
spec.when = when
|
|
161
|
+
after = body.get("after")
|
|
162
|
+
if after is not None:
|
|
163
|
+
if not isinstance(after, str) or after not in raw:
|
|
164
|
+
problems.append(error("rule_after", f"after names {after!r}, which is no rule", f"{path}.after",
|
|
165
|
+
hint="the rules are " + ", ".join(str(key) for key in raw)))
|
|
166
|
+
elif after == name:
|
|
167
|
+
problems.append(error("rule_after", "a rule cannot wait for itself", f"{path}.after"))
|
|
168
|
+
else:
|
|
169
|
+
spec.after = after
|
|
170
|
+
once = body.get("once", True)
|
|
171
|
+
if isinstance(once, bool):
|
|
172
|
+
spec.once = once
|
|
173
|
+
else:
|
|
174
|
+
problems.append(error("rule_value", f"once is true or false, got {once!r}", f"{path}.once"))
|
|
175
|
+
settings = body.get("set")
|
|
176
|
+
rejected = False
|
|
177
|
+
if "set" in body and not isinstance(settings, dict):
|
|
178
|
+
problems.append(error("rule_set", f"set is a mapping of plan path to value, got {type_name(settings)}",
|
|
179
|
+
f"{path}.set", hint="for example set: {optimizers.main.lr: {times: 0.5}}"))
|
|
180
|
+
elif settings:
|
|
181
|
+
for key, value in settings.items():
|
|
182
|
+
try:
|
|
183
|
+
tokenize(key)
|
|
184
|
+
except RunError as failure:
|
|
185
|
+
problems.append(error("rule_path", str(failure), f"{path}.set"))
|
|
186
|
+
rejected = True
|
|
187
|
+
continue
|
|
188
|
+
message = patch_value_problem(value)
|
|
189
|
+
if message is not None:
|
|
190
|
+
problems.append(error("rule_value", f"{key}: {message}", f"{path}.set"))
|
|
191
|
+
rejected = True
|
|
192
|
+
continue
|
|
193
|
+
spec.set[str(key)] = value
|
|
194
|
+
stop = body.get("stop", False)
|
|
195
|
+
if isinstance(stop, bool):
|
|
196
|
+
spec.stop = stop
|
|
197
|
+
else:
|
|
198
|
+
problems.append(error("rule_value", f"stop is true or false, got {stop!r}", f"{path}.stop"))
|
|
199
|
+
run = body.get("run")
|
|
200
|
+
if run is not None and not is_call(run) and not isinstance(run, str):
|
|
201
|
+
problems.append(error("rule_value", f"run is an action call, got {type_name(run)}", f"{path}.run"))
|
|
202
|
+
else:
|
|
203
|
+
spec.run = run
|
|
204
|
+
malformed_set = rejected or ("set" in body and not isinstance(settings, dict))
|
|
205
|
+
if not spec.set and not spec.stop and spec.run is None and not malformed_set:
|
|
206
|
+
problems.append(error("rule_empty", "a rule needs set, stop or run to do anything", path))
|
|
207
|
+
specs.append(spec)
|
|
208
|
+
return specs
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def holder_with(holder, key):
|
|
212
|
+
if hasattr(holder, key):
|
|
213
|
+
return holder
|
|
214
|
+
for inner in INNER:
|
|
215
|
+
wrapped = getattr(holder, inner, None)
|
|
216
|
+
if wrapped is not None and hasattr(wrapped, key):
|
|
217
|
+
return wrapped
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def read(holder, token, path):
|
|
222
|
+
if isinstance(holder, dict):
|
|
223
|
+
if token not in holder:
|
|
224
|
+
raise RunError(f"{path}: {token!r} is not a key of the mapping; its keys are {sorted(map(str, holder))}")
|
|
225
|
+
return holder[token]
|
|
226
|
+
if isinstance(holder, (list, tuple)):
|
|
227
|
+
if not isinstance(token, int) or not 0 <= token < len(holder):
|
|
228
|
+
raise RunError(f"{path}: {token!r} is no index of a list of {len(holder)}")
|
|
229
|
+
return holder[token]
|
|
230
|
+
if isinstance(token, int):
|
|
231
|
+
raise RunError(f"{path}: {type_name(holder)} takes no index")
|
|
232
|
+
owner = holder_with(holder, token)
|
|
233
|
+
if owner is not None:
|
|
234
|
+
return getattr(owner, token)
|
|
235
|
+
params = getattr(holder, "params", None)
|
|
236
|
+
if isinstance(params, dict) and token in params:
|
|
237
|
+
return params[token]
|
|
238
|
+
raise RunError(f"{path}: {type_name(holder)} has no attribute {token!r}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def walk(holder, rest, path):
|
|
242
|
+
if not rest:
|
|
243
|
+
raise RunError(f"{path}: the path names an object, not one of its params")
|
|
244
|
+
for token in rest[:-1]:
|
|
245
|
+
holder = read(holder, token, path)
|
|
246
|
+
kind = "train" if isinstance(holder, TrainSettings) else "generic"
|
|
247
|
+
return holder, rest[-1], kind
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def root_of(ctx, tokens, path):
|
|
251
|
+
root = tokens[0]
|
|
252
|
+
if root in TABLES:
|
|
253
|
+
if len(tokens) < 2:
|
|
254
|
+
raise RunError(f"{path}: a path names an entry like {root}.<name>")
|
|
255
|
+
table = getattr(ctx, root, None) or {}
|
|
256
|
+
name = tokens[1]
|
|
257
|
+
if name not in table:
|
|
258
|
+
raise RunError(f"{path}: no {SINGULAR[root]} named {name!r}; the {root} are {sorted(table)}")
|
|
259
|
+
return table[name], tokens[2:]
|
|
260
|
+
if root == "train":
|
|
261
|
+
if len(tokens) > 1 and tokens[1] == "step":
|
|
262
|
+
if ctx.step is None:
|
|
263
|
+
raise RunError(f"{path}: there is no step object yet")
|
|
264
|
+
return ctx.step, tokens[2:]
|
|
265
|
+
if ctx.train is None:
|
|
266
|
+
raise RunError(f"{path}: the train settings are not built yet")
|
|
267
|
+
return ctx.train, tokens[1:]
|
|
268
|
+
if root == "rules":
|
|
269
|
+
if len(tokens) < 4 or tokens[2] not in ("when", "run"):
|
|
270
|
+
raise RunError(f"{path}: a rule path is rules.<name>.when.<param> or rules.<name>.run.<param>")
|
|
271
|
+
table = rules_by_name(ctx)
|
|
272
|
+
rule = table.get(tokens[1])
|
|
273
|
+
if rule is None:
|
|
274
|
+
raise RunError(f"{path}: no rule named {tokens[1]!r}; the rules are {sorted(table)}")
|
|
275
|
+
part = getattr(rule, tokens[2], None)
|
|
276
|
+
if part is None:
|
|
277
|
+
raise RunError(f"{path}: rule {tokens[1]!r} has no {tokens[2]}")
|
|
278
|
+
return part, tokens[3:]
|
|
279
|
+
raise RunError(f"{path}: unknown path root {root!r}; the roots are {', '.join(ROOTS)}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def read_path(ctx, path):
|
|
283
|
+
tokens = tokenize(path)
|
|
284
|
+
holder, rest = root_of(ctx, tokens, path)
|
|
285
|
+
for token in rest:
|
|
286
|
+
holder = read(holder, token, path)
|
|
287
|
+
return holder
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def locate(ctx, tokens, path):
|
|
291
|
+
root = tokens[0]
|
|
292
|
+
holder, rest = root_of(ctx, tokens, path)
|
|
293
|
+
if root in TABLES:
|
|
294
|
+
if not rest:
|
|
295
|
+
raise RunError(f"{path}: a patch names an entry and one of its params, like {root}.<name>.<param>")
|
|
296
|
+
if root == "optimizers" and len(rest) == 1:
|
|
297
|
+
return holder, rest[0], "optimizer"
|
|
298
|
+
if root == "models" and rest == ["freeze"]:
|
|
299
|
+
return holder, "freeze", "model"
|
|
300
|
+
return walk(holder, rest, path)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def current_of(holder, key, kind):
|
|
304
|
+
if kind == "optimizer":
|
|
305
|
+
if key == "minimizes":
|
|
306
|
+
return getattr(holder, "minimizes", None)
|
|
307
|
+
params = getattr(holder, "params", None)
|
|
308
|
+
if isinstance(params, dict) and key in params:
|
|
309
|
+
return params[key]
|
|
310
|
+
groups = getattr(getattr(holder, "real", None), "param_groups", None)
|
|
311
|
+
if groups:
|
|
312
|
+
return groups[0].get(key)
|
|
313
|
+
return None
|
|
314
|
+
if kind == "model":
|
|
315
|
+
return getattr(holder, "freeze", None)
|
|
316
|
+
try:
|
|
317
|
+
return read(holder, key, "")
|
|
318
|
+
except RunError:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def as_int(value, path):
|
|
323
|
+
try:
|
|
324
|
+
return int(value)
|
|
325
|
+
except (TypeError, ValueError):
|
|
326
|
+
raise RunError(f"{path}: expected an integer, got {value!r}") from None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def write(holder, key, value, kind, path):
|
|
330
|
+
if kind == "optimizer":
|
|
331
|
+
if key == "minimizes":
|
|
332
|
+
holder.minimizes = value
|
|
333
|
+
return
|
|
334
|
+
if hasattr(holder, "set_param"):
|
|
335
|
+
holder.set_param(key, value)
|
|
336
|
+
return
|
|
337
|
+
elif kind == "model":
|
|
338
|
+
if hasattr(holder, "set_freeze"):
|
|
339
|
+
holder.set_freeze(value)
|
|
340
|
+
else:
|
|
341
|
+
holder.freeze = value
|
|
342
|
+
return
|
|
343
|
+
elif kind == "train":
|
|
344
|
+
if key == "epochs":
|
|
345
|
+
holder.epochs = as_int(value, path)
|
|
346
|
+
return
|
|
347
|
+
if key == "updates" and isinstance(holder.updates, dict) and not isinstance(value, dict):
|
|
348
|
+
holder.updates["total"] = as_int(value, path)
|
|
349
|
+
return
|
|
350
|
+
if isinstance(holder, dict):
|
|
351
|
+
holder[key] = value
|
|
352
|
+
return
|
|
353
|
+
if isinstance(holder, tuple):
|
|
354
|
+
raise RunError(f"{path}: a tuple cannot be patched in place")
|
|
355
|
+
if isinstance(holder, list):
|
|
356
|
+
if not isinstance(key, int) or not 0 <= key < len(holder):
|
|
357
|
+
raise RunError(f"{path}: {key!r} is no index of a list of {len(holder)}")
|
|
358
|
+
holder[key] = value
|
|
359
|
+
return
|
|
360
|
+
if isinstance(key, int):
|
|
361
|
+
raise RunError(f"{path}: {type_name(holder)} takes no index")
|
|
362
|
+
owner = holder_with(holder, key)
|
|
363
|
+
if owner is None:
|
|
364
|
+
raise RunError(f"{path}: {type_name(holder)} has no attribute {key!r}")
|
|
365
|
+
setattr(owner, key, value)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def resolve_ref(ctx, text):
|
|
369
|
+
label = f"@{text}"
|
|
370
|
+
tokens = tokenize(text)
|
|
371
|
+
root, rest = tokens[0], tokens[1:]
|
|
372
|
+
if root in TABLES:
|
|
373
|
+
if not rest:
|
|
374
|
+
raise RunError(f"{label} names a section; a reference names an entry like @{root}.<name>")
|
|
375
|
+
table = getattr(ctx, root, None) or {}
|
|
376
|
+
name = rest[0]
|
|
377
|
+
if name not in table:
|
|
378
|
+
raise RunError(f"{label}: no {SINGULAR[root]} named {name!r}; the {root} are {sorted(table)}")
|
|
379
|
+
obj = table[name]
|
|
380
|
+
rest = rest[1:]
|
|
381
|
+
if root == "models":
|
|
382
|
+
if rest and rest[0] == "ema":
|
|
383
|
+
obj = getattr(obj, "ema", None)
|
|
384
|
+
if obj is None:
|
|
385
|
+
raise RunError(f"{label}: model {name!r} has no ema copy")
|
|
386
|
+
rest = rest[1:]
|
|
387
|
+
else:
|
|
388
|
+
obj = getattr(obj, "module", obj)
|
|
389
|
+
elif root in ("data", "generate"):
|
|
390
|
+
obj = getattr(ctx, root, None)
|
|
391
|
+
if obj is None:
|
|
392
|
+
raise RunError(f"{label}: there is no {root} object")
|
|
393
|
+
else:
|
|
394
|
+
raise RunError(f"{label}: unknown reference root {root!r}; a reference starts with "
|
|
395
|
+
+ ", ".join(TABLES + ("data", "generate")))
|
|
396
|
+
for token in rest:
|
|
397
|
+
obj = read(obj, token, label)
|
|
398
|
+
return obj
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def resolve_value(ctx, current, asked):
|
|
402
|
+
text = ref_parts(asked)
|
|
403
|
+
if text is not None:
|
|
404
|
+
return resolve_ref(ctx, text)
|
|
405
|
+
if isinstance(asked, dict) and len(asked) == 1:
|
|
406
|
+
operation, amount = next(iter(asked.items()))
|
|
407
|
+
if operation in OPERATIONS:
|
|
408
|
+
if current is None:
|
|
409
|
+
raise RunError(f"{operation} needs a current value to work from, and there is none")
|
|
410
|
+
return current * amount if operation == "times" else current + amount
|
|
411
|
+
return asked
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def node_prefix(nodes, tokens):
|
|
415
|
+
for count in range(len(tokens) - 1, 0, -1):
|
|
416
|
+
text = join_tokens(tokens[:count])
|
|
417
|
+
if text in nodes:
|
|
418
|
+
return text, tokens[count:]
|
|
419
|
+
return None, tokens
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def assign_nested(container, tokens, value):
|
|
423
|
+
for token in tokens[:-1]:
|
|
424
|
+
if isinstance(container, dict) and token in container:
|
|
425
|
+
container = container[token]
|
|
426
|
+
elif isinstance(container, list) and isinstance(token, int) and 0 <= token < len(container):
|
|
427
|
+
container = container[token]
|
|
428
|
+
else:
|
|
429
|
+
return
|
|
430
|
+
last = tokens[-1]
|
|
431
|
+
if isinstance(container, dict) and not isinstance(last, int):
|
|
432
|
+
container[last] = value
|
|
433
|
+
elif isinstance(container, list) and isinstance(last, int) and 0 <= last < len(container):
|
|
434
|
+
container[last] = value
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def bucket_of(node, key):
|
|
438
|
+
keys = node.get("keys")
|
|
439
|
+
params = node.get("params")
|
|
440
|
+
if isinstance(keys, dict) and key in keys:
|
|
441
|
+
return keys
|
|
442
|
+
if isinstance(params, dict) and key in params:
|
|
443
|
+
return params
|
|
444
|
+
from .build import SLOT_KEYS
|
|
445
|
+
|
|
446
|
+
if key in SLOT_KEYS.get(node.get("slot"), ()):
|
|
447
|
+
return node.setdefault("keys", {})
|
|
448
|
+
return node.setdefault("params", {})
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def plan_write(plan, tokens, value):
|
|
452
|
+
if not isinstance(plan, dict):
|
|
453
|
+
return
|
|
454
|
+
if tokens[0] == "train" and (len(tokens) < 2 or tokens[1] != "step"):
|
|
455
|
+
section = plan.get("train")
|
|
456
|
+
if isinstance(section, dict) and len(tokens) > 1:
|
|
457
|
+
assign_nested(section, tokens[1:], value)
|
|
458
|
+
return
|
|
459
|
+
nodes = plan.get("nodes")
|
|
460
|
+
if not isinstance(nodes, dict):
|
|
461
|
+
return
|
|
462
|
+
node_path, rest = node_prefix(nodes, tokens)
|
|
463
|
+
if node_path is None or not rest or not isinstance(nodes[node_path], dict):
|
|
464
|
+
return
|
|
465
|
+
assign_nested(bucket_of(nodes[node_path], rest[0]), rest, value)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def apply_patch(ctx, path, asked, by="", turn=None):
|
|
469
|
+
turn = ctx.turn if turn is None else turn
|
|
470
|
+
tokens = tokenize(path)
|
|
471
|
+
holder, key, kind = locate(ctx, tokens, path)
|
|
472
|
+
current = current_of(holder, key, kind)
|
|
473
|
+
value = resolve_value(ctx, current, asked)
|
|
474
|
+
write(holder, key, value, kind, path)
|
|
475
|
+
value_json = ref_json(asked) if ref_parts(asked) is not None else jsonable(value, ctx)
|
|
476
|
+
plan_write(getattr(ctx, "plan", None), tokens, value_json)
|
|
477
|
+
record = getattr(ctx, "record", None)
|
|
478
|
+
if record is not None:
|
|
479
|
+
record.event("patch", turn=turn, by=by, path=path, value=value_json, asked=jsonable(asked, ctx))
|
|
480
|
+
write_plan(ctx, record)
|
|
481
|
+
patch = Patch(path, value, asked, by, turn)
|
|
482
|
+
patches = getattr(ctx, "patches", None)
|
|
483
|
+
if isinstance(patches, list):
|
|
484
|
+
patches.append(patch)
|
|
485
|
+
return patch
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def write_plan(ctx, record):
|
|
489
|
+
plan = getattr(ctx, "plan", None)
|
|
490
|
+
writer = getattr(record, "write_json", None)
|
|
491
|
+
if isinstance(plan, dict) and callable(writer):
|
|
492
|
+
writer("plan.json", plan)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def replay(ctx, events, upto_turn):
|
|
496
|
+
record = ctx.record
|
|
497
|
+
ctx.record = None
|
|
498
|
+
count = 0
|
|
499
|
+
try:
|
|
500
|
+
for event in events:
|
|
501
|
+
if not isinstance(event, dict) or event.get("kind") != "patch":
|
|
502
|
+
continue
|
|
503
|
+
turn = event.get("turn") or 0
|
|
504
|
+
if turn > upto_turn:
|
|
505
|
+
continue
|
|
506
|
+
by = f"{event.get('by') or 'patch'} (replay)"
|
|
507
|
+
apply_patch(ctx, event["path"], event["value"], by=by, turn=turn)
|
|
508
|
+
count += 1
|
|
509
|
+
finally:
|
|
510
|
+
ctx.record = record
|
|
511
|
+
if count and record is not None:
|
|
512
|
+
write_plan(ctx, record)
|
|
513
|
+
return count
|