Compare commits
3 Commits
359a0eaa44
...
77eece81c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 77eece81c4 | |||
| 443ede99c7 | |||
| e45626c036 |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,101 +0,0 @@
|
||||
"""Report harness: GQA end-to-end latency + op/engine breakdown.
|
||||
|
||||
Isolated under ``scripts/paper/`` for the 1H codesign report only — it is
|
||||
NOT a registered bench and does not touch other people's benches. It
|
||||
reuses the existing GQA headline panels (the real GQA kernels wired in
|
||||
``milestone_gqa_headline``) but, unlike that milestone (which records only
|
||||
op-counts), it also harvests per-panel end-to-end latency and per-engine
|
||||
occupancy from ``result.engine.op_log``.
|
||||
|
||||
Latency definition (same window convention as ``milestone_1h_gemm``):
|
||||
end_to_end_ns = max(r.t_end) - min(r.t_start) over all op_log records.
|
||||
|
||||
Output: docs/report/1H-codesign-paper/figures/gqa_latency.json
|
||||
|
||||
Run:
|
||||
python scripts/paper/paper_gqa_latency.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches.milestone_gqa_headline import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
_make_bench_fn,
|
||||
_summarize_op_log,
|
||||
)
|
||||
|
||||
_REPORT_DIR = Path(__file__).resolve().parents[2] / "docs" / "report" / "1H-codesign-paper"
|
||||
_FIG_DIR = _REPORT_DIR / "figures"
|
||||
_OUT_JSON = _FIG_DIR / "gqa_latency.json"
|
||||
|
||||
# PE engine component suffixes whose occupancy we break out.
|
||||
_ENGINES = ("pe_gemm", "pe_math", "pe_dma", "pe_fetch_store", "pe_ipcq", "pe_cpu")
|
||||
|
||||
|
||||
def _occupancy_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 _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 _run_panel(panel: str, 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=_make_bench_fn(panel),
|
||||
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"panel {panel!r} failed: {result.completion}")
|
||||
|
||||
op_log = result.engine.op_log
|
||||
kind, params = _PANEL_DISPATCH[panel]
|
||||
return {
|
||||
"panel": panel,
|
||||
"kind": kind,
|
||||
**params,
|
||||
"latency_ns": _end_to_end_ns(op_log),
|
||||
"op_log_summary": _summarize_op_log(op_log),
|
||||
"engine_occupancy_ns": {
|
||||
eng: _occupancy_ns(op_log, eng) for eng in _ENGINES
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
topology = "topology.yaml"
|
||||
rows = [_run_panel(panel, topology) for panel in _PANELS]
|
||||
_FIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out = {"version": 1, "panels": list(_PANELS), "rows": rows}
|
||||
_OUT_JSON.write_text(json.dumps(out, indent=2))
|
||||
print(f"wrote {_OUT_JSON}")
|
||||
for r in rows:
|
||||
s = r["op_log_summary"]
|
||||
print(
|
||||
f" {r['panel']:24s} latency={r['latency_ns']:10.1f} ns "
|
||||
f"gemm={s['gemm_count']:3d} ipcq={s['ipcq_copy_count']:3d} "
|
||||
f"dma_rd={s['dma_read_count']:3d} dma_wr={s['dma_write_count']:2d}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Comparative figures for milestone-gqa-decode-long-ctx-4cases.
|
||||
|
||||
Reads sweep.json (emitted by ``kernbench run --bench
|
||||
milestone-gqa-decode-long-ctx-4cases``) and writes three PNGs into
|
||||
milestone-gqa-decode-long-ctx-4cases``) and writes four PNGs into
|
||||
``docs/report/1H-codesign-paper/figures/``:
|
||||
|
||||
gqa_decode_long_ctx_4cases_latency.png end-to-end latency per case
|
||||
gqa_decode_long_ctx_4cases_traffic.png ipcq/dma op-count breakdown
|
||||
gqa_decode_long_ctx_4cases_memory.png KV bytes per cube per case
|
||||
gqa_decode_long_ctx_4cases_memory.png per-PE KV bytes per case
|
||||
gqa_decode_long_ctx_4cases_parallelism.png per-PE S_local (compute work)
|
||||
|
||||
Run (after the bench):
|
||||
GQA_DECODE_LONG_CTX_4CASES_RUN=1 python -m kernbench.cli.main run \\
|
||||
@@ -24,11 +25,12 @@ matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
_FIG_DIR = _REPO_ROOT / "docs" / "report" / "1H-codesign-paper" / "figures"
|
||||
_SWEEP_JSON = (
|
||||
# Sweep JSON + PNGs live together under the bench output dir.
|
||||
_FIG_DIR = (
|
||||
_REPO_ROOT / "src" / "kernbench" / "benches"
|
||||
/ "1H_milestone_output" / "gqa_decode_long_ctx_4cases" / "sweep.json"
|
||||
/ "1H_milestone_output" / "gqa" / "long_ctx"
|
||||
)
|
||||
_SWEEP_JSON = _FIG_DIR / "sweep_decode.json"
|
||||
|
||||
# Panel name → (short label, case ordinal for left-to-right plot order).
|
||||
_CASE_INFO = {
|
||||
@@ -98,39 +100,63 @@ def _plot_traffic(rows: list[dict]) -> Path:
|
||||
return out
|
||||
|
||||
|
||||
def _kv_bytes_per_cube(panel: str, *, S_kv: int, h_kv: int,
|
||||
d_head: int, C: int) -> int:
|
||||
"""KV bytes a single cube's HBM holds (K + V together, f16)."""
|
||||
# Cube-replicate ⇒ each cube holds full S_kv.
|
||||
# Cube-SP ⇒ each cube holds S_kv / C.
|
||||
# PE replicate vs row_wise share the cube's HBM and don't change
|
||||
# per-cube bytes.
|
||||
def _s_local_per_pe(panel: str, *, S_kv: int, C: int, P: int) -> int:
|
||||
"""S_local (token count) each PE attends over locally.
|
||||
|
||||
Encodes the cube/pe sharding axes from the panel name:
|
||||
cube_sp_pe_tp (Case 1): S_kv / C (pe=replicate within cube)
|
||||
cube_repl_pe_tp (Case 2): S_kv (pe=replicate; only 1 PE works)
|
||||
cube_repl_pe_sp (Case 3): S_kv / P (pe=row_wise within cube)
|
||||
cube_sp_pe_sp (Case 4): S_kv / (C·P) (★ 64-way split)
|
||||
"""
|
||||
S_per_cube = S_kv if "cube_repl" in panel else S_kv // C
|
||||
return 2 * S_per_cube * h_kv * d_head * 2 # K + V, f16 (2 B/elem)
|
||||
return S_per_cube // P if "pe_sp" in panel else S_per_cube
|
||||
|
||||
|
||||
def _active_pe_count(panel: str, *, C: int, P: int) -> int:
|
||||
"""Number of PEs doing non-idle attention work.
|
||||
|
||||
cube_sp_pe_tp (Case 1): C (PE 0 of each cube; 7 PEs idle per cube)
|
||||
cube_repl_pe_tp (Case 2): 1 (only PE 0 of CUBE 0)
|
||||
cube_repl_pe_sp (Case 3): C·P (all PEs busy, but cubes are redundant)
|
||||
cube_sp_pe_sp (Case 4): C·P (all 64 PEs doing unique work)
|
||||
"""
|
||||
if "cube_repl" in panel and "pe_tp" in panel:
|
||||
return 1
|
||||
if "cube_sp" in panel and "pe_tp" in panel:
|
||||
return C
|
||||
return C * P
|
||||
|
||||
|
||||
def _kv_bytes_per_pe(panel: str, *, S_kv: int, h_kv: int,
|
||||
d_head: int, C: int, P: int) -> int:
|
||||
"""KV bytes a single PE references (K + V, f16, 2 B/elem)."""
|
||||
s_local = _s_local_per_pe(panel, S_kv=S_kv, C=C, P=P)
|
||||
return 2 * s_local * h_kv * d_head * 2
|
||||
|
||||
|
||||
def _plot_memory(rows: list[dict]) -> Path:
|
||||
"""Per-PE KV bytes — Case 4 wins (64-way split)."""
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
mib_per_cube = [
|
||||
_kv_bytes_per_cube(
|
||||
mib_per_pe = [
|
||||
_kv_bytes_per_pe(
|
||||
r["panel"], S_kv=r["S_kv"], h_kv=r["h_kv"],
|
||||
d_head=r["d_head"], C=r["C"],
|
||||
d_head=r["d_head"], C=r["C"], P=r["P"],
|
||||
) / (1024 * 1024)
|
||||
for r in rows
|
||||
]
|
||||
# SP = blue (efficient); Repl = red (wasteful).
|
||||
colors = ["#3b6ea5", "#c0504d", "#c0504d", "#3b6ea5"]
|
||||
colors = ["#888", "#c0504d", "#888", "#3b6ea5"] # 4 highlighted, 2 marked red
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.5))
|
||||
bars = ax.bar(labels, mib_per_cube, color=colors, width=0.6)
|
||||
ax.set_ylabel("KV bytes per cube (MiB, K + V, f16)")
|
||||
bars = ax.bar(labels, mib_per_pe, color=colors, width=0.6)
|
||||
ax.set_ylabel("KV bytes per PE (MiB, K + V, f16)")
|
||||
ax.set_title(
|
||||
"Long-context decode 4-cases — KV memory per cube\n"
|
||||
"Long-context decode 4-cases — KV memory per PE\n"
|
||||
"(one KV-head group; per-layer, per-token state)"
|
||||
)
|
||||
ax.bar_label(bars, fmt="%.1f", padding=3, fontsize=9)
|
||||
ax.bar_label(bars, fmt="%.3f", padding=3, fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
ax.set_ylim(0, max(mib_per_cube) * 1.15)
|
||||
ax.set_ylim(0, max(mib_per_pe) * 1.15)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_decode_long_ctx_4cases_memory.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
@@ -138,15 +164,44 @@ def _plot_memory(rows: list[dict]) -> Path:
|
||||
return out
|
||||
|
||||
|
||||
def _plot_parallelism(rows: list[dict]) -> Path:
|
||||
"""Total active PE-token compute load — exposes Case 3's redundancy."""
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
total_work = [
|
||||
_active_pe_count(r["panel"], C=r["C"], P=r["P"])
|
||||
* _s_local_per_pe(r["panel"], S_kv=r["S_kv"], C=r["C"], P=r["P"])
|
||||
for r in rows
|
||||
]
|
||||
colors = ["#888", "#888", "#c0504d", "#3b6ea5"] # 4 highlighted, 3 marked red
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.5))
|
||||
bars = ax.bar(labels, total_work, color=colors, width=0.6)
|
||||
ax.set_ylabel("active-PE × S_local (PE-tokens; lower ⇒ less wasted work)")
|
||||
ax.set_title(
|
||||
"Long-context decode 4-cases — total compute load across active PEs\n"
|
||||
"(Case 3 replicates the full K/V across 8 cubes ⇒ 8× wasted PE-tokens)"
|
||||
)
|
||||
ax.bar_label(bars, fmt="%d", padding=3, fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
ax.set_ylim(0, max(total_work) * 1.15)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_decode_long_ctx_4cases_parallelism.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rows = _load()
|
||||
_FIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p1 = _plot_latency(rows)
|
||||
p2 = _plot_traffic(rows)
|
||||
p3 = _plot_memory(rows)
|
||||
p4 = _plot_parallelism(rows)
|
||||
print(f"wrote {p1}")
|
||||
print(f"wrote {p2}")
|
||||
print(f"wrote {p3}")
|
||||
print(f"wrote {p4}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""Comparative figures for milestone-gqa-prefill-long-ctx-4cases.
|
||||
|
||||
Mirror of paper_plot_gqa_decode_long_ctx_4cases but for the prefill
|
||||
variant. Reads sweep.json (emitted by ``kernbench run --bench
|
||||
milestone-gqa-prefill-long-ctx-4cases``) and writes four PNGs into
|
||||
``docs/report/1H-codesign-paper/figures/``:
|
||||
|
||||
gqa_prefill_long_ctx_4cases_latency.png end-to-end latency per case
|
||||
gqa_prefill_long_ctx_4cases_traffic.png ipcq/dma op-count breakdown
|
||||
gqa_prefill_long_ctx_4cases_memory.png per-PE KV bytes per case
|
||||
gqa_prefill_long_ctx_4cases_parallelism.png active-PE × S_local work load
|
||||
|
||||
Run (after the bench):
|
||||
GQA_PREFILL_LONG_CTX_4CASES_RUN=1 python -m kernbench.cli.main run \\
|
||||
--bench milestone-gqa-prefill-long-ctx-4cases --topology topology.yaml
|
||||
python scripts/paper/paper_plot_gqa_prefill_long_ctx_4cases.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]
|
||||
# Sweep JSON + PNGs live together under the bench output dir.
|
||||
_FIG_DIR = (
|
||||
_REPO_ROOT / "src" / "kernbench" / "benches"
|
||||
/ "1H_milestone_output" / "gqa" / "long_ctx"
|
||||
)
|
||||
_SWEEP_JSON = _FIG_DIR / "sweep_prefill.json"
|
||||
|
||||
# Panel name → (short label, case ordinal for left-to-right plot order).
|
||||
_CASE_INFO = {
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp": (
|
||||
"Case 1\nCube-SP × PE-TP", 1),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp": (
|
||||
"Case 2\nCube-Repl × PE-TP", 2),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp": (
|
||||
"Case 3\nCube-Repl × PE-SP", 3),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp": (
|
||||
"Case 4 ★\nCube-SP × PE-SP", 4),
|
||||
}
|
||||
|
||||
|
||||
def _load() -> list[dict]:
|
||||
return json.loads(_SWEEP_JSON.read_text())["rows"]
|
||||
|
||||
|
||||
def _sorted_by_case(rows: list[dict]) -> list[dict]:
|
||||
return sorted(rows, key=lambda r: _CASE_INFO[r["panel"]][1])
|
||||
|
||||
|
||||
def _plot_latency(rows: list[dict]) -> Path:
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
lat_us = [r["latency_ns"] / 1e3 for r in rows]
|
||||
colors = ["#888", "#888", "#888", "#3b6ea5"] # Case 4 highlighted
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.5))
|
||||
bars = ax.bar(labels, lat_us, color=colors, width=0.6)
|
||||
ax.set_ylabel("end-to-end latency (µs)")
|
||||
ax.set_title(
|
||||
"Long-context prefill 4-cases — end-to-end latency per case\n"
|
||||
"LLaMA-3.1-70B single-KV-head group (8 cubes × 8 PEs)"
|
||||
)
|
||||
ax.bar_label(bars, fmt="%.1f", padding=3, fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
ax.set_ylim(0, max(lat_us) * 1.15)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_prefill_long_ctx_4cases_latency.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
return out
|
||||
|
||||
|
||||
def _plot_traffic(rows: list[dict]) -> Path:
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
x = list(range(len(rows)))
|
||||
keys = ["ipcq_copy_count", "dma_read_count", "dma_write_count"]
|
||||
disp = ["IPCQ copy", "DMA read", "DMA write"]
|
||||
colors = ["#c0504d", "#9bbb59", "#8064a2"]
|
||||
w = 0.25
|
||||
fig, ax = plt.subplots(figsize=(9.0, 4.5))
|
||||
for i, (k, d, c) in enumerate(zip(keys, disp, colors)):
|
||||
vals = [r["op_log_summary"][k] for r in rows]
|
||||
ax.bar([xi + (i - 1) * w for xi in x], vals, width=w, label=d, color=c)
|
||||
ax.set_xticks(list(x))
|
||||
ax.set_xticklabels(labels, fontsize=9)
|
||||
ax.set_ylabel("op count")
|
||||
ax.set_title("Long-context prefill 4-cases — op-count breakdown per case")
|
||||
ax.legend(fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_prefill_long_ctx_4cases_traffic.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
return out
|
||||
|
||||
|
||||
def _s_local_per_pe(panel: str, *, S_kv: int, C: int, P: int) -> int:
|
||||
"""S_local (token count) each PE attends over locally.
|
||||
|
||||
Encodes the cube/pe sharding axes from the panel name:
|
||||
cube_sp_pe_tp (Case 1): S_kv / C (pe=replicate within cube)
|
||||
cube_repl_pe_tp (Case 2): S_kv (full S_kv per active PE)
|
||||
cube_repl_pe_sp (Case 3): S_kv / P (pe=row_wise within cube)
|
||||
cube_sp_pe_sp (Case 4): S_kv / (C·P) (★ 64-way split)
|
||||
"""
|
||||
S_per_cube = S_kv if "cube_repl" in panel else S_kv // C
|
||||
return S_per_cube // P if "pe_sp" in panel else S_per_cube
|
||||
|
||||
|
||||
def _active_pe_count(panel: str, *, C: int, P: int) -> int:
|
||||
"""Number of PEs doing non-idle attention work.
|
||||
|
||||
Prefill T_q≫1 means PE-TP is *useful* (not wasted as in decode):
|
||||
cube_sp_pe_tp (Case 1): C·P (T_q sharded across all 64 ranks)
|
||||
cube_repl_pe_tp (Case 2): P (CUBE 0 only; T_q sharded across its P PEs)
|
||||
cube_repl_pe_sp (Case 3): C·P (all PEs busy but cubes redundant)
|
||||
cube_sp_pe_sp (Case 4): C·P (all 64 PEs doing unique work)
|
||||
"""
|
||||
if "cube_repl" in panel and "pe_tp" in panel:
|
||||
return P
|
||||
return C * P
|
||||
|
||||
|
||||
def _kv_bytes_per_pe(panel: str, *, S_kv: int, h_kv: int,
|
||||
d_head: int, C: int, P: int) -> int:
|
||||
"""KV bytes a single PE references (K + V, f16, 2 B/elem)."""
|
||||
s_local = _s_local_per_pe(panel, S_kv=S_kv, C=C, P=P)
|
||||
return 2 * s_local * h_kv * d_head * 2
|
||||
|
||||
|
||||
def _plot_memory(rows: list[dict]) -> Path:
|
||||
"""Per-PE KV bytes — Case 4 wins (64-way split)."""
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
mib_per_pe = [
|
||||
_kv_bytes_per_pe(
|
||||
r["panel"], S_kv=r["S_kv"], h_kv=r["h_kv"],
|
||||
d_head=r["d_head"], C=r["C"], P=r["P"],
|
||||
) / (1024 * 1024)
|
||||
for r in rows
|
||||
]
|
||||
colors = ["#888", "#c0504d", "#888", "#3b6ea5"]
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.5))
|
||||
bars = ax.bar(labels, mib_per_pe, color=colors, width=0.6)
|
||||
ax.set_ylabel("KV bytes per PE (MiB, K + V, f16)")
|
||||
ax.set_title(
|
||||
"Long-context prefill 4-cases — KV memory per PE\n"
|
||||
"(one KV-head group; per-layer, full S_kv state)"
|
||||
)
|
||||
ax.bar_label(bars, fmt="%.3f", padding=3, fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
ax.set_ylim(0, max(mib_per_pe) * 1.15)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_prefill_long_ctx_4cases_memory.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
return out
|
||||
|
||||
|
||||
def _t_q_per_pe(panel: str, *, T_q: int, C: int, P: int) -> int:
|
||||
"""T_q row count each active PE computes attention for.
|
||||
|
||||
cube_sp_pe_tp (Case 1): T_q / (C·P) (T_q sharded across all 64 ranks)
|
||||
cube_repl_pe_tp (Case 2): T_q / P (T_q sharded across CUBE 0's P PEs)
|
||||
cube_repl_pe_sp (Case 3): T_q (Q replicated on every PE)
|
||||
cube_sp_pe_sp (Case 4): T_q / C (Q sharded by cube, replicated within)
|
||||
"""
|
||||
if "cube_sp" in panel and "pe_tp" in panel:
|
||||
return T_q // (C * P)
|
||||
if "cube_repl" in panel and "pe_tp" in panel:
|
||||
return T_q // P
|
||||
if "cube_repl" in panel and "pe_sp" in panel:
|
||||
return T_q
|
||||
return T_q // C # cube_sp_pe_sp
|
||||
|
||||
|
||||
def _s_kv_processed_per_pe(panel: str, *, S_kv: int, C: int, P: int) -> int:
|
||||
"""S_kv tokens each PE actually processes attention over.
|
||||
|
||||
Differs from ``_s_local_per_pe`` (OWNED KV bytes): for cases with
|
||||
a Ring (Case 1, 4) each PE sees C ring steps so processes more
|
||||
tokens than it locally owns.
|
||||
|
||||
cube_sp_pe_tp (Case 1): S_kv (Ring + pe=replicate within cube)
|
||||
cube_repl_pe_tp (Case 2): S_kv (full KV per PE)
|
||||
cube_repl_pe_sp (Case 3): S_kv / P (pe=row_wise; no Ring)
|
||||
cube_sp_pe_sp (Case 4): S_kv / P (Ring restores full S_kv/P per PE)
|
||||
"""
|
||||
if "cube_sp" in panel and "pe_tp" in panel:
|
||||
return S_kv
|
||||
if "cube_repl" in panel and "pe_tp" in panel:
|
||||
return S_kv
|
||||
return S_kv // P # both pe_sp cases
|
||||
|
||||
|
||||
def _plot_parallelism(rows: list[dict]) -> Path:
|
||||
"""Total compute work (PE × T_q × S_kv token-pairs) — exposes Case 3's
|
||||
redundancy. Cases 1, 2, 4 all do the same total work (correct
|
||||
attention over T_q × S_kv); Case 3 does C× more (cubes redundantly
|
||||
repeat the same compute because K/V is cube-replicated).
|
||||
"""
|
||||
rows = _sorted_by_case(rows)
|
||||
labels = [_CASE_INFO[r["panel"]][0] for r in rows]
|
||||
total_work = [
|
||||
_active_pe_count(r["panel"], C=r["C"], P=r["P"])
|
||||
* _t_q_per_pe(r["panel"], T_q=r["T_q"], C=r["C"], P=r["P"])
|
||||
* _s_kv_processed_per_pe(
|
||||
r["panel"], S_kv=r["S_kv"], C=r["C"], P=r["P"],
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
colors = ["#888", "#888", "#c0504d", "#3b6ea5"] # Case 3 red, Case 4 highlighted
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.5))
|
||||
bars = ax.bar(labels, total_work, color=colors, width=0.6)
|
||||
ax.set_ylabel(
|
||||
"total compute (PE × T_q × S_kv token-pairs; lower ⇒ less wasted work)"
|
||||
)
|
||||
ax.set_title(
|
||||
"Long-context prefill 4-cases — total compute work across active PEs\n"
|
||||
"(Case 3 replicates K/V across 8 cubes ⇒ 8× redundant compute)"
|
||||
)
|
||||
ax.bar_label(bars, fmt="%d", padding=3, fontsize=9)
|
||||
ax.grid(axis="y", ls=":", alpha=0.5)
|
||||
ax.set_ylim(0, max(total_work) * 1.15)
|
||||
fig.tight_layout()
|
||||
out = _FIG_DIR / "gqa_prefill_long_ctx_4cases_parallelism.png"
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rows = _load()
|
||||
_FIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p1 = _plot_latency(rows)
|
||||
p2 = _plot_traffic(rows)
|
||||
p3 = _plot_memory(rows)
|
||||
p4 = _plot_parallelism(rows)
|
||||
print(f"wrote {p1}")
|
||||
print(f"wrote {p2}")
|
||||
print(f"wrote {p3}")
|
||||
print(f"wrote {p4}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"version": 1,
|
||||
"panels": [
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_sp_pe_sp",
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_tp",
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_sp",
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_sp_pe_tp"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"panel": "single_kv_group_decode_long_ctx_gqa_cube_sp_pe_sp",
|
||||
"kind": "decode_long_ctx_cube_sp_pe_sp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 1,
|
||||
"S_kv": 8192,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 128,
|
||||
"ipcq_copy_count": 189,
|
||||
"dma_read_count": 192,
|
||||
"dma_write_count": 1
|
||||
},
|
||||
"latency_ns": 34030.00300000079,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 4194.303999999538,
|
||||
"pe_math": 2282.0,
|
||||
"pe_dma": 1138201.8199999991,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_decode_long_ctx_gqa_cube_repl_pe_tp",
|
||||
"kind": "decode_long_ctx_cube_repl_pe_tp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 1,
|
||||
"S_kv": 8192,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 16,
|
||||
"ipcq_copy_count": 0,
|
||||
"dma_read_count": 17,
|
||||
"dma_write_count": 1
|
||||
},
|
||||
"latency_ns": 25428.38200085424,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 4194.303999997675,
|
||||
"pe_math": 714.0,
|
||||
"pe_dma": 18217.080000881106,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_decode_long_ctx_gqa_cube_repl_pe_sp",
|
||||
"kind": "decode_long_ctx_cube_repl_pe_sp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 1,
|
||||
"S_kv": 8192,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 128,
|
||||
"ipcq_copy_count": 168,
|
||||
"dma_read_count": 192,
|
||||
"dma_write_count": 1
|
||||
},
|
||||
"latency_ns": 20152.09400000231,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 33554.431999996305,
|
||||
"pe_math": 5684.0,
|
||||
"pe_dma": 853218.7100000716,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_decode_long_ctx_gqa_cube_sp_pe_tp",
|
||||
"kind": "decode_long_ctx_cube_sp_pe_tp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 1,
|
||||
"S_kv": 8192,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 16,
|
||||
"ipcq_copy_count": 21,
|
||||
"dma_read_count": 24,
|
||||
"dma_write_count": 1
|
||||
},
|
||||
"latency_ns": 26987.291500001098,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 4194.303999999538,
|
||||
"pe_math": 714.0,
|
||||
"pe_dma": 126624.99000000354,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"version": 1,
|
||||
"panels": [
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp",
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp",
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp",
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"panel": "single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp",
|
||||
"kind": "prefill_long_ctx_cube_sp_pe_sp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 512,
|
||||
"S_kv": 512,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 1024,
|
||||
"ipcq_copy_count": 1064,
|
||||
"dma_read_count": 192,
|
||||
"dma_write_count": 8
|
||||
},
|
||||
"latency_ns": 86702.76799999712,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 134217.72799998685,
|
||||
"pe_math": 546656.0,
|
||||
"pe_dma": 2388650.3709999938,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp",
|
||||
"kind": "prefill_long_ctx_cube_repl_pe_tp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 512,
|
||||
"S_kv": 512,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 1024,
|
||||
"ipcq_copy_count": 0,
|
||||
"dma_read_count": 1088,
|
||||
"dma_write_count": 64
|
||||
},
|
||||
"latency_ns": 93564.33200003332,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 134217.72799998525,
|
||||
"pe_math": 81279.99999999997,
|
||||
"pe_dma": 177951.73600022023,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp",
|
||||
"kind": "prefill_long_ctx_cube_repl_pe_sp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 512,
|
||||
"S_kv": 512,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 8192,
|
||||
"ipcq_copy_count": 10752,
|
||||
"dma_read_count": 12288,
|
||||
"dma_write_count": 64
|
||||
},
|
||||
"latency_ns": 190579.57100032456,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 1073741.8240003586,
|
||||
"pe_math": 635904.0,
|
||||
"pe_dma": 1756854.05751659,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel": "single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp",
|
||||
"kind": "prefill_long_ctx_cube_sp_pe_tp",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 512,
|
||||
"S_kv": 512,
|
||||
"d_head": 128,
|
||||
"h_q": 8,
|
||||
"h_kv": 1,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 1024,
|
||||
"ipcq_copy_count": 896,
|
||||
"dma_read_count": 192,
|
||||
"dma_write_count": 64
|
||||
},
|
||||
"latency_ns": 38876.32300000038,
|
||||
"engine_occupancy_ns": {
|
||||
"pe_gemm": 134217.7280000001,
|
||||
"pe_math": 81280.0,
|
||||
"pe_dma": 916284.0330000015,
|
||||
"pe_fetch_store": 0,
|
||||
"pe_ipcq": 0,
|
||||
"pe_cpu": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"failures": []
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"panels": [
|
||||
"single_kv_group_prefill_gqa_c8_p8"
|
||||
],
|
||||
"config": {
|
||||
"T_q_prefill": 4,
|
||||
"T_q_decode": 1,
|
||||
"S_kv_prefill": 16,
|
||||
"h_q_decode": 8,
|
||||
"h_kv_decode": 1,
|
||||
"d_head": 64
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"panel": "single_kv_group_prefill_gqa_c8_p8",
|
||||
"kind": "prefill",
|
||||
"C": 8,
|
||||
"P": 8,
|
||||
"T_q": 1024,
|
||||
"S_kv": 1024,
|
||||
"d_head": 128,
|
||||
"op_log_summary": {
|
||||
"gemm_count": 1024,
|
||||
"ipcq_copy_count": 896,
|
||||
"dma_read_count": 192,
|
||||
"dma_write_count": 64
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
"""GQA fused-attention prefill kernel — long context (ADR-0060 §5.5).
|
||||
|
||||
Head-parallel: each CUBE owns one Q head and one KV slice. Over C ring
|
||||
steps the KV blocks rotate around the C CUBEs (W-send / E-recv) so every
|
||||
CUBE sees every block; the online-softmax merge folds each step into the
|
||||
running ``(m, ℓ, O)``. No inter-CUBE reduce — each CUBE writes its own
|
||||
head's output.
|
||||
|
||||
The ring is **tile-granular** (ADR-0060 §5.5 + ADR-0063 §A.2): each
|
||||
ring step transmits ``n_tiles`` tiles of size ``TILE_S_KV`` rather than
|
||||
one full ``S_local`` slice. The kernel loop is nested over
|
||||
``(ring_step, tile_idx)``, with the bootstrap at ``(k=0, t=0)``
|
||||
establishing the persistent ``(m, ℓ, O)`` and every subsequent
|
||||
iteration folding its tile in inside a ``tl.scratch_scope``. Per-rank
|
||||
persistent scratch is ``(m, ℓ, O)`` only (~1 KB); per-tile in-scope
|
||||
scratch is bounded by ``TILE_S_KV`` regardless of ``S_local``.
|
||||
|
||||
Topology / SFR:
|
||||
- Requires ``configure_sfr_intercube_ring`` — either ``ring_size=C``
|
||||
(1D row, ``C ≤ mesh_w``) or ``submesh_shape=(rows, cols)`` (snake
|
||||
Hamiltonian cycle through a rectangular sub-mesh, for ``C > mesh_w``).
|
||||
- ``P == 1`` (default): only PE 0 of each CUBE participates;
|
||||
intra-CUBE PE parallelism is disabled.
|
||||
- ``P > 1``: all P PEs of each CUBE participate via query-axis
|
||||
split (ADR-0060 §5.5 last bullet + §B-item-3) — each PE owns
|
||||
``T_q/P`` disjoint query rows. Output rows are disjoint per PE,
|
||||
so no intra-CUBE reduce is needed. Each PE drives its own
|
||||
same-lane ring via independent IPCQ channels (P parallel rings).
|
||||
Requires ``T_q % P == 0``.
|
||||
|
||||
Layout caveats:
|
||||
- GEMMs use ``tl.dot`` (no composite epilogue / ``softmax_scale``).
|
||||
- K loaded as ``(d_head, tile_s)`` via byte-conserving reshape of
|
||||
the deployed ``(tile_s, d_head)`` shard (ADR-0060 §3
|
||||
reshape-not-transpose caveat).
|
||||
- No causal masking / step-skip; blocking ``tl.recv``.
|
||||
- ``TILE_S_KV`` is assumed to divide ``S_local`` evenly; last-tile
|
||||
padding is not modelled.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TILE_S_KV = 1024 # ADR-0063 §A.2 S_kv-axis tile sweep (per-tile width).
|
||||
|
||||
|
||||
def _merge_running(m, l, O, m_step, l_step, O_step, *, tl):
|
||||
"""Online-softmax merge of two partial ``(m, ℓ, O)`` triples."""
|
||||
m_new = tl.maximum(m, m_step)
|
||||
scale_old = tl.exp(m - m_new)
|
||||
scale_step = tl.exp(m_step - m_new)
|
||||
l_new = l * scale_old + l_step * scale_step
|
||||
O_new = O * scale_old + O_step * scale_step
|
||||
return m_new, l_new, O_new
|
||||
|
||||
|
||||
def gqa_attention_prefill_long_kernel(
|
||||
q_ptr: int,
|
||||
k_ptr: int,
|
||||
v_ptr: int,
|
||||
o_ptr: int,
|
||||
T_q: int,
|
||||
S_kv: int,
|
||||
d_head: int,
|
||||
C: int,
|
||||
P: int = 1,
|
||||
*,
|
||||
tl,
|
||||
) -> None:
|
||||
"""Head-parallel prefill attention with tile-granular Ring KV (ADR-0060 §5.5).
|
||||
|
||||
Tensor layout (per-rank shapes):
|
||||
Q : (T_q_local, d_head) one head per CUBE; replicated within
|
||||
a CUBE for P=1, or sharded ``pe="row_wise"`` for P>1 so each
|
||||
PE owns ``T_q_local = T_q // P`` query rows.
|
||||
K : (S_kv, d_head) sharded cube_row_wise → each CUBE owns
|
||||
(S_local, d_head). Tiles loaded as (d_head, tile_s) via
|
||||
byte-conserving reshape.
|
||||
V : (S_kv, d_head) sharded cube_row_wise → each CUBE owns
|
||||
(S_local, d_head). Tiles loaded as (tile_s, d_head).
|
||||
O : (T_q * C, d_head) sharded cube_row_wise → each CUBE
|
||||
writes its own ``T_q`` rows; for P>1 each PE writes a
|
||||
disjoint ``(T_q_local, d_head)`` slice (no intra-CUBE
|
||||
reduce — disjoint output rows). NO inter-CUBE reduce.
|
||||
|
||||
Algorithm: nested loop over (ring_step k, tile_idx t). At k=0 each
|
||||
rank loads its own block's tiles from HBM; at k > 0 it receives
|
||||
tiles from its E neighbour. Tiles are forwarded W to the next
|
||||
ring step. Each tile's partial is folded into the running
|
||||
(m, ℓ, O) via online-softmax merge.
|
||||
"""
|
||||
pe_id = tl.program_id(axis=0)
|
||||
if P == 1:
|
||||
# Head-parallel only: PE 0 of each CUBE participates.
|
||||
if pe_id != 0:
|
||||
return
|
||||
T_q_local = T_q
|
||||
else:
|
||||
# Intra-CUBE PE-SP (ADR-0060 §5.5 last bullet + §B-item-3):
|
||||
# query-axis split across P PEs of each CUBE. Output rows are
|
||||
# disjoint per PE ⇒ no intra-CUBE reduce. Each PE drives its
|
||||
# own same-lane ring via independent IPCQ channels.
|
||||
if pe_id >= P:
|
||||
return
|
||||
if T_q % P != 0:
|
||||
raise ValueError(
|
||||
f"T_q={T_q} must be divisible by P={P} when P > 1; "
|
||||
"use P=1 for the PE-0-only fallback path."
|
||||
)
|
||||
T_q_local = T_q // P
|
||||
|
||||
S_local = S_kv // C
|
||||
n_tiles = (S_local + TILE_S_KV - 1) // TILE_S_KV
|
||||
KV_ROW_BYTES = d_head * 2 # f16
|
||||
Q = tl.load(q_ptr, shape=(T_q_local, d_head), dtype="f16")
|
||||
|
||||
# ── Bootstrap: (t=0, k=0) — load my own tile 0, establish (m, ℓ, O) ──
|
||||
#
|
||||
# Cannot be folded into the (t, k) loop (kernbench-only limitation):
|
||||
# - persistent (m, ℓ, O) must live OUTSIDE ``tl.scratch_scope``,
|
||||
# otherwise scope teardown discards them before the next tile's
|
||||
# merge can read them;
|
||||
# - kernbench has no scratch-backed initializer — ``tl.zeros`` /
|
||||
# ``tl.full`` return addr=0 handles with no backing storage, so
|
||||
# they cannot be overwritten via ``tl.copy_to`` to seed (-inf, 0, 0);
|
||||
# - the ring-step ordering also requires k=0 to send its loaded
|
||||
# tile W before any k>0 iteration can recv from E, so the very
|
||||
# first (t=0, k=0) step has to run outside the scoped loop body
|
||||
# where the send is conditional on ``C > 1``.
|
||||
# Triton port: limitation does not apply (SSA tensors stay live
|
||||
# across iterations); the bootstrap collapses into the loop.
|
||||
tile_s = min(TILE_S_KV, S_local)
|
||||
K_t = tl.load(k_ptr, shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr, shape=(tile_s, d_head), dtype="f16")
|
||||
if C > 1:
|
||||
tl.send(dir="W", src=K_t)
|
||||
tl.send(dir="W", src=V_t)
|
||||
scores = tl.dot(Q, K_t)
|
||||
m = tl.max(scores, axis=-1)
|
||||
exp_scores = tl.exp(scores - m)
|
||||
l = tl.sum(exp_scores, axis=-1)
|
||||
O = tl.dot(exp_scores, V_t)
|
||||
|
||||
# ── Nested loop: outer tile, inner ring step ──
|
||||
# Each tile propagates all the way through the ring before the next
|
||||
# tile starts; IPCQ in-flight depth stays at 1 per direction.
|
||||
#
|
||||
# Per outer ``t``:
|
||||
# k=0 : load my own tile ``t`` from HBM
|
||||
# k=1..C-1 : receive tile ``t`` of a peer's block from E
|
||||
# (sent by my E neighbour during their previous k step)
|
||||
# k<C-1 : forward the tile to W
|
||||
#
|
||||
# The ``(t=0, k=0)`` iteration is the bootstrap above; the loop skips
|
||||
# it via ``k_start``. Every other iteration uses ``scratch_scope`` +
|
||||
# ``copy_to`` to recycle per-tile intermediates.
|
||||
#
|
||||
# Triton port: drop ``with tl.scratch_scope():`` and replace each
|
||||
# ``copy_to`` with a Python rebind (e.g. ``m = m_new``).
|
||||
for t in range(n_tiles):
|
||||
tile_start = t * TILE_S_KV
|
||||
tile_s = min(TILE_S_KV, S_local - tile_start)
|
||||
k_start = 1 if t == 0 else 0
|
||||
for k in range(k_start, C):
|
||||
with tl.scratch_scope():
|
||||
if k == 0:
|
||||
K_t = tl.load(k_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(tile_s, d_head), dtype="f16")
|
||||
else:
|
||||
K_t = tl.recv(dir="E", shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.recv(dir="E", shape=(tile_s, d_head), dtype="f16")
|
||||
if k < C - 1:
|
||||
tl.send(dir="W", src=K_t)
|
||||
tl.send(dir="W", src=V_t)
|
||||
scores = tl.dot(Q, K_t)
|
||||
m_step = tl.max(scores, axis=-1)
|
||||
exp_scores = tl.exp(scores - m_step)
|
||||
l_step = tl.sum(exp_scores, axis=-1)
|
||||
O_step = tl.dot(exp_scores, V_t)
|
||||
m_new, l_new, O_new = _merge_running(
|
||||
m, l, O, m_step, l_step, O_step, tl=tl,
|
||||
)
|
||||
tl.copy_to(m, m_new)
|
||||
tl.copy_to(l, l_new)
|
||||
tl.copy_to(O, O_new)
|
||||
|
||||
# ── Final normalise + store (each CUBE writes its own head) ──
|
||||
O_final = O / l
|
||||
tl.store(o_ptr, O_final)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""GQA attention kernels, panel runners, and helpers (internal subpackage).
|
||||
|
||||
The bench-package auto-discovery in ``kernbench.benches.registry``
|
||||
skips subpackages, so this folder is not audited for ``@bench``
|
||||
decorators. The only public-facing GQA bench is
|
||||
``kernbench.benches.milestone_1h_gqa`` (umbrella that drives all
|
||||
panels by importing this subpackage's ``gqa_decode_long_ctx_4cases``
|
||||
and ``gqa_prefill_long_ctx_4cases`` modules).
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""GQA long-context kernels + 4-cases comparative-study runners."""
|
||||
@@ -0,0 +1,177 @@
|
||||
"""GQA prefill kernel — Case 3 (Cube-Repl × PE-SP) at single-KV-head group.
|
||||
|
||||
Per GQA_full_deck.pptx slide 11 (prefill counterpart, T_q ≫ 1):
|
||||
- K, V replicated across all 8 cubes (the 8× memory waste).
|
||||
- PEs SP on S_kv inside each cube: each PE attends to its
|
||||
``S_local = S_kv / P`` slice.
|
||||
- Q replicated on every rank (every cube does full T_q × S_local).
|
||||
- Intra-CUBE 8-way reduce on the partial ``(m, ℓ, O)`` triple
|
||||
per Q-tile (row chain along intra_W + col bridge along intra_N
|
||||
over the 2×4 PE grid).
|
||||
- NO inter-CUBE communication — every cube ends with the full
|
||||
answer (8× redundant compute is the inherent cost of Case 3).
|
||||
- Designated writer: only PE 0 of CUBE 0 stores ``O`` to avoid
|
||||
8 redundant DMA writes.
|
||||
|
||||
Q-axis tiling (vs decode Case 3): an outer Q-tile loop wraps the
|
||||
entire per-tile pipeline (Q load → bootstrap → S_kv-tile fold →
|
||||
intra-CUBE AR → store) in ``tl.scratch_scope``. Peak scratch is
|
||||
bounded by ``TILE_Q × TILE_S_KV`` instead of ``T_q × S_kv``, so the
|
||||
kernel runs at large T_q without exceeding the ~1 MB per-PE budget.
|
||||
Each Q-tile's (m, ℓ, O) is independent of other Q-tiles, so no
|
||||
state leaks across iterations.
|
||||
|
||||
Tensor layout:
|
||||
Q : (T_q, h_q · d_head) replicated on every rank; loaded per
|
||||
Q-tile as ``(TILE_Q, d_head)`` slices (G is folded into the
|
||||
T_q row axis, total ``G·T_q`` rows).
|
||||
K : (S_kv, h_kv · d_head) with cube=replicate, pe=row_wise — each
|
||||
PE owns its ``(S_local, h_kv·d_head)`` shard within its cube.
|
||||
V : same as K.
|
||||
O : (T_q, h_q · d_head) — only PE 0 of CUBE 0 stores, one
|
||||
``(TILE_Q, d_head)`` slice per outer Q-tile iteration.
|
||||
|
||||
Topology / SFR:
|
||||
- Requires intra_* lanes; ``configure_sfr_intercube_ring`` (with the
|
||||
snake submesh) or ``configure_sfr_intercube_multisip`` both install
|
||||
them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TILE_S_KV = 64 # per-tile S_kv width (small to keep ``scores`` bounded).
|
||||
TILE_Q = 64 # per-tile Q row count (outer-loop tile size).
|
||||
|
||||
|
||||
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_prefill_long_ctx_cube_repl_pe_sp_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-3 prefill: PE-SP S_kv split + Q-tiled intra-CUBE AR."""
|
||||
G = h_q // h_kv
|
||||
S_local = S_kv // P # each PE owns S_kv/P (cube=replicate, pe=row_wise)
|
||||
pe_id = tl.program_id(axis=0)
|
||||
cube_id = tl.program_id(axis=1)
|
||||
|
||||
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
|
||||
|
||||
KV_ROW_BYTES = d_head * 2 # f16
|
||||
Q_ROW_BYTES = d_head * 2 # G is folded into the row axis, not d_head
|
||||
total_q_rows = G * T_q
|
||||
n_q_tiles = (total_q_rows + TILE_Q - 1) // TILE_Q
|
||||
n_kv_tiles = (S_local + TILE_S_KV - 1) // TILE_S_KV
|
||||
|
||||
# ── Outer Q-tile loop ──
|
||||
# Per Q-tile: load Q-slice, run full S_kv attention, intra-CUBE AR,
|
||||
# store. All scratch is recycled at outer-scope exit.
|
||||
for q_idx in range(n_q_tiles):
|
||||
q_start = q_idx * TILE_Q
|
||||
q_rows = min(TILE_Q, total_q_rows - q_start)
|
||||
with tl.scratch_scope():
|
||||
Q = tl.load(q_ptr + q_start * Q_ROW_BYTES,
|
||||
shape=(q_rows, d_head), dtype="f16")
|
||||
|
||||
# ── Bootstrap (S_kv tile 0) ──
|
||||
# K_T, V, scores, exp_scores live in the OUTER (Q-tile)
|
||||
# scope. They survive the inner S_kv-tile loop (each
|
||||
# iteration's allocations are recycled) and are freed
|
||||
# together when the outer scope exits.
|
||||
tile_s0 = min(TILE_S_KV, S_local)
|
||||
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_scores = tl.exp(scores - m_local)
|
||||
l_local = tl.sum(exp_scores, axis=-1)
|
||||
O_local = tl.dot(exp_scores, V)
|
||||
|
||||
# ── S_kv tiles 1..N: fold via online-softmax merge ──
|
||||
for tile_idx in range(1, n_kv_tiles):
|
||||
tile_start = tile_idx * TILE_S_KV
|
||||
tile_s = min(TILE_S_KV, S_local - tile_start)
|
||||
with tl.scratch_scope():
|
||||
K_T_t = tl.load(k_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(tile_s, d_head), dtype="f16")
|
||||
scores_t = tl.dot(Q, K_T_t)
|
||||
m_tile = tl.max(scores_t, axis=-1)
|
||||
exp_scores_t = tl.exp(scores_t - m_tile)
|
||||
l_tile = tl.sum(exp_scores_t, axis=-1)
|
||||
O_tile = tl.dot(exp_scores_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)
|
||||
|
||||
# ── Intra-CUBE 8-way reduce-to-PE0 (row chain + col bridge) ──
|
||||
# (m_local, l_local, O_local) are still alive in the outer
|
||||
# scope; the send/recv operate on them directly.
|
||||
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)
|
||||
|
||||
# ── Store this Q-tile (designated writer: cube 0, PE 0) ──
|
||||
if pe_id == 0 and cube_id == 0:
|
||||
O_final = O_local / l_local
|
||||
tl.store(o_ptr + q_start * Q_ROW_BYTES, O_final)
|
||||
@@ -0,0 +1,128 @@
|
||||
"""GQA prefill kernel — Case 2 (Cube-Repl × PE-TP) at single-KV-head group.
|
||||
|
||||
Per GQA_full_deck.pptx slide 11 (prefill counterpart, T_q ≫ 1):
|
||||
- K, V replicated across all 8 cubes (the 8× memory waste — this is
|
||||
the inherent cost Case 2 demonstrates). PE-replicate within each
|
||||
cube too: every PE on every cube holds an HBM copy.
|
||||
- PE-TP on T_q: only CUBE 0 has work; within CUBE 0 the P PEs split
|
||||
the T_q axis disjointly. CUBEs 1..7 idle (slide-11 "PE-TP only
|
||||
uses one cube; replicate is pure waste here").
|
||||
- NO inter-rank communication (each active rank has full KV and
|
||||
disjoint T_q rows — slide 11 lists comm cost as "none").
|
||||
|
||||
Distinction from decode Case 2: decode T_q=1 makes PE-TP wasteful
|
||||
(only PE 0 of CUBE 0 works → 1 of 64 ranks active). Prefill T_q≫1
|
||||
makes PE-TP useful — all P PEs of CUBE 0 work on disjoint T_q rows
|
||||
(P of 64 ranks active; CUBEs 1..7 are pure memory waste).
|
||||
|
||||
Q-axis tiling: an outer Q-tile loop wraps the entire per-tile
|
||||
pipeline (Q load → bootstrap → S_kv-tile fold → store) in
|
||||
``tl.scratch_scope``. Peak scratch is bounded by
|
||||
``TILE_Q × TILE_S_KV`` instead of ``T_q_local × S_kv``, so the
|
||||
kernel runs at large T_q without exceeding the ~1 MB per-PE budget.
|
||||
Each Q-tile is independent (disjoint output rows; no inter-tile state).
|
||||
|
||||
Tensor layout:
|
||||
Q : (T_q, h_q · d_head) cube=replicate, pe=row_wise on T_q. Only
|
||||
CUBE 0's PEs load — each owns ``G · T_q_local`` total rows with
|
||||
``T_q_local = T_q / P``; loaded per Q-tile.
|
||||
K : (S_kv, h_kv · d_head) replicated on every rank.
|
||||
V : (S_kv, h_kv · d_head) replicated on every rank.
|
||||
O : (T_q, h_q · d_head) cube=replicate, pe=row_wise on T_q —
|
||||
only CUBE 0's PEs write their disjoint ``T_q_local`` rows,
|
||||
one ``(TILE_Q, d_head)`` slice per outer iteration.
|
||||
|
||||
Topology / SFR:
|
||||
- SFR setup is benign (no inter-rank sends/recvs); any of the
|
||||
``configure_sfr_intercube_*`` helpers works.
|
||||
|
||||
Requires ``T_q % P == 0``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TILE_S_KV = 64 # per-tile S_kv width (small to keep ``scores`` bounded).
|
||||
TILE_Q = 64 # per-tile Q row count (outer-loop tile size).
|
||||
|
||||
|
||||
def gqa_attention_prefill_long_ctx_cube_repl_pe_tp_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-2 prefill: CUBE 0 only; Q-tiled PE-TP; full KV per rank; no comm."""
|
||||
if T_q % P != 0:
|
||||
raise ValueError(
|
||||
f"Case 2 prefill requires T_q={T_q} divisible by P={P}"
|
||||
)
|
||||
|
||||
cube_id = tl.program_id(axis=1)
|
||||
# CUBE 0 only — CUBEs 1..7 are pure memory-replication waste, no work.
|
||||
if cube_id != 0:
|
||||
return
|
||||
|
||||
G = h_q // h_kv
|
||||
T_q_local = T_q // P
|
||||
n_kv_tiles = (S_kv + TILE_S_KV - 1) // TILE_S_KV
|
||||
KV_ROW_BYTES = d_head * 2 # f16
|
||||
Q_ROW_BYTES = d_head * 2
|
||||
total_q_rows = G * T_q_local
|
||||
n_q_tiles = (total_q_rows + TILE_Q - 1) // TILE_Q
|
||||
|
||||
# ── Outer Q-tile loop ──
|
||||
# Per Q-tile: load Q-slice, run full S_kv attention, store. All
|
||||
# scratch is recycled at outer-scope exit, so peak usage is bounded
|
||||
# by TILE_Q × TILE_S_KV.
|
||||
for q_idx in range(n_q_tiles):
|
||||
q_start = q_idx * TILE_Q
|
||||
q_rows = min(TILE_Q, total_q_rows - q_start)
|
||||
with tl.scratch_scope():
|
||||
Q = tl.load(q_ptr + q_start * Q_ROW_BYTES,
|
||||
shape=(q_rows, d_head), dtype="f16")
|
||||
|
||||
# ── Bootstrap (S_kv tile 0) ──
|
||||
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_scores = tl.exp(scores - m_local)
|
||||
l_local = tl.sum(exp_scores, axis=-1)
|
||||
O_local = tl.dot(exp_scores, V)
|
||||
|
||||
# ── S_kv tiles 1..N: fold via online-softmax merge ──
|
||||
for tile_idx in range(1, n_kv_tiles):
|
||||
tile_start = tile_idx * TILE_S_KV
|
||||
tile_s = min(TILE_S_KV, S_kv - tile_start)
|
||||
with tl.scratch_scope():
|
||||
K_T_t = tl.load(k_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(tile_s, d_head), dtype="f16")
|
||||
scores_t = tl.dot(Q, K_T_t)
|
||||
m_tile = tl.max(scores_t, axis=-1)
|
||||
exp_scores_t = tl.exp(scores_t - m_tile)
|
||||
l_tile = tl.sum(exp_scores_t, axis=-1)
|
||||
O_tile = tl.dot(exp_scores_t, V_t)
|
||||
m_new = tl.maximum(m_local, m_tile)
|
||||
scale_old = tl.exp(m_local - m_new)
|
||||
scale_new = tl.exp(m_tile - m_new)
|
||||
l_new = l_local * scale_old + l_tile * scale_new
|
||||
O_new = O_local * scale_old + O_tile * scale_new
|
||||
tl.copy_to(m_local, m_new)
|
||||
tl.copy_to(l_local, l_new)
|
||||
tl.copy_to(O_local, O_new)
|
||||
|
||||
# ── Store this Q-tile (each active PE writes its slice) ──
|
||||
O_final = O_local / l_local
|
||||
tl.store(o_ptr + q_start * Q_ROW_BYTES, O_final)
|
||||
@@ -0,0 +1,186 @@
|
||||
"""GQA prefill kernel — Case 4 (Cube-SP × PE-SP) at single-KV-head group. ★
|
||||
|
||||
Per GQA_full_deck.pptx slide 11 (prefill counterpart, T_q ≫ 1):
|
||||
- K, V split 64-way (cube=row_wise, pe=row_wise on S_kv); each rank
|
||||
owns ``S_local_pe = S_kv / (C·P)``.
|
||||
- Q, O split T_q-wise across cubes (cube=row_wise on T_q),
|
||||
pe=replicate within each cube — each cube owns
|
||||
``T_q_cube = T_q / C`` rows, all P PEs of the cube hold a copy.
|
||||
- Inter-CUBE Ring KV (snake over 4×2 sub-mesh, ADR-0060 §5.5):
|
||||
P parallel rings — each PE drives its own same-lane ring via
|
||||
independent IPCQ channels. Over C ring steps each PE has
|
||||
processed its strided lane of S_kv (= ``S_kv / P`` tokens) for
|
||||
the cube's T_q_cube query rows.
|
||||
- Intra-CUBE 8-way reduce on the partial ``(m, ℓ, O)`` triple
|
||||
(row chain along intra_W + col bridge along intra_N over the
|
||||
2×4 PE grid) — combines the P PE-lanes per cube so cube's PE 0
|
||||
holds the full S_kv attention for its T_q_cube rows.
|
||||
- NO inter-CUBE reduce — each cube writes its own disjoint
|
||||
T_q_cube rows of O (PE 0 of each cube is the writer).
|
||||
|
||||
Distinction from decode Case 4: decode T_q=1 forces an inter-CUBE
|
||||
lrab reduce-to-root over (m,ℓ,O). Prefill T_q≫1 allows Q to be
|
||||
T_q-sharded across cubes, so disjoint output rows avoid the
|
||||
inter-cube reduce — the prefill-native pattern (per user's
|
||||
"Ring KV" choice).
|
||||
|
||||
Tensor layout:
|
||||
Q : (T_q, h_q · d_head) sharded cube=row_wise on T_q, pe=replicate
|
||||
within cube; each rank loads ``(G · T_q_cube, d_head)``.
|
||||
K : (S_kv, h_kv · d_head) cube=row_wise + pe=row_wise on S_kv;
|
||||
each rank owns ``(S_local_pe, h_kv·d_head)``.
|
||||
V : same as K.
|
||||
O : (T_q, h_q · d_head) sharded same as Q — PE 0 of each cube
|
||||
writes its ``(G · T_q_cube, d_head)`` slice; PEs 1..P-1 idle
|
||||
the write.
|
||||
|
||||
Topology / SFR:
|
||||
- Requires ``configure_sfr_intercube_ring(submesh_shape=(2, 4))`` —
|
||||
that helper installs both the snake E/W ring across 8 cubes and
|
||||
the intra_* lanes needed for the intra-CUBE reduce.
|
||||
|
||||
Requires ``T_q % C == 0`` and ``S_kv % (C · P) == 0``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TILE_S_KV = 1024 # ADR-0063 §A.2 S_kv-axis tile sweep (per-tile width).
|
||||
|
||||
|
||||
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_prefill_long_ctx_cube_sp_pe_sp_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-4 prefill: Ring KV across cubes + intra-CUBE AR; disjoint T_q/C rows per cube."""
|
||||
if T_q % C != 0:
|
||||
raise ValueError(
|
||||
f"Case 4 prefill requires T_q={T_q} divisible by C={C}"
|
||||
)
|
||||
if S_kv % (C * P) != 0:
|
||||
raise ValueError(
|
||||
f"Case 4 prefill requires S_kv={S_kv} divisible by C·P={C * P}"
|
||||
)
|
||||
|
||||
G = h_q // h_kv
|
||||
T_q_cube = T_q // C
|
||||
S_local_pe = S_kv // (C * P)
|
||||
KV_ROW_BYTES = d_head * 2 # f16
|
||||
n_tiles = (S_local_pe + TILE_S_KV - 1) // TILE_S_KV
|
||||
|
||||
pe_id = tl.program_id(axis=0)
|
||||
|
||||
# Q is this cube's T_q slice (pe=replicate within cube ⇒ every PE
|
||||
# loads the same G·T_q_cube rows).
|
||||
Q = tl.load(q_ptr, shape=(G * T_q_cube, d_head), dtype="f16")
|
||||
|
||||
# ── Bootstrap (t=0, k=0): load own rank's tile 0; send W for ring ──
|
||||
# Persistent (m, ℓ, O) must live OUTSIDE tl.scratch_scope (kernbench
|
||||
# scope teardown discards in-scope tensors).
|
||||
tile_s = min(TILE_S_KV, S_local_pe)
|
||||
K_T = tl.load(k_ptr, shape=(d_head, tile_s), dtype="f16")
|
||||
V = tl.load(v_ptr, shape=(tile_s, d_head), dtype="f16")
|
||||
if C > 1:
|
||||
tl.send(dir="W", src=K_T)
|
||||
tl.send(dir="W", src=V)
|
||||
scores = tl.dot(Q, K_T)
|
||||
m_local = tl.max(scores, axis=-1)
|
||||
exp_scores = tl.exp(scores - m_local)
|
||||
l_local = tl.sum(exp_scores, axis=-1)
|
||||
O_local = tl.dot(exp_scores, V)
|
||||
|
||||
# ── Nested loop (outer tile, inner ring step) — P parallel rings ──
|
||||
for t in range(n_tiles):
|
||||
tile_start = t * TILE_S_KV
|
||||
tile_s = min(TILE_S_KV, S_local_pe - tile_start)
|
||||
k_start = 1 if t == 0 else 0
|
||||
for k in range(k_start, C):
|
||||
with tl.scratch_scope():
|
||||
if k == 0:
|
||||
K_T_t = tl.load(k_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(tile_s, d_head), dtype="f16")
|
||||
else:
|
||||
K_T_t = tl.recv(dir="E", shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.recv(dir="E", shape=(tile_s, d_head), dtype="f16")
|
||||
if k < C - 1:
|
||||
tl.send(dir="W", src=K_T_t)
|
||||
tl.send(dir="W", src=V_t)
|
||||
scores_t = tl.dot(Q, K_T_t)
|
||||
m_step = tl.max(scores_t, axis=-1)
|
||||
exp_scores_t = tl.exp(scores_t - m_step)
|
||||
l_step = tl.sum(exp_scores_t, axis=-1)
|
||||
O_step = tl.dot(exp_scores_t, V_t)
|
||||
m_new, l_new, O_new = _merge_running(
|
||||
m_local, l_local, O_local, m_step, l_step, O_step, tl=tl,
|
||||
)
|
||||
tl.copy_to(m_local, m_new)
|
||||
tl.copy_to(l_local, l_new)
|
||||
tl.copy_to(O_local, O_new)
|
||||
|
||||
# ── Intra-CUBE 8-way reduce-to-PE0 (row chain + col bridge) ──
|
||||
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)
|
||||
|
||||
# ── Final normalise + store (PE 0 of each cube writes its T_q_cube rows) ──
|
||||
if pe_id == 0:
|
||||
O_final = O_local / l_local
|
||||
tl.store(o_ptr, O_final)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""GQA prefill kernel — Case 1 (Cube-SP × PE-TP) at single-KV-head group.
|
||||
|
||||
Per GQA_full_deck.pptx slide 11 (prefill counterpart, T_q ≫ 1):
|
||||
- K, V split S_kv-wise across the 8 cubes (cube=row_wise);
|
||||
replicated within each cube (pe=replicate). Each cube owns
|
||||
``S_local = S_kv / C``.
|
||||
- Q, O split T_q-wise across cubes AND across PEs intra-cube
|
||||
(cube=row_wise, pe=row_wise on T_q). Each rank owns disjoint
|
||||
``T_q_local = T_q / (C · P)`` query rows.
|
||||
- Inter-CUBE Ring KV (snake over 4×2 sub-mesh, ADR-0060 §5.5):
|
||||
P parallel rings — each PE drives its own same-lane ring via
|
||||
independent IPCQ channels. Over C ring steps the cube-owned
|
||||
K/V blocks rotate through all C cubes, so every rank sees
|
||||
every block.
|
||||
- NO inter-CUBE reduce — each rank writes its own disjoint
|
||||
T_q_local rows of O.
|
||||
- NO intra-CUBE reduce — PEs are disjoint on T_q.
|
||||
|
||||
Distinction from decode Case 1: decode T_q=1 makes PE-TP wasteful
|
||||
(only PE 0 of each cube works). Prefill T_q≫1 makes PE-TP useful —
|
||||
all 64 ranks work on disjoint Q rows.
|
||||
|
||||
Tensor layout:
|
||||
Q : (T_q, h_q · d_head) sharded (cube_row_wise, pe_row_wise) on T_q;
|
||||
each rank loads ``(G · T_q_local, d_head)``.
|
||||
K : (S_kv, h_kv · d_head) with cube=row_wise, pe=replicate — each
|
||||
cube's PE 0..P-1 each hold an HBM copy of the cube's
|
||||
``(S_local, h_kv·d_head)`` shard.
|
||||
V : same as K.
|
||||
O : (T_q, h_q · d_head) sharded same as Q — each rank writes its
|
||||
own ``(G · T_q_local, d_head)`` slice.
|
||||
|
||||
Topology / SFR:
|
||||
- Requires ``configure_sfr_intercube_ring(submesh_shape=(2, 4))``
|
||||
for the snake E/W ring across 8 cubes (wrap from cube 3 ↔ cube 7).
|
||||
|
||||
Requires ``T_q % (C · P) == 0``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TILE_S_KV = 1024 # ADR-0063 §A.2 S_kv-axis tile sweep (per-tile width).
|
||||
|
||||
|
||||
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_prefill_long_ctx_cube_sp_pe_tp_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-1 prefill: Ring KV across cubes; disjoint T_q rows per rank."""
|
||||
if T_q % (C * P) != 0:
|
||||
raise ValueError(
|
||||
f"Case 1 prefill requires T_q={T_q} divisible by C·P={C * P}"
|
||||
)
|
||||
|
||||
G = h_q // h_kv
|
||||
T_q_local = T_q // (C * P)
|
||||
S_local = S_kv // C
|
||||
KV_ROW_BYTES = d_head * 2 # f16
|
||||
n_tiles = (S_local + TILE_S_KV - 1) // TILE_S_KV
|
||||
|
||||
# Q is this rank's own disjoint T_q slice (G·T_q_local rows for the
|
||||
# G-grouped attention).
|
||||
Q = tl.load(q_ptr, shape=(G * T_q_local, d_head), dtype="f16")
|
||||
|
||||
# ── Bootstrap (t=0, k=0): load own cube's tile 0; send W for ring ──
|
||||
# Persistent (m, ℓ, O) must live OUTSIDE tl.scratch_scope (kernbench
|
||||
# scope teardown discards in-scope tensors). Mirrors decode Cases
|
||||
# 1, 3, 4 and the existing prefill_long bootstrap pattern.
|
||||
tile_s = min(TILE_S_KV, S_local)
|
||||
K_T = tl.load(k_ptr, shape=(d_head, tile_s), dtype="f16")
|
||||
V = tl.load(v_ptr, shape=(tile_s, d_head), dtype="f16")
|
||||
if C > 1:
|
||||
tl.send(dir="W", src=K_T)
|
||||
tl.send(dir="W", src=V)
|
||||
scores = tl.dot(Q, K_T)
|
||||
m_local = tl.max(scores, axis=-1)
|
||||
exp_scores = tl.exp(scores - m_local)
|
||||
l_local = tl.sum(exp_scores, axis=-1)
|
||||
O_local = tl.dot(exp_scores, V)
|
||||
|
||||
# ── Nested loop (outer tile, inner ring step) ──
|
||||
# Each tile propagates around the ring before the next tile starts;
|
||||
# IPCQ in-flight depth stays at 1 per direction per PE-lane.
|
||||
for t in range(n_tiles):
|
||||
tile_start = t * TILE_S_KV
|
||||
tile_s = min(TILE_S_KV, S_local - tile_start)
|
||||
k_start = 1 if t == 0 else 0
|
||||
for k in range(k_start, C):
|
||||
with tl.scratch_scope():
|
||||
if k == 0:
|
||||
K_T_t = tl.load(k_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.load(v_ptr + tile_start * KV_ROW_BYTES,
|
||||
shape=(tile_s, d_head), dtype="f16")
|
||||
else:
|
||||
K_T_t = tl.recv(dir="E", shape=(d_head, tile_s), dtype="f16")
|
||||
V_t = tl.recv(dir="E", shape=(tile_s, d_head), dtype="f16")
|
||||
if k < C - 1:
|
||||
tl.send(dir="W", src=K_T_t)
|
||||
tl.send(dir="W", src=V_t)
|
||||
scores_t = tl.dot(Q, K_T_t)
|
||||
m_step = tl.max(scores_t, axis=-1)
|
||||
exp_scores_t = tl.exp(scores_t - m_step)
|
||||
l_step = tl.sum(exp_scores_t, axis=-1)
|
||||
O_step = tl.dot(exp_scores_t, V_t)
|
||||
m_new, l_new, O_new = _merge_running(
|
||||
m_local, l_local, O_local, m_step, l_step, O_step, tl=tl,
|
||||
)
|
||||
tl.copy_to(m_local, m_new)
|
||||
tl.copy_to(l_local, l_new)
|
||||
tl.copy_to(O_local, O_new)
|
||||
|
||||
# ── Final normalise + store (every rank writes its own T_q_local rows) ──
|
||||
O_final = O_local / l_local
|
||||
tl.store(o_ptr, O_final)
|
||||
@@ -26,32 +26,32 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_decode_long_ctx_cube_repl_pe_sp import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_repl_pe_sp import (
|
||||
gqa_attention_decode_long_ctx_cube_repl_pe_sp_kernel,
|
||||
)
|
||||
from kernbench.benches._gqa_attention_decode_long_ctx_cube_repl_pe_tp import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_repl_pe_tp import (
|
||||
gqa_attention_decode_long_ctx_cube_repl_pe_tp_kernel,
|
||||
)
|
||||
from kernbench.benches._gqa_attention_decode_long_ctx_cube_sp_pe_sp import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_sp import (
|
||||
gqa_attention_decode_long_ctx_cube_sp_pe_sp_kernel,
|
||||
)
|
||||
from kernbench.benches._gqa_attention_decode_long_ctx_cube_sp_pe_tp import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_decode_long_ctx_cube_sp_pe_tp import (
|
||||
gqa_attention_decode_long_ctx_cube_sp_pe_tp_kernel,
|
||||
)
|
||||
from kernbench.benches.milestone_gqa_headline import (
|
||||
from kernbench.benches.gqa_helpers.shared._gqa_panel_helpers import (
|
||||
_ccl_cfg,
|
||||
_summarize_op_log,
|
||||
)
|
||||
from kernbench.benches.registry import bench
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_multisip
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
|
||||
# File is at benches/gqa_helpers/long_ctx/ — go up 2 parents to reach
|
||||
# benches/, then into 1H_milestone_output/gqa/long_ctx/.
|
||||
_OUTPUT_DIR = (
|
||||
Path(__file__).resolve().parent
|
||||
/ "1H_milestone_output"
|
||||
/ "gqa_decode_long_ctx_4cases"
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "1H_milestone_output" / "gqa" / "long_ctx"
|
||||
)
|
||||
_SWEEP_JSON = _OUTPUT_DIR / "sweep.json"
|
||||
_SWEEP_JSON = _OUTPUT_DIR / "sweep_decode.json"
|
||||
|
||||
|
||||
# ── Panel registry ───────────────────────────────────────────────────
|
||||
@@ -75,7 +75,7 @@ _PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
# The sub_w=4/sub_h=2 lrab center-root geometry (root cube 6) is
|
||||
# baked into the Case-4 wrapper kernel.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 1, "S_kv": 131_072,
|
||||
"T_q": 1, "S_kv": 8_192,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_tp": ("decode_long_ctx_cube_repl_pe_tp", {
|
||||
@@ -83,7 +83,7 @@ _PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
# on batch. For B=1 only one rank works (slide-11 PE-TP waste).
|
||||
# No inter-rank communication.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 1, "S_kv": 131_072,
|
||||
"T_q": 1, "S_kv": 8_192,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_sp": ("decode_long_ctx_cube_repl_pe_sp", {
|
||||
@@ -91,7 +91,7 @@ _PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
# within each cube. Intra-CUBE 8-way reduce; no inter-CUBE comm
|
||||
# (every cube ends with full answer; designated writer = cube 0).
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 1, "S_kv": 131_072,
|
||||
"T_q": 1, "S_kv": 8_192,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_decode_long_ctx_gqa_cube_sp_pe_tp": ("decode_long_ctx_cube_sp_pe_tp", {
|
||||
@@ -99,7 +99,7 @@ _PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
# PEs TP on batch — at B=1 only PE 0 of each cube works (PE-TP
|
||||
# waste). Inter-CUBE lrab AR (root = cube 6); no intra-CUBE comm.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 1, "S_kv": 131_072,
|
||||
"T_q": 1, "S_kv": 8_192,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
}
|
||||
@@ -316,31 +316,12 @@ def _run_panel(panel: str, topology: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
# ── Bench entry ──────────────────────────────────────────────────────
|
||||
# ── Sweep entry (called by the umbrella ``milestone_1h_gqa``) ────────
|
||||
|
||||
|
||||
@bench(
|
||||
name="milestone-gqa-decode-long-ctx-4cases",
|
||||
description=(
|
||||
"Long-context decode 4-cases comparative study on the "
|
||||
"LLaMA-3.1-70B single-KV-head group (8 cubes × 8 PEs)."
|
||||
),
|
||||
)
|
||||
def run(torch) -> None:
|
||||
"""Drive the registered decode case panels; write sweep.json.
|
||||
|
||||
Gated by GQA_DECODE_LONG_CTX_4CASES_RUN=1.
|
||||
"""
|
||||
def run_sweep(topology: str = "topology.yaml") -> int:
|
||||
"""Drive all decode case panels; write sweep.json. Returns row count."""
|
||||
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if not os.environ.get("GQA_DECODE_LONG_CTX_4CASES_RUN"):
|
||||
raise RuntimeError(
|
||||
"milestone-gqa-decode-long-ctx-4cases needs "
|
||||
"GQA_DECODE_LONG_CTX_4CASES_RUN=1."
|
||||
)
|
||||
|
||||
topology = os.environ.get(
|
||||
"GQA_DECODE_LONG_CTX_4CASES_TOPOLOGY", "topology.yaml",
|
||||
)
|
||||
rows = [_run_panel(panel, topology) for panel in _PANELS]
|
||||
sweep = {
|
||||
"version": 1,
|
||||
@@ -348,6 +329,5 @@ def run(torch) -> None:
|
||||
"rows": rows,
|
||||
}
|
||||
_SWEEP_JSON.write_text(json.dumps(sweep, indent=2))
|
||||
print(
|
||||
f" milestone-gqa-decode-long-ctx-4cases: {len(rows)} rows -> {_SWEEP_JSON}"
|
||||
)
|
||||
print(f" gqa-decode-long-ctx-4cases: {len(rows)} rows -> {_SWEEP_JSON}")
|
||||
return len(rows)
|
||||
@@ -0,0 +1,384 @@
|
||||
"""milestone-gqa-prefill-long-ctx-4cases: long-context prefill 4-cases study.
|
||||
|
||||
Prefill counterpart to milestone-gqa-decode-long-ctx-4cases. Per
|
||||
GQA_full_deck.pptx slides 11-17: 4 KV-cache sharding strategies on
|
||||
the LLaMA-3.1-70B single-KV-head group (1 KV head, 8 Q heads,
|
||||
d_head=128) on 8 cubes × 8 PEs.
|
||||
|
||||
Bench size T_q=S_kv=512 (equal — prompt length matches K/V length
|
||||
for prefill). Cases 2 and 3 use Q-axis tiling (``TILE_Q``) so the
|
||||
per-PE scratch is bounded by ``TILE_Q × TILE_S_KV`` regardless of
|
||||
T_q — much larger sizes are possible at the cost of bench
|
||||
wall-clock (Case 2's single-cube serial path dominates).
|
||||
|
||||
Case 1 Cube-SP / PE-TP → KV split S_kv across cubes (Ring),
|
||||
Q/O split T_q across all 64 ranks
|
||||
Case 2 Cube-Repl / PE-TP → full KV per cube; only CUBE 0's P PEs
|
||||
split T_q; CUBEs 1..7 are pure memory waste
|
||||
Case 3 Cube-Repl / PE-SP → full KV per cube; PEs SP on S_kv
|
||||
(intra-cube AR); 8× redundant compute
|
||||
Case 4 Cube-SP / PE-SP → KV split 64-way + Q split T_q across cubes;
|
||||
Ring KV + intra-cube AR per cube ★ optimal
|
||||
|
||||
Each case is a separate panel. The bench drives all panels in one
|
||||
invocation and writes per-panel op_log_summary + latency to sweep.json
|
||||
so the comparative analysis (latency, comm volume, parallelism,
|
||||
memory) can be generated from a single sweep.
|
||||
|
||||
Distinction from decode: decode T_q=1 makes PE-TP cases wasteful (1
|
||||
active PE). Prefill T_q≫1 makes PE-TP useful (full parallelism). The
|
||||
narrative differs — the figures emphasise Case 2/3 memory waste and
|
||||
Case 3 compute redundancy, not PE-TP-at-B=1 wasted ranks.
|
||||
|
||||
Per-panel try/except in ``run()`` writes whichever cases succeed —
|
||||
the bench tolerates per-panel scratch / SFR failures so sweep.json
|
||||
always lands with at least the successful rows + a ``failures`` list.
|
||||
|
||||
Gated by ``GQA_PREFILL_LONG_CTX_4CASES_RUN=1``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_prefill_long_ctx_cube_repl_pe_sp import (
|
||||
gqa_attention_prefill_long_ctx_cube_repl_pe_sp_kernel,
|
||||
)
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_prefill_long_ctx_cube_repl_pe_tp import (
|
||||
gqa_attention_prefill_long_ctx_cube_repl_pe_tp_kernel,
|
||||
)
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_prefill_long_ctx_cube_sp_pe_sp import (
|
||||
gqa_attention_prefill_long_ctx_cube_sp_pe_sp_kernel,
|
||||
)
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_prefill_long_ctx_cube_sp_pe_tp import (
|
||||
gqa_attention_prefill_long_ctx_cube_sp_pe_tp_kernel,
|
||||
)
|
||||
from kernbench.benches.gqa_helpers.shared._gqa_panel_helpers import (
|
||||
_ccl_cfg,
|
||||
_summarize_op_log,
|
||||
)
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
|
||||
# File is at benches/gqa_helpers/long_ctx/ — go up 2 parents to reach
|
||||
# benches/, then into 1H_milestone_output/gqa/long_ctx/.
|
||||
_OUTPUT_DIR = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "1H_milestone_output" / "gqa" / "long_ctx"
|
||||
)
|
||||
_SWEEP_JSON = _OUTPUT_DIR / "sweep_prefill.json"
|
||||
|
||||
|
||||
# ── Panel registry ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
_PANELS = (
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp", # Case 4 ★ optimal
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp", # Case 2
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp", # Case 3
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp", # Case 1
|
||||
)
|
||||
|
||||
# Each entry: (kind, panel-specific params).
|
||||
# LLaMA-3.1-70B single-KV-head group target:
|
||||
# 1 KV head, h_q = 8 (G = 8 group), d_head = 128
|
||||
# 8 cubes (head-parallel group), 8 PEs/cube
|
||||
# T_q = S_kv = 4096 (long-context prefill)
|
||||
_PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp": ("prefill_long_ctx_cube_sp_pe_sp", {
|
||||
# Case 4: KV split 64-way + Q split T_q across cubes; Ring KV
|
||||
# + intra-CUBE AR per cube. PE 0 of each cube writes its
|
||||
# T_q/C disjoint output rows.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 512, "S_kv": 512,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp": ("prefill_long_ctx_cube_repl_pe_tp", {
|
||||
# Case 2: K, V replicated everywhere (8× memory waste); within
|
||||
# CUBE 0 the P PEs split T_q disjointly. CUBEs 1..7 idle.
|
||||
# No inter-rank communication.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 512, "S_kv": 512,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp": ("prefill_long_ctx_cube_repl_pe_sp", {
|
||||
# Case 3: K, V replicated per cube (8× memory); PEs SP on S_kv
|
||||
# within each cube. Intra-CUBE 8-way reduce; no inter-CUBE comm
|
||||
# (every cube does redundant T_q × S_kv compute; designated
|
||||
# writer = cube 0).
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 512, "S_kv": 512,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
"single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp": ("prefill_long_ctx_cube_sp_pe_tp", {
|
||||
# Case 1: K, V split S_kv across cubes (S_local = S_kv/C per
|
||||
# cube); Q/O split T_q across all 64 ranks. Inter-CUBE snake
|
||||
# Ring KV (P parallel rings); no intra-CUBE comm.
|
||||
"C": 8, "P": 8,
|
||||
"T_q": 512, "S_kv": 512,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
# ── Per-panel runner ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ring_sfr(ctx):
|
||||
"""Install snake Ring SFR (also provides intra_* lanes for AR cases).
|
||||
|
||||
Snake submesh of shape (2, 4) over the default 4×4 cube mesh
|
||||
yields a Hamiltonian cycle through all 8 cubes
|
||||
(0→1→2→3→7→6→5→4→0), with intra_* lanes available for the
|
||||
Case 3/4 intra-CUBE reduce.
|
||||
"""
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(),
|
||||
submesh_shape=(2, 4),
|
||||
)
|
||||
|
||||
|
||||
def _run_prefill_panel_long_ctx_cube_repl_pe_tp(
|
||||
ctx, *, panel: str, C: int, P: int,
|
||||
T_q: int, S_kv: int,
|
||||
d_head: int, h_q: int, h_kv: int,
|
||||
) -> None:
|
||||
"""Case 2 runner: K, V replicated everywhere; CUBE 0 PE-TP on T_q.
|
||||
|
||||
DPPolicy models the cluster-wide 8× memory waste — every rank
|
||||
holds full K, V in its HBM region. Within CUBE 0, P PEs split
|
||||
T_q (so P of 64 ranks actually compute). CUBEs 1..7 idle.
|
||||
"""
|
||||
_ring_sfr(ctx)
|
||||
dp_repl = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=P)
|
||||
dp_qo = DPPolicy(cube="replicate", pe="row_wise",
|
||||
num_cubes=C, num_pes=P)
|
||||
q = ctx.zeros((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_q")
|
||||
k = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_repl, name=f"{panel}_k")
|
||||
v = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_repl, name=f"{panel}_v")
|
||||
o = ctx.empty((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_o")
|
||||
ctx.launch(
|
||||
panel, gqa_attention_prefill_long_ctx_cube_repl_pe_tp_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, h_q, h_kv, d_head, C, P,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
|
||||
def _run_prefill_panel_long_ctx_cube_repl_pe_sp(
|
||||
ctx, *, panel: str, C: int, P: int,
|
||||
T_q: int, S_kv: int,
|
||||
d_head: int, h_q: int, h_kv: int,
|
||||
) -> None:
|
||||
"""Case 3 runner: K, V replicated per cube; PEs SP on S_kv intra-cube.
|
||||
|
||||
DPPolicy models the cluster-wide 8× memory waste — every cube
|
||||
holds full K, V; intra-cube splits the S_kv axis row_wise across
|
||||
8 PEs. Every rank holds full Q (cube=replicate, pe=replicate).
|
||||
The kernel does an intra-CUBE 8-way reduce on (m, ℓ, O); only
|
||||
cube 0's PE 0 writes the output.
|
||||
"""
|
||||
_ring_sfr(ctx)
|
||||
dp_full = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=P)
|
||||
dp_kv = DPPolicy(cube="replicate", pe="row_wise",
|
||||
num_cubes=C, num_pes=P)
|
||||
q = ctx.zeros((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_full, name=f"{panel}_q")
|
||||
k = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_k")
|
||||
v = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_v")
|
||||
o = ctx.empty((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_full, name=f"{panel}_o")
|
||||
ctx.launch(
|
||||
panel, gqa_attention_prefill_long_ctx_cube_repl_pe_sp_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, h_q, h_kv, d_head, C, P,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
|
||||
def _run_prefill_panel_long_ctx_cube_sp_pe_tp(
|
||||
ctx, *, panel: str, C: int, P: int,
|
||||
T_q: int, S_kv: int,
|
||||
d_head: int, h_q: int, h_kv: int,
|
||||
) -> None:
|
||||
"""Case 1 runner: K, V split S_kv across cubes (Ring); Q/O split T_q 64-way.
|
||||
|
||||
DPPolicy: K, V are cube=row_wise on S_kv (S_local = S_kv/C per
|
||||
cube), pe=replicate within cube. Q, O are cube=row_wise +
|
||||
pe=row_wise on T_q — every rank owns T_q/(C·P) disjoint Q rows.
|
||||
The kernel runs P parallel Rings through the snake submesh
|
||||
(each PE has its own IPCQ lane); no intra-CUBE reduce.
|
||||
"""
|
||||
_ring_sfr(ctx)
|
||||
dp_qo = DPPolicy(cube="row_wise", pe="row_wise",
|
||||
num_cubes=C, num_pes=P)
|
||||
dp_kv = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=P)
|
||||
q = ctx.zeros((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_q")
|
||||
k = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_k")
|
||||
v = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_v")
|
||||
o = ctx.empty((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_o")
|
||||
ctx.launch(
|
||||
panel, gqa_attention_prefill_long_ctx_cube_sp_pe_tp_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, h_q, h_kv, d_head, C, P,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
|
||||
def _run_prefill_panel_long_ctx_cube_sp_pe_sp(
|
||||
ctx, *, panel: str, C: int, P: int,
|
||||
T_q: int, S_kv: int,
|
||||
d_head: int, h_q: int, h_kv: int,
|
||||
) -> None:
|
||||
"""Case 4 runner: K, V split 64-way; Q split T_q across cubes.
|
||||
|
||||
DPPolicy: K, V are cube=row_wise + pe=row_wise on S_kv — each
|
||||
rank owns S_local_pe = S_kv/(C·P). Q, O are cube=row_wise on
|
||||
T_q (T_q_cube = T_q/C per cube), pe=replicate within cube. Ring
|
||||
KV through the snake submesh (P parallel lanes per cube); after
|
||||
the ring, an intra-CUBE 8-way AR combines PE lanes so cube's
|
||||
PE 0 has the full attention for its T_q_cube rows. PE 0 of each
|
||||
cube writes its disjoint output rows (no inter-cube reduce).
|
||||
"""
|
||||
_ring_sfr(ctx)
|
||||
dp_qo = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=P)
|
||||
dp_kv = DPPolicy(cube="row_wise", pe="row_wise",
|
||||
num_cubes=C, num_pes=P)
|
||||
q = ctx.zeros((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_q")
|
||||
k = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_k")
|
||||
v = ctx.zeros((S_kv, h_kv * d_head),
|
||||
dtype="f16", dp=dp_kv, name=f"{panel}_v")
|
||||
o = ctx.empty((T_q, h_q * d_head),
|
||||
dtype="f16", dp=dp_qo, name=f"{panel}_o")
|
||||
ctx.launch(
|
||||
panel, gqa_attention_prefill_long_ctx_cube_sp_pe_sp_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, h_q, h_kv, d_head, C, P,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
|
||||
def _make_bench_fn(panel: str):
|
||||
kind, params = _PANEL_DISPATCH[panel]
|
||||
|
||||
def _bench_fn(ctx):
|
||||
if kind == "prefill_long_ctx_cube_sp_pe_sp":
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_sp(ctx, panel=panel, **params)
|
||||
elif kind == "prefill_long_ctx_cube_repl_pe_tp":
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_tp(ctx, panel=panel, **params)
|
||||
elif kind == "prefill_long_ctx_cube_repl_pe_sp":
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_sp(ctx, panel=panel, **params)
|
||||
elif kind == "prefill_long_ctx_cube_sp_pe_tp":
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_tp(ctx, panel=panel, **params)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"milestone-gqa-prefill-long-ctx-4cases panel {panel!r} has "
|
||||
f"unsupported kind={kind!r}."
|
||||
)
|
||||
return _bench_fn
|
||||
|
||||
|
||||
# ── Panel metrics (sweep.json carries these for the comparative plot) ─
|
||||
|
||||
|
||||
_ENGINE_SUFFIXES = (
|
||||
"pe_gemm", "pe_math", "pe_dma", "pe_fetch_store", "pe_ipcq", "pe_cpu",
|
||||
)
|
||||
|
||||
|
||||
def _end_to_end_ns(op_log) -> float:
|
||||
"""End-to-end window: ``max(t_end) - min(t_start)`` over all records."""
|
||||
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_occupancy_ns(op_log) -> dict[str, float]:
|
||||
"""Per-engine summed occupancy (component_id suffix match)."""
|
||||
return {
|
||||
eng: sum(
|
||||
r.t_end - r.t_start
|
||||
for r in op_log
|
||||
if r.component_id.endswith("." + eng)
|
||||
)
|
||||
for eng in _ENGINE_SUFFIXES
|
||||
}
|
||||
|
||||
|
||||
def _run_panel(panel: str, 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=_make_bench_fn(panel),
|
||||
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"milestone-gqa-prefill-long-ctx-4cases panel {panel!r} failed: "
|
||||
f"{result.completion}"
|
||||
)
|
||||
kind, params = _PANEL_DISPATCH[panel]
|
||||
op_log = result.engine.op_log
|
||||
return {
|
||||
"panel": panel,
|
||||
"kind": kind,
|
||||
**params,
|
||||
"op_log_summary": _summarize_op_log(op_log),
|
||||
"latency_ns": _end_to_end_ns(op_log),
|
||||
"engine_occupancy_ns": _engine_occupancy_ns(op_log),
|
||||
}
|
||||
|
||||
|
||||
# ── Sweep entry (called by the umbrella ``milestone_1h_gqa``) ────────
|
||||
|
||||
|
||||
def run_sweep(topology: str = "topology.yaml") -> int:
|
||||
"""Drive all prefill case panels; write sweep.json. Returns row count.
|
||||
|
||||
Per-panel try/except: even with Q-axis tiling, very large T_q or
|
||||
unusual config combinations can hit the per-PE scratch budget. We
|
||||
keep going so sweep.json carries whichever panels succeed; failures
|
||||
land in a ``failures`` list for follow-up.
|
||||
"""
|
||||
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
rows: list[dict] = []
|
||||
failures: list[dict] = []
|
||||
for panel in _PANELS:
|
||||
try:
|
||||
rows.append(_run_panel(panel, topology))
|
||||
except Exception as e:
|
||||
print(f" panel {panel!r} FAILED: {e}")
|
||||
failures.append({"panel": panel, "error": str(e)})
|
||||
sweep = {
|
||||
"version": 1,
|
||||
"panels": list(_PANELS),
|
||||
"rows": rows,
|
||||
"failures": failures,
|
||||
}
|
||||
_SWEEP_JSON.write_text(json.dumps(sweep, indent=2))
|
||||
print(f" gqa-prefill-long-ctx-4cases: {len(rows)} rows -> {_SWEEP_JSON}")
|
||||
return len(rows)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""GQA helpers shared across long-ctx and short-ctx variants.
|
||||
|
||||
Includes the panel-metrics helpers (``_gqa_panel_helpers``) and the
|
||||
context-agnostic decode-opt2 kernel.
|
||||
"""
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Shared helpers for the GQA 4-cases milestone benches.
|
||||
|
||||
Used by both ``milestone_gqa_decode_long_ctx_4cases`` and
|
||||
``milestone_gqa_prefill_long_ctx_4cases``. Underscore-prefixed so the
|
||||
benches package auto-discovery (which skips ``_*.py``) doesn't try to
|
||||
audit it for a ``@bench`` decorator.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
|
||||
|
||||
def _ccl_cfg() -> dict:
|
||||
"""Resolve the lrab hierarchical AllReduce CCL config."""
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
def _summarize_op_log(op_log) -> dict[str, int]:
|
||||
"""Per-panel op_log counts (gemm, ipcq_copy, dma_read, dma_write)."""
|
||||
gemm_count = 0
|
||||
ipcq_copy_count = 0
|
||||
dma_read_count = 0
|
||||
dma_write_count = 0
|
||||
for r in op_log:
|
||||
if r.op_kind == "gemm":
|
||||
gemm_count += 1
|
||||
elif r.op_name == "dma_read":
|
||||
dma_read_count += 1
|
||||
elif r.op_name == "dma_write":
|
||||
dma_write_count += 1
|
||||
elif r.op_name == "ipcq_copy":
|
||||
ipcq_copy_count += 1
|
||||
return {
|
||||
"gemm_count": gemm_count,
|
||||
"ipcq_copy_count": ipcq_copy_count,
|
||||
"dma_read_count": dma_read_count,
|
||||
"dma_write_count": dma_write_count,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""GQA short-context kernels."""
|
||||
@@ -0,0 +1,78 @@
|
||||
"""milestone-1h-gqa: umbrella GQA bench for the 1H code-sign milestone.
|
||||
|
||||
Single ``@bench`` entry that drives all GQA panels by delegating to
|
||||
internal helpers under ``kernbench.benches.gqa_helpers``. Mirrors the
|
||||
``milestone_1h_gemm`` umbrella pattern.
|
||||
|
||||
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)
|
||||
|
||||
Each sub-sweep writes its own ``sweep_{prefill,decode}.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``.
|
||||
|
||||
Gated by ``GQA_1H_RUN=1`` to keep CI fast.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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_prefill_long_ctx_4cases import (
|
||||
run_sweep as _run_prefill_sweep,
|
||||
)
|
||||
from kernbench.benches.registry import bench
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
|
||||
|
||||
@bench(
|
||||
name="milestone-1h-gqa",
|
||||
description=(
|
||||
"Umbrella GQA milestone — drives long-context prefill + decode "
|
||||
"4-cases sweeps on the LLaMA-3.1-70B single-KV-head group "
|
||||
"(8 cubes × 8 PEs). Gated by GQA_1H_RUN=1."
|
||||
),
|
||||
)
|
||||
def run(torch) -> None:
|
||||
"""Drive selected GQA sub-sweeps; each writes its own sweep_*.json.
|
||||
|
||||
Env vars:
|
||||
GQA_1H_RUN=1 (required gate)
|
||||
GQA_1H_TOPOLOGY=topology.yaml (override topology path)
|
||||
GQA_1H_SWEEPS=prefill,decode (default: both; comma-separated)
|
||||
"""
|
||||
if not os.environ.get("GQA_1H_RUN"):
|
||||
raise RuntimeError("milestone-1h-gqa needs GQA_1H_RUN=1.")
|
||||
|
||||
topology = os.environ.get("GQA_1H_TOPOLOGY", "topology.yaml")
|
||||
requested = os.environ.get("GQA_1H_SWEEPS", "prefill,decode")
|
||||
sweeps = [s.strip() for s in requested.split(",") if s.strip()]
|
||||
|
||||
runners = {
|
||||
"prefill": _run_prefill_sweep,
|
||||
"decode": _run_decode_sweep,
|
||||
}
|
||||
unknown = [s for s in sweeps if s not in runners]
|
||||
if unknown:
|
||||
raise RuntimeError(
|
||||
f"GQA_1H_SWEEPS contains unknown sweep(s) {unknown}; "
|
||||
f"valid: {sorted(runners)}"
|
||||
)
|
||||
|
||||
for s in sweeps:
|
||||
runners[s](topology)
|
||||
|
||||
# Sentinel tensor (ADR-0045 D4 / ADR-0054 D2 carve-out) — the
|
||||
# sub-sweeps each spin up their own GraphEngine via ``run_bench``,
|
||||
# so this outer @bench needs to submit at least one request.
|
||||
torch.zeros(
|
||||
(1, 1), dtype="f16",
|
||||
dp=DPPolicy(cube="row_wise", pe="replicate", num_cubes=1, num_pes=1),
|
||||
name="milestone_1h_gqa_sentinel",
|
||||
)
|
||||
@@ -1,231 +0,0 @@
|
||||
"""milestone-gqa-headline bench: real GQA prefill — single-KV-group target.
|
||||
|
||||
Drives the LLaMA-3.1-70B single-KV-head-group prefill panel through
|
||||
``_gqa_attention_prefill_long`` (C=8 snake Ring KV + intra-CUBE PE-SP,
|
||||
all 64 ranks), writing ``op_log_summary`` into ``sweep.json``.
|
||||
Independent from the existing ``milestone-gqa-llama70b`` validation-scale
|
||||
bench (which stays on the legacy baseline kernels).
|
||||
|
||||
Decode panels live in ``milestone_gqa_decode_long_ctx_4cases.py`` (the
|
||||
4-cases long-context decode comparative study).
|
||||
|
||||
Restrictions:
|
||||
- Single SIP (default ``topology.yaml`` 4×4 cube mesh; 4-SIP
|
||||
headline deferred per ADR-0060 §B-item-1)
|
||||
- T_q = S_kv = 1024 (scratch-limited; 32K headline awaits Q-axis
|
||||
kernel tiling)
|
||||
- No figure renderers (defer to a separate cycle)
|
||||
|
||||
Panels:
|
||||
single_kv_group_prefill_gqa_c8_p8: prefill C=8 snake Ring KV +
|
||||
intra-CUBE PE-SP, T_q=S_kv=1K,
|
||||
d_head=128 — the LLaMA-3.1-70B
|
||||
single-KV-group prefill target.
|
||||
|
||||
Gated by ``GQA_HEADLINE_RUN=1``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel
|
||||
from kernbench.benches.registry import bench
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
|
||||
_OUTPUT_DIR = Path(__file__).resolve().parent / "1H_milestone_output" / "gqa_headline"
|
||||
_SWEEP_JSON = _OUTPUT_DIR / "sweep.json"
|
||||
|
||||
# ── Panel configs ────────────────────────────────────────────────────
|
||||
|
||||
_DTYPE = "f16"
|
||||
_D_HEAD = 64
|
||||
_T_Q_PREFILL = 4
|
||||
_S_KV_PREFILL = 16
|
||||
|
||||
_PANELS = (
|
||||
"single_kv_group_prefill_gqa_c8_p8",
|
||||
)
|
||||
|
||||
# Each entry: (kind, panel-specific params)
|
||||
_PANEL_DISPATCH: dict[str, tuple[str, dict]] = {
|
||||
"single_kv_group_prefill_gqa_c8_p8": ("prefill", {
|
||||
"C": 8, "P": 8,
|
||||
# T_q = S_kv = 1024 (one-shot long-context prefill, scratch-limited).
|
||||
# The bootstrap section of the prefill kernel leaves K_t/V_t/scores/
|
||||
# exp_scores persistent (outside tl.scratch_scope), inflating the
|
||||
# baseline. At T_q=2048 the peak hits ~1.05 MB vs 1.0 MB budget; at
|
||||
# 1024 it fits comfortably. The true LLaMA 32K headline awaits a
|
||||
# future increment to (a) add Q-axis tiling and (b) move the
|
||||
# bootstrap into scratch discipline.
|
||||
"T_q": 1_024, "S_kv": 1_024,
|
||||
"d_head": 128,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
def _ccl_cfg():
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
# ── Per-kind launch helpers ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _run_prefill_panel(
|
||||
ctx, *, panel: str, C: int, S_kv: int,
|
||||
P: int = 1,
|
||||
T_q: int = _T_Q_PREFILL,
|
||||
d_head: int = _D_HEAD,
|
||||
) -> None:
|
||||
if C > 1:
|
||||
mesh_w = int(ctx.spec["sip"]["cube_mesh"]["w"])
|
||||
if C <= mesh_w:
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(), ring_size=C,
|
||||
)
|
||||
else:
|
||||
if C % mesh_w != 0:
|
||||
raise ValueError(
|
||||
f"C={C} > mesh_w={mesh_w} requires C divisible by mesh_w"
|
||||
)
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(),
|
||||
submesh_shape=(C // mesh_w, mesh_w),
|
||||
)
|
||||
# Q and O switch to pe="row_wise" when intra-CUBE PE-SP is active
|
||||
# (ADR-0060 §5.5 + §B-item-3: disjoint query-row split across PEs).
|
||||
q_pe = "row_wise" if P > 1 else "replicate"
|
||||
o_pe = "row_wise" if P > 1 else "replicate"
|
||||
dp_q = DPPolicy(cube="replicate", pe=q_pe,
|
||||
num_cubes=C, num_pes=P)
|
||||
dp_kv = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe="replicate", num_cubes=C, num_pes=P)
|
||||
dp_o = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe=o_pe, num_cubes=C, num_pes=P)
|
||||
q = ctx.zeros((T_q, d_head),
|
||||
dtype=_DTYPE, dp=dp_q, name=f"{panel}_q")
|
||||
k = ctx.zeros((S_kv, d_head),
|
||||
dtype=_DTYPE, dp=dp_kv, name=f"{panel}_k")
|
||||
v = ctx.zeros((S_kv, d_head),
|
||||
dtype=_DTYPE, dp=dp_kv, name=f"{panel}_v")
|
||||
o = ctx.empty((T_q * C, d_head),
|
||||
dtype=_DTYPE, dp=dp_o, name=f"{panel}_o")
|
||||
ctx.launch(
|
||||
panel, gqa_attention_prefill_long_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, d_head, C, P,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
|
||||
def _make_bench_fn(panel: str):
|
||||
kind, params = _PANEL_DISPATCH[panel]
|
||||
assert kind == "prefill", (
|
||||
f"milestone-gqa-headline only registers prefill panels; got {kind!r}"
|
||||
)
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel(ctx, panel=panel, **params)
|
||||
|
||||
return _bench_fn
|
||||
|
||||
|
||||
# ── Op-log summary ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _summarize_op_log(op_log) -> dict[str, int]:
|
||||
"""Per-panel op_log counts (gemm, ipcq_copy, dma_read, dma_write)."""
|
||||
gemm_count = 0
|
||||
ipcq_copy_count = 0
|
||||
dma_read_count = 0
|
||||
dma_write_count = 0
|
||||
for r in op_log:
|
||||
if r.op_kind == "gemm":
|
||||
gemm_count += 1
|
||||
elif r.op_name == "dma_read":
|
||||
dma_read_count += 1
|
||||
elif r.op_name == "dma_write":
|
||||
dma_write_count += 1
|
||||
elif r.op_name == "ipcq_copy":
|
||||
ipcq_copy_count += 1
|
||||
return {
|
||||
"gemm_count": gemm_count,
|
||||
"ipcq_copy_count": ipcq_copy_count,
|
||||
"dma_read_count": dma_read_count,
|
||||
"dma_write_count": dma_write_count,
|
||||
}
|
||||
|
||||
|
||||
def _run_panel(panel: str, topology: str) -> dict:
|
||||
"""Run one panel in a fresh engine; return its row 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=_make_bench_fn(panel),
|
||||
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"milestone-gqa-headline panel {panel!r} failed: "
|
||||
f"{result.completion}"
|
||||
)
|
||||
kind, params = _PANEL_DISPATCH[panel]
|
||||
return {
|
||||
"panel": panel,
|
||||
"kind": kind,
|
||||
**params,
|
||||
"op_log_summary": _summarize_op_log(result.engine.op_log),
|
||||
}
|
||||
|
||||
|
||||
# ── Bench entry ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@bench(
|
||||
name="milestone-gqa-headline",
|
||||
description="Headline GQA prefill milestone — LLaMA-3.1-70B single-KV-group target (C=8, P=8).",
|
||||
)
|
||||
def run(torch) -> None:
|
||||
"""Drive the headline prefill panel; write sweep.json.
|
||||
|
||||
Gated by GQA_HEADLINE_RUN=1.
|
||||
"""
|
||||
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if not os.environ.get("GQA_HEADLINE_RUN"):
|
||||
raise RuntimeError(
|
||||
"milestone-gqa-headline needs GQA_HEADLINE_RUN=1."
|
||||
)
|
||||
|
||||
topology = os.environ.get("GQA_HEADLINE_TOPOLOGY", "topology.yaml")
|
||||
rows = [_run_panel(panel, topology) for panel in _PANELS]
|
||||
sweep = {
|
||||
"version": 1,
|
||||
"panels": list(_PANELS),
|
||||
"config": {
|
||||
"T_q_prefill": _T_Q_PREFILL,
|
||||
"S_kv_prefill": _S_KV_PREFILL,
|
||||
"d_head": _D_HEAD,
|
||||
},
|
||||
"rows": rows,
|
||||
}
|
||||
_SWEEP_JSON.write_text(json.dumps(sweep, indent=2))
|
||||
print(f" milestone-gqa-headline: {len(rows)} rows -> {_SWEEP_JSON}")
|
||||
|
||||
# Sentinel tensor (ADR-0045 D4 / ADR-0054 D2 carve-out).
|
||||
torch.zeros(
|
||||
(1, 1), dtype="f16",
|
||||
dp=DPPolicy(cube="row_wise", pe="replicate", num_cubes=1, num_pes=1),
|
||||
name="milestone_gqa_headline_sentinel",
|
||||
)
|
||||
@@ -99,7 +99,11 @@ def _audit_modules(imported: list[str], registered: set[str]) -> None:
|
||||
def _eager_import_and_audit(pkg_path: list[str], pkg_name: str) -> None:
|
||||
imported: list[str] = []
|
||||
for m in iter_modules(pkg_path):
|
||||
if m.name == "registry" or m.name.startswith("_"):
|
||||
# Skip helper-name modules, the registry itself, and any
|
||||
# subpackages (audit policy applies to ``.py`` bench files
|
||||
# only — subpackages are explicitly imported by the benches
|
||||
# that need them).
|
||||
if m.ispkg or m.name == "registry" or m.name.startswith("_"):
|
||||
continue
|
||||
mod = import_module(f"{pkg_name}.{m.name}")
|
||||
imported.append(mod.__name__)
|
||||
|
||||
@@ -151,7 +151,7 @@ def test_k_before_v_in_opt2_plan():
|
||||
def test_opt2_bench_completes_oplog_mode():
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_decode_opt2 import ( # noqa: F401
|
||||
from kernbench.benches.gqa_helpers.shared._gqa_attention_decode_opt2 import ( # noqa: F401
|
||||
gqa_attention_decode_opt2_kernel,
|
||||
)
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"""Phase 1 spec test for P6a GQA prefill kernel (head-parallel, C=1 baseline).
|
||||
|
||||
P6a introduces ``_gqa_attention_prefill_long.py`` with the head-parallel structure (one
|
||||
Q head per CUBE, per-CUBE distributed output, no reduce). C=1 is the
|
||||
degenerate case — no Ring KV, no IPCQ traffic. Validates kernel
|
||||
structure and T_q > 1 attention.
|
||||
|
||||
P6b (deferred) adds the Ring KV rotation for C > 1, which needs either
|
||||
a new SFR install function (intra_* + wrapped E/W at CUBE level) or a
|
||||
topology-specific config — separate design call.
|
||||
|
||||
The prefill kernel differs from decode (P1a/P2a/P2b) in three ways
|
||||
(ADR-0060 §5.5 / TL;DR):
|
||||
1. Q has T_q > 1 rows (not just decode's single timestep).
|
||||
2. Head-parallel placement: each CUBE owns ONE Q head — no M-fold.
|
||||
3. Each CUBE writes its own head's output — NO reduce.
|
||||
|
||||
Phase 1 (this commit): tests only — production code lands in Phase 2.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel # noqa: F401
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
D_HEAD = 64
|
||||
DTYPE = "f16"
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _run_prefill(*, T_q: int, S_kv: int, C: int = 1):
|
||||
"""C=1 head-parallel prefill: single CUBE owns the one head + full KV."""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
dp = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
# Q: (T_q, d_head) — one head per CUBE (head-parallel; for C=1
|
||||
# only one head total). 2D layout matches what the kernel loads.
|
||||
# P6b will use a 3D (h_q, T_q, d_head) Q with cube_row_wise
|
||||
# sharding so each CUBE owns its head.
|
||||
q = ctx.zeros((T_q, D_HEAD), dtype=DTYPE, dp=dp,
|
||||
name=f"q_t{T_q}_c{C}")
|
||||
# K, V: full local for C=1 (no ring). Kernel loads K as
|
||||
# (d_head, S_kv) via byte-conserving reshape.
|
||||
k = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp,
|
||||
name=f"k_t{T_q}_c{C}")
|
||||
v = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp,
|
||||
name=f"v_t{T_q}_c{C}")
|
||||
# O: (T_q, d_head) — per-CUBE distributed output.
|
||||
o = ctx.empty((T_q, D_HEAD), dtype=DTYPE, dp=dp,
|
||||
name=f"o_t{T_q}_c{C}")
|
||||
ctx.launch(
|
||||
f"gqa_prefill_p6a_t{T_q}_s{S_kv}_c{C}",
|
||||
gqa_attention_prefill_long_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, D_HEAD, C,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
def test_prefill_c_one_t_q_one_completes():
|
||||
"""C=1, T_q=1: smallest workload (decode-like)."""
|
||||
result = _run_prefill(T_q=1, S_kv=16, C=1)
|
||||
assert result.completion.ok, (
|
||||
f"prefill C=1 T_q=1 failed: {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_prefill_c_one_t_q_four_completes():
|
||||
"""C=1, T_q=4: real prefill (Q has multiple rows) — distinguishes
|
||||
prefill from decode (T_q=1)."""
|
||||
result = _run_prefill(T_q=4, S_kv=16, C=1)
|
||||
assert result.completion.ok, (
|
||||
f"prefill C=1 T_q=4 failed: {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_prefill_c_one_no_ipcq_traffic():
|
||||
"""C=1: no ring step, no IPCQ traffic."""
|
||||
result = _run_prefill(T_q=4, S_kv=16, C=1)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
assert n_copy == 0, (
|
||||
f"C=1 must have no IPCQ traffic (no ring); got {n_copy}"
|
||||
)
|
||||
|
||||
|
||||
def test_prefill_c_one_one_dma_write():
|
||||
"""C=1, one head: exactly one dma_write (per-CUBE distributed output;
|
||||
no reduce). For C > 1 in P6b this becomes dma_write_count == C."""
|
||||
result = _run_prefill(T_q=4, S_kv=16, C=1)
|
||||
assert result.completion.ok
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == 1, (
|
||||
f"C=1 prefill: expected 1 dma_write (one head per cube); "
|
||||
f"got {n_writes}"
|
||||
)
|
||||
@@ -1,154 +0,0 @@
|
||||
"""Tests for prefill_long Ring KV at C=8 over a 2×4 snake sub-mesh.
|
||||
|
||||
ADR-0060 §5.5 design target for the single-KV-group LLaMA-3.1-70B
|
||||
configuration: ``C = G = 8`` (one Q head per CUBE), Ring KV rotates
|
||||
the 8 KV slices around all 8 CUBEs.
|
||||
|
||||
Increment 1 wired ``configure_sfr_intercube_ring(submesh_shape=(2, 4))``
|
||||
to map the kernel's logical E/W to a snake/serpentine 1-hop path
|
||||
through the top 2×4 sub-mesh of the 4×4 SIP CUBE mesh. The kernel
|
||||
itself uses only logical ``dir="W"`` / ``dir="E"`` — the snake is
|
||||
transparent at kernel level.
|
||||
|
||||
This file verifies the assembly works end-to-end at C=8:
|
||||
T1 kernel completes (no scratch overflow, no deadlock)
|
||||
T2 per-CUBE head-parallel output (8 dma_writes, one per CUBE)
|
||||
T3 tile-granular ring traffic at C=8 follows the same
|
||||
``(C-1)·n_tiles·2·C`` formula as the existing tile-ring tests
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel # noqa: F401
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
D_HEAD = 64
|
||||
DTYPE = "f16"
|
||||
|
||||
|
||||
def _ccl_cfg():
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
def _run_prefill_c8_snake(*, T_q: int, S_kv: int):
|
||||
"""Head-parallel prefill at C=8 with snake-mapped Ring KV over the
|
||||
top 2×4 sub-mesh of the 4×4 SIP CUBE mesh."""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
C = 8
|
||||
|
||||
def _bench_fn(ctx):
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(),
|
||||
submesh_shape=(2, 4),
|
||||
)
|
||||
dp_q = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
dp_kv = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
dp_o = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
q = ctx.zeros((T_q, D_HEAD), dtype=DTYPE, dp=dp_q,
|
||||
name=f"q_c8_snake_t{T_q}_s{S_kv}")
|
||||
k = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"k_c8_snake_t{T_q}_s{S_kv}")
|
||||
v = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"v_c8_snake_t{T_q}_s{S_kv}")
|
||||
o = ctx.empty((T_q * C, D_HEAD), dtype=DTYPE, dp=dp_o,
|
||||
name=f"o_c8_snake_t{T_q}_s{S_kv}")
|
||||
ctx.launch(
|
||||
f"gqa_prefill_long_c8_snake_t{T_q}_s{S_kv}",
|
||||
gqa_attention_prefill_long_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, D_HEAD, C,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
# ── T1: kernel completes end-to-end at C=8 ───────────────────────────
|
||||
|
||||
|
||||
def test_prefill_long_c8_snake_completes():
|
||||
"""ADR-0060 §5.5 design target: prefill Ring KV at C=G=8 over the
|
||||
2×4 snake sub-mesh. With zero inputs, output is trivially zero;
|
||||
this test guards that the kernel reaches completion without
|
||||
deadlock, scratch overflow, or routing failure.
|
||||
|
||||
Configuration: T_q=4, S_kv=8192 → S_local=1024, n_tiles=1
|
||||
(TILE_S_KV=1024). Bounded scratch.
|
||||
"""
|
||||
result = _run_prefill_c8_snake(T_q=4, S_kv=8192)
|
||||
assert result.completion.ok, (
|
||||
f"prefill at C=8 with snake ring must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
# ── T2: 8 dma_writes (head-parallel, one head per CUBE) ──────────────
|
||||
|
||||
|
||||
def test_prefill_long_c8_snake_distributed_output_count():
|
||||
"""Head-parallel: each CUBE owns one Q head and writes its own
|
||||
head's output (T_q, d_head) rows. No inter-CUBE reduce on the
|
||||
output side — so ``dma_write_count == C == 8``.
|
||||
|
||||
This is the C=8 analogue of
|
||||
``test_prefill_long_tile_ring_dma_write_count`` at C=4.
|
||||
"""
|
||||
result = _run_prefill_c8_snake(T_q=4, S_kv=8192)
|
||||
assert result.completion.ok
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == 8, (
|
||||
f"head-parallel C=8: expected 8 dma_writes (one per CUBE); "
|
||||
f"got {n_writes}"
|
||||
)
|
||||
|
||||
|
||||
# ── T3: tile-granular ring ipcq_copy count scales as (C-1)·n_tiles·2·C
|
||||
|
||||
|
||||
def test_prefill_long_c8_snake_tile_ipcq_count():
|
||||
"""ADR-0060 §5.5.1 tile-granular ring: per-CUBE send count is
|
||||
``2·n_tiles·(C-1)`` (K + V per tile per ring step). Aggregated
|
||||
across C CUBEs, total ipcq_copy = ``(C-1)·n_tiles·2·C``.
|
||||
|
||||
Configuration: T_q=4, S_kv=8192, C=8 → S_local=1024, n_tiles=1
|
||||
(TILE_S_KV=1024). Expected total = ``7·1·2·8 = 112``.
|
||||
|
||||
Verifies that the snake-mapped 1-hop physical links carry the
|
||||
same logical ring traffic as the existing C=4 1D-row ring tests.
|
||||
"""
|
||||
C = 8
|
||||
n_tiles = 1 # S_local=1024 / TILE_S_KV=1024
|
||||
result = _run_prefill_c8_snake(T_q=4, S_kv=8192)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
expected = (C - 1) * n_tiles * 2 * C
|
||||
assert n_copy == expected, (
|
||||
f"tile-granular ring at C=8: expected {expected} ipcq_copy "
|
||||
f"((C-1)·n_tiles·2·C = {C - 1}·{n_tiles}·2·{C}); got {n_copy}"
|
||||
)
|
||||
@@ -1,233 +0,0 @@
|
||||
"""Tests for intra-CUBE PE-SP in prefill_long (query-axis split).
|
||||
|
||||
ADR-0060 §5.5 last bullet + §B-item-3: split the head's query rows
|
||||
``[T_q, d_head]`` across the ``P`` PEs of each CUBE so all P PEs work
|
||||
in parallel. Output rows are disjoint across PEs ⇒ no intra-CUBE
|
||||
reduce needed.
|
||||
|
||||
Same-lane SFR wiring: PE ``i`` in CUBE A has its own E/W ring link to
|
||||
PE ``i`` in CUBE B (the snake's prev/next). All P PEs of a CUBE see
|
||||
the same K, V (HBM-resident, ``pe="replicate"``) ⇒ P parallel rings
|
||||
run in lockstep, each PE rotating its own K/V copies via its own IPCQ
|
||||
channels.
|
||||
|
||||
Activation contract:
|
||||
- ``P == 1`` (default; omitted from launch args) → existing
|
||||
PE-0-only behavior. Byte-for-byte unchanged.
|
||||
- ``P > 1`` → all P PEs active; each handles ``T_q // P`` query
|
||||
rows. Requires ``T_q % P == 0`` (degenerate T_q < P is rejected
|
||||
— caller must use ``P=1`` for that workload, ADR-0060 §B-item-3).
|
||||
|
||||
Phase 1: tests only — production code lands in Phase 2.
|
||||
T1, T2, T3, T5 fail today (TypeError: kernel signature has no P).
|
||||
T4 passes today as the backward-compat anchor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel # noqa: F401
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
D_HEAD = 64
|
||||
DTYPE = "f16"
|
||||
|
||||
|
||||
def _ccl_cfg():
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
def _run_prefill_pe_sp(
|
||||
*, T_q: int, S_kv: int, C: int, P: int | None,
|
||||
snake: bool,
|
||||
):
|
||||
"""Drive a prefill_long launch with optional intra-CUBE PE-SP.
|
||||
|
||||
``P=None`` → omit P from the launch args (exercises the kernel's
|
||||
default behaviour).
|
||||
``snake=True`` → install the 2×4 snake ring SFR (Increment 1);
|
||||
else install the 1D-row ring at ``ring_size=C``.
|
||||
"""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
if snake:
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(),
|
||||
submesh_shape=(2, 4),
|
||||
)
|
||||
else:
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(),
|
||||
ring_size=C,
|
||||
)
|
||||
num_pes = P if (P is not None and P > 1) else 1
|
||||
# When PE-SP is active, Q and O are split row-wise across PEs;
|
||||
# K, V remain replicated within a CUBE.
|
||||
q_pe = "row_wise" if num_pes > 1 else "replicate"
|
||||
o_pe = "row_wise" if num_pes > 1 else "replicate"
|
||||
dp_q = DPPolicy(cube="replicate", pe=q_pe,
|
||||
num_cubes=C, num_pes=num_pes)
|
||||
dp_kv = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe="replicate", num_cubes=C, num_pes=num_pes)
|
||||
dp_o = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe=o_pe, num_cubes=C, num_pes=num_pes)
|
||||
suffix = f"c{C}_p{P}_t{T_q}_s{S_kv}"
|
||||
q = ctx.zeros((T_q, D_HEAD), dtype=DTYPE, dp=dp_q,
|
||||
name=f"q_pe_sp_{suffix}")
|
||||
k = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"k_pe_sp_{suffix}")
|
||||
v = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"v_pe_sp_{suffix}")
|
||||
o = ctx.empty((T_q * C, D_HEAD), dtype=DTYPE, dp=dp_o,
|
||||
name=f"o_pe_sp_{suffix}")
|
||||
launch_args = [q, k, v, o, T_q, S_kv, D_HEAD, C]
|
||||
if P is not None:
|
||||
launch_args.append(P)
|
||||
ctx.launch(
|
||||
f"gqa_prefill_long_pe_sp_{suffix}",
|
||||
gqa_attention_prefill_long_kernel,
|
||||
*launch_args,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
# ── T1: C=8 P=8 completes end-to-end ─────────────────────────────────
|
||||
|
||||
|
||||
def test_prefill_c8_p8_pe_sp_completes():
|
||||
"""ADR-0060 §5.5 + §B-item-3 design target: 64 ranks active
|
||||
(8 CUBEs × 8 PEs), query-axis split across PEs, snake-mapped
|
||||
Ring KV. Guards no scratch overflow, no deadlock across the
|
||||
P parallel same-lane rings.
|
||||
"""
|
||||
result = _run_prefill_pe_sp(
|
||||
T_q=8, S_kv=8192, C=8, P=8, snake=True,
|
||||
)
|
||||
assert result.completion.ok, (
|
||||
f"prefill at C=8, P=8 (PE-SP) must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
# ── T2: 64 dma_writes (one per PE, disjoint query rows) ──────────────
|
||||
|
||||
|
||||
def test_prefill_c8_p8_pe_sp_64_dma_writes():
|
||||
"""With query-axis split, each PE owns ``T_q/P = 1`` query row
|
||||
and stores its own ``(1, d_head)`` slice. Across all 64 ranks
|
||||
(8 CUBEs × 8 PEs), ``dma_write_count == 64``.
|
||||
|
||||
This is the structural signal that PE-SP is actually wired:
|
||||
PE-0-only would give 8 dma_writes (one per CUBE).
|
||||
"""
|
||||
result = _run_prefill_pe_sp(
|
||||
T_q=8, S_kv=8192, C=8, P=8, snake=True,
|
||||
)
|
||||
assert result.completion.ok
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == 64, (
|
||||
f"PE-SP at C=8, P=8: expected 64 dma_writes "
|
||||
f"(8 cubes × 8 PEs, each writes its T_q/P=1 query row); "
|
||||
f"got {n_writes}"
|
||||
)
|
||||
|
||||
|
||||
# ── T3: P parallel rings — ipcq_copy scales by P ─────────────────────
|
||||
|
||||
|
||||
def test_prefill_c8_p8_pe_sp_ring_ipcq_count():
|
||||
"""Per-PE same-lane rings: each PE runs its own ring traffic via
|
||||
its own IPCQ channels. Total inter-CUBE ipcq_copy:
|
||||
``(C-1) · n_tiles · 2 · C · P`` (= existing C=8 formula × P).
|
||||
|
||||
Configuration: T_q=8, S_kv=8192, C=8 → S_local=1024, n_tiles=1
|
||||
(TILE_S_KV=1024). Expected = ``7·1·2·8·8`` = **896**.
|
||||
"""
|
||||
C = 8
|
||||
P = 8
|
||||
n_tiles = 1
|
||||
result = _run_prefill_pe_sp(
|
||||
T_q=8, S_kv=8192, C=C, P=P, snake=True,
|
||||
)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
expected = (C - 1) * n_tiles * 2 * C * P
|
||||
assert n_copy == expected, (
|
||||
f"PE-SP parallel rings at C=8, P=8: expected {expected} "
|
||||
f"ipcq_copy ((C-1)·n_tiles·2·C·P = "
|
||||
f"{C - 1}·{n_tiles}·2·{C}·{P}); got {n_copy}"
|
||||
)
|
||||
|
||||
|
||||
# ── T4: backward-compat — P omitted → existing PE-0-only behaviour ───
|
||||
|
||||
|
||||
def test_prefill_p_default_1_backward_compat():
|
||||
"""When ``P`` is omitted from the launch args, the kernel must
|
||||
behave identically to today: only PE 0 of each CUBE participates;
|
||||
one head per CUBE; one dma_write per CUBE.
|
||||
|
||||
At C=4 this gives 4 dma_writes (matches the existing
|
||||
``test_prefill_long_tile_ring_dma_write_count``). Phase 2 must
|
||||
not regress this path.
|
||||
|
||||
Passes today AND after Phase 2.
|
||||
"""
|
||||
C = 4
|
||||
result = _run_prefill_pe_sp(
|
||||
T_q=4, S_kv=8192, C=C, P=None, snake=False,
|
||||
)
|
||||
assert result.completion.ok, (
|
||||
f"prefill at C=4 with default P (PE-0-only) must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == C, (
|
||||
f"default P=1 (PE-0-only): expected {C} dma_writes "
|
||||
f"(one per CUBE); got {n_writes}"
|
||||
)
|
||||
|
||||
|
||||
# ── T5: validation — T_q must be divisible by P when P > 1 ───────────
|
||||
|
||||
|
||||
def test_prefill_pe_sp_rejects_non_divisible_t_q():
|
||||
"""ADR-0060 §B-item-3 fallback (KV-block split + intra-CUBE
|
||||
reduce for T_q < P) is deferred. Callers must request ``P=1`` for
|
||||
workloads where T_q < P or T_q % P != 0.
|
||||
|
||||
C=4, P=8, T_q=4 violates ``T_q % P == 0`` (and also T_q < P).
|
||||
The kernel must raise ValueError with a clear error message
|
||||
rather than silently producing wrong results.
|
||||
"""
|
||||
with pytest.raises((ValueError, AssertionError), match=r"T_q"):
|
||||
_run_prefill_pe_sp(
|
||||
T_q=4, S_kv=8192, C=4, P=8, snake=False,
|
||||
)
|
||||
@@ -1,162 +0,0 @@
|
||||
"""Phase 1 spec test for P3c: tile-granular Ring KV in prefill_long
|
||||
(ADR-0060 §5.5 amendment + ADR-0063 §A.2).
|
||||
|
||||
Today's prefill_long ring sends and receives full ``(d_head, S_local)``
|
||||
KV slices per step. Step 0's local-attention intermediates also live
|
||||
outside any ``scratch_scope`` (they're loaded as full-slice ``Kc``,
|
||||
``Vc`` and feed ``scores``, ``exp_scores`` as persistent allocations).
|
||||
At larger ``S_local`` both the step-0 leak and the ring step's
|
||||
in-scope intermediates grow linearly with ``S_local``, and at
|
||||
``S_local = 32K`` (S_kv=128K, C=4, T_q=4) the peak overflows the
|
||||
1 MiB pool.
|
||||
|
||||
P3c converts the ring to **tile-granular**: a nested loop
|
||||
``for k in range(C): for t in range(n_tiles): ...`` where each
|
||||
iteration sends/recvs one ``(d_head, TILE_S_KV)`` tile (and its V
|
||||
counterpart). The persistent state shrinks to ``(m, ℓ, O)`` only
|
||||
(~1 KB); per-tile in-scope scratch is bounded by
|
||||
``TILE_S_KV`` regardless of ``S_local``. Ceiling lifted.
|
||||
|
||||
Trade-off: the per-CUBE send count grows from ``2·(C-1)`` to
|
||||
``2·n_tiles·(C-1)``. At ``n_tiles=1`` (small ``S_local``) the count is
|
||||
unchanged, so the existing ``test_prefill_ring_c_*`` tests at
|
||||
``S_kv ∈ {16, 32}`` still pass.
|
||||
|
||||
Phase 1 (this commit): tests only — production code lands in Phase 2.
|
||||
T1 fails today with a ``TLContext scratch overflow``; T2 fails today
|
||||
with the slice-granular ipcq_copy count; T5 passes today and is a
|
||||
regression guard for the per-CUBE output write count.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel # noqa: F401
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
D_HEAD = 64
|
||||
DTYPE = "f16"
|
||||
|
||||
|
||||
def _ccl_cfg():
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
def _run_prefill_ring(*, T_q: int, S_kv: int, C: int):
|
||||
"""Head-parallel prefill with Ring KV across C CUBEs."""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(), ring_size=C,
|
||||
)
|
||||
dp_q = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
dp_kv = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe="replicate", num_cubes=C, num_pes=1)
|
||||
dp_o = DPPolicy(cube="row_wise" if C > 1 else "replicate",
|
||||
pe="replicate", num_cubes=C, num_pes=1)
|
||||
q = ctx.zeros((T_q, D_HEAD), dtype=DTYPE, dp=dp_q,
|
||||
name=f"q_tlr_t{T_q}_c{C}_s{S_kv}")
|
||||
k = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"k_tlr_t{T_q}_c{C}_s{S_kv}")
|
||||
v = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"v_tlr_t{T_q}_c{C}_s{S_kv}")
|
||||
o = ctx.empty((T_q * C, D_HEAD), dtype=DTYPE, dp=dp_o,
|
||||
name=f"o_tlr_t{T_q}_c{C}_s{S_kv}")
|
||||
ctx.launch(
|
||||
f"gqa_prefill_long_tile_ring_t{T_q}_c{C}_s{S_kv}",
|
||||
gqa_attention_prefill_long_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, D_HEAD, C,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
# ── T1: 128K ceiling lift ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_prefill_long_context_128k_completes():
|
||||
"""ADR-0063 §A.2 headline ceiling lift. At S_kv=128K with C=4,
|
||||
S_local=32K. Today: step-0 score-stack (~768 KB persistent) + ring
|
||||
scope (~768 KB) → 1.5 MB peak → TLContext scratch overflow. After
|
||||
P3c the persistent state is just ``(m, ℓ, O)`` (≈ 1 KB) and the
|
||||
per-tile in-scope scratch is bounded by TILE_S_KV.
|
||||
"""
|
||||
result = _run_prefill_ring(T_q=4, S_kv=131_072, C=4)
|
||||
assert result.completion.ok, (
|
||||
f"prefill_long at S_kv=128K must complete after tile-granular "
|
||||
f"ring lands; got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
# ── T2: tile-granular ipcq_copy count ────────────────────────────────
|
||||
|
||||
|
||||
def test_prefill_long_tile_granular_ipcq_count():
|
||||
"""ADR-0060 §5.5 amendment: with tile-granular sends, the per-CUBE
|
||||
send count grows from ``2·(C-1)`` to ``2·n_tiles·(C-1)``. Aggregated
|
||||
across all C CUBEs the total ipcq_copy becomes
|
||||
``2·n_tiles·(C-1)·C``.
|
||||
|
||||
Config: T_q=4, S_kv=4096, C=2 → S_local=2048, n_tiles=2 (with
|
||||
TILE_S_KV=1024). Today: slice-granular total =
|
||||
``(C-1)·2·C = 4``. After P3c: tile-granular total =
|
||||
``(C-1)·n_tiles·2·C = 8``.
|
||||
"""
|
||||
C = 2
|
||||
n_tiles = 2 # S_local=2048 / TILE_S_KV=1024
|
||||
result = _run_prefill_ring(T_q=4, S_kv=4096, C=C)
|
||||
assert result.completion.ok, (
|
||||
f"prefill_long multi-tile ring must complete; got {result.completion}"
|
||||
)
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
expected = (C - 1) * n_tiles * 2 * C
|
||||
assert n_copy == expected, (
|
||||
f"tile-granular ring: expected {expected} ipcq_copy "
|
||||
f"((C-1)·n_tiles·2·C = {C - 1}·{n_tiles}·2·{C}); got {n_copy}"
|
||||
)
|
||||
|
||||
|
||||
# ── T5: per-CUBE distributed output unchanged (regression guard) ─────
|
||||
|
||||
|
||||
def test_prefill_long_tile_ring_dma_write_count():
|
||||
"""ADR-0060 §5.5: per-CUBE distributed output must hold under the
|
||||
tile-granular ring rewrite. Each CUBE still writes its own head's
|
||||
output (no inter-CUBE reduce); dma_write_count == C.
|
||||
|
||||
Today passes; must continue to pass after P3c.
|
||||
"""
|
||||
C = 4
|
||||
result = _run_prefill_ring(T_q=4, S_kv=4096, C=C)
|
||||
assert result.completion.ok
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == C, (
|
||||
f"per-CUBE distributed output: expected {C} dma_writes (one per "
|
||||
f"CUBE); got {n_writes}"
|
||||
)
|
||||
@@ -1,161 +0,0 @@
|
||||
"""Phase 1 spec test for P6b GQA prefill Ring KV (head-parallel, C>1).
|
||||
|
||||
P6b adds the Ring KV rotation (ADR-0060 §5.5) to the prefill kernel.
|
||||
Each CUBE owns one Q head + its KV slice; over C ring steps the KV
|
||||
blocks rotate around the C CUBEs so every CUBE sees every block. The
|
||||
running ``(m, ℓ, O)`` is folded inside the loop. No reduce — each CUBE
|
||||
writes its own head's output.
|
||||
|
||||
Requires a new SFR install ``configure_sfr_intercube_ring(ring_size=C)``
|
||||
that wires:
|
||||
- ``intra_*`` : 2×4 PE grid within a CUBE (same as multisip)
|
||||
- ``E/W`` : 1D ring of CUBEs 0..ring_size-1 WITH WRAP
|
||||
(symmetric to ``configure_sfr_intracube_pe_ring``,
|
||||
applied at CUBE level)
|
||||
- ``global_*`` : SIP-level (same as multisip)
|
||||
|
||||
The kernel ring body:
|
||||
for step in range(1, C):
|
||||
tl.send(dir="W", src=Kc)
|
||||
tl.send(dir="W", src=Vc)
|
||||
Kc = tl.recv(dir="E", ...)
|
||||
Vc = tl.recv(dir="E", ...)
|
||||
... local partial + online-softmax merge into (m, ℓ, O) ...
|
||||
|
||||
Per CUBE per step: 2 sends (K, V) → 2 ``ipcq_copy``. Across all CUBEs:
|
||||
``(C-1) * 2 * C`` total ipcq_copy.
|
||||
|
||||
Restriction in P6b first cut: ``C ∈ {1, 2, 4}`` (single row of the 4×4
|
||||
cube mesh). C=8 ring would span rows — follow-on.
|
||||
|
||||
Phase 1 (this commit): tests only — production code lands in Phase 2.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches._gqa_attention_prefill_long import gqa_attention_prefill_long_kernel # noqa: F401
|
||||
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
|
||||
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring # noqa: F401 (Phase 2)
|
||||
from kernbench.policy.placement.dp import DPPolicy
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
D_HEAD = 64
|
||||
DTYPE = "f16"
|
||||
|
||||
|
||||
def _ccl_cfg():
|
||||
return resolve_algorithm_config(
|
||||
load_ccl_config(), name="lrab_hierarchical_allreduce",
|
||||
)
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _run_prefill_ring(*, T_q: int, S_kv: int, C: int):
|
||||
"""Head-parallel prefill with Ring KV across C CUBEs."""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
# P6b: new SFR install with cube-level ring wrap.
|
||||
configure_sfr_intercube_ring(
|
||||
ctx.engine, ctx.spec, _ccl_cfg(), ring_size=C,
|
||||
)
|
||||
# Q replicated on every CUBE (zeros for testing; per-CUBE head
|
||||
# indexing is implicit). KV sequence-sharded by CUBE. O
|
||||
# distributed — each CUBE writes its slice of (T_q*C, d_head).
|
||||
dp_q = DPPolicy(cube="replicate", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
dp_kv = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
dp_o = DPPolicy(cube="row_wise", pe="replicate",
|
||||
num_cubes=C, num_pes=1)
|
||||
q = ctx.zeros((T_q, D_HEAD), dtype=DTYPE, dp=dp_q,
|
||||
name=f"q_t{T_q}_c{C}_ring")
|
||||
k = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"k_t{T_q}_c{C}_ring")
|
||||
v = ctx.zeros((S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
|
||||
name=f"v_t{T_q}_c{C}_ring")
|
||||
# O: (T_q * C, d_head), each CUBE writes (T_q, d_head) at its
|
||||
# slice. dma_write_count = C (per-CUBE distributed output).
|
||||
o = ctx.empty((T_q * C, D_HEAD), dtype=DTYPE, dp=dp_o,
|
||||
name=f"o_t{T_q}_c{C}_ring")
|
||||
ctx.launch(
|
||||
f"gqa_prefill_ring_t{T_q}_s{S_kv}_c{C}",
|
||||
gqa_attention_prefill_long_kernel,
|
||||
q, k, v, o,
|
||||
T_q, S_kv, D_HEAD, C,
|
||||
_auto_dim_remap=False,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
# ── C=2 Ring KV ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_prefill_ring_c_two_completes():
|
||||
"""C=2: 1 ring step rotates KV between the 2 CUBEs."""
|
||||
result = _run_prefill_ring(T_q=4, S_kv=16, C=2)
|
||||
assert result.completion.ok, (
|
||||
f"prefill ring C=2 failed: {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_prefill_ring_c_two_distributed_output():
|
||||
"""C=2: per-CUBE distributed output, no reduce → dma_write_count == 2."""
|
||||
result = _run_prefill_ring(T_q=4, S_kv=16, C=2)
|
||||
assert result.completion.ok
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == 2, (
|
||||
f"C=2 prefill: expected 2 dma_write (one per CUBE); got {n_writes}"
|
||||
)
|
||||
|
||||
|
||||
def test_prefill_ring_c_two_ipcq_count():
|
||||
"""C=2: 1 ring step × 2 handles (K, V) × 2 CUBEs = 4 ipcq_copy."""
|
||||
result = _run_prefill_ring(T_q=4, S_kv=16, C=2)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
expected = (2 - 1) * 2 * 2
|
||||
assert n_copy == expected, (
|
||||
f"C=2 ring: expected {expected} ipcq_copy "
|
||||
f"((C-1)·2·C = 1·2·2); got {n_copy}"
|
||||
)
|
||||
|
||||
|
||||
# ── C=4 Ring KV (combined assertions) ─────────────────────────────────
|
||||
|
||||
|
||||
def test_prefill_ring_c_four_combined():
|
||||
"""C=4: 3 ring steps rotate KV around 4 CUBEs.
|
||||
Expected: completes; 4 dma_writes; (4-1)·2·4 = 24 ipcq_copy."""
|
||||
result = _run_prefill_ring(T_q=4, S_kv=32, C=4)
|
||||
assert result.completion.ok, (
|
||||
f"prefill ring C=4 failed: {result.completion}"
|
||||
)
|
||||
n_writes = _count(result.engine.op_log, "dma_write")
|
||||
assert n_writes == 4, (
|
||||
f"C=4 prefill: expected 4 dma_write (one per CUBE); got {n_writes}"
|
||||
)
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
expected = (4 - 1) * 2 * 4
|
||||
assert n_copy == expected, (
|
||||
f"C=4 ring: expected {expected} ipcq_copy "
|
||||
f"((C-1)·2·C = 3·2·4); got {n_copy}"
|
||||
)
|
||||
@@ -76,7 +76,7 @@ def _run_decode_short(*, h_q: int, h_kv: int, kv_per_cube: int,
|
||||
O: replicated; each group root writes the full byte-conserving
|
||||
(h_q·T_q, D_HEAD) result.
|
||||
"""
|
||||
from kernbench.benches._gqa_attention_decode_short import gqa_attention_decode_short_kernel # Phase 2
|
||||
from kernbench.benches.gqa_helpers.short_ctx._gqa_attention_decode_short import gqa_attention_decode_short_kernel # Phase 2
|
||||
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
@@ -196,7 +196,7 @@ def _run_prefill_short(*, h_kv: int, kv_per_cube: int,
|
||||
Layout: same head-stacked K/V scheme as decode short, with Q/O
|
||||
replicated and byte-conserving reshape inside the kernel.
|
||||
"""
|
||||
from kernbench.benches._gqa_attention_prefill_short import gqa_attention_prefill_short_kernel # Phase 2
|
||||
from kernbench.benches.gqa_helpers.short_ctx._gqa_attention_prefill_short import gqa_attention_prefill_short_kernel # Phase 2
|
||||
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def _run_case4_smoke(*, S_kv: int):
|
||||
``S_kv=128K`` runs come from ``kernbench run --bench
|
||||
milestone-gqa-decode-long-ctx-4cases``, not pytest.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_run_decode_panel_long_ctx_cube_sp_pe_sp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
@@ -99,13 +99,13 @@ def test_case4_panel_registered():
|
||||
C = 8 (head-parallel CUBE Group)
|
||||
P = 8 (intra-CUBE PE-SP)
|
||||
T_q = 1 (decode: one new token per pass)
|
||||
S_kv = 131_072 (LLaMA long-context decode target)
|
||||
S_kv = 8_192 (LLaMA long-context decode target)
|
||||
d_head = 128, h_q = 8, h_kv = 1
|
||||
|
||||
The lrab sub_w=4 / sub_h=2 geometry is baked into the Case 4
|
||||
wrapper kernel; it is not a panel parameter.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
@@ -120,7 +120,7 @@ def test_case4_panel_registered():
|
||||
assert params.get("C") == 8
|
||||
assert params.get("P") == 8
|
||||
assert params.get("T_q") == 1
|
||||
assert params.get("S_kv") == 131_072
|
||||
assert params.get("S_kv") == 8_192
|
||||
assert params.get("d_head") == 128
|
||||
assert params.get("h_q") == 8
|
||||
assert params.get("h_kv") == 1
|
||||
@@ -197,7 +197,7 @@ def _run_case2_smoke(*, S_kv: int):
|
||||
slide-11 memory waste); for B=1 only one rank does the work; no
|
||||
inter-rank comm.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_run_decode_panel_long_ctx_cube_repl_pe_tp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
@@ -229,7 +229,7 @@ def test_case2_panel_registered():
|
||||
For B=1 only one rank works (PEs 1-7 idle — slide-11 calls
|
||||
out this PE-TP waste).
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
@@ -242,7 +242,7 @@ def test_case2_panel_registered():
|
||||
assert params.get("C") == 8
|
||||
assert params.get("P") == 8
|
||||
assert params.get("T_q") == 1
|
||||
assert params.get("S_kv") == 131_072
|
||||
assert params.get("S_kv") == 8_192
|
||||
assert params.get("d_head") == 128
|
||||
assert params.get("h_q") == 8
|
||||
assert params.get("h_kv") == 1
|
||||
@@ -303,7 +303,7 @@ def _run_case3_smoke(*, S_kv: int):
|
||||
8-way across PEs within each cube. Intra-CUBE 8-way reduce on
|
||||
(m, ℓ, O); no inter-CUBE comm (every cube ends with full answer).
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_run_decode_panel_long_ctx_cube_repl_pe_sp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
@@ -333,7 +333,7 @@ def test_case3_panel_registered():
|
||||
Case 3: Cube-Repl × PE-SP. K, V replicated per cube; PEs SP on
|
||||
S_kv. Intra-CUBE 8-way AR; no inter-CUBE comm.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
@@ -346,7 +346,7 @@ def test_case3_panel_registered():
|
||||
assert params.get("C") == 8
|
||||
assert params.get("P") == 8
|
||||
assert params.get("T_q") == 1
|
||||
assert params.get("S_kv") == 131_072
|
||||
assert params.get("S_kv") == 8_192
|
||||
assert params.get("d_head") == 128
|
||||
assert params.get("h_q") == 8
|
||||
assert params.get("h_kv") == 1
|
||||
@@ -423,7 +423,7 @@ def _run_case1_smoke(*, S_kv: int):
|
||||
each cube has work; PEs 1-7 idle. Inter-CUBE 8-way reduce via the
|
||||
lrab-adapted center-root pattern (root cube 6); no intra-CUBE comm.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_run_decode_panel_long_ctx_cube_sp_pe_tp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
@@ -454,7 +454,7 @@ def test_case1_panel_registered():
|
||||
batch. At B=1 only PE 0 of each cube works (PE-TP waste).
|
||||
Inter-CUBE lrab AR; no intra-CUBE comm.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
@@ -467,7 +467,7 @@ def test_case1_panel_registered():
|
||||
assert params.get("C") == 8
|
||||
assert params.get("P") == 8
|
||||
assert params.get("T_q") == 1
|
||||
assert params.get("S_kv") == 131_072
|
||||
assert params.get("S_kv") == 8_192
|
||||
assert params.get("d_head") == 128
|
||||
assert params.get("h_q") == 8
|
||||
assert params.get("h_kv") == 1
|
||||
@@ -548,7 +548,7 @@ def test_panel_metrics_helpers_present_and_correct():
|
||||
Helpers exercised via the Case 2 smoke runner's op_log to avoid
|
||||
paying the 128K-config ``_run_panel`` runtime.
|
||||
"""
|
||||
from kernbench.benches.milestone_gqa_decode_long_ctx_4cases import (
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases import (
|
||||
_end_to_end_ns,
|
||||
_engine_occupancy_ns,
|
||||
)
|
||||
@@ -579,7 +579,7 @@ def test_run_panel_returns_latency_and_engine_occupancy(monkeypatch):
|
||||
Uses ``monkeypatch.setitem`` on ``_PANEL_DISPATCH`` to lower S_kv to
|
||||
``_SMOKE_S_KV`` just for this test — avoids the 131K runtime.
|
||||
"""
|
||||
import kernbench.benches.milestone_gqa_decode_long_ctx_4cases as mod
|
||||
import kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_4cases as mod
|
||||
|
||||
orig_kind, orig_params = mod._PANEL_DISPATCH[_CASE2_PANEL]
|
||||
fast_params = {**orig_params, "S_kv": _SMOKE_S_KV}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Tests for the milestone-gqa-headline bench.
|
||||
|
||||
The bench wires the new ``_gqa_attention_prefill_long`` kernel into a
|
||||
single-KV-group milestone panel (LLaMA-3.1-70B target, C=8 P=8). The
|
||||
4 legacy C=1 / C=4 panels (single_user_*, multi_user_*) were removed
|
||||
once pytest regression covered those configurations comprehensively
|
||||
and the comparative decode work moved into the
|
||||
``milestone-gqa-decode-4cases`` bench.
|
||||
|
||||
Single SIP scope (default ``topology.yaml`` 4×4 cube mesh; 4-SIP
|
||||
headline deferred per ADR-0060 §B-item-1).
|
||||
"""
|
||||
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_kv_group_prefill_gqa_c8_p8",
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Per-panel architectural assertions for the surviving single-KV-group
|
||||
# panel live in tests/attention/test_milestone_gqa_single_kv_group_prefill_panel.py.
|
||||
# Decode architectural assertions live in
|
||||
# tests/attention/test_milestone_gqa_decode_4cases.py.
|
||||
@@ -0,0 +1,471 @@
|
||||
"""Tests for the long-context prefill 4-cases comparative-study bench.
|
||||
|
||||
Prefill counterpart to test_milestone_gqa_decode_long_ctx_4cases. The
|
||||
4 cases differ in how KV cache is sharded across the 8 cubes and 8
|
||||
PEs of a single KV-head group on LLaMA-3.1-70B GQA:
|
||||
|
||||
Case 1 Cube-SP / PE-TP → KV split by S_kv across cubes (Ring);
|
||||
Q/O split T_q across all 64 ranks
|
||||
Case 2 Cube-Repl / PE-TP → full KV per cube; only CUBE 0's P PEs
|
||||
split T_q (CUBEs 1..7 = pure memory waste)
|
||||
Case 3 Cube-Repl / PE-SP → full KV per cube; PEs SP on S_kv
|
||||
(intra-cube AR); 8× redundant compute
|
||||
Case 4 Cube-SP / PE-SP → KV split 64-way + Q split T_q across cubes;
|
||||
Ring KV + intra-cube AR per cube ★ optimal
|
||||
|
||||
Smoke sizes (T_q=256, S_kv=2048) keep the per-PE scratch comfortably
|
||||
inside budget; the headline T_q=S_kv=4096 runs come from
|
||||
``kernbench run --bench milestone-gqa-prefill-long-ctx-4cases``, not
|
||||
pytest.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
_CASE4_PANEL = "single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_sp"
|
||||
_CASE3_PANEL = "single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_sp"
|
||||
_CASE2_PANEL = "single_kv_group_prefill_long_ctx_gqa_cube_repl_pe_tp"
|
||||
_CASE1_PANEL = "single_kv_group_prefill_long_ctx_gqa_cube_sp_pe_tp"
|
||||
_CUBE_RE = re.compile(r"\bcube(\d+)\b")
|
||||
|
||||
# Smoke sizes: T_q=256 (divisible by C·P=64 for Case 1, by P=8 for
|
||||
# Case 2, by C=8 for Case 4) and S_kv=2048 (≥ 2·TILE_S_KV so the
|
||||
# per-tile fold loop runs; Case 1/4 have S_local sufficient to exercise
|
||||
# the Ring). Assertions are size-independent. The headline T_q=S_kv=4096
|
||||
# bench runs come from the env-gated bench, not pytest.
|
||||
# Smoke sizes — kept small so pytest stays fast:
|
||||
# T_q=64 : minimum that satisfies Case 1's ``T_q % (C·P) == 0``.
|
||||
# S_kv=512: enough to exercise multi-tile fold for Cases 2/3
|
||||
# (TILE_S_KV=64 → 8 tiles for Case 2; 1 tile/PE for Case 3
|
||||
# after intra-cube S_kv split) without burning minutes on
|
||||
# Case 2's single-rank serial path.
|
||||
# Cases 2/3 also use TILE_S_KV=64 in their kernels to keep the
|
||||
# persistent ``scores`` tensor inside the 1 MB scratch budget at
|
||||
# T_q=64. Headline T_q=S_kv=4096 runs come from the env-gated bench,
|
||||
# not pytest.
|
||||
_SMOKE_T_Q = 64
|
||||
_SMOKE_S_KV = 512
|
||||
|
||||
_HEADLINE_T_Q = 512
|
||||
_HEADLINE_S_KV = 512
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
def _count(op_log, name: str) -> int:
|
||||
return sum(1 for r in op_log if r.op_name == name)
|
||||
|
||||
|
||||
def _dma_write_cubes(op_log) -> list[int]:
|
||||
cubes: list[int] = []
|
||||
for r in op_log:
|
||||
if r.op_name != "dma_write":
|
||||
continue
|
||||
m = _CUBE_RE.search(r.component_id)
|
||||
if m is not None:
|
||||
cubes.append(int(m.group(1)))
|
||||
return cubes
|
||||
|
||||
|
||||
# ── Case 4 (Cube-SP × PE-SP) — ★ optimal ────────────────────────────
|
||||
|
||||
|
||||
def _run_case4_smoke(*, T_q: int, S_kv: int):
|
||||
"""Drive the Case 4 prefill panel via the case-specific runner."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_sp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_sp(
|
||||
ctx, panel=_CASE4_PANEL,
|
||||
C=8, P=8,
|
||||
T_q=T_q, S_kv=S_kv,
|
||||
d_head=128, h_q=8, h_kv=1,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def test_case4_panel_registered():
|
||||
"""Case 4 panel must be in ``_PANELS`` + ``_PANEL_DISPATCH`` with
|
||||
the LLaMA-3.1-70B single-KV-group prefill dims (T_q=S_kv=4096).
|
||||
"""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
assert _CASE4_PANEL in _PANELS, f"{_CASE4_PANEL!r} not in {_PANELS}"
|
||||
assert _CASE4_PANEL in _PANEL_DISPATCH
|
||||
kind, params = _PANEL_DISPATCH[_CASE4_PANEL]
|
||||
assert kind == "prefill_long_ctx_cube_sp_pe_sp"
|
||||
assert params == {
|
||||
"C": 8, "P": 8,
|
||||
"T_q": _HEADLINE_T_Q, "S_kv": _HEADLINE_S_KV,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_case4_runner_smoke():
|
||||
"""Case 4 runner drives the new kernel to completion at smoke sizes."""
|
||||
result = _run_case4_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok, (
|
||||
f"Case 4 prefill smoke at C=8 P=8 must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_case4_writers_one_per_cube():
|
||||
"""Case 4 prefill: each cube writes its own disjoint T_q/C rows
|
||||
(PE 0 of each cube). Output cubes must cover all 8.
|
||||
"""
|
||||
result = _run_case4_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
cubes = _dma_write_cubes(result.engine.op_log)
|
||||
assert cubes, "expected at least one dma_write for the final O store"
|
||||
distinct = set(cubes)
|
||||
assert distinct == set(range(8)), (
|
||||
f"Case 4 expected PE 0 of every cube to write; got cubes={sorted(distinct)}"
|
||||
)
|
||||
|
||||
|
||||
def test_case4_ipcq_copy_positive():
|
||||
"""Case 4 prefill has both inter-CUBE Ring and intra-CUBE AR
|
||||
traffic; total ipcq_copy must be strictly positive.
|
||||
"""
|
||||
result = _run_case4_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
assert n_copy > 0, f"Case 4 must have Ring + intra-CUBE comm; got 0 ipcq_copy"
|
||||
|
||||
|
||||
# ── Case 2 (Cube-Repl × PE-TP) ──────────────────────────────────────
|
||||
|
||||
|
||||
def _run_case2_smoke(*, T_q: int, S_kv: int):
|
||||
"""Drive the Case 2 prefill panel via the case-specific runner."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_tp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_tp(
|
||||
ctx, panel=_CASE2_PANEL,
|
||||
C=8, P=8,
|
||||
T_q=T_q, S_kv=S_kv,
|
||||
d_head=128, h_q=8, h_kv=1,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def test_case2_panel_registered():
|
||||
"""Case 2 panel registered with single-KV-group prefill dims."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
assert _CASE2_PANEL in _PANELS, f"{_CASE2_PANEL!r} not in {_PANELS}"
|
||||
assert _CASE2_PANEL in _PANEL_DISPATCH
|
||||
kind, params = _PANEL_DISPATCH[_CASE2_PANEL]
|
||||
assert kind == "prefill_long_ctx_cube_repl_pe_tp"
|
||||
assert params == {
|
||||
"C": 8, "P": 8,
|
||||
"T_q": _HEADLINE_T_Q, "S_kv": _HEADLINE_S_KV,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_case2_runner_smoke():
|
||||
"""Case 2 runner drives the new kernel to completion at smoke sizes."""
|
||||
result = _run_case2_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok, (
|
||||
f"Case 2 prefill smoke at C=8 P=8 must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_case2_zero_ipcq_copy_no_comm():
|
||||
"""Case 2's defining property: full KV per rank ⇒ NO inter-rank
|
||||
communication. Slide 11 lists comm cost as 'none'.
|
||||
"""
|
||||
result = _run_case2_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
assert n_copy == 0, (
|
||||
f"Case 2 must have zero inter-rank comm; got ipcq_copy={n_copy}"
|
||||
)
|
||||
|
||||
|
||||
def test_case2_writers_only_cube_0():
|
||||
"""Case 2: only CUBE 0 active (PEs 0..P-1 each writing their T_q/P
|
||||
rows). All dma_writes come from cube 0.
|
||||
"""
|
||||
result = _run_case2_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
cubes = _dma_write_cubes(result.engine.op_log)
|
||||
assert cubes, "expected at least one dma_write for the final O store"
|
||||
distinct = set(cubes)
|
||||
assert distinct == {0}, (
|
||||
f"Case 2 writers must be cube 0 only; got cubes={sorted(distinct)}"
|
||||
)
|
||||
|
||||
|
||||
# ── Case 3 (Cube-Repl × PE-SP) ──────────────────────────────────────
|
||||
|
||||
|
||||
def _run_case3_smoke(*, T_q: int, S_kv: int):
|
||||
"""Drive the Case 3 prefill panel via the case-specific runner."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_sp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel_long_ctx_cube_repl_pe_sp(
|
||||
ctx, panel=_CASE3_PANEL,
|
||||
C=8, P=8,
|
||||
T_q=T_q, S_kv=S_kv,
|
||||
d_head=128, h_q=8, h_kv=1,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def test_case3_panel_registered():
|
||||
"""Case 3 panel registered with single-KV-group prefill dims."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
assert _CASE3_PANEL in _PANELS, f"{_CASE3_PANEL!r} not in {_PANELS}"
|
||||
assert _CASE3_PANEL in _PANEL_DISPATCH
|
||||
kind, params = _PANEL_DISPATCH[_CASE3_PANEL]
|
||||
assert kind == "prefill_long_ctx_cube_repl_pe_sp"
|
||||
assert params == {
|
||||
"C": 8, "P": 8,
|
||||
"T_q": _HEADLINE_T_Q, "S_kv": _HEADLINE_S_KV,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_case3_runner_smoke():
|
||||
"""Case 3 runner drives the new kernel to completion at smoke sizes."""
|
||||
result = _run_case3_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok, (
|
||||
f"Case 3 prefill smoke at C=8 P=8 must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_case3_intra_cube_ar_only_ipcq():
|
||||
"""Case 3 reduce pattern: per-CUBE 8-way PE-SP AR (same structural
|
||||
cost as decode Case 3's intra-CUBE phase = 21 ipcq_copy per cube),
|
||||
and NO inter-CUBE traffic (each cube has a full copy of KV).
|
||||
|
||||
Q-axis tiling tradeoff: prefill Case 3 wraps the intra-CUBE AR
|
||||
INSIDE the outer Q-tile loop (the kernel can't hold full
|
||||
(m, ℓ, O) for the whole T_q in scratch). So the AR runs once
|
||||
per Q-tile → total ipcq = n_q_tiles × per-tile cost.
|
||||
|
||||
per-CUBE intra (2×4 PE grid), PER Q-TILE:
|
||||
row chain along intra_W: cols 1,2,3 each row × 2 rows ×
|
||||
3 tensors (m, ℓ, O) = 18
|
||||
col bridge along intra_N: pe4 only × 3 tensors = 3
|
||||
per-CUBE intra total = 21
|
||||
× 8 CUBEs = 168
|
||||
× n_q_tiles (= G·T_q / TILE_Q = 8·64/64 = 8) = 1344
|
||||
|
||||
inter-CUBE: 0 (replicated KV ⇒ no AllReduce needed).
|
||||
"""
|
||||
from kernbench.benches.gqa_helpers.long_ctx._gqa_attention_prefill_long_ctx_cube_repl_pe_sp import (
|
||||
TILE_Q,
|
||||
)
|
||||
|
||||
result = _run_case3_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
G = 8 # h_q / h_kv
|
||||
n_q_tiles = (G * _SMOKE_T_Q + TILE_Q - 1) // TILE_Q
|
||||
expected = n_q_tiles * 168
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
assert n_copy == expected, (
|
||||
f"Case 3 expected {expected} ipcq_copy "
|
||||
f"({n_q_tiles} Q-tiles × 168 per Q-tile intra-CUBE AR); "
|
||||
f"got {n_copy}"
|
||||
)
|
||||
|
||||
|
||||
def test_case3_single_dma_write_at_cube_0():
|
||||
"""Every cube ends with the full answer after intra-CUBE AR; only
|
||||
the designated writer (cube 0, PE 0) stores O.
|
||||
"""
|
||||
result = _run_case3_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
cubes = _dma_write_cubes(result.engine.op_log)
|
||||
assert cubes, "expected at least one dma_write for the final O store"
|
||||
distinct = set(cubes)
|
||||
assert distinct == {0}, (
|
||||
f"Case 3 designated writer must be cube 0; got cubes={sorted(distinct)}"
|
||||
)
|
||||
|
||||
|
||||
# ── Case 1 (Cube-SP × PE-TP) ────────────────────────────────────────
|
||||
|
||||
|
||||
def _run_case1_smoke(*, T_q: int, S_kv: int):
|
||||
"""Drive the Case 1 prefill panel via the case-specific runner."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_tp,
|
||||
)
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel_long_ctx_cube_sp_pe_tp(
|
||||
ctx, panel=_CASE1_PANEL,
|
||||
C=8, P=8,
|
||||
T_q=T_q, S_kv=S_kv,
|
||||
d_head=128, h_q=8, h_kv=1,
|
||||
)
|
||||
|
||||
return run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
|
||||
|
||||
def test_case1_panel_registered():
|
||||
"""Case 1 panel registered with single-KV-group prefill dims."""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
)
|
||||
assert _CASE1_PANEL in _PANELS, f"{_CASE1_PANEL!r} not in {_PANELS}"
|
||||
assert _CASE1_PANEL in _PANEL_DISPATCH
|
||||
kind, params = _PANEL_DISPATCH[_CASE1_PANEL]
|
||||
assert kind == "prefill_long_ctx_cube_sp_pe_tp"
|
||||
assert params == {
|
||||
"C": 8, "P": 8,
|
||||
"T_q": _HEADLINE_T_Q, "S_kv": _HEADLINE_S_KV,
|
||||
"d_head": 128, "h_q": 8, "h_kv": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_case1_runner_smoke():
|
||||
"""Case 1 runner drives the new kernel to completion at smoke sizes."""
|
||||
result = _run_case1_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok, (
|
||||
f"Case 1 prefill smoke at C=8 P=8 must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
def test_case1_ipcq_copy_positive_ring_only():
|
||||
"""Case 1 has inter-CUBE Ring (P parallel lanes) only; total ipcq
|
||||
must be positive. No intra-CUBE comm (PEs are disjoint on T_q).
|
||||
"""
|
||||
result = _run_case1_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
n_copy = _count(result.engine.op_log, "ipcq_copy")
|
||||
assert n_copy > 0, f"Case 1 must have Ring KV comm; got 0 ipcq_copy"
|
||||
|
||||
|
||||
def test_case1_writers_all_64_ranks():
|
||||
"""Case 1 prefill: every rank owns disjoint T_q/(C·P) rows and
|
||||
writes them. All 8 cubes appear in dma_write component ids.
|
||||
"""
|
||||
result = _run_case1_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
cubes = _dma_write_cubes(result.engine.op_log)
|
||||
assert cubes, "expected at least one dma_write for the final O store"
|
||||
distinct = set(cubes)
|
||||
assert distinct == set(range(8)), (
|
||||
f"Case 1 expected every cube to write; got cubes={sorted(distinct)}"
|
||||
)
|
||||
|
||||
|
||||
# ── Panel-metrics helpers + _run_panel wiring ───────────────────────
|
||||
|
||||
|
||||
_EXPECTED_ENGINES = {
|
||||
"pe_gemm", "pe_math", "pe_dma", "pe_fetch_store", "pe_ipcq", "pe_cpu",
|
||||
}
|
||||
|
||||
|
||||
def test_panel_metrics_helpers_present_and_correct():
|
||||
"""Bench file must expose ``_end_to_end_ns`` and
|
||||
``_engine_occupancy_ns`` for the comparative plot script.
|
||||
Exercised via the Case 2 smoke runner's op_log (cheapest case).
|
||||
"""
|
||||
from kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases import (
|
||||
_end_to_end_ns,
|
||||
_engine_occupancy_ns,
|
||||
)
|
||||
result = _run_case2_smoke(T_q=_SMOKE_T_Q, S_kv=_SMOKE_S_KV)
|
||||
assert result.completion.ok
|
||||
op_log = result.engine.op_log
|
||||
|
||||
lat = _end_to_end_ns(op_log)
|
||||
assert lat > 0, f"expected positive end-to-end latency; got {lat}"
|
||||
|
||||
occ = _engine_occupancy_ns(op_log)
|
||||
assert isinstance(occ, dict)
|
||||
assert set(occ.keys()) >= _EXPECTED_ENGINES, (
|
||||
f"engine_occupancy_ns missing required engines; "
|
||||
f"got {set(occ.keys())}"
|
||||
)
|
||||
assert occ["pe_gemm"] > 0, (
|
||||
f"expected pe_gemm occupancy > 0; got {occ['pe_gemm']}"
|
||||
)
|
||||
|
||||
|
||||
def test_run_panel_returns_latency_and_engine_occupancy(monkeypatch):
|
||||
"""``_run_panel`` must include ``latency_ns`` + ``engine_occupancy_ns``
|
||||
alongside ``op_log_summary`` so sweep.json carries them for the
|
||||
comparative plot script. Uses monkeypatch to swap the Case 2 panel's
|
||||
T_q/S_kv to smoke sizes for speed.
|
||||
"""
|
||||
import kernbench.benches.gqa_helpers.long_ctx.gqa_prefill_long_ctx_4cases as mod
|
||||
|
||||
orig_kind, orig_params = mod._PANEL_DISPATCH[_CASE2_PANEL]
|
||||
fast_params = {**orig_params, "T_q": _SMOKE_T_Q, "S_kv": _SMOKE_S_KV}
|
||||
monkeypatch.setitem(
|
||||
mod._PANEL_DISPATCH, _CASE2_PANEL, (orig_kind, fast_params),
|
||||
)
|
||||
|
||||
row = mod._run_panel(_CASE2_PANEL, str(TOPOLOGY_DEFAULT))
|
||||
assert "op_log_summary" in row
|
||||
assert "latency_ns" in row, (
|
||||
f"_run_panel row missing latency_ns; keys={sorted(row.keys())}"
|
||||
)
|
||||
assert row["latency_ns"] > 0
|
||||
assert "engine_occupancy_ns" in row, (
|
||||
f"_run_panel row missing engine_occupancy_ns; "
|
||||
f"keys={sorted(row.keys())}"
|
||||
)
|
||||
assert isinstance(row["engine_occupancy_ns"], dict)
|
||||
assert set(row["engine_occupancy_ns"].keys()) >= _EXPECTED_ENGINES
|
||||
@@ -1,121 +0,0 @@
|
||||
"""Tests for the single-KV-group prefill panel in milestone_gqa_headline.
|
||||
|
||||
The single-KV-group (LLaMA-3.1-70B target) prefill panel wires together
|
||||
all Increment 1–4 work in a single bench panel:
|
||||
- C=8 head-parallel + Ring KV via snake-mapped 2×4 sub-mesh (Inc 1, 3)
|
||||
- P=8 intra-CUBE PE-SP (Inc 4)
|
||||
- d_head=128 (LLaMA-3.1-70B), one-shot prefill T_q = S_kv = 1K
|
||||
(scratch-limited; LLaMA 32K headline awaits Q-axis kernel tiling)
|
||||
|
||||
This file verifies the bench-config wiring:
|
||||
T1 the new ``single_kv_group_prefill_gqa_c8_p8`` panel is registered
|
||||
with the expected dims in ``_PANELS`` + ``_PANEL_DISPATCH``.
|
||||
T2 ``_run_prefill_panel`` accepts the new ``P``, ``T_q``, ``d_head``
|
||||
kwargs and drives the prefill kernel to completion at the
|
||||
milestone-target ``(C, P) = (8, 8)``. Uses smaller T_q/S_kv to
|
||||
keep test time bounded — full 32K runs come from
|
||||
``kernbench run milestone-gqa-headline``.
|
||||
T3 Existing prefill panels still work when ``_run_prefill_panel``
|
||||
is called without the new kwargs (backward compat anchor).
|
||||
|
||||
Phase 1: tests only — production code lands in Phase 2.
|
||||
T1 fails today (panel not registered).
|
||||
T2 fails today (helper doesn't accept the new kwargs).
|
||||
T3 passes today as the backward-compat anchor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from kernbench.benches.milestone_gqa_headline import ( # noqa: F401
|
||||
_PANEL_DISPATCH,
|
||||
_PANELS,
|
||||
_run_prefill_panel,
|
||||
)
|
||||
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
|
||||
|
||||
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
|
||||
|
||||
|
||||
def _engine_factory(t, d):
|
||||
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
|
||||
|
||||
|
||||
# ── T1: new panel registered ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_single_kv_group_prefill_panel_registered():
|
||||
"""The ``single_kv_group_prefill_gqa_c8_p8`` panel must be in ``_PANELS`` and
|
||||
its dispatch entry must have the expected LLaMA-3.1-70B params.
|
||||
|
||||
Headline config (scratch-limited; LLaMA 32K headline awaits
|
||||
Q-axis kernel tiling):
|
||||
C = 8 (head-parallel, snake sub-mesh)
|
||||
P = 8 (intra-CUBE PE-SP)
|
||||
T_q = 1_024 (one-shot long-context prefill)
|
||||
S_kv = 1_024
|
||||
(scratch-limited; LLaMA 32K headline awaits Q-axis kernel tiling)
|
||||
d_head = 128 (LLaMA-3.1-70B)
|
||||
"""
|
||||
panel_name = "single_kv_group_prefill_gqa_c8_p8"
|
||||
assert panel_name in _PANELS, (
|
||||
f"{panel_name!r} not in _PANELS; got {_PANELS}"
|
||||
)
|
||||
assert panel_name in _PANEL_DISPATCH, (
|
||||
f"{panel_name!r} not in _PANEL_DISPATCH"
|
||||
)
|
||||
kind, params = _PANEL_DISPATCH[panel_name]
|
||||
assert kind == "prefill", f"kind={kind!r}, expected 'prefill'"
|
||||
assert params.get("C") == 8, f"C={params.get('C')}, expected 8"
|
||||
assert params.get("P") == 8, f"P={params.get('P')}, expected 8"
|
||||
assert params.get("T_q") == 1_024, (
|
||||
f"T_q={params.get('T_q')}, expected 1_024"
|
||||
)
|
||||
assert params.get("S_kv") == 1_024, (
|
||||
f"S_kv={params.get('S_kv')}, expected 1_024"
|
||||
)
|
||||
assert params.get("d_head") == 128, (
|
||||
f"d_head={params.get('d_head')}, expected 128"
|
||||
)
|
||||
|
||||
|
||||
# ── T2: helper drives the C=8, P=8 prefill kernel to completion ──────
|
||||
|
||||
|
||||
def test_single_kv_group_prefill_panel_runner_smoke():
|
||||
"""``_run_prefill_panel`` must accept the new ``P``, ``T_q``,
|
||||
``d_head`` kwargs and successfully launch the prefill kernel at
|
||||
``(C, P) = (8, 8)`` with the snake-mapped 2×4 SFR.
|
||||
|
||||
Uses **smaller** T_q/S_kv than the headline panel so the test
|
||||
completes quickly. The headline 32K dims are exercised via
|
||||
``kernbench run milestone-gqa-headline``, not pytest.
|
||||
"""
|
||||
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
|
||||
|
||||
def _bench_fn(ctx):
|
||||
_run_prefill_panel(
|
||||
ctx,
|
||||
panel="single_kv_group_prefill_gqa_c8_p8",
|
||||
C=8, P=8,
|
||||
T_q=8, S_kv=8192, # smoke dims, not the 32K headline
|
||||
d_head=128,
|
||||
)
|
||||
|
||||
result = run_bench(
|
||||
topology=topo, bench_fn=_bench_fn,
|
||||
device=resolve_device(None),
|
||||
engine_factory=_engine_factory,
|
||||
)
|
||||
assert result.completion.ok, (
|
||||
f"single_kv_group prefill panel smoke at C=8 P=8 must complete; "
|
||||
f"got {result.completion}"
|
||||
)
|
||||
|
||||
|
||||
# Backward-compat test removed when the legacy multi_user_prefill_gqa
|
||||
# panel was dropped; signature-extension contract is now exercised
|
||||
# implicitly by the live single_kv_group_prefill_gqa_c8_p8 panel.
|
||||