Adds analysis/pd_sep_paper_section/ as the home for the "PD separation is net negative under agentic workloads" paper section: plot scripts for C1 (workload chars), C6 (roofline), C7 (routing-vs-PD-sep lever), the C6/C7 PDFs already rendered, and a README mapping candidate claims to required figures plus open re-run items. Removes --enforce-eager from bench.sh and all active launch scripts so cuda graphs are captured -- the prior methodology suppressed one of PD-sep's structural advantages (D-node fixed-shape decode). Legacy scripts under scripts/legacy/ are intentionally untouched as historical records. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
145 lines
4.8 KiB
Python
145 lines
4.8 KiB
Python
"""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()
|