Files
wehub-resource-sync c3749daf48
Tests / test-linux (3.13) (push) Failing after 0s
Tests / test-linux (3.11) (push) Failing after 1s
Tests / lint (push) Failing after 0s
Tests / test-linux (3.9) (push) Failing after 1s
Docker / build (push) Failing after 1s
Docker / build-gpu (push) Failing after 2s
Tests / test-windows (push) Has been cancelled
Tests / test-macos (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:03 +08:00

336 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Render a benchmark CSV as a readable markdown report.
Usage:
python -m benchmarks.model_eval.summarize \\
--csv benchmarks/model_eval/results/2026-05-10-host.csv \\
--output benchmarks/model_eval/reports/2026-05-10-host.md
The report includes:
- Per-task accuracy rankings
- Per-task speed rankings (e2e p50, TPS p50)
- VRAM consumption table
- Combined production recommendation (accuracy ≥ 0.8 AND e2e_p50 < 500ms)
- Open-set viability (does any model meet the discover-mode threshold?)
- Instruct vs reasoning comparison for qwen3:4b pair
"""
from __future__ import annotations
import argparse
import csv
import json
from collections import defaultdict
from pathlib import Path
from typing import Optional
def load_rows(path: Path) -> list[dict]:
with open(path, "r", encoding="utf-8", newline="") as f:
return list(csv.DictReader(f))
def fmt(v, decimals: int = 2, default: str = "—") -> str:
if v is None or v == "":
return default
try:
f = float(v)
return f"{f:.{decimals}f}"
except (ValueError, TypeError):
return str(v)
def rank_by(rows: list[dict], task: str, mode: str, key: str, reverse: bool = True) -> list[dict]:
filtered = [r for r in rows if r["task"] == task and r["mode"] == mode and not r.get("error")]
return sorted(filtered, key=lambda r: float(r.get(key) or 0), reverse=reverse)
def render_accuracy_table(rows: list[dict], task: str, mode: str, primary_key: str = "accuracy") -> str:
ranked = rank_by(rows, task, mode, primary_key, reverse=True)
if not ranked:
return f"_No successful runs for {task}/{mode}._\n"
lines = []
extras_keys = sorted({k for r in ranked for k in json.loads(r.get("extras_json") or "{}").keys()})
extras_keys = [k for k in extras_keys if k not in {"correct", "total", "error_count", "error_sample"}][:4]
header = ["Rank", "Model", primary_key]
if extras_keys:
header.extend(extras_keys)
header.extend(["e2e p50 ms", "TPS p50", "VRAM resident MB"])
lines.append("| " + " | ".join(header) + " |")
lines.append("|" + "|".join(["---"] * len(header)) + "|")
for i, r in enumerate(ranked, 1):
extras = json.loads(r.get("extras_json") or "{}")
row = [str(i), r["model_tag"], fmt(r[primary_key], 3)]
for k in extras_keys:
row.append(fmt(extras.get(k), 2))
row.extend([fmt(r.get("e2e_p50_ms"), 1), fmt(r.get("tps_p50"), 1), r.get("vram_resident_mb") or "—"])
lines.append("| " + " | ".join(row) + " |")
return "\n".join(lines) + "\n"
def render_speed_table(rows: list[dict], task: str, mode: str) -> str:
valid = [r for r in rows if r["task"] == task and r["mode"] == mode and not r.get("error") and r.get("e2e_p50_ms")]
ranked = sorted(valid, key=lambda r: float(r.get("e2e_p50_ms") or 99999))
if not ranked:
return ""
lines = ["| Rank | Model | e2e p50 ms | e2e p95 ms | TTFT p50 ms | TPS p50 | TPS p95 |",
"|---|---|---|---|---|---|---|"]
for i, r in enumerate(ranked, 1):
lines.append(
f"| {i} | {r['model_tag']} | "
f"{fmt(r['e2e_p50_ms'], 1)} | {fmt(r['e2e_p95_ms'], 1)} | "
f"{fmt(r['ttft_p50_ms'], 1)} | {fmt(r['tps_p50'], 1)} | {fmt(r['tps_p95'], 1)} |"
)
return "\n".join(lines) + "\n"
def render_vram_table(rows: list[dict]) -> str:
by_model: dict[str, dict] = {}
for r in rows:
if r.get("error"):
continue
tag = r["model_tag"]
if tag not in by_model:
by_model[tag] = {"resident": r.get("vram_resident_mb"), "peak": r.get("vram_peak_mb")}
else:
cur_peak = by_model[tag]["peak"]
new_peak = r.get("vram_peak_mb")
if new_peak and (not cur_peak or int(new_peak) > int(cur_peak)):
by_model[tag]["peak"] = new_peak
if not by_model[tag]["resident"] and r.get("vram_resident_mb"):
by_model[tag]["resident"] = r.get("vram_resident_mb")
rows_sorted = sorted(
by_model.items(),
key=lambda kv: int(kv[1]["resident"] or 0),
)
lines = ["| Model | Resident MB | Peak MB | Delta MB |", "|---|---|---|---|"]
for tag, vram in rows_sorted:
resident = vram["resident"]
peak = vram["peak"]
try:
delta = int(peak) - int(resident) if resident and peak else None
except (TypeError, ValueError):
delta = None
lines.append(f"| {tag} | {resident or '—'} | {peak or '—'} | {delta if delta is not None else '—'} |")
return "\n".join(lines) + "\n"
def render_production_picks(rows: list[dict], min_acc: float = 0.80, max_e2e_ms: float = 500) -> str:
"""Models that meet a quality threshold AND a speed threshold across all tasks."""
by_model: dict[str, list[dict]] = defaultdict(list)
for r in rows:
if r.get("error"):
continue
by_model[r["model_tag"]].append(r)
picks = []
for tag, model_rows in by_model.items():
primary_metrics = []
e2e_max = 0.0
for r in model_rows:
if r["task"] == "memory_extraction":
# coverage is the primary; mean_coverage is in extras
extras = json.loads(r.get("extras_json") or "{}")
primary_metrics.append(extras.get("mean_coverage", 0.0))
elif r["task"] == "entity_extraction":
primary_metrics.append(float(r.get("accuracy") or 0))
elif r["task"] == "room_classification" and r["mode"] == "open":
# similarity is the primary
primary_metrics.append(float(r.get("accuracy") or 0))
else:
primary_metrics.append(float(r.get("accuracy") or 0))
e2e = float(r.get("e2e_p50_ms") or 0)
if e2e > e2e_max:
e2e_max = e2e
if not primary_metrics:
continue
avg_metric = sum(primary_metrics) / len(primary_metrics)
meets_acc = all(m >= min_acc for m in primary_metrics)
meets_speed = e2e_max <= max_e2e_ms
if meets_acc and meets_speed:
picks.append((tag, avg_metric, e2e_max))
picks.sort(key=lambda x: -x[1])
if not picks:
return f"_No model met both thresholds (min_acc={min_acc}, max_e2e={max_e2e_ms}ms across all tasks)._\n"
lines = [f"Models meeting min_accuracy ≥ {min_acc} on every task AND e2e p50 ≤ {max_e2e_ms}ms:\n"]
lines.append("| Rank | Model | Avg primary metric | Worst e2e p50 ms |")
lines.append("|---|---|---|---|")
for i, (tag, avg, e2e) in enumerate(picks, 1):
lines.append(f"| {i} | {tag} | {avg:.3f} | {e2e:.1f} |")
return "\n".join(lines) + "\n"
def render_instruct_vs_reasoning(rows: list[dict]) -> str:
instruct = [r for r in rows if r["model_tag"].startswith("qwen3:4b-instruct-2507") and not r.get("error")]
reasoning = [r for r in rows if r["model_tag"].startswith("qwen3:4b-thinking") and not r.get("error")]
if not instruct or not reasoning:
return "_Not enough qwen3:4b paired results to compare._\n"
inst_q4 = next((r for r in instruct if r["model_tag"] == "qwen3:4b-instruct-2507-q4_K_M"), None)
reas_q4 = next((r for r in reasoning if r["model_tag"] == "qwen3:4b-thinking-2507-q4_K_M"), None)
if not inst_q4 or not reas_q4:
return "_qwen3:4b-instruct-2507-q4_K_M vs qwen3:4b-thinking-2507-q4_K_M comparison unavailable._\n"
by_pair: dict[tuple[str, str], dict[str, dict]] = defaultdict(dict)
for r in instruct + reasoning:
if r["model_tag"] not in {"qwen3:4b-instruct-2507-q4_K_M", "qwen3:4b-thinking-2507-q4_K_M"}:
continue
by_pair[(r["task"], r["mode"])][r["model_tag"]] = r
lines = ["Direct comparison: qwen3:4b instruct vs reasoning at q4_K_M.\n"]
lines.append("| Task | Mode | Instruct accuracy | Reasoning accuracy | Instruct e2e p50 | Reasoning e2e p50 |")
lines.append("|---|---|---|---|---|---|")
for (task, mode), pair in sorted(by_pair.items()):
inst = pair.get("qwen3:4b-instruct-2507-q4_K_M")
reas = pair.get("qwen3:4b-thinking-2507-q4_K_M")
if not inst or not reas:
continue
lines.append(
f"| {task} | {mode} | "
f"{fmt(inst['accuracy'], 3)} | {fmt(reas['accuracy'], 3)} | "
f"{fmt(inst['e2e_p50_ms'], 1)} | {fmt(reas['e2e_p50_ms'], 1)} |"
)
return "\n".join(lines) + "\n"
def render_open_set_viability(rows: list[dict], min_similarity: float = 0.7) -> str:
open_runs = [r for r in rows if r["task"] == "room_classification" and r["mode"] == "open" and not r.get("error")]
if not open_runs:
return "_No open-set runs available._\n"
qualified = [r for r in open_runs if float(r.get("accuracy") or 0) >= min_similarity]
lines = [f"Open-set discovery viability. Threshold: mean cosine similarity ≥ {min_similarity}.\n"]
if not qualified:
lines.append(f"**No model met the threshold.** Best score: {max(float(r.get('accuracy') or 0) for r in open_runs):.3f}.")
lines.append("\nRecommendation: do NOT ship `mempalace mine --mode discover`. Closed-set classification stays required.\n")
return "\n".join(lines)
lines.append(f"**{len(qualified)} model(s) met the threshold.**\n")
lines.append("| Model | Mean similarity | Exact match count | High-sim (≥0.8) | Low-sim (<0.5) |")
lines.append("|---|---|---|---|---|")
for r in sorted(qualified, key=lambda r: -float(r.get("accuracy") or 0)):
extras = json.loads(r.get("extras_json") or "{}")
lines.append(
f"| {r['model_tag']} | "
f"{fmt(r['accuracy'], 3)} | "
f"{extras.get('exact_match_count', '—')} | "
f"{extras.get('high_similarity_count', '—')} | "
f"{extras.get('low_similarity_count', '—')} |"
)
lines.append(f"\nRecommendation: ship `mempalace mine --mode discover` with the top model as default.\n")
return "\n".join(lines)
def render_report(rows: list[dict]) -> str:
if not rows:
return "# Empty results\n\nNo rows in the CSV.\n"
sample = next((r for r in rows if not r.get("error")), rows[0])
host = sample.get("host", "unknown")
gpu = sample.get("gpu", "unknown")
ollama_v = sample.get("ollama_version", "unknown")
run_date = sample.get("run_date", "unknown")
n_models = len({r["model_tag"] for r in rows})
n_runs = len(rows)
n_errors = sum(1 for r in rows if r.get("error"))
sections = [
f"# Model evaluation report",
"",
f"- Host: `{host}`",
f"- GPU: `{gpu}`",
f"- Ollama: `{ollama_v}`",
f"- Run date: {run_date}",
f"- Runs: {n_runs} ({n_models} models × task/mode pairs); errors: {n_errors}",
"",
"## Production picks",
"",
render_production_picks(rows),
"",
"## Open-set discovery viability",
"",
render_open_set_viability(rows),
"",
"## Instruct vs reasoning (qwen3:4b)",
"",
render_instruct_vs_reasoning(rows),
"",
"## Per-task rankings",
"",
"### Calibration (sentence-type, exact match)",
"",
render_accuracy_table(rows, "calibration", "default"),
"",
"### Room classification — closed-set (exact match)",
"",
render_accuracy_table(rows, "room_classification", "closed"),
"",
"### Room classification — open-set (cosine similarity)",
"",
render_accuracy_table(rows, "room_classification", "open"),
"",
"### Entity extraction (mean F1)",
"",
render_accuracy_table(rows, "entity_extraction", "default"),
"",
"### Memory extraction (mean coverage)",
"",
render_accuracy_table(rows, "memory_extraction", "default"),
"",
"## Speed (calibration, smallest task — most stable timing)",
"",
render_speed_table(rows, "calibration", "default"),
"",
"## VRAM",
"",
render_vram_table(rows),
"",
]
errored = [r for r in rows if r.get("error")]
if errored:
sections.extend([
"## Errors",
"",
"| Model | Task | Mode | Error |",
"|---|---|---|---|",
])
for r in errored:
err_short = r["error"][:120].replace("|", "\\|")
sections.append(f"| {r['model_tag']} | {r['task']} | {r['mode']} | {err_short} |")
sections.append("")
return "\n".join(sections)
def main():
parser = argparse.ArgumentParser(description="Render benchmark CSV as a markdown report")
parser.add_argument("--csv", required=True, type=Path)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
rows = load_rows(args.csv)
report = render_report(rows)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8", newline="\n") as f:
f.write(report)
print(f"Wrote {args.output}")
else:
print(report)
if __name__ == "__main__":
main()