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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-09 18:44:29 +01:00
parent 7751f7615a
commit 0355fb17bc
2 changed files with 21 additions and 5 deletions

View File

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

View File

@@ -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::<f32>() / 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::<f32>() / 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,