Score the product path against corpus ids, not storage ids
The frozen baseline claimed the product's top-10 shared 0.0% with the fast BM25 path. That was a harness bug: gold names a corpus source (mmlongbench:PH_2016.06.08_Economy-Final.pdf) while the store names a content hash (PH_2016_06_08_Economy_Final_10e57152f44d1a2b), so the two id spaces never met and the right document, ranked first, scored as a miss. - bench/systems.py: `_corpus_ids` joins store sources through the `source_map()` the counterpart already uses, and refuses a store whose sources are not corpus files instead of silently scoring zero. - bench/systems.py: `live_product_rankings(..., as_holders=...)` asks the product as the query's own holder, which is how the counterpart is scored. - bench/run.py: the live section now reports recall@10 / MRR / hit@10 with a 95% lower bound, and probes 300 queries instead of 40 — the SQL path answers in well under a second, so the old cap was a cost estimate that expired. - bench/run.py: `--name CURRENT` writes a post-P0 report beside the frozen BASELINE without touching it, stamped with the commit and the store schema version the pass started on (the live pass migrates the store mid-run, so a plain stamp would be a lie). - bench/stamp_holders.py: the pre-P0 store holds 165,973 memories nobody owns. The corpus index states the owner, so holders, scenarios and event times are stamped in place — 165,973 atoms across 50,278 sources, 0 left unowned. Numbers, same machine, load 1.0-1.7: - CURRENT (post-P0, schema v5): no_rerank recall@10 0.565 p50 0.643s; fusion+rerank 0.453 p50 7.000s; graph 0.001. - Product as its holder: recall@10 0.870, MRR 0.740, n=173. - Counterpart: mem0_flat holder scope 0.866, whole corpus 0.412; the holder boundary is worth +0.454 with an identical ranker and identical items. Verification: pytest backend/tests bench/tests -q -> 173 passed; ruff and mypy (fresh cache) clean on every touched file.
This commit is contained in:
1 parent
4ab36cd5eb
commit
6de7439db6
8 files changed
+975
-68
No files matched your search
+76
-30
@@ -41,7 +41,7 @@ from bench.systems import (
|
||||
ROOT = SETS.parent
|
||||
REPORTS = ROOT / "reports"
|
||||
DB_HOME = ROOT / "runs" / "baseline"
|
||||
LIVE_QUERIES = 40 # the product rebuilds BM25 per query, so a live pass costs minutes per query
|
||||
LIVE_QUERIES = 300 # the acceptance bar names n>=300; the SQL path answers in well under a second
|
||||
PROGRESS_EVERY = 250 # an arm takes tens of minutes over 5,308 queries; silence looks like a hang
|
||||
SCALE_SIZES = (500, 2_000, 10_000, 50_000)
|
||||
SCALE_PROBES = 40
|
||||
@@ -163,13 +163,19 @@ def live_check(
|
||||
top_k: int = 10,
|
||||
live_queries: int = LIVE_QUERIES,
|
||||
) -> dict[str, Any]:
|
||||
"""Compare the unmodified RetrievalService with the fast BM25 path."""
|
||||
"""Score the unmodified RetrievalService on its own, as its holder would ask it.
|
||||
|
||||
The arms measure retrieval configurations; the acceptance bar is about the product. A query is
|
||||
asked as the holder the frozen set assigns it, because a transactive memory is used that way.
|
||||
"""
|
||||
probe = [query for query in queries if query.answerable]
|
||||
stride = max(1, len(probe) // live_queries)
|
||||
chosen = probe[::stride][:live_queries]
|
||||
if not chosen:
|
||||
raise SystemExit("live check needs answerable queries")
|
||||
rankings, distinct, timings = live_product_rankings(DB_HOME, [query.text for query in chosen], top_k)
|
||||
rankings, distinct, timings = live_product_rankings(
|
||||
DB_HOME, [query.text for query in chosen], top_k, as_holders=[query.holder for query in chosen]
|
||||
)
|
||||
overlaps = [
|
||||
len(set(fast_rankings[query.query_id]) & set(ranking)) / top_k
|
||||
for query, ranking in zip(chosen, rankings, strict=True)
|
||||
@@ -177,20 +183,30 @@ def live_check(
|
||||
]
|
||||
if not overlaps:
|
||||
raise SystemExit("live check found no fast rankings to compare against")
|
||||
return {
|
||||
"queries": len(chosen),
|
||||
"top_k": top_k,
|
||||
"p50": sorted(timings)[len(timings) // 2],
|
||||
"mean": sum(timings) / len(timings),
|
||||
"max": max(timings),
|
||||
"top10_overlap_with_bm25_only": sum(overlaps) / len(overlaps),
|
||||
"identical_top10": sum(1 for overlap in overlaps if overlap == 1.0) / len(overlaps),
|
||||
"distinct_sources_in_top10": {
|
||||
"mean": sum(distinct) / len(distinct),
|
||||
"min": min(distinct),
|
||||
"max": max(distinct),
|
||||
},
|
||||
}
|
||||
scores = [
|
||||
score_query(query.query_id, ranking, set(query.gold), query.holder)
|
||||
for query, ranking in zip(chosen, rankings, strict=True)
|
||||
]
|
||||
metrics: dict[str, Any] = dict(aggregate(scores))
|
||||
ordered = sorted(timings)
|
||||
metrics.update(
|
||||
{
|
||||
"queries": len(chosen),
|
||||
"top_k": top_k,
|
||||
"p50": ordered[len(ordered) // 2],
|
||||
"p95": ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))],
|
||||
"mean": sum(ordered) / len(ordered),
|
||||
"max": max(ordered),
|
||||
"top10_overlap_with_bm25_only": sum(overlaps) / len(overlaps),
|
||||
"identical_top10": sum(1 for overlap in overlaps if overlap == 1.0) / len(overlaps),
|
||||
"distinct_sources_in_top10": {
|
||||
"mean": sum(distinct) / len(distinct),
|
||||
"min": min(distinct),
|
||||
"max": max(distinct),
|
||||
},
|
||||
}
|
||||
)
|
||||
return metrics
|
||||
|
||||
|
||||
def git_commit() -> str:
|
||||
@@ -203,7 +219,23 @@ def git_commit() -> str:
|
||||
return commit
|
||||
|
||||
|
||||
def meta(corpus_sources: int, queries: Sequence[Query], limit: int | None = None) -> dict[str, Any]:
|
||||
def store_schema_version(store: Path) -> int:
|
||||
"""The schema the measured store actually carries, read from the file."""
|
||||
import sqlite3
|
||||
|
||||
if not store.exists():
|
||||
raise SystemExit(f"{store}: the measured store is missing")
|
||||
connection = sqlite3.connect(f"file:{store}?mode=ro", uri=True)
|
||||
try:
|
||||
row = connection.execute("PRAGMA user_version").fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
return int(row[0])
|
||||
|
||||
|
||||
def meta(
|
||||
corpus_sources: int, queries: Sequence[Query], limit: int | None = None, store: Path | None = None
|
||||
) -> dict[str, Any]:
|
||||
manifest_path = SETS / "manifest.json"
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
@@ -220,6 +252,7 @@ def meta(corpus_sources: int, queries: Sequence[Query], limit: int | None = None
|
||||
"answerable": sum(1 for query in queries if query.answerable),
|
||||
"limit_per_modality": limit,
|
||||
"corpus_sources": corpus_sources,
|
||||
"store_schema_version_at_start": None if store is None else store_schema_version(store),
|
||||
"models": {"dense": DENSE_MODEL, "rerank": RERANK_MODEL},
|
||||
}
|
||||
|
||||
@@ -240,12 +273,14 @@ def scale_arm_names(rows: Sequence[dict[str, Any]]) -> list[str]:
|
||||
return names
|
||||
|
||||
|
||||
def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
def write_markdown(report: dict[str, Any], path: Path, name: str = "BASELINE") -> None:
|
||||
"""Human-readable twin of the JSON report."""
|
||||
schema = report["meta"].get("store_schema_version_at_start")
|
||||
lines = [
|
||||
"# BASELINE: what the product retrieves today",
|
||||
f"# {name}: what the product retrieves at this build",
|
||||
"",
|
||||
f"- generated {report['meta']['generated_at']} at commit {report['meta']['git_commit'][:12]}",
|
||||
("" if schema is None else f"- measured store schema: v{schema}"),
|
||||
(
|
||||
f"- corpus {report['meta']['corpus_sources']} sources, {report['meta']['queries']} queries "
|
||||
f"({report['meta']['answerable']} answerable)"
|
||||
@@ -302,21 +337,26 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
live = report["live_product"]
|
||||
lines += [
|
||||
"",
|
||||
"## the unmodified product path",
|
||||
"## the product path, asked as its holder",
|
||||
"",
|
||||
(
|
||||
f"- RetrievalService.search: p50 {live['p50']:.2f}s, mean {live['mean']:.2f}s, max {live['max']:.2f}s "
|
||||
f"over {live['queries']} queries"
|
||||
f"- RetrievalService.search as the query's own holder: recall@10 {live['recall_at_10']:.3f}, "
|
||||
f"MRR {live['mrr']:.3f}, hit@10 {live['hit_at_10']:.3f} (95% lower {live['hit_at_10_wilson_lower']:.3f}) "
|
||||
f"over n={live['queries']} answerable queries"
|
||||
),
|
||||
(
|
||||
f"- latency p50 {live['p50']:.3f}s, p95 {live['p95']:.3f}s, mean {live['mean']:.3f}s, "
|
||||
f"max {live['max']:.3f}s"
|
||||
),
|
||||
(
|
||||
f"- top-10 overlap with the fast BM25 path: {live['top10_overlap_with_bm25_only']:.1%}, "
|
||||
f"identical order {live['identical_top10']:.1%}"
|
||||
),
|
||||
(
|
||||
f"- its top-{live['top_k']} atoms cover only "
|
||||
f"{live['distinct_sources_in_top10']['mean']:.1f} distinct sources "
|
||||
f"- its top-{live['top_k']} atoms cover "
|
||||
f"{live['distinct_sources_in_top10']['mean']:.1f} distinct sources on average "
|
||||
f"(min {live['distinct_sources_in_top10']['min']}): the product ranks atoms and never diversifies, "
|
||||
"so ten slots fill with fragments of one document and the overlap with a source-level ranking is near zero"
|
||||
"so ten slots can fill with fragments of one document"
|
||||
),
|
||||
]
|
||||
if report.get("scale"):
|
||||
@@ -463,6 +503,11 @@ def main() -> int:
|
||||
help="re-time this many sampled queries with warm models instead of trusting cached per-query seconds",
|
||||
)
|
||||
parser.add_argument("--out", type=Path, default=REPORTS)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
default="BASELINE",
|
||||
help="report name: writes NAME.md and name.json (BASELINE stays the frozen pre-P0 snapshot)",
|
||||
)
|
||||
parser.add_argument("--config", type=Path, help="JSON defaults for these flags (bench/configs/ablation.json)")
|
||||
args = parser.parse_args()
|
||||
if args.config:
|
||||
@@ -485,7 +530,7 @@ def main() -> int:
|
||||
print(f"{len(queries)} queries, {len(atoms)} atoms, {corpus_sources} sources", flush=True)
|
||||
|
||||
report: dict[str, Any] = {
|
||||
"meta": meta(corpus_sources, queries, args.limit),
|
||||
"meta": meta(corpus_sources, queries, args.limit, store=DB_HOME / "data" / "ely_eye.sqlite"),
|
||||
"chance": chance_row(queries, corpus_sources),
|
||||
}
|
||||
# One pass over the queries: each signal is computed once and every arm is derived
|
||||
@@ -553,9 +598,10 @@ def main() -> int:
|
||||
report["scale"] = scale_curve(queries, atoms, [arm for arm in arms_requested if arm in ("bm25_only", "dense_only")])
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
(args.out / "baseline.json").write_text(json.dumps(report, indent=1, sort_keys=True), encoding="utf-8")
|
||||
write_markdown(report, args.out / "BASELINE.md")
|
||||
print(f"== wrote {args.out / 'baseline.json'} and {args.out / 'BASELINE.md'}", flush=True)
|
||||
stem = args.name.lower()
|
||||
(args.out / f"{stem}.json").write_text(json.dumps(report, indent=1, sort_keys=True), encoding="utf-8")
|
||||
write_markdown(report, args.out / f"{args.name}.md", name=args.name)
|
||||
print(f"== wrote {args.out / (stem + '.json')} and {args.out / (args.name + '.md')}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user