Asynchronous Bayesian Optimization#

In this notebook we demonstrate Trieste’s ability to perform asynchronous Bayesian optimisation, as is suitable for scenarios where the objective function can be run for several points in parallel but where observations might return back at different times. To avoid wasting resources waiting for the evaluation of the whole batch, we immediately request the next point asynchronously, taking into account points that are still being evaluated. Besides saving resources, asynchronous approach also can potentially improve sample efficiency in comparison with synchronous batch strategies, although this is highly dependent on the use case.

To contrast this approach with regular batch optimization, this notebook also shows how to run parallel synchronous batch approach.

[1]:
# silence TF warnings and info messages, only print errors
# https://stackoverflow.com/questions/35911252/disable-tensorflow-debugging-information
import os

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
import tensorflow as tf

tf.get_logger().setLevel("ERROR")
import numpy as np
import time
import timeit

First, let’s define a simple objective that will emulate evaluations taking variable time. We will be using a classic Bayesian optimisation benchmark function Branin with a sleep call inserted in the middle of the calculation to emulate delay. Our sleep delay is a scaled sum of all input values to make sure delays are uneven.

[2]:
from trieste.objectives import ScaledBranin


def objective(points, sleep=True):
    if points.shape[1] != 2:
        raise ValueError(
            f"Incorrect input shape, expected (*, 2), got {points.shape}"
        )

    observations = []
    for point in points:
        observation = ScaledBranin.objective(point)
        if sleep:
            # insert some artificial delay
            # increases linearly with the absolute value of points
            # which means our evaluations will take different time
            delay = 3 * np.sum(point)
            pid = os.getpid()
            print(
                f"Process {pid}: Objective: pretends like it's doing something for {delay:.2}s",
                flush=True,
            )
            time.sleep(delay)
        observations.append(observation)

    return np.array(observations)


# test the defined objective function
objective(np.array([[0.1, 0.5]]), sleep=False)
2024-11-05 15:35:43,011 INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.
[2]:
array([[-0.42052567]])

As always, we need to prepare the model and some initial data to kick-start the optimization process.

[3]:
from trieste.space import Box
from trieste.data import Dataset

search_space = Box([0, 0], [1, 1])
num_initial_points = 3
initial_query_points = search_space.sample(num_initial_points)
initial_observations = objective(initial_query_points.numpy(), sleep=False)
initial_data = Dataset(
    query_points=initial_query_points,
    observations=tf.constant(initial_observations, dtype=tf.float64),
)

import gpflow
from trieste.models.gpflow import GaussianProcessRegression, build_gpr

# We set the likelihood variance to a small number because
# we are dealing with a noise-free problem.
gpflow_model = build_gpr(initial_data, search_space, likelihood_variance=1e-7)
model = GaussianProcessRegression(gpflow_model)


# these imports will be used later for optimization
from trieste.acquisition import LocalPenalization
from trieste.acquisition.rule import (
    AsynchronousGreedy,
    EfficientGlobalOptimization,
)
from trieste.ask_tell_optimization import AskTellOptimizer

Multiprocessing setup#

To keep this notebook as reproducible as possible, we will only be using Python’s multiprocessing package here. In this section we will explain our setup and define some common code to be used later.

In both synchronous and asynchronous scenarios we will have a fixed set of worker processes performing observations. We will also have a main process responsible for optimization process with Trieste. When Trieste suggests a new point, it is inserted into a points queue. One of the workers picks this point from the queue, performs the observation, and inserts the output into the observations queue. The main process then picks up the observation from the queue, at which moment it either waits for the rest of the points in the batch to come back (synchronous scenario) or immediately suggests a new point (asynchronous scenario). This process continues either for a certain number of iterations or until we accumulate necessary number of observations.

The overall setup is illustrated in this diagram: multiprocessing setup

[4]:
# Necessary multiprocessing primitives
from multiprocessing import Manager, Process

We now define several common functions to implement the described setup. First we define a worker function that will be running a single observation in a separate process. Worker takes both queues as an input, reads next point from the points queue, makes an observation, and inserts observed data into the observations queue.

[5]:


def observer_proc(points_queue, observations_queue): pid = os.getpid() while True: point_to_observe = points_queue.get() if point_to_observe is None: return print( f"Process {pid}: Observer : observing data at point {point_to_observe}", flush=True, ) new_observation = objective(point_to_observe, sleep=enable_sleep_delays) new_data = (point_to_observe, new_observation) print(f"Process {pid}: Observer : observed data {new_data}", flush=True) observations_queue.put(new_data)

Next we define two helper functions, one is to create a certain number of worker processes, and another is to terminate them once we are done.

[6]:


