gqa: ADR-0060/0062/0063/0064 unified GQA kernels + CPU cost model

Land the new GQA fused-attention kernels (ADR-0060) for prefill/decode
across long and short context, the TL discipline primitives they depend
on (ADR-0062 lazy load, ADR-0063 scratch_scope + copy_to), and the
per-op-type CPU issue cost model (ADR-0064). Remove the pre-ADR-0060
mesh-attention baseline now that the unified kernels supersede it.

ADR-0060 (long context)
- _gqa_decode.py: M-fold + 2-level chain reduce-to-root (Level-2
  intra-CUBE row-then-col + Level-1 inter-CUBE) — root-only output.
- _gqa_prefill.py: head-parallel + Ring KV rotation around C CUBEs,
  online-softmax merge per ring step, per-CUBE distributed output.
- Each merge stage wraps in scratch_scope() and persists running
  (m, l, O) via copy_to() to lift the 1 MiB scratch ceiling.

ADR-0060 §B.split.2 (short context, kv_per_cube in {1,2,4,8})
- _gqa_decode_short.py / _gqa_prefill_short.py: no cube-SP; each CUBE
  owns whole KV heads; PE-parallel heads with intra-group chain
  reduce. Prefill has no Ring KV (each head fully resident).

ADR-0062 (lazy tl.load): future-bearing TensorHandle, auto-wait at
first consuming op (dot/MATH/store/send/copy_to/composite).

ADR-0063 (tl.scratch_scope + tl.copy_to): scoped per-tile arena with
copy_to writeback primitive for persistent running state.

ADR-0064 (CPU issue cost model)
- common/cpu_issue_cost.py: per-op-type table (composite=40 ns,
  primitives=5 ns); ratios are load-bearing per D1.
- TLContext: issue_cost_table param; _emit_dispatch_overhead(kind)
  consults table with dispatch_cycles fallback (ADR-0046 §D6
  back-compat).
- Live PE_CPU paths (greenlet + legacy) construct TLContext with
  DEFAULT_CPU_ISSUE_COST so saturation lever (ADR-0060 §1) is
  measurable end-to-end.

P7 headline bench: milestone-gqa-headline writes per-panel
op_log_summary to 1H_milestone_output/gqa_headline/sweep.json. No
figure renderers yet (deferred).

Removals (pre-ADR-0060 baseline now superseded):
- benches: _attention_mesh_kv.py, _attention_mesh_mlo.py,
  _attention_mesh_mlo_2d.py, milestone_gqa_llama70b.py
- tests: test_attention_*, test_mesh_*, test_milestone_gqa_llama70b
- topology: llama70b_4sip.yaml (only consumer was the deleted diag)
- artifacts: 1H_milestone_output/gqa/ (sweep.json + 5 PNGs)
- tests/gqa/ plot helper + test (broken on Windows Tcl/Tkinter)
- ADR-0060/0061 references to deleted file paths cleaned up
  (EN + KO kept in sync).

