experimaestro 1.5.3__py3-none-any.whl → 1.6.0__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.
Potentially problematic release.
This version of experimaestro might be problematic. Click here for more details.
- experimaestro/__init__.py +0 -1
- experimaestro/__main__.py +3 -423
- experimaestro/cli/__init__.py +312 -0
- experimaestro/{filter.py → cli/filter.py} +4 -4
- experimaestro/cli/jobs.py +261 -0
- experimaestro/click.py +0 -35
- experimaestro/connectors/ssh.py +26 -7
- experimaestro/core/objects.py +13 -6
- experimaestro/core/types.py +8 -3
- experimaestro/experiments/cli.py +97 -63
- experimaestro/experiments/configuration.py +7 -1
- experimaestro/launcherfinder/__init__.py +1 -1
- experimaestro/launcherfinder/base.py +2 -18
- experimaestro/launcherfinder/registry.py +22 -129
- experimaestro/launchers/direct.py +0 -47
- experimaestro/launchers/slurm/base.py +3 -1
- experimaestro/notifications.py +24 -8
- experimaestro/run.py +23 -5
- experimaestro/scheduler/base.py +26 -15
- experimaestro/scheduler/workspace.py +26 -8
- experimaestro/scriptbuilder.py +5 -1
- experimaestro/settings.py +43 -5
- experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
- experimaestro/tests/test_findlauncher.py +1 -1
- experimaestro/tests/test_ssh.py +7 -0
- experimaestro/tests/test_tags.py +35 -0
- experimaestro/tokens.py +8 -8
- experimaestro/utils/resources.py +5 -1
- {experimaestro-1.5.3.dist-info → experimaestro-1.6.0.dist-info}/METADATA +4 -5
- {experimaestro-1.5.3.dist-info → experimaestro-1.6.0.dist-info}/RECORD +33 -59
- {experimaestro-1.5.3.dist-info → experimaestro-1.6.0.dist-info}/WHEEL +1 -1
- {experimaestro-1.5.3.dist-info → experimaestro-1.6.0.dist-info}/entry_points.txt +0 -4
- experimaestro/launchers/slurm/cli.py +0 -29
- experimaestro/launchers/slurm/configuration.py +0 -597
- experimaestro/scheduler/environment.py +0 -94
- experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
- experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
- experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
- experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro/server/data/favicon.ico +0 -0
- experimaestro/server/data/index.css +0 -22844
- experimaestro/server/data/index.css.map +0 -1
- experimaestro/server/data/index.html +0 -27
- experimaestro/server/data/index.js +0 -100947
- experimaestro/server/data/index.js.map +0 -1
- experimaestro/server/data/login.html +0 -22
- experimaestro/server/data/manifest.json +0 -15
- experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
- experimaestro/utils/yaml.py +0 -202
- {experimaestro-1.5.3.dist-info → experimaestro-1.6.0.dist-info}/LICENSE +0 -0
experimaestro/core/objects.py
CHANGED
|
@@ -583,6 +583,9 @@ class ConfigInformation:
|
|
|
583
583
|
# State information
|
|
584
584
|
self.job = None
|
|
585
585
|
|
|
586
|
+
#: True when this configuration was loaded from disk
|
|
587
|
+
self.loaded = False
|
|
588
|
+
|
|
586
589
|
# Explicitely added dependencies
|
|
587
590
|
self.dependencies = []
|
|
588
591
|
|
|
@@ -846,8 +849,8 @@ class ConfigInformation:
|
|
|
846
849
|
dependencies, path + ["__init_tasks__"], taskids
|
|
847
850
|
)
|
|
848
851
|
|
|
849
|
-
# Check for an associated task
|
|
850
|
-
if self.task:
|
|
852
|
+
# Check for an associated task (and not loaded)
|
|
853
|
+
if self.task and not self.loaded:
|
|
851
854
|
if id(self.task) not in taskids:
|
|
852
855
|
taskids.add(id(self.task))
|
|
853
856
|
dependencies.add(self.task.__xpm__.dependency())
|
|
@@ -916,14 +919,14 @@ class ConfigInformation:
|
|
|
916
919
|
|
|
917
920
|
# --- Submit the job
|
|
918
921
|
|
|
922
|
+
# Sets the init tasks
|
|
923
|
+
self.init_tasks = init_tasks
|
|
924
|
+
|
|
919
925
|
# Creates a new job
|
|
920
926
|
self.job = self.xpmtype.task(
|
|
921
927
|
self.pyobject, launcher=launcher, workspace=workspace, run_mode=run_mode
|
|
922
928
|
)
|
|
923
929
|
|
|
924
|
-
# Sets the init tasks
|
|
925
|
-
self.init_tasks = init_tasks
|
|
926
|
-
|
|
927
930
|
# Validate the object
|
|
928
931
|
job_context = JobContext(self.job)
|
|
929
932
|
self.validate_and_seal(job_context)
|
|
@@ -979,6 +982,9 @@ class ConfigInformation:
|
|
|
979
982
|
elif self.job.failedpath.is_file():
|
|
980
983
|
color = "light_red"
|
|
981
984
|
cprint(f"[failed] {s}", color, file=sys.stderr)
|
|
985
|
+
elif self.job.pidpath.is_file():
|
|
986
|
+
color = "blue"
|
|
987
|
+
cprint(f"[running] {s}", color, file=sys.stderr)
|
|
982
988
|
else:
|
|
983
989
|
color = "light_blue"
|
|
984
990
|
cprint(f"[not run] {s}", color, file=sys.stderr)
|
|
@@ -1382,12 +1388,13 @@ class ConfigInformation:
|
|
|
1382
1388
|
else:
|
|
1383
1389
|
o.__init__()
|
|
1384
1390
|
xpminfo = o.__xpm__ # type: ConfigInformation
|
|
1391
|
+
xpminfo.loaded = True
|
|
1385
1392
|
|
|
1386
1393
|
meta = definition.get("meta", None)
|
|
1387
1394
|
if meta:
|
|
1388
1395
|
xpminfo._meta = meta
|
|
1389
1396
|
if xpminfo.xpmtype.task is not None:
|
|
1390
|
-
|
|
1397
|
+
xpminfo.job = object()
|
|
1391
1398
|
|
|
1392
1399
|
# Set the fields
|
|
1393
1400
|
for name, value in definition["fields"].items():
|
experimaestro/core/types.py
CHANGED
|
@@ -325,12 +325,17 @@ class ObjectType(Type):
|
|
|
325
325
|
|
|
326
326
|
# Get the module
|
|
327
327
|
module = inspect.getmodule(self.originaltype)
|
|
328
|
-
|
|
328
|
+
self._module = module.__name__
|
|
329
|
+
self._package = module.__package__
|
|
330
|
+
|
|
331
|
+
if self._module and self._package:
|
|
329
332
|
self._file = None
|
|
330
333
|
else:
|
|
331
334
|
self._file = Path(inspect.getfile(self.originaltype)).absolute()
|
|
332
|
-
|
|
333
|
-
|
|
335
|
+
|
|
336
|
+
assert (
|
|
337
|
+
self._module and self._package
|
|
338
|
+
) or self._file, f"Could not detect module/file for {self.originaltype}"
|
|
334
339
|
|
|
335
340
|
# The class of the object
|
|
336
341
|
|
experimaestro/experiments/cli.py
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
+
import imp
|
|
2
|
+
import importlib
|
|
1
3
|
import inspect
|
|
2
|
-
import itertools
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Any, List, Optional, Protocol, Tuple
|
|
8
|
+
from typing import Any, List, Optional, Protocol, Tuple
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
import omegaconf
|
|
11
12
|
import yaml
|
|
12
|
-
from experimaestro import LauncherRegistry, RunMode, experiment
|
|
13
|
-
from experimaestro.experiments.configuration import ConfigurationBase
|
|
14
|
-
from experimaestro.exceptions import HandledException
|
|
15
|
-
from experimaestro.settings import get_settings, get_workspace
|
|
16
13
|
from omegaconf import OmegaConf, SCMode
|
|
17
14
|
from termcolor import cprint
|
|
18
15
|
|
|
16
|
+
from experimaestro import LauncherRegistry, RunMode, experiment
|
|
17
|
+
from experimaestro.exceptions import HandledException
|
|
18
|
+
from experimaestro.experiments.configuration import ConfigurationBase
|
|
19
|
+
from experimaestro.settings import find_workspace
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class ExperimentHelper:
|
|
21
23
|
"""Helper for experiments"""
|
|
22
24
|
|
|
23
|
-
# The experiment
|
|
24
25
|
xp: experiment
|
|
26
|
+
"""The experiment object"""
|
|
25
27
|
|
|
26
28
|
#: Run function
|
|
27
29
|
callable: "ExperimentCallable"
|
|
@@ -54,18 +56,29 @@ class ExperimentCallable(Protocol):
|
|
|
54
56
|
...
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
class ConfigurationLoader:
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.yamls = []
|
|
62
|
+
self.pythonpath = set()
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
data.extend(load(yaml_file.parent / parent))
|
|
64
|
+
def load(self, yaml_file: Path):
|
|
65
|
+
"""Loads a YAML file, and parents one if they exist"""
|
|
66
|
+
if not yaml_file.exists() and yaml_file.suffix != ".yaml":
|
|
67
|
+
yaml_file = yaml_file.with_suffix(".yaml")
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
with yaml_file.open("rt") as fp:
|
|
70
|
+
_data = yaml.full_load(fp)
|
|
71
|
+
if parent := _data.get("parent", None):
|
|
72
|
+
self.load(yaml_file.parent / parent)
|
|
73
|
+
|
|
74
|
+
self.yamls.append(_data)
|
|
75
|
+
|
|
76
|
+
for path in _data.get("pythonpath", []):
|
|
77
|
+
path = Path(path)
|
|
78
|
+
if path.is_absolute():
|
|
79
|
+
self.pythonpath.add(path.resolve())
|
|
80
|
+
else:
|
|
81
|
+
self.pythonpath.add((yaml_file.parent / path).resolve())
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
@click.option("--debug", is_flag=True, help="Print debug information")
|
|
@@ -105,11 +118,20 @@ def load(yaml_file: Path):
|
|
|
105
118
|
@click.option(
|
|
106
119
|
"--file", "xp_file", help="The file containing the main experimental code"
|
|
107
120
|
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--module-name", "module_name", help="Module containing the experimental code"
|
|
123
|
+
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--workspace",
|
|
126
|
+
type=str,
|
|
127
|
+
default=None,
|
|
128
|
+
help="Workspace ID (reads from settings.yaml in experimaestro config)",
|
|
129
|
+
)
|
|
108
130
|
@click.option(
|
|
109
131
|
"--workdir",
|
|
110
132
|
type=str,
|
|
111
133
|
default=None,
|
|
112
|
-
help="Working
|
|
134
|
+
help="Working environment",
|
|
113
135
|
)
|
|
114
136
|
@click.option("--conf", "-c", "extra_conf", type=str, multiple=True)
|
|
115
137
|
@click.option(
|
|
@@ -128,36 +150,55 @@ def experiments_cli( # noqa: C901
|
|
|
128
150
|
port: int,
|
|
129
151
|
xpm_config_dir: Path,
|
|
130
152
|
workdir: Optional[Path],
|
|
153
|
+
workspace: Optional[str],
|
|
131
154
|
env: List[Tuple[str, str]],
|
|
132
155
|
run_mode: RunMode,
|
|
133
156
|
extra_conf: List[str],
|
|
134
157
|
pre_yaml: List[str],
|
|
135
158
|
post_yaml: List[str],
|
|
159
|
+
module_name: Optional[str],
|
|
136
160
|
args: List[str],
|
|
137
161
|
show: bool,
|
|
138
162
|
debug: bool,
|
|
139
163
|
):
|
|
140
164
|
"""Run an experiment"""
|
|
165
|
+
|
|
141
166
|
# --- Set the logger
|
|
142
167
|
logging.getLogger().setLevel(logging.DEBUG if debug else logging.INFO)
|
|
143
168
|
logging.getLogger("xpm.hash").setLevel(logging.INFO)
|
|
144
169
|
|
|
145
170
|
# --- Loads the YAML
|
|
146
|
-
|
|
171
|
+
conf_loader = ConfigurationLoader()
|
|
147
172
|
for y in pre_yaml:
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
conf_loader.load(Path(y))
|
|
174
|
+
conf_loader.load(Path(yaml_file))
|
|
150
175
|
for y in post_yaml:
|
|
151
|
-
|
|
176
|
+
conf_loader.load(Path(y))
|
|
177
|
+
|
|
178
|
+
# --- Merge the YAMLs
|
|
179
|
+
configuration = OmegaConf.merge(*conf_loader.yamls)
|
|
180
|
+
if extra_conf:
|
|
181
|
+
configuration.merge_with(OmegaConf.from_dotlist(extra_conf))
|
|
152
182
|
|
|
153
183
|
# --- Get the XP file
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
break
|
|
184
|
+
pythonpath = list(conf_loader.pythonpath)
|
|
185
|
+
if module_name is None:
|
|
186
|
+
module_name = configuration.get("module", None)
|
|
158
187
|
|
|
159
|
-
|
|
160
|
-
|
|
188
|
+
if xp_file is None:
|
|
189
|
+
xp_file = configuration.get("file", None)
|
|
190
|
+
if xp_file:
|
|
191
|
+
assert (
|
|
192
|
+
not module_name
|
|
193
|
+
), "Module name and experiment file are mutually exclusive options"
|
|
194
|
+
xp_file = Path(xp_file)
|
|
195
|
+
if not pythonpath:
|
|
196
|
+
pythonpath.append(xp_file.parent)
|
|
197
|
+
logging.info("Using python path: %s", ", ".join(str(s) for s in pythonpath))
|
|
198
|
+
|
|
199
|
+
assert (
|
|
200
|
+
module_name or xp_file
|
|
201
|
+
), "Either the module name or experiment file should be given"
|
|
161
202
|
|
|
162
203
|
# --- Set some options
|
|
163
204
|
|
|
@@ -165,32 +206,35 @@ def experiments_cli( # noqa: C901
|
|
|
165
206
|
assert xpm_config_dir.is_dir()
|
|
166
207
|
LauncherRegistry.set_config_dir(xpm_config_dir)
|
|
167
208
|
|
|
168
|
-
# ---
|
|
169
|
-
xp_file = Path(xp_file)
|
|
170
|
-
if not xp_file.exists() and xp_file.suffix != ".py":
|
|
171
|
-
xp_file = xp_file.with_suffix(".py")
|
|
172
|
-
xp_file = Path(yaml_file).parent / xp_file
|
|
173
|
-
|
|
174
|
-
with open(xp_file, "r") as f:
|
|
175
|
-
source = f.read()
|
|
176
|
-
if sys.version_info < (3, 9):
|
|
177
|
-
the__file__ = str(xp_file)
|
|
178
|
-
else:
|
|
179
|
-
the__file__ = str(xp_file.absolute())
|
|
209
|
+
# --- Finds the "run" function
|
|
180
210
|
|
|
181
|
-
code = compile(source, filename=the__file__, mode="exec")
|
|
182
|
-
_locals: Dict[str, Any] = {}
|
|
183
|
-
|
|
184
|
-
sys.path.append(str(xp_file.parent.absolute()))
|
|
185
211
|
try:
|
|
186
|
-
|
|
212
|
+
for path in pythonpath:
|
|
213
|
+
sys.path.append(str(path))
|
|
214
|
+
|
|
215
|
+
if xp_file:
|
|
216
|
+
if not xp_file.exists() and xp_file.suffix != ".py":
|
|
217
|
+
xp_file = xp_file.with_suffix(".py")
|
|
218
|
+
xp_file: Path = Path(yaml_file).parent / xp_file
|
|
219
|
+
with open(xp_file) as src:
|
|
220
|
+
module_name = xp_file.with_suffix("").name
|
|
221
|
+
mod = imp.load_module(
|
|
222
|
+
module_name,
|
|
223
|
+
src,
|
|
224
|
+
str(xp_file.absolute()),
|
|
225
|
+
(".py", "r", imp.PY_SOURCE),
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
# Module
|
|
229
|
+
mod = importlib.import_module(module_name)
|
|
230
|
+
|
|
231
|
+
helper = getattr(mod, "run", None)
|
|
187
232
|
finally:
|
|
188
|
-
|
|
233
|
+
pass
|
|
189
234
|
|
|
190
235
|
# --- ... and runs it
|
|
191
|
-
helper = _locals.get("run", None)
|
|
192
236
|
if helper is None:
|
|
193
|
-
raise ValueError(f"Could not find run function in {
|
|
237
|
+
raise ValueError(f"Could not find run function in {xp_file}")
|
|
194
238
|
|
|
195
239
|
if not isinstance(helper, ExperimentHelper):
|
|
196
240
|
helper = ExperimentHelper(helper)
|
|
@@ -205,9 +249,6 @@ def experiments_cli( # noqa: C901
|
|
|
205
249
|
schema = list_parameters[1].annotation
|
|
206
250
|
omegaconf_schema = OmegaConf.structured(schema())
|
|
207
251
|
|
|
208
|
-
configuration = OmegaConf.merge(*yamls)
|
|
209
|
-
if extra_conf:
|
|
210
|
-
configuration.merge_with(OmegaConf.from_dotlist(extra_conf))
|
|
211
252
|
if omegaconf_schema is not None:
|
|
212
253
|
try:
|
|
213
254
|
configuration = OmegaConf.merge(omegaconf_schema, configuration)
|
|
@@ -224,26 +265,19 @@ def experiments_cli( # noqa: C901
|
|
|
224
265
|
configuration, structured_config_mode=SCMode.INSTANTIATE
|
|
225
266
|
)
|
|
226
267
|
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
workdir = Path(workdir) if workdir else None
|
|
231
|
-
if (workdir is None) or (not workdir.is_dir()):
|
|
232
|
-
logging.info("Searching for workspace %s", workdir)
|
|
233
|
-
ws_settings = get_workspace(str(workdir))
|
|
234
|
-
workdir = ws_settings.path.expanduser()
|
|
235
|
-
ws_env = ws_settings.env
|
|
268
|
+
# Define the workspace
|
|
269
|
+
ws_env = find_workspace(workdir=workdir, workspace=workspace)
|
|
270
|
+
workdir = ws_env.path
|
|
236
271
|
|
|
237
272
|
logging.info("Using working directory %s", str(workdir.resolve()))
|
|
238
273
|
|
|
239
274
|
# --- Runs the experiment
|
|
240
275
|
with experiment(
|
|
241
|
-
|
|
276
|
+
ws_env, configuration.id, host=host, port=port, run_mode=run_mode
|
|
242
277
|
) as xp:
|
|
243
278
|
# Set up the environment
|
|
244
279
|
# (1) global settings (2) workspace settings and (3) command line settings
|
|
245
|
-
for key, value in
|
|
246
|
-
logging.info("Setting environment: %s=%s", key, value)
|
|
280
|
+
for key, value in env:
|
|
247
281
|
xp.setenv(key, value)
|
|
248
282
|
|
|
249
283
|
try:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from omegaconf import MISSING
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, List
|
|
3
3
|
import attr
|
|
4
4
|
|
|
5
5
|
try:
|
|
@@ -31,6 +31,12 @@ class ConfigurationBase:
|
|
|
31
31
|
file: str = "experiment"
|
|
32
32
|
"""Relative path of the file containing a run function"""
|
|
33
33
|
|
|
34
|
+
module: Optional[str] = None
|
|
35
|
+
"""Relative path of the file containing a run function"""
|
|
36
|
+
|
|
37
|
+
pythonpath: Optional[List[str]] = None
|
|
38
|
+
"""Python path relative to the parent directory of the YAML file"""
|
|
39
|
+
|
|
34
40
|
parent: Optional[str] = None
|
|
35
41
|
"""Relative path of a YAML file that should be merged"""
|
|
36
42
|
|
|
@@ -1,33 +1,17 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import TYPE_CHECKING, List, Optional
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
3
2
|
|
|
4
|
-
from experimaestro.utils.yaml import YAMLDataClass
|
|
5
|
-
from .specs import HostRequirement
|
|
6
3
|
|
|
7
4
|
if TYPE_CHECKING:
|
|
8
|
-
from experimaestro.launchers import Launcher
|
|
9
5
|
from experimaestro.connectors import Connector
|
|
10
6
|
from experimaestro.tokens import Token
|
|
11
7
|
from .registry import LauncherRegistry
|
|
12
8
|
|
|
13
9
|
|
|
14
|
-
class LauncherConfiguration:
|
|
15
|
-
tags: List[str]
|
|
16
|
-
weight: int
|
|
17
|
-
|
|
18
|
-
"""Generic class for a launcher configuration"""
|
|
19
|
-
|
|
20
|
-
def get(
|
|
21
|
-
self, registry: "LauncherRegistry", requirement: HostRequirement
|
|
22
|
-
) -> Optional["Launcher"]:
|
|
23
|
-
raise NotImplementedError(f"For {self.__class__}")
|
|
24
|
-
|
|
25
|
-
|
|
26
10
|
class ConnectorConfiguration:
|
|
27
11
|
def create(self, registry: "LauncherRegistry") -> "Connector":
|
|
28
12
|
raise NotImplementedError(f"For {self.__class__}")
|
|
29
13
|
|
|
30
14
|
|
|
31
|
-
class TokenConfiguration
|
|
15
|
+
class TokenConfiguration:
|
|
32
16
|
def create(self, registry: "LauncherRegistry", identifier: str) -> "Token":
|
|
33
17
|
raise NotImplementedError(f"For {self.__class__}")
|
|
@@ -1,27 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Configuration registers
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Dict, Optional, Set, Type, Union
|
|
2
4
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
import itertools
|
|
5
|
-
from types import new_class
|
|
6
|
-
from typing import ClassVar, Dict, List, Optional, Set, Type, Union
|
|
7
|
-
from experimaestro import Annotated
|
|
8
5
|
from pathlib import Path
|
|
9
6
|
import typing
|
|
7
|
+
from omegaconf import DictConfig, OmegaConf, SCMode
|
|
10
8
|
import pkg_resources
|
|
11
|
-
import humanfriendly
|
|
12
|
-
import yaml
|
|
13
|
-
from yaml import Loader, Dumper
|
|
14
9
|
from experimaestro.utils import logger
|
|
15
|
-
from experimaestro.utils.yaml import (
|
|
16
|
-
Initialize,
|
|
17
|
-
YAMLDataClass,
|
|
18
|
-
YAMLException,
|
|
19
|
-
YAMLList,
|
|
20
|
-
add_path_resolvers,
|
|
21
|
-
)
|
|
22
10
|
|
|
23
|
-
from .base import
|
|
24
|
-
from .specs import
|
|
11
|
+
from .base import ConnectorConfiguration, TokenConfiguration
|
|
12
|
+
from .specs import HostRequirement
|
|
25
13
|
|
|
26
14
|
if typing.TYPE_CHECKING:
|
|
27
15
|
from experimaestro.launchers import Launcher
|
|
@@ -32,80 +20,20 @@ class LauncherNotFoundError(Exception):
|
|
|
32
20
|
pass
|
|
33
21
|
|
|
34
22
|
|
|
35
|
-
@dataclass
|
|
36
|
-
class GPU(YAMLDataClass):
|
|
37
|
-
"""Represents a GPU"""
|
|
38
|
-
|
|
39
|
-
model: str
|
|
40
|
-
count: int
|
|
41
|
-
memory: Annotated[int, Initialize(humanfriendly.parse_size)]
|
|
42
|
-
|
|
43
|
-
def to_spec(self):
|
|
44
|
-
return [CudaSpecification(self.memory, self.model) for _ in range(self.count)]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class GPUList(YAMLList[GPU]):
|
|
48
|
-
"""Represents a list of GPUs"""
|
|
49
|
-
|
|
50
|
-
def __repr__(self):
|
|
51
|
-
return f"GPUs({super().__repr__()})"
|
|
52
|
-
|
|
53
|
-
def to_spec(self) -> List[CudaSpecification]:
|
|
54
|
-
return list(itertools.chain(*[gpu.to_spec() for gpu in self]))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class CPU(YAMLDataClass):
|
|
59
|
-
"""Represents a CPU"""
|
|
60
|
-
|
|
61
|
-
memory: Annotated[int, Initialize(humanfriendly.parse_size)] = 0
|
|
62
|
-
cores: int = 1
|
|
63
|
-
|
|
64
|
-
def to_spec(self):
|
|
65
|
-
return CPUSpecification(self.memory, self.cores)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@dataclass
|
|
69
|
-
class Host(YAMLDataClass):
|
|
70
|
-
name: str
|
|
71
|
-
gpus: List[GPU]
|
|
72
|
-
launchers: List[str]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Launchers = Dict[str, List[LauncherConfiguration]]
|
|
76
23
|
Connectors = Dict[str, Dict[str, ConnectorConfiguration]]
|
|
77
24
|
Tokens = Dict[str, Dict[str, TokenConfiguration]]
|
|
78
25
|
|
|
79
26
|
|
|
80
|
-
def
|
|
81
|
-
return new_class("LauncherLoader", (yaml.FullLoader,)) # type: ignore
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def load_yaml(loader_cls: Type[Loader], path: Path):
|
|
27
|
+
def load_yaml(schema, path: Path):
|
|
85
28
|
if not path.is_file():
|
|
86
|
-
return
|
|
29
|
+
return {}
|
|
87
30
|
|
|
88
|
-
logger.warning(
|
|
89
|
-
"Using YAML file to configure launchers is deprecated. Please remove %s using launchers.py",
|
|
90
|
-
path,
|
|
91
|
-
)
|
|
92
31
|
logger.debug("Loading %s", path)
|
|
93
32
|
with path.open("rt") as fp:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
loader.dispose()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def unknown_error(loader: Loader, node):
|
|
102
|
-
raise YAMLException(
|
|
103
|
-
"",
|
|
104
|
-
node.start_mark.name,
|
|
105
|
-
node.start_mark.line,
|
|
106
|
-
node.start_mark.column,
|
|
107
|
-
f"No handler defined for key {node}",
|
|
108
|
-
)
|
|
33
|
+
cfg = OmegaConf.load(fp)
|
|
34
|
+
return OmegaConf.to_container(
|
|
35
|
+
OmegaConf.merge(cfg, schema), structured_config_mode=SCMode.INSTANTIATE
|
|
36
|
+
)
|
|
109
37
|
|
|
110
38
|
|
|
111
39
|
class LauncherRegistry:
|
|
@@ -132,27 +60,14 @@ class LauncherRegistry:
|
|
|
132
60
|
LauncherRegistry.CURRENT_CONFIG_DIR = config_dir
|
|
133
61
|
|
|
134
62
|
def __init__(self, basepath: Path):
|
|
135
|
-
self.
|
|
136
|
-
self.
|
|
137
|
-
self.TokenLoader: Type[Loader] = new_loader("TokenLoader")
|
|
138
|
-
self.Dumper: Type[Dumper] = new_class("CustomDumper", (Dumper,), {})
|
|
63
|
+
self.connectors_schema = DictConfig({})
|
|
64
|
+
self.tokens_schema = DictConfig({})
|
|
139
65
|
self.find_launcher_fn = None
|
|
140
66
|
|
|
141
|
-
# Add safeguards
|
|
142
|
-
add_path_resolvers(
|
|
143
|
-
self.LauncherLoader,
|
|
144
|
-
[],
|
|
145
|
-
Dict[str, LauncherConfiguration],
|
|
146
|
-
dumper=self.Dumper,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
67
|
# Use entry points for connectors and launchers
|
|
150
68
|
for entry_point in pkg_resources.iter_entry_points("experimaestro.connectors"):
|
|
151
69
|
entry_point.load().init_registry(self)
|
|
152
70
|
|
|
153
|
-
for entry_point in pkg_resources.iter_entry_points("experimaestro.launchers"):
|
|
154
|
-
entry_point.load().init_registry(self)
|
|
155
|
-
|
|
156
71
|
for entry_point in pkg_resources.iter_entry_points("experimaestro.tokens"):
|
|
157
72
|
entry_point.load().init_registry(self)
|
|
158
73
|
|
|
@@ -172,32 +87,16 @@ class LauncherRegistry:
|
|
|
172
87
|
logger.warn("No find_launcher() function was found in %s", launchers_py)
|
|
173
88
|
|
|
174
89
|
# Read the configuration file
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
self.launchers = sorted(
|
|
179
|
-
itertools.chain(*launchers.values()), key=lambda launcher: -launcher.weight
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
self.connectors: Connectors = (
|
|
183
|
-
load_yaml(self.ConnectorLoader, basepath / "connectors.yaml") or {}
|
|
184
|
-
)
|
|
185
|
-
self.tokens: Tokens = (
|
|
186
|
-
load_yaml(self.TokenLoader, basepath / "tokens.yaml") or {}
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
def register_launcher(self, identifier: str, cls: Type[YAMLDataClass]):
|
|
190
|
-
add_path_resolvers(
|
|
191
|
-
self.LauncherLoader, [identifier, None], cls, dumper=self.Dumper
|
|
90
|
+
self.connectors = load_yaml(
|
|
91
|
+
self.connectors_schema, basepath / "connectors.yaml"
|
|
192
92
|
)
|
|
93
|
+
self.tokens = load_yaml(self.tokens_schema, basepath / "tokens.yaml")
|
|
193
94
|
|
|
194
|
-
def register_connector(self, identifier: str, cls: Type
|
|
195
|
-
|
|
196
|
-
self.ConnectorLoader, [identifier, None], cls, dumper=self.Dumper
|
|
197
|
-
)
|
|
95
|
+
def register_connector(self, identifier: str, cls: Type):
|
|
96
|
+
self.connectors_schema.merge_with({identifier: cls})
|
|
198
97
|
|
|
199
|
-
def register_token(self, identifier: str, cls: Type
|
|
200
|
-
|
|
98
|
+
def register_token(self, identifier: str, cls: Type):
|
|
99
|
+
self.tokens_schema.merge_with({identifier: cls})
|
|
201
100
|
|
|
202
101
|
def getToken(self, identifier: str) -> "Token":
|
|
203
102
|
for tokens in self.tokens.values():
|
|
@@ -227,7 +126,7 @@ class LauncherRegistry:
|
|
|
227
126
|
tags: Restrict the launchers to those containing one of the specified tags
|
|
228
127
|
"""
|
|
229
128
|
|
|
230
|
-
if
|
|
129
|
+
if self.find_launcher_fn is None:
|
|
231
130
|
logger.info("No launchers.yaml file: using local host ")
|
|
232
131
|
from experimaestro.launchers.direct import DirectLauncher
|
|
233
132
|
from experimaestro.connectors.local import LocalConnector
|
|
@@ -250,12 +149,6 @@ class LauncherRegistry:
|
|
|
250
149
|
if launcher := self.find_launcher_fn(spec, tags):
|
|
251
150
|
return launcher
|
|
252
151
|
|
|
253
|
-
# We have registered launchers
|
|
254
|
-
for spec in specs:
|
|
255
|
-
for handler in self.launchers:
|
|
256
|
-
if (not tags) or any((tag in tags) for tag in handler.tags):
|
|
257
|
-
if launcher := handler.get(self, spec):
|
|
258
|
-
return launcher
|
|
259
152
|
return None
|
|
260
153
|
|
|
261
154
|
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
from functools import cached_property
|
|
3
|
-
from typing import Dict, List, Optional
|
|
4
|
-
from experimaestro.launcherfinder import (
|
|
5
|
-
LauncherConfiguration,
|
|
6
|
-
LauncherRegistry,
|
|
7
|
-
HostRequirement,
|
|
8
|
-
)
|
|
9
|
-
from experimaestro.launcherfinder.registry import CPU, GPUList, YAMLDataClass
|
|
10
|
-
from experimaestro.launcherfinder.specs import (
|
|
11
|
-
HostSpecification,
|
|
12
|
-
)
|
|
13
1
|
from experimaestro.scriptbuilder import PythonScriptBuilder
|
|
14
2
|
from . import Launcher
|
|
15
3
|
|
|
@@ -18,40 +6,5 @@ class DirectLauncher(Launcher):
|
|
|
18
6
|
def scriptbuilder(self):
|
|
19
7
|
return PythonScriptBuilder()
|
|
20
8
|
|
|
21
|
-
@staticmethod
|
|
22
|
-
def init_registry(registry: LauncherRegistry):
|
|
23
|
-
registry.register_launcher("local", DirectLauncherConfiguration)
|
|
24
|
-
|
|
25
9
|
def __str__(self):
|
|
26
10
|
return f"DirectLauncher({self.connector})"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass
|
|
30
|
-
class DirectLauncherConfiguration(YAMLDataClass, LauncherConfiguration):
|
|
31
|
-
connector: str = "connector"
|
|
32
|
-
cpu: CPU = field(default_factory=CPU)
|
|
33
|
-
gpus: GPUList = field(default_factory=GPUList)
|
|
34
|
-
tokens: Optional[Dict[str, int]] = None
|
|
35
|
-
tags: List[str] = field(default_factory=lambda: [])
|
|
36
|
-
weight: int = 0
|
|
37
|
-
disable: bool = False
|
|
38
|
-
|
|
39
|
-
@cached_property
|
|
40
|
-
def spec(self) -> HostSpecification:
|
|
41
|
-
return HostSpecification(cpu=self.cpu.to_spec(), cuda=self.gpus.to_spec())
|
|
42
|
-
|
|
43
|
-
def get(
|
|
44
|
-
self, registry: LauncherRegistry, requirement: "HostRequirement"
|
|
45
|
-
) -> Optional[Launcher]:
|
|
46
|
-
if requirement.match(self.spec):
|
|
47
|
-
launcher = DirectLauncher(connector=registry.getConnector(self.connector))
|
|
48
|
-
if self.tokens:
|
|
49
|
-
for token_identifier, count in self.tokens.items():
|
|
50
|
-
token = registry.getToken(token_identifier)
|
|
51
|
-
# TODO: handle the case where this is not a CounterToken
|
|
52
|
-
launcher.addListener(
|
|
53
|
-
lambda job: job.dependencies.add(token.dependency(count))
|
|
54
|
-
)
|
|
55
|
-
return launcher
|
|
56
|
-
|
|
57
|
-
return None
|