Distributed inference across Macs
Run a model that does not fit any single machine by sharding its layers across two or more Macs over Thunderbolt. One OpenAI-compatible endpoint, the same serving code path a single Mac uses.
What this is for
Pipeline parallelism is a capacity feature, not a speed feature. Rank 0 runs its layers, hands the activation to rank 1, and waits. There is no overlap within a single request, so every token pays a network hop and you gain nothing by splitting a model that already fits.
Where it does win: Qwen3.5-122B-A10B-OptiQ-2bit is 42.8 GiB of weights. On a single 36 GB Mac it only runs by streaming MoE experts from the SSD, at 4.9 tok/s. Split across a 36 GB M3 Max and a 24 GB M4 it is fully resident and decodes at 20.5 tok/s. That is 4.2× faster, because the comparison is against a crippled path rather than a healthy one.
Setup
You need SSH between the Macs, a Thunderbolt cable, and the model cached on
each node (or the peers download it on first serve). Connect the Macs and
macOS creates a bridge0 interface with link-local addresses;
OptiQ finds them itself.
pip install mlx-optiq
optiq cluster up # advertise this Mac over Bonjour
optiq cluster peers # who is reachable, on what link
ssh-ing into each peer and running
cd <dir> && <dir>/<venv>/bin/python3 from that peer's home. Usernames
differ between machines, so the path is resolved relative to each home.
Put the project (and its virtualenv) in the same place on both, for example
~/optiq/.venv on each. Or point at it explicitly with
--cwd / OPTIQ_CLUSTER_CWD. By default OptiQ uses the
virtualenv it is running inside.
optiq cluster serve measures the real round-trip to each peer and
refuses a slow link by default. Pipeline inference over Wi-Fi is
slower than one Mac, because every token pays the latency. It fails loudly
instead of quietly degrading. Pass --link any if you want it anyway.
Thunderbolt is ~0.6 ms; Wi-Fi is ~85 ms.
From the Lab
Settings → Cluster shows the discovered Macs on the ring with each node's free memory, swap pressure, and how many layers it would take. The number it reports is what the ring can spare right now, not the RAM you bought.

Enter a model and press Start serving. A 40 GB model takes time to page in, so chat does not switch to the cluster until the endpoint answers. You get a loading state rather than a connection error.

Chat then talks to the ring exactly as it talks to a single Mac: streaming, tool calls, structured output, LoRA adapters. Below, the 122B writing a flight simulator, its HTML running in the Lab's sandboxed artifact panel.

