1use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq)]
15pub enum DiffKind {
16 Added,
18 Removed,
20 ShapeChanged {
22 old_shape: Vec<usize>,
23 new_shape: Vec<usize>,
24 },
25 ValueChanged {
27 max_abs_diff: f32,
28 mean_abs_diff: f32,
29 changed_elements: usize,
30 },
31 Unchanged,
33}
34
35#[derive(Debug, Clone)]
41pub struct TensorSnapshot {
42 pub name: String,
44 pub shape: Vec<usize>,
46 pub data: Vec<f32>,
48}
49
50impl TensorSnapshot {
51 pub fn new(name: impl Into<String>, shape: Vec<usize>, data: Vec<f32>) -> Self {
53 Self {
54 name: name.into(),
55 shape,
56 data,
57 }
58 }
59
60 pub fn numel(&self) -> usize {
63 if self.shape.is_empty() {
64 self.data.len()
65 } else {
66 self.shape.iter().product()
67 }
68 }
69
70 pub fn is_compatible(&self, other: &TensorSnapshot) -> bool {
73 self.shape == other.shape
74 }
75}
76
77#[derive(Debug, Clone)]
83pub struct TensorDiff {
84 pub name: String,
86 pub kind: DiffKind,
88}
89
90impl TensorDiff {
91 pub fn is_significant(&self, threshold: f32) -> bool {
98 match &self.kind {
99 DiffKind::ValueChanged { max_abs_diff, .. } => *max_abs_diff > threshold,
100 DiffKind::ShapeChanged { .. } | DiffKind::Added | DiffKind::Removed => true,
101 DiffKind::Unchanged => false,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default)]
112pub struct DiffSummary {
113 pub added: usize,
114 pub removed: usize,
115 pub shape_changed: usize,
116 pub value_changed: usize,
117 pub unchanged: usize,
118 pub total_changed_elements: usize,
119}
120
121impl DiffSummary {
122 pub fn has_changes(&self) -> bool {
124 self.added > 0 || self.removed > 0 || self.shape_changed > 0 || self.value_changed > 0
125 }
126}
127
128pub struct TensorDiffEngine {
134 pub threshold: f32,
137}
138
139impl TensorDiffEngine {
140 pub fn new(threshold: f32) -> Self {
142 Self { threshold }
143 }
144
145 pub fn diff_tensors(&self, old: &TensorSnapshot, new: &TensorSnapshot) -> TensorDiff {
151 if old.shape != new.shape {
152 return TensorDiff {
153 name: new.name.clone(),
154 kind: DiffKind::ShapeChanged {
155 old_shape: old.shape.clone(),
156 new_shape: new.shape.clone(),
157 },
158 };
159 }
160
161 let len = old.data.len().max(new.data.len());
162 let mut max_abs: f32 = 0.0;
163 let mut sum_abs: f32 = 0.0;
164 let mut changed: usize = 0;
165
166 for i in 0..len {
167 let o = old.data.get(i).copied().unwrap_or(0.0);
168 let n = new.data.get(i).copied().unwrap_or(0.0);
169 let d = (n - o).abs();
170 if d > max_abs {
171 max_abs = d;
172 }
173 sum_abs += d;
174 if d > self.threshold {
175 changed += 1;
176 }
177 }
178
179 let kind = if changed == 0 {
180 DiffKind::Unchanged
181 } else {
182 let mean_abs = if len > 0 { sum_abs / len as f32 } else { 0.0 };
183 DiffKind::ValueChanged {
184 max_abs_diff: max_abs,
185 mean_abs_diff: mean_abs,
186 changed_elements: changed,
187 }
188 };
189
190 TensorDiff {
191 name: new.name.clone(),
192 kind,
193 }
194 }
195
196 pub fn diff_snapshots(
200 &self,
201 old_set: &[TensorSnapshot],
202 new_set: &[TensorSnapshot],
203 ) -> Vec<TensorDiff> {
204 let old_map: HashMap<&str, &TensorSnapshot> =
205 old_set.iter().map(|t| (t.name.as_str(), t)).collect();
206 let new_map: HashMap<&str, &TensorSnapshot> =
207 new_set.iter().map(|t| (t.name.as_str(), t)).collect();
208
209 let mut diffs: Vec<TensorDiff> = Vec::new();
210
211 for (name, old_tensor) in &old_map {
213 match new_map.get(name) {
214 None => diffs.push(TensorDiff {
215 name: (*name).to_string(),
216 kind: DiffKind::Removed,
217 }),
218 Some(new_tensor) => {
219 diffs.push(self.diff_tensors(old_tensor, new_tensor));
220 }
221 }
222 }
223
224 for name in new_map.keys() {
226 if !old_map.contains_key(name) {
227 diffs.push(TensorDiff {
228 name: (*name).to_string(),
229 kind: DiffKind::Added,
230 });
231 }
232 }
233
234 diffs.sort_by(|a, b| a.name.cmp(&b.name));
235 diffs
236 }
237
238 pub fn summarize(&self, diffs: &[TensorDiff]) -> DiffSummary {
240 let mut summary = DiffSummary::default();
241 for diff in diffs {
242 match &diff.kind {
243 DiffKind::Added => summary.added += 1,
244 DiffKind::Removed => summary.removed += 1,
245 DiffKind::ShapeChanged { .. } => summary.shape_changed += 1,
246 DiffKind::ValueChanged {
247 changed_elements, ..
248 } => {
249 summary.value_changed += 1;
250 summary.total_changed_elements += changed_elements;
251 }
252 DiffKind::Unchanged => summary.unchanged += 1,
253 }
254 }
255 summary
256 }
257
258 pub fn significant_diffs<'a>(&self, diffs: &'a [TensorDiff]) -> Vec<&'a TensorDiff> {
260 diffs
261 .iter()
262 .filter(|d| d.is_significant(self.threshold))
263 .collect()
264 }
265}
266
267#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn make_snap(name: &str, shape: Vec<usize>, data: Vec<f32>) -> TensorSnapshot {
276 TensorSnapshot::new(name, shape, data)
277 }
278
279 #[test]
281 fn test_engine_new_threshold() {
282 let engine = TensorDiffEngine::new(1e-4);
283 assert!((engine.threshold - 1e-4_f32).abs() < f32::EPSILON);
284 }
285
286 #[test]
288 fn test_numel_2d() {
289 let snap = make_snap("w", vec![128, 256], vec![0.0; 128 * 256]);
290 assert_eq!(snap.numel(), 128 * 256);
291 }
292
293 #[test]
295 fn test_numel_empty_shape() {
296 let snap = make_snap("scalar", vec![], vec![1.0, 2.0, 3.0]);
297 assert_eq!(snap.numel(), 3);
298 }
299
300 #[test]
302 fn test_is_compatible_matching() {
303 let a = make_snap("a", vec![4, 4], vec![0.0; 16]);
304 let b = make_snap("b", vec![4, 4], vec![1.0; 16]);
305 assert!(a.is_compatible(&b));
306 }
307
308 #[test]
310 fn test_is_compatible_mismatch() {
311 let a = make_snap("a", vec![4, 4], vec![0.0; 16]);
312 let b = make_snap("b", vec![4, 8], vec![0.0; 32]);
313 assert!(!a.is_compatible(&b));
314 }
315
316 #[test]
318 fn test_diff_tensors_identical_unchanged() {
319 let engine = TensorDiffEngine::new(1e-6);
320 let data = vec![1.0, 2.0, 3.0, 4.0];
321 let old = make_snap("t", vec![4], data.clone());
322 let new = make_snap("t", vec![4], data);
323 let diff = engine.diff_tensors(&old, &new);
324 assert_eq!(diff.kind, DiffKind::Unchanged);
325 }
326
327 #[test]
329 fn test_diff_tensors_small_delta_unchanged() {
330 let threshold = 1e-5_f32;
331 let engine = TensorDiffEngine::new(threshold);
332 let old = make_snap("t", vec![3], vec![1.0, 2.0, 3.0]);
333 let new = make_snap("t", vec![3], vec![1.0 + 1e-7, 2.0, 3.0]);
334 let diff = engine.diff_tensors(&old, &new);
335 assert_eq!(diff.kind, DiffKind::Unchanged);
336 }
337
338 #[test]
340 fn test_diff_tensors_value_changed_max_mean() {
341 let engine = TensorDiffEngine::new(1e-6);
342 let old = make_snap("t", vec![2], vec![0.0, 0.0]);
343 let new = make_snap("t", vec![2], vec![1.0, 3.0]);
344 let diff = engine.diff_tensors(&old, &new);
345 match diff.kind {
346 DiffKind::ValueChanged {
347 max_abs_diff,
348 mean_abs_diff,
349 changed_elements,
350 } => {
351 assert!((max_abs_diff - 3.0).abs() < 1e-5, "max={max_abs_diff}");
352 assert!((mean_abs_diff - 2.0).abs() < 1e-5, "mean={mean_abs_diff}");
353 assert_eq!(changed_elements, 2);
354 }
355 other => panic!("Expected ValueChanged, got {other:?}"),
356 }
357 }
358
359 #[test]
361 fn test_diff_tensors_shape_mismatch() {
362 let engine = TensorDiffEngine::new(1e-6);
363 let old = make_snap("t", vec![2, 2], vec![0.0; 4]);
364 let new = make_snap("t", vec![4], vec![0.0; 4]);
365 let diff = engine.diff_tensors(&old, &new);
366 match diff.kind {
367 DiffKind::ShapeChanged {
368 old_shape,
369 new_shape,
370 } => {
371 assert_eq!(old_shape, vec![2, 2]);
372 assert_eq!(new_shape, vec![4]);
373 }
374 other => panic!("Expected ShapeChanged, got {other:?}"),
375 }
376 }
377
378 #[test]
380 fn test_diff_tensors_mean_abs_diff_four_elements() {
381 let engine = TensorDiffEngine::new(1e-6);
382 let old = make_snap("t", vec![4], vec![0.0, 0.0, 0.0, 0.0]);
384 let new = make_snap("t", vec![4], vec![1.0, 2.0, 3.0, 4.0]);
385 let diff = engine.diff_tensors(&old, &new);
386 match diff.kind {
387 DiffKind::ValueChanged { mean_abs_diff, .. } => {
388 assert!((mean_abs_diff - 2.5).abs() < 1e-5, "mean={mean_abs_diff}");
389 }
390 other => panic!("Expected ValueChanged, got {other:?}"),
391 }
392 }
393
394 #[test]
396 fn test_diff_snapshots_added() {
397 let engine = TensorDiffEngine::new(1e-6);
398 let old_set: Vec<TensorSnapshot> = vec![];
399 let new_set = vec![make_snap("layer.weight", vec![2], vec![1.0, 2.0])];
400 let diffs = engine.diff_snapshots(&old_set, &new_set);
401 assert_eq!(diffs.len(), 1);
402 assert_eq!(diffs[0].name, "layer.weight");
403 assert_eq!(diffs[0].kind, DiffKind::Added);
404 }
405
406 #[test]
408 fn test_diff_snapshots_removed() {
409 let engine = TensorDiffEngine::new(1e-6);
410 let old_set = vec![make_snap("layer.weight", vec![2], vec![1.0, 2.0])];
411 let new_set: Vec<TensorSnapshot> = vec![];
412 let diffs = engine.diff_snapshots(&old_set, &new_set);
413 assert_eq!(diffs.len(), 1);
414 assert_eq!(diffs[0].name, "layer.weight");
415 assert_eq!(diffs[0].kind, DiffKind::Removed);
416 }
417
418 #[test]
420 fn test_diff_snapshots_unchanged() {
421 let engine = TensorDiffEngine::new(1e-6);
422 let snap = make_snap("w", vec![3], vec![1.0, 2.0, 3.0]);
423 let old_set = vec![snap.clone()];
424 let new_set = vec![snap];
425 let diffs = engine.diff_snapshots(&old_set, &new_set);
426 assert_eq!(diffs.len(), 1);
427 assert_eq!(diffs[0].kind, DiffKind::Unchanged);
428 }
429
430 #[test]
432 fn test_diff_snapshots_changed() {
433 let engine = TensorDiffEngine::new(1e-6);
434 let old_set = vec![make_snap("w", vec![2], vec![0.0, 0.0])];
435 let new_set = vec![make_snap("w", vec![2], vec![1.0, 1.0])];
436 let diffs = engine.diff_snapshots(&old_set, &new_set);
437 assert_eq!(diffs.len(), 1);
438 assert!(matches!(diffs[0].kind, DiffKind::ValueChanged { .. }));
439 }
440
441 #[test]
443 fn test_diff_snapshots_sorted_by_name() {
444 let engine = TensorDiffEngine::new(1e-6);
445 let old_set = vec![
446 make_snap("zebra", vec![1], vec![1.0]),
447 make_snap("apple", vec![1], vec![2.0]),
448 make_snap("mango", vec![1], vec![3.0]),
449 ];
450 let new_set = vec![
451 make_snap("mango", vec![1], vec![3.0]),
452 make_snap("zebra", vec![1], vec![1.0]),
453 make_snap("apple", vec![1], vec![2.0]),
454 ];
455 let diffs = engine.diff_snapshots(&old_set, &new_set);
456 let names: Vec<&str> = diffs.iter().map(|d| d.name.as_str()).collect();
457 assert_eq!(names, vec!["apple", "mango", "zebra"]);
458 }
459
460 #[test]
462 fn test_summarize_counts() {
463 let engine = TensorDiffEngine::new(1e-6);
464 let diffs = vec![
465 TensorDiff {
466 name: "a".into(),
467 kind: DiffKind::Added,
468 },
469 TensorDiff {
470 name: "b".into(),
471 kind: DiffKind::Removed,
472 },
473 TensorDiff {
474 name: "c".into(),
475 kind: DiffKind::ShapeChanged {
476 old_shape: vec![2],
477 new_shape: vec![4],
478 },
479 },
480 TensorDiff {
481 name: "d".into(),
482 kind: DiffKind::ValueChanged {
483 max_abs_diff: 0.5,
484 mean_abs_diff: 0.25,
485 changed_elements: 7,
486 },
487 },
488 TensorDiff {
489 name: "e".into(),
490 kind: DiffKind::Unchanged,
491 },
492 ];
493 let summary = engine.summarize(&diffs);
494 assert_eq!(summary.added, 1);
495 assert_eq!(summary.removed, 1);
496 assert_eq!(summary.shape_changed, 1);
497 assert_eq!(summary.value_changed, 1);
498 assert_eq!(summary.unchanged, 1);
499 assert_eq!(summary.total_changed_elements, 7);
500 assert!(summary.has_changes());
501 }
502
503 #[test]
505 fn test_significant_diffs_filters() {
506 let engine = TensorDiffEngine::new(0.1);
507 let diffs = vec![
508 TensorDiff {
509 name: "big".into(),
510 kind: DiffKind::ValueChanged {
511 max_abs_diff: 0.5,
512 mean_abs_diff: 0.3,
513 changed_elements: 3,
514 },
515 },
516 TensorDiff {
517 name: "small".into(),
518 kind: DiffKind::ValueChanged {
519 max_abs_diff: 0.05,
520 mean_abs_diff: 0.02,
521 changed_elements: 1,
522 },
523 },
524 TensorDiff {
525 name: "added".into(),
526 kind: DiffKind::Added,
527 },
528 ];
529 let sig = engine.significant_diffs(&diffs);
530 let names: Vec<&str> = sig.iter().map(|d| d.name.as_str()).collect();
532 assert!(names.contains(&"big"));
533 assert!(names.contains(&"added"));
534 assert!(!names.contains(&"small"));
535 }
536
537 #[test]
539 fn test_is_significant_unchanged_false() {
540 let diff = TensorDiff {
541 name: "t".into(),
542 kind: DiffKind::Unchanged,
543 };
544 assert!(!diff.is_significant(0.0));
545 assert!(!diff.is_significant(1e-10));
546 assert!(!diff.is_significant(f32::MAX));
547 }
548
549 #[test]
551 fn test_has_changes_false_when_all_unchanged() {
552 let engine = TensorDiffEngine::new(1e-6);
553 let diffs = vec![TensorDiff {
554 name: "t".into(),
555 kind: DiffKind::Unchanged,
556 }];
557 let summary = engine.summarize(&diffs);
558 assert!(!summary.has_changes());
559 }
560}