use crate::device::GpuInfo;
pub fn total_power_watts(gpus: &[GpuInfo]) -> f64 {
gpus.iter()
.filter_map(GpuInfo::power_consumption_reading)
.sum()
}
pub fn mean_utilization(gpus: &[GpuInfo]) -> Option<f64> {
mean(gpus.iter().filter_map(GpuInfo::utilization_reading))
}
pub fn mean_temperature(gpus: &[GpuInfo]) -> Option<f64> {
mean(
gpus.iter()
.filter_map(GpuInfo::temperature_reading)
.map(f64::from),
)
}
pub fn temperature_std_dev(gpus: &[GpuInfo]) -> Option<f64> {
let temps: Vec<f64> = gpus
.iter()
.filter_map(GpuInfo::temperature_reading)
.map(f64::from)
.collect();
if temps.len() < 2 {
return None;
}
let mean = temps.iter().sum::<f64>() / temps.len() as f64;
let variance = temps.iter().map(|t| (t - mean).powi(2)).sum::<f64>() / (temps.len() - 1) as f64;
Some(variance.sqrt())
}
pub fn first_ane_power_watts(gpus: &[GpuInfo]) -> Option<f64> {
gpus.iter()
.find_map(GpuInfo::ane_utilization_reading)
.map(|mw| mw / 1000.0)
}
fn mean(values: impl Iterator<Item = f64>) -> Option<f64> {
let mut count = 0usize;
let mut sum = 0.0;
for value in values {
sum += value;
count += 1;
}
(count > 0).then(|| sum / count as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::types::GPU_METRIC_UNAVAILABLE;
use std::collections::HashMap;
fn gpu(utilization: f64, temperature: u32, power: f64) -> GpuInfo {
GpuInfo {
uuid: "GPU-1".to_string(),
time: String::new(),
name: "test".to_string(),
device_type: "GPU".to_string(),
host_id: "h".to_string(),
hostname: "h".to_string(),
instance: "h".to_string(),
utilization,
ane_utilization: GPU_METRIC_UNAVAILABLE,
dla_utilization: None,
tensorcore_utilization: None,
temperature,
used_memory: 0,
total_memory: 0,
frequency: 0,
power_consumption: power,
gpu_core_count: None,
temperature_threshold_slowdown: None,
temperature_threshold_shutdown: None,
temperature_threshold_max_operating: None,
temperature_threshold_acoustic: None,
performance_state: None,
fan_speed_rpm: None,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail: HashMap::new(),
}
}
fn unavailable() -> GpuInfo {
gpu(GPU_METRIC_UNAVAILABLE, 0, GPU_METRIC_UNAVAILABLE)
}
#[test]
fn absent_rows_are_skipped_not_counted_as_zero() {
let gpus = vec![gpu(80.0, 70, 300.0), unavailable()];
assert_eq!(mean_utilization(&gpus), Some(80.0));
assert_eq!(mean_temperature(&gpus), Some(70.0));
assert_eq!(total_power_watts(&gpus), 300.0);
}
#[test]
fn a_genuine_zero_still_counts() {
let gpus = vec![gpu(0.0, 30, 0.0), gpu(100.0, 50, 200.0)];
assert_eq!(mean_utilization(&gpus), Some(50.0));
assert_eq!(mean_temperature(&gpus), Some(40.0));
assert_eq!(total_power_watts(&gpus), 200.0);
}
#[test]
fn nothing_reported_yields_none_rather_than_zero() {
let gpus = vec![unavailable(), unavailable()];
assert_eq!(mean_utilization(&gpus), None);
assert_eq!(mean_temperature(&gpus), None);
assert_eq!(temperature_std_dev(&gpus), None);
assert_eq!(first_ane_power_watts(&gpus), None);
assert_eq!(total_power_watts(&gpus), 0.0);
}
#[test]
fn empty_input_is_handled() {
assert_eq!(mean_utilization(&[]), None);
assert_eq!(total_power_watts(&[]), 0.0);
assert_eq!(temperature_std_dev(&[]), None);
}
#[test]
fn std_dev_needs_two_reporting_devices() {
let gpus = vec![gpu(10.0, 60, 5.0), unavailable()];
assert_eq!(temperature_std_dev(&gpus), None);
let gpus = vec![gpu(10.0, 60, 5.0), gpu(10.0, 70, 5.0)];
let sd = temperature_std_dev(&gpus).expect("two readings");
assert!((sd - 7.0710678).abs() < 1e-6, "got {sd}");
}
#[test]
fn ane_power_converts_milliwatts_to_watts() {
let mut g = gpu(1.0, 40, 1.0);
g.ane_utilization = 2500.0;
assert_eq!(first_ane_power_watts(&[g]), Some(2.5));
}
}