* Studio: show Apple GPU temperature and power in the GPU monitor (macOS) The GPU monitor on Apple Silicon always showed -- for Temperature and Power: the MLX branch of get_gpu_utilization() hardcoded None because ioreg's AGXAccelerator PerformanceStatistics carries neither metric. Add utils/hardware/apple.py, mirroring macmon's no-sudo approach: - Temperature: average of the AppleSMC "Tg*" float keys via the AppleSMCKeysEndpoint user client (ctypes/IOKit, macOS 14+). - Power: IOReport "Energy Model" group, "GPU Energy" channels; each poll diffs the energy counter against the previous poll's sample, so the value is the average wattage over the polling window. The first poll only sets the baseline and returns None. Both readers latch to None on first failure and never raise, so non-Mac platforms and locked-down hosts keep the previous behavior. * Sample IOReport with the subscribed channels descriptor for PR #6187 IOReportCreateSubscription writes the channel descriptor that later samples must use; sampling with the original requested group can return no Energy Model entries on hosts that normalize the channel set, leaving power_draw_w null after the baseline. Use the subscribed descriptor (matching macmon) and fall back to the requested channels if the OS leaves it unset. --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: danielhanchen <23090290+danielhanchen@users.noreply.github.com>
74 lines
2.3 KiB
Python
74 lines
2.3 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Tests for Apple Silicon GPU sensors (SMC temperature + IOReport power)."""
|
|
|
|
import ctypes
|
|
import platform
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from utils.hardware import apple
|
|
|
|
_IS_APPLE_SILICON = platform.system() == "Darwin" and platform.machine() == "arm64"
|
|
|
|
|
|
class TestFourcc:
|
|
def test_roundtrip(self):
|
|
for key in ("#KEY", "Tg0D", "flt "):
|
|
assert apple._fourcc_str(apple._fourcc(key)) == key
|
|
|
|
def test_known_value(self):
|
|
# "flt " FourCC, same constant macmon uses.
|
|
assert apple._fourcc("flt ") == 1718383648
|
|
|
|
|
|
class TestWatts:
|
|
def test_millijoules(self):
|
|
assert apple._watts(2000, "mJ", 2.0) == pytest.approx(1.0)
|
|
|
|
def test_microjoules(self):
|
|
assert apple._watts(5_000_000, "uJ", 1.0) == pytest.approx(5.0)
|
|
|
|
def test_nanojoules(self):
|
|
assert apple._watts(1_500_000_000, "nJ", 1.0) == pytest.approx(1.5)
|
|
|
|
def test_unknown_unit_returns_none(self):
|
|
assert apple._watts(1000, "J", 1.0) is None
|
|
|
|
def test_zero_elapsed_returns_none(self):
|
|
assert apple._watts(1000, "mJ", 0.0) is None
|
|
|
|
|
|
class TestAverageValidTemps:
|
|
def test_averages_and_rounds(self):
|
|
assert apple._average_valid_temps([40.0, 50.0, 60.05]) == 50.0
|
|
|
|
def test_filters_invalid(self):
|
|
assert apple._average_valid_temps([-1.0, 0.0, 151.0, 42.0]) == 42.0
|
|
|
|
def test_empty_returns_none(self):
|
|
assert apple._average_valid_temps([]) is None
|
|
assert apple._average_valid_temps([0.0, 200.0]) is None
|
|
|
|
|
|
class TestSmcStructLayout:
|
|
def test_key_data_matches_smc_protocol_size(self):
|
|
# The AppleSMC user client rejects calls whose struct size differs.
|
|
assert ctypes.sizeof(apple._SMCKeyData) == 80
|
|
|
|
|
|
@pytest.mark.skipif(not _IS_APPLE_SILICON, reason = "requires Apple Silicon")
|
|
class TestLiveSensors:
|
|
def test_gpu_temperature_in_plausible_range(self):
|
|
temp = apple.read_gpu_temperature_c()
|
|
assert temp is not None
|
|
assert 0.0 < temp <= 150.0
|
|
|
|
def test_gpu_power_after_baseline(self):
|
|
apple.read_gpu_power_w() # first call only sets the baseline
|
|
time.sleep(0.3)
|
|
power = apple.read_gpu_power_w()
|
|
assert power is not None
|
|
assert power >= 0.0
|