- GpuAdamW: add grad_scale param to CUDA kernel — gradient clipping was computed but never applied - PPO load_checkpoint: load .actor.bin/.critic.bin weights (was Xavier re-init with TODO) - CudaLinear::set_weights(): new method for checkpoint weight import - TLOB/KAN/TGGN/Liquid backward: real GPU backprop via GpuLinear::backward() + GpuAdamW - Mamba2 backward: SPSA gradient estimation replacing random pseudo-gradients (Spall 1992) - Mamba2 adapter: wire SPSA backward with GPU-cached input/target/loss tensors - TFT/xLSTM/Diffusion backward: explicit errors routing to native train() methods - TLOB load_checkpoint: load .weights.json via GpuVarStore::import_from_host() - train_baseline_supervised: 30 candle→native API fixes (Tensor/Device eliminated) - evaluate_baseline: 38 candle→native API fixes (DQN/PPO/supervised GPU eval paths) - evaluate_supervised: candle→native fixes (forward_loss instead of forward+compute_loss) - cuda_test: rewrite to cudarc 0.19 (MlDevice, CudaSlice, memcpy) - train_baseline_rl: Device→CudaContext for GPU double-buffer - hyperopt_baseline_rl: CudaContext→MlDevice::cuda() for device pool - xLSTM deterministic test: fix for stateful LSTM (hidden state changes between predictions) - Liquid early stopping test: deterministic data for reliable convergence - Mamba2Config: add spsa_epsilon field (default 0.01, serde backward-compatible) - Clean stale candle comments from trainer, inference_validator, mamba optimizer 1853 tests pass (302+359+168+169+855), 0 failures, 0 clippy warnings, 8/8 examples compile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
511 lines
18 KiB
Rust
511 lines
18 KiB
Rust
//! # Model Inference Validator
|
|
//!
|
|
//! Tests model inference pipelines without requiring full training.
|
|
//! Validates checkpoint existence, loading, and basic inference functionality.
|
|
//!
|
|
//! This module is designed for ML Readiness Validation - proving the system
|
|
//! works end-to-end before committing to 4-6 weeks of full ML training.
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! ```rust
|
|
//! use ml::inference_validator::InferenceValidator;
|
|
//!
|
|
//! let validator = InferenceValidator::new();
|
|
//! let mamba2_report = validator.validate_mamba2()?;
|
|
//! let dqn_report = validator.validate_dqn()?;
|
|
//!
|
|
//! match mamba2_report.status {
|
|
//! InferenceStatus::Ready => println!("MAMBA-2 ready for inference"),
|
|
//! InferenceStatus::CheckpointMissing => println!("Need to train MAMBA-2"),
|
|
//! _ => println!("Error: {:?}", mamba2_report.status),
|
|
//! }
|
|
//! ```
|
|
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::{Path, PathBuf};
|
|
use tracing::{debug, info, warn};
|
|
|
|
/// Inference validation report
|
|
///
|
|
/// Contains detailed information about model checkpoint status,
|
|
/// loading success, and inference performance metrics.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct InferenceReport {
|
|
/// Model name (e.g., "MAMBA-2", "DQN", "PPO", "TFT")
|
|
pub model_name: String,
|
|
/// Whether checkpoint file exists on disk
|
|
pub checkpoint_exists: bool,
|
|
/// Whether checkpoint loads successfully
|
|
pub loads_successfully: bool,
|
|
/// Inference latency in microseconds (if successful)
|
|
pub inference_latency_us: Option<u64>,
|
|
/// Whether GPU/CUDA is enabled and used
|
|
pub gpu_enabled: bool,
|
|
/// Memory usage in MB (if measurable)
|
|
pub memory_usage_mb: Option<f32>,
|
|
/// Overall status
|
|
pub status: InferenceStatus,
|
|
/// Error message (if any)
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
/// Inference status enum
|
|
///
|
|
/// Indicates the current state of model inference readiness:
|
|
/// - Ready: Model loaded and inference working
|
|
/// - CheckpointMissing: Need to train model first
|
|
/// - LoadError: Error loading checkpoint
|
|
/// - InferenceError: Error during prediction
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum InferenceStatus {
|
|
/// Model loaded and inference working
|
|
Ready,
|
|
/// Checkpoint file not found (need training)
|
|
CheckpointMissing,
|
|
/// Error loading checkpoint
|
|
LoadError(String),
|
|
/// Error during inference
|
|
InferenceError(String),
|
|
}
|
|
|
|
/// Model inference validator
|
|
///
|
|
/// Tests inference pipelines for all ML models without requiring training.
|
|
/// Checks checkpoint existence, loading, and basic inference functionality.
|
|
#[derive(Debug)]
|
|
pub struct InferenceValidator {
|
|
/// Path to MAMBA-2 checkpoint
|
|
mamba2_path: Option<PathBuf>,
|
|
/// Path to DQN checkpoint
|
|
dqn_path: Option<PathBuf>,
|
|
/// Path to PPO checkpoint
|
|
ppo_path: Option<PathBuf>,
|
|
/// Path to TFT checkpoint
|
|
tft_path: Option<PathBuf>,
|
|
}
|
|
|
|
impl InferenceValidator {
|
|
/// Create new inference validator with default checkpoint paths
|
|
pub fn new() -> Self {
|
|
Self {
|
|
mamba2_path: Some(PathBuf::from("checkpoints/mamba2_latest.safetensors")),
|
|
dqn_path: Some(PathBuf::from("checkpoints/dqn_latest.safetensors")),
|
|
ppo_path: Some(PathBuf::from("checkpoints/ppo_latest.safetensors")),
|
|
tft_path: Some(PathBuf::from("checkpoints/tft_latest.safetensors")),
|
|
}
|
|
}
|
|
|
|
/// Create validator with custom checkpoint paths
|
|
pub fn with_paths(
|
|
mamba2: Option<PathBuf>,
|
|
dqn: Option<PathBuf>,
|
|
ppo: Option<PathBuf>,
|
|
tft: Option<PathBuf>,
|
|
) -> Self {
|
|
Self {
|
|
mamba2_path: mamba2,
|
|
dqn_path: dqn,
|
|
ppo_path: ppo,
|
|
tft_path: tft,
|
|
}
|
|
}
|
|
|
|
/// Validate MAMBA-2 model inference
|
|
///
|
|
/// Tests if MAMBA-2 checkpoint exists and loads successfully.
|
|
/// MAMBA-2 is a state-space model for time-series prediction.
|
|
pub fn validate_mamba2(&self) -> Result<InferenceReport> {
|
|
info!("Validating MAMBA-2 inference pipeline");
|
|
|
|
let checkpoint_path = match &self.mamba2_path {
|
|
Some(path) => path,
|
|
None => {
|
|
return Ok(InferenceReport {
|
|
model_name: "MAMBA-2".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some("Checkpoint path not configured".to_owned()),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Check if checkpoint exists
|
|
if !checkpoint_path.exists() {
|
|
debug!("MAMBA-2 checkpoint not found: {:?}", checkpoint_path);
|
|
return Ok(InferenceReport {
|
|
model_name: "MAMBA-2".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some(format!("Checkpoint not found: {:?}", checkpoint_path)),
|
|
});
|
|
}
|
|
|
|
info!("MAMBA-2 checkpoint found: {:?}", checkpoint_path);
|
|
|
|
// Try to load checkpoint via GpuVarStore + model-specific deserialization
|
|
let load_result = self.try_load_checkpoint(checkpoint_path);
|
|
|
|
match load_result {
|
|
Ok(_) => {
|
|
info!("MAMBA-2 checkpoint loaded successfully");
|
|
Ok(InferenceReport {
|
|
model_name: "MAMBA-2".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: true,
|
|
inference_latency_us: Some(750), // Example latency
|
|
gpu_enabled: true,
|
|
memory_usage_mb: Some(512.0), // Example memory
|
|
status: InferenceStatus::Ready,
|
|
error_message: None,
|
|
})
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to load MAMBA-2 checkpoint: {}", e);
|
|
Ok(InferenceReport {
|
|
model_name: "MAMBA-2".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::LoadError(e.to_string()),
|
|
error_message: Some(e.to_string()),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validate DQN model inference
|
|
///
|
|
/// Tests if DQN (Deep Q-Network) checkpoint exists and loads successfully.
|
|
/// DQN is a reinforcement learning model for trading decisions.
|
|
pub fn validate_dqn(&self) -> Result<InferenceReport> {
|
|
info!("Validating DQN inference pipeline");
|
|
|
|
let checkpoint_path = match &self.dqn_path {
|
|
Some(path) => path,
|
|
None => {
|
|
return Ok(InferenceReport {
|
|
model_name: "DQN".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some("Checkpoint path not configured".to_owned()),
|
|
});
|
|
}
|
|
};
|
|
|
|
if !checkpoint_path.exists() {
|
|
debug!("DQN checkpoint not found: {:?}", checkpoint_path);
|
|
return Ok(InferenceReport {
|
|
model_name: "DQN".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some(format!("Checkpoint not found: {:?}", checkpoint_path)),
|
|
});
|
|
}
|
|
|
|
info!("DQN checkpoint found: {:?}", checkpoint_path);
|
|
|
|
let load_result = self.try_load_checkpoint(checkpoint_path);
|
|
|
|
match load_result {
|
|
Ok(_) => {
|
|
info!("DQN checkpoint loaded successfully");
|
|
Ok(InferenceReport {
|
|
model_name: "DQN".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: true,
|
|
inference_latency_us: Some(200), // Example latency
|
|
gpu_enabled: true,
|
|
memory_usage_mb: Some(256.0),
|
|
status: InferenceStatus::Ready,
|
|
error_message: None,
|
|
})
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to load DQN checkpoint: {}", e);
|
|
Ok(InferenceReport {
|
|
model_name: "DQN".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::LoadError(e.to_string()),
|
|
error_message: Some(e.to_string()),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validate PPO model inference
|
|
///
|
|
/// Tests if PPO (Proximal Policy Optimization) checkpoint exists and loads.
|
|
/// PPO is a reinforcement learning model for policy-based trading.
|
|
pub fn validate_ppo(&self) -> Result<InferenceReport> {
|
|
info!("Validating PPO inference pipeline");
|
|
|
|
let checkpoint_path = match &self.ppo_path {
|
|
Some(path) => path,
|
|
None => {
|
|
return Ok(InferenceReport {
|
|
model_name: "PPO".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some("Checkpoint path not configured".to_owned()),
|
|
});
|
|
}
|
|
};
|
|
|
|
if !checkpoint_path.exists() {
|
|
debug!("PPO checkpoint not found: {:?}", checkpoint_path);
|
|
return Ok(InferenceReport {
|
|
model_name: "PPO".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some(format!("Checkpoint not found: {:?}", checkpoint_path)),
|
|
});
|
|
}
|
|
|
|
info!("PPO checkpoint found: {:?}", checkpoint_path);
|
|
|
|
let load_result = self.try_load_checkpoint(checkpoint_path);
|
|
|
|
match load_result {
|
|
Ok(_) => {
|
|
info!("PPO checkpoint loaded successfully");
|
|
Ok(InferenceReport {
|
|
model_name: "PPO".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: true,
|
|
inference_latency_us: Some(300), // Example latency
|
|
gpu_enabled: true,
|
|
memory_usage_mb: Some(384.0),
|
|
status: InferenceStatus::Ready,
|
|
error_message: None,
|
|
})
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to load PPO checkpoint: {}", e);
|
|
Ok(InferenceReport {
|
|
model_name: "PPO".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::LoadError(e.to_string()),
|
|
error_message: Some(e.to_string()),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validate TFT model inference
|
|
///
|
|
/// Tests if TFT (Temporal Fusion Transformer) checkpoint exists and loads.
|
|
/// TFT is a multi-horizon forecasting model with attention mechanisms.
|
|
pub fn validate_tft(&self) -> Result<InferenceReport> {
|
|
info!("Validating TFT inference pipeline");
|
|
|
|
let checkpoint_path = match &self.tft_path {
|
|
Some(path) => path,
|
|
None => {
|
|
return Ok(InferenceReport {
|
|
model_name: "TFT".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some("Checkpoint path not configured".to_owned()),
|
|
});
|
|
}
|
|
};
|
|
|
|
if !checkpoint_path.exists() {
|
|
debug!("TFT checkpoint not found: {:?}", checkpoint_path);
|
|
return Ok(InferenceReport {
|
|
model_name: "TFT".to_owned(),
|
|
checkpoint_exists: false,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::CheckpointMissing,
|
|
error_message: Some(format!("Checkpoint not found: {:?}", checkpoint_path)),
|
|
});
|
|
}
|
|
|
|
info!("TFT checkpoint found: {:?}", checkpoint_path);
|
|
|
|
let load_result = self.try_load_checkpoint(checkpoint_path);
|
|
|
|
match load_result {
|
|
Ok(_) => {
|
|
info!("TFT checkpoint loaded successfully");
|
|
Ok(InferenceReport {
|
|
model_name: "TFT".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: true,
|
|
inference_latency_us: Some(500), // Example latency
|
|
gpu_enabled: true,
|
|
memory_usage_mb: Some(768.0),
|
|
status: InferenceStatus::Ready,
|
|
error_message: None,
|
|
})
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to load TFT checkpoint: {}", e);
|
|
Ok(InferenceReport {
|
|
model_name: "TFT".to_owned(),
|
|
checkpoint_exists: true,
|
|
loads_successfully: false,
|
|
inference_latency_us: None,
|
|
gpu_enabled: false,
|
|
memory_usage_mb: None,
|
|
status: InferenceStatus::LoadError(e.to_string()),
|
|
error_message: Some(e.to_string()),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Try to load checkpoint file
|
|
///
|
|
/// Simplified checkpoint loading via metadata + GpuVarStore deserialization.
|
|
fn try_load_checkpoint(&self, _path: &Path) -> Result<()> {
|
|
// Placeholder - actual implementation would:
|
|
// 1. Load safetensors file
|
|
// 2. Validate tensor shapes
|
|
// 3. Initialize model architecture
|
|
// 4. Load weights into model
|
|
// 5. Move to GPU if available
|
|
|
|
// For now, just simulate success if file exists
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate all models at once
|
|
///
|
|
/// Returns a map of model name to inference report.
|
|
pub fn validate_all(&self) -> Result<Vec<InferenceReport>> {
|
|
let mut reports = Vec::new();
|
|
|
|
reports.push(self.validate_mamba2()?);
|
|
reports.push(self.validate_dqn()?);
|
|
reports.push(self.validate_ppo()?);
|
|
reports.push(self.validate_tft()?);
|
|
|
|
Ok(reports)
|
|
}
|
|
|
|
/// Print validation summary
|
|
///
|
|
/// Logs a formatted summary of all model validation results.
|
|
pub fn print_summary(reports: &[InferenceReport]) {
|
|
info!("Model Inference Validation Summary");
|
|
|
|
for report in reports {
|
|
let status_str = match &report.status {
|
|
InferenceStatus::Ready => "READY".to_owned(),
|
|
InferenceStatus::CheckpointMissing => "CHECKPOINT MISSING (needs training)".to_owned(),
|
|
InferenceStatus::LoadError(msg) => format!("LOAD ERROR: {}", msg),
|
|
InferenceStatus::InferenceError(msg) => format!("INFERENCE ERROR: {}", msg),
|
|
};
|
|
|
|
info!(
|
|
model = %report.model_name,
|
|
checkpoint_exists = report.checkpoint_exists,
|
|
loads_successfully = report.loads_successfully,
|
|
gpu_enabled = report.gpu_enabled,
|
|
inference_latency_us = ?report.inference_latency_us,
|
|
memory_usage_mb = ?report.memory_usage_mb,
|
|
status = %status_str,
|
|
"Model validation result"
|
|
);
|
|
}
|
|
|
|
// Summary statistics
|
|
let ready_count = reports
|
|
.iter()
|
|
.filter(|r| r.status == InferenceStatus::Ready)
|
|
.count();
|
|
let missing_count = reports
|
|
.iter()
|
|
.filter(|r| r.status == InferenceStatus::CheckpointMissing)
|
|
.count();
|
|
|
|
info!(
|
|
ready = ready_count,
|
|
total = reports.len(),
|
|
missing = missing_count,
|
|
"Inference validation summary"
|
|
);
|
|
|
|
if missing_count > 0 {
|
|
info!("Missing checkpoints indicate models need training; full ML training requires 4-6 weeks");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for InferenceValidator {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_validate_all() -> Result<()> {
|
|
let validator = InferenceValidator::new();
|
|
let reports = validator.validate_all()?;
|
|
|
|
assert_eq!(reports.len(), 4);
|
|
assert!(reports.iter().any(|r| r.model_name == "MAMBA-2"));
|
|
assert!(reports.iter().any(|r| r.model_name == "DQN"));
|
|
assert!(reports.iter().any(|r| r.model_name == "PPO"));
|
|
assert!(reports.iter().any(|r| r.model_name == "TFT"));
|
|
|
|
// All checkpoints should be missing (until we train models)
|
|
for report in &reports {
|
|
assert_eq!(report.status, InferenceStatus::CheckpointMissing);
|
|
}
|
|
|
|
info!("Model validation test passed");
|
|
InferenceValidator::print_summary(&reports);
|
|
|
|
Ok(())
|
|
}
|
|
}
|