#!/usr/bin/env python3 """hardware_check.py — Detect whether this machine can realistically run ComfyUI locally. Improvements over v1: - Multi-GPU detection: scans all NVIDIA / AMD GPUs, picks the best one (most VRAM) - Apple Silicon: detects Rosetta-via-x86_64 false negative; warns instead of misclassifying - Apple generation: defaults to None (unknown) instead of mis-tagging as M1 - WSL2 detection: identifies WSL2 + nvidia-smi situation explicitly - ROCm: prefers `rocm-smi --json` for new ROCm 6.x output - Disk space check: warns if /home or workspace volume has < 25 GB free - PyTorch verification (optional): tries to import torch and check device availability - Windows: prefers PowerShell `Get-CimInstance` over deprecated `wmic` - More accurate VRAM thresholds and verdict reasons Emits a structured JSON report. Exit codes match `verdict`: 0 → ok 1 → marginal 2 → cloud Usage: python3 hardware_check.py [--json] [--check-pytorch] """ from __future__ import annotations import json import os import platform import re import shutil import subprocess import sys from typing import Any # Thresholds (GiB). MIN_VRAM_GB_USABLE = 6 OK_VRAM_GB = 8 GREAT_VRAM_GB = 12 MIN_MAC_RAM_GB = 16 OK_MAC_RAM_GB = 32 MIN_FREE_DISK_GB = 25 # ComfyUI core ~5 GB + one model ~5–24 GB _COMFY_CLI_FLAG = { "nvidia": "--nvidia", "amd": "--amd", "apple-silicon": "--m-series", "intel": None, "comfy-cloud": None, "cpu": "--cpu", } def _run(cmd: list[str], timeout: int = 8) -> str: try: out = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, check=False ) return (out.stdout or "") + (out.stderr or "") except (FileNotFoundError, subprocess.TimeoutExpired, OSError): return "" def is_wsl() -> bool: """Return True when running under Windows Subsystem for Linux.""" if platform.system() != "Linux": return False if "microsoft" in platform.release().lower() or "wsl" in platform.release().lower(): return True try: with open("/proc/version", "r") as fh: return "microsoft" in fh.read().lower() except OSError: return False def is_rosetta() -> bool: """Return True when Python is running translated under Rosetta on Apple Silicon.""" if platform.system() != "Darwin": return False if platform.machine() == "arm64": return False # x86_64 on Darwin — could be Intel Mac or Rosetta. Probe sysctl. out = _run(["sysctl", "-in", "sysctl.proc_translated"]).strip() return out == "1" def detect_nvidia() -> dict | None: """Detect NVIDIA GPUs. Returns the GPU with the most VRAM, plus list of all.""" if not shutil.which("nvidia-smi"): return None out = _run([ "nvidia-smi", "--query-gpu=index,name,memory.total,driver_version", "--format=csv,noheader,nounits", ]) if not out.strip(): return None gpus = [] for line in out.strip().splitlines(): parts = [p.strip() for p in line.split(",")] if len(parts) < 3: continue try: idx = int(parts[0]) name = parts[1] vram_mb = int(parts[2]) except ValueError: continue driver = parts[3] if len(parts) > 3 else "" gpus.append({ "vendor": "nvidia", "index": idx, "name": name, "vram_gb": round(vram_mb / 1024, 1), "driver": driver, }) if not gpus: return None # Pick GPU with most VRAM best = max(gpus, key=lambda g: g["vram_gb"]) if len(gpus) > 1: best["all_gpus"] = gpus return best def detect_rocm() -> dict | None: if not shutil.which("rocm-smi"): return None # Prefer JSON output (new ROCm 6.x) out = _run(["rocm-smi", "--showproductname", "--showmeminfo", "vram", "--json"]) if out.strip().startswith("{"): try: data = json.loads(out) cards = [] for card_id, info in data.items(): if not card_id.startswith("card"): continue name = (info.get("Card series") or info.get("Card model") or info.get("Marketing Name") or "AMD GPU") vram_b = info.get("VRAM Total Memory (B)") or info.get("vram_total_memory_b") or 0 try: vram_b = int(vram_b) except (ValueError, TypeError): vram_b = 0 cards.append({ "vendor": "amd", "name": str(name).strip(), "vram_gb": round(vram_b / (1024**3), 1), "driver": "rocm", }) if cards: best = max(cards, key=lambda c: c["vram_gb"]) if len(cards) > 1: best["all_gpus"] = cards return best except json.JSONDecodeError: pass # Fall back to text parsing out = _run(["rocm-smi", "--showproductname", "--showmeminfo", "vram"]) if not out.strip(): return None name_m = re.search(r"Card (?:series|model|Marketing Name):\s*(.+)", out) vram_m = re.search(r"VRAM Total Memory \(B\):\s*(\d+)", out) vram_gb = round(int(vram_m.group(1)) / (1024**3), 1) if vram_m else 0.0 return { "vendor": "amd", "name": name_m.group(1).strip() if name_m else "AMD GPU", "vram_gb": vram_gb, "driver": "rocm", } def detect_apple_silicon() -> dict | None: if platform.system() != "Darwin": return None if platform.machine() != "arm64": return None chip = _run(["sysctl", "-n", "machdep.cpu.brand_string"]).strip() m = re.search(r"Apple M(\d+)", chip) generation = int(m.group(1)) if m else None mem_bytes = 0 try: mem_bytes = int(_run(["sysctl", "-n", "hw.memsize"]).strip() or 0) except ValueError: pass ram_gb = round(mem_bytes / (1024**3), 1) if mem_bytes else 0.0 # Detect chip variant ("Pro", "Max", "Ultra") — affects performance even at same gen variant = None for v in ("Ultra", "Max", "Pro"): if v in chip: variant = v break return { "vendor": "apple", "name": chip or "Apple Silicon", "generation": generation, "variant": variant, "unified_memory_gb": ram_gb, } def detect_intel_arc() -> dict | None: if platform.system() not in ("Linux", "Windows"): return None if shutil.which("clinfo"): out = _run(["clinfo", "--list"]) if "Intel" in out and ("Arc" in out or "Xe" in out): return {"vendor": "intel", "name": "Intel Arc/Xe", "vram_gb": 0.0} # Windows: try Get-CimInstance if platform.system() == "Windows" and shutil.which("powershell"): out = _run(["powershell", "-NoProfile", "Get-CimInstance Win32_VideoController | Select-Object Name | Format-List"]) if "Intel" in out and ("Arc" in out or "Iris Xe" in out): return {"vendor": "intel", "name": "Intel Arc/Iris Xe", "vram_gb": 0.0} return None def total_system_ram_gb() -> float: sysname = platform.system() if sysname == "Darwin": try: return round(int(_run(["sysctl", "-n", "hw.memsize"]).strip() or 0) / (1024**3), 1) except ValueError: return 0.0 if sysname == "Linux": try: with open("/proc/meminfo", "r") as fh: for line in fh: if line.startswith("MemTotal:"): kb = int(line.split()[1]) return round(kb / (1024**2), 1) except OSError: return 0.0 if sysname == "Windows": if shutil.which("powershell"): out = _run([ "powershell", "-NoProfile", "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory", ]) m = re.search(r"(\d{8,})", out) if m: return round(int(m.group(1)) / (1024**3), 1) # Fall back to wmic for older Windows out = _run(["wmic", "ComputerSystem", "get", "TotalPhysicalMemory"]) m = re.search(r"(\d{6,})", out) if m: return round(int(m.group(1)) / (1024**3), 1) return 0.0 def total_free_disk_gb(path: str = ".") -> float: try: usage = shutil.disk_usage(path) return round(usage.free / (1024**3), 1) except OSError: return 0.0 def check_pytorch_cuda() -> dict | None: """Optional PyTorch availability check. Only run when --check-pytorch is set.""" try: import torch # type: ignore[import-not-found] except Exception as e: return {"available": False, "reason": f"torch not importable: {e}"} info: dict[str, Any] = { "available": True, "torch_version": torch.__version__, } try: info["cuda_available"] = bool(torch.cuda.is_available()) if info["cuda_available"]: info["cuda_device_count"] = torch.cuda.device_count() info["cuda_device_0"] = torch.cuda.get_device_name(0) except Exception: info["cuda_available"] = False try: info["mps_available"] = bool(torch.backends.mps.is_available()) except Exception: info["mps_available"] = False return info def classify(gpu: dict | None, ram_gb: float, free_disk_gb: float, *, wsl: bool, rosetta: bool) -> tuple[str, str, list[str]]: notes: list[str] = [] if rosetta: notes.append( "Detected Python running under Rosetta on Apple Silicon. " "ComfyUI MPS support requires native ARM64 Python — install via " "`brew install python` or arm64 Miniforge, then re-run." ) return "cloud", "comfy-cloud", notes if wsl and gpu and gpu["vendor"] == "nvidia": notes.append("Detected WSL2 + NVIDIA — confirm `nvidia-smi` works in your WSL distro before installing.") if free_disk_gb and free_disk_gb < MIN_FREE_DISK_GB: notes.append( f"Free disk space ({free_disk_gb} GB) is below the {MIN_FREE_DISK_GB} GB recommended minimum. " "ComfyUI core (~5 GB) plus one SDXL model (~6.5 GB) needs space; Flux Dev needs ~24 GB." ) # Host RAM matters even for discrete-GPU systems: ComfyUI swaps model # weights through CPU RAM when shuffling between text encoders / VAE / UNet. # Apple's unified-memory check is handled below so don't double-warn. if ram_gb and ram_gb < 8 and gpu and gpu.get("vendor") != "apple": notes.append( f"System RAM ({ram_gb} GB) is low. ComfyUI swaps model weights through " "host RAM; <8 GB causes severe slowdowns. 16+ GB recommended." ) if gpu is None: notes.append( "No supported accelerator found (NVIDIA CUDA / AMD ROCm / Apple Silicon / Intel Arc)." ) notes.append( "CPU-only ComfyUI works but is unusably slow for modern models — use Comfy Cloud." ) return "cloud", "comfy-cloud", notes if gpu["vendor"] == "apple": gen = gpu.get("generation") variant = gpu.get("variant") mem = gpu.get("unified_memory_gb", 0.0) gen_str = f"M{gen}" if gen else "Apple Silicon" if variant: gen_str += f" {variant}" if mem < MIN_MAC_RAM_GB: notes.append( f"{gen_str} with {mem} GB unified memory — below the {MIN_MAC_RAM_GB} GB practical minimum." ) notes.append("SD1.5 may work; SDXL/Flux will swap or OOM. Recommend Comfy Cloud.") return "cloud", "comfy-cloud", notes if mem < OK_MAC_RAM_GB: notes.append( f"{gen_str} with {mem} GB — SDXL works but slow. Flux/video likely too tight." ) return "marginal", "apple-silicon", notes notes.append(f"{gen_str} with {mem} GB unified memory — good for SDXL/Flux.") return "ok", "apple-silicon", notes if gpu["vendor"] == "intel": notes.append("Intel Arc detected — ComfyUI IPEX support is experimental; Comfy Cloud is more reliable.") return "marginal", "intel", notes # Discrete NVIDIA / AMD vram = gpu.get("vram_gb", 0.0) name = gpu["name"] if vram < MIN_VRAM_GB_USABLE: notes.append( f"{name} has only {vram} GB VRAM — below the {MIN_VRAM_GB_USABLE} GB practical minimum." ) notes.append("Most modern models won't load. Recommend Comfy Cloud.") return "cloud", "comfy-cloud", notes if vram < OK_VRAM_GB: notes.append( f"{name} ({vram} GB VRAM) — SD1.5 works, SDXL tight, Flux/video unlikely." ) return "marginal", gpu["vendor"], notes if vram < GREAT_VRAM_GB: notes.append(f"{name} ({vram} GB VRAM) — SDXL comfortable, Flux possible with optimizations.") return "ok", gpu["vendor"], notes notes.append(f"{name} ({vram} GB VRAM) — can run everything including Flux/video.") return "ok", gpu["vendor"], notes def build_report(*, check_pytorch: bool = False) -> dict: sysname = platform.system() arch = platform.machine() ram_gb = total_system_ram_gb() free_disk_gb = total_free_disk_gb(os.path.expanduser("~")) rosetta = is_rosetta() wsl = is_wsl() gpu = ( detect_nvidia() or detect_rocm() or detect_apple_silicon() or detect_intel_arc() ) # Intel Mac: arm64 detect failed AND no other GPU paths if gpu is None and sysname == "Darwin" and arch != "arm64" and not rosetta: notes = [ "Intel Mac detected — no MPS backend available.", "ComfyUI will fall back to CPU which is unusably slow. Use Comfy Cloud.", ] report = { "os": sysname, "arch": arch, "system_ram_gb": ram_gb, "free_disk_gb": free_disk_gb, "wsl": False, "rosetta": False, "gpu": None, "verdict": "cloud", "recommended_install_path": "comfy-cloud", "comfy_cli_flag": None, "notes": notes, "install_urls": _install_urls(), } if check_pytorch: report["pytorch"] = check_pytorch_cuda() return report verdict, install_path, notes = classify( gpu, ram_gb, free_disk_gb, wsl=wsl, rosetta=rosetta, ) report = { "os": sysname, "arch": arch, "system_ram_gb": ram_gb, "free_disk_gb": free_disk_gb, "wsl": wsl, "rosetta": rosetta, "gpu": gpu, "verdict": verdict, "recommended_install_path": install_path, "comfy_cli_flag": _COMFY_CLI_FLAG.get(install_path), "notes": notes, "install_urls": _install_urls(), } if check_pytorch: report["pytorch"] = check_pytorch_cuda() return report def _install_urls() -> dict: return { "desktop": "https://docs.comfy.org/installation/desktop", "manual": "https://docs.comfy.org/installation/manual_install", "comfy_cli": "https://docs.comfy.org/comfy-cli/getting-started", "cloud": "https://platform.comfy.org", } def main(argv: list[str] | None = None) -> int: import argparse p = argparse.ArgumentParser(description="Check whether this machine can run ComfyUI locally.") p.add_argument("--json", action="store_true", help="Emit machine-readable JSON only") p.add_argument("--check-pytorch", action="store_true", help="Also probe `torch` for CUDA/MPS availability (slower)") args = p.parse_args(argv) report = build_report(check_pytorch=args.check_pytorch) if args.json: print(json.dumps(report, indent=2)) else: print(f"OS: {report['os']} ({report['arch']})") if report.get("wsl"): print("Env: WSL2") if report.get("rosetta"): print("Env: Rosetta (x86_64 Python on Apple Silicon)") print(f"RAM: {report['system_ram_gb']} GB") print(f"Free disk: {report['free_disk_gb']} GB (~/)") if report["gpu"]: g = report["gpu"] if g["vendor"] == "apple": print(f"GPU: {g['name']} — {g.get('unified_memory_gb', 0)} GB unified memory") else: print(f"GPU: {g['name']} — {g.get('vram_gb', 0)} GB VRAM") if g.get("all_gpus") and len(g["all_gpus"]) > 1: print(f" ({len(g['all_gpus'])} GPUs total; using best by VRAM)") else: print("GPU: (none detected)") print(f"Verdict: {report['verdict']} → {report['recommended_install_path']}") if report["comfy_cli_flag"]: print(f" run: comfy --skip-prompt install {report['comfy_cli_flag']}") if report.get("pytorch"): pt = report["pytorch"] if pt.get("available"): line = f"PyTorch: {pt.get('torch_version')}" if pt.get("cuda_available"): line += f" + CUDA ({pt.get('cuda_device_0', '?')})" if pt.get("mps_available"): line += " + MPS" print(line) else: print(f"PyTorch: not available — {pt.get('reason')}") for n in report["notes"]: print(f" • {n}") if report["verdict"] == "ok": return 0 if report["verdict"] == "marginal": return 1 return 2 if __name__ == "__main__": sys.exit(main())