-
Notifications
You must be signed in to change notification settings - Fork 15
/
_launch.py
259 lines (203 loc) · 9.12 KB
/
_launch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# Copyright (c) 2022 Massachusetts Institute of Technology
# SPDX-License-Identifier: MIT
from pathlib import Path
from typing import Any, Callable, List, Mapping, Optional, Type, Union
from hydra._internal.callbacks import Callbacks
from hydra._internal.hydra import Hydra
from hydra._internal.utils import create_config_search_path
from hydra.core.config_store import ConfigStore
from hydra.core.global_hydra import GlobalHydra
from hydra.core.utils import JobReturn
from hydra.plugins.sweeper import Sweeper
from hydra.types import HydraContext, RunMode
from omegaconf import DictConfig, OmegaConf
from hydra_zen._hydra_overloads import instantiate
from hydra_zen.typing._implementations import DataClass
def _store_config(
cfg: Union[DataClass, Type[DataClass], DictConfig, Mapping[Any, Any]],
config_name: str = "hydra_launch",
) -> str:
"""Stores configuration object in Hydra's ConfigStore.
Parameters
----------
cfg : Union[DataClass, DictConfig, Mapping]
A configuration as a dataclass, configuration object, or a dictionary.
config_name : str (default: hydra_launch)
The configuration name used to store the configuration.
Returns
-------
config_name : str
The configuration name used to store the default configuration.
Notes
-----
The input configuration is registered in the Hydra ConfigStore [1]_ using a
user-provided config name.
References
----------
.. [1] https://hydra.cc/docs/tutorials/structured_config/config_store
"""
cs = ConfigStore().instance()
cs.store(name=config_name, node=cfg)
return config_name
def launch(
config: Union[DataClass, Type[DataClass], Mapping[str, Any]],
task_function: Callable[[DictConfig], Any],
overrides: Optional[List[str]] = None,
config_dir: Optional[Union[str, Path]] = None,
config_name: str = "zen_launch",
job_name: str = "zen_launch",
with_log_configuration: bool = True,
multirun: bool = False,
) -> Union[JobReturn, Any]:
r"""Launch a Hydra job using a Python-based interface.
`launch` is designed to closely match the interface of the standard Hydra CLI.
For example, launching a Hydra job from the CLI via::
$ python my_task.py job/group=group_name job.group.param=1
corresponds to the following usage of `launch`:
>>> job = launch(config, task_function, overrides=["job/group=group_name", "job.group.param=1"])
Parameters
----------
config : DataClass | Type[DataClass] | Mapping[str, Any]
A config that will be passed to ``task_function``.
task_function : Callable[[DictConfig], Any]
The function that Hydra will execute. Its input will be ``config``, which
has been modified via the specified ``overrides``
overrides : Optional[List[str]]
If provided, sets/overrides values in ``config``. See [1]_ and [2]_
for a detailed discussion of the "grammar" supported by ``overrides``.
config_dir : Optional[Union[str, Path]]
The config path: a directory where Hydra will search for configs.
config_name : str (default: "zen_launch")
Name of the stored configuration in Hydra's ConfigStore API.
job_name : str (default: "zen_launch")
with_log_configuration : bool (default: True)
If ``True``, enables the configuration of the logging subsystem from the loaded config.
multirun : bool (default: False)
Launch a Hydra multi-run ([3]_).
Returns
-------
result : JobReturn | Any
If ``multirun is False``:
A ``JobReturn`` object storing the results of the Hydra experiment via the following attributes
- ``cfg``: Reflects ``config``
- ``overrides``: Reflects ``overrides``
- ``return_value``: The return value of the task function
- ``hydra_cfg``: The Hydra configuration object
- ``working_dir``: The experiment working directory
- ``task_name``: The task name of the Hydra job
- ``status``: A ``JobStatus`` enum reporting whether or not the job completed successfully
Else:
Return values of all launched jobs (depends on the Sweeper implementation).
References
----------
.. [1] https://hydra.cc/docs/next/advanced/override_grammar/basic
.. [2] https://hydra.cc/docs/next/configure_hydra/intro
.. [3] https://hydra.cc/docs/tutorials/basic/running_your_app/multi-run
Examples
--------
**Basic usage**
Let's define and launch a trivial Hydra app.
>>> from hydra_zen import make_config, launch, to_yaml
First, we will define a config, which determines the configurable interface to our
"app". For the purpose of example, we'll design the "interface" of this config to accept
two configurable parameters: ``a`` and ``b``.
>>> Conf = make_config("a", "b")
Our task function accepts the config as an input and uses it to run some generic functionality.
For simplicity's sake, let's design this task function to: convert the job's config to a
yaml-formatted string, print it, and then return the string.
>>> def task_fn(cfg):
... out = to_yaml(cfg) # task's input config, converted to yaml-string
... print(out)
... return out
Now, let's use `launch` to run this task function via Hydra, using particular configured
values (or, "overrides") for ``a`` and ``b``.
>>> job_out = launch(Conf, task_fn, overrides=["a=1", "b='foo'"])
a: 1
b: foo
Let's inspect ``job_out`` to see the ways that it summarizes the results of this job.
>>> job_out.return_value # the value returned by `task_fn`
'a: 1\nb: foo\n'
>>> job_out.working_dir # where the job's outputs, logs, and configs are saved
'outputs/2021-10-19/15-27-11'
>>> job_out.cfg # the particular config used to run our task-function
{'a': 1, 'b': 'foo'}
>>> job_out.overrides # the overrides that we provides
['a=1', "b='foo'"]
>>> job_out.status # the job's completion status
<JobStatus.COMPLETED: 1>
**Launching a multirun job**
We can launch multiple runs of our task-function, using various configured values.
Let's launch a multirun that sweeps over three configurations
>>> (outputs,) = launch(
... Conf,
... task_fn,
... overrides=["a=1,2,3", "b='bar'"],
... multirun=True,
... )
[2021-10-19 17:50:07,334][HYDRA] Launching 3 jobs locally
[2021-10-19 17:50:07,334][HYDRA] #0 : a=1 b='bar'
a: 1
b: bar
[2021-10-19 17:50:07,434][HYDRA] #1 : a=2 b='bar'
a: 2
b: bar
[2021-10-19 17:50:07,535][HYDRA] #2 : a=3 b='bar'
a: 3
b: bar
``outputs`` contains three corresponding ``JobReturns`` instances.
>>> len(outputs)
3
>>> [j.cfg for j in outputs]
[{'a': 1, 'b': 'bar'}, {'a': 2, 'b': 'bar'}, {'a': 3, 'b': 'bar'}]
Each run's outputs, logs, and configs are saved to separate working directories
>>> [j.working_dir for j in outputs]
['multirun/2021-10-19/17-50-07\\0',
'multirun/2021-10-19/17-50-07\\1',
'multirun/2021-10-19/17-50-07\\2']
"""
config_name = _store_config(config, config_name)
if config_dir is not None:
config_dir = str(Path(config_dir).absolute())
search_path = create_config_search_path(config_dir)
hydra = Hydra.create_main_hydra2(task_name=job_name, config_search_path=search_path)
try:
if not multirun:
# Here we can use Hydra's `run` method
job = hydra.run(
config_name=config_name,
task_function=task_function,
overrides=overrides if overrides is not None else [],
with_log_configuration=with_log_configuration,
)
else:
# Instead of running Hydra's `multirun` method we instantiate
# and run the sweeper method. This allows us to run local
# sweepers and launchers without installing them in `hydra_plugins`
# package directory.
cfg = hydra.compose_config(
config_name=config_name,
overrides=overrides if overrides is not None else [],
with_log_configuration=with_log_configuration,
run_mode=RunMode.MULTIRUN,
)
callbacks = Callbacks(cfg)
callbacks.on_multirun_start(config=cfg, config_name=config_name)
# Instantiate sweeper without using Hydra's Plugin discovery (Zen!)
sweeper = instantiate(cfg.hydra.sweeper)
assert isinstance(sweeper, Sweeper)
sweeper.setup(
config=cfg,
hydra_context=HydraContext(
config_loader=hydra.config_loader, callbacks=callbacks
),
task_function=task_function,
)
task_overrides = OmegaConf.to_container(
cfg.hydra.overrides.task, resolve=False
)
assert isinstance(task_overrides, list)
job = sweeper.sweep(arguments=task_overrides)
callbacks.on_multirun_end(config=cfg, config_name=config_name)
finally:
GlobalHydra.instance().clear()
return job