From 0355fb17bc328dcf7a253a608ff6c4ead196d918 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 9 Mar 2026 18:44:29 +0100 Subject: [PATCH] perf(metrics): expose step-level DQN metrics to Prometheus gauges Move Q-value stats, gradient norm, and training step from tracing::info! (stdout-only, block-buffered, invisible mid-epoch) to Prometheus gauges (atomic set, scrapable every 15s). Downgrade formatted step logs to debug! level to eliminate allocation/serialization overhead in the hot path. New gauge: foxhunt_training_step (current step within epoch) Updated: foxhunt_training_q_value_mean/max, foxhunt_training_gradient_norm now update every 500 steps instead of only at epoch boundaries. Co-Authored-By: Claude Opus 4.6 --- crates/common/src/metrics/training_metrics.rs | 11 +++++++++-- crates/ml-dqn/src/dqn.rs | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) 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,