def create_worker_processes(n_workers, points_queue, obseverations_queue): observer_processes = [] for i in range(n_workers): worker_proc = Process( target=observer_proc, args=(points_queue, obseverations_queue) ) worker_proc.daemon = True worker_proc.start() observer_processes.append(worker_proc) return observer_processes def terminate_processes(processes): for prc in processes: prc.terminate() prc.join() prc.close()

Finally we set some common parameters. See comments below for explanation of what each one means.

[7]:
# Number of worker processes to run simultaneously
# Setting this to 1 will turn both setups into non-batch sequential optimization
num_workers = 3
# Number of iterations to run the sycnhronous scenario for
num_iterations = 10
# Number of observations to collect in the asynchronous scenario
num_observations = num_workers * num_iterations
# Set this flag to False to disable sleep delays in case you want the notebook to execute quickly
enable_sleep_delays = True

Asynchronous optimization#

This section runs the asynchronous optimization routine. We first setup the ask/tell optimizer as we cannot hand over the evaluation of the objective to Trieste. Next we create thread-safe queues for points and observations, and run the optimization loop.

Crucially, even though we are using batch acquisition function Local Penalization, we specify batch size of 1. This is because we don’t really want a batch. Since the amount of workers we have is fixed, whenever we see a new observation we only need one point back. However this process can only be done with acquisition functions that implement greedy batch collection strategies, because they are able to take into account points that are currently being observed (in Trieste we call them “pending”). Trieste currently provides two such functions: Local Penalization and GIBBON. Notice that we use AsynchronousGreedy rule specifically designed for using greedy batch acquisition functions in asynchronous scenarios.

[8]:

