2 Commits

Author SHA1 Message Date
ywkang 24c705419e paper(gqa): two-regime "use of composite commands" — decode + compute-bound prefill
Complete the composite-command study across both attention regimes and
write up the result, resolving when the composite command helps latency.

Decode (memory-bound, T_q=1, M=8): the command form is latency-neutral —
the kernel is bound by KV streaming and the MAC array is near-idle, so the
composite's only benefit here is host-issue offload (O(n_tiles) -> O(1)
PE_CPU commands). Figure: gqa_decode_long_ctx_composite (latency flat,
command count saturates).

Prefill (compute-bound, large M=G*T_q tile-filling): add three command-form
variants of a single-rank FlashAttention prefill kernel
(_gqa_prefill_compute_bound: primitive / composite / composite_extended).
Here the composite's scheduler-internal per-tile DMA<->compute pipeline
keeps the MAC array fed while the primitive's blocking tl.dot starves it,
so composite wins on MAC utilization (68% flat -> 83%) and wall-clock
(19% faster at ctx=1024), with the margin growing with context (deeper
P.V reduction = more tiles to pipeline) — the compute-bound mirror of the
GEMM result in section 3. Figure: gqa_prefill_compute_bound (latency +
MAC util). Sweep wired as GQA_1H_SWEEPS=prefill_cb; tests cover structure,
e2e, and composite<primitive in the compute-bound regime.

The synthesis: composite has two benefits set by roofline position —
host-issue offload (always) and MAC-array feeding (compute-bound only).
Decode exercises the first, prefill both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:04:26 -07:00
ywkang cd6f0ed91d feat(gqa): Case-6 composite-command decode variants + on-chip-operand DMA fix
Add two command-form variants of the Case-6 (Cube-SP x PE-SP) long-context
decode kernel alongside the primitive baseline:
  - composite: per-tile GEMMs issued as one coarse tl.composite over the
    full S_local (PE_SCHEDULER tiles/streams K,V) -- O(1) PE_CPU commands
    vs the primitive kernel's O(n_tiles).
  - composite_extended: Q.K^T composite + softmax_merge recipe composite
    (ADR-0065), folding the per-tile online merge + P.V.

Extract the shared two-level (m,l,O) reduce into _gqa_mlo_reduce and
refactor the baseline to use it (byte-equal, guarded by a 64-rank
command-stream digest test).

Engine fix: _make_compute_out omitted pinned=True, so an on-chip TCM
compute result consumed as a composite GEMM operand was scheduled as an
HBM DMA_READ of its bit-61 scratch address -> PhysAddrError in data mode.
Mark compute outputs pinned (resident), matching the composite
auto-output and recipe scratch handles. .pinned is read only by the
tiling DMA gate, so this is byte-equal for existing benches.

Add the composite sweep (emit-level PE_CPU dispatch to 1M + data-mode
latency to 128K), its plot, umbrella wiring (GQA_1H_SWEEPS=composite),
and tests (refactor guard, dispatch saturation, engine-fix, e2e).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:07:41 -07:00
21 changed files with 1945 additions and 193 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

