hydra-ax-sweeper 1.3.0.dev0__tar.gz → 1.4.0.dev5__tar.gz

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 (22) hide show
  1. hydra_ax_sweeper-1.4.0.dev5/MANIFEST.in +3 -0
  2. hydra_ax_sweeper-1.4.0.dev5/PKG-INFO +35 -0
  3. hydra_ax_sweeper-1.4.0.dev5/hydra_ax_sweeper.egg-info/PKG-INFO +35 -0
  4. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_ax_sweeper.egg-info/SOURCES.txt +3 -1
  5. hydra_ax_sweeper-1.4.0.dev5/hydra_ax_sweeper.egg-info/requires.txt +3 -0
  6. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_plugins/hydra_ax_sweeper/__init__.py +1 -1
  7. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_plugins/hydra_ax_sweeper/_core.py +180 -89
  8. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_plugins/hydra_ax_sweeper/_earlystopper.py +2 -4
  9. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_plugins/hydra_ax_sweeper/config.py +4 -6
  10. hydra_ax_sweeper-1.4.0.dev5/hydra_plugins/hydra_ax_sweeper/py.typed +0 -0
  11. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/setup.py +8 -8
  12. hydra_ax_sweeper-1.4.0.dev5/tests/test_ax_sweeper_plugin.py +478 -0
  13. hydra-ax-sweeper-1.3.0.dev0/MANIFEST.in +0 -3
  14. hydra-ax-sweeper-1.3.0.dev0/PKG-INFO +0 -21
  15. hydra-ax-sweeper-1.3.0.dev0/hydra_ax_sweeper.egg-info/PKG-INFO +0 -21
  16. hydra-ax-sweeper-1.3.0.dev0/hydra_ax_sweeper.egg-info/requires.txt +0 -4
  17. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/README.md +0 -0
  18. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_ax_sweeper.egg-info/dependency_links.txt +0 -0
  19. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_ax_sweeper.egg-info/top_level.txt +0 -0
  20. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/hydra_plugins/hydra_ax_sweeper/ax_sweeper.py +0 -0
  21. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/pyproject.toml +0 -0
  22. {hydra-ax-sweeper-1.3.0.dev0 → hydra_ax_sweeper-1.4.0.dev5}/setup.cfg +0 -0
