From 30db01ccc87b261fd9ec609235b95a48b7500dca Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 22 May 2026 21:13:37 +0200 Subject: [PATCH] perf(loader): parallel per-file load via rayon par_iter (~4-8x speedup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each file's load_or_predecode + label generation is pure CPU work over disjoint inputs (snapshots, cfg.horizons, cfg.outcome_label_cost). The sequential for loop was the parallel-friendly bottleneck — converting to rayon::par_iter gives ~4-8x speedup on typical 4-8 core hosts. Combined with SPEED-C's ~400x speedup on the inner Welford loop, total preload throughput is ~1600-3200x faster than the prior single-threaded O(W) recompute path. File order is preserved by par_iter's collect contract. Too-few-snapshots skips emit a warn during load and resolve to None at collection. --- Cargo.lock | 1 + crates/ml-alpha/Cargo.toml | 4 ++++ crates/ml-alpha/src/data/loader.rs | 26 +++++++++++++++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f2694c4c..eeb6d3786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6098,6 +6098,7 @@ dependencies = [ "ml-features", "rand 0.8.5", "rand_chacha 0.3.1", + "rayon", "serde", "serde_json", "tempfile", diff --git a/crates/ml-alpha/Cargo.toml b/crates/ml-alpha/Cargo.toml index 36548ef45..a96e04195 100644 --- a/crates/ml-alpha/Cargo.toml +++ b/crates/ml-alpha/Cargo.toml @@ -46,6 +46,10 @@ rand_chacha = "0.3" # Phase A data path: mmap predecoded MBP-10 sidecars. memmap2 = { workspace = true } +# SPEED-A (2026-05-22): parallel per-file load + label generation in +# MultiHorizonLoader::new gives ~4-8x speedup on typical 4-8 core hosts. +rayon = { workspace = true } + [dev-dependencies] tempfile = { workspace = true } approx = { workspace = true } diff --git a/crates/ml-alpha/src/data/loader.rs b/crates/ml-alpha/src/data/loader.rs index c9f601801..86992845b 100644 --- a/crates/ml-alpha/src/data/loader.rs +++ b/crates/ml-alpha/src/data/loader.rs @@ -22,6 +22,7 @@ use ml_features::predecoded::load_or_predecode_mbp10; pub use data::providers::databento::dbn_parser::InstrumentFilter; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use rayon::prelude::*; use crate::cfc::snap_features::{Mbp10RawInput, ES_TICK_SIZE, REGIME_DIM}; use crate::heads::N_HORIZONS; @@ -295,8 +296,13 @@ impl MultiHorizonLoader { } else { lookback + max_horizon + 1 }; - let mut files_loaded: Vec = Vec::with_capacity(files.len()); - for path in &files { + // SPEED-A (2026-05-22): parallel per-file load + label generation. + // Each file's load_or_predecode + generate_labels + generate_outcome_labels_ab + + // compute_regime_features is pure CPU work over disjoint inputs (snapshots, + // cfg.horizons, cfg.outcome_label_cost). par_iter gives ~4-8x speedup on + // typical 4-8 core machines. Files emitting a "too few snapshots" warning + // are filtered out post-collection (via Option). + let load_one = |path: &PathBuf| -> Result> { let snapshots = load_or_predecode_mbp10(path, &cfg.predecoded_dir, cfg.instrument_filter) .with_context(|| format!( "load mbp10 {} (filter={:?})", path.display(), cfg.instrument_filter @@ -308,7 +314,7 @@ impl MultiHorizonLoader { min_size, "skipping file (too few snapshots for required_lookback_ticks + max_horizon)", ); - continue; + return Ok(None); } let mut labels_full: [Vec; N_HORIZONS] = Default::default(); let mut outcome_prof_long_full: [Vec; N_HORIZONS] = Default::default(); @@ -350,7 +356,7 @@ impl MultiHorizonLoader { pos_fraction = outcome.pos_fraction; } let regime_full = compute_regime_features(&snapshots); - files_loaded.push(LoadedFile { + Ok(Some(LoadedFile { snapshots, labels_full, outcome_prof_long_full, @@ -360,7 +366,17 @@ impl MultiHorizonLoader { sigma_k_full, pos_fraction, regime_full, - }); + })) + }; + + let files_loaded_results: Vec>> = + files.par_iter().map(load_one).collect(); + let mut files_loaded: Vec = Vec::with_capacity(files.len()); + for result in files_loaded_results { + match result? { + Some(lf) => files_loaded.push(lf), + None => {} // Already warned in load_one. + } } anyhow::ensure!( !files_loaded.is_empty(),