#!/usr/bin/env python3 """Generate matplotlib figures for the current characterization package.""" from __future__ import annotations import json from pathlib import Path from typing import Any import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt ROOT = Path("analysis/characterization/current_results") FIG_DIR = ROOT / "figures" def main() -> None: FIG_DIR.mkdir(parents=True, exist_ok=True) full_trace = load_json(ROOT / "full_trace_summary.json") runs = load_json(ROOT / "run_summaries.json") claims = load_json(ROOT / "claim_matrix.json") paths = [ plot_full_trace_workload(full_trace), plot_session_skew(full_trace), plot_pdsep_vs_combined(runs), plot_elastic_vs_baseline(runs), plot_gpu_balance(runs), plot_claim_status(claims), ] write_figures_index(paths) for path in paths: print(path) def load_json(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def plot_full_trace_workload(summary: dict[str, Any]) -> str: labels = ["p50", "p90", "p99"] series = { "input tokens": [summary["input"][k] for k in labels], "output tokens": [summary["output"][k] for k in labels], "input/output": [summary["input_output_ratio"][k] for k in labels], } fig, ax = plt.subplots(figsize=(9, 5.5)) width = 0.24 x = range(len(labels)) colors = ["#2f6fab", "#dd8452", "#4c995c"] for idx, (name, values) in enumerate(series.items()): offset = (idx - 1) * width ax.bar([v + offset for v in x], values, width=width, label=name, color=colors[idx]) for xpos, value in zip([v + offset for v in x], values): ax.text(xpos, value * 1.08, short_num(value), ha="center", va="bottom", fontsize=9) ax.set_yscale("log") ax.set_xticks(list(x), labels) ax.set_ylabel("value, log scale") ax.set_title("Full Trace Workload Shape") ax.text( 0.01, -0.22, f"Requests={summary['records']:,}; sessions={summary['sessions']:,}; span={summary['trace_span_s']:.1f}s", transform=ax.transAxes, fontsize=10, color="#555", ) ax.grid(True, axis="y", alpha=0.25) ax.legend() return save(fig, "fig_full_trace_workload.png") def plot_session_skew(summary: dict[str, Any]) -> str: vals = summary["top_session_input_fraction"] labels = ["top 1%", "top 5%", "top 10%"] fractions = [vals["top1pct"] * 100, vals["top5pct"] * 100, vals["top10pct"] * 100] fig, ax = plt.subplots(figsize=(8, 5)) bars = ax.bar(labels, fractions, color=["#c44e52", "#dd8452", "#2f6fab"]) for bar, value in zip(bars, fractions): ax.text(bar.get_x() + bar.get_width() / 2, value + 1.5, f"{value:.1f}%", ha="center") ax.set_ylim(0, 100) ax.set_ylabel("% of input-token mass") ax.set_title("Session Token-Mass Skew") ax.text( 0.01, -0.20, "Session input-token p50/p90/p99/max = " f"{short_num(summary['session_input_tokens']['p50'])} / " f"{short_num(summary['session_input_tokens']['p90'])} / " f"{short_num(summary['session_input_tokens']['p99'])} / " f"{short_num(summary['session_input_tokens']['max'])}", transform=ax.transAxes, fontsize=10, color="#555", ) ax.grid(True, axis="y", alpha=0.25) return save(fig, "fig_session_skew.png") def plot_pdsep_vs_combined(runs: list[dict[str, Any]]) -> str: by_run = {run["run"]: run for run in runs} combined = by_run["outputs/gpu_ab_combined"] pdsep = by_run["outputs/gpu_ab_pdsep"] labels = ["TTFT p50", "TTFT p90", "E2E p50", "E2E p90"] combined_vals = [ stat(combined, "ttft_stats_s", "p50"), stat(combined, "ttft_stats_s", "p90"), stat(combined, "latency_stats_s", "p50"), stat(combined, "latency_stats_s", "p90"), ] pdsep_vals = [ stat(pdsep, "ttft_stats_s", "p50"), stat(pdsep, "ttft_stats_s", "p90"), stat(pdsep, "latency_stats_s", "p50"), stat(pdsep, "latency_stats_s", "p90"), ] fig, ax = plt.subplots(figsize=(9, 5)) grouped_bars(ax, labels, [("combined", combined_vals), ("PD-sep", pdsep_vals)], ["#2f6fab", "#c44e52"]) ax.set_ylabel("seconds") ax.set_title("Static PD-Sep vs Combined Baseline") ax.text( 0.01, -0.22, f"Errors: combined={combined['error_count']}, PD-sep={pdsep['error_count']}; " f"wall-clock delta={pct_delta(combined['wall_clock_s'], pdsep['wall_clock_s']):+.1f}%", transform=ax.transAxes, fontsize=10, color="#555", ) ax.grid(True, axis="y", alpha=0.25) ax.legend() return save(fig, "fig_pdsep_vs_combined.png") def plot_elastic_vs_baseline(runs: list[dict[str, Any]]) -> str: by_run = {run["run"]: run for run in runs} baseline = by_run["outputs/contention_16s_ts10"] elastic = by_run["outputs/contention_16s_elastic"] labels = ["TTFT p50", "TTFT p90", "E2E p50", "E2E p90", "TPOT p90"] baseline_vals = [ stat(baseline, "ttft_stats_s", "p50"), stat(baseline, "ttft_stats_s", "p90"), stat(baseline, "latency_stats_s", "p50"), stat(baseline, "latency_stats_s", "p90"), stat(baseline, "tpot_stats_s", "p90"), ] elastic_vals = [ stat(elastic, "ttft_stats_s", "p50"), stat(elastic, "ttft_stats_s", "p90"), stat(elastic, "latency_stats_s", "p50"), stat(elastic, "latency_stats_s", "p90"), stat(elastic, "tpot_stats_s", "p90"), ] fig, ax = plt.subplots(figsize=(10, 5)) grouped_bars(ax, labels, [("baseline", baseline_vals), ("elastic", elastic_vals)], ["#2f6fab", "#dd8452"]) ax.set_ylabel("seconds") ax.set_title("Elastic Transfer-Based Migration vs High-Contention Baseline") ax.text( 0.01, -0.22, f"GPU imbalance ratio: baseline={nested(baseline, ['gpu_summary', 'max_min_ratio']):.2f}x, " f"elastic={nested(elastic, ['gpu_summary', 'max_min_ratio']):.2f}x", transform=ax.transAxes, fontsize=10, color="#555", ) ax.grid(True, axis="y", alpha=0.25) ax.legend() return save(fig, "fig_elastic_vs_baseline.png") def plot_gpu_balance(runs: list[dict[str, Any]]) -> str: selected = [ ("combined", "outputs/gpu_ab_combined"), ("PD-sep", "outputs/gpu_ab_pdsep"), ("16s base", "outputs/contention_16s_ts10"), ("16s elastic", "outputs/contention_16s_elastic"), ] by_run = {run["run"]: run for run in runs} labels = [label for label, _ in selected] mean_util = [nested(by_run[path], ["gpu_summary", "mean_util_pct"]) for _, path in selected] imbalance = [nested(by_run[path], ["gpu_summary", "max_min_ratio"]) for _, path in selected] fig, axes = plt.subplots(1, 2, figsize=(11, 4.8)) axes[0].bar(labels, mean_util, color="#4c995c") axes[0].set_ylabel("mean GPU util (%)") axes[0].set_title("Mean Utilization") axes[0].tick_params(axis="x", rotation=20) axes[0].grid(True, axis="y", alpha=0.25) axes[1].bar(labels, imbalance, color="#76619c") axes[1].set_ylabel("max/min mean util") axes[1].set_title("Imbalance Ratio") axes[1].tick_params(axis="x", rotation=20) axes[1].grid(True, axis="y", alpha=0.25) fig.suptitle("GPU Utilization Balance in Existing Runs") fig.text( 0.02, 0.01, "GPU util imbalance is suggestive only; hot-spot causality still needs per-worker queue and session mapping.", fontsize=10, color="#555", ) return save(fig, "fig_gpu_balance.png") def plot_claim_status(claims: list[dict[str, Any]]) -> str: order = [ "supported_by_existing_artifact", "supported_for_trace_shape", "partially_supported", "not_yet_supported", ] counts = {status: 0 for status in order} for claim in claims: counts[claim["status"]] = counts.get(claim["status"], 0) + 1 labels = [status.replace("_", "\n") for status in order if counts.get(status)] values = [counts[status] for status in order if counts.get(status)] fig, ax = plt.subplots(figsize=(9, 5)) bars = ax.bar(labels, values, color=["#4c995c", "#2f6fab", "#dd8452", "#c44e52"][: len(values)]) for bar, value in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, value + 0.05, str(value), ha="center") ax.set_ylabel("claim count") ax.set_title("Current Claim Support Status") ax.grid(True, axis="y", alpha=0.25) return save(fig, "fig_claim_status.png") def grouped_bars(ax: Any, labels: list[str], series: list[tuple[str, list[float]]], colors: list[str]) -> None: x = list(range(len(labels))) width = 0.35 for idx, ((name, values), color) in enumerate(zip(series, colors)): offset = (idx - (len(series) - 1) / 2) * width bars = ax.bar([pos + offset for pos in x], values, width=width, label=name, color=color) for bar, value in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, value * 1.02, short_num(value), ha="center", va="bottom", fontsize=8) ax.set_xticks(x, labels) def stat(run: dict[str, Any], stat_name: str, key: str) -> float: return float(run[stat_name][key]) def nested(run: dict[str, Any], keys: list[str]) -> float: current: Any = run for key in keys: current = current[key] return float(current) def pct_delta(base: float, variant: float) -> float: return (variant - base) / base * 100.0 def short_num(value: float) -> str: if abs(value) >= 1_000_000: return f"{value / 1_000_000:.1f}M" if abs(value) >= 10_000: return f"{value / 1000:.1f}k" if abs(value) >= 1000: return f"{value / 1000:.2f}k" if abs(value) >= 100: return f"{value:.0f}" if abs(value) >= 10: return f"{value:.1f}" return f"{value:.2f}" def save(fig: Any, name: str) -> str: path = FIG_DIR / name fig.tight_layout(rect=(0, 0.04, 1, 0.95)) fig.savefig(path, dpi=180) plt.close(fig) return str(path) def write_figures_index(paths: list[str]) -> None: claims = { "fig_full_trace_workload.png": ( "Full Trace Workload", "Full GLM-5.1 trace is long-input, short-output, and high input/output ratio.", ), "fig_session_skew.png": ( "Session Skew", "Session input-token mass is highly skewed; top sessions dominate work.", ), "fig_pdsep_vs_combined.png": ( "PD-Sep vs Combined", "Existing static PD-sep A/B regresses TTFT/E2E vs combined.", ), "fig_elastic_vs_baseline.png": ( "Elastic vs Baseline", "Existing elastic transfer-based run does not improve TTFT/TPOT over high-contention baseline.", ), "fig_gpu_balance.png": ( "GPU Balance", "Existing runs show GPU-util imbalance, but more data is needed for hot-spot causality.", ), "fig_claim_status.png": ( "Claim Status", "Current audit separates supported, partial, and unsupported claims.", ), } lines = [ "# Figures Index", "", "Generated by:", "", "```bash", ".venv/bin/python analysis/characterization/plot_current_results.py", "```", "", "| Figure | Intended Claim |", "|---|---|", ] for path in paths: name = Path(path).name title, claim = claims[name] rel_path = f"figures/{name}" lines.append(f"| [{name}]({rel_path}) | {claim} |") lines.extend(["", "## Figure Previews", ""]) for path in paths: name = Path(path).name title, claim = claims[name] rel_path = f"figures/{name}" lines.extend([f"### {title}", "", claim, "", f"![{title}]({rel_path})", ""]) (ROOT / "all_figures_index.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") if __name__ == "__main__": main()