Skip to main content

trustformers_debug/gradient_debugger/
performance_tracking.rs

1//! Performance Tracking and Bottleneck Analysis for Gradient Computation
2//!
3//! This module provides comprehensive performance tracking capabilities for gradient
4//! computation, including bottleneck identification, throughput analysis, and
5//! resource utilization monitoring.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11/// Performance tracking for gradient computation
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GradientPerformanceTracker {
14    pub total_gradient_computations: usize,
15    pub average_computation_time: Duration,
16    /// Sum of the per-layer average memory usages, over the layers that
17    /// actually reported a memory sample.
18    ///
19    /// `None` while no layer has reported one. It used to be a plain `usize`
20    /// that stayed at `0`, because the only in-tree caller of
21    /// [`Self::record_layer_performance`] passed a hardcoded `0` -- publishing
22    /// "this run used zero bytes" as if it had been measured.
23    pub memory_usage_bytes: Option<usize>,
24    pub throughput_gradients_per_second: f64,
25    pub bottleneck_layers: Vec<String>,
26    pub layer_performance_map: HashMap<String, LayerPerformanceMetrics>,
27    pub resource_utilization: ResourceUtilization,
28    pub performance_history: Vec<PerformanceSnapshot>,
29}
30
31impl Default for GradientPerformanceTracker {
32    fn default() -> Self {
33        Self {
34            total_gradient_computations: 0,
35            average_computation_time: Duration::from_millis(0),
36            memory_usage_bytes: None,
37            throughput_gradients_per_second: 0.0,
38            bottleneck_layers: Vec::new(),
39            layer_performance_map: HashMap::new(),
40            resource_utilization: ResourceUtilization::default(),
41            performance_history: Vec::new(),
42        }
43    }
44}
45
46impl GradientPerformanceTracker {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    pub fn start_timing(&mut self, layer_name: &str) -> PerformanceTimer {
52        PerformanceTimer::new(layer_name.to_string())
53    }
54
55    /// Record one timing (and optionally one memory sample) for `layer_name`.
56    ///
57    /// `memory_used` is `None` when the caller has no real memory figure, which
58    /// keeps the aggregates honestly absent instead of averaging in a
59    /// fabricated zero.
60    pub fn record_layer_performance(
61        &mut self,
62        layer_name: &str,
63        computation_time: Duration,
64        memory_used: Option<usize>,
65    ) {
66        let metrics = self
67            .layer_performance_map
68            .entry(layer_name.to_string())
69            .or_insert_with(|| LayerPerformanceMetrics::new(layer_name.to_string()));
70
71        metrics.update(computation_time, memory_used);
72        self.total_gradient_computations += 1;
73
74        // Update overall averages
75        self.update_overall_metrics();
76        self.identify_bottlenecks();
77    }
78
79    fn update_overall_metrics(&mut self) {
80        if self.layer_performance_map.is_empty() {
81            return;
82        }
83
84        let total_time: Duration =
85            self.layer_performance_map.values().map(|m| m.average_computation_time).sum();
86
87        let total_layers = self.layer_performance_map.len();
88        self.average_computation_time = total_time / total_layers as u32;
89
90        // Only layers that really reported memory contribute; if none did, the
91        // aggregate stays absent.
92        let mut memory_total = None;
93        for average in self.layer_performance_map.values().filter_map(|m| m.average_memory_usage) {
94            memory_total = Some(memory_total.unwrap_or(0) + average);
95        }
96        self.memory_usage_bytes = memory_total;
97
98        // Calculate throughput
99        if self.average_computation_time.as_secs_f64() > 0.0 {
100            self.throughput_gradients_per_second =
101                1.0 / self.average_computation_time.as_secs_f64();
102        }
103    }
104
105    fn identify_bottlenecks(&mut self) {
106        self.bottleneck_layers.clear();
107
108        if self.layer_performance_map.len() < 2 {
109            return;
110        }
111
112        // Calculate mean and standard deviation of computation times
113        let times: Vec<f64> = self
114            .layer_performance_map
115            .values()
116            .map(|m| m.average_computation_time.as_secs_f64())
117            .collect();
118
119        let mean = times.iter().sum::<f64>() / times.len() as f64;
120        let variance = times.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / times.len() as f64;
121        let std_dev = variance.sqrt();
122
123        // Identify layers that are significantly slower than average
124        let threshold = mean + 1.5 * std_dev;
125
126        for (layer_name, metrics) in &self.layer_performance_map {
127            if metrics.average_computation_time.as_secs_f64() > threshold {
128                self.bottleneck_layers.push(layer_name.clone());
129            }
130        }
131    }
132
133    pub fn get_performance_trends(&self) -> PerformanceTrends {
134        if self.performance_history.len() < 2 {
135            return PerformanceTrends::default();
136        }
137
138        let recent_snapshots: Vec<&PerformanceSnapshot> =
139            self.performance_history.iter().rev().take(10).collect();
140
141        let older_snapshots: Vec<&PerformanceSnapshot> =
142            self.performance_history.iter().rev().skip(10).take(10).collect();
143
144        if older_snapshots.is_empty() {
145            return PerformanceTrends::default();
146        }
147
148        let recent_avg_throughput = recent_snapshots.iter().map(|s| s.throughput).sum::<f64>()
149            / recent_snapshots.len() as f64;
150
151        let older_avg_throughput = older_snapshots.iter().map(|s| s.throughput).sum::<f64>()
152            / older_snapshots.len() as f64;
153
154        // Average only over the snapshots that carry a real memory figure; when
155        // either window has none, the memory trend is honestly unknown.
156        let mean_memory = |window: &[&PerformanceSnapshot]| -> Option<f64> {
157            let samples: Vec<usize> = window.iter().filter_map(|s| s.memory_usage).collect();
158            if samples.is_empty() {
159                None
160            } else {
161                Some(samples.iter().sum::<usize>() as f64 / samples.len() as f64)
162            }
163        };
164        let memory_trend = match (
165            mean_memory(&recent_snapshots),
166            mean_memory(&older_snapshots),
167        ) {
168            (Some(recent), Some(older)) => Some(Self::classify_trend(recent, older)),
169            _ => None,
170        };
171
172        PerformanceTrends {
173            throughput_trend: Self::classify_trend(recent_avg_throughput, older_avg_throughput),
174            memory_trend,
175            bottleneck_stability: self
176                .analyze_bottleneck_stability(&recent_snapshots, &older_snapshots),
177            overall_performance_direction: self
178                .analyze_overall_direction(&recent_snapshots, &older_snapshots),
179        }
180    }
181
182    fn classify_trend(recent: f64, older: f64) -> TrendDirection {
183        let change_ratio = (recent - older) / older.max(1e-10);
184        let threshold = 0.05; // 5% change threshold
185
186        if change_ratio > threshold {
187            TrendDirection::Improving
188        } else if change_ratio < -threshold {
189            TrendDirection::Degrading
190        } else {
191            TrendDirection::Stable
192        }
193    }
194
195    fn analyze_bottleneck_stability(
196        &self,
197        recent: &[&PerformanceSnapshot],
198        older: &[&PerformanceSnapshot],
199    ) -> BottleneckStability {
200        let recent_bottlenecks: std::collections::HashSet<&String> =
201            recent.iter().flat_map(|s| &s.active_bottlenecks).collect();
202
203        let older_bottlenecks: std::collections::HashSet<&String> =
204            older.iter().flat_map(|s| &s.active_bottlenecks).collect();
205
206        let intersection_size = recent_bottlenecks.intersection(&older_bottlenecks).count();
207        let union_size = recent_bottlenecks.union(&older_bottlenecks).count();
208
209        if union_size == 0 {
210            return BottleneckStability::Stable;
211        }
212
213        let stability_ratio = intersection_size as f64 / union_size as f64;
214
215        if stability_ratio > 0.8 {
216            BottleneckStability::Stable
217        } else if stability_ratio > 0.5 {
218            BottleneckStability::Moderate
219        } else {
220            BottleneckStability::Unstable
221        }
222    }
223
224    fn analyze_overall_direction(
225        &self,
226        recent: &[&PerformanceSnapshot],
227        older: &[&PerformanceSnapshot],
228    ) -> PerformanceDirection {
229        let recent_avg_time =
230            recent.iter().map(|s| s.average_time.as_secs_f64()).sum::<f64>() / recent.len() as f64;
231
232        let older_avg_time =
233            older.iter().map(|s| s.average_time.as_secs_f64()).sum::<f64>() / older.len() as f64;
234
235        if recent_avg_time < older_avg_time * 0.95 {
236            PerformanceDirection::Improving
237        } else if recent_avg_time > older_avg_time * 1.05 {
238            PerformanceDirection::Degrading
239        } else {
240            PerformanceDirection::Stable
241        }
242    }
243
244    pub fn generate_optimization_recommendations(&self) -> Vec<OptimizationRecommendation> {
245        let mut recommendations = Vec::new();
246
247        // Analyze bottlenecks
248        for layer_name in &self.bottleneck_layers {
249            if let Some(metrics) = self.layer_performance_map.get(layer_name) {
250                recommendations.push(OptimizationRecommendation {
251                    layer_name: layer_name.clone(),
252                    issue_type: OptimizationIssue::ComputationalBottleneck,
253                    severity: self.calculate_bottleneck_severity(metrics),
254                    recommendations: vec![
255                        format!("Consider optimizing {} layer computation", layer_name),
256                        "Check for inefficient operations or memory access patterns".to_string(),
257                        "Consider layer-specific optimizations or hardware acceleration"
258                            .to_string(),
259                    ],
260                    expected_improvement: self.estimate_improvement_potential(metrics),
261                });
262            }
263        }
264
265        // Memory usage analysis -- only when a real figure was reported.
266        if self.memory_usage_bytes.is_some_and(|bytes| bytes > 1_000_000_000) {
267            // > 1GB
268            recommendations.push(OptimizationRecommendation {
269                layer_name: "Global".to_string(),
270                issue_type: OptimizationIssue::HighMemoryUsage,
271                severity: OptimizationSeverity::High,
272                recommendations: vec![
273                    "Consider gradient checkpointing to reduce memory usage".to_string(),
274                    "Optimize batch size and sequence length".to_string(),
275                    "Use memory-efficient attention mechanisms".to_string(),
276                ],
277                expected_improvement: 0.3,
278            });
279        }
280
281        // Low throughput analysis
282        if self.throughput_gradients_per_second < 1.0 {
283            recommendations.push(OptimizationRecommendation {
284                layer_name: "Global".to_string(),
285                issue_type: OptimizationIssue::LowThroughput,
286                severity: OptimizationSeverity::Medium,
287                recommendations: vec![
288                    "Consider mixed precision training".to_string(),
289                    "Optimize data loading and preprocessing pipelines".to_string(),
290                    "Use gradient accumulation for larger effective batch sizes".to_string(),
291                ],
292                expected_improvement: 0.4,
293            });
294        }
295
296        recommendations
297    }
298
299    fn calculate_bottleneck_severity(
300        &self,
301        metrics: &LayerPerformanceMetrics,
302    ) -> OptimizationSeverity {
303        let relative_slowness = metrics.average_computation_time.as_secs_f64()
304            / self.average_computation_time.as_secs_f64();
305
306        if relative_slowness > 3.0 {
307            OptimizationSeverity::Critical
308        } else if relative_slowness > 2.0 {
309            OptimizationSeverity::High
310        } else if relative_slowness > 1.5 {
311            OptimizationSeverity::Medium
312        } else {
313            OptimizationSeverity::Low
314        }
315    }
316
317    fn estimate_improvement_potential(&self, metrics: &LayerPerformanceMetrics) -> f64 {
318        let relative_slowness = metrics.average_computation_time.as_secs_f64()
319            / self.average_computation_time.as_secs_f64();
320
321        // Estimate potential improvement based on how much slower this layer is
322        (relative_slowness - 1.0).min(0.8).max(0.1)
323    }
324
325    /// Start monitoring performance
326    pub fn start_monitoring(&mut self) {
327        // Reset performance tracking state
328        self.total_gradient_computations = 0;
329        self.average_computation_time = Duration::from_millis(0);
330        self.memory_usage_bytes = None;
331        self.throughput_gradients_per_second = 0.0;
332        self.bottleneck_layers.clear();
333        self.layer_performance_map.clear();
334
335        // Initialize resource utilization monitoring
336        self.resource_utilization = ResourceUtilization {
337            cpu_usage_percent: 0.0,
338            memory_usage_percent: 0.0,
339            gpu_usage_percent: 0.0,
340            io_wait_percent: 0.0,
341        };
342    }
343
344    /// Take a performance snapshot
345    pub fn take_performance_snapshot(&self) -> PerformanceSnapshot {
346        PerformanceSnapshot {
347            timestamp: std::time::SystemTime::now(),
348            total_computations: self.total_gradient_computations,
349            average_time: self.average_computation_time,
350            memory_usage: self.memory_usage_bytes,
351            throughput: self.throughput_gradients_per_second,
352            active_bottlenecks: self.bottleneck_layers.clone(),
353            layer_count: self.layer_performance_map.len(),
354        }
355    }
356}
357
358/// Layer-specific performance metrics
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct LayerPerformanceMetrics {
361    pub layer_name: String,
362    pub computation_count: usize,
363    pub total_computation_time: Duration,
364    pub average_computation_time: Duration,
365    /// Sum of the real memory samples reported for this layer; `None` until
366    /// one is reported.
367    pub total_memory_usage: Option<usize>,
368    /// Mean of the real memory samples reported for this layer; `None` until
369    /// one is reported.
370    pub average_memory_usage: Option<usize>,
371    /// How many memory samples went into the two fields above (which is not
372    /// the same as `computation_count`: timings are always recorded, memory
373    /// only when the caller has a real figure).
374    pub memory_sample_count: usize,
375    pub min_computation_time: Duration,
376    pub max_computation_time: Duration,
377    pub performance_variance: f64,
378}
379
380impl LayerPerformanceMetrics {
381    pub fn new(layer_name: String) -> Self {
382        Self {
383            layer_name,
384            computation_count: 0,
385            total_computation_time: Duration::from_millis(0),
386            average_computation_time: Duration::from_millis(0),
387            total_memory_usage: None,
388            average_memory_usage: None,
389            memory_sample_count: 0,
390            min_computation_time: Duration::from_secs(u64::MAX),
391            max_computation_time: Duration::from_millis(0),
392            performance_variance: 0.0,
393        }
394    }
395
396    pub fn update(&mut self, computation_time: Duration, memory_used: Option<usize>) {
397        self.computation_count += 1;
398        self.total_computation_time += computation_time;
399        if let Some(bytes) = memory_used {
400            self.memory_sample_count += 1;
401            let total = self.total_memory_usage.unwrap_or(0) + bytes;
402            self.total_memory_usage = Some(total);
403            self.average_memory_usage = Some(total / self.memory_sample_count);
404        }
405
406        self.average_computation_time = self.total_computation_time / self.computation_count as u32;
407
408        if computation_time < self.min_computation_time {
409            self.min_computation_time = computation_time;
410        }
411        if computation_time > self.max_computation_time {
412            self.max_computation_time = computation_time;
413        }
414
415        self.update_variance(computation_time);
416    }
417
418    fn update_variance(&mut self, new_time: Duration) {
419        if self.computation_count < 2 {
420            self.performance_variance = 0.0;
421            return;
422        }
423
424        let mean = self.average_computation_time.as_secs_f64();
425        let new_value = new_time.as_secs_f64();
426
427        // Incremental variance calculation
428        let old_variance = self.performance_variance;
429        let delta = new_value - mean;
430        self.performance_variance = ((self.computation_count - 1) as f64 * old_variance
431            + delta * delta)
432            / self.computation_count as f64;
433    }
434}
435
436/// Performance timer for measuring gradient computation time
437#[derive(Debug)]
438pub struct PerformanceTimer {
439    layer_name: String,
440    start_time: Instant,
441}
442
443impl PerformanceTimer {
444    pub fn new(layer_name: String) -> Self {
445        Self {
446            layer_name,
447            start_time: Instant::now(),
448        }
449    }
450
451    pub fn finish(self) -> (String, Duration) {
452        (self.layer_name, self.start_time.elapsed())
453    }
454}
455
456/// Resource utilization metrics
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ResourceUtilization {
459    pub cpu_usage_percent: f64,
460    pub gpu_usage_percent: f64,
461    pub memory_usage_percent: f64,
462    pub io_wait_percent: f64,
463}
464
465impl Default for ResourceUtilization {
466    fn default() -> Self {
467        Self {
468            cpu_usage_percent: 0.0,
469            gpu_usage_percent: 0.0,
470            memory_usage_percent: 0.0,
471            io_wait_percent: 0.0,
472        }
473    }
474}
475
476/// Performance snapshot at a point in time
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct PerformanceSnapshot {
479    pub timestamp: std::time::SystemTime,
480    pub total_computations: usize,
481    pub average_time: Duration,
482    /// Aggregate memory usage at snapshot time; `None` when no layer had
483    /// reported a real memory sample yet.
484    pub memory_usage: Option<usize>,
485    pub throughput: f64,
486    pub active_bottlenecks: Vec<String>,
487    pub layer_count: usize,
488}
489
490/// Performance trends analysis
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct PerformanceTrends {
493    pub throughput_trend: TrendDirection,
494    /// Direction of the memory trend, or `None` when neither comparison window
495    /// contained a snapshot with a real memory figure.
496    pub memory_trend: Option<TrendDirection>,
497    pub bottleneck_stability: BottleneckStability,
498    pub overall_performance_direction: PerformanceDirection,
499}
500
501impl Default for PerformanceTrends {
502    fn default() -> Self {
503        Self {
504            throughput_trend: TrendDirection::Stable,
505            memory_trend: None,
506            bottleneck_stability: BottleneckStability::Stable,
507            overall_performance_direction: PerformanceDirection::Stable,
508        }
509    }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub enum TrendDirection {
514    Improving,
515    Stable,
516    Degrading,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub enum BottleneckStability {
521    Stable,
522    Moderate,
523    Unstable,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub enum PerformanceDirection {
528    Improving,
529    Stable,
530    Degrading,
531}
532
533/// Optimization recommendation
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct OptimizationRecommendation {
536    pub layer_name: String,
537    pub issue_type: OptimizationIssue,
538    pub severity: OptimizationSeverity,
539    pub recommendations: Vec<String>,
540    pub expected_improvement: f64,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub enum OptimizationIssue {
545    ComputationalBottleneck,
546    HighMemoryUsage,
547    LowThroughput,
548    ResourceUnderutilization,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub enum OptimizationSeverity {
553    Low,
554    Medium,
555    High,
556    Critical,
557}