Coverage for src/lilbee/providers/fleet/gpu_backends/apple.py: 100%
21 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Apple Metal GPU utilization via ioreg PerformanceStatistics."""
3from __future__ import annotations
5import re
7from lilbee.providers.fleet.gpu_backends.base import UtilSample, run_smi
9# llama-server --list-devices emits "MTL0: Apple M1 Pro (21845 MiB, ...)" -> prefix
10# "MTL". The build-dependent "Metal" variant is also registered in __init__.py.
11BACKEND_KEY = "MTL"
13_TOOL = "ioreg"
14# Root at the GPU accelerator (AGXAccelerator conforms to IOAccelerator on every
15# Apple Silicon generation) and read its properties. A plain "-d 1 -k
16# PerformanceStatistics" never reaches the GPU node -- it caps traversal at depth
17# 1 and matches a shallow always-zero entry, so the bar read 0% even under load.
18_ARGS = ("-r", "-c", "IOAccelerator", "-d", "1")
19_TIMEOUT_S = 5.0
21# Apple Silicon exposes one integrated GPU; its load is "Device Utilization %".
22_UTIL_RE = re.compile(r'"Device Utilization %"=(\d+)')
25class AppleBackend:
26 """Apple Metal util via ioreg; VRAM stays structural via the orchestrator."""
28 def sample(self, indices: frozenset[int]) -> dict[int, UtilSample]:
29 return _ioreg_samples(indices)
32def _ioreg_output() -> str:
33 """ioreg PerformanceStatistics stdout, or "" when it can't run."""
34 return run_smi(_TOOL, list(_ARGS), _TIMEOUT_S)
37def _parse_ioreg(raw: str, indices: frozenset[int]) -> dict[int, UtilSample]:
38 """Apply the single Apple GPU's utilization to every requested index."""
39 match = _UTIL_RE.search(raw)
40 if match is None:
41 return {}
42 util = int(match.group(1))
43 # Apple has no per-card temperature without sudo, and unified-memory totals
44 # come from the structural probe, so leave temp/VRAM as the 0 sentinel.
45 return {
46 index: UtilSample(
47 index=index,
48 utilization_pct=util,
49 temperature_c=None,
50 free_bytes=0,
51 total_bytes=0,
52 )
53 for index in indices
54 }
57def _ioreg_samples(indices: frozenset[int]) -> dict[int, UtilSample]:
58 return _parse_ioreg(_ioreg_output(), indices)