% Author: Simon-Pierre Boucher — contact@spboucher.ai
\documentclass[11pt,a4paper]{article}

\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{amsmath,amssymb}
\usepackage{booktabs}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{graphicx}
\usepackage[margin=1in]{geometry}
\usepackage{hyperref}
\hypersetup{colorlinks=true,linkcolor=blue,citecolor=blue,urlcolor=blue}
\usepackage{microtype}

\lstdefinestyle{msl}{
  basicstyle=\ttfamily\footnotesize,
  keywordstyle=\color{blue!70!black}\bfseries,
  commentstyle=\color{green!40!black}\itshape,
  stringstyle=\color{red!60!black},
  numbers=none, breaklines=true, frame=single, framesep=4pt,
  backgroundcolor=\color{gray!5}, showstringspaces=false,
  morekeywords={kernel,threadgroup,device,constant,constexpr,simdgroup_float8x8,
    simdgroup_matrix,enum,uint,template,typename,tensor,dextents,tensor_inline,half,bfloat}
}
\lstset{style=msl}

\title{\textbf{Forge}: Building an LLM Training Framework from Scratch\\
in C++ and Metal on Apple Silicon\\
\large Measured Lessons on Compiler Traps, Register Pressure,\\
and the M5 Neural Accelerators}

\author{Simon-Pierre Boucher\\
\texttt{contact@spboucher.ai}}

\date{31 July 2026}

\begin{document}
\maketitle

\begin{abstract}
We report engineering findings from \emph{Forge}, a transformer training framework
written from scratch in C++20 with hand-written Metal compute kernels, with no
PyTorch, MLX, or other machine-learning dependency. Every GPU kernel is validated
against a CPU reference to $\leq 10^{-4}$, and every optimization reported here was
accepted only after the training loss trajectory remained numerically unchanged.

Four findings are, we believe, of general interest to practitioners writing Metal
compute kernels. First, the idiom \texttt{constant constexpr} for tile constants in
the Metal Shading Language silently declares an \emph{address-space variable} rather
than a compile-time constant; the resulting loss of loop unrolling spills every
matrix accumulator to the stack and cost a factor of \textbf{12$\times$} on our GEMM.
Second, this class of defect is invisible in the AIR intermediate representation at
every optimization level, because unrolling and fragment promotion occur in the
driver's back end --- practitioners must benchmark rather than read the IR. Third,
per-thread register pressure, not bandwidth, is the dominant constraint in
attention backward kernels; measured spill counts guided three successive
restructurings that took the backward pass from 107\,ms to
7.05\,ms, a \textbf{15.2$\times$} improvement. Fourth, on
M5-generation hardware the Metal Performance Primitives cooperative-tensor
\texttt{matmul2d} reaches \textbf{51.5\,TFLOP/s} with \texttt{half} operands against
10.6\,TFLOP/s for a well-tuned hand-written \texttt{simdgroup\_matrix} kernel,
a \textbf{4.9$\times$} gap that inverts the usual cost/benefit assessment of mixed
precision.

We also report a negative result that we consider equally useful: \texttt{half} and
\texttt{bfloat} operands buy only 18--22\% over \texttt{float} on the
\texttt{simdgroup\_matrix} path, so mixed precision on that path is a memory
optimization and not a compute one. The framework trains models from 12M to 205M
parameters on a single machine.
\end{abstract}

\tableofcontents

\section{Introduction}

Apple Silicon offers a large unified memory pool and substantial GPU throughput, but
the software ecosystem for \emph{training} on it is thin. Inference frameworks are
mature; training-specific kernels, in particular a fused attention backward pass, are
largely absent. A survey we conducted on 31 July 2026 (Section~\ref{sec:survey})
found that of the major open-source frameworks, none ships a fused attention backward
kernel for Metal.

This paper reports what we learned building one. \emph{Forge} is a decoder-only
transformer training framework in pure C++20 with Metal compute kernels. Its
architecture is entirely configuration-driven, and it has no machine-learning
dependency: only Apple's \texttt{metal-cpp} bindings and a single-header JSON parser.

Our contribution is not the framework itself but the measurements. Each section below
states a hypothesis, the measurement that tested it, and the outcome --- including
the cases where the hypothesis was wrong.

\subsection{Methodology and reproducibility}

All measurements were taken on an Apple M5 Max (40-core GPU) running macOS 27.0
(build 26A5388g) with Xcode 26.6 and Metal toolchain 32023.883. Kernels are compiled
at \texttt{-std=metal3.2} except where Metal 4 features are required.
Throughput figures are computed from
\texttt{MTLCommandBuffer.GPUStartTime}/\texttt{GPUEndTime}, which excludes CPU
encoding time.

