evalstone/webui/results_scan.py
sora 541b0b477d Improve EvalStone web UI paths, custom suites, and results charts.
Move required path fields up front, persist custom benchmark suites, enlarge readable typography, show full suite benchmark names, add capability-domain trend charts with local Chart.js, and polish ranking name display.
2026-07-29 07:38:05 +00:00

385 lines
12 KiB
Python

#!/usr/bin/env python3
"""Scan EvalScope output directories and aggregate per-model benchmark scores."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# Display order for comparison charts
CATEGORY_ORDER = [
('math', '数学推理', [
'aime24', 'aime25', 'aime26', 'hmmt26',
'imo_answerbench', 'competition_math', 'gsm8k',
]),
('code', '代码', ['humaneval', 'live_code_bench', 'bigcodebench']),
('science', '科学 / 高难推理', ['gpqa_diamond', 'super_gpqa', 'hle']),
('knowledge', '知识与通用能力', [
'mmlu', 'mmlu_pro', 'cmmlu', 'bbh', 'arc',
'drop', 'hellaswag', 'winogrande', 'simple_qa', 'trivia_qa',
]),
('long_context', '长文本', ['longbench_v2', 'openai_mrcr']),
('tool_agent', '工具调用 / 智能体', ['bfcl_v3', 'general_fc', 'tau2_bench']),
]
BENCHMARK_ALIAS = {
'hle_low': 'hle',
}
SKIP_DIR_NAMES = {
'active_time', 'perf_stats_backup', 'predictions_archive',
'logs', 'configs', 'reports', 'predictions', 'reviews',
}
def _is_seed_dir(path: Path) -> bool:
return path.is_dir() and path.name.startswith('seed_')
def _is_benchmark_dir(path: Path) -> bool:
if not path.is_dir() or path.name in SKIP_DIR_NAMES:
return False
try:
return any(_is_seed_dir(child) for child in path.iterdir())
except OSError:
return False
def _is_model_folder(path: Path) -> bool:
if not path.is_dir() or path.name.startswith('.'):
return False
try:
return any(_is_benchmark_dir(child) for child in path.iterdir())
except OSError:
return False
def extract_score(report_data: dict) -> Optional[float]:
score = report_data.get('score')
if score is not None:
try:
return float(score)
except (TypeError, ValueError):
pass
for metric in report_data.get('metrics') or []:
if metric.get('name') in ('mean_acc', 'acc', 'accuracy', 'pass@1', 'Score'):
for key in ('score', 'macro_score'):
if metric.get(key) is not None:
try:
return float(metric[key])
except (TypeError, ValueError):
continue
# fallback: first metric with a score
for metric in report_data.get('metrics') or []:
for key in ('score', 'macro_score'):
if metric.get(key) is not None:
try:
return float(metric[key])
except (TypeError, ValueError):
continue
return None
def _find_report_files(seed_dir: Path, benchmark: str) -> List[Path]:
reports_dir = seed_dir / 'reports'
if not reports_dir.is_dir():
return []
preferred = [
reports_dir / f'{benchmark}.json',
reports_dir / f'{BENCHMARK_ALIAS.get(benchmark, benchmark)}.json',
]
found = [p for p in preferred if p.is_file()]
if found:
return found
# nested: reports/<model>/<benchmark>.json (legacy)
nested = sorted(reports_dir.glob(f'*/*.json')) + sorted(reports_dir.glob('*.json'))
return [p for p in nested if p.is_file()]
def _read_report(path: Path) -> Optional[dict]:
try:
return json.loads(path.read_text(encoding='utf-8'))
except Exception:
return None
def collect_benchmark_scores(model_dir: Path, benchmark: str) -> Optional[dict]:
bench_dir = model_dir / benchmark
if not bench_dir.is_dir():
# alias: hle stored as hle_low
for alias_src, alias_dst in BENCHMARK_ALIAS.items():
if benchmark == alias_dst:
alt = model_dir / alias_src
if alt.is_dir():
bench_dir = alt
break
else:
return None
scores: List[float] = []
runs: List[dict] = []
model_name = None
num_samples = None
for seed_dir in sorted(p for p in bench_dir.iterdir() if _is_seed_dir(p)):
for report_path in _find_report_files(seed_dir, benchmark):
data = _read_report(report_path)
if not data:
continue
score = extract_score(data)
if score is None:
continue
scores.append(score)
if model_name is None:
model_name = data.get('model_name')
if num_samples is None:
num_samples = data.get('num')
if num_samples is None:
metrics = data.get('metrics') or []
if metrics:
num_samples = metrics[0].get('num')
try:
rel = str(report_path.relative_to(model_dir))
except ValueError:
rel = str(report_path)
runs.append({
'seed_dir': seed_dir.name,
'score': score,
'report': rel,
})
break # one report per seed dir
if not scores:
return None
avg = sum(scores) / len(scores)
return {
'benchmark': BENCHMARK_ALIAS.get(benchmark, benchmark),
'score': round(avg, 6),
'scores': scores,
'n_runs': len(scores),
'num_samples': num_samples,
'model_name': model_name,
'runs': runs,
}
def list_benchmarks_in_model(model_dir: Path) -> List[str]:
names = []
for child in sorted(model_dir.iterdir()):
if not _is_benchmark_dir(child):
continue
names.append(BENCHMARK_ALIAS.get(child.name, child.name))
# unique preserve order
seen = set()
out = []
for n in names:
if n not in seen:
seen.add(n)
out.append(n)
return out
def scan_output_dir(output_dir: Path) -> dict:
output_dir = Path(output_dir)
if not output_dir.is_dir():
return {
'output_dir': str(output_dir),
'models': [],
'benchmarks': [],
'categories': [],
'matrix': {},
}
models = []
all_benchmarks = set()
matrix: Dict[str, Dict[str, dict]] = {}
for child in sorted(output_dir.iterdir()):
if not _is_model_folder(child):
continue
folder = child.name
bench_names = list_benchmarks_in_model(child)
model_scores: Dict[str, dict] = {}
served_model = None
for bench in bench_names:
info = collect_benchmark_scores(child, bench)
if not info:
continue
model_scores[bench] = info
all_benchmarks.add(bench)
if served_model is None and info.get('model_name'):
served_model = info['model_name']
if not model_scores:
continue
models.append({
'folder': folder,
'model_name': served_model or folder,
'benchmarks': sorted(model_scores.keys()),
'n_benchmarks': len(model_scores),
'path': str(child),
})
matrix[folder] = model_scores
# Ordered benchmark list by category, then leftovers
ordered = []
seen = set()
categories = []
for cat_id, cat_name, items in CATEGORY_ORDER:
present = [b for b in items if b in all_benchmarks]
if present:
categories.append({'id': cat_id, 'name': cat_name, 'items': present})
for b in present:
if b not in seen:
seen.add(b)
ordered.append(b)
others = sorted(all_benchmarks - seen)
if others:
categories.append({'id': 'other', 'name': '其他', 'items': others})
ordered.extend(others)
# Compact matrix for API: folder -> benchmark -> score summary
compact = {}
for folder, benches in matrix.items():
compact[folder] = {
b: {
'score': info['score'],
'n_runs': info['n_runs'],
'num_samples': info.get('num_samples'),
'model_name': info.get('model_name'),
}
for b, info in benches.items()
}
return {
'output_dir': str(output_dir),
'models': models,
'benchmarks': ordered,
'categories': categories,
'matrix': compact,
}
def compare_models(
output_dir: Path,
folders: Optional[List[str]] = None,
benchmarks: Optional[List[str]] = None,
) -> dict:
overview = scan_output_dir(output_dir)
available = {m['folder'] for m in overview['models']}
if folders:
selected = [f for f in folders if f in available]
else:
selected = [m['folder'] for m in overview['models']]
if benchmarks:
bench_list = [b for b in benchmarks if b in overview['benchmarks']]
else:
bench_list = list(overview['benchmarks'])
series = []
for folder in selected:
scores = []
for b in bench_list:
cell = overview['matrix'].get(folder, {}).get(b)
scores.append(cell['score'] if cell else None)
model_meta = next((m for m in overview['models'] if m['folder'] == folder), None)
series.append({
'folder': folder,
'label': model_meta['model_name'] if model_meta else folder,
'display': folder,
'scores': scores,
})
# Per-benchmark ranking
ranking = []
for i, b in enumerate(bench_list):
rows = []
for s in series:
if s['scores'][i] is not None:
rows.append({'folder': s['folder'], 'label': s['label'], 'score': s['scores'][i]})
rows.sort(key=lambda x: x['score'], reverse=True)
ranking.append({'benchmark': b, 'rows': rows})
# Capability-domain aggregation: mean score over selected benches in each category
bench_set = set(bench_list)
active_categories = []
for cat in overview['categories']:
items = [b for b in cat['items'] if b in bench_set]
if items:
active_categories.append({
'id': cat['id'],
'name': cat['name'],
'items': items,
})
category_labels = [c['name'] for c in active_categories]
category_series = []
for folder in selected:
scores = []
details = []
for cat in active_categories:
vals = []
for b in cat['items']:
cell = overview['matrix'].get(folder, {}).get(b)
if cell and cell.get('score') is not None:
vals.append(float(cell['score']))
if vals:
avg = sum(vals) / len(vals)
scores.append(round(avg, 6))
else:
avg = None
scores.append(None)
details.append({
'category': cat['name'],
'score': avg,
'n_benchmarks': len(vals),
'benchmarks': cat['items'],
})
model_meta = next((m for m in overview['models'] if m['folder'] == folder), None)
category_series.append({
'folder': folder,
'label': model_meta['model_name'] if model_meta else folder,
'display': folder,
'scores': scores,
'details': details,
})
category_ranking = []
for i, cat in enumerate(active_categories):
rows = []
for s in category_series:
if s['scores'][i] is not None:
rows.append({
'folder': s['folder'],
'label': s['label'],
'score': s['scores'][i],
'n_benchmarks': s['details'][i]['n_benchmarks'],
})
rows.sort(key=lambda x: x['score'], reverse=True)
category_ranking.append({
'category': cat['name'],
'id': cat['id'],
'items': cat['items'],
'rows': rows,
})
return {
'output_dir': overview['output_dir'],
'benchmarks': bench_list,
'categories': overview['categories'],
'models': [m for m in overview['models'] if m['folder'] in selected],
'series': series,
'ranking': ranking,
'category_labels': category_labels,
'category_series': category_series,
'category_ranking': category_ranking,
'matrix': {
f: {b: overview['matrix'].get(f, {}).get(b) for b in bench_list}
for f in selected
},
}