Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename max_iterations to max_suggestions and track in Optimizer .suggest() instead of .register() #713

Merged
merged 16 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mlos_bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ Searching for an optimal set of tunable parameters is very similar to running a
All we have to do is specifying the [`Optimizer`](./mlos_bench/optimizers/) in the top-level configuration, like in our [`azure-redis-opt.jsonc`](./mlos_bench/config/cli/azure-redis-opt.jsonc) example.

```sh
mlos_bench --config "./mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc" --globals "experiment_MyBenchmark.jsonc" --max_iterations 10
mlos_bench --config "./mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc" --globals "experiment_MyBenchmark.jsonc" --max_suggestions 10
motus marked this conversation as resolved.
Show resolved Hide resolved
```

Note that again we use the command line option `--max_iterations` to override the default value from [`mlos_core_flaml.jsonc`](./mlos_bench/config/optimizers/mlos_core_flaml.jsonc).
Note that again we use the command line option `--max_suggestions` to override the default value from [`mlos_core_flaml.jsonc`](./mlos_bench/config/optimizers/mlos_core_flaml.jsonc).

We don't have to specify the `"tunable_values"` for the optimization: the optimizer will suggest new values on each iteration and the framework will feed this data into the benchmarking environment.

Expand Down
2 changes: 1 addition & 1 deletion mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Licensed under the MIT License.
//
// Run:
// mlos_bench --config mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc --globals experiment_RedisBench.jsonc --max_iterations 10
// mlos_bench --config mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc --globals experiment_RedisBench.jsonc --max_suggestions 10
{
"config_path": [
"mlos_bench/mlos_bench/config",
Expand Down
2 changes: 1 addition & 1 deletion mlos_bench/mlos_bench/config/experiments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ will be pushed down to the `Optimizer` configuration, e.g., [`mlos_core_flaml.js
> NOTE: it is perfectly ok to have several files with the experiment-specific parameters (say, one for Azure, another one for Storage, and so on) and either include them in the `"globals"` section of the CLI config, and/or specify them in the command line when running the experiment, e.g.
>
> ```bash
> mlos_bench --config mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc --globals experiment_Redis_Azure.jsonc experiment_Redis_Tunables.jsonc --max_iterations 10
> mlos_bench --config mlos_bench/mlos_bench/config/cli/azure-redis-opt.jsonc --globals experiment_Redis_Azure.jsonc experiment_Redis_Tunables.jsonc --max_suggestions 10
> ```
>
> (Note several files after the `--globals` option).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"config": {
"optimization_target": "score",
"optimization_direction": "min",
"max_iterations": 100
"max_suggestions": 100
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"config": {
"optimization_target": "score",
"optimization_direction": "min",
"max_iterations": 100,
"max_suggestions": 100,
"optimizer_type": "FLAML"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"config": {
"optimization_target": "score",
"optimization_direction": "min",
"max_iterations": 100,
"max_suggestions": 100,
"optimizer_type": "SMAC",
"output_directory": null // Override to have a permanent output with SMAC history etc.
}
Expand Down
2 changes: 1 addition & 1 deletion mlos_bench/mlos_bench/config/optimizers/mock_opt.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

"config": {
"optimization_target": "score",
"max_iterations": 5,
"max_suggestions": 5,
"seed": 42
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"enum": ["min", "max"],
"example": "min"
},
"max_iterations": {
"description": "The maximum number of additional (in the case of merging experiment data or resuming experiments) iterations to run when we launch the app.",
"max_suggestions": {
"description": "The maximum number of additional (in the case of merging experiment data or resuming experiments) suggestions/trials to run when we launch the app.",
motus marked this conversation as resolved.
Show resolved Hide resolved
"type": "integer",
"minimum": 0,
"example": 100
Expand Down
23 changes: 11 additions & 12 deletions mlos_bench/mlos_bench/optimizers/base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Optimizer(metaclass=ABCMeta): # pylint: disable=too-many-instance-attr
BASE_SUPPORTED_CONFIG_PROPS = {
"optimization_target",
"optimization_direction",
"max_iterations",
"max_suggestions",
"seed",
"start_with_defaults",
}
Expand Down Expand Up @@ -71,12 +71,12 @@ def __init__(self,
experiment_id = self._global_config.get('experiment_id')
self.experiment_id = str(experiment_id).strip() if experiment_id else None

self._iter = 1
self._iter = 0
# If False, use the optimizer to suggest the initial configuration;
# if True (default), use the already initialized values for the first iteration.
self._start_with_defaults: bool = bool(
strtobool(str(self._config.pop('start_with_defaults', True))))
self._max_iter = int(self._config.pop('max_iterations', 100))
self._max_iter = int(self._config.pop('max_suggestions', 100))
self._opt_target = str(self._config.pop('optimization_target', 'score'))
self._opt_sign = {"min": 1, "max": -1}[self._config.pop('optimization_direction', 'min')]

Expand Down Expand Up @@ -224,7 +224,7 @@ def supports_preload(self) -> bool:

@abstractmethod
def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float]],
status: Optional[Sequence[Status]] = None, is_warm_up: bool = False) -> bool:
status: Optional[Sequence[Status]] = None) -> bool:
"""
Pre-load the optimizer with the bulk data from previous experiments.

Expand All @@ -236,16 +236,13 @@ def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float
Benchmark results from experiments that correspond to `configs`.
status : Optional[Sequence[float]]
Status of the experiments that correspond to `configs`.
is_warm_up : bool
True for the initial load, False for subsequent calls.

Returns
-------
is_not_empty : bool
True if there is data to register, false otherwise.
"""
_LOG.info("%s the optimizer with: %d configs, %d scores, %d status values",
"Warm-up" if is_warm_up else "Load",
_LOG.info("Update the optimizer with: %d configs, %d scores, %d status values",
len(configs or []), len(scores or []), len(status or []))
if len(configs or []) != len(scores or []):
raise ValueError("Numbers of configs and scores do not match.")
Expand All @@ -257,10 +254,11 @@ def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float
self._start_with_defaults = False
return has_data

@abstractmethod
def suggest(self) -> TunableGroups:
"""
Generate the next suggestion.
Base class' implementation increments the iteration count
and returns the current values of the tunables.

Returns
-------
Expand All @@ -269,13 +267,15 @@ def suggest(self) -> TunableGroups:
These are the same tunables we pass to the constructor,
but with the values set to the next suggestion.
"""
self._iter += 1
motus marked this conversation as resolved.
Show resolved Hide resolved
_LOG.debug("Iteration %d :: Suggest", self._iter)
return self._tunables.copy()

@abstractmethod
def register(self, tunables: TunableGroups, status: Status,
score: Optional[Union[float, Dict[str, float]]] = None) -> Optional[float]:
"""
Register the observation for the given configuration.
Base class' implementations logs and increments the iteration count.

Parameters
----------
Expand All @@ -295,7 +295,6 @@ def register(self, tunables: TunableGroups, status: Status,
"""
_LOG.info("Iteration %d :: Register: %s = %s score: %s",
self._iter, tunables, status, score)
self._iter += 1
if status.is_succeeded() == (score is None): # XOR
raise ValueError("Status and score must be consistent.")
return self._get_score(status, score)
Expand Down Expand Up @@ -336,7 +335,7 @@ def not_converged(self) -> bool:
Return True if not converged, False otherwise.
Base implementation just checks the iteration count.
"""
return self._iter <= self._max_iter
return self._iter < self._max_iter

@abstractmethod
def get_best_observation(self) -> Union[Tuple[float, TunableGroups], Tuple[None, None]]:
Expand Down
11 changes: 4 additions & 7 deletions mlos_bench/mlos_bench/optimizers/grid_search_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,24 @@ def suggested_configs(self) -> Iterable[Dict[str, TunableValue]]:
return (dict(zip(self._config_keys, config)) for config in self._suggested_configs)

def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float]],
status: Optional[Sequence[Status]] = None, is_warm_up: bool = False) -> bool:
if not super().bulk_register(configs, scores, status, is_warm_up):
status: Optional[Sequence[Status]] = None) -> bool:
if not super().bulk_register(configs, scores, status):
return False
if status is None:
status = [Status.SUCCEEDED] * len(configs)
for (params, score, trial_status) in zip(configs, scores, status):
tunables = self._tunables.copy().assign(params)
self.register(tunables, trial_status, nullable(float, score))
if is_warm_up:
# Do not advance the iteration counter during warm-up.
self._iter -= 1
if _LOG.isEnabledFor(logging.DEBUG):
(score, _) = self.get_best_observation()
_LOG.debug("%s end: %s = %s", "Warm-up" if is_warm_up else "Update", self.target, score)
_LOG.debug("Update end: %s = %s", self.target, score)
return True

def suggest(self) -> TunableGroups:
"""
Generate the next grid search suggestion.
"""
tunables = self._tunables.copy()
tunables = super().suggest()
if self._start_with_defaults:
_LOG.info("Use default values for the first trial")
self._start_with_defaults = False
Expand Down
9 changes: 4 additions & 5 deletions mlos_bench/mlos_bench/optimizers/mlos_core_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ def name(self) -> str:
return f"{self.__class__.__name__}:{self._opt.__class__.__name__}"

def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float]],
status: Optional[Sequence[Status]] = None, is_warm_up: bool = False) -> bool:
if not super().bulk_register(configs, scores, status, is_warm_up):
status: Optional[Sequence[Status]] = None) -> bool:
if not super().bulk_register(configs, scores, status):
return False
df_configs = self._to_df(configs) # Impute missing values, if necessary
df_scores = pd.Series(scores, dtype=float) * self._opt_sign
Expand All @@ -103,8 +103,6 @@ def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float
df_configs = df_configs[df_status_completed]
df_scores = df_scores[df_status_completed]
self._opt.register(df_configs, df_scores)
if not is_warm_up:
self._iter += len(df_scores)
if _LOG.isEnabledFor(logging.DEBUG):
(score, _) = self.get_best_observation()
_LOG.debug("Warm-up end: %s = %s", self.target, score)
Expand Down Expand Up @@ -154,12 +152,13 @@ def _to_df(self, configs: Sequence[Dict[str, TunableValue]]) -> pd.DataFrame:
return df_configs

def suggest(self) -> TunableGroups:
tunables = super().suggest()
if self._start_with_defaults:
_LOG.info("Use default values for the first trial")
df_config = self._opt.suggest(defaults=self._start_with_defaults)
self._start_with_defaults = False
_LOG.info("Iteration %d :: Suggest:\n%s", self._iter, df_config)
return self._tunables.copy().assign(
return tunables.assign(
configspace_data_to_tunable_values(df_config.loc[0].to_dict()))

def register(self, tunables: TunableGroups, status: Status,
Expand Down
11 changes: 4 additions & 7 deletions mlos_bench/mlos_bench/optimizers/mock_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,24 @@ def __init__(self,
}

def bulk_register(self, configs: Sequence[dict], scores: Sequence[Optional[float]],
status: Optional[Sequence[Status]] = None, is_warm_up: bool = False) -> bool:
if not super().bulk_register(configs, scores, status, is_warm_up):
status: Optional[Sequence[Status]] = None) -> bool:
if not super().bulk_register(configs, scores, status):
return False
if status is None:
status = [Status.SUCCEEDED] * len(configs)
for (params, score, trial_status) in zip(configs, scores, status):
tunables = self._tunables.copy().assign(params)
self.register(tunables, trial_status, nullable(float, score))
if is_warm_up:
# Do not advance the iteration counter during warm-up.
self._iter -= 1
if _LOG.isEnabledFor(logging.DEBUG):
(score, _) = self.get_best_observation()
_LOG.debug("Warm-up end: %s = %s", self.target, score)
_LOG.debug("Bulk register end: %s = %s", self.target, score)
return True

def suggest(self) -> TunableGroups:
"""
Generate the next (random) suggestion.
"""
tunables = self._tunables.copy()
tunables = super().suggest()
if self._start_with_defaults:
_LOG.info("Use default values for the first trial")
self._start_with_defaults = False
Expand Down
4 changes: 0 additions & 4 deletions mlos_bench/mlos_bench/optimizers/one_shot_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,3 @@ def __init__(self,
@property
def supports_preload(self) -> bool:
return False

def suggest(self) -> TunableGroups:
_LOG.info("Suggest: %s", self._tunables)
return self._tunables.copy()
18 changes: 11 additions & 7 deletions mlos_bench/mlos_bench/schedulers/base_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(self, *,
self.optimizer = optimizer
self.storage = storage
self._root_env_config = root_env_config
self._last_trial_id = -1

_LOG.debug("Scheduler instantiated: %s :: %s", self, config)

Expand Down Expand Up @@ -177,21 +178,24 @@ def load_config(self, config_id: int) -> TunableGroups:
_LOG.debug("Config %d ::\n%s", config_id, json.dumps(tunable_values, indent=2))
return tunables

def _get_optimizer_suggestions(self, last_trial_id: int = -1, is_warm_up: bool = False) -> int:
def _get_optimizer_suggestions(self) -> bool:
motus marked this conversation as resolved.
Show resolved Hide resolved
"""
Optimizer part of the loop. Load the results of the executed trials
into the optimizer, suggest new configurations, and add them to the queue.
Return the last trial ID processed by the optimizer.
Return True if optimization is not over, False otherwise.
"""
assert self.experiment is not None
(trial_ids, configs, scores, status) = self.experiment.load(last_trial_id)
(trial_ids, configs, scores, status) = self.experiment.load(self._last_trial_id)
_LOG.info("QUEUE: Update the optimizer with trial results: %s", trial_ids)
self.optimizer.bulk_register(configs, scores, status, is_warm_up)
self.optimizer.bulk_register(configs, scores, status)
self._last_trial_id = max(trial_ids, default=self._last_trial_id)

tunables = self.optimizer.suggest()
self.schedule_trial(tunables)
not_converged = self.optimizer.not_converged()
if not_converged:
tunables = self.optimizer.suggest()
self.schedule_trial(tunables)

return max(trial_ids, default=last_trial_id)
return not_converged

def schedule_trial(self, tunables: TunableGroups) -> None:
"""
Expand Down
9 changes: 4 additions & 5 deletions mlos_bench/mlos_bench/schedulers/sync_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,15 @@ def start(self) -> None:
"""
super().start()

last_trial_id = -1
is_warm_up = self.optimizer.supports_preload
if not is_warm_up:
_LOG.warning("Skip pending trials and warm-up: %s", self.optimizer)

while self.optimizer.not_converged():
_LOG.info("Optimization loop: %s Last trial ID: %d",
"Warm-up" if is_warm_up else "Run", last_trial_id)
not_converged = True
while not_converged:
_LOG.info("Optimization loop: Last trial ID: %d", self._last_trial_id)
self._run_schedule(is_warm_up)
last_trial_id = self._get_optimizer_suggestions(last_trial_id, is_warm_up)
not_converged = self._get_optimizer_suggestions()
is_warm_up = False

def run_trial(self, trial: Storage.Trial) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"pathVarWithEnvVarRef": "$CUSTOM_PATH_FROM_ENV/foo",
"varWithEnvVarRef": "user:$USER",

// Override the default value of the "max_iterations" parameter
// Override the default value of the "max_suggestions" parameter
// of the optimizer when running local tests:
"max_iterations": 5
"max_suggestions": 5
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"class": "mlos_bench.optimizers.grid_search_optimizer.GridSearchOptimizer",
"config": {
"max_iterations": null,
"max_suggestions": null,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

"config": {
"optimization_target": "score",
"max_iterations": 20,
"max_suggestions": 20,
"seed": 12345,
"start_with_defaults": false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"config": {
// Here we do our best to list the exhaustive set of full configs available for the base optimizer config.
"optimization_target": "score",
"max_iterations": 20,
"max_suggestions": 20,
"seed": 12345,
"start_with_defaults": false,
"optimizer_type": "SMAC",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

"config": {
"optimization_target": "score",
"max_iterations": 20,
"max_suggestions": 20,
"seed": 12345,
"start_with_defaults": false,
"optimizer_type": "SMAC",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

"config": {
"optimization_target": "score",
"max_iterations": 20,
"max_suggestions": 20,
"seed": 12345,
"start_with_defaults": false
}
Expand Down
Loading
Loading