Add OdinLink (odl_tb5) TP transport: --transport odl - #1018
Open
davidcanar wants to merge 3 commits into
Open
Conversation
Native OdinLink stream API over Thunderbolt, dlopen-loaded like the verbs stack (no link-time dependency). Each rank receives on its own stream, ODL_READY barrier at attach, non-blocking recv poll with the control-socket peer-death check and the gate deadline, odl branches in the gate/batch/big exchanges, bulk path with a 4 MiB send-ahead window. Protocol v11 (hello pad word becomes odl_ok); auto prefers odl. Transport A/B on 2x Strix Halo (tp_odl_test.c, real ds4_tp.o, 24KB gate vectors): odl p50 30.1us / p99 52.4us vs tcp p50 31.0us / p99 60.5us on an idle link; tcp degrades to p50 262us under concurrent disk load while odl stays at 30us. 50k-gate soak clean. Bulk ~1.1 GB/s per direction (stream-API kernel ceiling).
…timeout
odl_tb5 delivers whole messages in order, but when one Thunderbolt frame of
a message fails CRC the driver drops the entire message ("fragment gap ...
dropping message" in dmesg) and the sender is never told. A gate that
waited for such a message parked both ranks on their spin kernels until the
timeout and took the TP session down: seen twice on a 2x Strix Halo pair
in one afternoon, once in an MTP verify block and once 4096 tokens into an
11k-token ds4-server prefill (the link negotiates 10 Gb/s per lane and logs
CRC errors every few GB).
Every stream message now carries a 16-byte header (magic, kind, chunk
index, exchange sequence). A receiver asks for the first chunk it misses
either as soon as a later index reveals the gap or after a retry interval
without progress (2 ms for gate vectors, 50 ms for bulk), and a sender
keeps the framed copies of its current and previous exchange: lockstep
bounds the peer's lag to one exchange (it cannot finish N+1 without our
N+1 data, which we only send after finishing N), so those two buffers
always cover a request. Chunks of the next exchange that arrive while
this one still misses data are parked and replayed. Requests are served
from the exchange loop and, for a rank that is idle or waiting on the
control channel (worker between commands, leader waiting for an ack,
either side in the big-gate header handshake), from the polling wait that
now fronts those reads. No extra round trip on the fast path: a decode
gate costs one 16 KB frame copy on each side.
Measured with tp_odl_test over the real link, 3000 gates of 16 KB plus the
batch and 2 MiB big exchanges: p50 29.0 us (29.3 us before), 2 MiB swap
2.2 ms (1.8 ms before, the header/frame copies). With
DS4_TP_ODL_DROP_TEST=5 on both ranks (every fifth data send dropped) all
exchanges complete with verified payloads and one retransmission per
drop; p90 gate latency becomes the 2 ms detection interval. Dropping 1 in
3 sends on one rank only also completes cleanly. The knob is a diagnostic
that exercises this path; it is off unless set.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
davidcanar
force-pushed
the
odinlink-transport
branch
from
September 13, 2026 22:30
73d9909 to
51f2b49
Compare
The reliable odl exchange numbers every round identically on both ranks and validates chunk sizes, so the header round trip on the TCP data socket that preceded each batch and big gate only added a ~150 us control-channel hop per gate: negligible per 2048-token prefill chunk, but ~6 ms of the 156 ms MTP verify cycle (42 batch gates). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Adds
--transport odlto the two-node tensor-parallel data plane: the OdinLink (odl_tb5) Thunderbolt RDMA userspace library, loaded withdlopenat runtime exactly like the verbs stack — no link-time dependency, and builds without OdinLink installed fall back to TCP at zero cost.autonow prefers odl over verbs over tcp.odl_ok, negotiated likerdma_ok.ODL_READYframe at slab attach is the rendezvous (a send to a stream the peer has not opened yet is dropped by its kernel demux).stream_sendper direction (the kernel fragments at 4024-byte frames internally); receive side runsstream_recvon anO_NONBLOCKfd with the same peer-death sampling and gate deadline as the verbs CQ poll loop.51f2b49): every stream message carries a 16-byte header and receivers request the chunks the driver dropped; details in the section below.15b6d6b): the reliable exchange numbers its rounds and validates chunk sizes, so the header round trip only added ~150 us per gate (harness batch gate 265 -> 104 us; about 6 ms of a GLM MTP verify cycle).odl_tb5.hsignatures through void pointers.tp_odl_test.cincluded: a model-free transport harness that drives the realds4_tp.oon both ranks with a synthetic identity, verifies payload integrity, and prints latency stats.Test evidence (per CONTRIBUTING.md)
Machine: 2x AMD Ryzen AI Max+ 395 (Radeon 8060S, gfx1151, 128 GB), Fedora 44, Thunderbolt 5 direct link, OdinLink-Five driver
odl_tb5, ROCm build (make strix-halo).Transport-level A/B on both ranks (24 KB gate vectors, payload integrity verified):
A 50,000-gate soak is clean (no desync, no corruption). Notably, TCP over
thunderbolt_netshows load-dependent variance (p50 between 31 and 262 us across runs taken while a large background download ran) while odl stays at ~30 us throughout — kernel bypass removes the tail under system load.End-to-end (GLM 5.3 Flash Q4_K across the same two boxes, short-prompt exact prefill + decode, ctx 8192): prefill 25.8 t/s on odl vs 18.4 t/s on tcp; generation 8.7 t/s on both. (Decode turned out to be memory-bandwidth-bound on the replicated BF16 KDA attention weights rather than compute-bound; the kernel-level breakdown is in #1024.)
Loss recovery (added after the initial submission,
51f2b49)In production the first version died twice in one afternoon: once in an MTP verify block, once 4096 tokens into an 11k-token ds4-server prefill. Both times dmesg on the receiving rank shows
odl_tb5: RX CRC error ... frame droppedfollowed byfragment gap ... dropping message: when one Thunderbolt frame of a message fails CRC, the driver drops the whole message and the sender is never told. The gate then parks both ranks on their spin kernels (100% GPU) until the timeout and the TP session is dead for good. On this pair the link negotiates 10 Gb/s per lane (2 lanes) and drops a frame every few GB, so any long prefill was doomed.The fix keeps the stream API and adds recovery on top:
DATA/NACK), chunk index, exchange sequence (both ranks number exchanges identically).DS4_TP_ODL_DROP_TEST=Ndrops every Nth data send as a diagnostic for this path; off unless set.Harness over the real link (16 KB gate vectors, 3000 iterations, payload verified on every receive):
The p90 under injected loss is the 2 ms detection interval; the bulk case with the last chunk dropped waits the 50 ms interval.
End-to-end with the GLM-5.3-Flash TP setup of #1024 (2x gfx1151, odl, Q8-attention GGUF): decode 15.35 t/s without and 15.23 t/s with
DS4_TP_ODL_DROP_TEST=400(12 dropped gate messages per rank recovered, greedy output byte-identical over 96 tokens); a 323-token prompt prefilled at 69.1 t/s without and 67.5 t/s withDROP_TEST=50(75 drops per rank recovered, output identical). Without injected drops the ranks still request a retransmission about once per second: a rank that reaches a gate more than the 2 ms interval late is asked as well, which costs a duplicate 16 KB send and nothing else, so the log prints only the first few and then every 100th request. With the batch/big-gate handshake removed the GLM MTP cycle went from 10.4 to 11.8 t/s and the 323-token prefill from 69 to 78 t/s (measurements in #1024).Known limitation: bulk throughput is bounded by the odl stream API's per-fragment kernel path (~1.1 GB/s per direction, and the observed link speed above); the double-buffer DMA API should raise it and is left as future work. The full
make testsuite was not run for this change (needs the CPU test binary on hand); transport behavior was validated with the included harness on both ranks plus the end-to-end runs above.🤖 Generated with Claude Code