1#!/usr/bin/env python2"""Phase 0 environment audit: records hardware, toolchain and framework capabilities.34Writes hardware/device_inventory.json (local machine) and prints a summary.5Never prints secrets. Remote cluster nodes are audited by scripts/cluster_audit.sh6(SSH, read-only commands) and merged into the same JSON under "remote_nodes".7"""8from __future__ import annotations9import argparse, json, os, platform, shutil, subprocess, sys, time10from pathlib import Path111213def sh(cmd: list[str]) -> str:14 try:15 return subprocess.run(cmd, capture_output=True, text=True, timeout=20).stdout.strip()16 except Exception as e: # noqa: BLE00117 return f"<unavailable: {type(e).__name__}>"181920def main() -> int:21 ap = argparse.ArgumentParser(description=__doc__)22 ap.add_argument("--out", default="hardware/device_inventory.json")23 ap.add_argument("--smoke", action="store_true", help="run a tiny forward/backward on every available backend")24 args = ap.parse_args()2526 inv: dict = {27 "audited_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),28 "hostname": platform.node(),29 "os": f"{platform.system()} {platform.release()} ({platform.mac_ver()[0] or platform.version()})",30 "machine": platform.machine(),31 "python": sys.version.split()[0],32 "python_executable": sys.executable,33 "cpu_brand": sh(["sysctl", "-n", "machdep.cpu.brand_string"]) if sys.platform == "darwin" else platform.processor(),34 "cpu_logical_cores": os.cpu_count(),35 "ram_bytes": int(sh(["sysctl", "-n", "hw.memsize"]) or 0) if sys.platform == "darwin" else None,36 "disk_free_bytes": shutil.disk_usage(os.getcwd()).free,37 "toolchains": {t: shutil.which(t) for t in ["git", "latexmk", "pdflatex", "lualatex", "pandoc", "nvidia-smi", "uv", "cmake", "clang"]},38 "frameworks": {},39 "backends": {},40 }41 if sys.platform == "darwin":42 sp = sh(["system_profiler", "SPDisplaysDataType"])43 gpu_cores = None44 for line in sp.splitlines():45 if "Total Number of Cores" in line:46 gpu_cores = int(line.split(":")[-1].strip())47 inv["gpu_cores"] = gpu_cores48 inv["metal"] = any("Metal" in l for l in sp.splitlines())49 try:50 import torch51 inv["frameworks"]["torch"] = torch.__version__52 inv["backends"]["cpu"] = True53 inv["backends"]["mps"] = bool(torch.backends.mps.is_available())54 inv["backends"]["cuda"] = bool(torch.cuda.is_available())55 if torch.cuda.is_available():56 inv["cuda_devices"] = [torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())]57 except Exception as e: # noqa: BLE00158 inv["frameworks"]["torch"] = f"<missing: {e}>"59 try:60 import mlx.core as mx61 inv["frameworks"]["mlx"] = mx.__version__62 except Exception: # noqa: BLE00163 inv["frameworks"]["mlx"] = None64 for pkg in ["numpy", "tokenizers", "datasets", "safetensors", "scipy", "pandas", "matplotlib", "pytest", "hypothesis", "yaml"]:65 try:66 m = __import__(pkg)67 inv["frameworks"][pkg] = getattr(m, "__version__", "present")68 except Exception: # noqa: BLE00169 inv["frameworks"][pkg] = None70 inv["network"] = {"note": "outbound HTTPS assumed available for dataset download; verified at data preparation time"}71 inv["secrets_policy"] = "no secret values are recorded in this inventory"7273 if args.smoke:74 import torch75 res = {}76 for dev in [d for d, ok in inv["backends"].items() if ok]:77 try:78 t0 = time.perf_counter()79 x = torch.randn(64, 256, device=dev, requires_grad=True)80 w = torch.randn(256, 256, device=dev, requires_grad=True)81 y = (x @ w).relu().sum()82 y.backward()83 if dev == "mps":84 torch.mps.synchronize()85 res[dev] = {"ok": bool(torch.isfinite(w.grad).all().item()), "seconds": round(time.perf_counter() - t0, 4)}86 except Exception as e: # noqa: BLE00187 res[dev] = {"ok": False, "error": repr(e)}88 inv["smoke_forward_backward"] = res8990 out = Path(args.out)91 existing = {}92 if out.exists():93 try:94 existing = json.loads(out.read_text())95 except Exception: # noqa: BLE00196 existing = {}97 if "remote_nodes" in existing:98 inv["remote_nodes"] = existing["remote_nodes"]99 out.parent.mkdir(parents=True, exist_ok=True)100 out.write_text(json.dumps(inv, indent=2, ensure_ascii=False))101 print(json.dumps({k: v for k, v in inv.items() if k != "remote_nodes"}, indent=2, ensure_ascii=False))102 return 0103104105if __name__ == "__main__":106 raise SystemExit(main())107