Files
papers/hanzo-heterogeneous-distributed-inference.tex
T
zeekay 7fdca28595 feat(distributed-inference): cross-vendor TP result + open-problems companion
Four papers on heterogeneous distributed LLM inference:

- hanzo-heterogeneous-distributed-inference: cross-vendor (AMD ROCm gfx1151 +
  NVIDIA GB10 CUDA) tensor-parallel decode over a 2.5GbE cable, coherent at
  50.4 tok/s (TTFT 0.09s, 213 tok/s prefill). Root cause of prior corruption
  proven: per-backend auto-dtype (ROCm->f16, CUDA->bf16) reinterpreted raw on
  the wire. Fixed by a canonical f32 ring wire + world_size-1 accumulating
  all-reduce (correct for any N>=2; spurious power-of-two gate removed).
  Same-vendor f16+f16 control matches token-for-token.
- hanzo-distributed-open-problems: NEW companion enumerating 8 open research
  problems (sampling-determinism boundaries, quant-aware TP sharding, PP
  layer-assignment ILP, adaptive speculation under RTT/acceptance drift, the
  prefill launch-overhead wall, KV wire compression, the geo decode latency
  floor, and cheap verifiable inference), each cross-referenced to the paper
  that owns the surrounding result.
- hanzo-distributed-decode-latency, hanzo-consensus-zap-compute: consistency
  pass (canonical f32 wire terminology; link goodput unified to 280 MB/s =
  2.24 Gbit/s; precise f16-vs-bf16 root-cause wording).

INDEX.md regenerated via `make index`; besides the 4 above it also indexes 6
pre-existing root papers the previous index had missed. All four compile clean
with pdflatex (10/10/13/9 pages).
2026-07-09 16:31:20 -07:00

469 lines
35 KiB
TeX

