use crate::context::{ContextConfig, InferenceContext};
use crate::error::{InferenceError, InferenceResult};
use kizzasi_core::HiddenState;
use scirs2_core::ndarray::Array1;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;
const CHECKPOINT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializableHiddenState {
hidden_dim: usize,
state_dim: usize,
state_data: Vec<f32>,
updated: bool,
}
impl SerializableHiddenState {
fn from_hidden_state(hs: &HiddenState) -> Self {
let state = hs.state();
let shape = state.shape();
let state_data: Vec<f32> = state.iter().copied().collect();
Self {
hidden_dim: shape[0],
state_dim: shape[1],
state_data,
updated: true, }
}
fn to_hidden_state(&self) -> InferenceResult<HiddenState> {
if self.state_data.len() != self.hidden_dim * self.state_dim {
return Err(InferenceError::DimensionMismatch {
expected: self.hidden_dim * self.state_dim,
got: self.state_data.len(),
});
}
let mut hs = HiddenState::new(self.hidden_dim, self.state_dim);
let state_array = scirs2_core::ndarray::Array2::from_shape_vec(
(self.hidden_dim, self.state_dim),
self.state_data.clone(),
)
.map_err(|e| InferenceError::SerializationError(e.to_string()))?;
hs.update(state_array);
Ok(hs)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
version: u32,
config: ContextConfig,
states: Vec<SerializableHiddenState>,
history: Vec<Vec<f32>>,
step_count: usize,
metadata: CheckpointMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointMetadata {
pub timestamp: u64,
pub description: String,
pub model_id: String,
pub tags: Vec<String>,
}
impl Default for CheckpointMetadata {
fn default() -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
timestamp,
description: String::new(),
model_id: String::from("unknown"),
tags: Vec::new(),
}
}
}
impl Checkpoint {
pub fn from_context(context: &InferenceContext) -> Self {
let states: Vec<SerializableHiddenState> = context
.states()
.iter()
.map(SerializableHiddenState::from_hidden_state)
.collect();
let history: Vec<Vec<f32>> = context
.recent_history(context.history_len())
.into_iter()
.rev() .map(|arr| arr.iter().copied().collect())
.collect();
Self {
version: CHECKPOINT_VERSION,
config: context.config().clone(),
states,
history,
step_count: context.step_count(),
metadata: CheckpointMetadata::default(),
}
}
pub fn to_context(&self) -> InferenceResult<InferenceContext> {
if self.version != CHECKPOINT_VERSION {
return Err(InferenceError::SerializationError(format!(
"Incompatible checkpoint version: expected {}, got {}",
CHECKPOINT_VERSION, self.version
)));
}
let mut context = InferenceContext::new(self.config.clone());
for (i, serialized_state) in self.states.iter().enumerate() {
let state = serialized_state.to_hidden_state()?;
context.update_state(i, state)?;
}
for hist_vec in &self.history {
let arr = Array1::from_vec(hist_vec.clone());
context.push(arr);
}
Ok(context)
}
pub fn with_metadata(mut self, metadata: CheckpointMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_description(mut self, description: String) -> Self {
self.metadata.description = description;
self
}
pub fn with_model_id(mut self, model_id: String) -> Self {
self.metadata.model_id = model_id;
self
}
pub fn with_tag(mut self, tag: String) -> Self {
self.metadata.tags.push(tag);
self
}
pub fn metadata(&self) -> &CheckpointMetadata {
&self.metadata
}
pub fn version(&self) -> u32 {
self.version
}
pub fn step_count(&self) -> usize {
self.step_count
}
pub fn save_json<P: AsRef<Path>>(&self, path: P) -> InferenceResult<()> {
let file =
File::create(path).map_err(|e| InferenceError::SerializationError(e.to_string()))?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, self)
.map_err(|e| InferenceError::SerializationError(e.to_string()))?;
Ok(())
}
pub fn load_json<P: AsRef<Path>>(path: P) -> InferenceResult<Self> {
let file =
File::open(path).map_err(|e| InferenceError::SerializationError(e.to_string()))?;
let reader = BufReader::new(file);
let checkpoint = serde_json::from_reader(reader)
.map_err(|e| InferenceError::SerializationError(e.to_string()))?;
Ok(checkpoint)
}
#[cfg(feature = "msgpack")]
pub fn save_msgpack<P: AsRef<Path>>(&self, path: P) -> InferenceResult<()> {
let file =
File::create(path).map_err(|e| InferenceError::SerializationError(e.to_string()))?;
let mut writer = BufWriter::new(file);
rmp_serde::encode::write(&mut writer, self)
.map_err(|e| InferenceError::SerializationError(e.to_string()))?;
Ok(())
}
#[cfg(feature = "msgpack")]
pub fn load_msgpack<P: AsRef<Path>>(path: P) -> InferenceResult<Self> {
let file =
File::open(path).map_err(|e| InferenceError::SerializationError(e.to_string()))?;
let reader = BufReader::new(file);
let checkpoint = rmp_serde::from_read(reader)
.map_err(|e| InferenceError::SerializationError(e.to_string()))?;
Ok(checkpoint)
}
pub fn to_bytes(&self) -> InferenceResult<Vec<u8>> {
serde_json::to_vec(self).map_err(|e| InferenceError::SerializationError(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> InferenceResult<Self> {
serde_json::from_slice(bytes).map_err(|e| InferenceError::SerializationError(e.to_string()))
}
}
#[derive(Debug)]
pub struct CheckpointManager {
max_checkpoints: usize,
checkpoints: VecDeque<Checkpoint>,
}
impl CheckpointManager {
pub fn new(max_checkpoints: usize) -> Self {
Self {
max_checkpoints,
checkpoints: VecDeque::new(),
}
}
pub fn save(&mut self, checkpoint: Checkpoint) {
if self.checkpoints.len() >= self.max_checkpoints {
self.checkpoints.pop_back();
}
self.checkpoints.push_front(checkpoint);
}
pub fn latest(&self) -> Option<&Checkpoint> {
self.checkpoints.front()
}
pub fn rollback(&mut self) -> Option<Checkpoint> {
self.checkpoints.pop_front()
}
pub fn get(&self, index: usize) -> Option<&Checkpoint> {
self.checkpoints.get(index)
}
pub fn len(&self) -> usize {
self.checkpoints.len()
}
pub fn is_empty(&self) -> bool {
self.checkpoints.is_empty()
}
pub fn clear(&mut self) {
self.checkpoints.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkpoint_creation() {
let config = ContextConfig::new().num_layers(2).store_history(true);
let mut context = InferenceContext::new(config);
context.push(Array1::from_vec(vec![1.0, 2.0]));
context.push(Array1::from_vec(vec![3.0, 4.0]));
let checkpoint = Checkpoint::from_context(&context);
assert_eq!(checkpoint.version(), CHECKPOINT_VERSION);
assert_eq!(checkpoint.states.len(), 2);
assert_eq!(checkpoint.history.len(), 2);
}
#[test]
fn test_checkpoint_roundtrip() {
let mut config = ContextConfig::new();
config.num_layers = 2;
config.hidden_dim = 4;
config.store_history = true;
let mut context = InferenceContext::new(config);
context.push(Array1::from_vec(vec![1.0, 2.0]));
context.push(Array1::from_vec(vec![3.0, 4.0]));
let checkpoint = Checkpoint::from_context(&context);
let restored = checkpoint.to_context().unwrap();
assert_eq!(restored.step_count(), context.step_count());
assert_eq!(restored.states().len(), context.states().len());
}
#[test]
fn test_checkpoint_serialization() {
let config = ContextConfig::new().num_layers(2).store_history(true);
let mut context = InferenceContext::new(config);
context.push(Array1::from_vec(vec![1.0]));
let checkpoint = Checkpoint::from_context(&context);
let bytes = checkpoint.to_bytes().unwrap();
let restored = Checkpoint::from_bytes(&bytes).unwrap();
assert_eq!(restored.version(), checkpoint.version());
assert_eq!(restored.states.len(), checkpoint.states.len());
}
#[test]
fn test_checkpoint_metadata() {
let config = ContextConfig::new().num_layers(1);
let context = InferenceContext::new(config);
let checkpoint = Checkpoint::from_context(&context)
.with_description("Test checkpoint".to_string())
.with_model_id("test-model".to_string())
.with_tag("v1".to_string());
assert_eq!(checkpoint.metadata().description, "Test checkpoint");
assert_eq!(checkpoint.metadata().model_id, "test-model");
assert_eq!(checkpoint.metadata().tags, vec!["v1"]);
}
#[test]
fn test_checkpoint_manager() {
let mut manager = CheckpointManager::new(3);
let config = ContextConfig::new().num_layers(1).store_history(true);
for i in 0..5 {
let mut context = InferenceContext::new(config.clone());
context.push(Array1::from_vec(vec![i as f32]));
let checkpoint = Checkpoint::from_context(&context);
manager.save(checkpoint);
}
assert_eq!(manager.len(), 3);
let latest = manager.latest().unwrap();
assert_eq!(latest.history[0][0], 4.0);
}
#[test]
fn test_checkpoint_rollback() {
let mut manager = CheckpointManager::new(5);
let config = ContextConfig::new().num_layers(1).store_history(true);
for i in 0..3 {
let mut context = InferenceContext::new(config.clone());
context.push(Array1::from_vec(vec![i as f32]));
manager.save(Checkpoint::from_context(&context));
}
assert_eq!(manager.len(), 3);
let rolled_back = manager.rollback().unwrap();
assert_eq!(rolled_back.history[0][0], 2.0);
assert_eq!(manager.len(), 2);
}
#[test]
fn test_checkpoint_file_io() {
use std::env;
let config = ContextConfig::new().num_layers(2).store_history(true);
let mut context = InferenceContext::new(config);
context.push(Array1::from_vec(vec![1.0, 2.0]));
context.push(Array1::from_vec(vec![3.0, 4.0]));
let checkpoint =
Checkpoint::from_context(&context).with_description("Test save/load".to_string());
let tmp_dir = env::temp_dir();
let path = tmp_dir.join("test_checkpoint.json");
checkpoint.save_json(&path).unwrap();
let loaded = Checkpoint::load_json(&path).unwrap();
assert_eq!(loaded.metadata().description, "Test save/load");
assert_eq!(loaded.states.len(), 2);
assert_eq!(loaded.history.len(), 2);
std::fs::remove_file(path).ok();
}
}