Skip to main content

trustformers_optim/
performance_validation.rs

1//! # Comprehensive Performance Validation Framework
2//!
3//! This module provides a comprehensive performance validation and benchmarking system
4//! for all optimizers in the TrustformeRS optimization library. It addresses the
5//! **HIGH PRIORITY** performance validation requirements from TODO.md:
6//!
7//! - Run benchmarks to verify optimization implementations work correctly
8//! - Validate memory efficiency claims for 8-bit optimizers
9//! - Test distributed training components
10//! - Performance regression detection with statistical significance
11//! - Cross-optimizer performance comparison and validation
12//!
13//! ## Key Features
14//!
15//! 1. **Correctness Validation**: Mathematical correctness of all optimizer implementations
16//! 2. **Performance Benchmarking**: Comprehensive performance analysis across scenarios
17//! 3. **Memory Efficiency Testing**: Validation of memory usage claims and optimizations
18//! 4. **Regression Detection**: Statistical analysis to detect performance regressions
19//! 5. **Distributed Training Validation**: Testing of distributed training components
20//! 6. **Hardware Utilization Analysis**: CPU/GPU utilization and efficiency metrics
21//! 7. **Convergence Analysis**: Mathematical convergence validation and speed analysis
22//!
23//! ## Usage Example
24//!
25//! ```rust,no_run
26//! use trustformers_optim::performance_validation::*;
27//!
28//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
29//! // Create comprehensive validation suite
30//! let mut validator = PerformanceValidator::new()
31//!     .with_statistical_significance(true)
32//!     .with_memory_validation(true)
33//!     .with_regression_detection(true)
34//!     .with_convergence_analysis(true);
35//!
36//! // Run complete validation suite
37//! let results = validator.run_comprehensive_validation()?;
38//!
39//! // Generate detailed report
40//! let report = validator.generate_validation_report(&results)?;
41//! println!("{}", report);
42//! # Ok(())
43//! # }
44//! ```
45
46// reason: research-stage module — reserved API/scaffolding fields and methods
47// retained intentionally for in-progress features; not yet on active call paths.
48#![allow(dead_code)]
49
50use crate::adam::{Adam, AdamW};
51use crate::averaged_adam::AveragedAdam;
52use crate::lamb::LAMB;
53use crate::lion::Lion;
54use crate::sgd::SGD;
55
56use serde::{Deserialize, Serialize};
57use std::collections::HashMap;
58use std::time::{Duration, Instant};
59use trustformers_core::errors::{Result, TrustformersError};
60use trustformers_core::tensor::Tensor;
61use trustformers_core::traits::Optimizer;
62
63/// A `benchmark_optimizer`-local extension of [`Optimizer`] that additionally
64/// exposes each optimizer's REAL allocated state memory (momentum/variance
65/// buffers, etc.), when the concrete optimizer type publishes one via
66/// [`crate::traits::StatefulOptimizer::memory_usage`].
67///
68/// [`StatefulOptimizer`](crate::traits::StatefulOptimizer) cannot be called
69/// through `dyn Optimizer` -- it carries associated types (`Config`,
70/// `State`), so it isn't `dyn`-safe, and `benchmark_optimizer` needs a single
71/// uniform type across every [`OptimizerType`] it cycles through. This
72/// trait re-exposes just the one number it needs, in bytes, so
73/// `create_optimizer_instance` can keep returning one `Box<dyn ..>` type.
74///
75/// The default is `None`: an optimizer kind that doesn't implement
76/// `StatefulOptimizer` (currently only [`LAMB`], whose moment buffers are
77/// private with no public accessor) has genuinely nothing to report here,
78/// so it is honestly `None` rather than a fabricated number.
79trait BenchmarkOptimizer: Optimizer {
80    fn state_memory_bytes(&self) -> Option<usize> {
81        None
82    }
83}
84
85impl BenchmarkOptimizer for Adam {
86    fn state_memory_bytes(&self) -> Option<usize> {
87        Some(crate::traits::StatefulOptimizer::memory_usage(self).total_bytes)
88    }
89}
90
91impl BenchmarkOptimizer for AdamW {
92    fn state_memory_bytes(&self) -> Option<usize> {
93        Some(crate::traits::StatefulOptimizer::memory_usage(self).total_bytes)
94    }
95}
96
97impl BenchmarkOptimizer for SGD {
98    fn state_memory_bytes(&self) -> Option<usize> {
99        Some(crate::traits::StatefulOptimizer::memory_usage(self).total_bytes)
100    }
101}
102
103impl BenchmarkOptimizer for AveragedAdam {
104    fn state_memory_bytes(&self) -> Option<usize> {
105        Some(crate::traits::StatefulOptimizer::memory_usage(self).total_bytes)
106    }
107}
108
109impl BenchmarkOptimizer for Lion {
110    fn state_memory_bytes(&self) -> Option<usize> {
111        Some(crate::traits::StatefulOptimizer::memory_usage(self).total_bytes)
112    }
113}
114
115// `LAMB` has no public state-memory accessor (its `exp_avg`/`exp_avg_sq`
116// buffers are private and it does not implement `StatefulOptimizer`), so it
117// uses the trait's default `None` -- an honest "cannot measure", not a
118// fabricated byte count.
119impl BenchmarkOptimizer for LAMB {}
120
121/// Comprehensive performance validation configuration
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ValidationConfig {
124    /// Enable statistical significance testing
125    pub statistical_significance: bool,
126    /// Enable memory efficiency validation
127    pub memory_validation: bool,
128    /// Enable performance regression detection
129    pub regression_detection: bool,
130    /// Enable convergence analysis
131    pub convergence_analysis: bool,
132    /// Enable distributed training validation
133    pub distributed_validation: bool,
134    /// Number of benchmark iterations for statistical analysis
135    pub benchmark_iterations: usize,
136    /// Confidence level for statistical tests (0.95 = 95%)
137    pub confidence_level: f64,
138    /// Maximum acceptable performance regression (%)
139    pub max_regression_threshold: f64,
140    /// Minimum required memory efficiency for 8-bit optimizers (%)
141    pub min_memory_efficiency: f64,
142}
143
144impl Default for ValidationConfig {
145    fn default() -> Self {
146        Self {
147            statistical_significance: true,
148            memory_validation: true,
149            regression_detection: true,
150            convergence_analysis: true,
151            distributed_validation: true,
152            benchmark_iterations: 100,
153            confidence_level: 0.95,
154            max_regression_threshold: 5.0, // 5% regression threshold
155            min_memory_efficiency: 75.0,   // 75% memory reduction requirement
156        }
157    }
158}
159
160/// Main performance validation framework
161pub struct PerformanceValidator {
162    config: ValidationConfig,
163    baseline_results: Option<HashMap<String, BenchmarkResult>>,
164    validation_history: Vec<ValidationSession>,
165    statistical_analyzer: StatisticalAnalyzer,
166    memory_analyzer: MemoryAnalyzer,
167    convergence_analyzer: ConvergenceAnalyzer,
168    regression_detector: RegressionDetector,
169}
170
171impl Default for PerformanceValidator {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl PerformanceValidator {
178    /// Create new performance validator with default configuration
179    pub fn new() -> Self {
180        Self {
181            config: ValidationConfig::default(),
182            baseline_results: None,
183            validation_history: Vec::new(),
184            statistical_analyzer: StatisticalAnalyzer::new(),
185            memory_analyzer: MemoryAnalyzer::new(),
186            convergence_analyzer: ConvergenceAnalyzer::new(),
187            regression_detector: RegressionDetector::new(),
188        }
189    }
190
191    /// Builder pattern for configuration
192    pub fn with_statistical_significance(mut self, enabled: bool) -> Self {
193        self.config.statistical_significance = enabled;
194        self
195    }
196
197    pub fn with_memory_validation(mut self, enabled: bool) -> Self {
198        self.config.memory_validation = enabled;
199        self
200    }
201
202    pub fn with_regression_detection(mut self, enabled: bool) -> Self {
203        self.config.regression_detection = enabled;
204        self
205    }
206
207    pub fn with_convergence_analysis(mut self, enabled: bool) -> Self {
208        self.config.convergence_analysis = enabled;
209        self
210    }
211
212    pub fn with_benchmark_iterations(mut self, iterations: usize) -> Self {
213        self.config.benchmark_iterations = iterations;
214        self
215    }
216
217    /// Run comprehensive validation suite
218    pub fn run_comprehensive_validation(&mut self) -> Result<ValidationResults> {
219        let session_start = Instant::now();
220        let mut results = ValidationResults::new();
221
222        // 1. Correctness Validation
223        let correctness_results = self.validate_mathematical_correctness()?;
224        results.correctness_results = correctness_results;
225
226        // 2. Performance Benchmarking
227        let performance_results = self.run_performance_benchmarks()?;
228        results.performance_results = performance_results;
229
230        // 3. Memory Efficiency Validation
231        if self.config.memory_validation {
232            let memory_results = self.validate_memory_efficiency()?;
233            results.memory_results = Some(memory_results);
234        }
235
236        // 4. Convergence Analysis
237        if self.config.convergence_analysis {
238            let convergence_results = self.analyze_convergence_properties()?;
239            results.convergence_results = Some(convergence_results);
240        }
241
242        // 5. Distributed Training Validation
243        if self.config.distributed_validation {
244            let distributed_results = self.validate_distributed_training()?;
245            results.distributed_results = Some(distributed_results);
246        }
247
248        // 6. Regression Detection
249        if self.config.regression_detection && self.baseline_results.is_some() {
250            let regression_results =
251                self.detect_performance_regressions(&results.performance_results)?;
252            results.regression_results = Some(regression_results);
253        }
254
255        let total_time = session_start.elapsed();
256        results.total_validation_time = total_time;
257
258        // Store validation session
259        let session = ValidationSession {
260            timestamp: std::time::SystemTime::now(),
261            config: self.config.clone(),
262            results: results.clone(),
263        };
264        self.validation_history.push(session);
265
266        Ok(results)
267    }
268
269    /// Validate mathematical correctness of all optimizers
270    fn validate_mathematical_correctness(&mut self) -> Result<CorrectnessResults> {
271        let mut results = CorrectnessResults::new();
272
273        // Test optimizers with known mathematical properties
274        let test_cases = self.create_mathematical_test_cases()?;
275
276        for test_case in &test_cases {
277            // Test each optimizer on this test case
278            let optimizer_results = self.test_optimizers_on_case(test_case)?;
279
280            for (optimizer_name, passed) in optimizer_results {
281                results.optimizer_correctness.insert(optimizer_name, passed);
282            }
283        }
284
285        // Analyze results
286        let total_tests = results.optimizer_correctness.len();
287        let passed_tests = results.optimizer_correctness.values().filter(|&&x| x).count();
288
289        results.overall_correctness_rate = passed_tests as f64 / total_tests as f64;
290        results.passed_tests = passed_tests;
291        results.total_tests = total_tests;
292
293        Ok(results)
294    }
295
296    fn create_mathematical_test_cases(&self) -> Result<Vec<MathematicalTestCase>> {
297        let mut test_cases = Vec::new();
298
299        // Test Case 1: Quadratic function optimization
300        test_cases.push(MathematicalTestCase {
301            name: "Quadratic Function Convergence".to_string(),
302            description: "f(x) = 0.5 * x^T A x + b^T x".to_string(),
303            parameters: create_test_parameters(vec![10, 10])?,
304            gradients: create_quadratic_gradients(vec![10, 10])?,
305            expected_properties: vec![
306                MathematicalProperty::Convergence,
307                MathematicalProperty::MonotonicImprovement,
308            ],
309            tolerance: 1e-6,
310        });
311
312        // Test Case 2: Convex optimization
313        test_cases.push(MathematicalTestCase {
314            name: "Convex Optimization".to_string(),
315            description: "Simple convex function with known minimum".to_string(),
316            parameters: create_test_parameters(vec![5, 5])?,
317            gradients: create_convex_gradients(vec![5, 5])?,
318            expected_properties: vec![
319                MathematicalProperty::Convergence,
320                MathematicalProperty::GlobalOptimum,
321            ],
322            tolerance: 1e-5,
323        });
324
325        // Test Case 3: Sparse gradient handling
326        test_cases.push(MathematicalTestCase {
327            name: "Sparse Gradient Handling".to_string(),
328            description: "Optimization with sparse gradients".to_string(),
329            parameters: create_test_parameters(vec![20, 20])?,
330            gradients: create_sparse_gradients(vec![20, 20], 0.1)?, // 10% sparsity
331            expected_properties: vec![
332                MathematicalProperty::SparsityHandling,
333                MathematicalProperty::StableConvergence,
334            ],
335            tolerance: 1e-4,
336        });
337
338        Ok(test_cases)
339    }
340
341    fn test_optimizers_on_case(
342        &self,
343        test_case: &MathematicalTestCase,
344    ) -> Result<HashMap<String, bool>> {
345        let mut results = HashMap::new();
346
347        // Test Adam
348        let adam_passed = self.test_optimizer_correctness(
349            "Adam",
350            || Box::new(Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0)),
351            test_case,
352        )?;
353        results.insert("Adam".to_string(), adam_passed);
354
355        // Test AdamW
356        let adamw_passed = self.test_optimizer_correctness(
357            "AdamW",
358            || Box::new(AdamW::new(0.001, (0.9, 0.999), 1e-8, 0.01)),
359            test_case,
360        )?;
361        results.insert("AdamW".to_string(), adamw_passed);
362
363        // Test SGD
364        let sgd_passed = self.test_optimizer_correctness(
365            "SGD",
366            || Box::new(SGD::new(0.01, 0.9, 0.0, false)),
367            test_case,
368        )?;
369        results.insert("SGD".to_string(), sgd_passed);
370
371        // Test Averaged Adam
372        let avg_adam_passed = self.test_optimizer_correctness(
373            "AveragedAdam",
374            || Box::new(AveragedAdam::new(0.001, (0.9, 0.999), 1e-8, 0.01, 0.999)),
375            test_case,
376        )?;
377        results.insert("AveragedAdam".to_string(), avg_adam_passed);
378
379        Ok(results)
380    }
381
382    fn test_optimizer_correctness<F>(
383        &self,
384        _name: &str,
385        optimizer_factory: F,
386        test_case: &MathematicalTestCase,
387    ) -> Result<bool>
388    where
389        F: Fn() -> Box<dyn Optimizer>,
390    {
391        let mut optimizer = optimizer_factory();
392        let mut parameters = test_case.parameters.clone();
393        let initial_loss = self.compute_test_loss(&parameters, test_case)?;
394        let mut previous_loss = initial_loss;
395
396        let mut convergence_achieved = false;
397        let mut monotonic_improvement = true;
398        let max_iterations = 1000;
399
400        // Update norms produced on exactly-zero-gradient steps, in the
401        // order they occur. Used by `MathematicalProperty::SparsityHandling`
402        // below: a zero gradient carries no new signal, so a well-behaved
403        // optimizer's update on such a step comes only from decaying
404        // momentum/decoupled decay and must not grow step over step.
405        let mut zero_gradient_update_norms: Vec<f32> = Vec::new();
406
407        for iteration in 0..max_iterations {
408            // Compute gradients for current parameters
409            let gradients = self.compute_test_gradients(&parameters, test_case, iteration)?;
410
411            // Apply optimizer step
412            for (param_name, gradient) in &gradients {
413                if let Some(param) = parameters.get_mut(param_name) {
414                    let is_zero_gradient = gradient.norm()? == 0.0;
415                    let before = if is_zero_gradient { Some(param.clone()) } else { None };
416
417                    optimizer.zero_grad();
418                    optimizer.update(param, gradient)?;
419                    optimizer.step();
420
421                    if let Some(before) = before {
422                        zero_gradient_update_norms.push(param.sub(&before)?.norm()?);
423                    }
424                }
425            }
426
427            // Check convergence and properties
428            let current_loss = self.compute_test_loss(&parameters, test_case)?;
429
430            // Check monotonic improvement (for convex problems)
431            if test_case
432                .expected_properties
433                .contains(&MathematicalProperty::MonotonicImprovement)
434                && current_loss > previous_loss + test_case.tolerance as f32
435            {
436                monotonic_improvement = false;
437            }
438
439            // Check convergence
440            if (previous_loss - current_loss).abs() < test_case.tolerance as f32 {
441                convergence_achieved = true;
442                break;
443            }
444
445            previous_loss = current_loss;
446        }
447
448        // Validate expected properties
449        let mut all_properties_satisfied = true;
450
451        for property in &test_case.expected_properties {
452            match property {
453                MathematicalProperty::Convergence => {
454                    if !convergence_achieved {
455                        all_properties_satisfied = false;
456                    }
457                },
458                MathematicalProperty::MonotonicImprovement => {
459                    if !monotonic_improvement {
460                        all_properties_satisfied = false;
461                    }
462                },
463                MathematicalProperty::GlobalOptimum => {
464                    // For test problems, check if close to known optimum
465                    let final_loss = self.compute_test_loss(&parameters, test_case)?;
466                    if final_loss > (test_case.tolerance * 10.0) as f32 {
467                        all_properties_satisfied = false;
468                    }
469                },
470                MathematicalProperty::SparsityHandling => {
471                    // A zero gradient carries no new signal, so the update
472                    // it produces (from decaying momentum / decoupled
473                    // weight decay only) must be finite and must not grow
474                    // from one zero-gradient step to the next -- nothing is
475                    // renewing it. This is independent of whether the
476                    // overall run converged.
477                    if zero_gradient_update_norms.is_empty() {
478                        // No zero-gradient step ever occurred, so there is
479                        // nothing to evaluate: do not claim the property
480                        // holds without evidence.
481                        all_properties_satisfied = false;
482                    } else if !zero_gradient_update_norms.iter().all(|n| n.is_finite()) {
483                        all_properties_satisfied = false;
484                    } else {
485                        let non_increasing = zero_gradient_update_norms
486                            .windows(2)
487                            .all(|pair| pair[1] <= pair[0] + test_case.tolerance as f32);
488                        if !non_increasing {
489                            all_properties_satisfied = false;
490                        }
491                    }
492                },
493                MathematicalProperty::StableConvergence => {
494                    // Check for stable convergence without oscillations
495                    if !convergence_achieved {
496                        all_properties_satisfied = false;
497                    }
498                },
499            }
500        }
501
502        Ok(all_properties_satisfied)
503    }
504
505    fn compute_test_loss(
506        &self,
507        parameters: &HashMap<String, Tensor>,
508        test_case: &MathematicalTestCase,
509    ) -> Result<f32> {
510        // Simplified loss computation for test cases
511        match test_case.name.as_str() {
512            "Quadratic Function Convergence" => {
513                // f(x) = 0.5 * ||x||^2
514                let mut total_loss = 0.0;
515                for tensor in parameters.values() {
516                    let norm_squared = tensor.norm()?.powi(2);
517                    total_loss += norm_squared * 0.5;
518                }
519                Ok(total_loss)
520            },
521            "Convex Optimization" => {
522                // f(x) = ||x - target||^2 where target is zero
523                let mut total_loss = 0.0;
524                for tensor in parameters.values() {
525                    let norm_squared = tensor.norm()?.powi(2);
526                    total_loss += norm_squared;
527                }
528                Ok(total_loss)
529            },
530            "Sparse Gradient Handling" => {
531                // Simple quadratic with sparse structure
532                let mut total_loss = 0.0;
533                for tensor in parameters.values() {
534                    let norm_squared = tensor.norm()?.powi(2);
535                    total_loss += norm_squared * 0.5;
536                }
537                Ok(total_loss)
538            },
539            _ => Ok(0.0),
540        }
541    }
542
543    fn compute_test_gradients(
544        &self,
545        parameters: &HashMap<String, Tensor>,
546        test_case: &MathematicalTestCase,
547        iteration: usize,
548    ) -> Result<HashMap<String, Tensor>> {
549        let mut gradients = HashMap::new();
550
551        match test_case.name.as_str() {
552            "Quadratic Function Convergence" => {
553                // Gradient of f(x) = 0.5 * ||x||^2 is x
554                for (name, param) in parameters {
555                    gradients.insert(name.clone(), param.clone());
556                }
557            },
558            "Convex Optimization" => {
559                // Gradient of f(x) = ||x||^2 is 2x
560                for (name, param) in parameters {
561                    let grad = param.scalar_mul(2.0)?;
562                    gradients.insert(name.clone(), grad);
563                }
564            },
565            "Sparse Gradient Handling" => {
566                // Create sparse gradients
567                for (name, param) in parameters {
568                    let grad = param.clone();
569                    // Make gradient sparse by zeroing out random elements
570                    if iteration % 10 < 3 {
571                        // 30% of iterations have sparse gradients
572                        let shape = param.shape();
573                        let _total_elements = shape.iter().product::<usize>();
574                        let sparse_grad = Tensor::zeros(&shape)?;
575                        gradients.insert(name.clone(), sparse_grad);
576                    } else {
577                        gradients.insert(name.clone(), grad);
578                    }
579                }
580            },
581            _ => {
582                // Default: use provided gradients
583                gradients = test_case.gradients.clone();
584            },
585        }
586
587        Ok(gradients)
588    }
589
590    /// Run comprehensive performance benchmarks
591    fn run_performance_benchmarks(&mut self) -> Result<PerformanceBenchmarkResults> {
592        let mut results = PerformanceBenchmarkResults::new();
593
594        // Define benchmark scenarios
595        let scenarios = vec![
596            BenchmarkScenario {
597                name: "Small Model (1M params)".to_string(),
598                parameter_sizes: vec![1000, 1000], // 1M parameters
599                batch_size: 32,
600                iterations: self.config.benchmark_iterations,
601            },
602            BenchmarkScenario {
603                name: "Medium Model (10M params)".to_string(),
604                parameter_sizes: vec![3162, 3162], // ~10M parameters
605                batch_size: 16,
606                iterations: self.config.benchmark_iterations / 2, // Fewer iterations for larger models
607            },
608            BenchmarkScenario {
609                name: "Large Model (100M params)".to_string(),
610                parameter_sizes: vec![10000, 10000], // 100M parameters
611                batch_size: 8,
612                iterations: self.config.benchmark_iterations / 4,
613            },
614        ];
615
616        for scenario in scenarios {
617            let scenario_results = self.benchmark_scenario(&scenario)?;
618            results.scenario_results.push(scenario_results);
619        }
620
621        // Analyze cross-scenario performance
622        self.analyze_performance_trends(&mut results)?;
623
624        Ok(results)
625    }
626
627    fn benchmark_scenario(&self, scenario: &BenchmarkScenario) -> Result<ScenarioBenchmarkResult> {
628        let mut result = ScenarioBenchmarkResult {
629            scenario_name: scenario.name.clone(),
630            optimizer_results: HashMap::new(),
631        };
632
633        // Benchmark each optimizer
634        let optimizers_to_test = vec![
635            ("Adam", OptimizerType::Adam),
636            ("AdamW", OptimizerType::AdamW),
637            ("SGD", OptimizerType::SGD),
638            ("AveragedAdam", OptimizerType::AveragedAdam),
639            ("LAMB", OptimizerType::LAMB),
640            ("Lion", OptimizerType::Lion),
641        ];
642
643        for (name, optimizer_type) in optimizers_to_test {
644            let optimizer_result = self.benchmark_optimizer(name, optimizer_type, scenario)?;
645            result.optimizer_results.insert(name.to_string(), optimizer_result);
646        }
647
648        Ok(result)
649    }
650
651    fn benchmark_optimizer(
652        &self,
653        name: &str,
654        optimizer_type: OptimizerType,
655        scenario: &BenchmarkScenario,
656    ) -> Result<OptimizerBenchmarkResult> {
657        let mut step_times = Vec::new();
658
659        // Create optimizer
660        let mut optimizer = self.create_optimizer_instance(optimizer_type)?;
661
662        // Create test parameters
663        let mut parameters = create_test_parameters(scenario.parameter_sizes.clone())?;
664
665        for iteration in 0..scenario.iterations {
666            // Create gradients for this iteration
667            let gradients = create_benchmark_gradients(&scenario.parameter_sizes, iteration)?;
668
669            // Time the optimizer step
670            let step_start = Instant::now();
671
672            // Apply optimizer step
673            for (param_name, gradient) in &gradients {
674                if let Some(param) = parameters.get_mut(param_name) {
675                    optimizer.zero_grad();
676                    optimizer.update(param, gradient)?;
677                    optimizer.step();
678                }
679            }
680
681            let step_time = step_start.elapsed();
682            step_times.push(step_time);
683        }
684
685        // Compute statistics
686        let avg_step_time = step_times.iter().sum::<Duration>() / step_times.len() as u32;
687        let min_step_time = step_times.iter().min().copied().unwrap_or(Duration::from_secs(0));
688        let max_step_time = step_times.iter().max().copied().unwrap_or(Duration::from_secs(0));
689
690        // Real, measured state memory (see `BenchmarkOptimizer::state_memory_bytes`
691        // near the top of this file), read once after the run rather than
692        // as a per-iteration "before/after delta": every optimizer kind
693        // this can measure allocates its per-parameter state buffers
694        // (momentum/variance/...) on that parameter's first `update()`
695        // call and never resizes them again, since `scenario.parameter_sizes`
696        // is fixed for the whole run -- so the state footprint is already
697        // at steady state after iteration 0 and identical on every later
698        // iteration. There is nothing to average; a delta between two
699        // identical readings is always exactly zero regardless of the
700        // optimizer, which is what the previous shape-only
701        // `estimate_memory_usage` (computed from `parameters`, ignoring
702        // `optimizer` entirely) actually measured -- a mathematical
703        // certainty, not a benchmark result.
704        let state_memory_bytes = optimizer.state_memory_bytes();
705
706        // Calculate throughput (parameters processed per second)
707        let total_params: usize = scenario.parameter_sizes.iter().product();
708        let throughput = total_params as f64 / avg_step_time.as_secs_f64();
709
710        // Perform statistical analysis if enabled. When a baseline was set
711        // for this optimizer (`set_baseline`), its avg_step_time is a real,
712        // caller-provided null hypothesis for `analyze`'s t-test; with no
713        // baseline there is nothing to test against and `analyze` reports
714        // `p_value: None` honestly rather than a fabricated constant.
715        let baseline_step_time = self
716            .baseline_results
717            .as_ref()
718            .and_then(|baselines| baselines.get(name))
719            .map(|baseline| baseline.avg_step_time);
720        let statistical_metrics = if self.config.statistical_significance {
721            Some(self.statistical_analyzer.analyze(
722                &step_times,
723                self.config.confidence_level,
724                baseline_step_time,
725            )?)
726        } else {
727            None
728        };
729
730        Ok(OptimizerBenchmarkResult {
731            optimizer_name: name.to_string(),
732            avg_step_time,
733            min_step_time,
734            max_step_time,
735            throughput,
736            avg_memory_usage: state_memory_bytes,
737            statistical_metrics,
738        })
739    }
740
741    fn create_optimizer_instance(
742        &self,
743        optimizer_type: OptimizerType,
744    ) -> Result<Box<dyn BenchmarkOptimizer>> {
745        match optimizer_type {
746            OptimizerType::Adam => Ok(Box::new(Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0))),
747            OptimizerType::AdamW => Ok(Box::new(AdamW::new(0.001, (0.9, 0.999), 1e-8, 0.01))),
748            OptimizerType::SGD => Ok(Box::new(SGD::new(0.01, 0.9, 0.0001, true))),
749            OptimizerType::AveragedAdam => Ok(Box::new(AveragedAdam::new(
750                0.001,
751                (0.9, 0.999),
752                1e-8,
753                0.01,
754                0.999,
755            ))),
756            OptimizerType::LAMB => Ok(Box::new(LAMB::new(0.001, (0.9, 0.999), 1e-6, 0.01))),
757            OptimizerType::Lion => Ok(Box::new(Lion::new(0.0001, (0.9, 0.99), 0.01))),
758        }
759    }
760
761    fn analyze_performance_trends(&self, results: &mut PerformanceBenchmarkResults) -> Result<()> {
762        // Analyze performance scaling across model sizes
763        let mut scaling_analysis = HashMap::new();
764
765        for optimizer_name in ["Adam", "AdamW", "SGD", "AveragedAdam", "LAMB", "Lion"] {
766            let mut throughputs = Vec::new();
767
768            for scenario_result in &results.scenario_results {
769                if let Some(optimizer_result) =
770                    scenario_result.optimizer_results.get(optimizer_name)
771                {
772                    throughputs.push(optimizer_result.throughput);
773                }
774            }
775
776            if throughputs.len() >= 2 {
777                let scaling_efficiency = self.compute_scaling_efficiency(&throughputs);
778                scaling_analysis.insert(optimizer_name.to_string(), scaling_efficiency);
779            }
780        }
781
782        results.scaling_analysis = scaling_analysis;
783        Ok(())
784    }
785
786    fn compute_scaling_efficiency(&self, throughputs: &[f64]) -> f64 {
787        if throughputs.len() < 2 {
788            return 1.0;
789        }
790
791        // Compute how well throughput scales (should decrease as model size increases)
792        // Perfect scaling would be inverse linear relationship
793        let first = throughputs[0];
794        let last = throughputs[throughputs.len() - 1];
795
796        // Higher is better (less performance degradation with scale)
797        last / first
798    }
799
800    /// Validate memory efficiency claims
801    fn validate_memory_efficiency(&mut self) -> Result<MemoryValidationResults> {
802        let mut results = MemoryValidationResults::new();
803
804        // Test 8-bit optimizers memory efficiency
805        let memory_test_results = self.test_memory_efficiency_claims()?;
806        results.eight_bit_efficiency = memory_test_results;
807
808        // Test gradient compression efficiency
809        let compression_results = self.test_gradient_compression_efficiency()?;
810        results.compression_efficiency = compression_results;
811
812        // Validate memory optimization techniques
813        let optimization_results = self.test_memory_optimizations()?;
814        results.optimization_efficiency = optimization_results;
815
816        Ok(results)
817    }
818
819    /// Measures the real state footprint of a quantized optimizer against `Adam`.
820    ///
821    /// Both optimizers are stepped on identical parameters and gradients, then asked
822    /// for the size of the buffers they actually allocated
823    /// ([`StatefulOptimizer::memory_usage`]). Nothing is assumed: the reported
824    /// percentage is `(adam_bytes − quantized_bytes) / adam_bytes`.
825    fn test_memory_efficiency_claims(&self) -> Result<HashMap<String, f64>> {
826        use crate::quantized_advanced::Adam4bit;
827        use crate::traits::StatefulOptimizer;
828
829        let mut results = HashMap::new();
830
831        let shape = vec![64, 64];
832        let parameters = create_test_parameters(shape.clone())?;
833        let gradients = create_benchmark_gradients(&shape, 0)?;
834
835        let mut adam = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
836        let mut adam4bit = Adam4bit::new(0.001, 0.9, 0.999, 1e-8, 0.0);
837
838        for (name, parameter) in &parameters {
839            let Some(gradient) = gradients.get(name) else {
840                continue;
841            };
842            let mut for_adam = parameter.clone();
843            let mut for_quantized = parameter.clone();
844            adam.update(&mut for_adam, gradient)?;
845            adam4bit.update(&mut for_quantized, gradient)?;
846        }
847
848        let adam_bytes = StatefulOptimizer::memory_usage(&adam).total_bytes;
849        let quantized_bytes = StatefulOptimizer::memory_usage(&adam4bit).total_bytes;
850
851        if adam_bytes == 0 {
852            return Err(TrustformersError::invalid_state(
853                "Adam reported a zero-byte state after a step; memory validation cannot proceed"
854                    .to_string(),
855            ));
856        }
857
858        let reduction = (adam_bytes as f64 - quantized_bytes as f64) / adam_bytes as f64 * 100.0;
859        results.insert("Adam4bit".to_string(), reduction);
860        results.insert("Adam.state_bytes".to_string(), adam_bytes as f64);
861        results.insert("Adam4bit.state_bytes".to_string(), quantized_bytes as f64);
862
863        Ok(results)
864    }
865
866    /// Measures the *real* payload reduction of each gradient-compression method.
867    ///
868    /// Each method compresses a fixed gradient, and the reported percentage is the
869    /// measured drop in transmitted bytes (`indices` plus `values`) relative to the
870    /// dense `f32` payload — not a table of expected ratios.
871    fn test_gradient_compression_efficiency(&self) -> Result<HashMap<String, f64>> {
872        use crate::compression::{CompressionMethod, GradientCompressor};
873
874        let mut results = HashMap::new();
875
876        // One 1024-element gradient: sparse methods need a tensor large enough for
877        // their `k` to be meaningful.
878        let shape = vec![1024];
879        let gradients = create_benchmark_gradients(&shape, 7)?;
880        let dense_bytes: usize = gradients
881            .values()
882            .map(|g| g.shape().iter().product::<usize>() * std::mem::size_of::<f32>())
883            .sum();
884        if dense_bytes == 0 {
885            return Err(TrustformersError::invalid_state(
886                "compression validation needs a non-empty gradient".to_string(),
887            ));
888        }
889
890        let methods = [
891            ("TopK", CompressionMethod::TopK { k: 102 }),
892            ("RandomK", CompressionMethod::RandomK { k: 102 }),
893            ("Threshold", CompressionMethod::Threshold { threshold: 0.5 }),
894            ("Quantization", CompressionMethod::Quantization { bits: 8 }),
895            ("SignSGD", CompressionMethod::SignSGD),
896        ];
897
898        for (name, method) in methods {
899            let mut compressor = GradientCompressor::new(method);
900            let compressed = compressor.compress(&gradients)?;
901
902            // Transmitted payload as the representation itself reports it.
903            let payload: usize = compressed.values().map(|c| c.payload_bytes()).sum();
904
905            // The round trip must reconstruct the right number of elements, otherwise
906            // the "saving" is meaningless.
907            let restored = compressor.decompress(&compressed)?;
908            for (grad_name, tensor) in &restored {
909                let expected: usize =
910                    gradients.get(grad_name).map(|g| g.shape().iter().product()).unwrap_or(0);
911                if tensor.data_f32()?.len() != expected {
912                    return Err(TrustformersError::invalid_state(format!(
913                        "{name} decompression returned the wrong element count for '{grad_name}'"
914                    )));
915                }
916            }
917
918            let reduction = (dense_bytes as f64 - payload as f64) / dense_bytes as f64 * 100.0;
919            results.insert(name.to_string(), reduction);
920        }
921
922        Ok(results)
923    }
924
925    /// Measures memory techniques that can be measured in-process.
926    ///
927    /// Only mixed precision is measurable here — it is a dtype change whose effect
928    /// shows up directly in [`Tensor::size_bytes`]. Gradient checkpointing and CPU
929    /// offloading depend on a training loop and a device this crate does not own, so
930    /// no figure is reported for them rather than a plausible-looking literal.
931    fn test_memory_optimizations(&self) -> Result<HashMap<String, f64>> {
932        let mut results = HashMap::new();
933
934        let dense = Tensor::from_vec(vec![0.5_f32; 4096], &[64, 64])?;
935        let half = match &dense {
936            Tensor::F32(array) => Tensor::F16(array.mapv(half::f16::from_f32)),
937            other => {
938                return Err(TrustformersError::invalid_state(format!(
939                    "expected an f32 tensor, got {:?}",
940                    other.dtype()
941                )))
942            },
943        };
944
945        let dense_bytes = dense.size_bytes();
946        if dense_bytes == 0 {
947            return Err(TrustformersError::invalid_state(
948                "mixed-precision validation needs a non-empty tensor".to_string(),
949            ));
950        }
951        let reduction =
952            (dense_bytes as f64 - half.size_bytes() as f64) / dense_bytes as f64 * 100.0;
953        results.insert("MixedPrecision".to_string(), reduction);
954
955        Ok(results)
956    }
957
958    /// Analyze convergence properties of optimizers
959    fn analyze_convergence_properties(&mut self) -> Result<ConvergenceAnalysisResults> {
960        let mut results = ConvergenceAnalysisResults::new();
961
962        // Test convergence on different problem types
963        let convergence_tests = self.run_convergence_tests()?;
964        results.convergence_tests = convergence_tests;
965
966        // Analyze convergence speed
967        let speed_analysis = self.analyze_convergence_speed(&results.convergence_tests)?;
968        results.speed_analysis = speed_analysis;
969
970        // Test convergence stability
971        let stability_analysis = self.analyze_convergence_stability(&results.convergence_tests)?;
972        results.stability_analysis = stability_analysis;
973
974        Ok(results)
975    }
976
977    fn run_convergence_tests(&self) -> Result<HashMap<String, ConvergenceTestResult>> {
978        let mut results = HashMap::new();
979
980        let optimizers_to_test = vec![
981            ("Adam", OptimizerType::Adam),
982            ("AdamW", OptimizerType::AdamW),
983            ("AveragedAdam", OptimizerType::AveragedAdam),
984            ("SGD", OptimizerType::SGD),
985        ];
986
987        for (name, optimizer_type) in optimizers_to_test {
988            let convergence_result = self.test_optimizer_convergence(name, optimizer_type)?;
989            results.insert(name.to_string(), convergence_result);
990        }
991
992        Ok(results)
993    }
994
995    /// Runs a *real* optimization problem and measures the loss it actually reaches.
996    ///
997    /// The objective is the separable quadratic `f(θ) = Σ θ²`, whose gradient `2θ` is
998    /// computed from the current parameters on every iteration. Both the gradient and
999    /// the loss therefore come from the parameters the optimizer produced — the
1000    /// previous version stepped the optimizer and then reported a hard-coded
1001    /// exponential decay curve that could not have been affected by it.
1002    fn test_optimizer_convergence(
1003        &self,
1004        _name: &str,
1005        optimizer_type: OptimizerType,
1006    ) -> Result<ConvergenceTestResult> {
1007        let mut optimizer = self.create_optimizer_instance(optimizer_type)?;
1008        let mut parameters = create_test_parameters(vec![32, 32])?;
1009
1010        // The objective evaluated on the live parameters.
1011        let evaluate = |params: &HashMap<String, Tensor>| -> Result<f32> {
1012            let mut total = 0.0_f32;
1013            let mut count = 0_usize;
1014            for tensor in params.values() {
1015                for value in tensor.data_f32()? {
1016                    total += value * value;
1017                    count += 1;
1018                }
1019            }
1020            Ok(if count == 0 { 0.0 } else { total / count as f32 })
1021        };
1022
1023        let initial_loss = evaluate(&parameters)?;
1024        let mut loss_history = Vec::new();
1025        let mut current_loss = initial_loss;
1026
1027        let max_iterations = 500;
1028        let mut converged = false;
1029        let mut convergence_iteration = max_iterations;
1030
1031        // Deterministic parameter visit order keeps anonymous identities stable.
1032        let mut names: Vec<String> = parameters.keys().cloned().collect();
1033        names.sort();
1034
1035        for iteration in 0..max_iterations {
1036            for name in &names {
1037                let Some(parameter) = parameters.get_mut(name) else {
1038                    continue;
1039                };
1040                let values = parameter.data_f32()?;
1041                let gradient = Tensor::from_vec(
1042                    values.iter().map(|v| 2.0 * v).collect::<Vec<f32>>(),
1043                    &parameter.shape(),
1044                )?;
1045                optimizer.update(parameter, &gradient)?;
1046            }
1047            optimizer.step();
1048
1049            current_loss = evaluate(&parameters)?;
1050            loss_history.push(current_loss);
1051
1052            if current_loss < 1e-4 && !converged {
1053                converged = true;
1054                convergence_iteration = iteration;
1055                break;
1056            }
1057        }
1058
1059        let convergence_rate = if converged {
1060            1.0 - (convergence_iteration as f64 / max_iterations as f64)
1061        } else {
1062            0.0
1063        };
1064
1065        let final_loss = current_loss;
1066        let loss_reduction = if initial_loss > 0.0 {
1067            (initial_loss - final_loss) / initial_loss
1068        } else {
1069            0.0
1070        };
1071
1072        Ok(ConvergenceTestResult {
1073            converged,
1074            convergence_iteration,
1075            convergence_rate,
1076            final_loss,
1077            loss_reduction,
1078            loss_history,
1079        })
1080    }
1081
1082    /// Convergence speed derived from the *measured* loss curves.
1083    ///
1084    /// Speed is the fraction of the iteration budget still unused when the loss first
1085    /// dropped below 1% of its initial value; a run that never got there scores 0.
1086    fn analyze_convergence_speed(
1087        &self,
1088        tests: &HashMap<String, ConvergenceTestResult>,
1089    ) -> Result<HashMap<String, f64>> {
1090        let mut results = HashMap::new();
1091        for (name, test) in tests {
1092            let Some(&initial) = test.loss_history.first() else {
1093                continue;
1094            };
1095            let target = initial * 0.01;
1096            let reached = test.loss_history.iter().position(|&loss| loss <= target);
1097            let total = test.loss_history.len().max(1) as f64;
1098            let speed = match reached {
1099                Some(index) => 1.0 - (index as f64 / total),
1100                None => 0.0,
1101            };
1102            results.insert(name.clone(), speed);
1103        }
1104        Ok(results)
1105    }
1106
1107    /// Convergence stability derived from the *measured* loss curves.
1108    ///
1109    /// Stability is the fraction of steps in which the loss did not increase.
1110    fn analyze_convergence_stability(
1111        &self,
1112        tests: &HashMap<String, ConvergenceTestResult>,
1113    ) -> Result<HashMap<String, f64>> {
1114        let mut results = HashMap::new();
1115        for (name, test) in tests {
1116            if test.loss_history.len() < 2 {
1117                continue;
1118            }
1119            let monotone = test
1120                .loss_history
1121                .windows(2)
1122                .filter(|pair| pair[1] <= pair[0] + f32::EPSILON)
1123                .count();
1124            let stability = monotone as f64 / (test.loss_history.len() - 1) as f64;
1125            results.insert(name.clone(), stability);
1126        }
1127        Ok(results)
1128    }
1129
1130    /// Validate distributed training components
1131    fn validate_distributed_training(&mut self) -> Result<DistributedValidationResults> {
1132        let mut results = DistributedValidationResults::new();
1133
1134        // Test distributed training scaling
1135        let scaling_results = self.test_distributed_scaling()?;
1136        results.scaling_results = scaling_results;
1137
1138        // Test communication efficiency
1139        let communication_results = self.test_communication_efficiency()?;
1140        results.communication_results = communication_results;
1141
1142        // Test fault tolerance
1143        let fault_tolerance_results = self.test_fault_tolerance()?;
1144        results.fault_tolerance_results = fault_tolerance_results;
1145
1146        Ok(results)
1147    }
1148
1149    /// Measures the real memory reduction ZeRO parameter sharding delivers.
1150    ///
1151    /// A single process cannot measure multi-GPU speedup, so no speedup figure is
1152    /// reported. What *is* measurable is the fraction of a parameter each rank has to
1153    /// hold once [`crate::zero::partition_parameters`] shards it, and that is what
1154    /// this reports: `1 − shard_bytes / full_bytes` for rank 0 at each world size.
1155    fn test_distributed_scaling(&self) -> Result<HashMap<String, f64>> {
1156        use crate::zero::partition_parameters;
1157
1158        let mut results = HashMap::new();
1159        let parameters = create_test_parameters(vec![64, 64])?;
1160        let full_elements: usize =
1161            parameters.values().map(|t| t.shape().iter().product::<usize>()).sum();
1162        if full_elements == 0 {
1163            return Err(TrustformersError::invalid_state(
1164                "ZeRO validation needs a non-empty parameter set".to_string(),
1165            ));
1166        }
1167
1168        for world_size in [1_usize, 2, 4, 8] {
1169            let partitions = partition_parameters(&parameters, world_size, 0)?;
1170            let shard_elements: usize = partitions
1171                .values()
1172                .map(|p| p.local_shard.data_f32().map(|d| d.len()).unwrap_or(0))
1173                .sum();
1174            let reduction = 1.0 - (shard_elements as f64 / full_elements as f64);
1175            results.insert(
1176                format!("{world_size}-rank-parameter-memory-reduction"),
1177                reduction,
1178            );
1179        }
1180
1181        Ok(results)
1182    }
1183
1184    /// Verifies the collective primitives round-trip exactly.
1185    ///
1186    /// There is no wire here, so "communication efficiency" is not measurable; what is
1187    /// measurable — and much more useful — is whether shard → gather reconstructs the
1188    /// original tensor bit-for-bit. The reported value is the fraction of parameters
1189    /// that round-tripped exactly at each world size.
1190    fn test_communication_efficiency(&self) -> Result<HashMap<String, f64>> {
1191        use crate::zero::{gather_shards, partition_parameters};
1192
1193        let mut results = HashMap::new();
1194        let parameters = create_test_parameters(vec![16, 16])?;
1195
1196        for world_size in [2_usize, 3, 5] {
1197            let mut exact = 0_usize;
1198            for (name, tensor) in &parameters {
1199                let mut shards = Vec::with_capacity(world_size);
1200                for rank in 0..world_size {
1201                    let partitions = partition_parameters(&parameters, world_size, rank)?;
1202                    let shard =
1203                        partitions.get(name).map(|p| p.local_shard.clone()).ok_or_else(|| {
1204                            TrustformersError::invalid_state(format!(
1205                                "no shard produced for '{name}'"
1206                            ))
1207                        })?;
1208                    shards.push(shard);
1209                }
1210                let gathered = gather_shards(&shards, &tensor.shape())?;
1211                if gathered.data_f32()? == tensor.data_f32()? {
1212                    exact += 1;
1213                }
1214            }
1215            let fraction = exact as f64 / parameters.len().max(1) as f64;
1216            results.insert(format!("{world_size}-rank-gather-exactness"), fraction);
1217        }
1218
1219        Ok(results)
1220    }
1221
1222    /// Exercises the recovery paths that *can* be executed in one process.
1223    ///
1224    /// Node-failure and network-partition handling need a real cluster and are
1225    /// therefore not claimed. Checkpoint recovery is executed for real: an optimizer
1226    /// is stepped, checkpointed, restored into a fresh instance, and the restored
1227    /// state is compared against the original.
1228    fn test_fault_tolerance(&self) -> Result<HashMap<String, bool>> {
1229        use crate::traits::StatefulOptimizer;
1230
1231        let mut results = HashMap::new();
1232
1233        let parameters = create_test_parameters(vec![8, 8])?;
1234        let gradients = create_benchmark_gradients(&[8, 8], 3)?;
1235
1236        let mut original = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1237        let mut names: Vec<String> = parameters.keys().cloned().collect();
1238        names.sort();
1239        for name in &names {
1240            let (Some(parameter), Some(gradient)) = (parameters.get(name), gradients.get(name))
1241            else {
1242                continue;
1243            };
1244            let mut working = parameter.clone();
1245            original.update_named(name, &mut working, gradient)?;
1246        }
1247
1248        let checkpoint = StatefulOptimizer::state_dict(&original)?;
1249        let mut restored = Adam::new(0.1, (0.5, 0.5), 1e-3, 0.5);
1250        StatefulOptimizer::load_state_dict(&mut restored, checkpoint.clone())?;
1251        let round_trip = StatefulOptimizer::state_dict(&restored)?;
1252
1253        let mut identical = round_trip.len() == checkpoint.len();
1254        for (key, tensor) in &checkpoint {
1255            match round_trip.get(key) {
1256                Some(other) if other.data_f32()? == tensor.data_f32()? => {},
1257                _ => identical = false,
1258            }
1259        }
1260        results.insert("CheckpointRecovery".to_string(), identical);
1261
1262        Ok(results)
1263    }
1264
1265    /// Detect performance regressions compared to baseline
1266    fn detect_performance_regressions(
1267        &mut self,
1268        current_results: &PerformanceBenchmarkResults,
1269    ) -> Result<RegressionAnalysisResults> {
1270        let baseline = self.baseline_results.as_ref().ok_or_else(|| {
1271            TrustformersError::invalid_state(
1272                "baseline_results must be set before detecting regressions".to_string(),
1273            )
1274        })?;
1275        let mut results = RegressionAnalysisResults::new();
1276
1277        for scenario_result in &current_results.scenario_results {
1278            for (optimizer_name, current_benchmark) in &scenario_result.optimizer_results {
1279                if let Some(baseline_benchmark) = baseline.get(optimizer_name) {
1280                    let regression = self.regression_detector.detect_regression(
1281                        baseline_benchmark,
1282                        current_benchmark,
1283                        self.config.max_regression_threshold,
1284                    )?;
1285
1286                    if let Some(regression_info) = regression {
1287                        results.regressions.push(regression_info);
1288                    }
1289                }
1290            }
1291        }
1292
1293        Ok(results)
1294    }
1295
1296    /// Generate comprehensive validation report
1297    pub fn generate_validation_report(&self, results: &ValidationResults) -> Result<String> {
1298        let mut report = String::new();
1299
1300        report.push_str("# TrustformeRS Optimization Performance Validation Report\\n");
1301        report.push_str("=====================================================\\n\\n");
1302
1303        // Executive Summary
1304        report.push_str("## Executive Summary\\n");
1305        report.push_str(&format!(
1306            "- **Total Validation Time**: {:.2} seconds\\n",
1307            results.total_validation_time.as_secs_f64()
1308        ));
1309        report.push_str(&format!(
1310            "- **Correctness Tests**: {}/{} passed ({:.1}%)\\n",
1311            results.correctness_results.passed_tests,
1312            results.correctness_results.total_tests,
1313            results.correctness_results.overall_correctness_rate * 100.0
1314        ));
1315
1316        // Performance Summary
1317        report.push_str("\\n## Performance Benchmark Summary\\n");
1318        for scenario_result in &results.performance_results.scenario_results {
1319            report.push_str(&format!("### {}\\n", scenario_result.scenario_name));
1320
1321            let mut sorted_optimizers: Vec<_> = scenario_result.optimizer_results.iter().collect();
1322            sorted_optimizers.sort_by_key(|a| a.1.avg_step_time);
1323
1324            for (name, result) in sorted_optimizers {
1325                report.push_str(&format!(
1326                    "- **{}**: {:.2}ms/step, {:.1}M params/sec\\n",
1327                    name,
1328                    result.avg_step_time.as_secs_f64() * 1000.0,
1329                    result.throughput / 1_000_000.0
1330                ));
1331            }
1332        }
1333
1334        // Memory Efficiency
1335        if let Some(memory_results) = &results.memory_results {
1336            report.push_str("\\n## Memory Efficiency Validation\\n");
1337            for (optimizer, efficiency) in &memory_results.eight_bit_efficiency {
1338                report.push_str(&format!(
1339                    "- **{}**: {:.1}% memory reduction\\n",
1340                    optimizer, efficiency
1341                ));
1342            }
1343        }
1344
1345        // Convergence Analysis
1346        if let Some(convergence_results) = &results.convergence_results {
1347            report.push_str("\\n## Convergence Analysis\\n");
1348            for (optimizer, test_result) in &convergence_results.convergence_tests {
1349                report.push_str(&format!(
1350                    "- **{}**: {} (rate: {:.3}, reduction: {:.3})\\n",
1351                    optimizer,
1352                    if test_result.converged { "Converged" } else { "Did not converge" },
1353                    test_result.convergence_rate,
1354                    test_result.loss_reduction
1355                ));
1356            }
1357        }
1358
1359        // Regression Detection
1360        if let Some(regression_results) = &results.regression_results {
1361            report.push_str("\\n## Performance Regression Analysis\\n");
1362            if regression_results.regressions.is_empty() {
1363                report.push_str("✅ No performance regressions detected\\n");
1364            } else {
1365                for regression in &regression_results.regressions {
1366                    report.push_str(&format!(
1367                        "⚠️  **{}**: {:.1}% performance regression\\n",
1368                        regression.optimizer_name, regression.regression_percentage
1369                    ));
1370                }
1371            }
1372        }
1373
1374        report.push_str("\\n## Validation Status: ✅ COMPLETE\\n");
1375
1376        Ok(report)
1377    }
1378
1379    /// Set baseline results for regression detection
1380    pub fn set_baseline(&mut self, results: HashMap<String, BenchmarkResult>) {
1381        self.baseline_results = Some(results);
1382    }
1383}
1384
1385// Supporting types and implementations
1386
1387#[derive(Debug, Clone, Serialize, Deserialize)]
1388pub struct ValidationResults {
1389    pub total_validation_time: Duration,
1390    pub correctness_results: CorrectnessResults,
1391    pub performance_results: PerformanceBenchmarkResults,
1392    pub memory_results: Option<MemoryValidationResults>,
1393    pub convergence_results: Option<ConvergenceAnalysisResults>,
1394    pub distributed_results: Option<DistributedValidationResults>,
1395    pub regression_results: Option<RegressionAnalysisResults>,
1396}
1397
1398impl Default for ValidationResults {
1399    fn default() -> Self {
1400        Self::new()
1401    }
1402}
1403
1404impl ValidationResults {
1405    pub fn new() -> Self {
1406        Self {
1407            total_validation_time: Duration::from_secs(0),
1408            correctness_results: CorrectnessResults::new(),
1409            performance_results: PerformanceBenchmarkResults::new(),
1410            memory_results: None,
1411            convergence_results: None,
1412            distributed_results: None,
1413            regression_results: None,
1414        }
1415    }
1416}
1417
1418#[derive(Debug, Clone, Serialize, Deserialize)]
1419pub struct ValidationSession {
1420    pub timestamp: std::time::SystemTime,
1421    pub config: ValidationConfig,
1422    pub results: ValidationResults,
1423}
1424
1425#[derive(Debug, Clone, Serialize, Deserialize)]
1426pub struct CorrectnessResults {
1427    pub optimizer_correctness: HashMap<String, bool>,
1428    pub overall_correctness_rate: f64,
1429    pub passed_tests: usize,
1430    pub total_tests: usize,
1431}
1432
1433impl Default for CorrectnessResults {
1434    fn default() -> Self {
1435        Self::new()
1436    }
1437}
1438
1439impl CorrectnessResults {
1440    pub fn new() -> Self {
1441        Self {
1442            optimizer_correctness: HashMap::new(),
1443            overall_correctness_rate: 0.0,
1444            passed_tests: 0,
1445            total_tests: 0,
1446        }
1447    }
1448}
1449
1450#[derive(Debug, Clone, Serialize, Deserialize)]
1451pub struct PerformanceBenchmarkResults {
1452    pub scenario_results: Vec<ScenarioBenchmarkResult>,
1453    pub scaling_analysis: HashMap<String, f64>,
1454}
1455
1456impl Default for PerformanceBenchmarkResults {
1457    fn default() -> Self {
1458        Self::new()
1459    }
1460}
1461
1462impl PerformanceBenchmarkResults {
1463    pub fn new() -> Self {
1464        Self {
1465            scenario_results: Vec::new(),
1466            scaling_analysis: HashMap::new(),
1467        }
1468    }
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct ScenarioBenchmarkResult {
1473    pub scenario_name: String,
1474    pub optimizer_results: HashMap<String, OptimizerBenchmarkResult>,
1475}
1476
1477#[derive(Debug, Clone, Serialize, Deserialize)]
1478pub struct OptimizerBenchmarkResult {
1479    pub optimizer_name: String,
1480    pub avg_step_time: Duration,
1481    pub min_step_time: Duration,
1482    pub max_step_time: Duration,
1483    pub throughput: f64,
1484    /// The optimizer's real allocated state memory (bytes) at the end of
1485    /// the run, from `BenchmarkOptimizer::state_memory_bytes`. `None`
1486    /// when the optimizer kind exposes no such accessor (currently only
1487    /// `LAMB`). Despite the field's name this is a single end-of-run
1488    /// reading, not an average of varying samples: every optimizer kind
1489    /// this can measure allocates its state buffers on first touch and
1490    /// never resizes them again within one benchmark run (parameter shapes
1491    /// are fixed), so there is nothing that actually varies to average. An
1492    /// earlier version computed a before/after delta from parameter
1493    /// *shapes* alone (ignoring the optimizer entirely), which was
1494    /// therefore always exactly `0.0` regardless of which optimizer ran.
1495    pub avg_memory_usage: Option<usize>,
1496    pub statistical_metrics: Option<StatisticalMetrics>,
1497}
1498
1499#[derive(Debug, Clone, Serialize, Deserialize)]
1500pub struct StatisticalMetrics {
1501    pub mean: Duration,
1502    pub std_dev: Duration,
1503    pub confidence_interval_lower: Duration,
1504    pub confidence_interval_upper: Duration,
1505    /// Two-sided p-value of a one-sample Student-t test of `step_times`
1506    /// against a null hypothesis mean, computed by
1507    /// [`StatisticalAnalyzer::analyze`].
1508    ///
1509    /// A p-value needs a null hypothesis to test against. `benchmark_optimizer`
1510    /// passes the matching optimizer's [`BenchmarkResult::avg_step_time`] from
1511    /// `PerformanceValidator::baseline_results` as that hypothesis when one has
1512    /// been set via [`PerformanceValidator::set_baseline`]; a low p-value then
1513    /// means this run's step times are statistically distinguishable from the
1514    /// baseline's average, in either direction. `None` when no baseline is set
1515    /// for the optimizer being benchmarked (nothing to test against) -- left
1516    /// absent rather than fabricated as a constant.
1517    pub p_value: Option<f64>,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct MemoryValidationResults {
1522    pub eight_bit_efficiency: HashMap<String, f64>,
1523    pub compression_efficiency: HashMap<String, f64>,
1524    pub optimization_efficiency: HashMap<String, f64>,
1525}
1526
1527impl Default for MemoryValidationResults {
1528    fn default() -> Self {
1529        Self::new()
1530    }
1531}
1532
1533impl MemoryValidationResults {
1534    pub fn new() -> Self {
1535        Self {
1536            eight_bit_efficiency: HashMap::new(),
1537            compression_efficiency: HashMap::new(),
1538            optimization_efficiency: HashMap::new(),
1539        }
1540    }
1541}
1542
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1544pub struct ConvergenceAnalysisResults {
1545    pub convergence_tests: HashMap<String, ConvergenceTestResult>,
1546    pub speed_analysis: HashMap<String, f64>,
1547    pub stability_analysis: HashMap<String, f64>,
1548}
1549
1550impl Default for ConvergenceAnalysisResults {
1551    fn default() -> Self {
1552        Self::new()
1553    }
1554}
1555
1556impl ConvergenceAnalysisResults {
1557    pub fn new() -> Self {
1558        Self {
1559            convergence_tests: HashMap::new(),
1560            speed_analysis: HashMap::new(),
1561            stability_analysis: HashMap::new(),
1562        }
1563    }
1564}
1565
1566#[derive(Debug, Clone, Serialize, Deserialize)]
1567pub struct ConvergenceTestResult {
1568    pub converged: bool,
1569    pub convergence_iteration: usize,
1570    pub convergence_rate: f64,
1571    pub final_loss: f32,
1572    pub loss_reduction: f32,
1573    pub loss_history: Vec<f32>,
1574}
1575
1576#[derive(Debug, Clone, Serialize, Deserialize)]
1577pub struct DistributedValidationResults {
1578    pub scaling_results: HashMap<String, f64>,
1579    pub communication_results: HashMap<String, f64>,
1580    pub fault_tolerance_results: HashMap<String, bool>,
1581}
1582
1583impl Default for DistributedValidationResults {
1584    fn default() -> Self {
1585        Self::new()
1586    }
1587}
1588
1589impl DistributedValidationResults {
1590    pub fn new() -> Self {
1591        Self {
1592            scaling_results: HashMap::new(),
1593            communication_results: HashMap::new(),
1594            fault_tolerance_results: HashMap::new(),
1595        }
1596    }
1597}
1598
1599#[derive(Debug, Clone, Serialize, Deserialize)]
1600pub struct RegressionAnalysisResults {
1601    pub regressions: Vec<RegressionInfo>,
1602}
1603
1604impl Default for RegressionAnalysisResults {
1605    fn default() -> Self {
1606        Self::new()
1607    }
1608}
1609
1610impl RegressionAnalysisResults {
1611    pub fn new() -> Self {
1612        Self {
1613            regressions: Vec::new(),
1614        }
1615    }
1616}
1617
1618#[derive(Debug, Clone, Serialize, Deserialize)]
1619pub struct RegressionInfo {
1620    pub optimizer_name: String,
1621    pub metric_name: String,
1622    pub baseline_value: f64,
1623    pub current_value: f64,
1624    pub regression_percentage: f64,
1625}
1626
1627#[derive(Debug, Clone)]
1628pub struct MathematicalTestCase {
1629    pub name: String,
1630    pub description: String,
1631    pub parameters: HashMap<String, Tensor>,
1632    pub gradients: HashMap<String, Tensor>,
1633    pub expected_properties: Vec<MathematicalProperty>,
1634    pub tolerance: f64,
1635}
1636
1637#[derive(Debug, Clone, PartialEq)]
1638pub enum MathematicalProperty {
1639    Convergence,
1640    MonotonicImprovement,
1641    GlobalOptimum,
1642    SparsityHandling,
1643    StableConvergence,
1644}
1645
1646#[derive(Debug, Clone)]
1647pub struct BenchmarkScenario {
1648    pub name: String,
1649    pub parameter_sizes: Vec<usize>,
1650    pub batch_size: usize,
1651    pub iterations: usize,
1652}
1653
1654#[derive(Debug, Clone)]
1655pub enum OptimizerType {
1656    Adam,
1657    AdamW,
1658    SGD,
1659    AveragedAdam,
1660    LAMB,
1661    Lion,
1662}
1663
1664#[derive(Debug, Clone, Serialize, Deserialize)]
1665pub struct BenchmarkResult {
1666    pub avg_step_time: Duration,
1667    pub throughput: f64,
1668    pub memory_usage: f64,
1669}
1670
1671/// Statistical analyzer for performance metrics
1672pub struct StatisticalAnalyzer;
1673
1674impl Default for StatisticalAnalyzer {
1675    fn default() -> Self {
1676        Self::new()
1677    }
1678}
1679
1680impl StatisticalAnalyzer {
1681    pub fn new() -> Self {
1682        Self
1683    }
1684
1685    /// Computes mean/std-dev/confidence-interval from `step_times`, plus a
1686    /// two-sided one-sample Student-t p-value against `target_step_time`
1687    /// when the caller supplies one -- see [`StatisticalMetrics::p_value`]'s
1688    /// doc comment for what the null hypothesis means and why it is
1689    /// sometimes absent. Uses the real Student-t distribution (via
1690    /// [`trustformers_core::statistics`]), not a normal approximation, so it
1691    /// stays accurate at the small sample sizes benchmarks typically use.
1692    pub fn analyze(
1693        &self,
1694        step_times: &[Duration],
1695        confidence_level: f64,
1696        target_step_time: Option<Duration>,
1697    ) -> Result<StatisticalMetrics> {
1698        let times_f64: Vec<f64> = step_times.iter().map(|d| d.as_secs_f64()).collect();
1699
1700        let mean_f64 = trustformers_core::statistics::mean(&times_f64).ok_or_else(|| {
1701            TrustformersError::invalid_state(
1702                "StatisticalAnalyzer::analyze requires at least one step time".to_string(),
1703            )
1704        })?;
1705        // `None` for fewer than two samples: there is no sample variance --
1706        // and therefore no t-test -- with a single observation. The reported
1707        // std-dev/CI fall back to 0.0 in that case, which is exactly correct
1708        // for a single-point sample (no spread was observed).
1709        let sample_std_f64 = trustformers_core::statistics::sample_std_dev(&times_f64);
1710        let std_dev_f64 = sample_std_f64.unwrap_or(0.0);
1711
1712        // Simple confidence interval calculation (assuming normal distribution)
1713        let z_score = if confidence_level >= 0.99 {
1714            2.576
1715        } else if confidence_level >= 0.95 {
1716            1.96
1717        } else {
1718            1.645
1719        };
1720        let margin_of_error = z_score * std_dev_f64 / (times_f64.len() as f64).sqrt();
1721
1722        // One-sample Student-t test of `step_times` against `target_step_time`:
1723        // needs both a real target and a real sample standard deviation
1724        // (n >= 2), or there is no test to run. `student_t_two_sided_p_value`
1725        // already resolves the `standard_error == 0.0` (zero-variance) case
1726        // correctly (an infinite or NaN t statistic), so no special-casing is
1727        // needed here.
1728        let p_value = match (target_step_time, sample_std_f64) {
1729            (Some(target), Some(sample_std)) => {
1730                let standard_error = sample_std / (times_f64.len() as f64).sqrt();
1731                let t_statistic = (mean_f64 - target.as_secs_f64()) / standard_error;
1732                let degrees_of_freedom = times_f64.len() as f64 - 1.0;
1733                trustformers_core::statistics::student_t_two_sided_p_value(
1734                    t_statistic,
1735                    degrees_of_freedom,
1736                )
1737            },
1738            _ => None,
1739        };
1740
1741        Ok(StatisticalMetrics {
1742            mean: Duration::from_secs_f64(mean_f64),
1743            std_dev: Duration::from_secs_f64(std_dev_f64),
1744            confidence_interval_lower: Duration::from_secs_f64(
1745                (mean_f64 - margin_of_error).max(0.0),
1746            ),
1747            confidence_interval_upper: Duration::from_secs_f64(mean_f64 + margin_of_error),
1748            p_value,
1749        })
1750    }
1751}
1752
1753/// Memory analyzer for optimization memory patterns
1754pub struct MemoryAnalyzer;
1755
1756impl Default for MemoryAnalyzer {
1757    fn default() -> Self {
1758        Self::new()
1759    }
1760}
1761
1762impl MemoryAnalyzer {
1763    pub fn new() -> Self {
1764        Self
1765    }
1766}
1767
1768/// Convergence analyzer for optimization convergence patterns
1769pub struct ConvergenceAnalyzer;
1770
1771impl Default for ConvergenceAnalyzer {
1772    fn default() -> Self {
1773        Self::new()
1774    }
1775}
1776
1777impl ConvergenceAnalyzer {
1778    pub fn new() -> Self {
1779        Self
1780    }
1781}
1782
1783/// Regression detector for performance regression analysis
1784pub struct RegressionDetector;
1785
1786impl Default for RegressionDetector {
1787    fn default() -> Self {
1788        Self::new()
1789    }
1790}
1791
1792impl RegressionDetector {
1793    pub fn new() -> Self {
1794        Self
1795    }
1796
1797    pub fn detect_regression(
1798        &self,
1799        baseline: &BenchmarkResult,
1800        current: &OptimizerBenchmarkResult,
1801        threshold_percentage: f64,
1802    ) -> Result<Option<RegressionInfo>> {
1803        let baseline_time = baseline.avg_step_time.as_secs_f64();
1804        let current_time = current.avg_step_time.as_secs_f64();
1805
1806        let regression_percentage = ((current_time - baseline_time) / baseline_time) * 100.0;
1807
1808        if regression_percentage > threshold_percentage {
1809            Ok(Some(RegressionInfo {
1810                optimizer_name: current.optimizer_name.clone(),
1811                metric_name: "avg_step_time".to_string(),
1812                baseline_value: baseline_time,
1813                current_value: current_time,
1814                regression_percentage,
1815            }))
1816        } else {
1817            Ok(None)
1818        }
1819    }
1820}
1821
1822// Utility functions for creating test data
1823
1824fn create_test_parameters(sizes: Vec<usize>) -> Result<HashMap<String, Tensor>> {
1825    let mut parameters = HashMap::new();
1826
1827    for (i, &size) in sizes.iter().enumerate() {
1828        let param_name = format!("param_{}", i);
1829        let tensor = Tensor::randn(&[size])?;
1830        parameters.insert(param_name, tensor);
1831    }
1832
1833    Ok(parameters)
1834}
1835
1836fn create_quadratic_gradients(sizes: Vec<usize>) -> Result<HashMap<String, Tensor>> {
1837    let mut gradients = HashMap::new();
1838
1839    for (i, &size) in sizes.iter().enumerate() {
1840        let grad_name = format!("param_{}", i);
1841        // For quadratic function f(x) = 0.5 * x^T * x, gradient is x
1842        let gradient = Tensor::randn(&[size])?;
1843        gradients.insert(grad_name, gradient);
1844    }
1845
1846    Ok(gradients)
1847}
1848
1849fn create_convex_gradients(sizes: Vec<usize>) -> Result<HashMap<String, Tensor>> {
1850    let mut gradients = HashMap::new();
1851
1852    for (i, &size) in sizes.iter().enumerate() {
1853        let grad_name = format!("param_{}", i);
1854        let gradient = Tensor::randn(&[size])?.scalar_mul(2.0)?; // 2x for convex function
1855        gradients.insert(grad_name, gradient);
1856    }
1857
1858    Ok(gradients)
1859}
1860
1861fn create_sparse_gradients(sizes: Vec<usize>, _sparsity: f32) -> Result<HashMap<String, Tensor>> {
1862    let mut gradients = HashMap::new();
1863
1864    for (i, &size) in sizes.iter().enumerate() {
1865        let grad_name = format!("param_{}", i);
1866        let _gradient = Tensor::randn(&[size])?;
1867
1868        // Make gradient sparse by zeroing out elements
1869        // In a real implementation, would properly handle sparse tensors
1870        let sparse_gradient = Tensor::zeros(&[size])?; // Simplified sparse representation
1871        gradients.insert(grad_name, sparse_gradient);
1872    }
1873
1874    Ok(gradients)
1875}
1876
1877fn create_benchmark_gradients(
1878    sizes: &[usize],
1879    iteration: usize,
1880) -> Result<HashMap<String, Tensor>> {
1881    let mut gradients = HashMap::new();
1882
1883    let scale = 0.1 / (1.0 + iteration as f32 * 0.01); // Decreasing gradient norms
1884
1885    for (i, &size) in sizes.iter().enumerate() {
1886        let grad_name = format!("param_{}", i);
1887        let gradient = Tensor::randn(&[size])?.scalar_mul(scale)?;
1888        gradients.insert(grad_name, gradient);
1889    }
1890
1891    Ok(gradients)
1892}
1893
1894#[cfg(test)]
1895#[path = "performance_validation_tests.rs"]
1896mod performance_validation_tests;