msrishav's picture
Add inference code, config, and technical report
e68eb1c verified
Raw
History Blame Contribute Delete
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
@dataclass(frozen=True)
class MemoryInfo:
total_gb: float
available_gb: Optional[float]
used_fraction: Optional[float]
@dataclass(frozen=True)
class GpuInfo:
name: str
memory_total_mb: int
memory_used_mb: Optional[int]
temperature_c: Optional[int]
driver_version: Optional[str]
@dataclass(frozen=True)
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
@dataclass(frozen=True)
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]
@dataclass(frozen=True)
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)}")