Conversation
…k in init errors On Apple's Thunderbolt RDMA stack a UC queue pair never reports a dead peer (no error completion, no event) and RC queue pairs are not available (ibv_create_qp fails with EOPNOTSUPP), so when a rank dies every other rank polls its completion queue forever at 100% CPU (ml-explore#4278). The TCP side channel already lives as long as the group. Expose its socket fds (TCPAllGather::fds, SideChannel::liveness_fds) and, in every completion wait loop of the mesh and ring implementations, check them while no completion arrives: at most every 250 ms, poll() with a zero timeout and a one-byte MSG_PEEK on POLLIN. HUP/ERR/NVAL or a zero-byte peek means the peer process is gone and the loop throws a runtime_error naming the peer and the operation. No reads, no writes, no locks, so it is safe from the ring wire threads. Rank 0 watches every peer and the other ranks watch rank 0, whose throw closes its sockets, so a non-zero rank's death reaches everyone in two hops. Measured on two M3 Ultras: 0.25 s from SIGKILL to the exception on the survivor, mesh and ring, either rank. A progress timeout (JACCL_PROGRESS_TIMEOUT_S / MLX_JACCL_PROGRESS_TIMEOUT_S, default 600 s, 0 disables) backstops the cases the side channel cannot see: a lost UC frame (no retransmission) or a peer that is alive but wedged. Diagnostics: on a device whose port is PORT_DOWN ibv_open_device succeeds but ibv_alloc_pd returns NULL, and a peer whose IPv4-mapped GID is not reachable on this link fails the RTR transition with errno 60; both errors now name the device, its port state and the peer address. TCPSocket::recv reports "peer closed the side channel" when recv() returns 0 instead of a stale errno (the "Recv failed with errno=2" that every other rank printed when one rank failed during init). The stock 3-argument SideChannel constructor is kept as an overload so the exported symbol set of libjaccl is a superset of the previous one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a rank of a JACCL group dies, the other ranks never find out. A UC queue pair on Apple's Thunderbolt RDMA stack produces no error completion and no event for a dead peer, and RC queue pairs are not available (
ibv_create_qpfails with errno 102), so every completion wait loop inmesh_impl.h/ring_impl.hspins at 100 % CPU indefinitely. This is #4278.Separately, the init errors hide their cause. On a device whose Thunderbolt port is
PORT_DOWN,ibv_open_devicesucceeds butibv_alloc_pdreturns NULL, reported as a bare "Couldn't allocate protection domain". A peer whose IPv4-mapped GID is not reachable on this link fails RTR with errno 60, reported without device or address. And when one rank fails during init, the coordinator closes the side channel and every other rank dies with "Recv failed with errno=2": a stale errno from::recvreturning 0, which reads like an RDMA failure. (#3467 is the same family: a wrong GID /sgid_indexgives RTR errno 22.)Change
Peer liveness from the side channel. The TCP side channel lives as long as the group.
TCPAllGather::fds()exposes its socket fds andConfig::get_side_channel()passes them toSideChannel::liveness_fds(), whichMeshGroup/RingGrouphand toMeshImpl/RingImpl. Every completion wait loop (6 in mesh, 4 in ring) owns aProgressGuardand callstick(n > 0)afteribv_poll_cq. While nothing completes, at most every 250 ms the guardpoll()s the fds with a zero timeout and does a one-byteMSG_PEEKonPOLLIN;POLLHUP/POLLERR/POLLNVALor a zero-byte peek means the peer process is gone →std::runtime_error("[jaccl] peer is gone: side channel to rank N closed while waiting in <op> …"). It never reads, writes or locks, so it is safe from the ring wire threads. Rank 0 watches every peer; other ranks watch rank 0, whose throw closes its sockets, so a non-zero rank's death reaches everyone in two hops. When JACCL is initialised with a user-supplied all-gather (no TCP coordinator) the fd list is empty and only the timeout below applies.Progress timeout backstop.
JACCL_PROGRESS_TIMEOUT_S/MLX_JACCL_PROGRESS_TIMEOUT_S(default 600 s, 0 disables): no completion for that long →runtime_error("[jaccl] no progress in <op> for N s …"). Covers what the side channel cannot see: a lost UC frame, or a peer that is alive but wedged. The default is deliberately longer than any legitimate wait.Diagnostics.
alloc_pdfailure names the device and its port state; RTR failure names the device, port state and the peer's IPv4 from the GID and decodes errno 60/22; side-channel EOF says "peer closed the side channel (another rank failed during init or exited)".Happy path cost:
tick(true)is two stores; the slow path runs after 256 empty polls and then at most every 250 ms. The stock 3-argumentSideChannelconstructor is kept as an overload, so the exported symbols oflibjacclare a superset of before (checked withnm -gUagainst the 0.32.2 wheel).Testing
There is no way to exercise JACCL without Macs on Thunderbolt 5 with RDMA enabled, so this cannot be an automated test in
python/tests. It was tested on five M3 Ultras in a full Thunderbolt 5 mesh (macOS 26.6.1, mlx 0.32.2 wheel with the rebuiltlibjaccl.dylibdropped in on every node). First the isolated scenarios on a 2-node group:all_sum16 MB, clean_exit()at iter 20_exit()MLX_JACCL_RING=1), rank 1_exit()Still on the 2-node group but inside a production orchestrator: a pipeline-parallel generation with a rank SIGKILLed mid-stream ends with the exception on the survivor in under a second, three times in a row, and the group re-forms afterwards.
Then on the 5-node group (five M3 Ultras, full Thunderbolt 5 mesh, 20 RDMA edges) serving GLM-5.3 (744B, 8-bit, pipeline parallel over JACCL):
[jaccl] peer is gone: side channel to rank 4 closed while waiting in all_gather(rank 0) and… to rank 0 (coordinator) closed …(ranks 1–3) within the same second, the orchestrator purged and re-formed the group with no operator action, twice;Facts behind the diagnostics, measured with a libibverbs-only program that
dlopenslibrdma.dylibexactly like JACCL: 10 queue pairs per device (errno 16 on the 11th, shared across processes), ≥ 512 MRs, no leak of QPs/MRs across clean exit,_exit, or SIGKILL;alloc_pdNULL on aPORT_DOWNdevice; RTR errno 60 for an unreachable peer GID; RTR errno 22 for a zero GID. The probe sources are in https://github.com/Odyssai-eu/OdyssAI-X/tree/main/scripts/jaccl (rdma_probe.c,rdma_pair.c,smoke_jaccl.py).