Heuristic:Avhz RustQuant MC Parallel Path Threshold
| Knowledge Sources | |
|---|---|
| Domains | Optimization, Monte_Carlo |
| Last Updated | 2026-02-07 19:00 GMT |
Overview
Enable parallel path generation via rayon when simulating more than ~1000 Monte Carlo paths to overcome threading overhead.
Description
The RustQuant stochastic process simulation engine provides a `parallel` flag in `StochasticProcessConfig` that switches between serial `iter_mut()` and rayon's `par_iter_mut()` for path generation. Each path is independently simulated with its own RNG seeded via `base_seed.wrapping_add(i)`, making parallelization embarrassingly parallel. However, rayon thread pool initialization and work-stealing overhead means that for small path counts, serial execution is faster.
Usage
Apply this heuristic when configuring Monte Carlo simulations via `StochasticProcessConfig`. Set `parallel = true` when `m_paths` exceeds approximately 1000. For fewer paths, keep `parallel = false` to avoid threading overhead.
The Insight (Rule of Thumb)
- Action: Set `parallel = true` in `StochasticProcessConfig` when `m_paths > 1000`.
- Value: Threshold of approximately 1000 paths.
- Trade-off: Below ~1000 paths, serial execution is faster due to rayon thread pool overhead. Above ~1000 paths, parallel execution scales near-linearly with CPU cores.
- Seed Determinism: Parallel execution remains deterministic when a seed is provided, as each path index generates its own seed via `wrapping_add`.
Reasoning
Rayon's work-stealing thread pool has fixed initialization cost and per-task scheduling overhead. For Monte Carlo simulation, each path is independent (embarrassingly parallel), so the speedup approaches the number of CPU cores for large path counts. The crossover point where parallelization overhead is amortized by the per-path computation cost is empirically around 1000 paths, depending on the number of time steps per path.
The codebase uses the same `scheme` closure for both serial and parallel execution, with only the iterator type changing:
Code Evidence
Parallel vs. serial branching from `simulation.rs:143-158`:
let base_seed: u64 = config.seed.unwrap_or_else(rand::random);
if config.parallel {
paths.par_iter_mut().enumerate().for_each(|(i, path)| {
let noise_gen = match fractional_config {
Some(fractional_config) => NoiseGenerator::Fractional(fractional_config),
None => NoiseGenerator::Dynamic(StdRng::seed_from_u64(base_seed.wrapping_add(i as u64))),
};
scheme(path, noise_gen);
});
} else {
paths.iter_mut().enumerate().for_each(|(i, path)| {
let noise_gen = match fractional_config {
Some(fractional_config) => NoiseGenerator::Fractional(fractional_config),
None => NoiseGenerator::Dynamic(StdRng::seed_from_u64(base_seed.wrapping_add(i as u64))),
};
scheme(path, noise_gen);
});
}
Time validation assertion from `simulation.rs:28`:
assert!(config.t_0 < config.t_n);