1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3use crate::{OptimizerError, OptimizerResult};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone)]
9pub struct CheckpointConfig {
10 pub checkpoint_dir: PathBuf,
12 pub max_checkpoints: usize,
14 pub checkpoint_frequency: usize,
16 pub save_optimizer_state: bool,
18 pub save_gradients: bool,
20 pub compress: bool,
22 pub async_save: bool,
24}
25
26impl Default for CheckpointConfig {
27 fn default() -> Self {
28 Self {
29 checkpoint_dir: PathBuf::from("checkpoints"),
30 max_checkpoints: 5,
31 checkpoint_frequency: 1000,
32 save_optimizer_state: true,
33 save_gradients: false,
34 compress: true,
35 async_save: true,
36 }
37 }
38}
39
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
42pub struct CheckpointMetadata {
43 pub step: usize,
44 pub timestamp: u64,
45 pub loss: Option<f32>,
46 pub learning_rate: f32,
47 pub epoch: Option<usize>,
48 pub gradient_norm: Option<f32>,
49 pub model_hash: Option<String>,
50}
51
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
54pub struct Checkpoint {
55 pub metadata: CheckpointMetadata,
56 pub optimizer_state: Option<Vec<u8>>,
57 pub gradients: Option<HashMap<String, Vec<f32>>>,
58 pub model_parameters: Option<HashMap<String, Vec<f32>>>,
59}
60
61pub struct CheckpointManager {
63 config: CheckpointConfig,
64 checkpoints: VecDeque<(PathBuf, CheckpointMetadata)>,
65 current_step: usize,
66 last_checkpoint_step: usize,
67}
68
69impl CheckpointManager {
70 pub fn new(config: CheckpointConfig) -> Result<Self, OptimizerError> {
72 std::fs::create_dir_all(&config.checkpoint_dir).map_err(|e| {
74 OptimizerError::CheckpointError(format!("Failed to create checkpoint directory: {e}"))
75 })?;
76
77 let mut manager = Self {
78 config,
79 checkpoints: VecDeque::new(),
80 current_step: 0,
81 last_checkpoint_step: 0,
82 };
83
84 manager.load_existing_checkpoints()?;
86
87 Ok(manager)
88 }
89
90 pub fn should_checkpoint(&self) -> bool {
92 self.current_step > 0
93 && (self.current_step - self.last_checkpoint_step) >= self.config.checkpoint_frequency
94 }
95
96 pub fn save_checkpoint(
98 &mut self,
99 optimizer_state: Option<&[u8]>,
100 gradients: Option<&HashMap<String, Vec<f32>>>,
101 model_parameters: Option<&HashMap<String, Vec<f32>>>,
102 loss: Option<f32>,
103 learning_rate: f32,
104 epoch: Option<usize>,
105 ) -> Result<PathBuf, OptimizerError> {
106 let timestamp = SystemTime::now()
107 .duration_since(UNIX_EPOCH)
108 .map_err(|e| OptimizerError::CheckpointError(format!("Time error: {e}")))?
109 .as_secs();
110
111 let gradient_norm = gradients
112 .as_ref()
113 .map(|grads| grads.values().flatten().map(|&x| x * x).sum::<f32>().sqrt());
114
115 let model_hash = model_parameters.as_ref().map(|params| {
116 format!(
118 "{:x}",
119 params
120 .values()
121 .flatten()
122 .map(|&x| (x * 1000.0) as i32)
123 .fold(0u64, |acc, x| acc.wrapping_add(x as u64))
124 )
125 });
126
127 let metadata = CheckpointMetadata {
128 step: self.current_step,
129 timestamp,
130 loss,
131 learning_rate,
132 epoch,
133 gradient_norm,
134 model_hash,
135 };
136
137 let checkpoint = Checkpoint {
138 metadata: metadata.clone(),
139 optimizer_state: optimizer_state.map(|s| s.to_vec()),
140 gradients: gradients.cloned(),
141 model_parameters: model_parameters.cloned(),
142 };
143
144 let checkpoint_path = self
145 .config
146 .checkpoint_dir
147 .join(format!("checkpoint_step_{}.bin", self.current_step));
148
149 if self.config.async_save {
150 self.save_checkpoint_async(checkpoint, checkpoint_path.clone())?;
151 } else {
152 self.save_checkpoint_sync(&checkpoint, &checkpoint_path)?;
153 }
154
155 self.checkpoints
157 .push_back((checkpoint_path.clone(), metadata));
158 self.last_checkpoint_step = self.current_step;
159
160 self.cleanup_old_checkpoints()?;
162
163 Ok(checkpoint_path)
164 }
165
166 pub fn load_checkpoint(&self, checkpoint_path: &Path) -> Result<Checkpoint, OptimizerError> {
168 let data = std::fs::read(checkpoint_path).map_err(|e| {
169 OptimizerError::CheckpointError(format!("Failed to read checkpoint: {e}"))
170 })?;
171
172 let checkpoint: Checkpoint = if self.config.compress {
173 let decompressed = self.decompress_data(&data)?;
175 {
176 let (checkpoint, _): (Checkpoint, usize) =
177 oxicode::serde::decode_from_slice(&decompressed, oxicode::config::standard())
178 .map_err(|e| {
179 OptimizerError::CheckpointError(format!(
180 "Failed to deserialize checkpoint: {e}"
181 ))
182 })?;
183 checkpoint
184 }
185 } else {
186 let (checkpoint, _): (Checkpoint, usize) = oxicode::serde::decode_from_slice(
187 &data,
188 oxicode::config::standard(),
189 )
190 .map_err(|e| {
191 OptimizerError::CheckpointError(format!("Failed to deserialize checkpoint: {e}"))
192 })?;
193 checkpoint
194 };
195
196 Ok(checkpoint)
197 }
198
199 pub fn latest_checkpoint(&self) -> Option<&PathBuf> {
201 self.checkpoints.back().map(|(path, _)| path)
202 }
203
204 pub fn list_checkpoints(&self) -> Vec<&CheckpointMetadata> {
206 self.checkpoints
207 .iter()
208 .map(|(_, metadata)| metadata)
209 .collect()
210 }
211
212 pub fn step(&mut self) {
214 self.current_step += 1;
215 }
216
217 pub fn current_step(&self) -> usize {
219 self.current_step
220 }
221
222 pub fn set_step(&mut self, step: usize) {
224 self.current_step = step;
225 }
226
227 pub fn resume_from_latest(&mut self) -> Result<Option<Checkpoint>, OptimizerError> {
229 if let Some((checkpoint_path, metadata)) = self.checkpoints.back() {
230 self.current_step = metadata.step;
231 self.last_checkpoint_step = metadata.step;
232 Ok(Some(self.load_checkpoint(checkpoint_path)?))
233 } else {
234 Ok(None)
235 }
236 }
237
238 pub fn cleanup(&mut self) -> Result<(), OptimizerError> {
240 self.cleanup_old_checkpoints()
241 }
242
243 pub fn statistics(&self) -> CheckpointStatistics {
245 let total_size = self
246 .checkpoints
247 .iter()
248 .filter_map(|(path, _)| std::fs::metadata(path).ok())
249 .map(|metadata| metadata.len())
250 .sum();
251
252 CheckpointStatistics {
253 total_checkpoints: self.checkpoints.len(),
254 total_size_bytes: total_size,
255 current_step: self.current_step,
256 last_checkpoint_step: self.last_checkpoint_step,
257 next_checkpoint_step: self.last_checkpoint_step + self.config.checkpoint_frequency,
258 }
259 }
260
261 fn load_existing_checkpoints(&mut self) -> Result<(), OptimizerError> {
264 if !self.config.checkpoint_dir.exists() {
265 return Ok(());
266 }
267
268 let mut checkpoints = Vec::new();
269
270 for entry in std::fs::read_dir(&self.config.checkpoint_dir).map_err(|e| {
271 OptimizerError::CheckpointError(format!("Failed to read checkpoint directory: {e}"))
272 })? {
273 let entry = entry.map_err(|e| {
274 OptimizerError::CheckpointError(format!("Failed to read directory entry: {e}"))
275 })?;
276 let path = entry.path();
277
278 if path.extension().and_then(|s| s.to_str()) == Some("bin") {
279 if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
280 if let Some(stripped) = filename.strip_prefix("checkpoint_step_") {
281 if let Ok(step) = stripped.parse::<usize>() {
282 if let Ok(checkpoint) = self.load_checkpoint(&path) {
284 checkpoints.push((path, checkpoint.metadata));
285 }
286 }
287 }
288 }
289 }
290 }
291
292 checkpoints.sort_by_key(|(_, metadata)| metadata.step);
294
295 if checkpoints.len() > self.config.max_checkpoints {
297 let to_remove = checkpoints.len() - self.config.max_checkpoints;
298 for (path, _) in checkpoints.drain(..to_remove) {
299 let _ = std::fs::remove_file(path);
300 }
301 }
302
303 self.checkpoints = checkpoints.into();
304
305 if let Some((_, metadata)) = self.checkpoints.back() {
307 self.current_step = metadata.step;
308 self.last_checkpoint_step = metadata.step;
309 }
310 Ok(())
311 }
312
313 fn save_checkpoint_sync(
314 &self,
315 checkpoint: &Checkpoint,
316 path: &Path,
317 ) -> Result<(), OptimizerError> {
318 if let Some(parent) = path.parent() {
320 std::fs::create_dir_all(parent).map_err(|e| {
321 OptimizerError::CheckpointError(format!(
322 "Failed to create checkpoint directory: {e}"
323 ))
324 })?;
325 }
326
327 let data = oxicode::serde::encode_to_vec(checkpoint, oxicode::config::standard()).map_err(
328 |e| OptimizerError::CheckpointError(format!("Failed to serialize checkpoint: {e}")),
329 )?;
330
331 let final_data = if self.config.compress {
332 self.compress_data(&data)?
333 } else {
334 data
335 };
336
337 std::fs::write(path, final_data).map_err(|e| {
338 OptimizerError::CheckpointError(format!("Failed to write checkpoint: {e}"))
339 })
340 }
341
342 fn save_checkpoint_async(
343 &self,
344 checkpoint: Checkpoint,
345 path: PathBuf,
346 ) -> Result<(), OptimizerError> {
347 let compress = self.config.compress;
348
349 std::thread::spawn(move || {
350 let data = oxicode::serde::encode_to_vec(&checkpoint, oxicode::config::standard())
351 .expect("checkpoint serialization should succeed");
352 let final_data = if compress {
353 data } else {
356 data
357 };
358
359 let _ = std::fs::write(path, final_data);
360 });
361
362 Ok(())
363 }
364
365 fn cleanup_old_checkpoints(&mut self) -> Result<(), OptimizerError> {
366 while self.checkpoints.len() > self.config.max_checkpoints {
367 if let Some((old_path, _)) = self.checkpoints.pop_front() {
368 std::fs::remove_file(old_path).map_err(|e| {
369 OptimizerError::CheckpointError(format!("Failed to remove old checkpoint: {e}"))
370 })?;
371 }
372 }
373 Ok(())
374 }
375
376 fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, OptimizerError> {
377 Ok(data.to_vec())
379 }
380
381 fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, OptimizerError> {
382 Ok(data.to_vec())
384 }
385}
386
387#[derive(Debug, Clone)]
389pub struct CheckpointStatistics {
390 pub total_checkpoints: usize,
391 pub total_size_bytes: u64,
392 pub current_step: usize,
393 pub last_checkpoint_step: usize,
394 pub next_checkpoint_step: usize,
395}
396
397impl std::fmt::Display for CheckpointStatistics {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 writeln!(f, "Checkpoint Statistics:")?;
400 writeln!(f, " Total Checkpoints: {}", self.total_checkpoints)?;
401 writeln!(
402 f,
403 " Total Size: {:.2} MB",
404 self.total_size_bytes as f64 / 1024.0 / 1024.0
405 )?;
406 writeln!(f, " Current Step: {}", self.current_step)?;
407 writeln!(f, " Last Checkpoint: {}", self.last_checkpoint_step)?;
408 writeln!(f, " Next Checkpoint: {}", self.next_checkpoint_step)?;
409 Ok(())
410 }
411}
412
413pub trait CheckpointSupport {
415 fn save_state_for_checkpoint(&self) -> Result<Vec<u8>, OptimizerError>;
417
418 fn load_state_from_checkpoint(&mut self, data: &[u8]) -> Result<(), OptimizerError>;
420
421 fn get_gradients_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>>;
423
424 fn get_parameters_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>>;
426}
427
428pub struct CheckpointingOptimizer<T> {
430 inner: T,
431 checkpoint_manager: CheckpointManager,
432 auto_checkpoint: bool,
433}
434
435impl<T> CheckpointingOptimizer<T>
436where
437 T: CheckpointSupport,
438{
439 pub fn new(inner: T, config: CheckpointConfig) -> Result<Self, OptimizerError> {
441 let checkpoint_manager = CheckpointManager::new(config)?;
442
443 Ok(Self {
444 inner,
445 checkpoint_manager,
446 auto_checkpoint: true,
447 })
448 }
449
450 pub fn set_auto_checkpoint(&mut self, enabled: bool) {
452 self.auto_checkpoint = enabled;
453 }
454
455 pub fn inner(&self) -> &T {
457 &self.inner
458 }
459
460 pub fn inner_mut(&mut self) -> &mut T {
462 &mut self.inner
463 }
464
465 pub fn checkpoint_manager(&self) -> &CheckpointManager {
467 &self.checkpoint_manager
468 }
469
470 pub fn checkpoint_manager_mut(&mut self) -> &mut CheckpointManager {
472 &mut self.checkpoint_manager
473 }
474
475 pub fn save_checkpoint(
477 &mut self,
478 loss: Option<f32>,
479 learning_rate: f32,
480 epoch: Option<usize>,
481 ) -> Result<PathBuf, OptimizerError> {
482 let optimizer_state = self.inner.save_state_for_checkpoint()?;
483 let gradients = self.inner.get_gradients_for_checkpoint();
484 let parameters = self.inner.get_parameters_for_checkpoint();
485
486 self.checkpoint_manager.save_checkpoint(
487 Some(&optimizer_state),
488 gradients.as_ref(),
489 parameters.as_ref(),
490 loss,
491 learning_rate,
492 epoch,
493 )
494 }
495
496 pub fn resume_from_latest(&mut self) -> Result<bool, OptimizerError> {
498 if let Some(checkpoint) = self.checkpoint_manager.resume_from_latest()? {
499 if let Some(optimizer_state) = &checkpoint.optimizer_state {
500 self.inner.load_state_from_checkpoint(optimizer_state)?;
501 }
502 Ok(true)
503 } else {
504 Ok(false)
505 }
506 }
507
508 pub fn step_with_checkpoint(
510 &mut self,
511 loss: Option<f32>,
512 learning_rate: f32,
513 epoch: Option<usize>,
514 ) -> Result<Option<PathBuf>, OptimizerError> {
515 self.checkpoint_manager.step();
517
518 let checkpoint_path = if self.auto_checkpoint && self.checkpoint_manager.should_checkpoint()
519 {
520 Some(self.save_checkpoint(loss, learning_rate, epoch)?)
521 } else {
522 None
523 };
524
525 Ok(checkpoint_path)
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use std::collections::HashMap;
533
534 #[derive(Debug)]
535 struct MockOptimizer {
536 state: HashMap<String, f32>,
537 }
538
539 impl CheckpointSupport for MockOptimizer {
540 fn save_state_for_checkpoint(&self) -> Result<Vec<u8>, OptimizerError> {
541 Ok(oxicode::serde::encode_to_vec(&self.state, oxicode::config::standard()).unwrap())
542 }
543
544 fn load_state_from_checkpoint(&mut self, data: &[u8]) -> Result<(), OptimizerError> {
545 let (state, _): (HashMap<String, f32>, usize) =
546 oxicode::serde::decode_from_slice(data, oxicode::config::standard()).unwrap();
547 self.state = state;
548 Ok(())
549 }
550
551 fn get_gradients_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>> {
552 None
553 }
554
555 fn get_parameters_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>> {
556 None
557 }
558 }
559
560 #[test]
561 fn test_checkpoint_manager() -> OptimizerResult<()> {
562 let temp_dir = tempfile::tempdir()?;
563 let config = CheckpointConfig {
564 checkpoint_dir: temp_dir.path().to_path_buf(),
565 max_checkpoints: 3,
566 checkpoint_frequency: 2,
567 async_save: false, ..Default::default()
569 };
570
571 let mut manager = CheckpointManager::new(config)?;
572
573 assert!(!manager.should_checkpoint()); manager.step();
576 assert!(!manager.should_checkpoint()); manager.step();
578 assert!(manager.should_checkpoint()); let checkpoint_path =
582 manager.save_checkpoint(None, None, None, Some(0.5), 0.01, Some(1))?;
583
584 assert!(checkpoint_path.exists());
585
586 let loaded = manager.load_checkpoint(&checkpoint_path)?;
588 assert_eq!(loaded.metadata.step, 2);
589 assert_eq!(loaded.metadata.loss, Some(0.5));
590 Ok(())
591 }
592
593 #[test]
594 fn test_checkpointing_optimizer() -> OptimizerResult<()> {
595 let temp_dir = tempfile::tempdir()?;
596 let config = CheckpointConfig {
597 checkpoint_dir: temp_dir.path().to_path_buf(),
598 checkpoint_frequency: 1,
599 async_save: false, ..Default::default()
601 };
602
603 let optimizer = MockOptimizer {
604 state: [("lr".to_string(), 0.01)].iter().cloned().collect(),
605 };
606
607 let mut checkpointing_optimizer = CheckpointingOptimizer::new(optimizer, config)?;
608
609 let checkpoint_path =
611 checkpointing_optimizer.step_with_checkpoint(Some(0.5), 0.01, Some(1))?;
612
613 assert!(checkpoint_path.is_some());
614
615 let resumed = checkpointing_optimizer.resume_from_latest()?;
617 assert!(resumed);
618 Ok(())
619 }
620}