//! Checkpoint save/load. Dumps the model's `params()` (in their stable order) to //! a flat binary file and reloads them into a model with matching architecture. //! //! Format (little-endian): //! ```text //! magic : u32 = 0x58545254 ("XTRT") //! version : u32 = 1 //! n_params: u32 //! repeat n_params times: //! ndim : u32 //! dims : [u32; ndim] //! data : [f32; prod(dims)] //! ``` //! Architecture/config is NOT stored here — the caller rebuilds the model from //! the same `Config` and `load_into`s the params (the round-trip and resume both //! know their config). Gated behind `not(no_cuda)` (it round-trips GPU tensors). #![cfg(not(no_cuda))] use std::fs::File; use std::io::{BufReader, BufWriter, Read, Write}; use std::path::Path; use xtrain_autodiff::tape::Var; use xtrain_tensor::{Device, Tensor}; const MAGIC: u32 = 0x5854_5254; const VERSION: u32 = 1; /// Write every parameter (value) to `path` in `params()` order. pub fn save(path: &Path, params: &[Var]) -> std::io::Result<()> { let mut w = BufWriter::new(File::create(path)?); w.write_all(&MAGIC.to_le_bytes())?; w.write_all(&VERSION.to_le_bytes())?; w.write_all(&(params.len() as u32).to_le_bytes())?; for p in params { let v = p.value().to_device(Device::Cpu); let shape = v.shape(); w.write_all(&(shape.len() as u32).to_le_bytes())?; for &d in shape { w.write_all(&(d as u32).to_le_bytes())?; } for &x in v.as_slice::() { w.write_all(&x.to_le_bytes())?; } } w.flush() } /// Read a checkpoint and overwrite each parameter's value in `params` (in order). /// Shapes must match the saved ones. Tensors are placed on each param's device. pub fn load_into(path: &Path, params: &[Var]) -> std::io::Result<()> { let mut r = BufReader::new(File::open(path)?); assert_eq!(read_u32(&mut r)?, MAGIC, "bad checkpoint magic"); assert_eq!(read_u32(&mut r)?, VERSION, "unsupported checkpoint version"); let n = read_u32(&mut r)? as usize; assert_eq!(n, params.len(), "checkpoint param count != model"); for p in params { let ndim = read_u32(&mut r)? as usize; let mut dims = Vec::with_capacity(ndim); for _ in 0..ndim { dims.push(read_u32(&mut r)? as usize); } let numel: usize = dims.iter().product(); let mut data = vec![0.0f32; numel]; for slot in data.iter_mut() { *slot = read_f32(&mut r)?; } let device = p.value().device(); assert_eq!( p.value().shape(), dims.as_slice(), "checkpoint shape mismatch" ); p.set_value(Tensor::from_slice(&data, &dims).to_device(device)); } Ok(()) } fn read_u32(r: &mut R) -> std::io::Result { let mut b = [0u8; 4]; r.read_exact(&mut b)?; Ok(u32::from_le_bytes(b)) } fn read_f32(r: &mut R) -> std::io::Result { let mut b = [0u8; 4]; r.read_exact(&mut b)?; Ok(f32::from_le_bytes(b)) }