feat(async): add run_async to every algorithm in the catalog

Async coverage was incomplete in 0.7 (only RandomSearch and
DifferentialEvolution had run_async). 0.8 closes the gap: every one
of the 33 algorithms now exposes
run_async(&problem, concurrency).await, gated on the async feature.

- Population-based algorithms fan out per-generation evaluations
  through evaluate_batch_async with concurrency-bounded
  FuturesOrdered chunks.
- Steady-state algorithms (HillClimber, SimulatedAnnealing,
  OnePlusOneEs, Paes, NelderMead) await each step sequentially;
  they accept the concurrency parameter for API uniformity.
- TabuSearch fans out the K-neighbor batch each step.
- Surrogate algorithms (BayesianOpt, Tpe) batch the initial design
  and await per-iteration acquisitions sequentially so the surrogate
  can update between picks.
- Hyperband uses a new AsyncPartialProblem trait (mirroring
  PartialProblem for multi-fidelity workloads) and a parallel
  evaluate_batch_at_budget_async helper; each Successive-Halving
  rung fans out its budgeted evaluations.

All paths preserve seeded determinism: RNG draws happen on the main
task in the same order as the sync path, and only the evaluations
are concurrent.

Adds a dedicated cookbook recipe at docs/book/src/cookbook/async.md
with a worked example (DifferentialEvolution under tokio) and
guidance on picking concurrency. Cross-references in SUMMARY.md
and cookbook.md are updated to surface the new recipe.

The follow-up docs commit reconciles the rest of the user guide
and README to describe the new feature; this commit is the bare
async surface.
This commit is contained in:
2026-05-06 11:51:13 -06:00
parent d1288aa623
commit cbfedd85fa
36 changed files with 3632 additions and 7 deletions
+31 -4
View File
@@ -7,10 +7,12 @@
//! thread.
//!
//! [`AsyncProblem`] mirrors [`Problem`](crate::core::Problem) but its
//! `evaluate_async` returns a future. Algorithms that support async
//! evaluation (NSGA-II, DE, RandomSearch as of v0.7.0; others land
//! incrementally) expose a `run_async` method that drives evaluations
//! through a user-chosen async runtime (typically tokio).
//! `evaluate_async` returns a future. Every algorithm in heuropt exposes
//! a `run_async` method that drives evaluations through a user-chosen
//! async runtime (typically tokio). Hyperband uses
//! [`AsyncPartialProblem`] instead, which mirrors
//! [`PartialProblem`](crate::core::partial_problem::PartialProblem) for
//! multi-fidelity workloads.
//!
//! Available only with the `async` feature.
@@ -52,3 +54,28 @@ pub trait AsyncProblem: Sync {
/// invoked from.
fn evaluate_async(&self, decision: &Self::Decision) -> impl Future<Output = Evaluation> + Send;
}
/// Async equivalent of [`PartialProblem`](crate::core::partial_problem::PartialProblem)
/// for multi-fidelity workloads — used by Hyperband's `run_async`.
///
/// Like [`AsyncProblem`], `evaluate_at_budget_async` returns a future
/// so callers can fan out budgeted evaluations across an async runtime.
pub trait AsyncPartialProblem: Sync {
/// The thing the optimizer changes. Same constraints as
/// [`PartialProblem::Decision`](crate::core::partial_problem::PartialProblem::Decision).
type Decision: Clone + Send + Sync;
/// Return the objectives for this problem.
fn objectives(&self) -> ObjectiveSpace;
/// Evaluate `decision` at the given fidelity `budget` asynchronously.
///
/// Same monotonicity contract as
/// [`PartialProblem::evaluate_at_budget`](crate::core::partial_problem::PartialProblem::evaluate_at_budget):
/// higher budget should give a more accurate estimate.
fn evaluate_at_budget_async(
&self,
decision: &Self::Decision,
budget: f64,
) -> impl Future<Output = Evaluation> + Send;
}