Files
kernbench2/scripts/paper/paper_plot_gqa_decode_long_ctx_4cases.py
T
mukesh e45626c036 gqa: reorganize benches into gqa_helpers/ subpackage; drop legacy headline
Splits the GQA helpers into a dedicated subpackage to make room for the
prefill 4-cases study (next commit) and a single umbrella bench
(milestone-1h-gqa, after that).

Layout:
  benches/gqa_helpers/
    long_ctx/    — decode 4-cases kernels + sweep runner
    short_ctx/   — prefill/decode short-context kernels
    shared/      — _gqa_panel_helpers + decode_opt2 (context-agnostic)

The registry audit now skips subpackages so gqa_helpers/ (without a
leading underscore) doesn't get audited for @bench decorators.

Also drops the legacy milestone-gqa-headline bench, its
_gqa_attention_prefill_long kernel, 6 dependent prefill tests, the
paper_gqa_latency.py report harness, and the 3 stale headline-derived
PNGs the §6 wire-up referenced (paper will re-pull from the new
1H_milestone_output/gqa/long_ctx/ once §6 is updated).

The _ccl_cfg and _summarize_op_log helpers used to live in the
headline bench; extracted them to gqa_helpers/shared/_gqa_panel_helpers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:05:41 -07:00

209 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 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 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 \\
--bench milestone-gqa-decode-long-ctx-4cases --topology topology.yaml
python scripts/paper/paper_plot_gqa_decode_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_decode.json"
# Panel name → (short label, case ordinal for left-to-right plot order).
_CASE_INFO = {
"single_kv_group_decode_long_ctx_gqa_cube_sp_pe_tp": (
"Case 1\nCube-SP × PE-TP", 1),
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_tp": (
"Case 2\nCube-Repl × PE-TP", 2),
"single_kv_group_decode_long_ctx_gqa_cube_repl_pe_sp": (
"Case 3\nCube-Repl × PE-SP", 3),
"single_kv_group_decode_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 decode 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_decode_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 decode 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_decode_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 (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 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_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"] # 4 highlighted, 2 marked red
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 decode 4-cases — KV memory per PE\n"
"(one KV-head group; per-layer, per-token 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_decode_long_ctx_4cases_memory.png"
fig.savefig(out, dpi=150)
plt.close(fig)
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__":
main()