feat: v0.7.0 — async evaluation
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).
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