#[cfg(feature = "cli")]
use crate::common::paths::cache_dir;
#[cfg(feature = "cli")]
use crate::common::secure_write::write_atomic_secure;
use crate::device::GpuReader;
use crate::device::readers::common_cache::{DeviceStaticInfo, MAX_DEVICES};
use crate::device::readers::intel_gpu_engine::{
EngineState, apply_engine_readout, refresh_with_lock,
};
use crate::device::readers::intel_gpu_fdinfo::{
build_intel_drm_basenames, build_intel_process_infos, sum_vram_by_card_from_fdinfo,
};
use crate::device::readers::intel_gpu_gtidle::{
GtidleState, apply_fallback as apply_gtidle_fallback,
};
use crate::device::readers::intel_gpu_names::{
classify_intel_architecture, resolve_intel_gpu_name,
};
use crate::device::readers::intel_gpu_sysfs::{
MemoryVariant, has_nonzero_u64, read_energy_uj, read_fan_rpm, read_frequency_mhz,
read_memory_bytes, read_power_watts, read_resource2_total_bytes, read_temperature_celsius,
};
use crate::device::types::{GpuInfo, ProcessInfo};
use crate::utils::get_hostname;
use chrono::Local;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
const MAX_GPU_POWER_WATTS: f64 = 750.0; const MAX_GPU_TEMP_CELSIUS: u32 = 125; const MAX_GPU_FREQ_MHZ: u32 = 5000;
const MAX_GPU_MEMORY_BYTES: u64 = 96 * 1024 * 1024 * 1024; const MAX_GPU_UTILIZATION: f64 = 100.0;
const ENERGY_CACHE_WRITE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
struct IntelGpuCard {
index: u32,
card_path: PathBuf,
driver: String,
device_id: u32,
variant: MemoryVariant,
static_info: OnceLock<DeviceStaticInfo>,
engine_state: Mutex<EngineState>,
gtidle_state: Mutex<GtidleState>,
energy_state: Mutex<EnergyState>,
#[cfg(feature = "level_zero")]
level_zero_state: Mutex<crate::device::readers::intel_gpu_level_zero::LevelZeroState>,
}
fn variant_label(variant: MemoryVariant) -> &'static str {
match variant {
MemoryVariant::Discrete => "Discrete",
MemoryVariant::Integrated => "Integrated",
}
}
struct EnergyState {
timestamp: Option<std::time::Instant>,
energy_uj: u64,
last_cache_write: Option<std::time::Instant>,
}
fn compute_power_from_energy(
state: &Mutex<EnergyState>,
device_dir: &Path,
cache_path: Option<&Path>,
) -> f64 {
let current_uj = read_energy_uj(device_dir);
if current_uj == 0 {
return 0.0;
}
let now_instant = std::time::Instant::now();
let now_unix_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0);
let mut guard = match state.lock() {
Ok(g) => g,
Err(_) => return 0.0,
};
let power = match guard.timestamp {
Some(prev_ts) => {
let delta_s = now_instant.duration_since(prev_ts).as_secs_f64();
if delta_s > 0.0 {
let delta_j = current_uj.saturating_sub(guard.energy_uj) as f64 / 1_000_000.0;
delta_j / delta_s
} else {
0.0
}
}
None => match cache_path.and_then(read_energy_cache) {
Some((cached_ts_ms, cached_uj)) => {
let delta_s = now_unix_ms.saturating_sub(cached_ts_ms) as f64 / 1000.0;
if delta_s > 0.0 && delta_s < 3600.0 {
let delta_j = current_uj.saturating_sub(cached_uj) as f64 / 1_000_000.0;
delta_j / delta_s
} else {
0.0
}
}
None => 0.0,
},
};
let should_write_cache = match guard.last_cache_write {
None => true,
Some(last) => now_instant.duration_since(last) >= ENERGY_CACHE_WRITE_INTERVAL,
};
guard.timestamp = Some(now_instant);
guard.energy_uj = current_uj;
if should_write_cache && let Some(path) = cache_path {
write_energy_cache(path, now_unix_ms, current_uj);
guard.last_cache_write = Some(now_instant);
}
power
}
#[cfg(feature = "cli")]
fn energy_cache_path(device_dir: &Path) -> Option<PathBuf> {
let pci = std::fs::canonicalize(device_dir)
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "unknown".to_string());
cache_dir().map(|d| d.join(format!("energy-hwmon-{pci}")))
}
#[cfg(feature = "cli")]
fn read_energy_cache(path: &Path) -> Option<(u64, u64)> {
let meta = std::fs::symlink_metadata(path).ok()?;
if meta.file_type().is_symlink() {
return None;
}
let content = std::fs::read_to_string(path).ok()?;
let mut parts = content.split_whitespace();
let ts_ms: u64 = parts.next()?.parse().ok()?;
let uj: u64 = parts.next()?.parse().ok()?;
Some((ts_ms, uj))
}
#[cfg(feature = "cli")]
fn write_energy_cache(path: &Path, ts_ms: u64, uj: u64) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = write_atomic_secure(path, format!("{ts_ms} {uj}").as_bytes());
}
#[cfg(not(feature = "cli"))]
fn energy_cache_path(_device_dir: &Path) -> Option<PathBuf> {
None
}
#[cfg(not(feature = "cli"))]
fn read_energy_cache(_path: &Path) -> Option<(u64, u64)> {
None
}
#[cfg(not(feature = "cli"))]
fn write_energy_cache(_path: &Path, _ts_ms: u64, _uj: u64) {}
pub struct IntelGpuReader {
cards: Vec<IntelGpuCard>,
intel_drm_basenames: HashMap<String, usize>,
proc_root: PathBuf,
}
impl Default for IntelGpuReader {
fn default() -> Self {
Self::new()
}
}
impl IntelGpuReader {
pub fn new() -> Self {
Self::new_with_roots(Path::new("/sys/class/drm"), Path::new("/proc"))
}
#[cfg(test)]
fn new_from_root(drm_root: &Path) -> Self {
Self::new_with_roots(drm_root, Path::new("/proc"))
}
fn new_with_roots(drm_root: &Path, proc_root: &Path) -> Self {
let cards = discover_cards(drm_root);
let card_refs: Vec<(PathBuf, usize)> = cards
.iter()
.enumerate()
.map(|(i, c)| (c.card_path.clone(), i))
.collect();
let intel_drm_basenames = build_intel_drm_basenames(&card_refs, drm_root);
Self {
cards,
intel_drm_basenames,
proc_root: proc_root.to_path_buf(),
}
}
fn ensure_static_info<'a>(&self, card: &'a IntelGpuCard) -> &'a DeviceStaticInfo {
card.static_info.get_or_init(|| {
let device_dir = card.card_path.join("device");
let name = resolve_device_name(&device_dir, card.device_id);
let mut detail = HashMap::new();
detail.insert("Device ID".to_string(), format!("{:#06x}", card.device_id));
detail.insert(
"Variant".to_string(),
variant_label(card.variant).to_string(),
);
if !card.driver.is_empty() {
detail.insert("Driver".to_string(), card.driver.clone());
}
if let Some(bus) = read_pci_bus_id(&device_dir) {
detail.insert("PCI Bus".to_string(), bus);
}
let arch = classify_intel_architecture(&name);
detail.insert("Architecture".to_string(), arch.label().to_string());
detail.insert(
"SYCL Capable".to_string(),
arch.sycl_capable_label().to_string(),
);
if card.variant == MemoryVariant::Integrated {
detail.insert(
"Memory".to_string(),
"Shared system memory (no dedicated VRAM)".to_string(),
);
}
detail.insert(
"Metrics Source".to_string(),
"sysfs (engine counters)".to_string(),
);
DeviceStaticInfo::with_details(name, None, detail)
})
}
}
impl GpuReader for IntelGpuReader {
fn get_gpu_info(&self) -> Vec<GpuInfo> {
let hostname = get_hostname();
let time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let mut out = Vec::with_capacity(self.cards.len());
let mut fdinfo_vram_by_card: Option<HashMap<usize, u64>> = None;
for (card_index, card) in self.cards.iter().enumerate() {
let static_info = self.ensure_static_info(card);
let mut detail = static_info.detail.clone();
let device_dir = card.card_path.join("device");
let (mut used_memory, total_memory) = read_memory_bytes(&device_dir, card.variant);
let total_from_bar2 = card.driver == "xe"
&& total_memory > 0
&& !has_nonzero_u64(&device_dir.join("tile0").join("vram0").join("total_bytes"));
let frequency = read_frequency_mhz(&device_dir);
let temperature = read_temperature_celsius(&device_dir);
let mut power_consumption = read_power_watts(&device_dir);
let fan_rpm = read_fan_rpm(&device_dir);
let temperature = temperature.min(MAX_GPU_TEMP_CELSIUS);
let frequency = frequency.min(MAX_GPU_FREQ_MHZ);
power_consumption = power_consumption.clamp(0.0, MAX_GPU_POWER_WATTS);
let total_memory = total_memory.min(MAX_GPU_MEMORY_BYTES);
used_memory = used_memory.min(total_memory);
sources::decorate_static_sources(
&mut detail,
total_memory,
temperature,
power_consumption,
frequency,
fan_rpm,
);
if total_from_bar2 {
detail.insert(
"Source: Memory".to_string(),
"PCI BAR2 size (xe)".to_string(),
);
}
if power_consumption == 0.0 && card.driver == "xe" {
let cache_path = energy_cache_path(&device_dir);
let derived = compute_power_from_energy(
&card.energy_state,
&device_dir,
cache_path.as_deref(),
)
.clamp(0.0, MAX_GPU_POWER_WATTS);
if derived > 0.0 {
power_consumption = derived;
detail.insert("Source: Power".to_string(), "energy delta (xe)".to_string());
}
}
if used_memory == 0 && total_memory > 0 && card.driver == "xe" {
let vram_by_card = fdinfo_vram_by_card.get_or_insert_with(|| {
sum_vram_by_card_from_fdinfo(&self.intel_drm_basenames, &self.proc_root)
});
if let Some(&fdinfo_vram) = vram_by_card.get(&card_index)
&& fdinfo_vram > 0
{
used_memory = fdinfo_vram.min(total_memory);
detail.insert(
"Source: Memory".to_string(),
if total_from_bar2 {
"fdinfo used, PCI BAR2 total (xe)".to_string()
} else {
"fdinfo (xe)".to_string()
},
);
}
}
let readout = refresh_with_lock(&card.engine_state, &device_dir);
let mut utilization = readout.primary_utilization.clamp(0.0, MAX_GPU_UTILIZATION);
apply_engine_readout(&mut detail, &readout);
sources::decorate_utilization_source(&mut detail, &readout);
apply_gtidle_fallback(
&card.driver,
&readout,
&card.gtidle_state,
&device_dir,
&mut detail,
&mut utilization,
);
let uuid = build_uuid(card, &device_dir);
out.push(GpuInfo {
uuid,
time: time.clone(),
name: static_info.name.clone(),
device_type: "GPU".to_string(),
host_id: hostname.clone(),
hostname: hostname.clone(),
instance: hostname.clone(),
utilization,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature,
used_memory,
total_memory,
frequency,
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,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail,
});
#[cfg(feature = "level_zero")]
level_zero_glue::augment(card, &mut out, &device_dir);
}
out
}
fn get_process_info(&self) -> Vec<ProcessInfo> {
let mut card_uuids = HashMap::with_capacity(self.cards.len());
for (idx, card) in self.cards.iter().enumerate() {
card_uuids.insert(idx, build_uuid(card, &card.card_path.join("device")));
}
build_intel_process_infos(&self.intel_drm_basenames, &card_uuids, &self.proc_root)
}
}
fn discover_cards(drm_root: &Path) -> Vec<IntelGpuCard> {
let entries = match std::fs::read_dir(drm_root) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut cards = Vec::new();
for entry in entries.flatten() {
if cards.len() >= MAX_DEVICES {
break;
}
let path = entry.path();
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if !is_card_node(&name) {
continue;
}
let device_dir = path.join("device");
if !is_intel_vendor(&device_dir) {
continue;
}
let driver = resolve_driver(&device_dir);
if driver != "i915" && driver != "xe" {
continue;
}
let device_id = read_device_id(&device_dir).unwrap_or(0);
let variant = classify_variant(&device_dir, &driver);
let index = parse_card_index(&name);
cards.push(IntelGpuCard {
index,
card_path: path,
driver,
device_id,
variant,
static_info: OnceLock::new(),
engine_state: Mutex::new(EngineState::empty()),
gtidle_state: Mutex::new(GtidleState::empty()),
energy_state: Mutex::new(EnergyState {
timestamp: None,
energy_uj: 0,
last_cache_write: None,
}),
#[cfg(feature = "level_zero")]
level_zero_state: Mutex::new(
crate::device::readers::intel_gpu_level_zero::LevelZeroState::empty(),
),
});
}
cards.sort_by_key(|c| c.index);
cards
}
fn is_card_node(name: &str) -> bool {
if let Some(rest) = name.strip_prefix("card") {
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
fn parse_card_index(name: &str) -> u32 {
name.strip_prefix("card")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0)
}
fn is_intel_vendor(device_dir: &Path) -> bool {
match std::fs::read_to_string(device_dir.join("vendor")) {
Ok(s) => s.trim().eq_ignore_ascii_case("0x8086"),
Err(_) => false,
}
}
fn resolve_driver(device_dir: &Path) -> String {
match std::fs::read_link(device_dir.join("driver")) {
Ok(target) => target
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string(),
Err(_) => String::new(),
}
}
fn read_device_id(device_dir: &Path) -> Option<u32> {
let s = std::fs::read_to_string(device_dir.join("device")).ok()?;
parse_hex_u32(s.trim())
}
fn parse_hex_u32(s: &str) -> Option<u32> {
let stripped = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
u32::from_str_radix(stripped, 16).ok()
}
fn read_pci_bus_id(device_dir: &Path) -> Option<String> {
let link = std::fs::read_link(device_dir).ok()?;
link.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
}
fn build_uuid(card: &IntelGpuCard, device_dir: &Path) -> String {
if let Some(bus) = read_pci_bus_id(device_dir) {
format!("Intel-GPU-{bus}")
} else {
format!("Intel-GPU-card{}", card.index)
}
}
fn classify_variant(device_dir: &Path, driver: &str) -> MemoryVariant {
if has_nonzero_u64(&device_dir.join("mem_info_vram_total"))
|| has_nonzero_u64(&device_dir.join("tile0").join("vram0").join("total_bytes"))
|| (driver == "xe" && read_resource2_total_bytes(device_dir) > 0)
{
MemoryVariant::Discrete
} else {
MemoryVariant::Integrated
}
}
fn resolve_device_name(device_dir: &Path, device_id: u32) -> String {
if let Ok(label) = std::fs::read_to_string(device_dir.join("label")) {
let trimmed = label.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
resolve_intel_gpu_name(device_id)
}
#[path = "intel_gpu_linux/detection.rs"]
mod detection;
pub fn has_intel_client_gpu() -> bool {
detection::has_intel_client_gpu_from_root(Path::new("/sys/class/drm"))
}
#[cfg(feature = "level_zero")]
#[path = "intel_gpu_linux/level_zero_glue.rs"]
mod level_zero_glue;
#[path = "intel_gpu_linux/sources.rs"]
mod sources;
#[cfg(test)]
#[path = "intel_gpu_linux/tests.rs"]
mod tests;