opal-optimizer 0.3.5__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.
- opal/__init__.py +7 -0
- opal/_version.py +3 -0
- opal/agent.py +394 -0
- opal/backends/__init__.py +31 -0
- opal/backends/_codex_client.py +450 -0
- opal/backends/_privacy.py +116 -0
- opal/backends/base.py +248 -0
- opal/backends/claude.py +665 -0
- opal/backends/codex.py +356 -0
- opal/backends/mailbox.py +1034 -0
- opal/backends/unavailable.py +161 -0
- opal/cli.py +396 -0
- opal/context.py +111 -0
- opal/defaults.json +11 -0
- opal/defaults.py +54 -0
- opal/estimate.py +120 -0
- opal/hf.py +109 -0
- opal/misc/__init__.py +2 -0
- opal/misc/aggressive.py +157 -0
- opal/misc/audit.py +84 -0
- opal/misc/boundary.py +226 -0
- opal/misc/checkpoint.py +399 -0
- opal/misc/ddp.py +147 -0
- opal/misc/integrity.py +263 -0
- opal/misc/nested.py +67 -0
- opal/misc/neutral_analysis.py +166 -0
- opal/misc/precision.py +210 -0
- opal/misc/recovery.py +231 -0
- opal/misc/schedulers.py +214 -0
- opal/misc/setup.py +253 -0
- opal/misc/skip.py +165 -0
- opal/misc/taskfacts.py +293 -0
- opal/misc/waiting.py +58 -0
- opal/programs.py +482 -0
- opal/prompts/example_updater.py +252 -0
- opal/prompts/neutral.md +206 -0
- opal/report.py +1001 -0
- opal/report_page.py +175 -0
- opal/report_privacy.py +159 -0
- opal/results.py +80 -0
- opal/schedule.py +134 -0
- opal/summaries.py +224 -0
- opal/wrapper.py +1674 -0
- opal_optimizer-0.3.5.dist-info/METADATA +232 -0
- opal_optimizer-0.3.5.dist-info/RECORD +49 -0
- opal_optimizer-0.3.5.dist-info/WHEEL +5 -0
- opal_optimizer-0.3.5.dist-info/entry_points.txt +2 -0
- opal_optimizer-0.3.5.dist-info/licenses/LICENSE +21 -0
- opal_optimizer-0.3.5.dist-info/top_level.txt +1 -0
opal/misc/setup.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Setup done once per run: the system prompt, the user's schedulers, activation hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import weakref
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
from .. import context, programs
|
|
12
|
+
from . import aggressive, neutral_analysis, taskfacts
|
|
13
|
+
from .schedulers import OWNER, follow_class
|
|
14
|
+
from .skip import MAX_SKIPS_IN_A_ROW, user_optimizer_pre_step
|
|
15
|
+
|
|
16
|
+
PROMPT_DIR = Path(__file__).parent.parent / "prompts"
|
|
17
|
+
PROMPT = PROMPT_DIR / "neutral.md" # the default prompt
|
|
18
|
+
# the prompt names; any other prompt is the path of a template file. "aggressive" (OPAL 0.1.2-aggressive): the
|
|
19
|
+
# neutral prompt with the authors' edits of misc/aggressive.py
|
|
20
|
+
CODE_PROMPTS = ("neutral", "aggressive")
|
|
21
|
+
# names of earlier releases, no longer shipped
|
|
22
|
+
REMOVED = ("legacy", "default", "v1")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def prompt_template(prompt) -> str:
|
|
26
|
+
"""The template text of a prompt: "neutral" (or None), "aggressive", or the path of a template file whose {fields}
|
|
27
|
+
are filled as in prompts/neutral.md. Raises ValueError for a removed name or a path that is not a file."""
|
|
28
|
+
if prompt is None or prompt == "neutral":
|
|
29
|
+
return PROMPT.read_text()
|
|
30
|
+
if prompt == "aggressive":
|
|
31
|
+
return aggressive.template(PROMPT.read_text())
|
|
32
|
+
if prompt in REMOVED:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f'opal: prompt={prompt!r} was removed; prompt is "neutral" (the default), "aggressive" or the path of a '
|
|
35
|
+
"template file"
|
|
36
|
+
)
|
|
37
|
+
path = Path(prompt)
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f'opal: prompt must be "neutral" (the default), "aggressive" or the path of a template file; {prompt!r} '
|
|
41
|
+
"is neither a name nor a file"
|
|
42
|
+
)
|
|
43
|
+
return path.read_text()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_prompt(prompt, validation=None):
|
|
47
|
+
"""The prompt a run uses: "neutral" when None. Raises for a prompt that is not a name or a file, and for
|
|
48
|
+
validation=N with a template that does not state the development set (it has no {validation_note})."""
|
|
49
|
+
text = prompt_template(prompt)
|
|
50
|
+
if validation is not None:
|
|
51
|
+
if "{validation_note}" not in text:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"opal: validation=N shows the agent a development set, but this prompt does not say so (it has no "
|
|
54
|
+
"{validation_note} field): use the default neutral prompt, or a template with that field"
|
|
55
|
+
)
|
|
56
|
+
return prompt
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
NEUTRAL_REF = "reference/ holds read-only copies of this training run's code: the training script, the source files of its model\nclass and of any custom optimizer or scheduler (code that is not part of torch), and starting_optimizer.txt with the\nstarting optimizer's full settings. Read them closely before your first edit, and again whenever they would help: the\narchitecture (which layers exist, how they are initialised, which are tied or zero-initialised), the loss, the data\nand batching, and what the training loop does around the optimizer (gradient clipping, accumulation, weight averaging,\nevaluation) tell you what this optimization problem looks like, and where a generic optimizer may not fit it. Let what\nyou find there shape your design. These copies are for reading only; your programs reach the user's optimizer and\nscheduler objects through spec (below)."
|
|
60
|
+
NEUTRAL_REF_PRIVATE = "The person running this training chose not to share the training script or the model source: learn the architecture\nfrom the parameter names and shapes below and from the model summary. reference/starting_optimizer.txt holds the\nstarting optimizer's full settings. Your programs reach the user's optimizer and scheduler objects through spec\n(below)."
|
|
61
|
+
NO_FACTS = "Not given; learn what you need from reference/ (if it is shared) and the model summary below."
|
|
62
|
+
# the neutral prompt's sentences on the agent's shell, and their form when the backend gives it no shell
|
|
63
|
+
SHELL = (
|
|
64
|
+
"Your programs do not run in your shell: they run inside\nthe training process, which reports import and runtime "
|
|
65
|
+
"errors back to you. Your shell runs the training's Python with\ntorch, on CPU only, for checking your programs on "
|
|
66
|
+
"small tensors (shapes, numerics, a few steps on random data). It\ncannot see the model, the data, the evaluation "
|
|
67
|
+
"or the GPUs."
|
|
68
|
+
)
|
|
69
|
+
NO_SHELL = "Your programs run inside the training process, which reports import and runtime errors back to you."
|
|
70
|
+
# reference/ when the training script could not be copied (a notebook, or a script above 300 KB)
|
|
71
|
+
NEUTRAL_REF_NO_SCRIPT = "reference/ holds read-only copies of this training run's code: the source files of its model class and of any custom\noptimizer or scheduler (code that is not part of torch; the training script itself could not be copied), and\nstarting_optimizer.txt with the starting optimizer's full settings. Read them closely before your first edit, and again\nwhenever they would help: the architecture (which layers exist, how they are initialised, which are tied or\nzero-initialised) tells you what this optimization problem looks like, and where a generic optimizer may not fit it.\nLet what you find there shape your design. These copies are for reading only; your programs reach the user's optimizer\nand scheduler objects through spec (below)."
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fill_template(template: str, fields: dict) -> str:
|
|
75
|
+
"""Replace {name} for the known fields only, in one pass (so braces in code examples need no escaping, and a
|
|
76
|
+
field's value is never itself searched for placeholders)."""
|
|
77
|
+
pattern = re.compile(r"\{(" + "|".join(re.escape(k) for k in fields) + r")\}")
|
|
78
|
+
return pattern.sub(lambda m: str(fields[m.group(1)]), template)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SetupMixin:
|
|
82
|
+
def _patch_schedulers(self):
|
|
83
|
+
for sch in self.user_schedulers:
|
|
84
|
+
if not hasattr(type(sch), "_opal_base"):
|
|
85
|
+
sch.__class__ = follow_class(type(sch))
|
|
86
|
+
OWNER[sch] = weakref.ref(self)
|
|
87
|
+
if isinstance(sch, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
88
|
+
self._warn(
|
|
89
|
+
"plateau-metric",
|
|
90
|
+
"ReduceLROnPlateau keeps the metric you pass to its step() (best, num_bad_epochs), and the agent's "
|
|
91
|
+
"programs can read it through spec['user_schedulers']: pass it only a value you would show the agent",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _hook_user_optimizers(self):
|
|
95
|
+
"""A skipped update also leaves the training script's optimizers as they were (misc/skip.py)."""
|
|
96
|
+
self._skip_capture = None
|
|
97
|
+
ref = weakref.ref(self)
|
|
98
|
+
self._opt_hooks = [o.register_step_pre_hook(user_optimizer_pre_step(ref)) for o in self.user_optimizers]
|
|
99
|
+
|
|
100
|
+
def _watch(self, names):
|
|
101
|
+
self._hooks = []
|
|
102
|
+
if not names:
|
|
103
|
+
return
|
|
104
|
+
if hasattr(self.model, "_orig_mod") or hasattr(getattr(self.model, "module", None), "_orig_mod"):
|
|
105
|
+
self._warn(
|
|
106
|
+
"hooks-compiled", "activation capture on a torch.compile'd model adds a graph; prefer an eager model"
|
|
107
|
+
)
|
|
108
|
+
modules = dict(context.bare_model(self.model).named_modules())
|
|
109
|
+
for name in names:
|
|
110
|
+
if name not in modules:
|
|
111
|
+
raise ValueError(f"opal: no module named {name!r} to watch")
|
|
112
|
+
|
|
113
|
+
def hook(_m, _inp, out, _n=name):
|
|
114
|
+
if self._capture_next and torch.is_tensor(out):
|
|
115
|
+
self.activations[_n] = out.detach()
|
|
116
|
+
|
|
117
|
+
self._hooks.append(modules[name].register_forward_hook(hook))
|
|
118
|
+
|
|
119
|
+
def _system_prompt(self, prompt, names_by_id):
|
|
120
|
+
template = prompt_template(prompt)
|
|
121
|
+
nan_pause = " A non-finite training loss pauses training until you answer." if self.pause_on_nonfinite else ""
|
|
122
|
+
auto = (
|
|
123
|
+
"Training pauses at a decision until you finish when your answer would otherwise take effect more than "
|
|
124
|
+
f"{self._auto.max_lag:g} updates late, judged from the time per update and your recent answer times, and "
|
|
125
|
+
"at the first decision, before they are measured; then your edits take effect from the update at which the "
|
|
126
|
+
"decision was requested. Otherwise training continues while you work and your edits take effect at the "
|
|
127
|
+
"first update boundary after you finish. Each message says which applies." + nan_pause
|
|
128
|
+
if getattr(self, "_auto", None) is not None
|
|
129
|
+
else None
|
|
130
|
+
)
|
|
131
|
+
mode = auto or (
|
|
132
|
+
"Training pauses at each decision until you finish; your edits take effect from the update at which the "
|
|
133
|
+
"decision was requested."
|
|
134
|
+
if self.blocking
|
|
135
|
+
else "Training does not pause while you work. Your edits take effect at the first update boundary after "
|
|
136
|
+
f"you finish (within a few updates on one GPU, within {2 * self.sync_every} updates in data-parallel runs); "
|
|
137
|
+
"the next message tells you the update and how many updates passed since the state you saw." + nan_pause
|
|
138
|
+
)
|
|
139
|
+
early = sum(1 for u in self.schedule if u <= max(1, int(self.total * 0.05)))
|
|
140
|
+
placed = f"({early if early else 'none'} of them in the first 5% of training)"
|
|
141
|
+
kind = getattr(self, "_schedule_kind", "front")
|
|
142
|
+
if kind != "front" and self.schedule:
|
|
143
|
+
at = ", ".join(f"{u:,}" for u in self.schedule[:3]) + (
|
|
144
|
+
f", ... {self.schedule[-1]:,}" if len(self.schedule) > 3 else ""
|
|
145
|
+
)
|
|
146
|
+
placed = f"evenly spaced (at updates {at})" if kind == "uniform" else f"(at updates {at})"
|
|
147
|
+
sched = (
|
|
148
|
+
f"{len(self.schedule)} scheduled decisions {placed}, plus any your "
|
|
149
|
+
f"analysis asks for by returning {{'wake': reason}} (at most {self.max_decisions} decisions in all). "
|
|
150
|
+
f"Decisions, and the check for a non-finite loss, happen every {self.default_every} updates (and at your "
|
|
151
|
+
"analysis steps), whatever EVERY you set."
|
|
152
|
+
) + (
|
|
153
|
+
" Each message gives the update of the next scheduled decision."
|
|
154
|
+
if getattr(self, "_show_next", False)
|
|
155
|
+
else ""
|
|
156
|
+
)
|
|
157
|
+
full = programs.describe_user_objects(
|
|
158
|
+
self.user_optimizers, self.user_schedulers, names_by_id, sources=self._share_code
|
|
159
|
+
)
|
|
160
|
+
clip = getattr(self, "_clip_grad_norm", None)
|
|
161
|
+
if clip is not None:
|
|
162
|
+
full += (
|
|
163
|
+
f"\ngradient clipping: the starting program clips the gradients of all these parameters to a global norm "
|
|
164
|
+
f"of {float(clip):g} before the optimizer step (CLIP_GRAD_NORM in updater.py)"
|
|
165
|
+
)
|
|
166
|
+
if "{starting_summary}" in template: # the neutral draft: one line here, the full dump in reference/
|
|
167
|
+
(self.run.workspace / "reference" / "starting_optimizer.txt").write_text(full + "\n")
|
|
168
|
+
example = (PROMPT_DIR / "example_updater.py").read_text() # structure only, no values of ours
|
|
169
|
+
if getattr(self, "_aggressive", False): # with a feedback controller of the learning rates, off
|
|
170
|
+
example = aggressive.example(example)
|
|
171
|
+
(self.run.workspace / "reference" / "example_updater.py").write_text(example)
|
|
172
|
+
groups = context.group_index(self.user_optimizers, self.named)
|
|
173
|
+
table = context.param_table(self.named, groups)
|
|
174
|
+
if len(self.named) > 300:
|
|
175
|
+
(self.run.workspace / "reference" / "parameters.txt").write_text(table + "\n")
|
|
176
|
+
table = (
|
|
177
|
+
f"{len(self.named)} tensors; the full table (name, shape, dtype, group) is in reference/parameters.txt. "
|
|
178
|
+
"First 40:\n" + "\n".join(table.splitlines()[:40])
|
|
179
|
+
)
|
|
180
|
+
fields = dict(
|
|
181
|
+
goal=self.goal,
|
|
182
|
+
model_summary=context.model_summary(self.model, self.named, self.world, self.device),
|
|
183
|
+
param_table=table,
|
|
184
|
+
starting_program=full,
|
|
185
|
+
starting_summary=taskfacts.starting_summary(
|
|
186
|
+
self.user_optimizers, self.user_schedulers, clip=clip, shared=self._share_code
|
|
187
|
+
),
|
|
188
|
+
neutral_reference_note=(
|
|
189
|
+
(NEUTRAL_REF if getattr(self, "_script_copied", True) else NEUTRAL_REF_NO_SCRIPT)
|
|
190
|
+
if self._share_code
|
|
191
|
+
else NEUTRAL_REF_PRIVATE
|
|
192
|
+
),
|
|
193
|
+
shell_note=NO_SHELL if getattr(self, "_agent_shell", True) is False else SHELL,
|
|
194
|
+
analysis_note=neutral_analysis.note(getattr(self, "_neutral_epochs", None)),
|
|
195
|
+
task_facts_section=(
|
|
196
|
+
"TASK FACTS\nFrom the person running the training:\n" + str(self._task_facts).strip() + "\n\n"
|
|
197
|
+
if getattr(self, "_task_facts", None)
|
|
198
|
+
else ""
|
|
199
|
+
),
|
|
200
|
+
time_note=taskfacts.time_limit(getattr(self, "_decision_timeout", None)),
|
|
201
|
+
script_note=(
|
|
202
|
+
", and the training script itself is in reference/"
|
|
203
|
+
if self._share_code and getattr(self, "_script_copied", True)
|
|
204
|
+
else ""
|
|
205
|
+
),
|
|
206
|
+
total_updates=f"{self.total:,}",
|
|
207
|
+
every=self.default_every,
|
|
208
|
+
schedule_note=sched,
|
|
209
|
+
mode_note=mode,
|
|
210
|
+
trial=self.trial_steps,
|
|
211
|
+
max_skips=MAX_SKIPS_IN_A_ROW,
|
|
212
|
+
task_facts=str(getattr(self, "_task_facts", None) or NO_FACTS).strip(),
|
|
213
|
+
run_facts=taskfacts.run_facts(self.total, **getattr(self, "_run_facts", {})),
|
|
214
|
+
validation_note=taskfacts.validation_note(
|
|
215
|
+
getattr(self, "_validation", None), getattr(self, "_validation_size", None)
|
|
216
|
+
),
|
|
217
|
+
evaluation_note=taskfacts.evaluation_note(getattr(self, "_validation", None)),
|
|
218
|
+
decisions_note=taskfacts.decisions(
|
|
219
|
+
self.schedule,
|
|
220
|
+
getattr(self, "_schedule_kind", "front"),
|
|
221
|
+
self.total,
|
|
222
|
+
self.blocking,
|
|
223
|
+
self._auto.max_lag if getattr(self, "_auto", None) is not None else None,
|
|
224
|
+
getattr(self, "_show_next", False),
|
|
225
|
+
getattr(self, "_run_facts", {}).get("epochs"),
|
|
226
|
+
segments=getattr(self, "_schedule_segments", None),
|
|
227
|
+
),
|
|
228
|
+
protocol_note=taskfacts.protocol_note(
|
|
229
|
+
self.default_every,
|
|
230
|
+
self.max_decisions,
|
|
231
|
+
self.blocking,
|
|
232
|
+
self.sync_every,
|
|
233
|
+
self.pause_on_nonfinite,
|
|
234
|
+
),
|
|
235
|
+
reference_note=(
|
|
236
|
+
(
|
|
237
|
+
"reference/ holds read-only copies of the training script and of user code that is not part of "
|
|
238
|
+
"torch (the model class, a custom optimizer or scheduler). Read them to learn the loss, the data, "
|
|
239
|
+
"what the loop does around the optimizer, and the architecture."
|
|
240
|
+
if getattr(self, "_script_copied", True)
|
|
241
|
+
else "reference/ holds read-only copies of user code that is not part of torch (the model class, a "
|
|
242
|
+
"custom optimizer or scheduler; the training script itself could not be copied). Read them to learn "
|
|
243
|
+
"the architecture."
|
|
244
|
+
)
|
|
245
|
+
+ " They are copies for reading: your programs reach the user's optimizer and scheduler objects "
|
|
246
|
+
"through spec (below)."
|
|
247
|
+
if self._share_code
|
|
248
|
+
else "The person running this training chose not to share the training script or the model source: "
|
|
249
|
+
"learn the architecture from the parameter names and shapes below and from the model summary."
|
|
250
|
+
" Your programs reach the user's optimizer and scheduler objects through spec (below)."
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
return fill_template(template, fields)
|
opal/misc/skip.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Skipping an update: a program that sets can_skip = True gets a copy of the parameters and of its state before each
|
|
2
|
+
update, and when its step returns True that update is undone. The program also gets this update's loss (the mean over
|
|
3
|
+
ranks, so every rank decides the same) when its step takes a third argument, ctx."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import types
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.distributed as dist
|
|
12
|
+
|
|
13
|
+
from .. import programs
|
|
14
|
+
from .nested import devices, restore_devices
|
|
15
|
+
|
|
16
|
+
MAX_SKIPS_IN_A_ROW = 10 # after this many, the next update is applied anyway and the agent is woken
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def takes_ctx(updater):
|
|
20
|
+
try:
|
|
21
|
+
params = [
|
|
22
|
+
p
|
|
23
|
+
for p in inspect.signature(updater.step).parameters.values()
|
|
24
|
+
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
|
25
|
+
]
|
|
26
|
+
except (TypeError, ValueError):
|
|
27
|
+
return False
|
|
28
|
+
return len(params) >= 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _tensor_ids(obj, out=None):
|
|
32
|
+
out = set() if out is None else out
|
|
33
|
+
if torch.is_tensor(obj):
|
|
34
|
+
out.add(id(obj))
|
|
35
|
+
elif isinstance(obj, dict):
|
|
36
|
+
for v in obj.values():
|
|
37
|
+
_tensor_ids(v, out)
|
|
38
|
+
elif isinstance(obj, (list, tuple)):
|
|
39
|
+
for v in obj:
|
|
40
|
+
_tensor_ids(v, out)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def user_optimizer_pre_step(ref):
|
|
45
|
+
"""A step pre-hook for each of the training script's optimizers: while a can_skip program's step runs, copy that
|
|
46
|
+
optimizer's state just before it steps (unless the program's state_dict() already holds it), so that a skipped
|
|
47
|
+
update also leaves the user's optimizer as it was. Does nothing otherwise."""
|
|
48
|
+
|
|
49
|
+
def hook(opt, args, kwargs):
|
|
50
|
+
w = ref()
|
|
51
|
+
cap = getattr(w, "_skip_capture", None) if w is not None else None
|
|
52
|
+
if cap is None or id(opt) in cap["opts"]:
|
|
53
|
+
return None
|
|
54
|
+
tensors = [v for st in opt.state.values() for v in st.values() if torch.is_tensor(v)]
|
|
55
|
+
if tensors and all(id(t) in cap["ids"] for t in tensors):
|
|
56
|
+
cap["opts"][id(opt)] = None # already copied with the program's state
|
|
57
|
+
return None
|
|
58
|
+
cap["opts"][id(opt)] = (opt, [(p, st, {k: (v, clone(v)) for k, v in st.items()}) for p, st in opt.state.items()])
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
return hook
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _restore_optimizer_state(opt, saved):
|
|
65
|
+
opt.state.clear()
|
|
66
|
+
for p, st, vals in saved:
|
|
67
|
+
st.clear()
|
|
68
|
+
for k, (v, c) in vals.items():
|
|
69
|
+
if torch.is_tensor(v):
|
|
70
|
+
v.copy_(c)
|
|
71
|
+
st[k] = v
|
|
72
|
+
else:
|
|
73
|
+
st[k] = c
|
|
74
|
+
opt.state[p] = st
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clone(obj):
|
|
78
|
+
if torch.is_tensor(obj):
|
|
79
|
+
return obj.detach().clone()
|
|
80
|
+
if isinstance(obj, dict):
|
|
81
|
+
return {k: clone(v) for k, v in obj.items()}
|
|
82
|
+
if isinstance(obj, (list, tuple)):
|
|
83
|
+
return type(obj)(clone(v) for v in obj)
|
|
84
|
+
return obj
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SkipMixin:
|
|
88
|
+
def _ctx(self, u):
|
|
89
|
+
"""What a step(grads, update, ctx) receives: ctx.loss, this update's loss as a 0-d tensor on the device (the
|
|
90
|
+
mean over ranks; None if the training script passes no loss), and ctx.update."""
|
|
91
|
+
loss = self._step_loss
|
|
92
|
+
if loss is not None:
|
|
93
|
+
loss = loss.detach().float().mean() if torch.is_tensor(loss) else torch.tensor(float(loss))
|
|
94
|
+
loss = loss.reshape(()).to(self.device, torch.float32).clone()
|
|
95
|
+
if self.dist:
|
|
96
|
+
loss = self._mean_over_ranks(loss)
|
|
97
|
+
return types.SimpleNamespace(loss=loss, update=u, skipped_in_a_row=self._skips_in_a_row)
|
|
98
|
+
|
|
99
|
+
def _mean_over_ranks(self, loss):
|
|
100
|
+
"""Every rank joins, whether or not the training script passed it a loss (some scripts pass it on rank 0
|
|
101
|
+
only): the mean over the ranks that did. Whether any rank passes one is agreed once per installed updater
|
|
102
|
+
(one gloo all-reduce); afterwards one all-reduce of (sum, count) on the device stream per update, no host
|
|
103
|
+
sync. None on every rank when no rank passes a loss."""
|
|
104
|
+
if getattr(self, "_ctx_any", None) is None:
|
|
105
|
+
t = torch.tensor([0 if loss is None else 1])
|
|
106
|
+
dist.all_reduce(t, group=self.ctrl)
|
|
107
|
+
self._ctx_any = int(t.item())
|
|
108
|
+
if not self._ctx_any:
|
|
109
|
+
return None
|
|
110
|
+
v = torch.zeros(2, device=self.device, dtype=torch.float32)
|
|
111
|
+
if loss is not None:
|
|
112
|
+
v[0] = loss
|
|
113
|
+
v[1] = 1.0
|
|
114
|
+
dist.all_reduce(v) # default group
|
|
115
|
+
return v[0] / v[1]
|
|
116
|
+
|
|
117
|
+
def _call_updater(self, grads, u):
|
|
118
|
+
"""Run the updater's step for update u, undoing it when it asks to skip."""
|
|
119
|
+
up = self.updater
|
|
120
|
+
args = (grads, u, self._ctx(u)) if self._step_ctx else (grads, u)
|
|
121
|
+
can_skip = bool(getattr(up, "can_skip", False))
|
|
122
|
+
if can_skip:
|
|
123
|
+
with programs.keep_param_group_identity():
|
|
124
|
+
state = up.state_dict()
|
|
125
|
+
before = (clone([p for _, p in self.named]), clone(state), devices(state))
|
|
126
|
+
# the user's optimizers this step drives are copied just before they step (user_optimizer_pre_step)
|
|
127
|
+
self._skip_capture = dict(ids=_tensor_ids(state), opts={})
|
|
128
|
+
try:
|
|
129
|
+
ret = up.step(*args)
|
|
130
|
+
finally:
|
|
131
|
+
captured, self._skip_capture = getattr(self, "_skip_capture", None), None
|
|
132
|
+
if ret is None or ret is False:
|
|
133
|
+
self._skips_in_a_row = 0
|
|
134
|
+
return
|
|
135
|
+
if not can_skip:
|
|
136
|
+
self._warn(
|
|
137
|
+
"skip-without-can_skip", "the updater's step returned a value; set can_skip = True to skip updates"
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
if not bool(ret): # one host read of the flag (a 0-d tensor), only for programs that can skip
|
|
141
|
+
self._skips_in_a_row = 0
|
|
142
|
+
return
|
|
143
|
+
if self._skips_in_a_row >= MAX_SKIPS_IN_A_ROW:
|
|
144
|
+
self._skips_in_a_row = 0
|
|
145
|
+
self.errors.append(
|
|
146
|
+
dict(
|
|
147
|
+
update=u,
|
|
148
|
+
where="skipping updates",
|
|
149
|
+
error=f"your program asked to skip more than {MAX_SKIPS_IN_A_ROW} updates in a row; update {u} was "
|
|
150
|
+
"applied anyway",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
self._skip_alarm = True
|
|
154
|
+
return
|
|
155
|
+
params, state, devs = before
|
|
156
|
+
for (_, p), w in zip(self.named, params):
|
|
157
|
+
p.data.copy_(w)
|
|
158
|
+
with programs.keep_param_group_identity():
|
|
159
|
+
up.load_state_dict(restore_devices(state, devs, self.device))
|
|
160
|
+
for saved in (captured or {}).get("opts", {}).values():
|
|
161
|
+
if saved is not None:
|
|
162
|
+
_restore_optimizer_state(*saved)
|
|
163
|
+
self._skips_in_a_row += 1
|
|
164
|
+
self._skipped.append(u)
|
|
165
|
+
del self._skipped[:-1000]
|