Files
projectAIpopular/runtime/hw_profile.py
T

141 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""硬件档位检测(runtime 运维层,纯标准库)。
把真实机器映射到三档保守模板之一:
- gpu12 : 约 ≥12GB 显存(NVIDIA / Vulkan 可探测) -> ngl 99, ctx 32768
- gpu8 : 约 ≥8GB 显存 -> ngl 14, ctx 16384
- cpu : 无独显或探测失败(保守兜底) -> ngl 0, ctx 8192
探测来源:nvidia-smiNVIDIA 显存)优先;其次 vulkaninfoAMD/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', '')}]"
)