Two disciplines govern every result reported here.

\paragraph{Correctness gates optimization.} Every Metal kernel has a CPU reference
implementation, and 85 parity assertions compare them (maximum absolute error
$\leq 10^{-4}$ in \texttt{float}; most are bit-exact). Gradients are additionally
checked against central finite differences. An optimization is accepted only if the
full suite still passes.

\paragraph{Numerical equivalence, not just correctness.} For the larger kernel
rewrites we required that the \emph{training loss trajectory} be unchanged. After
replacing the scalar attention kernels with tiled matrix-multiply versions, the loss
at every logged step over 250 optimizer steps was identical
(8.3618, 7.6130, 6.6455, 5.0902, 4.9845; validation 4.9018). This is a stronger
check than parity on a single call, since it accumulates any discrepancy through the
optimizer.

\section{System overview}

The framework comprises roughly 6{,}500 lines of C++ and Metal. A \texttt{Tensor}
type provides shared-storage views over pooled \texttt{MTLBuffer} allocations in
\texttt{MTLStorageModeShared}, so the same memory is addressable from CPU and GPU
with no copies. A dynamic autograd tape records backward closures. Operations
dispatch to either a CPU reference backend or the Metal backend behind one interface,
which is what makes the parity testing possible.

Execution follows the batching discipline recommended for Apple GPUs: the whole
training micro-batch is encoded into one command buffer with a small number of
long-lived compute encoders, and the GPU is synchronized only at the loss-readback
boundary.

\section{Finding 1: \texttt{constant constexpr} is not a compile-time constant}
\label{sec:constexpr}

\subsection{The trap}

Tile geometry in a GEMM kernel is naturally expressed as named constants. The
apparently idiomatic C++ spelling is:

\begin{lstlisting}
constant constexpr uint TM = 4;   // WRONG in MSL
\end{lstlisting}

In the Metal Shading Language, however, \texttt{constant} is an \emph{address-space
qualifier}, and program-scope variables are \emph{required} to live in it. The
declaration above therefore creates a variable in the constant address space, not a
compile-time constant. Loop bounds derived from it are not known at compile time, so
the fragment loops do not unroll; \texttt{acc[i][j]} becomes dynamic indexing into an
array of opaque \texttt{simdgroup\_matrix} values; and the compiler is forced to
place all sixteen 8$\times$8 accumulators (256 bytes each) on the stack. Every
matrix-multiply-accumulate then pays roughly a kilobyte of stack traffic.

The correct spelling uses enumerators, which are true integral constant expressions:

\begin{lstlisting}
enum : uint { BM = 64, BN = 64, BK = 16, WM = 2, WN = 2,
              TM = BM / (8 * WM), TN = BN / (8 * WN) };
\end{lstlisting}

together with \verb|#pragma clang loop unroll(full)| on the fragment loops. Note also
that \verb|#pragma unroll full|, which appears in Apple's own header documentation,
is not valid here.

\subsection{Measurement}

The change is textual and semantically neutral. Its effect is not:

\begin{table}[h]
\centering
\begin{tabular}{lrr}
\toprule
Shape & \texttt{constant constexpr} & \texttt{enum} + unroll pragma \\
\midrule
$2048^3$          & 0.82 & 10.21 \\
$4096^3$          & 0.85 & \phantom{0}9.83 \\
$65536\times512\times1408$ & 0.83 & 10.68 \\
\bottomrule
\end{tabular}
\caption{f32 GEMM throughput (TFLOP/s), M5 Max. A 12$\times$ difference from the
declaration form of tile constants.}
\end{table}

Before this fix the ``optimized'' \texttt{simdgroup\_matrix} kernel was
\emph{three times slower} than a naive 16$\times$16 tiled kernel --- a result that
invites the wrong conclusion, namely that the matrix instructions are not worth
using.

\section{Finding 2: the intermediate representation cannot diagnose this}
\label{sec:ir}

Having observed the anomaly, our first instinct was to inspect the compiler output:

\begin{lstlisting}
xcrun metal -std=metal3.2 -O2 -S -emit-llvm -c matmul_simd.metal -o out.ll
\end{lstlisting}

