Unified, nicer result tables + conda env setup in README
- cli: rich Run Summary table for multi-benchmark runs (green/red rows, fallback to aligned plain text); unified _fmt_score (fractions render as percentages everywhere -- was 1.0 in summary vs 100.0% in detail); fix the stray "summary csv -> None/viz/..." print without --out-dir; summary.md upgraded to a proper table with model/timestamp/ok-count header -- one table for a whole N-benchmark run - text renderer: single-bench headline deduped (dataset==recipe) and compacted to one facts line; adaptive metric-name column (long names no longer break alignment) - md_compare: auto-switches to one-row-per-benchmark when comparing different benchmarks with different metrics; same-bench model comparison gains baseline delta markers (+/- percentage points) - README: conda create/activate in the install block Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
27cf8b3c7e
commit
ea93602dfa
16
README.md
16
README.md
@ -28,24 +28,17 @@
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
要求 Python ≥ 3.10。
|
要求 Python ≥ 3.10,一条命令装完即可跑全部 28 个 benchmark,无任何可选依赖
|
||||||
|
(代码执行类需要宿主机有 Docker,镜像判分时自动拉取)。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.meta-stone.net/sora/EvalHarness.git
|
git clone https://git.meta-stone.net/sora/EvalHarness.git
|
||||||
cd EvalHarness
|
cd EvalHarness
|
||||||
|
conda create -n evalharness python=3.10 -y
|
||||||
|
conda activate evalharness
|
||||||
pip install .
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
**一条命令装完即可跑全部 28 个 benchmark,没有任何可选依赖。**
|
|
||||||
|
|
||||||
| 官方判分逻辑形态 | 涉及 benchmark | 位置 |
|
|
||||||
|---|---|---|
|
|
||||||
| 纯 Python 算法 | 数学、DROP、MCQ | 核心依赖(sympy/numpy/scipy) |
|
|
||||||
| BFCL 官方 AST 判定器 | bfcl_v3 | 已内置(`evalharness/third_party/bfcl/`,Apache-2.0) |
|
|
||||||
| 官方执行环境 | humaneval、bigcodebench、live_code_bench、swe_bench | Docker 镜像,判分时按需拉取 |
|
|
||||||
|
|
||||||
另有本地引擎类(`tau2_bench`)使用官方 tau2 包(本地源码安装,无重依赖)。
|
|
||||||
|
|
||||||
离线验证安装(不需要模型、不联网):
|
离线验证安装(不需要模型、不联网):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -54,7 +47,6 @@ evalharness eval run gsm8k --model mock:boxed --limit 8
|
|||||||
# 数据 → prompt → 生成 → 判分 → 报告 全链路可用
|
# 数据 → prompt → 生成 → 判分 → 报告 全链路可用
|
||||||
```
|
```
|
||||||
|
|
||||||
代码执行类 benchmark 需要宿主机有 Docker。
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|||||||
@ -332,14 +332,7 @@ def _cmd_eval_run(args) -> int:
|
|||||||
'failed', _time.time() - t0)
|
'failed', _time.time() - t0)
|
||||||
|
|
||||||
if len(rows) > 1:
|
if len(rows) > 1:
|
||||||
print(f'\n{"benchmark":<20} {"metric":<14} {"score":>8} {"n":>5} {"time":>9}')
|
_print_summary_table(console, rows)
|
||||||
print('-' * 62)
|
|
||||||
for r in rows:
|
|
||||||
val = 'ERR' if not r['ok'] else _f3(r['value'])
|
|
||||||
h = r.get('hours') or 0
|
|
||||||
tdisp = f'{h:.2f}h' if h >= 0.995 else f"{r.get('secs', 0):.0f}s"
|
|
||||||
print(f"{r['name']:<20} {r['metric']:<14} {val!s:>8} {r.get('n', '')!s:>5} {tdisp:>9}"
|
|
||||||
+ (f" {r.get('err', '')}" if not r['ok'] else ''))
|
|
||||||
ok = sum(1 for r in rows if r['ok'])
|
ok = sum(1 for r in rows if r['ok'])
|
||||||
print(f'\n{ok}/{len(rows)} ok' + (f' -> artifacts in {out_dir}/' if out_dir else ''))
|
print(f'\n{ok}/{len(rows)} ok' + (f' -> artifacts in {out_dir}/' if out_dir else ''))
|
||||||
if out_dir:
|
if out_dir:
|
||||||
@ -371,13 +364,65 @@ def _cmd_eval_run(args) -> int:
|
|||||||
'tpot_mean_s', 'tpot_p90_s', 'tpot_p99_s')] +
|
'tpot_mean_s', 'tpot_p90_s', 'tpot_p99_s')] +
|
||||||
[cats])
|
[cats])
|
||||||
with open(f'{out_dir}/viz/summary.md', 'w', encoding='utf-8') as f:
|
with open(f'{out_dir}/viz/summary.md', 'w', encoding='utf-8') as f:
|
||||||
f.write(f'# eval run summary\n\n| dataset | metric | value | secs |\n|---|---|---|---|\n')
|
import time as _tt
|
||||||
|
model_names = {r.get('model', '') for r in rows if r.get('model')}
|
||||||
|
head = f"# eval run summary\n\n- model: {', '.join(model_names) or '?'}\n"
|
||||||
|
head += f"- created: {_tt.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||||
|
head += f"- benchmarks: {sum(1 for r in rows if r['ok'])}/{len(rows)} ok\n\n"
|
||||||
|
f.write(head)
|
||||||
|
f.write('| benchmark | metric | score | n | time | status |\n'
|
||||||
|
'|---|---|---:|---:|---:|---|\n')
|
||||||
for r in rows:
|
for r in rows:
|
||||||
f.write(f"| {r['name']} | {r['metric']} | {r['value']} | {r['secs']} |\n")
|
v = _fmt_score(r.get('value'))
|
||||||
print(f'summary csv -> {out_dir}/viz/summary.csv')
|
h = r.get('hours') or 0
|
||||||
|
t = f'{h:.2f}h' if h >= 0.995 else f"{r.get('secs', 0):.0f}s"
|
||||||
|
st = 'ok' if r['ok'] else f"failed: {r.get('err', '')[:60]}"
|
||||||
|
f.write(f"| {r['name']} | {r['metric']} | {v} | "
|
||||||
|
f"{r.get('n', '')} | {t} | {st} |\n")
|
||||||
|
if out_dir:
|
||||||
|
print(f'summary -> {out_dir}/viz/summary.md (+ summary.csv)')
|
||||||
return 0 if all(r['ok'] for r in rows) else 1
|
return 0 if all(r['ok'] for r in rows) else 1
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_score(v):
|
||||||
|
"""Unified score format: fractions render as percentages everywhere."""
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 'ERR'
|
||||||
|
return f'{v * 100:.1f}%' if 0.0 <= v <= 1.0 else f'{v:g}'
|
||||||
|
|
||||||
|
|
||||||
|
def _print_summary_table(console, rows):
|
||||||
|
"""Rich multi-benchmark summary (falls back to aligned plain text)."""
|
||||||
|
if console is not None:
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
t = Table(title='Run Summary', header_style='bold cyan',
|
||||||
|
title_style='bold', expand=False)
|
||||||
|
for col, just in (('benchmark', 'left'), ('metric', 'left'),
|
||||||
|
('score', 'right'), ('n', 'right'), ('time', 'right')):
|
||||||
|
t.add_column(col, justify=just)
|
||||||
|
for r in rows:
|
||||||
|
v = _fmt_score(r.get('value')) if r['ok'] else 'ERR'
|
||||||
|
h = r.get('hours') or 0
|
||||||
|
tm = f'{h:.2f}h' if h >= 0.995 else f"{r.get('secs', 0):.0f}s"
|
||||||
|
style = 'green' if r['ok'] else 'red'
|
||||||
|
t.add_row(r['name'], r['metric'], v, str(r.get('n', '')), tm,
|
||||||
|
style=style)
|
||||||
|
console.print(t)
|
||||||
|
return
|
||||||
|
print(f'\n{"benchmark":<20} {"metric":<16} {"score":>8} {"n":>6} {"time":>8}')
|
||||||
|
print('-' * 64)
|
||||||
|
for r in rows:
|
||||||
|
v = _fmt_score(r.get('value')) if r['ok'] else 'ERR'
|
||||||
|
h = r.get('hours') or 0
|
||||||
|
tm = f'{h:.2f}h' if h >= 0.995 else f"{r.get('secs', 0):.0f}s"
|
||||||
|
err = f" {r.get('err', '')}" if not r['ok'] else ''
|
||||||
|
print(f"{r['name']:<20} {r['metric']:<16} {v:>8} "
|
||||||
|
f"{str(r.get('n', '')):>6} {tm:>8}{err}")
|
||||||
|
|
||||||
|
|
||||||
def _f3(v):
|
def _f3(v):
|
||||||
try:
|
try:
|
||||||
return round(float(v), 4)
|
return round(float(v), 4)
|
||||||
|
|||||||
@ -21,37 +21,42 @@ def text_table(target: Union[EvalReport, List[EvalReport]], opts: Dict) -> str:
|
|||||||
reports = target if isinstance(target, list) else [target]
|
reports = target if isinstance(target, list) else [target]
|
||||||
out: List[str] = []
|
out: List[str] = []
|
||||||
for rep in reports:
|
for rep in reports:
|
||||||
head = f'{rep.dataset} [{rep.recipe}] model={rep.model or "?"} n={rep.num_samples}'
|
# headline: dedupe dataset/recipe when identical; join facts compactly
|
||||||
out.append('=' * max(len(head), 40))
|
title = rep.dataset if rep.dataset == rep.recipe else f'{rep.dataset} [{rep.recipe}]'
|
||||||
|
facts = [f'model={rep.model or "?"}', f'n={rep.num_samples}']
|
||||||
|
info = rep.metric_groups.get('run_info', {}) or {}
|
||||||
|
secs = sum(float((s.usage or {}).get('latency_s', 0) or 0) for s in rep.samples)
|
||||||
|
if secs >= 3600:
|
||||||
|
facts.append(f'time={secs / 3600:.2f}h')
|
||||||
|
elif secs:
|
||||||
|
facts.append(f'time={secs:.0f}s')
|
||||||
|
if info.get('gen_total_tokens'):
|
||||||
|
facts.append(f'tokens={info["gen_total_tokens"]}')
|
||||||
|
head = f'{title} · ' + ' '.join(facts)
|
||||||
out.append(head)
|
out.append(head)
|
||||||
out.append('=' * max(len(head), 40))
|
out.append('=' * max(len(head), 40))
|
||||||
# run stats: duration, tokens, cost (from run_info + usage aggregates)
|
|
||||||
info = rep.metric_groups.get('run_info', {}) or {}
|
|
||||||
total_tokens = info.get('gen_total_tokens', 0)
|
|
||||||
tok_s = ''
|
|
||||||
if total_tokens:
|
|
||||||
tok_s = f' tokens={total_tokens}'
|
|
||||||
secs = 0.0
|
|
||||||
for s in rep.samples:
|
|
||||||
secs += float((s.usage or {}).get('latency_s', 0) or 0)
|
|
||||||
dur = f' time={secs / 3600:.2f}h' if secs >= 3600 else (f' time={secs:.0f}s' if secs else '')
|
|
||||||
if dur or tok_s:
|
|
||||||
out.append(f'n_samples={rep.num_samples}{dur}{tok_s}')
|
|
||||||
if rep.num_failed_extractions:
|
if rep.num_failed_extractions:
|
||||||
warn = (f'!! {rep.num_failed_extractions}/{rep.num_samples} extractions failed '
|
warn = (f'!! {rep.num_failed_extractions}/{rep.num_samples} extractions failed '
|
||||||
f'({_pct(rep.metrics.get("extraction_failure_rate", 0))}) -- check recipe/model fit')
|
f'({_pct(rep.metrics.get("extraction_failure_rate", 0))}) -- check recipe/model fit')
|
||||||
out.append(warn)
|
out.append(warn)
|
||||||
for metric, value in rep.metrics.items():
|
|
||||||
if metric == 'extraction_failure_rate':
|
metrics = [(m, v) for m, v in rep.metrics.items()
|
||||||
continue
|
if m != 'extraction_failure_rate' and isinstance(v, (int, float))]
|
||||||
out.append(f'{metric:<16} {_pct(value):>7} {_bars(value)}')
|
w = max([len(m) for m, _ in metrics] + [12]) # adaptive, long names survive
|
||||||
|
for metric, value in metrics:
|
||||||
|
out.append(f'{metric:<{w}} {_pct(value):>8} {_bars(value)}')
|
||||||
|
|
||||||
for group_name, groups in rep.metric_groups.items():
|
for group_name, groups in rep.metric_groups.items():
|
||||||
if group_name == 'run_info' or group_name.startswith('agg_error'):
|
if group_name == 'run_info' or group_name.startswith('agg_error'):
|
||||||
continue
|
continue
|
||||||
|
numeric = {g: v for g, v in groups.items() if isinstance(v, (int, float))}
|
||||||
|
if not numeric:
|
||||||
|
continue
|
||||||
|
gw = max([len(str(g)) for g in numeric] + [12])
|
||||||
out.append(f'-- {group_name} ' + '-' * max(0, 30 - len(group_name)))
|
out.append(f'-- {group_name} ' + '-' * max(0, 30 - len(group_name)))
|
||||||
for g, v in groups.items():
|
for g, v in numeric.items():
|
||||||
if isinstance(v, (int, float)):
|
out.append(f' {str(g):<{gw}} {_pct(v):>8} {_bars(v, 20)}')
|
||||||
out.append(f' {g:<28} {_pct(v):>7} {_bars(v, 20)}')
|
|
||||||
out.append('')
|
out.append('')
|
||||||
return '\n'.join(out)
|
return '\n'.join(out)
|
||||||
|
|
||||||
@ -79,22 +84,58 @@ def markdown(target: Union[EvalReport, List[EvalReport]], opts: Dict) -> str:
|
|||||||
|
|
||||||
@register_renderer('md_compare')
|
@register_renderer('md_compare')
|
||||||
def md_compare(target: List[EvalReport], opts: Dict) -> str:
|
def md_compare(target: List[EvalReport], opts: Dict) -> str:
|
||||||
"""Side-by-side metric table for N reports (e.g. two models on one bench)."""
|
"""Side-by-side metric table for N reports (models on one bench, or many
|
||||||
|
benches of one model -- the shape is the same: one row per benchmark).
|
||||||
|
The first report is the baseline; later columns get a delta marker."""
|
||||||
if not isinstance(target, list) or len(target) < 1:
|
if not isinstance(target, list) or len(target) < 1:
|
||||||
raise ValueError('md_compare needs a list of reports')
|
raise ValueError('md_compare needs a list of reports')
|
||||||
|
|
||||||
|
# many DIFFERENT benchmarks, each with its own primary metric: a compact
|
||||||
|
# one-row-per-bench table beats a sparse metric-x-bench grid
|
||||||
|
datasets = {r.dataset for r in target}
|
||||||
|
primary_metrics = {next((m for m in r.metrics if m != 'extraction_failure_rate'), '')
|
||||||
|
for r in target}
|
||||||
|
if len(datasets) > 1 and len(target) == len(datasets) and len(primary_metrics) > 1:
|
||||||
|
out = ['# Comparison', '', '| benchmark | metric | score | n |', '|---|---|---:|---:|']
|
||||||
|
best = max((next((v for m, v in r.metrics.items()
|
||||||
|
if m != 'extraction_failure_rate'), 0.0) for r in target))
|
||||||
|
for r in target:
|
||||||
|
m = next((m for m in r.metrics if m != 'extraction_failure_rate'), '')
|
||||||
|
v = r.metrics.get(m, 0.0)
|
||||||
|
cell = f'**{_pct(v)}**' if v == best and len(target) > 1 else _pct(v)
|
||||||
|
out.append(f'| {r.dataset} | {m} | {cell} | {r.num_samples} |')
|
||||||
|
return '\n'.join(out + [''])
|
||||||
|
|
||||||
metrics: List[str] = []
|
metrics: List[str] = []
|
||||||
for rep in target:
|
for rep in target:
|
||||||
for m in rep.metrics:
|
for m in rep.metrics:
|
||||||
if m not in metrics and m != 'extraction_failure_rate':
|
if m not in metrics and m != 'extraction_failure_rate':
|
||||||
metrics.append(m)
|
metrics.append(m)
|
||||||
cols = [f'{rep.dataset}/{rep.recipe}[{rep.model or "?"}]' for rep in target]
|
cols = []
|
||||||
out = ['# Comparison', '', '| metric | ' + ' | '.join(cols) + ' |',
|
for rep in target:
|
||||||
|
# models on one bench -> show the model; many benches -> show the bench
|
||||||
|
same_dataset = len({r.dataset for r in target}) == 1
|
||||||
|
name = rep.model or '?' if same_dataset else rep.dataset
|
||||||
|
cols.append(name)
|
||||||
|
out = ['# Comparison', '',
|
||||||
|
'| metric | ' + ' | '.join(cols) + ' |',
|
||||||
'|---' * (len(cols) + 1) + '|']
|
'|---' * (len(cols) + 1) + '|']
|
||||||
for m in metrics:
|
for m in metrics:
|
||||||
cells = [_pct(rep.metrics.get(m, 0.0)) for rep in target]
|
cells = []
|
||||||
best = max(rep.metrics.get(m, 0.0) for rep in target)
|
base = target[0].metrics.get(m, 0.0)
|
||||||
cells = [f'**{c}**' if rep.metrics.get(m, 0.0) == best and len(target) > 1 else c
|
for rep, col_i in zip(target, range(len(target))):
|
||||||
for c, rep in zip(cells, target)]
|
v = rep.metrics.get(m, 0.0)
|
||||||
|
cell = _pct(v)
|
||||||
|
best = max(r.metrics.get(m, 0.0) for r in target)
|
||||||
|
if v == best and len(target) > 1:
|
||||||
|
cell = f'**{cell}**'
|
||||||
|
if rep is not target[0] and isinstance(base, (int, float)):
|
||||||
|
d = v - base
|
||||||
|
if d > 0.0005:
|
||||||
|
cell += f' ▲{d * 100:+.1f}'
|
||||||
|
elif d < -0.0005:
|
||||||
|
cell += f' ▼{d * 100:+.1f}'
|
||||||
|
cells.append(cell)
|
||||||
out.append(f'| {m} | ' + ' | '.join(cells) + ' |')
|
out.append(f'| {m} | ' + ' | '.join(cells) + ' |')
|
||||||
out.append('')
|
out.append('')
|
||||||
return '\n'.join(out)
|
return '\n'.join(out)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user