"""硬件档位检测(runtime 运维层,纯标准库)。 把真实机器映射到三档保守模板之一: - gpu12 : 约 ≥12GB 显存(NVIDIA / Vulkan 可探测) -> ngl 99, ctx 32768 - gpu8 : 约 ≥8GB 显存 -> ngl 14, ctx 16384 - cpu : 无独显或探测失败(保守兜底) -> ngl 0, ctx 8192 探测来源:nvidia-smi(NVIDIA 显存)优先;其次 vulkaninfo(AMD/Intel/通用, 只能判断是否存在 Vulkan 设备,无法可靠拿到显存 -> 保守回退 cpu,并在结果标注 probe:"conservative")。总系统内存仅作为 cpu 档提示参考,不作为分档依据。 任何探测失败都回退到 cpu 保守档,保证不崩、可离线运行(D5 / D8)。 """ from __future__ import annotations import shutil import subprocess from typing import Any, Callable, Dict, List, Optional # 三档硬件模板(保守默认,可被 config.tiers 手动覆盖) TIER_SPECS: Dict[str, Dict[str, Any]] = { "gpu12": {"tier": "gpu12", "ngl": 99, "ctx": 32768, "kv_quant": "q8_0"}, "gpu8": {"tier": "gpu8", "ngl": 14, "ctx": 16384, "kv_quant": "q8_0"}, "cpu": {"tier": "cpu", "ngl": 0, "ctx": 8192, "kv_quant": "q8_0"}, } _GPU12_THRESHOLD_GB = 12.0 _GPU8_THRESHOLD_GB = 8.0 def _run(cmd: List[str], timeout: float = 10.0, runner: Optional[Callable[[List[str], float], subprocess.CompletedProcess]] = None ) -> Optional[subprocess.CompletedProcess]: """执行命令并捕获输出;失败/超时返回 None(不抛异常)。""" if runner is not None: try: return runner(cmd, timeout) except Exception: return None try: return subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, creationflags=subprocess.CREATE_NO_WINDOW, ) except Exception: return None def nvidia_vram_gb(runner: Optional[Callable[..., subprocess.CompletedProcess]] = None) -> Optional[float]: """通过 nvidia-smi 读取显存总量(GB);无 NVIDIA 返回 None。""" exe = shutil.which("nvidia-smi") if not exe: return None out = _run([exe, "--query-gpu=memory.total", "--format=csv,noheader,nounits"], runner=runner) if out is None or out.returncode != 0 or not out.stdout.strip(): return None try: # 多卡取最大值(第一行也接受,但保守起见取最大以保证模板内存够用) vals = [float(v.strip()) for v in out.stdout.strip().splitlines() if v.strip().isdigit()] if not vals: return None return max(vals) / 1024.0 except Exception: return None def vulkan_present(runner: Optional[Callable[..., subprocess.CompletedProcess]] = None) -> bool: """检测是否存在 Vulkan 设备(无法可靠拿显存 -> 只用于判定非 cpu 的候选)。""" exe = shutil.which("vulkaninfo") if not exe: return False out = _run([exe, "--summary"], timeout=15.0, runner=runner) if out is None or out.returncode != 0: return False low = out.stdout.lower() # 出现 deviceName 且非 "llvmpipe"/"software" 视为有真实设备 return ("devicename" in low or "gpu" in low) and "llvmpipe" not in low and "lavapipe" not in low def pick_tier(vram_gb: Optional[float]) -> str: """按显存选择档位;None/未知 -> cpu 保守档。""" if vram_gb is None: return "cpu" if vram_gb >= _GPU12_THRESHOLD_GB: return "gpu12" if vram_gb >= _GPU8_THRESHOLD_GB: return "gpu8" return "cpu" def detect(override: Optional[Dict[str, Any]] = None, runner: Optional[Callable[..., subprocess.CompletedProcess]] = None) -> Dict[str, Any]: """检测并返回当前档位规格。 override(可选):{"tier": "gpu12"} 强制指定档位;或覆盖单个字段如 {"ctx": 16384}。 返回形如 {"tier": "cpu", "ngl": 0, "ctx": 8192, "kv_quant": "q8_0", "probe": "nvidia|vulkan|cpu|override", "note": str} """ if override and override.get("tier") in TIER_SPECS: spec = dict(TIER_SPECS[override["tier"]]) spec.update({k: v for k, v in override.items() if k in spec}) spec["probe"] = "override" spec["note"] = f"手动指定档位 {override['tier']}" return spec vram = nvidia_vram_gb(runner=runner) probe = "nvidia" if vram is None: if vulkan_present(runner=runner): probe = "vulkan" note = "检测到 Vulkan 设备但无法读取显存,按保守档 cpu 运行(可在 config 手动覆盖 tier)" else: probe = "cpu" note = "未检测到 GPU,按 cpu 档运行(-ngl 0,速度受限)" else: note = f"nvidia-smi 探测显存 {vram:.1f}GB" tier = pick_tier(vram) spec = dict(TIER_SPECS[tier]) spec["probe"] = probe spec["note"] = note if probe != "nvidia" else f"{note} -> 档位 {tier}" return spec def tier_spec(tier: str) -> Dict[str, Any]: """返回指定档位的规格副本(供 config.tiers 兜底)。""" if tier not in TIER_SPECS: raise ValueError(f"未知硬件档位: {tier}(支持: {sorted(TIER_SPECS)})") return dict(TIER_SPECS[tier]) def detect_summary() -> str: """人类可读的检测摘要(setup_runtime / serve 启动时打印)。""" spec = detect() return ( f"硬件档位: {spec['tier']} (ngl={spec['ngl']}, ctx={spec['ctx']}, " f"kv_quant={spec['kv_quant']}) [{spec.get('note', '')}]" )