The AIR showed three \texttt{alloca} instructions for the fragment arrays and only
two matrix-multiply intrinsics where thirty-two were expected --- apparently
confirming the spill hypothesis. It also, however, showed \emph{exactly the same
thing} at \texttt{-O0}, \texttt{-O2} and \texttt{-O3}, and continued to show it after
the fix that produced the 12$\times$ speedup.

The explanation is that loop unrolling and promotion of \texttt{simdgroup\_matrix}
values into registers happen in the driver's AIR-to-ISA back end, at pipeline
creation time, not in the front end that \texttt{-emit-llvm} exposes. The IR is
therefore uninformative for exactly the class of question one is most tempted to ask
of it.

\paragraph{Practical rule.} On Metal, benchmark; do not read the IR. We lost time to
this and record it because the failure mode is silent: the IR looks like a
confirmation.

\section{Finding 3: register pressure dominates attention backward}
\label{sec:registers}

\subsection{Three restructurings, each guided by measurement}

Our first fused attention implementation assigned one thread per query row and held
$q$, $o$ (forward) or $k$, $v$, $dk$, $dv$ (backward) in per-thread arrays. At head
dimension 64 the backward variant holds $4 \times 64 = 256$ floats, or one kilobyte
per thread.

We tested the hypothesis that this spills, by progressively reducing what each thread
holds:

\begin{table}[h]
\centering
\begin{tabular}{lrr}
\toprule
Attention backward, per layer & gpt-10m shapes & gpt-25m shapes \\
\midrule
$k,v,dk,dv$ all in registers        & 150 & --- \\
read-only $k,v$ from device         & 118 & 611 \\
$dK$ and $dV$ split into two kernels & 105 & 549 \\
\midrule
tiled with \texttt{simdgroup\_matrix} & \phantom{0}9.66 & \phantom{0}45.5 \\
\quad + $dK$/$dV$ split again        & \textbf{\phantom{0}7.05} & \textbf{\phantom{0}32.9} \\
\bottomrule
\end{tabular}
\caption{Time in milliseconds. gpt-10m shapes are $B{=}64$, $T{=}512$, $H{=}6$,
$d_h{=}64$; gpt-25m shapes are $B{=}64$, $T{=}1024$, $H{=}8$. Total improvement
15.2$\times$ and 17.0$\times$ respectively.}
\end{table}

The read-only operands $k$ and $v$ are re-read from device memory on every iteration
in the second row, which sounds wasteful; because a thread reads the same address
every iteration, the L1 cache serves it, and removing the two arrays from registers
is the larger effect.

\subsection{Confirming the mechanism directly}

The reasoning above was indirect. Xcode~26 ships \texttt{gpudebug}, a headless
command-line GPU debugger that reports compiler statistics per kernel. It requires
shader sources to be embedded in the library, which in turn requires
\texttt{-frecord-sources} at \emph{both} the compile and link steps. (The link must
be performed by \texttt{metal}; \texttt{metallib} rejects the flag.) With that fixed,
the mechanism is directly visible:

\begin{table}[h]
\centering
\begin{tabular}{lrrr}
\toprule
Kernel & Temp registers & Spilled bytes & Cost \\
\midrule
flash forward, scalar        & 126 & 368 & 38.9\% \\
flash forward, MMA-tiled     & \phantom{0}85 & \textbf{0} & 29.6\% \\
flash backward $dQ$, MMA     & \phantom{0}95 & \textbf{0} & \phantom{0}8.5\% \\
flash backward $dKV$, MMA (fused) & 111 & \textbf{4352} & 16.4\% \\
\bottomrule
\end{tabular}
\caption{Compiler statistics from \texttt{gpudebug}, M5 Max.}
\end{table}

Two things follow. The matrix-tiled forward eliminates the spill entirely
($368 \rightarrow 0$ bytes), which is the mechanism behind its speedup: an
8$\times$8 fragment occupies two floats per lane, where the scalar formulation held
whole arrays of head-dimension length. And the fused $dK/dV$ backward kernel
\emph{still spilled} 4352 bytes --- a defect we had not suspected, and which made it
the most expensive backward kernel. Splitting it recovered a further 27\%.

We regard the tooling lesson as the transferable one: register pressure is
measurable, cheaply and without a GUI, and it is worth measuring before restructuring
a kernel on intuition.

\section{Finding 4: cooperative tensors and the M5 neural accelerators}
\label{sec:mpp}

\subsection{Mixed precision on the classical path is a memory optimization}

