Remove use_double_dqn, use_dueling, use_per, use_branching,
use_distributional, use_noisy_nets, use_huber_loss, and use_cql
from DQNConfig, DQNHyperparameters, and DqnParams structs.
These features are always enabled (Rainbow DQN standard). The boolean
flags were dead code — every constructor set them to true, and the
only code paths that set them to false were in tests that disabled
features for simplicity. With the fields removed, the features are
unconditionally active, eliminating ~490 lines of dead configuration.
Key changes:
- Struct field declarations removed from 3 core config structs
- Conditional branches (if use_X { ... } else { ... }) simplified:
dueling/branching/PER network creation is now unconditional
- Checkpoint metadata hardcodes "true" for backward compatibility
- Hyperopt search space index 11 (use_branching) fixed at 1.0
- TOML/YAML config files cleaned of removed fields
- Tests that toggled these flags updated or rewritten
45 files changed, -487 net lines. Zero new test failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
368 lines
12 KiB
Rust
368 lines
12 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,
|
|
)]
|
|
//! 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"
|
|
);
|
|
}
|