Files
Gahow Wang d71a111099 Paper section: PD-sep scaffold + drop --enforce-eager from launch scripts
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>
2026-05-25 11:24:16 +08:00

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