Compare commits
2 Commits
e9a5c438e3
...
24c705419e
| Author | SHA1 | Date | |
|---|---|---|---|
| 24c705419e | |||
| cd6f0ed91d |
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()
|
||||
BIN
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
|
||||
}
|
||||
]
|
||||
}
|
||||
+105
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
-186
@@ -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:
|
||||
|
||||
+71
@@ -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)
|
||||
+94
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user