Tests: 124/124 focused regression green (attention + Phase E + TL
discipline + triton_emu + pe_components). Full regression: 764 pass,
2 pre-existing test_bench_registry failures (stale EXPECTED_NAMES
across multiple benches, not introduced here).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 18:15:59 -07:00
parent b3730a33eb
commit d282144339
52 changed files with 3952 additions and 2526 deletions
@@ -8,11 +8,11 @@ Proposed
**Decision drivers:** agentic workload → low batch, long context;
KV-load-bound decode; sequence-parallel (Ring KV) for long-context prefill.
**Supersedes / extends:** the existing mesh-native attention kernels
`_attention_mesh_kv` (prefill) and `_attention_mesh_mlo` (decode) and the
`milestone-gqa-llama70b` eval bench. See *§A. Relationship to existing
kernbench work* — that code is the baseline this ADR upgrades to a real
GQA, causal, long-context kernel.
**Supersedes / extends:** pre-existing mesh-native attention kernels and
their `milestone-gqa-llama70b` eval bench (removed when this ADR's
kernels landed). See *§A. Relationship to existing kernbench work* — that
code was the baseline this ADR upgrades to a real GQA, causal,
long-context kernel.
**Supporting ADRs** (efficiency / scale enablers — *not* GQA blockers;
see §8 correction): **ADR-0063** `tl.scratch_scope` (per-tile scratch
@@ -168,18 +168,16 @@ def gqa_prefill_sp(q_ptr, k_ptr, v_ptr, o_ptr, T_q, S_kv_local, d, C, scale, q_b
## A. Relationship to existing kernbench work (read first)
kernbench **already runs FlashAttention with an online-softmax `(m, , O)`
merge over IPCQ today.** Two kernels exist:
kernbench previously ran FlashAttention with an online-softmax `(m, , O)`
merge over IPCQ via two pre-ADR-0060 mesh kernels (now removed):
| File | Role | Mechanism |
|---|---|---|
| `src/kernbench/benches/_attention_mesh_kv.py` | prefill (Ring K/V) | per-rank partial attention, bidirectional K/V fan-out, online-softmax fold |
| `src/kernbench/benches/_attention_mesh_mlo.py` | decode (split-KV) | per-rank one-shot partial attention, bidirectional `(m,,O)` fan-out, log-sum-exp merge |
| Role | Mechanism |
|---|---|
| prefill (Ring K/V) | per-rank partial attention, bidirectional K/V fan-out, online-softmax fold |
| decode (split-KV) | per-rank one-shot partial attention, bidirectional `(m,,O)` fan-out, log-sum-exp merge |
Both are driven by `milestone-gqa-llama70b` (4 panels:
`{single,multi}_user × {prefill,decode}`,
`src/kernbench/benches/milestone_gqa_llama70b.py`) and tested in
`tests/attention/test_milestone_gqa_llama70b.py`.
Both were driven by `milestone-gqa-llama70b` (4 panels:
`{single,multi}_user × {prefill,decode}`).
**They are written in the greenlet `tl` API:** `tl.load`, `tl.dot`,
`tl.softmax`/`tl.max`/`tl.sum`/`tl.exp`, `tl.send`/`tl.recv`, and Python
@@ -193,8 +191,8 @@ GEMMs onto the scheduler-managed `tl.composite` path** (see §1) — this
**Three deliberate limitations of the baseline** — exactly what an
*efficient GQA* kernel must lift:
1. **No GQA reuse.** `h_q == h_kv == 1`
(`test_milestone_gqa_llama70b.py:137-142`). The test attributes this to
1. **No GQA reuse.** Baseline was capped at `h_q == h_kv == 1`. The
baseline test attributed this to
a MemoryStore byte-conservation failure on a *broadcast view*, but that
failure is a property of the baseline's **head-packing hack**
(`_view(K, (h_q·d, S_kv))`, which conflates all heads into one matmul
@@ -209,9 +207,9 @@ GEMMs onto the scheduler-managed `tl.composite` path** (see §1) — this
reduce-to-root** (intra-CUBE tree + intra-CUBE-Group center-mesh, §4) is
`⌈log₂ P⌉` + center-mesh-over-`C` steps.
3. **Validation scale only.** `S = 16` because the 1 MiB scratch bump
allocator leaks per-tile temporaries
(`test_milestone_gqa_llama70b.py:123-148`) and there is no causal
masking / tiling → fixed by **ADR-0063** (recycling) + §5 (tiling,
allocator leaks per-tile temporaries (baseline test capped S at 16
for this reason) and there is no causal masking / tiling → fixed by
**ADR-0063** (recycling) + §5 (tiling,
causal skip) + composite K/V streaming (§3) + **ADR-0062** (lazy load
overlap).
@@ -616,7 +614,7 @@ sharded, small `O`). Prefill-SP does **not** reduce — it rotates KV (Ring,
After its tile sweep each rank holds `(m_i, _i, O_i)` (unnormalised) over
its `1/(C·P)` shard. Combine via the associative/commutative log-sum-exp
merge (identical math to the baseline's fold, `_attention_mesh_mlo.py:117-122`):
merge (identical math to the baseline's fold):
```python
def merge(m_a, l_a, O_a, m_b, l_b, O_b):
@@ -750,8 +748,8 @@ def gqa_decode_sp(q_ptr, k_ptr, v_ptr, o_ptr, counter, start_pe, start_cube,
batch axis would *not* be counted — see §8).
- If `S_rank` fits in scratch (small/medium context) this degenerates to a
**one-shot** partial attention (one composite for Q·Kᵀ, one softmax, one
composite for P·V) — exactly the baseline `_attention_mesh_mlo`
`_partial_attention`, just GQA-batched and on the composite path. Tiling
composite for P·V) — exactly the baseline `_partial_attention` shape,
just GQA-batched and on the composite path. Tiling
(§3) only kicks in when `S_rank` exceeds the scratch scope's tile budget.
### 5.3 DECODE, with SP / KV-parallel (`C × P` ranks)
@@ -809,8 +807,8 @@ head needs in full anyway.
`(m,,O)` reduce** — each CUBE normalises and writes its own head's rows.
- **Ring KV:** the `C` KV slices **rotate** around the CUBE ring; each CUBE
folds the incoming block into its head's running `(m,,O)` (online-softmax,
carried across ring steps as Python handles, as `_attention_mesh_kv` does
today). After `C` steps every head has seen all KV. **GQA reuse comes from
carried across ring steps as Python handles). After `C` steps every
head has seen all KV. **GQA reuse comes from
the rotation** — slice `j` visits all `C` CUBEs, serving all `G` heads.
- IPCQ overlaps the next step's KV receive with the current step's compute
via `tl.recv_async`/`tl.wait` (`tl_context.py:543-560` — already exists);
@@ -821,8 +819,8 @@ head needs in full anyway.
current KV block tile across the `P` PEs (a query-axis split exists here,
unlike decode); details in §B.
The baseline `_attention_mesh_kv` already implements the ring fold; this
ADR adds GQA reuse, the head-parallel placement, causal step-skip, and the
The (now-removed) baseline already implemented the ring fold; this ADR
adds GQA reuse, the head-parallel placement, causal step-skip, and the
composite-hybrid inner tile (§3).
### 5.6 Decode CPU-pipelining variants (opt1 / opt3 / opt2)
@@ -1184,8 +1182,8 @@ predicted default; revise on review.
fix `C = G` for the prefill kernel at headline scale; treat `C ≠ G` as a
separate study.
5. **Reconcile with `_attention_mesh_mlo_2d` (current impl).** The remote
impl's 2D kernel is an **AllReduce** over cubes with **Q replicated** —
5. **Reconcile with the prior `_attention_mesh_mlo_2d` impl (now removed).**
That 2D kernel was an **AllReduce** over cubes with **Q replicated** —
i.e. the decode-reduce family, but all-reduce (broadcast-back) not
reduce-to-root, and not yet the prefill head-parallel ring. **Recommend:**
(a) move its 2D AllReduce → reduce-to-root (drop broadcast-back) for the
@@ -1208,3 +1206,50 @@ predicted default; revise on review.
command kind. Defer **opt2** (the
two-composite `ex_composite`, only `#2` is new) until ADR-0064's cost
model makes its fewer-issues win measurable.
### Items from the long/short context split (this revision)
The split between the long-context kernels (§5.2 decode, §5.5 prefill —
both already specified) and a short-context variant is referenced
abstractly in items §B-1 and §B-6 above ("short-context balance is a
separate study"). This subsection pins the two open numbers.
1. **Short/long context threshold = 256 K tokens.** Below the threshold,
the §5.5 Ring KV prefill pays C-1 ring rotations whose IPCQ cost is
not amortised by enough per-rank compute; above it, the per-rank
KV sweep dominates and ring is the right choice (§9 line 803).
Likewise §5.2's per-rank `S_kv/C·P` shard is small enough at
short context that the 2-level reduce-to-root hop count
dominates the per-rank compute. The 256 K boundary matches the
point at which `S_kv/C·P` (`C=4, P=8 → /32`) crosses the
8 K tokens-per-rank mark above which the per-rank tile sweep
(§3 / §B-3 §scratch_scope) becomes the limiting factor rather
than collective overhead. **Recommend:** treat 256 K as the
bench dispatch threshold; expose it as a launch knob for sweeps.
2. **Short-context kernel design — `kv_per_cube ∈ {1, 2, 4, 8}`.**
At short context the §5.5 Ring KV motion is wasted work and the
§5.2 cube-SP shard is too thin to feed the engines. The
short-context variant therefore drops cube-SP entirely: each
CUBE owns `kv_per_cube` *whole* KV heads (no `S_kv` sharding
across CUBEs), and the existing PE-SP within a CUBE shards `S_kv`
across the `P` PEs for each owned head.
For `h_kv = 8` the natural distributions are:
| `kv_per_cube` | participating CUBEs | head→CUBE map |
|---|---|---|
| 1 | `C = 8` | head `i` → CUBE `i` |
| 2 | `C = 4` | heads `[2i, 2i+1]` → CUBE `i` |
| 4 | `C = 2` | heads `[4i..4i+3]` → CUBE `i` |
| 8 | `C = 1` | all heads → single CUBE |
No inter-CUBE reduce within a head (each head fully owned by one
CUBE), so the kernel runs Level-2 (PE) chain reduce-to-root only;
Level-1 (inter-CUBE) collapses to a no-op. The output stays
distributed per CUBE (each CUBE writes its owned heads' `O` slice).
**Recommend:** ship as a separate kernel file (`_gqa_decode_short.py` /
`_gqa_prefill_short.py`) and a separate bench dispatcher that picks
short vs long by `S_kv ⋚ 256K`. Keep `kv_per_cube` as a kernel arg so
the topology cost trade-off can be measured per workload.