Skip to main content

torsh_optim/benchmarks/
core.rs

1//! Core benchmarking functionality and types
2//!
3//! This module provides the fundamental types and core benchmarking operations
4//! for evaluating optimizer performance. It includes basic performance tests,
5//! memory usage analysis, and convergence benchmarks.
6
7use crate::{Optimizer, OptimizerResult};
8use std::time::{Duration, Instant};
9use torsh_core::device::DeviceType;
10use torsh_tensor::{creation, Tensor};
11
12/// Statistical analysis of benchmark results
13#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
15pub struct StatisticalAnalysis {
16    /// Mean execution time
17    pub mean_time: Duration,
18    /// Median execution time
19    pub median_time: Duration,
20    /// Standard deviation of execution times
21    pub std_dev: Duration,
22    /// Confidence interval (lower, upper)
23    pub confidence_interval: (Duration, Duration),
24    /// Effect size for statistical significance
25    pub effect_size: Option<f64>,
26    /// P-value for statistical significance
27    pub p_value: Option<f64>,
28}
29
30/// Benchmark configuration
31///
32/// Controls the behavior of benchmark runs including iteration counts,
33/// time limits, and profiling options.
34#[derive(Debug, Clone)]
35pub struct BenchmarkConfig {
36    /// Number of benchmark iterations to run
37    pub num_iterations: usize,
38    /// Number of warmup iterations (excluded from timing)
39    pub warmup_iterations: usize,
40    /// Maximum time to run each benchmark (in seconds)
41    pub max_time_seconds: f32,
42    /// Device to run benchmarks on
43    pub device: DeviceType,
44    /// Whether to include memory profiling
45    pub profile_memory: bool,
46}
47
48impl Default for BenchmarkConfig {
49    fn default() -> Self {
50        Self {
51            num_iterations: 1000,
52            warmup_iterations: 100,
53            max_time_seconds: 60.0,
54            device: DeviceType::Cpu,
55            profile_memory: false,
56        }
57    }
58}
59
60/// Results from a single benchmark run
61///
62/// Contains comprehensive timing, convergence, and memory statistics
63/// from a benchmark execution.
64#[derive(Debug, Clone)]
65#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
66pub struct BenchmarkResult {
67    /// Benchmark name
68    pub name: String,
69    /// Number of iterations completed
70    pub iterations_completed: usize,
71    /// Total elapsed time
72    pub total_time: Duration,
73    /// Average time per iteration
74    pub avg_time_per_iteration: Duration,
75    /// Minimum time per iteration
76    pub min_time_per_iteration: Duration,
77    /// Maximum time per iteration
78    pub max_time_per_iteration: Duration,
79    /// Standard deviation of iteration times
80    pub time_std_dev: Duration,
81    /// Final convergence metric (if applicable)
82    pub final_loss: Option<f32>,
83    /// Memory usage statistics
84    pub memory_stats: Option<MemoryStats>,
85    /// Convergence rate (loss reduction per iteration)
86    pub convergence_rate: Option<f32>,
87}
88
89/// Memory usage statistics during benchmark execution
90#[derive(Debug, Clone)]
91#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
92pub struct MemoryStats {
93    /// Peak memory usage in bytes
94    pub peak_memory_bytes: usize,
95    /// Memory usage at start
96    pub initial_memory_bytes: usize,
97    /// Memory usage at end
98    pub final_memory_bytes: usize,
99    /// Average memory usage
100    pub avg_memory_bytes: usize,
101}
102
103/// Core optimizer benchmark suite
104///
105/// Provides fundamental benchmarking operations including step performance,
106/// convergence analysis, memory scaling, and sparse gradient handling.
107pub struct OptimizerBenchmarks {
108    config: BenchmarkConfig,
109}
110
111impl OptimizerBenchmarks {
112    /// Create a new benchmark suite with default configuration
113    pub fn new() -> Self {
114        Self {
115            config: BenchmarkConfig::default(),
116        }
117    }
118
119    /// Create a new benchmark suite with custom configuration
120    pub fn with_config(config: BenchmarkConfig) -> Self {
121        Self { config }
122    }
123
124    /// Get the current configuration
125    pub fn config(&self) -> &BenchmarkConfig {
126        &self.config
127    }
128
129    /// Update the configuration
130    pub fn set_config(&mut self, config: BenchmarkConfig) {
131        self.config = config;
132    }
133
134    /// Estimate memory usage for parameters and optimizer state
135    fn estimate_memory_usage<O: Optimizer>(params: &[Tensor], optimizer: &O) -> usize {
136        let mut total_bytes = 0;
137
138        // Calculate memory for parameters
139        for param in params {
140            let shape = param.shape();
141            let element_count = shape.dims().iter().product::<usize>();
142            // Assume f32 elements (4 bytes each)
143            total_bytes += element_count * 4;
144
145            // Add memory for gradients if present
146            if param.has_grad() {
147                total_bytes += element_count * 4;
148            }
149        }
150
151        // Estimate optimizer state memory
152        // This is a rough approximation based on common optimizer patterns
153        let state_multiplier = match optimizer.get_lr().len() {
154            // Simple optimizers like SGD might have minimal state
155            1 => 1.2,
156            // More complex optimizers like Adam have momentum and squared gradients
157            _ => 3.0,
158        };
159
160        let optimizer_state_bytes = (total_bytes as f64 * state_multiplier) as usize;
161        total_bytes + optimizer_state_bytes
162    }
163
164    /// Benchmark optimizer step performance
165    ///
166    /// Measures the raw computational performance of the optimizer's step operation
167    /// across multiple iterations to get reliable timing statistics.
168    ///
169    /// # Arguments
170    ///
171    /// * `optimizer` - Optimizer to benchmark
172    /// * `problem_size` - Number of parameters in the optimization problem
173    ///
174    /// # Returns
175    ///
176    /// Benchmark results with timing statistics
177    pub fn benchmark_step_performance<O: Optimizer>(
178        &self,
179        mut optimizer: O,
180        problem_size: usize,
181    ) -> OptimizerResult<BenchmarkResult> {
182        let mut params = creation::randn::<f32>(&[problem_size])?;
183        let mut iteration_times = Vec::new();
184
185        // Warmup iterations to stabilize timing
186        for _ in 0..self.config.warmup_iterations {
187            let grads = creation::randn::<f32>(&[problem_size])?;
188            params.set_grad(Some(grads));
189            optimizer.step()?;
190        }
191
192        let start_time = Instant::now();
193        let mut iterations_completed = 0;
194
195        // Main benchmark loop
196        for _i in 0..self.config.num_iterations {
197            // Check time limit
198            if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
199                break;
200            }
201
202            let grads = creation::randn::<f32>(&[problem_size])?;
203            params.set_grad(Some(grads));
204
205            let iter_start = Instant::now();
206            optimizer.step()?;
207            let iter_time = iter_start.elapsed();
208
209            iteration_times.push(iter_time);
210            iterations_completed += 1;
211        }
212
213        let total_time = start_time.elapsed();
214
215        // Calculate statistics
216        let avg_time = total_time / iterations_completed as u32;
217        let min_time = iteration_times
218            .iter()
219            .min()
220            .copied()
221            .unwrap_or(Duration::ZERO);
222        let max_time = iteration_times
223            .iter()
224            .max()
225            .copied()
226            .unwrap_or(Duration::ZERO);
227
228        // Calculate standard deviation
229        let mean_nanos = avg_time.as_nanos() as f64;
230        let variance = iteration_times
231            .iter()
232            .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
233            .sum::<f64>()
234            / iterations_completed as f64;
235        let std_dev = Duration::from_nanos(variance.sqrt() as u64);
236
237        Ok(BenchmarkResult {
238            name: format!("step_performance_size_{}", problem_size),
239            iterations_completed,
240            total_time,
241            avg_time_per_iteration: avg_time,
242            min_time_per_iteration: min_time,
243            max_time_per_iteration: max_time,
244            time_std_dev: std_dev,
245            final_loss: None,
246            memory_stats: None,
247            convergence_rate: None,
248        })
249    }
250
251    /// Benchmark convergence on quadratic function
252    ///
253    /// Tests how quickly the optimizer converges on a simple quadratic optimization
254    /// problem. This provides insight into convergence behavior and rate.
255    ///
256    /// # Arguments
257    ///
258    /// * `optimizer` - Optimizer to benchmark
259    /// * `dimension` - Dimensionality of the optimization problem
260    ///
261    /// # Returns
262    ///
263    /// Benchmark results with convergence metrics
264    pub fn benchmark_quadratic_convergence<O: Optimizer>(
265        &self,
266        mut optimizer: O,
267        dimension: usize,
268    ) -> OptimizerResult<BenchmarkResult> {
269        let mut params = creation::randn::<f32>(&[dimension])?;
270        let target = creation::zeros::<f32>(&[dimension])?;
271
272        let mut losses = Vec::new();
273        let mut iteration_times = Vec::new();
274
275        // Initial loss: ||params - target||^2
276        let initial_loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
277        losses.push(initial_loss);
278
279        let start_time = Instant::now();
280        let mut iterations_completed = 0;
281
282        for _i in 0..self.config.num_iterations {
283            // Check time limit
284            if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
285                break;
286            }
287
288            // Compute gradients for quadratic loss: grad = 2 * (params - target)
289            let grads = params.sub(&target)?.mul_scalar(2.0)?;
290
291            let iter_start = Instant::now();
292            params.set_grad(Some(grads));
293            optimizer.step()?;
294            let iter_time = iter_start.elapsed();
295
296            iteration_times.push(iter_time);
297
298            // Compute loss
299            let loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
300            losses.push(loss);
301
302            iterations_completed += 1;
303
304            // Early stopping if converged
305            if loss < 1e-8 {
306                break;
307            }
308        }
309
310        let total_time = start_time.elapsed();
311        let final_loss = losses.last().copied().unwrap_or(f32::INFINITY);
312
313        // Calculate convergence rate (log reduction per iteration)
314        let convergence_rate = if losses.len() > 1 {
315            let log_reduction = (initial_loss.ln() - final_loss.ln()).max(0.0);
316            Some(log_reduction / iterations_completed as f32)
317        } else {
318            None
319        };
320
321        // Calculate timing statistics
322        let avg_time = total_time / iterations_completed as u32;
323        let min_time = iteration_times
324            .iter()
325            .min()
326            .copied()
327            .unwrap_or(Duration::ZERO);
328        let max_time = iteration_times
329            .iter()
330            .max()
331            .copied()
332            .unwrap_or(Duration::ZERO);
333
334        let mean_nanos = avg_time.as_nanos() as f64;
335        let variance = iteration_times
336            .iter()
337            .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
338            .sum::<f64>()
339            / iterations_completed as f64;
340        let std_dev = Duration::from_nanos(variance.sqrt() as u64);
341
342        Ok(BenchmarkResult {
343            name: format!("quadratic_convergence_dim_{}", dimension),
344            iterations_completed,
345            total_time,
346            avg_time_per_iteration: avg_time,
347            min_time_per_iteration: min_time,
348            max_time_per_iteration: max_time,
349            time_std_dev: std_dev,
350            final_loss: Some(final_loss),
351            memory_stats: None,
352            convergence_rate,
353        })
354    }
355
356    /// Benchmark sparse gradient handling
357    ///
358    /// Tests optimizer performance when dealing with sparse gradients,
359    /// which is common in many machine learning scenarios.
360    ///
361    /// # Arguments
362    ///
363    /// * `optimizer` - Optimizer to benchmark
364    /// * `total_params` - Total number of parameters
365    /// * `sparsity` - Fraction of gradients that are non-zero (0.0 to 1.0)
366    ///
367    /// # Returns
368    ///
369    /// Benchmark results for sparse gradient performance
370    pub fn benchmark_sparse_gradients<O: Optimizer>(
371        &self,
372        mut optimizer: O,
373        total_params: usize,
374        sparsity: f32,
375    ) -> OptimizerResult<BenchmarkResult> {
376        let mut params = creation::randn::<f32>(&[total_params])?;
377
378        let mut iteration_times = Vec::new();
379
380        // Warmup with sparse gradients
381        for _ in 0..self.config.warmup_iterations {
382            let mut grads = creation::zeros::<f32>(&[total_params])?;
383
384            // Set sparse gradients
385            for i in 0..total_params {
386                if (i as f32 / total_params as f32) < sparsity {
387                    let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
388                    grads.set(&[i], grad_val)?;
389                }
390            }
391
392            params.set_grad(Some(grads));
393            optimizer.step()?;
394        }
395
396        let start_time = Instant::now();
397        let mut iterations_completed = 0;
398
399        for _ in 0..self.config.num_iterations {
400            if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
401                break;
402            }
403
404            let mut grads = creation::zeros::<f32>(&[total_params])?;
405
406            // Set sparse gradients
407            for i in 0..total_params {
408                if (i as f32 / total_params as f32) < sparsity {
409                    let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
410                    grads.set(&[i], grad_val)?;
411                }
412            }
413
414            let iter_start = Instant::now();
415            params.set_grad(Some(grads));
416            optimizer.step()?;
417            let iter_time = iter_start.elapsed();
418
419            iteration_times.push(iter_time);
420            iterations_completed += 1;
421        }
422
423        let total_time = start_time.elapsed();
424        let avg_time = total_time / iterations_completed as u32;
425        let min_time = iteration_times
426            .iter()
427            .min()
428            .copied()
429            .unwrap_or(Duration::ZERO);
430        let max_time = iteration_times
431            .iter()
432            .max()
433            .copied()
434            .unwrap_or(Duration::ZERO);
435
436        let mean_nanos = avg_time.as_nanos() as f64;
437        let variance = iteration_times
438            .iter()
439            .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
440            .sum::<f64>()
441            / iterations_completed as f64;
442        let std_dev = Duration::from_nanos(variance.sqrt() as u64);
443
444        Ok(BenchmarkResult {
445            name: format!(
446                "sparse_gradients_params_{}_sparsity_{:.2}",
447                total_params, sparsity
448            ),
449            iterations_completed,
450            total_time,
451            avg_time_per_iteration: avg_time,
452            min_time_per_iteration: min_time,
453            max_time_per_iteration: max_time,
454            time_std_dev: std_dev,
455            final_loss: None,
456            memory_stats: None,
457            convergence_rate: None,
458        })
459    }
460
461    /// Run a comprehensive set of core benchmarks
462    ///
463    /// Executes multiple benchmark scenarios to provide a complete performance
464    /// profile of the optimizer including step performance, convergence, and
465    /// sparse gradient handling.
466    ///
467    /// # Arguments
468    ///
469    /// * `optimizer` - Optimizer to benchmark (must be cloneable)
470    ///
471    /// # Returns
472    ///
473    /// Vector of benchmark results covering different scenarios
474    pub fn run_comprehensive_benchmarks<O: Optimizer + Clone>(
475        &self,
476        optimizer: O,
477    ) -> OptimizerResult<Vec<BenchmarkResult>> {
478        let mut results = Vec::new();
479
480        // Step performance benchmarks for different problem sizes
481        for &size in &[100, 1000, 10000] {
482            results.push(self.benchmark_step_performance(optimizer.clone(), size)?);
483        }
484
485        // Convergence benchmarks
486        for &dim in &[10, 100, 1000] {
487            results.push(self.benchmark_quadratic_convergence(optimizer.clone(), dim)?);
488        }
489
490        // Sparse gradient benchmarks
491        for &sparsity in &[0.1, 0.01, 0.001] {
492            results.push(self.benchmark_sparse_gradients(optimizer.clone(), 10000, sparsity)?);
493        }
494
495        Ok(results)
496    }
497
498    /// Print benchmark results in a formatted table
499    ///
500    /// Displays benchmark results in an easy-to-read tabular format
501    /// with timing, convergence, and performance metrics.
502    pub fn print_results(&self, results: &[BenchmarkResult]) {
503        println!("\n{:=<100}", "");
504        println!("{:^100}", "OPTIMIZER BENCHMARK RESULTS");
505        println!("{:=<100}", "");
506
507        println!(
508            "{:<40} {:>12} {:>12} {:>12} {:>12} {:>10}",
509            "Benchmark", "Iterations", "Total Time", "Avg Time", "Min Time", "Max Time"
510        );
511        println!("{:-<100}", "");
512
513        for result in results {
514            println!(
515                "{:<40} {:>12} {:>12.3?} {:>12.3?} {:>12.3?} {:>12.3?}",
516                result.name,
517                result.iterations_completed,
518                result.total_time,
519                result.avg_time_per_iteration,
520                result.min_time_per_iteration,
521                result.max_time_per_iteration
522            );
523
524            if let Some(loss) = result.final_loss {
525                println!("{:<40} Final Loss: {:.6e}", "", loss);
526            }
527
528            if let Some(rate) = result.convergence_rate {
529                println!("{:<40} Convergence Rate: {:.6e}", "", rate);
530            }
531        }
532
533        println!("{:=<100}", "");
534    }
535}
536
537impl Default for OptimizerBenchmarks {
538    fn default() -> Self {
539        Self::new()
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn test_benchmark_config_default() {
549        let config = BenchmarkConfig::default();
550        assert_eq!(config.num_iterations, 1000);
551        assert_eq!(config.warmup_iterations, 100);
552        assert_eq!(config.max_time_seconds, 60.0);
553    }
554
555    #[test]
556    fn test_benchmark_result_creation() {
557        let result = BenchmarkResult {
558            name: "test_benchmark".to_string(),
559            iterations_completed: 100,
560            total_time: Duration::from_secs(1),
561            avg_time_per_iteration: Duration::from_millis(10),
562            min_time_per_iteration: Duration::from_millis(5),
563            max_time_per_iteration: Duration::from_millis(15),
564            time_std_dev: Duration::from_millis(2),
565            final_loss: Some(0.1),
566            memory_stats: None,
567            convergence_rate: Some(0.05),
568        };
569
570        assert_eq!(result.name, "test_benchmark");
571        assert_eq!(result.iterations_completed, 100);
572        assert_eq!(result.final_loss, Some(0.1));
573    }
574
575    #[test]
576    fn test_memory_stats() {
577        let stats = MemoryStats {
578            peak_memory_bytes: 1000,
579            initial_memory_bytes: 500,
580            final_memory_bytes: 800,
581            avg_memory_bytes: 750,
582        };
583
584        assert_eq!(stats.peak_memory_bytes, 1000);
585        assert_eq!(stats.avg_memory_bytes, 750);
586    }
587}