533 lines
18 KiB
Python
533 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Build Frontier-vs-vLLM alignment tables and plots for the current H20 runs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import json
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import matplotlib
|
|
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUT_DIR = ROOT / "docs" / "assets" / "frontier_vllm_alignment"
|
|
DASH1_VLLM_ROOT = Path("/home/admin/cpfs/wjh/replayserve/runs/vllm_gpu_smoke_20260625_dash1")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RunSpec:
|
|
run_id: str
|
|
label: str
|
|
tp: int
|
|
request_count: int
|
|
scale_label: str
|
|
scale_value: float
|
|
fixture: str
|
|
frontier_summary: str
|
|
vllm_summary: str
|
|
vllm_preemptions: int
|
|
kv_blocks: int
|
|
notes: str = ""
|
|
vllm_remote: bool = False
|
|
|
|
|
|
RUNS: list[RunSpec] = [
|
|
RunSpec(
|
|
run_id="tp1_n100_scale1",
|
|
label="TP1 N100 raw",
|
|
tp=1,
|
|
request_count=100,
|
|
scale_label="raw",
|
|
scale_value=1.0,
|
|
fixture="coder_100",
|
|
frontier_summary=(
|
|
"runs/rs6_frontier_h20_tp1_profile_full32k_20260624/"
|
|
"frontier_h20_tp1_profile_full32k/coder_100/"
|
|
"vllm_kv_15281_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary="runs/vllm_gpu_smoke_20260624/tp1_coder100_uncapped/summary.json",
|
|
vllm_preemptions=8,
|
|
kv_blocks=15281,
|
|
notes="Frontier incomplete before lifecycle fix; included as TP1 100-request baseline.",
|
|
),
|
|
RunSpec(
|
|
run_id="tp1_n500_scale1",
|
|
label="TP1 N500 raw",
|
|
tp=1,
|
|
request_count=500,
|
|
scale_label="raw",
|
|
scale_value=1.0,
|
|
fixture="coder_500",
|
|
frontier_summary=(
|
|
"runs/rs8_frontier_h20_tp1_profile_full32k_coder500_20260625/"
|
|
"frontier_h20_tp1_profile_full32k/coder_500/"
|
|
"vllm_kv_15281_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary="runs/vllm_gpu_smoke_20260625_dash1/tp1_coder500_uncapped/summary.json",
|
|
vllm_preemptions=63,
|
|
kv_blocks=15281,
|
|
notes="Frontier incomplete; useful as high-pressure stress signal.",
|
|
),
|
|
RunSpec(
|
|
run_id="tp1_n200_scale0667",
|
|
label="TP1 N200 scale 0.667",
|
|
tp=1,
|
|
request_count=200,
|
|
scale_label="0.667",
|
|
scale_value=2 / 3,
|
|
fixture="coder_200_ts0667",
|
|
frontier_summary=(
|
|
"runs/rs9_frontier_h20_tp1_profile_full32k_coder200_ts0667/"
|
|
"frontier_h20_tp1_profile_full32k/coder_200_ts0667/"
|
|
"vllm_kv_15281_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary="runs/vllm_gpu_smoke_20260625_dash1/tp1_coder200_ts0667_uncapped/summary.json",
|
|
vllm_preemptions=26,
|
|
kv_blocks=15281,
|
|
notes="Dense-arrival run; Frontier incomplete before lifecycle fix.",
|
|
),
|
|
RunSpec(
|
|
run_id="tp1_n200_scale2",
|
|
label="TP1 N200 scale 2",
|
|
tp=1,
|
|
request_count=200,
|
|
scale_label="2",
|
|
scale_value=2.0,
|
|
fixture="coder_200_ts2",
|
|
frontier_summary=(
|
|
"runs/rs10_preemption_replay_fix_ts2/frontier_h20_tp1_profile_full32k/"
|
|
"coder_200_ts2/vllm_kv_15281_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary="runs/vllm_gpu_smoke_20260625_dash1/tp1_coder_200_ts2_uncapped/summary.json",
|
|
vllm_preemptions=43,
|
|
kv_blocks=15281,
|
|
notes="After Frontier decode-preemption lifecycle fix.",
|
|
),
|
|
RunSpec(
|
|
run_id="tp1_n200_scale3",
|
|
label="TP1 N200 scale 3",
|
|
tp=1,
|
|
request_count=200,
|
|
scale_label="3",
|
|
scale_value=3.0,
|
|
fixture="coder_200_ts3",
|
|
frontier_summary=(
|
|
"runs/rs10_preemption_replay_fix_ts3/frontier_h20_tp1_profile_full32k/"
|
|
"coder_200_ts3/vllm_kv_15281_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary="runs/vllm_gpu_smoke_20260625_dash1/tp1_coder_200_ts3_uncapped/summary.json",
|
|
vllm_preemptions=16,
|
|
kv_blocks=15281,
|
|
notes="After Frontier decode-preemption lifecycle fix.",
|
|
),
|
|
RunSpec(
|
|
run_id="tp2_n200_scale2",
|
|
label="TP2 N200 scale 2",
|
|
tp=2,
|
|
request_count=200,
|
|
scale_label="2",
|
|
scale_value=2.0,
|
|
fixture="coder_200_ts2",
|
|
frontier_summary=(
|
|
"runs/rs12_frontier_h20_tp2_tp4_profile_full32k_coder200_ts2_ts3/"
|
|
"frontier_h20_tp2_tp4_profile_full32k/coder_200_ts2/"
|
|
"tp2_vllm_kv_69055_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary=str(DASH1_VLLM_ROOT / "tp2_coder_200_ts2_uncapped" / "summary.json"),
|
|
vllm_preemptions=0,
|
|
kv_blocks=69055,
|
|
notes="Uses true-mixed TP2/TP4 attention profile.",
|
|
vllm_remote=True,
|
|
),
|
|
RunSpec(
|
|
run_id="tp2_n200_scale3",
|
|
label="TP2 N200 scale 3",
|
|
tp=2,
|
|
request_count=200,
|
|
scale_label="3",
|
|
scale_value=3.0,
|
|
fixture="coder_200_ts3",
|
|
frontier_summary=(
|
|
"runs/rs12_frontier_h20_tp2_tp4_profile_full32k_coder200_ts2_ts3/"
|
|
"frontier_h20_tp2_tp4_profile_full32k/coder_200_ts3/"
|
|
"tp2_vllm_kv_69055_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary=str(DASH1_VLLM_ROOT / "tp2_coder_200_ts3_uncapped" / "summary.json"),
|
|
vllm_preemptions=0,
|
|
kv_blocks=69055,
|
|
notes="Uses true-mixed TP2/TP4 attention profile.",
|
|
vllm_remote=True,
|
|
),
|
|
RunSpec(
|
|
run_id="tp4_n200_scale2",
|
|
label="TP4 N200 scale 2",
|
|
tp=4,
|
|
request_count=200,
|
|
scale_label="2",
|
|
scale_value=2.0,
|
|
fixture="coder_200_ts2",
|
|
frontier_summary=(
|
|
"runs/rs12_frontier_h20_tp2_tp4_profile_full32k_coder200_ts2_ts3/"
|
|
"frontier_h20_tp2_tp4_profile_full32k/coder_200_ts2/"
|
|
"tp4_vllm_kv_177077_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary=str(DASH1_VLLM_ROOT / "tp4_coder_200_ts2_uncapped" / "summary.json"),
|
|
vllm_preemptions=0,
|
|
kv_blocks=177077,
|
|
notes="Uses true-mixed TP2/TP4 attention profile.",
|
|
vllm_remote=True,
|
|
),
|
|
RunSpec(
|
|
run_id="tp4_n200_scale3",
|
|
label="TP4 N200 scale 3",
|
|
tp=4,
|
|
request_count=200,
|
|
scale_label="3",
|
|
scale_value=3.0,
|
|
fixture="coder_200_ts3",
|
|
frontier_summary=(
|
|
"runs/rs12_frontier_h20_tp2_tp4_profile_full32k_coder200_ts2_ts3/"
|
|
"frontier_h20_tp2_tp4_profile_full32k/coder_200_ts3/"
|
|
"tp4_vllm_kv_177077_profile_full32k/postprocess_summary.json"
|
|
),
|
|
vllm_summary=str(DASH1_VLLM_ROOT / "tp4_coder_200_ts3_uncapped" / "summary.json"),
|
|
vllm_preemptions=0,
|
|
kv_blocks=177077,
|
|
notes="Uses true-mixed TP2/TP4 attention profile.",
|
|
vllm_remote=True,
|
|
),
|
|
]
|
|
|
|
|
|
FIELDNAMES = [
|
|
"run_id",
|
|
"label",
|
|
"tp",
|
|
"request_count",
|
|
"scale_label",
|
|
"scale_value",
|
|
"fixture",
|
|
"kv_blocks",
|
|
"frontier_completed",
|
|
"frontier_total",
|
|
"frontier_complete",
|
|
"vllm_completed",
|
|
"vllm_total",
|
|
"frontier_preemptions",
|
|
"vllm_preemptions",
|
|
"frontier_prefix_hit",
|
|
"vllm_prefix_hit",
|
|
"prefix_hit_delta",
|
|
"frontier_rps",
|
|
"vllm_rps",
|
|
"rps_ratio",
|
|
"frontier_total_tps",
|
|
"vllm_total_tps",
|
|
"total_tps_ratio",
|
|
"frontier_decode_tps",
|
|
"vllm_decode_tps",
|
|
"decode_tps_ratio",
|
|
"frontier_ttft_p50_s",
|
|
"vllm_ttft_p50_s",
|
|
"ttft_p50_ratio",
|
|
"frontier_ttft_p95_s",
|
|
"vllm_ttft_p95_s",
|
|
"ttft_p95_ratio",
|
|
"frontier_tpot_p50_s",
|
|
"vllm_tpot_p50_s",
|
|
"tpot_p50_ratio",
|
|
"frontier_tpot_p95_s",
|
|
"vllm_tpot_p95_s",
|
|
"tpot_p95_ratio",
|
|
"frontier_e2e_p50_s",
|
|
"vllm_e2e_p50_s",
|
|
"e2e_p50_ratio",
|
|
"frontier_e2e_p95_s",
|
|
"vllm_e2e_p95_s",
|
|
"e2e_p95_ratio",
|
|
"notes",
|
|
]
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
data = json.load(handle)
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path}: expected JSON object")
|
|
return data
|
|
|
|
|
|
def load_vllm_summary(spec: RunSpec) -> dict[str, Any]:
|
|
path = Path(spec.vllm_summary)
|
|
if not spec.vllm_remote:
|
|
return load_json(ROOT / path)
|
|
local_candidate = ROOT / "runs" / "vllm_gpu_smoke_20260625_dash1" / path.parent.name / path.name
|
|
if local_candidate.exists():
|
|
return load_json(local_candidate)
|
|
raw = subprocess.check_output(["ssh", "dash1", f"cat {spec.vllm_summary}"], text=True)
|
|
data = json.loads(raw)
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{spec.vllm_summary}: expected JSON object")
|
|
return data
|
|
|
|
|
|
def load_frontier_summary(spec: RunSpec) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
post = load_json(ROOT / spec.frontier_summary)
|
|
system_path = Path(post["system_metrics"])
|
|
if not system_path.is_absolute():
|
|
system_path = ROOT / system_path
|
|
return post, load_json(system_path)
|
|
|
|
|
|
def ratio(numerator: float | int | None, denominator: float | int | None) -> float | None:
|
|
if numerator is None or denominator in (None, 0):
|
|
return None
|
|
return float(numerator) / float(denominator)
|
|
|
|
|
|
def nested(data: dict[str, Any], *keys: str) -> Any:
|
|
value: Any = data
|
|
for key in keys:
|
|
if not isinstance(value, dict):
|
|
return None
|
|
value = value.get(key)
|
|
return value
|
|
|
|
|
|
def summarize(spec: RunSpec) -> dict[str, Any]:
|
|
post, system = load_frontier_summary(spec)
|
|
vllm = load_vllm_summary(spec)
|
|
|
|
completion = post.get("completion", {})
|
|
preemption = post.get("preemption_statistics", {})
|
|
prefix = post.get("prefix_cache_postprocess", {})
|
|
token_weighted = prefix.get("replayserve_token_weighted", {})
|
|
throughput = system.get("throughput_metrics", {})
|
|
|
|
frontier_total_tps = throughput.get("tokens_per_second")
|
|
vllm_total_tps = vllm["prompt_tokens_per_second"] + vllm["generated_tokens_per_second"]
|
|
frontier_prefix_hit = token_weighted.get("hit_ratio")
|
|
vllm_prefix_hit = nested(vllm, "estimated_prefix_reuse", "token_hit_ratio")
|
|
|
|
row: dict[str, Any] = {
|
|
"run_id": spec.run_id,
|
|
"label": spec.label,
|
|
"tp": spec.tp,
|
|
"request_count": spec.request_count,
|
|
"scale_label": spec.scale_label,
|
|
"scale_value": spec.scale_value,
|
|
"fixture": spec.fixture,
|
|
"kv_blocks": spec.kv_blocks,
|
|
"frontier_completed": completion.get("completed_requests"),
|
|
"frontier_total": completion.get("total_requests"),
|
|
"frontier_complete": completion.get("is_complete"),
|
|
"vllm_completed": vllm.get("rows"),
|
|
"vllm_total": vllm.get("rows"),
|
|
"frontier_preemptions": preemption.get("total_preemption_events"),
|
|
"vllm_preemptions": spec.vllm_preemptions,
|
|
"frontier_prefix_hit": frontier_prefix_hit,
|
|
"vllm_prefix_hit": vllm_prefix_hit,
|
|
"prefix_hit_delta": (
|
|
float(frontier_prefix_hit) - float(vllm_prefix_hit)
|
|
if frontier_prefix_hit is not None and vllm_prefix_hit is not None
|
|
else None
|
|
),
|
|
"frontier_rps": throughput.get("requests_per_second"),
|
|
"vllm_rps": vllm.get("requests_per_second"),
|
|
"frontier_total_tps": frontier_total_tps,
|
|
"vllm_total_tps": vllm_total_tps,
|
|
"frontier_decode_tps": throughput.get("decode_tokens_per_second"),
|
|
"vllm_decode_tps": vllm.get("generated_tokens_per_second"),
|
|
"frontier_ttft_p50_s": nested(system, "ttft_statistics", "p50") / 1000,
|
|
"vllm_ttft_p50_s": nested(vllm, "ttft_s", "p50"),
|
|
"frontier_ttft_p95_s": nested(system, "ttft_statistics", "p95") / 1000,
|
|
"vllm_ttft_p95_s": nested(vllm, "ttft_s", "p95"),
|
|
"frontier_tpot_p50_s": nested(system, "tpot_statistics", "p50") / 1000,
|
|
"vllm_tpot_p50_s": nested(vllm, "tpot_s", "p50"),
|
|
"frontier_tpot_p95_s": nested(system, "tpot_statistics", "p95") / 1000,
|
|
"vllm_tpot_p95_s": nested(vllm, "tpot_s", "p95"),
|
|
"frontier_e2e_p50_s": nested(system, "request_e2e_time_statistics", "p50") / 1000,
|
|
"vllm_e2e_p50_s": nested(vllm, "e2e_s", "p50"),
|
|
"frontier_e2e_p95_s": nested(system, "request_e2e_time_statistics", "p95") / 1000,
|
|
"vllm_e2e_p95_s": nested(vllm, "e2e_s", "p95"),
|
|
"notes": spec.notes,
|
|
}
|
|
|
|
for name in [
|
|
"rps",
|
|
"total_tps",
|
|
"decode_tps",
|
|
"ttft_p50_s",
|
|
"ttft_p95_s",
|
|
"tpot_p50_s",
|
|
"tpot_p95_s",
|
|
"e2e_p50_s",
|
|
"e2e_p95_s",
|
|
]:
|
|
row[f"{name.removesuffix('_s')}_ratio"] = ratio(
|
|
row.get(f"frontier_{name}"), row.get(f"vllm_{name}")
|
|
)
|
|
|
|
return row
|
|
|
|
|
|
def fmt(value: Any) -> str:
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, bool):
|
|
return "true" if value else "false"
|
|
if isinstance(value, float):
|
|
return f"{value:.10g}"
|
|
return str(value)
|
|
|
|
|
|
def write_csv(rows: list[dict[str, Any]]) -> None:
|
|
path = OUT_DIR / "frontier_vllm_alignment.csv"
|
|
with path.open("w", encoding="utf-8", newline="") as handle:
|
|
writer = csv.DictWriter(handle, fieldnames=FIELDNAMES)
|
|
writer.writeheader()
|
|
for row in rows:
|
|
writer.writerow({key: fmt(row.get(key)) for key in FIELDNAMES})
|
|
|
|
|
|
def write_json(rows: list[dict[str, Any]]) -> None:
|
|
path = OUT_DIR / "frontier_vllm_alignment.json"
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(rows, handle, indent=2, sort_keys=True)
|
|
handle.write("\n")
|
|
|
|
|
|
def setup_axis(ax: plt.Axes, title: str, ylabel: str) -> None:
|
|
ax.set_title(title, fontsize=12, pad=10)
|
|
ax.set_ylabel(ylabel)
|
|
ax.grid(axis="y", alpha=0.25)
|
|
ax.spines["top"].set_visible(False)
|
|
ax.spines["right"].set_visible(False)
|
|
|
|
|
|
def annotate_bars(ax: plt.Axes, bars: Any, fmt_text: str = "{:.2f}") -> None:
|
|
for bar in bars:
|
|
height = bar.get_height()
|
|
if height != height:
|
|
continue
|
|
ax.annotate(
|
|
fmt_text.format(height),
|
|
xy=(bar.get_x() + bar.get_width() / 2, height),
|
|
xytext=(0, 3),
|
|
textcoords="offset points",
|
|
ha="center",
|
|
va="bottom",
|
|
fontsize=7,
|
|
rotation=90 if height > 2.5 else 0,
|
|
)
|
|
|
|
|
|
def savefig(name: str) -> None:
|
|
plt.tight_layout()
|
|
plt.savefig(OUT_DIR / name, dpi=180)
|
|
plt.close()
|
|
|
|
|
|
def plot_throughput_ratio(rows: list[dict[str, Any]]) -> None:
|
|
labels = [row["label"] for row in rows]
|
|
x = range(len(rows))
|
|
colors = {1: "#4C78A8", 2: "#F58518", 4: "#54A24B"}
|
|
fig, ax = plt.subplots(figsize=(12, 4.8))
|
|
bars = ax.bar(
|
|
x,
|
|
[row["total_tps_ratio"] for row in rows],
|
|
color=[colors[row["tp"]] for row in rows],
|
|
alpha=0.9,
|
|
)
|
|
for bar, row in zip(bars, rows, strict=True):
|
|
if not row["frontier_complete"]:
|
|
bar.set_hatch("//")
|
|
bar.set_alpha(0.65)
|
|
ax.axhline(1.0, color="#222222", linewidth=1, linestyle="--")
|
|
ax.set_xticks(list(x))
|
|
ax.set_xticklabels(labels, rotation=35, ha="right")
|
|
setup_axis(ax, "Frontier Throughput Relative to vLLM", "Frontier / vLLM total tok/s")
|
|
annotate_bars(ax, bars)
|
|
savefig("throughput_ratio.png")
|
|
|
|
|
|
def plot_latency_ratios(rows: list[dict[str, Any]]) -> None:
|
|
labels = [row["label"] for row in rows]
|
|
x = list(range(len(rows)))
|
|
width = 0.26
|
|
fig, ax = plt.subplots(figsize=(13, 5.2))
|
|
b1 = ax.bar([i - width for i in x], [row["ttft_p95_ratio"] for row in rows], width, label="TTFT p95")
|
|
b2 = ax.bar(x, [row["tpot_p50_ratio"] for row in rows], width, label="TPOT p50")
|
|
b3 = ax.bar([i + width for i in x], [row["e2e_p95_ratio"] for row in rows], width, label="E2E p95")
|
|
ax.axhline(1.0, color="#222222", linewidth=1, linestyle="--")
|
|
ax.set_xticks(x)
|
|
ax.set_xticklabels(labels, rotation=35, ha="right")
|
|
ax.legend(frameon=False, ncols=3, loc="upper left")
|
|
setup_axis(ax, "Latency Ratios", "Frontier / vLLM")
|
|
annotate_bars(ax, b1)
|
|
annotate_bars(ax, b2)
|
|
annotate_bars(ax, b3)
|
|
savefig("latency_ratios.png")
|
|
|
|
|
|
def plot_tp_scaling(rows: list[dict[str, Any]]) -> None:
|
|
selected = [row for row in rows if row["request_count"] == 200 and row["scale_label"] in {"2", "3"}]
|
|
groups = {}
|
|
for row in selected:
|
|
groups.setdefault(row["scale_label"], {})[row["tp"]] = row
|
|
|
|
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2), sharey=False)
|
|
for ax, scale in zip(axes, ["2", "3"], strict=True):
|
|
group = groups[scale]
|
|
tps = sorted(group)
|
|
ax.plot(tps, [group[tp]["frontier_total_tps"] for tp in tps], marker="o", label="Frontier")
|
|
ax.plot(tps, [group[tp]["vllm_total_tps"] for tp in tps], marker="o", label="vLLM")
|
|
ax.set_xticks(tps)
|
|
ax.set_xlabel("Tensor parallel size")
|
|
setup_axis(ax, f"N=200, timestamp scale {scale}", "total tok/s")
|
|
ax.legend(frameon=False)
|
|
savefig("tp_scaling_total_tps.png")
|
|
|
|
|
|
def plot_completion_prefix(rows: list[dict[str, Any]]) -> None:
|
|
labels = [row["label"] for row in rows]
|
|
x = list(range(len(rows)))
|
|
fig, ax1 = plt.subplots(figsize=(12, 4.8))
|
|
completion = [row["frontier_completed"] / row["frontier_total"] for row in rows]
|
|
bars = ax1.bar(x, completion, color="#72B7B2", alpha=0.8, label="Frontier completion")
|
|
ax1.set_ylim(0, 1.08)
|
|
ax1.set_xticks(x)
|
|
ax1.set_xticklabels(labels, rotation=35, ha="right")
|
|
setup_axis(ax1, "Completion and Prefix Reuse", "Frontier completed / total")
|
|
ax2 = ax1.twinx()
|
|
ax2.plot(x, [row["frontier_prefix_hit"] for row in rows], color="#E45756", marker="o", label="Frontier prefix hit")
|
|
ax2.plot(x, [row["vllm_prefix_hit"] for row in rows], color="#4C78A8", marker="x", linestyle="--", label="vLLM trace-side prefix hit")
|
|
ax2.set_ylabel("prefix token hit ratio")
|
|
ax2.set_ylim(0, 0.45)
|
|
lines, labels2 = ax2.get_legend_handles_labels()
|
|
ax1.legend([bars, *lines], ["Frontier completion", *labels2], frameon=False, loc="upper left", ncols=2)
|
|
savefig("completion_prefix.png")
|
|
|
|
|
|
def main() -> None:
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
rows = [summarize(spec) for spec in RUNS]
|
|
write_csv(rows)
|
|
write_json(rows)
|
|
plot_throughput_ratio(rows)
|
|
plot_latency_ratios(rows)
|
|
plot_tp_scaling(rows)
|
|
plot_completion_prefix(rows)
|
|
print(f"Wrote {len(rows)} rows to {OUT_DIR}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|