trustformers_debug/model_diagnostics/
performance.rs1use super::types::{ModelPerformanceMetrics, PerformanceSummary};
8
9#[derive(Debug)]
11pub struct PerformanceAnalyzer {
12 performance_history: Vec<ModelPerformanceMetrics>,
14 max_history_length: usize,
16 thresholds: PerformanceThresholds,
18}
19
20#[derive(Debug, Clone)]
22pub struct PerformanceThresholds {
23 pub max_memory_mb: f64,
25 pub min_throughput: f64,
27 pub max_loss_increase_percent: f64,
29 pub max_loss_variance: f64,
31}
32
33impl Default for PerformanceThresholds {
34 fn default() -> Self {
35 Self {
36 max_memory_mb: 8192.0, min_throughput: 100.0,
38 max_loss_increase_percent: 10.0,
39 max_loss_variance: 0.1,
40 }
41 }
42}
43
44impl PerformanceAnalyzer {
45 pub fn new() -> Self {
47 Self {
48 performance_history: Vec::new(),
49 max_history_length: 1000,
50 thresholds: PerformanceThresholds::default(),
51 }
52 }
53
54 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 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 pub fn record_performance(&mut self, metrics: ModelPerformanceMetrics) {
73 self.performance_history.push(metrics);
74
75 if self.performance_history.len() > self.max_history_length {
77 self.performance_history.remove(0);
78 }
79 }
80
81 pub fn record_metrics(&mut self, metrics: ModelPerformanceMetrics) {
83 self.record_performance(metrics);
84 }
85
86 pub fn get_performance_history(&self) -> &[ModelPerformanceMetrics] {
88 &self.performance_history
89 }
90
91 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 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 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 if let Some(anomaly) = self.detect_memory_leak() {
162 anomalies.push(anomaly);
163 }
164
165 if let Some(anomaly) = self.detect_performance_degradation() {
167 anomalies.push(anomaly);
168 }
169
170 if let Some(anomaly) = self.detect_training_instability() {
172 anomalies.push(anomaly);
173 }
174
175 if let Some(anomaly) = self.detect_throughput_drops() {
177 anomalies.push(anomaly);
178 }
179
180 anomalies
181 }
182
183 pub fn generate_optimization_recommendations(&self) -> Vec<OptimizationRecommendation> {
185 let mut recommendations = Vec::new();
186 let summary = self.generate_performance_summary();
187
188 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 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 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 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 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 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 let trend_strength = trend.abs();
281 let confidence = trend_strength / (1.0 + volatility);
282 confidence.min(1.0)
283 }
284
285 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 if memory_trend > 10.0 {
297 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 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 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 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 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#[derive(Debug, Clone)]
418pub struct PerformanceTrends {
419 pub loss_trend: f64,
421 pub throughput_trend: f64,
423 pub memory_trend: f64,
425 pub loss_volatility: f64,
427 pub throughput_volatility: f64,
429 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#[derive(Debug, Clone)]
448pub struct PerformanceAnomaly {
449 pub anomaly_type: AnomalyType,
451 pub severity: AnomalySeverity,
453 pub description: String,
455 pub detected_at_step: usize,
457 pub confidence: f64,
459}
460
461#[derive(Debug, Clone)]
463pub enum AnomalyType {
464 MemoryLeak,
466 PerformanceDegradation,
468 TrainingInstability,
470 ThroughputDrop,
472}
473
474#[derive(Debug, Clone)]
476pub enum AnomalySeverity {
477 Low,
479 Medium,
481 High,
483 Critical,
485}
486
487#[derive(Debug, Clone)]
489pub struct OptimizationRecommendation {
490 pub category: String,
492 pub priority: PerformanceRecommendationPriority,
494 pub description: String,
496 pub suggestion: String,
498 pub expected_improvement: f64,
500}
501
502#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
504pub enum PerformanceRecommendationPriority {
505 Low,
507 Medium,
509 High,
511 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 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); }
577
578 #[test]
579 fn test_memory_leak_detection() {
580 let mut analyzer = PerformanceAnalyzer::new();
581
582 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}