Files
2026-07-13 12:12:21 +08:00

195 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""
Scraper for localmaxxing.com benchmark data.
Fetches leaderboard results for all hardware presets and caches them locally
so the TUI has a fallback when the API is unreachable.
Usage:
python3 scrape_benchmarks.py # Scrape all presets, 100 results each
python3 scrape_benchmarks.py --limit 50 # Fewer results per preset
python3 scrape_benchmarks.py --api-key bhk_... # Use API key for auth
python3 scrape_benchmarks.py --presets "RTX 4090,M4 Max" # Specific presets only
Output:
llmfit-core/data/benchmark_cache.json (compiled into binary)
The cache format is:
{
"scraped_at": "2026-04-27T15:00:00Z",
"presets": {
"RTX 4090 (24 GB)": { "rows": [...], "total": 47 },
"Apple M4 Max (128 GB)": { "rows": [...], "total": 12 },
...
}
}
"""
import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone
BASE_URL = "https://localmaxxing.com/api"
# Mirror of the Rust HARDWARE_PRESETS — keep in sync with benchmarks.rs
HARDWARE_PRESETS = [
# NVIDIA consumer
{"label": "RTX 5090 (32 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 5090", "memTier": 32},
{"label": "RTX 5080 (16 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 5080", "memTier": 16},
{"label": "RTX 4090 (24 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 4090", "memTier": 24},
{"label": "RTX 4080 (16 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 4080", "memTier": 16},
{"label": "RTX 4070 Ti (12 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 4070", "memTier": 12},
{"label": "RTX 3090 (24 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 3090", "memTier": 24},
{"label": "RTX 3080 (10 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 3080", "memTier": 12},
{"label": "RTX 3060 (12 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "RTX 3060", "memTier": 12},
# NVIDIA datacenter
{"label": "A100 (80 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "A100", "memTier": 80},
{"label": "A100 (40 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "A100", "memTier": 48},
{"label": "H100 (80 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "H100", "memTier": 80},
{"label": "L40S (48 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "L40S", "memTier": 48},
{"label": "T4 (16 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "T4", "memTier": 16},
# AMD
{"label": "RX 7900 XTX (24 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "7900 XTX", "memTier": 24},
{"label": "RX 7900 XT (20 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "7900 XT", "memTier": 24},
{"label": "MI300X (192 GB)", "hwClass": "DISCRETE_GPU", "hardwareName": "MI300X", "memTier": 128},
# Apple Silicon
{"label": "Apple M4 Max (128 GB)", "hwClass": "UNIFIED", "hardwareName": "M4 Max", "memTier": 128},
{"label": "Apple M4 Max (64 GB)", "hwClass": "UNIFIED", "hardwareName": "M4 Max", "memTier": 48},
{"label": "Apple M4 Pro (48 GB)", "hwClass": "UNIFIED", "hardwareName": "M4 Pro", "memTier": 48},
{"label": "Apple M4 Pro (24 GB)", "hwClass": "UNIFIED", "hardwareName": "M4 Pro", "memTier": 24},
{"label": "Apple M3 Max (128 GB)", "hwClass": "UNIFIED", "hardwareName": "M3 Max", "memTier": 128},
{"label": "Apple M3 Max (96 GB)", "hwClass": "UNIFIED", "hardwareName": "M3 Max", "memTier": 96},
{"label": "Apple M2 Ultra (192 GB)", "hwClass": "UNIFIED", "hardwareName": "M2 Ultra", "memTier": 128},
{"label": "Apple M2 Max (96 GB)", "hwClass": "UNIFIED", "hardwareName": "M2 Max", "memTier": 96},
{"label": "Apple M2 Pro (32 GB)", "hwClass": "UNIFIED", "hardwareName": "M2 Pro", "memTier": 32},
{"label": "Apple M1 Max (64 GB)", "hwClass": "UNIFIED", "hardwareName": "M1 Max", "memTier": 48},
# CPU only
{"label": "CPU Only", "hwClass": "CPU_ONLY", "hardwareName": None, "memTier": None},
]
def fetch_leaderboard(preset: dict, api_key: str | None, limit: int) -> dict:
"""Fetch leaderboard for a single hardware preset."""
params = [f"hwClass={preset['hwClass']}"]
if preset["hardwareName"]:
name = preset["hardwareName"].replace(" ", "+")
params.append(f"hardwareName={name}")
if preset["memTier"]:
params.append(f"memTier={preset['memTier']}")
params.append(f"limit={limit}")
url = f"{BASE_URL}/leaderboard?{'&'.join(params)}"
headers = {"User-Agent": "llmfit-benchmark-scraper/1.0"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode())
return data
except urllib.error.HTTPError as e:
print(f" HTTP {e.code}: {e.reason}")
return {"rows": [], "total": 0}
except urllib.error.URLError as e:
print(f" Network error: {e.reason}")
return {"rows": [], "total": 0}
except Exception as e:
print(f" Error: {e}")
return {"rows": [], "total": 0}
def main():
parser = argparse.ArgumentParser(
description="Scrape localmaxxing.com benchmark data for offline cache"
)
parser.add_argument(
"--limit", type=int, default=100,
help="Max results per hardware preset (default: 100)"
)
parser.add_argument(
"--api-key", type=str, default=None,
help="localmaxxing.com API key (or set LOCALMAXXING_API_KEY env var)"
)
parser.add_argument(
"--presets", type=str, default=None,
help="Comma-separated list of preset labels to scrape (default: all)"
)
args = parser.parse_args()
api_key = args.api_key or os.environ.get("LOCALMAXXING_API_KEY")
# Filter presets if specified
presets = HARDWARE_PRESETS
if args.presets:
filter_names = {s.strip().lower() for s in args.presets.split(",")}
presets = [p for p in presets if p["label"].lower() in filter_names]
if not presets:
print(f"No matching presets found. Available:")
for p in HARDWARE_PRESETS:
print(f" {p['label']}")
sys.exit(1)
print(f"Scraping {len(presets)} hardware presets from localmaxxing.com...")
if api_key:
print(f" Using API key: {api_key[:8]}...")
print()
cache = {
"scraped_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"presets": {},
}
total_results = 0
for i, preset in enumerate(presets):
label = preset["label"]
print(f" [{i+1}/{len(presets)}] {label}...", end=" ", flush=True)
data = fetch_leaderboard(preset, api_key, args.limit)
count = len(data.get("rows", []))
total = data.get("total", count)
cache["presets"][label] = {
"rows": data.get("rows", []),
"total": total,
}
total_results += count
print(f"{count} results (total: {total})")
# Be polite to the API
if i < len(presets) - 1:
time.sleep(0.5)
output_paths = ["llmfit-core/data/benchmark_cache.json"]
for path in output_paths:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
json.dump(cache, f, indent=2)
file_size = os.path.getsize(output_paths[0])
size_str = f"{file_size / 1024:.0f} KB" if file_size < 1024 * 1024 else f"{file_size / (1024*1024):.1f} MB"
print(f"\nWrote {total_results} total benchmark results ({size_str}) to:")
for p in output_paths:
print(f" {p}")
# Summary table
print(f"\n{'Hardware':<30} {'Results':>8} {'Total':>8}")
print("-" * 48)
for label, data in cache["presets"].items():
count = len(data["rows"])
total = data["total"]
if count > 0:
print(f"{label:<30} {count:>8} {total:>8}")
if __name__ == "__main__":
main()