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.
92 lines
2.9 KiB
Rust
92 lines
2.9 KiB
Rust
//! Async population evaluator.
|
|
//!
|
|
//! Available only with the `async` feature. Used by the `run_async`
|
|
//! method on algorithms that support async problems.
|
|
|
|
use futures::stream::{FuturesOrdered, StreamExt};
|
|
|
|
use crate::core::async_problem::{AsyncPartialProblem, AsyncProblem};
|
|
use crate::core::candidate::Candidate;
|
|
use crate::core::evaluation::Evaluation;
|
|
|
|
/// Evaluate every decision concurrently against `problem`, preserving
|
|
/// input order in the returned vector. Concurrency is bounded by
|
|
/// `concurrency` (≥ 1) — too high a value wastes memory and may
|
|
/// overload downstream services; too low forfeits parallelism.
|
|
///
|
|
/// Returns a future that the caller drives via their preferred
|
|
/// runtime (typically tokio).
|
|
pub async fn evaluate_batch_async<P>(
|
|
problem: &P,
|
|
decisions: Vec<P::Decision>,
|
|
concurrency: usize,
|
|
) -> Vec<Candidate<P::Decision>>
|
|
where
|
|
P: AsyncProblem,
|
|
{
|
|
assert!(
|
|
concurrency >= 1,
|
|
"evaluate_batch_async concurrency must be >= 1"
|
|
);
|
|
let mut out: Vec<Candidate<P::Decision>> = Vec::with_capacity(decisions.len());
|
|
|
|
// Process in concurrency-bounded chunks to keep peak memory low
|
|
// and avoid blasting downstream services. Each chunk uses
|
|
// FuturesOrdered to preserve per-chunk order, and chunks are
|
|
// emitted in their natural order.
|
|
let mut iter = decisions.into_iter();
|
|
loop {
|
|
let mut futs = FuturesOrdered::new();
|
|
for _ in 0..concurrency {
|
|
match iter.next() {
|
|
Some(d) => {
|
|
futs.push_back(async move {
|
|
let e = problem.evaluate_async(&d).await;
|
|
Candidate::new(d, e)
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
if futs.is_empty() {
|
|
break;
|
|
}
|
|
while let Some(c) = futs.next().await {
|
|
out.push(c);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Evaluate every decision at the given `budget` concurrently against a
|
|
/// multi-fidelity `problem`, preserving input order. Hyperband's async
|
|
/// path uses this for each Successive-Halving rung.
|
|
pub async fn evaluate_batch_at_budget_async<P>(
|
|
problem: &P,
|
|
decisions: &[P::Decision],
|
|
budget: f64,
|
|
concurrency: usize,
|
|
) -> Vec<Evaluation>
|
|
where
|
|
P: AsyncPartialProblem,
|
|
{
|
|
assert!(
|
|
concurrency >= 1,
|
|
"evaluate_batch_at_budget_async concurrency must be >= 1"
|
|
);
|
|
let mut out: Vec<Evaluation> = Vec::with_capacity(decisions.len());
|
|
let mut idx = 0usize;
|
|
while idx < decisions.len() {
|
|
let mut futs = FuturesOrdered::new();
|
|
let end = (idx + concurrency).min(decisions.len());
|
|
for d in &decisions[idx..end] {
|
|
futs.push_back(async move { problem.evaluate_at_budget_async(d, budget).await });
|
|
}
|
|
while let Some(e) = futs.next().await {
|
|
out.push(e);
|
|
}
|
|
idx = end;
|
|
}
|
|
out
|
|
}
|