Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11", "3.12", "3.13"]
fail-fast: false

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🏎️ Dyno — llama.cpp Auto-Tuner & Benchmark

**Dyno** is an open-source CLI that auto-tunes and benchmarks [llama.cpp](https://github.com/ggml-org/llama.cpp) / [ik_llama.cpp](https://github.com/ikawrakow/ik_llama.cpp) inference on NVIDIA GPUs, producing reproducible, shareable results.
**Dyno** is an open-source CLI that auto-tunes and benchmarks [llama.cpp](https://github.com/ggml-org/llama.cpp) / [ik_llama.cpp](https://github.com/ikawrakow/ik_llama.cpp) inference on NVIDIA and Apple Silicon GPUs, producing reproducible, shareable results.

```bash
pipx install llama-dyno
Expand Down Expand Up @@ -34,8 +34,8 @@ dyno report ~/Downloads/my-model.q4_k_m.gguf
## Prerequisites

- **Python 3.11+**
- **NVIDIA GPU** with drivers + CUDA
- **llama-bench** binary from [llama.cpp](https://github.com/ggml-org/llama.cpp) or [ik_llama.cpp](https://github.com/ikawrakow/ik_llama.cpp)
- **A GPU**: NVIDIA (drivers + CUDA) or Apple Silicon (M-series, uses Metal + unified memory)
- **llama-bench** binary from [llama.cpp](https://github.com/ggml-org/llama.cpp) or [ik_llama.cpp](https://github.com/ikawrakow/ik_llama.cpp) — built with the matching backend (CUDA or Metal)

Install llama.cpp:

Expand Down Expand Up @@ -182,7 +182,7 @@ pytest

- GUI
- Multi-GPU
- Non-NVIDIA backends (AMD, Intel, Apple Silicon)
- AMD (ROCm) / Intel backends (planned)
- Server hosting for results

## License
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ build-backend = "hatchling.build"
[project]
name = "llama-dyno"
version = "1.1.0"
description = "Auto-tune and benchmark llama.cpp / ik_llama.cpp inference on NVIDIA GPUs"
description = "Auto-tune and benchmark llama.cpp / ik_llama.cpp inference on NVIDIA and Apple Silicon GPUs"
readme = "README.md"
requires-python = ">=3.11"
license = "Apache-2.0"
authors = [
{ name = "Flowdesk Systems", email = "lachy@flowdesk.systems" },
]
keywords = ["llama.cpp", "benchmark", "tuning", "gpu", "nvidia", "inference"]
keywords = ["llama.cpp", "benchmark", "tuning", "gpu", "nvidia", "apple-silicon", "metal", "inference"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
Expand Down
3 changes: 2 additions & 1 deletion src/llama_dyno/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ def detect():
table.add_column("Value")

table.add_row("GPU", hw.gpu_name)
table.add_row("VRAM", f"{hw.vram_total_mib} MiB")
mem_label = "Unified memory" if hw.gpu_name.startswith("Apple") else "VRAM"
table.add_row(mem_label, f"{hw.vram_total_mib} MiB")
table.add_row("Driver", hw.driver_version)
table.add_row("CUDA", hw.cuda_version or "N/A")
table.add_row("CPU", hw.cpu_name)
Expand Down
29 changes: 29 additions & 0 deletions src/llama_dyno/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ def _detect_ram() -> int:
return 0


def _apple_silicon_gpu() -> tuple[str, int] | None:
"""Detect an Apple Silicon GPU as (name, usable memory MiB), else None.

Apple GPUs share unified memory with the CPU — there is no discrete VRAM, so
we report total RAM as the pool the GPU can draw from (macOS lets Metal use
most of it).
# ponytail: unified mem ≈ total RAM; refine via Metal recommendedMaxWorkingSetSize if OOM heuristics misfire
"""
if platform.system() != "Darwin" or platform.machine() != "arm64":
return None
chip = ""
try:
out = subprocess.run(
["sysctl", "-n", "machdep.cpu.brand_string"],
capture_output=True, text=True, timeout=5,
)
chip = out.stdout.strip()
except Exception:
pass
return chip or "Apple Silicon GPU", _detect_ram()


def _find_binary(name: str) -> str | None:
"""Find a binary in PATH. Returns path or None."""
# WSL detection: prefer Linux binaries; Windows binaries under /mnt/c/
Expand Down Expand Up @@ -245,6 +267,13 @@ def detect_ik_features(binary_path: str | None = None) -> IkFeatures:
def detect_hardware() -> HardwareFingerprint:
"""Fingerprint the current hardware and detect llama.cpp backend."""
gpu_name, vram, driver, cuda_ver = _detect_gpu()
# No NVIDIA GPU found — fall back to Apple Silicon (unified memory) if present.
if gpu_name == "Unknown" or vram == 0:
Comment on lines +270 to +271
apple = _apple_silicon_gpu()
if apple:
gpu_name, vram = apple
driver = f"macOS {platform.mac_ver()[0]}"
cuda_ver = None
cpu_name, cpu_cores = _detect_cpu()
ram = _detect_ram()

Expand Down
12 changes: 8 additions & 4 deletions src/llama_dyno/tune.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,9 @@ def _hill_climb(


def _detect_vram_mib() -> int:
"""Detect total GPU VRAM in MiB (pynvml, then nvidia-smi). 0 if unknown.
"""Detect GPU memory in MiB (NVIDIA VRAM, then Apple unified memory). 0 if unknown.

A single monkeypatchable seam for the tuner's VRAM heuristic.
A single monkeypatchable seam for the tuner's memory heuristic.
"""
try:
import pynvml
Expand All @@ -416,9 +416,13 @@ def _detect_vram_mib() -> int:
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=5,
)
return int(out.stdout.strip()) if out.stdout.strip() else 0
if out.stdout.strip():
return int(out.stdout.strip())
except Exception:
return 0
pass
from .detect import _apple_silicon_gpu
apple = _apple_silicon_gpu()
return apple[1] if apple else 0


def _estimate_model_size(model_path: str) -> int:
Expand Down
62 changes: 62 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Tests for hardware detection, focused on the Apple Silicon path (mocked)."""

from __future__ import annotations

import subprocess

from llama_dyno import detect


def _fake_sysctl(out: str):
def run(cmd, capture_output=True, text=True, timeout=5, **kw):
return subprocess.CompletedProcess(cmd, 0, stdout=out, stderr="")
return run


def test_apple_silicon_gpu_on_arm_mac(monkeypatch):
monkeypatch.setattr(detect.platform, "system", lambda: "Darwin")
monkeypatch.setattr(detect.platform, "machine", lambda: "arm64")
monkeypatch.setattr(detect, "_detect_ram", lambda: 16384)
monkeypatch.setattr(detect.subprocess, "run", _fake_sysctl("Apple M2 Max\n"))

assert detect._apple_silicon_gpu() == ("Apple M2 Max", 16384)


def test_apple_silicon_gpu_none_on_linux(monkeypatch):
monkeypatch.setattr(detect.platform, "system", lambda: "Linux")
monkeypatch.setattr(detect.platform, "machine", lambda: "x86_64")
assert detect._apple_silicon_gpu() is None


def test_apple_silicon_gpu_none_on_intel_mac(monkeypatch):
monkeypatch.setattr(detect.platform, "system", lambda: "Darwin")
monkeypatch.setattr(detect.platform, "machine", lambda: "x86_64")
assert detect._apple_silicon_gpu() is None


def test_detect_hardware_uses_apple_fallback(monkeypatch):
monkeypatch.setattr(detect, "_detect_gpu", lambda: ("Unknown", 0, "unknown", None))
monkeypatch.setattr(detect, "_apple_silicon_gpu", lambda: ("Apple M2 Max", 32768))
monkeypatch.setattr(detect.platform, "mac_ver", lambda: ("14.5", ("", "", ""), "arm64"))
monkeypatch.setattr(detect, "_find_binary", lambda name: None)

hw = detect.detect_hardware()
assert hw.gpu_name == "Apple M2 Max"
assert hw.vram_total_mib == 32768
assert hw.cuda_version is None
assert hw.driver_version.startswith("macOS")


def test_detect_hardware_keeps_nvidia(monkeypatch):
monkeypatch.setattr(detect, "_detect_gpu", lambda: ("NVIDIA RTX 4070", 12282, "550.0", "12.4"))

def _boom():
raise AssertionError("Apple fallback must not run when NVIDIA is present")

monkeypatch.setattr(detect, "_apple_silicon_gpu", _boom)
monkeypatch.setattr(detect, "_find_binary", lambda name: None)

hw = detect.detect_hardware()
assert hw.gpu_name == "NVIDIA RTX 4070"
assert hw.vram_total_mib == 12282
assert hw.cuda_version == "12.4"
Loading