Files
foxhunt/crates/ml-ensemble/src/metrics.rs
jgrusewski b4178952d4 fix(ml): BF16/F32 boundary alignment, GPU-resident ops across all ML crates
- Cast input to weight dtype in DQN residual, rmsnorm, noisy_layers
- Set use_gpu=true in QNetworkConfig defaults and all config sites
- Resolve BF16 boundary mismatches in attention, curiosity, branching,
  distributional_dueling across ml-dqn
- GPU-resident regime ops with BF16 boundary casts, eliminate .expect() in CUDA paths
- Eliminate all Device::Cpu fallbacks — GPU-only across 10 ML crates
- PPO: cast logits to F32 before softmax, cast batch tensors to training dtype
- Gradient collapse detection for RegimeConditionalDQN
- Wire halt_grad_collapse from CUDA guard kernel to halt training
- Dead neuron detection uses active network VarMap + squeeze factored readback
- Increment gradient_logging_step in GPU PER path
- Gradient collapse warmup guards use original buffer_size
- Cap training steps per epoch + tracing migration
- Replace Tensor::all() with sum_all() for pinned Candle compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 11:59:31 +01:00

199 lines
6.6 KiB
Rust

//! Prometheus metrics for ensemble hot-swapping
//!
//! This module provides comprehensive metrics for monitoring checkpoint
//! hot-swapping operations, including swap success rates, latencies,
//! validation results, and canary monitoring.
use once_cell::sync::Lazy;
use prometheus::{register_counter_vec, register_histogram_vec, CounterVec, HistogramVec};
// Metric registration is infallible in practice; once_cell::Lazy closures cannot use `?`
#[allow(clippy::expect_used)]
/// Counter for checkpoint swaps by status
pub static CHECKPOINT_SWAPS_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
register_counter_vec!(
"checkpoint_swaps_total",
"Total checkpoint hot-swaps by model and status",
&["model_id", "status"] // status: success, rollback, failed
)
.expect("Failed to register checkpoint_swaps_total")
});
#[allow(clippy::expect_used)]
/// Histogram for checkpoint swap latency
pub static CHECKPOINT_SWAP_LATENCY_MICROSECONDS: Lazy<HistogramVec> = Lazy::new(|| {
register_histogram_vec!(
"checkpoint_swap_latency_microseconds",
"Checkpoint swap latency in microseconds (atomic swap operation)",
&["model_id"],
vec![0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0] // Sub-microsecond to 100μs
)
.expect("Failed to register checkpoint_swap_latency_microseconds")
});
#[allow(clippy::expect_used)]
/// Counter for checkpoint validations
pub static CHECKPOINT_VALIDATION_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
register_counter_vec!(
"checkpoint_validation_total",
"Total checkpoint validations by model and result",
&["model_id", "result"] // result: passed, failed
)
.expect("Failed to register checkpoint_validation_total")
});
#[allow(clippy::expect_used)]
/// Histogram for checkpoint validation latency
pub static CHECKPOINT_VALIDATION_LATENCY_MILLISECONDS: Lazy<HistogramVec> = Lazy::new(|| {
register_histogram_vec!(
"checkpoint_validation_latency_milliseconds",
"Checkpoint validation latency in milliseconds (1000 predictions)",
&["model_id"],
vec![10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0]
)
.expect("Failed to register checkpoint_validation_latency_milliseconds")
});
#[allow(clippy::expect_used)]
/// Histogram for validated checkpoint P99 inference latency
pub static CHECKPOINT_P99_LATENCY_MICROSECONDS: Lazy<HistogramVec> = Lazy::new(|| {
register_histogram_vec!(
"checkpoint_p99_latency_microseconds",
"P99 inference latency from checkpoint validation",
&["model_id"],
vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0]
)
.expect("Failed to register checkpoint_p99_latency_microseconds")
});
#[allow(clippy::expect_used)]
/// Counter for canary monitoring results
pub static CANARY_MONITORING_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
register_counter_vec!(
"canary_monitoring_total",
"Total canary monitoring runs by model and result",
&["model_id", "result"] // result: success, failed
)
.expect("Failed to register canary_monitoring_total")
});
#[allow(clippy::expect_used)]
/// Histogram for canary monitoring duration
pub static CANARY_MONITORING_DURATION_SECONDS: Lazy<HistogramVec> = Lazy::new(|| {
register_histogram_vec!(
"canary_monitoring_duration_seconds",
"Canary monitoring duration in seconds",
&["model_id"],
vec![60.0, 120.0, 300.0, 600.0, 1800.0] // 1 min to 30 min
)
.expect("Failed to register canary_monitoring_duration_seconds")
});
#[allow(clippy::expect_used)]
/// Counter for rollbacks by reason
pub static CHECKPOINT_ROLLBACKS_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
register_counter_vec!(
"checkpoint_rollbacks_total",
"Total checkpoint rollbacks by model and reason",
&["model_id", "reason"] // reason: latency, error_rate, accuracy_drop
)
.expect("Failed to register checkpoint_rollbacks_total")
});
/// Ensemble metrics collector
#[derive(Debug)]
pub struct EnsembleMetrics;
impl EnsembleMetrics {
/// Record successful checkpoint swap
pub fn record_swap_success(model_id: &str, latency_us: u64) {
CHECKPOINT_SWAPS_TOTAL
.with_label_values(&[model_id, "success"])
.inc();
CHECKPOINT_SWAP_LATENCY_MICROSECONDS
.with_label_values(&[model_id])
.observe(latency_us as f64);
}
/// Record checkpoint swap rollback
pub fn record_swap_rollback(model_id: &str) {
CHECKPOINT_SWAPS_TOTAL
.with_label_values(&[model_id, "rollback"])
.inc();
}
/// Record checkpoint swap failure
pub fn record_swap_failed(model_id: &str) {
CHECKPOINT_SWAPS_TOTAL
.with_label_values(&[model_id, "failed"])
.inc();
}
/// Record checkpoint validation result
pub fn record_validation(
model_id: &str,
passed: bool,
validation_latency_ms: u64,
p99_latency_us: u64,
) {
let result = if passed { "passed" } else { "failed" };
CHECKPOINT_VALIDATION_TOTAL
.with_label_values(&[model_id, result])
.inc();
CHECKPOINT_VALIDATION_LATENCY_MILLISECONDS
.with_label_values(&[model_id])
.observe(validation_latency_ms as f64);
CHECKPOINT_P99_LATENCY_MICROSECONDS
.with_label_values(&[model_id])
.observe(p99_latency_us as f64);
}
/// Record canary monitoring result
pub fn record_canary(model_id: &str, success: bool, duration_secs: u64) {
let result = if success { "success" } else { "failed" };
CANARY_MONITORING_TOTAL
.with_label_values(&[model_id, result])
.inc();
CANARY_MONITORING_DURATION_SECONDS
.with_label_values(&[model_id])
.observe(duration_secs as f64);
}
/// Record rollback with reason
pub fn record_rollback(model_id: &str, reason: &str) {
CHECKPOINT_ROLLBACKS_TOTAL
.with_label_values(&[model_id, reason])
.inc();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::info;
#[test]
fn test_metrics_recording() {
// Record successful swap
EnsembleMetrics::record_swap_success("DQN", 500);
// Record validation
EnsembleMetrics::record_validation("DQN", true, 150, 35);
// Record canary success
EnsembleMetrics::record_canary("DQN", true, 300);
// Record rollback
EnsembleMetrics::record_rollback("DQN", "latency");
// Verify metrics were recorded (metrics are global, just test they don't panic)
info!("All metrics recorded successfully");
}
}