Files
HVideoTool/hvideotool/core/torch_info.py
T

220 lines
9.6 KiB
Python

"""Diagnose the PyTorch / CUDA situation so the UI can explain *why* it's on CPU.
Pure (no Qt). ``gather()`` is the heavy part — it imports torch and shells out to
``nvidia-smi`` — so call it off the GUI thread. ``analyze()`` is fast formatting on
the gathered dict and figures out the most likely cause + concrete fix.
"""
from __future__ import annotations
import re
import shutil
import subprocess
import sys
# pip indexes for the CUDA builds (see README). cu121 needs a driver with CUDA >= 12.1.
CUDA_WHEELS = {
"cu121": "https://download.pytorch.org/whl/cu121",
"cu118": "https://download.pytorch.org/whl/cu118",
}
_NO_WINDOW = 0x08000000 if sys.platform == "win32" else 0 # CREATE_NO_WINDOW
def _run_nvidia_smi() -> dict:
"""Probe the NVIDIA driver/GPU via nvidia-smi. Never raises."""
out: dict = {"found": False, "gpus": [], "driver_version": None, "cuda_driver": None}
exe = shutil.which("nvidia-smi")
if not exe and sys.platform == "win32":
candidate = r"C:\Windows\System32\nvidia-smi.exe"
exe = candidate if shutil.os.path.isfile(candidate) else None
if not exe:
return out
# GPU names + driver version (robust CSV form).
try:
r = subprocess.run(
[exe, "--query-gpu=name,driver_version", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,
)
if r.returncode == 0:
out["found"] = True
for line in r.stdout.strip().splitlines():
parts = [p.strip() for p in line.split(",")]
if parts and parts[0]:
out["gpus"].append(parts[0])
if len(parts) > 1 and parts[1]:
out["driver_version"] = parts[1]
except Exception:
return out
# Max CUDA version the driver supports (only in the plain header).
try:
r2 = subprocess.run(
[exe], capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,
)
m = re.search(r"CUDA Version:\s*([\d.]+)", r2.stdout)
if m:
out["cuda_driver"] = m.group(1)
except Exception:
pass
return out
def gather() -> dict:
"""Collect torch + NVIDIA facts. Never raises — missing torch/GPU are valid."""
info: dict = {
"installed": False,
"version": None, # torch.__version__ (e.g. "2.12.0+cpu")
"built_cuda": None, # torch.version.cuda (None on a CPU-only build)
"cuda_available": False,
"device_name": None, # the active GPU's name, if any
"import_error": None,
# filled by nvidia-smi:
"nvidia_smi": False,
"gpus": [],
"driver_version": None,
"cuda_driver": None,
}
try:
import torch
except Exception as exc:
info["import_error"] = str(exc)
else:
info["installed"] = True
info["version"] = getattr(torch, "__version__", None)
try:
info["built_cuda"] = torch.version.cuda
except Exception:
info["built_cuda"] = None
try:
info["cuda_available"] = bool(torch.cuda.is_available())
except Exception:
info["cuda_available"] = False
if info["cuda_available"]:
try:
info["device_name"] = torch.cuda.get_device_name(0)
except Exception:
info["device_name"] = None
smi = _run_nvidia_smi()
info["nvidia_smi"] = smi["found"]
info["gpus"] = smi["gpus"]
info["driver_version"] = smi["driver_version"]
info["cuda_driver"] = smi["cuda_driver"]
return info
def _ver_tuple(v: str | None) -> tuple[int, ...]:
try:
return tuple(int(x) for x in str(v).split(".")[:2])
except (ValueError, AttributeError):
return ()
def recommend_channel(info: dict) -> str:
"""Pick the pip CUDA wheel index that matches the driver (cu118 for older)."""
cd = _ver_tuple(info.get("cuda_driver"))
if cd and cd < (12, 1):
return "cu118"
return "cu121"
def install_command(channel: str = "cu121") -> str:
"""The pip commands to (re)install the chosen CUDA build.
Targets the **running interpreter** (``sys.executable -m pip``) so the command
hits the same venv that runs the app — not whatever ``pip`` is on PATH. (A common
trap: running bare ``pip`` in a global shell while torch lives in the project venv.)
"""
index = CUDA_WHEELS.get(channel, CUDA_WHEELS["cu121"])
py = sys.executable or "python"
q = f'"{py}"' if " " in py else py
return (
f"{q} -m pip uninstall -y torch torchvision\n"
f"{q} -m pip install torch torchvision --index-url {index}"
)
def analyze(info: dict) -> dict:
"""Turn the raw facts into {summary, details[list], steps, command}."""
installed = info.get("installed")
cuda = info.get("cuda_available")
built = info.get("built_cuda")
gpus = info.get("gpus") or []
smi = info.get("nvidia_smi")
driver = info.get("driver_version")
cuda_driver = info.get("cuda_driver")
channel = recommend_channel(info)
command = install_command(channel)
if not installed:
build_str = "не установлен"
elif built:
build_str = f"CUDA {built}"
else:
build_str = "CPU-only (+cpu)"
gpu_str = (
", ".join(gpus) if gpus
else ("не обнаружена" if smi or info.get("nvidia_smi") is False else "nvidia-smi не найден")
)
if not gpus and not smi:
gpu_str = "nvidia-smi не найден (нет драйвера NVIDIA?)"
details = [
f"PyTorch: {info.get('version') or 'не установлен'}",
f"Сборка PyTorch: {build_str}",
f"CUDA доступна в PyTorch: {'да' if cuda else 'нет'}",
f"Видеокарта (nvidia-smi): {gpu_str}",
f"Драйвер NVIDIA: {driver or '—'}",
f"Макс. CUDA драйвера: {cuda_driver or '—'}",
f"Интерпретатор (venv): {sys.executable}",
]
if info.get("import_error"):
details.append(f"Ошибка импорта torch: {info['import_error']}")
if not installed:
summary = "PyTorch не установлен — детекция YOLO и DeepMosaics идут на CPU."
steps = ("Установите PyTorch (CUDA-сборку, если есть NVIDIA-видеокарта):\n\n"
+ command + "\n\nДля YOLO также: pip install -e \".[yolo]\"")
elif cuda:
gpu = info.get("device_name") or (gpus[0] if gpus else "GPU")
summary = f"Всё в порядке: PyTorch использует CUDA. Активный GPU: {gpu}."
steps = "GPU уже задействован — ничего делать не нужно."
elif not built: # CPU-only torch build — the usual case
if gpus:
summary = (
"Главная причина: установлена CPU-сборка PyTorch (+cpu) — она физически "
f"не умеет в CUDA. Видеокарта ({gpus[0]}) и драйвер {driver or '?'} на месте, "
"поэтому достаточно переустановить PyTorch со сборкой CUDA."
)
steps = ("Переустановите PyTorch под CUDA, затем перезапустите приложение:\n\n"
+ command)
else:
summary = (
"Установлена CPU-сборка PyTorch (+cpu), и видеокарта NVIDIA не обнаружена "
"(nvidia-smi не отвечает). Либо нет NVIDIA GPU, либо не установлен драйвер."
)
steps = ("1. Проверьте видеокарту и драйвер: в консоли выполните nvidia-smi\n"
"2. Если NVIDIA GPU есть — переустановите PyTorch под CUDA:\n\n"
+ command +
"\n\nБез NVIDIA GPU всё работает на CPU — просто медленнее.")
else: # CUDA-enabled torch build, but CUDA still not available
if not gpus:
summary = (
f"PyTorch собран под CUDA {built}, но видеокарта/драйвер NVIDIA не найдены. "
"Скорее всего не установлен драйвер NVIDIA или нет GPU."
)
steps = "Установите свежий драйвер NVIDIA и перезапустите. Проверка: nvidia-smi."
elif cuda_driver and _ver_tuple(cuda_driver) < _ver_tuple(built):
summary = (
f"Драйвер поддерживает CUDA {cuda_driver}, а PyTorch собран под CUDA {built} — "
"версия драйвера слишком старая."
)
steps = ("Вариант A — обновите драйвер NVIDIA (рекомендуется).\n"
"Вариант B — поставьте PyTorch под CUDA вашего драйвера:\n\n" + command)
else:
summary = (
f"PyTorch собран под CUDA {built}, GPU ({gpus[0]}) есть, но CUDA недоступна — "
"вероятен конфликт версий или повреждённая установка."
)
steps = "Переустановите PyTorch под CUDA:\n\n" + command
return {"summary": summary, "details": details, "steps": steps, "command": command}