Repository navigation
Guide: RDMA file transfer over Thunderbolt 5 with JACCL (3.5+ GB/s) #3481
Replies: 4 comments
|
Hi, you can take a look at Example video at https://x.com/angeloskath/status/2022502242893586670?s=20 . Regarding the bridge issue, running PD exhaustion is interesting. We should investigate that. And I should fix issue #3149 . |
|
Thanks for the quick response @angeloskath! We feel a bit silly for reinventing That said, glad the PD exhaustion report is useful — it's reproducible if you launch ~60+ separate JACCL sessions (one per file, which is what our naive first attempt did). Only recovery is a reboot. Happy to provide more detail if it helps the investigation. And +1 on fixing #3149 — the init barrier with One question: Thanks for building this — |
|
Another barrier pattern you can look into for send/recv (workaround #1) is receiver initiated rendezvous. I had a great deal of luck borrowing from patterns established by MPI when I ran into issues other than data corruption. (26.3 gives us SIGABRT for that now at least) For RIR, you would want an out of band channel that doesn't rely on RDMA. Needs to be very low latency for obvious reasons. I've played with the idea of a secondary SideChannel that is accessible from user code (i.e. a "StateChannel"), but I'm not sure how receptive the maintainers would be to that idea. Opinions? Edit:
|
Finding: JACCL works WITHOUT destroying bridge0After extensive testing on a 3-node Thunderbolt 5 cluster, we discovered that bridge0 does NOT need to be destroyed for JACCL to work. Simply assigning IP addresses to the individual Thunderbolt interfaces is sufficient -- JACCL and bridge0 can coexist. This eliminates the need for the bridge-fix script on every boot, and avoids a critical issue where destroying bridge0 can kill USB Ethernet adapters on Mac minis. Hardware
All three connected in a full mesh (3 Thunderbolt cables). The Problem with Destroying bridge0The guide recommends The Fix: Just Assign IPsInstead of destroying bridge0, simply assign IP addresses to the individual Thunderbolt interfaces: # Node 0 (Mac mini)
sudo ifconfig en4 10.0.2.2/24 up # TB cable to Node 1
# Node 1 (Mac Studio)
sudo ifconfig en3 10.0.1.1/24 up # TB cable to Node 2
sudo ifconfig en4 10.0.2.1/24 up # TB cable to Node 0
# Node 2 (Mac mini)
sudo ifconfig en4 10.0.1.2/24 up # TB cable to Node 1
sudo ifconfig en3 10.0.3.1/24 up # TB cable to Node 0bridge0 remains intact. USB adapters stay connected. RDMA devices show Proof: 3-Node Simultaneous JACCL InitWith bridge0 present and active on all three nodes: import mlx.core as mx
import os
os.environ["MLX_IBV_DEVICES"] = "/tmp/jaccl_3node.json"
os.environ["MLX_RANK"] = "0" # 0, 1, or 2 per node
os.environ["MLX_JACCL_COORDINATOR"] = "0.0.0.0:5555" # rank 0; others use rank 0's LAN IP
g = mx.distributed.init(backend="jaccl", strict=True)
print(f"Rank {g.rank()}: size={g.size()} SUCCESS")Device matrix ( [[null, "rdma_en4", "rdma_en3"], ["rdma_en4", null, "rdma_en3"], ["rdma_en3", "rdma_en4", null]]Output (all three nodes): Throughput Benchmark (3-Node, bridge0 present)7.4 GB/s sustained across 3 nodes with bridge0 intact -- matches throughput reported in this guide with bridge0 destroyed. Real-World Validation: Distributed LLM InferenceWe run this with Exo using the JACCL backend across all 3 nodes (368 GB unified memory):
Why It Worksbridge0 aggregates the TB interfaces at the network layer, but the RDMA layer ( The errno 22 ( Summary
Note on Mac Studio TB Port SelectionOn the Mac Studio M3 Ultra, we found that Receptacle 1 (front-right TB port, nearest to the Ethernet port) was unreliable for RDMA connections. Moving the cable to Receptacle 2 or 3 resolved the issue. This is a known limitation -- if you experience intermittent RDMA failures on a Mac Studio, try a different TB port before debugging further. Patches Used in Our SetupOur testing environment includes several patches that may be relevant if you encounter issues reproducing these results: MLX (built from source, v0.31.1):
Exo (on custom
The core finding (JACCL works without destroying bridge0) was verified using raw MLX Python without Exo, so the bridge0 discovery itself does not depend on any of these patches. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RDMA File Transfer over Thunderbolt 5 with JACCL
We built a high-speed file transfer tool using MLX's distributed primitives over RDMA/Thunderbolt 5, achieving 3.5–3.8 GB/s sustained throughput — roughly 23× faster than rsync over the same link. Sharing the working solution and the critical workarounds we discovered, since there's no existing documentation for this use case.
Tested on Mac Studio M3 Ultra (macOS 26.3.1) with MLX 0.30+ and JACCL backend.
The Problem
JACCL provides RDMA over Thunderbolt 5, but
mx.distributed.send/recvis designed for tensor operations during training and inference, not file I/O. Using it naively for file transfer hits several issues that crash or hang the process. After extensive debugging, we found three critical workarounds and one macOS-level fix that are required.Results
We distributed 705 GB of quantized LLM weights across 3 nodes in ~3.5 minutes using sequential RDMA transfers between directly-connected pairs.
Critical Workaround 1: Use
stream=mx.cpufor All Send/RecvSymptom: Receiver crashes with
[METAL] Command buffer execution failed: Caused GPU Timeout Error. Sender reports high throughput but receiver writes zero files.Root cause: By default,
mx.distributed.send/recvare scheduled on the GPU stream. Metal has an unconfigurable command buffer timeout (~5 seconds). RDMA recv operations that take longer than this trigger the timeout and kill the process. The sender doesn't crash because send operations complete quickly once data is in the RDMA buffer, but the receiver must wait for data to arrive.Fix: Pass
stream=mx.cpuon everysend()andrecv()call:This bypasses Metal entirely. The RDMA transfer still happens at full hardware speed — the CPU stream just means MLX doesn't create a Metal command buffer for the operation.
What doesn't work:
mx.eval()after every recv — can deadlock with the senderMLX_METAL_FAST_SYNCH=0— no effectRelated: #3142
Critical Workaround 2: Init Barrier After
distributed.init()Symptom: First
send/recvafter init returns garbage data or wrong shapes. Subsequent operations may hang or produce corrupted results.Root cause: JACCL needs all ranks to synchronize after initialization before point-to-point operations work reliably. Without a barrier, one rank may start sending before the other has finished setting up its RDMA resources.
Fix: Run an
all_sumbarrier immediately after init, before any send/recv:Important: Use
mx.ones(10), notmx.ones(1). The single-element variant doesn't reliably trigger the full synchronization path in JACCL.Related: #3149
Critical Workaround 3: Single JACCL Session (PD Exhaustion)
Symptom: After ~60 successful file transfers, JACCL fails with
Couldn't allocate protection domain.Root cause: Each
mx.distributed.init()/ teardown cycle allocates and (incompletely) releases RDMA protection domains. The kernel has a hard limit. An approach that launches a separate JACCL session per file hits this limit quickly.Fix: Transfer all files in a single JACCL session. One init, all transfers, one teardown.
Recovery: If you hit PD exhaustion, the only fix is to reboot the affected node. There is no way to release protection domains without a reboot.
macOS Thunderbolt Bridge (Required Fix)
macOS creates a "Thunderbolt Bridge" network service that absorbs all TB ports into a single
bridge0interface. This completely breaks RDMA — JACCL cannot address individual Thunderbolt ports through a bridge.Symptom: Every RDMA device combination fails with
RTR errno 22 (EINVAL)during queue pair setup.Critical: macOS re-enables Thunderbolt Bridge on every reboot and OS update. You must run the fix after every boot.
After running this,
ibv_devinfoshould show individual RDMA devices withPORT_ACTIVEstate for each connected Thunderbolt cable.Complete Transfer Script
A working script that transfers an entire directory in a single JACCL session with all workarounds applied:
Wire Protocol
Header is packed as
struct.pack("<QQ", name_len, file_size)— two little-endian 64-bit unsigned integers. The last chunk of each file may be smaller than CHUNK_SIZE.Troubleshooting
RTR errno 22 (EINVAL)fix-tb-bridge.shRTR errno 16 (EBUSY)Caused GPU Timeout Errorstream=mx.cpuCouldn't allocate protection domainall_sum(mx.ones(10))after initibv_devinfoshows PORT_DOWNHostfile Notes
RDMA device names (
rdma_en2,rdma_en5, etc.) and TB5 interface IPs are dynamically assigned every timemlx.distributedconfigures the mesh. They change after every reboot or mesh reconfiguration. The only stable identifiers are the 10GbE IPs in the hostfile'sipsfield. The hostfile must be present on all participating nodes — not just the launcher.Limitations
Hardware
ibv_devinforeports 8X width at 10.0 Gbps per lane = 80 Gbps per portHope this helps others working with MLX distributed and Thunderbolt 5!
All reactions