Files
club-3090/scripts/lib/profiles/hwdetect.py
T
noonghunnaandClaude Opus 4.7 39177282b7 feat(pull): v0.8.2 STEP V4 — optional whichllm hw-detect subprocess (CONTRACT-3, hw-detect-only)
CONTRACT-3 §8: an OPTIONAL, bounded subprocess that augments hardware
ENUMERATION for the eval path where nvidia-smi does not apply (AMD ROCm /
Apple / other-vendor). Strictly detect-only; never feeds kv-calc (kv-calc
stays the sole fit authority); no new hard dependency.

New isolated leaf module scripts/lib/profiles/hwdetect.py:
- detect_non_nvidia_hw()/detect_non_nvidia_sm(): bounded `whichllm list
  --json` subprocess, defensively parsed into a structured HwDetectResult;
  maps a recognised non-NVIDIA device class to an SM-equivalent for the
  [C0] SM gate ONLY.
- Every non-delivery path (tool absent / failed / timeout / unparseable /
  NVIDIA-only / unrecognised) degrades to None and NEVER raises out.

Additive consume-point wiring in run_pull (the eval path): a new optional
`hwdetect_fn` kwarg, consulted ONLY inside the existing
`if hardware_sm is None:` stratum-3 block — i.e. only when nvidia-smi
already returned nothing. The NVIDIA majority never enters the seam, so
that path is byte-identical whether the augment is absent OR
present-but-degrading. On a recognised non-NVIDIA device the eval path
gets an SM-equivalent (the [C0] SM gate runs instead of the blind-refuse
`hardware-sm-undetermined` terminal) plus an additive notice/diagnostic;
no shipped decision field is mutated and kv-calc is not consulted.

BOTH RED-LINE halves covered + proven by scripts/tests/test-hwdetect.sh:
(a) safety — optional/no-hard-dep, graceful degrade, NVIDIA-path
byte-identity, never feeds kv-calc; (b) delivery — a simulated
(explicit, deterministic) non-nvidia env yields a structured enumeration
the eval path observably consumes (outcome moves OFF the degrade
terminal). Rig-independent leak assertion (str(abs_dir) not in shared).

All 9 shipped v0.8.0/V1/V2/V3 decision modules byte-unchanged
(dedup.py:262 FInput.dedup_hash(_EffProxy()) idiom fenced/untouched).
Full scripts/tests/test-*.sh suite green in the CI condition (25/25);
kv-calc --calibration unchanged (11/11).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-18 19:29:43 +00:00

218 lines
8.8 KiB
Python

