Skip to content

Commit 5893142

Browse files
Merge pull request #1025 from yasinBursali/fix/gpu-detailed-apple-silicon
fix(dashboard-api): wire Apple Silicon into /api/gpu/detailed
2 parents 81740c9 + e893b1e commit 5893142

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

dream-server/extensions/services/dashboard-api/routers/gpu.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from gpu import (
1616
decode_gpu_assignment,
1717
get_gpu_info_amd_detailed,
18+
get_gpu_info_apple,
1819
get_gpu_info_nvidia_detailed,
1920
read_gpu_topology,
2021
)
@@ -39,8 +40,29 @@
3940
# Internal helpers
4041
# ============================================================================
4142

43+
def _apple_info_to_individual(info: GPUInfo) -> IndividualGPU:
44+
"""Wrap an Apple Silicon aggregate GPUInfo as a single IndividualGPU entry."""
45+
return IndividualGPU(
46+
index=0,
47+
uuid="apple-unified-0", # 15 chars; GPUCard.jsx calls uuid.slice(-8)
48+
name=info.name,
49+
memory_used_mb=info.memory_used_mb,
50+
memory_total_mb=info.memory_total_mb,
51+
memory_percent=info.memory_percent,
52+
utilization_percent=info.utilization_percent,
53+
temperature_c=info.temperature_c,
54+
power_w=info.power_w,
55+
assigned_services=[],
56+
)
57+
58+
4259
def _get_raw_gpus(gpu_backend: str) -> Optional[list[IndividualGPU]]:
4360
"""Return per-GPU list from the appropriate backend, with fallback."""
61+
if gpu_backend == "apple":
62+
info = get_gpu_info_apple()
63+
if info is None:
64+
return None
65+
return [_apple_info_to_individual(info)]
4466
if gpu_backend == "amd":
4567
return get_gpu_info_amd_detailed()
4668
result = get_gpu_info_nvidia_detailed()

dream-server/extensions/services/dashboard-api/tests/test_gpu_detailed.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_gpu_info_nvidia_detailed,
1313
read_gpu_topology,
1414
)
15+
from models import GPUInfo
1516

1617

1718
# ============================================================================
@@ -337,3 +338,80 @@ def test_history_maxlen_rolls_over(self):
337338
gpu_mod._GPU_HISTORY.clear()
338339
for item in saved:
339340
gpu_mod._GPU_HISTORY.append(item)
341+
342+
343+
# ============================================================================
344+
# _get_raw_gpus — Apple Silicon dispatch (routers/gpu.py)
345+
# ============================================================================
346+
347+
348+
def _sample_apple_gpu_info() -> GPUInfo:
349+
return GPUInfo(
350+
name="Apple M3 Max",
351+
memory_used_mb=24000,
352+
memory_total_mb=65536,
353+
memory_percent=36.6,
354+
utilization_percent=0,
355+
temperature_c=0,
356+
power_w=None,
357+
memory_type="unified",
358+
gpu_backend="apple",
359+
)
360+
361+
362+
class TestGetRawGpusApple:
363+
def test_apple_returns_single_entry(self, monkeypatch):
364+
"""Apple backend wraps the single GPUInfo into a one-element IndividualGPU list."""
365+
import routers.gpu as gpu_mod
366+
monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: _sample_apple_gpu_info())
367+
368+
result = gpu_mod._get_raw_gpus("apple")
369+
assert result is not None
370+
assert len(result) == 1
371+
g = result[0]
372+
assert g.index == 0
373+
assert len(g.uuid) >= 8 # GPUCard.jsx calls uuid.slice(-8)
374+
assert g.name == "Apple M3 Max"
375+
assert g.memory_used_mb == 24000
376+
assert g.memory_total_mb == 65536
377+
assert g.memory_percent == 36.6
378+
assert g.utilization_percent == 0
379+
assert g.temperature_c == 0
380+
assert g.power_w is None
381+
assert g.assigned_services == []
382+
383+
def test_apple_returns_none_when_detection_fails(self, monkeypatch):
384+
"""Detection returning None propagates as None — endpoint will raise 503."""
385+
import routers.gpu as gpu_mod
386+
monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: None)
387+
388+
assert gpu_mod._get_raw_gpus("apple") is None
389+
390+
391+
class TestGpuDetailedEndpointApple:
392+
def test_endpoint_returns_apple_aggregate(self, monkeypatch, test_client):
393+
"""/api/gpu/detailed with GPU_BACKEND=apple returns 200 with single-GPU aggregate."""
394+
import routers.gpu as gpu_mod
395+
# Bypass the 3 s TTL cache so this test sees fresh data.
396+
gpu_mod._detailed_cache["expires"] = 0.0
397+
gpu_mod._detailed_cache["value"] = None
398+
399+
monkeypatch.setenv("GPU_BACKEND", "apple")
400+
monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: _sample_apple_gpu_info())
401+
monkeypatch.setattr(gpu_mod, "decode_gpu_assignment", lambda: None)
402+
403+
try:
404+
response = test_client.get("/api/gpu/detailed", headers=test_client.auth_headers)
405+
assert response.status_code == 200
406+
body = response.json()
407+
assert body["backend"] == "apple"
408+
assert body["gpu_count"] == 1
409+
assert len(body["gpus"]) == 1
410+
assert body["gpus"][0]["name"] == "Apple M3 Max"
411+
assert body["gpus"][0]["index"] == 0
412+
assert len(body["gpus"][0]["uuid"]) >= 8
413+
assert body["aggregate"]["name"] == "Apple M3 Max"
414+
assert body["aggregate"]["gpu_backend"] == "apple"
415+
finally:
416+
gpu_mod._detailed_cache["expires"] = 0.0
417+
gpu_mod._detailed_cache["value"] = None

0 commit comments

Comments
 (0)