Add to_varstore() compatibility shims on DuelingQNetwork and DistributionalDuelingQNetwork so test/example code can rebuild a GpuVarStore snapshot when needed. Delete dead tests that referenced removed DQNAgent, PrioritizedReplayBuffer, and ReplayBufferType. Fix action index references (action_19/21 -> action_28/30) and type annotation issues (sin ambiguity, remainder operator). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
208 lines
6.3 KiB
Rust
208 lines
6.3 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,
|
|
)]
|
|
//! WAVE 26 P0 Features Integration Tests
|
|
//!
|
|
//! Comprehensive integration tests verifying all P0 features work together:
|
|
//! - P0.1: TD-error clamping (1e6 threshold)
|
|
//! - P0.2: Batch diversity enforcement (no duplicate sampling)
|
|
//! - P0.6: LR scheduler (exponential decay)
|
|
//! - P0.7: Priority staleness tracking (automatic decay)
|
|
//!
|
|
//! Each test validates the feature in isolation and in combination.
|
|
|
|
use ml::trainers::dqn::lr_scheduler::{LRScheduler, LRDecayType};
|
|
use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer};
|
|
use anyhow::Result;
|
|
|
|
/// P0.6: Test LR scheduler decay over training epochs
|
|
#[tokio::test]
|
|
async fn test_p0_lr_scheduler_decay() -> Result<()> {
|
|
// Create LR scheduler with exponential decay
|
|
let initial_lr = 0.001;
|
|
let decay_rate = 0.95;
|
|
let min_lr = 1e-6;
|
|
|
|
let mut scheduler = LRScheduler::new(
|
|
initial_lr,
|
|
0, // warmup_steps (0 = no warmup)
|
|
LRDecayType::Exponential {
|
|
decay_rate,
|
|
min_lr,
|
|
},
|
|
);
|
|
|
|
// Verify initial LR
|
|
assert_eq!(scheduler.get_lr(), initial_lr);
|
|
assert_eq!(scheduler.get_initial_lr(), initial_lr);
|
|
|
|
// Step through 300 iterations
|
|
let mut last_lr = initial_lr;
|
|
for step in 1..=300 {
|
|
scheduler.step();
|
|
let current_lr = scheduler.get_lr();
|
|
|
|
// Verify LR is decreasing (or at minimum)
|
|
assert!(
|
|
current_lr <= last_lr || current_lr == min_lr,
|
|
"LR should decrease or reach minimum at step {}",
|
|
step
|
|
);
|
|
|
|
// Verify exponential decay formula: lr = initial_lr * decay_rate^step
|
|
let expected_lr = (initial_lr * decay_rate.powf(step as f64)).max(min_lr);
|
|
assert!(
|
|
(current_lr - expected_lr).abs() < 1e-8,
|
|
"LR at step {}: expected {:.6e}, got {:.6e}",
|
|
step,
|
|
expected_lr,
|
|
current_lr
|
|
);
|
|
|
|
last_lr = current_lr;
|
|
}
|
|
|
|
// Verify minimum LR is respected
|
|
assert!(scheduler.get_lr() >= min_lr);
|
|
|
|
// Verify LR has decayed from initial value
|
|
assert!(scheduler.get_lr() < initial_lr);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// P0 Integration: Test all features working together
|
|
#[tokio::test]
|
|
async fn test_p0_all_features_together() -> Result<()> {
|
|
// Create hyperparameters with all P0 features enabled
|
|
let mut hyperparams = DQNHyperparameters::default();
|
|
hyperparams.learning_rate = 0.001;
|
|
hyperparams.lr_decay_rate = 0.95; // Enable P0.6 (LR scheduler)
|
|
hyperparams.lr_decay_steps = 100;
|
|
hyperparams.lr_min = 1e-6;
|
|
hyperparams.epochs = 2; // Just 2 epochs for integration test
|
|
hyperparams.batch_size = 32;
|
|
hyperparams.buffer_size = 1000;
|
|
|
|
// Create trainer
|
|
let trainer = DQNTrainer::new(hyperparams)?;
|
|
|
|
// Verify LR scheduler is initialized
|
|
assert_eq!(trainer.get_current_lr(), 0.001);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// P0.1: Verify TD-error clamping threshold exists
|
|
#[test]
|
|
fn test_p0_td_error_clamping_threshold() {
|
|
// This is a compile-time test to verify the clamp exists
|
|
const MAX_LOSS_THRESHOLD: f32 = 1e6;
|
|
assert_eq!(MAX_LOSS_THRESHOLD, 1_000_000.0);
|
|
|
|
// Verify clamp logic
|
|
let test_losses = vec![
|
|
(100.0, 100.0), // Normal loss
|
|
(1000.0, 1000.0), // High but acceptable
|
|
(1e6, 1e6), // At threshold
|
|
(1e7, 1e6), // Above threshold - should clamp
|
|
(f32::INFINITY, 1e6), // Extreme case
|
|
];
|
|
|
|
for (input, expected) in test_losses {
|
|
let clamped = if input > 1e6 { 1e6 } else { input };
|
|
assert_eq!(clamped, expected, "Loss clamping failed for input {}", input);
|
|
}
|
|
}
|
|
|
|
/// Integration test: Verify DQNTrainer has LR scheduler access
|
|
#[tokio::test]
|
|
async fn test_p0_trainer_lr_scheduler_access() -> Result<()> {
|
|
let mut hyperparams = DQNHyperparameters::default();
|
|
hyperparams.learning_rate = 0.001;
|
|
hyperparams.lr_decay_rate = 0.9;
|
|
hyperparams.lr_decay_steps = 50;
|
|
hyperparams.lr_min = 1e-5;
|
|
hyperparams.epochs = 1;
|
|
|
|
let trainer = DQNTrainer::new(hyperparams)?;
|
|
|
|
// Verify initial LR
|
|
let initial_lr = trainer.get_current_lr();
|
|
assert_eq!(initial_lr, 0.001);
|
|
|
|
Ok(())
|
|
}
|
|
|