1use std::collections::{HashMap, VecDeque};
8
9#[derive(Debug, Clone)]
15pub struct CheckpointConfig {
16 pub max_checkpoints: usize,
18 pub auto_checkpoint_interval: u64,
20}
21
22impl Default for CheckpointConfig {
23 fn default() -> Self {
24 Self {
25 max_checkpoints: 5,
26 auto_checkpoint_interval: 100,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
37pub struct Checkpoint {
38 pub id: u64,
40 pub tick: u64,
42 pub label: String,
44 pub tensor_data: Vec<Vec<f64>>,
46 pub metadata: HashMap<String, String>,
48}
49
50#[derive(Debug, Clone)]
56pub struct CheckpointerStats {
57 pub total_checkpoints: usize,
59 pub oldest_tick: Option<u64>,
61 pub newest_tick: Option<u64>,
63 pub rollbacks_performed: u64,
65}
66
67pub struct TensorCheckpointer {
74 config: CheckpointConfig,
75 checkpoints: VecDeque<Checkpoint>,
76 next_id: u64,
77 current_tick: u64,
78 last_checkpoint_tick: u64,
79 rollbacks_performed: u64,
80}
81
82impl TensorCheckpointer {
83 pub fn new(config: CheckpointConfig) -> Self {
85 Self {
86 config,
87 checkpoints: VecDeque::new(),
88 next_id: 0,
89 current_tick: 0,
90 last_checkpoint_tick: 0,
91 rollbacks_performed: 0,
92 }
93 }
94
95 pub fn create_checkpoint(
102 &mut self,
103 label: &str,
104 tensor_data: Vec<Vec<f64>>,
105 metadata: HashMap<String, String>,
106 ) -> u64 {
107 let id = self.next_id;
108 self.next_id += 1;
109
110 let checkpoint = Checkpoint {
111 id,
112 tick: self.current_tick,
113 label: label.to_string(),
114 tensor_data,
115 metadata,
116 };
117
118 self.checkpoints.push_back(checkpoint);
119 self.last_checkpoint_tick = self.current_tick;
120
121 while self.checkpoints.len() > self.config.max_checkpoints {
123 self.checkpoints.pop_front();
124 }
125
126 id
127 }
128
129 pub fn rollback(&mut self, checkpoint_id: u64) -> Result<Checkpoint, String> {
137 let pos = self
138 .checkpoints
139 .iter()
140 .position(|c| c.id == checkpoint_id)
141 .ok_or_else(|| format!("checkpoint id {} not found", checkpoint_id))?;
142
143 self.checkpoints.truncate(pos + 1);
145
146 let checkpoint = self
148 .checkpoints
149 .pop_back()
150 .ok_or_else(|| "internal error: checkpoint disappeared".to_string())?;
151
152 self.rollbacks_performed += 1;
153
154 Ok(checkpoint)
155 }
156
157 pub fn latest_checkpoint(&self) -> Option<&Checkpoint> {
159 self.checkpoints.back()
160 }
161
162 pub fn get_checkpoint(&self, id: u64) -> Option<&Checkpoint> {
164 self.checkpoints.iter().find(|c| c.id == id)
165 }
166
167 pub fn checkpoint_count(&self) -> usize {
169 self.checkpoints.len()
170 }
171
172 pub fn should_auto_checkpoint(&self) -> bool {
175 self.current_tick.saturating_sub(self.last_checkpoint_tick)
176 >= self.config.auto_checkpoint_interval
177 }
178
179 pub fn tick(&mut self) {
181 self.current_tick += 1;
182 }
183
184 pub fn list_checkpoints(&self) -> Vec<(u64, String, u64)> {
187 self.checkpoints
188 .iter()
189 .map(|c| (c.id, c.label.clone(), c.tick))
190 .collect()
191 }
192
193 pub fn clear_all(&mut self) {
195 self.checkpoints.clear();
196 }
197
198 pub fn stats(&self) -> CheckpointerStats {
200 CheckpointerStats {
201 total_checkpoints: self.checkpoints.len(),
202 oldest_tick: self.checkpoints.front().map(|c| c.tick),
203 newest_tick: self.checkpoints.back().map(|c| c.tick),
204 rollbacks_performed: self.rollbacks_performed,
205 }
206 }
207}
208
209#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn default_config() -> CheckpointConfig {
218 CheckpointConfig::default()
219 }
220
221 fn config_with_max(max: usize) -> CheckpointConfig {
222 CheckpointConfig {
223 max_checkpoints: max,
224 ..Default::default()
225 }
226 }
227
228 fn empty_meta() -> HashMap<String, String> {
229 HashMap::new()
230 }
231
232 fn sample_tensors() -> Vec<Vec<f64>> {
233 vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0]]
234 }
235
236 #[test]
239 fn test_create_checkpoint_returns_unique_ids() {
240 let mut cp = TensorCheckpointer::new(default_config());
241 let id0 = cp.create_checkpoint("a", vec![], empty_meta());
242 let id1 = cp.create_checkpoint("b", vec![], empty_meta());
243 assert_ne!(id0, id1);
244 assert_eq!(cp.checkpoint_count(), 2);
245 }
246
247 #[test]
248 fn test_create_checkpoint_stores_label() {
249 let mut cp = TensorCheckpointer::new(default_config());
250 let id = cp.create_checkpoint("my_label", vec![], empty_meta());
251 let c = cp.get_checkpoint(id);
252 assert!(c.is_some());
253 assert_eq!(c.map(|c| c.label.as_str()), Some("my_label"));
254 }
255
256 #[test]
257 fn test_create_checkpoint_stores_tensor_data() {
258 let mut cp = TensorCheckpointer::new(default_config());
259 let data = sample_tensors();
260 let id = cp.create_checkpoint("t", data.clone(), empty_meta());
261 let c = cp.get_checkpoint(id).expect("checkpoint should exist");
262 assert_eq!(c.tensor_data, data);
263 }
264
265 #[test]
266 fn test_create_checkpoint_records_tick() {
267 let mut cp = TensorCheckpointer::new(default_config());
268 cp.tick();
269 cp.tick();
270 let id = cp.create_checkpoint("at_tick_2", vec![], empty_meta());
271 let c = cp.get_checkpoint(id).expect("checkpoint should exist");
272 assert_eq!(c.tick, 2);
273 }
274
275 #[test]
278 fn test_max_checkpoints_eviction_fifo() {
279 let mut cp = TensorCheckpointer::new(config_with_max(3));
280 let id0 = cp.create_checkpoint("0", vec![], empty_meta());
281 let _id1 = cp.create_checkpoint("1", vec![], empty_meta());
282 let _id2 = cp.create_checkpoint("2", vec![], empty_meta());
283 assert_eq!(cp.checkpoint_count(), 3);
284
285 let _id3 = cp.create_checkpoint("3", vec![], empty_meta());
287 assert_eq!(cp.checkpoint_count(), 3);
288 assert!(cp.get_checkpoint(id0).is_none());
289 }
290
291 #[test]
292 fn test_eviction_preserves_newest() {
293 let mut cp = TensorCheckpointer::new(config_with_max(2));
294 let _id0 = cp.create_checkpoint("a", vec![], empty_meta());
295 let _id1 = cp.create_checkpoint("b", vec![], empty_meta());
296 let id2 = cp.create_checkpoint("c", vec![], empty_meta());
297
298 let latest = cp.latest_checkpoint().expect("should have latest");
299 assert_eq!(latest.id, id2);
300 }
301
302 #[test]
303 fn test_eviction_with_max_one() {
304 let mut cp = TensorCheckpointer::new(config_with_max(1));
305 let _id0 = cp.create_checkpoint("first", vec![], empty_meta());
306 let id1 = cp.create_checkpoint("second", vec![], empty_meta());
307 assert_eq!(cp.checkpoint_count(), 1);
308 assert!(cp.get_checkpoint(id1).is_some());
309 }
310
311 #[test]
314 fn test_rollback_returns_checkpoint_data() {
315 let mut cp = TensorCheckpointer::new(default_config());
316 let data = sample_tensors();
317 let id = cp.create_checkpoint("snap", data.clone(), empty_meta());
318 let restored = cp.rollback(id).expect("rollback should succeed");
319 assert_eq!(restored.tensor_data, data);
320 assert_eq!(restored.label, "snap");
321 }
322
323 #[test]
324 fn test_rollback_removes_newer_checkpoints() {
325 let mut cp = TensorCheckpointer::new(default_config());
326 let id0 = cp.create_checkpoint("0", vec![], empty_meta());
327 let id1 = cp.create_checkpoint("1", vec![], empty_meta());
328 let id2 = cp.create_checkpoint("2", vec![], empty_meta());
329
330 let _restored = cp.rollback(id1).expect("rollback should succeed");
331 assert_eq!(cp.checkpoint_count(), 1);
333 assert!(cp.get_checkpoint(id0).is_some());
334 assert!(cp.get_checkpoint(id1).is_none());
335 assert!(cp.get_checkpoint(id2).is_none());
336 }
337
338 #[test]
339 fn test_rollback_unknown_id_errors() {
340 let mut cp = TensorCheckpointer::new(default_config());
341 let result = cp.rollback(999);
342 assert!(result.is_err());
343 let msg = result.expect_err("should be error");
344 assert!(msg.contains("999"));
345 }
346
347 #[test]
348 fn test_rollback_increments_counter() {
349 let mut cp = TensorCheckpointer::new(default_config());
350 let id = cp.create_checkpoint("x", vec![], empty_meta());
351 assert_eq!(cp.stats().rollbacks_performed, 0);
352 let _ = cp.rollback(id);
353 assert_eq!(cp.stats().rollbacks_performed, 1);
354 }
355
356 #[test]
357 fn test_rollback_to_oldest() {
358 let mut cp = TensorCheckpointer::new(default_config());
359 let id0 = cp.create_checkpoint("oldest", vec![vec![0.0]], empty_meta());
360 let _id1 = cp.create_checkpoint("mid", vec![], empty_meta());
361 let _id2 = cp.create_checkpoint("newest", vec![], empty_meta());
362
363 let restored = cp.rollback(id0).expect("rollback should succeed");
364 assert_eq!(restored.label, "oldest");
365 assert_eq!(cp.checkpoint_count(), 0);
366 }
367
368 #[test]
369 fn test_rollback_to_latest() {
370 let mut cp = TensorCheckpointer::new(default_config());
371 let _id0 = cp.create_checkpoint("a", vec![], empty_meta());
372 let id1 = cp.create_checkpoint("b", vec![], empty_meta());
373
374 let restored = cp.rollback(id1).expect("rollback should succeed");
375 assert_eq!(restored.label, "b");
376 assert_eq!(cp.checkpoint_count(), 1); }
378
379 #[test]
380 fn test_multiple_rollbacks() {
381 let mut cp = TensorCheckpointer::new(default_config());
382 let _id0 = cp.create_checkpoint("a", vec![], empty_meta());
383 let id1 = cp.create_checkpoint("b", vec![], empty_meta());
384 let _ = cp.rollback(id1);
385 assert_eq!(cp.stats().rollbacks_performed, 1);
386
387 let id2 = cp.create_checkpoint("c", vec![], empty_meta());
388 let _ = cp.rollback(id2);
389 assert_eq!(cp.stats().rollbacks_performed, 2);
390 }
391
392 #[test]
395 fn test_latest_checkpoint_empty() {
396 let cp = TensorCheckpointer::new(default_config());
397 assert!(cp.latest_checkpoint().is_none());
398 }
399
400 #[test]
401 fn test_latest_checkpoint_returns_last_added() {
402 let mut cp = TensorCheckpointer::new(default_config());
403 let _id0 = cp.create_checkpoint("first", vec![], empty_meta());
404 let id1 = cp.create_checkpoint("second", vec![], empty_meta());
405 let latest = cp.latest_checkpoint().expect("should exist");
406 assert_eq!(latest.id, id1);
407 }
408
409 #[test]
410 fn test_get_checkpoint_nonexistent() {
411 let cp = TensorCheckpointer::new(default_config());
412 assert!(cp.get_checkpoint(42).is_none());
413 }
414
415 #[test]
418 fn test_should_auto_checkpoint_initially_true_when_interval_zero() {
419 let config = CheckpointConfig {
420 auto_checkpoint_interval: 0,
421 ..Default::default()
422 };
423 let cp = TensorCheckpointer::new(config);
424 assert!(cp.should_auto_checkpoint());
425 }
426
427 #[test]
428 fn test_should_auto_checkpoint_false_initially() {
429 let cp = TensorCheckpointer::new(default_config()); assert!(!cp.should_auto_checkpoint());
431 }
432
433 #[test]
434 fn test_should_auto_checkpoint_after_enough_ticks() {
435 let config = CheckpointConfig {
436 auto_checkpoint_interval: 5,
437 ..Default::default()
438 };
439 let mut cp = TensorCheckpointer::new(config);
440 for _ in 0..4 {
441 cp.tick();
442 assert!(!cp.should_auto_checkpoint());
443 }
444 cp.tick(); assert!(cp.should_auto_checkpoint());
446 }
447
448 #[test]
449 fn test_auto_checkpoint_resets_after_create() {
450 let config = CheckpointConfig {
451 auto_checkpoint_interval: 3,
452 ..Default::default()
453 };
454 let mut cp = TensorCheckpointer::new(config);
455 for _ in 0..3 {
456 cp.tick();
457 }
458 assert!(cp.should_auto_checkpoint());
459 cp.create_checkpoint("auto", vec![], empty_meta());
460 assert!(!cp.should_auto_checkpoint());
461 }
462
463 #[test]
466 fn test_list_checkpoints_empty() {
467 let cp = TensorCheckpointer::new(default_config());
468 assert!(cp.list_checkpoints().is_empty());
469 }
470
471 #[test]
472 fn test_list_checkpoints_order() {
473 let mut cp = TensorCheckpointer::new(default_config());
474 cp.create_checkpoint("a", vec![], empty_meta());
475 cp.tick();
476 cp.create_checkpoint("b", vec![], empty_meta());
477 let list = cp.list_checkpoints();
478 assert_eq!(list.len(), 2);
479 assert_eq!(list[0].1, "a");
480 assert_eq!(list[1].1, "b");
481 assert!(list[0].2 < list[1].2);
482 }
483
484 #[test]
485 fn test_list_checkpoints_after_eviction() {
486 let mut cp = TensorCheckpointer::new(config_with_max(2));
487 cp.create_checkpoint("a", vec![], empty_meta());
488 cp.create_checkpoint("b", vec![], empty_meta());
489 cp.create_checkpoint("c", vec![], empty_meta());
490 let list = cp.list_checkpoints();
491 assert_eq!(list.len(), 2);
492 assert_eq!(list[0].1, "b");
493 assert_eq!(list[1].1, "c");
494 }
495
496 #[test]
499 fn test_clear_all() {
500 let mut cp = TensorCheckpointer::new(default_config());
501 cp.create_checkpoint("a", vec![], empty_meta());
502 cp.create_checkpoint("b", vec![], empty_meta());
503 assert_eq!(cp.checkpoint_count(), 2);
504 cp.clear_all();
505 assert_eq!(cp.checkpoint_count(), 0);
506 assert!(cp.latest_checkpoint().is_none());
507 }
508
509 #[test]
510 fn test_clear_all_on_empty() {
511 let mut cp = TensorCheckpointer::new(default_config());
512 cp.clear_all(); assert_eq!(cp.checkpoint_count(), 0);
514 }
515
516 #[test]
519 fn test_stats_empty() {
520 let cp = TensorCheckpointer::new(default_config());
521 let s = cp.stats();
522 assert_eq!(s.total_checkpoints, 0);
523 assert!(s.oldest_tick.is_none());
524 assert!(s.newest_tick.is_none());
525 assert_eq!(s.rollbacks_performed, 0);
526 }
527
528 #[test]
529 fn test_stats_with_checkpoints() {
530 let mut cp = TensorCheckpointer::new(default_config());
531 cp.create_checkpoint("a", vec![], empty_meta());
532 cp.tick();
533 cp.tick();
534 cp.create_checkpoint("b", vec![], empty_meta());
535 let s = cp.stats();
536 assert_eq!(s.total_checkpoints, 2);
537 assert_eq!(s.oldest_tick, Some(0));
538 assert_eq!(s.newest_tick, Some(2));
539 }
540
541 #[test]
542 fn test_stats_after_rollback() {
543 let mut cp = TensorCheckpointer::new(default_config());
544 let id0 = cp.create_checkpoint("a", vec![], empty_meta());
545 cp.create_checkpoint("b", vec![], empty_meta());
546 let _ = cp.rollback(id0);
547 let s = cp.stats();
548 assert_eq!(s.total_checkpoints, 0);
549 assert_eq!(s.rollbacks_performed, 1);
550 }
551
552 #[test]
555 fn test_metadata_preserved() {
556 let mut cp = TensorCheckpointer::new(default_config());
557 let mut meta = HashMap::new();
558 meta.insert("epoch".to_string(), "42".to_string());
559 meta.insert("loss".to_string(), "0.01".to_string());
560 let id = cp.create_checkpoint("meta_test", vec![], meta.clone());
561 let c = cp.get_checkpoint(id).expect("should exist");
562 assert_eq!(c.metadata, meta);
563 }
564
565 #[test]
566 fn test_metadata_preserved_after_rollback() {
567 let mut cp = TensorCheckpointer::new(default_config());
568 let mut meta = HashMap::new();
569 meta.insert("key".to_string(), "value".to_string());
570 let id = cp.create_checkpoint("m", vec![], meta.clone());
571 let restored = cp.rollback(id).expect("rollback should succeed");
572 assert_eq!(restored.metadata, meta);
573 }
574
575 #[test]
578 fn test_empty_checkpointer_count() {
579 let cp = TensorCheckpointer::new(default_config());
580 assert_eq!(cp.checkpoint_count(), 0);
581 }
582
583 #[test]
584 fn test_tick_without_checkpoints() {
585 let mut cp = TensorCheckpointer::new(default_config());
586 for _ in 0..200 {
587 cp.tick();
588 }
589 assert!(cp.should_auto_checkpoint());
591 }
592
593 #[test]
594 fn test_create_after_clear() {
595 let mut cp = TensorCheckpointer::new(default_config());
596 cp.create_checkpoint("before", vec![], empty_meta());
597 cp.clear_all();
598 let id = cp.create_checkpoint("after", vec![], empty_meta());
599 assert_eq!(cp.checkpoint_count(), 1);
600 let c = cp.get_checkpoint(id).expect("should exist");
601 assert_eq!(c.label, "after");
602 }
603
604 #[test]
605 fn test_rollback_then_create() {
606 let mut cp = TensorCheckpointer::new(default_config());
607 let id0 = cp.create_checkpoint("a", vec![], empty_meta());
608 cp.create_checkpoint("b", vec![], empty_meta());
609 let _ = cp.rollback(id0);
610
611 let id_new = cp.create_checkpoint("c", vec![], empty_meta());
613 assert_eq!(cp.checkpoint_count(), 1);
614 let c = cp.get_checkpoint(id_new).expect("should exist");
615 assert_eq!(c.label, "c");
616 }
617
618 #[test]
619 fn test_large_tensor_data() {
620 let mut cp = TensorCheckpointer::new(default_config());
621 let big = vec![vec![0.5; 1000]; 10];
622 let id = cp.create_checkpoint("large", big.clone(), empty_meta());
623 let c = cp.get_checkpoint(id).expect("should exist");
624 assert_eq!(c.tensor_data.len(), 10);
625 assert_eq!(c.tensor_data[0].len(), 1000);
626 assert!((c.tensor_data[0][500] - 0.5).abs() < f64::EPSILON);
627 }
628
629 #[test]
630 fn test_checkpoint_ids_are_monotonic() {
631 let mut cp = TensorCheckpointer::new(default_config());
632 let mut prev = cp.create_checkpoint("0", vec![], empty_meta());
633 for i in 1..10 {
634 let id = cp.create_checkpoint(&i.to_string(), vec![], empty_meta());
635 assert!(id > prev);
636 prev = id;
637 }
638 }
639
640 #[test]
641 fn test_default_config_values() {
642 let cfg = CheckpointConfig::default();
643 assert_eq!(cfg.max_checkpoints, 5);
644 assert_eq!(cfg.auto_checkpoint_interval, 100);
645 }
646}