Everything goes through one entrypoint: ./run-benchmark.sh.
On the host: docker, python3 with PyYAML, git. Nothing else. The database clients and the scientific Python stack live inside the images, so running a benchmark does not install anything onto the machine being measured.
# Debian / Ubuntu
sudo apt-get install -y docker.io python3-yaml git
sudo usermod -aG docker "$USER" # then log out and back inDisk: budget roughly 40 GB for images and sources, plus 2–4× your dataset
size for index data. The gist-960-euclidean dataset alone is 3.6 GB.
cd vector-bench
# 1. Export engine sources at their pinned tags and build all six images.
# pgvector ~10 min, MariaDB ~40 min, AliSQL 1.5-3 h. See the note below.
./run-benchmark.sh build
# 2. Download the datasets.
./run-benchmark.sh fetch --datasets fashion-mnist-784-euclidean
# 3. Prove the whole pipeline works before spending real time on it.
./run-benchmark.sh run --profile smokeOn build times. AliSQL dominates. Its CMake calls
MYSQL_CHECK_DUCKDB()unconditionally on Linux, so the bundled DuckDB inextra/duckdbis compiled on every build —WITH_DUCKDB_STORAGE_ENGINE=OFFonly makes the plugin a separate.soinstead of linking it intomysqld; it does not skip the compile. DuckDB's ExternalProject also redirects its output to/dev/null, so the build looks stalled for a long stretch. It is not. Build engines separately (--engines alisql) if you want to start measuring the other two sooner.
The smoke profile takes about 30 minutes per resource pass and exercises every stage for all three engines. Do not skip it. It is far cheaper to discover a broken image or a missing dataset here than eight hours into a full run.
When it finishes:
results/
├── annb/ ann-benchmarks recall/QPS results
│ ├── normalized/<dataset>/<k>/<engine>/*.hdf5
│ └── tuned/<dataset>/<k>/<engine>/*.hdf5
└── smoke-20260803-141500/
├── run-manifest.json provenance: CPU, versions, limits
├── checkpoints.json what completed, for --resume
├── ops-*.jsonl ops-harness records
├── mem-*.jsonl server memory timeseries
└── report/
├── report.html self-contained, shareable
├── report.md
├── records.jsonl every measurement, one flat schema
└── charts/*.svg, *.png
The annb/ tree is split by resource pass on purpose. ann-benchmarks names its
result files from the algorithm and its parameters only, with no notion of a
resource pass — a shared tree would make the tuned pass skip every point the
normalized pass had already computed, and the report would then present
normalized numbers as tuned ones. It also sits outside the per-run directory so
an interrupted run can resume without recomputing points it already has.
# Download the rest of the datasets first (~5 GB).
./run-benchmark.sh fetch
# The profile sized to actually be run: two datasets at 1M scale, ~2 days.
./run-benchmark.sh run --profile main
# One dataset, ~1 day — a complete three-way comparison at 1M scale.
./run-benchmark.sh run --profile main --datasets glove-100-angular
--profile full --resource-pass bothis ~272 h of ingest — about 11 days — before a single query runs. It describes the complete measurement space; it is not a recommendation. Every run prints its own estimate before starting. 07-planning-a-run.md has the measured rates and what each way of narrowing the scope costs you.
| Command | Purpose |
|---|---|
build |
Export sources at pinned tags, build runtime + bench images |
fetch |
Download datasets into datasets/ |
run |
Execute a benchmark run |
report |
Regenerate the report from an existing run directory |
render |
Regenerate the ann-benchmarks configs for a profile |
sources |
Export sources only, without building |
clean |
Remove containers, networks and volumes left by a run |
| Option | Default | Meaning |
|---|---|---|
--profile |
quick |
smoke, quick, full, or your own in config/profiles/ |
--engines |
all three | e.g. mariadb,alisql |
--datasets |
from profile | Override the profile's dataset list |
--resource-pass |
both |
normalized, tuned, or both |
--phases |
both |
ann (recall/QPS), ops (build/concurrency/filtered/churn) |
--resume |
off | Skip units already recorded complete |
--force |
off | Re-run ann-benchmarks points that already have results |
--fail-fast |
off | Stop at the first failure instead of continuing |
--no-report |
off | Skip report generation |
--run-id |
timestamped | Reuse an id to add to an existing run directory |
| Option | Meaning |
|---|---|
--engines |
Build one engine only |
--target |
runtime (server only), bench (server + Python), or all |
--march |
SIMD baseline for every engine. See below. |
--jobs |
Compile parallelism (default: all cores) |
--no-cache |
Force a clean rebuild |
All three engines compile SIMD distance kernels. -march decides which
instructions those kernels get, and it is applied identically to all three:
./run-benchmark.sh build --march x86-64-v3 # AVX2 + FMA. Portable. Default.
./run-benchmark.sh build --march native # Whatever this CPU has, incl. AVX-512.Use native when the build host and the benchmark host are the same machine and
you want each engine's best case. Use x86-64-v3 when the images must move
between machines. Never mix: rebuilding one engine with a different -march
turns the benchmark into a comparison of compiler flags.
The value used is baked into each image and reported in the manifest.
Work is checkpointed on success at two levels: per
(resource pass, engine, dataset, phase), and within an ops phase per
(M, build_mode). The second matters — an ops phase can run a dozen hours
across several M values, and checkpointing only the whole phase would discard
every M that had already finished. After an interruption:
./run-benchmark.sh run --profile main --resume --run-id main-20260804-185624Completed units are skipped, so an interruption costs one unit rather than the
run. Failed units are not checkpointed, so --resume retries exactly those.
If a run leaves containers behind (SIGKILL, host reboot):
./run-benchmark.sh clean --run-id full-20260803-120000normalized gives every engine identical CPU, memory and cache budgets, so any
difference belongs to the implementations. tuned gives each engine its
vendor-recommended configuration on the full machine, so the numbers reflect
what each can actually do.
Run both. They answer different questions, and either one alone is easy to argue with. See 05-methodology.md §3.
Profiles and resource passes are plain YAML; copy and edit.
cp config/profiles/quick.yml config/profiles/mine.yml
$EDITOR config/profiles/mine.yml
./run-benchmark.sh run --profile mineThe knobs worth changing first:
ann.m_valuesandann.ef_search— sweep density. More points make a smoother Pareto frontier and cost linearly more time.ops.client_counts— how far the concurrency sweep goes. Push past your core count to find the saturation point.ops.subset_rows— load only part of the training set, for faster iteration.memory.server_limit_gbinconfig/resources/normalized.yml— the single most consequential value, because an engine whose graph does not fit in cache behaves completely differently from one whose graph does.
Start with report/report.html. Its order is deliberate:
- Environment — the hardware and versions the numbers are scoped to.
- Validity — anything that invalidates or narrows a result: measurements that ran without the vector index, filtered queries that returned short, environment warnings. Read this before the charts.
- Known asymmetries — structural differences no configuration removes.
- Recall vs throughput, then build cost, concurrency, filtered, churn.
report/records.jsonl holds every measurement in one flat schema, so you can
query it directly:
jq -r 'select(.phase=="recall_qps" and .recall_at_k>0.95)
| [.engine, .m, .ef_search, .recall_at_k, .qps] | @tsv' \
results/<run-id>/report/records.jsonl | sort -k5 -rn | head"image not found" — build it: ./run-benchmark.sh build --engines <name>.
"dataset not found" — fetch it: ./run-benchmark.sh fetch --datasets <name>.
A server container exits during startup. The orchestrator prints the
container's log tail with the error. The usual causes are a memory limit too
small for the configured buffer pool, or a shm_size too small for PostgreSQL.
"the vector index is NOT in the query plan". Not a framework failure — the
engine's optimizer chose a full scan. The run continues and the affected
measurements are listed in the report's Validity section. For AliSQL this is
expected at large LIMIT values; see 04-engine-notes.md.
Warnings about CPU topology. On a machine with fewer homogeneous cores than
the profile requests, the server and client containers are forced to share
cores. That adds latency noise. Lower cpu.server_cpus in the resource config,
or accept the noise — it is recorded in the manifest either way.
Results owned by root. The engines run as root inside their containers. The
orchestrator chowns the results back at the end of a run; if it was killed
first, sudo chown -R "$USER" results/ fixes it.