"""Prefill mode-comparison sweep across context lengths. Measures per (mode ∈ {A1, A2, A4, B}) × (S_kv ∈ {8K, 16K, 32K, 64K}): 1. Wall clock latency (μs) 2. Hardware utilization - GEMM engine utilization (Σ gemm time / wall × n_pe) - MATH op count 3. HBM bandwidth utilization (Σ dma_read+write bytes / (wall × n_pe × peak_pe_hbm_bw)) 4. Communication cost (IPCQ bytes — broadcast) 5. KV cache space cost (per-cube bytes) Sliced-prefill convention: T_q = 8. Output: ``docs/sweeps/prefill_sweep.csv`` (16 rows). Run: ``pytest tests/attention/test_gqa_short_context_sweep_prefill.py -m slow`` """ from __future__ import annotations import csv from pathlib import Path import pytest from tests.attention.test_gqa_short_context import ( D_HEAD, H_KV, P, TILE_S_KV, _run_prefill, ) # Peak HBM BW per PE — derived from topology (cube total / PE count). # topology.yaml: hbm_total_bw_gbs = 1024.0 per cube, 8 PE per cube. PEAK_PE_HBM_BPS = 8 * 32.0 # 8 channels/PE x 32 GB/s = 256 bytes/ns per PE MATH_OPS = {"max", "sum", "exp", "sub", "mul_bcast", "add", "div", "maximum", "minimum", "neg", "rmax", "rsum", "exp_diff", "max_elem"} MODES = [ ("A1", 1, 8), ("A2", 2, 4), ("A4", 4, 2), ("B", 8, 1), ] CONTEXT_LENGTHS = [8 * 1024, 16 * 1024, 32 * 1024, 64 * 1024] T_Q_PREFILL = 8 CSV_OUT = (Path(__file__).resolve().parents[2] / "docs" / "sweeps" / "short_context_prefill_sweep.csv") def _compute_metrics(r, *, mode, kv_per_cube, C, S_kv): op_log = r.engine.op_log wall_ns = max(rec.t_end for rec in op_log) - min(rec.t_start for rec in op_log) # Per-PE detection (cube.pe) active_pes = set() for rec in op_log: cid = rec.component_id or "" if cid and ".cube" in cid and ".pe" in cid: parts = cid.split(".") active_pes.add(".".join(parts[:3])) n_pe = len(active_pes) # Stage time / count / bytes gemm_time_ns = 0.0 math_count = 0 dma_read_bytes = 0 dma_write_bytes = 0 ipcq_bytes = 0 for rec in op_log: name = rec.op_name nbytes = rec.params.get("nbytes", 0) or 0 if name == "gemm_f16": gemm_time_ns += rec.t_end - rec.t_start elif name in MATH_OPS: math_count += 1 elif name == "dma_read": dma_read_bytes += nbytes elif name == "dma_write": dma_write_bytes += nbytes elif name == "ipcq_copy": ipcq_bytes += nbytes gemm_util = gemm_time_ns / (wall_ns * n_pe) if n_pe else 0.0 hbm_bw_util = ( (dma_read_bytes + dma_write_bytes) / (wall_ns * n_pe * PEAK_PE_HBM_BPS) ) if n_pe else 0.0 # KV cache space — per-cube bytes (K + V, f16) # Per cube owns kv_per_cube heads × S_kv tokens × d_head × 2 (K+V) × 2 (f16) kv_cache_per_cube_bytes = kv_per_cube * S_kv * D_HEAD * 2 * 2 return { "mode": mode, "kv_per_cube": kv_per_cube, "C": C, "S_kv": S_kv, "wall_us": wall_ns / 1000, "n_pe": n_pe, "gemm_util": gemm_util, "math_count": math_count, "hbm_bw_util": hbm_bw_util, "hbm_read_mb": dma_read_bytes / (1024 * 1024), "hbm_write_kb": dma_write_bytes / 1024, "ipcq_kb": ipcq_bytes / 1024, "kv_cache_per_cube_mb": kv_cache_per_cube_bytes / (1024 * 1024), } @pytest.mark.slow def test_prefill_mode_context_sweep(): """Run 4 modes × 4 context lengths, dump 5 metrics to CSV. Smoke-asserts each run completes; the measurement output is the CSV. """ rows = [] for mode, kv_per_cube, C in MODES: for S_kv in CONTEXT_LENGTHS: print(f">>> PREFILL {mode} S_kv={S_kv//1024}K " f"(kv_per_cube={kv_per_cube}, C={C})", flush=True) r = _run_prefill(kv_per_cube=kv_per_cube, C=C, T_q=T_Q_PREFILL, S_kv=S_kv, h_q=64) assert r.completion.ok, ( f"PREFILL {mode} S_kv={S_kv}: {r.completion}" ) m = _compute_metrics(r, mode=mode, kv_per_cube=kv_per_cube, C=C, S_kv=S_kv) rows.append(m) print(f" wall={m['wall_us']:.1f}μs " f"gemm_util={m['gemm_util']:.3f} " f"hbm_bw_util={m['hbm_bw_util']:.3f} " f"ipcq={m['ipcq_kb']:.1f}KB " f"kv_cache={m['kv_cache_per_cube_mb']:.1f}MB/cube", flush=True) CSV_OUT.parent.mkdir(parents=True, exist_ok=True) with CSV_OUT.open("w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader() for row in rows: w.writerow(row) print(f"\nWrote {len(rows)} rows to {CSV_OUT}")