Skip to main content

torsh_optim/
checkpointing.rs

1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3// use std::sync::Arc;
4use crate::{OptimizerError, OptimizerResult};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// Configuration for gradient checkpointing
8#[derive(Debug, Clone)]
9pub struct CheckpointConfig {
10    /// Directory to store checkpoints
11    pub checkpoint_dir: PathBuf,
12    /// Maximum number of checkpoints to keep
13    pub max_checkpoints: usize,
14    /// Frequency of checkpointing (every N steps)
15    pub checkpoint_frequency: usize,
16    /// Whether to save optimizer state
17    pub save_optimizer_state: bool,
18    /// Whether to save gradients
19    pub save_gradients: bool,
20    /// Whether to compress checkpoints
21    pub compress: bool,
22    /// Async checkpoint saving
23    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/// Checkpoint metadata
41#[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/// Checkpoint data structure
53#[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
61/// Gradient checkpointing manager
62pub struct CheckpointManager {
63    config: CheckpointConfig,
64    checkpoints: VecDeque<(PathBuf, CheckpointMetadata)>,
65    current_step: usize,
66    last_checkpoint_step: usize,
67}
68
69impl CheckpointManager {
70    /// Create a new checkpoint manager
71    pub fn new(config: CheckpointConfig) -> Result<Self, OptimizerError> {
72        // Create checkpoint directory if it doesn't exist
73        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        // Load existing checkpoints
85        manager.load_existing_checkpoints()?;
86
87        Ok(manager)
88    }
89
90    /// Check if a checkpoint should be saved at the current step
91    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    /// Save a checkpoint
97    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            // Simple hash of model parameters for integrity check
117            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        // Update checkpoint list
156        self.checkpoints
157            .push_back((checkpoint_path.clone(), metadata));
158        self.last_checkpoint_step = self.current_step;
159
160        // Remove old checkpoints if necessary
161        self.cleanup_old_checkpoints()?;
162
163        Ok(checkpoint_path)
164    }
165
166    /// Load a checkpoint
167    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            // Decompress if needed
174            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    /// Get the latest checkpoint path
200    pub fn latest_checkpoint(&self) -> Option<&PathBuf> {
201        self.checkpoints.back().map(|(path, _)| path)
202    }
203
204    /// Get all checkpoint metadata
205    pub fn list_checkpoints(&self) -> Vec<&CheckpointMetadata> {
206        self.checkpoints
207            .iter()
208            .map(|(_, metadata)| metadata)
209            .collect()
210    }
211
212    /// Increment current step
213    pub fn step(&mut self) {
214        self.current_step += 1;
215    }
216
217    /// Get current step
218    pub fn current_step(&self) -> usize {
219        self.current_step
220    }
221
222    /// Set current step
223    pub fn set_step(&mut self, step: usize) {
224        self.current_step = step;
225    }
226
227    /// Resume from the latest checkpoint
228    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    /// Clean up checkpoints manually
239    pub fn cleanup(&mut self) -> Result<(), OptimizerError> {
240        self.cleanup_old_checkpoints()
241    }
242
243    /// Get checkpoint statistics
244    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    // Private methods
262
263    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                            // Try to load metadata from the checkpoint
283                            if let Ok(checkpoint) = self.load_checkpoint(&path) {
284                                checkpoints.push((path, checkpoint.metadata));
285                            }
286                        }
287                    }
288                }
289            }
290        }
291
292        // Sort by step
293        checkpoints.sort_by_key(|(_, metadata)| metadata.step);
294
295        // Keep only the most recent checkpoints
296        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        // Update current step to the latest checkpoint
306        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        // Ensure parent directory exists
319        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                // Simple compression implementation
354                data // For now, just use the original data
355            } 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        // Simple compression - in practice, you'd use a real compression library
378        Ok(data.to_vec())
379    }
380
381    fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, OptimizerError> {
382        // Simple decompression - in practice, you'd use a real compression library
383        Ok(data.to_vec())
384    }
385}
386
387/// Checkpoint statistics
388#[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
413/// Trait for optimizers that support checkpointing
414pub trait CheckpointSupport {
415    /// Save optimizer state for checkpointing
416    fn save_state_for_checkpoint(&self) -> Result<Vec<u8>, OptimizerError>;
417
418    /// Load optimizer state from checkpoint
419    fn load_state_from_checkpoint(&mut self, data: &[u8]) -> Result<(), OptimizerError>;
420
421    /// Get current gradients for checkpointing
422    fn get_gradients_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>>;
423
424    /// Get current model parameters for checkpointing
425    fn get_parameters_for_checkpoint(&self) -> Option<HashMap<String, Vec<f32>>>;
426}
427
428/// Wrapper optimizer that adds checkpointing support
429pub 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    /// Create a new checkpointing optimizer wrapper
440    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    /// Enable or disable automatic checkpointing
451    pub fn set_auto_checkpoint(&mut self, enabled: bool) {
452        self.auto_checkpoint = enabled;
453    }
454
455    /// Get the inner optimizer
456    pub fn inner(&self) -> &T {
457        &self.inner
458    }
459
460    /// Get the inner optimizer mutably
461    pub fn inner_mut(&mut self) -> &mut T {
462        &mut self.inner
463    }
464
465    /// Get the checkpoint manager
466    pub fn checkpoint_manager(&self) -> &CheckpointManager {
467        &self.checkpoint_manager
468    }
469
470    /// Get the checkpoint manager mutably
471    pub fn checkpoint_manager_mut(&mut self) -> &mut CheckpointManager {
472        &mut self.checkpoint_manager
473    }
474
475    /// Manually save a checkpoint
476    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    /// Resume from the latest checkpoint
497    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    /// Step the optimizer and handle checkpointing
509    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        // Step the checkpoint manager
516        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, // Use synchronous saving for tests
568            ..Default::default()
569        };
570
571        let mut manager = CheckpointManager::new(config)?;
572
573        // Test checkpoint frequency
574        assert!(!manager.should_checkpoint()); // step 0
575        manager.step();
576        assert!(!manager.should_checkpoint()); // step 1
577        manager.step();
578        assert!(manager.should_checkpoint()); // step 2
579
580        // Save a checkpoint
581        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        // Load the checkpoint
587        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, // Use synchronous saving for tests
600            ..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        // Test step with checkpoint
610        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        // Test resume
616        let resumed = checkpointing_optimizer.resume_from_latest()?;
617        assert!(resumed);
618        Ok(())
619    }
620}