Let every harness work on a copy of the frozen store

copy_store lived in dense_scale, so the two harnesses that also needed it reached back into a
measurement module for a plumbing helper. It now sits in systems beside the product path it
protects, and bench.run passes a scratch directory to the live check: the product path migrates
whatever database it opens, and the frozen baseline is cited evidence.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 15:11:27 -04:00
1 parent b8f83f422a
commit 91fbff34ee
5 files changed
+39 -22

No files matched your search

+1 -16
View File
@@ -10,7 +10,6 @@ from __future__ import annotations
import argparse import argparse
import json import json
import shutil
import sqlite3 import sqlite3
import time import time
from collections.abc import Sequence from collections.abc import Sequence
@@ -22,7 +21,7 @@ import numpy as np
from bench.query_set import load_queries, scale_probes from bench.query_set import load_queries, scale_probes
from bench.report import git_commit from bench.report import git_commit
from bench.run import latency from bench.run import latency
from bench.systems import DENSE_MODEL, MODELS, machine_facts from bench.systems import DENSE_MODEL, MODELS, copy_store, machine_facts
if TYPE_CHECKING: if TYPE_CHECKING:
from ely_eye.db import Database from ely_eye.db import Database
@@ -65,20 +64,6 @@ def vector_footprint(database: Any, table: str) -> tuple[int, str]:
return int(payload), "vector bytes only, this sqlite3 has no dbstat" return int(payload), "vector bytes only, this sqlite3 has no dbstat"
def copy_store(home: Path, scratch: Path) -> tuple[Path, float]:
"""Work on a copy: the baseline store is frozen evidence and must not be migrated in place."""
source = home / "data" / "ely_eye.sqlite"
if not source.is_file():
raise SystemExit(f"{source}: no product database, build it with python -m bench.ingest_baseline")
data = scratch / "data"
data.mkdir(parents=True, exist_ok=True)
began = time.perf_counter()
for suffix in ("", "-wal", "-shm"):
if (path := Path(str(source) + suffix)).is_file():
shutil.copyfile(path, data / (source.name + suffix))
return scratch, time.perf_counter() - began
def stored_vectors(database: Database) -> tuple[list[str], np.ndarray]: def stored_vectors(database: Database) -> tuple[list[str], np.ndarray]:
"""Every vector the engine holds, for a scan it can be checked against.""" """Every vector the engine holds, for a scan it can be checked against."""
with database.connect() as connection: with database.connect() as connection:
+15 -2
View File
@@ -73,6 +73,7 @@ def live_check(
fast_rankings: dict[str, list[str]], fast_rankings: dict[str, list[str]],
top_k: int = 10, top_k: int = 10,
live_queries: int = LIVE_QUERIES, live_queries: int = LIVE_QUERIES,
scratch: Path | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Score the unmodified RetrievalService on its own, as its holder would ask it. """Score the unmodified RetrievalService on its own, as its holder would ask it.
@@ -85,7 +86,11 @@ def live_check(
if not chosen: if not chosen:
raise SystemExit("live check needs answerable queries") raise SystemExit("live check needs answerable queries")
rankings, distinct, timings = live_product_rankings( rankings, distinct, timings = live_product_rankings(
DB_HOME, [query.text for query in chosen], top_k, as_holders=[query.holder for query in chosen] DB_HOME,
[query.text for query in chosen],
top_k,
as_holders=[query.holder for query in chosen],
scratch=scratch,
) )
overlaps = [ overlaps = [
len(set(fast_rankings[query.query_id]) & set(ranking)) / top_k len(set(fast_rankings[query.query_id]) & set(ranking)) / top_k
@@ -229,6 +234,12 @@ def main() -> int:
parser.add_argument("--limit", type=int, default=None, help="score at most this many queries per modality") parser.add_argument("--limit", type=int, default=None, help="score at most this many queries per modality")
parser.add_argument("--no-live", action="store_true", help="skip the unmodified RetrievalService path") parser.add_argument("--no-live", action="store_true", help="skip the unmodified RetrievalService path")
parser.add_argument("--live-queries", type=int, default=LIVE_QUERIES) parser.add_argument("--live-queries", type=int, default=LIVE_QUERIES)
parser.add_argument(
"--live-scratch",
type=Path,
default=ROOT / "runs" / "live",
help="copy the store here before the product path opens it, so the frozen baseline stays frozen",
)
parser.add_argument("--no-scale", action="store_true", help="skip the latency/scale curve") parser.add_argument("--no-scale", action="store_true", help="skip the latency/scale curve")
parser.add_argument( parser.add_argument(
"--remeasure-latency", "--remeasure-latency",
@@ -325,7 +336,9 @@ def main() -> int:
# A crash in the live or scale phase should not cost every finished arm. # A crash in the live or scale phase should not cost every finished arm.
(DB_HOME / "partial_report.json").write_text(json.dumps(report, indent=1, sort_keys=True), encoding="utf-8") (DB_HOME / "partial_report.json").write_text(json.dumps(report, indent=1, sort_keys=True), encoding="utf-8")
if not args.no_live: if not args.no_live:
report["live_product"] = live_check(queries, fast_rankings, live_queries=args.live_queries) report["live_product"] = live_check(
queries, fast_rankings, live_queries=args.live_queries, scratch=args.live_scratch
)
if not args.no_scale: if not args.no_scale:
report["scale"] = scale_curve(queries, atoms, [arm for arm in arms_requested if arm in ("bm25_only", "dense_only")]) report["scale"] = scale_curve(queries, atoms, [arm for arm in arms_requested if arm in ("bm25_only", "dense_only")])
+1 -2
View File
@@ -14,11 +14,10 @@ import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from bench.dense_scale import copy_store
from bench.query_set import load_queries, scale_probes from bench.query_set import load_queries, scale_probes
from bench.report import git_commit from bench.report import git_commit
from bench.run import latency from bench.run import latency
from bench.systems import machine_facts from bench.systems import copy_store, machine_facts
DEFAULT_HOME = Path("bench/runs/baseline") DEFAULT_HOME = Path("bench/runs/baseline")
DEFAULT_SCRATCH = Path("bench/runs/scale") DEFAULT_SCRATCH = Path("bench/runs/scale")
+21
View File
@@ -13,6 +13,7 @@ import json
import os import os
import platform import platform
import re import re
import shutil
import sqlite3 import sqlite3
import time import time
from collections.abc import Sequence from collections.abc import Sequence
@@ -445,11 +446,26 @@ def _corpus_ids(database: Database) -> dict[str, str]:
return ids return ids
def copy_store(home: Path, scratch: Path) -> tuple[Path, float]:
"""Work on a copy: the baseline store is frozen evidence and must not be migrated in place."""
source = home / "data" / "ely_eye.sqlite"
if not source.is_file():
raise SystemExit(f"{source}: no product database, build it with python -m bench.ingest_baseline")
data = scratch / "data"
data.mkdir(parents=True, exist_ok=True)
began = time.perf_counter()
for suffix in ("", "-wal", "-shm"):
if (path := Path(str(source) + suffix)).is_file():
shutil.copyfile(path, data / (source.name + suffix))
return scratch, time.perf_counter() - began
def live_product_rankings( def live_product_rankings(
db_home: Path, db_home: Path,
queries: Sequence[str], queries: Sequence[str],
top_k: int = 10, top_k: int = 10,
as_holders: Sequence[str | None] | None = None, as_holders: Sequence[str | None] | None = None,
scratch: Path | None = None,
) -> tuple[list[list[str]], list[int], list[float]]: ) -> tuple[list[list[str]], list[int], list[float]]:
"""Rankings from the unmodified RetrievalService, with per-query wall time. """Rankings from the unmodified RetrievalService, with per-query wall time.
@@ -460,6 +476,9 @@ def live_product_rankings(
The second list counts how many distinct sources the product's top-k atoms came from: the The second list counts how many distinct sources the product's top-k atoms came from: the
product ranks atoms and never diversifies, so ten slots can be filled by fragments of one product ranks atoms and never diversifies, so ten slots can be filled by fragments of one
document, and that is invisible once the hits are collapsed. document, and that is invisible once the hits are collapsed.
`scratch` copies the store before opening it. The product path migrates whatever it opens, and
the baseline store is cited evidence, so a benchmark that measures the product must not edit it.
""" """
from ely_eye.config import Settings from ely_eye.config import Settings
from ely_eye.db import Database from ely_eye.db import Database
@@ -468,6 +487,8 @@ def live_product_rankings(
if as_holders is not None and len(as_holders) != len(queries): if as_holders is not None and len(as_holders) != len(queries):
raise SystemExit(f"{len(as_holders)} holders for {len(queries)} queries") raise SystemExit(f"{len(as_holders)} holders for {len(queries)} queries")
if scratch is not None:
db_home, _ = copy_store(db_home, scratch)
settings = Settings(home=db_home) settings = Settings(home=db_home)
database = Database(settings) database = Database(settings)
service = RetrievalService(settings=settings, database=database) service = RetrievalService(settings=settings, database=database)
+1 -2
View File
@@ -16,12 +16,11 @@ import statistics
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from bench.dense_scale import copy_store
from bench.metrics import aggregate, score_query from bench.metrics import aggregate, score_query
from bench.query_set import load_queries, scale_probes from bench.query_set import load_queries, scale_probes
from bench.report import git_commit from bench.report import git_commit
from bench.run import latency from bench.run import latency
from bench.systems import live_product_rankings, machine_facts from bench.systems import copy_store, live_product_rankings, machine_facts
FRACTIONS = [0.20, 0.10, 0.05, 0.02, 0.01, 0.005] FRACTIONS = [0.20, 0.10, 0.05, 0.02, 0.01, 0.005]
TOP_K = 10 TOP_K = 10