Benchmark design¶
The discovery study spans three objectives, dimensions 8/32/128/768, corpus
sizes 1k/10k/100k/1M, query batches 1/32/1024, and k values 1/10/100. The
full Cartesian product would create 432 semantic cases before implementations
and timing views, and includes allocations that exceed practical host memory.
Profiles¶
The discovery core anchors on N=10k, D=128, Q=32, and k=10, then
varies one factor at a time for each objective. This produces 33 semantic
cases. Collection separates 24 standard-cost cases from nine stress cases that
isolate N=1M, Q=1024, or D=768. A 12-case small profile keeps scalar
Python implementations in scope. Deterministic feasibility rules record every
included and excluded implementation/workload cell and reject cells that
exceed an 8 GiB conservative peak-allocation budget or a scalar runtime ceiling.
Synthetic normalized-cosine data uses unit-normalized Gaussian vectors from a
pinned NumPy PCG64 seed. Squared-L2 and inner-product data uses unnormalized
Gaussian vectors. Natural embeddings are a separate 384-dimensional slice:
SciFact corpus and queries embedded with all-MiniLM-L6-v2. Both Hugging Face
repository revisions are pinned in NaturalDatasetSpec; downloading and
embedding are not part of this milestone.
Capability matrix¶
| Implementation | Squared L2 | Inner product | Normalized cosine |
|---|---|---|---|
| Python sort/heap | yes | yes | yes |
| NumPy full/argpartition/blocked | yes | yes | yes |
| scikit-learn brute | yes | no | yes |
| scikit-learn KDTree/BallTree | yes | no | yes, via normalized L2 |
| SciPy cKDTree | yes | no | yes, via normalized L2 |
| Faiss Flat L2 | yes | no | no |
| Faiss Flat IP | no | yes | yes |
| PyTorch matmul/topk | yes | yes | yes |
Tree backends are not assigned inner product because they do not implement it natively. Faiss is restricted to float32. Current PyTorch releases do not ship an Intel-macOS wheel, so that cell is absent on that host and remains available on supported CPU hosts.
Timing and provenance¶
Each benchmark cell builds one index and prepares backend-specific query data before timing. Timed work includes exact scoring, selection, native-result conversion, and canonical ordering. Benchmatrix records latency and query throughput for every included cell. Tail latency is preselected for all small workloads and the three objective-specific core anchors rather than duplicated across every costly cell.
Every returned result is validated after timing. Workloads at or below one
million scalar coordinate evaluations use the independent math.fsum
reference. Larger workloads use bounded float64 query/corpus blocks after that
path has been checked against the scalar oracle across objectives and dtypes.
Only one generated workload and reference result is retained at a time, while
all implementations and metrics for that workload reuse it. Raw benchmark JSON
records the oracle method, strict top-k boundary margin, and a canonical SHA-256
digest of expected identities and rounded scores. Selected identity sets must
match exactly. An implementation-specific internal ordering may differ from
the float64 oracle only when the corresponding reference scores are
indistinguishable at an explicit dtype-scaled numerical tolerance; resolvable
ordering inversions still fail validation.
Case metadata is strict JSON and contains objective, score convention, shapes, dtype, normalization, generator revision, seed, dataset identity, profile, and thread policy. It excludes timestamps, local paths, and Python object representations. Collection should set BLAS and backend thread counts to one, retain raw pytest-benchmark JSON, and capture the lockfile, Git revision, host, CPU, OS, and package versions alongside artifacts.
Collection workflow¶
The checked-in benchmatrix policy requires five independent runs for formal evidence, raw samples, 50,000 deterministic BCa resamples, Bonferroni multiplicity control, and stricter tail-latency evidence. Discovery pilots use one or two runs only for feasibility and exploratory crossover inspection; they do not satisfy that evidence policy and cannot support confirmatory claims.
Use a fresh output root for every collection:
make benchmark-discovery-small DISCOVERY_OUTPUT=benchmark-results/pilot-001
make benchmark-discovery-core DISCOVERY_OUTPUT=benchmark-results/pilot-001
make benchmark-discovery-stress \
DISCOVERY_OUTPUT=benchmark-results/pilot-001-stress-n1m \
BENCHMARK_FILTER='n1000000__d128__q32__k10'
Stress collection requires a filter so only one high-cost workload runs at a
time. Each successful output directory contains the benchmatrix manifest and
raw run JSON plus discovery-plan.json, which retains excluded cells, and
resource-usage.json, which records elapsed collection time and child-process
peak resident memory where the platform exposes it. On Windows, the peak is
recorded as JSON null with an unavailable measurement scope because the
standard-library resource module is absent. The plan also records the Git
revision and dirty state, the uv lockfile digest, and a digest over every
tracked or non-ignored source file, so an exploratory dirty-tree pilot remains
identifiable. Use
BENCHMARK_RUNS, BENCHMARK_ROUNDS, and
BENCHMARK_WARMUP_ROUNDS to control pilot repetition without changing case
identity. Tail matrices use BENCHMARK_TAIL_ROUNDS, which defaults to the 100
independent call samples required by the evidence policy.
For a clean, complete sequential collection, use:
make benchmark-discovery-study \
DISCOVERY_OUTPUT=benchmark-results/discovery-001 \
BENCHMARK_RUNS=2
The orchestrator refuses a dirty tree, splits every stress workload into its own child collection, and writes lifecycle state after each child. Five runs are required only when discovery output will itself support formal inference; two-run discovery remains suitable for selecting confirmatory questions.
Build the deterministic evidence audit, CSV tables, source-backed PNG/SVG figures, chart contracts, and technical Markdown report with:
make benchmark-analyze-discovery \
DISCOVERY_OUTPUT=benchmark-results/discovery-001
The generated analysis.json explicitly labels evidence as exploratory or
formal-ready. Winner counts and crossover percentages remain descriptive even
when all collection gates pass; formal comparisons use the paired workflow.