- 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>
310 lines
9.5 KiB
Rust
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"
|
|
);
|
|
}
|