- Reduce CI GPU test datasets 16x for walltime reduction - Reduce early-stop epochs 50→10, add --test-threads=1 - Serialize all GPU lib tests to prevent cuBLAS init race - Align state_dim to 16 for BF16 tensor core HMMA dispatch - BF16 precision tolerance in ml-dqn tests - Enable branching DQN + tracing subscriber in smoke tests - Prevent min_replay_size > buffer_size deadlock in early-stop tests - Prevent AutoReplaySizer from breaking gradient collapse warmup - Replace racy tokio::spawn checkpoint counter with AtomicUsize - Set warmup_steps=0 and max_training_steps_per_epoch=300 in early-stop tests - RealDataLoader respects TEST_DATA_DIR for CI PVC layout - Add collapse_warmup_capacity to gpu_smoketest DQNConfig - Drain CUDA context between test binaries - Detached HEAD checkout prevents local branch corruption - GPU pipeline tests: fix BF16 dtype and rank-1 squeeze assertions - OOD input handling tests use use_gpu: true Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
680 lines
22 KiB
Rust
680 lines
22 KiB
Rust
#![allow(
|
||
clippy::assertions_on_constants,
|
||
clippy::assertions_on_result_states,
|
||
clippy::clone_on_copy,
|
||
clippy::decimal_literal_representation,
|
||
clippy::doc_markdown,
|
||
clippy::empty_line_after_doc_comments,
|
||
clippy::field_reassign_with_default,
|
||
clippy::get_unwrap,
|
||
clippy::identity_op,
|
||
clippy::inconsistent_digit_grouping,
|
||
clippy::indexing_slicing,
|
||
clippy::integer_division,
|
||
clippy::len_zero,
|
||
clippy::let_underscore_must_use,
|
||
clippy::manual_div_ceil,
|
||
clippy::manual_let_else,
|
||
clippy::manual_range_contains,
|
||
clippy::modulo_arithmetic,
|
||
clippy::needless_range_loop,
|
||
clippy::non_ascii_literal,
|
||
clippy::redundant_clone,
|
||
clippy::shadow_reuse,
|
||
clippy::shadow_same,
|
||
clippy::shadow_unrelated,
|
||
clippy::single_match_else,
|
||
clippy::str_to_string,
|
||
clippy::string_slice,
|
||
clippy::tests_outside_test_module,
|
||
clippy::too_many_lines,
|
||
clippy::unnecessary_wraps,
|
||
clippy::unseparated_literal_suffix,
|
||
clippy::use_debug,
|
||
clippy::useless_vec,
|
||
clippy::wildcard_enum_match_arm,
|
||
clippy::else_if_without_else,
|
||
clippy::expect_used,
|
||
clippy::missing_const_for_fn,
|
||
clippy::similar_names,
|
||
clippy::type_complexity,
|
||
clippy::collapsible_else_if,
|
||
clippy::doc_lazy_continuation,
|
||
clippy::items_after_test_module,
|
||
clippy::map_clone,
|
||
clippy::multiple_unsafe_ops_per_block,
|
||
clippy::unwrap_or_default,
|
||
clippy::assign_op_pattern,
|
||
clippy::needless_borrow,
|
||
clippy::println_empty_string,
|
||
clippy::unnecessary_cast,
|
||
clippy::used_underscore_binding,
|
||
clippy::create_dir,
|
||
clippy::implicit_saturating_sub,
|
||
clippy::exit,
|
||
clippy::expect_fun_call,
|
||
clippy::too_many_arguments,
|
||
clippy::unnecessary_map_or,
|
||
clippy::unwrap_used,
|
||
dead_code,
|
||
unused_imports,
|
||
unused_variables,
|
||
clippy::cloned_ref_to_slice_refs,
|
||
clippy::neg_multiply,
|
||
clippy::while_let_loop,
|
||
clippy::bool_assert_comparison,
|
||
clippy::excessive_precision,
|
||
clippy::trivially_copy_pass_by_ref,
|
||
clippy::op_ref,
|
||
clippy::redundant_closure,
|
||
clippy::unnecessary_lazy_evaluations,
|
||
clippy::if_then_some_else_none,
|
||
clippy::unnecessary_to_owned,
|
||
clippy::single_component_path_imports,
|
||
)]
|
||
//! Liquid NN Training Pipeline TDD Test Suite
|
||
//!
|
||
//! Comprehensive E2E tests for Liquid Neural Network training with CPU-only
|
||
//! fixed-point arithmetic. Tests cover forward/backward passes, training loop
|
||
//! convergence, checkpoint persistence, inference determinism, and memory usage.
|
||
//!
|
||
//! Architecture:
|
||
//! - CPU-ONLY (fixed-point arithmetic for <100μs inference)
|
||
//! - No CUDA dependencies (by design, not a limitation)
|
||
//! - Fixed-point precision: 8 decimal places (PRECISION = 100_000_000)
|
||
//! - Training: CPU-based gradient descent with MSE loss
|
||
//! - Inference: Deterministic fixed-point computation
|
||
//!
|
||
//! Test Coverage:
|
||
//! 1. Forward pass: Fixed-point computation correctness
|
||
//! 2. Backward pass: Gradient calculation (CPU only)
|
||
//! 3. Training loop: Loss convergence over epochs
|
||
//! 4. Checkpoint save/load: Safetensors persistence
|
||
//! 5. Inference determinism: Same input → same output
|
||
//! 6. Memory usage: CPU memory within limits
|
||
|
||
#![allow(unused_crate_dependencies)]
|
||
|
||
use ml::liquid::{
|
||
ActivationType, FixedPoint, LTCConfig, LayerConfig, LiquidNetwork, LiquidNetworkConfig,
|
||
LiquidTrainer, LiquidTrainingConfig, NetworkType, OutputLayerConfig, SolverType, TrainingBatch,
|
||
TrainingSample, TrainingUtils, PRECISION,
|
||
};
|
||
use std::time::Instant;
|
||
use tracing::info;
|
||
|
||
// ============================================================================
|
||
// Test 1: Forward Pass - Fixed-Point Computation
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_liquid_nn_forward_pass() -> anyhow::Result<()> {
|
||
info!("=== Test 1: Forward Pass - Fixed-Point Computation ===");
|
||
|
||
// Create minimal Liquid NN (16 input → 8 hidden → 3 output)
|
||
let ltc_config = LTCConfig {
|
||
input_size: 16,
|
||
hidden_size: 8,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::Euler, // Simplest solver for testing
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 16,
|
||
output_size: 3,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let mut network = LiquidNetwork::new(network_config)?;
|
||
info!(parameter_count = network.parameter_count(), "Created network: 16 inputs -> 8 hidden (LTC) -> 3 outputs");
|
||
|
||
// Create input with fixed-point values
|
||
let input: Vec<FixedPoint> = (0..16)
|
||
.map(|i| FixedPoint::from_f64(0.5 + (i as f64) * 0.01))
|
||
.collect();
|
||
|
||
info!(?input, "Input features (first 5 shown in debug)");
|
||
|
||
// Forward pass
|
||
let start = Instant::now();
|
||
let output = network.forward(&input)?;
|
||
let duration = start.elapsed();
|
||
|
||
info!(?duration, output_len = output.len(), ?output, "Forward pass completed");
|
||
|
||
// Assertions
|
||
assert_eq!(output.len(), 3, "Output should have 3 values");
|
||
assert!(
|
||
duration.as_micros() < 1000,
|
||
"Forward pass should be <1ms (target: <100μs in production)"
|
||
);
|
||
|
||
// Verify fixed-point arithmetic correctness
|
||
for &val in &output {
|
||
assert!(
|
||
val.is_finite(),
|
||
"Output values should be finite (no overflow)"
|
||
);
|
||
}
|
||
|
||
info!("Forward pass completed successfully");
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test 2: Backward Pass - Gradient Computation (CPU Only)
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_liquid_nn_backward_pass() -> anyhow::Result<()> {
|
||
info!("=== Test 2: Backward Pass - Gradient Computation ===");
|
||
|
||
// Create network
|
||
let ltc_config = LTCConfig {
|
||
input_size: 4,
|
||
hidden_size: 4,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::Euler,
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 4,
|
||
output_size: 2,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let mut network = LiquidNetwork::new(network_config.clone())?;
|
||
let trainer_config = LiquidTrainingConfig::default();
|
||
let mut trainer = LiquidTrainer::new(trainer_config);
|
||
|
||
info!("Created network: 4 -> 4 (LTC) -> 2");
|
||
|
||
// Create training sample
|
||
let input = vec![
|
||
FixedPoint::from_f64(0.5),
|
||
FixedPoint::from_f64(0.3),
|
||
FixedPoint::from_f64(0.7),
|
||
FixedPoint::from_f64(0.2),
|
||
];
|
||
let target = vec![FixedPoint::one(), FixedPoint::zero()];
|
||
|
||
let sample = TrainingSample {
|
||
input: input.clone(),
|
||
target: target.clone(),
|
||
timestamp: None,
|
||
market_regime: None,
|
||
volatility: None,
|
||
};
|
||
|
||
info!(?input, ?target, "Training sample");
|
||
|
||
// Forward pass to get predictions
|
||
let predictions = network.forward(&input)?;
|
||
info!(?predictions, "Predictions before training");
|
||
|
||
// Calculate loss manually (MSE)
|
||
let loss_before: f64 = predictions
|
||
.iter()
|
||
.zip(target.iter())
|
||
.map(|(pred, tgt)| {
|
||
let diff = pred.to_f64() - tgt.to_f64();
|
||
diff * diff
|
||
})
|
||
.sum::<f64>()
|
||
/ predictions.len() as f64;
|
||
info!(loss_before, "Loss before training");
|
||
|
||
// Train to verify gradient computation (use public train method)
|
||
let batches = vec![TrainingBatch::new(vec![sample])];
|
||
trainer.train(&mut network, &batches, None)?;
|
||
|
||
let history = trainer.get_training_history();
|
||
let batch_loss = history.last().map(|m| m.training_loss).unwrap_or(0.0);
|
||
let gradient_history_len = trainer.gradient_history.len();
|
||
info!(batch_loss, gradient_history_len, "Training results");
|
||
|
||
// Verify gradient was computed
|
||
assert!(
|
||
!trainer.gradient_history.is_empty(),
|
||
"Gradient history should contain gradients after training"
|
||
);
|
||
|
||
// Verify gradient is finite
|
||
let last_gradient = trainer.gradient_history.last().unwrap();
|
||
assert!(
|
||
last_gradient.is_finite(),
|
||
"Gradient should be finite (no overflow)"
|
||
);
|
||
|
||
info!(last_gradient = last_gradient.to_f64(), "Backward pass completed successfully");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test 3: Training Loop Convergence - Loss Decreases Over Epochs
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_training_loop_convergence() -> anyhow::Result<()> {
|
||
info!("=== Test 3: Training Loop Convergence ===");
|
||
|
||
// Create small network for fast convergence test
|
||
let ltc_config = LTCConfig {
|
||
input_size: 3,
|
||
hidden_size: 4,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::Euler,
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 3,
|
||
output_size: 2,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let mut network = LiquidNetwork::new(network_config)?;
|
||
info!("Created network: 3 -> 4 (LTC) -> 2");
|
||
|
||
// Create synthetic training data (simple XOR-like problem)
|
||
let mut training_samples = Vec::new();
|
||
for i in 0..20 {
|
||
let x = (i % 4) as f64;
|
||
let input = vec![
|
||
FixedPoint::from_f64(x / 4.0),
|
||
FixedPoint::from_f64((x * 2.0) / 4.0),
|
||
FixedPoint::from_f64((x * 3.0) / 4.0),
|
||
];
|
||
let target = if i % 2 == 0 {
|
||
vec![FixedPoint::one(), FixedPoint::zero()]
|
||
} else {
|
||
vec![FixedPoint::zero(), FixedPoint::one()]
|
||
};
|
||
|
||
training_samples.push(TrainingSample {
|
||
input,
|
||
target,
|
||
timestamp: None,
|
||
market_regime: None,
|
||
volatility: None,
|
||
});
|
||
}
|
||
|
||
info!(sample_count = training_samples.len(), "Created training samples");
|
||
|
||
// Create batches
|
||
let batches = TrainingUtils::create_batches(training_samples, 4);
|
||
info!(batch_count = batches.len(), "Created batches (batch size: 4)");
|
||
|
||
// Configure training (10 epochs for convergence test)
|
||
let training_config = LiquidTrainingConfig {
|
||
learning_rate: FixedPoint(PRECISION / 100), // 0.01
|
||
batch_size: 4,
|
||
max_epochs: 10,
|
||
early_stopping_patience: 5,
|
||
gradient_clip_threshold: FixedPoint::one(),
|
||
l2_regularization: FixedPoint::zero(),
|
||
adaptive_learning_rate: false,
|
||
market_regime_adaptation: false,
|
||
validation_split: 0.0,
|
||
};
|
||
|
||
let mut trainer = LiquidTrainer::new(training_config);
|
||
info!("Training configuration: learning_rate=0.01, max_epochs=10, batch_size=4");
|
||
|
||
// Train network
|
||
info!("Starting training");
|
||
let start = Instant::now();
|
||
trainer.train(&mut network, &batches, None)?;
|
||
let training_time = start.elapsed();
|
||
|
||
info!(?training_time, "Training completed");
|
||
|
||
// Verify loss convergence
|
||
let history = trainer.get_training_history();
|
||
assert!(
|
||
history.len() >= 2,
|
||
"Training history should have at least 2 epochs"
|
||
);
|
||
|
||
let first_loss = history[0].training_loss;
|
||
let last_loss = history.last().unwrap().training_loss;
|
||
|
||
info!(first_loss, "Epoch 0 loss");
|
||
for (i, metrics) in history.iter().enumerate().skip(1) {
|
||
info!(epoch = i, loss = metrics.training_loss, "Epoch loss");
|
||
}
|
||
info!(last_loss, "Final loss");
|
||
|
||
// Assert loss decreased (convergence)
|
||
assert!(
|
||
last_loss < first_loss,
|
||
"Loss should decrease during training (first={:.6}, last={:.6})",
|
||
first_loss,
|
||
last_loss
|
||
);
|
||
|
||
let loss_reduction = ((first_loss - last_loss) / first_loss) * 100.0;
|
||
info!(loss_reduction, "Training converged successfully");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test 4: Checkpoint Save/Load - Safetensors Persistence
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_checkpoint_save_load() -> anyhow::Result<()> {
|
||
info!("=== Test 4: Checkpoint Save/Load ===");
|
||
|
||
// Create network
|
||
let ltc_config = LTCConfig {
|
||
input_size: 5,
|
||
hidden_size: 6,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::Euler,
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 5,
|
||
output_size: 3,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let network = LiquidNetwork::new(network_config.clone())?;
|
||
info!("Created original network: 5 -> 6 (LTC) -> 3");
|
||
|
||
// Create test input
|
||
let input: Vec<FixedPoint> = (0..5)
|
||
.map(|i| FixedPoint::from_f64(0.1 + (i as f64) * 0.1))
|
||
.collect();
|
||
|
||
// Save checkpoint BEFORE running forward pass to preserve initial state
|
||
info!("Saving checkpoint");
|
||
let original_network = network.clone();
|
||
let checkpoint_json = serde_json::to_string(&original_network)?;
|
||
info!(checkpoint_size_bytes = checkpoint_json.len(), "Checkpoint saved");
|
||
|
||
// Run forward pass on original network
|
||
let mut original_network_mut = original_network.clone();
|
||
let original_output = original_network_mut.forward(&input)?;
|
||
info!(?original_output, "Original predictions");
|
||
|
||
// Load checkpoint
|
||
info!("Loading checkpoint");
|
||
let mut loaded_network: LiquidNetwork = serde_json::from_str(&checkpoint_json)?;
|
||
info!("Checkpoint loaded successfully");
|
||
|
||
// Verify predictions match
|
||
let loaded_output = loaded_network.forward(&input)?;
|
||
info!(?loaded_output, "Loaded predictions");
|
||
|
||
// Compare outputs
|
||
for (i, (&orig, &loaded)) in original_output.iter().zip(loaded_output.iter()).enumerate() {
|
||
let diff = (orig.0 - loaded.0).abs();
|
||
info!(
|
||
output_idx = i,
|
||
orig = orig.to_f64(),
|
||
loaded = loaded.to_f64(),
|
||
diff,
|
||
"Output comparison"
|
||
);
|
||
assert_eq!(
|
||
orig, loaded,
|
||
"Output {} should match exactly after checkpoint reload",
|
||
i
|
||
);
|
||
}
|
||
|
||
info!("Checkpoint save/load verified (deterministic)");
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test 5: Inference Determinism - Same Input → Same Output
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_inference_determinism() -> anyhow::Result<()> {
|
||
info!("=== Test 5: Inference Determinism ===");
|
||
|
||
// Create network
|
||
let ltc_config = LTCConfig {
|
||
input_size: 8,
|
||
hidden_size: 8,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::RK4, // Higher-order solver
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 8,
|
||
output_size: 4,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let mut network = LiquidNetwork::new(network_config)?;
|
||
info!("Created network: 8 -> 8 (LTC, RK4) -> 4");
|
||
|
||
// Create test input
|
||
let input: Vec<FixedPoint> = vec![
|
||
FixedPoint::from_f64(0.123),
|
||
FixedPoint::from_f64(0.456),
|
||
FixedPoint::from_f64(0.789),
|
||
FixedPoint::from_f64(0.234),
|
||
FixedPoint::from_f64(0.567),
|
||
FixedPoint::from_f64(0.890),
|
||
FixedPoint::from_f64(0.345),
|
||
FixedPoint::from_f64(0.678),
|
||
];
|
||
|
||
info!("Running 10 inference passes with identical input");
|
||
|
||
// Run inference 10 times with same input
|
||
let mut outputs = Vec::new();
|
||
for i in 0..10 {
|
||
// Reset network state before each inference
|
||
network.reset_states();
|
||
|
||
let output = network.forward(&input)?;
|
||
outputs.push(output);
|
||
|
||
if i == 0 {
|
||
info!(output = ?outputs[0], run = i, "First run output");
|
||
}
|
||
}
|
||
|
||
// Verify all outputs are identical
|
||
let first_output = &outputs[0];
|
||
for (run_idx, output) in outputs.iter().enumerate().skip(1) {
|
||
for (i, (&expected, &actual)) in first_output.iter().zip(output.iter()).enumerate() {
|
||
assert_eq!(
|
||
expected, actual,
|
||
"Run {} output[{}] should match run 0 (deterministic)",
|
||
run_idx, i
|
||
);
|
||
}
|
||
}
|
||
|
||
info!(?first_output, "Inference is deterministic (10/10 runs identical)");
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test 6: Memory Usage - CPU Memory Within Limits
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_memory_usage() -> anyhow::Result<()> {
|
||
info!("=== Test 6: Memory Usage ===");
|
||
|
||
// Create realistic-sized network (16 → 128 → 3)
|
||
let ltc_config = LTCConfig {
|
||
input_size: 16,
|
||
hidden_size: 128,
|
||
tau_min: FixedPoint::from_f64(0.1),
|
||
tau_max: FixedPoint::from_f64(1.0),
|
||
use_bias: true,
|
||
solver_type: SolverType::RK4,
|
||
activation: ActivationType::Tanh,
|
||
};
|
||
|
||
let network_config = LiquidNetworkConfig {
|
||
network_type: NetworkType::LTC,
|
||
input_size: 16,
|
||
output_size: 3,
|
||
layer_configs: vec![LayerConfig::LTC(ltc_config)],
|
||
output_layer: OutputLayerConfig {
|
||
use_linear_output: true,
|
||
output_activation: Some(ActivationType::Linear),
|
||
dropout_rate: None,
|
||
},
|
||
default_dt: FixedPoint::from_f64(0.01),
|
||
market_regime_adaptation: false,
|
||
};
|
||
|
||
let network = LiquidNetwork::new(network_config)?;
|
||
info!("Created network: 16 -> 128 (LTC) -> 3");
|
||
|
||
// Calculate memory footprint
|
||
let param_count = network.parameter_count();
|
||
let bytes_per_param = size_of::<FixedPoint>(); // 8 bytes (i64)
|
||
let total_bytes = param_count * bytes_per_param;
|
||
let kb = total_bytes as f64 / 1024.0;
|
||
let mb = kb / 1024.0;
|
||
|
||
info!(
|
||
param_count,
|
||
bytes_per_param,
|
||
total_bytes,
|
||
kb,
|
||
mb,
|
||
"Memory analysis"
|
||
);
|
||
|
||
// Parameter breakdown
|
||
let input_weights = 16 * 128; // input_size × hidden_size
|
||
let recurrent_weights = 128 * 128; // hidden_size × hidden_size
|
||
let bias = 128; // hidden_size
|
||
let output_weights = 128 * 3; // hidden_size × output_size
|
||
let output_bias = 3; // output_size
|
||
let total_calculated = input_weights + recurrent_weights + bias + output_weights + output_bias;
|
||
|
||
info!(
|
||
input_weights,
|
||
recurrent_weights,
|
||
hidden_bias = bias,
|
||
output_weights,
|
||
output_bias,
|
||
total_calculated,
|
||
"Parameter breakdown"
|
||
);
|
||
|
||
// Verify memory is reasonable (<10 MB for this size)
|
||
assert!(
|
||
mb < 10.0,
|
||
"Network memory should be <10 MB (actual: {:.3} MB)",
|
||
mb
|
||
);
|
||
|
||
// Create training dataset and measure memory
|
||
info!("Testing with 1000 training samples");
|
||
let mut samples = Vec::new();
|
||
for i in 0..1000 {
|
||
let input: Vec<FixedPoint> = (0..16)
|
||
.map(|j| FixedPoint::from_f64((i * j) as f64 / 1000.0))
|
||
.collect();
|
||
let target = vec![
|
||
FixedPoint::from_f64((i % 3 == 0) as u8 as f64),
|
||
FixedPoint::from_f64((i % 3 == 1) as u8 as f64),
|
||
FixedPoint::from_f64((i % 3 == 2) as u8 as f64),
|
||
];
|
||
|
||
samples.push(TrainingSample {
|
||
input,
|
||
target,
|
||
timestamp: None,
|
||
market_regime: None,
|
||
volatility: None,
|
||
});
|
||
}
|
||
|
||
let sample_memory = samples.len() * (16 + 3) * size_of::<FixedPoint>();
|
||
let sample_mb = sample_memory as f64 / 1024.0 / 1024.0;
|
||
info!(sample_mb, "Sample dataset memory");
|
||
|
||
// Total memory (network + samples)
|
||
let total_mb = mb + sample_mb;
|
||
info!(total_mb, "Total memory usage");
|
||
|
||
// Verify total memory is reasonable (<50 MB)
|
||
assert!(
|
||
total_mb < 50.0,
|
||
"Total memory (network + samples) should be <50 MB (actual: {:.3} MB)",
|
||
total_mb
|
||
);
|
||
|
||
info!(network_mb = mb, sample_mb, total_mb, "Memory usage within limits (<50 MB limit)");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Helper Functions
|
||
// ============================================================================
|