# setup Ask Tell BO local_penalization_acq = LocalPenalization(search_space, num_samples=2000) local_penalization_rule = AsynchronousGreedy(builder=local_penalization_acq) # type: ignore async_bo = AskTellOptimizer( search_space, initial_data, model, local_penalization_rule ) # retrieve process id for nice logging pid = os.getpid() # create point and observation queues m = Manager() pq = m.Queue() oq = m.Queue() # keep track of all workers we have launched observer_processes = [] # counter to keep track of collected observations points_observed = 0 start = timeit.default_timer() try: observer_processes = create_worker_processes(num_workers, pq, oq) # init the queue with first batch of points for _ in range(num_workers): point = async_bo.ask() pq.put(np.atleast_2d(point.numpy())) while points_observed < num_observations: # keep asking queue for new observations until one arrives try: new_data = oq.get_nowait() print( f"Process {pid}: Main : received data {new_data}", flush=True, ) except Exception: continue # new_data is a tuple of (point, observation value) # here we turn it into a Dataset and tell of it Trieste points_observed += 1 new_data = Dataset( query_points=tf.constant(new_data[0], dtype=tf.float64), observations=tf.constant(new_data[1], dtype=tf.float64), ) async_bo.tell(new_data) # now we can ask Trieste for one more point # and feed that back into the points queue point = async_bo.ask() print(f"Process {pid}: Main : acquired point {point}", flush=True) pq.put(np.atleast_2d(point)) finally: terminate_processes(observer_processes) stop = timeit.default_timer() # Collect the observations, compute the running time async_lp_observations = ( async_bo.to_result().try_get_final_dataset().observations - ScaledBranin.minimum ) async_lp_time = stop - start print(f"Got {len(async_lp_observations)} observations in {async_lp_time:.2f}s")
Process 3012: Observer : observing data at point [[0.20318639 0.77700943]]
Process 3012: Objective: pretends like it's doing something for 2.9s
Process 3016: Observer : observing data at point [[0.12230503 0.77276553]]
Process 3016: Objective: pretends like it's doing something for 2.7s
Process 3020: Observer : observing data at point [[0.23485222 0.70706535]]
Process 3020: Objective: pretends like it's doing something for 2.8s
Process 3012: Observer : observed data (array([[0.20318639, 0.77700943]]), array([[-0.85000871]]))
Process 2967: Main     : received data (array([[0.20318639, 0.77700943]]), array([[-0.85000871]]))
Process 3016: Observer : observed data (array([[0.12230503, 0.77276553]]), array([[-1.03677587]]))
Process 3020: Observer : observed data (array([[0.23485222, 0.70706535]]), array([[-0.77035541]]))
Process 2967: Main     : acquired point [[0.18175208 0.68609424]]
Process 2967: Main     : received data (array([[0.12230503, 0.77276553]]), array([[-1.03677587]]))
Process 3012: Observer : observing data at point [[0.18175208 0.68609424]]
Process 3012: Objective: pretends like it's doing something for 2.6s
Process 2967: Main     : acquired point [[0.10210616 0.73887441]]
Process 2967: Main     : received data (array([[0.23485222, 0.70706535]]), array([[-0.77035541]]))
Process 3016: Observer : observing data at point [[0.10210616 0.73887441]]
Process 3016: Objective: pretends like it's doing something for 2.5s
Process 2967: Main     : acquired point [[0.06819198 0.74385374]]
Process 3020: Observer : observing data at point [[0.06819198 0.74385374]]
Process 3020: Objective: pretends like it's doing something for 2.4s
Process 3012: Observer : observed data (array([[0.18175208, 0.68609424]]), array([[-0.9820455]]))
Process 2967: Main     : received data (array([[0.18175208, 0.68609424]]), array([[-0.9820455]]))
Process 3016: Observer : observed data (array([[0.10210616, 0.73887441]]), array([[-0.96129749]]))
Process 2967: Main     : acquired point [[0.11864862 0.86911504]]
Process 2967: Main     : received data (array([[0.10210616, 0.73887441]]), array([[-0.96129749]]))
Process 3012: Observer : observing data at point [[0.11864862 0.86911504]]
Process 3012: Objective: pretends like it's doing something for 3.0s
Process 2967: Main     : acquired point [[0.0776855  0.90854861]]
Process 3016: Observer : observing data at point [[0.0776855  0.90854861]]
Process 3016: Objective: pretends like it's doing something for 3.0s
Process 3020: Observer : observed data (array([[0.06819198, 0.74385374]]), array([[-0.78753575]]))
Process 2967: Main     : received data (array([[0.06819198, 0.74385374]]), array([[-0.78753575]]))
Process 2967: Main     : acquired point [[0.10318715 0.98709729]]
Process 3020: Observer : observing data at point [[0.10318715 0.98709729]]
Process 3020: Objective: pretends like it's doing something for 3.3s
Process 3012: Observer : observed data (array([[0.11864862, 0.86911504]]), array([[-1.04052749]]))
Process 2967: Main     : received data (array([[0.11864862, 0.86911504]]), array([[-1.04052749]]))
Process 3016: Observer : observed data (array([[0.0776855 , 0.90854861]]), array([[-1.00204324]]))
Process 2967: Main     : acquired point [[0.05963279 1.        ]]
Process 2967: Main     : received data (array([[0.0776855 , 0.90854861]]), array([[-1.00204324]]))
Process 3012: Observer : observing data at point [[0.05963279 1.        ]]
Process 3012: Objective: pretends like it's doing something for 3.2s
Process 2967: Main     : acquired point [[0.12759885 1.        ]]
Process 3016: Observer : observing data at point [[0.12759885 1.        ]]
Process 3016: Objective: pretends like it's doing something for 3.4s
Process 3020: Observer : observed data (array([[0.10318715, 0.98709729]]), array([[-0.9780697]]))
Process 2967: Main     : received data (array([[0.10318715, 0.98709729]]), array([[-0.9780697]]))
Process 2967: Main     : acquired point [[0.4389946  0.01697035]]
Process 3020: Observer : observing data at point [[0.4389946  0.01697035]]
Process 3020: Objective: pretends like it's doing something for 1.4s
Process 3012: Observer : observed data (array([[0.05963279, 1.        ]]), array([[-0.96637072]]))
Process 2967: Main     : received data (array([[0.05963279, 1.        ]]), array([[-0.96637072]]))
Process 3020: Observer : observed data (array([[0.4389946 , 0.01697035]]), array([[-0.62292634]]))
Process 2967: Main     : acquired point [[0.51345594 0.17895946]]
Process 2967: Main     : received data (array([[0.4389946 , 0.01697035]]), array([[-0.62292634]]))
Process 3012: Observer : observing data at point [[0.51345594 0.17895946]]
Process 3012: Objective: pretends like it's doing something for 2.1s
Process 3016: Observer : observed data (array([[0.12759885, 1.        ]]), array([[-0.88985943]]))
Process 2967: Main     : acquired point [[0.64958516 0.39148872]]
Process 2967: Main     : received data (array([[0.12759885, 1.        ]]), array([[-0.88985943]]))
Process 3020: Observer : observing data at point [[0.64958516 0.39148872]]
Process 3020: Objective: pretends like it's doing something for 3.1s
Process 3012: Observer : observed data (array([[0.51345594, 0.17895946]]), array([[-1.02977509]]))
Process 2967: Main     : acquired point [[0.67639951 0.19655793]]
Process 2967: Main     : received data (array([[0.51345594, 0.17895946]]), array([[-1.02977509]]))
Process 3016: Observer : observing data at point [[0.67639951 0.19655793]]
Process 3016: Objective: pretends like it's doing something for 2.6s
Process 2967: Main     : acquired point [[0.57501594 0.29443361]]
Process 3012: Observer : observing data at point [[0.57501594 0.29443361]]
Process 3012: Objective: pretends like it's doing something for 2.6s
Process 3020: Observer : observed data (array([[0.64958516, 0.39148872]]), array([[-0.464325]]))
Process 2967: Main     : received data (array([[0.64958516, 0.39148872]]), array([[-0.464325]]))
Process 3016: Observer : observed data (array([[0.67639951, 0.19655793]]), array([[-0.72812148]]))
Process 2967: Main     : acquired point [[0.4854162  0.32561289]]
Process 2967: Main     : received data (array([[0.67639951, 0.19655793]]), array([[-0.72812148]]))
Process 3020: Observer : observing data at point [[0.4854162  0.32561289]]
Process 3020: Objective: pretends like it's doing something for 2.4s
Process 3012: Observer : observed data (array([[0.57501594, 0.29443361]]), array([[-0.90699434]]))
Process 2967: Main     : acquired point [[0.56728814 0.08361468]]
Process 2967: Main     : received data (array([[0.57501594, 0.29443361]]), array([[-0.90699434]]))
Process 3016: Observer : observing data at point [[0.56728814 0.08361468]]
Process 3016: Objective: pretends like it's doing something for 2.0s
Process 3020: Observer : observed data (array([[0.4854162 , 0.32561289]]), array([[-0.9177476]]))
Process 2967: Main     : acquired point [[0.56440743 0.12958401]]
Process 2967: Main     : received data (array([[0.4854162 , 0.32561289]]), array([[-0.9177476]]))
Process 3012: Observer : observing data at point [[0.56440743 0.12958401]]
Process 3012: Objective: pretends like it's doing something for 2.1s
Process 3016: Observer : observed data (array([[0.56728814, 0.08361468]]), array([[-1.02416677]]))
Process 2967: Main     : acquired point [[0.58215371 0.04289026]]
Process 2967: Main     : received data (array([[0.56728814, 0.08361468]]), array([[-1.02416677]]))
Process 3020: Observer : observing data at point [[0.58215371 0.04289026]]
Process 3020: Objective: pretends like it's doing something for 1.9s
Process 3012: Observer : observed data (array([[0.56440743, 0.12958401]]), array([[-1.03758426]]))
Process 2967: Main     : acquired point [[0.54849178 0.17153427]]
Process 2967: Main     : received data (array([[0.56440743, 0.12958401]]), array([[-1.03758426]]))
Process 3016: Observer : observing data at point [[0.54849178 0.17153427]]
Process 3016: Objective: pretends like it's doing something for 2.2s
Process 3020: Observer : observed data (array([[0.58215371, 0.04289026]]), array([[-0.98760722]]))
Process 2967: Main     : acquired point [[0.5395062  0.17723512]]
Process 2967: Main     : received data (array([[0.58215371, 0.04289026]]), array([[-0.98760722]]))
Process 3012: Observer : observing data at point [[0.5395062  0.17723512]]
Process 3012: Objective: pretends like it's doing something for 2.2s
Process 3016: Observer : observed data (array([[0.54849178, 0.17153427]]), array([[-1.04416429]]))
Process 2967: Main     : acquired point [[0.54008453 0.16490195]]
Process 2967: Main     : received data (array([[0.54849178, 0.17153427]]), array([[-1.04416429]]))
Process 3020: Observer : observing data at point [[0.54008453 0.16490195]]
Process 3020: Objective: pretends like it's doing something for 2.1s
Process 3012: Observer : observed data (array([[0.5395062 , 0.17723512]]), array([[-1.04488085]]))
Process 2967: Main     : acquired point [[0.96815144 0.        ]]
Process 2967: Main     : received data (array([[0.5395062 , 0.17723512]]), array([[-1.04488085]]))
Process 3016: Observer : observing data at point [[0.96815144 0.        ]]
Process 3016: Objective: pretends like it's doing something for 2.9s
Process 3020: Observer : observed data (array([[0.54008453, 0.16490195]]), array([[-1.04670762]]))
Process 2967: Main     : acquired point [[1.         0.17891857]]
Process 2967: Main     : received data (array([[0.54008453, 0.16490195]]), array([[-1.04670762]]))
Process 3012: Observer : observing data at point [[1.         0.17891857]]
Process 3012: Objective: pretends like it's doing something for 3.5s
Process 2967: Main     : acquired point [[1.       0.373209]]
Process 3020: Observer : observing data at point [[1.       0.373209]]
Process 3020: Objective: pretends like it's doing something for 4.1s
Process 3016: Observer : observed data (array([[0.96815144, 0.        ]]), array([[-0.92051526]]))
Process 2967: Main     : received data (array([[0.96815144, 0.        ]]), array([[-0.92051526]]))
Process 2967: Main     : acquired point [[1.         0.49844266]]
Process 3016: Observer : observing data at point [[1.         0.49844266]]
Process 3016: Objective: pretends like it's doing something for 4.5s
Process 3012: Observer : observed data (array([[1.        , 0.17891857]]), array([[-1.01568787]]))
Process 2967: Main     : received data (array([[1.        , 0.17891857]]), array([[-1.01568787]]))
Process 2967: Main     : acquired point [[1.         0.59815834]]
Process 3012: Observer : observing data at point [[1.         0.59815834]]
Process 3012: Objective: pretends like it's doing something for 4.8s
Process 3020: Observer : observed data (array([[1.      , 0.373209]]), array([[-0.88800594]]))
Process 2967: Main     : received data (array([[1.      , 0.373209]]), array([[-0.88800594]]))
Process 2967: Main     : acquired point [[0.92046852 0.19092612]]
Process 3020: Observer : observing data at point [[0.92046852 0.19092612]]
Process 3020: Objective: pretends like it's doing something for 3.3s
Process 3016: Observer : observed data (array([[1.        , 0.49844266]]), array([[-0.63239686]]))
Process 2967: Main     : received data (array([[1.        , 0.49844266]]), array([[-0.63239686]]))
Process 2967: Main     : acquired point [[0.9087719  0.14219283]]
Process 3016: Observer : observing data at point [[0.9087719  0.14219283]]
Process 3016: Objective: pretends like it's doing something for 3.2s
Process 3012: Observer : observed data (array([[1.        , 0.59815834]]), array([[-0.33172093]]))
Process 2967: Main     : received data (array([[1.        , 0.59815834]]), array([[-0.33172093]]))
Process 3020: Observer : observed data (array([[0.92046852, 0.19092612]]), array([[-0.99897436]]))
Process 2967: Main     : acquired point [[0.92903974 0.23667601]]
Process 2967: Main     : received data (array([[0.92046852, 0.19092612]]), array([[-0.99897436]]))
Process 3012: Observer : observing data at point [[0.92903974 0.23667601]]
Process 3012: Objective: pretends like it's doing something for 3.5s
Process 3016: Observer : observed data (array([[0.9087719 , 0.14219283]]), array([[-0.99107176]]))
Process 2967: Main     : acquired point [[0.83417346 0.        ]]
Process 2967: Main     : received data (array([[0.9087719 , 0.14219283]]), array([[-0.99107176]]))
Process 3020: Observer : observing data at point [[0.83417346 0.        ]]
Process 3020: Objective: pretends like it's doing something for 2.5s
Process 2967: Main     : acquired point [[0.96165073 0.16835968]]
Process 3016: Observer : observing data at point [[0.96165073 0.16835968]]
Process 3016: Objective: pretends like it's doing something for 3.4s
Got 33 observations in 47.26s

