#!/usr/bin/env python3 """Plot Figures 1-5 from connector_tax aggregate. Requires aggregate.json + aggregate.csv from analyze.py. Figure 1: TTFT p90 vs send rate, line per config (Phase A) Figure 2: TPOT p90 vs send rate Figure 3: Achieved throughput vs requested Figure 4: Substrate tax bar at ref_safe and ref_load Figure 5: Shape-dependent tax heatmap (Phase B) """ import argparse import json from collections import defaultdict from pathlib import Path import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt CONFIG_COLORS = { "plain": "#000000", "noop_connector": "#7f7f7f", "mooncake_producer": "#1f77b4", "mooncake_consumer": "#17becf", "mooncake_both": "#d62728", "nixl_both": "#ff7f0e", "lmcache_only": "#2ca02c", "multi_mooncake_lmcache": "#9467bd", } def load(root: Path): agg = json.loads((root / "aggregate.json").read_text()) return agg def fig1_2_3(agg, out_dir): configs = agg["configs"] # Rates rates = agg["rates_swept"] # ----- fig 1: TTFT p90 ----- fig, ax = plt.subplots(figsize=(10, 5.5)) for cfg, d in configs.items(): cells = sorted(d["phase_a"], key=lambda c: c["rate_target"]) xs = [c["rate_target"] for c in cells] ys = [c.get("ttft_ms_p90") for c in cells] ax.plot(xs, ys, "o-", label=cfg, color=CONFIG_COLORS.get(cfg, None), linewidth=2) # mark saturation for c in cells: if c.get("saturated"): ax.plot([c["rate_target"]], [c["ttft_ms_p90"]], "x", markersize=12, mew=2, color=CONFIG_COLORS.get(cfg, "red")) ax.set_xscale("log", base=2) ax.set_yscale("log") ax.set_xticks(rates) ax.set_xticklabels([str(r) for r in rates]) ax.set_xlabel("Send rate (req/s)") ax.set_ylabel("TTFT p90 (ms, log)") ax.set_title("Figure 1 — TTFT p90 vs send rate (Phase A)\n" "× = saturation criterion fired") ax.grid(True, which="both", linestyle="--", alpha=0.4) ax.legend(fontsize=8, loc="upper left") fig.tight_layout() fig.savefig(out_dir / "fig1_ttft_vs_rate.png", dpi=160) plt.close(fig) # ----- fig 2: TPOT p90 ----- fig, ax = plt.subplots(figsize=(10, 5.5)) for cfg, d in configs.items(): cells = sorted(d["phase_a"], key=lambda c: c["rate_target"]) xs = [c["rate_target"] for c in cells] ys = [c.get("tpot_ms_p90") for c in cells] ax.plot(xs, ys, "o-", label=cfg, color=CONFIG_COLORS.get(cfg, None), linewidth=2) ax.set_xscale("log", base=2) ax.set_xticks(rates) ax.set_xticklabels([str(r) for r in rates]) ax.set_xlabel("Send rate (req/s)") ax.set_ylabel("TPOT p90 (ms)") ax.set_title("Figure 2 — TPOT p90 vs send rate (Phase A)") ax.grid(True, linestyle="--", alpha=0.4) ax.legend(fontsize=8, loc="upper left") fig.tight_layout() fig.savefig(out_dir / "fig2_tpot_vs_rate.png", dpi=160) plt.close(fig) # ----- fig 3: throughput ----- fig, ax = plt.subplots(figsize=(10, 5.5)) max_x = max(rates) if rates else 1 ax.plot([0, max_x], [0, max_x], "k--", alpha=0.4, label="ideal y=x") for cfg, d in configs.items(): cells = sorted(d["phase_a"], key=lambda c: c["rate_target"]) xs = [c["rate_target"] for c in cells] ys = [c.get("throughput_effective_rps") for c in cells] ax.plot(xs, ys, "o-", label=cfg, color=CONFIG_COLORS.get(cfg, None), linewidth=2) ax.set_xlabel("Send rate (req/s)") ax.set_ylabel("Effective throughput (req/s)") ax.set_title("Figure 3 — Throughput tracking (Phase A)") ax.grid(True, linestyle="--", alpha=0.4) ax.legend(fontsize=8, loc="upper left") fig.tight_layout() fig.savefig(out_dir / "fig3_throughput_vs_rate.png", dpi=160) plt.close(fig) def fig4(agg, out_dir): configs = agg["configs"] if "plain" not in configs: return plain = configs["plain"] def cell_at(d, r): for c in d["phase_a"]: if abs(c["rate_target"] - r) < 1e-6: return c return None def tax(c_cfg, c_plain, key): if c_cfg is None or c_plain is None: return None a, b = c_cfg.get(key), c_plain.get(key) if not a or not b: return None return a / b - 1 rates_used = [] if agg.get("ref_safe") is not None: rates_used.append(("ref_safe", agg["ref_safe"])) if agg.get("ref_load") is not None and agg["ref_load"] != agg.get("ref_safe"): rates_used.append(("ref_load", agg["ref_load"])) if not rates_used: return cfg_names = [c for c in configs if c != "plain"] fig, axes = plt.subplots(1, 2, figsize=(13, 5)) if len(rates_used) == 1: axes = [axes[0]] for ax, (label, r) in zip(axes, rates_used): plain_cell = cell_at(plain, r) ttft_taxes = [] tpot_taxes = [] for cfg in cfg_names: c = cell_at(configs[cfg], r) ttft_taxes.append(tax(c, plain_cell, "ttft_ms_p90") or 0) tpot_taxes.append(tax(c, plain_cell, "tpot_ms_p90") or 0) x = np.arange(len(cfg_names)) w = 0.4 ax.bar(x - w/2, [v * 100 for v in ttft_taxes], width=w, label="TTFT p90 tax %", color="#d62728", alpha=0.85) ax.bar(x + w/2, [v * 100 for v in tpot_taxes], width=w, label="TPOT p90 tax %", color="#1f77b4", alpha=0.85) ax.axhline(0, color="black", linewidth=0.5) ax.set_xticks(x) ax.set_xticklabels(cfg_names, rotation=30, ha="right", fontsize=8) ax.set_ylabel("Tax vs plain (%)") ax.set_title(f"{label} (rate={r} req/s)") ax.grid(True, axis="y", linestyle="--", alpha=0.4) ax.legend(fontsize=8) fig.suptitle("Figure 4 — Substrate tax (TTFT p90 + TPOT p90) " "at reference rates", fontweight="bold") fig.tight_layout() fig.savefig(out_dir / "fig4_substrate_tax.png", dpi=160) plt.close(fig) def fig5(agg, out_dir): configs = agg["configs"] if "plain" not in configs: return # Build (input, output) → ttft_p90 per config cfg_names = [c for c in configs if c != "plain"] def shape_map(d): m = {} for c in d.get("phase_b", []): key = (c["input_tokens"], c["output_tokens"]) m[key] = c.get("ttft_ms_p90") return m plain_map = shape_map(configs["plain"]) if not plain_map: return inputs = sorted({k[0] for k in plain_map}) outputs = sorted({k[1] for k in plain_map}) n = len(cfg_names) cols = min(3, n) rows = (n + cols - 1) // cols fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows)) if n == 1: axes = np.array([[axes]]) elif rows == 1: axes = axes.reshape(1, -1) for idx, cfg in enumerate(cfg_names): ax = axes[idx // cols][idx % cols] cmap = shape_map(configs[cfg]) mat = np.full((len(outputs), len(inputs)), np.nan) for i, ip in enumerate(inputs): for j, op in enumerate(outputs): a = cmap.get((ip, op)) b = plain_map.get((ip, op)) if a and b: mat[j, i] = a / b - 1 im = ax.imshow(mat * 100, cmap="YlOrRd", aspect="auto") ax.set_xticks(range(len(inputs))) ax.set_xticklabels([f"{x//1024}k" if x >= 1024 else str(x) for x in inputs]) ax.set_yticks(range(len(outputs))) ax.set_yticklabels([str(y) for y in outputs]) ax.set_xlabel("input") ax.set_ylabel("output") ax.set_title(cfg, fontsize=10) for i in range(len(inputs)): for j in range(len(outputs)): v = mat[j, i] if not np.isnan(v): txt = f"{v*100:.0f}%" ax.text(i, j, txt, ha="center", va="center", fontsize=9, color="white" if v * 100 > 30 else "black") plt.colorbar(im, ax=ax, fraction=0.04, pad=0.02) # Hide leftover axes for idx in range(n, rows * cols): axes[idx // cols][idx % cols].axis("off") fig.suptitle("Figure 5 — TTFT p90 substrate tax (%) by shape (Phase B)", fontweight="bold") fig.tight_layout() fig.savefig(out_dir / "fig5_shape_tax_heatmap.png", dpi=160) plt.close(fig) def main(): ap = argparse.ArgumentParser() ap.add_argument("--root", type=Path, required=True) args = ap.parse_args() agg = load(args.root) out = args.root out.mkdir(parents=True, exist_ok=True) fig1_2_3(agg, out) fig4(agg, out) fig5(agg, out) print(f"Saved figures into {out}") if __name__ == "__main__": main()