diff --git a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs index b5c5c92b7..e184805f3 100644 --- a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs +++ b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs @@ -857,6 +857,43 @@ impl GpuBacktestEvaluator { self.dqn_graph = None; } + /// Task 0.7 — read back the actions_history buffer and compute the + /// per-magnitude action distribution observed during the most recent + /// evaluation. Action encoding is `dir*27 + mag*9 + ord*3 + urg`, so + /// magnitude = `(action / 9) % 3` (0=Quarter, 1=Half, 2=Full). + /// + /// Returns `[quarter_frac, half_frac, full_frac]` summing to 1.0 over + /// non-negative action entries (negative entries indicate uninitialized + /// or skipped steps and are excluded). + pub fn read_eval_action_distribution_per_magnitude(&self) -> Result<[f32; 3], MLError> { + let len = self.n_windows * self.max_len; + let mut host = vec![0_i32; len]; + self.stream + .memcpy_dtoh(&self.actions_history_buf, &mut host) + .map_err(|e| MLError::ModelError(format!("actions_history dtoh: {e}")))?; + let mut counts = [0_u64; 3]; + let mut total = 0_u64; + for &a in &host { + if a < 0 { + continue; + } + let mag = ((a / 9) % 3) as usize; + if mag < 3 { + counts[mag] += 1; + total += 1; + } + } + if total == 0 { + return Ok([0.0; 3]); + } + let t = total as f32; + Ok([ + counts[0] as f32 / t, + counts[1] as f32 / t, + counts[2] as f32 / t, + ]) + } + /// Reset mutable evaluation state: portfolio, done flags, step returns, actions history. /// Must be called before each `evaluate_dqn_graphed` when reusing the evaluator /// across epochs — otherwise done_flags=1 from the previous evaluation causes diff --git a/crates/ml/src/trainers/dqn/trainer/constructor.rs b/crates/ml/src/trainers/dqn/trainer/constructor.rs index 1a7333734..235788bb6 100644 --- a/crates/ml/src/trainers/dqn/trainer/constructor.rs +++ b/crates/ml/src/trainers/dqn/trainer/constructor.rs @@ -581,6 +581,7 @@ impl DQNTrainer { prev_controller_values: super::ControllerPrevValues::default(), controller_fire_counts: super::ControllerFireCounts::default(), controller_total_epochs: 0, + last_eval_magnitude_dist: [0.0_f32; 3], // Wave 16 Portfolio Features (action masking always active) max_position, diff --git a/crates/ml/src/trainers/dqn/trainer/metrics.rs b/crates/ml/src/trainers/dqn/trainer/metrics.rs index 2953193c9..7f1ea7734 100644 --- a/crates/ml/src/trainers/dqn/trainer/metrics.rs +++ b/crates/ml/src/trainers/dqn/trainer/metrics.rs @@ -628,6 +628,16 @@ impl DQNTrainer { 0.0 }; + // Task 0.7 — capture eval-mode per-magnitude action distribution from + // the actions_history buffer for H10 detection. Failure is non-fatal: + // distribution stays at last value if readback errors (won't break val). + if let Some(ref ev) = self.gpu_evaluator { + match ev.read_eval_action_distribution_per_magnitude() { + Ok(dist) => self.last_eval_magnitude_dist = dist, + Err(e) => tracing::warn!("eval magnitude dist readback failed (non-fatal): {e}"), + } + } + Ok(-val_sharpe) } diff --git a/crates/ml/src/trainers/dqn/trainer/mod.rs b/crates/ml/src/trainers/dqn/trainer/mod.rs index a64aa30ea..7856d9563 100644 --- a/crates/ml/src/trainers/dqn/trainer/mod.rs +++ b/crates/ml/src/trainers/dqn/trainer/mod.rs @@ -285,6 +285,13 @@ pub struct DQNTrainer { /// doesn't collapse to a single bin too fast. pub(crate) explore_entropy_mag_history: Vec<(u32, f32)>, + /// Task 0.7 — per-magnitude action distribution observed during the + /// most recent VALIDATION backtest (eval-mode rollout, distinct from + /// training-mode action_dist). H10 detection signal: training-mode + /// entropy may look uniform while eval-mode argmax collapses to Quarter. + /// Layout: [Quarter, Half, Full] in [0, 1]. + pub(crate) last_eval_magnitude_dist: [f32; 3], + /// Task 0.9 — prior-epoch values of adaptive-controller outputs, used to /// detect "fire" = value changed from the prior epoch. Controllers audited: /// anti-LR, adaptive tau, adaptive gamma, adaptive grad-clip, CQL alpha diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 1971e77b8..37377cfad 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -2307,8 +2307,11 @@ impl DQNTrainer { 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, // Track 1 — noisy/VSN/drift (6 f32) 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, - // Track 1 — eval dist (3 f32) - 0.0_f32, 0.0_f32, 0.0_f32, + // Track 1 — eval dist (3 f32): per-magnitude action distribution + // observed in the most recent validation backtest. H10 signal. + self.last_eval_magnitude_dist[0], + self.last_eval_magnitude_dist[1], + self.last_eval_magnitude_dist[2], // Track 2 — reward contrib (6 f32) 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, // Track 3 — controllers (6 bool per-epoch fire + 1 f32 max running rate)