cache_aware_proxy: add lmetric_decode_weight (decode-load penalty in the LMetric fallback score) and a v3 anti-hotspot recent-migration penalty (effective_load = num_req + recent-migration count over a sliding window), preventing back-to-back migration clustering. UNIFIED_ABLATION.md documents the A (overload_factor=1.3) + B' (decode-weight, max(num_req,1)) + RaceFix sweep: A+B'+RaceFix reaches TTFT p90 7770ms, beating v3 PD-sep migration by ~20%. Runners/analyzer for the b3 trace replay included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
6.3 KiB
Python
Executable File
170 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""B3 5-policy re-test analyser.
|
|
|
|
Compute TTFT/TPOT/E2E mean/p50/p90/p99 for each policy from
|
|
metrics.jsonl, compare against the historical b3_policy_comparison.json
|
|
that drives fig_b3_latency_bars.png, and emit a side-by-side table
|
|
plus a new figure with the same layout as the original.
|
|
|
|
Usage:
|
|
python analyze_b3_replay.py --root <outroot> [--old-data <path>] [--figure <path>]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import statistics
|
|
from pathlib import Path
|
|
|
|
|
|
POLICIES = ["lmetric", "load_only", "sticky", "unified", "unified_v2"]
|
|
|
|
|
|
def pct(xs, p):
|
|
if not xs:
|
|
return None
|
|
xs = sorted(xs)
|
|
k = max(0, min(len(xs) - 1, int(p / 100.0 * (len(xs) - 1))))
|
|
return xs[k]
|
|
|
|
|
|
def summarise(path):
|
|
rows = [json.loads(l) for l in open(path) if l.strip()]
|
|
ok = [r for r in rows if not r.get("error")]
|
|
ttft = [r["ttft_s"] * 1000 for r in ok if r.get("ttft_s") is not None]
|
|
tpot = [r["tpot_s"] * 1000 for r in ok if r.get("tpot_s")]
|
|
e2e = [r["latency_s"] * 1000 for r in ok if r.get("latency_s") is not None]
|
|
return {
|
|
"n_total": len(rows),
|
|
"n_ok": len(ok),
|
|
"ttft_mean_ms": statistics.mean(ttft) if ttft else None,
|
|
"ttft_p50_ms": pct(ttft, 50),
|
|
"ttft_p90_ms": pct(ttft, 90),
|
|
"ttft_p99_ms": pct(ttft, 99),
|
|
"tpot_mean_ms": statistics.mean(tpot) if tpot else None,
|
|
"tpot_p50_ms": pct(tpot, 50),
|
|
"tpot_p90_ms": pct(tpot, 90),
|
|
"tpot_p99_ms": pct(tpot, 99),
|
|
"e2e_mean_ms": statistics.mean(e2e) if e2e else None,
|
|
"e2e_p50_ms": pct(e2e, 50),
|
|
"e2e_p90_ms": pct(e2e, 90),
|
|
"e2e_p99_ms": pct(e2e, 99),
|
|
}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--root", type=Path, required=True)
|
|
ap.add_argument("--old-data", type=Path,
|
|
default=Path("analysis/characterization/window_1_results/b3_policy_comparison.json"))
|
|
ap.add_argument("--figure", type=Path, default=None)
|
|
args = ap.parse_args()
|
|
|
|
new = {}
|
|
for p in POLICIES:
|
|
path = args.root / p / "metrics.jsonl"
|
|
if not path.exists():
|
|
print(f"MISSING: {path}")
|
|
continue
|
|
new[p] = summarise(path)
|
|
|
|
old = {}
|
|
if args.old_data.exists():
|
|
d = json.load(open(args.old_data))
|
|
for r in d.get("rows", []):
|
|
old[r["policy"]] = {
|
|
"ttft_p50_ms": r["ttft_p50_s"] * 1000,
|
|
"ttft_p90_ms": r["ttft_p90_s"] * 1000,
|
|
"ttft_p99_ms": r["ttft_p99_s"] * 1000,
|
|
"tpot_p90_ms": r["tpot_p90_s"] * 1000,
|
|
"e2e_p90_ms": r.get("e2e_p90_s", 0) * 1000,
|
|
}
|
|
|
|
def fmt(v): return f"{v:.0f}" if v is not None else "-"
|
|
def pctd(a, b):
|
|
if a is None or b is None or a == 0: return "-"
|
|
return f"{(b/a-1)*100:+.1f}%"
|
|
|
|
# Headline table
|
|
print(f"\n# NEW: today's re-test")
|
|
print(f"{'policy':<14}{'n_ok':>6}{'TTFTp50':>10}{'TTFTp90':>10}{'TTFTp99':>10}{'TPOTp90':>10}{'E2Ep90':>10}")
|
|
print("-" * 70)
|
|
for p in POLICIES:
|
|
if p not in new: continue
|
|
r = new[p]
|
|
print(f"{p:<14}{r['n_ok']:>6}{fmt(r['ttft_p50_ms']):>9}ms{fmt(r['ttft_p90_ms']):>9}ms{fmt(r['ttft_p99_ms']):>9}ms{fmt(r['tpot_p90_ms']):>9}ms{fmt(r['e2e_p90_ms']):>9}ms")
|
|
|
|
print(f"\n# OLD: window_1_results/b3_policy_comparison.json")
|
|
print(f"{'policy':<14}{'TTFTp50':>10}{'TTFTp90':>10}{'TTFTp99':>10}{'TPOTp90':>10}{'E2Ep90':>10}")
|
|
print("-" * 60)
|
|
for p in POLICIES:
|
|
if p not in old: continue
|
|
r = old[p]
|
|
print(f"{p:<14}{fmt(r['ttft_p50_ms']):>9}ms{fmt(r['ttft_p90_ms']):>9}ms{fmt(r['ttft_p99_ms']):>9}ms{fmt(r['tpot_p90_ms']):>9}ms{fmt(r['e2e_p90_ms']):>9}ms")
|
|
|
|
print(f"\n# DRIFT: today vs old (same policy)")
|
|
print(f"{'policy':<14}{'ΔTTFTp50':>10}{'ΔTTFTp90':>10}{'ΔTTFTp99':>10}{'ΔTPOTp90':>10}{'ΔE2Ep90':>10}")
|
|
print("-" * 60)
|
|
for p in POLICIES:
|
|
if p not in new or p not in old: continue
|
|
n, o = new[p], old[p]
|
|
print(f"{p:<14}{pctd(o['ttft_p50_ms'], n['ttft_p50_ms']):>10}"
|
|
f"{pctd(o['ttft_p90_ms'], n['ttft_p90_ms']):>10}"
|
|
f"{pctd(o['ttft_p99_ms'], n['ttft_p99_ms']):>10}"
|
|
f"{pctd(o['tpot_p90_ms'], n['tpot_p90_ms']):>10}"
|
|
f"{pctd(o['e2e_p90_ms'], n['e2e_p90_ms']):>10}")
|
|
|
|
# Relative ordering check
|
|
def ranks(values_dict, key):
|
|
items = [(p, r[key]) for p, r in values_dict.items() if r.get(key)]
|
|
items.sort(key=lambda x: x[1])
|
|
return [p for p, _ in items]
|
|
|
|
print(f"\n# TTFT p90 ranking (best → worst)")
|
|
for label, src in [("OLD", old), ("NEW", new)]:
|
|
if src:
|
|
order = ranks(src, "ttft_p90_ms")
|
|
print(f" {label}: {' < '.join(order)}")
|
|
|
|
out = {"new": new, "old": old}
|
|
out_path = args.root / "b3_replay_summary.json"
|
|
out_path.write_text(json.dumps(out, indent=2))
|
|
print(f"\nWrote {out_path}")
|
|
|
|
# Bar plot (matplotlib)
|
|
if not args.figure:
|
|
args.figure = args.root / "fig_b3_latency_bars_new.png"
|
|
try:
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
|
|
pols = [p for p in POLICIES if p in new]
|
|
metrics = [("TTFT p90 (s)", "ttft_p90_ms", 1000),
|
|
("TPOT p90 (ms)", "tpot_p90_ms", 1),
|
|
("E2E p90 (s)", "e2e_p90_ms", 1000)]
|
|
colors = {"lmetric": "tab:blue", "load_only": "tab:orange",
|
|
"sticky": "tab:green", "unified": "tab:red",
|
|
"unified_v2": "tab:purple"}
|
|
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
|
|
for ax, (label, key, div) in zip(axes, metrics):
|
|
vals = [new[p][key] / div for p in pols]
|
|
bars = ax.bar(pols, vals,
|
|
color=[colors.get(p, "gray") for p in pols],
|
|
edgecolor="black", linewidth=0.5)
|
|
ax.set_title(label)
|
|
ax.tick_params(axis="x", rotation=20)
|
|
for b, v in zip(bars, vals):
|
|
ax.text(b.get_x() + b.get_width() / 2, v, f"{v:.1f}",
|
|
ha="center", va="bottom", fontsize=9)
|
|
ax.grid(alpha=0.3, axis="y")
|
|
fig.suptitle(f"B3 5-policy re-test ({args.root.name})")
|
|
fig.tight_layout()
|
|
fig.savefig(args.figure, dpi=120)
|
|
print(f"Wrote {args.figure}")
|
|
except Exception as e:
|
|
print(f"(figure skipped: {e})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|