1use std::collections::{HashMap, HashSet, VecDeque};
44
45#[derive(Debug, Clone)]
51pub struct TensorRef {
52 pub tensor_id: u64,
54 pub name: Option<String>,
56 pub size_bytes: u64,
58 pub ref_count: u32,
61 pub dependencies: Vec<u64>,
63 pub pinned: bool,
66}
67
68#[derive(Clone, Debug, PartialEq)]
74pub enum GcPhase {
75 MarkRoots,
77 Trace,
79 Sweep,
82}
83
84#[derive(Debug, Clone, Default)]
90pub struct GcStats {
91 pub total_tensors: usize,
93 pub reachable: usize,
95 pub collected: usize,
97 pub freed_bytes: u64,
99 pub pinned_tensors: usize,
101}
102
103impl GcStats {
104 pub fn collection_rate(&self) -> f64 {
109 self.collected as f64 / self.total_tensors.max(1) as f64
110 }
111}
112
113pub struct TensorGarbageCollector {
123 pub tensors: HashMap<u64, TensorRef>,
125 pub roots: Vec<u64>,
127}
128
129impl TensorGarbageCollector {
130 pub fn new() -> Self {
132 Self {
133 tensors: HashMap::new(),
134 roots: Vec::new(),
135 }
136 }
137
138 pub fn register(&mut self, tensor: TensorRef) {
142 self.tensors.insert(tensor.tensor_id, tensor);
143 }
144
145 pub fn add_root(&mut self, tensor_id: u64) {
150 if !self.roots.contains(&tensor_id) {
151 self.roots.push(tensor_id);
152 }
153 }
154
155 pub fn remove_root(&mut self, tensor_id: u64) {
159 self.roots.retain(|&id| id != tensor_id);
160 }
161
162 pub fn pin(&mut self, tensor_id: u64) {
166 if let Some(t) = self.tensors.get_mut(&tensor_id) {
167 t.pinned = true;
168 }
169 }
170
171 pub fn add_ref(&mut self, tensor_id: u64) {
175 if let Some(t) = self.tensors.get_mut(&tensor_id) {
176 t.ref_count = t.ref_count.saturating_add(1);
177 }
178 }
179
180 pub fn remove_ref(&mut self, tensor_id: u64) {
184 if let Some(t) = self.tensors.get_mut(&tensor_id) {
185 t.ref_count = t.ref_count.saturating_sub(1);
186 }
187 }
188
189 pub fn reachable_set(&self) -> Vec<u64> {
195 let mut visited: HashSet<u64> = HashSet::new();
196 let mut queue: VecDeque<u64> = VecDeque::new();
197
198 for &id in &self.roots {
200 if self.tensors.contains_key(&id) && visited.insert(id) {
201 queue.push_back(id);
202 }
203 }
204
205 for (id, tensor) in &self.tensors {
207 if tensor.pinned && visited.insert(*id) {
208 queue.push_back(*id);
209 }
210 }
211
212 while let Some(current) = queue.pop_front() {
214 if let Some(tensor) = self.tensors.get(¤t) {
215 for &dep in &tensor.dependencies {
216 if self.tensors.contains_key(&dep) && visited.insert(dep) {
217 queue.push_back(dep);
218 }
219 }
220 }
221 }
222
223 visited.into_iter().collect()
224 }
225
226 pub fn collect(&mut self) -> GcStats {
236 let total_tensors = self.tensors.len();
237 let pinned_tensors = self.tensors.values().filter(|t| t.pinned).count();
238
239 let _phase = GcPhase::MarkRoots;
241 let mut visited: HashSet<u64> = HashSet::new();
242 let mut queue: VecDeque<u64> = VecDeque::new();
243
244 for &id in &self.roots {
245 if self.tensors.contains_key(&id) && visited.insert(id) {
246 queue.push_back(id);
247 }
248 }
249
250 for (id, tensor) in &self.tensors {
251 if tensor.pinned && visited.insert(*id) {
252 queue.push_back(*id);
253 }
254 }
255
256 let _phase = GcPhase::Trace;
258 while let Some(current) = queue.pop_front() {
259 if let Some(tensor) = self.tensors.get(¤t) {
260 for &dep in &tensor.dependencies {
261 if self.tensors.contains_key(&dep) && visited.insert(dep) {
262 queue.push_back(dep);
263 }
264 }
265 }
266 }
267
268 let reachable = visited.len();
269
270 let _phase = GcPhase::Sweep;
272 let mut collected = 0usize;
273 let mut freed_bytes = 0u64;
274
275 let to_remove: Vec<u64> = self
277 .tensors
278 .iter()
279 .filter_map(|(id, tensor)| {
280 if !visited.contains(id) && tensor.ref_count == 0 {
281 Some(*id)
282 } else {
283 None
284 }
285 })
286 .collect();
287
288 for id in to_remove {
289 if let Some(tensor) = self.tensors.remove(&id) {
290 freed_bytes += tensor.size_bytes;
291 collected += 1;
292 }
293 }
294
295 GcStats {
296 total_tensors,
297 reachable,
298 collected,
299 freed_bytes,
300 pinned_tensors,
301 }
302 }
303}
304
305impl Default for TensorGarbageCollector {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn make_tensor(id: u64, size: u64) -> TensorRef {
321 TensorRef {
322 tensor_id: id,
323 name: None,
324 size_bytes: size,
325 ref_count: 0,
326 dependencies: vec![],
327 pinned: false,
328 }
329 }
330
331 #[test]
336 fn test_register_inserts_tensor() {
337 let mut gc = TensorGarbageCollector::new();
338 gc.register(make_tensor(1, 128));
339 assert!(gc.tensors.contains_key(&1));
340 }
341
342 #[test]
343 fn test_register_overwrites_existing() {
344 let mut gc = TensorGarbageCollector::new();
345 gc.register(make_tensor(1, 128));
346 gc.register(TensorRef {
347 tensor_id: 1,
348 name: Some("updated".to_string()),
349 size_bytes: 256,
350 ref_count: 0,
351 dependencies: vec![],
352 pinned: false,
353 });
354 assert_eq!(gc.tensors[&1].size_bytes, 256);
355 assert_eq!(gc.tensors[&1].name.as_deref(), Some("updated"));
356 }
357
358 #[test]
363 fn test_add_root_appends() {
364 let mut gc = TensorGarbageCollector::new();
365 gc.add_root(42);
366 assert!(gc.roots.contains(&42));
367 }
368
369 #[test]
370 fn test_add_root_no_duplicates() {
371 let mut gc = TensorGarbageCollector::new();
372 gc.add_root(7);
373 gc.add_root(7);
374 assert_eq!(gc.roots.iter().filter(|&&id| id == 7).count(), 1);
375 }
376
377 #[test]
378 fn test_remove_root_removes_entry() {
379 let mut gc = TensorGarbageCollector::new();
380 gc.add_root(5);
381 gc.add_root(6);
382 gc.remove_root(5);
383 assert!(!gc.roots.contains(&5));
384 assert!(gc.roots.contains(&6));
385 }
386
387 #[test]
388 fn test_remove_root_nonexistent_is_noop() {
389 let mut gc = TensorGarbageCollector::new();
390 gc.add_root(3);
391 gc.remove_root(999); assert_eq!(gc.roots.len(), 1);
393 }
394
395 #[test]
400 fn test_collect_removes_unreachable() {
401 let mut gc = TensorGarbageCollector::new();
402 gc.register(make_tensor(1, 100)); gc.register(make_tensor(2, 200)); gc.add_root(1);
405
406 let stats = gc.collect();
407
408 assert_eq!(stats.collected, 1);
409 assert_eq!(stats.freed_bytes, 200);
410 assert!(gc.tensors.contains_key(&1));
411 assert!(!gc.tensors.contains_key(&2));
412 }
413
414 #[test]
415 fn test_collect_keeps_reachable_tensors() {
416 let mut gc = TensorGarbageCollector::new();
417 gc.register(TensorRef {
418 tensor_id: 1,
419 name: None,
420 size_bytes: 50,
421 ref_count: 0,
422 dependencies: vec![2],
423 pinned: false,
424 });
425 gc.register(make_tensor(2, 75));
426 gc.add_root(1);
427
428 let stats = gc.collect();
429 assert_eq!(stats.collected, 0);
430 assert!(gc.tensors.contains_key(&2));
431 }
432
433 #[test]
438 fn test_pinned_never_collected() {
439 let mut gc = TensorGarbageCollector::new();
440 gc.register(TensorRef {
441 tensor_id: 10,
442 name: None,
443 size_bytes: 512,
444 ref_count: 0,
445 dependencies: vec![],
446 pinned: true,
447 });
448 let stats = gc.collect();
450 assert_eq!(stats.collected, 0);
451 assert!(gc.tensors.contains_key(&10));
452 }
453
454 #[test]
455 fn test_pin_method_marks_tensor() {
456 let mut gc = TensorGarbageCollector::new();
457 gc.register(make_tensor(99, 1024));
458 assert!(!gc.tensors[&99].pinned);
459 gc.pin(99);
460 assert!(gc.tensors[&99].pinned);
461 }
462
463 #[test]
464 fn test_pin_nonexistent_is_noop() {
465 let mut gc = TensorGarbageCollector::new();
466 gc.pin(1234); }
468
469 #[test]
474 fn test_ref_count_prevents_collection() {
475 let mut gc = TensorGarbageCollector::new();
476 gc.register(TensorRef {
477 tensor_id: 5,
478 name: None,
479 size_bytes: 300,
480 ref_count: 1, dependencies: vec![],
482 pinned: false,
483 });
484 let stats = gc.collect();
486 assert_eq!(stats.collected, 0);
487 assert!(gc.tensors.contains_key(&5));
488 }
489
490 #[test]
495 fn test_bfs_traces_multi_level_chain() {
496 let mut gc = TensorGarbageCollector::new();
498 for (id, dep) in [(1u64, Some(2u64)), (2, Some(3)), (3, Some(4)), (4, None)] {
499 gc.register(TensorRef {
500 tensor_id: id,
501 name: None,
502 size_bytes: id * 10,
503 ref_count: 0,
504 dependencies: dep.into_iter().collect(),
505 pinned: false,
506 });
507 }
508 gc.register(make_tensor(5, 999)); gc.add_root(1);
510
511 let stats = gc.collect();
512 assert_eq!(stats.reachable, 4);
513 assert_eq!(stats.collected, 1);
514 assert_eq!(stats.freed_bytes, 999);
515 assert!(!gc.tensors.contains_key(&5));
516 }
517
518 #[test]
523 fn test_reachable_set_correct() {
524 let mut gc = TensorGarbageCollector::new();
525 gc.register(TensorRef {
526 tensor_id: 1,
527 name: None,
528 size_bytes: 10,
529 ref_count: 0,
530 dependencies: vec![2],
531 pinned: false,
532 });
533 gc.register(make_tensor(2, 20));
534 gc.register(make_tensor(3, 30)); gc.add_root(1);
536
537 let mut reachable = gc.reachable_set();
538 reachable.sort();
539 assert_eq!(reachable, vec![1, 2]);
540 assert_eq!(gc.tensors.len(), 3);
542 }
543
544 #[test]
545 fn test_reachable_set_includes_pinned() {
546 let mut gc = TensorGarbageCollector::new();
547 gc.register(make_tensor(1, 10));
548 gc.register(TensorRef {
549 tensor_id: 2,
550 name: None,
551 size_bytes: 20,
552 ref_count: 0,
553 dependencies: vec![],
554 pinned: true,
555 });
556 let reachable = gc.reachable_set();
558 assert!(reachable.contains(&2));
559 assert!(!reachable.contains(&1));
560 }
561
562 #[test]
567 fn test_gc_stats_fields() {
568 let mut gc = TensorGarbageCollector::new();
569 gc.register(make_tensor(1, 100));
570 gc.register(make_tensor(2, 200));
571 gc.register(TensorRef {
572 tensor_id: 3,
573 name: None,
574 size_bytes: 50,
575 ref_count: 0,
576 dependencies: vec![],
577 pinned: true,
578 });
579 gc.add_root(1);
580
581 let stats = gc.collect();
582 assert_eq!(stats.total_tensors, 3);
583 assert_eq!(stats.reachable, 2); assert_eq!(stats.collected, 1); assert_eq!(stats.freed_bytes, 200);
586 assert_eq!(stats.pinned_tensors, 1);
587 }
588
589 #[test]
590 fn test_collection_rate_correct() {
591 let stats = GcStats {
592 total_tensors: 10,
593 reachable: 6,
594 collected: 4,
595 freed_bytes: 0,
596 pinned_tensors: 0,
597 };
598 let rate = stats.collection_rate();
599 assert!((rate - 0.4).abs() < f64::EPSILON * 10.0);
600 }
601
602 #[test]
603 fn test_collection_rate_zero_total() {
604 let stats = GcStats::default();
605 assert_eq!(stats.collection_rate(), 0.0);
607 }
608
609 #[test]
614 fn test_add_ref_increments() {
615 let mut gc = TensorGarbageCollector::new();
616 gc.register(make_tensor(1, 64));
617 assert_eq!(gc.tensors[&1].ref_count, 0);
618 gc.add_ref(1);
619 assert_eq!(gc.tensors[&1].ref_count, 1);
620 gc.add_ref(1);
621 assert_eq!(gc.tensors[&1].ref_count, 2);
622 }
623
624 #[test]
625 fn test_remove_ref_decrements_saturating() {
626 let mut gc = TensorGarbageCollector::new();
627 gc.register(make_tensor(1, 64));
628 gc.add_ref(1);
629 gc.remove_ref(1);
630 assert_eq!(gc.tensors[&1].ref_count, 0);
631 gc.remove_ref(1);
633 assert_eq!(gc.tensors[&1].ref_count, 0);
634 }
635
636 #[test]
637 fn test_add_ref_remove_ref_nonexistent_is_noop() {
638 let mut gc = TensorGarbageCollector::new();
639 gc.add_ref(9999); gc.remove_ref(9999); }
642
643 #[test]
648 fn test_diamond_dependency_all_reachable() {
649 let mut gc = TensorGarbageCollector::new();
651 gc.register(TensorRef {
652 tensor_id: 1,
653 name: None,
654 size_bytes: 10,
655 ref_count: 0,
656 dependencies: vec![2, 3],
657 pinned: false,
658 });
659 gc.register(TensorRef {
660 tensor_id: 2,
661 name: None,
662 size_bytes: 20,
663 ref_count: 0,
664 dependencies: vec![4],
665 pinned: false,
666 });
667 gc.register(TensorRef {
668 tensor_id: 3,
669 name: None,
670 size_bytes: 30,
671 ref_count: 0,
672 dependencies: vec![4],
673 pinned: false,
674 });
675 gc.register(make_tensor(4, 40));
676 gc.add_root(1);
677
678 let stats = gc.collect();
679 assert_eq!(stats.reachable, 4);
680 assert_eq!(stats.collected, 0);
681 }
682
683 #[test]
688 fn test_multiple_roots() {
689 let mut gc = TensorGarbageCollector::new();
690 gc.register(make_tensor(1, 10));
691 gc.register(make_tensor(2, 20));
692 gc.register(make_tensor(3, 30)); gc.add_root(1);
694 gc.add_root(2);
695
696 let stats = gc.collect();
697 assert_eq!(stats.reachable, 2);
698 assert_eq!(stats.collected, 1);
699 }
700
701 #[test]
706 fn test_remove_root_enables_collection() {
707 let mut gc = TensorGarbageCollector::new();
708 gc.register(make_tensor(1, 100));
709 gc.register(make_tensor(2, 200));
710 gc.add_root(1);
711 gc.add_root(2);
712
713 let s1 = gc.collect();
715 assert_eq!(s1.collected, 0);
716
717 gc.remove_root(2);
719 let s2 = gc.collect();
720 assert_eq!(s2.collected, 1);
721 assert!(!gc.tensors.contains_key(&2));
722 }
723
724 #[test]
729 fn test_named_tensor_survives_as_root() {
730 let mut gc = TensorGarbageCollector::new();
731 gc.register(TensorRef {
732 tensor_id: 7,
733 name: Some("embedding_weights".to_string()),
734 size_bytes: 4096,
735 ref_count: 0,
736 dependencies: vec![],
737 pinned: false,
738 });
739 gc.add_root(7);
740 let stats = gc.collect();
741 assert_eq!(stats.collected, 0);
742 assert_eq!(gc.tensors[&7].name.as_deref(), Some("embedding_weights"));
743 }
744
745 #[test]
750 fn test_empty_gc_collect_returns_zero_stats() {
751 let mut gc = TensorGarbageCollector::new();
752 let stats = gc.collect();
753 assert_eq!(stats.total_tensors, 0);
754 assert_eq!(stats.reachable, 0);
755 assert_eq!(stats.collected, 0);
756 assert_eq!(stats.freed_bytes, 0);
757 assert_eq!(stats.pinned_tensors, 0);
758 }
759
760 #[test]
765 fn test_gc_phase_clone_debug_partialeq() {
766 let p = GcPhase::Trace;
767 let q = p.clone();
768 assert_eq!(p, q);
769 let r = GcPhase::Sweep;
770 assert_ne!(p, r);
771 let debug_str = format!("{:?}", GcPhase::MarkRoots);
772 assert!(debug_str.contains("MarkRoots"));
773 }
774}