use crate::{Optimizer, OptimizerResult};
use std::time::{Duration, Instant};
use torsh_core::device::DeviceType;
use torsh_tensor::{creation, Tensor};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct StatisticalAnalysis {
pub mean_time: Duration,
pub median_time: Duration,
pub std_dev: Duration,
pub confidence_interval: (Duration, Duration),
pub effect_size: Option<f64>,
pub p_value: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct BenchmarkConfig {
pub num_iterations: usize,
pub warmup_iterations: usize,
pub max_time_seconds: f32,
pub device: DeviceType,
pub profile_memory: bool,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
num_iterations: 1000,
warmup_iterations: 100,
max_time_seconds: 60.0,
device: DeviceType::Cpu,
profile_memory: false,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct BenchmarkResult {
pub name: String,
pub iterations_completed: usize,
pub total_time: Duration,
pub avg_time_per_iteration: Duration,
pub min_time_per_iteration: Duration,
pub max_time_per_iteration: Duration,
pub time_std_dev: Duration,
pub final_loss: Option<f32>,
pub memory_stats: Option<MemoryStats>,
pub convergence_rate: Option<f32>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct MemoryStats {
pub peak_memory_bytes: usize,
pub initial_memory_bytes: usize,
pub final_memory_bytes: usize,
pub avg_memory_bytes: usize,
}
pub struct OptimizerBenchmarks {
config: BenchmarkConfig,
}
impl OptimizerBenchmarks {
pub fn new() -> Self {
Self {
config: BenchmarkConfig::default(),
}
}
pub fn with_config(config: BenchmarkConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &BenchmarkConfig {
&self.config
}
pub fn set_config(&mut self, config: BenchmarkConfig) {
self.config = config;
}
fn estimate_memory_usage<O: Optimizer>(params: &[Tensor], optimizer: &O) -> usize {
let mut total_bytes = 0;
for param in params {
let shape = param.shape();
let element_count = shape.dims().iter().product::<usize>();
total_bytes += element_count * 4;
if param.has_grad() {
total_bytes += element_count * 4;
}
}
let state_multiplier = match optimizer.get_lr().len() {
1 => 1.2,
_ => 3.0,
};
let optimizer_state_bytes = (total_bytes as f64 * state_multiplier) as usize;
total_bytes + optimizer_state_bytes
}
pub fn benchmark_step_performance<O: Optimizer>(
&self,
mut optimizer: O,
problem_size: usize,
) -> OptimizerResult<BenchmarkResult> {
let mut params = creation::randn::<f32>(&[problem_size])?;
let mut iteration_times = Vec::new();
for _ in 0..self.config.warmup_iterations {
let grads = creation::randn::<f32>(&[problem_size])?;
params.set_grad(Some(grads));
optimizer.step()?;
}
let start_time = Instant::now();
let mut iterations_completed = 0;
for _i in 0..self.config.num_iterations {
if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
break;
}
let grads = creation::randn::<f32>(&[problem_size])?;
params.set_grad(Some(grads));
let iter_start = Instant::now();
optimizer.step()?;
let iter_time = iter_start.elapsed();
iteration_times.push(iter_time);
iterations_completed += 1;
}
let total_time = start_time.elapsed();
let avg_time = total_time / iterations_completed as u32;
let min_time = iteration_times
.iter()
.min()
.copied()
.unwrap_or(Duration::ZERO);
let max_time = iteration_times
.iter()
.max()
.copied()
.unwrap_or(Duration::ZERO);
let mean_nanos = avg_time.as_nanos() as f64;
let variance = iteration_times
.iter()
.map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
.sum::<f64>()
/ iterations_completed as f64;
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
Ok(BenchmarkResult {
name: format!("step_performance_size_{}", problem_size),
iterations_completed,
total_time,
avg_time_per_iteration: avg_time,
min_time_per_iteration: min_time,
max_time_per_iteration: max_time,
time_std_dev: std_dev,
final_loss: None,
memory_stats: None,
convergence_rate: None,
})
}
pub fn benchmark_quadratic_convergence<O: Optimizer>(
&self,
mut optimizer: O,
dimension: usize,
) -> OptimizerResult<BenchmarkResult> {
let mut params = creation::randn::<f32>(&[dimension])?;
let target = creation::zeros::<f32>(&[dimension])?;
let mut losses = Vec::new();
let mut iteration_times = Vec::new();
let initial_loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
losses.push(initial_loss);
let start_time = Instant::now();
let mut iterations_completed = 0;
for _i in 0..self.config.num_iterations {
if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
break;
}
let grads = params.sub(&target)?.mul_scalar(2.0)?;
let iter_start = Instant::now();
params.set_grad(Some(grads));
optimizer.step()?;
let iter_time = iter_start.elapsed();
iteration_times.push(iter_time);
let loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
losses.push(loss);
iterations_completed += 1;
if loss < 1e-8 {
break;
}
}
let total_time = start_time.elapsed();
let final_loss = losses.last().copied().unwrap_or(f32::INFINITY);
let convergence_rate = if losses.len() > 1 {
let log_reduction = (initial_loss.ln() - final_loss.ln()).max(0.0);
Some(log_reduction / iterations_completed as f32)
} else {
None
};
let avg_time = total_time / iterations_completed as u32;
let min_time = iteration_times
.iter()
.min()
.copied()
.unwrap_or(Duration::ZERO);
let max_time = iteration_times
.iter()
.max()
.copied()
.unwrap_or(Duration::ZERO);
let mean_nanos = avg_time.as_nanos() as f64;
let variance = iteration_times
.iter()
.map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
.sum::<f64>()
/ iterations_completed as f64;
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
Ok(BenchmarkResult {
name: format!("quadratic_convergence_dim_{}", dimension),
iterations_completed,
total_time,
avg_time_per_iteration: avg_time,
min_time_per_iteration: min_time,
max_time_per_iteration: max_time,
time_std_dev: std_dev,
final_loss: Some(final_loss),
memory_stats: None,
convergence_rate,
})
}
pub fn benchmark_sparse_gradients<O: Optimizer>(
&self,
mut optimizer: O,
total_params: usize,
sparsity: f32,
) -> OptimizerResult<BenchmarkResult> {
let mut params = creation::randn::<f32>(&[total_params])?;
let mut iteration_times = Vec::new();
for _ in 0..self.config.warmup_iterations {
let mut grads = creation::zeros::<f32>(&[total_params])?;
for i in 0..total_params {
if (i as f32 / total_params as f32) < sparsity {
let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
grads.set(&[i], grad_val)?;
}
}
params.set_grad(Some(grads));
optimizer.step()?;
}
let start_time = Instant::now();
let mut iterations_completed = 0;
for _ in 0..self.config.num_iterations {
if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
break;
}
let mut grads = creation::zeros::<f32>(&[total_params])?;
for i in 0..total_params {
if (i as f32 / total_params as f32) < sparsity {
let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
grads.set(&[i], grad_val)?;
}
}
let iter_start = Instant::now();
params.set_grad(Some(grads));
optimizer.step()?;
let iter_time = iter_start.elapsed();
iteration_times.push(iter_time);
iterations_completed += 1;
}
let total_time = start_time.elapsed();
let avg_time = total_time / iterations_completed as u32;
let min_time = iteration_times
.iter()
.min()
.copied()
.unwrap_or(Duration::ZERO);
let max_time = iteration_times
.iter()
.max()
.copied()
.unwrap_or(Duration::ZERO);
let mean_nanos = avg_time.as_nanos() as f64;
let variance = iteration_times
.iter()
.map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
.sum::<f64>()
/ iterations_completed as f64;
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
Ok(BenchmarkResult {
name: format!(
"sparse_gradients_params_{}_sparsity_{:.2}",
total_params, sparsity
),
iterations_completed,
total_time,
avg_time_per_iteration: avg_time,
min_time_per_iteration: min_time,
max_time_per_iteration: max_time,
time_std_dev: std_dev,
final_loss: None,
memory_stats: None,
convergence_rate: None,
})
}
pub fn run_comprehensive_benchmarks<O: Optimizer + Clone>(
&self,
optimizer: O,
) -> OptimizerResult<Vec<BenchmarkResult>> {
let mut results = Vec::new();
for &size in &[100, 1000, 10000] {
results.push(self.benchmark_step_performance(optimizer.clone(), size)?);
}
for &dim in &[10, 100, 1000] {
results.push(self.benchmark_quadratic_convergence(optimizer.clone(), dim)?);
}
for &sparsity in &[0.1, 0.01, 0.001] {
results.push(self.benchmark_sparse_gradients(optimizer.clone(), 10000, sparsity)?);
}
Ok(results)
}
pub fn print_results(&self, results: &[BenchmarkResult]) {
println!("\n{:=<100}", "");
println!("{:^100}", "OPTIMIZER BENCHMARK RESULTS");
println!("{:=<100}", "");
println!(
"{:<40} {:>12} {:>12} {:>12} {:>12} {:>10}",
"Benchmark", "Iterations", "Total Time", "Avg Time", "Min Time", "Max Time"
);
println!("{:-<100}", "");
for result in results {
println!(
"{:<40} {:>12} {:>12.3?} {:>12.3?} {:>12.3?} {:>12.3?}",
result.name,
result.iterations_completed,
result.total_time,
result.avg_time_per_iteration,
result.min_time_per_iteration,
result.max_time_per_iteration
);
if let Some(loss) = result.final_loss {
println!("{:<40} Final Loss: {:.6e}", "", loss);
}
if let Some(rate) = result.convergence_rate {
println!("{:<40} Convergence Rate: {:.6e}", "", rate);
}
}
println!("{:=<100}", "");
}
}
impl Default for OptimizerBenchmarks {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benchmark_config_default() {
let config = BenchmarkConfig::default();
assert_eq!(config.num_iterations, 1000);
assert_eq!(config.warmup_iterations, 100);
assert_eq!(config.max_time_seconds, 60.0);
}
#[test]
fn test_benchmark_result_creation() {
let result = BenchmarkResult {
name: "test_benchmark".to_string(),
iterations_completed: 100,
total_time: Duration::from_secs(1),
avg_time_per_iteration: Duration::from_millis(10),
min_time_per_iteration: Duration::from_millis(5),
max_time_per_iteration: Duration::from_millis(15),
time_std_dev: Duration::from_millis(2),
final_loss: Some(0.1),
memory_stats: None,
convergence_rate: Some(0.05),
};
assert_eq!(result.name, "test_benchmark");
assert_eq!(result.iterations_completed, 100);
assert_eq!(result.final_loss, Some(0.1));
}
#[test]
fn test_memory_stats() {
let stats = MemoryStats {
peak_memory_bytes: 1000,
initial_memory_bytes: 500,
final_memory_bytes: 800,
avg_memory_bytes: 750,
};
assert_eq!(stats.peak_memory_bytes, 1000);
assert_eq!(stats.avg_memory_bytes, 750);
}
}