← All courses ← Course home

Lesson 05 of 05

Long prompts and serving many agents

Even Apple's fastest setup took about 40 seconds to start answering a 64K-token prompt. Plan for that, tune prefill, serve the cluster with mlx_lm.server, and measure what each agent waits.

Agent brief (llms.md)

Environment setup

Use the ~/mlx-cluster project from lesson 01 on every Mac (uv, Python 3.11+, uv add mlx mlx-lm huggingface_hub, optional torch with MPS). If you skipped it, do the setup in lesson 01 first. The server runs on the cluster. The agent test script runs on any machine that can reach rank 0.

Long prompts are slow to start

From Apple's post, for Muse Spark Open on four M5 Ultra Mac Studios with tensor parallel:

Agent tools often send large contexts. So a coding agent on this cluster can wait about 40 seconds before the first word, on the best hardware Apple tested. Older Macs are slower: for Mistral Medium 3.5 at 8,192 tokens, one M3 Ultra took 95.9 s and one M5 Ultra took 24.6 s. Design for it: stream output, show that work is in progress, and keep the start of each prompt the same between calls so the prompt cache can reuse it (see Keep the prompt cache warm).

Tune the prefill chunk size

MLX processes a long prompt in chunks. mlx_lm.server uses --prefill-step-size 2048 by default (mlx-lm 0.32.2). Apple's pipeline benchmark used 8,192, and chunking is what lets pipeline overlap work across Macs. We did not measure the best value. Measure it on your cluster with the benchmark tool Apple used:

cd ~/mlx-cluster
for step in 2048 4096 8192; do
  uv run mlx.launch --hostfile jaccl.json --backend jaccl -- \
    ~/mlx-cluster/.venv/bin/python -m mlx_lm benchmark --model "$MODEL" \
    -p 65536 -g 128 -b 1 --prefill-step-size "$step"
done

Add --pipeline if you run pipeline. Write down the first-token time and peak memory for each value.

Run the server

uv run mlx.launch --hostfile jaccl.json --backend jaccl -- \
  ~/mlx-cluster/.venv/bin/python -m mlx_lm server \
  --model "$MODEL" --host 0.0.0.0 --port 8080 --prefill-step-size 8192

Facts from the mlx-lm source and docs that change how you run it:

Apple's post shows how to point OpenCode at the server with an OpenAI-compatible provider and baseURL http://127.0.0.1:8080/v1. Use rank 0's address if the agent runs on another machine.

Measure what each agent waits

This script sends several streaming requests at once and prints the first-token time for each:

"""Send N agent requests at once and measure time to first token for each."""
import argparse
import json
import statistics
import threading
import time
import urllib.request


def one_request(url, prompt, max_tokens, results, i):
    body = json.dumps({
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "stream": True,
    }).encode()
    req = urllib.request.Request(url, body, {"Content-Type": "application/json"})
    start = time.perf_counter()
    first = None
    chunks = 0
    with urllib.request.urlopen(req, timeout=600) as resp:
        for raw in resp:
            line = raw.decode().strip()
            if not line.startswith("data:") or line == "data: [DONE]":
                continue
            delta = json.loads(line[5:])["choices"][0].get("delta", {})
            if delta.get("content") or delta.get("reasoning"):
                chunks += 1
                if first is None:
                    first = time.perf_counter() - start
    total = time.perf_counter() - start
    results[i] = (first, total, chunks)


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--url", default="http://127.0.0.1:8080/v1/chat/completions")
    p.add_argument("--agents", type=int, default=4)
    p.add_argument("--prompt-words", type=int, default=2000)
    p.add_argument("--max-tokens", type=int, default=64)
    a = p.parse_args()

    filler = " ".join(["word"] * a.prompt_words)
    results = [None] * a.agents
    threads = [
        threading.Thread(target=one_request, args=(
            a.url, f"Agent {i}. Read this and reply with one short line.\n{filler}",
            a.max_tokens, results, i))
        for i in range(a.agents)
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    ttfts = [r[0] for r in results if r and r[0] is not None]
    for i, r in enumerate(results):
        print(f"agent {i}: first token {r[0]:.2f}s, total {r[1]:.2f}s, {r[2]} chunks")
    print(f"TTFT median {statistics.median(ttfts):.2f}s, worst {max(ttfts):.2f}s")


if __name__ == "__main__":
    main()
uv run python agents_ttft.py --url http://mac1:8080/v1/chat/completions \
  --agents 4 --prompt-words 2000 --max-tokens 64

We could not run this script end to end. On our Linux CPU test box, the mlx-lm server stopped answering streaming requests after the first few, so we have no numbers to show. Run it on your cluster with 1, 2, 4, and 8 agents, and with one very long prompt mixed in with short ones. Watch the worst first-token time, not only the median.

Rules for a shared cluster

Sources

Key concepts

  • First-token time grows with prompt length. Apple measured about 40 s at 64K tokens on four M5 Ultras.
  • --prefill-step-size sets the chunk size for long prompts. The server default is 2048.
  • Only rank 0 serves HTTP, and it binds to 127.0.0.1 unless you set --host.
  • The mlx-lm server is not meant for production security. Keep it on a private network.

Takeaways

  • Stream answers and set agent timeouts for long prompts.
  • Benchmark prefill step sizes on your own cluster before you pick one.
  • Measure worst-case first-token time with several agents and one long prompt.