feat(async): AsyncProblem trait + run_async on RandomSearch and DifferentialEvolution
Adds the headline async/await capability for IO-bound evaluations (HTTP services, RPC clients, spawned subprocesses) — the differentiator vs pymoo / hyperopt / MOEA Framework. No public-API breaks for synchronous users. The new surface is gated behind a new `async` feature flag. - core::async_problem::AsyncProblem trait (async fn evaluate_async). - algorithms::parallel_eval_async::evaluate_batch_async helper using futures::stream::FuturesOrdered with concurrency-bounded chunks; preserves input order so seeded determinism holds when evaluations are themselves deterministic. - run_async on RandomSearch and DifferentialEvolution. - examples/async_eval.rs: simulated 20 ms remote service. concurrency=1 → 4.2 s, concurrency=4 → 2.1 s (2× speedup). Bumps Cargo.toml to 0.8.0; CHANGELOG entry covers the above plus a note that 0.6.0/0.7.0 on crates.io are yanked experimentals and 0.8 picks up cleanly from 0.5.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
//! Async evaluation example: optimize hyperparameters where each
|
||||
//! evaluation is an awaitable (simulated HTTP) call.
|
||||
//!
|
||||
//! Demonstrates:
|
||||
//! - Implementing [`AsyncProblem`].
|
||||
//! - Driving the optimizer through `tokio` with bounded concurrency.
|
||||
//! - Comparing wall-clock time at concurrency = 1 vs 8.
|
||||
//!
|
||||
//! Run with: `cargo run --release --features async --example async_eval`
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use heuropt::core::async_problem::AsyncProblem;
|
||||
use heuropt::prelude::*;
|
||||
|
||||
struct RemoteService;
|
||||
|
||||
impl AsyncProblem for RemoteService {
|
||||
type Decision = Vec<f64>;
|
||||
|
||||
fn objectives(&self) -> ObjectiveSpace {
|
||||
ObjectiveSpace::new(vec![Objective::minimize("loss")])
|
||||
}
|
||||
|
||||
async fn evaluate_async(&self, x: &Vec<f64>) -> Evaluation {
|
||||
// Simulate a 20 ms remote-service round-trip per evaluation.
|
||||
// The compute itself is ~free; the latency is the bottleneck.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
let loss: f64 = x.iter().map(|v| v * v).sum();
|
||||
Evaluation::new(vec![loss])
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let bounds = vec![(-1.0_f64, 1.0_f64); 4];
|
||||
let problem = RemoteService;
|
||||
|
||||
println!("RandomSearch with 200 evaluations (20 ms each)");
|
||||
println!();
|
||||
|
||||
for &concurrency in &[1_usize, 4, 16] {
|
||||
let mut opt = RandomSearch::new(
|
||||
RandomSearchConfig {
|
||||
iterations: 100,
|
||||
batch_size: 2,
|
||||
seed: 42,
|
||||
},
|
||||
RealBounds::new(bounds.clone()),
|
||||
);
|
||||
let started = Instant::now();
|
||||
let result = opt.run_async(&problem, concurrency).await;
|
||||
let elapsed = started.elapsed();
|
||||
println!(
|
||||
"concurrency = {:>2} elapsed = {:>5} ms best loss = {:>8.5} evaluations = {}",
|
||||
concurrency,
|
||||
elapsed.as_millis(),
|
||||
result.best.unwrap().evaluation.objectives[0],
|
||||
result.evaluations,
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("DifferentialEvolution at concurrency=8");
|
||||
let started = Instant::now();
|
||||
let mut de = DifferentialEvolution::new(
|
||||
DifferentialEvolutionConfig {
|
||||
population_size: 8,
|
||||
generations: 10,
|
||||
differential_weight: 0.5,
|
||||
crossover_probability: 0.9,
|
||||
seed: 42,
|
||||
},
|
||||
RealBounds::new(bounds.clone()),
|
||||
);
|
||||
let result = de.run_async(&problem, 8).await;
|
||||
let elapsed = started.elapsed();
|
||||
println!(
|
||||
"elapsed = {:>5} ms best loss = {:>8.5} evaluations = {}",
|
||||
elapsed.as_millis(),
|
||||
result.best.unwrap().evaluation.objectives[0],
|
||||
result.evaluations,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user