Synchronous parallel optimization#

This section runs the synchronous parallel optimization with Trieste. We again use Local Penalization acquisition function, but this time with batch size equal to the number of workers we have available. Once Trieste suggests the batch, we add all points to the point queue, and workers immediatelly pick them up, one point per worker. Therefore all points in the batch are evaluated in parallel.

[9]:
# setup Ask Tell BO
gpflow_model = build_gpr(initial_data, search_space, likelihood_variance=1e-7)
model = GaussianProcessRegression(gpflow_model)

local_penalization_acq = LocalPenalization(search_space, num_samples=2000)
local_penalization_rule = EfficientGlobalOptimization(  # type: ignore
    num_query_points=num_workers, builder=local_penalization_acq
)

sync_bo = AskTellOptimizer(
    search_space, initial_data, model, local_penalization_rule
)


# retrieve process id for nice logging
pid = os.getpid()
# create point and observation queues
m = Manager()
pq = m.Queue()
oq = m.Queue()
# keep track of all workers we have launched
observer_processes = []

start = timeit.default_timer()
try:
    observer_processes = create_worker_processes(num_workers, pq, oq)

    # BO loop starts here
    for i in range(num_iterations):
        print(f"Process {pid}: Main     : iteration {i} starts", flush=True)

        # get a batch of points from Trieste, send them to points queue
        # each worker picks up a point and processes it
        points = sync_bo.ask()
        for point in points.numpy():
            pq.put(point.reshape(1, -1))  # reshape is to make point a 2d array

        # now we wait for all workers to finish
        # we create an empty dataset and wait
        # until we collected as many observations in it
        # as there were points in the batch
        all_new_data = Dataset(
            tf.zeros((0, initial_data.query_points.shape[1]), tf.float64),
            tf.zeros((0, initial_data.observations.shape[1]), tf.float64),
        )
        while len(all_new_data) < num_workers:
            # this line blocks the process until new data is available in the queue
            new_data = oq.get()
            print(
                f"Process {pid}: Main     : received data {new_data}",
                flush=True,
            )

            new_data = Dataset(
                query_points=tf.constant(new_data[0], dtype=tf.float64),
                observations=tf.constant(new_data[1], dtype=tf.float64),
            )

            all_new_data = all_new_data + new_data

        # tell Trieste of new batch of observations
        sync_bo.tell(all_new_data)

