← All courses ← Course home

Lesson 04 of 05

Fix startup errors, fall back to ring

JACCL is new and people report startup timeouts, queue pair errors, and crashes that only a reboot of every Mac clears. Learn the error messages, a recovery order, and a script that falls back to the ring backend.

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. You also need a ring hostfile, made in the last section of this lesson.

What people report

Error textWhenReports
[jaccl] Couldn't connect (error: 60) after retries of 1, 2, 4, 8 sAt mx.distributed.init(), before weights load. Error 60 is ETIMEDOUT.r/LocalLLM (4 × M3 Ultra, macOS 26.4.1, MLX 0.31.2), mlx #3755 (open)
ValueError: [jaccl] Changing queue pair to RTR failed with errno 22When loading a model or mid-inference, on all nodes at onceexo #1847 (open, 3 × M3 Ultra, macOS 26.3.1)
RuntimeError: [jaccl] Recv failed with errno=2 or errno=60Mid-inference, and in one comment after 23 minutes idleexo #1847
JACCL errno 14 or errno=2 on a healthy MacThe other Mac's runner died because its shard did not fitexo #2430

Things we learned from these reports:

About MLX_METAL_FAST_SYNCH=1: Apple's post passes --env MLX_METAL_FAST_SYNCH=1 in its benchmark commands. The MLX docs say this setting is not reliable, can deadlock and leave the GPU wedged (mlx #3142, open), is off by default, and is best left unset. For a service that must stay up, leave it off. If you turn it on for a benchmark, say so in your results.

Recovery runbook

  1. Stop every rank on every Mac: for h in mac1 mac2 mac3 mac4; do ssh $h 'pkill -f mlx_lm; pkill -f check_link.py'; done.
  2. On every Mac run ibv_devinfo. Reseat any cable whose port is not PORT_ACTIVE.
  3. Run check_link.py again (lesson 03).
  4. If it still fails, reboot all Macs, not only the one that printed the error. Then run the test again.
  5. If it still fails, run the cluster on the ring backend (below) and keep notes: macOS build, MLX version, exact error, which host. That is what maintainers ask for in the issues above.

Make a ring hostfile

The ring backend uses TCP sockets, so it works over Thunderbolt IP links or Ethernet. Rank i talks only to rank i − 1 and rank i + 1. The docs describe it as always available and use it for Thunderbolt rings:

uv run mlx.distributed_config --verbose \
  --hosts mac1,mac2,mac3,mac4 --over thunderbolt --backend ring \
  --auto-setup --output-hostfile ~/mlx-cluster/ring.json
# or over your normal network:
#   --over ethernet

On the ring backend, use --pipeline. Tensor parallel sends many small messages per token, Apple says very low latency is critical for it, and the MLX docs describe JACCL as necessary for things like tensor parallelism. If your model only supports tensor (lesson 01), it may still run on ring, but we could not measure how slow that is.

The MLX source also has a jaccl-ring backend option in mlx.distributed_config and mlx.launch. It is not described on the docs page and we did not test it.

Start script with fallback

This script tests JACCL first. If the ok: line is missing, it tests the ring backend and starts the server with --pipeline. It uses Python for the time limit because macOS does not ship a timeout command, and it avoids bash features that macOS's older bash lacks.

#!/usr/bin/env bash
# Test the link first, then start mlx_lm.server on the best backend that works.
set -uo pipefail

MODEL="${MODEL:?set MODEL to a Hugging Face repo or a local path}"
NODES="${NODES:-4}"
JACCL_HOSTFILE="${JACCL_HOSTFILE:-$HOME/mlx-cluster/jaccl.json}"
RING_HOSTFILE="${RING_HOSTFILE:-$HOME/mlx-cluster/ring.json}"
PY="${PY:-$HOME/mlx-cluster/.venv/bin/python}"
CHECK="${CHECK:-$HOME/mlx-cluster/check_link.py}"
PORT="${PORT:-8080}"

smoke() {  # $1 = backend, $2 = hostfile
  local out
  # macOS has no `timeout` command, so use Python for the time limit.
  # mlx.launch returns 0 even when a rank fails, so read the output instead.
  out=$("$PY" -c 'import subprocess, sys; sys.exit(subprocess.run(sys.argv[1:], timeout=120).returncode)' \
          mlx.launch --hostfile "$2" --backend "$1" \
          --env EXPECTED_WORLD_SIZE="$NODES" --env CHECK_BACKEND="$1" \
          --python "$PY" "$CHECK" 2>&1)
  echo "$out"
  grep -q "^ok: $NODES ranks" <<<"$out"
}

if smoke jaccl "$JACCL_HOSTFILE"; then
  BACKEND=jaccl; HOSTFILE="$JACCL_HOSTFILE"; SPLIT=()           # tensor parallel
elif smoke ring "$RING_HOSTFILE"; then
  echo "JACCL failed. Falling back to ring with pipeline split." >&2
  BACKEND=ring; HOSTFILE="$RING_HOSTFILE"; SPLIT=(--pipeline)
else
  echo "Both backends failed. Stop every rank, check cables, reboot all Macs." >&2
  exit 1
fi

echo "starting server: backend=$BACKEND split=${SPLIT[*]:-tensor}"
${DRY_RUN:+echo} mlx.launch --hostfile "$HOSTFILE" --backend "$BACKEND" -- \
  "$PY" -m mlx_lm server --model "$MODEL" --host 0.0.0.0 --port "$PORT" ${SPLIT[@]+"${SPLIT[@]}"}

What we could test: on our Linux box (no Macs, so no JACCL), with DRY_RUN=1, a broken JACCL hostfile and a two-rank local ring, the script fell back and printed the ring command:

mlx.launch: error: The hostfile is malformed: no RDMA device is listed for m3-ultra-1 to m3-ultra-2, ...
ok: 2 ranks, all_sum correct (3)
...
JACCL failed. Falling back to ring with pipeline split.
starting server: backend=ring split=--pipeline
mlx.launch --hostfile ring2.json --backend ring -- /path/.venv/bin/python -m mlx_lm server --model test/model --host 0.0.0.0 --port 8080 --pipeline

We did not run the JACCL success path, because that needs real Macs with RDMA. If the Python time limit stops mlx.launch, ranks on other Macs can keep running. Step 1 of the runbook cleans them up.

Sources

Key concepts

  • Couldn't connect (error: 60) is a timeout at startup. Changing queue pair to RTR failed with errno 22 is a queue pair setup failure.
  • Reports show some JACCL failures clear only after rebooting every Mac. No documented reset API was found.
  • MLX_METAL_FAST_SYNCH=1 is in Apple's benchmark commands, but the MLX docs say leave it unset.
  • The ring backend uses TCP and fits pipeline parallel. JACCL is what makes tensor parallel practical.

Takeaways

  • Keep a written recovery order: stop all ranks, check ports, test, reboot all, test, then fall back.
  • Start the server only after a passing link test, and fall back to ring plus --pipeline on failure.
  • Log the macOS build, MLX version, and exact error every time, so you can compare with open issues.