| title | Running on macOS |
|---|---|
| description | Serving on the Apple Silicon GPU through vllm-metal, and what it changes. |
On Apple Silicon, llm-helper serves on the GPU through vllm-metal, a vLLM plugin that runs models with MLX and Metal.
It needs macOS 15 or later on an Apple Silicon Mac.
When vllm-metal is not installed beside VLLM_BIN, vLLM's CPU backend still works as a fallback; see CPU backend.
This page is about the vLLM provider: mlx-lm and llama.cpp also serve on the Apple Silicon GPU, see Providers.
Install the stable release with Homebrew:
brew tap vllm-project/vllm-metal https://github.com/vllm-project/vllm-metal
brew install vllm-project/vllm-metal/vllm-metalTo try a development build instead, run the install script.
It creates a venv at ~/.venv-vllm-metal:
curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bashpip install vllm-metal is not a supported way to install it.
The vllm-metal docs cover what each release supports.
- Run
llm-helper setup.
Setup looks for vllm on your PATH first.
If it is not there, it tries where the two installers put it: /opt/homebrew/opt/vllm-metal/libexec/bin/vllm, then ~/.venv-vllm-metal/bin/vllm.
A Homebrew install is recorded by its opt path rather than the versioned Cellar path, so brew upgrade does not leave VLLM_BIN pointing at nothing.
Setup then asks the Python beside VLLM_BIN whether vllm-metal is installed, and asks MLX how much memory the GPU may use.
When both answer, setup says so:
Apple M5 Ultra (unified memory), serving on the GPU through vllm-metal (239 GB Metal working set)
llm-helper doctor shows the same on its backend: line.
On the CPU backend, that line says so and suggests installing vllm-metal.
vllm-metal loads MLX checkpoints, usually from mlx-community on Hugging Face:
llm-helper new mlx-community/Qwen3-Coder-30B-A3B-Instruct-8bitOn an M5 Ultra under a system-scope LaunchDaemon, that model serves at about 85 tokens/sec for a single stream.
Its card has no vllm serve snippet, so llm-helper new adds no flags.
For tool calls, set the parser by hand:
llm-helper set qwen3-coder-30b-a3b-instruct-8bit -- EXTRA_ARGS="--enable-auto-tool-choice --tool-call-parser qwen3_coder"Its chat template puts only the first system message at the top of the prompt. A client that sends several, as Coder Agents does, gets the rest placed as separate turns just before the user's message, and the model can read them as things the user said. To avoid that, join them into one before the request reaches the server, for example in a gateway in front of it.
Which models run follows vllm-metal's supported models list, not vLLM's. Quantizations made for CUDA, such as FP8 block or NVFP4, do not run. The architecture has to be supported by vllm-metal as well. For example, DeepSeek-V4-Flash does not run on vllm-metal 0.32: stock mlx-lm has no model for it, and vllm-metal's paged attention does not handle its compressed sparse attention. Mainline llama.cpp does run it on the GPU, from Unsloth's GGUF.
GPU_MEMORY_UTILIZATIONis a share of the Metal working set. The working set is how much unified memory macOS lets the GPU use, and macOS already keeps it below installed RAM. New profiles default to0.9here. vllm-metal allocates the KV cache as it goes and checks for memory pressure at startup, sollm-helper startleaves the value as it is.- The memory budget comes from the working set. It is 90% of the working set, minus about 2 GiB for vllm-metal itself and room for a KV cache. A 256 GiB M5 Ultra gets a weights budget of about 194 GB. If MLX cannot be asked, the working set is estimated at 75% of RAM, and the budget's reason says it is an estimate.
doctorskips the build toolchain. vllm-metal ships prebuilt wheels and Metal kernels, so there is nothing to compile.- Logs are a file, not a journal. Each model's launchd job writes to a log file; see Logs.
Each running model is its own launchd job, labelled com.coder.llm-helper.<profile>.
By default each is a LaunchAgent in ~/Library/LaunchAgents, so controlling it needs no root.
A LaunchAgent needs a GUI login session.
Over SSH on a Mac nobody is logged in to, loading it fails with launchd error 125, and llm-helper says why.
On a headless or server Mac, run it as a LaunchDaemon instead:
- Stop any model that is running with
llm-helper down <profile>. - Set
SERVICE_SCOPE=systemin~/.config/vllm-helper/config.env. - Run
llm-helper up <profile>for each model.
up installs /Library/LaunchDaemons/com.coder.llm-helper.<profile>.plist.
It starts at boot and runs as the user who installed it, so it reads that user's config and model cache.
llm-helper status and doctor read its state without sudo.
Starting, stopping and restarting a model need sudo, as system scope does on Linux.
Earlier releases ran one job, com.coder.vllm, for whichever model was active.
llm-helper migrate moves that model to its own job and removes the old plist.
A LaunchDaemon installed by vllm-helper 0.5.0 or earlier runs as root; the job migrate installs runs as you.
macOS caps how much unified memory the GPU may wire, and that cap is the working set.
llm-helper metal-limit shows the RAM, the current iogpu.wired_limit_mb (0 is the macOS default), the working set that results, and a suggested limit.
The suggestion leaves 16 GiB for macOS on a Mac with 128 GiB or more, and 8 GiB on a smaller one.
llm-helper metal-limit # what it is now, and a suggestion
llm-helper metal-limit --set auto # apply the suggestion, or pass a size in MiB
llm-helper restart <profile> # vllm-metal reads the working set at startup
llm-helper metal-limit --reset # back to the macOS default--set writes METAL_WIRED_LIMIT_MB to config.env and applies the limit now with sudo.
The setting does not survive a reboot, so it also installs a LaunchDaemon, com.coder.vllm-helper.wired-limit, that applies it again at every boot.
When the live limit is already at or above the suggestion, --set auto keeps it rather than raising it, and still installs the daemon and records the limit if either is missing.
Whenever the live limit differs from METAL_WIRED_LIMIT_MB, llm-helper start waits up to 60 seconds for it to match before it starts vLLM, then warns and starts anyway.
--reset removes that daemon and sets the limit back to 0.
doctor shows the current limit and suggests raising it.
metal-limit needs macOS 15 or later on Apple Silicon, the same as vllm-metal.
After a raise, the ceiling vLLM may touch grows on the next restart, because GPU_MEMORY_UTILIZATION is a share of the working set.
What happens to the KV cache depends on the vllm-metal build:
- vllm-metal 0.32.0 and earlier ignore
KV_CACHE_MEMORY_BYTESand give the cache whateverGPU_MEMORY_UTILIZATIONleaves after the weights.llm-helper newleavesKV_CACHE_MEMORY_BYTESout of the profile there and says why, soKV_CONTEXT_TARGEThas no effect. The cache grows with the working set on the next restart, with nothing to change. - Development builds after 0.32.0 honor
KV_CACHE_MEMORY_BYTES, andllm-helper newsizes it forKV_CONTEXT_TARGETfull contexts. That size does not grow: each profile keeps the cache size it was planned with against the old working set. Planning again only changes a cache that was cut short to fit; one that already holdsKV_CONTEXT_TARGETfull contexts comes out the same. To use the extra room in the cache, either set the size yourself withllm-helper set <profile> KV_CACHE_MEMORY_BYTES=<bytes>, or plan the profile again.llm-helper new <repo>reuses a profile that already exists, so remove it first withllm-helper remove <profile>. A running profile cannot be removed, so stop it withllm-helper down <profile>first.
A higher limit gives the GPU more of the RAM and leaves less headroom for macOS. That is a good trade on a dedicated server, and a worse one on a desktop you also work on.
Each model's launchd job writes to <label>.log, which is com.coder.llm-helper.<profile>.log:
- User scope:
~/Library/Logs/vllm-helper/com.coder.llm-helper.<profile>.log - System scope:
/Library/Logs/vllm-helper/com.coder.llm-helper.<profile>.log
llm-helper logs <profile> follows that model's log.
Without vllm-metal beside VLLM_BIN, vLLM runs models on the CPU through its experimental CPU backend.
It is useful for small models and development, not for serving anything large.
A 0.6B model in BF16 serves at roughly 30 tokens/sec on an M4 Pro.
There are no Apple Silicon wheels for the CPU backend, so it has to be built from source:
brew install cmake ninja
uv venv --python 3.12 ~/venvs/vllm
git clone https://github.com/vllm-project/vllm.git ~/vllm-src && cd ~/vllm-src
VIRTUAL_ENV=~/venvs/vllm uv pip install -r requirements/cpu.txt --index-strategy unsafe-best-match
VIRTUAL_ENV=~/venvs/vllm VLLM_TARGET_DEVICE=cpu uv pip install -e . --index-strategy unsafe-best-matchThen run llm-helper setup and point VLLM_BIN at ~/venvs/vllm/bin/vllm.
On the CPU backend:
- Quantized weights don't work. It handles FP32, FP16, and BF16, so pick a model small enough to run unquantized.
GPU_MEMORY_UTILIZATIONmeans system RAM. It reserves a fraction of total RAM, and vLLM refuses to start unless that much is free right then. Profiles default to0.3, andllm-helper startlowers it further when the machine is busy, logging the change.doctorchecks the build toolchain. It looks for cmake, ninja, and the Command Line Tools, and with--yesinstalls what Homebrew can.
- Troubleshooting: reading the log, and when vLLM refuses to start.
- Memory budget: what decides whether a model fits, and how to override it.
- Models and quantizations: finding a model that fits this machine.