finally:
    terminate_processes(observer_processes)
stop = timeit.default_timer()

# Collect the observations, compute the running time
sync_lp_observations = (
    sync_bo.to_result().try_get_final_dataset().observations
    - ScaledBranin.minimum
)
sync_lp_time = stop - start
print(f"Got {len(sync_lp_observations)} observations in {sync_lp_time:.2f}s")
Process 2967: Main     : iteration 0 starts
Process 3440: Observer : observing data at point [[0.12235613 0.77279154]]Process 3444: Observer : observing data at point [[0.23457661 0.70776602]]

Process 3440: Objective: pretends like it's doing something for 2.7sProcess 3444: Objective: pretends like it's doing something for 2.8s

Process 3436: Observer : observing data at point [[0.20318632 0.77700941]]
Process 3436: Objective: pretends like it's doing something for 2.9s
Process 3440: Observer : observed data (array([[0.12235613, 0.77279154]]), array([[-1.03684289]]))
Process 2967: Main     : received data (array([[0.12235613, 0.77279154]]), array([[-1.03684289]]))
Process 3444: Observer : observed data (array([[0.23457661, 0.70776602]]), array([[-0.77093772]]))
Process 2967: Main     : received data (array([[0.23457661, 0.70776602]]), array([[-0.77093772]]))
Process 3436: Observer : observed data (array([[0.20318632, 0.77700941]]), array([[-0.85000907]]))
Process 2967: Main     : received data (array([[0.20318632, 0.77700941]]), array([[-0.85000907]]))
Process 2967: Main     : iteration 1 starts
Process 3444: Observer : observing data at point [[0.06883031 0.74328861]]Process 3440: Observer : observing data at point [[0.10187281 0.74087098]]Process 3436: Observer : observing data at point [[0.12700033 0.74331114]]


