#![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, )] //! TDD Tests for Gradient Accumulation (WAVE 26 P2.2) //! //! Gradient accumulation allows larger effective batch sizes by accumulating //! gradients over multiple forward/backward passes before applying optimizer step. //! //! Benefits: //! - Larger effective batch size without GPU memory limits //! - More stable gradient estimates (reduced variance) //! - Better convergence in low-data regimes //! //! Example: batch_size=64, accumulation_steps=4 -> effective_batch_size=256 use ml::trainers::dqn::DQNHyperparameters; use ml::trainers::dqn::DQNTrainer; /// Helper to create minimal hyperparameters for testing fn create_test_hyperparams() -> DQNHyperparameters { let mut params = DQNHyperparameters::conservative(); params.epochs = 2; // Minimal epochs for fast testing params.batch_size = 32; params.buffer_size = 1000; params.min_replay_size = 100; params.learning_rate = 0.001; params.gradient_accumulation_steps = 1; // Default: no accumulation params } /// TEST 1: Verify default configuration (no accumulation) #[test] fn test_default_accumulation_steps_is_one() { let params = create_test_hyperparams(); assert_eq!( params.gradient_accumulation_steps, 1, "Default accumulation_steps should be 1 (no accumulation)" ); } /// TEST 2: Verify accumulation_steps field exists in config #[test] fn test_accumulation_steps_field_in_config() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 4; assert_eq!( params.gradient_accumulation_steps, 4, "Should be able to set gradient_accumulation_steps" ); } /// TEST 3: Verify trainer accepts gradient_accumulation_steps config #[test] fn test_trainer_accepts_accumulation_config() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 2; let trainer = DQNTrainer::new(params); assert!( trainer.is_ok(), "Trainer should accept gradient_accumulation_steps config" ); } /// TEST 4: Verify gradient accumulation field is stored in trainer #[test] fn test_trainer_stores_accumulation_steps() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 4; let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // Trainer should store the accumulation_steps value // This will be accessible via trainer.hyperparams().gradient_accumulation_steps assert_eq!( trainer.hyperparams().gradient_accumulation_steps, 4, "Trainer should store accumulation_steps from config" ); } /// TEST 5: Validate accumulation_steps > 0 #[test] fn test_reject_zero_accumulation_steps() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 0; // Invalid let trainer = DQNTrainer::new(params); assert!( trainer.is_err(), "Should reject gradient_accumulation_steps = 0" ); let err_msg = trainer.unwrap_err().to_string(); assert!( err_msg.contains("gradient_accumulation_steps") || err_msg.contains("greater than 0"), "Error message should mention gradient_accumulation_steps validation" ); } /// TEST 6: Verify effective batch size calculation #[test] fn test_effective_batch_size_calculation() { let mut params = create_test_hyperparams(); params.batch_size = 64; params.gradient_accumulation_steps = 4; let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // Effective batch size = batch_size * accumulation_steps let expected_effective_batch = 64 * 4; // 256 // This validates that the trainer understands the concept assert_eq!( trainer.hyperparams().batch_size * trainer.hyperparams().gradient_accumulation_steps, expected_effective_batch, "Effective batch size should be batch_size * accumulation_steps" ); } /// TEST 7: Verify gradient accumulation doesn't exceed replay buffer size #[test] fn test_accumulation_bounded_by_replay_buffer() { let mut params = create_test_hyperparams(); params.batch_size = 64; params.gradient_accumulation_steps = 4; // Effective batch = 256 params.buffer_size = 1000; // Buffer has enough samples let trainer = DQNTrainer::new(params); assert!( trainer.is_ok(), "Should accept accumulation when buffer is large enough" ); // But if effective batch exceeds buffer... let mut params2 = create_test_hyperparams(); params2.batch_size = 64; params2.gradient_accumulation_steps = 100; // Effective batch = 6400 > buffer_size=1000 params2.buffer_size = 1000; let trainer2 = DQNTrainer::new(params2); // Trainer creation should succeed (validation happens at runtime) assert!( trainer2.is_ok(), "Trainer creation should succeed (runtime will validate)" ); } /// TEST 8: Verify gradient accumulation preserves loss scaling /// Loss should be scaled by 1/accumulation_steps to maintain gradient magnitude #[tokio::test] async fn test_loss_scaling_during_accumulation() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 4; params.batch_size = 32; params.min_replay_size = 50; // Low threshold for faster testing let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // When training with accumulation, each mini-batch loss should be divided by accumulation_steps // This test verifies the concept exists (actual implementation will test functional behavior) // The scaled loss prevents gradient explosion when accumulating let accumulation_steps = trainer.hyperparams().gradient_accumulation_steps; let scale_factor = 1.0 / accumulation_steps as f64; assert!( scale_factor > 0.0 && scale_factor <= 1.0, "Loss scale factor should be in range (0, 1] for accumulation_steps >= 1" ); } /// TEST 9: Verify optimizer step called once per accumulation cycle /// With accumulation_steps=4, optimizer.step() should be called every 4 mini-batches #[test] fn test_optimizer_step_frequency() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 4; let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // Verify the trainer has the accumulation_steps configured assert_eq!( trainer.hyperparams().gradient_accumulation_steps, 4, "Trainer should have accumulation_steps=4" ); // The implementation will ensure optimizer.step() is called after every 4 mini-batches // This test validates the configuration exists } /// TEST 10: Verify gradient accumulation resets after optimizer step #[tokio::test] async fn test_gradients_reset_after_optimizer_step() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 2; params.batch_size = 32; let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // After accumulating gradients and calling optimizer.step(), // gradients should be zeroed via optimizer.zero_grad() // This test verifies the configuration (implementation will test functional behavior) assert_eq!( trainer.hyperparams().gradient_accumulation_steps, 2, "Should use accumulation_steps=2" ); } /// TEST 11: Verify accumulation works with various batch sizes #[test] fn test_accumulation_with_different_batch_sizes() { // Test small batch + accumulation let mut params1 = create_test_hyperparams(); params1.batch_size = 16; params1.gradient_accumulation_steps = 8; // Effective: 128 assert!(DQNTrainer::new(params1).is_ok()); // Test medium batch + accumulation let mut params2 = create_test_hyperparams(); params2.batch_size = 64; params2.gradient_accumulation_steps = 4; // Effective: 256 assert!(DQNTrainer::new(params2).is_ok()); // Test large batch + no accumulation let mut params3 = create_test_hyperparams(); params3.batch_size = 128; params3.gradient_accumulation_steps = 1; // Effective: 128 assert!(DQNTrainer::new(params3).is_ok()); } /// TEST 12: Verify accumulation respects GPU memory limits /// Even with accumulation, each mini-batch must fit in GPU memory #[test] fn test_accumulation_respects_gpu_memory_limit() { let mut params = create_test_hyperparams(); params.batch_size = 250; // Exceeds GPU limit (230) params.gradient_accumulation_steps = 4; let trainer = DQNTrainer::new(params); assert!( trainer.is_err(), "Should reject batch_size > 230 even with accumulation" ); let err_msg = trainer.unwrap_err().to_string(); assert!( err_msg.contains("230") || err_msg.contains("GPU memory"), "Error should mention GPU memory limit" ); } /// TEST 13: Verify accumulation_steps=1 behaves like normal training #[tokio::test] async fn test_no_accumulation_is_backward_compatible() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 1; // No accumulation params.batch_size = 32; let trainer = DQNTrainer::new(params).expect("Failed to create trainer"); // With accumulation_steps=1, behavior should be identical to normal training // (optimizer.step() called after every batch, no gradient accumulation) assert_eq!( trainer.hyperparams().gradient_accumulation_steps, 1, "accumulation_steps=1 should behave like normal training" ); } /// TEST 14: Verify large accumulation steps (stress test) #[test] fn test_large_accumulation_steps() { let mut params = create_test_hyperparams(); params.batch_size = 32; params.gradient_accumulation_steps = 16; // Effective batch: 512 params.buffer_size = 10000; // Large buffer to support effective batch let trainer = DQNTrainer::new(params); assert!( trainer.is_ok(), "Should accept large accumulation_steps with sufficient buffer" ); } /// TEST 15: Verify accumulation works with all DQN features #[test] fn test_accumulation_with_rainbow_dqn_features() { let mut params = create_test_hyperparams(); params.gradient_accumulation_steps = 4; let trainer = DQNTrainer::new(params); assert!( trainer.is_ok(), "Gradient accumulation should work with all Rainbow DQN features" ); }