ado-ray-tune 0.0.0__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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: ado-ray-tune
3
+ Version: 0.0.0
4
+ Requires-Dist: ray[tune]>=2.9
5
+ Requires-Dist: ax-platform==0.3.6
6
+ Requires-Dist: bayesian-optimization>=1.4.0
7
+ Requires-Dist: nevergrad>=1.0.12
8
+ Requires-Dist: paretoset>=1.2.4
@@ -0,0 +1,2 @@
1
+ # Copyright (c) IBM Corporation
2
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,238 @@
1
+ # Copyright (c) IBM Corporation
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Optional
5
+
6
+ import pydantic
7
+ import ray
8
+ from pydantic import ConfigDict
9
+
10
+ from ado_ray_tune.samplers import LhuSampler
11
+ from orchestrator.modules.module import (
12
+ ModuleConf,
13
+ ModuleTypeEnum,
14
+ load_module_class_or_function,
15
+ )
16
+
17
+
18
+ class RayTuneOrchestratorConfiguration(pydantic.BaseModel):
19
+ """Model for specific orchestrator options related to ray tune"""
20
+
21
+ single_measurement_per_property: bool = pydantic.Field(
22
+ default=True,
23
+ description="Indicate that each property (experiment) "
24
+ "should only be executed once",
25
+ )
26
+ failed_metric_value: float = pydantic.Field(
27
+ default=float("nan"),
28
+ description="Assign this value as the metric value for points if the measurements fail",
29
+ )
30
+ result_dump: str = pydantic.Field(
31
+ default="none",
32
+ deprecated=True,
33
+ description="Location to store the result of ray.tune() (Not Used)",
34
+ )
35
+
36
+ model_config = ConfigDict(extra="forbid")
37
+
38
+
39
+ class OrchSearchAlgorithm(pydantic.BaseModel):
40
+ name: str = pydantic.Field(description="The name of the search alg")
41
+ params: dict = pydantic.Field(
42
+ default={}, description="The params of the search alg"
43
+ )
44
+
45
+ @pydantic.model_validator(mode="after")
46
+ def map_nevergrad_optimizer_name_to_type(self):
47
+
48
+ if self.name == "nevergrad":
49
+ # nevergrad wrapper requires passing the class of the optimizer in the "optimizer" param
50
+ # here we have to switch from string to class
51
+ # Note: The NevergradSearch interface types optimizer as optional but it's not
52
+ # We let Nevergrad handle this
53
+ if optimizer := self.params.get("optimizer"):
54
+ import nevergrad
55
+
56
+ self.params["optimizer"] = nevergrad.optimizers.registry[optimizer]
57
+
58
+ return self
59
+
60
+
61
+ class OrchStopperAlgorithm(pydantic.BaseModel):
62
+ name: str = pydantic.Field(description="The name of the stopper")
63
+ positionalParams: list = pydantic.Field(
64
+ default=[], description="The positional params of the stopper"
65
+ )
66
+ keywordParams: dict = pydantic.Field(
67
+ default={}, description="The keyword params of the stopper"
68
+ )
69
+
70
+
71
+ class OrchTuneConfig(pydantic.BaseModel):
72
+ """Model for options that will initialize a ray.tune.TuneConfig instance
73
+
74
+ The aim of this class is to translate fields between the orchestrator RayTune agent's
75
+ config and RayTune "TuneConfig"
76
+
77
+ We need this as the values of certain TuneConfig fields e.g. search_alg, are complex objects which need to
78
+ be instantiated, and we can't do this in our YAML config file.
79
+ This class replaces the values of these fields with simple pydantic models that provide the information required
80
+ for instantiation of the TuneConfig object.
81
+ Then instances of this class can use this information to create the object and use it to init the actual TuneConfig
82
+
83
+ Any keyword args passed to this classes init will become a field and be used to create the TuneConfig
84
+ """
85
+
86
+ # The following fields are required
87
+ mode: str = pydantic.Field(default="min")
88
+ metric: str = pydantic.Field(description="The metric to optimize")
89
+ max_concurrent_trials: int = pydantic.Field(
90
+ default=1,
91
+ description="The maximum number of trials to have running at a time. Default 1",
92
+ )
93
+
94
+ # Here are the special fields that are used to create the inputs for TuneConfig
95
+ search_alg: OrchSearchAlgorithm
96
+ model_config = ConfigDict(extra="allow")
97
+
98
+ def rayTuneConfig(self):
99
+ # Get all values passed
100
+ tune_options = self.model_dump()
101
+
102
+ # To make the API of 'BasicVariantGenerator' compatible to all others
103
+ if (
104
+ hasattr(self, "max_concurrent_trials")
105
+ and self.search_alg.name == "variant_generator"
106
+ ):
107
+ self.search_alg.params["max_concurrent"] = self.max_concurrent_trials
108
+ del self.max_concurrent_trials # suppress warning
109
+ # For the special fields we need to deal with like search_alg do it and replace them
110
+ # TODO: Do we need to pass "mode" and "metric" to the search algorithm?
111
+ # Since we are also passing them to TuneConfig three lines below this.
112
+ if self.search_alg.name == "lhu_sampler":
113
+ search_alg = LhuSampler(
114
+ mode=self.mode, metric=self.metric, **self.search_alg.params
115
+ )
116
+ else:
117
+ search_alg = ray.tune.search.create_searcher(
118
+ self.search_alg.name,
119
+ mode=self.mode,
120
+ metric=self.metric,
121
+ **self.search_alg.params,
122
+ )
123
+ tune_options["search_alg"] = search_alg
124
+ return ray.tune.TuneConfig(**tune_options)
125
+
126
+
127
+ class OrchRunConfig(pydantic.BaseModel):
128
+ """Model for options that will initialize a ray.tune.RunConfig instance
129
+
130
+ The aim of this class is to translate fields between the orchestrator RayTune agent's
131
+ config and Ray Air "RunConfig"
132
+
133
+ We need this as the values of certain RuneConfig fields e.g. stop, are complex objects which need to be
134
+ instantiated, and we can't do this in our YAML config file.
135
+ This class replaces the values of these fields with simple pydantic models that provide the information required
136
+ for instantiation of the TuneConfig object.
137
+ Then instances of this class can use this information to create the object and use it to init the actual TuneConfig
138
+
139
+ Any keyword args passed to this classes init will become a field and be used to create the TuneConfig
140
+ """
141
+
142
+ # Here are the special fields that are used to create the inputs for RayConfig
143
+ stop: Optional[list[OrchStopperAlgorithm]] = pydantic.Field(
144
+ default=None,
145
+ description="A list of stopper(s) to use. If more than one will be combined with CombinedStopper",
146
+ )
147
+ model_config = ConfigDict(extra="allow")
148
+
149
+ def rayRuntimeConfig(self) -> ray.tune.RunConfig:
150
+
151
+ # Get all values passed
152
+ run_options = self.model_dump()
153
+
154
+ # Create the stoppers
155
+ if self.stop is not None and len(self.stop) > 0:
156
+ stoppers = []
157
+ for stopperConf in self.stop:
158
+ if stopperConf.name in [
159
+ "SimpleStopper",
160
+ "GrowthStopper",
161
+ "MaxSamplesStopper",
162
+ "InformationGainStopper",
163
+ ]:
164
+ module_name = "ado_ray_tune.stoppers"
165
+ else:
166
+ module_name = "ray.tune.stopper"
167
+
168
+ module_conf = ModuleConf(
169
+ moduleType=ModuleTypeEnum.GENERIC,
170
+ moduleName=module_name,
171
+ moduleClass=stopperConf.name,
172
+ )
173
+
174
+ stopper_class = load_module_class_or_function(module_conf)
175
+
176
+ if stopperConf.name in [
177
+ "SimpleStopper",
178
+ "GrowthStopper",
179
+ "MaxSamplesStopper",
180
+ "InformationGainStopper",
181
+ ]:
182
+ # There is some problem passing the in-build stoppers params via init
183
+ stopper = stopper_class()
184
+ stopper.set_config(
185
+ *stopperConf.positionalParams, **stopperConf.keywordParams
186
+ )
187
+ else:
188
+ stopper = stopper_class(
189
+ *stopperConf.positionalParams, **stopperConf.keywordParams
190
+ )
191
+
192
+ stoppers.append(stopper)
193
+
194
+ if len(stoppers) > 1:
195
+ stopper = ray.tune.stopper.CombinedStopper(*stoppers)
196
+ else:
197
+ stopper = stoppers[0]
198
+
199
+ run_options["stop"] = stopper
200
+
201
+ return ray.tune.RunConfig(
202
+ failure_config=ray.tune.FailureConfig(max_failures=0, fail_fast=True),
203
+ **run_options,
204
+ )
205
+
206
+
207
+ class RayTuneConfiguration(pydantic.BaseModel):
208
+ """Model for options related to using ray tune"""
209
+
210
+ tuneConfig: OrchTuneConfig = pydantic.Field(
211
+ description="ray tune configuration options"
212
+ )
213
+ # This is a ray.tune.config.RunConfig object which is also pydantic model
214
+ # However pydantic is throwing "pydantic.errors.ConfigError: field "callbacks"
215
+ # not yet prepared so type is still a ForwardRef, you might need to call RunConfig.update_forward_refs()." error
216
+ # When it is explicitly typed.
217
+ # To get around this were are using Any and then converting any dicts to RunConfig in a validator
218
+ runtimeConfig: Optional[OrchRunConfig] = pydantic.Field(
219
+ default=OrchRunConfig(), description="ray tune runtime options"
220
+ )
221
+ orchestratorConfig: RayTuneOrchestratorConfiguration = pydantic.Field(
222
+ default=RayTuneOrchestratorConfiguration(), description="orchestrator options"
223
+ )
224
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
225
+
226
+ @pydantic.field_validator("runtimeConfig")
227
+ def validate_runtime_config(cls, value):
228
+ # Check we can create the runtime config
229
+ _ = value.rayRuntimeConfig()
230
+
231
+ return value
232
+
233
+ @pydantic.field_validator("tuneConfig")
234
+ def validate_tune_config(cls, value):
235
+ # Check we can create the tune config
236
+ _ = value.rayTuneConfig()
237
+
238
+ return value
@@ -0,0 +1,74 @@
1
+ # Copyright (c) IBM Corporation
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from scipy.stats import qmc
5
+
6
+
7
+ class LatinHypercubeSampler:
8
+
9
+ def __init__(self, dict_space):
10
+ self.dict_space = dict_space
11
+ space_int_repr = []
12
+ ts_orig_to_int = {}
13
+ ts_int_to_orig = {}
14
+ l_bounds = []
15
+ u_bounds = []
16
+ skipped_orig_values = {}
17
+ total_size = 1
18
+ i = 0
19
+ index_to_label = {}
20
+ for label, dim in dict_space.items():
21
+ num_points_in_dim = len(dim)
22
+ if num_points_in_dim < 2:
23
+ skipped_orig_values[label] = dim
24
+ continue
25
+ total_size *= num_points_in_dim
26
+ nd = list(range(num_points_in_dim)) # codespell:ignore nd
27
+ l_bounds.append(nd[0]) # codespell:ignore nd
28
+ u_bounds.append(nd[-1] + 1) # codespell:ignore nd
29
+ tti = dict(zip(dim, nd)) # codespell:ignore nd
30
+ tto = dict(zip(nd, dim)) # codespell:ignore nd
31
+ space_int_repr.append(nd) # codespell:ignore nd
32
+ index_to_label[i] = label
33
+ i += 1
34
+ ts_orig_to_int[label] = tti
35
+ ts_int_to_orig[label] = tto
36
+ self.total_size = total_size
37
+ self.space_int_repr = space_int_repr
38
+ self.ts_orig_to_int = ts_orig_to_int
39
+ self.ts_int_to_orig = ts_int_to_orig
40
+ self.l_bounds = l_bounds
41
+ self.u_bounds = u_bounds
42
+ self.skipped_orig_values = skipped_orig_values
43
+ self.index_to_label = index_to_label
44
+
45
+ self.d = len(space_int_repr)
46
+ self.sampler = qmc.LatinHypercube(d=self.d)
47
+ # sample = sampler.random(2*self.d)
48
+ # sample_scaled = qmc.scale(sample, l_bounds, u_bounds)
49
+ print(
50
+ f"[LatinHypercubeSampler] configured for {self.d} actual dimensions, total size of search space is {self.total_size}."
51
+ )
52
+
53
+ def generate_new_categorical_samples(self, n=None, n_factor=2):
54
+ if n is None:
55
+ n = n_factor * self.d
56
+ # requires scipy>=1.11.3
57
+ sample_ints = self.sampler.integers(
58
+ l_bounds=self.l_bounds, u_bounds=self.u_bounds, n=n
59
+ )
60
+ print(f"[LatinHypercubeSampler] Generated {n} samples (by request).")
61
+ ret_list = []
62
+ for sample in sample_ints:
63
+ ret = {}
64
+ cur_i = 0
65
+ for dim_e in list(sample):
66
+ label = self.index_to_label[cur_i]
67
+ cur_i += 1
68
+ # ret[label] = []
69
+ # ret[label].append(self.ts_int_to_orig[label][dim_e])
70
+ ret[label] = self.ts_int_to_orig[label][dim_e]
71
+ for k, v in self.skipped_orig_values.items():
72
+ ret[k] = v[0]
73
+ ret_list.append(ret)
74
+ return ret_list