Files
Karti Tripathi 006feee0f7
CI / verify (push) Successful in 24s
CI / deploy (push) Failing after 1m14s
Lumbridge Bench
2026-08-04 00:44:07 -07:00

255 lines
10 KiB
Python

"""Result schema.
One run produces one JSON file in results/, committed to git. The commit
history IS the longitudinal record -- there is no database to migrate or rot.
Two properties of this schema are load-bearing and expensive to retrofit:
1. PER-SAMPLE OUTCOMES, not just aggregates. An aggregate tells you a
checkpoint got worse. Per-sample diffs tell you *which capability broke*.
That difference is what separates a leaderboard from a debugging tool, and
it is the entire reason this bench is useful for fine-tuning.
2. THE FULL TARGET IS SNAPSHOTTED INTO THE RESULT. Registry entries change.
A result that merely references a target id becomes unreproducible the
first time someone edits models.yaml. Every run carries its own copy of
the serving flags it actually ran against.
Output storage policy: we store the score plus a short excerpt per sample.
Full model outputs stay in the raw Inspect .eval log, which is gitignored --
they are large, and for signal-tier tasks they can echo sensitive content
from the prompts they were derived from. Deep debugging uses the local log;
git carries the shape of the failure, not the whole transcript.
"""
from __future__ import annotations
import json
import platform
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
SCHEMA_VERSION = 1
RESULTS_DIR = Path(__file__).resolve().parent.parent / "results"
def _json_safe(value: Any) -> Any:
"""Map non-finite floats to None, recursively.
Python's json emits bare `NaN`/`Infinity`, which its own loader accepts and almost
nothing else does — JSON.parse, Go, and serde all reject them. A score card is meant
to be a portable, durable record, so a card that only Python can read is a broken one.
An ungraded sample surfaced as NaN here and made the first committed card unparseable
by the website that has to render it.
None means "no score", which is exactly what a non-finite score meant anyway.
"""
if isinstance(value, float):
# NaN != NaN, and inf fails the range check; both are unrepresentable in JSON.
return None if value != value or value in (float("inf"), float("-inf")) else value
if isinstance(value, dict):
return {k: _json_safe(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_json_safe(v) for v in value]
return value
# How much of each model output to keep in the committed record.
EXCERPT_CHARS = 400
def utc_now() -> str:
return datetime.now(UTC).isoformat(timespec="seconds")
@dataclass
class SampleOutcome:
"""One sample's result. The unit of regression debugging."""
sample_id: str
score: float
passed: bool
excerpt: str | None = None # truncated model output; full text in .eval log
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass
class QualityResult:
"""One task's scores against one target."""
task: str
tier: str # "reference" (public, contaminated, calibration only) | "signal"
dataset_version: str | None = None
# Content hash of the samples actually run. dataset_version is inspect's static
# task_version and does not move when the data changes; this does. Two cards are
# only comparable if their fingerprints match.
dataset_fingerprint: str | None = None
n_samples: int = 0
metrics: dict[str, float] = field(default_factory=dict)
samples: list[SampleOutcome] = field(default_factory=list)
duration_s: float | None = None
error: str | None = None
@dataclass
class PerfPoint:
"""Serving performance at one concurrency level."""
concurrency: int
input_tokens: int
output_tokens: int
n_requests: int
completed: int
failed: int
duration_s: float
# Aggregate throughput across all streams -- the number that matters for
# multi-user serving, and the one single-stream reviews miss entirely.
output_tps_total: float
# Per-stream decode rate -- what a single user actually feels.
output_tps_per_stream: float
ttft_p50_ms: float | None = None
ttft_p95_ms: float | None = None
tpot_p50_ms: float | None = None # time per output token, inter-token latency
prefill_tps: float | None = None
# Speculative decoding, when the engine reports it. Throughput alone cannot say whether
# drafting is working: a draft rejected almost every time still emits tokens, having paid
# for both passes. `None` means the engine exposed no counters, not that the rate was zero.
spec_acceptance_rate: float | None = None
spec_draft_tokens: int | None = None
# True when output length was NOT pinned with ignore_eos. Comparability across points is
# weaker in that mode, and a reader has to know which they are looking at.
natural_stop: bool = False
output_token_stdev: float | None = None
error: str | None = None
@dataclass
class PerfResult:
engine: str
points: list[PerfPoint] = field(default_factory=list)
peak_memory_gb: float | None = None
idle_memory_gb: float | None = None
notes: str | None = None
@dataclass
class Run:
"""A complete score card: one target, one point in time."""
run_id: str
timestamp: str
target_id: str
target: dict[str, Any] # full snapshot -- see module docstring
host: dict[str, Any]
quality: list[QualityResult] = field(default_factory=list)
perf: PerfResult | None = None
verdict: dict[str, Any] = field(default_factory=dict)
runner: dict[str, Any] = field(default_factory=dict)
# How the model was sampled. Two cards are only comparable if this matches: a score
# taken at temperature 1 is a draw from a distribution, and differs run to run by more
# than most real regressions do. Recorded rather than assumed, because the quality half
# sampled unpinned for its entire life and nothing in the card said so.
sampling: dict[str, Any] = field(default_factory=dict)
schema_version: int = SCHEMA_VERSION
# ---------------------------------------------------------------- io --
@property
def filename(self) -> str:
date = self.timestamp[:10]
slug = self.target.get("slug") or self.target_id.replace("@", "__")
return f"{date}__{slug}__{self.run_id[:8]}.json"
def to_dict(self) -> dict[str, Any]:
return _json_safe(asdict(self))
def save(self, results_dir: Path | None = None) -> Path:
d = results_dir or RESULTS_DIR
d.mkdir(parents=True, exist_ok=True)
path = d / self.filename
# allow_nan=False so this raises instead of emitting a token that is not JSON.
# to_dict has already mapped the non-finite values to null; if a new one appears,
# the write should fail loudly rather than commit an unparseable score card.
path.write_text(
json.dumps(self.to_dict(), indent=2, sort_keys=False, allow_nan=False) + "\n"
)
return path
@staticmethod
def load(path: Path) -> dict[str, Any]:
return json.loads(Path(path).read_text())
def runner_info() -> dict[str, Any]:
"""Provenance of the machine that drove the benchmark (not the one serving)."""
return {
"host": platform.node(),
"python": platform.python_version(),
"platform": platform.platform(),
}
def truncate(text: str | None, limit: int = EXCERPT_CHARS) -> str | None:
if text is None:
return None
text = text.strip()
return text if len(text) <= limit else text[:limit] + f"... [+{len(text) - limit} chars]"
def compute_verdict(quality: list[QualityResult], perf: PerfResult | None) -> dict[str, Any]:
"""Derive a self-hosting verdict from the raw numbers.
Deliberately simple and explicit rather than clever. The thresholds encode
what makes a model usable in our own stack, and they live in code (and
therefore in git history) so that a changed verdict is always traceable to
a changed rule rather than to drift.
"""
verdict: dict[str, Any] = {}
signal = [q for q in quality if q.tier == "signal" and q.error is None]
reference = [q for q in quality if q.tier == "reference" and q.error is None]
def mean_accuracy(results: list[QualityResult]) -> float | None:
vals = [q.metrics.get("accuracy") for q in results]
vals = [v for v in vals if v is not None]
return round(sum(vals) / len(vals), 4) if vals else None
verdict["signal_score"] = mean_accuracy(signal)
verdict["reference_score"] = mean_accuracy(reference)
# Contamination gate. A probe nobody reads is the same as no probe, so this does not just
# record a number — when a probe fires it takes `signal_score` away.
#
# Nulling it is deliberate. Anything downstream that reads `verdict["signal_score"]` —
# `kbench results`, a comparison, a published card — would otherwise happily render a
# memorised score next to a small warning, and the number is what people quote. The raw
# value is preserved under a name nobody reads by accident, so nothing is lost for
# forensics and nothing is quotable by mistake.
canary = [q for q in quality if q.tier == "canary" and q.error is None]
canary_score = mean_accuracy(canary)
verdict["canary_score"] = canary_score
if canary_score is None:
# No probe ran. The signal score stands, but it is unverified, and saying so is the
# difference between "clean" and "nobody checked".
verdict["contamination"] = "unverified"
elif canary_score > 0:
verdict["contamination"] = "detected"
verdict["signal_score_unverified"] = verdict["signal_score"]
verdict["signal_score"] = None
else:
verdict["contamination"] = "clean"
if perf and perf.points:
single = next((p for p in perf.points if p.concurrency == 1), None)
best = max(perf.points, key=lambda p: p.output_tps_total)
if single:
verdict["single_stream_tps"] = round(single.output_tps_per_stream, 1)
# Below ~15 tok/s a model reads slower than a person, which rules
# out anything interactive regardless of how good its answers are.
verdict["interactive_viable"] = single.output_tps_per_stream >= 15.0
verdict["peak_throughput_tps"] = round(best.output_tps_total, 1)
verdict["peak_throughput_concurrency"] = best.concurrency
return verdict