use crate::device::GpuInfo;
pub const TEMP_FLOOR_C: f64 = 30.0;
pub const TEMP_FALLBACK_CEIL_C: f64 = 100.0;
pub const ANE_MIN_CEIL_W: f64 = 8.0;
pub const POWER_MIN_CEIL_W: f64 = 10.0;
const POWER_LIMIT_KEYS: [&str; 3] = [
"power_limit_current",
"power_limit_max",
"power_limit_default",
];
#[must_use]
pub fn nice_ceil(v: f64) -> f64 {
if !v.is_finite() || v <= 0.0 {
return 1.0;
}
let exp = v.log10().floor();
let pow = 10_f64.powf(exp);
let frac = v / pow; let nice = if frac <= 1.0 {
1.0
} else if frac <= 2.0 {
2.0
} else if frac <= 5.0 {
5.0
} else {
10.0
};
let ceil = nice * pow;
if ceil.is_finite() { ceil } else { v }
}
#[must_use]
pub fn temp_range(gpu: Option<&GpuInfo>) -> (f64, f64) {
let ceil = gpu
.and_then(|g| {
g.temperature_threshold_slowdown
.or(g.temperature_threshold_max_operating)
.or(g.temperature_threshold_shutdown)
})
.map(f64::from)
.filter(|&c| c > TEMP_FLOOR_C)
.unwrap_or(TEMP_FALLBACK_CEIL_C);
(TEMP_FLOOR_C, ceil)
}
#[must_use]
pub fn power_range(gpus: &[GpuInfo], history: &[f64]) -> (f64, f64) {
let mut total_limit = 0.0_f64;
let mut all_have_limit = !gpus.is_empty();
for g in gpus {
match gpu_power_limit(g) {
Some(w) => total_limit += w,
None => {
all_have_limit = false;
break;
}
}
}
let ceil = if all_have_limit && total_limit.is_finite() && total_limit > 0.0 {
total_limit
} else {
nice_ceil(history_peak(history).max(POWER_MIN_CEIL_W))
};
(0.0, ceil)
}
fn gpu_power_limit(g: &GpuInfo) -> Option<f64> {
POWER_LIMIT_KEYS.iter().find_map(|k| {
g.detail
.get(*k)
.and_then(|s| s.parse::<f64>().ok())
.filter(|&w| w.is_finite() && w > 0.0)
})
}
#[must_use]
pub fn ane_range(history: &[f64]) -> (f64, f64) {
(0.0, nice_ceil(history_peak(history).max(ANE_MIN_CEIL_W)))
}
fn history_peak(history: &[f64]) -> f64 {
history
.iter()
.copied()
.filter(|v| v.is_finite())
.fold(0.0_f64, f64::max)
}
#[must_use]
pub fn scale_badge(min: f64, max: f64) -> String {
format!("{min:.0}-{max:.0}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::GpuInfo;
use std::collections::HashMap;
fn gpu_with(
slowdown: Option<u32>,
max_operating: Option<u32>,
shutdown: Option<u32>,
detail: HashMap<String, String>,
) -> GpuInfo {
GpuInfo {
uuid: "gpu-0".to_string(),
time: String::new(),
name: "Test GPU".to_string(),
device_type: "GPU".to_string(),
host_id: "localhost".to_string(),
hostname: "localhost".to_string(),
instance: "localhost".to_string(),
utilization: 0.0,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature: 50,
used_memory: 0,
total_memory: 0,
frequency: 0,
power_consumption: 0.0,
gpu_core_count: None,
temperature_threshold_slowdown: slowdown,
temperature_threshold_shutdown: shutdown,
temperature_threshold_max_operating: max_operating,
temperature_threshold_acoustic: None,
performance_state: None,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail,
}
}
#[test]
fn nice_ceil_rounds_to_1_2_5_decades() {
assert_eq!(nice_ceil(1.0), 1.0);
assert_eq!(nice_ceil(1.5), 2.0);
assert_eq!(nice_ceil(2.0), 2.0);
assert_eq!(nice_ceil(3.5), 5.0);
assert_eq!(nice_ceil(5.0), 5.0);
assert_eq!(nice_ceil(7.0), 10.0);
assert_eq!(nice_ceil(10.0), 10.0);
assert_eq!(nice_ceil(17.5), 20.0);
assert_eq!(nice_ceil(158.0), 200.0);
assert_eq!(nice_ceil(287.0), 500.0);
}
#[test]
fn nice_ceil_handles_degenerate_input() {
assert_eq!(nice_ceil(0.0), 1.0);
assert_eq!(nice_ceil(-5.0), 1.0);
assert_eq!(nice_ceil(f64::NAN), 1.0);
assert_eq!(nice_ceil(f64::INFINITY), 1.0);
}
#[test]
fn nice_ceil_result_is_always_finite() {
assert!(nice_ceil(f64::MAX).is_finite());
assert!(nice_ceil(1.0e308).is_finite());
assert!(nice_ceil(8.0e307).is_finite());
}
#[test]
fn temp_range_uses_threshold_priority() {
let g = gpu_with(Some(83), Some(90), Some(95), HashMap::new());
assert_eq!(temp_range(Some(&g)), (30.0, 83.0));
let g = gpu_with(None, Some(90), Some(95), HashMap::new());
assert_eq!(temp_range(Some(&g)), (30.0, 90.0));
let g = gpu_with(None, None, Some(95), HashMap::new());
assert_eq!(temp_range(Some(&g)), (30.0, 95.0));
}
#[test]
fn temp_range_falls_back_without_thresholds() {
assert_eq!(temp_range(None), (30.0, TEMP_FALLBACK_CEIL_C));
let g = gpu_with(None, None, None, HashMap::new());
assert_eq!(temp_range(Some(&g)), (30.0, TEMP_FALLBACK_CEIL_C));
}
#[test]
fn temp_range_ignores_threshold_at_or_below_floor() {
let g = gpu_with(Some(20), None, None, HashMap::new());
assert_eq!(temp_range(Some(&g)), (30.0, TEMP_FALLBACK_CEIL_C));
}
#[test]
fn power_range_prefers_enforced_limit() {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), "350.00".to_string());
let g = gpu_with(None, None, None, detail);
assert_eq!(
power_range(std::slice::from_ref(&g), &[100.0, 200.0, 320.0]),
(0.0, 350.0)
);
}
#[test]
fn power_range_limit_key_priority() {
let mut detail = HashMap::new();
detail.insert("power_limit_max".to_string(), "450".to_string());
detail.insert("power_limit_default".to_string(), "400".to_string());
let g = gpu_with(None, None, None, detail);
assert_eq!(power_range(std::slice::from_ref(&g), &[]), (0.0, 450.0));
}
#[test]
fn power_range_tries_next_key_when_first_invalid() {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), "0".to_string());
detail.insert("power_limit_max".to_string(), "450".to_string());
let g = gpu_with(None, None, None, detail);
assert_eq!(power_range(std::slice::from_ref(&g), &[40.0]), (0.0, 450.0));
}
#[test]
fn power_range_sums_multi_gpu_limits() {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), "350".to_string());
let gpus: Vec<GpuInfo> = (0..4)
.map(|_| gpu_with(None, None, None, detail.clone()))
.collect();
assert_eq!(power_range(&gpus, &[900.0, 1200.0]), (0.0, 1400.0));
}
#[test]
fn power_range_multi_gpu_falls_back_when_any_limit_missing() {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), "350".to_string());
let with_limit = gpu_with(None, None, None, detail);
let without_limit = gpu_with(None, None, None, HashMap::new());
let gpus = [with_limit, without_limit];
assert_eq!(power_range(&gpus, &[500.0, 600.0]), (0.0, nice_ceil(600.0)));
}
#[test]
fn power_range_falls_back_to_nice_ceil_peak() {
let g = gpu_with(None, None, None, HashMap::new());
assert_eq!(
power_range(std::slice::from_ref(&g), &[120.0, 140.0, 158.0]),
(0.0, 200.0)
);
assert_eq!(
power_range(&[], &[2.0, 3.0]),
(0.0, nice_ceil(POWER_MIN_CEIL_W))
);
}
#[test]
fn power_range_ignores_nonpositive_limit() {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), "0".to_string());
let g = gpu_with(None, None, None, detail);
assert_eq!(
power_range(std::slice::from_ref(&g), &[40.0]),
(0.0, nice_ceil(40.0))
);
}
#[test]
fn power_range_ignores_non_finite_limit() {
for bogus in ["inf", "Inf", "infinity", "-inf", "NaN", "nan"] {
let mut detail = HashMap::new();
detail.insert("power_limit_current".to_string(), bogus.to_string());
let g = gpu_with(None, None, None, detail);
assert_eq!(
power_range(std::slice::from_ref(&g), &[40.0]),
(0.0, nice_ceil(40.0)),
"limit {bogus:?} should fall back to the peak"
);
}
}
#[test]
fn ane_range_floors_at_min_ceiling() {
assert_eq!(
ane_range(&[0.0, 0.5, 3.8]),
(0.0, nice_ceil(ANE_MIN_CEIL_W))
);
assert_eq!(ane_range(&[]), (0.0, nice_ceil(ANE_MIN_CEIL_W)));
assert_eq!(ane_range(&[2.0, 12.0]), (0.0, nice_ceil(12.0)));
}
#[test]
fn power_range_is_stable_under_window_shift() {
let g = gpu_with(None, None, None, HashMap::new());
let a = power_range(std::slice::from_ref(&g), &[280.0, 290.0]);
let b = power_range(std::slice::from_ref(&g), &[290.0, 295.0]);
assert_eq!(a, b);
assert_eq!(a, (0.0, 500.0));
}
#[test]
fn scale_badge_formats_without_decimals() {
assert_eq!(scale_badge(30.0, 83.0), "30-83");
assert_eq!(scale_badge(0.0, 350.0), "0-350");
assert_eq!(scale_badge(0.0, 100.0), "0-100");
}
#[test]
fn history_peak_ignores_non_finite() {
assert_eq!(history_peak(&[1.0, f64::NAN, 5.0, f64::INFINITY]), 5.0);
assert_eq!(history_peak(&[]), 0.0);
assert_eq!(history_peak(&[f64::NAN]), 0.0);
}
}