The literature disagrees about \texttt{half} on Apple GPUs. Published
microbenchmarks report that \texttt{half} and \texttt{float} fused multiply-add
execute at the same rate, the benefit arising from register and bandwidth pressure;
Apple's material for the M3 generation describes up to twice the arithmetic
throughput from co-issue. Since plumbing mixed precision through a training framework
is a substantial amount of work, we measured before committing. Holding the tiling
and the \texttt{float} accumulator fixed and varying only the operand type:

\begin{table}[h]
\centering
\begin{tabular}{lrrr}
\toprule
Shape & \texttt{float} & \texttt{half} & \texttt{bfloat} \\
\midrule
$2048^3$ & \phantom{0}9.29 & 10.64 & 10.82 \\
$4096^3$ & \phantom{0}9.85 & 11.81 & 12.07 \\
$65536\times512\times1408$ & \phantom{0}9.90 & 12.45 & 12.44 \\
\bottomrule
\end{tabular}
\caption{\texttt{simdgroup\_matrix} throughput (TFLOP/s) by operand precision,
\texttt{float} accumulator throughout.}
\end{table}

The gain is 18--22\%, consistent with the register/bandwidth explanation rather than
with doubled arithmetic rate. On this path, mixed precision halves activation memory
--- valuable, since activation memory bounds trainable model size --- but it is not a
compute optimization.

\subsection{The same operands through \texttt{matmul2d}}

\texttt{MetalPerformancePrimitives.framework} ships in the macOS 26.5 SDK and exposes
\texttt{mpp::tensor\_ops::matmul2d}, a cooperative-tensor matrix multiply that targets
the per-core neural accelerators introduced with the M5 generation. Compiling at
\texttt{-std=metal4.0}, with a 64$\times$32 tile over four SIMD groups and a
\texttt{float} accumulator:

\begin{table}[h]
\centering
\begin{tabular}{lrrrr}
\toprule
 & \multicolumn{2}{c}{\texttt{simdgroup\_matrix}} & \multicolumn{2}{c}{\texttt{matmul2d}} \\
\cmidrule(lr){2-3}\cmidrule(lr){4-5}
Shape & \texttt{float} & \texttt{half} & \texttt{float} & \texttt{half} \\
\midrule
$2048^3$ & \phantom{0}9.3 & 10.6 & 14.9 & \textbf{51.5} \\
$4096^3$ & \phantom{0}9.9 & 11.8 & 14.6 & \textbf{44.3} \\
$65536\times512\times1408$ & \phantom{0}9.9 & 12.4 & 14.4 & \textbf{23.8} \\
\bottomrule
\end{tabular}
\caption{Throughput (TFLOP/s), M5 Max. A 1.5$\times$ gain in \texttt{float} and
4.3--4.9$\times$ in \texttt{half}.}
\end{table}

Because a 4.9$\times$ claim invites scepticism, we verified numerically rather than
only timing: against the CPU reference the \texttt{float} path is bit-exact, the
\texttt{half} path differs by $3.8 \times 10^{-6}$ from a reference fed identically
rounded inputs, and every output element is non-zero, confirming that the full
reduction over $K$ occurs. The transposed variants required for training
($X W^{\!\top}$ in the forward, $dX = dY W$ and $dW = dY^{\!\top} X$ in the backward)
are likewise bit-exact.

\paragraph{Generational caveat.} Published work on M4 Max hardware found
\texttt{matmul2d} still executing on the shader cores and losing to a hand-fused
GEMM. Our M5 result is the opposite. The accelerator hardware differs by generation;
this measurement should be repeated per target device rather than assumed.

\paragraph{Consequence.} This inverts the priority we had assigned to mixed
precision. Judged on the classical path it is worth 20\% and is chiefly a memory
feature; as the entry condition for the accelerator path it is worth 4.9$\times$.

\subsection{Interface notes}

Several details cost us time and are not documented accurately.

\begin{itemize}
  \item Bind ordinary buffers and construct tensors inside the kernel with the
        \texttt{tensor\_inline} descriptor. The default \texttt{tensor\_handle}
        descriptor wraps an opaque handle obtainable only from a host-side
        \texttt{MTLTensor}, which would require substantial host plumbing.
  \item Extents are ordered (columns, rows), and \texttt{slice()} takes column then
        row; a transposed operand must be sliced the other way and its extents
        swapped.
  \item The element type must be non-\texttt{const}; a \texttt{const float} tensor
        fails a static assertion.
  \item Zero the destination cooperative tensor using
        \texttt{is\_valid\_element(i)}. Apple's own header example calls
        \texttt{get\_mask(i)}, which does not exist in this SDK.
