#!/usr/bin/env python3 """CMP 170HX single-card checker - ONE card per run, no queue. Per run: wait for card -> auto serial -> VBIOS backup -> PCIe link/AER/config checks -> retrain + rescan tests -> dmesg scrape -> cmpunlocker verify.sh (read-only) -> opt-in cmpunlocker unlock (install.sh, y/N or --unlock; a successful install skips VRAM stress and asks for a cold reboot, then you re-run this script for the final Gen2 x16 PCIe check) -> optional 5-min Vulkan VRAM stress (last step, ask yes/no) -> cards//report.json. Scoring: FAIL on no-device/link-down/lost-after-rescan, and on a final post-unlock link other than Gen2 x16; WARN on correctable AER/Xid/width< x16/ verify issues/failed unlock install. Exit codes: 0 = PASS/WARN, 1 = FAIL, 2 = no card, 3 = preflight blocking. """ import argparse import json import sys import time import cxlib as X from cxlib import C, ask, detect_gpus, err, info, now_iso, ok, sanitize, step, warn # ---------------------------------------------------------------- scoring def score(rep): fails, warns = [], [] ver = rep.get("cmpunlocker_verify", {}) unlocked = ver.get("status") == "unlocked-ok" if not rep.get("bdf"): fails.append("no CMP 170HX device detected on the bus") link = rep.get("link", {}) if rep.get("bdf"): if link.get("lspci_rc") not in (0, None): fails.append("lspci could not read the device") if link.get("lnksta_width") in ("x0", "x1", None) or not link.get("lnksta_speed"): fails.append(f"link down or untrained: {link.get('lnksta_raw') or 'no LnkSta'}") width = link.get("lnksta_width") gen = link.get("lnksta_gen") if unlocked: # Post-unlock final acceptance: only Gen2 x16 passes. The pre-unlock # width/Gen1 warns are suppressed here - the error replaces them. if width != "x16" or gen is None or gen < 2: fails.append(f"[ERROR] final PCIe link after unlock is " f"{link.get('lnksta_speed') or '?'} {width or '?'} " f"- expected Gen2 x16") else: if width and width != "x16": warns.append(f"link width {width} (capacitor mod expects x16 - mod likely " f"missing/incomplete on this card)") if gen == 1: warns.append("link negotiated Gen1 only (cmpunlocker Gen2 patch/service not active?)") if rep.get("rescan", {}).get("ok") is False: fails.append("device lost after bus rescan") if rep.get("retrain", {}).get("ok") is False: fails.append("link did not survive retrain") aer = rep.get("aer_after", {}) aer_before = rep.get("aer_before", {}) if (aer.get("ce_active") or aer.get("parent_ce_active") or (aer.get("aer_correctable_sum", 0) > aer_before.get("aer_correctable_sum", 0))): warns.append("correctable AER errors present/incremented") if aer.get("ue_active"): fails.append(f"uncorrectable AER errors active: {aer['ue_active']}") dm = rep.get("dmesg", {}) if dm.get("errors"): warns.append(f"{len(dm['errors'])} kernel error line(s) related to this card (see dmesg.log)") vu = rep.get("vulkan_test", {}) if vu.get("requested") and vu.get("ran"): if vu.get("tool_errors"): warns.append("errors reported during Vulkan VRAM stress") if dm.get("errors"): warns.append("kernel errors around VRAM stress window") if not rep.get("bios", {}).get("ok"): warns.append("VBIOS backup failed/incomplete") if ver.get("ran") and ver.get("status") not in ("unlocked-ok",): warns.append(f"cmpunlocker verify status: {ver.get('status')}") unlock = rep.get("unlock", {}) if unlock.get("attempted") and not unlock.get("ok"): warns.append("cmpunlocker install failed (see install_unlocker.log)") result = "FAIL" if fails else ("WARN" if warns else "PASS") return result, fails, warns # ---------------------------------------------------------------- card detect def wait_for_gpu(): """Wait for a single CMP 170HX (with rescan retries). Returns BDF or None.""" while True: for _ in range(5): bdfs = detect_gpus() if bdfs: if len(bdfs) > 1: warn(f"multiple CMP 170HX devices present: {', '.join(bdfs)} - using {bdfs[0]}") return bdfs[0] time.sleep(1) ans = ask("CMP 170HX not detected - insert/reseat the card, press Enter to rescan " "(q=quit)", "") if ans.lower().startswith("q"): return None if not X.rescan_bus(): warn("bus rescan not permitted (no root/sudo) - reseat the card physically") # ---------------------------------------------------------------- one card def process_card(args): step("Card check") bdf = wait_for_gpu() if not bdf: err("no card detected - nothing to do") return None devid = "" for line in X.run(["lspci", "-D", "-s", bdf, "-nn"])["out"].splitlines(): if "[10de:" in line: i = line.index("[10de:") devid = line[i + 1:i + 10].rstrip("]") ok(f"device found at {bdf} ({devid})") nf = X.nvflash_bin(args) serial, serial_src = X.get_serial(nf, args) folder = X.CARDS_DIR / sanitize(serial) folder.mkdir(parents=True, exist_ok=True) info(f"folder: {folder}") rep = { "serial": serial, "serial_source": serial_src, "started_at": now_iso(), "bdf": bdf, "device_id": devid, "host": X.run(["uname", "-n"])["out"].strip(), "kernel": X.run(["uname", "-r"])["out"].strip(), } step("VBIOS backup") rep["bios"] = X.save_bios(nf, folder / "bios.rom", args) (ok if rep["bios"]["ok"] else warn)(f"bios: {rep['bios']['method'] or 'FAILED'} " f"{rep['bios']['size']} bytes - {rep['bios']['note']}") step("PCIe link + config") link, lspci_txt = X.check_link(bdf) rep["link"] = link (folder / "lspci.txt").write_text(lspci_txt) (folder / "config_space.hex").write_text(X.dump_config(bdf)) info(f"LnkSta: {link.get('lnksta_raw') or 'n/a'} (setpci gen={link.get('setpci_current_gen')})") rep["aer_before"] = X.aer_snapshot(bdf) step("retrain + rescan tests") rep["retrain"] = X.retrain_test(bdf) info(f"retrain: ok={rep['retrain']['ok']} - {rep['retrain']['note']}") rep["rescan"] = X.rescan_test(bdf) info(f"rescan: ok={rep['rescan']['ok']} - {rep['rescan']['note']}") rep["aer_after"] = X.aer_snapshot(bdf) link2, lspci2 = X.check_link(bdf) rep["link_after"] = link2 if lspci2: (folder / "lspci_after.txt").write_text(lspci2) step("kernel log scrape") dm = X.dmesg_capture(bdf) rep["dmesg"] = dm (folder / "dmesg.log").write_text("\n".join(dm.get("related", []))) info(f"{dm.get('note')} (source: {dm.get('source')})") for e in dm.get("errors", [])[-5:]: warn(e.strip()[:160]) step("nvidia-smi + cmpunlocker verify") rep["nvidia_smi"] = X.nvidia_smi_info() rep["cmpunlocker_verify"] = X.run_verify_unlocker() v = rep["cmpunlocker_verify"] (folder / "verify_unlocker.log").write_text( "\n".join(v.get("tail", [])) or (v.get("note") or "not run")) info("verify: " + (f"rc={v.get('rc')} -> {v.get('status')}" if v.get("ran") else f"skipped - {v.get('note')}")) step("cmpunlocker unlock (install)") unlock_installed = False if rep["cmpunlocker_verify"].get("status") == "unlocked-ok": info("memory already unlocked - skipping install") rep["unlock"] = {"attempted": False, "ok": False, "note": "already unlocked"} elif getattr(args, "demo", False): # --demo never prompts: force the unlock install path off. rep["unlock"] = {"attempted": False, "ok": False, "note": "demo"} info("unlock install skipped (demo mode)") elif not X.INSTALL_SH.exists(): rep["unlock"] = {"attempted": False, "ok": False, "note": "harness/cmpunlocker not cloned - run --preflight"} warn("harness/cmpunlocker not cloned - run --preflight (unlock install skipped)") elif args.unlock or ask("Run cmpunlocker install.sh now? (y/N)", "n").lower().startswith("y"): X.update_cmpunlocker() rep["unlock"] = X.run_installer(folder / "install_unlocker.log") unlock_installed = bool(rep["unlock"].get("attempted") and rep["unlock"].get("ok")) if unlock_installed: # Kernel modules are replaced but the card is not unlocked until the # cold reboot - VRAM stress against the half-applied state is unsafe. rep["vulkan_test"] = {"requested": False, "ran": False, "note": "skipped - unlock installed, reboot first"} else: warn("cmpunlocker install failed - continuing with the Vulkan step") else: rep["unlock"] = {"attempted": False, "ok": False, "note": "operator declined"} info("unlock install skipped") step("optional Vulkan VRAM test (last step)") if unlock_installed: info("vulkan test skipped - unlock installed, cold reboot first") elif args.vulkan == "never": rep["vulkan_test"] = {"requested": False, "ran": False, "note": "disabled via --vulkan never"} elif args.vulkan == "always": rep["vulkan_test"] = X.run_vulkan_test() else: if ask("Run 5-min Vulkan VRAM stress test now? (y/N)", "n").lower().startswith("y"): rep["vulkan_test"] = X.run_vulkan_test() else: rep["vulkan_test"] = {"requested": False, "ran": False, "note": "operator declined"} info("vulkan test skipped") result, fails, warns = score(rep) rep.update(finished_at=now_iso(), result=result, failures=fails, warnings=warns) (folder / "report.json").write_text(json.dumps(rep, indent=2)) print() if result == "PASS": ok(f"{serial}: PASS") elif result == "WARN": warn(f"{serial}: WARN") else: err(f"{serial}: FAIL") for f in fails: err(" FAIL: " + f) for w in warns: warn(" WARN: " + w) info(f"report: {folder / 'report.json'}") return rep # ---------------------------------------------------------------- demo def demo(args): """Validate report pipeline without hardware: writes cards/DEMO-*.""" step("Demo mode (no hardware)") args.vulkan = "never" args.serial = "DEMO-SERIAL-001" args.skip_bios = True orig_wait = globals()["wait_for_gpu"] orig_detect, orig_check = X.detect_gpus, X.check_link orig_aer, orig_dm, orig_cfg = X.aer_snapshot, X.dmesg_capture, X.dump_config orig_retrain, orig_rescan = X.retrain_test, X.rescan_test orig_smi, orig_verify = X.nvidia_smi_info, X.run_verify_unlocker orig_update, orig_installer = X.update_cmpunlocker, X.run_installer globals()["wait_for_gpu"] = lambda: "0000:0a:00.0" X.detect_gpus = lambda: ["0000:0a:00.0"] X.check_link = lambda b: ({"lnksta_speed": "5GT/s", "lnksta_width": "x4", "lnksta_gen": 2, "lnkcap_speed": "8GT/s", "lnkcap_width": "x16", "lnksta_raw": "LnkSta: Speed 5GT/s, Width x4, TrErr-", "setpci_current_gen": 2, "lspci_rc": 0}, "demo lspci output\n") X.aer_snapshot = lambda b: {"aer_sysfs_correctable": None, "aer_sysfs_fatal": None, "aer_correctable_sum": 0, "ce_active": [], "ue_active": [], "parent_bdf": None, "parent_ce_active": []} X.dmesg_capture = lambda b: {"source": "demo", "related": ["demo line"], "errors": [], "note": "demo"} X.dump_config = lambda b: "demo config space dump\n" X.retrain_test = lambda b: {"attempted": True, "ok": True, "note": "x4 -> x4", "width_before": "x4", "width_after": "x4"} X.rescan_test = lambda b: {"attempted": True, "ok": True, "note": "re-enumerated"} X.nvidia_smi_info = lambda: {"available": False} X.run_verify_unlocker = lambda: {"ran": False, "note": "demo"} X.update_cmpunlocker = lambda: {"ok": True, "note": "demo"} X.run_installer = lambda log_path: {"attempted": False, "ok": False, "rc": None, "note": "demo", "log": str(log_path)} try: rep = process_card(args) finally: globals()["wait_for_gpu"] = orig_wait X.detect_gpus, X.check_link = orig_detect, orig_check X.aer_snapshot, X.dmesg_capture, X.dump_config = orig_aer, orig_dm, orig_cfg X.retrain_test, X.rescan_test = orig_retrain, orig_rescan X.nvidia_smi_info, X.run_verify_unlocker = orig_smi, orig_verify X.update_cmpunlocker, X.run_installer = orig_update, orig_installer return rep # ---------------------------------------------------------------- main def main(): ap = argparse.ArgumentParser(description="CMP 170HX single-card checker (one card per run)") ap.add_argument("--preflight", action="store_true", help="tool check + package install (requires sudo) + harness clone, then exit") ap.add_argument("--demo", action="store_true", help="run pipeline with fake hardware (self-test)") ap.add_argument("--nvflash", help="path to nvflash binary") ap.add_argument("--serial", help="override serial (skips nvflash auto-read)") ap.add_argument("--skip-bios", action="store_true", help="skip VBIOS backup") ap.add_argument("--vulkan", choices=("ask", "always", "never"), default="ask", help="5-min Vulkan VRAM test policy (default ask, runs as last step)") ap.add_argument("--unlock", action="store_true", help="run cmpunlocker install.sh without prompting (default: ask y/N)") ap.add_argument("--no-install", action="store_true", help="do not auto-install packages (skips the sudo precondition)") ap.add_argument("--no-clone", action="store_true", help="do not clone harness repos") args = ap.parse_args() pf = X.preflight(auto_install=not args.no_install, do_clone=not args.no_clone) if args.preflight: return 0 if pf.get("preflight_ok") else 3 if args.demo: demo(args) return 0 X.CARDS_DIR.mkdir(exist_ok=True) rep = process_card(args) if not rep: return 2 return 1 if rep.get("result") == "FAIL" else 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: print() warn("interrupted") sys.exit(130)