adr-0070: gqa short context attention prefill/decode + 3 composite tiers
This commit is contained in:
@@ -0,0 +1,243 @@
|
|||||||
|
# ADR-0070: GQA Short-Context Attention — Unified A1/A2/A4/B Mapping for Prefill and Decode
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed — short-context (single tile to a handful of KV tiles) GQA
|
||||||
|
attention benchmark covering **both prefill and decode** in **four
|
||||||
|
mapping modes** (A1/A2/A4/B) and **three composite tiers** (without
|
||||||
|
composite, GEMM-only composite, composite + softmax_merge fused).
|
||||||
|
Compute-only attention — weight DMA, ITL batching, and end-to-end
|
||||||
|
LLM driver scope are out of scope (separate ADRs).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A GQA layer at the LLaMA-3.1-70B headline shape (`h_q=64`, `h_kv=8`,
|
||||||
|
`d_head=128`, GQA group `G = h_q/h_kv = 8`) on a 4×4 cube SIP can be
|
||||||
|
mapped to AHBM in several ways that trade KV-cache memory, HBM read
|
||||||
|
volume per cube, and intra-cube IPCQ traffic against PE parallelism.
|
||||||
|
|
||||||
|
> **Bench-vs-headline shape.** The kernels and tests use a
|
||||||
|
> `d_head = 64` proxy (`tests/attention/test_gqa_short_context.py`
|
||||||
|
> `D_HEAD = 64`) so the per-tile working set fits comfortably in
|
||||||
|
> scratch across all four modes. The mapping decisions are
|
||||||
|
> `d_head`-independent — only the per-PE GEMM tile bytes scale.
|
||||||
|
|
||||||
|
The four candidate mappings (per *GQA-mapping-on-AHBM-short-context* note §1):
|
||||||
|
|
||||||
|
```
|
||||||
|
Mode kv_per_cube C group_size Reduce / broadcast topology
|
||||||
|
---- ----------- ------- ---------- ----------------------------------
|
||||||
|
A1 1 h_kv P (=8) row chain + col bridge (2×4 mesh)
|
||||||
|
A2 2 h_kv/2 P/2 (=4) row chain only
|
||||||
|
A4 4 h_kv/4 P/4 (=2) single intra_W / intra_E hop
|
||||||
|
B 8 1 1 no broadcast / no reduce (single PE)
|
||||||
|
```
|
||||||
|
|
||||||
|
Within each cube the 8 PEs split into `kv_per_cube` groups of
|
||||||
|
`group_size = P/kv_per_cube` PEs; each group owns one KV head.
|
||||||
|
|
||||||
|
The kernel must be able to compare these mappings at multiple context
|
||||||
|
lengths (8K..64K KV tokens) and across the three composite tiers so
|
||||||
|
the team can size the per-cube HBM / IPCQ / GEMM tradeoff empirically.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Mapping (unified A1/A2/A4/B)
|
||||||
|
|
||||||
|
Two unified kernels select mode at launch via `kv_per_cube ∈ {1,2,4,8}`:
|
||||||
|
|
||||||
|
- **Prefill** (`gqa_attention_prefill_short_kernel`):
|
||||||
|
- Q-tile split — `T_q` rows floor-balanced across the group's
|
||||||
|
`group_size` PEs.
|
||||||
|
- FA2 head fusion — `G` Q heads fused into the M dimension of one
|
||||||
|
batched GEMM per PE per tile.
|
||||||
|
- **IPCQ KV broadcast** — group root (`pe_in_group == 0`) loads K/V
|
||||||
|
from HBM and IPCQ-broadcasts each tile to the rest of its group;
|
||||||
|
q-tiles are independent so no intra-group reduce.
|
||||||
|
|
||||||
|
- **Decode** (`gqa_attention_decode_short_kernel`):
|
||||||
|
- Sequence-shard — each PE owns `S_local = S_kv/group_size` tokens
|
||||||
|
of the group's KV head.
|
||||||
|
- FA2 head fusion — same as prefill.
|
||||||
|
- **IPCQ chain reduce** — per-PE partial `(m, ℓ, O)` chain-reduce
|
||||||
|
up to group root (PE 0), which normalizes and stores.
|
||||||
|
|
||||||
|
Geometry (2×4 mesh):
|
||||||
|
|
||||||
|
```
|
||||||
|
group_size = 8 : 2 row × 4 col (A1)
|
||||||
|
group_size = 4 : 1 row × 4 col (A2)
|
||||||
|
group_size = 2 : 1 row × 2 col (A4)
|
||||||
|
group_size = 1 : single PE (B)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Shard addressing (ADR-0011 D-VA1 contract)
|
||||||
|
|
||||||
|
Deploy is per-(sip, cube, pe) but `tl.load` receives a single global
|
||||||
|
VA per tensor. The kernel computes its own shard base offset from
|
||||||
|
`tl.program_id(axis=0)` (PE id) and `tl.program_id(axis=1)` (cube id):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Decode shape (pe=row_wise): K is split across PEs by sequence shard.
|
||||||
|
cube_K_base = cube_id * kv_per_cube * K_HEAD_BYTES
|
||||||
|
head_K_base = group_id_in_cube * K_HEAD_BYTES
|
||||||
|
pe_K_seq_offset = pe_in_group * n_tiles_per_pe * K_TILE_BYTES
|
||||||
|
k_shard_base = k_ptr + cube_K_base + head_K_base + pe_K_seq_offset
|
||||||
|
```
|
||||||
|
|
||||||
|
For **prefill** the K/V dp is `pe=replicate` (group root reads and
|
||||||
|
broadcasts) so the `pe_in_group` term drops out:
|
||||||
|
`k_head_shard_base = k_ptr + cube_K_base + head_K_base`.
|
||||||
|
|
||||||
|
Skipping `cube_id` collapses all cubes onto cube 0's HBM region
|
||||||
|
(observed pre-fix as an 11.5× per-cube DMA imbalance).
|
||||||
|
|
||||||
|
### 3. Three composite tiers
|
||||||
|
|
||||||
|
The same mapping is exercised against three GEMM/MATH dispatch styles
|
||||||
|
so the contribution of the composite API vs the recipe-driven fusion
|
||||||
|
can be isolated:
|
||||||
|
|
||||||
|
```
|
||||||
|
(1) without composite primitives only (tl.dot, tl.exp, …)
|
||||||
|
(2) with composite (GEMM-only) tl.composite(op="gemm")
|
||||||
|
(3) with composite + softmax_merge tl.composite(prologue=[softmax_merge],
|
||||||
|
op="gemm", out=O,
|
||||||
|
epilogue=[add])
|
||||||
|
```
|
||||||
|
|
||||||
|
File layout (`src/kernbench/benches/gqa_helpers/short_ctx/`):
|
||||||
|
|
||||||
|
```
|
||||||
|
_gqa_attention_{prefill,decode}_short.py (1)
|
||||||
|
_gqa_attention_{prefill,decode}_short_composite.py (2)
|
||||||
|
_gqa_attention_{prefill,decode}_short_composite_fused.py (3)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tier (2) is **GEMM-only by definition** — no recipe-driven fusion,
|
||||||
|
so both prefill's and decode's P·V stay `tl.dot`. Recipe-driven
|
||||||
|
fusion is exactly what tier (3) adds.
|
||||||
|
|
||||||
|
Tier (3) on **multi-cube modes (A1/A2/A4) of prefill** relies on the
|
||||||
|
D4 supplement: any composite operand that is an IPCQ recv'd slot
|
||||||
|
(non-root PE's K_T in Q·Kᵀ, V in P·V) is pinned and read in place
|
||||||
|
instead of being DMA-streamed from HBM. Without the supplement every
|
||||||
|
recv slot fed to a composite — both Q·Kᵀ's `b=K_T` and P·V's `b=V`
|
||||||
|
— PageFaults on PA decode (PE scratch addresses overflow the 51-bit
|
||||||
|
PA range).
|
||||||
|
|
||||||
|
### 4. Caller contract — `_validate_config`
|
||||||
|
|
||||||
|
Each kernel module exposes a single `_validate_config(...)` helper
|
||||||
|
called by the bench wrapper before launch. The kernel itself is lean
|
||||||
|
(no inline `if` guards): the contract block is enforced caller-side,
|
||||||
|
sim cost zero, but catches every silent-shard-corruption foot-gun
|
||||||
|
(`kv_per_cube=3 → group_size=2` via integer division, non-integer
|
||||||
|
GQA group, mismatched cube count, partial-tile `S_kv`, …) before any
|
||||||
|
address arithmetic runs.
|
||||||
|
|
||||||
|
### 5. Tensor layouts (host-side, mode-invariant byte totals)
|
||||||
|
|
||||||
|
- **Q**: `(kv_per_cube·T_q, h_q·d_head/kv_per_cube)`
|
||||||
|
`dp=(cube=column_wise, pe=replicate)` over `C = h_kv/kv_per_cube`.
|
||||||
|
- **K**: `(h_kv·S_kv·d_head/TILE_S_KV, TILE_S_KV)` tile-major.
|
||||||
|
Decode: `dp=(cube=row_wise, pe=row_wise)`.
|
||||||
|
Prefill: `dp=(cube=row_wise, pe=replicate)` (broadcast model).
|
||||||
|
- **V**: `(h_kv·S_kv, d_head)` native. Same dp as K.
|
||||||
|
- **O**: same dp as Q.
|
||||||
|
|
||||||
|
Caller pre-scales Q by `1/√d_head`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Smoke + regression (`tests/attention/test_gqa_short_context.py`)
|
||||||
|
|
||||||
|
- Prefill / decode smoke (all 4 modes, single-tile S_kv).
|
||||||
|
- Multi-tile coverage (all 4 modes, S_kv chosen so each PE owns
|
||||||
|
≥2 tiles).
|
||||||
|
- Op-count invariants — prefill `dma_writes = group_size·kv_per_cube·C`,
|
||||||
|
`dma_reads = P·C + 2·kv_per_cube·C·n_tiles`; decode
|
||||||
|
`dma_writes = kv_per_cube·C`.
|
||||||
|
- IPCQ topology — short-context kernel never emits inter-CUBE E/W
|
||||||
|
IPCQ.
|
||||||
|
- **ADR-0011 D-VA1 regression** — `per_cube_disjoint_src_addrs`,
|
||||||
|
`per_cube_dma_balanced` (max/min DMA busy ratio < 1.1×) per mode
|
||||||
|
per phase.
|
||||||
|
|
||||||
|
### Composite smoke
|
||||||
|
|
||||||
|
- `test_{prefill,decode}_composite_smoke[A1/A2/A4/B]` — tier (2),
|
||||||
|
4 modes × 2 phases.
|
||||||
|
- `test_{prefill,decode}_composite_fused_smoke[A1/A2/A4/B]` — tier
|
||||||
|
(3), 4 modes × 2 phases.
|
||||||
|
- `test_decode_composite_fused_multitile[A1/A2/A4/B]` —
|
||||||
|
`n_tiles_per_pe == 2` per mode so the recipe-fused tile loop
|
||||||
|
actually executes.
|
||||||
|
|
||||||
|
### Mode × context sweep (`tests/attention/test_gqa_short_context_sweep_*.py`)
|
||||||
|
|
||||||
|
Six sweep files (3 tiers × {prefill, decode}) measure each
|
||||||
|
`(mode, S_kv)` cell and dump the same metric set to CSV:
|
||||||
|
|
||||||
|
```
|
||||||
|
wall_us, n_pe, gemm_util, math_count, math_pipeline_us, hbm_bw_util,
|
||||||
|
hbm_read_mb, hbm_write_kb, ipcq_kb, kv_cache_per_cube_mb
|
||||||
|
```
|
||||||
|
|
||||||
|
S_kv ∈ {8K, 16K, 32K, 64K}; outputs under `docs/sweeps/`.
|
||||||
|
|
||||||
|
Composite/fused sweeps count both `gemm_f16` (non-pipeline) **and**
|
||||||
|
`TileToken/GEMM` (pipeline composite path); fused additionally counts
|
||||||
|
`TileToken/MATH`. Baseline sweeps count `gemm_f16` only.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
1. **Causal mask** — kernel is non-causal. Adding causal masking is
|
||||||
|
orthogonal to mapping and is scheduled as a separate ADR.
|
||||||
|
2. **f16 online-softmax accumulator** — `(m, ℓ, O)` are f16 throughout.
|
||||||
|
Long-context numerical drift will need an f32 accumulator before
|
||||||
|
correctness-grade use.
|
||||||
|
3. **End-to-end numeric validation** — current tests are op-count /
|
||||||
|
topology / per-cube-DMA invariants. No full-kernel
|
||||||
|
`np.allclose` against a torch/numpy reference yet.
|
||||||
|
4. **Skinny-M GEMM underfill** — decode (`T_q=1`) gives `M = G = 8`,
|
||||||
|
well below `PE_SCHEDULER` supertile `TILE_M = 32`. The two GEMM
|
||||||
|
paths handle this differently:
|
||||||
|
- **Primitive `tl.dot`** dispatches the GEMM at the actual
|
||||||
|
`m = 8` (no padding); GEMM time reflects the real 8-row work.
|
||||||
|
- **`tl.composite(op="gemm")`** tiles at `TILE_M = 32`, padding
|
||||||
|
`M` 4× with zeros; GEMM time reflects the padded 32-row work.
|
||||||
|
Consequence: when sweep CSVs show tier (2)/(3) `gemm_util`
|
||||||
|
higher than tier (1), most of that gap is supertile padding
|
||||||
|
overhead, **not** extra useful work or fusion savings — read it as
|
||||||
|
"composite tile shape doesn't match decode-skinny shape" rather
|
||||||
|
than "composite/fusion is more compute-intensive." This is the
|
||||||
|
**intended decode shape** for this benchmark; lifting it requires
|
||||||
|
batched-M (multi-request inference, `B_sys > 1`), tracked as a
|
||||||
|
separate batched variant — see "Future work".
|
||||||
|
5. **Multi-cube prefill composite without the ADR-0065 D4 supplement**
|
||||||
|
PageFaults. The supplement — IPCQ recv'd slots are pinned and read
|
||||||
|
in place as composite operands — ships alongside this ADR.
|
||||||
|
|
||||||
|
## Future work
|
||||||
|
|
||||||
|
- Batched-M variant (`B_sys ∈ {1,2,4,8,16}`) so composite/fused
|
||||||
|
pipeline overlap shows up in wall_clock rather than only in
|
||||||
|
per-engine utilization.
|
||||||
|
- Long-context (S_kv ≥ 128K) sweep extension for the headline LLaMA
|
||||||
|
decode target.
|
||||||
|
- Causal-mask + f32 accumulator promotion.
|
||||||
|
- 3-variant comparison plots from the sweep CSVs (wall, gemm util,
|
||||||
|
hbm bw util, ipcq, kv cache) — generated under `docs/diagrams/`.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-0011 — PhysAddr + VA contract (D-VA1: kernel-computed shard
|
||||||
|
offset).
|
||||||
|
- ADR-0060 — GQA fused attention on AHBM (precursor of the unified
|
||||||
|
mapping).
|
||||||
|
- ADR-0064 — CPU issue-cost model (composite supertile motivation).
|
||||||
|
- ADR-0065 — Flat ops, composite, softmax_merge recipe (with the D4
|
||||||
|
supplement applied here).
|
||||||
|
- ADR-0070 supersedes ADR-0060 §B.split.2 short-prefill clause.
|
||||||
|
- *GQA-mapping-on-AHBM-short-context* note (research design rationale).
|
||||||
Reference in New Issue
Block a user