Process 3444: Objective: pretends like it's doing something for 2.4sProcess 3436: Objective: pretends like it's doing something for 2.6sProcess 3440: Objective: pretends like it's doing something for 2.5s


Process 3444: Observer : observed data (array([[0.06883031, 0.74328861]]), array([[-0.79089082]]))
Process 2967: Main     : received data (array([[0.06883031, 0.74328861]]), array([[-0.79089082]]))
Process 3440: Observer : observed data (array([[0.10187281, 0.74087098]]), array([[-0.96270814]]))
Process 2967: Main     : received data (array([[0.10187281, 0.74087098]]), array([[-0.96270814]]))
Process 3436: Observer : observed data (array([[0.12700033, 0.74331114]]), array([[-1.02741574]]))
Process 2967: Main     : received data (array([[0.12700033, 0.74331114]]), array([[-1.02741574]]))
Process 2967: Main     : iteration 2 starts
Process 3444: Observer : observing data at point [[0.10663684 0.91558794]]Process 3436: Observer : observing data at point [[0.13954667 0.76412575]]Process 3440: Observer : observing data at point [[0.08419382 1.        ]]


Process 3436: Objective: pretends like it's doing something for 2.7sProcess 3440: Objective: pretends like it's doing something for 3.3sProcess 3444: Objective: pretends like it's doing something for 3.1s


Process 3436: Observer : observed data (array([[0.13954667, 0.76412575]]), array([[-1.04106129]]))
Process 2967: Main     : received data (array([[0.13954667, 0.76412575]]), array([[-1.04106129]]))
Process 3444: Observer : observed data (array([[0.10663684, 0.91558794]]), array([[-1.0280366]]))
Process 2967: Main     : received data (array([[0.10663684, 0.91558794]]), array([[-1.0280366]]))
Process 3440: Observer : observed data (array([[0.08419382, 1.        ]]), array([[-0.98559444]]))
Process 2967: Main     : received data (array([[0.08419382, 1.        ]]), array([[-0.98559444]]))
Process 2967: Main     : iteration 3 starts
Process 3436: Observer : observing data at point [[0.47978831 0.16835372]]Process 3444: Observer : observing data at point [[0.51633761 0.34285255]]Process 3440: Observer : observing data at point [[0.42663517 0.        ]]


