Skip to main content

sklears_neural/
benchmarking.rs

1//! Comprehensive benchmarking framework for neural network performance evaluation.
2//!
3//! This module provides tools for benchmarking neural network training and inference:
4//! - Training speed benchmarks
5//! - Inference latency measurements
6//! - Memory usage profiling
7//! - Throughput measurements
8//! - Accuracy vs speed tradeoff analysis
9//! - Comparison with baseline implementations
10
11use std::time::{Duration, Instant};
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16/// Benchmark configuration
17#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub struct BenchmarkConfig {
20    /// Number of warmup iterations
21    pub warmup_iterations: usize,
22    /// Number of benchmark iterations
23    pub benchmark_iterations: usize,
24    /// Batch sizes to test
25    pub batch_sizes: Vec<usize>,
26    /// Whether to measure memory usage
27    pub measure_memory: bool,
28    /// Whether to measure throughput
29    pub measure_throughput: bool,
30}
31
32impl Default for BenchmarkConfig {
33    fn default() -> Self {
34        Self {
35            warmup_iterations: 10,
36            benchmark_iterations: 100,
37            batch_sizes: vec![1, 8, 16, 32, 64, 128],
38            measure_memory: true,
39            measure_throughput: true,
40        }
41    }
42}
43
44/// Benchmark results for a single configuration
45#[derive(Debug, Clone)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47pub struct BenchmarkResult {
48    /// Batch size used
49    pub batch_size: usize,
50    /// Mean latency (milliseconds)
51    pub mean_latency_ms: f64,
52    /// Standard deviation of latency
53    pub std_latency_ms: f64,
54    /// Minimum latency
55    pub min_latency_ms: f64,
56    /// Maximum latency
57    pub max_latency_ms: f64,
58    /// Median latency
59    pub median_latency_ms: f64,
60    /// Throughput (samples per second)
61    pub throughput_samples_per_sec: f64,
62    /// Memory usage (MB) - estimated
63    pub memory_mb: Option<f64>,
64}
65
66impl BenchmarkResult {
67    /// Create a new benchmark result
68    pub fn new(batch_size: usize, latencies: Vec<Duration>) -> Self {
69        let latencies_ms: Vec<f64> = latencies.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
70
71        let mean = latencies_ms.iter().sum::<f64>() / latencies_ms.len() as f64;
72        let variance = latencies_ms.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
73            / latencies_ms.len() as f64;
74        let std = variance.sqrt();
75
76        let mut sorted_latencies = latencies_ms.clone();
77        sorted_latencies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
78
79        let min = *sorted_latencies
80            .first()
81            .expect("collection should not be empty");
82        let max = *sorted_latencies.last().expect("empty collection");
83        let median = sorted_latencies[sorted_latencies.len() / 2];
84
85        // Throughput: samples per second
86        let throughput = (batch_size as f64) / (mean / 1000.0);
87
88        Self {
89            batch_size,
90            mean_latency_ms: mean,
91            std_latency_ms: std,
92            min_latency_ms: min,
93            max_latency_ms: max,
94            median_latency_ms: median,
95            throughput_samples_per_sec: throughput,
96            memory_mb: None,
97        }
98    }
99
100    /// Set memory usage
101    pub fn with_memory(mut self, memory_mb: f64) -> Self {
102        self.memory_mb = Some(memory_mb);
103        self
104    }
105
106    /// Get latency percentile
107    pub fn latency_percentile(&self, percentile: f64) -> f64 {
108        // Simplified: use mean + std approximation
109        self.mean_latency_ms + self.std_latency_ms * percentile
110    }
111}
112
113/// Complete benchmark suite results
114#[derive(Debug, Clone)]
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116pub struct BenchmarkSuite {
117    /// Name of the benchmark
118    pub name: String,
119    /// Results for each batch size
120    pub results: Vec<BenchmarkResult>,
121    /// Total benchmark duration
122    pub total_duration_secs: f64,
123}
124
125impl BenchmarkSuite {
126    /// Create a new benchmark suite
127    pub fn new(name: String) -> Self {
128        Self {
129            name,
130            results: Vec::new(),
131            total_duration_secs: 0.0,
132        }
133    }
134
135    /// Add a result
136    pub fn add_result(&mut self, result: BenchmarkResult) {
137        self.results.push(result);
138    }
139
140    /// Set total duration
141    pub fn set_duration(&mut self, duration: Duration) {
142        self.total_duration_secs = duration.as_secs_f64();
143    }
144
145    /// Get result for specific batch size
146    pub fn get_result(&self, batch_size: usize) -> Option<&BenchmarkResult> {
147        self.results.iter().find(|r| r.batch_size == batch_size)
148    }
149
150    /// Get best throughput result
151    pub fn best_throughput(&self) -> Option<&BenchmarkResult> {
152        self.results.iter().max_by(|a, b| {
153            a.throughput_samples_per_sec
154                .partial_cmp(&b.throughput_samples_per_sec)
155                .expect("value should be present")
156        })
157    }
158
159    /// Get best latency result
160    pub fn best_latency(&self) -> Option<&BenchmarkResult> {
161        self.results.iter().min_by(|a, b| {
162            a.mean_latency_ms
163                .partial_cmp(&b.mean_latency_ms)
164                .unwrap_or(std::cmp::Ordering::Equal)
165        })
166    }
167
168    /// Print summary
169    pub fn print_summary(&self) {
170        println!("\n=== Benchmark Suite: {} ===", self.name);
171        println!("Total Duration: {:.2}s", self.total_duration_secs);
172        println!(
173            "\n{:<12} {:<15} {:<15} {:<15} {:<15}",
174            "Batch Size", "Mean (ms)", "Std (ms)", "Throughput", "Memory (MB)"
175        );
176        println!("{}", "-".repeat(75));
177
178        for result in &self.results {
179            let memory_str = result
180                .memory_mb
181                .map(|m| format!("{:.2}", m))
182                .unwrap_or_else(|| "N/A".to_string());
183
184            println!(
185                "{:<12} {:<15.2} {:<15.2} {:<15.2} {:<15}",
186                result.batch_size,
187                result.mean_latency_ms,
188                result.std_latency_ms,
189                result.throughput_samples_per_sec,
190                memory_str
191            );
192        }
193
194        if let Some(best_throughput) = self.best_throughput() {
195            println!(
196                "\nBest Throughput: {:.2} samples/sec @ batch_size={}",
197                best_throughput.throughput_samples_per_sec, best_throughput.batch_size
198            );
199        }
200
201        if let Some(best_latency) = self.best_latency() {
202            println!(
203                "Best Latency: {:.2}ms @ batch_size={}",
204                best_latency.mean_latency_ms, best_latency.batch_size
205            );
206        }
207    }
208}
209
210/// Benchmarking utilities
211pub struct Benchmarker {
212    config: BenchmarkConfig,
213}
214
215impl Default for Benchmarker {
216    fn default() -> Self {
217        Self::new(BenchmarkConfig::default())
218    }
219}
220
221impl Benchmarker {
222    /// Create a new benchmarker
223    pub fn new(config: BenchmarkConfig) -> Self {
224        Self { config }
225    }
226
227    /// Benchmark a function
228    pub fn benchmark<F, T>(&self, name: &str, mut f: F) -> BenchmarkSuite
229    where
230        F: FnMut() -> T,
231    {
232        let total_start = Instant::now();
233        let mut suite = BenchmarkSuite::new(name.to_string());
234
235        // Single batch size benchmark (for non-batch operations)
236        let mut latencies = Vec::new();
237
238        // Warmup
239        for _ in 0..self.config.warmup_iterations {
240            let _ = f();
241        }
242
243        // Benchmark
244        for _ in 0..self.config.benchmark_iterations {
245            let start = Instant::now();
246            let _ = f();
247            let duration = start.elapsed();
248            latencies.push(duration);
249        }
250
251        let result = BenchmarkResult::new(1, latencies);
252        suite.add_result(result);
253
254        suite.set_duration(total_start.elapsed());
255        suite
256    }
257
258    /// Benchmark a batch operation
259    pub fn benchmark_batch<F, T>(&self, name: &str, mut f: F) -> BenchmarkSuite
260    where
261        F: FnMut(usize) -> T,
262    {
263        let total_start = Instant::now();
264        let mut suite = BenchmarkSuite::new(name.to_string());
265
266        for &batch_size in &self.config.batch_sizes {
267            let mut latencies = Vec::new();
268
269            // Warmup
270            for _ in 0..self.config.warmup_iterations {
271                let _ = f(batch_size);
272            }
273
274            // Benchmark
275            for _ in 0..self.config.benchmark_iterations {
276                let start = Instant::now();
277                let _ = f(batch_size);
278                let duration = start.elapsed();
279                latencies.push(duration);
280            }
281
282            let result = BenchmarkResult::new(batch_size, latencies);
283            suite.add_result(result);
284        }
285
286        suite.set_duration(total_start.elapsed());
287        suite
288    }
289
290    /// Compare two implementations
291    pub fn compare<F1, F2, T1, T2>(
292        &self,
293        name1: &str,
294        mut f1: F1,
295        name2: &str,
296        mut f2: F2,
297    ) -> (BenchmarkSuite, BenchmarkSuite, f64)
298    where
299        F1: FnMut() -> T1,
300        F2: FnMut() -> T2,
301    {
302        let suite1 = self.benchmark(name1, &mut f1);
303        let suite2 = self.benchmark(name2, &mut f2);
304
305        // Calculate speedup
306        let speedup = if let (Some(r1), Some(r2)) = (suite1.results.first(), suite2.results.first())
307        {
308            r2.mean_latency_ms / r1.mean_latency_ms
309        } else {
310            1.0
311        };
312
313        (suite1, suite2, speedup)
314    }
315}
316
317/// Training benchmark metrics
318#[derive(Debug, Clone)]
319#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
320pub struct TrainingBenchmark {
321    /// Epochs completed
322    pub epochs: usize,
323    /// Total training time
324    pub total_time_secs: f64,
325    /// Time per epoch
326    pub time_per_epoch_secs: f64,
327    /// Samples per second
328    pub samples_per_sec: f64,
329    /// Final training loss
330    pub final_loss: f64,
331    /// Final validation accuracy (if available)
332    pub final_accuracy: Option<f64>,
333}
334
335impl TrainingBenchmark {
336    /// Create a new training benchmark
337    pub fn new(
338        epochs: usize,
339        total_time: Duration,
340        total_samples: usize,
341        final_loss: f64,
342        final_accuracy: Option<f64>,
343    ) -> Self {
344        let total_time_secs = total_time.as_secs_f64();
345        let time_per_epoch_secs = total_time_secs / epochs as f64;
346        let samples_per_sec = total_samples as f64 / total_time_secs;
347
348        Self {
349            epochs,
350            total_time_secs,
351            time_per_epoch_secs,
352            samples_per_sec,
353            final_loss,
354            final_accuracy,
355        }
356    }
357
358    /// Print summary
359    pub fn print_summary(&self) {
360        println!("\n=== Training Benchmark ===");
361        println!("Epochs: {}", self.epochs);
362        println!("Total Time: {:.2}s", self.total_time_secs);
363        println!("Time per Epoch: {:.2}s", self.time_per_epoch_secs);
364        println!("Throughput: {:.2} samples/sec", self.samples_per_sec);
365        println!("Final Loss: {:.4}", self.final_loss);
366        if let Some(acc) = self.final_accuracy {
367            println!("Final Accuracy: {:.2}%", acc * 100.0);
368        }
369    }
370}
371
372/// Memory profiling results
373#[derive(Debug, Clone)]
374#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
375pub struct MemoryProfile {
376    /// Peak memory usage (MB)
377    pub peak_memory_mb: f64,
378    /// Average memory usage (MB)
379    pub avg_memory_mb: f64,
380    /// Memory usage by component
381    pub component_memory: Vec<(String, f64)>,
382}
383
384impl MemoryProfile {
385    /// Create a new memory profile
386    pub fn new() -> Self {
387        Self {
388            peak_memory_mb: 0.0,
389            avg_memory_mb: 0.0,
390            component_memory: Vec::new(),
391        }
392    }
393
394    /// Add component memory
395    pub fn add_component(&mut self, name: String, memory_mb: f64) {
396        self.component_memory.push((name, memory_mb));
397    }
398
399    /// Print summary
400    pub fn print_summary(&self) {
401        println!("\n=== Memory Profile ===");
402        println!("Peak Memory: {:.2} MB", self.peak_memory_mb);
403        println!("Average Memory: {:.2} MB", self.avg_memory_mb);
404
405        if !self.component_memory.is_empty() {
406            println!("\nMemory by Component:");
407            for (name, memory) in &self.component_memory {
408                println!("  {}: {:.2} MB", name, memory);
409            }
410        }
411    }
412}
413
414impl Default for MemoryProfile {
415    fn default() -> Self {
416        Self::new()
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_benchmark_config_default() {
426        let config = BenchmarkConfig::default();
427        assert_eq!(config.warmup_iterations, 10);
428        assert_eq!(config.benchmark_iterations, 100);
429        assert!(!config.batch_sizes.is_empty());
430    }
431
432    #[test]
433    fn test_benchmark_result_creation() {
434        let durations = vec![
435            Duration::from_millis(10),
436            Duration::from_millis(12),
437            Duration::from_millis(11),
438        ];
439
440        let result = BenchmarkResult::new(32, durations);
441
442        assert_eq!(result.batch_size, 32);
443        assert!(result.mean_latency_ms > 0.0);
444        assert!(result.std_latency_ms >= 0.0);
445        assert!(result.throughput_samples_per_sec > 0.0);
446    }
447
448    #[test]
449    fn test_benchmark_suite() {
450        let mut suite = BenchmarkSuite::new("Test Suite".to_string());
451
452        let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
453        let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
454
455        suite.add_result(result1);
456        suite.add_result(result2);
457
458        assert_eq!(suite.results.len(), 2);
459        assert!(suite.get_result(8).is_some());
460        assert!(suite.get_result(16).is_some());
461        assert!(suite.get_result(32).is_none());
462    }
463
464    #[test]
465    fn test_benchmark_best_throughput() {
466        let mut suite = BenchmarkSuite::new("Test".to_string());
467
468        let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
469        let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
470
471        suite.add_result(result1);
472        suite.add_result(result2);
473
474        let best = suite.best_throughput().expect("operation should succeed");
475        assert_eq!(best.batch_size, 16); // Higher throughput
476    }
477
478    #[test]
479    fn test_benchmark_best_latency() {
480        let mut suite = BenchmarkSuite::new("Test".to_string());
481
482        let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
483        let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
484
485        suite.add_result(result1);
486        suite.add_result(result2);
487
488        let best = suite.best_latency().expect("operation should succeed");
489        assert_eq!(best.batch_size, 16); // Lower latency
490    }
491
492    #[test]
493    fn test_benchmarker_creation() {
494        let config = BenchmarkConfig::default();
495        let benchmarker = Benchmarker::new(config);
496
497        assert!(benchmarker.config.warmup_iterations > 0);
498    }
499
500    #[test]
501    fn test_benchmarker_simple() {
502        let config = BenchmarkConfig {
503            warmup_iterations: 2,
504            benchmark_iterations: 5,
505            batch_sizes: vec![1],
506            measure_memory: false,
507            measure_throughput: true,
508        };
509
510        let benchmarker = Benchmarker::new(config);
511
512        let suite = benchmarker.benchmark("simple_test", || {
513            // Simple operation
514            let mut sum = 0;
515            for i in 0..100 {
516                sum += i;
517            }
518            sum
519        });
520
521        assert_eq!(suite.results.len(), 1);
522        assert!(suite.results[0].mean_latency_ms >= 0.0);
523    }
524
525    #[test]
526    fn test_training_benchmark() {
527        let benchmark =
528            TrainingBenchmark::new(10, Duration::from_secs(100), 1000, 0.123, Some(0.95));
529
530        assert_eq!(benchmark.epochs, 10);
531        assert_eq!(benchmark.total_time_secs, 100.0);
532        assert_eq!(benchmark.time_per_epoch_secs, 10.0);
533        assert_eq!(benchmark.samples_per_sec, 10.0);
534        assert_eq!(benchmark.final_loss, 0.123);
535        assert_eq!(benchmark.final_accuracy, Some(0.95));
536    }
537
538    #[test]
539    fn test_memory_profile() {
540        let mut profile = MemoryProfile::new();
541        profile.peak_memory_mb = 512.0;
542        profile.avg_memory_mb = 256.0;
543
544        profile.add_component("Weights".to_string(), 100.0);
545        profile.add_component("Activations".to_string(), 50.0);
546
547        assert_eq!(profile.peak_memory_mb, 512.0);
548        assert_eq!(profile.component_memory.len(), 2);
549    }
550
551    #[test]
552    fn test_benchmark_result_with_memory() {
553        let result = BenchmarkResult::new(32, vec![Duration::from_millis(10)]);
554        let result_with_mem = result.with_memory(256.0);
555
556        assert_eq!(result_with_mem.memory_mb, Some(256.0));
557    }
558
559    #[test]
560    fn test_benchmarker_compare() {
561        let config = BenchmarkConfig {
562            warmup_iterations: 1,
563            benchmark_iterations: 3,
564            batch_sizes: vec![1],
565            measure_memory: false,
566            measure_throughput: true,
567        };
568
569        let benchmarker = Benchmarker::new(config);
570
571        let (suite1, suite2, speedup) = benchmarker.compare(
572            "impl1",
573            || {
574                std::thread::sleep(Duration::from_micros(100));
575                42
576            },
577            "impl2",
578            || {
579                std::thread::sleep(Duration::from_micros(200));
580                42
581            },
582        );
583
584        assert!(!suite1.results.is_empty());
585        assert!(!suite2.results.is_empty());
586        assert!(speedup > 0.0);
587    }
588}