Keep the vectors beside the atoms instead of rebuilding them per query
The dense leg used to be a pair of files next to the database: a full .npy re-encoded from every atom, re-normalized on every query, and stale the moment anything was ingested. It is now a vec0 table inside the same SQLite file, written in the same transaction as the atom it describes, so a memory is queryable the instant it is remembered. - vector_index.py maintains the table; the writer inserts, the reader never rebuilds. A repair scan (ely-eye build-dense-index) catches what a crash hid, and it is not on the search path. - Ranking stays in the engine; every boundary (closed, retired, not yet true, another holder's private memory, the mounted cartridge) is checked once against the atoms table afterwards. A visibility subquery inside the KNN cost 48.9s on 153,857 vectors; the same check over 200 candidates costs 0.2ms. When the boundaries reject enough candidates the ask widens once, never twice. - The Go mirror and the web panel carry the new status fields. - bench/dense_scale.py and bench/quant_probe.py measure the leg at corpus size; bench/scale_check.py now works on a copy so the frozen baseline stays frozen. Measured on the frozen 165,973-atom corpus. bench/reports/DENSE.md, QUANT.md and SCALE.md carry the numbers and the machine they were taken on. Gates: 285 pytest, ruff clean, pyright 0 on the touched files, go build/vet/test and gofmt clean, frontend build and eslint clean. mypy is 25 findings against the parent's 24: the new one is "Skipping analyzing sqlite_vec: missing library stubs", the same class as the two existing qwen_vl_utils lines, and the repo keeps no type: ignore anywhere.
This commit is contained in:
1 parent
be08e7bf9c
commit
cd9851a12c
26 files changed
+1602
-118
No files matched your search
+26
-10
@@ -14,33 +14,35 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from bench.dense_scale import copy_store
|
||||
from bench.query_set import load_queries, scale_probes
|
||||
from bench.report import git_commit
|
||||
from bench.run import latency
|
||||
from bench.systems import machine_facts
|
||||
|
||||
DEFAULT_HOME = Path("bench/runs/baseline")
|
||||
DEFAULT_SCRATCH = Path("bench/runs/scale")
|
||||
MIN_ATOMS = 100_000
|
||||
DEFAULT_PROBES = 300
|
||||
|
||||
|
||||
def measure(home: Path, probes: int) -> dict[str, Any]:
|
||||
"""Open the product store (migrating it in place if it is old) and time real searches."""
|
||||
def measure(home: Path, scratch: Path, probes: int) -> dict[str, Any]:
|
||||
"""Open a copy of the product store (migrating it if it is old) and time real searches."""
|
||||
from ely_eye.config import Settings
|
||||
from ely_eye.db import Database
|
||||
from ely_eye.retrieval import RetrievalService
|
||||
|
||||
if not (home / "data" / "ely_eye.sqlite").is_file():
|
||||
raise SystemExit(
|
||||
f"{home / 'data' / 'ely_eye.sqlite'}: no product database, "
|
||||
"build it with python -m bench.ingest_baseline"
|
||||
)
|
||||
settings = Settings(home=home)
|
||||
copy_home, copy_seconds = copy_store(home, scratch)
|
||||
settings = Settings(home=copy_home)
|
||||
started = time.perf_counter()
|
||||
database = Database(settings)
|
||||
migration_seconds = time.perf_counter() - started
|
||||
atoms = database.atom_count()
|
||||
if atoms < MIN_ATOMS:
|
||||
raise SystemExit(f"{database.path}: {atoms} atoms, this check needs at least {MIN_ATOMS}")
|
||||
with database.connect() as connection:
|
||||
schema = int(connection.execute("PRAGMA user_version").fetchone()[0])
|
||||
print(f"{atoms} atoms copied in {copy_seconds:.1f}s, migrated in {migration_seconds:.1f}s", flush=True)
|
||||
|
||||
queries = scale_probes(load_queries(), probes)
|
||||
service = RetrievalService(settings, database)
|
||||
@@ -50,10 +52,15 @@ def measure(home: Path, probes: int) -> dict[str, Any]:
|
||||
began = time.perf_counter()
|
||||
service.search(query.text)
|
||||
times.append(time.perf_counter() - began)
|
||||
if len(times) % 100 == 0:
|
||||
print(f"searched {len(times)}/{len(queries)}", flush=True)
|
||||
|
||||
return {
|
||||
"commit": git_commit(),
|
||||
"database": str(database.path),
|
||||
"atoms": atoms,
|
||||
"copy_seconds": copy_seconds,
|
||||
"schema_version": schema,
|
||||
"migration_seconds": migration_seconds,
|
||||
"dense_embeddings_enabled": settings.dense_embeddings_enabled,
|
||||
"probes": len(queries),
|
||||
@@ -71,13 +78,18 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
[
|
||||
"# SCALE: the product's own search at corpus size",
|
||||
"",
|
||||
f"- commit: `{report['commit']}`",
|
||||
f"- database: {report['database']}",
|
||||
(
|
||||
f"- copied from the frozen baseline in {report['copy_seconds']:.1f}s and migrated "
|
||||
f"in {report['migration_seconds']:.1f}s to schema {report['schema_version']}; "
|
||||
"the baseline store itself is left untouched"
|
||||
),
|
||||
(
|
||||
f"- atoms: {report['atoms']}, probes: {report['probes']} "
|
||||
f"({', '.join(report['probe_modalities'])})"
|
||||
),
|
||||
f"- dense embeddings: {report['dense_embeddings_enabled']}",
|
||||
f"- opening the store (migrations included): {report['migration_seconds']:.3f}s",
|
||||
(
|
||||
f"- p50 {timing['p50']:.4f}s · p95 {timing['p95']:.4f}s · max {timing['max']:.4f}s "
|
||||
f"over {timing['queries']} queries"
|
||||
@@ -88,6 +100,9 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
f"load {machine['load_average_1_5_15']}"
|
||||
),
|
||||
"",
|
||||
"The baseline recorded p50 20.16s on this corpus because the old search pulled every",
|
||||
"atom into Python and rebuilt a BM25 index per query.",
|
||||
"",
|
||||
(
|
||||
"_Latency is wall time per search call on an idle machine with the thread budget "
|
||||
"the benchmark pins; a run on a loaded machine is not comparable._"
|
||||
@@ -102,6 +117,7 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--home", type=Path, default=DEFAULT_HOME)
|
||||
parser.add_argument("--scratch", type=Path, default=DEFAULT_SCRATCH)
|
||||
parser.add_argument("--probes", type=int, default=DEFAULT_PROBES)
|
||||
parser.add_argument("--out", type=Path, default=Path("bench/reports/SCALE.md"))
|
||||
parser.add_argument("--json-out", type=Path, default=Path("bench/reports/scale.json"))
|
||||
@@ -109,7 +125,7 @@ def main() -> None:
|
||||
if args.probes <= 0:
|
||||
raise SystemExit(f"--probes must be positive, got {args.probes}")
|
||||
|
||||
report = measure(args.home, args.probes)
|
||||
report = measure(args.home, args.scratch, args.probes)
|
||||
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json_out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
write_markdown(report, args.out)
|
||||
|
||||
Reference in new issue
Block a user