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>
320 lines
12 KiB
Rust
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
|
|
}
|
|
}
|