refactor: replace rand 0.10 with fastrand 2.3
fastrand is smaller, faster, and has no dependencies. Add rng_util helper for f64 range generation since fastrand lacks a built-in equivalent. Migrate all samplers, KDE modules, and fANOVA to use fastrand's concrete Rng type instead of rand's trait-based generics.
This commit is contained in:
@@ -116,14 +116,13 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
|
||||
use super::{FixedGamma, GammaStrategy};
|
||||
use crate::distribution::Distribution;
|
||||
use crate::error::Result;
|
||||
use crate::param::ParamValue;
|
||||
use crate::parameter::ParamId;
|
||||
use crate::rng_util;
|
||||
use crate::sampler::{CompletedTrial, PendingTrial, Sampler};
|
||||
|
||||
/// Strategy for imputing objective values for pending/running trials during parallel optimization.
|
||||
@@ -189,7 +188,7 @@ pub struct MultivariateTpeSampler {
|
||||
/// Strategy for imputing objective values for pending trials in parallel optimization.
|
||||
constant_liar: ConstantLiarStrategy,
|
||||
/// Thread-safe RNG for sampling.
|
||||
rng: Mutex<StdRng>,
|
||||
rng: Mutex<fastrand::Rng>,
|
||||
/// Cache for joint samples to maintain consistency across parameters within the same trial.
|
||||
/// The tuple contains (`trial_id`, cached joint sample).
|
||||
joint_sample_cache: Mutex<Option<(u64, HashMap<ParamId, ParamValue>)>>,
|
||||
@@ -219,7 +218,7 @@ impl MultivariateTpeSampler {
|
||||
n_ei_candidates: 24,
|
||||
group: false,
|
||||
constant_liar: ConstantLiarStrategy::None,
|
||||
rng: Mutex::new(rand::make_rng()),
|
||||
rng: Mutex::new(fastrand::Rng::new()),
|
||||
joint_sample_cache: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
@@ -695,7 +694,7 @@ impl MultivariateTpeSampler {
|
||||
|
||||
// Generate candidates from the good distribution
|
||||
let candidates: Vec<Vec<f64>> = (0..self.n_ei_candidates)
|
||||
.map(|_| good_kde.sample(&mut *rng))
|
||||
.map(|_| good_kde.sample(&mut rng))
|
||||
.collect();
|
||||
|
||||
// Compute log(l(x)) - log(g(x)) for each candidate
|
||||
@@ -731,7 +730,7 @@ impl MultivariateTpeSampler {
|
||||
&self,
|
||||
good_kde: &crate::kde::MultivariateKDE,
|
||||
bad_kde: &crate::kde::MultivariateKDE,
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> Vec<f64> {
|
||||
// Generate candidates from the good distribution
|
||||
let candidates: Vec<Vec<f64>> = (0..self.n_ei_candidates)
|
||||
@@ -769,7 +768,7 @@ impl MultivariateTpeSampler {
|
||||
fn sample_all_uniform(
|
||||
&self,
|
||||
search_space: &HashMap<ParamId, Distribution>,
|
||||
rng: &mut rand::rngs::StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> HashMap<ParamId, ParamValue> {
|
||||
search_space
|
||||
.iter()
|
||||
@@ -778,25 +777,22 @@ impl MultivariateTpeSampler {
|
||||
}
|
||||
|
||||
/// Samples a single parameter uniformly at random from its distribution.
|
||||
fn sample_uniform_single(
|
||||
distribution: &Distribution,
|
||||
rng: &mut rand::rngs::StdRng,
|
||||
) -> ParamValue {
|
||||
fn sample_uniform_single(distribution: &Distribution, rng: &mut fastrand::Rng) -> ParamValue {
|
||||
match distribution {
|
||||
Distribution::Float(d) => {
|
||||
let value = if d.log_scale {
|
||||
let log_low = d.low.ln();
|
||||
let log_high = d.high.ln();
|
||||
rng.random_range(log_low..=log_high).exp()
|
||||
rng_util::f64_range(rng, log_low, log_high).exp()
|
||||
} else if let Some(step) = d.step {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let n_steps = ((d.high - d.low) / step).floor() as i64;
|
||||
let k = rng.random_range(0..=n_steps);
|
||||
let k = rng.i64(0..=n_steps);
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let result = d.low + (k as f64) * step;
|
||||
result
|
||||
} else {
|
||||
rng.random_range(d.low..=d.high)
|
||||
rng_util::f64_range(rng, d.low, d.high)
|
||||
};
|
||||
ParamValue::Float(value)
|
||||
}
|
||||
@@ -806,20 +802,20 @@ impl MultivariateTpeSampler {
|
||||
let log_low = (d.low as f64).ln();
|
||||
let log_high = (d.high as f64).ln();
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let raw = rng.random_range(log_low..=log_high).exp().round() as i64;
|
||||
let raw = rng_util::f64_range(rng, log_low, log_high).exp().round() as i64;
|
||||
raw.clamp(d.low, d.high)
|
||||
} else if let Some(step) = d.step {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let n_steps = (d.high - d.low) / step;
|
||||
let k = rng.random_range(0..=n_steps);
|
||||
let k = rng.i64(0..=n_steps);
|
||||
d.low + k * step
|
||||
} else {
|
||||
rng.random_range(d.low..=d.high)
|
||||
rng.i64(d.low..=d.high)
|
||||
};
|
||||
ParamValue::Int(value)
|
||||
}
|
||||
Distribution::Categorical(d) => {
|
||||
let index = rng.random_range(0..d.n_choices);
|
||||
let index = rng.usize(0..d.n_choices);
|
||||
ParamValue::Categorical(index)
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1014,7 @@ impl MultivariateTpeSampler {
|
||||
&self,
|
||||
search_space: &HashMap<ParamId, Distribution>,
|
||||
history: &[CompletedTrial],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> HashMap<ParamId, ParamValue> {
|
||||
use super::IntersectionSearchSpace;
|
||||
use crate::kde::MultivariateKDE;
|
||||
@@ -1220,7 +1216,7 @@ impl MultivariateTpeSampler {
|
||||
_intersection: &HashMap<ParamId, Distribution>,
|
||||
history: &[CompletedTrial],
|
||||
result: &mut HashMap<ParamId, ParamValue>,
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) {
|
||||
// Identify parameters not in result (and not in intersection)
|
||||
let missing_params: Vec<(&ParamId, &Distribution)> = search_space
|
||||
@@ -1253,7 +1249,7 @@ impl MultivariateTpeSampler {
|
||||
distribution: &Distribution,
|
||||
good_trials: &[&CompletedTrial],
|
||||
bad_trials: &[&CompletedTrial],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> ParamValue {
|
||||
match distribution {
|
||||
Distribution::Float(d) => {
|
||||
@@ -1370,7 +1366,7 @@ impl MultivariateTpeSampler {
|
||||
step: Option<f64>,
|
||||
good_values: Vec<f64>,
|
||||
bad_values: Vec<f64>,
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> f64 {
|
||||
use crate::kde::KernelDensityEstimator;
|
||||
|
||||
@@ -1391,7 +1387,7 @@ impl MultivariateTpeSampler {
|
||||
|
||||
// If KDE construction fails, fall back to uniform sampling
|
||||
let (Ok(l_kde), Ok(g_kde)) = (l_kde, g_kde) else {
|
||||
return rng.random_range(low..=high);
|
||||
return rng_util::f64_range(rng, low, high);
|
||||
};
|
||||
|
||||
// Generate candidates from l(x) and select the one with best l(x)/g(x) ratio
|
||||
@@ -1455,7 +1451,7 @@ impl MultivariateTpeSampler {
|
||||
step: Option<i64>,
|
||||
good_values: &[i64],
|
||||
bad_values: &[i64],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> i64 {
|
||||
// Convert to floats for KDE
|
||||
let good_floats: Vec<f64> = good_values.iter().map(|&v| v as f64).collect();
|
||||
@@ -1518,7 +1514,7 @@ impl MultivariateTpeSampler {
|
||||
&self,
|
||||
search_space: &HashMap<ParamId, Distribution>,
|
||||
history: &[CompletedTrial],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> HashMap<ParamId, ParamValue> {
|
||||
// Split trials for independent sampling
|
||||
let (good_trials, bad_trials) = self.split_trials(&history.iter().collect::<Vec<_>>());
|
||||
@@ -1562,7 +1558,7 @@ impl MultivariateTpeSampler {
|
||||
n_choices: usize,
|
||||
good_indices: &[usize],
|
||||
bad_indices: &[usize],
|
||||
rng: &mut rand::rngs::StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> usize {
|
||||
// Count occurrences in good and bad groups
|
||||
let mut good_counts = vec![0usize; n_choices];
|
||||
@@ -1593,7 +1589,7 @@ impl MultivariateTpeSampler {
|
||||
|
||||
// Sample proportionally to weights
|
||||
let total_weight: f64 = weights.iter().sum();
|
||||
let threshold = rng.random::<f64>() * total_weight;
|
||||
let threshold = rng.f64() * total_weight;
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, &w) in weights.iter().enumerate() {
|
||||
@@ -2069,8 +2065,8 @@ impl MultivariateTpeSamplerBuilder {
|
||||
};
|
||||
|
||||
let rng = match self.seed {
|
||||
Some(s) => StdRng::seed_from_u64(s),
|
||||
None => rand::make_rng(),
|
||||
Some(s) => fastrand::Rng::with_seed(s),
|
||||
None => fastrand::Rng::new(),
|
||||
};
|
||||
|
||||
Ok(MultivariateTpeSampler {
|
||||
@@ -4854,8 +4850,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sample_tpe_categorical_basic() {
|
||||
use rand::SeedableRng;
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||
let mut rng = fastrand::Rng::with_seed(42);
|
||||
|
||||
// Category 0 is good (appears more in good trials)
|
||||
let good_indices = vec![0, 0, 0, 1];
|
||||
@@ -4890,8 +4885,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sample_tpe_categorical_laplace_smoothing() {
|
||||
use rand::SeedableRng;
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||
let mut rng = fastrand::Rng::with_seed(42);
|
||||
|
||||
// Category 2 never appears, but should still be sampled due to Laplace smoothing
|
||||
let good_indices = vec![0, 0, 1];
|
||||
@@ -4919,8 +4913,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sample_tpe_categorical_empty_good() {
|
||||
use rand::SeedableRng;
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||
let mut rng = fastrand::Rng::with_seed(42);
|
||||
|
||||
// Empty good group - all categories should have equal probability
|
||||
let good_indices: Vec<usize> = vec![];
|
||||
@@ -4945,8 +4938,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sample_tpe_categorical_all_indices_valid() {
|
||||
use rand::SeedableRng;
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||
let mut rng = fastrand::Rng::with_seed(42);
|
||||
|
||||
let n_choices = 4;
|
||||
let good_indices = vec![0, 1, 2, 3];
|
||||
|
||||
+20
-23
@@ -59,13 +59,12 @@ use core::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
|
||||
use crate::distribution::Distribution;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::kde::KernelDensityEstimator;
|
||||
use crate::param::ParamValue;
|
||||
use crate::rng_util;
|
||||
use crate::sampler::tpe::gamma::{FixedGamma, GammaStrategy};
|
||||
use crate::sampler::{CompletedTrial, Sampler};
|
||||
|
||||
@@ -131,7 +130,7 @@ pub struct TpeSampler {
|
||||
/// Optional fixed bandwidth for KDE. If None, uses Scott's rule.
|
||||
kde_bandwidth: Option<f64>,
|
||||
/// Thread-safe RNG for sampling.
|
||||
rng: Mutex<StdRng>,
|
||||
rng: Mutex<fastrand::Rng>,
|
||||
}
|
||||
|
||||
impl TpeSampler {
|
||||
@@ -149,7 +148,7 @@ impl TpeSampler {
|
||||
n_startup_trials: 10,
|
||||
n_ei_candidates: 24,
|
||||
kde_bandwidth: None,
|
||||
rng: Mutex::new(rand::make_rng()),
|
||||
rng: Mutex::new(fastrand::Rng::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,8 +249,8 @@ impl TpeSampler {
|
||||
}
|
||||
|
||||
let rng = match seed {
|
||||
Some(s) => StdRng::seed_from_u64(s),
|
||||
None => rand::make_rng(),
|
||||
Some(s) => fastrand::Rng::with_seed(s),
|
||||
None => fastrand::Rng::new(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -328,19 +327,19 @@ impl TpeSampler {
|
||||
clippy::cast_precision_loss,
|
||||
clippy::unused_self
|
||||
)]
|
||||
fn sample_uniform(&self, distribution: &Distribution, rng: &mut StdRng) -> ParamValue {
|
||||
fn sample_uniform(&self, distribution: &Distribution, rng: &mut fastrand::Rng) -> ParamValue {
|
||||
match distribution {
|
||||
Distribution::Float(d) => {
|
||||
let value = if d.log_scale {
|
||||
let log_low = d.low.ln();
|
||||
let log_high = d.high.ln();
|
||||
rng.random_range(log_low..=log_high).exp()
|
||||
rng_util::f64_range(rng, log_low, log_high).exp()
|
||||
} else if let Some(step) = d.step {
|
||||
let n_steps = ((d.high - d.low) / step).floor() as i64;
|
||||
let k = rng.random_range(0..=n_steps);
|
||||
let k = rng.i64(0..=n_steps);
|
||||
d.low + (k as f64) * step
|
||||
} else {
|
||||
rng.random_range(d.low..=d.high)
|
||||
rng_util::f64_range(rng, d.low, d.high)
|
||||
};
|
||||
ParamValue::Float(value)
|
||||
}
|
||||
@@ -348,20 +347,18 @@ impl TpeSampler {
|
||||
let value = if d.log_scale {
|
||||
let log_low = (d.low as f64).ln();
|
||||
let log_high = (d.high as f64).ln();
|
||||
let raw = rng.random_range(log_low..=log_high).exp().round() as i64;
|
||||
let raw = rng_util::f64_range(rng, log_low, log_high).exp().round() as i64;
|
||||
raw.clamp(d.low, d.high)
|
||||
} else if let Some(step) = d.step {
|
||||
let n_steps = (d.high - d.low) / step;
|
||||
let k = rng.random_range(0..=n_steps);
|
||||
let k = rng.i64(0..=n_steps);
|
||||
d.low + k * step
|
||||
} else {
|
||||
rng.random_range(d.low..=d.high)
|
||||
rng.i64(d.low..=d.high)
|
||||
};
|
||||
ParamValue::Int(value)
|
||||
}
|
||||
Distribution::Categorical(d) => {
|
||||
ParamValue::Categorical(rng.random_range(0..d.n_choices))
|
||||
}
|
||||
Distribution::Categorical(d) => ParamValue::Categorical(rng.usize(0..d.n_choices)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +372,7 @@ impl TpeSampler {
|
||||
step: Option<f64>,
|
||||
good_values: Vec<f64>,
|
||||
bad_values: Vec<f64>,
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> f64 {
|
||||
// Transform to internal space (log space if needed)
|
||||
let (internal_low, internal_high, good_internal, bad_internal) = if log_scale {
|
||||
@@ -400,7 +397,7 @@ impl TpeSampler {
|
||||
|
||||
// If KDE construction fails, fall back to uniform sampling
|
||||
let (Ok(l_kde), Ok(g_kde)) = (l_kde, g_kde) else {
|
||||
return rng.random_range(low..=high);
|
||||
return rng_util::f64_range(rng, low, high);
|
||||
};
|
||||
|
||||
// Generate candidates from l(x) and select the one with best l(x)/g(x) ratio
|
||||
@@ -464,7 +461,7 @@ impl TpeSampler {
|
||||
step: Option<i64>,
|
||||
good_values: &[i64],
|
||||
bad_values: &[i64],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> i64 {
|
||||
// Convert to floats for KDE
|
||||
let good_floats: Vec<f64> = good_values.iter().map(|&v| v as f64).collect();
|
||||
@@ -503,7 +500,7 @@ impl TpeSampler {
|
||||
n_choices: usize,
|
||||
good_indices: &[usize],
|
||||
bad_indices: &[usize],
|
||||
rng: &mut StdRng,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> usize {
|
||||
// Count occurrences in good and bad groups
|
||||
let mut good_counts = vec![0usize; n_choices];
|
||||
@@ -534,7 +531,7 @@ impl TpeSampler {
|
||||
|
||||
// Sample proportionally to weights
|
||||
let total_weight: f64 = weights.iter().sum();
|
||||
let threshold = rng.random::<f64>() * total_weight;
|
||||
let threshold = rng.f64() * total_weight;
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, &w) in weights.iter().enumerate() {
|
||||
@@ -856,8 +853,8 @@ impl TpeSamplerBuilder {
|
||||
}
|
||||
|
||||
let rng = match self.seed {
|
||||
Some(s) => StdRng::seed_from_u64(s),
|
||||
None => rand::make_rng(),
|
||||
Some(s) => fastrand::Rng::with_seed(s),
|
||||
None => fastrand::Rng::new(),
|
||||
};
|
||||
|
||||
Ok(TpeSampler {
|
||||
|
||||
Reference in New Issue
Block a user