← All courses ← Course home

Lesson 03 of 05

Test the link before loading weights

Loading a 500 GB model takes a long time. A link test takes seconds. Run a tiny all_sum on every Mac first, check the answer, time it, and read the output, because mlx.launch can return 0 when a rank fails.

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. Copy the script below to ~/mlx-cluster/check_link.py on every Mac.

Why test first

In the r/LocalLLM thread, a 4 × M3 Ultra cluster failed the same way for the full model launch and for a trivial 10-element all_sum: [jaccl] Couldn't connect (error: 60), before any weights loaded. The same report is in mlx #3755. If the link is broken, you want to know in seconds, not after a long weight load. The MLX docs also say to test locally first.

The test script

It checks three things: the world size is what you expect, all_sum gives the right answer, and how long all_sum takes for small, medium, and large messages.

import os
import sys
import time

import mlx.core as mx

EXPECTED = int(os.environ.get("EXPECTED_WORLD_SIZE", "0"))
BACKEND = os.environ.get("CHECK_BACKEND", "any")

world = mx.distributed.init(backend=BACKEND, strict=True)
rank, size = world.rank(), world.size()

def say(msg):
    if rank == 0:
        print(msg, flush=True)

if EXPECTED and size != EXPECTED:
    print(f"rank {rank}: world size is {size}, expected {EXPECTED}", flush=True)
    sys.exit(2)

# 1. Correctness: every rank adds its rank + 1. The sum must be 1 + 2 + ... + size.
x = mx.full((10,), rank + 1, dtype=mx.float32)
total = mx.distributed.all_sum(x)
mx.eval(total)
want = size * (size + 1) / 2
if not mx.all(total == want).item():
    print(f"rank {rank}: all_sum gave {total.tolist()[:3]}, expected {want}", flush=True)
    sys.exit(3)
say(f"ok: {size} ranks, all_sum correct ({want:.0f})")

# 2. Speed: time all_sum for a few sizes. This uses the same link the model will use.
for n_bytes in (4 * 1024, 1024 * 1024, 64 * 1024 * 1024):
    a = mx.ones((n_bytes // 4,), dtype=mx.float32)
    mx.eval(mx.distributed.all_sum(a))  # warm up
    runs = 10
    start = time.perf_counter()
    for _ in range(runs):
        mx.eval(mx.distributed.all_sum(a))
    ms = (time.perf_counter() - start) / runs * 1000
    say(f"all_sum {n_bytes / 1024:>8.0f} KiB: {ms:8.3f} ms per call")

Run it on one machine first

The ring backend over localhost needs no cables. This checks your Python setup and the script itself:

cd ~/mlx-cluster
EXPECTED_WORLD_SIZE=2 CHECK_BACKEND=ring \
  uv run mlx.launch -n 2 --backend ring --python "$PWD/.venv/bin/python" check_link.py

Output on our Linux test box (CPU only, mlx 0.32.4). Your Mac numbers will be different:

ok: 2 ranks, all_sum correct (3)
all_sum        4 KiB:    0.107 ms per call
all_sum     1024 KiB:    0.621 ms per call
all_sum    65536 KiB:   46.188 ms per call

Pass --python with the full path to the venv Python. Without it, mlx.launch runs the command as given, and on our box -- check_link.py failed with exec: check_link.py: not found.

Run it on the cluster

Environment variables from your shell do not reach other Macs. Pass them with --env:

cd ~/mlx-cluster
uv run mlx.launch --verbose --hostfile jaccl.json --backend jaccl \
  --env EXPECTED_WORLD_SIZE=4 --env CHECK_BACKEND=jaccl \
  --python ~/mlx-cluster/.venv/bin/python check_link.py

We do not have Macs on our test box, so we have no JACCL numbers to show you. The docs say JACCL latency is about an order of magnitude lower than the ring backend. Save your first good output as a baseline for each cable layout, and run the test again after every reboot, cable change, or macOS update.

Read the output, not the exit code

On our test box, when both ranks exited with code 2, mlx.launch printed warnings and still returned exit code 0:

[WARN] Node with rank 0 exited with code 2
[WARN] Node with rank 1 exited with code 2
rank 0: world size is 2, expected 3
rank 1: world size is 2, expected 3
$ echo $?
0

So a script that runs this test must look for the ok: line. Lesson 04 does that.

Size the test like the real job

Sources

Key concepts

  • A broken link fails at mx.distributed.init(), before any weights load.
  • all_sum with known inputs gives a correctness check: the result must be 1 + 2 + ... + N.
  • mlx.launch can return 0 even when ranks fail, so check the output text.
  • Small-message time is latency (matters for tensor). Large-message time is bandwidth.

Takeaways

  • Run check_link.py locally, then on the cluster, before every model launch.
  • Pass variables with --env and the interpreter with --python.
  • Keep a baseline per cable layout and compare after every change.