Skip to content

Commit 99a2d6e

Browse files
committed
update docs with storage and dist.
1 parent ac7610a commit 99a2d6e

File tree

4 files changed

+305
-19
lines changed

4 files changed

+305
-19
lines changed

docs/source/api_reference.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,38 @@ Control output during search:
225225
Distributed Evaluation
226226
----------------------
227227

228-
Backends for parallel objective function evaluation.
228+
Backends for parallel objective function evaluation. Sync backends evaluate
229+
full batches, async backends process results as they arrive.
229230

230231
.. code-block:: python
231232
232-
from gradient_free_optimizers.distributed import Multiprocessing, BaseDistribution
233+
from gradient_free_optimizers.distributed import Joblib, Ray, Dask
233234
234235
.. autosummary::
235236
:toctree: api_reference/generated/
236237
:template: class.rst
237238

238239
gradient_free_optimizers.distributed.BaseDistribution
239240
gradient_free_optimizers.distributed.Multiprocessing
241+
gradient_free_optimizers.distributed.Joblib
242+
gradient_free_optimizers.distributed.Ray
243+
gradient_free_optimizers.distributed.Dask
244+
245+
246+
Evaluation Storage
247+
------------------
248+
249+
Pluggable backends for caching objective function evaluations. Pass a storage
250+
instance to the ``memory`` parameter of ``search()``.
251+
252+
.. code-block:: python
253+
254+
from gradient_free_optimizers.storage import SQLiteStorage
255+
256+
.. autosummary::
257+
:toctree: api_reference/generated/
258+
:template: class.rst
259+
260+
gradient_free_optimizers.storage.BaseStorage
261+
gradient_free_optimizers.storage.MemoryStorage
262+
gradient_free_optimizers.storage.SQLiteStorage

docs/source/user_guide.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ Guide Sections
156156
Parallel evaluation across
157157
multiple workers.
158158

159+
.. grid-item-card:: Evaluation Storage
160+
:link: user_guide/storage
161+
:link-type: doc
162+
163+
Persistent caching and crash
164+
recovery with pluggable backends.
165+
159166
----
160167

161168
Algorithms
@@ -220,6 +227,7 @@ GFO provides 22 optimization algorithms organized into four categories:
220227
user_guide/stopping_conditions
221228
user_guide/search_interface
222229
user_guide/distributed
230+
user_guide/storage
223231

224232
.. toctree::
225233
:maxdepth: 2

docs/source/user_guide/distributed.rst

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3939
evaluates them in parallel, and the results are fed back. The initialization
4040
phase (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
138235
list of parameter dictionaries. It must return a list of scores in the
139236
same 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

161282
Limitations
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
173286
or 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

Comments
 (0)