Files
foxhunt/crates/ml-supervised/src/checkpoint.rs
jgrusewski d313486dc2 refactor(ml): split monolith into 9 sub-crates + delete dead code
Extract 9 new sub-crates from the ml monolith to enable parallel
compilation across the workspace:

New crates (this commit):
- ml-features (282 tests): feature engineering, 21 modules
- ml-labeling (45 tests): triple barrier, meta-labeling, fractional diff
- ml-ensemble (116 tests): ensemble coordination, voting, confidence
- ml-hyperopt (47 tests): core PSO/TPE optimizer, parameter space
- ml-checkpoint (41 tests): checkpoint persistence, compression, signing
- ml-regime (68 tests): CUSUM, Bayesian changepoint, regime classification
- ml-data-validation (67 tests): FDR correction, CPCV, data quality
- ml-risk (33 tests): neural VaR, Kelly criterion, circuit breakers
- ml-validation (43 tests): statistical validation, walk-forward, DSR

Extended existing crates:
- ml-dqn: added evaluation/ (backtesting engine, metrics, reports)
  and checkpoint implementation
- ml-supervised: added checkpoint implementations
- ml-core: added shared types needed by new sub-crates

Pattern: each module in ml/ becomes a thin facade (pub use subcrate::*)
with bridge modules staying in ml for cross-model adapter code.

Dead code deleted (~7K lines):
- 13 undeclared files in microstructure/ (never compiled)
- 7 undeclared files + tests/ in risk/ (never compiled)
- parquet_io, cache_service, cache_storage, minio_integration (unused)
- extraction_wave_d_impl.rs (bare fn outside impl block)

All 2,746 sub-crate tests + 951 ml tests pass.
Full workspace builds clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00

320 lines
12 KiB
Rust

