Clean up LSP diagnostics across backend and frontend
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).
This commit is contained in:
1 parent
9006c22fde
commit
930e56bd07
33 files changed
+1571
-804
No files matched your search
+29
-11
@@ -9,14 +9,19 @@ Uses NVML (via pynvml) for all operations:
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import pynvml
|
||||
import pynvml as _pynvml_import
|
||||
|
||||
_NVML_AVAILABLE = True
|
||||
except ImportError:
|
||||
_pynvml_import = None
|
||||
_NVML_AVAILABLE = False
|
||||
|
||||
# Aliased as Any so attribute access is not flagged when the import failed.
|
||||
pynvml: Any = _pynvml_import
|
||||
|
||||
log = logging.getLogger("nvcurve.hal.fans")
|
||||
|
||||
# We use fan index 0 (first/primary fan) for all operations.
|
||||
@@ -35,7 +40,7 @@ def get_fan_info(gpu_index: int = 0) -> dict:
|
||||
|
||||
Returns None values on failure.
|
||||
"""
|
||||
out = {
|
||||
out: dict[str, float | None] = {
|
||||
"fan_pct": None,
|
||||
"fan_mode": None,
|
||||
"min_fan_pct": None,
|
||||
@@ -75,7 +80,10 @@ def get_fan_info(gpu_index: int = 0) -> dict:
|
||||
|
||||
def set_fan_speed(gpu_index: int, pct: int) -> tuple[bool, str]:
|
||||
"""Set fan speed to a percentage (0-100) on the primary fan."""
|
||||
pct = max(0, min(100, int(pct)))
|
||||
try:
|
||||
pct = max(0, min(100, int(pct)))
|
||||
except (TypeError, ValueError):
|
||||
return False, "Invalid fan speed"
|
||||
if not _NVML_AVAILABLE:
|
||||
return False, "NVML not available"
|
||||
try:
|
||||
@@ -102,7 +110,9 @@ def reset_fan(gpu_index: int = 0) -> tuple[bool, str]:
|
||||
try:
|
||||
ret = subprocess.run(
|
||||
["nvidia-smi", "-i", str(gpu_index), "-fan", "default"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if ret.returncode == 0:
|
||||
return True, "OK"
|
||||
@@ -123,19 +133,21 @@ def reset_fan(gpu_index: int = 0) -> tuple[bool, str]:
|
||||
return False, f"Failed to reset fan: {exc}"
|
||||
|
||||
|
||||
def get_temp(gpu_index: int = 0) -> Optional[float]:
|
||||
def get_temp(gpu_index: int = 0) -> float | None:
|
||||
"""Read current GPU temperature in °C."""
|
||||
if not _NVML_AVAILABLE:
|
||||
return None
|
||||
try:
|
||||
handle = _get_handle(gpu_index)
|
||||
return float(pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU))
|
||||
return float(
|
||||
pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
|
||||
)
|
||||
except pynvml.NVMLError as exc:
|
||||
log.debug("get_temp: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
|
||||
def interpolate_fan_speed(curve: list[dict], temp_c: float) -> int | None:
|
||||
"""Interpolate target fan speed from a curve at a given temperature.
|
||||
|
||||
curve: list of {temp_c: int, fan_pct: int} sorted by temp_c
|
||||
@@ -144,7 +156,10 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
|
||||
if not curve or len(curve) < 2:
|
||||
return None
|
||||
|
||||
temp = float(temp_c)
|
||||
try:
|
||||
temp = float(temp_c)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# Find the two surrounding points
|
||||
for i in range(len(curve) - 1):
|
||||
@@ -157,7 +172,10 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
|
||||
if t0 <= temp <= t1:
|
||||
fraction = (temp - t0) / (t1 - t0)
|
||||
result = f0 + fraction * (f1 - f0)
|
||||
return max(0, min(100, int(round(result))))
|
||||
try:
|
||||
return max(0, min(100, int(round(result))))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# Outside range: clamp to first or last point
|
||||
if temp <= curve[0]["temp_c"]:
|
||||
@@ -165,7 +183,7 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
|
||||
return max(0, min(100, curve[-1]["fan_pct"]))
|
||||
|
||||
|
||||
def validate_curve(curve: List[dict]) -> tuple[bool, str]:
|
||||
def validate_curve(curve: list[dict]) -> tuple[bool, str]:
|
||||
"""Validate a fan curve.
|
||||
|
||||
Returns (True, "OK") or (False, error_message).
|
||||
|
||||
+38
-20
@@ -1,12 +1,17 @@
|
||||
"""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."""
|
||||
@@ -19,7 +24,10 @@ 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()
|
||||
query_interface(FUNC["EnumPhysicalGPUs"])(ctypes.byref(gpus), ctypes.byref(ngpu))
|
||||
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)
|
||||
@@ -29,7 +37,10 @@ def enumerate_gpus() -> tuple[ctypes.Array, int]:
|
||||
def get_gpu_name(gpu) -> str:
|
||||
"""Return the full name string for a GPU handle."""
|
||||
name_buf = ctypes.create_string_buffer(256)
|
||||
query_interface(FUNC["GetFullName"])(gpu, name_buf)
|
||||
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")
|
||||
|
||||
|
||||
@@ -40,38 +51,45 @@ def discover_gpus() -> list[GpuInfo]:
|
||||
infos = []
|
||||
|
||||
try:
|
||||
import pynvml
|
||||
pynvml.nvmlInit()
|
||||
has_nvml = True
|
||||
import pynvml as _pynvml
|
||||
|
||||
_pynvml.nvmlInit()
|
||||
except Exception:
|
||||
has_nvml = False
|
||||
_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 has_nvml:
|
||||
if pynvml is not None:
|
||||
try:
|
||||
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
|
||||
uuid = pynvml.nvmlDeviceGetUUID(handle)
|
||||
raw_uuid = pynvml.nvmlDeviceGetUUID(handle)
|
||||
# NVML might return bytes
|
||||
if isinstance(uuid, bytes):
|
||||
uuid = uuid.decode('utf-8', errors='ignore')
|
||||
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
|
||||
if isinstance(pci_info.bus, bytes):
|
||||
pci_bus_id = int(pci_info.bus.decode('utf-8', errors='ignore'), 16)
|
||||
# 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 = pci_info.bus
|
||||
except Exception:
|
||||
pass
|
||||
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 has_nvml:
|
||||
try:
|
||||
if pynvml is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
pynvml.nvmlShutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return infos
|
||||
|
||||
|
||||
+75
-38
@@ -11,21 +11,26 @@ that are explicitly specified, leaving others unchanged on hardware.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import Optional
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import pynvml
|
||||
import pynvml as _pynvml_import
|
||||
|
||||
_NVML_AVAILABLE = True
|
||||
except ImportError:
|
||||
_pynvml_import = None
|
||||
_NVML_AVAILABLE = False
|
||||
|
||||
# Aliased as Any so attribute access is not flagged when the import failed.
|
||||
pynvml: Any = _pynvml_import
|
||||
|
||||
log = logging.getLogger("nvcurve.hal.limits")
|
||||
|
||||
# ── NVML library / handle helpers ─────────────────────────────────────────────
|
||||
|
||||
_nvml_lib: Optional[ctypes.CDLL] = None
|
||||
_nvml_lib: ctypes.CDLL | None = None
|
||||
|
||||
|
||||
def _nvml_cdll() -> ctypes.CDLL:
|
||||
@@ -34,12 +39,12 @@ def _nvml_cdll() -> ctypes.CDLL:
|
||||
if _nvml_lib is not None:
|
||||
return _nvml_lib
|
||||
# Prefer to reuse the library already loaded by pynvml to avoid dlopen races.
|
||||
for attr in ("nvml", "_nvml"): # attribute name varies by pynvml version
|
||||
for attr in ("nvml", "_nvml"): # attribute name varies by pynvml version
|
||||
mod = getattr(pynvml, attr, None)
|
||||
lib = getattr(mod, "_lib", None) or getattr(mod, "_nvmlLib", None)
|
||||
if lib is not None:
|
||||
_nvml_lib = lib
|
||||
return _nvml_lib
|
||||
return lib
|
||||
_nvml_lib = ctypes.CDLL("libnvidia-ml.so.1")
|
||||
return _nvml_lib
|
||||
|
||||
@@ -53,9 +58,10 @@ def _get_handle(gpu_index: int):
|
||||
|
||||
# ── Power limit ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_power_limit(gpu_index: int = 0) -> dict:
|
||||
"""Return dict with power_limit_w, default_power_limit_w, min_power_limit_w, max_power_limit_w."""
|
||||
out = {
|
||||
out: dict[str, int | None] = {
|
||||
"power_limit_w": None,
|
||||
"default_power_limit_w": None,
|
||||
"min_power_limit_w": None,
|
||||
@@ -71,8 +77,8 @@ def get_power_limit(gpu_index: int = 0) -> dict:
|
||||
try:
|
||||
default = pynvml.nvmlDeviceGetPowerManagementDefaultLimit(handle)
|
||||
out["default_power_limit_w"] = default // 1000
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
log.debug("nvmlDeviceGetPowerManagementDefaultLimit: %s", exc)
|
||||
except Exception as exc:
|
||||
log.warning("get_power_limit: %s", exc)
|
||||
return out
|
||||
@@ -89,7 +95,8 @@ def set_power_limit(limit_w: int, gpu_index: int = 0) -> tuple[bool, str]:
|
||||
|
||||
ret = subprocess.run(
|
||||
["nvidia-smi", "-i", str(gpu_index), "-pl", str(limit_w)],
|
||||
capture_output=True, text=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if ret.returncode == 0:
|
||||
return True, "OK"
|
||||
@@ -111,34 +118,38 @@ def set_power_limit(limit_w: int, gpu_index: int = 0) -> tuple[bool, str]:
|
||||
# pynvml (nvidia-ml-py ≥ 12) exposes c_nvmlClockOffset_t and nvmlClockOffset_v1
|
||||
# as ctypes objects; we use them when available and fall back to our own definition.
|
||||
|
||||
|
||||
class _ClockOffset(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("version", ctypes.c_uint),
|
||||
("type", ctypes.c_uint), # nvmlClockType_t
|
||||
("pstate", ctypes.c_uint), # nvmlPstates_t
|
||||
("version", ctypes.c_uint),
|
||||
("type", ctypes.c_uint), # nvmlClockType_t
|
||||
("pstate", ctypes.c_uint), # nvmlPstates_t
|
||||
("clockOffsetMHz", ctypes.c_int),
|
||||
]
|
||||
|
||||
|
||||
_CLOCK_OFFSET_VER = (1 << 24) | ctypes.sizeof(_ClockOffset) # = 0x01000010 (16 bytes)
|
||||
|
||||
# NVML clock-type constants (same values as pynvml).
|
||||
_NVML_CLOCK_GRAPHICS = 0
|
||||
_NVML_CLOCK_MEM = 2
|
||||
_NVML_CLOCK_MEM = 2
|
||||
|
||||
|
||||
def _make_clock_offset(clock_type: int, pstate: int = 0, offset_mhz: int = 0) -> ctypes.Structure:
|
||||
def _make_clock_offset(
|
||||
clock_type: int, pstate: int = 0, offset_mhz: int = 0
|
||||
) -> ctypes.Structure:
|
||||
"""Return a populated nvmlClockOffset_t struct, using pynvml's type when available."""
|
||||
if hasattr(pynvml, "c_nvmlClockOffset_t") and hasattr(pynvml, "nvmlClockOffset_v1"):
|
||||
info = pynvml.c_nvmlClockOffset_t()
|
||||
info.version = pynvml.nvmlClockOffset_v1
|
||||
info.type = clock_type
|
||||
info.pstate = pstate
|
||||
info.version = pynvml.nvmlClockOffset_v1
|
||||
info.type = clock_type
|
||||
info.pstate = pstate
|
||||
info.clockOffsetMHz = offset_mhz
|
||||
return info
|
||||
info = _ClockOffset()
|
||||
info.version = _CLOCK_OFFSET_VER
|
||||
info.type = clock_type
|
||||
info.pstate = pstate
|
||||
info.version = _CLOCK_OFFSET_VER
|
||||
info.type = clock_type
|
||||
info.pstate = pstate
|
||||
info.clockOffsetMHz = offset_mhz
|
||||
return info
|
||||
|
||||
@@ -158,7 +169,7 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
|
||||
Keys: gpc_offset_mhz, mem_offset_mhz (both int or None on failure).
|
||||
Calls nvmlDeviceGetClockOffsets once per clock domain (GRAPHICS, MEM).
|
||||
"""
|
||||
out = {"gpc_offset_mhz": None, "mem_offset_mhz": None}
|
||||
out: dict[str, int | None] = {"gpc_offset_mhz": None, "mem_offset_mhz": None}
|
||||
if not _NVML_AVAILABLE:
|
||||
return out
|
||||
try:
|
||||
@@ -167,11 +178,15 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
|
||||
# Try pynvml wrapper first (nvidia-ml-py ≥ 12 exposes it correctly).
|
||||
# Fall back to ctypes-direct if pynvml doesn't have it.
|
||||
_pynvml_get = getattr(pynvml, "nvmlDeviceGetClockOffsets", None)
|
||||
fn_get = _try_nvml_fn("nvmlDeviceGetClockOffsets") if _pynvml_get is None else None
|
||||
fn_get = (
|
||||
_try_nvml_fn("nvmlDeviceGetClockOffsets") if _pynvml_get is None else None
|
||||
)
|
||||
|
||||
used_new_api = False
|
||||
for clock_type, key in ((_NVML_CLOCK_GRAPHICS, "gpc_offset_mhz"),
|
||||
(_NVML_CLOCK_MEM, "mem_offset_mhz")):
|
||||
for clock_type, key in (
|
||||
(_NVML_CLOCK_GRAPHICS, "gpc_offset_mhz"),
|
||||
(_NVML_CLOCK_MEM, "mem_offset_mhz"),
|
||||
):
|
||||
info = _make_clock_offset(clock_type, pstate=0)
|
||||
try:
|
||||
if _pynvml_get is not None:
|
||||
@@ -184,7 +199,9 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
|
||||
out[key] = int(info.clockOffsetMHz)
|
||||
used_new_api = True
|
||||
else:
|
||||
log.debug("nvmlDeviceGetClockOffsets(type=%d) returned %d", clock_type, rc)
|
||||
log.debug(
|
||||
"nvmlDeviceGetClockOffsets(type=%d) returned %d", clock_type, rc
|
||||
)
|
||||
except Exception as exc:
|
||||
log.debug("nvmlDeviceGetClockOffsets(type=%d): %s", clock_type, exc)
|
||||
|
||||
@@ -200,7 +217,9 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
|
||||
if hasattr(pynvml, "nvmlDeviceGetMemClkVfOffset"):
|
||||
try:
|
||||
res = pynvml.nvmlDeviceGetMemClkVfOffset(handle)
|
||||
out["mem_offset_mhz"] = int(res[0] if isinstance(res, (list, tuple)) else res)
|
||||
out["mem_offset_mhz"] = int(
|
||||
res[0] if isinstance(res, (list, tuple)) else res
|
||||
)
|
||||
except Exception as exc:
|
||||
log.debug("nvmlDeviceGetMemClkVfOffset: %s", exc)
|
||||
|
||||
@@ -210,8 +229,8 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
|
||||
|
||||
|
||||
def set_clock_offsets(
|
||||
gpc_offset_mhz: Optional[int] = None,
|
||||
mem_offset_mhz: Optional[int] = None,
|
||||
gpc_offset_mhz: int | None = None,
|
||||
mem_offset_mhz: int | None = None,
|
||||
gpu_index: int = 0,
|
||||
) -> tuple[bool, str]:
|
||||
"""Set clock offsets (MHz) for the specified domains only.
|
||||
@@ -235,20 +254,35 @@ def set_clock_offsets(
|
||||
domains.append((_NVML_CLOCK_MEM, mem_offset_mhz))
|
||||
|
||||
_pynvml_set = getattr(pynvml, "nvmlDeviceSetClockOffsets", None)
|
||||
fn_set = _try_nvml_fn("nvmlDeviceSetClockOffsets") if _pynvml_set is None else None
|
||||
fn_set = (
|
||||
_try_nvml_fn("nvmlDeviceSetClockOffsets") if _pynvml_set is None else None
|
||||
)
|
||||
|
||||
if _pynvml_set is not None or fn_set is not None:
|
||||
all_ok = True
|
||||
for clock_type, offset in domains:
|
||||
info = _make_clock_offset(clock_type, pstate=0, offset_mhz=offset)
|
||||
try:
|
||||
rc = _pynvml_set(handle, ctypes.byref(info)) if _pynvml_set else fn_set(handle, ctypes.byref(info))
|
||||
if _pynvml_set is not None:
|
||||
rc = _pynvml_set(handle, ctypes.byref(info))
|
||||
elif fn_set is not None:
|
||||
rc = fn_set(handle, ctypes.byref(info))
|
||||
else:
|
||||
break
|
||||
if rc != 0:
|
||||
log.debug("nvmlDeviceSetClockOffsets(type=%d) returned %d — trying fallback", clock_type, rc)
|
||||
log.debug(
|
||||
"nvmlDeviceSetClockOffsets(type=%d) returned %d — trying fallback",
|
||||
clock_type,
|
||||
rc,
|
||||
)
|
||||
all_ok = False
|
||||
break
|
||||
except Exception as exc:
|
||||
log.debug("nvmlDeviceSetClockOffsets(type=%d): %s — trying fallback", clock_type, exc)
|
||||
log.debug(
|
||||
"nvmlDeviceSetClockOffsets(type=%d): %s — trying fallback",
|
||||
clock_type,
|
||||
exc,
|
||||
)
|
||||
all_ok = False
|
||||
break
|
||||
if all_ok:
|
||||
@@ -257,12 +291,16 @@ def set_clock_offsets(
|
||||
|
||||
# Deprecated per-domain fallback (works on Blackwell/driver 590.x).
|
||||
errs = []
|
||||
if gpc_offset_mhz is not None and hasattr(pynvml, "nvmlDeviceSetGpcClkVfOffset"):
|
||||
if gpc_offset_mhz is not None and hasattr(
|
||||
pynvml, "nvmlDeviceSetGpcClkVfOffset"
|
||||
):
|
||||
try:
|
||||
pynvml.nvmlDeviceSetGpcClkVfOffset(handle, gpc_offset_mhz)
|
||||
except Exception as exc:
|
||||
errs.append(f"GPC: {exc}")
|
||||
if mem_offset_mhz is not None and hasattr(pynvml, "nvmlDeviceSetMemClkVfOffset"):
|
||||
if mem_offset_mhz is not None and hasattr(
|
||||
pynvml, "nvmlDeviceSetMemClkVfOffset"
|
||||
):
|
||||
try:
|
||||
pynvml.nvmlDeviceSetMemClkVfOffset(handle, mem_offset_mhz)
|
||||
except Exception as exc:
|
||||
@@ -278,6 +316,7 @@ def set_clock_offsets(
|
||||
|
||||
# ── Range queries ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_mem_offset_range(gpu_index: int = 0) -> dict:
|
||||
"""Return the min/max allowed memory clock offset (MHz).
|
||||
|
||||
@@ -285,7 +324,7 @@ def get_mem_offset_range(gpu_index: int = 0) -> dict:
|
||||
Uses nvmlDeviceGetMemClkMinMaxVfOffset; falls back to observed RTX values.
|
||||
"""
|
||||
# Observed RTX 5090 defaults (NvAPI GetClockBoostRanges says -1000/+3000).
|
||||
out = {"min_mem_offset_mhz": -2000, "max_mem_offset_mhz": 3000}
|
||||
out: dict[str, int] = {"min_mem_offset_mhz": -2000, "max_mem_offset_mhz": 3000}
|
||||
if not _NVML_AVAILABLE:
|
||||
return out
|
||||
try:
|
||||
@@ -317,5 +356,3 @@ def get_mem_offset_range(gpu_index: int = 0) -> dict:
|
||||
except Exception as exc:
|
||||
log.debug("get_mem_offset_range: %s", exc)
|
||||
return out
|
||||
|
||||
|
||||
+61
-30
@@ -2,18 +2,20 @@
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..nvapi.bootstrap import nvcall_raw
|
||||
from ..nvapi.constants import FUNC, CT_SIZE, CT_BASE, CT_STRIDE, CT_DELTA_OFF, CT_POINTS
|
||||
from ..nvapi.constants import CT_BASE, CT_DELTA_OFF, CT_SIZE, CT_STRIDE, FUNC
|
||||
from ..nvapi.types import SnapshotInfo
|
||||
from .vfcurve import read_clock_table_raw, get_boost_mask
|
||||
from .vfcurve import get_boost_mask, read_clock_table_raw
|
||||
|
||||
log = logging.getLogger("nvcurve.hal.snapshot")
|
||||
|
||||
|
||||
def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optional[str]:
|
||||
def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> str | None:
|
||||
"""Save the current ClockBoostTable to disk.
|
||||
|
||||
Writes both a binary .bin file and a human-readable .json metadata file.
|
||||
@@ -25,13 +27,21 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
|
||||
print(f"Failed to read ClockBoostTable: {err}")
|
||||
return None
|
||||
|
||||
os.makedirs(snapshot_dir, exist_ok=True)
|
||||
try:
|
||||
os.makedirs(snapshot_dir, exist_ok=True)
|
||||
except OSError as exc:
|
||||
print(f"Failed to create snapshot dir {snapshot_dir}: {exc}")
|
||||
return None
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
bin_path = os.path.join(snapshot_dir, f"clock_boost_table_{ts}.bin")
|
||||
meta_path = os.path.join(snapshot_dir, f"clock_boost_table_{ts}.json")
|
||||
|
||||
with open(bin_path, "wb") as f:
|
||||
f.write(raw)
|
||||
try:
|
||||
with open(bin_path, "wb") as f:
|
||||
f.write(raw)
|
||||
except OSError as exc:
|
||||
print(f"Failed to write snapshot {bin_path}: {exc}")
|
||||
return None
|
||||
|
||||
offsets = []
|
||||
max_entries = (len(raw) - CT_BASE) // CT_STRIDE
|
||||
@@ -48,10 +58,14 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
|
||||
"offsets_kHz": offsets,
|
||||
"nonzero_offsets": sum(1 for o in offsets if o != 0),
|
||||
}
|
||||
with open(meta_path, "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
try:
|
||||
with open(meta_path, "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
except OSError as exc:
|
||||
print(f"Failed to write snapshot metadata {meta_path}: {exc}")
|
||||
return None
|
||||
|
||||
print(f"Snapshot saved:")
|
||||
print("Snapshot saved:")
|
||||
print(f" Binary: {bin_path}")
|
||||
print(f" Metadata: {meta_path}")
|
||||
print(f" Size: {len(raw)} bytes")
|
||||
@@ -65,20 +79,22 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
|
||||
|
||||
def _prune_snapshots(snapshot_dir: str, max_snapshots: int) -> None:
|
||||
"""Delete oldest snapshots (both .bin and .json) to stay within max_snapshots."""
|
||||
bins = sorted(
|
||||
f for f in os.listdir(snapshot_dir) if f.endswith(".bin")
|
||||
) # oldest first (lexicographic = chronological for our timestamp format)
|
||||
# Oldest first (lexicographic = chronological for our timestamp format).
|
||||
try:
|
||||
bins = sorted(f for f in os.listdir(snapshot_dir) if f.endswith(".bin"))
|
||||
except OSError:
|
||||
return
|
||||
excess = len(bins) - max_snapshots
|
||||
for fname in bins[:excess]:
|
||||
stem = fname[:-4] # strip .bin
|
||||
for ext in (".bin", ".json"):
|
||||
try:
|
||||
os.remove(os.path.join(snapshot_dir, stem + ext))
|
||||
except OSError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
log.debug("Could not remove %s: %s", stem + ext, exc)
|
||||
|
||||
|
||||
def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
|
||||
def restore(gpu, snapshot_dir: str, filepath: str | None = None) -> bool:
|
||||
"""Restore a ClockBoostTable snapshot from disk.
|
||||
|
||||
If no filepath is given, uses the most recent snapshot in snapshot_dir.
|
||||
@@ -88,10 +104,14 @@ def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
print(f"No snapshots found in {snapshot_dir}")
|
||||
return False
|
||||
bins = sorted(
|
||||
[f for f in os.listdir(snapshot_dir) if f.endswith(".bin")],
|
||||
reverse=True,
|
||||
)
|
||||
try:
|
||||
bins = sorted(
|
||||
[f for f in os.listdir(snapshot_dir) if f.endswith(".bin")],
|
||||
reverse=True,
|
||||
)
|
||||
except OSError:
|
||||
print(f"No snapshots found in {snapshot_dir}")
|
||||
return False
|
||||
if not bins:
|
||||
print(f"No snapshot .bin files in {snapshot_dir}")
|
||||
return False
|
||||
@@ -101,8 +121,12 @@ def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
|
||||
print(f"Snapshot file not found: {filepath}")
|
||||
return False
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
raw = f.read()
|
||||
except OSError as exc:
|
||||
print(f"Failed to read snapshot {filepath}: {exc}")
|
||||
return False
|
||||
|
||||
if len(raw) != CT_SIZE:
|
||||
print(f"Snapshot size mismatch: expected {CT_SIZE}, got {len(raw)}")
|
||||
@@ -134,8 +158,13 @@ def list_snapshots(snapshot_dir: str) -> list[SnapshotInfo]:
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
return []
|
||||
|
||||
try:
|
||||
fnames = sorted(os.listdir(snapshot_dir), reverse=True)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for fname in sorted(os.listdir(snapshot_dir), reverse=True):
|
||||
for fname in fnames:
|
||||
if not fname.endswith(".json"):
|
||||
continue
|
||||
meta_path = os.path.join(snapshot_dir, fname)
|
||||
@@ -143,13 +172,15 @@ def list_snapshots(snapshot_dir: str) -> list[SnapshotInfo]:
|
||||
with open(meta_path) as f:
|
||||
meta = json.load(f)
|
||||
bin_path = meta.get("file", meta_path.replace(".json", ".bin"))
|
||||
results.append(SnapshotInfo(
|
||||
filepath=bin_path,
|
||||
timestamp=meta.get("timestamp", ""),
|
||||
gpu=meta.get("gpu", ""),
|
||||
nonzero_offsets=meta.get("nonzero_offsets", 0),
|
||||
size=meta.get("size", 0),
|
||||
))
|
||||
results.append(
|
||||
SnapshotInfo(
|
||||
filepath=bin_path,
|
||||
timestamp=meta.get("timestamp", ""),
|
||||
gpu=meta.get("gpu", ""),
|
||||
nonzero_offsets=meta.get("nonzero_offsets", 0),
|
||||
size=meta.get("size", 0),
|
||||
)
|
||||
)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
continue
|
||||
|
||||
|
||||
Reference in new issue
Block a user