\end{itemize}

\section{Finding 5: concurrency, not Metal 4, is the encoding win}

Metal's default compute encoder orders every dispatch against its predecessor. For
the optimizer step this is pure loss: AdamW issues one dispatch per parameter tensor
--- of order one hundred for a 100M-parameter model --- and each gradient-norm
reduction is a single threadgroup. These are mutually independent.

We introduced a scoped concurrent-dispatch region and applied it to both optimizer
passes. On the 100M configuration end-to-end throughput rose from 7.9k to
\textbf{9.6k tokens/s}, a 22\% improvement, with identical losses. The change is
about twenty lines and requires no Metal 4 adoption.

An independent investigation confirmed the general shape of this result --- roughly
15$\times$ on a synthetic batch of small independent dispatches --- while also
establishing that Metal 4's command-encoding path offers essentially no CPU-side
advantage once invariant bindings are hoisted out of the dispatch loop. The
concurrency, available since macOS 10.14, is the entire effect.

\section{Ecosystem survey}
\label{sec:survey}

We surveyed the major open-source frameworks on 31 July 2026 for a fused attention
backward pass on Metal:

\begin{table}[h]
\centering
\begin{tabular}{ll}
\toprule
Framework & Fused attention backward on Metal \\
\midrule
MLX & No. \texttt{use\_fallback} returns \texttt{true}; \texttt{eval\_gpu} throws
      \texttt{"NYI"}. The fused backward is CUDA-only. \\
llama.cpp & No. The Metal backend does not support
            \texttt{GGML\_OP\_FLASH\_ATTN\_BACK}. \\
PyTorch MPS & No. Hand-written Metal forward kernels only. \\
Candle & No. Vendored MLX forward only. \\
tinygrad & No. Its flash-attention backward targets AMD. \\
Burn / CubeCL & Kernels exist but are a scalar scaffold, not wired to autodiff. \\
\bottomrule
\end{tabular}
\end{table}

MLX's Metal forward additionally declines the fused path under gradient tracing, with
the comment that unfused is faster for training on Metal. Production prior art is
limited to Philip Turner's \emph{metal-flash-attention} and one third-party package.
Both use the same three-stage, atomic-free structure we arrived at independently:
a preprocessing kernel for $D = \mathrm{rowsum}(dO \circ O)$ in \texttt{float}, a
$dQ$ kernel parallel over queries, and a $dK/dV$ kernel parallel over key/value rows.

\section{Attention kernel design}

Our tiled forward kernel assigns 32 query rows per threadgroup across four SIMD
groups, holding $Q$ and the output accumulator in registers as 8$\times$8 fragments
and staging $K$ and $V$ through threadgroup memory. The score tile is round-tripped
through threadgroup memory so that the softmax reductions run on ordinary threads.

\paragraph{A scoped problem worth recording.} Online softmax requires rescaling the
output accumulator by a per-row factor at every key/value block,
$O \leftarrow \mathrm{diag}(\mathrm{corr})\, O$. But the Metal specification leaves
the mapping from \texttt{simdgroup\_matrix} elements to lanes \emph{unspecified}, so a
lane cannot determine which row its registers correspond to. MLX resolves this by
reverse-engineering the mapping. We instead construct the 8$\times$8 diagonal matrix
in threadgroup memory and apply it with an ordinary matrix multiply. This is
specification-clean, costs $d_h/8$ additional multiplies per block (about 25\% more
matrix work), and keeps the accumulator in registers --- staging it in threadgroup
memory would have cost 8\,KB and halved residency. The backward pass needs none of
this, since the saved logsumexp already fixes the normalization.

A second technique worth noting: the $dK/dV$ kernel requires transposed score
matrices, which are obtained not by moving data but by exchanging the operand roles,
$S^{\!\top} = K Q^{\!\top}$ and $dP^{\!\top} = V\, dO^{\!\top}$, with
\texttt{simdgroup\_load(..., transpose=true)} supplying the transposed operands
directly from the staged tiles.

\section{Results}

\begin{table}[h]
\centering
\begin{tabular}{lrrr}
\toprule
Kernel & Naive & Tiled & \texttt{simdgroup\_matrix} \\
\midrule
$4096^3$                    & 1.29 & 2.53 & \phantom{0}9.49 \\
Forward MLP $XW_1^{\!\top}$ & 1.46 & 2.53 & 10.68 \\
Backward $dX = dY W$        & 1.51 & 2.54 & 10.58 \\
Backward $dW = dY^{\!\top} X$ & 0.76 & 2.15 & \phantom{0}4.69 \\
\bottomrule
\end{tabular}
\caption{GEMM throughput (TFLOP/s). The $dW$ case lags because $K = BT$ is very large
with few threadgroups; split-K would address it and is not implemented.}
\end{table}

