1use 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
52pub type TrackId = u64;
54
55#[derive(Debug, Clone)]
57pub struct TensorValueStats<T: TensorElement> {
58 pub min: Option<T>,
60 pub max: Option<T>,
62 pub mean: Option<f64>,
64 pub std: Option<f64>,
66 pub nan_count: usize,
68 pub inf_count: usize,
70 pub zero_count: usize,
72 pub total_elements: usize,
74}
75
76impl<T: TensorElement> TensorValueStats<T> {
77 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 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 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 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 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#[derive(Debug, Clone)]
201pub struct OperationRecord {
202 pub operation: String,
204 pub parameters: Vec<String>,
206 pub timestamp: Instant,
208 pub duration: Option<Duration>,
210 pub shape_before: Vec<usize>,
212 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#[derive(Clone)]
232pub struct TensorSnapshot<T: TensorElement> {
233 pub values: Vec<T>,
235 pub shape: Vec<usize>,
237 pub timestamp: Instant,
239 pub label: String,
241}
242
243pub struct TrackedTensor<T: TensorElement> {
245 pub id: TrackId,
247 pub label: String,
249 pub tensor: Tensor<T>,
251 pub operations: Vec<OperationRecord>,
253 pub snapshots: Vec<TensorSnapshot<T>>,
255 pub start_time: Instant,
257}
258
259impl<T: TensorElement> TrackedTensor<T> {
260 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 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 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#[derive(Debug, Clone)]
316pub struct TrackingConfig {
317 pub enabled: bool,
319 pub max_operations: usize,
321 pub max_snapshots: usize,
323 pub auto_snapshot: bool,
325 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 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 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 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
376pub struct TensorTracker<T: TensorElement> {
378 config: Arc<RwLock<TrackingConfig>>,
380 tensors: Arc<RwLock<HashMap<TrackId, TrackedTensor<T>>>>,
382 next_id: Arc<RwLock<TrackId>>,
384}
385
386impl<T: TensorElement> TensorTracker<T> {
387 pub fn new() -> Self {
389 Self::with_config(TrackingConfig::default())
390 }
391
392 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 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 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 pub fn untrack(&mut self, id: TrackId) -> Result<()> {
435 self.tensors.write_or_recover().remove(&id);
436 Ok(())
437 }
438
439 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 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 if tracked.operations.len() > max_operations {
478 tracked.operations.remove(0);
479 }
480
481 if auto_snapshot {
483 tracked.take_snapshot(format!("after_{}", operation_str))?;
484 }
485
486 Ok(())
487 }
488
489 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 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 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 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 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 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 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 pub fn tracked_ids(&self) -> Vec<TrackId> {
581 self.tensors.read_or_recover().keys().copied().collect()
582 }
583
584 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 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 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); assert_eq!(tracked.operations[0].operation, "mul");
731 }
732}