Files
foxhunt/crates/ml/tests/mamba2_early_stopping_test.rs
jgrusewski bfd2253a9d fix: wire real GPU backprop + SPSA gradients, fix checkpoint loading, eliminate candle from examples
- 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>
2026-03-20 15:41:42 +01:00

310 lines
9.5 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,
)]
//! MAMBA-2 Early Stopping Tests (TDD Implementation)
//!
//! This test suite validates the early stopping implementation for MAMBA-2
//! following the TFT reference pattern at `ml/src/trainers/tft.rs:1702-1727`.
//!
//! ## Test Coverage
//!
//! 1. **State Tracking**: Verify best_val_loss, patience_counter initialization
//! 2. **Plateau Detection**: Verify training stops when no improvement
//! 3. **Reset on Improvement**: Verify patience_counter resets when val_loss improves
//! 4. **Minimum Epochs**: Verify min_epochs prevents premature stopping
//! 5. **Hyperopt Integration**: Verify hyperopt trials use early stopping
// candle eliminated — test uses native cudarc StreamTensor APIs
use std::sync::OnceLock;
use ml::mamba::{Mamba2Config, Mamba2SSM, OptimizerType};
use ml_core::cuda_autograd::stream_ops::StreamTensor;
use ml_core::device::MlDevice;
use tracing::info;
static SHARED_CUDA: OnceLock<MlDevice> = OnceLock::new();
fn cuda_device() -> MlDevice {
SHARED_CUDA.get_or_init(|| MlDevice::cuda(0).expect("CUDA required")).clone()
}
/// Helper function to create a test Mamba2Config with early stopping
fn create_test_config(
patience: usize,
min_epochs: usize,
seq_len: usize,
batch_size: usize,
) -> Mamba2Config {
Mamba2Config {
d_model: 54,
d_state: 16,
d_head: 7,
num_heads: 8,
expand: 2,
num_layers: 2,
dropout: 0.0, // No dropout for deterministic tests
use_ssd: false,
use_selective_state: false,
hardware_aware: false,
target_latency_us: 5,
max_seq_len: 120,
learning_rate: 1e-3,
weight_decay: 0.0,
grad_clip: 1.0,
warmup_steps: 0,
adam_beta1: 0.9,
adam_beta2: 0.999,
adam_epsilon: 1e-8,
total_decay_steps: 100,
batch_size,
seq_len,
shuffle_batches: false,
optimizer_type: OptimizerType::Adam,
sgd_momentum: 0.9,
sequence_stride: 1,
norm_eps: 1e-5,
early_stopping_enabled: true,
early_stopping_patience: patience,
early_stopping_min_delta: 1e-4,
early_stopping_min_epochs: min_epochs,
spsa_epsilon: 0.01,
}
}
/// Test 1: Early Stopping State Tracking
///
/// Verifies that MAMBA-2 early stopping state is correctly initialized
/// and tracked during training.
#[test]
fn test_mamba2_early_stopping_state() {
// Create MAMBA-2 model with early stopping config
let config = create_test_config(5, 5, 10, 4);
let device = cuda_device();
let stream = device.cuda_stream().expect("CUDA stream required");
let model = Mamba2SSM::new(config, stream).expect("Failed to create MAMBA-2 model");
// Verify early stopping state fields exist
assert_eq!(
model.state.best_val_loss,
f64::INFINITY,
"best_val_loss should initialize to INFINITY"
);
assert_eq!(
model.state.patience_counter, 0,
"patience_counter should initialize to 0"
);
assert!(
!model.state.stopped,
"stopped flag should initialize to false"
);
assert_eq!(
model.state.stopped_at_epoch, None,
"stopped_at_epoch should be None initially"
);
info!("Early stopping state correctly initialized");
}
/// Test 2: check_early_stopping Method Behavior
///
/// Verifies that check_early_stopping correctly implements the TFT pattern
#[test]
fn test_check_early_stopping_method() {
let config = create_test_config(5, 10, 10, 4);
let device = cuda_device();
let stream = device.cuda_stream().expect("CUDA stream required");
let mut model = Mamba2SSM::new(config, stream).expect("Failed to create MAMBA-2 model");
// Test 1: Before min_epochs, should NOT stop
for epoch in 0..10 {
let should_stop = model.check_early_stopping(epoch, 1.0);
assert!(
!should_stop,
"Should not stop before min_epochs ({})",
epoch
);
}
// Test 2: After min_epochs, with improvement, should NOT stop
model.check_early_stopping(10, 0.5); // Improvement
assert_eq!(
model.state.patience_counter, 0,
"Patience should reset on improvement"
);
assert_eq!(
model.state.best_val_loss, 0.5,
"Best val loss should update"
);
// Test 3: After min_epochs, without improvement, should increment patience
for i in 1..=4 {
model.check_early_stopping(10 + i, 0.6); // No improvement
assert_eq!(
model.state.patience_counter, i,
"Patience should increment without improvement"
);
}
// Test 4: After patience exhausted, should stop
let should_stop = model.check_early_stopping(15, 0.6);
assert!(should_stop, "Should stop after patience exhausted");
assert!(model.state.stopped, "Stopped flag should be set");
assert_eq!(
model.state.stopped_at_epoch,
Some(15),
"Stopped epoch should be recorded"
);
info!("check_early_stopping method works correctly");
}
/// Test 3: Verify early stopping default config
#[test]
fn test_early_stopping_default_config() {
let config = Mamba2Config::default();
assert!(
config.early_stopping_enabled,
"Early stopping should be enabled by default"
);
assert_eq!(
config.early_stopping_patience, 20,
"Default patience should be 20"
);
assert_eq!(
config.early_stopping_min_delta, 1e-4,
"Default min_delta should be 1e-4"
);
assert_eq!(
config.early_stopping_min_epochs, 20,
"Default min_epochs should be 20"
);
info!("Default early stopping config validated");
}
/// Test 4: Integration test - verify early stopping actually stops training
///
/// This test creates a small training scenario and verifies that training
/// stops early when validation loss plateaus.
#[tokio::test]
async fn test_early_stopping_integration() {
let seq_len = 10;
let d_model = 54;
let batch_size = 4;
let device = cuda_device();
let stream = device.cuda_stream().expect("CUDA stream required");
// Create minimal training data as (StreamTensor, StreamTensor) pairs
let mut train_data = Vec::new();
for _ in 0..20 {
let input_data = vec![1.0_f32; 1 * seq_len * d_model];
let target_data = vec![1.0_f32; 1 * 1 * 1];
let input = StreamTensor::from_vec(input_data, &[1, seq_len, d_model], stream)
.expect("Failed to create input");
let target = StreamTensor::from_vec(target_data, &[1, 1, 1], stream)
.expect("Failed to create target");
train_data.push((input, target));
}
let val_data = train_data[15..20].to_vec();
// Create config with VERY short patience for fast testing
let config = create_test_config(3, 2, seq_len, batch_size);
let mut model = Mamba2SSM::new(config, stream).expect("Failed to create MAMBA-2 model");
// Train with early stopping
let max_epochs = 50;
let history = model
.train(&train_data, &val_data, max_epochs, None)
.await
.expect("Training failed");
// Verify training stopped early
assert!(
history.len() < max_epochs,
"Training should stop early, but ran {} epochs (max: {})",
history.len(),
max_epochs
);
info!(
stopped_at = history.len(),
max_epochs,
"Early stopping integration test passed"
);
}