Make performance evidence reproducible - #24
Merged
Conversation
tvanreenen
marked this pull request as ready for review
August 21, 2026 19:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this change
The README included a useful-looking performance table, but only identified the hardware. It did not preserve the source revision, Python or NumPy version, BLAS implementation, thread environment, seeded workload, warmups, repetitions, raw samples, or summary method. That made the figures difficult to reproduce, compare, or refresh as the implementation changed.
This PR replaces that unsupported table with repeatable evidence. It adds benchmark commands for the two performance-sensitive workflows already described by the library—ingestion and exact search—without adding a runtime dependency or turning variable CI machines into latency gates.
What changes
The repository now provides
ingestandsearchsubcommands throughuv run python benchmarks/benchmark.py. Each command writes one JSON record containing:Inputs use an explicit NumPy
Generator(PCG64). The record still captures the NumPy version and array digests because NumPy does not promise identical distribution streams across every version. Data generation, digest calculation, environment inspection, explicit garbage collection, and final-result disposal occur outside timed regions. Cyclic garbage collection is temporarily disabled during measured operations and restored afterward.The ingestion benchmark constructs and fills a fresh store on every trial. The search benchmark prepares the store once, then runs the same seeded query sequence through the selected public unfiltered search method. Search supports cosine, dot-product, and Euclidean metrics as well as normalized and raw stores.
Updated performance evidence
The README now reports clean measurements from commit
801de9aon a 24 GB Apple M4 Mac mini with macOS 26.6.1, CPython 3.13.5, NumPy 2.3.3, and Accelerate BLAS. The search table uses the same 1,000-, 10,000-, and 100,000-row counts at 384, 1,536, and 3,072 dimensions, so readers can compare row scaling and embedding-width scaling directly. Each cell shows both stored-vector memory and the median duration of seven measured 20-query trials divided by 20, after two warmup trials.For a prepared 10,000-by-384 input, single-row ingestion measured 77.3 ms at the median, while batches of 1,000 rows measured 6.28 ms. Those figures demonstrate why callers should pass an existing batch as a batch, even though the store's geometric capacity growth keeps repeated additions from recopying every prior row.
These measurements are reference points, not portable latency guarantees. The README and benchmark guide state which memory is counted, which work is timed, and which hardware and software differences can change the result.
The README also makes the intended scale explicit: the benchmark stops at 100,000 rows because this package targets small-to-medium, in-process exact search. That row count is an upper reference rather than a promised limit or a target for continued scaling. Workloads that routinely reach millions of vectors generally need an indexed or service-backed system.
Complexity guarantees
The existing suite already verifies that repeated additions grow capacity geometrically and that unfiltered search uses the stored vector matrix without a full copy. This PR adds the remaining structural regression check named in the roadmap: a small
top_ksearch must use partial selection and sort only the selected rows rather than fully sorting every candidate.Wall-clock thresholds remain out of shared CI. CI continues checking algorithmic structure and observable correctness, which avoids failures caused only by transient runner load.
User impact
There is no change to the
VectorStoreruntime API, archive format, package dependencies, or runtime package contents. Users evaluating the library get current, qualified performance reference points and commands they can run on their own workload and machine. Contributors get an inspectable JSON artifact for release evidence and performance investigations.The benchmark command and guide are included in the source distribution. The wheel remains limited to the runtime package. The package version remains 0.5.0 on this branch; the dedicated release PR will perform the 0.6.0 version change.
Documentation and ancillary changes
Verification
uv run ruff check .uv run ruff format --check .uv run mypy src benchmarksuv run pytest -W error -q— 298 tests passed on the locked Python 3.13 environmentuv run --no-project --isolated --python 3.11 --with numpy==1.23.2 --with pytest==8.4.2 --with-editable . pytest -W error -q— 298 tests passed after explicitly verifying Python 3.11 and NumPy 1.23.2 were imported801de9awithdirty: false; shared workloads produced matching input digests.Commit organization