274 lines
10 KiB
Python
274 lines
10 KiB
Python
"""
|
|
Linux Intel GPU Backend
|
|
=======================
|
|
Reads GPU stats from Linux sysfs for Intel GPUs (integrated + Arc).
|
|
Supports both integrated iGPUs and discrete Intel Arc GPUs.
|
|
|
|
Data sources:
|
|
- GPU utilization: /sys/class/drm/card*/device/gt_cur_freq_mhz (relative)
|
|
- Temperature: /sys/class/drm/card*/device/hwmon/hwmon*/temp1_input
|
|
- Memory: Intel iGPUs use shared system RAM
|
|
- Frequency: /sys/class/drm/card*/device/gt_act_freq_mhz
|
|
|
|
APU/iGPU handling:
|
|
- Intel iGPUs always use shared memory
|
|
- memory_total = 0, memory_shared = system RAM
|
|
- No division by zero or "missing VRAM" errors
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Optional
|
|
|
|
from .base import MonitorBackend, HardwareStats
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LinuxIntelBackend(MonitorBackend):
|
|
"""Intel GPU monitoring on Linux via sysfs"""
|
|
name = "linux-intel-sysfs"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.vendor = "intel"
|
|
self._card_paths: list[str] = []
|
|
self._is_discrete: bool = False # True for Arc dGPU
|
|
self._system_ram_total: int = 0
|
|
self._hwmon_paths: list[str] = []
|
|
self._temp_cache_timestamp: float = 0
|
|
self._cached_temp: float = 0.0
|
|
self._TEMP_CACHE_SECONDS = 5.0
|
|
|
|
def initialize(self) -> bool:
|
|
import platform
|
|
if platform.system().lower() != "linux":
|
|
return False
|
|
|
|
try:
|
|
self._find_intel_cards()
|
|
if not self._card_paths:
|
|
return False
|
|
|
|
self._detect_gpu_type()
|
|
self._detect_hwmon()
|
|
self._detect_system_ram()
|
|
|
|
self.gpu_count = len(self._card_paths)
|
|
self.gpu_names = [f"Intel GPU {i}" for i in range(self.gpu_count)]
|
|
self.available = True
|
|
|
|
logger.info(
|
|
f"Linux Intel: {self.gpu_count} GPU(s), "
|
|
f"dGPU={self._is_discrete}"
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Linux Intel init error: {e}")
|
|
return False
|
|
|
|
def _find_intel_cards(self):
|
|
"""Find Intel GPU card directories."""
|
|
drm_path = "/sys/class/drm"
|
|
if not os.path.exists(drm_path):
|
|
return
|
|
|
|
for entry in sorted(os.listdir(drm_path)):
|
|
if not entry.startswith("card") or "-" in entry:
|
|
continue
|
|
|
|
card_path = os.path.join(drm_path, entry)
|
|
|
|
# Check vendor
|
|
vendor_file = os.path.join(card_path, "device", "vendor")
|
|
if os.path.exists(vendor_file):
|
|
try:
|
|
with open(vendor_file, 'r') as f:
|
|
vendor_id = f.read().strip().lower()
|
|
if '8086' in vendor_id:
|
|
self._card_paths.append(card_path)
|
|
except Exception:
|
|
pass
|
|
|
|
def _detect_gpu_type(self):
|
|
"""Detect if Intel GPU is discrete (Arc) or integrated."""
|
|
for card_path in self._card_paths:
|
|
# Check for dedicated VRAM (Arc dGPU has this)
|
|
vram_file = os.path.join(card_path, "device", "lmem_total")
|
|
if os.path.exists(vram_file):
|
|
try:
|
|
with open(vram_file, 'r') as f:
|
|
val = int(f.read().strip())
|
|
if val > 512 * 1024 * 1024: # > 512MB = discrete
|
|
self._is_discrete = True
|
|
return
|
|
except (ValueError, OSError):
|
|
pass
|
|
|
|
def _detect_hwmon(self):
|
|
"""Find hwmon paths for Intel GPU temperatures."""
|
|
for card_path in self._card_paths:
|
|
hwmon_dir = os.path.join(card_path, "device", "hwmon")
|
|
if os.path.exists(hwmon_dir):
|
|
for hwmon_entry in os.listdir(hwmon_dir):
|
|
hwmon_path = os.path.join(hwmon_dir, hwmon_entry)
|
|
name_file = os.path.join(hwmon_path, "name")
|
|
if os.path.exists(name_file):
|
|
try:
|
|
with open(name_file, 'r') as f:
|
|
name = f.read().strip().lower()
|
|
if 'intel' in name or 'i915' in name:
|
|
self._hwmon_paths.append(hwmon_path)
|
|
except Exception:
|
|
pass
|
|
|
|
def _detect_system_ram(self):
|
|
"""Read total system RAM for shared memory tracking."""
|
|
try:
|
|
with open('/proc/meminfo', 'r') as f:
|
|
for line in f:
|
|
if line.startswith('MemTotal:'):
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
self._system_ram_total = int(parts[1]) * 1024
|
|
break
|
|
except Exception:
|
|
self._system_ram_total = 8 * 1024 * 1024 * 1024
|
|
|
|
def _read_sysfs(self, path: str) -> Optional[str]:
|
|
"""Safely read a sysfs file."""
|
|
try:
|
|
if os.path.exists(path):
|
|
with open(path, 'r') as f:
|
|
return f.read().strip()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def get_stats(self, gpu_id: int = 0) -> HardwareStats:
|
|
try:
|
|
if gpu_id >= len(self._card_paths):
|
|
return HardwareStats(
|
|
gpu_id=gpu_id,
|
|
vendor="intel",
|
|
is_available=False,
|
|
error_message=f"GPU {gpu_id} not found",
|
|
)
|
|
|
|
card_path = self._card_paths[gpu_id]
|
|
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"Intel GPU {gpu_id}"
|
|
|
|
# Intel iGPUs always use shared memory — mark as APU
|
|
is_apu = not self._is_discrete
|
|
|
|
stats = HardwareStats(
|
|
gpu_id=gpu_id,
|
|
gpu_name=name,
|
|
vendor="intel",
|
|
driver="intel-sysfs",
|
|
is_apu=is_apu,
|
|
is_available=True,
|
|
)
|
|
|
|
# For discrete Arc GPUs, try to read dedicated VRAM
|
|
if self._is_discrete:
|
|
lmem_total = self._read_sysfs(os.path.join(card_path, "device", "lmem_total"))
|
|
lmem_used = self._read_sysfs(os.path.join(card_path, "device", "lmem_used"))
|
|
if lmem_total:
|
|
try:
|
|
total = int(lmem_total)
|
|
stats.memory_total = total
|
|
if lmem_used:
|
|
used = int(lmem_used)
|
|
stats.memory_used = used
|
|
stats.memory_free = max(0, total - used)
|
|
if total > 0:
|
|
stats.utilization_memory = (used / total) * 100.0
|
|
except ValueError:
|
|
pass
|
|
|
|
# For integrated GPUs, report shared system RAM
|
|
if is_apu and stats.memory_total == 0:
|
|
stats.memory_shared = self._system_ram_total
|
|
try:
|
|
import psutil
|
|
svmem = psutil.virtual_memory()
|
|
if stats.memory_used == 0:
|
|
stats.memory_used = int(svmem.used * 0.15) # Estimate GPU portion
|
|
stats.utilization_memory = svmem.percent
|
|
except ImportError:
|
|
pass
|
|
|
|
# GPU utilization — estimate from frequency ratio
|
|
gt_act_freq = self._read_sysfs(os.path.join(card_path, "device", "gt_act_freq_mhz"))
|
|
gt_cur_freq = self._read_sysfs(os.path.join(card_path, "device", "gt_cur_freq_mhz"))
|
|
gt_max_freq = self._read_sysfs(os.path.join(card_path, "device", "gt_max_freq_mhz"))
|
|
|
|
if gt_act_freq and gt_max_freq:
|
|
try:
|
|
act = float(gt_act_freq)
|
|
max_f = float(gt_max_freq)
|
|
if max_f > 0:
|
|
stats.utilization_gpu = min(100.0, (act / max_f) * 100.0)
|
|
except ValueError:
|
|
pass
|
|
elif gt_cur_freq and gt_max_freq:
|
|
try:
|
|
cur = float(gt_cur_freq)
|
|
max_f = float(gt_max_freq)
|
|
if max_f > 0:
|
|
stats.utilization_gpu = min(100.0, (cur / max_f) * 100.0)
|
|
except ValueError:
|
|
pass
|
|
|
|
# Temperature (cached)
|
|
stats.temperature = self._get_temperature()
|
|
|
|
# Core clock
|
|
if gt_act_freq:
|
|
try:
|
|
stats.core_clock = int(float(gt_act_freq))
|
|
except ValueError:
|
|
pass
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"Linux Intel get_stats({gpu_id}) error: {e}")
|
|
return HardwareStats(
|
|
gpu_id=gpu_id,
|
|
vendor="intel",
|
|
is_available=False,
|
|
error_message=str(e),
|
|
)
|
|
|
|
def _get_temperature(self) -> float:
|
|
"""Read GPU temperature from hwmon with caching."""
|
|
now = time.time()
|
|
if now - self._temp_cache_timestamp < self._TEMP_CACHE_SECONDS:
|
|
return self._cached_temp
|
|
|
|
temp = 0.0
|
|
for hwmon_path in self._hwmon_paths:
|
|
try:
|
|
for entry in sorted(os.listdir(hwmon_path)):
|
|
if entry.startswith("temp") and entry.endswith("_input"):
|
|
try:
|
|
with open(os.path.join(hwmon_path, entry), 'r') as f:
|
|
raw = int(f.read().strip())
|
|
celsius = raw / 1000.0
|
|
if 20 <= celsius <= 120:
|
|
if celsius > temp:
|
|
temp = celsius
|
|
except (ValueError, OSError):
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
self._cached_temp = temp
|
|
self._temp_cache_timestamp = now
|
|
return temp
|
|
|
|
def close(self):
|
|
pass |