Skip to main content

torsh_tensor/
tensor_tracker.rs

1//! Tensor Value Tracking for Debugging
2//!
3//! This module provides comprehensive tensor value tracking capabilities for debugging purposes.
4//! It allows tracking tensor operations, values, and transformations with conditional compilation
5//! to avoid performance overhead in release builds.
6//!
7//! # Features
8//!
9//! - **Operation tracking**: Record all operations performed on tracked tensors
10//! - **Value snapshots**: Capture tensor values at specific points
11//! - **Transformation history**: Track how tensor values change over time
12//! - **Conditional compilation**: Zero overhead in release builds when disabled
13//! - **Filtering**: Track only specific tensors or operations
14//! - **Analysis**: Generate reports on tensor value ranges, statistics, and changes
15//!
16//! # Example
17//!
18//! ```rust
19//! use torsh_tensor::{Tensor, tensor_tracker::*};
20//! use torsh_core::device::DeviceType;
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // Create a tracked tensor
24//! let mut tracker = TensorTracker::new();
25//! let tensor = Tensor::<f32>::ones(&[2, 2], DeviceType::Cpu)?;
26//! let tracked_id = tracker.track(tensor.clone(), "input_tensor")?;
27//!
28//! // Perform operations
29//! let result = tensor.mul_scalar(2.0)?;
30//! tracker.record_operation(tracked_id, "mul_scalar", vec![2.0], &result)?;
31//!
32//! // Generate report
33//! let report = tracker.generate_report(tracked_id)?;
34//! println!("{}", report);
35//! # Ok(())
36//! # }
37//! ```
38
39use std::collections::HashMap;
40use std::fmt;
41use std::sync::{Arc, RwLock};
42use std::time::{Duration, Instant};
43use torsh_core::sync::RwLockExt;
44
45use torsh_core::{
46    dtype::TensorElement,
47    error::{Result, TorshError},
48};
49
50use crate::Tensor;
51
52/// Unique identifier for tracked tensors
53pub type TrackId = u64;
54
55/// Statistics about tensor values
56#[derive(Debug, Clone)]
57pub struct TensorValueStats<T: TensorElement> {
58    /// Minimum value in the tensor
59    pub min: Option<T>,
60    /// Maximum value in the tensor
61    pub max: Option<T>,
62    /// Mean value (if applicable)
63    pub mean: Option<f64>,
64    /// Standard deviation (if applicable)
65    pub std: Option<f64>,
66    /// Number of NaN values
67    pub nan_count: usize,
68    /// Number of Inf values
69    pub inf_count: usize,
70    /// Number of zero values
71    pub zero_count: usize,
72    /// Total number of elements
73    pub total_elements: usize,
74}
75
76impl<T: TensorElement> TensorValueStats<T> {
77    /// Create statistics from a tensor
78    pub fn from_tensor(tensor: &Tensor<T>) -> Result<Self>
79    where
80        T: Copy + PartialOrd + num_traits::Zero + num_traits::ToPrimitive,
81    {
82        let data = tensor.to_vec()?;
83        let total_elements = data.len();
84
85        let mut min = None;
86        let mut max = None;
87        let mut nan_count = 0;
88        let mut inf_count = 0;
89        let mut zero_count = 0;
90        let mut sum = 0.0f64;
91
92        for &val in &data {
93            // Check for special values
94            if let Some(f_val) = num_traits::ToPrimitive::to_f64(&val) {
95                if f_val.is_nan() {
96                    nan_count += 1;
97                    continue;
98                }
99                if f_val.is_infinite() {
100                    inf_count += 1;
101                    continue;
102                }
103                sum += f_val;
104            }
105
106            // Track min/max
107            match (min, max) {
108                (None, None) => {
109                    min = Some(val);
110                    max = Some(val);
111                }
112                (Some(current_min), Some(current_max)) => {
113                    if val < current_min {
114                        min = Some(val);
115                    }
116                    if val > current_max {
117                        max = Some(val);
118                    }
119                }
120                _ => unreachable!(),
121            }
122
123            // Count zeros
124            if val == <T as num_traits::Zero>::zero() {
125                zero_count += 1;
126            }
127        }
128
129        let mean = if total_elements > 0 && nan_count + inf_count < total_elements {
130            Some(sum / (total_elements - nan_count - inf_count) as f64)
131        } else {
132            None
133        };
134
135        // Calculate standard deviation
136        let std = if let Some(mean_val) = mean {
137            let variance: f64 = data
138                .iter()
139                .filter_map(|&v| num_traits::ToPrimitive::to_f64(&v))
140                .filter(|&f| !f.is_nan() && !f.is_infinite())
141                .map(|v| {
142                    let diff = v - mean_val;
143                    diff * diff
144                })
145                .sum::<f64>()
146                / (total_elements - nan_count - inf_count) as f64;
147            Some(variance.sqrt())
148        } else {
149            None
150        };
151
152        Ok(Self {
153            min,
154            max,
155            mean,
156            std,
157            nan_count,
158            inf_count,
159            zero_count,
160            total_elements,
161        })
162    }
163}
164
165impl<T: TensorElement + fmt::Display> fmt::Display for TensorValueStats<T> {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        writeln!(f, "Tensor Value Statistics:")?;
168        writeln!(f, "  Total elements: {}", self.total_elements)?;
169
170        if let (Some(min), Some(max)) = (&self.min, &self.max) {
171            writeln!(f, "  Min: {}", min)?;
172            writeln!(f, "  Max: {}", max)?;
173        }
174
175        if let Some(mean) = self.mean {
176            writeln!(f, "  Mean: {:.6}", mean)?;
177        }
178
179        if let Some(std) = self.std {
180            writeln!(f, "  Std: {:.6}", std)?;
181        }
182
183        if self.nan_count > 0 {
184            writeln!(f, "  NaN count: {}", self.nan_count)?;
185        }
186
187        if self.inf_count > 0 {
188            writeln!(f, "  Inf count: {}", self.inf_count)?;
189        }
190
191        if self.zero_count > 0 {
192            writeln!(f, "  Zero count: {}", self.zero_count)?;
193        }
194
195        Ok(())
196    }
197}
198
199/// Record of a tensor operation
200#[derive(Debug, Clone)]
201pub struct OperationRecord {
202    /// Name of the operation
203    pub operation: String,
204    /// Parameters used in the operation (as strings for display)
205    pub parameters: Vec<String>,
206    /// Timestamp when operation was performed
207    pub timestamp: Instant,
208    /// Duration of the operation
209    pub duration: Option<Duration>,
210    /// Shape before the operation
211    pub shape_before: Vec<usize>,
212    /// Shape after the operation
213    pub shape_after: Vec<usize>,
214}
215
216impl fmt::Display for OperationRecord {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.operation)?;
219        if !self.parameters.is_empty() {
220            write!(f, "({}) ", self.parameters.join(", "))?;
221        }
222        write!(f, ": {:?} -> {:?}", self.shape_before, self.shape_after)?;
223        if let Some(duration) = self.duration {
224            write!(f, " [{:?}]", duration)?;
225        }
226        Ok(())
227    }
228}
229
230/// A snapshot of tensor values at a specific point
231#[derive(Clone)]
232pub struct TensorSnapshot<T: TensorElement> {
233    /// The actual tensor values
234    pub values: Vec<T>,
235    /// Shape of the tensor
236    pub shape: Vec<usize>,
237    /// Timestamp when snapshot was taken
238    pub timestamp: Instant,
239    /// Label for this snapshot
240    pub label: String,
241}
242
243/// Tracked tensor information
244pub struct TrackedTensor<T: TensorElement> {
245    /// Unique identifier
246    pub id: TrackId,
247    /// Label/name for this tensor
248    pub label: String,
249    /// Original tensor reference
250    pub tensor: Tensor<T>,
251    /// History of operations performed
252    pub operations: Vec<OperationRecord>,
253    /// Value snapshots taken over time
254    pub snapshots: Vec<TensorSnapshot<T>>,
255    /// When tracking started
256    pub start_time: Instant,
257}
258
259impl<T: TensorElement> TrackedTensor<T> {
260    /// Create a new tracked tensor
261    pub fn new(id: TrackId, label: String, tensor: Tensor<T>) -> Self {
262        Self {
263            id,
264            label,
265            tensor,
266            operations: Vec::new(),
267            snapshots: Vec::new(),
268            start_time: Instant::now(),
269        }
270    }
271
272    /// Record an operation
273    pub fn record_operation(
274        &mut self,
275        operation: String,
276        parameters: Vec<String>,
277        new_tensor: &Tensor<T>,
278        duration: Option<Duration>,
279    ) {
280        let shape_before = self.tensor.shape().dims().to_vec();
281        let shape_after = new_tensor.shape().dims().to_vec();
282
283        self.operations.push(OperationRecord {
284            operation,
285            parameters,
286            timestamp: Instant::now(),
287            duration,
288            shape_before,
289            shape_after,
290        });
291
292        self.tensor = new_tensor.clone();
293    }
294
295    /// Take a snapshot of current values
296    pub fn take_snapshot(&mut self, label: String) -> Result<()>
297    where
298        T: Copy,
299    {
300        let values = self.tensor.to_vec()?;
301        let shape = self.tensor.shape().dims().to_vec();
302
303        self.snapshots.push(TensorSnapshot {
304            values,
305            shape,
306            timestamp: Instant::now(),
307            label,
308        });
309
310        Ok(())
311    }
312}
313
314/// Configuration for tensor tracking
315#[derive(Debug, Clone)]
316pub struct TrackingConfig {
317    /// Whether tracking is enabled
318    pub enabled: bool,
319    /// Maximum number of operations to track per tensor
320    pub max_operations: usize,
321    /// Maximum number of snapshots to keep per tensor
322    pub max_snapshots: usize,
323    /// Whether to automatically take snapshots after each operation
324    pub auto_snapshot: bool,
325    /// Operations to filter (empty = track all)
326    pub operation_filter: Vec<String>,
327}
328
329impl Default for TrackingConfig {
330    fn default() -> Self {
331        Self {
332            enabled: true,
333            max_operations: 1000,
334            max_snapshots: 100,
335            auto_snapshot: false,
336            operation_filter: Vec::new(),
337        }
338    }
339}
340
341impl TrackingConfig {
342    /// Create a minimal tracking config (low memory usage)
343    pub fn minimal() -> Self {
344        Self {
345            enabled: true,
346            max_operations: 100,
347            max_snapshots: 10,
348            auto_snapshot: false,
349            operation_filter: Vec::new(),
350        }
351    }
352
353    /// Create a comprehensive tracking config (high memory usage)
354    pub fn comprehensive() -> Self {
355        Self {
356            enabled: true,
357            max_operations: 10000,
358            max_snapshots: 1000,
359            auto_snapshot: true,
360            operation_filter: Vec::new(),
361        }
362    }
363
364    /// Create a config that tracks only specific operations
365    pub fn filtered(operations: Vec<String>) -> Self {
366        Self {
367            enabled: true,
368            max_operations: 1000,
369            max_snapshots: 100,
370            auto_snapshot: false,
371            operation_filter: operations,
372        }
373    }
374}
375
376/// Main tensor tracker
377pub struct TensorTracker<T: TensorElement> {
378    /// Configuration
379    config: Arc<RwLock<TrackingConfig>>,
380    /// Tracked tensors
381    tensors: Arc<RwLock<HashMap<TrackId, TrackedTensor<T>>>>,
382    /// Next ID to assign
383    next_id: Arc<RwLock<TrackId>>,
384}
385
386impl<T: TensorElement> TensorTracker<T> {
387    /// Create a new tensor tracker
388    pub fn new() -> Self {
389        Self::with_config(TrackingConfig::default())
390    }
391
392    /// Create a new tensor tracker with custom config
393    pub fn with_config(config: TrackingConfig) -> Self {
394        Self {
395            config: Arc::new(RwLock::new(config)),
396            tensors: Arc::new(RwLock::new(HashMap::new())),
397            next_id: Arc::new(RwLock::new(0)),
398        }
399    }
400
401    /// Start tracking a tensor
402    pub fn track(&mut self, tensor: Tensor<T>, label: impl Into<String>) -> Result<TrackId>
403    where
404        T: Copy,
405    {
406        let config = self.config.read_or_recover();
407        if !config.enabled {
408            return Err(TorshError::InvalidArgument(
409                "Tracking is disabled".to_string(),
410            ));
411        }
412        drop(config);
413
414        let mut next_id = self.next_id.write_or_recover();
415        let id = *next_id;
416        *next_id += 1;
417        drop(next_id);
418
419        let mut tracked = TrackedTensor::new(id, label.into(), tensor.clone());
420
421        // Take initial snapshot if auto_snapshot is enabled
422        let config = self.config.read_or_recover();
423        if config.auto_snapshot {
424            tracked.take_snapshot("initial".to_string())?;
425        }
426        drop(config);
427
428        self.tensors.write_or_recover().insert(id, tracked);
429
430        Ok(id)
431    }
432
433    /// Stop tracking a tensor
434    pub fn untrack(&mut self, id: TrackId) -> Result<()> {
435        self.tensors.write_or_recover().remove(&id);
436        Ok(())
437    }
438
439    /// Record an operation on a tracked tensor
440    pub fn record_operation<P: fmt::Display>(
441        &self,
442        id: TrackId,
443        operation: impl Into<String>,
444        parameters: Vec<P>,
445        result_tensor: &Tensor<T>,
446    ) -> Result<()>
447    where
448        T: Copy,
449    {
450        let config = self.config.read_or_recover();
451        if !config.enabled {
452            return Ok(());
453        }
454
455        let operation_str = operation.into();
456
457        // Check filter
458        if !config.operation_filter.is_empty() && !config.operation_filter.contains(&operation_str)
459        {
460            return Ok(());
461        }
462
463        let auto_snapshot = config.auto_snapshot;
464        let max_operations = config.max_operations;
465        drop(config);
466
467        let mut tensors = self.tensors.write_or_recover();
468        let tracked = tensors.get_mut(&id).ok_or_else(|| {
469            TorshError::InvalidArgument(format!("Tensor with ID {} is not tracked", id))
470        })?;
471
472        let params: Vec<String> = parameters.iter().map(|p| format!("{}", p)).collect();
473
474        tracked.record_operation(operation_str.clone(), params, result_tensor, None);
475
476        // Trim if needed
477        if tracked.operations.len() > max_operations {
478            tracked.operations.remove(0);
479        }
480
481        // Auto snapshot if enabled
482        if auto_snapshot {
483            tracked.take_snapshot(format!("after_{}", operation_str))?;
484        }
485
486        Ok(())
487    }
488
489    /// Take a manual snapshot of a tracked tensor
490    pub fn snapshot(&self, id: TrackId, label: impl Into<String>) -> Result<()>
491    where
492        T: Copy,
493    {
494        let mut tensors = self.tensors.write_or_recover();
495        let tracked = tensors.get_mut(&id).ok_or_else(|| {
496            TorshError::InvalidArgument(format!("Tensor with ID {} is not tracked", id))
497        })?;
498
499        tracked.take_snapshot(label.into())?;
500
501        // Trim if needed
502        let config = self.config.read_or_recover();
503        if tracked.snapshots.len() > config.max_snapshots {
504            tracked.snapshots.remove(0);
505        }
506
507        Ok(())
508    }
509
510    /// Generate a comprehensive report for a tracked tensor
511    pub fn generate_report(&self, id: TrackId) -> Result<String>
512    where
513        T: Copy + PartialOrd + num_traits::Zero + num_traits::ToPrimitive + fmt::Display,
514    {
515        let tensors = self.tensors.read_or_recover();
516        let tracked = tensors.get(&id).ok_or_else(|| {
517            TorshError::InvalidArgument(format!("Tensor with ID {} is not tracked", id))
518        })?;
519
520        let mut report = String::new();
521        report.push_str(&format!(
522            "=== Tracking Report for '{}' (ID: {}) ===\n\n",
523            tracked.label, tracked.id
524        ));
525        report.push_str(&format!(
526            "Tracking duration: {:?}\n",
527            tracked.start_time.elapsed()
528        ));
529        report.push_str(&format!(
530            "Current shape: {:?}\n",
531            tracked.tensor.shape().dims()
532        ));
533        report.push_str(&format!(
534            "Operations performed: {}\n",
535            tracked.operations.len()
536        ));
537        report.push_str(&format!("Snapshots taken: {}\n\n", tracked.snapshots.len()));
538
539        // Current statistics
540        if let Ok(stats) = TensorValueStats::from_tensor(&tracked.tensor) {
541            report.push_str("Current Value Statistics:\n");
542            report.push_str(&format!("{}\n", stats));
543        }
544
545        // Operation history
546        if !tracked.operations.is_empty() {
547            report.push_str("\nOperation History:\n");
548            for (i, op) in tracked.operations.iter().enumerate() {
549                report.push_str(&format!("  {}. {}\n", i + 1, op));
550            }
551        }
552
553        // Snapshot summary
554        if !tracked.snapshots.is_empty() {
555            report.push_str("\nSnapshots:\n");
556            for (i, snapshot) in tracked.snapshots.iter().enumerate() {
557                report.push_str(&format!(
558                    "  {}. '{}' - shape: {:?}, elements: {}\n",
559                    i + 1,
560                    snapshot.label,
561                    snapshot.shape,
562                    snapshot.values.len()
563                ));
564            }
565        }
566
567        Ok(report)
568    }
569
570    /// Get the current tensor for a tracked ID
571    pub fn get_tensor(&self, id: TrackId) -> Result<Tensor<T>> {
572        let tensors = self.tensors.read_or_recover();
573        let tracked = tensors.get(&id).ok_or_else(|| {
574            TorshError::InvalidArgument(format!("Tensor with ID {} is not tracked", id))
575        })?;
576        Ok(tracked.tensor.clone())
577    }
578
579    /// Get all tracked tensor IDs
580    pub fn tracked_ids(&self) -> Vec<TrackId> {
581        self.tensors.read_or_recover().keys().copied().collect()
582    }
583
584    /// Clear all tracking data
585    pub fn clear(&mut self) {
586        self.tensors.write_or_recover().clear();
587        *self.next_id.write_or_recover() = 0;
588    }
589}
590
591impl<T: TensorElement> Default for TensorTracker<T> {
592    fn default() -> Self {
593        Self::new()
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use crate::creation;
601    use torsh_core::device::DeviceType;
602
603    #[test]
604    fn test_tensor_tracker_basic() {
605        let mut tracker = TensorTracker::new();
606        let tensor = creation::ones::<f32>(&[2, 2]).expect("ones creation should succeed");
607
608        let id = tracker
609            .track(tensor.clone(), "test_tensor")
610            .expect("tracking should succeed");
611        assert_eq!(tracker.tracked_ids().len(), 1);
612
613        let result = tensor
614            .mul_scalar(2.0)
615            .expect("scalar multiplication should succeed");
616        tracker
617            .record_operation(id, "mul_scalar", vec![2.0], &result)
618            .expect("multiplication should succeed");
619
620        let retrieved = tracker.get_tensor(id).expect("operation should succeed");
621        assert_eq!(retrieved.shape().dims(), &[2, 2]);
622
623        tracker.untrack(id).expect("untracking should succeed");
624        assert_eq!(tracker.tracked_ids().len(), 0);
625    }
626
627    #[test]
628    fn test_tensor_value_stats() {
629        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
630        let tensor = Tensor::from_data(data, vec![5], DeviceType::Cpu)
631            .expect("tensor creation should succeed");
632
633        let stats = TensorValueStats::from_tensor(&tensor).expect("from_tensor should succeed");
634        assert_eq!(stats.total_elements, 5);
635        assert_eq!(stats.min, Some(1.0));
636        assert_eq!(stats.max, Some(5.0));
637        assert!((stats.mean.expect("stat value should be available") - 3.0).abs() < 1e-6);
638    }
639
640    #[test]
641    fn test_tracking_snapshots() {
642        let mut tracker = TensorTracker::new();
643        let tensor = creation::ones::<f32>(&[3, 3]).expect("ones creation should succeed");
644
645        let id = tracker
646            .track(tensor.clone(), "snapshot_test")
647            .expect("tracking should succeed");
648
649        tracker
650            .snapshot(id, "first_snapshot")
651            .expect("snapshot should succeed");
652        tracker
653            .snapshot(id, "second_snapshot")
654            .expect("snapshot should succeed");
655
656        let tensors = tracker.tensors.read_or_recover();
657        let tracked = tensors.get(&id).expect("get should succeed");
658        assert_eq!(tracked.snapshots.len(), 2);
659    }
660
661    #[test]
662    fn test_tracking_report() {
663        let mut tracker = TensorTracker::new();
664        let data = vec![1.0f32, 2.0, 3.0];
665        let tensor = Tensor::from_data(data, vec![3], DeviceType::Cpu)
666            .expect("tensor creation should succeed");
667
668        let id = tracker
669            .track(tensor.clone(), "report_test")
670            .expect("tracking should succeed");
671
672        let result = tensor
673            .mul_scalar(2.0)
674            .expect("scalar multiplication should succeed");
675        tracker
676            .record_operation(id, "mul_scalar", vec![2.0], &result)
677            .expect("tensor creation should succeed");
678
679        let report = tracker
680            .generate_report(id)
681            .expect("report generation should succeed");
682        assert!(report.contains("report_test"));
683        assert!(report.contains("mul_scalar"));
684        assert!(report.contains("Operations performed: 1"));
685    }
686
687    #[test]
688    fn test_tracking_config() {
689        let config = TrackingConfig::minimal();
690        let mut tracker = TensorTracker::with_config(config);
691
692        let tensor = creation::ones::<f32>(&[2, 2]).expect("ones creation should succeed");
693        let id = tracker
694            .track(tensor, "config_test")
695            .expect("tracking should succeed");
696
697        assert_eq!(tracker.tracked_ids().len(), 1);
698        assert!(id == 0);
699    }
700
701    #[test]
702    fn test_operation_filtering() {
703        let config = TrackingConfig::filtered(vec!["add".to_string(), "mul".to_string()]);
704        let mut tracker = TensorTracker::with_config(config);
705
706        let tensor = creation::ones::<f32>(&[2, 2]).expect("ones creation should succeed");
707        let id = tracker
708            .track(tensor.clone(), "filter_test")
709            .expect("tracking should succeed");
710
711        // This should be tracked
712        let result = tensor
713            .mul_scalar(2.0)
714            .expect("scalar multiplication should succeed");
715        tracker
716            .record_operation(id, "mul", vec![2.0], &result)
717            .expect("multiplication should succeed");
718
719        // This should be filtered out
720        let result2 = result
721            .add_scalar(1.0)
722            .expect("scalar addition should succeed");
723        tracker
724            .record_operation(id, "sub", vec![1.0], &result2)
725            .expect("multiplication should succeed");
726
727        let tensors = tracker.tensors.read_or_recover();
728        let tracked = tensors.get(&id).expect("get should succeed");
729        assert_eq!(tracked.operations.len(), 1); // Only "mul" should be tracked
730        assert_eq!(tracked.operations[0].operation, "mul");
731    }
732}