@@ -122,9 +122,10 @@ overlaps score computation; and per-tile \emph{scratch recycling} that
keeps the running softmax accumulators ($m,\ell,O$) in a persistent arena
while freeing per-tile temporaries, so the kernel fits the
\SI{1}{\mebi\byte} scratch budget across many tiles. A further refinement
that restructures the decode step into two stateful composites (a named
\textsf{softmax\_merge} recipe) is designed but not yet wired into the
measured path; results below reflect the implemented kernel only.
restructures the decode step so the per-tile matrix products and the
online-softmax merge are issued as \emph{composite} commands rather than
hand-tiled primitives; \S\ref{sec:gqa-composite} measures it against the
primitive baseline at long context.
% TODO: CUBE <-> KV-head mapping diagram for the short-context regime
% (h_kv=8 KV heads -> 8 CUBEs, 1:1; intra-CUBE PE usage).
@@ -221,6 +222,128 @@ memory-feasible family, and the cross-PE softmax reduction it does pay is
precisely the traffic the communication-side codesign of this report is
built to move quickly.
\subsection{Use of Composite Commands}
\label{sec:gqa-composite}
The decode kernel of \S\ref{sec:gqa-long} issues its local attention as
primitive operations: it walks each PE's $S_{\text{local}}$ token slice in
\SI{1024}{}-token tiles, and for every tile issues a $Q\!\cdot\!K^{\top}$
\textsf{dot}, the online-softmax primitives, and a $P\!\cdot\!V$
\textsf{dot}, merging the running $(m,\ell,O)$ state by hand. The number
of PE\_CPU commands this costs grows with the context. At a production
context of $S_{kv}{=}1\,\text{M}$ tokens the Case-6 64-way split gives
each PE $S_{\text{local}}{=}16384$ tokens, so its local attention is a
$Q\!\cdot\!K^{\top}$ of $(8,128)\!\cdot\!(128,16384)$ and a
$P\!\cdot\!V$ of $(8,16384)\!\cdot\!(16384,128)$---sixteen hand-issued
tiles, each a fresh batch of CPU commands.
The composite command lets the kernel hand that tiling to PE\_SCHEDULER.
We compare three command forms of the \emph{same} Case-6 kernel---identical
placement and identical $(m,\ell,O)$ reduce, differing only in how the
local attention is issued:
\begin{itemize}
\item \textbf{primitive}---the hand-tiled \textsf{dot}/softmax kernel
above (the baseline of \S\ref{sec:gqa-long}).
\item \textbf{composite}---each matrix product is one coarse
\textsf{composite} GEMM over the \emph{whole} $S_{\text{local}}$, with
$K$ and $V$ passed as HBM references so PE\_SCHEDULER streams and
tiles them on the fixed $32\!\times\!64\!\times\!32$ MAC tile; the
softmax stays primitive.
\item \textbf{composite\,+\,softmax\_merge}---additionally folds the
online-softmax merge and $P\!\cdot\!V$ into a single stateful
\textsf{softmax\_merge} recipe composite.
\end{itemize}
\begin{figure}[t]
\centering
\includegraphics[width=\linewidth]{gqa_decode_long_ctx_composite.png}
\caption{Three command forms of the Case-6 decode kernel, swept over
context length ($S_{\text{local}}{=}S_{kv}/64$ per PE). \emph{Right:}
PE\_CPU commands issued. The hand-tiled primitive kernel rises
$O(n_{\text{tiles}})$---from 96 commands at one tile to 426 at the
1\,M-token, sixteen-tile production point---while both composite forms
issue a context-\emph{independent} $O(1)$ count (94 and 98) that
\emph{saturates}: one coarse descriptor offloads the entire per-tile
fan-out. \emph{Left:} the consequence for wall-clock latency is none---all
three land on the same curve (\SI{30.6}{}, \SI{231}{},
\SI{461}{\micro\second} at 8\,K\,/\,64\,K\,/\,128\,K), because decode is
bound by streaming the KV cache, not by issue. Command-count is measured
at emit time (exact, to 1\,M); latency on the data-mode engine over the
tractable range.}
\label{fig:gqa-composite}
\end{figure}
Figure~\ref{fig:gqa-composite} reads off the two quantities that matter,
and they point in opposite directions. The PE\_CPU command count (right)
collapses from a context-growing $O(n_{\text{tiles}})$ to a flat $O(1)$:
at 1\,M tokens the composite form issues \num{94} commands against the
primitive kernel's \num{426}, a $4.5\times$ reduction
($4.2\times$ in modeled dispatch cost), and---crucially---that number no
longer grows with context. The wall-clock latency (left), by contrast,
is unchanged across all three forms: decode is bound by streaming the KV
cache out of HBM, so the command form does not move the critical path.
That juxtaposition is the point. The composite command is not a latency
optimization for this memory-bound decode; it is a \emph{CPU-issue}
optimization. Its value is removing the per-tile dispatch work that would
otherwise grow without bound as context grows, freeing PE\_CPU to run
ahead and keep the engines fed---which is exactly what lets the
data-movement cost analyzed next show through as the true bottleneck
rather than being masked by issue overhead. The \textsf{softmax\_merge}
recipe folds the online merge into the same descriptor; on this
memory-bound path its marginal cost over the plain GEMM composite is small
(98 vs.\ 94 commands), and like the plain composite it keeps the issued
count flat as context scales.
\paragraph{The compute-bound mirror: prefill.} Decode's verdict---command
form is latency-neutral---is a property of its regime, not of the
composite command. A decode step has $T_q{=}1$, so its score and context
products are skinny ($M{=}G\,T_q{=}8$): the MAC array is barely fed and
the kernel is bound by streaming the KV cache. Prefill is the opposite
corner. It processes a block of query positions at once, so $M{=}G\,T_q$
is large and tile-filling, the GEMMs carry real arithmetic intensity
($\sim$$M$ flops/byte, well above the roofline ridge), and the kernel is
\emph{compute-bound}. This is the regime the composite command was built
for (\S\ref{sec:gemm}): it streams the per-HW-tile
DMA$\rightleftarrows$compute pipeline so the MAC array stays fed, whereas
the primitive kernel's blocking \textsf{tl.dot} serializes each tile's
load and compute and starves the array between tiles. We run the same
three command forms on a single-rank compute-bound prefill (FlashAttention
$Q$-block $\times$ $S_{kv}$-tile, online softmax) and sweep the context
length (Figure~\ref{fig:gqa-prefill-cb}).
\begin{figure}[t]
\centering
\includegraphics[width=\linewidth]{gqa_prefill_compute_bound.png}
\caption{Compute-bound prefill, three command forms, swept over context
length ($M{=}8\,T_q$ tile-filling). \emph{Left:} end-to-end latency.
\emph{Right:} MAC utilization (achieved $\div$ the
\SI{8}{\tera\flop\per\second} per-PE peak). The hand-tiled primitive sits
flat at $\sim$\SI{68}{\percent}---its serial load$\to$dot path leaves the
MAC array idle between tiles regardless of context. The composite forms
climb with context (\SI{67}{}$\to$\SI{80}{\percent} plain,
\SI{67}{}$\to$\SI{83}{\percent} with the recipe) because a deeper $P\!\cdot
\!V$ reduction gives more HW tiles to pipeline, and they convert that into
wall-clock: at \num{1024} the recipe form is \SI{646.9}{} vs.\
\SI{794.1}{\micro\second} (\SI{19}{\percent} faster). The margin
\emph{grows} with context---the compute-bound mirror of the GEMM result of
\S\ref{sec:gemm}.}
\label{fig:gqa-prefill-cb}
\end{figure}
The two studies together state the composite command's value precisely. It
has two distinct benefits, and which one matters is set by the workload's
roofline position. The first is \emph{host-issue offload}: one macro
command in place of $O(n_{\text{tiles}})$ fine ones, which removes
PE\_CPU dispatch work and is regime-independent (it shows in the decode
command count). The second is \emph{MAC-array feeding}: the
scheduler-internal per-tile DMA$\rightleftarrows$compute pipeline, which
only converts to latency when the workload is compute-bound enough to have
a MAC array worth keeping busy (it shows in the prefill utilization). A
memory-bound decode exercises only the first; a compute-bound prefill
exercises both. The composite command is the single mechanism that
delivers each where it applies.
\subsection{Comprehensive Analysis}
\label{sec:gqa-analysis}
@@ -0,0 +1,146 @@
"""Comparative figure for the Case-6 composite-command decode study.
Reads sweep_decode_composite.json (emitted by milestone-1h-gqa, sweep
``composite``) and writes one two-panel PNG into the bench-output dir:
gqa_decode_long_ctx_composite.png
Left — end-to-end decode latency (µs) vs context length, per command
form (primitive / composite / composite_extended).
Right — PE_CPU command count vs context length: the hand-tiled
primitive kernel issues O(n_tiles) commands (rises with
context), while the coarse composite forms issue O(1) and
*saturate* — PE_SCHEDULER absorbs the per-tile fan-out.
The x-axis is the global context length S_kv; each PE owns
S_local = S_kv/(C·P=64) tokens, so at the 1M production point every PE
runs Q·Kᵀ of (G·T_q, d_head)·(d_head, 16384) and P·V of
(G·T_q, 16384)·(16384, d_head).
Run (after the bench):
GQA_1H_RUN=1 GQA_1H_SWEEPS=composite python -m kernbench.cli.main run \\
--bench milestone-1h-gqa --topology topology.yaml
python scripts/paper/paper_plot_gqa_decode_long_ctx_composite.py
"""
from __future__ import annotations
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
_REPO_ROOT = Path(__file__).resolve().parents[2]
_FIG_DIR = (
_REPO_ROOT / "src" / "kernbench" / "benches"
/ "1H_milestone_output" / "gqa" / "long_ctx"
)
_SWEEP_JSON = _FIG_DIR / "sweep_decode_composite.json"
_PAPER_FIG_DIR = (
_REPO_ROOT / "docs" / "report" / "1H-codesign-paper" / "figures"
)
_N_RANKS = 64 # C·P for the Case-6 64-way split.
# variant key → (display label, colour, marker)
_VARIANT_STYLE = {
"primitive": ("primitive (tl.dot, hand-tiled)", "#c0504d", "o"),
"composite": ("composite GEMM", "#3b6ea5", "s"),
"composite_extended": ("composite + softmax_merge", "#4f8a4f", "^"),
}
_ORDER = ("primitive", "composite", "composite_extended")
def _load() -> dict:
return json.loads(_SWEEP_JSON.read_text())
def _series(rows: list[dict], variant: str, key: str):
"""Sorted (S_kv, value) series for a variant, skipping null values
(latency is only measured over the tractable S_kv subset)."""
pts = sorted(
((r["S_kv"], r[key]) for r in rows
if r["variant"] == variant and r.get(key) is not None),
key=lambda t: t[0],
)
return [p[0] for p in pts], [p[1] for p in pts]
def _xticklabels(s_kvs: list[int]) -> list[str]:
out = []
for s in s_kvs:
if s >= 1 << 20:
out.append(f"{s // (1 << 20)}M")
else:
out.append(f"{s // 1024}K")
return out
def main() -> None:
sweep = _load()
rows = sweep["rows"]
s_kv_op = sweep["s_kv_opcount"]
s_kv_lat = sweep["s_kv_latency"]
fig, (ax_lat, ax_cmd) = plt.subplots(1, 2, figsize=(13.0, 4.8))
for v in _ORDER:
label, color, marker = _VARIANT_STYLE[v]
xs, lat = _series(rows, v, "latency_ns")
ax_lat.plot(xs, [y / 1e3 for y in lat], marker=marker,
color=color, label=label, lw=2)
xs, cmds = _series(rows, v, "pe_cpu_cmd_count")
ax_cmd.plot(xs, cmds, marker=marker, color=color, label=label, lw=2)
ax_lat.set_xticks(s_kv_lat)
ax_lat.set_xticklabels(_xticklabels(s_kv_lat))
ax_cmd.set_xticks(s_kv_op)
ax_cmd.set_xticklabels(_xticklabels(s_kv_op), fontsize=8)
for ax in (ax_lat, ax_cmd):
ax.set_xscale("log", base=2)
ax.set_xlabel(
r"context length $S_{kv}$ "
r"($S_{\mathrm{local}}=S_{kv}/64$ per PE)"
)
ax.grid(True, ls=":", alpha=0.5)
ax.legend(fontsize=9)
ax_lat.set_ylabel("end-to-end decode latency (µs)")
ax_lat.set_title(
"Case-6 decode latency per command form\n"
"(memory-bound: command form does not move the critical path)"
)
ax_cmd.set_ylabel("PE_CPU commands issued")
ax_cmd.set_title(
"PE_CPU command count per command form\n"
"(primitive O(n$_\\mathrm{tiles}$) rises; composite O(1) saturates)"
)
ax_cmd.axvline(1 << 20, color="#888", ls="--", lw=1, alpha=0.7)
ax_cmd.annotate("1M production\ncontext", xy=(1 << 20, 0),
xytext=(1 << 18, 0.6), fontsize=8,
textcoords=("data", "axes fraction"), ha="right",
color="#555")
fig.suptitle(
"Case-6 (Cube-SP × PE-SP) long-context decode — use of composite "
"commands\nLLaMA-3.1-70B single-KV-head group (8 cubes × 8 PEs), "
"$T_q{=}1$",
fontsize=11,
)
fig.tight_layout(rect=(0, 0, 1, 0.94))
out = _FIG_DIR / "gqa_decode_long_ctx_composite.png"
fig.savefig(out, dpi=150)
plt.close(fig)
print(f"wrote {out}")
# Mirror into the paper figures dir (derived artifact).
if _PAPER_FIG_DIR.is_dir():
dst = _PAPER_FIG_DIR / out.name
dst.write_bytes(out.read_bytes())
print(f"copied {dst}")
if __name__ == "__main__":
main()
@@ -0,0 +1,111 @@
"""Comparative figure for the compute-bound prefill composite study.
Reads sweep_prefill_compute_bound.json (emitted by milestone-1h-gqa,
sweep ``prefill_cb``) and writes one two-panel PNG:
gqa_prefill_compute_bound.png
Left — end-to-end prefill latency (µs) vs context length.
Right — MAC utilization (achieved / 8 TFLOP·s⁻¹ per-PE peak) vs context.
Unlike memory-bound decode (where command form is latency-neutral), in
compute-bound prefill the composite command keeps the MAC array fed by
streaming DMA↔compute per HW tile, so it wins on both latency and
utilization — and the margin grows with context (deeper P·V reduction =
more tiles to pipeline).
Run (after the bench):
GQA_1H_RUN=1 GQA_1H_SWEEPS=prefill_cb python -m kernbench.cli.main run \\
--bench milestone-1h-gqa --topology topology.yaml
python scripts/paper/paper_plot_gqa_prefill_compute_bound.py
"""
from __future__ import annotations
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
_REPO_ROOT = Path(__file__).resolve().parents[2]
_FIG_DIR = (
_REPO_ROOT / "src" / "kernbench" / "benches"
/ "1H_milestone_output" / "gqa" / "long_ctx"
)
_SWEEP_JSON = _FIG_DIR / "sweep_prefill_compute_bound.json"
_PAPER_FIG_DIR = (
_REPO_ROOT / "docs" / "report" / "1H-codesign-paper" / "figures"
)
_VARIANT_STYLE = {
"primitive": ("primitive (tl.dot, hand-tiled)", "#c0504d", "o"),
"composite": ("composite GEMM", "#3b6ea5", "s"),
"composite_extended": ("composite + softmax_merge", "#4f8a4f", "^"),
}
_ORDER = ("primitive", "composite", "composite_extended")
def _ctx_label(c: int) -> str:
return f"{c // 1024}K" if c >= 1024 else str(c)
def _series(rows, variant, key):
pts = sorted(((r["ctx_len"], r[key]) for r in rows
if r["variant"] == variant), key=lambda t: t[0])
return [p[0] for p in pts], [p[1] for p in pts]
def main() -> None:
sweep = json.loads(_SWEEP_JSON.read_text())
rows = sweep["rows"]
ctxs = sweep["ctx_points"]
fig, (ax_lat, ax_util) = plt.subplots(1, 2, figsize=(13.0, 4.8))
for v in _ORDER:
label, color, marker = _VARIANT_STYLE[v]
xs, lat = _series(rows, v, "latency_ns")
ax_lat.plot(xs, [y / 1e3 for y in lat], marker=marker,
color=color, label=label, lw=2)
xs, util = _series(rows, v, "mac_util")
ax_util.plot(xs, [u * 100 for u in util], marker=marker,
color=color, label=label, lw=2)
for ax in (ax_lat, ax_util):
ax.set_xscale("log", base=2)
ax.set_xticks(ctxs)
ax.set_xticklabels([_ctx_label(c) for c in ctxs])
ax.set_xlabel(r"context length (= $T_q$ = $S_{kv}$)")
ax.grid(True, ls=":", alpha=0.5)
ax.legend(fontsize=9)
ax_lat.set_ylabel("end-to-end prefill latency (µs)")
ax_lat.set_title("Compute-bound prefill latency per command form")
ax_util.set_ylabel("MAC utilization (% of 8 TFLOP·s⁻¹ peak)")
ax_util.set_title(
"MAC utilization — composite keeps the array fed; primitive starves"
)
ax_util.axhline(100, color="#888", ls="--", lw=1, alpha=0.6)
fig.suptitle(
"Compute-bound prefill attention — use of composite commands\n"
"single-rank, GQA single-KV-head group ($h_q{=}8$, $d_{\\text{head}}"
"{=}128$); $M{=}8T_q$ tile-filling",
fontsize=11,
)
fig.tight_layout(rect=(0, 0, 1, 0.92))
out = _FIG_DIR / "gqa_prefill_compute_bound.png"
fig.savefig(out, dpi=150)
plt.close(fig)
print(f"wrote {out}")
if _PAPER_FIG_DIR.is_dir():
dst = _PAPER_FIG_DIR / out.name
dst.write_bytes(out.read_bytes())
print(f"copied {dst}")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