//! Checkpoint implementations for supervised models (Mamba2, TGGN).
use std::collections::HashMap;
use async_trait::async_trait;
use ml_core::{Checkpointable, MLError, ModelType};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::debug;
use crate::mamba::{Mamba2Config, Mamba2SSM};
use crate::tgnn::{TGGNConfig, TGGN};
// ========== MAMBA2 ==========
/// Serializable state for Mamba2 model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MambaCheckpointState {
pub config: Mamba2Config,
pub epoch: Option<u64>,
pub step: Option<u64>,
pub training_loss: f64,
pub validation_loss: f64,
pub ssd_layer_weights: Vec<Vec<f32>>,
pub input_projection_weights: Vec<f32>,
pub output_projection_weights: Vec<f32>,
pub layer_norm_weights: Vec<Vec<f32>>,
pub ssm_a_matrices: Vec<Vec<f32>>,
pub ssm_b_matrices: Vec<Vec<f32>>,
pub ssm_c_matrices: Vec<Vec<f32>>,
pub ssm_delta_params: Vec<f32>,
pub total_inferences: u64,
pub avg_latency_us: f64,
pub throughput_pps: f64,
}
#[async_trait]
impl Checkpointable for Mamba2SSM {
fn model_type(&self) -> ModelType {
ModelType::MAMBA
}
fn model_name(&self) -> &str {
&self.metadata.model_id
}
fn model_version(&self) -> &str {
&self.metadata.version
}
async fn serialize_state(&self) -> Result<Vec<u8>, MLError> {
let (current_epoch, current_step) = self.get_current_training_state();
let training_metrics = self.get_training_metrics();
let performance_stats = self.get_inference_stats();
let state = MambaCheckpointState {
config: self.config.clone(),
epoch: current_epoch,
step: current_step,
training_loss: training_metrics
.get("training_loss")
.copied()
.unwrap_or(0.0),
validation_loss: training_metrics
.get("validation_loss")
.copied()
.unwrap_or(0.0),
ssd_layer_weights: self.extract_ssd_weights(),
input_projection_weights: self.extract_input_projection_weights(),
output_projection_weights: self.extract_output_projection_weights(),
layer_norm_weights: self.extract_layer_norm_weights(),
ssm_a_matrices: self.extract_ssm_matrices("A"),
ssm_b_matrices: self.extract_ssm_matrices("B"),
ssm_c_matrices: self.extract_ssm_matrices("C"),
ssm_delta_params: self.extract_delta_params(),
total_inferences: self
.total_inferences
.load(std::sync::atomic::Ordering::Relaxed),
avg_latency_us: performance_stats
.get("avg_latency_us")
.copied()
.unwrap_or(0.0),
throughput_pps: performance_stats
.get("throughput_pps")
.copied()
.unwrap_or(0.0),
};
let serialized = serde_json::to_vec(&state)
.map_err(|e| MLError::ModelError(format!("MAMBA serialization failed: {}", e)))?;
debug!("Serialized MAMBA state: {} bytes", serialized.len());
Ok(serialized)
}
async fn deserialize_state(&mut self, data: &[u8]) -> Result<(), MLError> {
let state: MambaCheckpointState = serde_json::from_slice(data)
.map_err(|e| MLError::ModelError(format!("MAMBA deserialization failed: {}", e)))?;
self.config = state.config;
self.restore_ssd_weights(&state.ssd_layer_weights);
self.restore_input_projection_weights(&state.input_projection_weights);
self.restore_output_projection_weights(&state.output_projection_weights);
self.restore_layer_norm_weights(&state.layer_norm_weights);
self.restore_ssm_matrices("A", &state.ssm_a_matrices);
self.restore_ssm_matrices("B", &state.ssm_b_matrices);
self.restore_ssm_matrices("C", &state.ssm_c_matrices);
self.restore_delta_params(&state.ssm_delta_params);
debug!("Deserialized MAMBA state from {} bytes", data.len());
Ok(())
}
fn get_training_state(&self) -> (Option<u64>, Option<u64>, Option<f64>, Option<f64>) {
if let Some(last_epoch) = self.metadata.training_history.back() {
let total_steps = self
.metadata
.training_history
.iter()
.map(|epoch| epoch.epoch as u64 * 1000)
.max()
.unwrap_or(0);
(
Some(last_epoch.epoch as u64),
Some(total_steps),
Some(last_epoch.loss),
Some(last_epoch.accuracy),
)
} else {
(Some(0), Some(0), Some(f64::INFINITY), Some(0.0))
}
}
fn get_hyperparameters(&self) -> HashMap<String, Value> {
let mut params = HashMap::new();
params.insert("d_model".to_owned(), Value::from(self.config.d_model));
params.insert("d_state".to_owned(), Value::from(self.config.d_state));
params.insert("d_head".to_owned(), Value::from(self.config.d_head));
params.insert("num_heads".to_owned(), Value::from(self.config.num_heads));
params.insert("expand".to_owned(), Value::from(self.config.expand));
params.insert(
"num_layers".to_owned(),
Value::from(self.config.num_layers),
);
params.insert("dropout".to_owned(), Value::from(self.config.dropout));
params.insert(
"learning_rate".to_owned(),
Value::from(self.config.learning_rate),
);
params.insert(
"target_latency_us".to_owned(),
Value::from(self.config.target_latency_us),
);
params
}
fn get_metrics(&self) -> HashMap<String, f64> {
self.get_performance_metrics()
}
fn get_architecture_info(&self) -> HashMap<String, Value> {
let mut info = HashMap::new();
info.insert("model_type".to_owned(), Value::from("MAMBA-2"));
info.insert("input_dim".to_owned(), Value::from(self.config.d_model));
info.insert(
"hidden_dim".to_owned(),
Value::from(self.config.d_model * self.config.expand),
);
info.insert("state_dim".to_owned(), Value::from(self.config.d_state));
info.insert(
"num_layers".to_owned(),
Value::from(self.config.num_layers),
);
info.insert("use_ssd".to_owned(), Value::from(self.config.use_ssd));
info.insert(
"use_selective_state".to_owned(),
Value::from(self.config.use_selective_state),
);
info.insert(
"hardware_aware".to_owned(),
Value::from(self.config.hardware_aware),
);
info
}
}
// ========== TGGN ==========
/// Serializable state for TGGN model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TGGNCheckpointState {
pub config: TGGNConfig,
pub epoch: Option<u64>,
pub step: Option<u64>,
pub training_loss: f64,
pub validation_loss: f64,
pub node_embeddings: HashMap<String, Vec<f32>>,
pub edge_embeddings: HashMap<String, Vec<f32>>,
pub graph_statistics: HashMap<String, f64>,
pub message_passing_weights: Vec<Vec<f32>>,
pub gating_weights: Vec<f32>,
pub total_inferences: u64,
pub graph_updates: u64,
pub avg_inference_latency_ns: u64,
pub avg_graph_update_latency_ns: u64,
}
#[async_trait]
impl Checkpointable for TGGN {
fn model_type(&self) -> ModelType {
ModelType::TGGN
}
fn model_name(&self) -> &str {
&self.metadata.version
}
fn model_version(&self) -> &str {
&self.metadata.version
}
async fn serialize_state(&self) -> Result<Vec<u8>, MLError> {
let node_embeddings: HashMap<String, Vec<f32>> = self
.node_embeddings()
.iter()
.map(|entry| {
let key = format!("{:?}", entry.key());
let value = entry.value().iter().map(|&x| x as f32).collect();
(key, value)
})
.collect();
let edge_embeddings: HashMap<String, Vec<f32>> = self
.edge_embeddings()
.iter()
.map(|entry| {
let key = format!("{:?}", entry.key());
let value = entry.value().iter().map(|&x| x as f32).collect();
(key, value)
})
.collect();
let state = TGGNCheckpointState {
config: self.config().clone(),
epoch: None,
step: None,
training_loss: 0.0,
validation_loss: 0.0,
node_embeddings,
edge_embeddings,
graph_statistics: HashMap::new(),
message_passing_weights: Vec::new(),
gating_weights: Vec::new(),
total_inferences: self
.inference_count()
.load(std::sync::atomic::Ordering::Relaxed),
graph_updates: self
.graph_updates()
.load(std::sync::atomic::Ordering::Relaxed),
avg_inference_latency_ns: self.calculate_avg_inference_latency(),
avg_graph_update_latency_ns: self.calculate_avg_graph_update_latency(),
};
let serialized = serde_json::to_vec(&state)
.map_err(|e| MLError::ModelError(format!("TGGN serialization failed: {}", e)))?;
debug!("Serialized TGGN state: {} bytes", serialized.len());
Ok(serialized)
}
async fn deserialize_state(&mut self, data: &[u8]) -> Result<(), MLError> {
let state: TGGNCheckpointState = serde_json::from_slice(data)
.map_err(|e| MLError::ModelError(format!("TGGN deserialization failed: {}", e)))?;
self.restore_node_embeddings(&state.node_embeddings)?;
self.restore_edge_embeddings(&state.edge_embeddings)?;
self.restore_graph_statistics(&state.graph_statistics)?;
self.restore_message_passing_weights(&state.message_passing_weights)?;
self.inference_count()
.store(state.total_inferences, std::sync::atomic::Ordering::Relaxed);
self.graph_updates()
.store(state.graph_updates, std::sync::atomic::Ordering::Relaxed);
debug!("Deserialized TGGN state from {} bytes", data.len());
Ok(())
}
fn get_hyperparameters(&self) -> HashMap<String, Value> {
let mut params = HashMap::new();
let config = self.config();
params.insert("max_nodes".to_owned(), Value::from(config.max_nodes));
params.insert("max_edges".to_owned(), Value::from(config.max_edges));
params.insert("node_dim".to_owned(), Value::from(config.node_dim));
params.insert("edge_dim".to_owned(), Value::from(config.edge_dim));
params.insert("hidden_dim".to_owned(), Value::from(config.hidden_dim));
params.insert("num_layers".to_owned(), Value::from(config.num_layers));
params.insert(
"temporal_decay".to_owned(),
Value::from(config.temporal_decay),
);
params.insert("use_simd".to_owned(), Value::from(config.use_simd));
params
}
fn get_architecture_info(&self) -> HashMap<String, Value> {
let mut info = HashMap::new();
let config = self.config();
info.insert("model_type".to_owned(), Value::from("TGGN"));
info.insert("max_nodes".to_owned(), Value::from(config.max_nodes));
info.insert("max_edges".to_owned(), Value::from(config.max_edges));
info.insert("node_feature_dim".to_owned(), Value::from(config.node_dim));
info.insert("edge_feature_dim".to_owned(), Value::from(config.edge_dim));
info.insert("gnn_layers".to_owned(), Value::from(config.num_layers));
info.insert("supports_temporal_decay".to_owned(), Value::from(true));
info
}
}