"""v0.8.2 CONTRACT-3 — optional `whichllm` hardware-detect subprocess.
§8 posture: `whichllm`-as-a-VRAM/fit-engine is OUT (kv-calc stays the sole
fit authority). The ONLY in-scope slice is an **optional, bounded subprocess
that augments hardware *enumeration*** for the eval path on environments
where `nvidia-smi` does not apply (AMD ROCm / Apple Silicon / other-vendor
accelerators). Strictly detect-only: this module enumerates accelerators and
maps a non-NVIDIA device to an SM-equivalent so the eval path can proceed
honestly instead of bare-degrading to `hardware-sm-undetermined`. It NEVER
performs fit/VRAM math and NEVER feeds kv-calc.
Design invariants (the STEP V4 RED-LINE — BOTH halves):
(a) Safety half. This module is OPTIONAL. There is no new hard
dependency: the `whichllm` tool being absent, failing, timing out,
or producing an unrecognised payload all degrade *gracefully* to
``None`` (the caller then keeps today's exact `nvidia-smi`-absent
behaviour). It is consulted ONLY when `nvidia-smi` already returned
nothing — so for the NVIDIA majority this code path is never
entered and the shipped detection is byte-identical. It never
raises out at the caller (every failure mode is caught here and
returns ``None``), and it never touches kv-calc.
(b) Delivery half (MANDATORY — an always-degrading no-op stub is a
FAIL). Given a non-`nvidia-smi` environment in which the optional
tool DOES enumerate an accelerator, `detect_non_nvidia_sm()`
returns a real structured `HwDetectResult` carrying an
SM-equivalent the eval path actually consumes — so the eval path's
hardware view in that environment differs observably from the bare
`nvidia-smi`-absent degrade path (it can reach the [C0] SM gate
instead of terminating at `hardware-sm-undetermined`).
PURE-STDLIB (json / shutil / subprocess), matching the no-external-deps
style of its `scripts/lib/profiles/` siblings. The `whichllm` enumeration
JSON is parsed defensively; nothing in this module imports the rest of the
pull pipeline (no decision-logic coupling — it is a leaf).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from dataclasses import dataclass, field
from typing import Optional
# The external enumeration tool. Absence is the COMMON case (NVIDIA rigs do
# not install it) and MUST degrade silently — it is never a hard dependency.
_WHICHLLM_BIN = "whichllm"
# Bounded subprocess (mirrors detect_hardware_sm()'s 15s nvidia-smi bound).
_SUBPROCESS_TIMEOUT_S = 15
# Non-NVIDIA vendor -> the SM-equivalent the eval path's [C0] SM gate keys
# on. These are deliberately CONSERVATIVE enumeration anchors, NOT a fit
# claim: the value only lets the SM gate run on a recognised device class
# instead of refusing blind. kv-calc is NEVER consulted with these — the
# fit authority is untouched (CONTRACT-3 hw-detect-only).
#
# Mapping rationale (enumeration-only, documented constraint):
# * AMD ROCm CDNA/RDNA -> 8.0 (Ampere-class capability tier; the eval
# path's SM gate is the only consumer — this is not a perf claim).
# * Apple Silicon (Metal/MPS) -> 8.0 (same: lets the gate proceed; the
# §7 boot-fit≠runtime caveat + kv-calc still own the real verdict).
# An unrecognised vendor stays None -> graceful degrade (safety half).
_VENDOR_SM_EQUIV = {
"amd": 8.0,
"rocm": 8.0,
"apple": 8.0,
"metal": 8.0,
"mps": 8.0,
}
@dataclass(frozen=True)
class HwDetectResult:
"""A structured non-NVIDIA hardware-enumeration result.
`sm_equiv` is the ONLY value the eval path consumes (it feeds the [C0]
SM gate exactly where `detect_hardware_sm()` would have). The vendor /
device-name / count fields are diagnostics-only enumeration metadata —
they are NEVER passed to kv-calc.
"""
sm_equiv: float
vendor: str
device_names: list = field(default_factory=list)
device_count: int = 0
# The raw enumeration source, for diagnostics/audit only.
source: str = _WHICHLLM_BIN
def _tool_available() -> bool:
"""True iff the optional enumeration tool is on PATH. Absence is the
common NVIDIA-rig case and is NOT an error — the caller degrades."""
return shutil.which(_WHICHLLM_BIN) is not None
def _run_whichllm(runner: Optional[object] = None) -> Optional[str]:
"""Run the bounded enumeration subprocess; return its stdout, or None
on ANY failure (absent / non-zero / timeout / OSError). `runner` is an
injection seam so tests are hermetic (CI has no `whichllm` and no
non-NVIDIA GPU) — None uses the real bounded subprocess."""
if runner is not None:
try:
return runner()
except Exception: # noqa: BLE001 - any runner failure -> degrade
return None
if not _tool_available(): # pragma: no cover - env dependent
return None
try: # pragma: no cover - exercised on non-NVIDIA rigs, injected in CI
out = subprocess.run(
[_WHICHLLM_BIN, "list", "--json"],
capture_output=True, text=True,
timeout=_SUBPROCESS_TIMEOUT_S,
)
except (OSError, subprocess.SubprocessError): # pragma: no cover
return None
if out.returncode != 0: # pragma: no cover
return None
return out.stdout
def _parse_enumeration(stdout: Optional[str]) -> Optional[HwDetectResult]:
"""Parse the enumeration JSON defensively into a HwDetectResult, or
None when it is absent / unparseable / has no recognised non-NVIDIA
device. Tolerant to a few plausible `whichllm`-style shapes; an
unrecognised shape degrades (never raises)."""
if not stdout:
return None
try:
payload = json.loads(stdout)
except (ValueError, TypeError):
return None
# Accept either a bare list of device dicts or a {"devices": [...]} /
# {"gpus": [...]} envelope — all degrade to None if no list is found.
devices = None
if isinstance(payload, list):
devices = payload
elif isinstance(payload, dict):
for key in ("devices", "gpus", "accelerators"):
if isinstance(payload.get(key), list):
devices = payload[key]
break
if not devices:
return None
names: list = []
matched_vendor: Optional[str] = None
matched_sm: Optional[float] = None
for dev in devices:
if not isinstance(dev, dict):
continue
vendor_raw = str(
dev.get("vendor") or dev.get("backend") or dev.get("type") or ""
).strip().lower()
name = str(
dev.get("name") or dev.get("device") or dev.get("model") or ""
).strip()
# Skip NVIDIA entries — those belong to the nvidia-smi path which,
# by construction, already ran and returned nothing if we are here.
if vendor_raw in ("nvidia", "cuda"):
continue
sm_equiv = _VENDOR_SM_EQUIV.get(vendor_raw)
if sm_equiv is None:
# Also probe the device name for a known vendor token (e.g.
# "Apple M3 Max", "AMD Instinct MI300X").
low = name.lower()
for token, val in _VENDOR_SM_EQUIV.items():
if token in low:
sm_equiv = val
vendor_raw = token
break
if sm_equiv is None:
continue
names.append(name or vendor_raw)
if matched_sm is None:
matched_sm = sm_equiv
matched_vendor = vendor_raw
if matched_sm is None or matched_vendor is None:
return None
return HwDetectResult(
sm_equiv=float(matched_sm),
vendor=matched_vendor,
device_names=names,
device_count=len(names),
source=_WHICHLLM_BIN,
)
def detect_non_nvidia_hw(
runner: Optional[object] = None,
) -> Optional[HwDetectResult]:
"""The structured entry point: enumerate non-NVIDIA accelerators via
the optional bounded subprocess, returning a HwDetectResult or None.
None on EVERY non-delivery path (tool absent / failed / timed out /
payload unparseable / no recognised non-NVIDIA device) — the caller
then keeps today's exact behaviour (graceful degrade; safety half).
A real recognised device yields a structured result (delivery half).
Never raises (every failure is caught -> None)."""
try:
return _parse_enumeration(_run_whichllm(runner=runner))
except Exception: # noqa: BLE001 - leaf module: NEVER raise at caller
return None
def detect_non_nvidia_sm(
runner: Optional[object] = None,
) -> Optional[float]:
"""Convenience: just the SM-equivalent the eval path's [C0] SM gate
consumes (None -> graceful degrade). This is the ONLY value that
reaches a decision gate; it NEVER reaches kv-calc."""
res = detect_non_nvidia_hw(runner=runner)
return res.sm_equiv if res is not None else None