"""C6: roofline plot for Qwen3-Coder-30B-A3B on H20. Reproduces the analytical roofline used in scripts/compute_roofline.py and plots it as a single PDF: AI vs achievable throughput, with annotated operating points for prefill at reuse {0, 70, 90, 95}% and decode. The constants must stay in lockstep with compute_roofline.py. If you change one, change the other. """ import argparse from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np # ---- model constants (mirror scripts/compute_roofline.py) ---- L, D, H_KV, D_HEAD, D_FFN = 48, 2048, 4, 128, 6144 K_EXPERTS = 8 BYTES = 2 # bf16 # ---- H20 ---- PEAK_FLOPS = 148e12 HBM_BW = 4.0e12 RIDGE = PEAK_FLOPS / HBM_BW # ~37 def attn_prefill_flops(seq_len, new_tokens): d_kv = H_KV * D_HEAD qkv = new_tokens * (D * D * 2 + D * d_kv * 2 * 2) attn = new_tokens * seq_len * D * 2 * 2 out = new_tokens * D * D * 2 return (qkv + attn + out) * L def attn_prefill_bytes(seq_len, new_tokens, cached_tokens): d_kv = H_KV * D_HEAD weight = D * (D + 2 * d_kv + D) * BYTES * L cached_kv = cached_tokens * 2 * d_kv * BYTES * L act = new_tokens * D * BYTES * 2 * L new_kv = new_tokens * 2 * d_kv * BYTES * L return weight + cached_kv + act + new_kv def ffn_flops(n): return 3 * n * D * D_FFN * 2 * K_EXPERTS * L def ffn_bytes(n): weight = K_EXPERTS * 3 * D * D_FFN * BYTES * L act = n * D * BYTES * 2 * L return weight + act def point(seq_len, reuse): cached = int(seq_len * reuse) new = max(1, seq_len - cached) f = attn_prefill_flops(seq_len, new) + ffn_flops(new) b = attn_prefill_bytes(seq_len, new, cached) + ffn_bytes(new) return f, b, new def decode_point(seq_len): f = attn_prefill_flops(seq_len, 1) + ffn_flops(1) b = attn_prefill_bytes(seq_len, 1, seq_len) + ffn_bytes(1) return f, b def plot(out_path, seq_len=64000): fig, ax = plt.subplots(figsize=(6.5, 4.2)) ai_grid = np.logspace(-1, 5, 400) achievable = np.minimum(ai_grid * HBM_BW, PEAK_FLOPS) / 1e12 ax.plot(ai_grid, achievable, color="#222", lw=1.5, label="H20 roofline") ax.axvline(RIDGE, color="#888", ls=":", lw=1) ax.text(RIDGE, 420, f"ridge = {RIDGE:.0f}", color="#666", fontsize=8, ha="center", va="top", bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="none", alpha=0.85)) ax.axhline(PEAK_FLOPS / 1e12, color="#aaa", ls="--", lw=0.6) ax.text(2, PEAK_FLOPS / 1e12 * 1.08, "compute ceiling (148 TFLOPS bf16)", fontsize=8, color="#666", ha="left") # operating points: use a legend (not annotations with leader lines, since # all 4 prefill points sit on the compute ceiling and would overlap). reuses = [0.0, 0.7, 0.9, 0.95] colors = ["#d62728", "#ff7f0e", "#2ca02c", "#1f77b4"] for reuse, color in zip(reuses, colors): f, b, new = point(seq_len, reuse) ai = f / b thpt = min(ai * HBM_BW, PEAK_FLOPS) / 1e12 ax.scatter([ai], [thpt], color=color, s=80, zorder=5, edgecolor="white", linewidth=1.2, label=f"prefill reuse={int(reuse*100):>2}% " f"(new={new:>6,} tok, AI={ai:>6,.0f})") f, b = decode_point(seq_len) ai_dec = f / b thpt_dec = min(ai_dec * HBM_BW, PEAK_FLOPS) / 1e12 ax.scatter([ai_dec], [thpt_dec], color="#8c564b", s=80, marker="D", zorder=5, edgecolor="white", linewidth=1.2, label=f"decode (per-token, seqlen={seq_len:,}, AI={ai_dec:.1f})") ax.legend(loc="lower right", fontsize=8.5, framealpha=0.95, prop={"family": "monospace", "size": 8}) ax.set_xscale("log") ax.set_yscale("log") ax.set_xlim(0.5, 1e5) ax.set_ylim(0.5, 500) ax.set_xlabel("Arithmetic intensity (FLOP/byte)") ax.set_ylabel("Achievable throughput (TFLOPS)") ax.set_title( f"Prefill stays compute-bound even at 95% reuse " f"(Qwen3-Coder-30B-A3B, H20, seqlen={seq_len:,})", fontsize=10, ) ax.grid(True, which="both", alpha=0.25) fig.tight_layout() fig.savefig(out_path, bbox_inches="tight") plt.close(fig) print(f"[C6] wrote {out_path}") for reuse in reuses: f, b, new = point(seq_len, reuse) print(f" reuse={int(reuse*100):>3}% new={new:>6,} AI={f/b:>8.1f} " f"bound={'COMPUTE' if f/b > RIDGE else 'MEMORY'}") f, b = decode_point(seq_len) print(f" decode AI={f/b:>8.1f} bound={'COMPUTE' if f/b > RIDGE else 'MEMORY'}") def main(): ap = argparse.ArgumentParser() ap.add_argument("--seq-len", type=int, default=64000) ap.add_argument("--outdir", default="analysis/pd_sep_paper_section/figures") args = ap.parse_args() out = Path(args.outdir) out.mkdir(parents=True, exist_ok=True) plot(out / "fig_c6_roofline.pdf", seq_len=args.seq_len) if __name__ == "__main__": main()