use super::{MetricBuilder, MetricExporter};
use crate::device::GpuInfo;
use crate::device::types::MAX_GPU_FAN_RPM;
use crate::parsing::common::{sanitize_label_name, sanitize_label_value};
pub(crate) const FAN_SPEED_DETAIL_KEY: &str = "Fan Speed";
pub(crate) fn parse_fan_speed_detail(value: &str) -> Option<f64> {
let (rpm, _) = value.split_once(" RPM")?;
let rpm = rpm.trim().parse::<f64>().ok()?;
(rpm.is_finite() && rpm.fract() == 0.0 && (0.0..=f64::from(MAX_GPU_FAN_RPM)).contains(&rpm))
.then_some(rpm)
}
pub struct GpuMetricExporter<'a> {
pub gpu_info: &'a [GpuInfo],
}
impl<'a> GpuMetricExporter<'a> {
pub fn new(gpu_info: &'a [GpuInfo]) -> Self {
Self { gpu_info }
}
fn export_basic_metrics(&self, builder: &mut MetricBuilder, row: &GpuRow<'a>) {
let info = row.gpu;
let base_labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
];
if let Some(utilization) = info.utilization_reading() {
builder
.help(
"all_smi_gpu_utilization",
"GPU utilization percentage (omitted when the device reports no utilization)",
)
.type_("all_smi_gpu_utilization", "gauge")
.metric("all_smi_gpu_utilization", &base_labels, utilization);
}
builder
.help("all_smi_gpu_memory_used_bytes", "GPU memory used in bytes")
.type_("all_smi_gpu_memory_used_bytes", "gauge")
.metric(
"all_smi_gpu_memory_used_bytes",
&base_labels,
info.used_memory,
);
builder
.help(
"all_smi_gpu_memory_total_bytes",
"GPU memory total in bytes",
)
.type_("all_smi_gpu_memory_total_bytes", "gauge")
.metric(
"all_smi_gpu_memory_total_bytes",
&base_labels,
info.total_memory,
);
if let Some(temperature) = info.temperature_reading() {
builder
.help(
"all_smi_gpu_temperature_celsius",
"GPU temperature in celsius (omitted when no sensor reports one)",
)
.type_("all_smi_gpu_temperature_celsius", "gauge")
.metric("all_smi_gpu_temperature_celsius", &base_labels, temperature);
}
if let Some(power) = info.power_consumption_reading() {
builder
.help(
"all_smi_gpu_power_consumption_watts",
"GPU power consumption in watts (omitted when the device reports no power)",
)
.type_("all_smi_gpu_power_consumption_watts", "gauge")
.metric("all_smi_gpu_power_consumption_watts", &base_labels, power);
}
if let Some(frequency) = info.frequency_reading() {
builder
.help(
"all_smi_gpu_frequency_mhz",
"GPU frequency in MHz (omitted when the device exposes no clock probe)",
)
.type_("all_smi_gpu_frequency_mhz", "gauge")
.metric("all_smi_gpu_frequency_mhz", &base_labels, frequency);
}
if let Some(ane) = info.ane_utilization_reading() {
builder
.help(
"all_smi_ane_utilization",
"ANE utilization in mW (omitted when the native metrics source is unavailable)",
)
.type_("all_smi_ane_utilization", "gauge")
.metric("all_smi_ane_utilization", &base_labels, ane);
}
if let Some(dla_util) = info.dla_utilization {
builder
.help("all_smi_dla_utilization", "DLA utilization percentage")
.type_("all_smi_dla_utilization", "gauge")
.metric("all_smi_dla_utilization", &base_labels, dla_util);
}
}
fn export_apple_silicon_metrics(&self, builder: &mut MetricBuilder, row: &GpuRow<'a>) {
let info = row.gpu;
if !info.name.contains("Apple") && !info.name.contains("Metal") {
return;
}
let base_labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
];
if let Some(ane_mw) = info.ane_utilization_reading() {
builder
.help(
"all_smi_ane_power_watts",
"ANE power consumption in watts (omitted when the native metrics source is unavailable)",
)
.type_("all_smi_ane_power_watts", "gauge")
.metric("all_smi_ane_power_watts", &base_labels, ane_mw / 1000.0);
}
if let Some(thermal_level) = info.detail.get("thermal_pressure") {
let thermal_labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
("level", thermal_level.as_str()),
];
builder
.help("all_smi_thermal_pressure_info", "Thermal pressure level")
.type_("all_smi_thermal_pressure_info", "gauge")
.metric("all_smi_thermal_pressure_info", &thermal_labels, 1);
}
if let Some(combined_power_str) = info.detail.get("combined_power_mw")
&& let Ok(combined_power_mw) = combined_power_str.parse::<f64>()
{
let combined_power_watts = combined_power_mw / 1000.0;
builder
.help(
"all_smi_combined_power_watts",
"Combined power consumption (CPU + GPU + ANE) in watts",
)
.type_("all_smi_combined_power_watts", "gauge")
.metric(
"all_smi_combined_power_watts",
&base_labels,
combined_power_watts,
);
}
}
fn export_device_info(&self, builder: &mut MetricBuilder, row: &GpuRow<'a>) {
let info = row.gpu;
let labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
("type", info.device_type.as_str()),
];
let detail_labels: Vec<(String, String)> = info
.detail
.iter()
.map(|(k, v)| (sanitize_label_name(k), sanitize_label_value(v)))
.collect();
builder
.help("all_smi_gpu_info", "GPU/NPU device information")
.type_("all_smi_gpu_info", "gauge");
let mut all_labels = Vec::new();
for (key, value) in labels.iter() {
all_labels.push((*key, *value));
}
for (key, value) in &detail_labels {
all_labels.push((key.as_str(), value.as_str()));
}
builder.metric("all_smi_gpu_info", &all_labels, 1);
}
fn export_cuda_metrics(&self, builder: &mut MetricBuilder, row: &GpuRow<'a>) {
let info = row.gpu;
let base_labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
];
if let Some(pcie_gen) = info.detail.get("pcie_gen_current")
&& let Ok(pcie_gen_value) = pcie_gen.parse::<f64>()
{
builder
.help("all_smi_gpu_pcie_gen_current", "Current PCIe generation")
.type_("all_smi_gpu_pcie_gen_current", "gauge")
.metric("all_smi_gpu_pcie_gen_current", &base_labels, pcie_gen_value);
}
if let Some(pcie_width) = info.detail.get("pcie_width_current")
&& let Ok(width) = pcie_width.parse::<f64>()
{
builder
.help("all_smi_gpu_pcie_width_current", "Current PCIe link width")
.type_("all_smi_gpu_pcie_width_current", "gauge")
.metric("all_smi_gpu_pcie_width_current", &base_labels, width);
}
if let Some(clock_max) = info.detail.get("clock_graphics_max")
&& let Ok(clock) = clock_max.parse::<f64>()
{
builder
.help(
"all_smi_gpu_clock_graphics_max_mhz",
"Maximum graphics clock in MHz",
)
.type_("all_smi_gpu_clock_graphics_max_mhz", "gauge")
.metric("all_smi_gpu_clock_graphics_max_mhz", &base_labels, clock);
}
if let Some(clock_max) = info.detail.get("clock_memory_max")
&& let Ok(clock) = clock_max.parse::<f64>()
{
builder
.help(
"all_smi_gpu_clock_memory_max_mhz",
"Maximum memory clock in MHz",
)
.type_("all_smi_gpu_clock_memory_max_mhz", "gauge")
.metric("all_smi_gpu_clock_memory_max_mhz", &base_labels, clock);
}
if let Some(power_limit) = info.detail.get("power_limit_current")
&& let Ok(power) = power_limit.parse::<f64>()
{
builder
.help(
"all_smi_gpu_power_limit_current_watts",
"Current power limit in watts",
)
.type_("all_smi_gpu_power_limit_current_watts", "gauge")
.metric("all_smi_gpu_power_limit_current_watts", &base_labels, power);
}
if let Some(power_limit) = info.detail.get("power_limit_max")
&& let Ok(power) = power_limit.parse::<f64>()
{
builder
.help(
"all_smi_gpu_power_limit_max_watts",
"Maximum power limit in watts",
)
.type_("all_smi_gpu_power_limit_max_watts", "gauge")
.metric("all_smi_gpu_power_limit_max_watts", &base_labels, power);
}
if let Some(pstate) = info.performance_state {
builder
.help(
"all_smi_gpu_performance_state",
"GPU performance state (0=P0 fastest, 15=P15 idlest; metric is omitted when the device does not report a P-state)",
)
.type_("all_smi_gpu_performance_state", "gauge")
.metric("all_smi_gpu_performance_state", &base_labels, pstate as f64);
} else if let Some(pstate_str) = info.detail.get("performance_state")
&& let Some(state_str) = pstate_str.strip_prefix('P')
&& let Ok(state_num) = state_str.parse::<f64>()
{
builder
.help(
"all_smi_gpu_performance_state",
"GPU performance state (0=P0 fastest, 15=P15 idlest; metric is omitted when the device does not report a P-state)",
)
.type_("all_smi_gpu_performance_state", "gauge")
.metric("all_smi_gpu_performance_state", &base_labels, state_num);
}
if let Some(rpm) = info.fan_speed_rpm {
builder
.help(
"all_smi_gpu_fan_speed_rpm",
"GPU fan speed in revolutions per minute (metric is omitted when the device reports no tachometer)",
)
.type_("all_smi_gpu_fan_speed_rpm", "gauge")
.metric("all_smi_gpu_fan_speed_rpm", &base_labels, rpm);
} else if let Some(fan) = info.detail.get(FAN_SPEED_DETAIL_KEY)
&& let Some(rpm) = parse_fan_speed_detail(fan)
{
builder
.help(
"all_smi_gpu_fan_speed_rpm",
"GPU fan speed in revolutions per minute (metric is omitted when the device reports no tachometer)",
)
.type_("all_smi_gpu_fan_speed_rpm", "gauge")
.metric("all_smi_gpu_fan_speed_rpm", &base_labels, rpm);
}
}
fn export_thermal_thresholds(&self, builder: &mut MetricBuilder, row: &GpuRow<'a>) {
let info = row.gpu;
let base_labels = [
("gpu", info.name.as_str()),
("instance", info.instance.as_str()),
("gpu_uuid", info.uuid.as_str()),
("gpu_index", row.index_str.as_str()),
];
if let Some(slowdown) = info.temperature_threshold_slowdown {
builder
.help(
"all_smi_gpu_temperature_threshold_slowdown_celsius",
"GPU slowdown temperature threshold in Celsius",
)
.type_(
"all_smi_gpu_temperature_threshold_slowdown_celsius",
"gauge",
)
.metric(
"all_smi_gpu_temperature_threshold_slowdown_celsius",
&base_labels,
slowdown,
);
}
if let Some(shutdown) = info.temperature_threshold_shutdown {
builder
.help(
"all_smi_gpu_temperature_threshold_shutdown_celsius",
"GPU shutdown temperature threshold in Celsius",
)
.type_(
"all_smi_gpu_temperature_threshold_shutdown_celsius",
"gauge",
)
.metric(
"all_smi_gpu_temperature_threshold_shutdown_celsius",
&base_labels,
shutdown,
);
}
if let Some(gpu_max) = info.temperature_threshold_max_operating {
builder
.help(
"all_smi_gpu_temperature_threshold_max_operating_celsius",
"GPU maximum operating temperature threshold in Celsius",
)
.type_(
"all_smi_gpu_temperature_threshold_max_operating_celsius",
"gauge",
)
.metric(
"all_smi_gpu_temperature_threshold_max_operating_celsius",
&base_labels,
gpu_max,
);
}
if let Some(acoustic) = info.temperature_threshold_acoustic {
builder
.help(
"all_smi_gpu_temperature_threshold_acoustic_celsius",
"GPU acoustic (noise) temperature threshold in Celsius",
)
.type_(
"all_smi_gpu_temperature_threshold_acoustic_celsius",
"gauge",
)
.metric(
"all_smi_gpu_temperature_threshold_acoustic_celsius",
&base_labels,
acoustic,
);
}
}
fn collect_rows(&self) -> Vec<GpuRow<'a>> {
self.gpu_info
.iter()
.enumerate()
.filter(|(_, info)| {
info.device_type == "GPU" || info.device_type == "NPU" || info.device_type == "TPU"
})
.map(|(idx, gpu)| GpuRow {
gpu,
index_str: idx.to_string(),
})
.collect()
}
}
struct GpuRow<'a> {
gpu: &'a GpuInfo,
index_str: String,
}
impl<'a> MetricExporter for GpuMetricExporter<'a> {
fn export_metrics(&self) -> String {
let rows = self.collect_rows();
let mut builder = MetricBuilder::new();
for row in &rows {
self.export_basic_metrics(&mut builder, row);
self.export_apple_silicon_metrics(&mut builder, row);
self.export_device_info(&mut builder, row);
self.export_cuda_metrics(&mut builder, row);
self.export_thermal_thresholds(&mut builder, row);
}
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn make_nvidia_gpu() -> GpuInfo {
GpuInfo {
uuid: "GPU-ABC".to_string(),
time: String::new(),
name: "NVIDIA A100".to_string(),
device_type: "GPU".to_string(),
host_id: "node-1".to_string(),
hostname: "node-1".to_string(),
instance: "node-1".to_string(),
utilization: 50.0,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature: 70,
used_memory: 1024,
total_memory: 8192,
frequency: 1500,
power_consumption: 200.0,
gpu_core_count: None,
temperature_threshold_slowdown: Some(90),
temperature_threshold_shutdown: Some(95),
temperature_threshold_max_operating: Some(85),
temperature_threshold_acoustic: Some(77),
performance_state: Some(2),
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(),
}
}
#[test]
fn exporter_emits_all_new_threshold_metrics() {
let gpu = make_nvidia_gpu();
let gpus = vec![gpu];
let exporter = GpuMetricExporter::new(&gpus);
let output = exporter.export_metrics();
assert!(
output.contains("all_smi_gpu_temperature_threshold_slowdown_celsius{"),
"slowdown metric missing:\n{output}"
);
assert!(
output.contains("all_smi_gpu_temperature_threshold_shutdown_celsius{"),
"shutdown metric missing:\n{output}"
);
assert!(
output.contains("all_smi_gpu_temperature_threshold_max_operating_celsius{"),
"max_operating metric missing:\n{output}"
);
assert!(
output.contains("all_smi_gpu_temperature_threshold_acoustic_celsius{"),
"acoustic metric missing:\n{output}"
);
}
#[test]
fn exporter_sanitizes_dynamic_detail_label_names() {
let mut gpu = make_nvidia_gpu();
gpu.detail
.insert("Source: Fan".to_string(), "hwmon".to_string());
gpu.detail
.insert("3D Engine".to_string(), "busy".to_string());
gpu.detail
.insert("GPU.Temp/Limit".to_string(), "90".to_string());
let gpus = vec![gpu];
let output = GpuMetricExporter::new(&gpus).export_metrics();
let info_line = output
.lines()
.find(|line| line.starts_with("all_smi_gpu_info{"))
.expect("GPU info metric");
assert!(info_line.contains("source__fan=\"hwmon\""));
assert!(info_line.contains("_3d_engine=\"busy\""));
assert!(info_line.contains("gpu_temp_limit=\"90\""));
}
#[test]
fn exporter_emits_pstate_from_structured_field() {
let gpu = make_nvidia_gpu();
let gpus = vec![gpu];
let output = GpuMetricExporter::new(&gpus).export_metrics();
assert!(
output.contains("all_smi_gpu_performance_state{"),
"pstate metric missing:\n{output}"
);
let pstate_line = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_performance_state{"))
.expect("pstate line");
assert!(
pstate_line.ends_with(" 2"),
"expected P2, got {pstate_line}"
);
}
#[test]
fn fan_speed_detail_parser_accepts_every_reader_value_shape() {
assert_eq!(parse_fan_speed_detail("1450 RPM"), Some(1450.0));
assert_eq!(parse_fan_speed_detail("1600 RPM (40%)"), Some(1600.0));
assert_eq!(parse_fan_speed_detail("40%"), None);
assert_eq!(parse_fan_speed_detail("unknown RPM"), None);
assert_eq!(parse_fan_speed_detail("-1 RPM"), None);
assert_eq!(parse_fan_speed_detail(""), None);
}
#[test]
fn fan_speed_detail_parser_rejects_fractional_and_out_of_range_values() {
assert_eq!(parse_fan_speed_detail("1450.5 RPM"), None);
assert_eq!(parse_fan_speed_detail("1600.25 RPM (40%)"), None);
assert_eq!(parse_fan_speed_detail("4294967295 RPM"), None);
assert_eq!(
parse_fan_speed_detail(&format!("{} RPM", MAX_GPU_FAN_RPM as u64 + 1)),
None
);
assert_eq!(
parse_fan_speed_detail(&format!("{MAX_GPU_FAN_RPM} RPM")),
Some(f64::from(MAX_GPU_FAN_RPM))
);
}
#[test]
fn exporter_emits_fan_speed_from_structured_field() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = Some(1450);
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
let line = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_fan_speed_rpm{"))
.unwrap_or_else(|| panic!("fan speed metric missing:\n{output}"));
assert!(line.ends_with(" 1450"), "expected 1450 RPM, got {line}");
assert!(line.contains("gpu=\"NVIDIA A100\""));
assert!(line.contains("instance=\"node-1\""));
assert!(line.contains("gpu_uuid=\"GPU-ABC\""));
assert!(line.contains("gpu_index=\"0\""));
}
#[test]
fn exporter_omits_fan_speed_when_device_reports_none() {
let gpu = make_nvidia_gpu();
assert!(gpu.fan_speed_rpm.is_none());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
assert!(
!output.contains("all_smi_gpu_fan_speed_rpm"),
"fan speed must be omitted without data:\n{output}"
);
}
#[test]
fn exporter_emits_fan_speed_from_detail_fallback() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = None;
gpu.detail
.insert("Fan Speed".to_string(), "1450 RPM".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
let line = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_fan_speed_rpm{"))
.unwrap_or_else(|| panic!("fan speed fallback missing:\n{output}"));
assert!(line.ends_with(" 1450"), "expected 1450 RPM, got {line}");
}
#[test]
fn exporter_prefers_structured_fan_speed_over_the_detail_string() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = Some(1450);
gpu.detail
.insert("Fan Speed".to_string(), "9999 RPM".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
let lines: Vec<&str> = output
.lines()
.filter(|l| l.starts_with("all_smi_gpu_fan_speed_rpm{"))
.collect();
assert_eq!(lines.len(), 1, "exactly one sample per device: {lines:?}");
assert!(lines[0].ends_with(" 1450"), "{}", lines[0]);
}
#[test]
fn exporter_omits_fan_speed_for_a_duty_cycle_only_detail() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = None;
gpu.detail
.insert("Fan Speed".to_string(), "40%".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
assert!(
!output.contains("all_smi_gpu_fan_speed_rpm"),
"a duty cycle is not an RPM reading:\n{output}"
);
}
#[test]
fn exporter_omits_fan_speed_for_a_fractional_detail_value() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = None;
gpu.detail
.insert("Fan Speed".to_string(), "1450.5 RPM".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
assert!(
!output.contains("all_smi_gpu_fan_speed_rpm"),
"a fractional detail value must not reach the exposition:\n{output}"
);
}
#[test]
fn exporter_omits_fan_speed_for_an_out_of_range_detail_value() {
let mut gpu = make_nvidia_gpu();
gpu.fan_speed_rpm = None;
gpu.detail
.insert("Fan Speed".to_string(), "4294967295 RPM".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
assert!(
!output.contains("all_smi_gpu_fan_speed_rpm"),
"an out-of-range detail value must not reach the exposition:\n{output}"
);
}
#[test]
fn exporter_skips_thresholds_when_none_present() {
let mut gpu = make_nvidia_gpu();
gpu.temperature_threshold_slowdown = None;
gpu.temperature_threshold_shutdown = None;
gpu.temperature_threshold_max_operating = None;
gpu.temperature_threshold_acoustic = None;
gpu.performance_state = None;
let gpus = vec![gpu];
let output = GpuMetricExporter::new(&gpus).export_metrics();
assert!(
!output.contains("all_smi_gpu_temperature_threshold_"),
"should not emit threshold metrics without data:\n{output}"
);
assert!(
!output.contains("all_smi_gpu_performance_state{"),
"should not emit pstate metric without data:\n{output}"
);
}
#[test]
fn exporter_emits_only_available_thresholds() {
let mut gpu = make_nvidia_gpu();
gpu.temperature_threshold_max_operating = None;
gpu.temperature_threshold_acoustic = None;
let gpus = vec![gpu];
let output = GpuMetricExporter::new(&gpus).export_metrics();
assert!(output.contains("all_smi_gpu_temperature_threshold_slowdown_celsius"));
assert!(output.contains("all_smi_gpu_temperature_threshold_shutdown_celsius"));
assert!(!output.contains("all_smi_gpu_temperature_threshold_max_operating_celsius"));
assert!(!output.contains("all_smi_gpu_temperature_threshold_acoustic_celsius"));
}
#[test]
fn exporter_omits_series_with_no_reading() {
use crate::device::types::GPU_METRIC_UNAVAILABLE;
let mut gpu = make_nvidia_gpu();
gpu.name = "Apple M2 Max GPU".to_string();
gpu.utilization = GPU_METRIC_UNAVAILABLE;
gpu.power_consumption = GPU_METRIC_UNAVAILABLE;
gpu.ane_utilization = GPU_METRIC_UNAVAILABLE;
gpu.temperature = 0;
gpu.frequency = 0;
gpu.detail
.insert("native_metrics".to_string(), "unavailable".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
for family in [
"all_smi_gpu_utilization{",
"all_smi_gpu_power_consumption_watts{",
"all_smi_gpu_temperature_celsius{",
"all_smi_gpu_frequency_mhz{",
"all_smi_ane_utilization{",
"all_smi_ane_power_watts{",
] {
assert!(
!output.contains(family),
"{family} must be omitted, not emitted as 0:\n{output}"
);
}
assert!(
!output.contains(" -1"),
"the in-band unavailable encoding leaked into the exposition:\n{output}"
);
let info_line = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_info{"))
.expect("identity series must survive");
assert!(
info_line.contains("native_metrics=\"unavailable\""),
"{info_line}"
);
assert!(output.contains("all_smi_gpu_memory_total_bytes{"));
}
#[test]
fn exporter_omits_unsourced_windows_utilization_and_power() {
use crate::device::types::GPU_METRIC_UNAVAILABLE;
let mut gpu = make_nvidia_gpu();
gpu.name = "AMD Radeon Graphics".to_string();
gpu.utilization = GPU_METRIC_UNAVAILABLE;
gpu.power_consumption = GPU_METRIC_UNAVAILABLE;
gpu.detail
.insert("Source: Utilization".to_string(), "unavailable".to_string());
gpu.detail
.insert("Source: Power".to_string(), "unavailable".to_string());
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
assert!(!output.contains("all_smi_gpu_utilization{"), "{output}");
assert!(
!output.contains("all_smi_gpu_power_consumption_watts{"),
"{output}"
);
assert!(output.contains("all_smi_gpu_info{"), "{output}");
assert!(output.contains("source__utilization=\"unavailable\""));
assert!(output.contains("source__power=\"unavailable\""));
}
#[test]
fn exporter_emits_genuine_zero_readings() {
let mut gpu = make_nvidia_gpu();
gpu.utilization = 0.0;
gpu.power_consumption = 0.0;
gpu.temperature = 42;
gpu.frequency = 300;
let output = GpuMetricExporter::new(&[gpu]).export_metrics();
let util = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_utilization{"))
.expect("idle GPU must still report 0% utilization");
assert!(util.ends_with(" 0"), "expected a zero reading, got {util}");
let power = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_power_consumption_watts{"))
.expect("a 0 W rail is a reading");
assert!(power.ends_with(" 0"), "{power}");
}
#[test]
fn exporter_keeps_emitting_zero_ane_for_non_apple_gpus() {
let output = GpuMetricExporter::new(&[make_nvidia_gpu()]).export_metrics();
assert!(output.contains("all_smi_ane_utilization{"));
assert!(!output.contains("all_smi_ane_power_watts{"));
}
#[test]
fn exporter_preserves_standard_gpu_labels_on_new_metrics() {
let gpu = make_nvidia_gpu();
let gpus = vec![gpu];
let output = GpuMetricExporter::new(&gpus).export_metrics();
let slowdown_line = output
.lines()
.find(|l| l.starts_with("all_smi_gpu_temperature_threshold_slowdown_celsius{"))
.expect("slowdown line");
assert!(slowdown_line.contains("gpu=\"NVIDIA A100\""));
assert!(slowdown_line.contains("instance=\"node-1\""));
assert!(slowdown_line.contains("gpu_uuid=\"GPU-ABC\""));
assert!(slowdown_line.contains("gpu_index=\"0\""));
}
}