diff --git a/crates/common/src/metrics/training_metrics.rs b/crates/common/src/metrics/training_metrics.rs index bad0c2049..ae32fc502 100644 --- a/crates/common/src/metrics/training_metrics.rs +++ b/crates/common/src/metrics/training_metrics.rs @@ -138,9 +138,12 @@ pub fn init() { "Number of active training workers", ); + // Step-level progress (updated every log_q_values / log_diagnostics call) + _ = register_gauge_vec("foxhunt_training_step", "Current training step within epoch", mf); + // RL diagnostic gauges (model + fold) - _ = register_gauge_vec("foxhunt_training_q_value_mean", "Mean Q-value per epoch (DQN)", mf); - _ = register_gauge_vec("foxhunt_training_q_value_max", "Max Q-value per epoch (DQN)", mf); + _ = register_gauge_vec("foxhunt_training_q_value_mean", "Mean Q-value (updated per step)", mf); + _ = register_gauge_vec("foxhunt_training_q_value_max", "Max Q-value (updated per step)", mf); _ = register_gauge_vec("foxhunt_training_q_overestimation_ratio", "Q-max / |Q-mean| ratio — spikes indicate overestimation (DQN)", mf); _ = register_gauge_vec("foxhunt_training_policy_entropy", "Policy entropy (PPO)", mf); _ = register_gauge_vec( @@ -332,6 +335,10 @@ pub fn init() { // Typed helpers // --------------------------------------------------------------------------- +pub fn set_training_step(model: &str, fold: &str, step: f64) { + set_gauge_vec("foxhunt_training_step", &[model, fold], step); +} + pub fn set_epoch(model: &str, fold: &str, epoch: f64) { set_gauge_vec("foxhunt_training_current_epoch", &[model, fold], epoch); } diff --git a/crates/ml-dqn/src/dqn.rs b/crates/ml-dqn/src/dqn.rs index abec88280..e62167472 100644 --- a/crates/ml-dqn/src/dqn.rs +++ b/crates/ml-dqn/src/dqn.rs @@ -25,6 +25,7 @@ use candle_optimisers::adam::ParamsAdam; use candle_optimisers::Decay; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; +use common::metrics::training_metrics; use tracing::debug; use super::{Experience, ExposureLevel, FactoredAction, OrderRouter, OrderType, Urgency}; @@ -3624,7 +3625,12 @@ impl DQN { let q_max = q_vec.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let q_mean = q_vec.iter().sum::() / n_actions.max(1) as f32; - tracing::info!( + // Push to Prometheus gauges (cheap atomic set, scrapable every 15s) + training_metrics::set_training_step("dqn", "current", self.training_steps as f64); + training_metrics::set_q_value_stats("dqn", "current", q_mean as f64, q_max as f64); + + // Formatted log for debug/forensics only — no allocation at info level + tracing::debug!( "Step {} Q-values ({} actions): range=[{:.2}, {:.2}], mean={:.2}", self.training_steps, n_actions, q_min, q_max, q_mean ); @@ -3638,7 +3644,7 @@ impl DQN { let top_actions: Vec<_> = indexed.iter().take(top_n) .map(|(idx, q)| format!("A{}={:.2}", idx, q)) .collect(); - tracing::info!("Top-{} actions: {}", top_n, top_actions.join(", ")); + tracing::debug!("Top-{} actions: {}", top_n, top_actions.join(", ")); // Alert if Q-value collapse detected (mean Q-value near zero and low variance) let q_variance = q_vec.iter().map(|&q| (q - q_mean).powi(2)).sum::() / q_vec.len() as f32; @@ -3713,7 +3719,10 @@ impl DQN { pub fn log_diagnostics(&mut self, grad_norm: f32) -> Result<(), MLError> { let dead_pct = self.detect_dead_neurons()?; - tracing::info!( + // Push to Prometheus gauges (cheap atomic set, scrapable every 15s) + training_metrics::set_gradient_norm("dqn", "current", grad_norm as f64); + + tracing::debug!( "Step {} Diagnostics: grad_norm={:.2}, dead_neurons={:.2}%", self.training_steps, grad_norm,