@@ -0,0 +1,3 @@
1
+ global-exclude *.pyc
2
+ global-exclude __pycache__
3
+ recursive-include hydra_plugins/* *.yaml py.typed
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: hydra-ax-sweeper
3
+ Version: 1.4.0.dev5
4
+ Summary: Hydra Ax Sweeper plugin
5
+ Home-page: https://github.com/facebookresearch/hydra/
6
+ Author: Omry Yadan, Shagun Sodhani
7
+ Author-email: omry@fb.com, sshagunsodhani@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Development Status :: 4 - Beta
16
+ Requires-Python: >=3.11,<3.15
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: hydra-core>=1.1.0.dev7
19
+ Requires-Dist: ax-platform<1.3.0,>=1.2.4
20
+ Requires-Dist: torch>=2.2
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # Hydra Ax Sweeper
33
+ Provides a [`Ax Sweeper`](https://ax.dev/) based Hydra Sweeper supporting parallel execution.
34
+
35
+ See [website](https://hydra.cc/docs/plugins/ax_sweeper/) for more information
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: hydra-ax-sweeper
3
+ Version: 1.4.0.dev5
4
+ Summary: Hydra Ax Sweeper plugin
5
+ Home-page: https://github.com/facebookresearch/hydra/
6
+ Author: Omry Yadan, Shagun Sodhani
7
+ Author-email: omry@fb.com, sshagunsodhani@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Development Status :: 4 - Beta
16
+ Requires-Python: >=3.11,<3.15
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: hydra-core>=1.1.0.dev7
19
+ Requires-Dist: ax-platform<1.3.0,>=1.2.4
20
+ Requires-Dist: torch>=2.2
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # Hydra Ax Sweeper
33
+ Provides a [`Ax Sweeper`](https://ax.dev/) based Hydra Sweeper supporting parallel execution.
34
+
35
+ See [website](https://hydra.cc/docs/plugins/ax_sweeper/) for more information
@@ -11,4 +11,6 @@ hydra_plugins/hydra_ax_sweeper/__init__.py
11
11
  hydra_plugins/hydra_ax_sweeper/_core.py
12
12
  hydra_plugins/hydra_ax_sweeper/_earlystopper.py
13
13
  hydra_plugins/hydra_ax_sweeper/ax_sweeper.py
14
- hydra_plugins/hydra_ax_sweeper/config.py
14
+ hydra_plugins/hydra_ax_sweeper/config.py
15
+ hydra_plugins/hydra_ax_sweeper/py.typed
16
+ tests/test_ax_sweeper_plugin.py
@@ -0,0 +1,3 @@
1
+ hydra-core>=1.1.0.dev7
2
+ ax-platform<1.3.0,>=1.2.4
3
+ torch>=2.2
@@ -1,3 +1,3 @@
1
1
  # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2
2
 
3
- __version__ = "1.3.0.dev0"
3
+ __version__ = "1.4.0.dev5"
@@ -1,11 +1,23 @@
1
1
  # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2
2
  import logging
3
3
  from dataclasses import dataclass
4
- from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union
5
-
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Iterable,
8
+ List,
9
+ Literal,
10
+ Mapping,
11
+ Optional,
12
+ Tuple,
13
+ Union,
14
+ cast,
15
+ )
16
+
17
+ from ax.api.client import Client # type: ignore
18
+ from ax.api.configs import ChoiceParameterConfig, RangeParameterConfig # type: ignore
6
19
  from ax.core import types as ax_types # type: ignore
7
- from ax.exceptions.core import SearchSpaceExhausted # type: ignore
8
- from ax.service.ax_client import AxClient # type: ignore
20
+ from ax.exceptions.core import SearchSpaceExhausted, UnsupportedError # type: ignore
9
21
  from hydra.core.override_parser.overrides_parser import OverridesParser
10
22
  from hydra.core.override_parser.types import IntervalSweep, Override, Transformer
11
23
  from hydra.core.plugins import Plugins
@@ -19,6 +31,12 @@ from .config import AxConfig, ClientConfig, ExperimentConfig
19
31
 
20
32
  log = logging.getLogger(__name__)
21
33
 
34
+ AxRangeParameterType = Literal["float", "int"]
35
+ AxChoiceParameterType = Literal["float", "int", "str", "bool"]
36
+ AxParameterConfig = Union[RangeParameterConfig, ChoiceParameterConfig]
37
+ AxMetricValue = Union[float, Tuple[float, float]]
38
+ AxRawData = Mapping[str, AxMetricValue]
39
+
22
40
 
23
41
  @dataclass
24
42
  class Trial:
@@ -34,7 +52,7 @@ class TrialBatch:
34
52
 
35
53
  def encoder_parameters_into_string(parameters: List[Dict[str, Any]]) -> str:
36
54
  """Convert a list of params into a string"""
37
- mandatory_keys = set(["name", "type", "bounds", "values", "value"])
55
+ mandatory_keys = {"name", "type", "bounds", "values", "value"}
38
56
  parameter_log_string = ""
39
57
  for parameter in parameters:
40
58
  parameter_log_string += "\n"
@@ -52,7 +70,9 @@ def encoder_parameters_into_string(parameters: List[Dict[str, Any]]) -> str:
52
70
  return parameter_log_string
53
71
 
54
72
 
55
- def map_params_to_arg_list(params: Mapping[str, Union[str, float, int]]) -> List[str]:
73
+ def map_params_to_arg_list(
74
+ params: Mapping[str, Union[str, float, int, bool]],
75
+ ) -> List[str]:
56
76
  """Method to map a dictionary of params to a list of string arguments"""
57
77
  arg_list = []
58
78
  for key in params:
@@ -60,45 +80,100 @@ def map_params_to_arg_list(params: Mapping[str, Union[str, float, int]]) -> List
60
80
  return arg_list
61
81
 
62
82
 
83
+ def get_ax_choice_parameter_type(values: Iterable[Any]) -> AxChoiceParameterType:
84
+ value_types = {type(value) for value in values}
85
+ if value_types == {bool}:
86
+ return "bool"
87
+ if value_types <= {int}:
88
+ return "int"
89
+ if value_types <= {int, float}:
90
+ return "float"
91
+ if value_types == {str}:
92
+ return "str"
93
+ raise ValueError(f"Unsupported mixed Ax parameter value types: {value_types}")
94
+
95
+
96
+ def create_ax_parameter_config(param: Dict[Any, Any]) -> AxParameterConfig:
97
+ name = param["name"]
98
+ if param["type"] == "range":
99
+ bounds = param["bounds"]
100
+ range_parameter_type: AxRangeParameterType = (
101
+ "int" if all(type(bound) is int for bound in bounds) else "float"
102
+ )
103
+ return RangeParameterConfig(
104
+ name=name,
105
+ bounds=cast(Tuple[float, float], tuple(bounds)),
106
+ parameter_type=range_parameter_type,
107
+ scaling="log" if param.get("log_scale") else None,
108
+ )
109
+
110
+ if param["type"] == "choice":
111
+ values = param["values"]
112
+ choice_parameter_type = get_ax_choice_parameter_type(values)
113
+ is_ordered = param.get("is_ordered", choice_parameter_type != "str")
114
+ elif param["type"] == "fixed":
115
+ values = [param["value"]]
116
+ choice_parameter_type = get_ax_choice_parameter_type(values)
117
+ is_ordered = None
118
+ else:
119
+ raise ValueError(f"Unexpected Ax parameter type: {param['type']}")
120
+
121
+ if choice_parameter_type == "float":
122
+ values = [float(value) for value in values]
123
+ return ChoiceParameterConfig(
124
+ name=name,
125
+ values=cast(Any, values),
126
+ parameter_type=choice_parameter_type,
127
+ is_ordered=is_ordered,
128
+ )
129
+
130
+
131
+ def create_ax_raw_data(value: Any, objective_name: str, is_noisy: bool) -> AxRawData:
132
+ def normalize_metric(metric_value: Any) -> AxMetricValue:
133
+ assert isinstance(metric_value, (int, float, tuple))
134
+ if isinstance(metric_value, (int, float)):
135
+ mean = float(metric_value)
136
+ return mean if is_noisy else (mean, 0.0)
137
+
138
+ assert len(metric_value) == 2
139
+ mean, sem = metric_value
140
+ assert isinstance(mean, (int, float))
141
+ assert sem is None or isinstance(sem, (int, float))
142
+ return (float(mean), float("nan") if sem is None else float(sem))
143
+
144
+ assert isinstance(value, (int, float, tuple, dict))
145
+ if isinstance(value, dict):
146
+ raw_data: Dict[str, AxMetricValue] = {}
147
+ for metric_name, metric_value in value.items():
148
+ assert isinstance(metric_name, str)
149
+ raw_data[metric_name] = normalize_metric(metric_value)
150
+ return raw_data
151
+
152
+ return {objective_name: normalize_metric(value)}
153
+
154
+
63
155
  def get_one_batch_of_trials(
64
- ax_client: AxClient,
65
- parallelism: Tuple[int, int],
66
- num_trials_so_far: int,
156
+ ax_client: Client,
67
157
  num_max_trials_to_do: int,
68
158
  ) -> TrialBatch:
69
159
  """Returns a TrialBatch that contains a list of trials that can be
70
160
  run in parallel. TrialBatch also flags if the search space is exhausted."""
71
161
 
72
- is_search_space_exhausted = False
73
- # Ax throws an exception if the search space is exhausted. We catch
74
- # the exception and set the flag to True
75
- (num_trials, max_parallelism_setting) = parallelism
76
- if max_parallelism_setting == -1:
77
- # Special case, we can group all the trials into one batch
78
- max_parallelism_setting = num_trials - num_trials_so_far
79
-
80
- if num_trials == -1:
81
- # This is a special case where we can run as many trials in parallel as we want.
82
- # Given that num_trials is also -1, we can run all the trials in parallel.
83
- max_parallelism_setting = num_max_trials_to_do
84
-
85
- list_of_trials = []
86
- for _ in range(max_parallelism_setting):
87
- try:
88
- parameters, trial_index = ax_client.get_next_trial()
89
- list_of_trials.append(
90
- Trial(
91
- overrides=map_params_to_arg_list(params=parameters),
92
- trial_index=trial_index,
93
- )
94
- )
95
- except SearchSpaceExhausted:
96
- is_search_space_exhausted = True
97
- break
162
+ try:
163
+ trials = ax_client.get_next_trials(max_trials=num_max_trials_to_do)
164
+ except SearchSpaceExhausted:
165
+ return TrialBatch(list_of_trials=[], is_search_space_exhausted=True)
98
166
 
167
+ list_of_trials = [
168
+ Trial(
169
+ overrides=map_params_to_arg_list(params=parameters),
170
+ trial_index=trial_index,
171
+ )
172
+ for trial_index, parameters in trials.items()
173
+ ]
99
174
  return TrialBatch(
100
175
  list_of_trials=list_of_trials,
101
- is_search_space_exhausted=is_search_space_exhausted,
176
+ is_search_space_exhausted=len(list_of_trials) == 0,
102
177
  )
103
178
 
104
179
 
@@ -146,56 +221,40 @@ class CoreAxSweeper(Sweeper):
146
221
  ax_client = self.setup_ax_client(arguments)
147
222
 
148
223
  num_trials_left = self.max_trials
149
- max_parallelism = ax_client.get_max_parallelism()
150
- current_parallelism_index = 0
151
- # Index to track the parallelism value we are using right now.
152
224
  is_search_space_exhausted = False
153
225
  # Ax throws an exception if the search space is exhausted. We catch
154
226
  # the exception and set the flag to True
155
227
 
156
228
  best_parameters = {}
157
229
  while num_trials_left > 0 and not is_search_space_exhausted:
158
- current_parallelism = max_parallelism[current_parallelism_index]
159
- num_trials, max_parallelism_setting = current_parallelism
160
- num_trials_so_far = 0
161
- while (
162
- num_trials > num_trials_so_far or num_trials == -1
163
- ) and num_trials_left > 0:
164
- trial_batch = get_one_batch_of_trials(
165
- ax_client=ax_client,
166
- parallelism=current_parallelism,
167
- num_trials_so_far=num_trials_so_far,
168
- num_max_trials_to_do=num_trials_left,
169
- )
170
-
171
- list_of_trials_to_launch = trial_batch.list_of_trials[:num_trials_left]
172
- is_search_space_exhausted = trial_batch.is_search_space_exhausted
230
+ num_trials_to_request = min(num_trials_left, self.max_batch_size or 5)
231
+ trial_batch = get_one_batch_of_trials(
232
+ ax_client=ax_client,
233
+ num_max_trials_to_do=num_trials_to_request,
234
+ )
173
235
 
174
- log.info(
175
- "AxSweeper is launching {} jobs".format(
176
- len(list_of_trials_to_launch)
177
- )
178
- )
236
+ list_of_trials_to_launch = trial_batch.list_of_trials[:num_trials_left]
237
+ is_search_space_exhausted = trial_batch.is_search_space_exhausted
179
238
 
180
- self.sweep_over_batches(
181
- ax_client=ax_client, list_of_trials=list_of_trials_to_launch
182
- )
239
+ log.info(
240
+ "AxSweeper is launching {} jobs".format(len(list_of_trials_to_launch))
241
+ )
183
242
 
184
- num_trials_so_far += len(list_of_trials_to_launch)
185
- num_trials_left -= len(list_of_trials_to_launch)
243
+ self.sweep_over_batches(
244
+ ax_client=ax_client, list_of_trials=list_of_trials_to_launch
245
+ )
186
246
 
187
- best_parameters, predictions = ax_client.get_best_parameters()
188
- metric = predictions[0][ax_client.objective_name]
247
+ num_trials_left -= len(list_of_trials_to_launch)
189
248
 
249
+ best_point = self.get_best_point(ax_client)
250
+ if best_point is not None:
251
+ best_parameters, metric = best_point
190
252
  if self.early_stopper.should_stop(metric, best_parameters):
191
- num_trials_left = -1
192
- break
193
-
194
- if is_search_space_exhausted:
195
- log.info("Ax has exhausted the search space")
196
253
  break
197
254
 
198
- current_parallelism_index += 1
255
+ if is_search_space_exhausted:
256
+ log.info("Ax has exhausted the search space")
257
+ break
199
258
 
200
259
  results_to_serialize = {"optimizer": "ax", "ax": best_parameters}
201
260
  OmegaConf.save(
@@ -205,7 +264,7 @@ class CoreAxSweeper(Sweeper):
205
264
  log.info("Best parameters: " + str(best_parameters))
206
265
 
207
266
  def sweep_over_batches(
208
- self, ax_client: AxClient, list_of_trials: List[Trial]
267
+ self, ax_client: Client, list_of_trials: List[Trial]
209
268
  ) -> None:
210
269
  assert self.launcher is not None
211
270
  assert self.job_idx is not None
@@ -225,20 +284,33 @@ class CoreAxSweeper(Sweeper):
225
284
  # Alternatively, the task function can return a dict whose values
226
285
  # represent multiple metrics, where each key is the name of the metric
227
286
  # and the item can be an int, float or tuple.
228
- assert isinstance(val, (int, float, tuple, dict))
229
- # is_noisy specifies how Ax should behave when not given an error value.
230
- # if true (default), the error of each measurement is inferred by Ax.
231
- # if false, the error of each measurement is set to 0.
232
- if isinstance(val, (int, float)):
233
- if self.is_noisy:
234
- val = (val, None) # specify unknown noise
235
- else:
236
- val = (val, 0) # specify no noise
287
+ # is_noisy specifies how Ax should behave when not given an
288
+ # error value: true means unknown error, false means zero error.
289
+ raw_data = create_ax_raw_data(
290
+ val, self.experiment.objective_name, self.is_noisy
291
+ )
237
292
  ax_client.complete_trial(
238
- trial_index=batch[idx].trial_index, raw_data=val
293
+ trial_index=batch[idx].trial_index, raw_data=raw_data
239
294
  )
240
295
 
241
- def setup_ax_client(self, arguments: List[str]) -> AxClient:
296
+ def get_best_point(
297
+ self, ax_client: Client
298
+ ) -> Optional[Tuple[Mapping[str, Any], float]]:
299
+ try:
300
+ best_parameters, metrics, _, _ = ax_client.get_best_parameterization(
301
+ use_model_predictions=False
302
+ )
303
+ except (AssertionError, UnsupportedError):
304
+ return None
305
+
306
+ metric = metrics.get(self.experiment.objective_name)
307
+ if metric is None:
308
+ return None
309
+ if isinstance(metric, tuple):
310
+ metric = metric[0]
311
+ return best_parameters, float(metric)
312
+
313
+ def setup_ax_client(self, arguments: List[str]) -> Client:
242
314
  """Method to setup the Ax Client"""
243
315
  parameters: List[Dict[Any, Any]] = []
244
316
  for key, value in self.ax_params.items():
@@ -249,7 +321,6 @@ class CoreAxSweeper(Sweeper):
249
321
  if not (all(isinstance(x, int) for x in bounds)):
250
322
  # Type mismatch. Promote all to float
251
323
  param["bounds"] = [float(x) for x in bounds]
252
-
253
324
  parameters.append(param)
254
325
  parameters[-1]["name"] = key
255
326
  commandline_params = self.parse_commandline_args(arguments)
@@ -265,11 +336,27 @@ class CoreAxSweeper(Sweeper):
265
336
  log.info(
266
337
  f"AxSweeper is optimizing the following parameters: {encoder_parameters_into_string(parameters)}"
267
338
  )
268
- ax_client = AxClient(
269
- verbose_logging=self.ax_client_config.verbose_logging,
270
- random_seed=self.ax_client_config.random_seed,
339
+
340
+ if not self.ax_client_config.verbose_logging:
341
+ logging.getLogger("ax.api.client").setLevel(logging.WARNING)
342
+ ax_client = Client(random_seed=self.ax_client_config.random_seed)
343
+ ax_client.configure_experiment(
344
+ parameters=[create_ax_parameter_config(param) for param in parameters],
345
+ parameter_constraints=self.experiment.parameter_constraints,
346
+ name=self.experiment.name,
347
+ )
348
+ if self.experiment.status_quo is not None:
349
+ ax_client.attach_baseline(
350
+ parameters=cast(Mapping[str, Any], self.experiment.status_quo)
351
+ )
352
+ ax_client.configure_optimization(
353
+ objective=(
354
+ f"-{self.experiment.objective_name}"
355
+ if self.experiment.minimize
356
+ else self.experiment.objective_name
357
+ ),
358
+ outcome_constraints=self.experiment.outcome_constraints,
271
359
  )
272
- ax_client.create_experiment(parameters=parameters, **self.experiment)
273
360
 
274
361
  return ax_client
275
362
 
@@ -288,8 +375,12 @@ class CoreAxSweeper(Sweeper):
288
375
  param = create_choice_param_from_range_override(override)
289
376
  elif override.is_interval_sweep():
290
377
  param = create_range_param_using_interval_override(override)
378
+ else:
379
+ raise ValueError(f"Unsupported sweep override: {override}")
291
380
  elif not override.is_hydra_override():
292
381
  param = create_fixed_param_from_element_override(override)
382
+ else:
383
+ continue
293
384
  parameters.append(param)
294
385
 
295
386
  return parameters
@@ -1,8 +1,6 @@
1
1
  # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2
2
  import logging
3
- from typing import Optional
4
-
5
- from ax import ParameterType # type: ignore
3
+ from typing import Any, Mapping, Optional
6
4
 
7
5
  log = logging.getLogger(__name__)
8
6
 
@@ -24,7 +22,7 @@ class EarlyStopper:
24
22
  self.current_epochs_without_improvement = 0
25
23
 
26
24
  def should_stop(
27
- self, potential_best_value: float, best_parameters: ParameterType
25
+ self, potential_best_value: float, best_parameters: Mapping[str, Any]
28
26
  ) -> bool:
29
27
  """Check if the optimization process should be stopped."""
30
28
  is_improving = True
@@ -34,7 +34,6 @@ class ExperimentConfig:
34
34
 
35
35
  @dataclass
36
36
  class ClientConfig:
37
-
38
37
  verbose_logging: bool = False
39
38
 
40
39
  # set random seed here to make Ax results reproducible
@@ -43,12 +42,11 @@ class ClientConfig:
43
42
 
44
43
  @dataclass
45
44
  class AxConfig:
46
-
47
45
  # max_trials is application-specific. Tune it for your use case
48
46
  max_trials: int = 10
49
- early_stop: EarlyStopConfig = EarlyStopConfig()
50
- experiment: ExperimentConfig = ExperimentConfig()
51
- client: ClientConfig = ClientConfig()
47
+ early_stop: EarlyStopConfig = field(default_factory=EarlyStopConfig)
48
+ experiment: ExperimentConfig = field(default_factory=ExperimentConfig)
49
+ client: ClientConfig = field(default_factory=ClientConfig)
52
50
  params: Dict[str, Any] = field(default_factory=dict)
53
51
  # is_noisy = True indicates measurements have unknown uncertainty
54
52
  # is_noisy = False indicates measurements have an uncertainty of zero
@@ -60,7 +58,7 @@ class AxSweeperConf:
60
58
  _target_: str = "hydra_plugins.hydra_ax_sweeper.ax_sweeper.AxSweeper"
61
59
  # Maximum number of trials to run in parallel
62
60
  max_batch_size: Optional[int] = None
63
- ax_config: AxConfig = AxConfig()
61
+ ax_config: AxConfig = field(default_factory=AxConfig)
64
62
 
65
63
 
66
64
  ConfigStore.instance().store(
@@ -11,25 +11,25 @@ setup(
11
11
  author="Omry Yadan, Shagun Sodhani",
12
12
  author_email="omry@fb.com, sshagunsodhani@gmail.com",
13
13
  description="Hydra Ax Sweeper plugin",
14
+ license="MIT",
14
15
  long_description=(Path(__file__).parent / "README.md").read_text(),
15
16
  long_description_content_type="text/markdown",
16
17
  url="https://github.com/facebookresearch/hydra/",
17
18
  packages=find_namespace_packages(include=["hydra_plugins.*"]),
18
19
  classifiers=[
19
- "License :: OSI Approved :: MIT License",
20
- "Programming Language :: Python :: 3.7",
21
- "Programming Language :: Python :: 3.8",
22
- "Programming Language :: Python :: 3.9",
23
- "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
24
  "Operating System :: POSIX :: Linux",
25
25
  "Operating System :: MacOS",
26
26
  "Development Status :: 4 - Beta",
27
27
  ],
28
+ python_requires=">=3.11,<3.15",
28
29
  install_requires=[
29
30
  "hydra-core>=1.1.0.dev7",
30
- "ax-platform>=0.1.20,<0.2.1", # https://github.com/facebookresearch/hydra/issues/1767
31
- "torch",
32
- "gpytorch<=1.8.1", # avoid deprecation warnings. This can probably be removed when ax-platform is unpinned.
31
+ "ax-platform>=1.2.4,<1.3.0",
32
+ "torch>=2.2",
33
33
  ],
34
34
  include_package_data=True,
35
35
  )
@@ -0,0 +1,478 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2
+ import math
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, List, Tuple
6
+
7
+ from hydra.core.plugins import Plugins
8
+ from hydra.plugins.sweeper import Sweeper
9
+ from hydra.test_utils.test_utils import (
10
+ TSweepRunner,
11
+ chdir_plugin_root,
12
+ run_python_script,
13
+ )
14
+ from omegaconf import DictConfig, OmegaConf
15
+ from pytest import mark, raises
16
+
17
+ from hydra_plugins.hydra_ax_sweeper.ax_sweeper import AxSweeper
18
+
19
+ chdir_plugin_root()
20
+
21
+ WARNING_FILTERS = [
22
+ # 2026-05-15: linear_operator 0.6.1 imports torch.jit.script via
23
+ # Ax -> Botorch -> GPyTorch on Python 3.14 with Torch 2.12.
24
+ # Remove when the current Ax stack no longer emits it under -Werror.
25
+ "ignore:`torch.jit.script` is deprecated:DeprecationWarning",
26
+ # 2026-05-15: same linear_operator import path as above, but Torch emits this
27
+ # alternate wording in some import paths. Remove with the filter above.
28
+ "ignore:`torch.jit.script` is not supported:DeprecationWarning",
29
+ # 2026-05-15: Ax 1.2.4 uses asyncio.iscoroutinefunction in retry helpers.
30
+ # Remove when Ax no longer emits it on Python 3.14 under -Werror.
31
+ "ignore:'asyncio.iscoroutinefunction' is deprecated:DeprecationWarning",
32
+ # 2026-05-15: Ax 1.2.4's JSON storage registry imports this moved shim.
33
+ # Remove when importing ax.api.Client no longer touches the shim.
34
+ "ignore:ax.service.utils.orchestrator_options has been moved:DeprecationWarning",
35
+ # 2026-05-15: Ax 1.2.4's overview analysis imports this moved shim.
36
+ # Remove when importing ax.api.Client no longer touches the shim.
37
+ "ignore:ax.service.orchestrator has been moved:DeprecationWarning",
38
+ # 2026-05-15: Botorch/linear_operator can add Cholesky jitter while fitting
39
+ # the Ax surrogate for log-scale tests. Remove when the current Ax stack no
40
+ # longer emits it under -Werror for the plugin's log-scale sweep.
41
+ "ignore:A not p.d., added jitter:Warning",
42
+ ]
43
+
44
+ pytestmark = [mark.filterwarnings(warning_filter) for warning_filter in WARNING_FILTERS]
45
+ PYTHON_WARNING_FILTERS = [f"-W{warning_filter}" for warning_filter in WARNING_FILTERS]
46
+
47
+
48
+ def run_ax_python_script(cmd: List[str]) -> Tuple[str, str]:
49
+ return run_python_script(PYTHON_WARNING_FILTERS + cmd)
50
+
51
+
52
+ def test_discovery() -> None:
53
+ """
54
+ Tests that this plugin can be discovered via the plugins subsystem when looking for Sweeper
55
+ :return:
56
+ """
57
+ assert AxSweeper.__name__ in [
58
+ x.__name__ for x in Plugins.instance().discover(Sweeper)
59
+ ]
60
+
61
+
62
+ def quadratic(cfg: DictConfig) -> Any:
63
+ return 100 * (cfg.quadratic.x**2) + 1 * cfg.quadratic.y
64
+
65
+
66
+ @mark.parametrize(
67
+ "n,expected",
68
+ [
69
+ (None, [[1, 2, 3, 4, 5]]),
70
+ (1, [[1], [2], [3], [4], [5]]),
71
+ (2, [[1, 2], [3, 4], [5]]),
72
+ (5, [[1, 2, 3, 4, 5]]),
73
+ (6, [[1, 2, 3, 4, 5]]),
74
+ ],
75
+ )
76
+ def test_chunk_method_for_valid_inputs(n: int, expected: List[List[int]]) -> None:
77
+ from hydra_plugins.hydra_ax_sweeper._core import CoreAxSweeper
78
+
79
+ chunk_func = CoreAxSweeper.chunks
80
+ batch = [1, 2, 3, 4, 5]
81
+ out = list(chunk_func(batch, n))
82
+ assert out == expected
83
+
84
+
85
+ @mark.parametrize("n", [-1, -11, 0])
86
+ def test_chunk_method_for_invalid_inputs(n: int) -> None:
87
+ from hydra_plugins.hydra_ax_sweeper._core import CoreAxSweeper
88
+
89
+ chunk_func = CoreAxSweeper.chunks
90
+ batch = [1, 2, 3, 4, 5]
91
+ with raises(ValueError):
92
+ list(chunk_func(batch, n))
93
+
94
+
95
+ def test_jobs_dirs(hydra_sweep_runner: TSweepRunner) -> None:
96
+ # Verify that the spawned jobs are not overstepping the directories of one another.
97
+ sweep = hydra_sweep_runner(
98
+ calling_file="tests/test_ax_sweeper_plugin.py",
99
+ calling_module=None,
100
+ task_function=quadratic,
101
+ config_path="config",
102
+ config_name="config.yaml",
103
+ overrides=[
104
+ "hydra/launcher=basic",
105
+ "hydra.sweeper.ax_config.max_trials=6",
106
+ "hydra.sweeper.ax_config.early_stop.max_epochs_without_improvement=100",
107
+ "hydra.sweeper.max_batch_size=2",
108
+ "params=basic",
109
+ ],
110
+ )
111
+ with sweep:
112
+ assert isinstance(sweep.temp_dir, str)
113
+ dirs = [
114
+ x
115
+ for x in os.listdir(sweep.temp_dir)
116
+ if os.path.isdir(os.path.join(sweep.temp_dir, x))
117
+ ]
118
+ assert len(dirs) == 6 # and a total of 6 unique output directories
119
+
120
+
121
+ @mark.parametrize("test_conf", ["basic", "logscale"])
122
+ def test_jobs_configured_via_config(
123
+ hydra_sweep_runner: TSweepRunner, test_conf: str
124
+ ) -> None:
125
+ sweep = hydra_sweep_runner(
126
+ calling_file="tests/test_ax_sweeper_plugin.py",
127
+ calling_module=None,
128
+ task_function=quadratic,
129
+ config_path="config",
130
+ config_name="config.yaml",
131
+ overrides=["hydra/launcher=basic", f"params={test_conf}"],
132
+ )
133
+ with sweep:
134
+ assert sweep.returns is None
135
+ returns = OmegaConf.load(f"{sweep.temp_dir}/optimization_results.yaml")
136
+ assert isinstance(returns, DictConfig)
137
+ assert returns["optimizer"] == "ax"
138
+ assert len(returns) == 2
139
+ best_parameters = returns.ax
140
+ assert math.isclose(best_parameters["quadratic.x"], 0.0, abs_tol=1e-4)
141
+ expected_y = 0.0 if test_conf == "basic" else -1.0
142
+ assert math.isclose(best_parameters["quadratic.y"], expected_y, abs_tol=1e-4)
143
+
144
+
145
+ @mark.parametrize(
146
+ "test_conf, override, expected_x",
147
+ [
148
+ ("basic", "int(interval(1, 5))", 1.0),
149
+ ],
150
+ )
151
+ def test_jobs_configured_via_cmd(
152
+ hydra_sweep_runner: TSweepRunner, test_conf: str, override: str, expected_x: float
153
+ ) -> None:
154
+ sweep = hydra_sweep_runner(
155
+ calling_file="tests/test_ax_sweeper_plugin.py",
156
+ calling_module=None,
157
+ task_function=quadratic,
158
+ config_path="config",
159
+ config_name="config.yaml",
160
+ overrides=[
161
+ "hydra/launcher=basic",
162
+ "hydra.sweeper.ax_config.max_trials=5",
163
+ f"quadratic.x={override}",
164
+ "quadratic.y=-2",
165
+ f"params={test_conf}",
166
+ ],
167
+ )
168
+ with sweep:
169
+ assert sweep.returns is None
170
+ returns = OmegaConf.load(f"{sweep.temp_dir}/optimization_results.yaml")
171
+ assert isinstance(returns, DictConfig)
172
+ assert returns["optimizer"] == "ax"
173
+ assert len(returns) == 2
174
+ best_parameters = returns.ax
175
+ assert math.isclose(best_parameters["quadratic.x"], expected_x, abs_tol=1e-4)
176
+ assert math.isclose(best_parameters["quadratic.y"], -2.0, abs_tol=1e-4)
177
+
178
+
179
+ def test_jobs_configured_via_cmd_and_config(hydra_sweep_runner: TSweepRunner) -> None:
180
+ sweep = hydra_sweep_runner(
181
+ calling_file="tests/test_ax_sweeper_plugin.py",
182
+ calling_module=None,
183
+ task_function=quadratic,
184
+ config_path="config",
185
+ config_name="config.yaml",
186
+ overrides=[
187
+ "hydra/launcher=basic",
188
+ "hydra.sweeper.ax_config.max_trials=2",
189
+ "quadratic.x=int(interval(-5, -2))",
190
+ "params=basic",
191
+ ],
192
+ )
193
+ with sweep:
194
+ assert sweep.returns is None
195
+ returns = OmegaConf.load(f"{sweep.temp_dir}/optimization_results.yaml")
196
+ assert isinstance(returns, DictConfig)
197
+ assert returns["optimizer"] == "ax"
198
+ assert len(returns) == 2
199
+ best_parameters = returns.ax
200
+ assert math.isclose(best_parameters["quadratic.x"], -3.0, abs_tol=1e-4)
201
+ assert math.isclose(best_parameters["quadratic.y"], 0.0, abs_tol=1e-4)
202
+
203
+
204
+ def test_command_line_int_interval_optimizes_integer_parabola(
205
+ hydra_sweep_runner: TSweepRunner,
206
+ ) -> None:
207
+ sweep = hydra_sweep_runner(
208
+ calling_file="tests/test_ax_sweeper_plugin.py",
209
+ calling_module=None,
210
+ task_function=quadratic,
211
+ config_path="config",
212
+ config_name="config.yaml",
213
+ overrides=[
214
+ "hydra/launcher=basic",
215
+ "hydra.sweeper.ax_config.max_trials=12",
216
+ "quadratic.x=int(interval(-5, 5))",
217
+ "quadratic.y=0",
218
+ ],
219
+ )
220
+ with sweep:
221
+ assert sweep.returns is None
222
+ returns = OmegaConf.load(f"{sweep.temp_dir}/optimization_results.yaml")
223
+ assert isinstance(returns, DictConfig)
224
+ best_parameters = returns.ax
225
+ assert isinstance(best_parameters["quadratic.x"], int)
226
+ assert best_parameters["quadratic.x"] == 0
227
+ assert best_parameters["quadratic.y"] == 0
228
+
229
+
230
+ def test_configuration_set_via_cmd_and_default_config(
231
+ hydra_sweep_runner: TSweepRunner,
232
+ ) -> None:
233
+ sweep = hydra_sweep_runner(
234
+ calling_file="tests/test_ax_sweeper_plugin.py",
235
+ calling_module=None,
236
+ task_function=quadratic,
237
+ config_path="config",
238
+ config_name="config.yaml",
239
+ overrides=[
240
+ "hydra/launcher=basic",
241
+ "hydra.sweeper.ax_config.max_trials=2",
242
+ "hydra.sweeper.ax_config.early_stop.max_epochs_without_improvement=2",
243
+ "quadratic=basic",
244
+ "quadratic.x=interval(-5, -2)",
245
+ "quadratic.y=interval(-1, 1)",
246
+ ],
247
+ )
248
+ with sweep:
249
+ assert sweep.returns is None
250
+ returns = OmegaConf.load(f"{sweep.temp_dir}/optimization_results.yaml")
251
+ assert isinstance(returns, DictConfig)
252
+ best_parameters = returns.ax
253
+ assert "quadratic.x" in best_parameters
254
+ assert "quadratic.y" in best_parameters
255
+
256
+
257
+ @mark.parametrize(
258
+ "cmd_arg, expected_str",
259
+ [
260
+ ("polynomial.y=choice(-1, 0, 1)", "polynomial.y: choice=[-1, 0, 1]"),
261
+ ("polynomial.y=range(-1, 2)", "polynomial.y: choice=[-1, 0, 1]"),
262
+ ("polynomial.y=range(-1, 3, 1)", "polynomial.y: choice=[-1, 0, 1, 2]"),
263
+ (
264
+ "polynomial.y=range(-1, 2, 0.5)",
265
+ "polynomial.y: choice=[-1.0, -0.5, 0.0, 0.5, 1.0, 1.5]",
266
+ ),
267
+ ("polynomial.y=int(interval(-1, 2))", "polynomial.y: range=[-1, 2]"),
268
+ ("polynomial.y=interval(-1, 2)", "polynomial.y: range=[-1.0, 2.0]"),
269
+ (
270
+ "polynomial.y=tag(log, interval(0.00001, 1))",
271
+ "polynomial.y: range=[1e-05, 1.0], log_scale = True",
272
+ ),
273
+ ("polynomial.y=2", "polynomial.y: fixed=2"),
274
+ ("polynomial.y=2.0", "polynomial.y: fixed=2.0"),
275
+ ],
276
+ )
277
+ def test_ax_logging(cmd_arg: str, expected_str: str) -> None:
278
+ from hydra_plugins.hydra_ax_sweeper._core import (
279
+ CoreAxSweeper,
280
+ encoder_parameters_into_string,
281
+ )
282
+ from hydra_plugins.hydra_ax_sweeper.config import AxConfig
283
+
284
+ sweeper = CoreAxSweeper(AxConfig(), max_batch_size=None)
285
+ parameters = sweeper.parse_commandline_args(
286
+ [
287
+ "polynomial.x=interval(-5, -2)",
288
+ "polynomial.z=10",
289
+ cmd_arg,
290
+ ]
291
+ )
292
+ result = encoder_parameters_into_string(parameters)
293
+ assert "polynomial.x: range=[-5.0, -2.0]" in result
294
+ assert expected_str in result
295
+ assert "polynomial.z: fixed=10" in result
296
+
297
+
298
+ def test_command_line_log_interval_configures_ax_log_range() -> None:
299
+ from ax.api.configs import RangeParameterConfig # type: ignore
300
+
301
+ from hydra_plugins.hydra_ax_sweeper._core import (
302
+ CoreAxSweeper,
303
+ create_ax_parameter_config,
304
+ )
305
+ from hydra_plugins.hydra_ax_sweeper.config import AxConfig
306
+
307
+ sweeper = CoreAxSweeper(AxConfig(), max_batch_size=None)
308
+ (parameter,) = sweeper.parse_commandline_args(
309
+ ["polynomial.y=tag(log, interval(0.00001, 1))"]
310
+ )
311
+ ax_parameter = create_ax_parameter_config(parameter)
312
+ assert isinstance(ax_parameter, RangeParameterConfig)
313
+ assert ax_parameter.name == "polynomial.y"
314
+ assert ax_parameter.parameter_type == "float"
315
+ assert ax_parameter.bounds == (1e-05, 1.0)
316
+ assert ax_parameter.scaling == "log"
317
+
318
+
319
+ def test_create_ax_raw_data() -> None:
320
+ from hydra_plugins.hydra_ax_sweeper._core import create_ax_raw_data
321
+
322
+ assert create_ax_raw_data(1, "loss", is_noisy=True) == {"loss": 1.0}
323
+ assert create_ax_raw_data(1, "loss", is_noisy=False) == {"loss": (1.0, 0.0)}
324
+ assert create_ax_raw_data({"loss": 1, "accuracy": (0.5, 0.1)}, "loss", True) == {
325
+ "loss": 1.0,
326
+ "accuracy": (0.5, 0.1),
327
+ }
328
+
329
+ raw_data = create_ax_raw_data((1, None), "loss", is_noisy=True)
330
+ loss = raw_data["loss"]
331
+ assert isinstance(loss, tuple)
332
+ sem = loss[1]
333
+ assert math.isnan(sem)
334
+
335
+
336
+ def test_ax_logging_from_hydra_app(tmpdir: Path) -> None:
337
+ cmd = [
338
+ "tests/apps/polynomial.py",
339
+ "-m",
340
+ f'hydra.run.dir="{str(tmpdir)}"',
341
+ "hydra.job.chdir=True",
342
+ "polynomial.x=interval(-5, -2)",
343
+ "polynomial.z=10",
344
+ "hydra.sweeper.ax_config.max_trials=2",
345
+ "polynomial.y=int(interval(-1, 2))",
346
+ ]
347
+ result, _ = run_ax_python_script(cmd)
348
+ assert "polynomial.x: range=[-5.0, -2.0]" in result
349
+ assert "polynomial.y: range=[-1, 2]" in result
350
+ assert "polynomial.z: fixed=10" in result
351
+
352
+
353
+ @mark.parametrize(
354
+ "cmd_args",
355
+ [
356
+ ["polynomial.y=choice(-1, 0, 1)", "polynomial.x=range(2,4)"],
357
+ ["polynomial.y=1", "polynomial.x=range(2,4)"],
358
+ ],
359
+ )
360
+ def test_search_space_exhausted_exception(tmpdir: Path, cmd_args: List[str]) -> None:
361
+ cmd = [
362
+ "tests/apps/polynomial.py",
363
+ "-m",
364
+ f'hydra.run.dir="{str(tmpdir)}"',
365
+ "hydra.job.chdir=True",
366
+ "hydra.sweeper.ax_config.max_trials=2",
367
+ ] + cmd_args
368
+ run_ax_python_script(cmd)
369
+
370
+
371
+ @mark.parametrize(
372
+ "cmd_args",
373
+ [
374
+ ["polynomial.y=choice(-1, 0, 1)", "polynomial.x=range(2,4)"],
375
+ ["polynomial.y=1", "polynomial.x=range(2,4)"],
376
+ ],
377
+ )
378
+ def test_search_space_with_constraint_metric(tmpdir: Path, cmd_args: List[str]) -> None:
379
+ # test that outcome_constraints experiment parameter `outcome_constraints`
380
+ # works correctly, and that the ax_sweeper supports outputting a dictionary
381
+ # from the evaluation function so that multiple metrics can be supported.
382
+ cmd = [
383
+ "tests/apps/polynomial_with_constraint.py",
384
+ "-m",
385
+ f'hydra.run.dir="{str(tmpdir)}"',
386
+ "hydra.job.chdir=True",
387
+ "hydra.sweeper.ax_config.max_trials=2",
388
+ ] + cmd_args
389
+ results, _ = run_ax_python_script(cmd)
390
+
391
+
392
+ @mark.parametrize(
393
+ "cmd_arg, serialized_encoding, best_coefficients, best_value",
394
+ [
395
+ (
396
+ "polynomial.coefficients=[-1, 0, 1],[2, 3, 4],[5, 6, 7]",
397
+ "choice=['[-1,0,1]', '[2,3,4]', '[5,6,7]']",
398
+ "'[-1,0,1]'",
399
+ 101.0,
400
+ ),
401
+ (
402
+ "polynomial.coefficients=choice([8, 12, 11],[-1, -1, 1000], [-2, 4, 7])",
403
+ "choice=['[8,12,11]', '[-1,-1,1000]', '[-2,4,7]']",
404
+ "'[-2,4,7]'",
405
+ 447,
406
+ ),
407
+ ],
408
+ )
409
+ def test_jobs_using_choice_between_lists(
410
+ tmpdir: Path,
411
+ cmd_arg: str,
412
+ serialized_encoding: str,
413
+ best_coefficients: str,
414
+ best_value: float,
415
+ ) -> None:
416
+ cmd = [
417
+ "tests/apps/polynomial_with_list_coefficients.py",
418
+ "-m",
419
+ f'hydra.run.dir="{str(tmpdir)}"',
420
+ "hydra.job.chdir=True",
421
+ "hydra.sweeper.ax_config.max_trials=3",
422
+ ] + [cmd_arg]
423
+ result, _ = run_ax_python_script(cmd)
424
+ assert f"polynomial.coefficients: {serialized_encoding}" in result
425
+ assert f"'polynomial.coefficients': {best_coefficients}" in result
426
+ assert f"New best value: {best_value}" in result
427
+
428
+
429
+ @mark.parametrize(
430
+ "cmd_arg, serialized_encoding, best_coefficients, best_value",
431
+ [
432
+ (
433
+ "+polynomial.coefficients=choice({x:-1, y:0, z:1},{x:2, y:3, z:4},{x:5, y:6, z:7})",
434
+ "choice=['{x:-1,y:0,z:1}', '{x:2,y:3,z:4}', '{x:5,y:6,z:7}']",
435
+ "'{x:-1,y:0,z:1}'",
436
+ 101.0,
437
+ ),
438
+ (
439
+ "+polynomial.coefficients=choice({x:8, y:12, z:11},{x:-1, y:-1, z:1000}, {x:-2, y:4, z:7})",
440
+ "choice=['{x:8,y:12,z:11}', '{x:-1,y:-1,z:1000}', '{x:-2,y:4,z:7}']",
441
+ "'{x:-2,y:4,z:7}'}",
442
+ 447,
443
+ ),
444
+ ],
445
+ )
446
+ def test_jobs_using_choice_between_dicts(
447
+ tmpdir: Path,
448
+ cmd_arg: str,
449
+ serialized_encoding: str,
450
+ best_coefficients: str,
451
+ best_value: float,
452
+ ) -> None:
453
+ cmd = [
454
+ "tests/apps/polynomial_with_dict_coefficients.py",
455
+ "-m",
456
+ f'hydra.run.dir="{str(tmpdir)}"',
457
+ "hydra.job.chdir=True",
458
+ "hydra.sweeper.ax_config.max_trials=3",
459
+ ] + [cmd_arg]
460
+ result, _ = run_ax_python_script(cmd)
461
+ assert f"polynomial.coefficients: {serialized_encoding}" in result
462
+ assert f"'+polynomial.coefficients': {best_coefficients}" in result
463
+ assert f"New best value: {best_value}" in result
464
+
465
+
466
+ def test_example_app(tmpdir: Path) -> None:
467
+ cmd = [
468
+ "example/banana.py",
469
+ "-m",
470
+ f'hydra.run.dir="{str(tmpdir)}"',
471
+ "hydra.job.chdir=True",
472
+ "banana.x=int(interval(-5, 5))",
473
+ "banana.y=interval(-5, 10.1)",
474
+ "hydra.sweeper.ax_config.max_trials=2",
475
+ ]
476
+ result, _ = run_ax_python_script(cmd)
477
+ assert "banana.x: range=[-5, 5]" in result
478
+ assert "banana.y: range=[-5.0, 10.1]" in result
@@ -1,3 +0,0 @@
1
- global-exclude *.pyc
2
- global-exclude __pycache__
3
- recursive-include hydra_plugins/* *.yaml
@@ -1,21 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: hydra-ax-sweeper
3
- Version: 1.3.0.dev0
4
- Summary: Hydra Ax Sweeper plugin
5
- Home-page: https://github.com/facebookresearch/hydra/
6
- Author: Omry Yadan, Shagun Sodhani
7
- Author-email: omry@fb.com, sshagunsodhani@gmail.com
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Programming Language :: Python :: 3.7
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Operating System :: POSIX :: Linux
14
- Classifier: Operating System :: MacOS
15
- Classifier: Development Status :: 4 - Beta
16
- Description-Content-Type: text/markdown
17
-
18
- # Hydra Ax Sweeper
19
- Provides a [`Ax Sweeper`](https://ax.dev/) based Hydra Sweeper supporting parallel execution.
20
-
21
- See [website](https://hydra.cc/docs/plugins/ax_sweeper/) for more information
@@ -1,21 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: hydra-ax-sweeper
3
- Version: 1.3.0.dev0
4
- Summary: Hydra Ax Sweeper plugin
5
- Home-page: https://github.com/facebookresearch/hydra/
6
- Author: Omry Yadan, Shagun Sodhani
7
- Author-email: omry@fb.com, sshagunsodhani@gmail.com
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Programming Language :: Python :: 3.7
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Operating System :: POSIX :: Linux
14
- Classifier: Operating System :: MacOS
15
- Classifier: Development Status :: 4 - Beta
16
- Description-Content-Type: text/markdown
17
-
18
- # Hydra Ax Sweeper
19
- Provides a [`Ax Sweeper`](https://ax.dev/) based Hydra Sweeper supporting parallel execution.
20
-
21
- See [website](https://hydra.cc/docs/plugins/ax_sweeper/) for more information
@@ -1,4 +0,0 @@
1
- hydra-core>=1.1.0.dev7
2
- ax-platform<0.2.1,>=0.1.20
3
- torch
4
- gpytorch<=1.8.1