Theme: async/await for IO-bound evaluations. Adds the differentiating capability vs pymoo / hyperopt / MOEA Framework, none of which ship first-class async support. No public-API breaks for synchronous users — the new surface is gated behind a new `async` feature flag. Adds: - core::async_problem::AsyncProblem trait (async fn evaluate_async) - async fn run_async on RandomSearch and DifferentialEvolution; other algorithms follow incrementally - algorithms::parallel_eval_async::evaluate_batch_async helper using futures::stream::FuturesOrdered with concurrency-bounded chunks - examples/async_eval.rs worked example with simulated 20 ms remote service: concurrency=1 → 4.2 s, concurrency=4 → 2.1 s (2× speedup) Bumps Cargo.toml to 0.7.0; CHANGELOG entry covers the above. Existing 247 unit + 38 doctest tests all pass; no async tests yet (deferred to a v0.7.x patch with tokio dev-deps wired in).
85 lines
2.6 KiB
Rust
85 lines
2.6 KiB
Rust
//! 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,
|
|
);
|
|
}
|