"""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()