use crate::{OptimizerError, OptimizerResult};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use torsh_tensor::{
creation::{randn, zeros},
Tensor,
};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StressTestConfig {
pub num_steps: usize,
pub param_size: Vec<usize>,
pub num_params: usize,
pub gradient_scale: f32,
pub test_edge_cases: bool,
pub max_execution_time: Duration,
pub track_memory: bool,
}
impl Default for StressTestConfig {
fn default() -> Self {
Self {
num_steps: 1000,
param_size: vec![100, 100],
num_params: 10,
gradient_scale: 1.0,
test_edge_cases: true,
max_execution_time: Duration::from_secs(30),
track_memory: true,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StressTestResult {
pub passed: bool,
pub execution_time: Duration,
pub avg_step_time: Duration,
pub memory_stats: MemoryStats,
pub performance_metrics: HashMap<String, f32>,
pub errors: Vec<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub peak_memory_mb: f32,
pub avg_memory_mb: f32,
pub memory_growth_rate: f32,
}
impl Default for MemoryStats {
fn default() -> Self {
Self {
peak_memory_mb: 0.0,
avg_memory_mb: 0.0,
memory_growth_rate: 0.0,
}
}
}
#[allow(dead_code)]
pub struct OptimizerStressTester {
config: StressTestConfig,
}
#[allow(dead_code)]
impl OptimizerStressTester {
pub fn new(config: StressTestConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(StressTestConfig::default())
}
pub fn run_stress_test<O>(&self, mut optimizer: O) -> OptimizerResult<StressTestResult>
where
O: crate::Optimizer,
{
let start_time = Instant::now();
let mut errors = Vec::new();
let mut step_times = Vec::new();
let mut memory_measurements = Vec::new();
let mut params = Vec::new();
for i in 0..self.config.num_params {
let param = Arc::new(RwLock::new(randn::<f32>(&self.config.param_size).map_err(
|e| {
OptimizerError::InvalidParameter(format!("Failed to create param {}: {}", i, e))
},
)?));
params.push(param);
}
for step in 0..self.config.num_steps {
let step_start = Instant::now();
for (i, param) in params.iter().enumerate() {
let gradient = if self.config.test_edge_cases && step % 100 == 50 {
self.create_extreme_gradient(&self.config.param_size, step)?
} else {
randn::<f32>(&self.config.param_size)
.map_err(|e| {
OptimizerError::InvalidParameter(format!(
"Failed to create gradient for param {}: {}",
i, e
))
})?
.mul_scalar(self.config.gradient_scale)
.map_err(|e| {
OptimizerError::InvalidParameter(format!(
"Failed to scale gradient: {}",
e
))
})?
};
param.write().set_grad(Some(gradient));
}
match optimizer.step() {
Ok(_) => {}
Err(e) => {
errors.push(format!("Step {}: {}", step, e));
if errors.len() > 10 {
break; }
}
}
let step_duration = step_start.elapsed();
step_times.push(step_duration);
if self.config.track_memory && step % 10 == 0 {
let estimated_memory = self.estimate_memory_usage(¶ms);
memory_measurements.push(estimated_memory);
}
if start_time.elapsed() > self.config.max_execution_time {
errors.push("Test exceeded maximum execution time".to_string());
break;
}
}
let total_time = start_time.elapsed();
let avg_step_time = if !step_times.is_empty() {
step_times.iter().sum::<Duration>() / step_times.len() as u32
} else {
Duration::from_nanos(0)
};
let memory_stats = if self.config.track_memory && !memory_measurements.is_empty() {
let peak_memory = memory_measurements
.iter()
.fold(0.0f32, |acc, x| acc.max(*x));
let avg_memory =
memory_measurements.iter().sum::<f32>() / memory_measurements.len() as f32;
let growth_rate = if memory_measurements.len() > 1 {
(memory_measurements[memory_measurements.len() - 1] - memory_measurements[0])
/ memory_measurements.len() as f32
} else {
0.0
};
MemoryStats {
peak_memory_mb: peak_memory,
avg_memory_mb: avg_memory,
memory_growth_rate: growth_rate,
}
} else {
MemoryStats::default()
};
let mut performance_metrics = HashMap::new();
performance_metrics.insert(
"steps_per_second".to_string(),
self.config.num_steps as f32 / total_time.as_secs_f32(),
);
performance_metrics.insert(
"error_rate".to_string(),
errors.len() as f32 / self.config.num_steps as f32,
);
if !step_times.is_empty() {
performance_metrics.insert(
"avg_step_time_ms".to_string(),
avg_step_time.as_millis() as f32,
);
performance_metrics.insert(
"max_step_time_ms".to_string(),
step_times
.iter()
.max()
.expect("step_times is non-empty")
.as_millis() as f32,
);
}
let passed = errors.is_empty() && total_time <= self.config.max_execution_time;
Ok(StressTestResult {
passed,
execution_time: total_time,
avg_step_time,
memory_stats,
performance_metrics,
errors,
})
}
pub fn test_extreme_conditions<O>(&self, mut optimizer: O) -> OptimizerResult<StressTestResult>
where
O: crate::Optimizer,
{
let start_time = Instant::now();
let mut errors = Vec::new();
let param = Arc::new(RwLock::new(zeros(&[10, 10])?));
let test_cases = vec![
(1e10, "Very large gradients"),
(1e-10, "Very small gradients"),
(0.0, "Zero gradients"),
(f32::INFINITY, "Infinite gradients"),
(f32::NAN, "NaN gradients"),
];
let test_cases_len = test_cases.len();
for (magnitude, description) in test_cases {
let mut grad_data = vec![magnitude; 100];
if magnitude.is_nan() {
grad_data = vec![f32::NAN; 100];
}
let grad_tensor = Tensor::from_vec(grad_data, &[10, 10]).map_err(|e| {
OptimizerError::InvalidParameter(format!("Failed to create test gradient: {}", e))
})?;
param.write().set_grad(Some(grad_tensor));
match optimizer.step() {
Ok(_) => {
let param_values = param.read().to_vec().map_err(|e| {
OptimizerError::InvalidParameter(format!(
"Failed to read parameter values: {}",
e
))
})?;
let has_invalid = param_values.iter().any(|&x| x.is_nan() || x.is_infinite());
if has_invalid {
errors.push(format!("{}: Parameters became invalid", description));
}
}
Err(e) => {
if !matches!(magnitude, val if val.is_infinite() || val.is_nan()) {
errors.push(format!("{}: Unexpected error: {}", description, e));
}
}
}
}
let total_time = start_time.elapsed();
let passed = errors.len() < test_cases_len / 2;
let mut performance_metrics = HashMap::new();
performance_metrics.insert(
"extreme_case_success_rate".to_string(),
(test_cases_len - errors.len()) as f32 / test_cases_len as f32,
);
Ok(StressTestResult {
passed,
execution_time: total_time,
avg_step_time: total_time / test_cases_len as u32,
memory_stats: MemoryStats::default(),
performance_metrics,
errors,
})
}
fn create_extreme_gradient(&self, shape: &[usize], step: usize) -> OptimizerResult<Tensor> {
let total_elements: usize = shape.iter().product();
let gradient_data = match step % 4 {
0 => vec![1e6; total_elements], 1 => vec![1e-6; total_elements], 2 => vec![0.0; total_elements], _ => {
(0..total_elements)
.map(|i| if i % 2 == 0 { 1e3 } else { -1e3 })
.collect()
}
};
Tensor::from_vec(gradient_data, shape).map_err(|e| {
OptimizerError::InvalidParameter(format!("Failed to create extreme gradient: {}", e))
})
}
fn estimate_memory_usage(&self, params: &[Arc<RwLock<Tensor>>]) -> f32 {
let mut total_elements = 0;
for param in params {
if let Some(param_read) = param.try_read() {
let shape = param_read.shape();
total_elements += shape.dims().iter().product::<usize>();
}
}
(total_elements * 4) as f32 / (1024.0 * 1024.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{adam::Adam, sgd::SGD};
#[test]
fn test_stress_tester_creation() -> OptimizerResult<()> {
let config = StressTestConfig::default();
let _tester = OptimizerStressTester::new(config);
Ok(())
}
#[test]
fn test_basic_stress_test() -> OptimizerResult<()> {
let mut config = StressTestConfig::default();
config.num_steps = 10; config.num_params = 2;
config.param_size = vec![5, 5];
let tester = OptimizerStressTester::new(config);
let param = Arc::new(RwLock::new(randn::<f32>(&[5, 5])?));
let optimizer = SGD::new(vec![param], 0.01, None, None, None, false);
let result = tester.run_stress_test(optimizer)?;
assert!(result.execution_time.as_secs() < 5);
assert!(result.performance_metrics.contains_key("steps_per_second"));
Ok(())
}
#[test]
fn test_extreme_conditions() -> OptimizerResult<()> {
let config = StressTestConfig::default();
let tester = OptimizerStressTester::new(config);
let param = Arc::new(RwLock::new(zeros(&[10, 10])?));
let optimizer = Adam::new(vec![param], Some(0.01), None, None, None, false);
let result = tester.test_extreme_conditions(optimizer)?;
assert!(result
.performance_metrics
.contains_key("extreme_case_success_rate"));
Ok(())
}
#[test]
fn test_memory_estimation() -> OptimizerResult<()> {
let tester = OptimizerStressTester::default();
let params = vec![
Arc::new(RwLock::new(zeros(&[100, 100])?)),
Arc::new(RwLock::new(zeros(&[50, 50])?)),
];
let memory_usage = tester.estimate_memory_usage(¶ms);
assert!(memory_usage > 0.04); assert!(memory_usage < 1.0); Ok(())
}
}