@@ -0,0 +1,258 @@
{
"version": 2,
"variants": [
"primitive",
"composite",
"composite_extended"
],
"s_kv_opcount": [
8192,
65536,
131072,
262144,
524288,
1048576
],
"s_kv_latency": [
8192,
32768,
65536,
131072
],
"rows": [
{
"variant": "primitive",
"S_kv": 8192,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 96,
"pe_cpu_dispatch_cycles": 930,
"latency_ns": 30646.00300000079
},
{
"variant": "composite",
"S_kv": 8192,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": 30566.1840000007
},
{
"variant": "composite_extended",
"S_kv": 8192,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": 30483.359500000362
},
{
"variant": "primitive",
"S_kv": 65536,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 96,
"pe_cpu_dispatch_cycles": 930,
"latency_ns": 231579.37900000165
},
{
"variant": "composite",
"S_kv": 65536,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": 231270.18400000152
},
{
"variant": "composite_extended",
"S_kv": 65536,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": 231211.1740000015
},
{
"variant": "primitive",
"S_kv": 131072,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 118,
"pe_cpu_dispatch_cycles": 1145,
"latency_ns": 461119.51900000183
},
{
"variant": "composite",
"S_kv": 131072,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": 460651.3170000027
},
{
"variant": "composite_extended",
"S_kv": 131072,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": 460552.9805000025
},
{
"variant": "primitive",
"S_kv": 262144,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 162,
"pe_cpu_dispatch_cycles": 1575,
"latency_ns": null
},
{
"variant": "composite",
"S_kv": 262144,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": null
},
{
"variant": "composite_extended",
"S_kv": 262144,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": null
},
{
"variant": "primitive",
"S_kv": 524288,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 250,
"pe_cpu_dispatch_cycles": 2435,
"latency_ns": null
},
{
"variant": "composite",
"S_kv": 524288,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": null
},
{
"variant": "composite_extended",
"S_kv": 524288,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": null
},
{
"variant": "primitive",
"S_kv": 1048576,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 426,
"pe_cpu_dispatch_cycles": 4155,
"latency_ns": null
},
{
"variant": "composite",
"S_kv": 1048576,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 94,
"pe_cpu_dispatch_cycles": 978,
"latency_ns": null
},
{
"variant": "composite_extended",
"S_kv": 1048576,
"C": 8,
"P": 8,
"T_q": 1,
"d_head": 128,
"h_q": 8,
"h_kv": 1,
"pe_cpu_cmd_count": 98,
"pe_cpu_dispatch_cycles": 1032,
"latency_ns": null
}
]
}
@@ -0,0 +1,105 @@
{
"version": 1,
"variants": [
"primitive",
"composite",
"composite_extended"
],
"ctx_points": [
256,
512,
1024
],
"rows": [
{
"variant": "primitive",
"ctx_len": 256,
"M": 2048,
"latency_ns": 48857.361999999725,
"gemm_busy_ns": 33554.43200000003,
"dma_busy_ns": 20880.000000000004,
"achieved_tflops": 5.4942683151825005,
"mac_util": 0.6867835393978126
},
{
"variant": "composite",
"ctx_len": 256,
"M": 2048,
"latency_ns": 49887.505999999594,
"gemm_busy_ns": 34531.32799999314,
"dma_busy_ns": 1095773.440000072,
"achieved_tflops": 5.380815308746887,
"mac_util": 0.6726019135933609
},
{
"variant": "composite_extended",
"ctx_len": 256,
"M": 2048,
"latency_ns": 50280.91399999841,
"gemm_busy_ns": 129096.19199996963,
"dma_busy_ns": 633563.5200000411,
"achieved_tflops": 5.338714725830331,
"mac_util": 0.6673393407287914
},
{
"variant": "primitive",
"ctx_len": 512,
"M": 4096,
"latency_ns": 197496.81800000605,
"gemm_busy_ns": 134217.72800000012,
"dma_busy_ns": 66880.0,
"achieved_tflops": 5.436755056985105,
"mac_util": 0.6795943821231382
},
{
"variant": "composite",
"ctx_len": 512,
"M": 4096,
"latency_ns": 177341.23400000148,
"gemm_busy_ns": 141168.63999995415,
"dma_busy_ns": 8567063.039998509,
"achieved_tflops": 6.054665346469796,
"mac_util": 0.7568331683087245
},
{
"variant": "composite_extended",
"ctx_len": 512,
"M": 4096,
"latency_ns": 174702.7699999963,
"gemm_busy_ns": 1076198.3999996716,
"dma_busy_ns": 6594394.87999886,
"achieved_tflops": 6.146106464139193,
"mac_util": 0.7682633080173992
},
{
"variant": "primitive",
"ctx_len": 1024,
"M": 8192,
"latency_ns": 794090.4820000405,
"gemm_busy_ns": 536870.9120000004,
"dma_busy_ns": 234240.0,
"achieved_tflops": 5.4086623544239645,
"mac_util": 0.6760827943029956
},
{
"variant": "composite",
"ctx_len": 1024,
"M": 8192,
"latency_ns": 668110.7059999453,
"gemm_busy_ns": 873011.1999923651,
"dma_busy_ns": 67794150.3999821,
"achieved_tflops": 6.428526376585786,
"mac_util": 0.8035657970732233
},
{
"variant": "composite_extended",
"ctx_len": 1024,
"M": 8192,
"latency_ns": 646937.2019999702,
"gemm_busy_ns": 8870907.903995967,
"dma_busy_ns": 59655820.79998447,
"achieved_tflops": 6.638924586068552,
"mac_util": 0.829865573258569
}
]
}
@@ -36,26 +36,14 @@ Topology / SFR:
"""
from __future__ import annotations
from kernbench.benches.gqa_helpers.long_ctx._gqa_mlo_reduce import (
_ROOT_CUBE,
_merge_running,
reduce_mlo,
)
TILE_S_KV = 1024 # ADR-0063 §A.2 S_kv-axis tile sweep (per-tile width).
# lrab geometry for the C=8 single-KV-head group (4×2 cube sub-mesh).
_SUB_W = 4
_SUB_H = 2
_ROOT_COL = _SUB_W // 2 # 2
_ROOT_ROW = _SUB_H // 2 # 1
_ROOT_CUBE = _ROOT_ROW * _SUB_W + _ROOT_COL # 6
def _merge_running(m_local, l_local, O_local, m_other, l_other, O_other, *, tl):
"""Online-softmax merge of two partial ``(m, , O)`` triples."""
m_new = tl.maximum(m_local, m_other)
scale_old = tl.exp(m_local - m_new)
scale_new = tl.exp(m_other - m_new)
l_new = l_local * scale_old + l_other * scale_new
O_new = O_local * scale_old + O_other * scale_new
return m_new, l_new, O_new
def gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel(
q_ptr: int,
@@ -119,175 +107,8 @@ def gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel(
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
# ── Intra-CUBE reduce: row chain (intra_W) + col bridge (intra_N) ──
PE_GRID_COLS = 4
pe_col = pe_id % PE_GRID_COLS
pe_row = pe_id // PE_GRID_COLS
pe_cols_used = min(PE_GRID_COLS, P)
pe_rows_used = (P + PE_GRID_COLS - 1) // PE_GRID_COLS
if pe_cols_used > 1:
if pe_col < pe_cols_used - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="intra_E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="intra_E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="intra_E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if pe_col > 0:
tl.send(dir="intra_W", src=m_local)
tl.send(dir="intra_W", src=l_local)
tl.send(dir="intra_W", src=O_local)
if pe_col == 0 and pe_rows_used > 1:
if pe_row < pe_rows_used - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="intra_S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="intra_S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="intra_S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if pe_row > 0:
tl.send(dir="intra_N", src=m_local)
tl.send(dir="intra_N", src=l_local)
tl.send(dir="intra_N", src=O_local)
# ── Inter-CUBE lrab-adapted center-root reduce (ADR-0060 §4.2) ──
# Only PE 0 of each CUBE participates. Adapts Phases 1-2 of
# lrab_hierarchical_allreduce.py: bidirectional row reduce converges
# at root_col; bidirectional col reduce on root_col converges at
# root_row. Plain ``+`` replaced by log-sum-exp ``_merge_running``.
if pe_id == 0:
row = cube_id // _SUB_W
col = cube_id % _SUB_W
# Phase 1: row reduce — converge at col == _ROOT_COL.
if col == 0:
tl.send(dir="E", src=m_local)
tl.send(dir="E", src=l_local)
tl.send(dir="E", src=O_local)
elif 0 < col < _ROOT_COL:
with tl.scratch_scope():
m_other = tl.recv(dir="W", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="W", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="W", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="E", src=m_local)
tl.send(dir="E", src=l_local)
tl.send(dir="E", src=O_local)
elif col == _ROOT_COL:
with tl.scratch_scope():
m_other = tl.recv(dir="W", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="W", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="W", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
with tl.scratch_scope():
m_other = tl.recv(dir="E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
elif _ROOT_COL < col < _SUB_W - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="W", src=m_local)
tl.send(dir="W", src=l_local)
tl.send(dir="W", src=O_local)
elif col == _SUB_W - 1:
tl.send(dir="W", src=m_local)
tl.send(dir="W", src=l_local)
tl.send(dir="W", src=O_local)
# Phase 2: col reduce on col == _ROOT_COL — converge at row == _ROOT_ROW.
if col == _ROOT_COL:
if row == 0:
tl.send(dir="S", src=m_local)
tl.send(dir="S", src=l_local)
tl.send(dir="S", src=O_local)
elif 0 < row < _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="N", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="N", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="N", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="S", src=m_local)
tl.send(dir="S", src=l_local)
tl.send(dir="S", src=O_local)
elif row == _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="N", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="N", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="N", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if _SUB_H - 1 > _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
elif _ROOT_ROW < row < _SUB_H - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="N", src=m_local)
tl.send(dir="N", src=l_local)
tl.send(dir="N", src=O_local)
elif row == _SUB_H - 1 and _SUB_H - 1 > _ROOT_ROW:
tl.send(dir="N", src=m_local)
tl.send(dir="N", src=l_local)
tl.send(dir="N", src=O_local)
# ── Two-level (m, , O) reduce-to-root (shared helper) ──
reduce_mlo(pe_id, cube_id, m_local, l_local, O_local, P, tl=tl)
# ── Final normalise + store (root only: PE 0 of CUBE 6) ──
if pe_id == 0 and cube_id == _ROOT_CUBE:
@@ -0,0 +1,71 @@
"""GQA decode kernel — Case 6, **composite-GEMM** command form.
Identical placement and (m, , O) reduce as the primitive Case-6 kernel
(``_gqa_attention_decode_long_ctx_cube_sp_pe_sp``); the difference is the
command *granularity* of the local attention. The primitive kernel walks
its KV slice in ``TILE_S_KV``-wide tiles, issuing per-tile loads + GEMMs
+ a manual online-softmax merge — O(n_tiles) PE_CPU commands. This kernel
instead issues **one coarse composite GEMM over the whole ``S_local``**
for each matrix product, passing K and V as HBM refs so PE_SCHEDULER
streams and tiles them (the DMA fan-out moves off PE_CPU). The
online-softmax stays primitive but now runs once over the full score row.
So the kernel issues O(1) coarse commands; the scheduler expands each
composite into the same per-tile DMA + MAC work the primitive kernel
issued by hand. Fewer, coarser PE_CPU commands ⇒ lower dispatch cost
under the ADR-0064 Rev2 structural model (the CPU-offload win).
"""
from __future__ import annotations
from kernbench.benches.gqa_helpers.long_ctx._gqa_mlo_reduce import (
_ROOT_CUBE,
reduce_mlo,
)
def gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_kernel(
q_ptr: int,
k_ptr: int,
v_ptr: int,
o_ptr: int,
T_q: int,
S_kv: int,
h_q: int,
h_kv: int,
d_head: int,
C: int,
P: int,
*,
tl,
) -> None:
"""Case-6 decode (Cube-SP × PE-SP) — coarse composite-GEMM command form."""
G = h_q // h_kv
n_ranks = C * P
S_local = S_kv // n_ranks
pe_id = tl.program_id(axis=0)
cube_id = tl.program_id(axis=1)
# ── Local attention as two coarse composite GEMMs ──
# K, V are HBM refs: PE_SCHEDULER streams + tiles them over S_local
# (the per-tile DMA fan-out the primitive kernel issued by hand).
Q = tl.load(q_ptr, shape=(G * T_q, d_head), dtype="f16")
K_T = tl.ref(k_ptr, shape=(d_head, S_local), dtype="f16")
V = tl.ref(v_ptr, shape=(S_local, d_head), dtype="f16")
scores = tl.composite(op="gemm", a=Q, b=K_T) # Q·Kᵀ, one command
m_local = tl.max(scores, axis=-1)
centered = scores - m_local
exp_scores = tl.exp(centered)
l_local = tl.sum(exp_scores, axis=-1)
# P·V into a pre-allocated TCM output (explicit out — the composite
# output must be a real TCM handle, not auto-allocated scratch).
O_local = tl.zeros((G * T_q, d_head), dtype="f16")
tl.composite(op="gemm", a=exp_scores, b=V, out=O_local) # P·V, one command
# ── Two-level (m, , O) reduce-to-root (shared helper) ──
reduce_mlo(pe_id, cube_id, m_local, l_local, O_local, P, tl=tl)
# ── Final normalise + store (root only: PE 0 of CUBE 6) ──
if pe_id == 0 and cube_id == _ROOT_CUBE:
O_final = O_local / l_local
tl.store(o_ptr, O_final)
@@ -0,0 +1,94 @@
"""GQA decode kernel — Case 6, **composite_extended** command form.
Same placement and (m, , O) reduce as the primitive / composite-GEMM
Case-6 kernels; the local attention is the opt2 **two-composite** form
(ADR-0060 §8 item 4 / ADR-0065), issued coarsely:
establish a small first KV slice computes the running (m, , O) with
primitives (kernbench has no scratch-backed ``-inf``
initializer, so the recipe — which *merges* into an existing
accumulator — needs a seed).
#1 Q·Kᵀ one coarse composite GEMM over the remaining S_local
(K passed as an HBM ref → PE_SCHEDULER streams + tiles it).
#2 softmax → one ``softmax_merge`` recipe composite (online-softmax
+ P·V merge of (m, , O)) whose head GEMM is P·V over the same
remaining slice (V as an HBM ref), with an ``add``
epilogue folding the P·V contribution into ``O``.
So the bulk of the KV slice is two coarse PE_CPU commands, and the recipe
additionally folds the per-tile online merge + P·V into one descriptor —
the fewest / cheapest commands of the three forms (the headline
CPU-offload win, ADR-0064 Rev2).
Scope: runnable in op_log mode (latency / dispatch only). Full data-mode
numeric parity of the recipe's 8 MATH ops is a separate follow-up
(DDD-0065 / the P5-numerics note).
"""
from __future__ import annotations
from kernbench.benches.gqa_helpers.long_ctx._gqa_mlo_reduce import (
_ROOT_CUBE,
reduce_mlo,
)
# Small primitive seed slice that establishes the running (m, , O) the
# recipe then merges into. One scheduler K-tile wide (ADR-0064 TILE_K).
_SEED_S_KV = 64
def gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext_kernel(
q_ptr: int,
k_ptr: int,
v_ptr: int,
o_ptr: int,
T_q: int,
S_kv: int,
h_q: int,
h_kv: int,
d_head: int,
C: int,
P: int,
*,
tl,
) -> None:
"""Case-6 decode (Cube-SP × PE-SP) — softmax_merge recipe command form."""
G = h_q // h_kv
n_ranks = C * P
S_local = S_kv // n_ranks
pe_id = tl.program_id(axis=0)
cube_id = tl.program_id(axis=1)
KV_ROW_BYTES = d_head * 2 # f16
# ── Establish running (m, , O) on a small seed slice (primitive) ──
Q = tl.load(q_ptr, shape=(G * T_q, d_head), dtype="f16")
seed = min(_SEED_S_KV, S_local)
K_T0 = tl.load(k_ptr, shape=(d_head, seed), dtype="f16")
V0 = tl.load(v_ptr, shape=(seed, d_head), dtype="f16")
scores0 = tl.dot(Q, K_T0)
m_local = tl.max(scores0, axis=-1)
exp0 = tl.exp(scores0 - m_local)
l_local = tl.sum(exp0, axis=-1)
O_local = tl.dot(exp0, V0)
# ── Remaining slice: one Q·Kᵀ composite + one softmax_merge recipe ──
rest = S_local - seed
if rest > 0:
K_T1 = tl.ref(k_ptr + seed * KV_ROW_BYTES,
shape=(d_head, rest), dtype="f16")
V1 = tl.ref(v_ptr + seed * KV_ROW_BYTES,
shape=(rest, d_head), dtype="f16")
scores1 = tl.composite(op="gemm", a=Q, b=K_T1)
tl.composite(
prologue=[{"op": "softmax_merge", "s": scores1,
"m": m_local, "l": l_local, "O": O_local}],
op="gemm", b=V1, out=O_local,
epilogue=[{"op": "add", "other": O_local}],
)
# ── Two-level (m, , O) reduce-to-root (shared helper) ──
reduce_mlo(pe_id, cube_id, m_local, l_local, O_local, P, tl=tl)
# ── Final normalise + store (root only: PE 0 of CUBE 6) ──
if pe_id == 0 and cube_id == _ROOT_CUBE:
O_final = O_local / l_local
tl.store(o_ptr, O_final)
@@ -0,0 +1,214 @@
"""Shared (m, , O) reduce for the Case-6 long-context decode kernels.
Extracted from the Cube-SP × PE-SP decode kernel so the three
command-form variants (primitive / composite / composite_extended)
share one identical reduce. The local attention differs per variant;
the reduce does not. Behavior is byte-equal to the pre-extraction inline
reduce (guarded by tests/attention/test_gqa_decode_long_ctx_composite.py).
The reduce is two-level:
• intra-CUBE 8-way (row chain along intra_W + col bridge along
intra_N) over the 2×4 PE grid;
• inter-CUBE 2-phase lrab over the 4×2 CUBE sub-mesh (ADR-0060 §4.2
lrab-adapted center-root reduce): Phase 1 row reduce converges at
root_col=2; Phase 2 col reduce on root_col converges at root_row=1;
root cube id = 6.
Plain ``+`` is replaced by log-sum-exp ``_merge_running``.
"""
from __future__ import annotations
# lrab geometry for the C=8 single-KV-head group (4×2 cube sub-mesh).
_SUB_W = 4
_SUB_H = 2
_ROOT_COL = _SUB_W // 2 # 2
_ROOT_ROW = _SUB_H // 2 # 1
_ROOT_CUBE = _ROOT_ROW * _SUB_W + _ROOT_COL # 6
PE_GRID_COLS = 4
def _merge_running(m_local, l_local, O_local, m_other, l_other, O_other, *, tl):
"""Online-softmax merge of two partial ``(m, , O)`` triples."""
m_new = tl.maximum(m_local, m_other)
scale_old = tl.exp(m_local - m_new)
scale_new = tl.exp(m_other - m_new)
l_new = l_local * scale_old + l_other * scale_new
O_new = O_local * scale_old + O_other * scale_new
return m_new, l_new, O_new
def reduce_mlo(pe_id, cube_id, m_local, l_local, O_local, P, *, tl):
"""Two-level (m, , O) reduce-to-root (PE 0 of CUBE 6). In place.
Updates ``m_local``/``l_local``/``O_local`` in place via ``copy_to``;
after this call only PE 0 of CUBE ``_ROOT_CUBE`` holds the full result.
"""
# ── Intra-CUBE reduce: row chain (intra_W) + col bridge (intra_N) ──
pe_col = pe_id % PE_GRID_COLS
pe_row = pe_id // PE_GRID_COLS
pe_cols_used = min(PE_GRID_COLS, P)
pe_rows_used = (P + PE_GRID_COLS - 1) // PE_GRID_COLS
if pe_cols_used > 1:
if pe_col < pe_cols_used - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="intra_E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="intra_E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="intra_E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if pe_col > 0:
tl.send(dir="intra_W", src=m_local)
tl.send(dir="intra_W", src=l_local)
tl.send(dir="intra_W", src=O_local)
if pe_col == 0 and pe_rows_used > 1:
if pe_row < pe_rows_used - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="intra_S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="intra_S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="intra_S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if pe_row > 0:
tl.send(dir="intra_N", src=m_local)
tl.send(dir="intra_N", src=l_local)
tl.send(dir="intra_N", src=O_local)
# ── Inter-CUBE lrab-adapted center-root reduce (ADR-0060 §4.2) ──
# Only PE 0 of each CUBE participates. Adapts Phases 1-2 of
# lrab_hierarchical_allreduce.py: bidirectional row reduce converges
# at root_col; bidirectional col reduce on root_col converges at
# root_row. Plain ``+`` replaced by log-sum-exp ``_merge_running``.
if pe_id == 0:
row = cube_id // _SUB_W
col = cube_id % _SUB_W
# Phase 1: row reduce — converge at col == _ROOT_COL.
if col == 0:
tl.send(dir="E", src=m_local)
tl.send(dir="E", src=l_local)
tl.send(dir="E", src=O_local)
elif 0 < col < _ROOT_COL:
with tl.scratch_scope():
m_other = tl.recv(dir="W", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="W", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="W", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="E", src=m_local)
tl.send(dir="E", src=l_local)
tl.send(dir="E", src=O_local)
elif col == _ROOT_COL:
with tl.scratch_scope():
m_other = tl.recv(dir="W", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="W", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="W", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
with tl.scratch_scope():
m_other = tl.recv(dir="E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
elif _ROOT_COL < col < _SUB_W - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="E", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="E", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="E", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="W", src=m_local)
tl.send(dir="W", src=l_local)
tl.send(dir="W", src=O_local)
elif col == _SUB_W - 1:
tl.send(dir="W", src=m_local)
tl.send(dir="W", src=l_local)
tl.send(dir="W", src=O_local)
# Phase 2: col reduce on col == _ROOT_COL — converge at row == _ROOT_ROW.
if col == _ROOT_COL:
if row == 0:
tl.send(dir="S", src=m_local)
tl.send(dir="S", src=l_local)
tl.send(dir="S", src=O_local)
elif 0 < row < _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="N", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="N", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="N", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="S", src=m_local)
tl.send(dir="S", src=l_local)
tl.send(dir="S", src=O_local)
elif row == _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="N", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="N", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="N", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
if _SUB_H - 1 > _ROOT_ROW:
with tl.scratch_scope():
m_other = tl.recv(dir="S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
elif _ROOT_ROW < row < _SUB_H - 1:
with tl.scratch_scope():
m_other = tl.recv(dir="S", shape=m_local.shape, dtype="f16")
l_other = tl.recv(dir="S", shape=l_local.shape, dtype="f16")
O_other = tl.recv(dir="S", shape=O_local.shape, dtype="f16")
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_other, l_other, O_other, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
tl.send(dir="N", src=m_local)
tl.send(dir="N", src=l_local)
tl.send(dir="N", src=O_local)
elif row == _SUB_H - 1 and _SUB_H - 1 > _ROOT_ROW:
tl.send(dir="N", src=m_local)
tl.send(dir="N", src=l_local)
tl.send(dir="N", src=O_local)
@@ -0,0 +1,161 @@
"""Compute-bound prefill attention — 3 command-form variants (single-rank).
Companion to the memory-bound decode study
(``_gqa_attention_decode_long_ctx_cube_sp_pe_sp*``). Prefill processes a
block of T_q query positions at once, so the score / context GEMMs have a
large M = G·T_q and high arithmetic intensity (~M flops/byte) — the
workload is **compute-bound** (above the roofline ridge), unlike T_q=1
decode (M=8, memory-bound). This is the regime where the composite
command's value shows: it streams DMA↔compute per HW tile to keep the MAC
array fed, while the primitive kernel serializes load→dot and starves it.
Single-rank (C=P=1): no cross-device reduce — the focus is the per-PE
GEMM-issue mechanism. FlashAttention 2-D tiling (Q-block × S_kv-tile,
online softmax) bounds the TCM scratch.
Three forms, differing only in how each Q-block's local attention is
issued (placement/softmax identical):
primitive tl.dot per S_kv-tile + primitive online merge.
composite one coarse composite GEMM per Q-block over the whole
S_kv (K, V as HBM refs → PE_SCHEDULER tiles/streams).
composite_extended Q·Kᵀ composite + softmax_merge recipe composite.
"""
from __future__ import annotations
from kernbench.benches.gqa_helpers.long_ctx._gqa_mlo_reduce import _merge_running
M_BLOCK = 128 # query rows per FlashAttention Q-block (tile-filling).
TILE_S_KV = 256 # primitive per-tile S_kv width.
_SEED_S_KV = 64 # composite_extended recipe seed slice.
def _qblock_bounds(T_q: int, h_q: int, h_kv: int):
G = h_q // h_kv
M_total = G * T_q
n_qblocks = (M_total + M_BLOCK - 1) // M_BLOCK
return G, M_total, n_qblocks
# ── primitive: hand-tiled monolithic dots + online merge ─────────────
def gqa_prefill_primitive_kernel(
q_ptr, k_ptr, v_ptr, o_ptr,
T_q, S_kv, h_q, h_kv, d_head, C, P, *, tl,
) -> None:
G, M_total, n_qblocks = _qblock_bounds(T_q, h_q, h_kv)
ROW = d_head * 2 # f16
n_tiles = (S_kv + TILE_S_KV - 1) // TILE_S_KV
for qb in range(n_qblocks):
# Per-Q-block scratch_scope: this block's running (m,,O) plus its
# tile-0 transients are freed before the next block, so scratch
# stays O(one block) rather than accumulating across all blocks.
with tl.scratch_scope():
m_blk = min(M_BLOCK, M_total - qb * M_BLOCK)
q_off = qb * M_BLOCK * ROW
Q = tl.load(q_ptr + q_off, shape=(m_blk, d_head), dtype="f16")
tile_s0 = min(TILE_S_KV, S_kv)
K_T = tl.load(k_ptr, shape=(d_head, tile_s0), dtype="f16")
V = tl.load(v_ptr, shape=(tile_s0, d_head), dtype="f16")
scores = tl.dot(Q, K_T)
m_local = tl.max(scores, axis=-1)
exp_s = tl.exp(scores - m_local)
l_local = tl.sum(exp_s, axis=-1)
O_local = tl.dot(exp_s, V)
for ti in range(1, n_tiles):
start = ti * TILE_S_KV
tile_s = min(TILE_S_KV, S_kv - start)
with tl.scratch_scope():
K_T_t = tl.load(k_ptr + start * ROW,
shape=(d_head, tile_s), dtype="f16")
V_t = tl.load(v_ptr + start * ROW,
shape=(tile_s, d_head), dtype="f16")
scores_t = tl.dot(Q, K_T_t)
m_tile = tl.max(scores_t, axis=-1)
exp_t = tl.exp(scores_t - m_tile)
l_tile = tl.sum(exp_t, axis=-1)
O_tile = tl.dot(exp_t, V_t)
m_new, l_new, O_new = _merge_running(
m_local, l_local, O_local, m_tile, l_tile, O_tile, tl=tl,
)
tl.copy_to(m_local, m_new)
tl.copy_to(l_local, l_new)
tl.copy_to(O_local, O_new)
O_final = O_local / l_local
tl.store(o_ptr + q_off, O_final)
# ── composite: one coarse composite GEMM per Q-block ─────────────────
def gqa_prefill_composite_kernel(
q_ptr, k_ptr, v_ptr, o_ptr,
T_q, S_kv, h_q, h_kv, d_head, C, P, *, tl,
) -> None:
G, M_total, n_qblocks = _qblock_bounds(T_q, h_q, h_kv)
ROW = d_head * 2
for qb in range(n_qblocks):
with tl.scratch_scope():
m_blk = min(M_BLOCK, M_total - qb * M_BLOCK)
q_off = qb * M_BLOCK * ROW
Q = tl.load(q_ptr + q_off, shape=(m_blk, d_head), dtype="f16")
K_T = tl.ref(k_ptr, shape=(d_head, S_kv), dtype="f16")
V = tl.ref(v_ptr, shape=(S_kv, d_head), dtype="f16")
scores = tl.composite(op="gemm", a=Q, b=K_T) # Q·Kᵀ, one command
m_local = tl.max(scores, axis=-1)
exp_s = tl.exp(scores - m_local)
l_local = tl.sum(exp_s, axis=-1)
O_local = tl.zeros((m_blk, d_head), dtype="f16")
tl.composite(op="gemm", a=exp_s, b=V, out=O_local) # P·V, one command
O_final = O_local / l_local
tl.store(o_ptr + q_off, O_final)
# ── composite_extended: Q·Kᵀ composite + softmax_merge recipe ────────
def gqa_prefill_composite_ext_kernel(
q_ptr, k_ptr, v_ptr, o_ptr,
T_q, S_kv, h_q, h_kv, d_head, C, P, *, tl,
) -> None:
G, M_total, n_qblocks = _qblock_bounds(T_q, h_q, h_kv)
ROW = d_head * 2
for qb in range(n_qblocks):
with tl.scratch_scope():
m_blk = min(M_BLOCK, M_total - qb * M_BLOCK)
q_off = qb * M_BLOCK * ROW
Q = tl.load(q_ptr + q_off, shape=(m_blk, d_head), dtype="f16")
seed = min(_SEED_S_KV, S_kv)
K_T0 = tl.load(k_ptr, shape=(d_head, seed), dtype="f16")
V0 = tl.load(v_ptr, shape=(seed, d_head), dtype="f16")
scores0 = tl.dot(Q, K_T0)
m_local = tl.max(scores0, axis=-1)
exp0 = tl.exp(scores0 - m_local)
l_local = tl.sum(exp0, axis=-1)
O_local = tl.dot(exp0, V0)
rest = S_kv - seed
if rest > 0:
K_T1 = tl.ref(k_ptr + seed * ROW,
shape=(d_head, rest), dtype="f16")
V1 = tl.ref(v_ptr + seed * ROW,
shape=(rest, d_head), dtype="f16")
scores1 = tl.composite(op="gemm", a=Q, b=K_T1)
tl.composite(
prologue=[{"op": "softmax_merge", "s": scores1,
"m": m_local, "l": l_local, "O": O_local}],
op="gemm", b=V1, out=O_local,
epilogue=[{"op": "add", "other": O_local}],
)
O_final = O_local / l_local
tl.store(o_ptr + q_off, O_final)
@@ -0,0 +1,182 @@
"""milestone-1h-gqa decode: Case-6 composite-command study.
Three command-form variants of the Case-6 (Cube-SP × PE-SP) long-context
decode kernel, swept over S_kv so the local-attention tile loop runs
multiple tiles (S_local = S_kv/(C·P) > TILE_S_KV = 1024):
primitive tl.dot + primitive online-softmax merge (baseline).
composite per-tile GEMMs as tl.composite GEMM commands.
composite_extended per-tile attention as a Q·Kᵀ composite + a
softmax_merge recipe composite (ADR-0065).
The placement, DP policy, and (m, , O) reduce are identical across
variants; only the per-tile command form differs. The sweep writes
per-(variant, S_kv) latency, engine occupancy, and PE_CPU command count
to sweep_decode_composite.json so the comparative plot
(paper_plot_gqa_decode_long_ctx_composite.py) can read off the
CPU-offload win.
Run in op_log mode (enable_data=False): latency / dispatch only — recipe
data-mode numeric parity is a separate follow-up (DDD-0065).
"""
from __future__ import annotations
import json
from pathlib import Path
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp import ( # noqa: E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel,
)
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite import ( # noqa: E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_kernel,
)
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext import ( # noqa: E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext_kernel,
)
from kernbench.benches.gqa_helpers.shared._gqa_panel_helpers import _ccl_cfg
from kernbench.ccl.sfr_config import configure_sfr_intercube_multisip
from kernbench.policy.placement.dp import DPPolicy
_OUTPUT_DIR = (
Path(__file__).resolve().parents[2]
/ "1H_milestone_output" / "gqa" / "long_ctx"
)
_SWEEP_JSON = _OUTPUT_DIR / "sweep_decode_composite.json"
# ── Variant + S_kv registry ──────────────────────────────────────────
# Each kernel implements the same Case-6 placement; only the per-tile
# command form differs (see module docstring).
_VARIANT_KERNELS = {
"primitive": gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel,
"composite": gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_kernel,
"composite_extended":
gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext_kernel,
}
_VARIANTS = ("primitive", "composite", "composite_extended")
# Op-count (PE_CPU dispatch) is computed at emit time (exact, instant), so
# it spans up to the 1M production point (S_local = 1M/64 = 16384, 16
# tiles): the per-PE work is Q·Kᵀ (8,128)·(128,16384) and P·V
# (8,16384)·(16384,128). End-to-end latency needs the data-mode engine,
# whose cost scales with S_kv, so it is swept only over a tractable range.
_S_KV_OPCOUNT = (8192, 65_536, 131_072, 262_144, 524_288, 1_048_576)
_S_KV_LATENCY = (8192, 32_768, 65_536, 131_072)
# LLaMA-3.1-70B single-KV-head group (8 cubes × 8 PEs), one decode step.
_BASE_PARAMS = dict(C=8, P=8, T_q=1, d_head=128, h_q=8, h_kv=1)
# ── Per-(variant, S_kv) runner ───────────────────────────────────────
def _run_panel_fn(variant: str, S_kv: int):
kernel = _VARIANT_KERNELS[variant]
p = _BASE_PARAMS
panel = f"decode_long_ctx_composite_{variant}_s{S_kv}"
def _bench_fn(ctx):
configure_sfr_intercube_multisip(ctx.engine, ctx.spec, _ccl_cfg())
dp_full = DPPolicy(cube="replicate", pe="replicate",
num_cubes=p["C"], num_pes=p["P"])
dp_kv = DPPolicy(cube="row_wise", pe="row_wise",
num_cubes=p["C"], num_pes=p["P"])
q = ctx.zeros((p["T_q"], p["h_q"] * p["d_head"]),
dtype="f16", dp=dp_full, name=f"{panel}_q")
k = ctx.zeros((S_kv, p["h_kv"] * p["d_head"]),
dtype="f16", dp=dp_kv, name=f"{panel}_k")
v = ctx.zeros((S_kv, p["h_kv"] * p["d_head"]),
dtype="f16", dp=dp_kv, name=f"{panel}_v")
o = ctx.empty((p["T_q"], p["h_q"] * p["d_head"]),
dtype="f16", dp=dp_full, name=f"{panel}_o")
ctx.launch(panel, kernel, q, k, v, o,
p["T_q"], S_kv, p["h_q"], p["h_kv"], p["d_head"],
p["C"], p["P"], _auto_dim_remap=False)
return _bench_fn
def _end_to_end_ns(op_log) -> float:
if not op_log:
return 0.0
return max(r.t_end for r in op_log) - min(r.t_start for r in op_log)
def _emit_dispatch(variant: str, S_kv: int) -> tuple[int, float]:
"""PE_CPU dispatch (# commands, summed cycles) the kernel emits at the
lrab center rank (cube 6, pe 0) — computed at command-emit time, so it
is exact and S_kv-independent for the composite forms (no engine)."""
from kernbench.common.pe_commands import PeCpuOverheadCmd
from kernbench.common.pe_cost_model import DEFAULT_PE_COST_MODEL
from kernbench.triton_emu.tl_context import TLContext, run_kernel
p = _BASE_PARAMS
tl = TLContext(
pe_id=0, num_programs=p["P"], cost_model=DEFAULT_PE_COST_MODEL,
cube_id=6, num_cubes=p["C"], scratch_base=1 << 61, scratch_size=1 << 20,
)
run_kernel(
_VARIANT_KERNELS[variant], tl,
0x1000, 0x2000, 0x3000, 0x4000,
p["T_q"], S_kv, p["h_q"], p["h_kv"], p["d_head"], p["C"], p["P"],
)
cmds = [c for c in tl.commands if isinstance(c, PeCpuOverheadCmd)]
return len(cmds), sum(c.cycles for c in cmds)
def _engine_latency_ns(variant: str, S_kv: int, topology: str) -> float:
from kernbench.runtime_api.bench_runner import run_bench
from kernbench.runtime_api.types import resolve_device
from kernbench.sim_engine.engine import GraphEngine
from kernbench.topology.builder import resolve_topology
topo = resolve_topology(topology)
result = run_bench(
topology=topo, bench_fn=_run_panel_fn(variant, S_kv),
device=resolve_device(None),
engine_factory=lambda t, d: GraphEngine(
getattr(t, "topology_obj", t), enable_data=True,
),
)
if not result.completion.ok:
raise RuntimeError(
f"gqa-decode-composite {variant}@{S_kv} failed: {result.completion}"
)
return _end_to_end_ns(result.engine.op_log)
# ── Sweep entry (called by the umbrella milestone_1h_gqa) ────────────
def run_sweep(topology: str = "topology.yaml") -> int:
"""Emit-level dispatch over the full S_kv range (to 1M) + engine
latency over the tractable range; write sweep.json. Returns row count."""
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
rows = []
for S_kv in _S_KV_OPCOUNT:
for variant in _VARIANTS:
n_cmds, cycles = _emit_dispatch(variant, S_kv)
latency = (
_engine_latency_ns(variant, S_kv, topology)
if S_kv in _S_KV_LATENCY else None
)
rows.append({
"variant": variant,
"S_kv": S_kv,
**_BASE_PARAMS,
"pe_cpu_cmd_count": n_cmds,
"pe_cpu_dispatch_cycles": cycles,
"latency_ns": latency,
})
sweep = {
"version": 2,
"variants": list(_VARIANTS),
"s_kv_opcount": list(_S_KV_OPCOUNT),
"s_kv_latency": list(_S_KV_LATENCY),
"rows": rows,
}
_SWEEP_JSON.write_text(json.dumps(sweep, indent=2))
print(f" gqa-decode-composite: {len(rows)} rows -> {_SWEEP_JSON}")
return len(rows)
@@ -0,0 +1,142 @@
"""milestone-1h-gqa: compute-bound prefill composite-command study.
Three command-form variants of a single-rank compute-bound prefill
attention kernel (``_gqa_prefill_compute_bound``), swept over context
length S_kv = T_q. Unlike the memory-bound decode study, prefill has a
large M = G·T_q, so the score / context GEMMs are compute-bound — the
regime where the composite command keeps the MAC array fed (DMA↔compute
pipelining) and the hand-tiled primitive starves it on the serial
load→dot path.
Records per (variant, context) the end-to-end latency, the GEMM-engine
busy time, and the MAC occupancy (gemm_busy / e2e) so the comparative
plot can show composite winning on both latency and utilization.
Runs in data mode (engine latency). Gated via the umbrella
``GQA_1H_SWEEPS=prefill_cb``.
"""
from __future__ import annotations
import json
from pathlib import Path
from kernbench.benches.gqa_helpers.long_ctx._gqa_prefill_compute_bound import (
gqa_prefill_composite_ext_kernel,
gqa_prefill_composite_kernel,
gqa_prefill_primitive_kernel,
)
from kernbench.policy.placement.dp import DPPolicy
_OUTPUT_DIR = (
Path(__file__).resolve().parents[2]
/ "1H_milestone_output" / "gqa" / "long_ctx"
)
_SWEEP_JSON = _OUTPUT_DIR / "sweep_prefill_compute_bound.json"
_VARIANT_KERNELS = {
"primitive": gqa_prefill_primitive_kernel,
"composite": gqa_prefill_composite_kernel,
"composite_extended": gqa_prefill_composite_ext_kernel,
}
_VARIANTS = ("primitive", "composite", "composite_extended")
# Context length S_kv = T_q (prefill processes T_q tokens against S_kv=T_q
# keys). M = G·T_q = 8·T_q is tile-filling/compute-bound at every point.
_CTX_POINTS = (256, 512, 1024)
_H_Q, _H_KV, _D_HEAD = 8, 1, 128
_PEAK_TFLOPS = 8.0 # per-PE f16 GEMM peak (topology.yaml pe_gemm.peak_tflops_f16)
def _run_panel_fn(variant: str, ctx_len: int):
kernel = _VARIANT_KERNELS[variant]
panel = f"prefill_cb_{variant}_c{ctx_len}"
def _bench_fn(ctx):
dp = DPPolicy(cube="replicate", pe="replicate",
num_cubes=1, num_pes=1)
q = ctx.zeros((ctx_len, _H_Q * _D_HEAD),
dtype="f16", dp=dp, name=f"{panel}_q")
k = ctx.zeros((ctx_len, _H_KV * _D_HEAD),
dtype="f16", dp=dp, name=f"{panel}_k")
v = ctx.zeros((ctx_len, _H_KV * _D_HEAD),
dtype="f16", dp=dp, name=f"{panel}_v")
o = ctx.empty((ctx_len, _H_Q * _D_HEAD),
dtype="f16", dp=dp, name=f"{panel}_o")
ctx.launch(panel, kernel, q, k, v, o,
ctx_len, ctx_len, _H_Q, _H_KV, _D_HEAD, 1, 1,
_auto_dim_remap=False)
return _bench_fn
def _end_to_end_ns(op_log) -> float:
if not op_log:
return 0.0
return max(r.t_end for r in op_log) - min(r.t_start for r in op_log)
def _engine_busy_ns(op_log, suffix: str) -> float:
return sum(r.t_end - r.t_start
for r in op_log if r.component_id.endswith("." + suffix))
def _run_panel(variant: str, ctx_len: int, topology: str) -> dict:
from kernbench.runtime_api.bench_runner import run_bench
from kernbench.runtime_api.types import resolve_device
from kernbench.sim_engine.engine import GraphEngine
from kernbench.topology.builder import resolve_topology
topo = resolve_topology(topology)
result = run_bench(
topology=topo, bench_fn=_run_panel_fn(variant, ctx_len),
device=resolve_device(None),
engine_factory=lambda t, d: GraphEngine(
getattr(t, "topology_obj", t), enable_data=True,
),
)
if not result.completion.ok:
raise RuntimeError(
f"gqa-prefill-cb {variant}@{ctx_len} failed: {result.completion}"
)
op_log = result.engine.op_log
e2e = _end_to_end_ns(op_log)
gemm = _engine_busy_ns(op_log, "pe_gemm")
dma = _engine_busy_ns(op_log, "pe_dma")
# Useful attention flops (Q·Kᵀ + P·V), single rank.
G = _H_Q // _H_KV
M = G * ctx_len
useful_flops = 4.0 * M * _D_HEAD * ctx_len
# MAC utilization = achieved / peak. ``achieved_tflops`` uses wall-clock
# (useful_flops / e2e) so it is bounded by peak even when the composite
# path overlaps many tile GEMMs (gemm_busy is a sum over overlapping ops
# and is kept only for diagnostics).
return {
"variant": variant,
"ctx_len": ctx_len,
"M": M,
"latency_ns": e2e,
"gemm_busy_ns": gemm,
"dma_busy_ns": dma,
"achieved_tflops": (useful_flops / e2e / 1e3) if e2e > 0 else 0.0,
"mac_util": (useful_flops / e2e / 1e3 / _PEAK_TFLOPS) if e2e > 0 else 0.0,
}
def run_sweep(topology: str = "topology.yaml") -> int:
"""Drive all (variant, context) prefill panels; write sweep.json."""
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
rows = [
_run_panel(variant, ctx_len, topology)
for ctx_len in _CTX_POINTS
for variant in _VARIANTS
]
sweep = {
"version": 1,
"variants": list(_VARIANTS),
"ctx_points": list(_CTX_POINTS),
"rows": rows,
}
_SWEEP_JSON.write_text(json.dumps(sweep, indent=2))
print(f" gqa-prefill-cb: {len(rows)} rows -> {_SWEEP_JSON}")
return len(rows)
+13 -4
View File
@@ -8,12 +8,13 @@ Currently exercises (long-context only — short-context panels are
future work):
- 4-cases prefill comparative study (gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases)
- 4-cases decode comparative study (gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases)
- Case-6 composite-command study (gqa_helpers.long_ctx.gqa_decode_long_ctx_composite)
Each sub-sweep writes its own ``sweep_{prefill,decode}.json`` into the
shared output dir ``benches/1H_milestone_output/gqa/gqa_long_ctx/``.
Each sub-sweep writes its own ``sweep_*.json`` into the shared output
dir ``benches/1H_milestone_output/gqa/gqa_long_ctx/``.
Selection via the env var ``GQA_1H_SWEEPS=prefill,decode`` (default
runs both). Toggle individual sweeps with ``GQA_1H_SWEEPS=prefill``
or ``GQA_1H_SWEEPS=decode``.
runs prefill+decode). The composite study is opt-in (it sweeps the
data-mode engine over several S_kv points): ``GQA_1H_SWEEPS=composite``.
Gated by ``GQA_1H_RUN=1`` to keep CI fast.
"""
@@ -24,6 +25,12 @@ import os
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
run_sweep as _run_decode_sweep,
)
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_composite import (
run_sweep as _run_composite_sweep,
)
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_compute_bound import (
run_sweep as _run_prefill_cb_sweep,
)
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
run_sweep as _run_prefill_sweep,
)
@@ -57,6 +64,8 @@ def run(torch) -> None:
runners = {
"prefill": _run_prefill_sweep,
"decode": _run_decode_sweep,
"composite": _run_composite_sweep,
"prefill_cb": _run_prefill_cb_sweep,
}
unknown = [s for s in sweeps if s not in runners]
if unknown:
+4
View File
@@ -264,6 +264,10 @@ class TLContext:
id=self._next_handle_id(),
addr=addr, shape=shape, dtype=dtype,
nbytes=nbytes, space="tcm",
# TCM-resident: a downstream composite operand reads it on-chip,
# not via a DMA_READ of its (bit-61) scratch address — same as
# the composite auto-output and recipe scratch handles below.
pinned=True,
)
# ── Reference (no DMA, metadata only) ────────────────────────
@@ -0,0 +1,191 @@
"""Phase 1 tests for the Case-6 composite-command decode variants.
Three command-form variants of the Case-6 (Cube-SP × PE-SP) long-context
decode kernel are compared in the "Use of Composite Commands" study:
async the current primitive kernel (``tl.dot`` + primitive
online-softmax merge) — the measured baseline.
composite per-tile GEMMs (Q·Kᵀ, P·V) issued as ``tl.composite``
GEMM commands; softmax merge stays primitive.
composite_extended per-tile attention as two composites — Q·Kᵀ GEMM +
a ``softmax_merge`` recipe composite folding the
online merge and P·V (ADR-0065).
This module currently holds the **refactor guard** only (Phase 1): it
freezes the byte-level command stream of the current baseline kernel so
the Phase-2 extraction of the shared (m,,O) reduce into a helper is
provably behavior-preserving. The variant kernels, the dispatch-ordering
test, and the e2e op_log test land in Phase 2 alongside the production
kernels.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp import ( # noqa: E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel as _baseline,
)
from kernbench.triton_emu.tl_context import TLContext, run_kernel
_TOPOLOGY = Path(__file__).resolve().parents[2] / "topology.yaml"
# C=8 CUBEs × P=8 PEs single-KV-head group; S_kv chosen so
# S_local = S_kv/(C·P) = 2048 > TILE_S_KV (1024) → the local-attention
# tile loop runs 2 tiles, exercising the per-tile merge path that the
# composite variants restructure.
_C, _P = 8, 8
_S_KV = 131_072
_D_HEAD, _H_Q, _H_KV, _T_Q = 128, 8, 1, 1
# Volatile per-handle identifiers (assigned by an internal counter); the
# refactor must not change *structure*, but raw ids are not part of the
# behavioral contract, so they are normalized out of the signature.
_VOLATILE_FIELDS = frozenset({"id", "completion", "cid"})
def _norm_value(v):
"""Normalize a command field to an id-free, hashable form."""
# TensorHandle / CompletionHandle-like: keep shape/addr/space/dir, drop id.
if hasattr(v, "shape") and hasattr(v, "addr"):
return ("H", getattr(v, "shape", None), getattr(v, "addr", None),
getattr(v, "space", None), getattr(v, "dtype", None))
if isinstance(v, (list, tuple)):
return tuple(_norm_value(x) for x in v)
if isinstance(v, dict):
return tuple(sorted((k, _norm_value(x)) for k, x in v.items()))
return v
def _cmd_signature(cmd) -> tuple:
fields = getattr(cmd, "__dict__", {})
return (
type(cmd).__name__,
tuple(sorted(
(name, _norm_value(val))
for name, val in fields.items()
if name not in _VOLATILE_FIELDS
)),
)
def _rank_stream(pe_id: int, cube_id: int) -> tuple:
tl = TLContext(
pe_id=pe_id, num_programs=_P, cube_id=cube_id, num_cubes=_C,
scratch_base=0x200000, scratch_size=1 << 20,
)
run_kernel(
_baseline, tl,
0x1000, 0x2000, 0x3000, 0x4000,
_T_Q, _S_KV, _H_Q, _H_KV, _D_HEAD, _C, _P,
)
return tuple(_cmd_signature(c) for c in tl.commands)
def _all_ranks_digest() -> str:
h = hashlib.sha256()
for cube_id in range(_C):
for pe_id in range(_P):
h.update(repr(_rank_stream(pe_id, cube_id)).encode())
return h.hexdigest()
# Golden digest captured from the current (pre-refactor) baseline kernel.
# Phase 2 extracts the intra-/inter-CUBE (m,,O) reduce into a shared
# helper; the refactored kernel MUST reproduce this exact stream.
_GOLDEN_DIGEST = "28069b4b1b19f427b33ee594fb71dc3987f3c92bf38b855058a2261e643047a9"
def test_case6_reduce_refactor_byte_equal():
"""Frozen command stream of the Case-6 baseline across all 64 ranks.
Guards the Phase-2 extraction of the shared reduce helper: the
refactored kernel must emit a byte-identical command stream."""
assert _all_ranks_digest() == _GOLDEN_DIGEST
# ── Variant command-form behaviour (ADR-0064 Rev2 / ADR-0065) ────────
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite import ( # noqa: E402,E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_kernel as _composite,
)
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext import ( # noqa: E402,E501
gqa_attention_decode_long_ctx_cube_sp_pe_sp_composite_ext_kernel as _composite_ext, # noqa: E501
)
from kernbench.common.pe_commands import CompositeCmd, PeCpuOverheadCmd # noqa: E402,E501
from kernbench.common.pe_cost_model import DEFAULT_PE_COST_MODEL # noqa: E402
def _dispatch(kernel, S_kv: int) -> tuple[int, float, int]:
"""(# PE_CPU dispatch commands, summed dispatch cycles, # composites)
emitted by ``kernel`` at the lrab center rank (cube 6, pe 0)."""
tl = TLContext(
pe_id=0, num_programs=_P, cost_model=DEFAULT_PE_COST_MODEL,
cube_id=6, num_cubes=_C, scratch_base=0x200000, scratch_size=1 << 20,
)
run_kernel(
kernel, tl, 0x1000, 0x2000, 0x3000, 0x4000,
_T_Q, S_kv, _H_Q, _H_KV, _D_HEAD, _C, _P,
)
n_disp = sum(1 for c in tl.commands if isinstance(c, PeCpuOverheadCmd))
cycles = sum(c.cycles for c in tl.commands if isinstance(c, PeCpuOverheadCmd))
n_comp = sum(1 for c in tl.commands if isinstance(c, CompositeCmd))
return n_disp, cycles, n_comp
# Two multi-tile S_kv points (S_local = 4096 / 8192 → 4 / 8 primitive tiles).
_S_KV_A, _S_KV_B = 262_144, 524_288
def test_variant_composite_counts():
"""The primitive kernel issues no composites; each composite variant
issues exactly two (Q·Kᵀ and P·V / the recipe head GEMM)."""
assert _dispatch(_baseline, _S_KV_A)[2] == 0
assert _dispatch(_composite, _S_KV_A)[2] == 2
assert _dispatch(_composite_ext, _S_KV_A)[2] == 2
def test_composite_dispatch_flat_in_skv():
"""Coarse composite issue is S_kv-independent: the kernel emits O(1)
commands and PE_SCHEDULER absorbs the per-tile fan-out, so dispatch
cost is identical at 4-tile and 8-tile S_kv (it *saturates*)."""
for kernel in (_composite, _composite_ext):
n_a, cyc_a, _ = _dispatch(kernel, _S_KV_A)
n_b, cyc_b, _ = _dispatch(kernel, _S_KV_B)
assert n_a == n_b, f"{kernel.__name__}: {n_a} != {n_b}"
assert cyc_a == cyc_b
def test_primitive_dispatch_grows_in_skv():
"""The hand-tiled primitive kernel issues O(n_tiles) commands, so its
dispatch cost grows with S_kv — the cost the composite forms remove."""
n_a, cyc_a, _ = _dispatch(_baseline, _S_KV_A)
n_b, cyc_b, _ = _dispatch(_baseline, _S_KV_B)
assert n_b > n_a
assert cyc_b > cyc_a
def test_composite_cheaper_than_primitive_at_long_ctx():
"""At a long-context (8-tile) S_kv, both composite forms issue far
fewer / cheaper PE_CPU commands than the primitive kernel — the
CPU-offload win (ADR-0064 Rev2). The shared (m,,O) reduce is a fixed
common term, so the margin is the local-attention command form alone."""
prim_cyc = _dispatch(_baseline, _S_KV_B)[1]
comp_cyc = _dispatch(_composite, _S_KV_B)[1]
ext_cyc = _dispatch(_composite_ext, _S_KV_B)[1]
assert prim_cyc > 1.5 * comp_cyc, (prim_cyc, comp_cyc)
assert prim_cyc > 1.5 * ext_cyc, (prim_cyc, ext_cyc)
def test_three_variants_complete_in_data_mode():
"""All three command forms run end-to-end through the 64-PE engine in
data mode and report a positive decode latency. Guards the composite
P·V path + the on-chip-operand engine fix (TCM operand not DMA-streamed)
at a multi-rank S_kv where S_local (=128) exceeds the recipe seed."""
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_composite import ( # noqa: E501
_engine_latency_ns,
)
for variant in ("primitive", "composite", "composite_extended"):
lat = _engine_latency_ns(variant, 8192, str(_TOPOLOGY))
assert lat > 0, f"{variant}: latency={lat}"
@@ -0,0 +1,61 @@
"""Tests for the compute-bound prefill composite-command study.
Unlike memory-bound decode (where command form is latency-neutral), in
compute-bound prefill the composite command keeps the MAC array fed by
pipelining DMA↔compute per HW tile, so it wins on wall-clock latency. The
rigorous claim here is therefore the *opposite* of the decode study:
``composite_latency < primitive_latency`` at a compute-bound context.
"""
from __future__ import annotations
from pathlib import Path
from kernbench.common.pe_commands import CompositeCmd
from kernbench.triton_emu.tl_context import TLContext, run_kernel
from kernbench.benches.gqa_helpers.long_ctx._gqa_prefill_compute_bound import (
gqa_prefill_composite_ext_kernel,
gqa_prefill_composite_kernel,
gqa_prefill_primitive_kernel,
)
_TOPOLOGY = Path(__file__).resolve().parents[2] / "topology.yaml"
_H_Q, _H_KV, _D_HEAD = 8, 1, 128
def _n_composites(kernel, T_q: int, S_kv: int) -> int:
tl = TLContext(pe_id=0, num_programs=1, scratch_base=1 << 61,
scratch_size=1 << 20)
run_kernel(kernel, tl, 0x1000, 0x2000, 0x3000, 0x4000,
T_q, S_kv, _H_Q, _H_KV, _D_HEAD, 1, 1)
return sum(1 for c in tl.commands if isinstance(c, CompositeCmd))
def test_command_form_composite_counts():
"""One Q-block (T_q=16 → M=128): the primitive issues no composites;
each composite form issues exactly two (Q·Kᵀ and P·V / recipe)."""
assert _n_composites(gqa_prefill_primitive_kernel, 16, 256) == 0
assert _n_composites(gqa_prefill_composite_kernel, 16, 256) == 2
assert _n_composites(gqa_prefill_composite_ext_kernel, 16, 256) == 2
def _latency_ns(variant: str, ctx_len: int) -> float:
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_compute_bound import (
_run_panel,
)
return _run_panel(variant, ctx_len, str(_TOPOLOGY))["latency_ns"]
def test_three_variants_complete_in_data_mode():
"""All three prefill forms run end-to-end through the engine."""
for variant in ("primitive", "composite", "composite_extended"):
assert _latency_ns(variant, 256) > 0
def test_composite_faster_in_compute_bound_prefill():
"""At a compute-bound context (512), the composite command's DMA↔compute
pipelining beats the primitive's serial load→dot — the opposite of the
memory-bound decode study, where the two tie."""
prim = _latency_ns("primitive", 512)
comp = _latency_ns("composite", 512)
assert comp < prim, f"composite {comp} not < primitive {prim}"
+59
View File
@@ -0,0 +1,59 @@
"""Engine-fix spec test — composite GEMM with an on-chip (TCM) operand.
Root cause: ``_make_compute_out`` (tl_context) builds compute-result
handles with ``space="tcm"`` but omits ``pinned=True``. The PE_SCHEDULER
tiling gate (tiling.py:306) skips the operand DMA_READ only when
``pinned`` is set, so a compute result fed as a composite head-GEMM
operand ``a`` (e.g. the attention probabilities into P·V) is wrongly
scheduled as an HBM ``DMA_READ`` of its bit-61 scratch address →
``PhysAddrError`` in data mode.
Every other TCM-resident handle in tl_context is already marked
``pinned=True`` for exactly this reason (composite auto-output, recipe
scratch / primary-out). ``_make_compute_out`` is the one site that
forgot. ``.pinned`` is read ONLY by the tiling DMA gate, so setting it on
compute outputs is side-effect-free and byte-equal for existing benches
(which never feed an unpinned compute output into a composite operand).
Phase 1: this test FAILS until the fix (a DMA_READ for operand A is
emitted for the on-chip ``a``).
"""
from __future__ import annotations
from kernbench.common.pe_commands import CompositeCmd
from kernbench.components.builtin.pe_types import StageType
from kernbench.components.builtin.tiling import generate_plan_from_ops
from kernbench.triton_emu.tl_context import TLContext, run_kernel
def _pv_composite_kernel(*, tl) -> None:
"""P·V composite whose ``a`` is a real on-chip compute result."""
x = tl.load(0x1000, shape=(8, 1024), dtype="f16")
a = tl.exp(x) # compute output → TCM
V = tl.ref(0x3000, shape=(1024, 128), dtype="f16") # HBM, streamed
O = tl.zeros((8, 128), dtype="f16")
tl.composite(op="gemm", a=a, b=V, out=O)
def _pv_plan():
tl = TLContext(pe_id=0, num_programs=1,
scratch_base=0x200000, scratch_size=1 << 20)
run_kernel(_pv_composite_kernel, tl)
comp = next(c for c in tl.commands if isinstance(c, CompositeCmd))
return generate_plan_from_ops(
ops=comp.ops, tile_m=32, tile_k=64, tile_n=32,
bytes_per_element=2, pe_prefix="sip0.cube0.pe0",
)
def test_onchip_operand_a_not_dma_read():
"""The on-chip ``a`` (a compute result) must NOT be streamed via a
DMA_READ; only the HBM ref ``b`` (V) is."""
plan = _pv_plan()
read_operands = [
s.params.get("operand")
for t in plan.tiles for s in t.stages
if s.stage_type == StageType.DMA_READ
]
assert "A" not in read_operands, read_operands
assert "B" in read_operands, read_operands