End-to-end training throughput, all configurations on one M5 Max:

\begin{table}[h]
\centering
\begin{tabular}{lrrr}
\toprule
Configuration & Parameters & Context & Tokens/s \\
\midrule
gpt-10m  & 12.2M  & \phantom{0}512 & 38.2k \\
gpt-25m  & 29.9M  & 1024 & 22.1k \\
gpt-100m & 97.5M  & 1024 & \phantom{0}9.7k \\
gpt-200m & 205.5M & 1024 & \phantom{0}1.8k\rlap{$^\dagger$} \\
\bottomrule
\end{tabular}
\caption{$^\dagger$ measured before the matrix-tiled backward and concurrent
optimizer landed; this figure is pessimistic.}
\end{table}

\subsection{A memory bug found by scaling}

The 100M configuration exhausted GPU memory even at a micro-batch of eight. The cause
was an interaction between two individually reasonable decisions: pooled buffers
released while a command buffer is open are parked on a retire list until the next
synchronization, and the trainer synchronized once per \emph{optimizer} step. With
sixteen gradient-accumulation micro-batches, none of the sixteen sets of activations
were ever recycled, so peak memory scaled with the accumulation factor.
Synchronizing once per micro-batch fixed it. We note this because both decisions are
defensible in isolation and the failure appears only at scale.

\subsection{End-to-end training run}
\label{sec:epoch}

To confirm that the optimized kernels train a model and not merely a benchmark, we
trained the 12.2M-parameter configuration for exactly one epoch over the
19.14M-token TinyStories corpus: 584 steps of 64 sequences $\times$ 512 tokens.
Learning rate follows linear warmup over the first 10\% of the run into a cosine
decay to one tenth of peak; AdamW with decoupled weight decay 0.1 on parameters of
rank at least two, gradient clipping at global norm 1.0.

RESULTS_PLACEHOLDER

\section{Related work}

Our GEMM structure follows MLX's STEEL kernels, and our attention design follows
FlashAttention-2 as adapted to Apple GPUs by \emph{metal-flash-attention}. The
training-loop details --- decoupled weight decay with $\varepsilon$ outside the square
root, weight decay applied only to parameters of rank at least two, the gradient-clip
factor folded into the optimizer's gradient read, and the fused classifier that
writes the logit gradient in place --- follow \texttt{llm.c} and \texttt{nanoGPT}.

\section{Limitations}

All measurements come from a single M5 Max. The neural-accelerator result in
particular is generation-specific and we expect it not to transfer to the M3
generation. Mixed precision is not implemented: every kernel is \texttt{float}, so
the 4.9$\times$ accelerator path is measured but not yet exploited by the training
loop. Activation checkpointing is absent, which is what bounds model size. Split-K
for the $dW$ GEMM is not implemented. Finally, brief benchmarks on Apple Silicon can
execute in a reduced GPU performance state; long-running configurations should be
cross-checked against the performance-state trace.

\section{Conclusion}

The largest factors we encountered were not algorithmic. A twelvefold loss came from
a declaration keyword whose meaning differs between C++ and the Metal Shading
Language. A fifteenfold gain in the attention backward came from reducing per-thread
register pressure, measurable directly once shader sources were embedded in the
library. A fivefold opportunity sits behind a Metal 4 interface whose own header
documentation is stale.

The methodological lesson is narrower and firmer than any single number. Twice in
this work a confident inference from static artifacts --- the intermediate
representation in Section~\ref{sec:ir}, a published tile configuration in
Section~\ref{sec:registers} --- pointed the wrong way, and was corrected only by
measurement. On this platform the profiler is cheap, scriptable, and headless; the
intuitions are not reliable.

\section*{Availability}

\emph{Forge} comprises approximately 6{,}500 lines of C++20 and Metal. The
benchmarks reported here are \texttt{tests/bench\_matmul.cpp},
\texttt{tests/bench\_attention.cpp} and \texttt{tests/bench\_precision.cpp}; the
numerical verification of the cooperative-tensor path is \texttt{tests/mppcheck.cpp}.
Research notes with exact interface signatures and a pitfalls list are maintained in
\texttt{RESEARCH.md}.

\end{document}