From the CLI
optiq cluster serve --model mlx-community/Qwen3.5-122B-A10B-OptiQ-2bit --port 8100
[rank 0] materialized 8/30 layers | resident 9.6 GiB
[rank 0] materialized 16/30 layers | resident 15.7 GiB
[rank 0] materialized 24/30 layers | resident 21.7 GiB
[rank 1/2] layers 30-47 (18/48) | loaded 7.4s | resident 16.6 GiB | cap 17.3 GiB (free 21.4, GPU limit 22.0)
[rank 0/2] layers 0-29 (30/48) | loaded 7.8s | resident 26.2 GiB | cap 26.6 GiB (free 27.0, GPU limit 30.0)
[cluster] OpenAI endpoint: http://127.0.0.1:8100/v1/chat/completions
[cluster] 2 nodes, model=mlx-community/Qwen3.5-122B-A10B-OptiQ-2bit. Ctrl+C to stop.
Rank 0 owns the embedding, the final norm, and the LM head, and drives the ring. The last rank returns a hidden state rather than logits, so what crosses the wire each token is ~12 KB instead of ~1 MB of vocabulary.
The memory rules
Layers are held resident, so the model must fit the memory the cluster can spare at that moment. Boundaries follow each node's free memory and the real per-layer byte counts. The Mac you are working on takes fewer layers than an idle one, and a MoE's heavier late blocks are counted as they actually are.
Each rank keeps a run-reserve free for activations, the KV cache, and MLX's buffer pool. Before a single weight is evaluated, OptiQ prices the model against what the ring can spare and refuses to load rather than swap a machine to death:
mlx-community/Qwen3.5-122B-A10B-OptiQ-2bit does not fit the cluster's *currently free* memory.
rank 0: needs 24.4 GiB | can spare 25.0 GiB (free 25.9 GiB - 0.9 GiB to run)
rank 1: needs 18.4 GiB | can spare 18.3 GiB (free 21.5 GiB - 3.2 GiB to run) <-- does not fit
Weights 42.8 GiB vs 43.3 GiB spare across 2 node(s).
Fix: free memory on the node(s) above (quit apps; a Mac whose swap is full needs a
reboot), use a smaller quant, add a Mac, or lower the run-reserve with
OPTIQ_CLUSTER_HEADROOM_GB=<gb> (risks swapping).
OPTIQ_CLUSTER_HEADROOM_GB exists so you can raise the reserve,
not shrink it. We forced a 42.8 GiB model onto a pair that could spare 42.3 GiB by
dropping the reserve to 0.8 GiB per node. It loaded cleanly, then the smaller Mac
swapped 2.1 GB and one generation ran for 22 minutes without finishing.
A refusal costs you nothing; a swap storm costs you the machine.
The smaller Mac has a residency ceiling
There is a second limit, and it is the one that surprises people. Past roughly 73% of its RAM in resident weights, a Mac's GPU throughput collapses. System memory is free, nothing swaps, and no error is raised. On a 24 GB M4 the cliff sits near 17.5 GiB:
| Resident weights | iogpu.wired_limit_mb | Throughput |
|---|---|---|
| 16.6 GiB | 20480 | 23.3 tok/s |
| 17.3 GiB | 20480 | 15.1 tok/s |
| 18.1 GiB | 20480 | 0.1 tok/s |
| 18.1 GiB | 22528 | 0.1 tok/s |
| 19.6 GiB | 22528 | 0.05 tok/s (swapping) |
Raising iogpu.wired_limit_mb does not move it. 18.1 GiB
reads 0.1 tok/s at both a 20 and a 22 GiB limit. If a ring loads but crawls, the
smallest node is holding too many layers. Give it a larger reserve so it takes
fewer. That is the opposite of what instinct suggests.
# per-rank reserve, in the ring's rank order
OPTIQ_CLUSTER_HEADROOM_GB="0.4,4.1" optiq cluster serve --model <repo>
What to expect
| Path | Resident | Steady throughput |
|---|---|---|
| Two Macs, sharded | 42.8 GiB across 2 | 20.5 tok/s |
| One Mac, SSD expert streaming | 9.97 GiB | 4.9 tok/s |
| One Mac, fully resident | does not fit | — |
Load takes 26 s end to end; weights are memory-mapped and materialized layer by layer. Throughput is flat in context. The gap between the two paths does not close as the conversation grows.
Troubleshooting
It refuses to load
Believe it. Quit apps on the node flagged <-- does not fit. A Mac whose
swap is full stays degraded until it reboots. macOS never shrinks the swap file.
The Lab's cluster page shows swap per node and will tell you which machine to restart.
It loads, then crawls
The smallest node is over its residency ceiling. Raise its reserve so it
takes fewer layers. Do not raise iogpu.wired_limit_mb; it will not help.
It refuses the link
You are on Wi-Fi. Connect the Thunderbolt cable and check ifconfig bridge0
reports status: active on both machines.
A peer cannot find python
zsh: no such file or directory: .venv/bin/python3 means the peer does
not have mlx-optiq where this Mac does. The path is relative to each machine's
home directory. Install it at the same relative location on every node, or pass
--cwd.
The ranks disagree about the model
Every node needs the same complete checkout in its Hugging Face cache. A partially downloaded copy on one peer makes the ranks compute different shard boundaries and the ring hangs at startup.