c164645aee
First milestone of the decode 4-cases comparative study per GQA_full_deck.pptx slides 11-17. Case 4 (Cube-SP × PE-SP, the optimal case per slide 11) is structurally the existing _gqa_attention_decode_long at sub_w=4 (Increment 2's lrab-adapted center-root reduce). This commit wires it into a dedicated bench so the remaining cases land alongside. Changes: - New bench: src/kernbench/benches/milestone_gqa_decode_4cases.py Houses the 4 case panels under one milestone-gqa-decode-4cases entry (gated by GQA_DECODE_4CASES_RUN=1; output to 1H_milestone_output/gqa_decode_4cases/sweep.json). Cases 1-3 are TBD in subsequent sub-increments (5C.A/B/C). - New panel: single_kv_group_decode_gqa_cube_sp_pe_sp C=8, P=8, sub_w=4, T_q=1, S_kv=131_072, d_head=128, h_q=8, h_kv=1. - src/kernbench/benches/milestone_gqa_headline.py: _run_decode_panel extended with keyword-only sub_w/T_q/d_head/h_q/h_kv overrides (defaults preserve existing-panel behaviour). - tests/attention/test_milestone_gqa_decode_4cases.py: 4 new tests asserting registration, smoke completion, reduce-to-root at the lrab center cube (cube 6), and the predicted 189-ipcq Case-4 traffic pattern (168 intra-CUBE + 21 inter-CUBE lrab Phase 1+2). - tests/attention/test_milestone_gqa_headline.py: rename test_sweep_json_has_four_panels -> test_sweep_json_has_expected_panels and switch hardcoded 4 to len(PANELS) (the panel set grew to 5 with Increment 5's single_kv_group_prefill_gqa_c8_p8). Deviation noted: slide 13 prescribes AllReduce on (m,ℓ,O); our kernel does reduce-to-root (only the lrab center cube has the answer) per ADR-0060 §4. Treated as the kernbench Case-4 baseline. Verification: all 4 new tests pass; 90 regression tests pass; the previously-failing test_sweep_json_has_four_panels now passes under its renamed form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
"""Phase 1 spec test for P7: headline milestone-gqa bench with real GQA.
|
||
|
||
P7 wires the new ``_gqa_decode`` and ``_gqa_prefill`` kernels into a
|
||
new 4-panel milestone bench (independent from the existing
|
||
``milestone-gqa-llama70b`` which still covers the baseline kernels).
|
||
Real GQA (``h_q > h_kv`` with G=8) runs end-to-end through a
|
||
milestone-style sweep + sweep.json output.
|
||
|
||
Restriction in P7 first cut:
|
||
- C ≤ 4 (single-row inter-CUBE ring SFR; ADR-0060 §B leaves
|
||
multi-row rings for follow-on)
|
||
- Single SIP (default ``topology.yaml`` 4×4 cube mesh; 4-SIP
|
||
headline left to follow-on)
|
||
- No figure renderers (defer to a separate cycle)
|
||
|
||
Panels (4 total):
|
||
single_user_prefill_gqa: C=1, T_q=4, S_kv=16 (no ring)
|
||
multi_user_prefill_gqa : C=4, T_q=4, S_kv=16 (Ring KV, 3 steps)
|
||
single_user_decode_gqa : C=1, P=8, h_q=8, h_kv=1, S_kv=64 (M-fold + intra-cube chain)
|
||
multi_user_decode_gqa : C=4, P=8, h_q=8, h_kv=1, S_kv=128 (M-fold + 2-level chain)
|
||
|
||
Phase 1 (this commit): tests only — bench module lands in Phase 2.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
import kernbench.benches.milestone_gqa_headline as bench_mod # noqa: F401 (Phase 2)
|
||
from kernbench.benches.registry import resolve
|
||
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
|
||
|
||
BENCH_NAME = "milestone-gqa-headline"
|
||
|
||
PANELS = (
|
||
"single_user_prefill_gqa",
|
||
"multi_user_prefill_gqa",
|
||
"single_kv_group_prefill_gqa_c8_p8",
|
||
"single_user_decode_gqa",
|
||
"multi_user_decode_gqa",
|
||
)
|
||
|
||
|
||
def _run_validation():
|
||
topo = resolve_topology("topology.yaml")
|
||
return run_bench(
|
||
topology=topo,
|
||
bench_fn=resolve(BENCH_NAME).run,
|
||
device=resolve_device(None),
|
||
engine_factory=lambda t, d: GraphEngine(
|
||
getattr(t, "topology_obj", t), enable_data=True,
|
||
),
|
||
)
|
||
|
||
|
||
def _sweep_json(monkeypatch) -> dict:
|
||
monkeypatch.setenv("GQA_HEADLINE_RUN", "1")
|
||
out = bench_mod._OUTPUT_DIR / "sweep.json"
|
||
if not out.exists():
|
||
result = _run_validation()
|
||
assert result.completion.ok, result.completion
|
||
assert out.exists(), f"missing {out}"
|
||
return json.loads(out.read_text())
|
||
|
||
|
||
# ── Registration ───────────────────────────────────────────────────────
|
||
|
||
|
||
def test_bench_registered():
|
||
spec = resolve(BENCH_NAME)
|
||
assert spec.name == BENCH_NAME
|
||
assert callable(spec.run)
|
||
assert spec.description.strip(), "description must be non-empty"
|
||
|
||
|
||
# ── Validation run completes ──────────────────────────────────────────
|
||
|
||
|
||
def test_validation_run_completes_ok(monkeypatch):
|
||
monkeypatch.setenv("GQA_HEADLINE_RUN", "1")
|
||
result = _run_validation()
|
||
assert result.completion.ok, (
|
||
f"headline validation run failed: {result.completion}"
|
||
)
|
||
|
||
|
||
# ── sweep.json shape ──────────────────────────────────────────────────
|
||
|
||
|
||
def test_sweep_json_has_expected_panels(monkeypatch):
|
||
data = _sweep_json(monkeypatch)
|
||
assert set(data["panels"]) == set(PANELS), (
|
||
f"panels mismatch: expected {set(PANELS)}, got {set(data['panels'])}"
|
||
)
|
||
assert len(data["rows"]) == len(PANELS)
|
||
assert {r["panel"] for r in data["rows"]} == set(PANELS)
|
||
|
||
|
||
def test_decode_panels_use_real_gqa(monkeypatch):
|
||
"""ADR-0060 §A.1 headline: decode panels must use h_q = G·h_kv with G>1."""
|
||
data = _sweep_json(monkeypatch)
|
||
cfg = data["config"]
|
||
assert cfg["h_q_decode"] > cfg["h_kv_decode"], (
|
||
f"decode must use real GQA (h_q > h_kv); got "
|
||
f"h_q={cfg['h_q_decode']}, h_kv={cfg['h_kv_decode']}"
|
||
)
|
||
|
||
|
||
# ── Per-panel architectural assertions ────────────────────────────────
|
||
|
||
|
||
def _row(rows, panel: str) -> dict:
|
||
for r in rows:
|
||
if r["panel"] == panel:
|
||
return r
|
||
raise AssertionError(f"missing row for panel {panel!r}")
|
||
|
||
|
||
def test_prefill_ring_panel_has_ipcq_traffic(monkeypatch):
|
||
"""multi_user_prefill_gqa uses Ring KV → IPCQ traffic > 0."""
|
||
data = _sweep_json(monkeypatch)
|
||
row = _row(data["rows"], "multi_user_prefill_gqa")
|
||
n_copy = row["op_log_summary"].get("ipcq_copy_count", 0)
|
||
assert n_copy > 0, (
|
||
f"multi_user_prefill_gqa must have Ring KV traffic; got "
|
||
f"ipcq_copy_count={n_copy}"
|
||
)
|
||
|
||
|
||
def test_decode_reduce_panel_writes_once(monkeypatch):
|
||
"""multi_user_decode_gqa: chain reduce-to-root → exactly 1 dma_write."""
|
||
data = _sweep_json(monkeypatch)
|
||
row = _row(data["rows"], "multi_user_decode_gqa")
|
||
n_writes = row["op_log_summary"]["dma_write_count"]
|
||
assert n_writes == 1, (
|
||
f"multi_user_decode_gqa root-only write: expected 1; got {n_writes}"
|
||
)
|
||
|
||
|
||
def test_prefill_panel_distributes_output(monkeypatch):
|
||
"""multi_user_prefill_gqa: per-CUBE distributed output → dma_write_count == C."""
|
||
data = _sweep_json(monkeypatch)
|
||
row = _row(data["rows"], "multi_user_prefill_gqa")
|
||
n_writes = row["op_log_summary"]["dma_write_count"]
|
||
assert n_writes == 4, (
|
||
f"multi_user_prefill_gqa per-CUBE distributed: expected 4; got {n_writes}"
|
||
)
|