Files
hermes-agent/hermes_platform/host/gpu_adapters.py
Teknium a274d3388f fix: core modules import again on Python 3.11-3.13 (re-quote eager forward references)
#135348 ran ruff's UP037 autofix with ruff.strict.toml's target-version = "py314". On 3.14
annotations are lazy (PEP 649), so stripping quotes is safe there; on 3.11-3.13 a function or
variable annotation is evaluated when the def or statement runs. Every unquoted annotation that
names the enclosing class (`def clone_for_agent(self) -> ContextEngine`), a class bound later in
the module, or a TYPE_CHECKING-only import now raises NameError on import: 300 modules on main
(gateway.config, run_agent, hermes_state, cron.jobs, the Telegram adapter, ...) fail to import
under 3.11, the update path crashes mid-`hermes update` on any install whose interpreter is
older than 3.14, and the e2e-upgrade suite's retry leg fails with
`NameError: name 'ContextEngine' is not defined`.

- Re-quote exactly the 79 annotations (36 files) that name something unbound when they are
  evaluated (including tui_gateway/server.py's `_TurnScopes`, which only reaches that module's
  globals through register() after import); no other text changes.
- agent/turn_scripted_prelude.py: `Generator[...]` gets its third argument back. d76cce4f11
  applied UP043 (default type args, valid only from 3.13) to a module-level alias, so
  `Generator[Y, S]` raises TypeError on 3.11/3.12 and takes gateway.run, agent.conversation_loop
  and tui_gateway.{server,ws,entry} down with it.
- ruff.strict.toml target-version = "py311", the requires-python floor. With it UP037 leaves
  these quotes alone and only strips the two that are genuinely redundant, which this commit
  strips; any future version-gated fix stays valid on every supported Python.
- tests/ci/test_strict_ruff_target.py pins the target to the requires-python floor (red on main).
2026-10-09 14:25:07 -07:00

86 lines
3.5 KiB
Python

"""Windows graphics adapters as DXGI reports them.
Task Manager's "GPU memory" for an adapter is DXGI's dedicated video memory plus shared system
memory, and that sum is what Windows lets the adapter allocate. An integrated GPU has almost no
dedicated memory, so its shared allowance is its real pool: an Intel Arc B390 on a 31.5 GB machine
gets 18 GB. Read through ctypes without environment-variable input or subprocesses.
"""
from __future__ import annotations
import ctypes
import functools
from contextlib import suppress
from ctypes import wintypes
from dataclasses import dataclass
_SOFTWARE_FLAG = 0x2 # DXGI_ADAPTER_FLAG_SOFTWARE (Microsoft Basic Render Driver)
_MAX_ADAPTERS = 16
# vtable slots: IUnknown::Release, IDXGIFactory1::EnumAdapters1, IDXGIAdapter1::GetDesc1.
_RELEASE, _ENUM_ADAPTERS1, _GET_DESC1 = 2, 12, 10
@dataclass(frozen=True)
class Adapter:
description: str
vendor_id: int
dedicated_bytes: int
shared_bytes: int
software: bool
@property
def memory_bytes(self) -> int:
"""What Windows lets this adapter allocate (Task Manager's GPU memory)."""
return self.dedicated_bytes + self.shared_bytes
class _Guid(ctypes.Structure):
_fields_ = [("Data1", wintypes.DWORD), ("Data2", wintypes.WORD), ("Data3", wintypes.WORD),
("Data4", ctypes.c_ubyte * 8)]
class _Luid(ctypes.Structure):
_fields_ = [("LowPart", wintypes.DWORD), ("HighPart", wintypes.LONG)]
class _AdapterDesc1(ctypes.Structure):
_fields_ = [("Description", ctypes.c_wchar * 128), ("VendorId", wintypes.UINT),
("DeviceId", wintypes.UINT), ("SubSysId", wintypes.UINT), ("Revision", wintypes.UINT),
("DedicatedVideoMemory", ctypes.c_size_t), ("DedicatedSystemMemory", ctypes.c_size_t),
("SharedSystemMemory", ctypes.c_size_t), ("AdapterLuid", _Luid), ("Flags", wintypes.UINT)]
_IID_DXGI_FACTORY1 = _Guid(0x770AAE78, 0xF26F, 0x4DBA, (ctypes.c_ubyte * 8)(0xA8, 0x29, 0x25, 0x3C, 0x83, 0xD1, 0xB3, 0x87))
def _method(obj: ctypes.c_void_p, slot: int, *argtypes):
vtable = ctypes.cast(obj, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p)))[0]
return ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p, *argtypes)(vtable[slot])
@functools.cache
def windows_gpu_adapters() -> tuple[Adapter, ...]:
"""Every adapter DXGI enumerates, in its order; empty off Windows or when DXGI can't answer."""
with suppress(OSError, AttributeError):
factory = ctypes.c_void_p()
if ctypes.WinDLL("dxgi").CreateDXGIFactory1(ctypes.byref(_IID_DXGI_FACTORY1), ctypes.byref(factory)) != 0:
return ()
found: list[Adapter] = []
try:
enum = _method(factory, _ENUM_ADAPTERS1, wintypes.UINT, ctypes.POINTER(ctypes.c_void_p))
for index in range(_MAX_ADAPTERS):
adapter = ctypes.c_void_p()
if enum(factory, index, ctypes.byref(adapter)) != 0: # DXGI_ERROR_NOT_FOUND ends the list
break
try:
desc = _AdapterDesc1()
if _method(adapter, _GET_DESC1, ctypes.POINTER(_AdapterDesc1))(adapter, ctypes.byref(desc)) == 0:
found.append(Adapter(desc.Description, desc.VendorId, desc.DedicatedVideoMemory,
desc.SharedSystemMemory, bool(desc.Flags & _SOFTWARE_FLAG)))
finally:
_method(adapter, _RELEASE)(adapter)
finally:
_method(factory, _RELEASE)(factory)
return tuple(found)
return ()