Companion to the feat(explorer) commit. Bumps the version and brings every cross-referencing doc up to v0.9 currency. - Cargo.toml: version 0.8.0 -> 0.9.0. - CHANGELOG: 0.9.0 entry covering the explorer export, the Problem-side metadata additions, the AlgorithmInfo trait, the pick_a_car example, and the new cookbook recipe. - README: closing paragraph of the PickACar example points users at the explorer with a one-call snippet (`ExplorerExport::from_result(...).with_algorithm_info(...) .to_file(...)?`). Version snippets bumped 0.8 -> 0.9. - New cookbook recipe at docs/book/src/cookbook/explorer.md covering: enabling the serde feature, enriching Problem with labels/units/decision-schema, the export call, the JSON schema, and custom decision-type handling. - SUMMARY.md and cookbook.md link the new recipe. - migration.md: new "To 0.9" section documenting the additive changes (purely backwards-compatible upgrade from 0.8.x). - introduction.md, comparison.md, choosing-an-algorithm.md, stability.md: version refs bumped 0.8 -> 0.9. - cookbook/parallel.md, cookbook/async.md: version refs bumped 0.8 -> 0.9. - getting-started.md: version refs bumped, serde feature description expanded to mention the explorer module. - SECURITY.md: supported-versions table moves to 0.9.x.
5.9 KiB
Async evaluation
When your evaluate does IO — calls an HTTP service, sends an
RPC, spawns a subprocess — await-ing it from the optimizer is
much more efficient than blocking a thread per evaluation. heuropt
ships first-class async support behind the async feature flag.
This is the differentiating capability vs pymoo / hyperopt / optuna / DEAP / MOEA Framework — none of those have a native async evaluation path.
Enable the feature
[dependencies]
heuropt = { version = "0.10", features = ["async"] }
# Pick whatever async runtime you want; heuropt itself depends only on
# `futures`. The example below uses tokio.
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
Implement AsyncProblem
It mirrors the regular Problem trait one-for-one — same
Decision type, same objectives(), but evaluate is replaced
with evaluate_async returning a future.
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 {
// Real workload: HTTP call to a model-scoring service, an RPC,
// a subprocess. Here we just sleep to model 20 ms latency.
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let loss: f64 = x.iter().map(|v| v * v).sum();
Evaluation::new(vec![loss])
}
}
Run the optimizer with run_async
run_async(&problem, concurrency).await is provided by every
algorithm in the catalog as of v0.8. concurrency caps how many
evaluations are in-flight at once.
# 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 {
# Evaluation::new(vec![x.iter().map(|v| v * v).sum::<f64>()])
# }
# }
#[tokio::main]
async fn main() {
let bounds = vec![(-1.0_f64, 1.0_f64); 4];
let mut opt = DifferentialEvolution::new(
DifferentialEvolutionConfig {
population_size: 16,
generations: 50,
differential_weight: 0.5,
crossover_probability: 0.9,
seed: 42,
},
RealBounds::new(bounds),
);
let r = opt.run_async(&RemoteService, /* concurrency */ 8).await;
println!("best: {}", r.best.unwrap().evaluation.objectives[0]);
}
Picking concurrency
Concurrency is the maximum in-flight evaluation count. Tradeoffs:
| Setting | Effect |
|---|---|
1 |
Sequential; equivalent to a sync run with extra overhead |
pop_size |
Full per-generation parallelism; fastest if your service tolerates it |
< pop_size |
Bounded — useful if your downstream service has a rate limit or finite worker pool |
The bigger you go, the more memory the in-flight futures hold and
the more load you put on the downstream service. A reasonable
starting point is min(pop_size, 16) and increase only if the
downstream service is comfortable.
Determinism
Same seed produces the same final result whether you use run or
run_async, provided your async evaluate_async is itself
deterministic. heuropt drives the RNG and selection on the main
task; only the evaluations are concurrent, and the
evaluate_batch_async helper preserves input order before feeding
results back to the algorithm.
What the worked example shows
examples/async_eval.rs runs Random Search (200 evaluations × 20 ms
each) at concurrency = 1, 4, 16 and Differential Evolution at
concurrency = 8. On a recent machine:
RandomSearch with 200 evaluations (20 ms each)
concurrency = 1 elapsed ≈ 4250 ms (sequential 200 × 20 ms)
concurrency = 4 elapsed ≈ 2100 ms (2× speedup, batch_size=2 caps it)
concurrency = 16 elapsed ≈ 2100 ms (same — batch_size dominates)
DifferentialEvolution at concurrency=8
elapsed ≈ 230 ms (8 ants run in parallel each generation)
Run it yourself: cargo run --release --features async --example async_eval.
Which algorithms support run_async?
All 33 algorithms in the catalog. The shape of the async path depends on the algorithm:
- Population-based / batch-evaluating — NSGA-II, NSGA-III, SPEA2,
MOEA/D, IBEA, SMS-EMOA, HypE, ε-MOEA, PESA-II, AGE-MOEA, KnEA,
GrEA, RVEA, MOPSO, GA, DE, PSO, CMA-ES, IPOP-CMA-ES, sNES, TLBO,
UMDA, Ant Colony, Random Search. Each generation's offspring
evaluations are fanned out concurrently up to
concurrency. - Steady-state (one-eval-per-step) — Hill Climber, Simulated
Annealing, (1+1)-ES, PAES, Nelder-Mead. The
concurrencyparameter is accepted for API uniformity but evaluation order is inherently sequential. - Tabu Search — fans out the K-neighbor batch each step.
- Surrogate (BO, TPE) — fans out the initial design batch, then awaits per-iteration acquisitions sequentially (the surrogate must update before the next point is chosen).
- Hyperband — uses the separate
AsyncPartialProblemtrait (multi-fidelity); each Successive-Halving rung's evaluations fan out concurrently.
Async vs parallel
If your evaluate is… |
Use |
|---|---|
| CPU-bound (math, simulation) | parallel feature → see Parallelize evaluation |
| IO-bound (HTTP, RPC, subprocess) | async feature (this recipe) |
Both can be on at once if your evaluation does both substantial CPU work and IO. The two features are independent.