feat(v2): T2 运维层 hw_profile + llama_server 进程管理
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""硬件档位检测(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', '')}]"
|
||||
)
|
||||
Reference in New Issue
Block a user