Gil EskayoAI Engineer · Security · Research

Distributed LLM Inference

Two Macs in different rooms running one model over Tailscale, after eight stacked bugs.

PythonMLXgRPCTailscale
The story

I wanted my two Macs to act as one bigger computer: a Mac mini at home and a MacBook Pro on a different network, joined only by Tailscale, with no cable between them. The open-source framework for that, exo (now archived), splits a language model's layers across machines. On my setup it just hung.

That hang turned out to be eight separate bugs stacked on top of each other, each hiding the next. I got past all eight, and the split model now answers word for word what one machine does. Then I measured what the split costs.

2 Macs (M4 and M1, 16 GB each) · 16 model layers split 8 and 8 · 8 bugs cleared · 114 tokens/s on one machine, 18.6 across two

01

How it works

exo gives each machine a share of the model's layers in proportion to its memory. With 16 GB on each Mac, Llama 3.2 1B's 16 layers split evenly: the MacBook Pro runs layers 0 to 7 and the Mac mini runs 8 to 15. A prompt arrives at the Mac mini's chat API, goes to the MacBook for the first half, and the activations travel back over Tailscale for the second half. The Mac mini picks the next token and sends it round again, once per token.

A prompt enters the chat API on the Mac mini, goes to the MacBook Pro for layers 0 to 7, then over Tailscale to the Mac mini for layers 8 to 15, which produces the next token and sends it back to layer 0.
The split, as exo's own partitioning code computes it for these two machines.
02

Eight bugs, stacked

Each fix exposed the next problem. The real hang was number four: the MacBook's Python was built against a newer libexpat than its macOS shipped, so Python's own platform check returned blank strings and MLX crashed reading them. Number six was the network itself, a work network that blocked Hugging Face, so the Mac mini at home fetched the model and copied it over.

The best one was last. The split ran, but the answer came back garbled. I instrumented the shard code and proved the layer math was right every time, so the bug had to be elsewhere. It was two things at once: the second node was quietly answering its own default prompt, and exo's token callback collected tokens from every request in the cluster. The fix is one line:

if _request_id == request_id:
  tokens.extend(_tokens)
Eight bugs in the order they were found: a stale dependency pin, a launcher that failed only in the background, environment variables lost over ssh, a libexpat version skew, a silent first launch, a blocked model download, a memory value overflowing a 32-bit field, and output from both nodes mixed together; then a clean answer.
The eight bugs, in the order I found them.
03

It does real work

To prove it and price it, I ran the same prompt three times on the Mac mini alone and three times split across both Macs, on October 6, 2026. Both setups produced exactly the same 319-token answer, character for character. The split ran at 18.6 tokens per second against 114 on one machine, about six times slower.

Bar chart: one machine generates 114.0 tokens per second; two machines over Tailscale generate 18.6.
Measured, not estimated: mean of three warm runs each, temperature 0.

The gap is the network. On one machine a token takes about 9 milliseconds. Split, every token crosses Tailscale, where a round trip between these two Macs averaged 21 milliseconds, and exo itself rates the older M1 at about half the M4's compute. For a model that fits on one machine, splitting only costs speed. The payoff is a model too big for either Mac alone, which only a split can run at all.

04

Status

It works on the path I tested: one small model, two nodes, peers listed by hand. Larger models, a third node and Tailscale's automatic discovery are untested. exo itself is archived and its maintained successor focuses on Macs linked by Thunderbolt, so my fixes live in my local checkout, not upstream.

← More AI & Machine Learning   All projects →