Files
Anonymzx-BangtrixToolkit/monitor/base_monitor.py
T
2026-05-19 02:29:09 +07:00

217 lines
9.4 KiB
Python

"""
Universal Hardware Monitor
===========================
Auto-detects OS, GPU vendor, and selects the best available backend.
Supports Windows and Linux with all GPU vendors (AMD, NVIDIA, Intel).
Architecture:
1. Detector layer: Identify OS + GPU vendor(s) using standard library
2. Backend selection: Try backends in priority order based on OS + vendor
3. Stats fetching: Thread-safe, non-blocking API
Backend Priority (Windows):
- NVIDIA detected -> pynvml -> PDH counters -> PowerShell -> psutil
- AMD/Intel detected -> PDH counters -> PowerShell -> LibreHardware -> ADL -> psutil
- Unknown -> PDH counters -> PowerShell -> LibreHardware -> psutil
Backend Priority (Linux):
- AMD detected -> amdgpu sysfs -> nvidia-smi -> lspci -> psutil
- NVIDIA detected -> nvidia-smi -> sysfs hwmon -> psutil
- Intel detected -> intel sysfs -> psutil
- Unknown -> Linux generic -> psutil
APU/iGPU Safety:
- All backends handle APU shared memory gracefully
- No division by zero on VRAM
- memory_shared field used for APU systems
"""
import logging
import importlib
from typing import Optional, List
from .detector import detect_hardware
from .backends.base import MonitorBackend, HardwareStats
logger = logging.getLogger(__name__)
class UniversalMonitor:
"""Universal hardware monitor with auto-detection and fallback chain"""
def __init__(self):
self.available = False
self.os_type: str = "unknown"
self.vendor: str = "unknown"
self.gpu_count = 0
self.gpu_names: List[str] = []
self.has_apu: bool = False
self.driver: str = ""
self._backend: Optional[MonitorBackend] = None
self._initialize()
def _initialize(self):
"""Detect hardware and select best backend"""
# Step 1: Detect OS and GPU hardware
hw_info = detect_hardware()
self.os_type = hw_info.get('os', 'unknown')
self.vendor = hw_info.get('primary_vendor', 'unknown')
self.has_apu = hw_info.get('has_apu', False)
# Step 2: Try backends in priority order based on OS + vendor
backends = self._get_backend_chain()
for name, backend_cls in backends:
try:
backend = backend_cls()
if backend.initialize():
self._backend = backend
self.available = True
self.gpu_count = backend.gpu_count
self.gpu_names = backend.gpu_names
# Only override vendor if backend has a real detection
# (detector.py via registry/WMI is more reliable than backend)
if backend.vendor and backend.vendor not in ("unknown", "virtual"):
self.vendor = backend.vendor
self.driver = backend.name
logger.info(
f"Universal Monitor: using {name} "
f"| OS={self.os_type} Vendor={self.vendor} "
f"| {self.gpu_count} GPU(s) APU={self.has_apu}"
)
return
except Exception as e:
logger.debug(f"Universal Monitor: {name} failed: {e}")
logger.error("Universal Monitor: no backend available")
def _get_backend_chain(self) -> list:
"""Get ordered list of (name, class) tuples to try."""
chain = []
if self.os_type == 'windows':
# For AMD: try ROCm first (hipInfo + amd-smi), then ADL (temp/fan), then PDH (utilization/VRAM)
if self.vendor == 'amd':
chain = [
("windows-rocm", self._import_backend("rocm_backend", "ROCMBackend")),
("windows-adl", self._import_backend("adl_backend", "ADLBackend")),
("windows-pdh", self._import_backend("pdh_backend", "PDHBackend")),
("windows-powershell", self._import_backend("powershell_backend", "PowerShellBackend")),
("windows-nvml", self._import_backend("windows_nvml", "WindowsNVIDIABackend")),
]
elif self.vendor == 'nvidia':
chain = [
("windows-nvml", self._import_backend("windows_nvml", "WindowsNVIDIABackend")),
("windows-pdh", self._import_backend("pdh_backend", "PDHBackend")),
("windows-powershell", self._import_backend("powershell_backend", "PowerShellBackend")),
("windows-adl", self._import_backend("adl_backend", "ADLBackend")),
]
else:
chain = [
("windows-pdh", self._import_backend("pdh_backend", "PDHBackend")),
("windows-powershell", self._import_backend("powershell_backend", "PowerShellBackend")),
("windows-nvml", self._import_backend("windows_nvml", "WindowsNVIDIABackend")),
("windows-adl", self._import_backend("adl_backend", "ADLBackend")),
]
elif self.os_type == 'linux':
if self.vendor == 'amd':
chain = [
("linux-amdgpu", self._import_backend("linux_amdgpu", "LinuxAMDGPUBackend")),
("linux-nvidia", self._import_backend("linux_nvidia", "LinuxNVIDIABackend")),
("linux-intel", self._import_backend("linux_intel", "LinuxIntelBackend")),
("linux-generic", self._import_backend("linux_generic", "LinuxGenericBackend")),
]
elif self.vendor == 'nvidia':
chain = [
("linux-nvidia", self._import_backend("linux_nvidia", "LinuxNVIDIABackend")),
("linux-amdgpu", self._import_backend("linux_amdgpu", "LinuxAMDGPUBackend")),
("linux-intel", self._import_backend("linux_intel", "LinuxIntelBackend")),
("linux-generic", self._import_backend("linux_generic", "LinuxGenericBackend")),
]
elif self.vendor == 'intel':
chain = [
("linux-intel", self._import_backend("linux_intel", "LinuxIntelBackend")),
("linux-nvidia", self._import_backend("linux_nvidia", "LinuxNVIDIABackend")),
("linux-amdgpu", self._import_backend("linux_amdgpu", "LinuxAMDGPUBackend")),
("linux-generic", self._import_backend("linux_generic", "LinuxGenericBackend")),
]
else:
chain = [
("linux-amdgpu", self._import_backend("linux_amdgpu", "LinuxAMDGPUBackend")),
("linux-nvidia", self._import_backend("linux_nvidia", "LinuxNVIDIABackend")),
("linux-intel", self._import_backend("linux_intel", "LinuxIntelBackend")),
("linux-generic", self._import_backend("linux_generic", "LinuxGenericBackend")),
]
# Always append universal fallback at the end
chain.append(("psutil-fallback", self._import_backend("fallback", "FallbackBackend")))
# Filter out None values (failed imports)
return [(name, cls) for name, cls in chain if cls is not None]
def _import_backend(self, module_name: str, class_name: str):
"""Safely import a backend class."""
try:
mod = importlib.import_module(f".backends.{module_name}", package="monitor")
return getattr(mod, class_name)
except Exception as e:
logger.debug(f"Cannot import {module_name}.{class_name}: {e}")
return None
def get_gpu_stats(self, gpu_id: int = 0) -> HardwareStats:
"""Get stats for a specific GPU. Returns safe fallback on error."""
if not self._backend or not self.available:
return HardwareStats(
gpu_id=gpu_id,
gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "N/A",
vendor=self.vendor,
is_available=False,
error_message="No backend available",
)
try:
stats = self._backend.get_stats(gpu_id)
# Ensure vendor is set (prefer detected vendor over backend's)
stats.vendor = self.vendor if (self.vendor and self.vendor != "unknown") else (stats.vendor or "unknown")
if not stats.gpu_name and gpu_id < len(self.gpu_names):
stats.gpu_name = self.gpu_names[gpu_id]
if not stats.driver:
stats.driver = self.driver
return stats
except Exception as e:
return HardwareStats(
gpu_id=gpu_id,
gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "N/A",
vendor=self.vendor,
is_available=False,
error_message=str(e),
)
def get_all_gpu_stats(self) -> List[HardwareStats]:
"""Get stats for all detected GPUs."""
if not self._backend or self.gpu_count == 0:
return []
return [self.get_gpu_stats(i) for i in range(self.gpu_count)]
def close(self):
"""Cleanup backend resources."""
if self._backend:
self._backend.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Singleton
_universal_monitor = None
def get_universal_monitor() -> UniversalMonitor:
"""Get or create the singleton UniversalMonitor instance."""
global _universal_monitor
if _universal_monitor is None:
_universal_monitor = UniversalMonitor()
return _universal_monitor