|
| 1 | +# inferenced |
| 2 | + |
| 3 | +> *Inference daemon for Apple Silicon. macOS-native MLX serving with an |
| 4 | +> OpenAI-compatible HTTP API and Tailscale-aware source filtering.* |
| 5 | +
|
| 6 | +`inferenced` is a small Rust daemon that runs on a macOS host. It supervises |
| 7 | +[`mlx_lm.server`](https://github.com/ml-explore/mlx-lm) (Apple's reference |
| 8 | +LLM server, which uses Metal under the hood for GPU-accelerated inference), |
| 9 | +adds proper process supervision, source-CIDR filtering, Prometheus metrics, |
| 10 | +and a clean OpenAI-compatible HTTP surface that's safe to expose on a |
| 11 | +Tailscale-only port. |
| 12 | + |
| 13 | +It exists because LLM inference on Apple Silicon **must** run as native |
| 14 | +macOS code — Apple's Virtualization framework doesn't expose Metal/MPS/ANE |
| 15 | +to Linux guests. So if your workloads (Kubernetes pods, scripts, agents) |
| 16 | +live in Linux land but you want to use the GPU your Mac mini already has, |
| 17 | +**you need a daemon on the host that serves inference and a clean way for |
| 18 | +clients to call it**. This is that daemon. |
| 19 | + |
| 20 | +It pairs with [`inferenced-operator`](https://github.com/dormlab/inferenced-operator), |
| 21 | +a Kubernetes operator that orchestrates fleets of `inferenced` instances |
| 22 | +across a cluster of Apple Silicon hosts. You can also run `inferenced` |
| 23 | +standalone — `curl localhost:11434/v1/chat/completions` and you're done. |
| 24 | + |
| 25 | +```text |
| 26 | + ┌──────────────────┐ ┌────────────────────┐ |
| 27 | + │ any client │ HTTP │ inferenced │ |
| 28 | + │ (curl, kubectl, ├────────►│ (axum, supervisor,│ |
| 29 | + │ pod, script) │ │ metrics, auth) │ |
| 30 | + └──────────────────┘ └─────────┬──────────┘ |
| 31 | + │ proxy /v1/* |
| 32 | + ▼ |
| 33 | + ┌────────────────────┐ |
| 34 | + │ mlx_lm.server │ |
| 35 | + │ (Python, MLX) │ |
| 36 | + └─────────┬──────────┘ |
| 37 | + ▼ |
| 38 | + ┌────────────────────┐ |
| 39 | + │ Apple Silicon GPU │ |
| 40 | + │ via Metal │ |
| 41 | + └────────────────────┘ |
| 42 | +``` |
| 43 | + |
| 44 | +## Quick start |
| 45 | + |
| 46 | +```sh |
| 47 | +# Prereqs: Rust 1.75+, Python 3.10+, Apple Silicon Mac |
| 48 | +brew install python@3.12 |
| 49 | +python3.12 -m pip install --user --break-system-packages mlx-lm |
| 50 | + |
| 51 | +# Build |
| 52 | +cargo build --release --target aarch64-apple-darwin |
| 53 | + |
| 54 | +# Run (defaults to Qwen2.5-3B-Instruct-4bit and binds 0.0.0.0:11434) |
| 55 | +./target/aarch64-apple-darwin/release/inferenced |
| 56 | + |
| 57 | +# In another terminal |
| 58 | +curl http://localhost:11434/v1/chat/completions \ |
| 59 | + -H 'Content-Type: application/json' \ |
| 60 | + -d '{ |
| 61 | + "model": "mlx-community/Qwen2.5-3B-Instruct-4bit", |
| 62 | + "messages": [{"role": "user", "content": "hello"}], |
| 63 | + "stream": false |
| 64 | + }' |
| 65 | +``` |
| 66 | + |
| 67 | +## Documentation |
| 68 | + |
| 69 | +| | | |
| 70 | +|---|---| |
| 71 | +| [Architecture](./docs/architecture.md) | How `inferenced` fits between clients, MLX, and the rest of your infrastructure. | |
| 72 | +| [Installation](./docs/installation.md) | Install on a single Mac — Homebrew, Rust toolchain, `mlx-lm`, and a `launchd` LaunchDaemon for boot persistence. | |
| 73 | +| [Configuration](./docs/configuration.md) | Every CLI flag and env var. | |
| 74 | +| [HTTP API](./docs/api.md) | OpenAI-compatible `/v1/*`, plus `/healthz`, `/metrics`, `/`. | |
| 75 | +| [Metrics](./docs/metrics.md) | Prometheus metric reference. | |
| 76 | +| [Development](./docs/development.md) | Building from source, running tests, contributing. | |
| 77 | +| [Troubleshooting](./docs/troubleshooting.md) | "It's not starting", "I get `source not allowed`", "tokens/sec is bad". | |
| 78 | + |
| 79 | +## Examples |
| 80 | + |
| 81 | +- [`examples/launchd/dev.dormlab.inferenced.plist`](./examples/launchd/dev.dormlab.inferenced.plist) — system-level LaunchDaemon (runs as root for Metal access). |
| 82 | +- [`examples/kubernetes/`](./examples/kubernetes/) — Service + EndpointSlice manifests so cluster pods can call your fleet of macOS hosts as a single in-cluster Service. |
| 83 | + |
| 84 | +## Features |
| 85 | + |
| 86 | +- ✅ **Single static binary**, ~3 MB (`cargo build --release`). |
| 87 | +- ✅ **OpenAI-compatible** — every `/v1/*` route is transparently proxied; SSE streaming preserved end-to-end. |
| 88 | +- ✅ **Source-CIDR filtering** — defaults to Tailscale + loopback, configurable. |
| 89 | +- ✅ **Process supervision** — restarts `mlx_lm.server` with capped exponential backoff. |
| 90 | +- ✅ **Prometheus `/metrics`** — request counters by route + status class. |
| 91 | +- ✅ **Healthchecks** — `/healthz` validates the upstream Python process. |
| 92 | +- ✅ **Graceful shutdown** — SIGTERM propagates to `mlx_lm.server`. |
| 93 | +- ✅ **`launchd` LaunchDaemon** template for boot persistence. |
| 94 | + |
| 95 | +## Status |
| 96 | + |
| 97 | +`v0.1` — single-model per daemon, fixed at startup via `--model`. Multi-model |
| 98 | +hot-loading is the v0.2 goal (admin API for `POST /admin/models/{load,unload}`) |
| 99 | +which the operator can drive, see [the architecture doc](./docs/architecture.md#roadmap). |
| 100 | + |
| 101 | +## License |
| 102 | + |
| 103 | +MIT. See [LICENSE](./LICENSE). |
0 commit comments