From 9b02f86fcab35f90598a0634412f879bdd30dc4b Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 28 Mar 2026 13:11:47 +0100 Subject: [PATCH] =?UTF-8?q?feat(bf16):=201722/1722=20tests=20pass=20?= =?UTF-8?q?=E2=80=94=20ALL=20PASS,=20zero=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix last 2 tests: - action_masking: test used 81-action FactoredAction decoder for 45-action mask. Fixed to use mask's own 5×9 exposure encoding. - hyperopt bounds: test both Full (all ranges) and Fast (architecture fixed) phases. Implemented phase_fast override in continuous_bounds to pin hidden_dim_base, num_atoms, dueling_hidden_dim from TOML. Final scorecard: ml-core: 300/300 (100%) ml-dqn: 359/359 (100%) ml-ppo: 168/168 (100%) ml: 895/895 (100%) Total: 1722/1722 (100%) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/ml-ppo/src/action_masking.rs | 69 +++++++++++++------------- crates/ml/src/hyperopt/adapters/dqn.rs | 33 ++++++++++-- crates/ml/src/training_profile.rs | 12 ++--- 3 files changed, 69 insertions(+), 45 deletions(-) diff --git a/crates/ml-ppo/src/action_masking.rs b/crates/ml-ppo/src/action_masking.rs index 853ae78f5..949bb2d04 100644 --- a/crates/ml-ppo/src/action_masking.rs +++ b/crates/ml-ppo/src/action_masking.rs @@ -210,50 +210,51 @@ mod tests { #[test] fn test_45_action_masking_canonical_layout() { - use ml_core::action_space::{ExposureLevel, FactoredAction}; - + // 45-action layout: 5 exposure levels × 9 sub-actions each + // exposure_idx = action_idx / 9: + // 0 = Short100, 1 = Short50, 2 = Flat, 3 = Long50, 4 = Long100 let max_position = 5.0; + // At max position: Long50 (idx 3) and Long100 (idx 4) should be masked let mask = create_action_mask(max_position, max_position, 45); for idx in 0..45 { - if let Ok(action) = FactoredAction::from_index(idx) { - match action.exposure { - ExposureLevel::Long50 | ExposureLevel::Long100 => { - assert!( - !mask.get(idx).copied().unwrap_or(true), - "Long action {} should be masked at max position", - idx - ); - } - _ => { - assert!( - mask.get(idx).copied().unwrap_or(false), - "Non-long action {} should NOT be masked at max position", - idx - ); - } + let exposure_idx = idx / 9; + match exposure_idx { + 3 | 4 => { + assert!( + !mask[idx], + "Long action {} (exposure_idx={}) should be masked at max position", + idx, exposure_idx + ); + } + _ => { + assert!( + mask[idx], + "Non-long action {} (exposure_idx={}) should NOT be masked at max position", + idx, exposure_idx + ); } } } + // At min position: Short100 (idx 0) and Short50 (idx 1) should be masked let mask = create_action_mask(-max_position, max_position, 45); for idx in 0..45 { - if let Ok(action) = FactoredAction::from_index(idx) { - match action.exposure { - ExposureLevel::Short50 | ExposureLevel::Short100 => { - assert!( - !mask.get(idx).copied().unwrap_or(true), - "Short action {} should be masked at min position", - idx - ); - } - _ => { - assert!( - mask.get(idx).copied().unwrap_or(false), - "Non-short action {} should NOT be masked at min position", - idx - ); - } + let exposure_idx = idx / 9; + match exposure_idx { + 0 | 1 => { + assert!( + !mask[idx], + "Short action {} (exposure_idx={}) should be masked at min position", + idx, exposure_idx + ); + } + _ => { + assert!( + mask[idx], + "Non-short action {} (exposure_idx={}) should NOT be masked at min position", + idx, exposure_idx + ); } } } diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 385a2d47c..1da62578f 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -531,7 +531,7 @@ impl ParameterSpace for DQNParams { let cw = b("c51_warmup_epochs", (3.0, 15.0)); let iq = b("iqn_lambda", (0.0, 1.0)); - let bounds = vec![ + let mut bounds = vec![ (lr.0.ln(), lr.1.ln()), // 0: learning_rate (log scale) bs, // 1: batch_size gm, // 2: gamma @@ -556,6 +556,20 @@ impl ParameterSpace for DQNParams { iq, // 21: iqn_lambda ]; + // Phase Fast: fix architecture dims to single-point bounds + if crate::training_profile::get_hyperopt_phase() == crate::training_profile::HyperoptPhase::Fast { + if let Some(pf) = &hp.phase_fast { + let fix = |bounds: &mut Vec<(f64, f64)>, idx: usize, val: Option| { + if let Some(v) = val { + bounds[idx] = (v, v); + } + }; + fix(&mut bounds, 10, pf.dueling_hidden_dim); // dueling_hidden_dim + fix(&mut bounds, 13, pf.num_atoms); // num_atoms + fix(&mut bounds, 18, pf.hidden_dim_base); // hidden_dim_base + } + } + bounds } @@ -3619,6 +3633,10 @@ mod tests { #[test] fn test_dqn_params_bounds() { + use crate::training_profile::{set_hyperopt_phase, HyperoptPhase}; + + // Test Full phase: all bounds must be ranges (lower < upper) + set_hyperopt_phase(HyperoptPhase::Full(vec![0.0; 22])); let bounds = DQNParams::continuous_bounds(); assert_eq!(bounds.len(), 22); // 22D consolidated search space @@ -3637,11 +3655,18 @@ mod tests { assert!(lr_min > 0.0 && lr_min < 1e-3, "Learning rate lower bound should be small, got {:.2e}", lr_min); assert!(lr_max >= 1e-4 && lr_max <= 1e-2, "Learning rate upper bound should be reasonable, got {:.2e}", lr_max); - // Check all bounds have valid ranges (lower < upper) - // Exact values may be overridden by HyperoptProfile YAML for (i, (lo, hi)) in bounds.iter().enumerate() { - assert!(lo < hi, "Bound {i}: lower ({lo}) must be < upper ({hi})"); + assert!(lo < hi, "Full phase: Bound {i}: lower ({lo}) must be < upper ({hi})"); } + + // Test Fast phase: architecture dims must be fixed (single-point bounds) + set_hyperopt_phase(HyperoptPhase::Fast); + let fast_bounds = DQNParams::continuous_bounds(); + assert_eq!(fast_bounds[18].0, fast_bounds[18].1, "Fast: hidden_dim_base must be fixed"); + assert_eq!(fast_bounds[13].0, fast_bounds[13].1, "Fast: num_atoms must be fixed"); + assert_eq!(fast_bounds[10].0, fast_bounds[10].1, "Fast: dueling_hidden_dim must be fixed"); + // Learning rate must still be a range in Fast phase + assert_ne!(fast_bounds[0].0, fast_bounds[0].1, "Fast: learning_rate must still be searchable"); } #[test] diff --git a/crates/ml/src/training_profile.rs b/crates/ml/src/training_profile.rs index 809160cee..9ad5fcfd2 100644 --- a/crates/ml/src/training_profile.rs +++ b/crates/ml/src/training_profile.rs @@ -1014,13 +1014,11 @@ mod tests { set_hyperopt_phase(HyperoptPhase::Fast); use crate::hyperopt::ParameterSpace; let bounds = crate::hyperopt::adapters::dqn::DQNParams::continuous_bounds(); - // Index 21: hidden_dim_base, 15: num_atoms, 28: branch_hidden_dim, - // 13: dueling_hidden_dim, 10: v_range - assert_eq!(bounds[21].0, bounds[21].1, "hidden_dim_base must be fixed"); - assert_eq!(bounds[15].0, bounds[15].1, "num_atoms must be fixed"); - assert_eq!(bounds[28].0, bounds[28].1, "branch_hidden_dim must be fixed"); - assert_eq!(bounds[13].0, bounds[13].1, "dueling_hidden_dim must be fixed"); - assert_eq!(bounds[10].0, bounds[10].1, "v_range must be fixed"); + // 22D search space indices (from DQNParams::continuous_bounds): + // 10: dueling_hidden_dim, 13: num_atoms, 18: hidden_dim_base + assert_eq!(bounds[18].0, bounds[18].1, "hidden_dim_base must be fixed"); + assert_eq!(bounds[13].0, bounds[13].1, "num_atoms must be fixed"); + assert_eq!(bounds[10].0, bounds[10].1, "dueling_hidden_dim must be fixed"); // Learning rate (index 0) must still be a range. assert_ne!(bounds[0].0, bounds[0].1, "learning_rate must still be searchable"); }