Apple Silicon LLM Serving (MLX / oMLX)
Operational guidance for running and tuning local LLM inference on Apple
Silicon (M-series). Covers two common backends: Apple's mlx-lm and the
oMLX server built on top of MLX.
Both expose OpenAI-compatible HTTP APIs, so client code is portable. The
differences are in features, packaging, and the failure modes you hit when
pushing models near the limits of unified memory.
Pre-Flight: Verify Environment
Before debugging any serving issue, confirm the basics:
# Hardware: confirm Apple Silicon and unified-memory size
system_profiler SPHardwareDataType | grep -E "Model|Chip|Memory"
# Activate the venv where mlx-lm is installed before the next two probes,
# otherwise they hit system Python and may report nothing useful.
python -c "import mlx.core as mx; print(mx.__version__, mx.default_device())"
python -c "import mlx_lm; print(mlx_lm.__version__)"
# oMLX (DMG install): inspect available subcommands and run health checks
omlx --help
omlx diagnose menubar # checks Tahoe ControlCenter visibility (oMLX-specific)
# Confirm a process is actually listening
lsof -nP -iTCP -sTCP:LISTEN | grep -E "omlx|mlx_lm"
Verify which backend is bound before debugging: both can serve OpenAI-
compatible endpoints on user-chosen ports, and a client request hitting the
wrong backend silently produces correct-looking but unrelated output.
Backend Decision: mlx-lm vs oMLX
| Concern |
mlx-lm |
oMLX |
| Install |
pip install mlx-lm |
DMG (macOS app bundle) |
| Process model |
One model per mlx_lm.server process |
Built-in registry; multi-model |
| Config surface |
Minimal CLI flags |
Rich per-model JSON (model_settings.json) |
| Feature flags |
--temp, --top-p, basic |
turboquant_kv, dflash, MTP, specprefill, thinking budget |
| Tool calling |
Format depends on chat template |
Per-model parser (configurable) |
| Models > RAM |
Not supported (OOM on load) |
OptiQ proxy build (sensitivity-driven quant) |
| GUI |
None |
Native macOS app + menu bar |
| Default port |
8080 |
8000 |
| Best for |
Library-first scripts, embedding in Python |
Long-running daemon, multiple models, GUI ops |
Skill bias: the depth here leans oMLX — feature flags, cache controls,
upstream bug patterns. mlx-lm coverage is lighter and focuses on the
shared substrate.
Pick mlx-lm when: you control the Python process, you want minimal
surface area, you serve one model at a time, and the model fits comfortably
in RAM.
Pick oMLX when: you need a daemon that survives terminal sessions, you
serve multiple models behind one endpoint, you want feature flags
(turboquant, dflash, MTP) without writing them yourself, or your model
exceeds RAM and you need OptiQ proxy.
Both backends share the same fundamental constraints (unified memory,
KV cache pressure, model architecture quirks), so most troubleshooting in
this skill applies to either.
Symptom → Cause Triage
| Symptom |
Likely cause |
First move |
| OOM during model load |
Model + KV scratch > available RAM |
Smaller quant; OptiQ proxy (oMLX); free other apps |
| OOM during long-context inference |
KV cache growth |
Enable turboquant_kv; set --paged-ssd-cache-dir for spill-to-SSD (CLI flag is the operative control; dflash_* keys are version-dependent overrides) |
| Crash / hang at batch > 1 |
Architecture cache shape mismatch |
Pin --max-concurrent-requests 1 (oMLX); see upstream-bug-patterns |
| Tool calls return as plaintext content |
Parser doesn't recognize the model's tool-call JSON shape |
See upstream-bug-patterns; check tool_calls field in response |
| Throughput regression after config change |
Feature flag interaction |
Bisect via bench, one flag at a time |
| Server hangs on startup |
Large checkpoint loading from cold disk |
Wait; tail server.log; check disk I/O via iostat |
| Wrong / garbled outputs |
Quantization too aggressive, or wrong chat template |
Reduce turboquant_kv_bits or disable; verify chat template matches model card |
| First request slow, rest fast |
Cold prefix cache |
Enable specprefill; warm cache with a dummy request |
| Output cut off mid-sentence |
max_tokens too low; thinking budget exhausted |
Raise max_tokens; check thinking_budget_enabled |
For deeper triage on each symptom: references/symptoms.md.
Core Principles
Lower --max-concurrent-requests to 1 for batch-fragile architectures.
oMLX defaults to --max-concurrent-requests 8. Higher concurrency
exposes architecture-specific batch handling bugs (notably in cache
implementations like ChunkedKVCache used by Llama-4). For affected
architectures, pin concurrency to 1 at launch and bench upward only
after confirming clean output at batch>1.
One flag at a time. Feature flags interact (e.g., dflash +
turboquant_kv both touch the KV cache path). Changing two flags
simultaneously and observing a regression makes attribution impossible.
Bench between each change.
Bench, don't guess. "It feels faster" is not a tuning signal.
Maintain a small bench suite (chat / coding / tool-calling correctness +
throughput) and re-run it after every config change. See
references/bench-methodology.md.
Match chat templates to model cards. Tool-calling and reasoning
behavior depend on the chat template applied at request time. The
model's HuggingFace card is authoritative.
Quantization is a curve, not a switch. turboquant_kv_bits=4.0
keeps most quality; lower bits trade quality for memory. Always include
a quality cell in the bench suite — a config that passes throughput
but fails coding can be worse than slower-but-correct.
OptiQ proxy is for the "model > RAM" case only. It builds a
sensitivity-driven proxy of the model so per-layer quant decisions can
be made without holding the full model in memory. It's not a general
speedup; for in-RAM models it adds startup cost without runtime benefit.
Server-side bugs masquerade as config problems. When a symptom
reproduces across multiple config combinations, suspect upstream rather
than chasing more flags. See references/upstream-bug-patterns.md.
References
references/symptoms.md — symptom triage with
diagnostics and fixes (OOM, batch>1 crashes, tool-call-as-text,
throughput regression, log triage)
references/omlx-feature-flags.md —
per-flag reference (turboquant_kv, dflash, MTP, specprefill, thinking
budget, max-concurrent-requests, force_sampling) with interactions
references/bench-methodology.md —
bench-it-don't-guess: suite design, backend-agnostic harness shape,
scoring, change attribution
references/upstream-bug-patterns.md
— when to suspect upstream vs config; two recurring bug patterns
(cache-shape mismatch under batched scheduling; model tool-call format
not parsed) with diagnostics and reporting guidance
What This Skill Does Not Cover
- Cloud LLM hosting (Bedrock, OpenAI API, Anthropic API) — different
surface entirely
- Non-MLX local backends (llama.cpp, Ollama, vLLM) — overlapping problem
space but different tooling
- Model training, fine-tuning, or LoRA — see MLX training tutorials
- General Python / HTTP debugging unrelated to MLX
- Model card creation or HuggingFace upload workflows
1---2name: mlx-serving3description: Use when the user asks about MLX serving, mlx_lm.server, oMLX, Apple Silicon LLM serving, or running a local LLM on a Mac, and when troubleshooting a model that fails to load, OOM during load or inference, a server that hangs or crashes at batch>1, tool calls returned as plaintext content, a throughput regression, or the choice between mlx-lm and oMLX. Also covers oMLX feature-flag tuning (turboquant_kv, dflash, MTP, specprefill, thinking_budget, max-concurrent-requests, force_sampling), the OptiQ proxy for models exceeding RAM, Llama-4 ChunkedKVCache batch handling, Llama-3 tool-call JSON format, and bench-driven validation of serving configs. Apple Silicon M-series only, not cloud LLM hosting, non-MLX backends such as llama.cpp, Ollama and vLLM, or model training.4license: MIT5---67# Apple Silicon LLM Serving (MLX / oMLX)89Operational guidance for running and tuning local LLM inference on Apple10Silicon (M-series). Covers two common backends: Apple's `mlx-lm` and the11oMLX server built on top of MLX.1213Both expose OpenAI-compatible HTTP APIs, so client code is portable. The14differences are in features, packaging, and the failure modes you hit when15pushing models near the limits of unified memory.1617---1819## Pre-Flight: Verify Environment2021Before debugging any serving issue, confirm the basics:2223```bash24# Hardware: confirm Apple Silicon and unified-memory size25system_profiler SPHardwareDataType | grep -E "Model|Chip|Memory"2627# Activate the venv where mlx-lm is installed before the next two probes,28# otherwise they hit system Python and may report nothing useful.29python -c "import mlx.core as mx; print(mx.__version__, mx.default_device())"30python -c "import mlx_lm; print(mlx_lm.__version__)"3132# oMLX (DMG install): inspect available subcommands and run health checks33omlx --help34omlx diagnose menubar # checks Tahoe ControlCenter visibility (oMLX-specific)3536# Confirm a process is actually listening37lsof -nP -iTCP -sTCP:LISTEN | grep -E "omlx|mlx_lm"38```3940Verify which backend is bound before debugging: both can serve OpenAI-41compatible endpoints on user-chosen ports, and a client request hitting the42wrong backend silently produces correct-looking but unrelated output.4344---4546## Backend Decision: mlx-lm vs oMLX4748| Concern | mlx-lm | oMLX |49|---|---|---|50| Install | `pip install mlx-lm` | DMG (macOS app bundle) |51| Process model | One model per `mlx_lm.server` process | Built-in registry; multi-model |52| Config surface | Minimal CLI flags | Rich per-model JSON (`model_settings.json`) |53| Feature flags | `--temp`, `--top-p`, basic | turboquant_kv, dflash, MTP, specprefill, thinking budget |54| Tool calling | Format depends on chat template | Per-model parser (configurable) |55| Models > RAM | Not supported (OOM on load) | OptiQ proxy build (sensitivity-driven quant) |56| GUI | None | Native macOS app + menu bar |57| Default port | 8080 | 8000 |58| Best for | Library-first scripts, embedding in Python | Long-running daemon, multiple models, GUI ops |5960> **Skill bias:** the depth here leans oMLX — feature flags, cache controls,61> upstream bug patterns. mlx-lm coverage is lighter and focuses on the62> shared substrate.6364Pick **mlx-lm** when: you control the Python process, you want minimal65surface area, you serve one model at a time, and the model fits comfortably66in RAM.6768Pick **oMLX** when: you need a daemon that survives terminal sessions, you69serve multiple models behind one endpoint, you want feature flags70(turboquant, dflash, MTP) without writing them yourself, or your model71exceeds RAM and you need OptiQ proxy.7273Both backends share the same fundamental constraints (unified memory,74KV cache pressure, model architecture quirks), so most troubleshooting in75this skill applies to either.7677---7879## Symptom → Cause Triage8081| Symptom | Likely cause | First move |82|---|---|---|83| OOM during model load | Model + KV scratch > available RAM | Smaller quant; OptiQ proxy (oMLX); free other apps |84| OOM during long-context inference | KV cache growth | Enable `turboquant_kv`; set `--paged-ssd-cache-dir` for spill-to-SSD (CLI flag is the operative control; `dflash_*` keys are version-dependent overrides) |85| Crash / hang at batch > 1 | Architecture cache shape mismatch | Pin `--max-concurrent-requests 1` (oMLX); see upstream-bug-patterns |86| Tool calls return as plaintext content | Parser doesn't recognize the model's tool-call JSON shape | See upstream-bug-patterns; check `tool_calls` field in response |87| Throughput regression after config change | Feature flag interaction | Bisect via bench, one flag at a time |88| Server hangs on startup | Large checkpoint loading from cold disk | Wait; tail `server.log`; check disk I/O via `iostat` |89| Wrong / garbled outputs | Quantization too aggressive, or wrong chat template | Reduce `turboquant_kv_bits` or disable; verify chat template matches model card |90| First request slow, rest fast | Cold prefix cache | Enable `specprefill`; warm cache with a dummy request |91| Output cut off mid-sentence | `max_tokens` too low; thinking budget exhausted | Raise `max_tokens`; check `thinking_budget_enabled` |9293For deeper triage on each symptom: `references/symptoms.md`.9495---9697## Core Principles98991. **Lower `--max-concurrent-requests` to 1 for batch-fragile architectures.**100 oMLX defaults to `--max-concurrent-requests 8`. Higher concurrency101 exposes architecture-specific batch handling bugs (notably in cache102 implementations like ChunkedKVCache used by Llama-4). For affected103 architectures, pin concurrency to 1 at launch and bench upward only104 after confirming clean output at batch>1.1051062. **One flag at a time.** Feature flags interact (e.g., `dflash` +107 `turboquant_kv` both touch the KV cache path). Changing two flags108 simultaneously and observing a regression makes attribution impossible.109 Bench between each change.1101113. **Bench, don't guess.** "It feels faster" is not a tuning signal.112 Maintain a small bench suite (chat / coding / tool-calling correctness +113 throughput) and re-run it after every config change. See114 `references/bench-methodology.md`.1151164. **Match chat templates to model cards.** Tool-calling and reasoning117 behavior depend on the chat template applied at request time. The118 model's HuggingFace card is authoritative.1191205. **Quantization is a curve, not a switch.** `turboquant_kv_bits=4.0`121 keeps most quality; lower bits trade quality for memory. Always include122 a quality cell in the bench suite — a config that passes throughput123 but fails coding can be worse than slower-but-correct.1241256. **OptiQ proxy is for the "model > RAM" case only.** It builds a126 sensitivity-driven proxy of the model so per-layer quant decisions can127 be made without holding the full model in memory. It's not a general128 speedup; for in-RAM models it adds startup cost without runtime benefit.1291307. **Server-side bugs masquerade as config problems.** When a symptom131 reproduces across multiple config combinations, suspect upstream rather132 than chasing more flags. See `references/upstream-bug-patterns.md`.133134---135136## References137138- [`references/symptoms.md`](references/symptoms.md) — symptom triage with139 diagnostics and fixes (OOM, batch>1 crashes, tool-call-as-text,140 throughput regression, log triage)141- [`references/omlx-feature-flags.md`](references/omlx-feature-flags.md) —142 per-flag reference (turboquant_kv, dflash, MTP, specprefill, thinking143 budget, max-concurrent-requests, force_sampling) with interactions144- [`references/bench-methodology.md`](references/bench-methodology.md) —145 bench-it-don't-guess: suite design, backend-agnostic harness shape,146 scoring, change attribution147- [`references/upstream-bug-patterns.md`](references/upstream-bug-patterns.md)148 — when to suspect upstream vs config; two recurring bug patterns149 (cache-shape mismatch under batched scheduling; model tool-call format150 not parsed) with diagnostics and reporting guidance151152---153154## What This Skill Does Not Cover155156- Cloud LLM hosting (Bedrock, OpenAI API, Anthropic API) — different157 surface entirely158- Non-MLX local backends (llama.cpp, Ollama, vLLM) — overlapping problem159 space but different tooling160- Model training, fine-tuning, or LoRA — see MLX training tutorials161- General Python / HTTP debugging unrelated to MLX162- Model card creation or HuggingFace upload workflows