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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user