Process 3436: Objective: pretends like it's doing something for 1.9sProcess 3444: Objective: pretends like it's doing something for 2.6s
Process 3440: Objective: pretends like it's doing something for 1.3s

Process 3440: Observer : observed data (array([[0.42663517, 0.        ]]), array([[-0.51911304]]))
Process 2967: Main     : received data (array([[0.42663517, 0.        ]]), array([[-0.51911304]]))
Process 3436: Observer : observed data (array([[0.47978831, 0.16835372]]), array([[-0.96388884]]))
Process 2967: Main     : received data (array([[0.47978831, 0.16835372]]), array([[-0.96388884]]))
Process 3444: Observer : observed data (array([[0.51633761, 0.34285255]]), array([[-0.90903559]]))
Process 2967: Main     : received data (array([[0.51633761, 0.34285255]]), array([[-0.90903559]]))
Process 2967: Main     : iteration 4 starts
Process 3444: Observer : observing data at point [[0.61429365 0.23384942]]Process 3436: Observer : observing data at point [[0.60520688 0.10340222]]Process 3440: Observer : observing data at point [[0.58171374 0.17222748]]


Process 3444: Objective: pretends like it's doing something for 2.5sProcess 3436: Objective: pretends like it's doing something for 2.1s
Process 3440: Objective: pretends like it's doing something for 2.3s

Process 3436: Observer : observed data (array([[0.60520688, 0.10340222]]), array([[-0.971873]]))
Process 2967: Main     : received data (array([[0.60520688, 0.10340222]]), array([[-0.971873]]))
Process 3440: Observer : observed data (array([[0.58171374, 0.17222748]]), array([[-1.0067737]]))
Process 2967: Main     : received data (array([[0.58171374, 0.17222748]]), array([[-1.0067737]]))
Process 3444: Observer : observed data (array([[0.61429365, 0.23384942]]), array([[-0.87983459]]))
Process 2967: Main     : received data (array([[0.61429365, 0.23384942]]), array([[-0.87983459]]))
Process 2967: Main     : iteration 5 starts
Process 3436: Observer : observing data at point [[0.53614473 0.1605479 ]]Process 3444: Observer : observing data at point [[0.53358876 0.17645295]]Process 3440: Observer : observing data at point [[0.53877165 0.1443234 ]]


Process 3436: Objective: pretends like it's doing something for 2.1sProcess 3440: Objective: pretends like it's doing something for 2.0sProcess 3444: Objective: pretends like it's doing something for 2.1s


Process 3440: Observer : observed data (array([[0.53877165, 0.1443234 ]]), array([[-1.04658407]]))
Process 2967: Main     : received data (array([[0.53877165, 0.1443234 ]]), array([[-1.04658407]]))
Process 3436: Observer : observed data (array([[0.53614473, 0.1605479 ]]), array([[-1.04642415]]))
Process 2967: Main     : received data (array([[0.53614473, 0.1605479 ]]), array([[-1.04642415]]))
Process 3444: Observer : observed data (array([[0.53358876, 0.17645295]]), array([[-1.04432234]]))
Process 2967: Main     : received data (array([[0.53358876, 0.17645295]]), array([[-1.04432234]]))
Process 2967: Main     : iteration 6 starts
Process 3444: Observer : observing data at point [[1. 1.]]Process 3440: Observer : observing data at point [[0.96282876 0.        ]]Process 3436: Observer : observing data at point [[1.         0.19636485]]


Process 3444: Objective: pretends like it's doing something for 6.0sProcess 3440: Objective: pretends like it's doing something for 2.9s
Process 3436: Objective: pretends like it's doing something for 3.6s

Process 3440: Observer : observed data (array([[0.96282876, 0.        ]]), array([[-0.92802422]]))
Process 2967: Main     : received data (array([[0.96282876, 0.        ]]), array([[-0.92802422]]))
Process 3436: Observer : observed data (array([[1.        , 0.19636485]]), array([[-1.01758527]]))
Process 2967: Main     : received data (array([[1.        , 0.19636485]]), array([[-1.01758527]]))
Process 3444: Observer : observed data (array([[1., 1.]]), array([[1.75288144]]))
Process 2967: Main     : received data (array([[1., 1.]]), array([[1.75288144]]))
Process 2967: Main     : iteration 7 starts
Process 3436: Observer : observing data at point [[0.88673994 0.11657718]]Process 3440: Observer : observing data at point [[0.90910008 0.16583685]]Process 3444: Observer : observing data at point [[0.90635463 0.21849171]]


