Download src/utils/hardware.py from msrishav/wunder-rnn-gru-ensemble: direct link, hf CLI and curl.
- Browser
- Download file 9.07 kB
-
https://huggingface.co/msrishav/wunder-rnn-gru-ensemble/resolve/main/src/utils/hardware.py
- Command line
-
hf download hf://msrishav/wunder-rnn-gru-ensemble/src/utils/hardware.py
-
curl -L -o hardware.py https://huggingface.co/msrishav/wunder-rnn-gru-ensemble/resolve/main/src/utils/hardware.py
9.07 kB
| """Hardware profiling and max-safe guard helpers for local-only experiments.""" | |
| from __future__ import annotations | |
| import ctypes | |
| import contextlib | |
| import json | |
| import os | |
| import platform | |
| import shutil | |
| import subprocess | |
| import tempfile | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| from typing import Callable, Iterable, Optional | |
| BYTES_PER_GB = 1024**3 | |
| class MemoryInfo: | |
| total_gb: float | |
| available_gb: Optional[float] | |
| used_fraction: Optional[float] | |
| class GpuInfo: | |
| name: str | |
| memory_total_mb: int | |
| memory_used_mb: Optional[int] | |
| temperature_c: Optional[int] | |
| driver_version: Optional[str] | |
| class TorchInfo: | |
| installed: bool | |
| version: Optional[str] | |
| cuda_available: bool | |
| cuda_version: Optional[str] | |
| device_name: Optional[str] | |
| device_memory_gb: Optional[float] | |
| error: Optional[str] = None | |
| class MaxSafeConfig: | |
| cpu_workers: int | |
| dataloader_workers: int | |
| max_dataloader_workers: int | |
| ram_limit_fraction: float | |
| ram_target_gb: float | |
| gpu_vram_fraction: float | |
| gpu_target_vram_gb: Optional[float] | |
| gpu_pause_temp_c: int | |
| gpu_abort_temp_c: int | |
| mixed_precision: bool | |
| gradient_accumulation: bool | |
| batch_candidates: list[int] | |
| class HardwareProfile: | |
| profile_name: str | |
| platform: str | |
| python: str | |
| cpu_name: str | |
| logical_cpu_count: int | |
| memory: MemoryInfo | |
| gpus: list[GpuInfo] | |
| torch: TorchInfo | |
| max_safe: MaxSafeConfig | |
| class ResourceLimitError(RuntimeError): | |
| """Raised when a max-safe guard would abort a run.""" | |
| def _round_gb(value_bytes: int) -> float: | |
| return round(value_bytes / BYTES_PER_GB, 2) | |
| def get_memory_info() -> MemoryInfo: | |
| """Return physical memory using stdlib-only platform APIs.""" | |
| if platform.system().lower() == "windows": | |
| class MEMORYSTATUSEX(ctypes.Structure): | |
| _fields_ = [ | |
| ("dwLength", ctypes.c_ulong), | |
| ("dwMemoryLoad", ctypes.c_ulong), | |
| ("ullTotalPhys", ctypes.c_ulonglong), | |
| ("ullAvailPhys", ctypes.c_ulonglong), | |
| ("ullTotalPageFile", ctypes.c_ulonglong), | |
| ("ullAvailPageFile", ctypes.c_ulonglong), | |
| ("ullTotalVirtual", ctypes.c_ulonglong), | |
| ("ullAvailVirtual", ctypes.c_ulonglong), | |
| ("sullAvailExtendedVirtual", ctypes.c_ulonglong), | |
| ] | |
| status = MEMORYSTATUSEX() | |
| status.dwLength = ctypes.sizeof(MEMORYSTATUSEX) | |
| ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)) | |
| used_fraction = 1.0 - (status.ullAvailPhys / status.ullTotalPhys) | |
| return MemoryInfo( | |
| total_gb=_round_gb(status.ullTotalPhys), | |
| available_gb=_round_gb(status.ullAvailPhys), | |
| used_fraction=round(used_fraction, 4), | |
| ) | |
| if hasattr(os, "sysconf") and "SC_PAGE_SIZE" in os.sysconf_names: | |
| page_size = os.sysconf("SC_PAGE_SIZE") | |
| pages = os.sysconf("SC_PHYS_PAGES") | |
| return MemoryInfo(total_gb=_round_gb(page_size * pages), available_gb=None, used_fraction=None) | |
| return MemoryInfo(total_gb=0.0, available_gb=None, used_fraction=None) | |
| def get_nvidia_gpus() -> list[GpuInfo]: | |
| """Read NVIDIA GPU status through nvidia-smi when available.""" | |
| if not shutil.which("nvidia-smi"): | |
| return [] | |
| cmd = [ | |
| "nvidia-smi", | |
| "--query-gpu=name,memory.total,memory.used,temperature.gpu,driver_version", | |
| "--format=csv,noheader,nounits", | |
| ] | |
| try: | |
| raw = subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL, timeout=10) | |
| except Exception: | |
| return [] | |
| gpus = [] | |
| for line in raw.splitlines(): | |
| parts = [part.strip() for part in line.split(",")] | |
| if len(parts) < 5: | |
| continue | |
| name, total_mb, used_mb, temp_c, driver = parts[:5] | |
| gpus.append( | |
| GpuInfo( | |
| name=name, | |
| memory_total_mb=int(float(total_mb)), | |
| memory_used_mb=int(float(used_mb)), | |
| temperature_c=int(float(temp_c)), | |
| driver_version=driver, | |
| ) | |
| ) | |
| return gpus | |
| def get_torch_info() -> TorchInfo: | |
| """Return PyTorch/CUDA availability without making Torch mandatory.""" | |
| try: | |
| with open(os.devnull, "w", encoding="utf-8") as devnull, contextlib.redirect_stderr(devnull): | |
| import torch | |
| cuda_available = bool(torch.cuda.is_available()) | |
| device_name = None | |
| device_memory_gb = None | |
| if cuda_available: | |
| props = torch.cuda.get_device_properties(0) | |
| device_name = torch.cuda.get_device_name(0) | |
| device_memory_gb = round(props.total_memory / BYTES_PER_GB, 2) | |
| return TorchInfo( | |
| installed=True, | |
| version=str(torch.__version__), | |
| cuda_available=cuda_available, | |
| cuda_version=getattr(torch.version, "cuda", None), | |
| device_name=device_name, | |
| device_memory_gb=device_memory_gb, | |
| ) | |
| except Exception as exc: | |
| return TorchInfo( | |
| installed=False, | |
| version=None, | |
| cuda_available=False, | |
| cuda_version=None, | |
| device_name=None, | |
| device_memory_gb=None, | |
| error=repr(exc), | |
| ) | |
| def build_max_safe_config(memory: MemoryInfo, gpus: list[GpuInfo]) -> MaxSafeConfig: | |
| """Choose conservative max-safe defaults for this laptop class.""" | |
| logical = os.cpu_count() or 1 | |
| cpu_workers = max(1, min(12, logical - 4 if logical > 4 else logical)) | |
| gpu_target_vram_gb = None | |
| if gpus: | |
| gpu_target_vram_gb = round((gpus[0].memory_total_mb / 1024) * 0.85, 2) | |
| return MaxSafeConfig( | |
| cpu_workers=cpu_workers, | |
| dataloader_workers=2, | |
| max_dataloader_workers=4, | |
| ram_limit_fraction=0.90, | |
| ram_target_gb=min(13.0, round(memory.total_gb * 0.82, 2)) if memory.total_gb else 13.0, | |
| gpu_vram_fraction=0.85, | |
| gpu_target_vram_gb=gpu_target_vram_gb, | |
| gpu_pause_temp_c=82, | |
| gpu_abort_temp_c=87, | |
| mixed_precision=True, | |
| gradient_accumulation=True, | |
| batch_candidates=[512, 256, 128, 64, 32, 16, 8], | |
| ) | |
| def build_hardware_profile(profile_name: str = "max_safe") -> HardwareProfile: | |
| memory = get_memory_info() | |
| gpus = get_nvidia_gpus() | |
| return HardwareProfile( | |
| profile_name=profile_name, | |
| platform=platform.platform(), | |
| python=platform.python_version(), | |
| cpu_name=platform.processor() or platform.machine(), | |
| logical_cpu_count=os.cpu_count() or 1, | |
| memory=memory, | |
| gpus=gpus, | |
| torch=get_torch_info(), | |
| max_safe=build_max_safe_config(memory, gpus), | |
| ) | |
| def assert_within_max_safe(profile: HardwareProfile) -> None: | |
| """Abort before a run if current memory/temperature already violates guardrails.""" | |
| if profile.memory.used_fraction is not None and profile.memory.used_fraction >= profile.max_safe.ram_limit_fraction: | |
| raise ResourceLimitError( | |
| f"System RAM usage {profile.memory.used_fraction:.1%} exceeds " | |
| f"{profile.max_safe.ram_limit_fraction:.0%} guard" | |
| ) | |
| if profile.gpus: | |
| gpu = profile.gpus[0] | |
| if gpu.temperature_c is not None and gpu.temperature_c >= profile.max_safe.gpu_abort_temp_c: | |
| raise ResourceLimitError( | |
| f"GPU temperature {gpu.temperature_c}C exceeds " | |
| f"{profile.max_safe.gpu_abort_temp_c}C abort guard" | |
| ) | |
| def calibrate_batch_size( | |
| trial_fn: Callable[[int], None], | |
| candidates: Iterable[int], | |
| *, | |
| repeats: int = 2, | |
| ) -> Optional[int]: | |
| """ | |
| Choose the largest candidate batch that survives repeated trial calls. | |
| `trial_fn` should raise RuntimeError for OOM-like failures. This helper is | |
| intentionally framework-agnostic so tests can simulate OOM without Torch. | |
| """ | |
| for batch_size in candidates: | |
| try: | |
| for _ in range(repeats): | |
| trial_fn(int(batch_size)) | |
| return int(batch_size) | |
| except RuntimeError: | |
| continue | |
| return None | |
| def atomic_write_json(path: str | Path, payload: object) -> None: | |
| """Write JSON atomically so interrupted runs do not corrupt manifests.""" | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| if hasattr(payload, "__dataclass_fields__"): | |
| payload = asdict(payload) | |
| with tempfile.NamedTemporaryFile("w", delete=False, dir=str(path.parent), encoding="utf-8") as tmp: | |
| json.dump(payload, tmp, indent=2, sort_keys=True) | |
| tmp.write("\n") | |
| tmp_path = Path(tmp.name) | |
| tmp_path.replace(path) | |
| def dataclass_to_dict(value: object) -> dict: | |
| if hasattr(value, "__dataclass_fields__"): | |
| return asdict(value) | |
| if isinstance(value, dict): | |
| return value | |
| raise TypeError(f"Unsupported JSON payload: {type(value)}") | |