chopratejas--headroom
0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
616 行
19 KiB
Python
616 行
19 KiB
Python
"""Comprehensive LLM Benchmarks using EleutherAI lm-evaluation-harness.
|
|
|
|
This module runs industry-standard benchmarks to prove Headroom preserves accuracy.
|
|
|
|
Approach:
|
|
1. Run benchmarks directly against LLM provider (baseline)
|
|
2. Run same benchmarks through Headroom proxy
|
|
3. Compare scores - goal is accuracy preserved or improved
|
|
|
|
Supported benchmarks:
|
|
- MMLU: General knowledge across 57 subjects
|
|
- HellaSwag: Commonsense reasoning
|
|
- TruthfulQA: Factual accuracy
|
|
- GSM8K: Math reasoning (requires more tokens)
|
|
- ARC: Science reasoning
|
|
|
|
Usage:
|
|
# Quick test (5 samples per task)
|
|
python -m headroom.evals.comprehensive_benchmark --quick
|
|
|
|
# Full benchmark
|
|
python -m headroom.evals.comprehensive_benchmark --tasks mmlu,hellaswag
|
|
|
|
# Compare with baseline
|
|
python -m headroom.evals.comprehensive_benchmark --compare
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from headroom._subprocess import run
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default benchmarks - chosen for reliability and relevance
|
|
DEFAULT_TASKS = [
|
|
"gsm8k", # Math reasoning - generation-based, works with chat APIs
|
|
]
|
|
|
|
# Tier 1 standard benchmarks for the suite runner
|
|
TIER1_TASKS = [
|
|
"gsm8k", # Math reasoning
|
|
"truthfulqa_gen", # Factual accuracy
|
|
"mmlu", # General knowledge (57 subjects)
|
|
"arc_challenge", # Science reasoning
|
|
]
|
|
|
|
# Code tasks (need --confirm_run_unsafe_code)
|
|
TIER1_CODE_TASKS = [
|
|
"humaneval", # Code generation (pass@1)
|
|
]
|
|
|
|
EXTENDED_TASKS = [
|
|
"mmlu", # 57 subjects - comprehensive but slow
|
|
"gsm8k", # Math - requires multi-step reasoning
|
|
"arc_challenge", # Harder science reasoning
|
|
]
|
|
|
|
TIER3_TASKS = [
|
|
"hellaswag", # Commonsense reasoning
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class BenchmarkResult:
|
|
"""Result from a single benchmark run."""
|
|
|
|
task: str
|
|
metric: str
|
|
score: float
|
|
stderr: float | None = None
|
|
samples: int = 0
|
|
duration_seconds: float = 0.0
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"task": self.task,
|
|
"metric": self.metric,
|
|
"score": round(self.score, 4),
|
|
"stderr": round(self.stderr, 4) if self.stderr else None,
|
|
"samples": self.samples,
|
|
"duration_seconds": round(self.duration_seconds, 2),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class ComparisonResult:
|
|
"""Comparison between baseline and Headroom results."""
|
|
|
|
task: str
|
|
metric: str
|
|
baseline_score: float
|
|
headroom_score: float
|
|
|
|
@property
|
|
def delta(self) -> float:
|
|
return self.headroom_score - self.baseline_score
|
|
|
|
@property
|
|
def accuracy_preserved(self) -> bool:
|
|
"""True if Headroom score is within 2% of baseline."""
|
|
return self.headroom_score >= self.baseline_score - 0.02
|
|
|
|
@property
|
|
def accuracy_improved(self) -> bool:
|
|
"""True if Headroom score exceeds baseline."""
|
|
return self.headroom_score > self.baseline_score
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"task": self.task,
|
|
"metric": self.metric,
|
|
"baseline": round(self.baseline_score, 4),
|
|
"headroom": round(self.headroom_score, 4),
|
|
"delta": round(self.delta, 4),
|
|
"preserved": self.accuracy_preserved,
|
|
"improved": self.accuracy_improved,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class BenchmarkSuiteResult:
|
|
"""Results from a complete benchmark suite."""
|
|
|
|
model: str
|
|
baseline_results: list[BenchmarkResult] = field(default_factory=list)
|
|
headroom_results: list[BenchmarkResult] = field(default_factory=list)
|
|
comparisons: list[ComparisonResult] = field(default_factory=list)
|
|
total_duration_seconds: float = 0.0
|
|
|
|
@property
|
|
def all_preserved(self) -> bool:
|
|
"""True if all benchmarks preserved accuracy."""
|
|
if not self.comparisons:
|
|
return True
|
|
return all(c.accuracy_preserved for c in self.comparisons)
|
|
|
|
@property
|
|
def avg_delta(self) -> float:
|
|
"""Average score delta (positive = Headroom better)."""
|
|
if not self.comparisons:
|
|
return 0.0
|
|
return sum(c.delta for c in self.comparisons) / len(self.comparisons)
|
|
|
|
def summary(self) -> dict[str, Any]:
|
|
return {
|
|
"model": self.model,
|
|
"all_preserved": self.all_preserved,
|
|
"avg_delta": round(self.avg_delta, 4),
|
|
"total_duration_seconds": round(self.total_duration_seconds, 2),
|
|
"baseline": [r.to_dict() for r in self.baseline_results],
|
|
"headroom": [r.to_dict() for r in self.headroom_results],
|
|
"comparisons": [c.to_dict() for c in self.comparisons],
|
|
}
|
|
|
|
def print_summary(self) -> None:
|
|
"""Print a formatted summary to stdout."""
|
|
print("\n" + "=" * 70)
|
|
print("HEADROOM COMPREHENSIVE BENCHMARK RESULTS")
|
|
print("=" * 70)
|
|
print(f"Model: {self.model}")
|
|
print(f"Duration: {self.total_duration_seconds:.1f}s")
|
|
print()
|
|
|
|
if self.comparisons:
|
|
print(f"{'Task':<20} {'Baseline':>10} {'Headroom':>10} {'Delta':>10} {'Status':>10}")
|
|
print("-" * 70)
|
|
for c in self.comparisons:
|
|
status = "PASS" if c.accuracy_preserved else "FAIL"
|
|
delta_str = f"{c.delta:+.4f}"
|
|
print(
|
|
f"{c.task:<20} {c.baseline_score:>10.4f} {c.headroom_score:>10.4f} {delta_str:>10} {status:>10}"
|
|
)
|
|
print("-" * 70)
|
|
print(f"{'AVERAGE':<20} {'':<10} {'':<10} {self.avg_delta:>+10.4f}")
|
|
print()
|
|
print(f"ALL BENCHMARKS PASSED: {'YES' if self.all_preserved else 'NO'}")
|
|
else:
|
|
print("Headroom results only (no baseline comparison):")
|
|
print(f"{'Task':<20} {'Score':>10} {'Metric':<15}")
|
|
print("-" * 50)
|
|
for r in self.headroom_results:
|
|
print(f"{r.task:<20} {r.score:>10.4f} {r.metric:<15}")
|
|
|
|
print("=" * 70 + "\n")
|
|
|
|
|
|
def run_lm_eval(
|
|
model: str = "openai-chat-completions",
|
|
model_args: str | None = None,
|
|
tasks: list[str] | None = None,
|
|
num_fewshot: int | None = None,
|
|
limit: int | None = None,
|
|
output_path: str | None = None,
|
|
base_url: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Run lm-evaluation-harness and return results.
|
|
|
|
Args:
|
|
model: Model type for lm-eval
|
|
model_args: Model arguments string
|
|
tasks: List of tasks to run
|
|
num_fewshot: Number of few-shot examples
|
|
limit: Limit samples per task (for quick testing)
|
|
output_path: Where to save results
|
|
base_url: Base URL for API (for Headroom proxy)
|
|
|
|
Returns:
|
|
Dictionary with results from lm-eval
|
|
"""
|
|
tasks = tasks or DEFAULT_TASKS
|
|
|
|
# Tasks that require code execution
|
|
UNSAFE_TASKS = {"humaneval", "mbpp"}
|
|
|
|
# Build command
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"lm_eval",
|
|
"--model",
|
|
model,
|
|
"--tasks",
|
|
",".join(tasks),
|
|
"--batch_size",
|
|
"1", # Safe default for API models
|
|
"--apply_chat_template", # Required for chat completion models
|
|
"--log_samples", # Required to save results to file
|
|
]
|
|
|
|
# Add flag for tasks that require code execution
|
|
if any(t in UNSAFE_TASKS for t in tasks):
|
|
cmd.append("--confirm_run_unsafe_code")
|
|
|
|
# Build model_args
|
|
args_parts = []
|
|
if model_args:
|
|
args_parts.append(model_args)
|
|
if base_url:
|
|
args_parts.append(f"base_url={base_url}")
|
|
|
|
if args_parts:
|
|
cmd.extend(["--model_args", ",".join(args_parts)])
|
|
|
|
if num_fewshot is not None:
|
|
cmd.extend(["--num_fewshot", str(num_fewshot)])
|
|
|
|
if limit is not None:
|
|
cmd.extend(["--limit", str(limit)])
|
|
|
|
# Use temp directory for output
|
|
if output_path is None:
|
|
output_dir = tempfile.mkdtemp(prefix="lm_eval_")
|
|
output_path = output_dir
|
|
|
|
cmd.extend(["--output_path", output_path])
|
|
|
|
logger.info(f"Running: {' '.join(cmd)}")
|
|
|
|
# Run lm-eval
|
|
start_time = time.time()
|
|
env = {
|
|
**os.environ,
|
|
"TOKENIZERS_PARALLELISM": "false",
|
|
"HF_ALLOW_CODE_EVAL": "1", # Required for humaneval/mbpp tasks
|
|
}
|
|
result = run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
duration = time.time() - start_time
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"lm-eval failed: {result.stderr}")
|
|
raise RuntimeError(f"lm-eval failed: {result.stderr}")
|
|
|
|
# Load results - lm-eval creates a directory structure with timestamped files
|
|
results_dir: Path = Path(output_path) if output_path else Path(".")
|
|
|
|
# Find results_*.json in the output directory (lm-eval uses timestamped filenames)
|
|
results_file: Path | None = None
|
|
if results_dir.is_dir():
|
|
# Look for results_*.json files
|
|
for f in sorted(results_dir.glob("**/results_*.json"), reverse=True):
|
|
results_file = f
|
|
break
|
|
|
|
if results_file is None or not results_file.exists():
|
|
# Parse results from stdout as fallback
|
|
logger.warning("No results file found, parsing from stdout")
|
|
return {"results": {}, "_duration_seconds": duration, "_stdout": result.stdout}
|
|
|
|
with open(results_file) as fp:
|
|
results: dict[str, Any] = json.load(fp)
|
|
|
|
results["_duration_seconds"] = duration
|
|
return results
|
|
|
|
|
|
def parse_lm_eval_results(raw_results: dict[str, Any]) -> list[BenchmarkResult]:
|
|
"""Parse lm-eval output into BenchmarkResult objects."""
|
|
results = []
|
|
duration = raw_results.get("_duration_seconds", 0.0)
|
|
|
|
# Primary metrics by task (in order of preference)
|
|
PRIMARY_METRICS = {
|
|
"gsm8k": ["exact_match,flexible-extract", "exact_match,strict-match"],
|
|
"truthfulqa_gen": ["bleu_acc,none", "rouge1_acc,none"],
|
|
"humaneval": ["pass@1,create_test", "pass@1,none"],
|
|
"mbpp": ["pass@1,create_test", "pass@1,none"],
|
|
"drop": ["f1,none", "em,none"],
|
|
"mmlu": ["acc,none", "accuracy,none"],
|
|
"arc_challenge": ["acc_norm,none", "acc,none"],
|
|
"hellaswag": ["acc_norm,none", "acc,none"],
|
|
}
|
|
|
|
for task_name, task_results in raw_results.get("results", {}).items():
|
|
# Get the primary metric for this task
|
|
preferred_metrics = PRIMARY_METRICS.get(task_name, [])
|
|
selected_metric = None
|
|
selected_value = None
|
|
selected_stderr = None
|
|
|
|
# Try preferred metrics first
|
|
for metric_key in preferred_metrics:
|
|
if metric_key in task_results:
|
|
selected_metric = metric_key
|
|
selected_value = task_results[metric_key]
|
|
# Look for stderr
|
|
parts = metric_key.split(",")
|
|
stderr_key = (
|
|
f"{parts[0]}_stderr,{parts[1]}" if len(parts) > 1 else f"{parts[0]}_stderr"
|
|
)
|
|
selected_stderr = task_results.get(stderr_key)
|
|
break
|
|
|
|
# Fallback: find any metric with filter "flexible-extract" or "none"
|
|
if selected_metric is None:
|
|
for metric_name, value in task_results.items():
|
|
if metric_name in ("alias",) or "_stderr" in metric_name:
|
|
continue
|
|
if "," in metric_name:
|
|
parts = metric_name.split(",")
|
|
filter_name = parts[1] if len(parts) > 1 else ""
|
|
if filter_name in ("flexible-extract", "none"):
|
|
selected_metric = metric_name
|
|
selected_value = value
|
|
stderr_key = f"{parts[0]}_stderr,{filter_name}"
|
|
selected_stderr = task_results.get(stderr_key)
|
|
break
|
|
|
|
if selected_metric and selected_value is not None:
|
|
# Clean up metric name for display
|
|
parts = selected_metric.split(",")
|
|
clean_metric = parts[0]
|
|
filter_name = parts[1] if len(parts) > 1 else ""
|
|
display_metric = (
|
|
f"{clean_metric}_{filter_name}"
|
|
if filter_name and filter_name != "none"
|
|
else clean_metric
|
|
)
|
|
|
|
results.append(
|
|
BenchmarkResult(
|
|
task=task_name,
|
|
metric=display_metric,
|
|
score=float(selected_value),
|
|
stderr=float(selected_stderr) if selected_stderr else None,
|
|
duration_seconds=duration / max(len(raw_results.get("results", {})), 1),
|
|
)
|
|
)
|
|
|
|
return results
|
|
|
|
|
|
def run_baseline_benchmark(
|
|
model: str = "gpt-4o-mini",
|
|
tasks: list[str] | None = None,
|
|
limit: int | None = None,
|
|
) -> list[BenchmarkResult]:
|
|
"""Run benchmark directly against OpenAI (baseline)."""
|
|
logger.info(f"Running baseline benchmark with {model}...")
|
|
|
|
raw_results = run_lm_eval(
|
|
model="openai-chat-completions",
|
|
model_args=f"model={model}",
|
|
tasks=tasks,
|
|
limit=limit,
|
|
)
|
|
|
|
return parse_lm_eval_results(raw_results)
|
|
|
|
|
|
def run_headroom_benchmark(
|
|
model: str = "gpt-4o-mini",
|
|
tasks: list[str] | None = None,
|
|
limit: int | None = None,
|
|
headroom_port: int = 8787,
|
|
) -> list[BenchmarkResult]:
|
|
"""Run benchmark through Headroom proxy."""
|
|
logger.info(f"Running Headroom benchmark with {model} through proxy...")
|
|
|
|
# lm_eval expects base_url to be the full path to chat/completions
|
|
base_url = f"http://localhost:{headroom_port}/v1/chat/completions"
|
|
|
|
# Get API key from environment
|
|
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
if not api_key:
|
|
raise ValueError("OPENAI_API_KEY environment variable required")
|
|
|
|
raw_results = run_lm_eval(
|
|
model="local-chat-completions",
|
|
model_args=f"model={model},tokenizer_backend=tiktoken,api_key={api_key}",
|
|
tasks=tasks,
|
|
limit=limit,
|
|
base_url=base_url,
|
|
)
|
|
|
|
return parse_lm_eval_results(raw_results)
|
|
|
|
|
|
def compare_results(
|
|
baseline: list[BenchmarkResult],
|
|
headroom: list[BenchmarkResult],
|
|
) -> list[ComparisonResult]:
|
|
"""Compare baseline and Headroom results."""
|
|
comparisons = []
|
|
|
|
# Match results by task
|
|
baseline_by_task = {r.task: r for r in baseline}
|
|
headroom_by_task = {r.task: r for r in headroom}
|
|
|
|
for task in baseline_by_task:
|
|
if task in headroom_by_task:
|
|
b = baseline_by_task[task]
|
|
h = headroom_by_task[task]
|
|
comparisons.append(
|
|
ComparisonResult(
|
|
task=task,
|
|
metric=b.metric,
|
|
baseline_score=b.score,
|
|
headroom_score=h.score,
|
|
)
|
|
)
|
|
|
|
return comparisons
|
|
|
|
|
|
def run_comprehensive_benchmark(
|
|
model: str = "gpt-4o-mini",
|
|
tasks: list[str] | None = None,
|
|
limit: int | None = None,
|
|
compare_baseline: bool = True,
|
|
headroom_port: int = 8787,
|
|
) -> BenchmarkSuiteResult:
|
|
"""Run comprehensive benchmark suite.
|
|
|
|
Args:
|
|
model: Model to benchmark
|
|
tasks: List of benchmark tasks
|
|
limit: Limit samples per task (for quick testing)
|
|
compare_baseline: Whether to run baseline comparison
|
|
headroom_port: Port where Headroom proxy is running
|
|
|
|
Returns:
|
|
BenchmarkSuiteResult with all results
|
|
"""
|
|
tasks = tasks or DEFAULT_TASKS
|
|
start_time = time.time()
|
|
|
|
suite = BenchmarkSuiteResult(model=model)
|
|
|
|
# Run baseline FIRST to warm up OpenAI infrastructure (KV cache, prefix caching)
|
|
# This ensures a fair comparison - any speedup from Headroom is real, not from warm cache
|
|
if compare_baseline:
|
|
suite.baseline_results = run_baseline_benchmark(
|
|
model=model,
|
|
tasks=tasks,
|
|
limit=limit,
|
|
)
|
|
|
|
# Run Headroom benchmark (after baseline warms things up)
|
|
suite.headroom_results = run_headroom_benchmark(
|
|
model=model,
|
|
tasks=tasks,
|
|
limit=limit,
|
|
headroom_port=headroom_port,
|
|
)
|
|
|
|
# Compare results
|
|
if compare_baseline:
|
|
suite.comparisons = compare_results(suite.baseline_results, suite.headroom_results)
|
|
|
|
suite.total_duration_seconds = time.time() - start_time
|
|
return suite
|
|
|
|
|
|
def check_headroom_proxy(port: int = 8787) -> bool:
|
|
"""Check if Headroom proxy is running."""
|
|
import socket
|
|
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(1)
|
|
s.connect(("localhost", port))
|
|
return True
|
|
except (TimeoutError, ConnectionRefusedError):
|
|
return False
|
|
|
|
|
|
def _load_env() -> None:
|
|
"""Load .env file for API keys."""
|
|
try:
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
def main() -> None:
|
|
"""CLI entry point."""
|
|
import argparse
|
|
|
|
_load_env()
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Run comprehensive LLM benchmarks to verify Headroom accuracy"
|
|
)
|
|
parser.add_argument(
|
|
"--model", "-m", default="gpt-4o-mini", help="Model to benchmark (default: gpt-4o-mini)"
|
|
)
|
|
parser.add_argument(
|
|
"--tasks",
|
|
"-t",
|
|
default=None,
|
|
help="Comma-separated list of tasks (default: hellaswag,truthfulqa_mc2,arc_easy)",
|
|
)
|
|
parser.add_argument(
|
|
"--quick", "-q", action="store_true", help="Quick test with 5 samples per task"
|
|
)
|
|
parser.add_argument("--limit", "-l", type=int, default=None, help="Limit samples per task")
|
|
parser.add_argument(
|
|
"--compare", "-c", action="store_true", help="Compare with baseline (run without Headroom)"
|
|
)
|
|
parser.add_argument(
|
|
"--port", "-p", type=int, default=8787, help="Headroom proxy port (default: 8787)"
|
|
)
|
|
parser.add_argument("--output", "-o", default=None, help="Output file for results JSON")
|
|
parser.add_argument(
|
|
"--headroom-only", action="store_true", help="Only run through Headroom (skip baseline)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
|
|
# Parse tasks
|
|
tasks = args.tasks.split(",") if args.tasks else DEFAULT_TASKS
|
|
|
|
# Set limit for quick mode
|
|
limit = args.limit
|
|
if args.quick and limit is None:
|
|
limit = 5
|
|
|
|
# Check proxy is running
|
|
if not check_headroom_proxy(args.port):
|
|
print(f"ERROR: Headroom proxy not running on port {args.port}")
|
|
print(f"Start it with: headroom proxy --port {args.port}")
|
|
sys.exit(1)
|
|
|
|
# Run benchmarks
|
|
compare = args.compare and not args.headroom_only
|
|
|
|
try:
|
|
result = run_comprehensive_benchmark(
|
|
model=args.model,
|
|
tasks=tasks,
|
|
limit=limit,
|
|
compare_baseline=compare,
|
|
headroom_port=args.port,
|
|
)
|
|
except Exception as e:
|
|
print(f"ERROR: Benchmark failed: {e}")
|
|
sys.exit(1)
|
|
|
|
# Print summary
|
|
result.print_summary()
|
|
|
|
# Save results if requested
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(result.summary(), f, indent=2)
|
|
print(f"Results saved to {args.output}")
|
|
|
|
# Exit with appropriate code
|
|
if compare and not result.all_preserved:
|
|
sys.exit(1)
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|