Spaces:
Sleeping
Sleeping
| """ | |
| Plotting and AIQ calculation script for InferRoute. | |
| This script parses evaluation results from benchmarks/results/eval_results.json, | |
| computes average metrics, groups swept scenarios to trace the cost-quality | |
| curves (Zero Router, KNN Router, MLP Router), computes the AIQ (Area Under the Curve) | |
| metric, and plots the results. | |
| Inspired by: | |
| "ROUTERBENCH: A Benchmark for Multi-LLM Routing System" (withmartian/routerbench) | |
| """ | |
| import os | |
| import json | |
| from typing import List, Dict, Tuple | |
| # Try to import pandas and matplotlib for visualization | |
| HAS_PLOT_LIBS = False | |
| try: | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| HAS_PLOT_LIBS = True | |
| except ImportError: | |
| pass | |
| RESULTS_DIR = os.path.join(os.path.dirname(__file__), "results") | |
| RESULTS_PATH = os.path.join(RESULTS_DIR, "eval_results.json") | |
| SUMMARY_PATH = os.path.join(RESULTS_DIR, "evaluation_summary.md") | |
| def calculate_auc(points: List[Tuple[float, float]]) -> float: | |
| """ | |
| Computes the Area Under the Curve (AUC) using the trapezoidal rule, | |
| normalized by the cost range (c_max - c_min) of the curve. | |
| This yields the average quality efficiency score (0.0 to 1.0) of the router. | |
| """ | |
| sorted_pts = sorted(points, key=lambda x: x[0]) | |
| if len(sorted_pts) < 2: | |
| return 0.0 | |
| area = 0.0 | |
| for i in range(len(sorted_pts) - 1): | |
| c1, q1 = sorted_pts[i] | |
| c2, q2 = sorted_pts[i+1] | |
| area += 0.5 * (q1 + q2) * (c2 - c1) | |
| cost_range = sorted_pts[-1][0] - sorted_pts[0][0] | |
| if cost_range > 0: | |
| return area / cost_range | |
| return sorted_pts[0][1] | |
| def analyze_results(): | |
| if not os.path.exists(RESULTS_PATH): | |
| print(f"Error: Results file not found at {RESULTS_PATH}. Please run run_router_eval.py first.") | |
| return | |
| with open(RESULTS_PATH, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| # Group data by scenario | |
| scenarios: Dict[str, List[dict]] = {} | |
| for entry in data: | |
| sc = entry["scenario"] | |
| if sc not in scenarios: | |
| scenarios[sc] = [] | |
| scenarios[sc].append(entry) | |
| summary_stats = {} | |
| # Average stats per scenario | |
| for name, entries in scenarios.items(): | |
| total = len(entries) | |
| avg_cost = sum(e["cost_usd"] for e in entries) / total | |
| avg_quality = sum(e["quality_score"] for e in entries) / total | |
| avg_latency = sum(e["latency_ms"] for e in entries) / total | |
| avg_ttft = sum(e["ttft_ms"] for e in entries) / total | |
| slo_met_percent = (sum(1 for e in entries if e["slo_compliant"]) / total) * 100.0 | |
| fallback_percent = (sum(1 for e in entries if e["fallback_triggered"]) / total) * 100.0 | |
| summary_stats[name] = { | |
| "avg_cost": avg_cost, | |
| "avg_quality": avg_quality, | |
| "avg_latency": avg_latency, | |
| "avg_ttft": avg_ttft, | |
| "slo_compliance": slo_met_percent, | |
| "fallback_rate": fallback_percent | |
| } | |
| # Group curves | |
| curves = { | |
| "zero-router": [], | |
| "knn-router": [], | |
| "mlp-router": [], | |
| "cascade-router": [] | |
| } | |
| for name, stats in summary_stats.items(): | |
| for prefix in curves.keys(): | |
| if name.startswith(prefix): | |
| curves[prefix].append((stats["avg_cost"], stats["avg_quality"])) | |
| # Sort curve points by cost | |
| for k in curves.keys(): | |
| curves[k] = sorted(curves[k], key=lambda x: x[0]) | |
| # Calculate AIQ (Area under the cost-quality curve) | |
| aiq_scores = {} | |
| for k, pts in curves.items(): | |
| aiq_scores[k] = calculate_auc(pts) | |
| print("\n=============================================================") | |
| print("InferRoute RouterBench Policy Evaluation Summary Results") | |
| print("=============================================================") | |
| markdown_lines = [ | |
| "# ๐ InferRoute RouterBench & FrugalGPT Evaluation Summary\n", | |
| "Inspired by the RouterBench framework (`withmartian/routerbench`) and FrugalGPT cascading LLMs, this report evaluates routing policies on cost, quality, and SLA compliance. We plot the Pareto curves by sweeping the willingness-to-pay ($\\lambda$), mixture ratio ($p$), and cascade threshold ($\\tau$).\n", | |
| "## ๐ Curve Efficiency: AIQ (Area Under the Trade-off Curve)", | |
| "AIQ measures the average quality efficiency score of a router across its swept cost range (normalized AUC, bounded between 0% and 100%). Higher is better.\n", | |
| "| Routing Curve | AIQ Score (Normalized AUC) | Description |", | |
| "| :--- | :--- | :--- |", | |
| f"| **Oracle Router Upper Bound** | *Theoretical Optimal* | Represents the perfect offline selection. |", | |
| f"| **Cascade Router (FrugalGPT)** | {aiq_scores['cascade-router'] * 100:.1f}% | Server-side cascading model escalation. |", | |
| f"| **KNN Router** | {aiq_scores['knn-router'] * 100:.1f}% | Jaccard similarity nearest-neighbor routing. |", | |
| f"| **MLP Router** | {aiq_scores['mlp-router'] * 100:.1f}% | Content-aware classifier routing. |", | |
| f"| **Zero Router Baseline** | {aiq_scores['zero-router'] * 100:.1f}% | Non-content-aware random model mixture. |\n", | |
| "## ๐ Comprehensive Performance Table", | |
| "| Scenario | Avg Cost ($ USD) | Avg Quality (0-1) | Avg Latency (ms) | Avg TTFT (ms) | SLO Compliance (%) | Fallback Rate (%) |", | |
| "| :--- | :--- | :--- | :--- | :--- | :--- | :--- |" | |
| ] | |
| # Sort scenarios alphabetically for display, but keep sweeps grouped | |
| sorted_scenarios = sorted(summary_stats.keys()) | |
| for name in sorted_scenarios: | |
| stats = summary_stats[name] | |
| row = ( | |
| f"| **{name}** " | |
| f"| ${stats['avg_cost']:.6f} " | |
| f"| {stats['avg_quality']:.2f} " | |
| f"| {stats['avg_latency']:.1f}ms " | |
| f"| {stats['avg_ttft']:.1f}ms " | |
| f"| {stats['slo_compliance']:.1f}% " | |
| f"| {stats['fallback_rate']:.1f}% |" | |
| ) | |
| print(f"Scenario: {name:<22} | Cost: ${stats['avg_cost']:.6f} | Quality: {stats['avg_quality']:.2f} | Latency: {stats['avg_latency']:.1f}ms | SLO: {stats['slo_compliance']:.1f}%") | |
| markdown_lines.append(row) | |
| # Save Markdown Summary Table | |
| with open(SUMMARY_PATH, "w", encoding="utf-8") as f: | |
| f.write("\n".join(markdown_lines)) | |
| print(f"\nMarkdown summary saved to {SUMMARY_PATH}") | |
| # Generate charts if libraries are present | |
| if HAS_PLOT_LIBS: | |
| print("\nPlotting libraries detected. Generating cost-quality trade-off curves...") | |
| # 1. Cost vs Quality Pareto Frontier Plot | |
| plt.figure(figsize=(10, 6)) | |
| # Colors | |
| colors = { | |
| "zero-router": "#e74c3c", | |
| "knn-router": "#3498db", | |
| "mlp-router": "#9b59b6", | |
| "cascade-router": "#2ecc71" | |
| } | |
| # Plot curves | |
| for key, pts in curves.items(): | |
| if not pts: | |
| continue | |
| costs, qualities = zip(*pts) | |
| # Scale cost to micro-USD | |
| scaled_costs = [c * 1_000_000 for c in costs] | |
| label = f"{key.upper()} (AIQ={aiq_scores[key]:.2f})" | |
| plt.plot(scaled_costs, qualities, 'o-', label=label, color=colors[key], linewidth=2.5, markersize=8) | |
| # Plot individual reference points | |
| ref_points = { | |
| "always-openai": "#27ae60", | |
| "always-gemini": "#1abc9c", | |
| "always-vllm": "#f1c40f", | |
| "always-ollama": "#e67e22", | |
| "rule-router": "#34495e", | |
| "oracle-router": "#d35400" | |
| } | |
| for name, color in ref_points.items(): | |
| if name in summary_stats: | |
| st = summary_stats[name] | |
| sc_cost = st["avg_cost"] * 1_000_000 | |
| marker = '*' if 'oracle' in name else 's' | |
| size = 180 if 'oracle' in name else 100 | |
| plt.scatter(sc_cost, st["avg_quality"], color=color, marker=marker, s=size, zorder=5, label=name) | |
| plt.text(sc_cost + 0.5, st["avg_quality"] - 0.01, f" {name}", fontsize=8, weight="bold") | |
| plt.xlabel("Average Cost per Request (USD per 1 Million Prompts)", fontsize=11) | |
| plt.ylabel("Average Response Quality Score (0 - 1.0)", fontsize=11) | |
| plt.title("Cost-Quality Pareto Trade-off Curves (RouterBench Framework)", fontsize=13, weight="bold") | |
| plt.grid(True, linestyle="--", alpha=0.5) | |
| plt.legend(loc="lower right", fontsize=9) | |
| plt.tight_layout() | |
| plot_path = os.path.join(RESULTS_DIR, "cost_quality_frontier.png") | |
| plt.savefig(plot_path, dpi=150) | |
| plt.close() | |
| print(f"Saved Cost-Quality trade-off chart to {plot_path}") | |
| # 2. Latency TTFT vs Total Latency Bar Plot (Only main representative scenarios) | |
| main_scenarios = [ | |
| "always-openai", "always-gemini", "always-vllm", "always-ollama", | |
| "rule-router", "oracle-router", "cascade-router_t0.60", | |
| "knn-router_l1.00", "mlp-router_l1.00" | |
| ] | |
| plt.figure(figsize=(10, 6)) | |
| plot_data = [] | |
| for name in main_scenarios: | |
| if name in summary_stats: | |
| st = summary_stats[name] | |
| st["name"] = name | |
| plot_data.append(st) | |
| if plot_data: | |
| df_plot = pd.DataFrame(plot_data) | |
| x = range(len(df_plot)) | |
| width = 0.35 | |
| plt.bar([i - width/2 for i in x], df_plot["avg_ttft"], width, label="Avg TTFT (ms)", color="#3498db") | |
| plt.bar([i + width/2 for i in x], df_plot["avg_latency"], width, label="Avg Total Latency (ms)", color="#2ecc71") | |
| plt.xticks(x, df_plot["name"], rotation=20, ha="right", fontsize=9, weight="bold") | |
| plt.ylabel("Duration (milliseconds)", fontsize=11) | |
| plt.title("Latency Performance Comparison (TTFT vs. Total Latency)", fontsize=13, weight="bold") | |
| plt.legend() | |
| plt.grid(True, axis="y", linestyle="--", alpha=0.5) | |
| plt.tight_layout() | |
| latency_plot_path = os.path.join(RESULTS_DIR, "latency_comparison.png") | |
| plt.savefig(latency_plot_path, dpi=150) | |
| plt.close() | |
| print(f"Saved Latency chart to {latency_plot_path}") | |
| else: | |
| print("\nWarning: pandas/matplotlib not found. Skipping PNG chart generation.") | |
| print("To generate cost-quality plots, please run: pip install pandas matplotlib") | |
| if __name__ == "__main__": | |
| analyze_results() | |