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}")
}
pub const PERCENT_SOFT_MIN_SPAN: f64 = 20.0;
pub const PERCENT_SOFT_GRID: f64 = 5.0;
pub const PERCENT_DOMAIN: (f64, f64) = (0.0, 100.0);
pub const TEMP_SOFT_MIN_SPAN: f64 = 10.0;
pub const TEMP_SOFT_GRID: f64 = 5.0;
pub const ANE_SOFT_MIN_SPAN: f64 = 2.0;
pub const ANE_SOFT_GRID: f64 = 1.0;
pub const POWER_SOFT_MIN_SPAN_FLOOR: f64 = 2.0;
pub const POWER_SOFT_MIN_SPAN_FRACTION: f64 = 0.2;
#[must_use]
pub fn power_soft_min_span(ceiling: f64) -> f64 {
(POWER_SOFT_MIN_SPAN_FRACTION * ceiling).max(POWER_SOFT_MIN_SPAN_FLOOR)
}
#[must_use]
pub fn power_soft_grid(ceiling: f64) -> f64 {
if ceiling <= 20.0 {
1.0
} else if ceiling <= 100.0 {
5.0
} else {
25.0
}
}
#[must_use]
pub fn soft_range(history: &[f64], min_span: f64, grid: f64, domain: (f64, f64)) -> (f64, f64) {
let (dlo, dhi) = domain;
let domain_valid = dlo.is_finite() && dhi.is_finite() && dhi > dlo;
let span = if min_span.is_finite() && min_span > 0.0 {
min_span
} else {
0.0
};
let use_grid = grid.is_finite() && grid > 0.0;
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &v in history {
if v.is_finite() {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
}
if !(lo.is_finite() && hi.is_finite()) {
let base = if dlo.is_finite() { dlo } else { 0.0 };
lo = base;
hi = base + span;
}
if domain_valid {
lo = lo.clamp(dlo, dhi);
hi = hi.clamp(dlo, dhi);
}
if hi - lo < span {
let center = (lo + hi) / 2.0;
lo = center - span / 2.0;
hi = center + span / 2.0;
if domain_valid {
if lo < dlo {
let shift = dlo - lo;
lo = dlo;
hi += shift;
}
if hi > dhi {
let shift = hi - dhi;
hi = dhi;
lo -= shift;
if lo < dlo {
lo = dlo;
}
}
}
}
if use_grid {
lo = (lo / grid).floor() * grid;
hi = (hi / grid).ceil() * grid;
}
if domain_valid {
lo = lo.max(dlo);
hi = hi.min(dhi);
}
if !(lo.is_finite() && hi.is_finite()) {
return (0.0, 1.0);
}
if hi < lo {
return (lo, lo);
}
(lo, hi)
}
#[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,
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,
}
}
#[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);
}
#[test]
fn soft_range_min_span_percent_near_constant() {
let (lo, hi) = soft_range(
&[41.3, 41.3, 41.3],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert!(hi - lo >= PERCENT_SOFT_MIN_SPAN);
assert_eq!((lo, hi), (30.0, 55.0));
}
#[test]
fn soft_range_min_span_hugging_zero() {
let (lo, hi) = soft_range(
&[0.0, 1.0, 2.0],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert_eq!((lo, hi), (0.0, 20.0));
assert!(hi - lo >= PERCENT_SOFT_MIN_SPAN);
}
#[test]
fn soft_range_min_span_hugging_hundred() {
let (lo, hi) = soft_range(
&[98.0, 99.0, 100.0],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert_eq!((lo, hi), (80.0, 100.0));
assert!(hi - lo >= PERCENT_SOFT_MIN_SPAN);
}
#[test]
fn soft_range_min_span_temperature() {
let (lo, hi) = soft_range(
&[49.5, 50.0, 50.5],
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, 100.0),
);
assert!(hi - lo >= TEMP_SOFT_MIN_SPAN);
assert_eq!((lo, hi), (45.0, 55.0));
}
#[test]
fn soft_range_min_span_ane() {
let (lo, hi) = soft_range(
&[0.0, 0.3, 0.5],
ANE_SOFT_MIN_SPAN,
ANE_SOFT_GRID,
(0.0, 10.0),
);
assert!(hi - lo >= ANE_SOFT_MIN_SPAN);
assert_eq!((lo, hi), (0.0, 2.0));
}
#[test]
fn power_soft_min_span_ladder() {
assert_eq!(power_soft_min_span(20.0), 4.0); assert_eq!(power_soft_min_span(5.0), 2.0); assert_eq!(power_soft_min_span(1000.0), 200.0);
assert_eq!(power_soft_min_span(f64::NAN), POWER_SOFT_MIN_SPAN_FLOOR);
}
#[test]
fn power_soft_grid_ladder() {
assert_eq!(power_soft_grid(10.0), 1.0);
assert_eq!(power_soft_grid(20.0), 1.0);
assert_eq!(power_soft_grid(80.0), 5.0);
assert_eq!(power_soft_grid(100.0), 5.0);
assert_eq!(power_soft_grid(700.0), 25.0);
assert_eq!(power_soft_grid(f64::NAN), 25.0);
}
#[test]
fn soft_range_min_span_power_apple_silicon() {
let ceiling = 20.0;
let (lo, hi) = soft_range(
&[12.5, 12.5],
power_soft_min_span(ceiling),
power_soft_grid(ceiling),
(0.0, ceiling),
);
assert!(hi - lo >= power_soft_min_span(ceiling));
assert_eq!((lo, hi), (10.0, 15.0));
}
#[test]
fn soft_range_grid_rounding_is_stable_under_jitter() {
let a = soft_range(&[41.0, 44.0, 48.0], 5.0, 5.0, (0.0, 100.0));
let b = soft_range(&[42.0, 45.0, 49.0], 5.0, 5.0, (0.0, 100.0));
assert_eq!(a, b);
assert_eq!(a, (40.0, 50.0));
}
#[test]
fn soft_range_clamps_to_percent_domain() {
let (lo, hi) = soft_range(
&[-5.0, 40.0, 130.0],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert!(lo >= 0.0 && hi <= 100.0);
assert_eq!((lo, hi), (0.0, 100.0));
}
#[test]
fn soft_range_clamps_to_temperature_domain() {
let (lo, hi) = soft_range(
&[85.0, 95.0],
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, 90.0),
);
assert!(hi <= 90.0);
assert_eq!((lo, hi), (80.0, 90.0));
}
#[test]
fn soft_range_clamps_to_power_domain() {
let ceiling = 100.0;
let (lo, hi) = soft_range(
&[95.0, 98.0],
power_soft_min_span(ceiling),
power_soft_grid(ceiling),
(0.0, ceiling),
);
assert!(hi <= ceiling);
assert_eq!((lo, hi), (80.0, 100.0));
}
#[test]
fn soft_range_empty_history_anchors_at_domain_floor() {
let (lo, hi) = soft_range(
&[],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert_eq!((lo, hi), (0.0, 20.0));
}
#[test]
fn soft_range_all_nan_history_anchors_at_domain_floor() {
let (lo, hi) = soft_range(
&[f64::NAN, f64::INFINITY, f64::NEG_INFINITY],
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, 90.0),
);
assert_eq!((lo, hi), (0.0, 10.0));
assert!(hi > lo);
}
#[test]
fn soft_range_min_span_wider_than_domain_returns_full_domain() {
let (lo, hi) = soft_range(&[40.0, 50.0], 200.0, 5.0, (0.0, 100.0));
assert_eq!((lo, hi), (0.0, 100.0));
}
#[test]
fn soft_range_non_positive_grid_skips_rounding() {
let (lo, hi) = soft_range(&[41.0, 42.0], PERCENT_SOFT_MIN_SPAN, 0.0, PERCENT_DOMAIN);
assert!(hi - lo >= PERCENT_SOFT_MIN_SPAN);
assert!(lo >= 0.0 && hi <= 100.0);
assert_eq!((lo, hi), (31.5, 51.5));
}
#[test]
fn soft_range_never_inverts_on_degenerate_domain() {
let (lo, hi) = soft_range(&[10.0, 20.0], 5.0, 5.0, (100.0, 0.0));
assert!(hi >= lo);
assert_eq!((lo, hi), (10.0, 20.0));
let (lo2, hi2) = soft_range(&[10.0, 20.0], 5.0, 5.0, (50.0, 50.0));
assert!(hi2 >= lo2);
}
#[test]
fn soft_range_non_finite_domain_degrades_gracefully() {
let (lo, hi) = soft_range(&[10.0, 20.0], 5.0, 5.0, (0.0, f64::INFINITY));
assert!(lo.is_finite() && hi.is_finite());
assert!(hi >= lo);
}
#[test]
fn soft_range_out_of_domain_data_stays_inside_domain() {
assert_eq!(
soft_range(&[95.0, 105.0], 10.0, 5.0, (0.0, 90.0)),
(80.0, 90.0)
);
assert_eq!(
soft_range(&[-20.0, -5.0], 10.0, 5.0, (0.0, 90.0)),
(0.0, 10.0)
);
assert_eq!(
soft_range(&[85.0, 105.0], 10.0, 5.0, (0.0, 90.0)),
(80.0, 90.0)
);
}
#[test]
fn scale_badge_shows_soft_range() {
let (lo, hi) = soft_range(
&[41.3, 41.3, 41.3, 41.3],
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
assert_eq!(scale_badge(lo, hi), "30-55");
}
}