\documentclass[11pt]{article}
\input{shared/kernel-preamble.tex}
\title{\textbf{One Cluster, Every Vendor:\\ Cross-Vendor Tensor-Parallel LLM Inference over Commodity Ethernet}}
\author{Hanzo AI --- ML Systems}
\date{Distributed companion to the \emph{One Source, Every Backend} series --- extends Paper 05 across the wire}
\begin{document}
\maketitle
\begin{abstract}
Real accelerator fleets are vendor-mixed: an AMD APU, an NVIDIA dev kit, and a Mac sit on the same
desk, and no collective-communication library spans them --- NCCL is NVIDIA-only, RCCL is AMD-only, and
neither will open a channel to the other. This paper reports a working result, with its limits stated
plainly. One inference engine (\code{hanzo-engine}, Rust, one codebase, per-backend kernels) runs
on three vendor-diverse accelerators --- AMD ROCm gfx1151 (a ``Strix Halo'' APU, node \emph{evo}), NVIDIA
CUDA sm\_121 (a GB10 Blackwell, node \emph{spark}), and Apple Metal M4 Max (node \emph{dbc}). We make its
Megatron-style tensor-parallel (TP) collectives \emph{vendor-agnostic} by host-staging every exchange
--- \code{device}$\to$\code{CPU}$\to$\code{TCP}$\to$\code{CPU}$\to$\code{device} --- so any device that
can copy to host storage can join the ring, which is exactly what NCCL/RCCL cannot do across a vendor
boundary. On a two-node ring (\emph{evo} ROCm $+$ \emph{spark} CUDA) over a 2.5GbE direct link, the
sharded engine \textbf{builds, connects, shards a \code{world\_size}$=$2 tensor-parallel model, and
generates fully coherent multi-token output --- chain-of-thought and a correct answer --- across the
AMD/NVIDIA boundary at 50.4~tok/s decode} (TTFT 0.09s, 213~tok/s prefill). To our knowledge this is the
first reported cross-vendor (AMD$+$NVIDIA) tensor-parallel LLM inference over commodity ethernet in a
single engine.
We are equally explicit about the engineering, because the honest failures are the paper's most useful
content. The socket-ring path had only ever been \emph{compiled}, never \emph{run} (only the NCCL path
was exercised), and bringing it up cross-vendor surfaced four heterogeneity bugs --- a \code{len}$^2$
byte-count overflow, a missing ROCm host-staging arm that meant the ring never even compiled for AMD, a
10s handshake timeout that raced heterogeneous startup, and --- the subtle one that blocked coherent
decode --- a \emph{heterogeneous-dtype-on-the-wire} hazard, where the ROCm rank auto-selects \code{f16}
and the CUDA rank auto-selects \code{bf16} and each reinterpreted its neighbour's 16-bit words as the
wrong floating-point layout, corrupting the reduction from the first layer. The cure is a canonical
\code{f32} ring wire (a lossless superset of both) plus a \code{world\_size}$-$1 accumulating all-reduce
correct for any \code{N}$\geq$2 and the removal of a spurious power-of-two world-size restriction; all
four fixes are merged on \code{main}, and a same-vendor control (both ranks \code{f16}) matches the
cross-vendor run token-for-token, isolating the wire dtype as the sole cross-vendor variable. The
measured cost of the primitive is itself a result: 50.4~tok/s is $\approx$3.3$\times$ slower than the same
model single-node (168~tok/s), and the gap is \emph{all} synchronization --- an all-reduce every layer,
over the wire. That measured tax is the empirical case for \textbf{pipeline parallelism} --- contiguous
layer ranges, activations-only on the wire (\code{O(nodes)} hops, not \code{O(layers)} all-reduces),
format-agnostic, one generation loop driving stateless workers --- as the structurally better primitive
for heterogeneous clusters: the numeric fixes made TP \emph{correct}, not well-matched. Three residuals
stay honestly open, and we develop them, with five further problems, as a research agenda in the
companion~\cite{openproblems}: the two ranks still sample independently from their own-dtype logits (a
near-tie argmax can diverge and desync the KV caches), the CUDA rank must run graphless to stay lockstep
with the graphless ROCm rank, and TP is still coupled to the dequantized (safetensors) weight path.
\end{abstract}
\section{The problem: fleets are vendor-mixed, and the collective libraries are not}
\label{sec:problem}
The single-node premise of this series is that the interesting inference target is a commodity box a
customer already owns~\cite{backend}. The multi-node reality is that they own \emph{several}, and not
from one vendor. A team that bought an AMD ``Strix Halo'' mini-PC last quarter, a Grace-Blackwell dev kit
this quarter, and codes on a MacBook has three capable accelerators totalling well over a hundred
gigabytes of unified memory --- enough to hold a model no single one of them can --- sitting idle on a
LAN. The obstacle is not the models and not the math. It is that \emph{the collective-communication
layer is single-vendor by construction}.
Tensor parallelism~\cite{megatron} shards each weight matrix across ranks and, twice per transformer
block (after attention, after the MLP), performs an \code{all\_reduce} to sum the partial results back
into a full activation. That \code{all\_reduce} is the whole game, and in practice it is delegated to a
vendor library: \textbf{NCCL}~\cite{nccl} on NVIDIA, \textbf{RCCL}~\cite{rccl} on AMD. Both are excellent
and both are closed at the vendor boundary: NCCL speaks to NVIDIA GPUs over NVLink/PCIe/GPUDirect and
has no concept of an AMD peer; RCCL is the symmetric mirror. There is no NCCL$\leftrightarrow$RCCL
channel, no cross-vendor GPUDirect, and no shared handle either side will accept. So the textbook path to
running one model across \emph{evo} and \emph{spark} together does not merely underperform --- it does
not exist.
The engine already dodges the \emph{first} half of this problem. \code{hanzo-engine} is one Rust codebase
that lowers to CUDA, ROCm, Metal, Vulkan, and CPU behind a single model loader, reaching \code{llama.cpp}
decode parity on all three vendors from that one build~\cite{backend}; the kernel layer that makes ``one
source, every backend'' hold is the subject of the companion DSL papers~\cite{umbrella,fuse,oracle}. What
was missing is the \emph{second} half: a way for those per-vendor engines to talk to \emph{each other}
inside one cluster. This paper builds that channel, runs it across a vendor boundary for the first time,
and reports --- without varnish --- exactly how far it gets and what it cost to get there.
\section{The design: a host-staged socket ring makes the collective vendor-agnostic}
\label{sec:design}
The engine carries a unified \code{Comm} abstraction with three backends behind one enum: \code{Nccl}
(the fast same-vendor NVIDIA path), \code{Ring} (a socket-based collective), and \code{Dummy}
(\code{world\_size}$=$1). The idea that makes cross-vendor work possible is in the \code{Ring} backend,
and it is deliberately humble: \textbf{stage every tensor through host memory and move the bytes over
TCP.}
Concretely, a ring \code{sum\_all\_reduce} does not touch a vendor collective at all. It pulls the
tensor's storage to the CPU, converts it to a canonical \code{f32} payload, exchanges the bytes with its
ring neighbours over ordinary TCP sockets, sums in \code{f32}, and copies the reduced result back to the
device in its native dtype (Listing~\ref{lst:stage}). The single load-bearing property is that the
exchange happens in \code{CpuStorage}, which \emph{every} backend can produce --- CUDA, Metal, and ROCm
each implement \code{to\_cpu\_storage()}. The device that produced the tensor is irrelevant by the time
the bytes reach the socket. \emph{Why} the wire dtype is a canonical \code{f32} rather than the raw
device bytes --- the difference between coherent and garbage output across vendors --- is the subject of
\S\ref{sec:rootcause}; here it is simply the shipped design.
\begin{lstlisting}[language=Rust,caption={The vendor-agnostic collective, condensed from the shipping
code. The reduce goes device$\to$host$\to$canonical \code{f32}$\to$TCP$\to$\code{f32} sum$\to$device; the
\code{match} over \code{Storage} is the only place the backend is named, and every arm lands in the same
\code{CpuStorage}. The \code{cpu\_to\_f32} stage is what makes an \code{f16} rank and a \code{bf16} rank
agree (\S\ref{sec:rootcause}). NCCL, by contrast, would keep the whole exchange on-device and refuse a
foreign-vendor peer.},label={lst:stage}]
fn sum_all_reduce(&self, xs: &Tensor) -> Result<Tensor> {
let storage = xs.storage_and_layout().0;
let host = match &*storage { // device -> host (any backend)
Storage::Cpu(s) => s.clone(),
Storage::Cuda(s) => s.to_cpu_storage()?,
Storage::Metal(s) => s.to_cpu_storage()?,
#[cfg(feature = "rocm")]
Storage::Rocm(s) => s.to_cpu_storage()?, // was missing: ring never built for AMD (Bug 2)
};
let wire = cpu_to_f32(&host)?; // CANONICAL wire dtype: f16|bf16|f32 -> f32
let sum = self.ring_exchange(&wire, xs.dims())?; // world_size-1 accumulating steps, sum in f32
Tensor::from_slice(&sum, xs.dims(), xs.device())? // host -> device
.to_dtype(xs.dtype()) // back to this rank's native compute dtype
}
\end{lstlisting}
This is not free. NCCL keeps the reduction on-device and rides NVLink; the host-staged ring pays two
extra device$\leftrightarrow$host copies per collective, a widening cast to \code{f32}, and then rides a
2.5GbE link whose measured goodput is $\approx$280~MB/s (\S\ref{sec:repro}). That is the correct trade for
the problem: we spend bandwidth to buy \emph{vendor-agnosticism}, because the alternative on a mixed fleet
is not a faster collective, it is \emph{no} collective. The ring is selected purely by configuration ---
\code{use\_ring()} is true when the \code{ring} feature is built and a \code{RING\_CONFIG} path is present
in the environment (\S\ref{sec:repro}) --- so the same engine binary is a single-node server, an NCCL
rank, or a socket-ring rank depending only on how it is launched.
\section{Compiles $\ne$ runs: an unexercised path was broken three ways}
\label{sec:compiles}
Here is the lesson that generalizes past this codebase. \emph{The ring path had been written, had type-
checked, had shipped in every build for months --- and had never once been run.} All multi-GPU testing
had gone through NCCL (the same-vendor path), so the socket ring was dead code that happened to compile.
The first time it was asked to carry a real tensor across a real vendor boundary, it failed three
separate ways, each a different species of the gap between ``the compiler accepted it'' and ``it does the
thing.'' All three are fixed on \code{main} (commit \code{63cc4c7ac}); the fourth, subtler failure ---
visible only once the generated tokens came out wrong --- is \S\ref{sec:rootcause}.
\paragraph{Bug 1 --- a \code{len}$^2$ byte count (a logic error the types could not catch).}
The wire code computed its transfer size as
\begin{center}
\code{let nbytes = x.len() * std::mem::size\_of\_val(x);}
\end{center}
This looks reasonable and is badly wrong. For a slice \code{x:\ \&[T]}, \code{size\_of\_val(x)} is
\emph{already} the size of the whole referent --- \code{x.len()}\,$\times$\,\code{size\_of::<T>()} --- so
multiplying by \code{x.len()} again yields \code{len}$^2$\,$\times$\,\code{elem\_size}, overshooting the
true byte count by a factor of \code{len}. The subsequent \code{from\_raw\_parts(ptr, nbytes)} then
fabricates a byte slice spanning far past the end of the allocation, and \code{write\_all} walks off the
buffer and hits \code{EFAULT}. It is invisible for a one-element tensor (\code{len}$^2 =$ \code{len}) and
fatal for every real activation. The fix is one token --- delete the \code{x.len()\ *} --- but the point
is that the type system was content the entire time: both operands are \code{usize}, the product is a
\code{usize}, and the meaning is nonsense. Only \emph{running} it with \code{len}$>$1 reveals it.
\paragraph{Bug 2 --- the ring never compiled for AMD (a hole hidden by a feature flag).}
The device$\to$host \code{match} in Listing~\ref{lst:stage} had arms for \code{Cpu}, \code{Cuda}, and
\code{Metal} but \emph{not} \code{Rocm}. Because \code{Storage::Rocm} only exists when the \code{rocm}
feature is enabled, and no one had ever built \code{ring} $+$ \code{rocm} together, the non-exhaustive
\code{match} was never forced into existence. The moment you try to build the one configuration a
heterogeneous ROCm$+$CUDA cluster needs, it fails to compile. The fix adds the two
\code{\#[cfg(feature = "rocm")]} arms (one in \code{sum\_all\_reduce}, one in \code{all\_gather}) that
stage a ROCm tensor to host exactly like the others. This is the subtler lesson: a feature-gated code
path is not merely untested, it may be \emph{unbuilt}, and a green CI that never enables that flag
combination proves nothing about it.
\paragraph{Bug 3 --- a handshake that assumed both nodes were the same speed.}
The ring's neighbour-connect loop gave up after 10 seconds. On a homogeneous cluster every rank boots in
lockstep and 10s is generous. On a \emph{heterogeneous} cluster the nodes do not load at the same rate ---
a Blackwell dev kit and an APU parse weights, warm kernels, and reach the barrier at visibly different
times --- so the faster node would time out waiting for the slower one and abort the whole ring. Raising
the timeout to 120s (and matching the panic message) lets asynchronous, uneven startup settle. The
assumption ``all ranks are interchangeable, so they arrive together'' is exactly the kind of invariant
that is silently true on one vendor and silently false across three.
None of these three is deep. That is the message. A distributed path can be well-typed, code-reviewed,
and shipped, and still be dead on arrival in three independent ways, because ``it compiles'' certifies
none of the things that actually matter at run time --- the arithmetic of a byte count, the existence of a
match arm under a flag nobody set, or a timing assumption baked into a constant. And a fourth bug, subtler
than all three and invisible until the generated tokens themselves came out wrong, waited past these; it
is the root cause of cross-vendor incoherence, and its fix is what turns prefill into coherent decode
(\S\ref{sec:rootcause}).
\section{Result: coherent decode across the AMD/NVIDIA boundary}
\label{sec:result}
With the four heterogeneity bugs fixed --- three mechanical (\S\ref{sec:compiles}), one numeric
(\S\ref{sec:rootcause}) --- the cross-vendor ring generates coherent output. On a two-node ring ---
\emph{evo} (AMD ROCm gfx1151) as rank~0, \emph{spark} (NVIDIA CUDA sm\_121) as rank~1 --- connected by a
2.5GbE direct cable, the sharded engine:
\begin{enumerate}[leftmargin=1.5em,itemsep=1pt]
\item \textbf{builds} with \code{ring} $+$ \code{rocm} on the AMD node and \code{ring} $+$ \code{cuda} on
the NVIDIA node (Bug~2 was the blocker);
\item \textbf{connects} the ring --- each rank binds its listen port, dials its right neighbour, and
accepts its left, the handshake surviving the uneven boot (Bug~3);
\item \textbf{shards} a Qwen3-0.6B at \code{world\_size}$=$2, each node holding half of every parallelized
weight matrix via the \code{ColumnParallelLayer}/\code{RowParallelLayer} split; and
\item \textbf{decodes coherently} --- running the host-staged \code{f32} \code{all\_reduce} across the
vendor boundary at every layer --- emitting a chain-of-thought and a correct final answer in lockstep, at
\textbf{50.4~tok/s decode} (TTFT 0.09s, 213~tok/s prefill).
\end{enumerate}
The two ranks run \emph{different} 16-bit compute dtypes --- \emph{evo} in \code{f16}, \emph{spark} in
\code{bf16} (\S\ref{sec:rootcause}) --- and still agree, because the reduction crosses the wire in
canonical \code{f32}. To our knowledge this is the first reported cross-vendor (AMD$+$NVIDIA)
tensor-parallel LLM inference over commodity ethernet in a single engine: two different-vendor
accelerators jointly executing one tensor-parallel forward \emph{and} autoregressive decode, over a
commodity cable, with no shared-vendor collective (NCCL/RCCL) between them --- the \code{all\_reduce} that
both libraries refuse across the boundary runs to completion on plain TCP.
\paragraph{Controls isolate the wire, not the model.} A \emph{same}-vendor control --- both ranks forced
to \code{f16}, so the raw-bytes shortcut and the canonical-\code{f32} wire coincide --- produces the same
tokens as the cross-vendor run, which pins the dtype wire (\S\ref{sec:rootcause}), not the ring
mechanism, as the only thing heterogeneity changed. A \emph{single}-node control --- the same Qwen3-0.6B
on \emph{spark} alone --- generates the same answer at 168~tok/s decode and 464~tok/s prefill. So the
cross-vendor 50.4~tok/s is $\approx$3.3$\times$ slower than single-node decode, and the entire gap is
synchronization: a host-staged \code{all\_reduce} every layer, over the wire, is the only added work. The
model is coherent and the kernels are at parity; what the ring costs is the wire.
\section{Root cause and fix: the heterogeneous-dtype-on-the-wire hazard}
\label{sec:rootcause}
The fourth bug was invisible until the tokens themselves came out wrong, and it is the one worth dwelling
on, because it is the trap waiting in every host-staged cross-vendor collective. We state the cause, the
fix, and the residuals the fix leaves.
\subsection{The cause: two ranks legitimately pick different 16-bit formats}
\label{sec:dtype}
The engine picks its compute dtype \emph{per node} from the \emph{local} device. The selector
(\code{determine\_auto\_dtype\_all}) returns \code{f16} when it sees a ROCm gfx1151 device --- RDNA3.5 has
fast \code{f16} matrix cores (WMMA) but no fast \code{bf16} matmul --- and \code{bf16} for the CUDA
Blackwell path. So on a mixed ring the two ranks legitimately and independently choose \emph{different}
16-bit formats: \emph{evo} runs the model in \code{f16}, \emph{spark} in \code{bf16}. The original
host-staged ring shipped \emph{raw device-dtype bytes} and the receiver reinterpreted them as \emph{its
own} local dtype. Since \code{f16} and \code{bf16} share a 16-bit container but split it differently ---
5-bit exponent / 10-bit mantissa versus 8-bit exponent / 7-bit mantissa --- every value crossing the
boundary was silently re-read with the wrong exponent/mantissa layout. The bit pattern was intact; its
\emph{interpretation} was wrong, which is worse, because nothing errored: the \code{all\_reduce} summed
correctly-typed nonsense into correctly-typed nonsense, corrupting activations from the first layer.
Notably, a \emph{same}-vendor ring never exposed this --- both ranks pick the same dtype and the raw-bytes
shortcut accidentally works --- which is precisely why the hazard survived months of same-vendor NCCL and
ring testing until the first cross-vendor run, and why the same-vendor control (\S\ref{sec:result}) is the
clean isolator.
\subsection{The fix: a canonical \code{f32} wire and an \code{N}-node accumulating reduce}
\label{sec:fix}
The remedy is structural, not a patch: the wire needs a \emph{canonical} dtype both ranks agree on
independent of their local compute dtype. \code{f32} is the natural choice --- a lossless superset of both
\code{f16} and \code{bf16}, and higher-precision accumulation for the reduction besides. The shipped
reduce (Listing~\ref{lst:stage}) stages each device tensor to host, converts to \code{f32}
(\code{cpu\_to\_f32}), exchanges and sums in \code{f32}, and converts the result back to the receiver's
native device dtype. The 16-bit formats now meet only \emph{inside} each node; the wire is always
\code{f32}, so both ranks reduce the same values bit-for-bit regardless of vendor, and the reduction is
carried out in the CPU-\code{f32} path with a canonical tie-break so a ROCm box and a CUDA box do not
diverge on rounding order.
The same pass closed a latent correctness bound. The original \code{sum\_all\_reduce} performed a
\emph{single} write-right / read-left exchange and summed the neighbour's shard --- a complete all-reduce
for two ranks, but for \code{N}$>$2 rank $i$ only ever incorporates rank $i{-}1$, so ranks $i{-}2,
i{-}3,\dots$ never reach it. The fix replaces the single exchange with the textbook \code{world\_size}$-$1
accumulating ring (each rank forwards and sums its running shard for \code{N}$-$1 steps), correct for any
\code{N}$\geq$2, and removes a spurious power-of-two world-size restriction inherited from the NCCL path
(\code{is\_power\_of\_two}) that had made four the smallest cluster past two nodes the ring would even
admit. The all-reduce is now algorithmically correct for any \code{N}$\geq$2; the \emph{measured}
cross-vendor demonstration in this paper is the two-node \emph{evo}$+$\emph{spark} ring, and multi-node
(\code{N}$>$2, e.g.\ adding \emph{dbc}/Metal) cross-vendor runs are the natural next validation, not a
claimed measurement here.
\subsection{Residuals: three problems the fix leaves open}
\label{sec:residual}
Coherent cross-vendor decode holds, but three residuals bound the result. We state each crisply and
develop all three, alongside five further problems, as a research agenda in the companion~\cite{openproblems}.
\paragraph{Sampling divergence (a determinism boundary).} The \emph{reduced activations} now agree
bit-for-bit, but each rank still computes its final logits on its own device in its own 16-bit dtype and
samples \emph{locally}. In the demonstrated runs the argmax stays in lockstep; a near-argmax-tie could in
principle resolve differently under \code{f16} versus \code{bf16} rounding and desync the per-rank KV
caches. The robust cure --- the head rank samples and broadcasts the token id, so every rank advances on
the same token --- removes the divergence by construction; it is a determinism-boundary problem in
replicated-state distributed inference, not a numeric one.
\paragraph{Graphless lockstep (an execution-symmetry assumption).} The two ranks run \emph{two}
generation loops kept in phase by the per-token \code{all\_reduce} barrier. To stay in phase the CUDA rank
must run \emph{graphless}, matching the graphless ROCm rank and forgoing the $\approx$1.5$\times$ decode
speedup CUDA-graph capture would give~\cite{backend}. Decode is coherent this way, but per-token
cross-node lockstep between nodes that legitimately want different execution strategies is fragile --- an
argument for pipeline parallelism (\S\ref{sec:pipeline}), where each node's execution is a purely local
choice, rather than a bug to patch.
\paragraph{Format coupling (a sharding limitation).} The TP collectives are wired through the
non-quantized (safetensors) model path --- \code{ColumnParallelLayer} and \code{RowParallelLayer} live in
the normal model definitions --- and \emph{not} through the GGUF quantized path. So even with the
numerics correct, TP cannot yet shard the very block-quantized formats that make edge inference fit in
commodity memory. Quant-aware sharding (splitting on superblock boundaries without a dequant--requant
loss) is an open problem~\cite{openproblems}; pipeline parallelism sidesteps it (\S\ref{sec:pipeline})
because activations, not weights, cross the seam.
\section{The case for pipeline parallelism}
\label{sec:pipeline}
The dtype hazard and the \code{N}-node reduce are fixed, so the tensor-parallel ring on \code{main} is now
\emph{correct}. But correct is not the same as well-matched, and the residuals of \S\ref{sec:residual}
together with the measured synchronization tax point the same way. Tensor parallelism was built for a
homogeneous, tightly-coupled, high-bandwidth, single-vendor machine, and every place it strains on our
cluster is a place it assumes that machine: a per-layer all-reduce assumes cheap bandwidth
(\S\ref{sec:design}) --- the measured $\approx$3.3$\times$ decode tax is exactly this bill; per-token
lockstep assumes symmetric execution (\S\ref{sec:residual}); the parallel layers assume dequantized
weights (\S\ref{sec:residual}). Fixing the numerics made the primitive correct; it did not make it the
right primitive for a heterogeneous fleet.
\textbf{Pipeline parallelism} inverts the mismatch. Partition the model into contiguous \emph{layer
ranges}, one range per node; a token's hidden state flows through node~0's layers, crosses the wire once
to node~1, flows through its layers, and so on. The consequences line up against the strains almost
one-to-one:
\begin{itemize}[leftmargin=1.4em,itemsep=2pt]
\item \textbf{Activations-only on the wire.} What crosses a node boundary is a single hidden-state tensor,
once per boundary, forward --- not an \code{all\_reduce} of every layer's output twice per block. On a
$\approx$280~MB/s link that is the difference between a per-token tax of \code{O(1)} small transfers and
\code{O(layers)} full-width collectives; it is the same $\approx$3.3$\times$ tax converted from
per-layer to per-node.
\item \textbf{One canonical cast, not a per-op reinterpretation.} The activation is a single tensor at
each boundary, so agreeing on one wire dtype (\S\ref{sec:dtype}) is one cast at one seam per node, not a
property every collective must carry. The heterogeneous-dtype hazard shrinks from pervasive to pointwise.
\item \textbf{Format-agnostic.} Activations are just floats; the boundary does not know or care whether the
node that produced them ran a safetensors or a GGUF model, on which vendor, with which kernel. Pipeline
parallelism sidesteps the format coupling of \S\ref{sec:residual} for free, so a mixed cluster can run
quantized shards.
\item \textbf{One generation loop, stateless workers.} The head node runs the single sampling/generation
loop; the others are stateless layer-executors that receive an activation, run their layers (holding only
their own KV cache), and forward. There is no second generation loop to keep in phase and no cross-node
per-token lockstep, so the CUDA-graph asymmetry (\S\ref{sec:residual}) becomes a purely \emph{local}
choice --- each node may capture graphs for its own layers or not, and nobody desynchronizes --- and the
sampling divergence collapses to the head's single sampler.
\end{itemize}
Pipeline parallelism is not a free lunch --- for a single decode stream it adds pipeline-bubble latency
proportional to the number of stages, and throughput needs micro-batching to fill the
pipe~\cite{gpipe}. Its wide-area latency budget --- one network hop per stage on the critical path of
every token --- is the subject of the companion latency analysis~\cite{latency}, which shows the budget
is spendable precisely because pipelining overlaps transport under compute and speculative decoding
amortizes the round-trip. But its \emph{coupling} to the cluster is minimal and its assumptions are the
ones a heterogeneous fleet can actually honour. The right primitive for one-model-across-many-vendors is
the one that puts the least on the wire and demands the least agreement between ranks, and on both axes
that is pipeline, not tensor, parallelism. The cross-vendor decode result (\S\ref{sec:result}) proves the
\emph{channel} works across the boundary; the argument here is about which \emph{traffic pattern} to run
over it, and it is under construction in the same codebase.
\section{Reproducibility}
\label{sec:repro}
\paragraph{Hardware and link.} \emph{evo}: AMD Ryzen AI Max+ 395 ``Strix Halo,'' Radeon 8060S iGPU
(gfx1151, RDNA3.5), ROCm. \emph{spark}: NVIDIA GB10 Grace-Blackwell (sm\_121), CUDA. \emph{dbc}: Apple
M4 Max, Metal. \emph{evo}$\leftrightarrow$\emph{spark} is a direct 2.5GbE cable (static
\code{192.168.77.0/24}), whose measured sustained-TCP goodput is $\approx$280~MB/s ($\approx$2.24~Gbit/s);
its wide-area latency budget is analysed in the companion~\cite{latency}. The three-node substrate is a
wired ethernet mesh with \emph{spark} kernel-IP-forwarding the closing \emph{evo}$\leftrightarrow$\emph{dbc}
edge, so no switch or router is required.
\paragraph{Build.} Each node builds the \emph{same} engine source with its own backend feature plus
\code{ring}: \code{--features "cuda ring"} on \emph{spark}, \code{--features "rocm ring"} on \emph{evo}.
The three mechanical fixes are at commit \code{63cc4c7ac}; the canonical-\code{f32} wire and the
\code{world\_size}$-$1 reduce are on \code{main} (\code{hanzo-quant/src/distributed/},
\code{hanzo-engine/src/ops.rs}).
\paragraph{Ring configuration.} The ring backend is activated by pointing \code{RING\_CONFIG} at a JSON
file per node; rank $r$ binds \code{port} and dials its right neighbour at
\code{right\_ip:right\_port}. A two-node \emph{evo}(rank~0)$+$\emph{spark}(rank~1) ring:
\begin{lstlisting}[language=Rust,caption={\code{RING\_CONFIG} for a 2-node cross-vendor ring. Rank~0
(evo) listens on 9000 and dials spark; rank~1 (spark) listens on 9001 and dials back to evo, closing the
ring.},label={lst:ring}]
// evo (rank 0, 192.168.77.1): RING_CONFIG=/etc/hanzo/ring0.json
{ "master_ip": null, "master_port": 9002,
"port": 9000, "right_ip": "192.168.77.2", "right_port": 9001,
"rank": 0, "world_size": 2 }
// spark (rank 1, 192.168.77.2): RING_CONFIG=/etc/hanzo/ring1.json
{ "master_ip": "192.168.77.1", "master_port": 9002,
"port": 9001, "right_ip": "192.168.77.1", "right_port": 9000,
"rank": 1, "world_size": 2 }
\end{lstlisting}
\noindent A \code{hanzo cluster init} / \code{hanzo cluster join <token>} pair wraps this JSON in a
scan-to-join token (base58 of the topology plus a terminal QR code) so the operator never hand-writes the
ports; the token bakes in the peer list and each worker self-assigns its rank. Both ranks launch the
ordinary \code{hanzo serve} with \code{RING\_CONFIG} set and the same model.
\paragraph{What is measured vs.\ open.} Measured on the real 2.5GbE link: cross-vendor build, connect,
\code{world\_size}$=$2 shard, and coherent multi-token \emph{decode} across the AMD/NVIDIA boundary
(50.4~tok/s decode, TTFT 0.09s, 213~tok/s prefill on Qwen3-0.6B); the same-vendor \code{f16}$+$\code{f16}
control matching token-for-token; and the single-node \emph{spark} control (168~tok/s decode, 464~tok/s
prefill). Open (\S\ref{sec:residual}, \cite{openproblems}): head-broadcast sampling to close the
determinism boundary, \code{N}$>$2 cross-vendor runs, GGUF-path TP, and the migration to pipeline
parallelism. No projected or synthetic number appears in this paper.
\section{Related work and contribution}
\label{sec:related}
Tensor parallelism is Megatron-LM~\cite{megatron}; pipeline parallelism is GPipe and its
successors~\cite{gpipe}; the on-device collectives are NCCL~\cite{nccl} and RCCL~\cite{rccl}, each
single-vendor by design. Distributed inference over commodity/heterogeneous networks has been explored
most prominently by Petals~\cite{petals}, which runs pipeline-parallel shards across volunteer nodes over
the public internet --- close in spirit to \S\ref{sec:pipeline}, and evidence that the activations-only
pattern is the one that survives a slow, loose network. What we add is narrow and, to our knowledge, not
previously shown: a \emph{single} inference engine, one source lowered to three vendors' kernels, running
a \emph{tensor-parallel} forward \emph{and coherent decode} across an \emph{AMD$+$NVIDIA} boundary on
\emph{commodity ethernet} with \emph{no} shared-vendor collective --- and an unusually explicit account of
the failure modes that a host-staged cross-vendor collective exposes (the \code{len}$^2$ overflow, the
unbuilt ROCm arm, the startup-race timeout, and the heterogeneous-dtype wire). The design contribution is
the observation, made concrete by those failures, that heterogeneity is a stress test for homogeneity
assumptions, and that pipeline parallelism minimizes exactly the assumptions that break. This paper is the
distributed extension of the ``one source, every backend'' thesis~\cite{backend,umbrella}: having made one
engine run on every backend, we make those backends run in one cluster. Two companions extend the arc: the
latency-physics analysis~\cite{latency} carries the wide-area budget for \S\ref{sec:pipeline}, and the
three-plane compute-fabric blueprint~\cite{consensus} turns the trusted-LAN ring into a permissionless,
consensus-managed, PQ-authenticated network. The open questions this result raises are collected as a
research agenda in the open-problems companion~\cite{openproblems}.
\section{Conclusion}
\label{sec:conclusion}
No collective library crosses a vendor boundary, so a mixed AMD/NVIDIA/Apple fleet cannot run one model
together through the standard path. Host-staging the collective through CPU memory and TCP makes the
channel vendor-agnostic, and on that channel one engine --- built from a single source for three vendors'
kernels --- shards a model \code{world\_size}$=$2 across an AMD/NVIDIA boundary over a 2.5GbE cable and
\emph{decodes coherently} at 50.4~tok/s, which we believe is a first. The road to that result ran through
three bugs in a path that compiled for months but had never run --- a reminder that ``it builds''
certifies almost nothing about a distributed system --- and one deeper hazard, a heterogeneous-dtype wire
that a same-vendor cluster can never expose, cured by a canonical \code{f32} payload and an accumulating
\code{world\_size}$-$1 all-reduce correct for any \code{N}$\geq$2. The measured $\approx$3.3$\times$
synchronization tax, and the residuals that remain --- local sampling that can diverge at a near-tie,
graphless lockstep between two generation loops, and TP's coupling to the dequantized weight path --- all
point one way: toward pipeline parallelism, whose activations-only, one-generation-loop, format-agnostic
shape asks of a heterogeneous cluster only what such a cluster can actually give. The channel is open
across the boundary and coherent over it; the companions drive the right traffic~\cite{latency}, harden
it into a network~\cite{consensus}, and enumerate what is left to solve~\cite{openproblems}.
\begin{thebibliography}{9}
\small
\bibitem{backend} Hanzo AI. \emph{Edge LLM Inference Across Commodity Accelerators: Measured AMD, NVIDIA,
and Apple Performance, and hanzo-engine at llama.cpp Decode Parity on Every Backend.} Paper 05.
\bibitem{umbrella} Hanzo AI. \emph{One Source, Every Backend: The hanzo-kernel DSL --- Thesis and Guided
Tour.} Paper 07 (umbrella).
\bibitem{fuse} Hanzo AI. \emph{Fusion Is Composition: Kernel Fusion as the Functor Law.} Paper 07.1.
\bibitem{oracle} Hanzo AI. \emph{One Oracle, Every Backend: The CPU Bit-Exactness Law.} Paper 07.4.
\bibitem{latency} Hanzo AI --- ML Systems. \emph{The Latency Physics of Geo-Distributed Autoregressive
Decode.} Distributed-inference companion (analytical).
\bibitem{consensus} Hanzo AI --- ML Systems. \emph{The Three-Plane Compute Fabric: Consensus-Managed,
ZAP-Transported Decentralized AI Inference.} Distributed-inference companion (architecture).
\bibitem{openproblems} Hanzo AI --- ML Systems. \emph{Open Problems in Heterogeneous Distributed
Inference.} Distributed-inference companion (research agenda).
\bibitem{megatron} M.\ Shoeybi, M.\ Patwary, R.\ Puri, P.\ LeGresley, J.\ Casper, B.\ Catanzaro.
\emph{Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism.} arXiv:1909.08053, 2019.
\bibitem{gpipe} Y.\ Huang et al. \emph{GPipe: Efficient Training of Giant Neural Networks using Pipeline
Parallelism.} NeurIPS, 2019.
\bibitem{petals} A.\ Borzunov et al. \emph{Petals: Collaborative Inference and Fine-tuning of Large
Models.} arXiv:2209.01188, 2022.
\bibitem{nccl} NVIDIA. \emph{NCCL: NVIDIA Collective Communications Library.}
\bibitem{rccl} AMD. \emph{RCCL: ROCm Communication Collectives Library.}
\end{thebibliography}
\end{document}