Skip to main content

trustformers_debug/model_diagnostics/
performance.rs

1//! Performance metrics and analysis functionality.
2//!
3//! This module provides comprehensive performance monitoring and analysis
4//! capabilities including throughput analysis, memory usage tracking,
5//! performance trend analysis, and performance optimization recommendations.
6
7use super::types::{ModelPerformanceMetrics, PerformanceSummary};
8
9/// Performance analyzer for tracking and analyzing model performance metrics.
10#[derive(Debug)]
11pub struct PerformanceAnalyzer {
12    /// Historical performance metrics
13    performance_history: Vec<ModelPerformanceMetrics>,
14    /// Maximum history length to maintain
15    max_history_length: usize,
16    /// Performance thresholds for alerts
17    thresholds: PerformanceThresholds,
18}
19
20/// Performance thresholds for triggering alerts.
21#[derive(Debug, Clone)]
22pub struct PerformanceThresholds {
23    /// Maximum acceptable memory usage in MB
24    pub max_memory_mb: f64,
25    /// Minimum acceptable throughput in samples/sec
26    pub min_throughput: f64,
27    /// Maximum acceptable loss increase percentage
28    pub max_loss_increase_percent: f64,
29    /// Maximum acceptable variance in loss
30    pub max_loss_variance: f64,
31}
32
33impl Default for PerformanceThresholds {
34    fn default() -> Self {
35        Self {
36            max_memory_mb: 8192.0, // 8GB
37            min_throughput: 100.0,
38            max_loss_increase_percent: 10.0,
39            max_loss_variance: 0.1,
40        }
41    }
42}
43
44impl PerformanceAnalyzer {
45    /// Create a new performance analyzer.
46    pub fn new() -> Self {
47        Self {
48            performance_history: Vec::new(),
49            max_history_length: 1000,
50            thresholds: PerformanceThresholds::default(),
51        }
52    }
53
54    /// Create a new performance analyzer with custom thresholds.
55    pub fn with_thresholds(thresholds: PerformanceThresholds) -> Self {
56        Self {
57            performance_history: Vec::new(),
58            max_history_length: 1000,
59            thresholds,
60        }
61    }
62
63    /// Set maximum history length.
64    pub fn set_max_history_length(&mut self, length: usize) {
65        self.max_history_length = length;
66        if self.performance_history.len() > length {
67            self.performance_history.drain(0..self.performance_history.len() - length);
68        }
69    }
70
71    /// Record a new performance measurement.
72    pub fn record_performance(&mut self, metrics: ModelPerformanceMetrics) {
73        self.performance_history.push(metrics);
74
75        // Maintain maximum history length
76        if self.performance_history.len() > self.max_history_length {
77            self.performance_history.remove(0);
78        }
79    }
80
81    /// Record metrics (alias for record_performance).
82    pub fn record_metrics(&mut self, metrics: ModelPerformanceMetrics) {
83        self.record_performance(metrics);
84    }
85
86    /// Get the complete performance history.
87    pub fn get_performance_history(&self) -> &[ModelPerformanceMetrics] {
88        &self.performance_history
89    }
90
91    /// Generate a performance summary.
92    pub fn generate_performance_summary(&self) -> PerformanceSummary {
93        let Some(current_metrics) = self.performance_history.last() else {
94            return PerformanceSummary::default();
95        };
96
97        let total_steps = self.performance_history.len();
98
99        let losses: Vec<f64> = self.performance_history.iter().map(|m| m.loss).collect();
100        let throughputs: Vec<f64> =
101            self.performance_history.iter().map(|m| m.throughput_samples_per_sec).collect();
102        let memory_usages: Vec<f64> =
103            self.performance_history.iter().map(|m| m.memory_usage_mb).collect();
104
105        let best_loss = losses.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
106        let avg_loss = losses.iter().sum::<f64>() / losses.len() as f64;
107        let avg_throughput = throughputs.iter().sum::<f64>() / throughputs.len() as f64;
108        let peak_memory_mb = memory_usages.iter().fold(0.0f64, |acc, &x| acc.max(x));
109        let avg_memory_mb = memory_usages.iter().sum::<f64>() / memory_usages.len() as f64;
110
111        PerformanceSummary {
112            total_steps,
113            current_loss: current_metrics.loss,
114            best_loss,
115            avg_loss,
116            current_throughput: current_metrics.throughput_samples_per_sec,
117            avg_throughput,
118            peak_memory_mb,
119            avg_memory_mb,
120        }
121    }
122
123    /// Analyze performance trends.
124    pub fn analyze_performance_trends(&self) -> PerformanceTrends {
125        if self.performance_history.len() < 10 {
126            return PerformanceTrends::default();
127        }
128
129        let losses: Vec<f64> = self.performance_history.iter().map(|m| m.loss).collect();
130        let throughputs: Vec<f64> =
131            self.performance_history.iter().map(|m| m.throughput_samples_per_sec).collect();
132        let memory_usages: Vec<f64> =
133            self.performance_history.iter().map(|m| m.memory_usage_mb).collect();
134
135        let loss_trend = self.compute_trend(&losses);
136        let throughput_trend = self.compute_trend(&throughputs);
137        let memory_trend = self.compute_trend(&memory_usages);
138
139        let loss_volatility = self.compute_volatility(&losses);
140        let throughput_volatility = self.compute_volatility(&throughputs);
141
142        PerformanceTrends {
143            loss_trend,
144            throughput_trend,
145            memory_trend,
146            loss_volatility,
147            throughput_volatility,
148            trend_confidence: self.compute_trend_confidence(&losses),
149        }
150    }
151
152    /// Check for performance anomalies.
153    pub fn detect_performance_anomalies(&self) -> Vec<PerformanceAnomaly> {
154        let mut anomalies = Vec::new();
155
156        if self.performance_history.len() < 5 {
157            return anomalies;
158        }
159
160        // Check for memory leaks
161        if let Some(anomaly) = self.detect_memory_leak() {
162            anomalies.push(anomaly);
163        }
164
165        // Check for performance degradation
166        if let Some(anomaly) = self.detect_performance_degradation() {
167            anomalies.push(anomaly);
168        }
169
170        // Check for training instability
171        if let Some(anomaly) = self.detect_training_instability() {
172            anomalies.push(anomaly);
173        }
174
175        // Check for throughput drops
176        if let Some(anomaly) = self.detect_throughput_drops() {
177            anomalies.push(anomaly);
178        }
179
180        anomalies
181    }
182
183    /// Generate performance optimization recommendations.
184    pub fn generate_optimization_recommendations(&self) -> Vec<OptimizationRecommendation> {
185        let mut recommendations = Vec::new();
186        let summary = self.generate_performance_summary();
187
188        // Memory optimization recommendations
189        if summary.peak_memory_mb > self.thresholds.max_memory_mb {
190            recommendations.push(OptimizationRecommendation {
191                category: "Memory".to_string(),
192                priority: PerformanceRecommendationPriority::High,
193                description: "High memory usage detected".to_string(),
194                suggestion: "Consider reducing batch size or using gradient checkpointing"
195                    .to_string(),
196                expected_improvement: 0.3,
197            });
198        }
199
200        // Throughput optimization recommendations
201        if summary.avg_throughput < self.thresholds.min_throughput {
202            recommendations.push(OptimizationRecommendation {
203                category: "Throughput".to_string(),
204                priority: PerformanceRecommendationPriority::Medium,
205                description: "Low throughput detected".to_string(),
206                suggestion: "Consider increasing batch size or optimizing data loading".to_string(),
207                expected_improvement: 0.4,
208            });
209        }
210
211        // Loss optimization recommendations
212        let trends = self.analyze_performance_trends();
213        if trends.loss_trend > 0.01 {
214            recommendations.push(OptimizationRecommendation {
215                category: "Training".to_string(),
216                priority: PerformanceRecommendationPriority::High,
217                description: "Loss is increasing".to_string(),
218                suggestion: "Consider reducing learning rate or adding regularization".to_string(),
219                expected_improvement: 0.5,
220            });
221        }
222
223        recommendations
224    }
225
226    /// Compute linear trend for a series of values.
227    fn compute_trend(&self, values: &[f64]) -> f64 {
228        if values.len() < 2 {
229            return 0.0;
230        }
231
232        let n = values.len() as f64;
233        let x_mean = (n - 1.0) / 2.0;
234        let y_mean = values.iter().sum::<f64>() / n;
235
236        let mut numerator = 0.0;
237        let mut denominator = 0.0;
238
239        for (i, &y) in values.iter().enumerate() {
240            let x = i as f64;
241            numerator += (x - x_mean) * (y - y_mean);
242            denominator += (x - x_mean).powi(2);
243        }
244
245        if denominator == 0.0 {
246            0.0
247        } else {
248            numerator / denominator
249        }
250    }
251
252    /// Compute volatility (coefficient of variation) for a series of values.
253    fn compute_volatility(&self, values: &[f64]) -> f64 {
254        if values.len() < 2 {
255            return 0.0;
256        }
257
258        let mean = values.iter().sum::<f64>() / values.len() as f64;
259        let variance =
260            values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
261        let std_dev = variance.sqrt();
262
263        if mean == 0.0 {
264            0.0
265        } else {
266            std_dev / mean.abs()
267        }
268    }
269
270    /// Compute confidence in trend analysis.
271    fn compute_trend_confidence(&self, values: &[f64]) -> f64 {
272        if values.len() < 10 {
273            return 0.0;
274        }
275
276        let trend = self.compute_trend(values);
277        let volatility = self.compute_volatility(values);
278
279        // Higher confidence for stronger trends with lower volatility
280        let trend_strength = trend.abs();
281        let confidence = trend_strength / (1.0 + volatility);
282        confidence.min(1.0)
283    }
284
285    /// Detect memory leak patterns.
286    fn detect_memory_leak(&self) -> Option<PerformanceAnomaly> {
287        if self.performance_history.len() < 10 {
288            return None;
289        }
290
291        let recent_metrics = &self.performance_history[self.performance_history.len() - 10..];
292        let memory_usages: Vec<f64> = recent_metrics.iter().map(|m| m.memory_usage_mb).collect();
293        let memory_trend = self.compute_trend(&memory_usages);
294
295        // Consider it a memory leak if memory is consistently growing
296        if memory_trend > 10.0 {
297            // More than 10MB increase per step on average
298            Some(PerformanceAnomaly {
299                anomaly_type: AnomalyType::MemoryLeak,
300                severity: AnomalySeverity::High,
301                description: format!("Memory usage increasing at {:.1} MB/step", memory_trend),
302                detected_at_step: self
303                    .performance_history
304                    .last()
305                    .map(|m| m.training_step)
306                    .unwrap_or(0),
307                confidence: 0.8,
308            })
309        } else {
310            None
311        }
312    }
313
314    /// Detect performance degradation.
315    fn detect_performance_degradation(&self) -> Option<PerformanceAnomaly> {
316        if self.performance_history.len() < 20 {
317            return None;
318        }
319
320        let recent_metrics = &self.performance_history[self.performance_history.len() - 10..];
321        let previous_metrics = &self.performance_history
322            [self.performance_history.len() - 20..self.performance_history.len() - 10];
323
324        let recent_avg_loss: f64 =
325            recent_metrics.iter().map(|m| m.loss).sum::<f64>() / recent_metrics.len() as f64;
326        let previous_avg_loss: f64 =
327            previous_metrics.iter().map(|m| m.loss).sum::<f64>() / previous_metrics.len() as f64;
328
329        let degradation_percent =
330            ((recent_avg_loss - previous_avg_loss) / previous_avg_loss) * 100.0;
331
332        if degradation_percent > self.thresholds.max_loss_increase_percent {
333            Some(PerformanceAnomaly {
334                anomaly_type: AnomalyType::PerformanceDegradation,
335                severity: AnomalySeverity::High,
336                description: format!("Performance degraded by {:.1}%", degradation_percent),
337                detected_at_step: self
338                    .performance_history
339                    .last()
340                    .map(|m| m.training_step)
341                    .unwrap_or(0),
342                confidence: 0.9,
343            })
344        } else {
345            None
346        }
347    }
348
349    /// Detect training instability.
350    fn detect_training_instability(&self) -> Option<PerformanceAnomaly> {
351        if self.performance_history.len() < 10 {
352            return None;
353        }
354
355        let recent_metrics = &self.performance_history[self.performance_history.len() - 10..];
356        let losses: Vec<f64> = recent_metrics.iter().map(|m| m.loss).collect();
357        let volatility = self.compute_volatility(&losses);
358
359        if volatility > self.thresholds.max_loss_variance {
360            Some(PerformanceAnomaly {
361                anomaly_type: AnomalyType::TrainingInstability,
362                severity: AnomalySeverity::Medium,
363                description: format!("High loss volatility: {:.3}", volatility),
364                detected_at_step: self
365                    .performance_history
366                    .last()
367                    .map(|m| m.training_step)
368                    .unwrap_or(0),
369                confidence: 0.7,
370            })
371        } else {
372            None
373        }
374    }
375
376    /// Detect throughput drops.
377    fn detect_throughput_drops(&self) -> Option<PerformanceAnomaly> {
378        if self.performance_history.len() < 10 {
379            return None;
380        }
381
382        let recent_metrics = &self.performance_history[self.performance_history.len() - 5..];
383        let avg_recent_throughput: f64 =
384            recent_metrics.iter().map(|m| m.throughput_samples_per_sec).sum::<f64>()
385                / recent_metrics.len() as f64;
386
387        if avg_recent_throughput < self.thresholds.min_throughput {
388            Some(PerformanceAnomaly {
389                anomaly_type: AnomalyType::ThroughputDrop,
390                severity: AnomalySeverity::Medium,
391                description: format!("Low throughput: {:.1} samples/sec", avg_recent_throughput),
392                detected_at_step: self
393                    .performance_history
394                    .last()
395                    .map(|m| m.training_step)
396                    .unwrap_or(0),
397                confidence: 0.8,
398            })
399        } else {
400            None
401        }
402    }
403
404    /// Clear performance history.
405    pub fn clear(&mut self) {
406        self.performance_history.clear();
407    }
408}
409
410impl Default for PerformanceAnalyzer {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416/// Performance trends analysis results.
417#[derive(Debug, Clone)]
418pub struct PerformanceTrends {
419    /// Loss trend (slope)
420    pub loss_trend: f64,
421    /// Throughput trend (slope)
422    pub throughput_trend: f64,
423    /// Memory usage trend (slope)
424    pub memory_trend: f64,
425    /// Loss volatility (coefficient of variation)
426    pub loss_volatility: f64,
427    /// Throughput volatility (coefficient of variation)
428    pub throughput_volatility: f64,
429    /// Confidence in trend analysis
430    pub trend_confidence: f64,
431}
432
433impl Default for PerformanceTrends {
434    fn default() -> Self {
435        Self {
436            loss_trend: 0.0,
437            throughput_trend: 0.0,
438            memory_trend: 0.0,
439            loss_volatility: 0.0,
440            throughput_volatility: 0.0,
441            trend_confidence: 0.0,
442        }
443    }
444}
445
446/// Performance anomaly detection results.
447#[derive(Debug, Clone)]
448pub struct PerformanceAnomaly {
449    /// Type of anomaly detected
450    pub anomaly_type: AnomalyType,
451    /// Severity of the anomaly
452    pub severity: AnomalySeverity,
453    /// Description of the anomaly
454    pub description: String,
455    /// Training step when anomaly was detected
456    pub detected_at_step: usize,
457    /// Confidence in the detection
458    pub confidence: f64,
459}
460
461/// Types of performance anomalies.
462#[derive(Debug, Clone)]
463pub enum AnomalyType {
464    /// Memory leak detected
465    MemoryLeak,
466    /// Performance degradation detected
467    PerformanceDegradation,
468    /// Training instability detected
469    TrainingInstability,
470    /// Throughput drop detected
471    ThroughputDrop,
472}
473
474/// Severity levels for anomalies.
475#[derive(Debug, Clone)]
476pub enum AnomalySeverity {
477    /// Low severity anomaly
478    Low,
479    /// Medium severity anomaly
480    Medium,
481    /// High severity anomaly
482    High,
483    /// Critical severity anomaly
484    Critical,
485}
486
487/// Performance optimization recommendation.
488#[derive(Debug, Clone)]
489pub struct OptimizationRecommendation {
490    /// Category of optimization
491    pub category: String,
492    /// Priority of the recommendation
493    pub priority: PerformanceRecommendationPriority,
494    /// Description of the issue
495    pub description: String,
496    /// Suggested optimization
497    pub suggestion: String,
498    /// Expected improvement (0.0 to 1.0)
499    pub expected_improvement: f64,
500}
501
502/// Priority levels for recommendations.
503#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
504pub enum PerformanceRecommendationPriority {
505    /// Low priority recommendation
506    Low,
507    /// Medium priority recommendation
508    Medium,
509    /// High priority recommendation
510    High,
511    /// Critical priority recommendation
512    Critical,
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use chrono::Utc;
519
520    fn create_test_metrics(
521        step: usize,
522        loss: f64,
523        memory: f64,
524        throughput: f64,
525    ) -> ModelPerformanceMetrics {
526        ModelPerformanceMetrics {
527            training_step: step,
528            loss,
529            accuracy: Some(0.8),
530            learning_rate: 0.001,
531            batch_size: 32,
532            throughput_samples_per_sec: throughput,
533            memory_usage_mb: memory,
534            gpu_utilization: Some(0.9),
535            timestamp: Utc::now(),
536        }
537    }
538
539    #[test]
540    fn test_performance_analyzer_creation() {
541        let analyzer = PerformanceAnalyzer::new();
542        assert_eq!(analyzer.performance_history.len(), 0);
543        assert_eq!(analyzer.max_history_length, 1000);
544    }
545
546    #[test]
547    fn test_record_performance() {
548        let mut analyzer = PerformanceAnalyzer::new();
549        let metrics = create_test_metrics(1, 0.5, 1000.0, 100.0);
550
551        analyzer.record_performance(metrics);
552        assert_eq!(analyzer.performance_history.len(), 1);
553    }
554
555    #[test]
556    fn test_performance_summary() {
557        let mut analyzer = PerformanceAnalyzer::new();
558
559        // Add some test data
560        for i in 1..=5 {
561            let metrics = create_test_metrics(i, 1.0 / i as f64, 1000.0, 100.0);
562            analyzer.record_performance(metrics);
563        }
564
565        let summary = analyzer.generate_performance_summary();
566        assert_eq!(summary.total_steps, 5);
567        assert!(summary.best_loss < summary.avg_loss);
568    }
569
570    #[test]
571    fn test_trend_computation() {
572        let analyzer = PerformanceAnalyzer::new();
573        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
574        let trend = analyzer.compute_trend(&values);
575        assert!(trend > 0.0); // Should be positive trend
576    }
577
578    #[test]
579    fn test_memory_leak_detection() {
580        let mut analyzer = PerformanceAnalyzer::new();
581
582        // Add metrics with increasing memory usage
583        for i in 1..=15 {
584            let metrics = create_test_metrics(i, 0.5, 1000.0 + (i as f64 * 50.0), 100.0);
585            analyzer.record_performance(metrics);
586        }
587
588        let anomalies = analyzer.detect_performance_anomalies();
589        assert!(!anomalies.is_empty());
590        assert!(matches!(anomalies[0].anomaly_type, AnomalyType::MemoryLeak));
591    }
592}