Source code for femorph_solver.estimators._host
"""HostSpec — per-machine feature vector for the estimator.
Captures the hardware + software axes that move wall-time and peak
RSS on modal solves: CPU count, RAM, BLAS vendor / thread count,
MKL availability. Extensible: the :class:`HostSpec` dataclass
carries an ``extras`` dict so future bench rows can ship any
field a retraining pass wants to grade as predictive without
breaking the loader.
"""
from __future__ import annotations
import contextlib
import os
import platform
from dataclasses import dataclass, field
from pathlib import Path
[docs]
@dataclass(frozen=True)
class HostSpec:
"""Per-host feature vector — stable across solves on one box.
The fields are what we can introspect cheaply from a running
process + the kernel's ``/proc`` interfaces. Missing fields
use sensible defaults so an ``HostSpec.auto()`` call can't fail.
"""
cpu_model: str = "unknown"
n_cores_total: int = 1
n_cores_affinity: int = 1
ram_mb: float = 0.0
os_name: str = "unknown"
arch: str = "unknown"
has_mkl: bool = False
mkl_version: str = ""
#: Whatever additional fields the TA-6 benchmark shipped that
#: the current estimator doesn't use. Keeping the payload
#: round-trippable means later retrain passes can pick up
#: extras as new features without schema migration.
extras: dict[str, str] = field(default_factory=dict)
[docs]
@classmethod
def auto(cls) -> HostSpec:
"""Introspect the current machine — never raises.
Every branch has a graceful fallback; in a sandbox or on
non-Linux kernels the values that can't be probed stay at
their defaults instead of crashing the estimator.
"""
cpu_model = "unknown"
proc = Path("/proc/cpuinfo")
if proc.is_file():
with contextlib.suppress(OSError):
for line in proc.read_text().splitlines():
if line.startswith("model name"):
cpu_model = line.split(":", 1)[1].strip()
break
if cpu_model == "unknown":
cpu_model = platform.processor() or "unknown"
n_total = os.cpu_count() or 1
try:
n_affinity = len(os.sched_getaffinity(0))
except (AttributeError, OSError):
n_affinity = n_total
ram_mb = 0.0
mem = Path("/proc/meminfo")
if mem.is_file():
with contextlib.suppress(OSError, ValueError):
for line in mem.read_text().splitlines():
if line.startswith("MemTotal:"):
ram_mb = int(line.split()[1]) / 1024.0
break
if ram_mb == 0.0:
with contextlib.suppress(ImportError):
import psutil # noqa: PLC0415
ram_mb = psutil.virtual_memory().total / (1024 * 1024)
has_mkl = False
mkl_version = ""
with contextlib.suppress(ImportError, Exception):
from femorph_solver.report import _mkl_version # noqa: PLC0415
v = _mkl_version()
has_mkl = v not in ("not loaded", "loaded (version string unavailable)")
mkl_version = v if has_mkl else ""
return cls(
cpu_model=cpu_model,
n_cores_total=n_total,
n_cores_affinity=n_affinity,
ram_mb=ram_mb,
os_name=platform.system(),
arch=platform.machine(),
has_mkl=has_mkl,
mkl_version=mkl_version,
)
[docs]
def signature(self) -> str:
"""Stable string identifier for "same host" checks.
Two rows with matching signatures can train each other's
estimator coefficients; rows with different signatures go
into the shared prior. The signature intentionally
ignores transient things like affinity or MKL patch level
— those are fields the estimator can regress on, but they
don't change the fundamental silicon.
"""
return f"{self.cpu_model}|{self.n_cores_total}|{int(self.ram_mb / 1024)}G|{self.os_name}|{self.arch}"