Files
papers/hanzo-kernel-optimization.tex
T
zeekay 1ccfab4d8f papers: kernel DSL migration + kernel-optimization catalog (incl. the flash-attention repetition bifurcation boundary) + cross-backend correctness finding
- hanzo-kernel-dsl: one-source #[kernel] DSL, 2 kernels drained to production, perf-gated migration
- hanzo-kernel-optimization: cross-backend kept-levers + dead-ends catalog + the flash-attn P*V
  bifurcation boundary (fusion & f32-P*V mutually exclusive on tensor cores -> eager is optimal)
- cross-backend-inference: the model-level correctness lesson (one numerically-vetted impl per op)
2026-07-03 19:04:20 -07:00

609 lines
42 KiB
TeX

\documentclass[11pt]{article}
\usepackage[margin=1in]{geometry}
\usepackage{booktabs}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{graphicx}
\usepackage{xcolor}
\usepackage{hyperref}
\hypersetup{colorlinks=true, linkcolor=blue, urlcolor=blue, citecolor=blue}
\usepackage{enumitem}
\usepackage{listings}
\usepackage{array}
\definecolor{kw}{rgb}{0.13,0.29,0.53}
\definecolor{cmt}{rgb}{0.40,0.40,0.40}
\definecolor{str}{rgb}{0.16,0.50,0.16}
\lstset{
basicstyle=\ttfamily\footnotesize,
keywordstyle=\color{kw}\bfseries,
commentstyle=\color{cmt}\itshape,
stringstyle=\color{str},
showstringspaces=false,
breaklines=true,
columns=fullflexible,
frame=single,
framesep=4pt,
}
\lstdefinelanguage{Rust}{
morekeywords={fn,let,mut,pub,struct,impl,trait,enum,match,if,else,for,while,loop,return,use,mod,crate,self,super,async,await,where,type,const,static,unsafe,ref,move,dyn,as,true,false},
morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]", sensitive=true,
}
\lstdefinelanguage{CUDAC}{
morekeywords={int,float,half,char,void,for,if,else,return,const,struct,template,typename,constexpr,__global__,__device__,__constant__,__shared__,if,unsigned,auto,using},
morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]", sensitive=true,
}
\newcommand{\x}{\ensuremath{\times}}
\newcommand{\dpa}{\texttt{dp4a}}
\title{\textbf{When Does a Kernel Win Surface? A Cross-Backend Catalog of\\
Proven Levers, Measured Dead-Ends, and a Bottleneck Taxonomy for\\
Quantized LLM Decode and Prefill}}
\author{Hanzo AI --- hanzo-engine / hanzo-ml technical white paper, 06}
\date{Consolidation pass: kernel work measured 2026-06}
\begin{document}
\maketitle
\begin{abstract}
Companion paper~05 established the \emph{model-level} result: hanzo-engine reaches
\texttt{llama.cpp} decode parity (0.98--1.04\x) and a bounded prefill gap (0.76--0.91\x) on
AMD, NVIDIA, and Apple from one multi-backend build. This paper documents the
\emph{kernel-level} engineering underneath that result --- the optimizations we wrote,
measured, and \emph{kept} in hanzo-ml/hanzo-engine, and, just as importantly, the ones we
wrote, measured, and \emph{reverted}. We organize roughly thirty controlled experiments
(ROCm/gfx1151, CUDA/GB10, Vulkan/gfx1151, Metal/M4~Max) into a four-regime
\textbf{bottleneck taxonomy} --- bandwidth-roofline decode, compute-bound prefill,
host/launch-bound decode, and GPU-busy-bound decode --- and show that the taxonomy
\emph{predicts} whether a given kernel win surfaces end-to-end. The strongest evidence for the
taxonomy is that the \emph{same} optimization is a real lever in one (platform, regime) and a
measured non-lever in another: int8 \dpa{} lifts ROCm i-quant decode 2.27\x{} (compute-bound) but
is end-to-end flat on GB10 (host-bound); a fused router gives ROCm $+13\%$ but CUDA $0\%$ (no
expensive sort to eliminate); a fused-coopmat tensor-core prefill is correct but $3.8\x$ slower
than int8-ALU \dpa{} on gfx1151 (decode-bound). The governing rule, stated once and tested
repeatedly: \emph{a fusion or kernel change helps end-to-end only if it eliminates work in the
binding resource; cutting launches or FLOPs in a non-binding resource is flat at best and
negative if it injects redundant work.} Every win we report is gated bit-exact against a CPU
reference; every A/B is device-map-controlled or kernel-isolated. We close with the one frontier
this program has fully characterized but not yet closed --- Vulkan \emph{decode}, which we show
by profiling is op-count-serialization-bound (1336 dispatches / 903 global barriers / 143~ms
GPU fence-wait per token, with CPU recording effectively free) --- and the fusion roadmap that
the ROCm path already validates.
\end{abstract}
\tableofcontents
\section{Introduction: one engine, every backend, and a measurement question}
hanzo-engine runs CUDA, ROCm, Metal, Vulkan, and CPU behind one model loader; hanzo-ml is the
tensor/kernel layer beneath it. The portability thesis is only credible if the \emph{same} code
is competitive with the hand-tuned, single-vendor baseline (\texttt{llama.cpp}) on each backend.
Paper~05 showed it is, at the model level. The engineering question this paper answers is the one
that actually consumes the work: \textbf{given a kernel that is provably faster in isolation, will
it make the end-to-end token rate go up?}
The naive assumption --- faster kernel $\Rightarrow$ faster model --- is wrong often enough to be
the central hazard of inference optimization. A 9\x{} matmul can be flat end-to-end; a $5\x$ i-quant
kernel can fully surface on one GPU and vanish on another; cutting half the launches can lose
$22\%$. The difference is always \emph{which resource is binding}. We therefore present the work
not as a list of tricks but as a taxonomy that tells you, before you write the kernel, whether the
win can possibly surface --- and we validate it on the cases where it predicted ``no'' and we
measured ``no.''
\paragraph{What this paper contributes.}
\begin{enumerate}[noitemsep]
\item A \textbf{bottleneck taxonomy} (\S\ref{sec:taxonomy}) for single-stream quantized
inference, with a falsifiable surfacing rule.
\item A \textbf{catalog of kept levers} (\S\ref{sec:rocm}--\ref{sec:vulkan}) per backend, each
with its bit-exact gate and a device-map-controlled or kernel-isolated A/B.
\item A \textbf{catalog of measured dead-ends} (\S\ref{sec:deadends}) --- the negatives are the
contribution: each one is explained by, and so confirms, the taxonomy.
\item The \textbf{Vulkan-decode frontier} (\S\ref{sec:frontier}): a profile-backed diagnosis and
the fusion roadmap that ROCm has already walked.
\end{enumerate}
\section{Architecture: three orthogonal axes and a bit-exact invariant}
\label{sec:arch}
The kernel surface is a product of three axes that we keep deliberately orthogonal, because
collapsing them is what produces an unmaintainable $N\times M\times K$ explosion of hand-written
kernels:
\begin{itemize}[noitemsep]
\item \textbf{Quant format} (the \emph{value}): the GGUF block layout --- how 1--8 bits/weight
plus per-block scales reconstruct a float. 22 families are wired
(Q8\_0/Q4\_0/Q4\_K/Q5\_K/Q6\_K/Q2\_K/Q3\_K, the IQ1/IQ2/IQ3 codebook quants, the TQ
ternary quants, etc.).
\item \textbf{Contraction} (the \emph{operation}): mat-vec (decode, $M{=}1$), GEMM (prefill,
$M{>}1$), and their indexed-MoE twins (gather/scatter over routed experts).
\item \textbf{Backend} (the \emph{functor}): how a contraction lowers to ROCm/CUDA/Metal/Vulkan
instructions and memory.
\end{itemize}
On ROCm this is already decomplected to one generic contraction with the type injected as a
trait: \texttt{qmatvec\_core<WTYPE,XT>} (decode) and \texttt{qmmq\_core<WTYPE,MOE,NWAVE\_M>}
(prefill), each instantiated per format, with the MoE variant differing only by an
\texttt{if constexpr (MOE)} gather/scatter so the dense path codegens byte-identically. The same
22 formats decode through one core; the int8-\dpa{} fast path is a second core selected by a single
trait predicate \texttt{RocmQuantType::dp4a\_active()}. The forward graph never branches on
``is this Q4\_K'' --- dispatch is by kernel-name string, so adding a format is additive.
\paragraph{The invariant that makes aggressive kernels safe.} Every kernel --- every \dpa{}
reinterpretation, every fusion, every tiling --- is gated against a CPU \texttt{to\_float} oracle
to \texttt{nbad=0} (bit-exact, modulo documented f32 reorder / f16-rounding tolerances). This is
not a test suite bolted on after; it is the precondition for shipping a kernel at all. The reason
the catalog below can be this aggressive (reinterpreting a 6-bit asymmetric quant as signed int8,
folding a residual-add into an RMS reduction, quantizing activations to int8 mid-GEMM) is that
each transform is proven equivalent before it is allowed to matter for performance. The oracles
are named inline (\texttt{qmatvec\_unified\_numeric}, \texttt{moe\_qmmq\_numeric},
\texttt{add\_rmsnorm\_numeric}, \texttt{qk\_rms\_norm\_rope\_numeric}, \texttt{moe\_route\_numeric},
\dots) and are the reusable gate for every follow-up.
\section{A bottleneck taxonomy for single-stream quantized inference}
\label{sec:taxonomy}
Single-stream decode and prefill bind on different resources, and within decode the binding
resource differs by platform and by whether HIP/CUDA graphs are active. We name four regimes.
\begin{table}[h]
\centering
\caption{The four regimes. ``Surfacing condition'' is what a kernel change must do to move the
end-to-end token rate.}
\small
\begin{tabular}{>{\raggedright\arraybackslash}p{2.8cm} >{\raggedright\arraybackslash}p{4.3cm} >{\raggedright\arraybackslash}p{6.0cm}}
\toprule
Regime & Binding resource & Surfacing condition \\
\midrule
\textbf{Bandwidth-roofline decode} & weight bytes / memory BW & Only fewer weight bytes moved (a smaller quant). Kernel arithmetic changes are flat once at roofline. \\
\textbf{Compute-bound prefill} & matrix/tensor-core FLOPs & Higher effective FLOP/s: better tiling, tensor cores, higher M-tile fill. \\
\textbf{Host/launch-bound decode} (eager, or graph-less) & per-token CPU dispatch / kernel launch & Fewer launches, or graph capture that hides launch latency. GPU-time cuts are flat. \\
\textbf{GPU-busy-bound decode} (graphs on) & sum of small-kernel GPU time & Eliminating GPU work (a true fusion). Repackaging the same work is flat. \\
\bottomrule
\end{tabular}
\end{table}
\paragraph{The surfacing rule (falsifiable).}
\begin{quote}\itshape
A kernel change improves the end-to-end token rate only if it reduces consumption of the
\emph{binding} resource. Reducing a non-binding resource is flat; reducing a non-binding resource
\emph{while adding} work to the binding one is negative.
\end{quote}
Three corollaries drive the rest of the paper. (i) On a bandwidth-roofline decode, the matvec is
already at the memory ceiling, so faster arithmetic is flat --- the only decode lever is reducing
\emph{kernel count} (in the launch- or busy-bound sub-regimes), not making any one kernel faster.
(ii) ``Fewer launches'' helps only in the launch/busy-bound regimes, and only if the launch you
removed was not already free (under graphs it is). (iii) The same kernel can flip regime across
platforms: a GPU with more launch overhead relative to memory bandwidth is launch-bound where
another is bandwidth-bound, so a launch-cut surfaces on one and not the other.
\section{Kept levers: ROCm (gfx1151, Strix Halo)}
\label{sec:rocm}
ROCm is the most mature backend and the one where the taxonomy was first forced on us. All decode
A/Bs use the device-map pin \texttt{-n 0:48} (the auto-mapper otherwise mis-reads unified memory
and offloads $\sim 1/3$ of layers to CPU, which silently corrupts the comparison --- a measurement
hazard worth more than any single lever).
\subsection{Unified \dpa{} decode cores (the K-quant and i-quant families)}
The decode matvec for K-quants reinterprets the block as int8 and uses the RDNA3.5 dot-product
builtin (\texttt{\_\_builtin\_amdgcn\_sudot4}; the plain \texttt{sdot4} does not compile on gfx1151,
which lacks the dot1-insts feature). The win is per-format and depends entirely on the regime:
\begin{itemize}[noitemsep]
\item \textbf{Q6\_K \dpa{}}: a $4.9\x$ \emph{kernel} win (260$\to$53\,$\mu$s per call) by reading
the 16-element scale group as four packed int reads and reconstructing 4 weights/int, one
branch per group not per element. The q6 centering ($q-32$) is handled as a bias term
$\sum(q-32)u = \dpa(q,u) - 32\,\dpa(\mathbf{1},u)$ because gfx1151 has no per-byte
\texttt{\_\_vsubss4}. Total GPU kernel time $-48\%$. \textbf{Model-level: $+17\%$}
($28.2\to33.0$ T/s) --- it surfaces because eager MoE decode here is partly compute-bound
on the slow Q6\_K (output.weight + attn\_v + ffn\_down experts $=37.7\%$ of GPU work).
\item \textbf{IQ codebook \dpa{} (IQ2\_XXS/XS/S, IQ3\_XXS/S, IQ1\_S/M)}: $2.27\x$ \emph{end-to-end}
($30.4\to 67.7$ T/s on Qwen3-30B-A3B-IQ3\_XXS), \emph{beating} \texttt{llama.cpp}-HIP
(67.6). The grid coords land in int8 and \dpa{} against the once-quantized activation; signs
without \texttt{\_\_vsub4} use $\sum(-1)^s g\,u = \dpa(g,u) - 2\,\dpa(g\,\&\,\text{mask}(s),u)$.
Unlike Q6\_K, this win fully surfaces \emph{and} is graph-flat --- i-quant decode was
\emph{compute-bound} (per-element f32 dequant + MAC), so the kernel \emph{was} the wall.
This is the textbook ``compute-bound decode'' case of the taxonomy.
\end{itemize}
\noindent The contrast between Q6\_K (kernel $4.9\x$, model $+17\%$) and IQ3 (kernel $2.27\x$, model
$2.27\x$) is the taxonomy in one comparison: the i-quant decode was compute-bound so the kernel win
\emph{is} the model win; the K-quant decode is mostly bandwidth-bound so only its compute-bound
fraction surfaces.
\subsection{Capture-clean strided ops $\to$ HIP-graph decode}
The single highest-leverage ROCm change was not a matmul. Decode under HIP graphs hides per-launch
host latency, but capture kept failing on the first strided op: each \texttt{Map/Reduce/cast/copy}
uploaded its tiny dims/strides metadata array host$\to$device every op every token, which is an
implicit capture violation (HIP 906). The fix rides the metadata \emph{inline by value} in the
kernel param block (\texttt{\#[repr(C)] DimsStrides \{ v: [usize; 32] \}}, 256~B), so zero
\texttt{clone\_htod} fires on the strided path. This both enabled graph capture \emph{and}, by
removing a blocking H2D from the eager hot path, lifted eager decode from $\sim$11 to $\sim$16--30
T/s on its own. It is the enabling lever beneath every launch-bound decode win.
\subsection{Indexed-MoE decode without a host round-trip}
MoE decode batches all routed slots into one launch over \texttt{grid.y}; the expert id is read
\emph{on device} (the kernel takes \texttt{ids: \&RocmStorage}, not \texttt{\&[u32]}), so no
\texttt{to\_vec1}/\texttt{copy\_from\_host} round-trip fires per token. This eliminated the
per-expert host launch loop for mixed-precision GGUF (Qwen3-30B-A3B has Q6\_K ffn\_down experts
inside a Q4\_K model), $10.22\to12.07$ T/s.
\subsection{The MoE graph bug: a position-refresh root-cause, not a buffer bug}
MoE decode under graphs produced garbage from $\sim$token~12 (``Let. Let. 1 1 1''). The
prior hypothesis (recycled routing buffer) was disproven by an env-gated replay-vs-eager
\texttt{max\_abs\_diff} probe: dense paths diffed $0.0$, MoE diffed $4$--$16$ from token~0, and
bisecting the MoE forward (even forcing the experts to identity) still diverged --- proving the
divergence was in the \emph{shared attention path}. The MoE model baked the decode RoPE position
from the host offset at capture and never refreshed it, so every replayed token rotated at the
frozen warmup position. The dense path already read RoPE off a stable device tensor
(\texttt{metadata.rope\_positions}) refreshed in place each token; the MoE model simply never got
that fix. Threading \texttt{positions: \&Tensor} through \texttt{forward\_attn} made MoE graphs
replay bit-identically ($340$-token coherent, $\times 3$). The lesson is methodological: a
replay-vs-eager numeric probe localizes a graph bug faster than any amount of staring at buffers.
\subsection{Fused indexed-MoE prefill GEMM: the $2.58\x$ prefill lever}
MoE prefill was the \emph{entire} deficit versus \texttt{llama.cpp}-HIP (0.34\x{} while dense was
already 1.12\x). Root cause (\texttt{rocprofv3}): routed prefill went through the per-\emph{slot}
decode matvec, so every routed token re-read its expert's whole weight and ran \emph{no} matrix
cores --- $70.7\%$ of GPU time on MoE matvecs. The fix is an expert-grouped WMMA GEMM
(\texttt{qmmq\_core<WTYPE,true>}, the same 16-warp/128$\times$128 int8-WMMA prefill machine with
\texttt{if constexpr (MOE)} gather/scatter): tokens are counting-sorted by expert host-side
(prefill is never captured, so the ids DtoH is free) into $\le$128-row tiles, and each block stages
\emph{one} expert's weight and amortizes it over all that expert's tokens (llama's
\texttt{mul\_mat\_id}). \textbf{Result: $366\to944.7$ T/s ($2.58\x$), $0.89\x$ of llama};
GPU MoE-matmul time $-78\%$. The A/B (\texttt{HANZO\_MOE\_QMMQ\_FALLBACK=1} $\Rightarrow$ 365.8)
confirms the kernel is the \emph{sole} lever.
\paragraph{Dynamic M-tile (the bandwidth/compute balance point).} At $\sim$64 tokens/expert a
128-row WMMA tile is half-empty. Parametrizing the core on the M-wave count
(\texttt{qmmq\_core<WTYPE,MOE,NWAVE\_M>}, TILE\_M 128/64/32 with thread-count-agnostic staging
loops so the dense path stays byte-identical) and picking TILE\_M by routed fill lifts M-fill
$49.9\to68.2\%$ but throughput only $+2.4\%$ --- because the full-prefill MoE GEMM is
\emph{weight-bandwidth-bound}, not WMMA-compute-bound, \emph{proven} by the A/B: TILE\_M=32 (even
less empty WMMA work) is \emph{slower} because it re-reads each expert weight in two tiles. TILE\_M=64
(one tile per $\sim$64-token expert, minimal weight re-reads) is the balance point. The residual gap
to llama is irreducible expert-weight fetch, not tile fill. This is a clean instance of the
surfacing rule: more compute headroom is flat when bandwidth binds.
\subsection{Fused MoE routing: \texttt{moe\_route} ($+13\%$)}
One kernel (one block/token: shared-mem softmax $\to$ top-$k$ $\to$ normalize) replaces the
softmax$\to$sort$\to$narrow$\to$sum$\to$div chain ($\sim$6 launches/layer $\times 48 \approx 240$
fewer kernels/token). \textbf{$52.9\to59.9$ T/s ($+13\%$), $0.90\x$ llama}; long-context run-loop
$+15\%$. This is the launch/busy-bound decode lever the bandwidth-wall analysis pointed to, and it
surfaces on ROCm precisely because the amdgcn \texttt{gpu-sort} it eliminates is disproportionately
expensive (a fact that, foreshadowing \S\ref{sec:deadends}, makes the \emph{same} fusion flat on
CUDA).
\subsection{Decode op-fusion that eliminates work ($+2.8\%$ each, stacked)}
\texttt{rocprofv3} of the shipped decode showed it is \textbf{GPU-busy-bound} under graphs
($12.17$~ms busy $=94.5\%$ of the $12.87$~ms wall), with non-matvec busy time spread across
\texttt{quantize\_q8\_1} (378\,$\mu$s), \texttt{rmsnorm} (330), \texttt{route} (297), casts (377),
etc. Two fusions that \emph{eliminate} work (not just launches) each surfaced:
\begin{itemize}[noitemsep]
\item \textbf{gate+up shared-quantize} (\texttt{moe\_matvec\_pair}): gate and up consume the same
routed token, so broadcast + quantize it \emph{once} and \dpa{} both banks. $-48$ quantize,
$-48$ ucopy per token.
\item \textbf{add\_rmsnorm}: $s = x + \text{residual}$ then $\text{rmsnorm}(s)\cdot\alpha$ in one
reduce launch, vs.\ a separate \texttt{badd} then \texttt{rmsnorm}.
\item \textbf{fused q/k RMSNorm + positions-RoPE} (\texttt{rope\_norm\_positions}, one block per
head-vector): one kernel replaces two standalone norms + RoPE \emph{and} the q/k
intermediate write+read. Same f32 math, no redundant work. $+2.8\%$ ($77.4\to79.5$ T/s),
the true-elimination twin of \texttt{moe\_route}.
\end{itemize}
Stacked, $1702\to1558$ kernels/token; decode past llama-HIP. Each is bit-exact. The discipline:
fuse only when the fusion \emph{removes} GPU work --- which is exactly what the next section's
biggest dead-end violated.
\section{Kept levers: CUDA (GB10, Grace-Blackwell sm\_121)}
\label{sec:cuda}
\subsection{i-quant completeness: IQ1\_S MMQ prefill, TQ1/TQ2 \dpa{} decode}
Two correctness/coverage gaps closed, both bit-exact (\texttt{cuda\_iquant\_tests} 13/13). IQ1\_S had
native \dpa{} decode but was MMQ-excluded at prefill only because its GPU grid table was a zeroed
stub mis-sized at $[512]$ (the indexer needs 11 bits $=2048$); filling the real
\texttt{iq1s\_grid\_gpu[2048]} verbatim from llama.cpp enabled the whole int8-WMMA prefill machine
that was already ported. TQ1\_0/TQ2\_0 ternary got native \dpa{} decode --- symmetric $\{-1,0,1\}$
packs straight to signed int8 with \emph{no} bias-sum term; notably \texttt{llama.cpp} has no CUDA
ternary path at all, so this is pioneering and the ROCm scalar \texttt{qdec} is the layout oracle.
\subsection{\texttt{unpack\_ksigns}: the real GB10 i-quant kernel wall ($1.71\x$)}
The i-quant decode matvec was $\sim$75\% of GB10 i-quant decode GPU-time. Diffing against
\texttt{llama.cpp}'s \texttt{vec\_dot\_iq2\_xxs\_q8\_1} found a ROCm-port artifact: the sign
codebook was a \emph{divergent} \texttt{\_\_constant\_\_} gather (each lane a different index,
serialized through the constant cache). llama computes it table-free --- the 7-bit index's 8th sign
is the popcount-parity of the other 7:
\begin{lstlisting}[language=CUDAC]
// bit-identical to KSIGNS_IQ2XS_D[v], no constant-cache gather:
unpack_ksigns(v) = (v ^ ((__popc(v & 127) & 1) << 7)) * 0x01010101;
\end{lstlisting}
\textbf{Kernel A/B (4096$^2$, GB10): IQ2\_XXS $84.67\to49.49\,\mu$s $=1.71\x$} ($51\to87$ GB/s);
IQ3\_XXS $1.40\x$; IQ2\_XS $1.25\x$. The control types that have no gather to kill (IQ2\_S/IQ3\_S,
raw-byte signs) stay \emph{flat} --- which \emph{isolates} the gather as the cause and corrects an
earlier ``47 GB/s VALU wall'' conclusion that had measured the baseline and mislabeled it.
\paragraph{The honest model-level result: a GB10 non-lever.} Same-engine A/B on
Qwen3-4B-UD-IQ2\_XXS: \textbf{flat} both graphs-on ($66.1$ vs $66.2$ T/s) and eager ($60.2$ vs
$60.2$). The $1.71\x$ kernel win does not surface because GB10 hanzo i-quant decode is
\textbf{host/launch-overhead-bound, not matvec-bound} --- the GPU finishes the faster matvec and
stalls on per-token host work. This is the same kernel ($\dpa$/codebook decode) that fully surfaced
on ROCm-eager (\S\ref{sec:rocm}); the regime flip across platforms is the taxonomy's sharpest
prediction. We ship it anyway: bit-exact, it drops a 128-entry table and a divergent gather, it
matches llama, and it is a latent win for genuinely matvec-bound regimes. The actual GB10 decode
lever is launch reduction (the dense decode-graph gate, $+6.8\%$).
\section{Kept levers: Vulkan (gfx1151) --- the prefill GEMM arc}
\label{sec:vulkan}
Vulkan is the youngest backend and the one where we walked the full optimization arc on a single
kernel (Q4\_K prefill GEMM) in the open, keeping every rung --- including the negative ones --- as
a documented ladder. This is the most complete single-kernel case study in the program.
\subsection{The deadlock: a missing uniform GEMM, surfaced as a liveness bug}
hanzo-Vulkan \emph{hung} at the first long prefill of any GGUF on gfx1151, while llama.cpp-Vulkan
and hanzo's own ROCm ran fine. \texttt{gdb} of the live hung process put the engine thread in the
kernel at \texttt{DRM\_IOCTL\_AMDGPU\_GEM\_CREATE} $\leftarrow$ \texttt{QTensor::dequantize}
$\leftarrow$ prefill. The prefill \texttt{else} branch dequantized each weight to a fresh
$\sim$100--235~MB f32 buffer; under the deferred single command batch \emph{none} free until the
end-of-forward flush, so a dense prefill re-expands the whole model to f32 ($\sim$32~GB for an 8B)
in fresh allocations --- on a unified-memory carveout this exhausts memory and \texttt{GEM\_CREATE}
blocks on a free that can only happen \emph{after} the still-recording batch flushes: a
self-deadlock. ROCm never hit it because ROCm prefill rides the int8-WMMA GEMM and never
dequantizes --- exactly the asymmetry that pointed at the fix. \texttt{vulkan\_prefill\_gemm\_max\_rows}
now returns \texttt{usize::MAX} for every dtype that \emph{has} a native quantized GEMM, so prefill
always uses the GEMM and never the f32-dequant fallback. The gate is now effectively a ``has a GEMM
kernel?'' predicate; the real cleanup (\S\ref{sec:vulkan-roadmap}) is to give all 22 formats a GEMM
so the fallback --- and the bug --- become impossible by construction.
\subsection{The L1 GEMM ladder: 1D dead-end $\to$ 2D-f32 $2.06\x$ $\to$ \dpa{}-2D $9.35\x$}
We measured each rung kernel-isolated (\texttt{bench\_matmul\_q4k}: upload operands once, loop the
GPU GEMM, one final synchronize --- the host-wrapper bench's per-iter 8~MB upload+readback
otherwise masks the kernel). Numbers are $m{=}512,\,n_{\text{out}}{=}k{=}4096$ (17.2 GFLOP).
\begin{table}[h]
\centering
\caption{Vulkan Q4\_K prefill GEMM ladder, kernel-isolated (gfx1151). Default = the legacy
column-per-invocation kernel.}
\small
\begin{tabular}{l r r l}
\toprule
Kernel & Time (ms) & vs.\ default & Verdict \\
\midrule
Legacy column-per-invocation & 38.5 & 1.0\x & baseline \\
1D weight-only LDS stage & 223.7 & 0.21\x & \textbf{dead-end} (\S\ref{sec:deadends}) \\
2D-tiled f32 (64$\times$64, BK=32) & 19.0 & 2.06\x & kept (universal fallback) \\
\textbf{2D-tiled int8 \dpa{}} & \textbf{3.4} & \textbf{9.35\x} & \textbf{shipped default} \\
Naive coopmat (tensor cores) & 17.4--19 & 0.26\x vs \dpa{} & \textbf{dead-end} (\S\ref{sec:deadends}) \\
\bottomrule
\end{tabular}
\end{table}
The shipped kernel (\texttt{mul\_mm\_q4k\_tiled\_dp4a.comp}) stages a 64$\times$64 output tile:
per K-step (BK=32, one Q4\_K sub-block) it stages the $n$-cols' 32 q4 codes + per-block $d_1/m_1$
and the $m$-rows' int8 activation $xq$ + $xs/xsum$ into LDS, and each of 256 threads \dpa{}s its
4$\times$4 sub-tile via the Q4\_K$\times$q8\_1 affine identity
\[
\text{acc} \mathrel{+}= d_1[\text{col}]\cdot xs[\text{row}]\cdot \dpa(q4, xq) \;-\; m_1[\text{col}]\cdot xsum[\text{row}].
\]
Activations are quantized to q8\_1 once per prefill. It is gated
\texttt{vulkan\_q4k\_dp4a2d\_matches\_default} (rel $<1.5\!\times\!10^{-2}$; the q8\_1 activation
quant adds $\sim$0.5--1\% on top of f32 reorder, a decode bug would be $\gg$ that). The default-flip
ships it for dense ($w_{\text{off}}{=}0$, $m{>}1$) where the device advertises integer dot-product
(queried via \texttt{PhysicalDeviceShaderIntegerDotProductFeatures}); otherwise the universal f32
2D tile. A gfx1151 engine now gets $\sim$224 T/s Vulkan prefill by default, up from $\sim$24.
\paragraph{The practical ceiling, pinned by a tile sweep.} At 5056 GFLOP/s the \dpa{}-2D kernel is
$\sim$34\% of the $\sim$14.8 TFLOP f32 peak. A tile sweep pins the bottleneck as \emph{occupancy},
not VRAM/LDS traffic: register-blocking the inner \dpa{} is flat (the compiler already did it / added
VGPRs that cost occupancy); BM=128 (half the weight re-read) is $3\x$ \emph{slower} (register spill
/ occupancy collapse); BM=32 is slightly slower (re-read + barrier overhead). 64$\times$64 is the
sweet spot. The residual gap to f32-peak is the \emph{inherent} Q4\_K cost (per-sub-block scale
application + in-kernel decode + the q8\_1 activation pass), not a tiling defect --- closing it
needs a structurally different kernel (decode-amortized coopmat), not more tuning. This is the
honest statement of where blind kernel-tuning ends on this GPU.
\section{Measured dead-ends: the boundary map}
\label{sec:deadends}
The negatives are load-bearing: each is explained by the surfacing rule, and together they fence
off the regimes. We report them because an inference team that does not know these boundaries will
spend weeks re-discovering them.
\begin{table}[h]
\centering
\caption{Things that were correct (bit-exact) but did not help, with the regime that explains why.}
\small
\begin{tabular}{>{\raggedright\arraybackslash}p{3.9cm} >{\raggedright\arraybackslash}p{2.2cm} >{\raggedright\arraybackslash}p{6.7cm}}
\toprule
Change (bit-exact) & Result & Why (taxonomy) \\
\midrule
Fold \texttt{quantize\_q8\_1} into the \dpa{} matvec (ROCm) & \textbf{$-22\%$} ($77.4\to59$) & Each matvec \emph{block} re-quantizes the full activation into its own LDS (256$\times$ for a 2048-row proj) + a \texttt{\_\_syncthreads}: redundant serialized work \emph{injected} into a bandwidth-bound matvec. Under graphs the separate quantize launch was already free. \emph{Violates the rule.} \\
Naive coopmat tensor-core prefill (Vulkan) & 0.26\x{} vs \dpa{} & Re-decodes the weight to f16 every K=16 step ($k/16$ passes); Q4\_K decode swamps the fast \texttt{coopMatMulAdd}s. Decode-bound, not tensor-core-bound. \\
1D weight-only LDS tiling (Vulkan) & 0.21\x & Weight re-read was not the bottleneck; few-heavy-workgroups lose to many-light register-tiled invocations. The lever was 2D (stage \emph{both} operands), not 1D. \\
TILE\_M=32 MoE GEMM (ROCm) & slower than 64 & Less empty WMMA work but re-reads each expert weight in 2 tiles; the GEMM is weight-bandwidth-bound. \\
\dpa{} i-quant decode, model-level (GB10) & flat & Host/launch-bound decode: GPU finishes the faster matvec and stalls on host work. (Same kernel is $2.27\x$ on ROCm-eager.) \\
\texttt{moe\_route} fusion, model-level (CUDA) & flat & Repackages routing into one block-serial launch; CUDA decode is not router-launch-bound and CUDA has no expensive \texttt{gpu-sort} to eliminate (ROCm's $+13\%$ came from that sort). \emph{Repackaging, not eliminating.} \\
f32-native cast cut (ROCm) & flat & The casts are dtype-tangled around the F32-residual/F16-KV boundary; removing one shifts cost across the boundary. \\
\bottomrule
\end{tabular}
\end{table}
\noindent Two of these are the \emph{same kernel} as a kept win elsewhere (\dpa{} i-quant: ROCm
$2.27\x$ / GB10 flat; \texttt{moe\_route}: ROCm $+13\%$ / CUDA flat). That is the cleanest possible
confirmation of the taxonomy: the kernel did not change, the \emph{binding resource} did. We keep
both on the flat platforms anyway --- bit-exact, decomplecting (one selector, fewer tables, matches
llama), and latent for the regime where they bind --- which is the right call precisely \emph{because}
the taxonomy says they cost nothing and may surface on the next model (e.g.\ a 256-expert router
doubles the routing math).
\section{The cross-backend law and model-level dilution}
\label{sec:law}
Stacking the per-backend work yields the model-level result of paper~05, restated as a law:
\begin{itemize}[noitemsep]
\item \textbf{Decode is bandwidth-roofline parity} on every backend (0.98--1.04\x): once the
matvec streams weights at the memory ceiling, all engines converge, because decode
throughput \emph{is} memory bandwidth / model bytes. hanzo's per-format decode matvec is at
88--99\% of the measured roofline (\texttt{rocprofv3} FETCH\_SIZE), so there is nothing left
to win there --- and indeed every decode \emph{kernel} win above surfaced only through
\emph{launch/work elimination}, never through faster matvec arithmetic.
\item \textbf{Prefill is the frontier} (0.76--0.91\x): it is compute-bound, so it rewards the
matrix/tensor-core GEMM scheduling that llama.cpp has most heavily tuned. Every prefill
lever in this paper (MoE qmmq $2.58\x$, dynamic M-tile, Vulkan \dpa{}-2D $9.35\x$) closes a
chunk of this gap; the residual is real engineering (decode-amortized coopmat on Vulkan,
the last $\sim$10\% of GEMM efficiency on CUDA), not a rounding error.
\end{itemize}
\paragraph{Model-level dilution.} A kernel win of factor $\kappa$ surfaces as
$\approx 1 + f\,(\,1 - 1/\kappa\,)$ end-to-end, where $f$ is the fraction of \emph{binding-resource}
time the kernel occupied. The Vulkan \dpa{}-2D $9.35\x$ kernel lifts engine prefill $\sim 24\to224$
T/s ($f$ large, prefill is GEMM-dominated) but would be flat on decode ($f\approx 0$, decode is not
the GEMM). The ROCm Q6\_K $4.9\x$ kernel gives $+17\%$ ($f\approx 0.38$, its compute-bound
fraction). The GB10 i-quant $1.71\x$ gives $0\%$ ($f\approx 0$, host-bound). The law is just
Amdahl's law with the binding resource as the denominator --- but the discipline it enforces is to
\emph{measure $f$ before writing the kernel}.
\section{A correctness boundary: the flash-attention repetition bifurcation}
\label{sec:flash-bifurcation}
The sharpest result of this work is not a speed lever but a \emph{correctness} one, and it maps the
exact limit of fused attention on tensor cores. Qwen3-8B-Q4\_K\_M generates coherently under an
eager attention (materialized \texttt{softmax}$(QK^\top/\sqrt d)V$ in f32) but \emph{collapses into
token repetition} under flash-attention-2 on CUDA --- byte-identical weights, same sampler, same
box; only the attention kernel differs. The collapse is deterministic and appears at the first
generated token.
\paragraph{The precision ladder, exhausted.} Flash-attention-2 casts the f32 softmax probabilities
$P=\mathrm{softmax}(S)$ to the kernel element type before the $P\!\cdot\!V$ tensor-core matmul
(\texttt{flash\_fwd\_kernel.h}: \texttt{convert\_type<Element>(acc\_s)}). We raised that precision in
three proved-by-build steps, each verified on the running model at two seeds:
\begin{center}\small
\begin{tabular}{@{}llll@{}}
\toprule
$P$ representation & mantissa bits & \texttt{exp} & bare-trigger continuation \\
\midrule
bf16 & 7 & \texttt{exp2f} & \texttt{"needle needle\dots"} \\
f16 & 10 & \texttt{exp2f} & \texttt{"0123456789\dots"} \\
split $P_{hi}{+}P_{lo}$ & $\sim$20 & \texttt{exp2f} & \texttt{".matmat.matmat\dots"} \\
split $P_{hi}{+}P_{lo}$ & $\sim$20 & \texttt{expf} (exact) & \texttt{"1)2)3)4)5)\dots"} \\
eager (reference) & 23 (f32), single-pass & \texttt{expf} & \texttt{"\dots Paris\dots"} (coherent) \\
\bottomrule
\end{tabular}
\end{center}
More precision only \emph{moves the attractor token} (needle $\to$ digits $\to$ matmat); it never
clears it. This is the signature of a knife-edge numerical bifurcation, not a localizable bug.
\paragraph{Everything else, eliminated.} For the bare trigger ($\sim$7 tokens) the remaining
flash-vs-eager differences are ruled out by structure, not guesswork: (i) the $QK^\top$ score MMA
already accumulates in f32 (CUTLASS default), so $S$ is f32-identical to eager; (ii) $V$ is bf16 in
the KV cache for \emph{both} paths, so the bf16-input/f32-accumulate MMA is numerically the eager
matmul --- there are no low bits of $V$ to recover; (iii) with $\texttt{kBlockN}=128$ the trigger is
a \emph{single} K-tile, so the cross-tile online-softmax rescale executes zero times yet the collapse
still appears; (iv) the \texttt{exp2f}-vs-\texttt{expf} gap is $\lesssim 2^{-23}$, orders below the
$2^{-8}$ bf16-$P$ error already shown non-curative --- and this last one was \emph{also} confirmed
empirically: exact \texttt{expf} atop f32-class $P$ still degenerates at both seeds (into a fresh
\texttt{"1)2)3)\dots"} attractor), exhausting the precision-and-transcendental hypothesis space.
\paragraph{The boundary.} What remains after all eliminations is a lone variable: $P$ is
$\sim$20-bit on the tensor-core path versus true 23-bit f32 in eager. Because tensor cores
\emph{require} $\le$tf32/f16/bf16 inputs, a genuinely f32 $P\!\cdot\!V$ \emph{is} the eager
(CUDA-core) path --- so on this hardware \textbf{fusion and f32-precision $P\!\cdot\!V$ are mutually
exclusive}. The shipped fix routes bf16 attention to the eager kernel; it is not a stopgap but the
\emph{correct} answer, and it is essentially optimal: any stable variant must carry f32 $P\!\cdot\!V$,
which forgoes tensor cores and thus lands at eager's throughput regardless. Its only cost is
long-context prefill ($2.66\x$ at 2409 tokens; short prompts and \emph{all} decode are unaffected,
since decode attention is a single query against the cache), and that cost is intrinsic to stability.
The methodological point echoes the rest of this paper: the result was reachable only by
\emph{verifying against the running model} --- five kernel edits that ``compiled and committed''
were disproved by the generated tokens before the boundary came into focus.
\section{The characterized-but-open frontier: Vulkan decode}
\label{sec:frontier}
Paper~05's parity claim is decode-on-AMD-via-ROCm. The Vulkan \emph{decode} path is the one place
hanzo is far from llama (8B Q4\_K: 3.3 vs 41 T/s, $\sim$0.08\x), and this program has now diagnosed
it precisely rather than left it as ``slow.'' A \texttt{VK\_PROFILE} breakdown of one decode token:
\begin{lstlisting}
[VK_PROFILE] flush: dispatch=1336 barriers=903 record=0.766ms submit=1.406ms fence_wait=142.827ms
\end{lstlisting}
The diagnosis: it is \textbf{not} recording-bound (CPU record $=0.77$~ms, so command-buffer/graph
reuse would buy nothing) --- it is \textbf{GPU-execution-bound on op-count serialization}. 1336
tiny $M{=}1$ dispatches chained through \textbf{903 global memory barriers} (\texttt{vk::MemoryBarrier},
full \texttt{SHADER\_WRITE}$\to$\texttt{READ}, on \emph{true} data dependencies) serialize the GPU at
$\sim$107~$\mu$s/op on compute that is only $\sim$$\mu$s. Per the taxonomy this is GPU-busy-bound
decode, so the lever is \emph{eliminating ops}, and the barriers are correct (real dependencies) ---
the only way to remove them is to fuse the dependent ops so the dependency lives \emph{inside} a
kernel.
\subsection{Roadmap: the campaign ROCm already validated}
\label{sec:vulkan-roadmap}
This is not speculative: it is the exact campaign that took ROCm decode from launch/busy-bound to
parity (\S\ref{sec:rocm}), ported to Vulkan.
\begin{enumerate}[noitemsep]
\item \textbf{Fuse the attention epilogue} --- q/k RMSNorm + RoPE + KV-cache write into one
kernel (ROCm's \texttt{rope\_norm\_positions} twin), and residual-add + RMSNorm
(\texttt{add\_rmsnorm}). These \emph{eliminate} per-op barriers, not just launches.
\item \textbf{Fuse the FFN} --- gate+up shared-quantize, silu+mul.
\item \textbf{Fuse routing} --- the \texttt{moe\_route} kernel (already proven $+13\%$ on ROCm,
where the eliminated sort is expensive --- and Vulkan, like ROCm, pays for the sort).
\item \textbf{Uniform Vulkan GEMM for all 22 formats} --- deletes the dequant fallback and the
deadlock gate by construction (\S\ref{sec:vulkan}), the structural endpoint.
\end{enumerate}
The honest status: \textbf{ROCm is at parity; Vulkan decode is the open frontier}, characterized
down to the dispatch/barrier counts, with a roadmap whose every step has a measured ROCm precedent.
The decomplection endpoint (axes \S\ref{sec:arch}) is a kernel DSL (CubeCL/Triton/MLIR-class) that
writes the contraction once and lowers it to every backend, collapsing the
backends$\times$formats$\times$ops product to backends$+$formats$+$ops --- at which point a fusion is
written once and surfaces on whichever backend's regime it binds.
\section{Verification methodology (so the numbers mean something)}
\label{sec:method}
Every claim in this paper rests on three controls, because inference benchmarks are unusually easy
to corrupt:
\begin{itemize}[noitemsep]
\item \textbf{Bit-exact gates.} A kernel ships only after \texttt{nbad=0} versus a CPU
\texttt{to\_float} oracle (documented f32-reorder / f16-rounding tolerances). Correctness is
the precondition for a performance claim, not a follow-up.
\item \textbf{Controlled A/B.} Decode A/Bs are device-map-pinned (\texttt{-n 0:48} on ROCm; all
layers resident) and run in-binary via a fallback env toggle (\texttt{HANZO\_*\_FALLBACK}),
so the \emph{only} variable is the kernel. The headline ``$+28\%$'' that an earlier pass
reported was a measurement artifact --- the auto-mapper offloading $1/3$ of layers --- and
the controlled A/B showed the real number was $+1.3\%$. We report the controlled number.
\item \textbf{Kernel-isolated bench.} GEMM ladders use operands resident on-GPU and one final
synchronize, because a per-iteration host upload/readback otherwise dominates and masks the
kernel (the difference between the column kernel looking 1.2\x{} and 9\x{} slower).
\end{itemize}
\paragraph{Contamination caveat (stated, not hidden).} The first evo Vulkan matrix in this pass was
run under load~59 (a concurrent ROCm build + a model download + a browser) and is discarded; the
numbers in \S\ref{sec:vulkan}--\ref{sec:frontier} are from the idle box (load $<0.5$). A benchmark
on a contended box is not a slow benchmark, it is a wrong one.
\section{What this paper does \emph{not} claim}
Single-stream only (no batched/serving throughput); the model-level numbers are paper~05's
single-model/single-quant pass, not re-swept here; Metal kernel-level work is summarized from its
own audit, not re-derived; and the Vulkan-decode roadmap (\S\ref{sec:vulkan-roadmap}) is
characterized and precedented but \emph{not yet shipped} --- we label it frontier, not result. Every
performance figure is a measured throughput or a kernel-isolated microbench on the named hardware;
derived quantities (effective GB/s, M-fill \%) are computed from measured values and labelled. No
number here was invented or projected without the label ``projection.''
\section{Conclusion}
The portable-engine thesis survives contact with the kernels: from one multi-backend codebase,
hanzo-engine is at \texttt{llama.cpp} decode parity and within a bounded, well-understood prefill
margin, and every step of that was a \emph{measured} lever gated bit-exact. The transferable result
is not any single kernel but the \textbf{surfacing rule and its taxonomy}: decode is
bandwidth-roofline (win by moving fewer bytes or eliminating launches/work, never by faster
arithmetic), prefill is compute-bound (win by tensor-core scheduling and tile fill), and the
\emph{same} kernel surfaces or vanishes depending on which resource binds on a given (platform,
regime). The dead-ends prove the rule as sharply as the wins --- the $-22\%$ quantize-fold, the
$0.26\x$ naive coopmat, the GB10 i-quant flat that is $2.27\x$ on ROCm. The one fully-characterized
open frontier, Vulkan decode, is op-count-serialization-bound by profile, and its fix is the exact
fusion campaign ROCm has already validated to parity. Optimize the binding resource; prove it
bit-exact; measure $f$ before you write the kernel.
\appendix
\section{Reproducibility}
\textbf{Hardware.} AMD Ryzen AI Max+ 395 ``Strix Halo'' (Radeon 8060S, gfx1151, RDNA3.5, 40 CU,
$\approx$231 GB/s); NVIDIA GB10 (Grace-Blackwell, sm\_121, CUDA 13, 121 GB unified, Linux aarch64);
Apple M4 Max (137 GB unified, Metal). \textbf{Software.} hanzo-ml (the catalog spans the 0.11.x
series) / hanzo-engine, built \texttt{--release} per box; \texttt{llama.cpp} stock HIP/CUDA/Metal/Vulkan
builds as the baseline. \textbf{Models.} Qwen3 family GGUF (0.6B/4B/8B dense, 30B-A3B MoE) at
Q8\_0/Q4\_K\_M and the UD i-quants. \textbf{A/B protocol.} In-binary fallback env toggles
(\texttt{HANZO\_*\_FALLBACK}) so the kernel is the only variable; ROCm decode device-map-pinned
\texttt{-n 0:NLAYERS}; GEMM ladders via the kernel-isolated \texttt{bench\_matmul\_q4k} /
\texttt{bench\_iquant\_decode}; bit-exact gates via the named \texttt{*\_numeric} oracles in
hanzo-cli. \textbf{Profilers.} \texttt{rocprofv3} (ROCm GPU-busy + FETCH\_SIZE roofline),
\texttt{nsys} (CUDA), \texttt{VK\_PROFILE=1} (the in-engine per-batch dispatch/barrier/fence-wait
breakdown). GPU-safety: ROCm/HIP processes are run strictly serially (concurrent kfd init on
gfx1151 can wedge the driver), benches background-only.
\end{document}