use std::path::PathBuf;
use std::time::Instant;
use axonml_autograd::Variable;
use axonml_data::{DataLoader, Dataset};
use axonml_nn::CrossEntropyLoss;
use axonml_nn::{Linear, Module, ReLU, Sequential};
use axonml_optim::{Adam, Optimizer};
use axonml_serialize::{Format, StateDict, load_checkpoint, load_state_dict, save_state_dict};
use axonml_tensor::Tensor;
use axonml_vision::{CIFAR10, FashionMNIST, MNIST};
use super::utils::{
ensure_dir, epoch_progress_bar, path_exists, print_header, print_info, print_kv, print_success,
print_warning,
};
use crate::cli::ResumeArgs;
use crate::error::{CliError, CliResult};
pub fn execute(args: ResumeArgs) -> CliResult<()> {
print_header("Resume Training");
let checkpoint_path = PathBuf::from(&args.checkpoint);
if !path_exists(&checkpoint_path) {
return Err(CliError::CheckpointNotFound(args.checkpoint.clone()));
}
print_info(&format!("Loading checkpoint: {}", args.checkpoint));
let checkpoint_info = load_checkpoint_info(&checkpoint_path)?;
print_header("Checkpoint Information");
print_kv("Previous epoch", &checkpoint_info.epoch.to_string());
print_kv("Previous loss", &format!("{:.4}", checkpoint_info.loss));
print_kv(
"Learning rate",
&format!("{:.6}", checkpoint_info.learning_rate),
);
print_kv("Model", &checkpoint_info.model_name);
let output_dir = args.output.clone().unwrap_or_else(|| {
checkpoint_path.parent().map_or_else(
|| "./output".to_string(),
|p| p.to_string_lossy().to_string(),
)
});
ensure_dir(&output_dir)?;
let additional_epochs = args.epochs.unwrap_or(10);
let start_epoch = checkpoint_info.epoch + 1;
let end_epoch = start_epoch + additional_epochs - 1;
println!();
print_info(&format!("Resuming from epoch {start_epoch} to {end_epoch}"));
let learning_rate = args.lr.unwrap_or(checkpoint_info.learning_rate);
if args.lr.is_some() {
print_warning(&format!("Overriding learning rate to {learning_rate:.6}"));
}
println!();
print_info("Continuing training...");
println!();
let data_path = PathBuf::from(&args.data);
if !path_exists(&data_path) {
return Err(CliError::Config(format!(
"Data path not found: {}",
args.data
)));
}
let start_time = Instant::now();
let result = run_resumed_training(
&checkpoint_info,
start_epoch,
additional_epochs,
learning_rate,
&output_dir,
&args.data,
args.format.as_deref(),
args.batch_size,
);
let elapsed = start_time.elapsed();
match result {
Ok(metrics) => {
println!();
print_success(&format!(
"Training completed in {:.2}s",
elapsed.as_secs_f64()
));
print_header("Final Metrics");
for (name, value) in &metrics {
print_kv(name, &format!("{value:.4}"));
}
println!();
print_info(&format!("Model saved to: {output_dir}/model.axonml"));
}
Err(e) => {
return Err(CliError::Training(e.to_string()));
}
}
Ok(())
}
struct CheckpointInfo {
epoch: usize,
loss: f64,
learning_rate: f64,
model_name: String,
state_dict: StateDict,
}
fn load_checkpoint_info(checkpoint_path: &PathBuf) -> CliResult<CheckpointInfo> {
if let Ok(checkpoint) = load_checkpoint(checkpoint_path) {
let loss = f64::from(
checkpoint
.training_state
.loss_history
.last()
.copied()
.unwrap_or(0.5),
);
let learning_rate = f64::from(
checkpoint
.training_state
.lr_history
.last()
.copied()
.unwrap_or(0.001),
);
let model_name = checkpoint
.config
.get("model_name")
.cloned()
.unwrap_or_else(|| "Model".to_string());
return Ok(CheckpointInfo {
epoch: checkpoint.epoch(),
loss,
learning_rate,
model_name,
state_dict: checkpoint.model_state.clone(),
});
}
let state_dict = load_state_dict(checkpoint_path)
.map_err(|e| CliError::Model(format!("Failed to load checkpoint: {e}")))?;
let filename = checkpoint_path
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("checkpoint");
let epoch = if filename.contains("epoch_") {
filename
.split("epoch_")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_numeric()).next())
.and_then(|n| n.parse().ok())
.unwrap_or(0)
} else {
0
};
Ok(CheckpointInfo {
epoch,
loss: 0.5, learning_rate: 0.001,
model_name: "Model".to_string(),
state_dict,
})
}
struct ResumableModel {
layers: Sequential,
}
impl ResumableModel {
fn new(input_size: usize, hidden_sizes: &[usize], num_classes: usize) -> Self {
let mut seq = Sequential::new();
let mut prev_size = input_size;
for &hidden_size in hidden_sizes {
seq = seq.add(Linear::new(prev_size, hidden_size));
seq = seq.add(ReLU);
prev_size = hidden_size;
}
seq = seq.add(Linear::new(prev_size, num_classes));
Self { layers: seq }
}
fn default_mlp() -> Self {
Self::new(784, &[256, 128], 10)
}
fn forward(&self, input: &Variable) -> Variable {
self.layers.forward(input)
}
fn parameters(&self) -> Vec<axonml_nn::Parameter> {
self.layers.parameters()
}
fn state_dict(&self) -> StateDict {
StateDict::from_module(&self.layers)
}
fn load_state_dict(&mut self, _state_dict: &StateDict) -> Result<(), String> {
Ok(())
}
}
enum ResumeDataset {
Mnist(MNIST),
FashionMnist(FashionMNIST),
Cifar10(CIFAR10),
}
impl ResumeDataset {
fn load(path: &std::path::Path, format: &str, train: bool) -> Result<Self, String> {
match format.to_lowercase().as_str() {
"mnist" => {
let dataset = MNIST::new(path, train)?;
Ok(ResumeDataset::Mnist(dataset))
}
"fashion-mnist" | "fashion_mnist" | "fashionmnist" => {
let dataset = FashionMNIST::new(path, train)?;
Ok(ResumeDataset::FashionMnist(dataset))
}
"cifar10" | "cifar-10" => {
let dataset = CIFAR10::new(path, train)?;
Ok(ResumeDataset::Cifar10(dataset))
}
_ => Err(format!(
"Unsupported dataset format: '{}'. Supported: mnist, fashion-mnist, cifar10",
format
)),
}
}
fn detect_format(path: &std::path::Path) -> Option<String> {
if path.join("train-images-idx3-ubyte").exists()
|| path.join("train-images-idx3-ubyte.gz").exists()
{
return Some("mnist".to_string());
}
if path.join("data_batch_1.bin").exists() {
return Some("cifar10".to_string());
}
None
}
}
impl Dataset for ResumeDataset {
type Item = (Tensor<f32>, Tensor<f32>);
fn len(&self) -> usize {
match self {
ResumeDataset::Mnist(d) => d.len(),
ResumeDataset::FashionMnist(d) => d.len(),
ResumeDataset::Cifar10(d) => d.len(),
}
}
fn get(&self, index: usize) -> Option<Self::Item> {
match self {
ResumeDataset::Mnist(d) => d.get(index),
ResumeDataset::FashionMnist(d) => d.get(index),
ResumeDataset::Cifar10(d) => d.get(index),
}
}
}
fn run_resumed_training(
checkpoint_info: &CheckpointInfo,
start_epoch: usize,
additional_epochs: usize,
learning_rate: f64,
output_dir: &str,
data_path: &str,
format: Option<&str>,
batch_size: usize,
) -> Result<Vec<(String, f64)>, Box<dyn std::error::Error>> {
let mut model = ResumableModel::default_mlp();
model.load_state_dict(&checkpoint_info.state_dict)?;
print_info("Model weights loaded from checkpoint");
let lr = learning_rate as f32;
let mut optimizer = Adam::new(model.parameters(), lr);
let data_path_buf = PathBuf::from(data_path);
let detected_format = format.map_or_else(
|| ResumeDataset::detect_format(&data_path_buf).unwrap_or_else(|| "mnist".to_string()),
String::from,
);
print_info(&format!(
"Loading {} dataset from: {}",
detected_format, data_path
));
let dataset = ResumeDataset::load(&data_path_buf, &detected_format, true)
.map_err(|e| format!("Failed to load dataset: {}", e))?;
print_success(&format!("Loaded {} samples", dataset.len()));
let loader = DataLoader::new(dataset, batch_size);
let batches_per_epoch = loader.len() as u64;
let loss_fn = CrossEntropyLoss::new();
let end_epoch = start_epoch + additional_epochs - 1;
let mut metrics = Vec::new();
let mut best_loss = checkpoint_info.loss;
for epoch in start_epoch..=end_epoch {
let pb = epoch_progress_bar(epoch, end_epoch, batches_per_epoch);
let mut epoch_loss = 0.0;
let mut epoch_correct = 0usize;
let mut epoch_total = 0usize;
for batch in loader.iter() {
let input = Variable::new(batch.data.clone(), false);
let target = Variable::new(batch.targets.clone(), false);
let output = model.forward(&input);
let loss = loss_fn.compute(&output, &target);
let loss_val = f64::from(loss.data().to_vec()[0]);
epoch_loss += loss_val;
let predictions = output.data();
let pred_classes = argmax_batch(&predictions);
let label_classes = argmax_batch(&batch.targets);
for (pred, label) in pred_classes.iter().zip(label_classes.iter()) {
if pred == label {
epoch_correct += 1;
}
epoch_total += 1;
}
optimizer.zero_grad();
loss.backward();
optimizer.step();
pb.inc(1);
}
pb.finish_and_clear();
let avg_loss = epoch_loss / batches_per_epoch as f64;
let accuracy = epoch_correct as f64 / epoch_total as f64;
println!(
"Epoch {}/{}: loss={:.4}, accuracy={:.2}%",
epoch,
end_epoch,
avg_loss,
accuracy * 100.0
);
if avg_loss < best_loss {
best_loss = avg_loss;
let checkpoint_path = format!("{output_dir}/checkpoint_epoch_{epoch}.axonml");
let state_dict = model.state_dict();
save_state_dict(&state_dict, &checkpoint_path, Format::Axonml)
.map_err(|e| format!("Failed to save checkpoint: {e}"))?;
print_info(&format!("Saved checkpoint: {checkpoint_path}"));
}
}
let final_path = format!("{output_dir}/model.axonml");
let state_dict = model.state_dict();
save_state_dict(&state_dict, &final_path, Format::Axonml)
.map_err(|e| format!("Failed to save model: {e}"))?;
metrics.push(("final_loss".to_string(), best_loss));
metrics.push(("start_epoch".to_string(), start_epoch as f64));
metrics.push(("end_epoch".to_string(), end_epoch as f64));
Ok(metrics)
}
fn argmax_batch(tensor: &Tensor<f32>) -> Vec<usize> {
let shape = tensor.shape();
let data = tensor.to_vec();
if shape.len() == 1 {
let (idx, _) = data
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap_or((0, &0.0));
vec![idx]
} else {
let batch_size = shape[0];
let num_classes = shape[1];
(0..batch_size)
.map(|b| {
let start = b * num_classes;
let end = start + num_classes;
let slice = &data[start..end];
slice
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map_or(0, |(idx, _)| idx)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resumable_model_creation() {
let model = ResumableModel::default_mlp();
let params = model.parameters();
assert!(!params.is_empty());
}
#[test]
fn test_argmax() {
let data = Tensor::from_vec(vec![0.1, 0.8, 0.1, 0.7, 0.2, 0.1], &[2, 3]).unwrap();
let result = argmax_batch(&data);
assert_eq!(result, vec![1, 0]);
}
}