@@ -15,7 +15,7 @@ minimal changes to your existing code.
1515
1616 .. code-block :: python
1717
18- @Multiprocessing (n_workers = 4 ).distribute
18+ @Joblib (n_workers = 4 ).distribute
1919 def objective (para ):
2020 ...
2121
@@ -39,6 +39,14 @@ The optimizer proposes ``n_workers`` positions per batch, the decorated function
3939evaluates them in parallel, and the results are fed back. The initialization
4040phase (where the optimizer evaluates starting positions) always runs serially.
4141
42+ Async backends like Ray and Dask go one step further. Instead of waiting for
43+ an entire batch, they process results individually as workers complete and
44+ immediately submit new work. This keeps all workers busy at all times:
45+
46+ .. code-block :: text
47+
48+ Async: submit(4) -> W1 done -> tell(1), submit(1) -> W3 done -> tell(1), submit(1) -> ...
49+
4250
4351 Basic Usage
4452-----------
@@ -47,9 +55,9 @@ Basic Usage
4755
4856 import numpy as np
4957 from gradient_free_optimizers import HillClimbingOptimizer
50- from gradient_free_optimizers.distributed import Multiprocessing
58+ from gradient_free_optimizers.distributed import Joblib
5159
52- @Multiprocessing (n_workers = 4 ).distribute
60+ @Joblib (n_workers = 4 ).distribute
5361 def objective (para ):
5462 return - (para[" x" ]** 2 + para[" y" ]** 2 )
5563
@@ -61,6 +69,14 @@ Basic Usage
6169 opt = HillClimbingOptimizer(search_space)
6270 opt.search(objective, n_iter = 100 )
6371
72+ The decorator can also be applied without the ``@ `` syntax, which is useful when
73+ the function is defined elsewhere:
74+
75+ .. code-block :: python
76+
77+ distributed_objective = Ray(n_workers = 8 ).distribute(objective)
78+ opt.search(distributed_objective, n_iter = 100 )
79+
6480
6581 Machine Learning Example
6682------------------------
@@ -76,12 +92,12 @@ model independently:
7692 from sklearn.datasets import load_wine
7793
7894 from gradient_free_optimizers import BayesianOptimizer
79- from gradient_free_optimizers.distributed import Multiprocessing
95+ from gradient_free_optimizers.distributed import Joblib
8096
8197 data = load_wine()
8298 X, y = data.data, data.target
8399
84- @Multiprocessing (n_workers = 4 ).distribute
100+ @Joblib (n_workers = 4 ).distribute
85101 def model (para ):
86102 gbc = GradientBoostingClassifier(
87103 n_estimators = para[" n_estimators" ],
@@ -104,14 +120,96 @@ Available Backends
104120
105121.. list-table ::
106122 :header-rows: 1
107- :widths: 25 25 50
123+ :widths: 20 15 15 50
108124
109125 * - Backend
126+ - Async
110127 - Dependencies
111128 - Use case
112129 * - ``Multiprocessing ``
130+ - No
113131 - stdlib only
114- - Local parallelism on a single machine
132+ - Local parallelism via fork. No extra dependencies needed.
133+ * - ``Joblib ``
134+ - No
135+ - joblib
136+ - Local parallelism with automatic fork/spawn handling. Included with scikit-learn.
137+ * - ``Ray ``
138+ - Yes
139+ - ray
140+ - Local or cluster-wide parallelism. Handles serialization via cloudpickle.
141+ * - ``Dask ``
142+ - Yes
143+ - dask[distributed]
144+ - Local or cluster-wide parallelism. Integrates with existing Dask infrastructure.
145+
146+ Sync backends (Multiprocessing, Joblib) evaluate a full batch and wait for all
147+ results before proposing the next batch. Async backends (Ray, Dask) process
148+ results individually as they arrive, keeping workers busy at all times.
149+
150+
151+ Async Mode
152+ ----------
153+
154+ When using an async backend like Ray, most optimizers run in true async mode
155+ where each completed evaluation immediately triggers a new proposal. Three
156+ optimizers (Downhill Simplex, Powell's Method, DIRECT) use a batch-async
157+ fallback where positions are submitted asynchronously but collected per batch.
158+ This distinction is handled automatically.
159+
160+ .. code-block :: python
161+
162+ from gradient_free_optimizers import ParticleSwarmOptimizer
163+ from gradient_free_optimizers.distributed import Ray
164+
165+ @Ray (n_workers = 8 ).distribute
166+ def expensive_simulation (para ):
167+ # Each evaluation takes 10-60 seconds
168+ return run_simulation(para)
169+
170+ opt = ParticleSwarmOptimizer(search_space, population = 20 )
171+ opt.search(expensive_simulation, n_iter = 200 )
172+
173+ True async is most beneficial when evaluation times vary widely. Slow evaluations
174+ no longer block fast ones from being processed and replaced.
175+
176+
177+ Error Handling
178+ --------------
179+
180+ The ``catch `` parameter works with distributed evaluation. Exceptions are caught
181+ inside each worker process, and the fallback score is returned in place of the
182+ failed evaluation:
183+
184+ .. code-block :: python
185+
186+ @Joblib (n_workers = 4 ).distribute
187+ def flaky_model (para ):
188+ # Might fail for certain hyperparameter combinations
189+ return train_and_evaluate(para)
190+
191+ opt.search(flaky_model, n_iter = 100 , catch = {ValueError : - 1000.0 })
192+
193+
194+ Storage Integration
195+ -------------------
196+
197+ Distributed evaluation works with :doc: `storage backends <storage >`. Positions
198+ are checked against the cache before being dispatched to workers, and new
199+ results are stored after evaluation. This avoids redundant computations and
200+ enables crash recovery:
201+
202+ .. code-block :: python
203+
204+ from gradient_free_optimizers.distributed import Joblib
205+ from gradient_free_optimizers.storage import SQLiteStorage
206+
207+ @Joblib (n_workers = 4 ).distribute
208+ def model (para ):
209+ return expensive_training(para)
210+
211+ storage = SQLiteStorage(" results.db" )
212+ opt.search(model, n_iter = 100 , memory = storage)
115213
116214
117215 Custom Backends
@@ -126,7 +224,6 @@ and implementing ``_distribute``:
126224
127225 class MyClusterBackend (BaseDistribution ):
128226 def _distribute (self , func , params_batch ):
129- # Send evaluations to your cluster, collect scores
130227 scores = my_cluster.map(func, params_batch)
131228 return scores
132229
@@ -138,6 +235,25 @@ The ``_distribute`` method receives the original objective function and a
138235list of parameter dictionaries. It must return a list of scores in the
139236same order.
140237
238+ For async support, also implement ``_submit `` and ``_wait_any `` and set
239+ ``_is_async = True ``:
240+
241+ .. code-block :: python
242+
243+ class MyAsyncBackend (BaseDistribution ):
244+ _is_async = True
245+
246+ def _distribute (self , func , params_batch ):
247+ futures = [self ._submit(func, p) for p in params_batch]
248+ return [f.result() for f in futures]
249+
250+ def _submit (self , func , params ):
251+ return my_cluster.submit(func, params)
252+
253+ def _wait_any (self , futures ):
254+ done = my_cluster.wait_any(futures)
255+ return done, done.result()
256+
141257
142258 Batch Size and Algorithm Interaction
143259------------------------------------
@@ -157,18 +273,16 @@ are evaluated simultaneously:
157273 opt = HillClimbingOptimizer(search_space, n_neighbours = 5 )
158274 opt.search(objective, n_iter = 100 ) # objective has n_workers=8
159275
276+ For surrogate model-based optimizers (Bayesian Optimization, TPE, Forest
277+ Optimizer), batch positions are selected using KMeans clustering on the
278+ acquisition landscape to ensure diversity. Without this, all batch positions
279+ would cluster around the single highest acquisition peak.
280+
160281
161282Limitations
162283-----------
163284
164- The current implementation has a few constraints:
165-
166- **Memory caching ** is not supported with distributed evaluation. When a
167- distributed decorator is detected, memory is automatically disabled.
168-
169- **The catch parameter ** for error handling is not yet supported in
170- distributed mode. Worker exceptions propagate directly.
171-
172285**The objective function ** must be defined at module level (not a lambda
173286or closure) for the ``Multiprocessing `` backend on systems that do not
174- support the ``fork `` start method.
287+ support the ``fork `` start method. Ray and Dask handle closures via
288+ cloudpickle.
0 commit comments