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.
- ado_ray_tune-0.0.0/PKG-INFO +8 -0
- ado_ray_tune-0.0.0/ado_ray_tune/__init__.py +2 -0
- ado_ray_tune-0.0.0/ado_ray_tune/config.py +238 -0
- ado_ray_tune-0.0.0/ado_ray_tune/doe.py +74 -0
- ado_ray_tune-0.0.0/ado_ray_tune/operator.py +851 -0
- ado_ray_tune-0.0.0/ado_ray_tune/operator_function.py +52 -0
- ado_ray_tune-0.0.0/ado_ray_tune/rifferla.py +365 -0
- ado_ray_tune-0.0.0/ado_ray_tune/rifferla_operation_debug.py +60 -0
- ado_ray_tune-0.0.0/ado_ray_tune/samplers.py +166 -0
- ado_ray_tune-0.0.0/ado_ray_tune/space_analysis.py +487 -0
- ado_ray_tune-0.0.0/ado_ray_tune/stoppers.py +444 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/PKG-INFO +8 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/SOURCES.txt +17 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/dependency_links.txt +1 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/entry_points.txt +3 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/requires.txt +5 -0
- ado_ray_tune-0.0.0/ado_ray_tune.egg-info/top_level.txt +1 -0
- ado_ray_tune-0.0.0/pyproject.toml +18 -0
- ado_ray_tune-0.0.0/setup.cfg +4 -0
|
@@ -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
|