use crate::training::config::CheckpointConfig;
use crate::training::metrics::TrainingMetrics;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
pub epoch: usize,
pub model_state: HashMap<String, Vec<f32>>,
pub optimizer_state: crate::training::optimizers::OptimizerState,
pub metrics: Option<TrainingMetrics>,
pub metadata: HashMap<String, String>,
}
pub struct CheckpointManager {
config: CheckpointConfig,
checkpoint_dir: PathBuf,
checkpoints: Vec<CheckpointInfo>,
}
#[derive(Debug, Clone)]
struct CheckpointInfo {
path: PathBuf,
epoch: usize,
metrics: Option<TrainingMetrics>,
created_at: chrono::DateTime<chrono::Utc>,
}
impl CheckpointManager {
pub fn new(config: &CheckpointConfig) -> Result<Self> {
let checkpoint_dir = PathBuf::from(&config.checkpoint_dir);
if !checkpoint_dir.exists() {
std::fs::create_dir_all(&checkpoint_dir)?;
}
let mut manager = Self {
config: config.clone(),
checkpoint_dir,
checkpoints: Vec::new(),
};
manager.load_existing_checkpoints()?;
Ok(manager)
}
pub async fn save_checkpoint(&mut self, checkpoint: &Checkpoint) -> Result<()> {
let filename = format!("checkpoint_epoch_{:04}.json", checkpoint.epoch);
let filepath = self.checkpoint_dir.join(&filename);
let json = serde_json::to_string_pretty(checkpoint)?;
fs::write(&filepath, json).await?;
let checkpoint_info = CheckpointInfo {
path: filepath.clone(),
epoch: checkpoint.epoch,
metrics: checkpoint.metrics.clone(),
created_at: chrono::Utc::now(),
};
self.checkpoints.push(checkpoint_info);
self.cleanup_old_checkpoints().await?;
tracing::info!("Checkpoint saved: {}", filepath.display());
Ok(())
}
pub fn load_latest_checkpoint(&self) -> Result<Option<Checkpoint>> {
if let Some(latest) = self.checkpoints.last() {
self.load_checkpoint(&latest.path)
} else {
Ok(None)
}
}
pub fn load_checkpoint_by_epoch(&self, epoch: usize) -> Result<Option<Checkpoint>> {
if let Some(checkpoint_info) = self.checkpoints.iter().find(|c| c.epoch == epoch) {
self.load_checkpoint(&checkpoint_info.path)
} else {
Ok(None)
}
}
pub fn load_best_checkpoint(&self) -> Result<Option<Checkpoint>> {
if let Some(best) = self.find_best_checkpoint() {
self.load_checkpoint(&best.path)
} else {
Ok(None)
}
}
fn load_checkpoint(&self, filepath: &Path) -> Result<Option<Checkpoint>> {
let json = std::fs::read_to_string(filepath)?;
let checkpoint: Checkpoint = serde_json::from_str(&json)?;
Ok(Some(checkpoint))
}
pub fn list_checkpoints(&self) -> Vec<CheckpointInfo> {
self.checkpoints.clone()
}
pub fn checkpoint_dir(&self) -> &Path {
&self.checkpoint_dir
}
fn load_existing_checkpoints(&mut self) -> Result<()> {
let mut entries = std::fs::read_dir(&self.checkpoint_dir)?;
while let Some(entry) = entries.next() {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
if filename.starts_with("checkpoint_epoch_") {
if let Some(epoch_str) = filename.strip_prefix("checkpoint_epoch_") {
if let Ok(epoch) = epoch_str.parse::<usize>() {
let checkpoint_info = CheckpointInfo {
path: path.clone(),
epoch,
metrics: None, created_at: chrono::Utc::now(),
};
self.checkpoints.push(checkpoint_info);
}
}
}
}
}
}
self.checkpoints.sort_by_key(|c| c.epoch);
Ok(())
}
async fn cleanup_old_checkpoints(&mut self) -> Result<()> {
if self.checkpoints.len() > self.config.max_checkpoints {
let to_remove = self.checkpoints.len() - self.config.max_checkpoints;
for _ in 0..to_remove {
if let Some(checkpoint_info) = self.checkpoints.pop() {
if checkpoint_info.path.exists() {
fs::remove_file(&checkpoint_info.path).await?;
tracing::info!(
"Removed old checkpoint: {}",
checkpoint_info.path.display()
);
}
}
}
}
Ok(())
}
fn find_best_checkpoint(&self) -> Option<&CheckpointInfo> {
if self.checkpoints.is_empty() {
return None;
}
match self.config.best_metric.as_str() {
"val_accuracy" | "accuracy" => self.checkpoints.iter().max_by(|a, b| {
let a_acc = a.metrics.as_ref().map(|m| m.accuracy()).unwrap_or(0.0);
let b_acc = b.metrics.as_ref().map(|m| m.accuracy()).unwrap_or(0.0);
a_acc
.partial_cmp(&b_acc)
.unwrap_or(std::cmp::Ordering::Equal)
}),
"val_loss" | "loss" => self.checkpoints.iter().min_by(|a, b| {
let a_loss = a
.metrics
.as_ref()
.map(|m| m.loss())
.unwrap_or(f32::INFINITY);
let b_loss = b
.metrics
.as_ref()
.map(|m| m.loss())
.unwrap_or(f32::INFINITY);
a_loss
.partial_cmp(&b_loss)
.unwrap_or(std::cmp::Ordering::Equal)
}),
_ => self.checkpoints.last(),
}
}
}
pub struct CheckpointUtils;
impl CheckpointUtils {
pub fn create_checkpoint(
epoch: usize,
model_state: HashMap<String, Vec<f32>>,
optimizer_state: crate::training::optimizers::OptimizerState,
metrics: Option<TrainingMetrics>,
) -> Checkpoint {
Checkpoint {
epoch,
model_state,
optimizer_state,
metrics,
metadata: HashMap::new(),
}
}
pub fn validate_checkpoint(checkpoint: &Checkpoint) -> Result<()> {
if checkpoint.model_state.is_empty() {
return Err(anyhow::anyhow!("Checkpoint model state is empty"));
}
if checkpoint.optimizer_state.optimizer_type.is_empty() {
return Err(anyhow::anyhow!("Checkpoint optimizer state is invalid"));
}
if let Some(ref metrics) = checkpoint.metrics {
if metrics.loss() < 0.0 {
return Err(anyhow::anyhow!("Invalid loss value in checkpoint"));
}
if metrics.accuracy() < 0.0 || metrics.accuracy() > 1.0 {
return Err(anyhow::anyhow!("Invalid accuracy value in checkpoint"));
}
}
Ok(())
}
pub fn compare_checkpoints(a: &Checkpoint, b: &Checkpoint) -> CheckpointComparison {
CheckpointComparison {
epoch_diff: a.epoch as i32 - b.epoch as i32,
loss_diff: a.metrics.as_ref().map(|m| m.loss()).unwrap_or(0.0)
- b.metrics.as_ref().map(|m| m.loss()).unwrap_or(0.0),
accuracy_diff: a.metrics.as_ref().map(|m| m.accuracy()).unwrap_or(0.0)
- b.metrics.as_ref().map(|m| m.accuracy()).unwrap_or(0.0),
}
}
}
#[derive(Debug, Clone)]
pub struct CheckpointComparison {
pub epoch_diff: i32,
pub loss_diff: f32,
pub accuracy_diff: f32,
}
pub struct CheckpointBackup {
backup_dir: PathBuf,
max_backups: usize,
}
impl CheckpointBackup {
pub fn new(backup_dir: PathBuf, max_backups: usize) -> Self {
Self {
backup_dir,
max_backups,
}
}
pub async fn backup_checkpoint(&self, checkpoint: &Checkpoint) -> Result<()> {
if !self.backup_dir.exists() {
fs::create_dir_all(&self.backup_dir).await?;
}
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!(
"checkpoint_backup_epoch_{}_at_{}.json",
checkpoint.epoch, timestamp
);
let filepath = self.backup_dir.join(filename);
let json = serde_json::to_string_pretty(checkpoint)?;
fs::write(&filepath, json).await?;
tracing::info!("Checkpoint backed up to: {}", filepath.display());
Ok(())
}
pub async fn list_backups(&self) -> Result<Vec<PathBuf>> {
if !self.backup_dir.exists() {
return Ok(Vec::new());
}
let mut backups = Vec::new();
let mut entries = fs::read_dir(&self.backup_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
backups.push(path);
}
}
backups.sort_by_key(|p| p.metadata().unwrap().created().unwrap());
backups.reverse();
Ok(backups)
}
pub async fn cleanup_old_backups(&self) -> Result<()> {
let backups = self.list_backups().await?;
if backups.len() > self.max_backups {
let to_remove = backups.len() - self.max_backups;
for backup in backups.into_iter().skip(self.max_backups) {
fs::remove_file(&backup).await?;
tracing::info!("Removed old backup: {}", backup.display());
}
}
Ok(())
}
}