#![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 Gradient Extraction Test (TDD) //! //! **Test-Driven Development**: This test verifies that gradients are properly computed //! after backward_pass() using finite-difference pseudo-gradients (StreamTensor has no autograd). //! //! **Expected Behavior**: //! 1. Call forward() to compute output //! 2. Compute loss via host-side MSE //! 3. Call backward_pass() to compute pseudo-gradients //! 4. Verify gradients are non-zero and valid (not NaN/Inf) #![allow(unused_crate_dependencies)] // candle eliminated — test uses native cudarc StreamTensor APIs use std::sync::OnceLock; use ml::mamba::Mamba2SSM; use ml::MLError; use ml_core::cuda_autograd::stream_ops::{StreamTensor, gpu_sub, gpu_sqr, gpu_mean_all}; use ml_core::device::MlDevice; use tracing::info; static SHARED_CUDA: OnceLock = OnceLock::new(); fn cuda_device() -> MlDevice { SHARED_CUDA.get_or_init(|| MlDevice::cuda(0).expect("CUDA required")).clone() } #[test] fn test_mamba2_gradient_extraction_from_backward_pass() -> Result<(), MLError> { info!("=== MAMBA-2 Gradient Extraction Test ==="); let device = cuda_device(); let stream = device.cuda_stream()?; info!("Device: CUDA"); // Create small MAMBA-2 model let mut model = Mamba2SSM::default_hft(stream)?; info!(num_parameters = model.metadata.num_parameters, "Model created"); // Create dummy input and target let batch_size = model.config.batch_size; let seq_len = model.config.seq_len; let d_model = model.config.d_model; let input_data = vec![0.1_f32; batch_size * seq_len * d_model]; let input = StreamTensor::from_vec(input_data, &[batch_size, seq_len, d_model], stream)?; let target_data = vec![0.5_f32; batch_size * seq_len]; let target = StreamTensor::from_vec(target_data, &[batch_size, seq_len, 1], stream)?; info!(input_shape = ?input.shape, "Input shape"); info!(target_shape = ?target.shape, "Target shape"); // Forward pass let output = model.forward(&input)?; info!(output_shape = ?output.shape, "Output shape"); // Compute loss (MSE) via StreamTensor free functions let diff = gpu_sub(&output, &target)?; let sq = gpu_sqr(&diff)?; let loss_value = gpu_mean_all(&sq)?; info!(loss_value, "Loss"); // Create a scalar loss StreamTensor for backward_pass let loss_tensor = StreamTensor::from_vec(vec![loss_value as f32], &[1], stream)?; // backward_pass computes finite-difference pseudo-gradients for SSM parameters model.backward_pass(&loss_tensor, &input, &target)?; // Extract gradients from model.gradients HashMap info!("=== Extracting Gradients from model.gradients ==="); let total_entries = model.gradients.len(); info!(total_entries, "Total gradient entries"); let mut vars_with_gradients = 0; let mut total_grad_norm = 0.0_f64; for (key, grad) in model.gradients.iter() { // Download gradient to host let grad_vec = grad.to_vec()?; let grad_norm: f64 = grad_vec.iter().map(|&g| (g as f64).powi(2)).sum::().sqrt(); info!(key, grad_norm, "Gradient entry norm"); // Verify gradient is valid assert!(!grad_norm.is_nan(), "Gradient {} is NaN", key); assert!(!grad_norm.is_infinite(), "Gradient {} is Inf", key); if grad_norm > 1e-9 { vars_with_gradients += 1; total_grad_norm += grad_norm; } } info!( vars_with_gradients, total_entries, total_grad_norm, "Gradient summary" ); // CRITICAL ASSERTION: At least some parameters should have non-zero gradients assert!( vars_with_gradients > 0, "FAIL: No variables have gradients! backward_pass() did not compute gradients." ); assert!( total_grad_norm > 1e-6, "FAIL: Total gradient norm is too small ({:.6}). Gradients may be zeros.", total_grad_norm ); info!("TEST PASSED: Gradients extracted from model.gradients"); Ok(()) } #[test] fn test_mamba2_backward_pass_extracts_real_gradients() -> Result<(), MLError> { info!("=== MAMBA-2 backward_pass() Real Gradient Test ==="); let device = cuda_device(); let stream = device.cuda_stream()?; let mut model = Mamba2SSM::default_hft(stream)?; // Create input/target (StreamTensor is always f32) let batch_size = model.config.batch_size; let seq_len = model.config.seq_len; let d_model = model.config.d_model; let input_data = vec![1.0_f32; batch_size * seq_len * d_model]; let input = StreamTensor::from_vec(input_data, &[batch_size, seq_len, d_model], stream)?; let target_data = vec![1.0_f32; batch_size * seq_len]; let target = StreamTensor::from_vec(target_data, &[batch_size, seq_len, 1], stream)?; // Forward + loss let output = model.forward(&input)?; let diff = gpu_sub(&output, &target)?; let sq = gpu_sqr(&diff)?; let loss_value = gpu_mean_all(&sq)?; info!(loss = loss_value, "Loss"); // Create a scalar loss StreamTensor for backward_pass let loss_tensor = StreamTensor::from_vec(vec![loss_value as f32], &[1], stream)?; // Call backward_pass (computes finite-difference pseudo-gradients) model.backward_pass(&loss_tensor, &input, &target)?; // Check model.gradients HashMap info!(total_entries = model.gradients.len(), "=== Model Gradients HashMap ==="); for (key, grad) in model.gradients.iter() { let grad_vec = grad.to_vec()?; let grad_norm: f64 = grad_vec.iter().map(|&g| (g as f64).powi(2)).sum::().sqrt(); let is_nonzero = grad_norm > 1e-9; info!(key, grad_norm, is_nonzero, "Gradient entry"); } // After backward_pass, gradients should be non-zero let total_grad_norm: f64 = model .gradients .values() .map(|grad| { let grad_vec = grad.to_vec().unwrap(); grad_vec.iter().map(|&g| (g as f64).powi(2)).sum::().sqrt() }) .sum(); info!(total_grad_norm, "Total gradient norm in model.gradients"); assert!( total_grad_norm > 1e-6, "FAIL: backward_pass() produced zero gradients. Need non-zero pseudo-gradients." ); info!("TEST PASSED: backward_pass() extracts real gradients"); Ok(()) }