diag(policy-quality): Task 0.7 — eval-mode action distribution (H10 signal)

Wires the eval_dist HEALTH_DIAG group with per-magnitude action distribution
read from the validation backtest. H10 detection signal — training-mode
entropy may look uniform while eval-mode argmax collapses to Quarter.

Implementation:
  * GpuBacktestEvaluator::read_eval_action_distribution_per_magnitude()
    reads actions_history_buf to host, decodes magnitude bin per sample
    (action layout: dir*27 + mag*9 + ord*3 + urg → mag = (a/9) % 3),
    returns [Quarter, Half, Full] normalized over non-skipped entries.
  * DQNTrainer.last_eval_magnitude_dist field, populated in metrics.rs
    after each validation backtest. Non-fatal on readback error.
  * HEALTH_DIAG eval_dist group [eq, eh, ef] now shows real values.

Per plan Task 0.7. Complete — no stubs.
This commit is contained in:
jgrusewski
2026-04-21 21:43:01 +02:00
parent 4e1a937cec
commit 0310b1d1eb
5 changed files with 60 additions and 2 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)