use std::collections::HashMap;
use serde_json::Value;
use crate::device::GpuInfo;
pub const ROCM_SMI_COMMAND: &str =
"rocm-smi --showuse --showmemuse --showtemp --showpower --showproductname --json";
#[derive(Debug, thiserror::Error)]
pub enum RocmSmiParseError {
#[error("rocm-smi JSON parse error: {0}")]
Json(String),
#[error("rocm-smi JSON top-level value is not an object")]
NotObject,
}
pub fn parse_rocm_smi_json(
json: &str,
host_id: &str,
hostname: &str,
timestamp: &str,
) -> Result<Vec<GpuInfo>, RocmSmiParseError> {
let v: Value =
serde_json::from_str(json).map_err(|e| RocmSmiParseError::Json(e.to_string()))?;
let obj = v.as_object().ok_or(RocmSmiParseError::NotObject)?;
let driver_version = obj
.get("system")
.and_then(|s| s.as_object())
.and_then(|s| s.get("Driver version"))
.and_then(|v| v.as_str())
.map(str::to_string);
let mut cards: Vec<(u32, &Value)> = obj
.iter()
.filter_map(|(k, v)| {
k.strip_prefix("card")
.and_then(|n| n.parse::<u32>().ok())
.map(|idx| (idx, v))
})
.collect();
cards.sort_by_key(|(i, _)| *i);
let mut out = Vec::with_capacity(cards.len());
for (idx, card_val) in cards {
let Some(card) = card_val.as_object() else {
continue;
};
let gpu = parse_card(
idx,
card,
driver_version.as_deref(),
host_id,
hostname,
timestamp,
);
out.push(gpu);
}
Ok(out)
}
fn parse_card(
idx: u32,
card: &serde_json::Map<String, Value>,
driver_version: Option<&str>,
host_id: &str,
hostname: &str,
timestamp: &str,
) -> GpuInfo {
let utilization = lookup_f64(
card,
&[
"GPU use (%)",
"GPU use %",
"GPU Utilization",
"GPU use (%) sum",
],
)
.unwrap_or(0.0);
let memory_utilization_pct = lookup_f64(
card,
&[
"GPU Memory Allocated (VRAM%)",
"GPU memory use (%)",
"Memory use (%)",
],
);
let total_memory_bytes = lookup_u64(
card,
&[
"VRAM Total Memory (B)",
"GPU memory total (B)",
"Total memory (B)",
],
)
.unwrap_or(0);
let used_memory_bytes = lookup_u64(
card,
&[
"VRAM Total Used Memory (B)",
"GPU memory used (B)",
"Used memory (B)",
],
)
.unwrap_or_else(|| {
if let Some(pct) = memory_utilization_pct {
((pct / 100.0) * total_memory_bytes as f64) as u64
} else {
0
}
});
let temperature = lookup_f64(
card,
&[
"Temperature (Sensor edge) (C)",
"Temperature (Sensor junction) (C)",
"Temperature (Sensor memory) (C)",
"Temperature (C)",
],
)
.unwrap_or(0.0) as u32;
let power_consumption = lookup_f64(
card,
&[
"Average Graphics Package Power (W)",
"Current Socket Graphics Package Power (W)",
"GPU Power (W)",
"Power (W)",
],
)
.unwrap_or(0.0);
let name = card
.get("Card series")
.or_else(|| card.get("Card model"))
.or_else(|| card.get("GFX Version"))
.and_then(|v| v.as_str())
.unwrap_or("AMD GPU")
.to_string();
let uuid = card
.get("Unique ID")
.or_else(|| card.get("GUID"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty() && !s.eq_ignore_ascii_case("n/a"))
.map(str::to_string)
.unwrap_or_else(|| format!("rocm-{hostname}-{idx}"));
let mut detail: HashMap<String, String> = HashMap::new();
if let Some(dv) = driver_version {
detail.insert("driver_version".to_string(), dv.to_string());
}
detail.insert("transport".to_string(), "ssh/rocm-smi".to_string());
GpuInfo {
uuid,
time: timestamp.to_string(),
name,
device_type: "GPU".to_string(),
host_id: host_id.to_string(),
hostname: hostname.to_string(),
instance: format!("{hostname}:{idx}"),
utilization,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature,
used_memory: used_memory_bytes,
total_memory: total_memory_bytes,
frequency: 0,
power_consumption,
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,
}
}
fn lookup_f64(card: &serde_json::Map<String, Value>, keys: &[&str]) -> Option<f64> {
for key in keys {
if let Some(val) = card.get(*key) {
if let Some(n) = val.as_f64() {
return Some(n);
}
if let Some(s) = val.as_str()
&& let Ok(n) = s.trim().parse::<f64>()
{
return Some(n);
}
}
}
None
}
fn lookup_u64(card: &serde_json::Map<String, Value>, keys: &[&str]) -> Option<u64> {
for key in keys {
if let Some(val) = card.get(*key) {
if let Some(n) = val.as_u64() {
return Some(n);
}
if let Some(s) = val.as_str()
&& let Ok(n) = s.trim().parse::<u64>()
{
return Some(n);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const GOLDEN_ROCM6: &str = r#"{
"card0": {
"GPU use (%)": "68",
"GPU Memory Allocated (VRAM%)": "42",
"VRAM Total Memory (B)": "68719476736",
"VRAM Total Used Memory (B)": "28823961600",
"Temperature (Sensor edge) (C)": "72.0",
"Temperature (Sensor junction) (C)": "78.0",
"Average Graphics Package Power (W)": "235.5",
"Card series": "Instinct MI250X",
"Unique ID": "0x1234abcd"
},
"card1": {
"GPU use (%)": "3",
"GPU Memory Allocated (VRAM%)": "5",
"VRAM Total Memory (B)": "68719476736",
"VRAM Total Used Memory (B)": "3435973836",
"Temperature (Sensor edge) (C)": "45.0",
"Average Graphics Package Power (W)": "95.2",
"Card series": "Instinct MI250X",
"Unique ID": "0xbeefcafe"
},
"system": {
"Driver version": "6.1.1"
}
}"#;
#[test]
fn parses_two_card_rocm6_sample() {
let gpus =
parse_rocm_smi_json(GOLDEN_ROCM6, "admin@amd01", "amd01", "2026-04-20T00:00:00Z")
.expect("valid golden sample must parse");
assert_eq!(gpus.len(), 2);
let g0 = &gpus[0];
assert_eq!(g0.uuid, "0x1234abcd");
assert_eq!(g0.name, "Instinct MI250X");
assert_eq!(g0.utilization, 68.0);
assert_eq!(g0.total_memory, 68719476736);
assert_eq!(g0.used_memory, 28823961600);
assert_eq!(g0.temperature, 72);
assert!((g0.power_consumption - 235.5).abs() < 1e-6);
assert_eq!(g0.hostname, "amd01");
assert_eq!(g0.host_id, "admin@amd01");
assert_eq!(g0.instance, "amd01:0");
assert_eq!(g0.detail.get("driver_version"), Some(&"6.1.1".to_string()));
assert_eq!(
g0.detail.get("transport"),
Some(&"ssh/rocm-smi".to_string())
);
let g1 = &gpus[1];
assert_eq!(g1.uuid, "0xbeefcafe");
assert_eq!(g1.instance, "amd01:1");
}
#[test]
fn parses_rocm5_alternate_key_names() {
let csv = r#"{
"card0": {
"GPU use %": 22.5,
"Used memory (B)": 12345,
"Total memory (B)": 68719476736,
"Temperature (C)": 60,
"Power (W)": 150
}
}"#;
let gpus = parse_rocm_smi_json(csv, "u@h", "h", "t").unwrap();
assert_eq!(gpus.len(), 1);
assert_eq!(gpus[0].utilization, 22.5);
assert_eq!(gpus[0].total_memory, 68719476736);
assert_eq!(gpus[0].used_memory, 12345);
assert_eq!(gpus[0].temperature, 60);
assert!((gpus[0].power_consumption - 150.0).abs() < 1e-6);
assert_eq!(gpus[0].uuid, "rocm-h-0");
assert_eq!(gpus[0].name, "AMD GPU");
}
#[test]
fn empty_object_yields_empty_vec() {
let gpus = parse_rocm_smi_json("{}", "u@h", "h", "t").unwrap();
assert!(gpus.is_empty());
}
#[test]
fn rejects_non_json_input() {
let err = parse_rocm_smi_json("not-json", "u@h", "h", "t").unwrap_err();
assert!(matches!(err, RocmSmiParseError::Json(_)));
}
#[test]
fn rejects_non_object_root() {
let err = parse_rocm_smi_json("[]", "u@h", "h", "t").unwrap_err();
assert!(matches!(err, RocmSmiParseError::NotObject));
}
#[test]
fn ignores_non_card_keys() {
let json = r#"{
"card0": { "GPU use (%)": "10" },
"some other key": { "x": "y" }
}"#;
let gpus = parse_rocm_smi_json(json, "u@h", "h", "t").unwrap();
assert_eq!(gpus.len(), 1);
}
#[test]
fn derives_used_memory_from_percent() {
let json = r#"{
"card0": {
"GPU Memory Allocated (VRAM%)": "50",
"VRAM Total Memory (B)": "1000000"
}
}"#;
let gpus = parse_rocm_smi_json(json, "u@h", "h", "t").unwrap();
assert_eq!(gpus[0].used_memory, 500_000);
}
}