use std::path::Path;
use crate::error::{Error, Result};
use crate::nn::Module;
use crate::optim::{Optimizer, OptimizerState};
use crate::tensor::Tensor;
use super::checkpoint::{Checkpoint, load_tensors, save_tensors};
const PARAM: &str = "param/";
const BUFFER: &str = "buffer/";
const OPTIM: &str = "optim/";
const STEP: &str = "meta/step";
const OPTIM_STEPS: &str = "meta/optim_steps";
pub fn save_training(
model: &dyn Module,
optimizer: &dyn Optimizer,
step: u64,
path: impl AsRef<Path>,
) -> Result<()> {
let mut file = Checkpoint::new();
for (name, param) in model.named_parameters() {
file.insert(format!("{PARAM}{name}"), param.value().cpu());
}
for (name, buffer) in model.named_buffers() {
file.insert(format!("{BUFFER}{name}"), buffer.value().cpu());
}
let state = optimizer.state();
for (name, tensor) in state.tensors {
file.insert(format!("{OPTIM}{name}"), tensor);
}
file.insert(STEP.into(), Tensor::scalar(step as f32));
file.insert(OPTIM_STEPS.into(), Tensor::scalar(state.steps as f32));
save_tensors(&file, path)
}
pub fn load_training(
model: &dyn Module,
optimizer: &mut dyn Optimizer,
path: impl AsRef<Path>,
) -> Result<u64> {
let file = load_tensors(path)?;
if !file.contains_key(STEP) {
return Err(Error::Checkpoint(
"not a training checkpoint; it has no optimizer state, so resuming \
from it would restart the optimizer. Use load() if that is what you want."
.into(),
));
}
for (name, param) in model.named_parameters() {
let value = expect(&file, &format!("{PARAM}{name}"), ¶m.shape())?;
param.set_value(value.to(param.device()));
}
for (name, buffer) in model.named_buffers() {
let device = buffer.value().device();
let value = expect(&file, &format!("{BUFFER}{name}"), buffer.value().shape())?;
buffer.set_value(value.to(device));
}
let mut state = OptimizerState::new(scalar(&file, OPTIM_STEPS)? as u64);
for (name, tensor) in &file {
if let Some(slot) = name.strip_prefix(OPTIM) {
state.tensors.insert(slot.to_string(), tensor.clone());
}
}
optimizer.load_state(state)?;
Ok(scalar(&file, STEP)? as u64)
}
fn expect(file: &Checkpoint, name: &str, shape: &[usize]) -> Result<Tensor> {
let tensor = file
.get(name)
.ok_or_else(|| Error::Checkpoint(format!("checkpoint has no '{name}'")))?;
if tensor.shape() != shape {
return Err(Error::Checkpoint(format!(
"'{name}' is {:?} in the file but {shape:?} in the model",
tensor.shape()
)));
}
Ok(tensor.clone())
}
fn scalar(file: &Checkpoint, name: &str) -> Result<f32> {
file.get(name)
.map(|t| t.item())
.ok_or_else(|| Error::Checkpoint(format!("checkpoint has no '{name}'")))
}