Backend (nvcurve/): - hal/fans.py, hal/limits.py, hal/gpu.py: replace conditional pynvml imports with the established 'pynvml: Any = _pynvml_import' pattern (fixes ~50 'possibly unbound' errors); type the result dicts; guard query_interface() results; explicit uuid/pci-bus parsing (int, hex convention documented); modernize Optional[T] -> T | None - cli.py: fix 'curve_state' possibly-unbound and snap_path None handling in cmd_setup; wrap unchecked int()/open()/makedirs() calls in try/except with clean CLI errors; add module logger for silent except-pass blocks; raise ... from exc; fix unused loop vars and set-comprehension - hal/snapshot.py: filepath: str | None; wrap all file ops; sorted imports; remove unused CT_POINTS import - daemon.py: extract 0o666 to _SOCKET_MODE constant (intentional for /run sockets) with nosemgrep - server.py: nosemgrep for Python 3.7-compat false positive (project requires >= 3.12); log previously-swallowed exception - profiles/native.py, profiles/apply.py: wrap file ops and int(k) profile-key parsing; sorted imports; modernize typing Frontend (frontend/src): - Add .js extensions to all relative imports (standard TS-ESM; Vite resolves .js -> .ts) - React.FormEvent (deprecated in React 19 types) -> React.SubmitEvent - catch (e: any) -> catch (e: unknown) + instanceof Error narrowing - React-hooks: move ref writes from render into effects; convert viewport reset to render-phase state adjustment; split selectPoint(index, multi) into selectPoint + togglePoint (no flag argument); remove non-null assertion - Static inline styles -> Tailwind classes (dynamic positioning/cursor styles kept) - Remove non-standard 'container' option from scrollIntoView (browsers ignore unknown options) which had orphaned a @ts-expect-error - Object.fromEntries for Map -> Record conversion Tooling: - .gitignore: ignore .codegraph/ local tool data Verified: tsc --noEmit, vite production build, python imports, and full LSP scan (0 errors/warnings in both projects).
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""GPU discovery and initialization."""
|
|
|
|
import contextlib
|
|
import ctypes
|
|
import logging
|
|
import sys
|
|
from typing import Any
|
|
|
|
from ..nvapi.bootstrap import query_interface
|
|
from ..nvapi.constants import FUNC
|
|
from ..nvapi.types import GpuInfo
|
|
|
|
log = logging.getLogger("nvcurve.hal.gpu")
|
|
|
|
|
|
def init_nvapi() -> None:
|
|
"""Initialize NvAPI. Must be called before any GPU operations."""
|
|
init_fn = query_interface(FUNC["Initialize"], nargs=0)
|
|
if not init_fn or init_fn() != 0:
|
|
raise RuntimeError("NvAPI_Initialize failed")
|
|
|
|
|
|
def enumerate_gpus() -> tuple[ctypes.Array, int]:
|
|
"""Return (gpu_handles_array, count). Exits if no GPUs found."""
|
|
gpus = (ctypes.c_void_p * 64)()
|
|
ngpu = ctypes.c_int32()
|
|
enum_fn = query_interface(FUNC["EnumPhysicalGPUs"])
|
|
if enum_fn is None:
|
|
raise RuntimeError("NvAPI function EnumPhysicalGPUs not available")
|
|
enum_fn(ctypes.byref(gpus), ctypes.byref(ngpu))
|
|
if ngpu.value == 0:
|
|
print("No NVIDIA GPUs found")
|
|
sys.exit(1)
|
|
return gpus, ngpu.value
|
|
|
|
|
|
def get_gpu_name(gpu) -> str:
|
|
"""Return the full name string for a GPU handle."""
|
|
name_buf = ctypes.create_string_buffer(256)
|
|
fn = query_interface(FUNC["GetFullName"])
|
|
if fn is None:
|
|
raise RuntimeError("NvAPI function GetFullName not available")
|
|
fn(gpu, name_buf)
|
|
return name_buf.value.decode(errors="replace")
|
|
|
|
|
|
def discover_gpus() -> list[GpuInfo]:
|
|
"""Initialize NvAPI and NVML, and return a list of GpuInfo for all physical GPUs."""
|
|
init_nvapi()
|
|
gpus, count = enumerate_gpus()
|
|
infos = []
|
|
|
|
try:
|
|
import pynvml as _pynvml
|
|
|
|
_pynvml.nvmlInit()
|
|
except Exception:
|
|
_pynvml = None
|
|
# Aliased as Any so attribute access is not flagged when the import failed.
|
|
pynvml: Any = _pynvml
|
|
|
|
for i in range(count):
|
|
name = get_gpu_name(gpus[i])
|
|
uuid = None
|
|
pci_bus_id = None
|
|
if pynvml is not None:
|
|
try:
|
|
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
|
|
raw_uuid = pynvml.nvmlDeviceGetUUID(handle)
|
|
# NVML might return bytes
|
|
if isinstance(raw_uuid, bytes):
|
|
uuid = raw_uuid.decode("utf-8", errors="ignore")
|
|
elif raw_uuid is not None:
|
|
uuid = str(raw_uuid)
|
|
pci_info = pynvml.nvmlDeviceGetPciInfo(handle)
|
|
# Parse something like "00000000:01:00.0" -> bus is 1.
|
|
# PCI bus numbers are hex by convention (pynvml's field is an
|
|
# int; the str/bytes branches are defensive).
|
|
bus = pci_info.bus
|
|
if isinstance(bus, bytes):
|
|
pci_bus_id = int(bus.decode("utf-8", errors="ignore"), 16)
|
|
elif isinstance(bus, str):
|
|
pci_bus_id = int(bus, 16)
|
|
else:
|
|
pci_bus_id = int(bus)
|
|
except Exception as exc:
|
|
log.debug("NVML query for GPU %d failed: %s", i, exc)
|
|
infos.append(GpuInfo(name=name, index=i, uuid=uuid, pci_bus_id=pci_bus_id))
|
|
|
|
if pynvml is not None:
|
|
with contextlib.suppress(Exception):
|
|
pynvml.nvmlShutdown()
|
|
|
|
return infos
|
|
|
|
|
|
def get_gpu(index: int = 0):
|
|
"""Initialize NvAPI, enumerate GPUs, and return the handle for `index`.
|
|
|
|
Also returns the GPU name as a convenience.
|
|
Returns (handle, name).
|
|
"""
|
|
init_nvapi()
|
|
gpus, count = enumerate_gpus()
|
|
if index >= count:
|
|
print(f"GPU index {index} out of range (found {count} GPU(s))")
|
|
sys.exit(1)
|
|
gpu = gpus[index]
|
|
name = get_gpu_name(gpu)
|
|
return gpu, name
|