Process 3440: Objective: pretends like it's doing something for 3.2sProcess 3436: Objective: pretends like it's doing something for 3.0s

Process 3444: Objective: pretends like it's doing something for 3.4s
Process 3436: Observer : observed data (array([[0.88673994, 0.11657718]]), array([[-0.94241076]]))
Process 2967: Main     : received data (array([[0.88673994, 0.11657718]]), array([[-0.94241076]]))
Process 3440: Observer : observed data (array([[0.90910008, 0.16583685]]), array([[-0.98601518]]))
Process 2967: Main     : received data (array([[0.90910008, 0.16583685]]), array([[-0.98601518]]))
Process 3444: Observer : observed data (array([[0.90635463, 0.21849171]]), array([[-0.94893246]]))
Process 2967: Main     : received data (array([[0.90635463, 0.21849171]]), array([[-0.94893246]]))
Process 2967: Main     : iteration 8 starts
Process 3440: Observer : observing data at point [[1.         0.10235892]]Process 3436: Observer : observing data at point [[1.         0.11424654]]Process 3444: Observer : observing data at point [[1.         0.12582045]]


Process 3436: Objective: pretends like it's doing something for 3.3sProcess 3440: Objective: pretends like it's doing something for 3.3s

Process 3444: Objective: pretends like it's doing something for 3.4s
Process 3440: Observer : observed data (array([[1.        , 0.10235892]]), array([[-0.97619036]]))
Process 2967: Main     : received data (array([[1.        , 0.10235892]]), array([[-0.97619036]]))
Process 3436: Observer : observed data (array([[1.        , 0.11424654]]), array([[-0.98565297]]))
Process 2967: Main     : received data (array([[1.        , 0.11424654]]), array([[-0.98565297]]))
Process 3444: Observer : observed data (array([[1.        , 0.12582045]]), array([[-0.99368979]]))
Process 2967: Main     : received data (array([[1.        , 0.12582045]]), array([[-0.99368979]]))
Process 2967: Main     : iteration 9 starts
Process 3436: Observer : observing data at point [[0.96175829 0.16984262]]Process 3440: Observer : observing data at point [[0.96245067 0.17535337]]Process 3444: Observer : observing data at point [[0.96310479 0.18085552]]


Process 3440: Objective: pretends like it's doing something for 3.4sProcess 3436: Objective: pretends like it's doing something for 3.4sProcess 3444: Objective: pretends like it's doing something for 3.4s


Process 3436: Observer : observed data (array([[0.96175829, 0.16984262]]), array([[-1.04729582]]))
Process 2967: Main     : received data (array([[0.96175829, 0.16984262]]), array([[-1.04729582]]))
Process 3440: Observer : observed data (array([[0.96245067, 0.17535337]]), array([[-1.04697493]]))
Process 2967: Main     : received data (array([[0.96245067, 0.17535337]]), array([[-1.04697493]]))
Process 3444: Observer : observed data (array([[0.96310479, 0.18085552]]), array([[-1.04642351]]))
Process 2967: Main     : received data (array([[0.96310479, 0.18085552]]), array([[-1.04642351]]))
Got 33 observations in 47.41s

Comparison#

To compare outcomes of sync and async runs, let’s plot their respective regrets side by side, and print out the running time. For this toy problem we expect async scenario to run a little bit faster on machines with multiple CPU.

[10]:
from trieste.experimental.plotting import plot_regret
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 2)

sync_lp_min_idx = tf.squeeze(tf.argmin(sync_lp_observations, axis=0))
async_lp_min_idx = tf.squeeze(tf.argmin(async_lp_observations, axis=0))

plot_regret(
    sync_lp_observations.numpy(),
    ax[0],
    num_init=len(initial_data),
    idx_best=sync_lp_min_idx,
)
ax[0].set_yscale("log")
ax[0].set_ylabel("Regret")
ax[0].set_ylim(0.0000001, 100)
ax[0].set_xlabel("# evaluations")
ax[0].set_title(
    f"Sync LP, {len(sync_lp_observations)} points, time {sync_lp_time:.2f}"
)

plot_regret(
    async_lp_observations.numpy(),
    ax[1],
    num_init=len(initial_data),
    idx_best=async_lp_min_idx,
)
ax[1].set_yscale("log")
ax[1].set_ylabel("Regret")
ax[1].set_ylim(0.0000001, 100)
ax[1].set_xlabel("# evaluations")
ax[1].set_title(
    f"Async LP, {len(async_lp_observations)} points, time {async_lp_time:.2f}s"
)

fig.tight_layout()
../_images/notebooks_asynchronous_greedy_multiprocessing_19_0.png