Skip to content

Proper parallel optimisation #347

@Rizhiy

Description

@Rizhiy

I have a single threaded function which I want to optimise.
I'm trying to write a wrapper that would handle multiple runs at the same time, but I'm noticing considerable degradation in results as I increase the number of parallel evaluations.

Here is the rough logic of what I'm doing:

from __future__ import annotations

from pprint import pprint
from typing import Callable

import numpy as np
from bayes_opt.bayesian_optimization import BayesianOptimization
from bayes_opt.util import UtilityFunction
from tqdm import tqdm, trange


def multivariable_func(r: float, x: float, y: float, diff: float) -> float:
    r = int(r)
    diff = diff > 0.5

    loss = (r - 5) ** 2
    loss += (x**2 + y**2 - r) ** 2
    loss += abs(x - y) * (-1) ** int(diff)
    loss += 0.5 * x
    loss += -0.25 * int(diff)
    return -loss


def optimize(func: Callable[..., float], num_iter: int, bounds: dict[str, tuple[float, float]], num_workers=0):
    init_samples = int(np.sqrt(num_iter))

    optimizer = BayesianOptimization(f=None, pbounds=bounds, verbose=0)
    init_kappa = 10
    kappa_decay = (0.1 / init_kappa) ** (1 / (num_iter - init_samples))
    utility = UtilityFunction(
        kind="ucb", kappa=init_kappa, xi=0.0, kappa_decay=kappa_decay, kappa_decay_delay=init_samples
    )

    init_queue = [optimizer.suggest(utility) for _ in range(init_samples)]
    result_queue = []
    tbar = tqdm(total=num_iter, leave=False)
    while len(optimizer.res) < num_iter:
        sample = init_queue.pop(0) if init_queue else optimizer.suggest(utility)
        loss = func(**sample)
        result_queue.append((sample, loss))
        if len(result_queue) >= num_workers:
            try:
                optimizer.register(*result_queue.pop(0))
                utility.update_params()
                tbar.update()
            except KeyError:
                pass
    return optimizer.max


bounds = {"r": [-10, 10], "x": [-10, 10], "y": [-10, 10], "diff": [0, 1]}

all_results = {}
for num_workers in tqdm([1, 2, 4, 8], desc="Checking num_workers"):
    results = []
    for idx in trange(2, desc=f"Sampling with {num_workers=}"):
        best = optimize(multivariable_func, 400, bounds, num_workers)
        results.append(best["target"])
    all_results[num_workers] = np.mean(results)
    tqdm.write(f"Result for optimizing with {num_workers=}: {all_results[num_workers]}")
print("\n")
pprint(all_results)

The result_queue variable is simulating evaluation across multiple processes.

Here are the results:

{1: 4.320798413579277,
 2: 4.320676522735756,
 4: 3.5379530743926133,
 8: 2.175667857740832}

As can be seen, the more processes I use, the worse the final result is. I don't understand why that would happen, even if a couple fewer suggestions are evaluated properly, the results should not differ so much.

What am I missing?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions