strawmANN benchmark report

strawmANN against Qdrant · AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M (24 logical) · generated 2026-10-14 06:03:33 CEST

Dataset
quora
Corpus
522,931 documents · 10,000 queries
Metric
BM25 over a text index
strawmANN: 6/6 ok Qdrant: 6/6 ok

Summary

strawmANN serves 1.21x to 2.37x Qdrant's BM25 throughput across the W15 rows, at equal results: every row's top 10 matches the f64 BM25 oracle on both engines. Qdrant served BM25 from optimized segments only: all 522,931 points in immutable indexes (indexing threshold 1 KB). strawmANN served it from its one text index. Every ratio on this page is against that Qdrant index.

peak memory (RSS)
strawmANN433.2 MiB
Qdrant1.6 GiB
largest before the concurrent-write rows (W11); of it, anonymous 399.9 MiB / 647.4 MiB
written to disk
strawmANN31.9 MiB
Qdrant810.3 MiB
before the concurrent-write rows (W11)

Every figure is the median of 3 passes per engine, run alternately (A/B/A/B). What this does not establish.

Glossary
recall@10
Of the ten nearest neighbours a query really has, the share the engine returned. 1.0 is a perfect answer. "Really has" is settled by an exhaustive fp64 search, not by the other engine.
qps
Queries per second. On a batched row one request carries several queries, and the request rate is shown under it.
p50 / p99 / p99.9
The latency half of requests beat, that 99% beat, that 99.9% beat. The tail is what a user notices.
closed / open loop
A closed loop sends the next request only when the last one comes back, so a slow server receives less work and its tail looks better than it is. An open loop sends at a fixed rate regardless.
noise floor
How much a number moves between identical runs on this machine. A ratio inside it is shown grey with ≈: no measured difference. Hover a ratio to see its band.
conformance tier
What the two engines were shown to agree on before any speed was quoted. T1 licenses a single engine's own numbers; T3 licenses comparing the two, and requires their recall to be statistically indistinguishable.
§ numbers
Sections of docs/spec.md, the written rule each claim in the appendix is measured against. §7.1 is the host gate, §7.4 the comparison rules, §8 what may be published.

Throughput

Queries per second; higher is better. The ratio is strawmANN over Qdrant: green is faster, red slower, grey ≈ inside the noise floor (hover a ratio for its band). A dash means the pair is not compared, and the note says why. An ef sweep is one row showing its range; every measurement is under All throughput rows.

workloadstrawmANNQdrantrationotes
W15text: BM25 query, single client1,7611,4501.21x
W15-sattext: BM25 query, saturating (closed loop)19,92713,7921.44x
W15-filteredtext: BM25 query filtered to half the points16,0116,7552.37x
W15-k0.9-b0.4text: BM25 query at k1 0.9, b 0.4, saturating19,87615,8111.26x
W15-b0text: BM25 query at k1 1.2, b 0.0, saturating21,10517,1811.23x

Throughput by workload

Higher is better.

Relevance

nDCG@10 and MRR@10 of each BM25 variant's results against the corpus's relevance judgements: what BM25 itself is worth on this corpus. Reported, not ratioed or gated: engines that agree on every score may still order a tie differently, and that moves them. A filtered variant reads low by construction, since its filter keeps half the corpus and the judgements cover all of it.

rowsvariantstrawmANN nDCG@10Qdrant nDCG@10strawmANN MRR@10Qdrant MRR@10
W15, W15-satk1 1.2, b 0.750.79060.79050.78340.7834
W15-filteredk1 1.2, b 0.75, filtered to keyword_00.43340.43340.46890.4688
W15-k0.9-b0.4k1 0.9, b 0.40.77240.77290.76040.7606
W15-b0k1 1.2, b 00.64870.64350.62130.6147

Ingest and index build

Qdrant indexes while it ingests, so its upload time already contains most of the indexing; strawmANN uploads raw and builds afterwards. Compare the sum, not the upload line.

workloadstrawmANNQdrant
W15-uploadupload3.01 s †19.08 s

† at the load generator's polling floor. bfb decides a collection is Green by polling once a second and requiring three consecutive Green replies, having slept a second before the first poll, so no Time-to-Green it can report is below 3 s regardless of how fast the build was. A marked cell is an upper bound on the build and a measurement of the polling loop. The bias is a constant added to both engines, so the difference between them survives it and the ratio does not — and it is the faster engine the ratio understates.

Memory and disk

What each engine held in memory and moved to and from disk over the whole run. Per-row figures are under Storage and I/O.

strawmANNQdrant
storage on disk31.9 MiBallocated at --capacity227.8 MiB
peak memory (RSS)433.2 MiB1.6 GiB
read from disk4.0 KiB400.6 MiB
written to disk31.9 MiB810.3 MiB

Peak memory and storage on disk are read before the concurrent-write rows (W11): an engine rewriting segments maps old and new files at once, and RSS counts each mapping. Bytes written are summed over the same rows, since W11's volume is set by the harness's write rate; the other disk figures are totals over every row.

Appendix

How the run was set up, every row of every table, and the diagnostics behind them. Charts on a logarithmic axis say so on the axis: read the positions there, not the distances.

Run conditions and conformance

Measured as-deployed: the scheduler was left as it ships, so a difference is what a user would see rather than the engine in isolation, and ambient load is part of the measurement. Qdrant ran equal-work: asked for one populated graph (--segments 1), as strawmANN serves, so ef means the same thing on both sides. This is the configuration §8's comparative licensing is built around, and it is a control rather than a deployment. Its optimizer treats the count as a target; what it actually held is read back from the engine and stated under Collection config.

Conformance tier reached: T2 tie-aware rank. T1 (every BM25 score within its tolerance of the f64 oracle) and T2 (tie-aware rank) passed on both engines: the same documents at the same scores, so a throughput comparison is licensed (§8). Conformance hash 5b1e22a97fb23473.

Every BM25 score strawmANN returned is within 2.249e-07 (relative) of the f64 oracle's, against T1's tolerance of 1e-05: the same documents, the same statistics and the same ranking, in f32 (§8.1).

Every figure is the median of 3 passes per row per engine, alternated A/B/A/B (§7.4), and the spread is this run's own over 5 of 6 rows. The ingest and index-build rows (W15-upload) have none, so no build time carries a verdict. Each pass rebuilds the index, so the bands include build variance and are wider than a floor measured against one standing graph.

Units are queries per second. bfb reports rps, which counts batch requests: at --search-batch-size 16 the two differ by 16x, and reading one as the other once turned a 2.2x speedup into an apparent 7x regression. The tables show the request rate wherever it diverges.

Qdrant served BM25 from optimized segments only: all 522,931 points in immutable indexes (indexing threshold 1 KB). strawmANN served it from its one text index. Every ratio on this page is against that Qdrant index.

What was measured builds, dataset, host
strawmANN
sm-quora-perf-1014
Qdrant
qd-quora-perf-1014
enginecommit 1bb4b85e0580
ReleaseFast, native build, vnni on
binary sha256 0e3374874428e058
built 2026-10-13T19:44:01Z with zig 0.16.0
version 1.19.3-dev
binary ~/.cache/strawmann/qdrant-ab/bin/qdrant-dev-d6d3bd35b
sha256 0e0fbc18c18433c5 build d6d3bd35 (from the server's banner, not a checkout)
native binary outside a checkout: sha256 is the identity (§8.9)
network native no container in the path, like strawmann
measured2026-10-14T01:21:23Z to 2026-10-14T01:37:48Z (3 passes)2026-10-14T01:24:40Z to 2026-10-14T01:41:39Z (3 passes)
profileas-deployed · both engines
§7.1 gatepass · both engines
environment hash373e8531fd3d1bf6 · both engines
load generatorbfb dev @ d2a6b6c (qdrant/bfb#189, TextQuery's scoring oneof at field 2 for qdrant#11112; carries #187's dependency update, #185's text requests and #172, findings 32's --rps reaping fix) · both engines
clientqdrant-client 1.16.1-dev (git dev branch) · both engines
launched as~/Workspace/strawmann/zig-out/bin/strawmann --port 6334 --capacity 522931 --connections 64 --workers 7 --io-threads 1 --pin --cpus 4-11 --data-dir ~/.cache/strawmann/strawmann-storage --default-placement cached~/.cache/strawmann/qdrant-ab/bin/qdrant-dev-d6d3bd35b
cores the engine could use4-11 (8 cores) requested by the harness
4-11 (8 cores) observed on 8 threads
0-23 (24 cores) observed on 1 thread
4-11 (8 cores) requested by the harness
4-11 (8 cores) observed on 33 threads

Dataset

quora, 522931 documents, BM25 over a text index, 10000 queries
truth: exact BM25 from the conformance oracle
1 file, each pinned by sha256 in datasets.json

Host

AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M
24 logical cores · 58.6 GB · kernel 7.0.0-38-generic
memory bandwidth: 75.1 GB/s aggregate (24 threads) · 43.9 GB/s single core (59% of bus)
strawmann startup banner, 2026-10-14T01:21:23Z; the Qdrant run agrees within 10%
All throughput rows every measurement, with its notes
workloadstrawmANNQdrantrationotes
W15text: BM25 query, single client1,7611,4501.21x
W15-sattext: BM25 query, saturating (closed loop)19,92713,7921.44x
W15-filteredtext: BM25 query filtered to half the points16,0116,7552.37x
W15-k0.9-b0.4text: BM25 query at k1 0.9, b 0.4, saturating19,87615,8111.26x
W15-b0text: BM25 query at k1 1.2, b 0.0, saturating21,10517,1811.23x
All latency rows closed loop included, p50 to max

A row flagged “… not server time” is one where the client saw far more than the server reported: below the rate at which a queue can form, what is left is the load generator's own scheduling.

workloadstrawmANN p50strawmANN p95strawmANN p99strawmANN p99.9strawmANN maxQdrant p50Qdrant p95Qdrant p99Qdrant p99.9Qdrant max
W15text: BM25 query, single client 539 µs1.07 ms1.33 ms1.74 ms3.93 ms548 µs1.57 ms2.26 ms3.27 ms5.69 ms
W15-sattext: BM25 query, saturating (closed loop) 3.15 ms3.93 ms4.33 ms18.02 ms23.34 ms4.13 ms9.17 ms13.41 ms21.35 ms28.85 ms
W15-filteredtext: BM25 query filtered to half the points 3.94 ms4.90 ms5.39 ms16.57 ms20.16 ms9.24 ms12.33 ms14.86 ms23.21 ms33.77 ms
W15-k0.9-b0.4text: BM25 query at k1 0.9, b 0.4, saturating 3.17 ms3.94 ms4.33 ms14.26 ms18.61 ms3.84 ms6.26 ms8.01 ms17.98 ms23.04 ms
W15-b0text: BM25 query at k1 1.2, b 0.0, saturating 2.98 ms3.71 ms4.11 ms12.57 ms17.08 ms3.54 ms5.80 ms7.50 ms18.27 ms22.61 ms
Storage and I/O block layer and syscalls, per row

The syscall rows count every descriptor, sockets included, so on a search row they measure the network rather than the disk.

strawmANNQdrant
storage on disk31.9 MiBallocated at --capacity227.8 MiB
peak RSS433.2 MiB1.6 GiB
of which anonymous399.9 MiB647.4 MiB
of which file-backed33.3 MiB191.4 MiB
disk read bytes4.0 KiB400.6 MiB
disk write bytes31.9 MiB810.3 MiB
disk read ops310,311
disk write ops51918,525
syscall reads (all fds)817,3312,824
syscall writes (all fds)506,619494,125

measured via strawmANN: proc+cgroup / Qdrant: proc+cgroup. proc supplies syscall counts and block-layer bytes, cgroup supplies block-layer operations and bytes, so a row one interface does not carry reads n/a via …. unknown means it was not measured, and 0 means it was: an engine started with no --data-dir has no store, which is the row this section exists for. storage on disk is the level before the rows with a concurrent writer: during those an engine that rewrites segments is caught mid-rewrite, and the same row has read 3.47 and 10.11 GiB on two runs of one binary. peak RSS does include them, being a peak.

Per workload

A search row doing block-layer reads is an engine going to disk to answer a query.

workloadstrawmANN readstrawmANN writtenstrawmANN read opsstrawmANN write opsQdrant readQdrant writtenQdrant read opsQdrant write ops
W15-upload4.0 KiB31.9 MiB30400.6 MiB810.3 MiB10,31118,525
W150005190000
W15-sat00000000
W15-filtered00000000
W15-k0.9-b0.400000000
W15-b000000000
Scheduler and memory CPU use, run-queue wait, migrations

Whether the engine was running while it ran. of wall is CPU over elapsed, waiting is runnable-but-not-scheduled, migrations checks the pinning claim, and switches gives voluntary over involuntary — the scheduler taking the core away against the engine choosing to sleep, which per unit of work is the cheapest signal of lock contention there is. A dash is not a zero: an index build's threads exit before the row does, and threads shows what fraction of the row the survivors account for.

Time spent waiting for a core

Lower is better; the bar between a pair is the gap.

Peak memory per row

Peak RSS while the row ran, from the engine's own process. Unlike the disk counters this is not refused across the two engines: residency decides where bytes live, and this is what the process held either way.

workloadstrawmANNQdrant
cpuof wallwaitingswitches vol/involmigrationsfaults min/majthreadscpuof wallwaitingswitches vol/involmigrationsfaults min/majthreads
W15-upload8.4 s95%8.3 ms322,039 / 200107,110 / 09 72% (banked)44.6 s145%440.6 ms112,860 / 17,4648,420549,705 / 182,73649 30% (banked)
W1524.9 s88%11.4 ms150,126 / 1000126 / 0935.0 s101%4.9 ms549,691 / 89370503 / 045 0% (banked)
W15-sat18.5 s728%1.2 ms55,869 / 350109 / 0928.1 s765%77,667.6 ms60,406 / 38,83715,9802,524 / 082
W15-filtered23.0 s726%1.1 ms64,096 / 37032 / 0959.0 s793%12,853.5 ms31,166 / 25,6341,087827 / 082
W15-k0.9-b0.418.6 s729%1.0 ms55,664 / 29013 / 0924.5 s764%11,306.7 ms23,997 / 11,9564,61395 / 050
W15-b017.5 s729%1.0 ms53,567 / 2604 / 0922.5 s762%10,723.0 ms24,494 / 10,9984,941155 / 050
Waiting and stalls where the time went when the engine was not running

waiting is runnable and not scheduled, blocked on disk is not runnable at all, and pressure is PSI for the engine's cgroup (some = at least one task stalled, full = every runnable task).

Time stalled on I/O

Time every task in the engine's cgroup was stalled on I/O. Lower is better. 5 rows never stalled on I/O and are not drawn.

workloadstrawmANNQdrant
waitingblocked on diskcpu some/fullio some/fullmemory some/fullwaitingblocked on diskcpu some/fullio some/fullmemory some/full
W15-upload8.3 ms0 ms26 / 26 ms22 / 22 ms0 / 0 ms440.6 ms0 ms94 / 21 ms109 / 108 ms0 / 0 ms
W1511.4 ms0 ms46 / 46 ms0 / 0 ms0 / 0 ms4.9 ms0 ms97 / 97 ms0 / 0 ms0 / 0 ms
W15-sat1.2 ms0 ms2 / 2 ms0 / 0 ms0 / 0 ms77,667.6 ms0 ms2,861 / 3 ms0 / 0 ms0 / 0 ms
W15-filtered1.1 ms0 ms2 / 2 ms0 / 0 ms0 / 0 ms12,853.5 ms0 ms1,487 / 1 ms0 / 0 ms0 / 0 ms
W15-k0.9-b0.41.0 ms0 ms2 / 2 ms0 / 0 ms0 / 0 ms11,306.7 ms0 ms1,181 / 2 ms0 / 0 ms0 / 0 ms
W15-b01.0 ms0 ms2 / 2 ms0 / 0 ms0 / 0 ms10,723.0 ms0 ms1,105 / 2 ms0 / 0 ms0 / 0 ms

A dash under blocked on disk is not a zero: most hosts ship with kernel.task_delayacct=0, which reports the field as a permanent zero and would manufacture the strongest claim here — that the memory-resident engine never waits for a device — out of a sysctl. bench/setup.py apply turns it on.

Hardware counters cycles, DRAM loads, TLB walks, IPC per query

What the core did per query, from perf stat attached to the engine for the same window as the /proc counters. §5's cost model is a set of claims about instructions per cycle, DRAM traffic and TLB reach; these are those quantities, measured on the row the headline quotes rather than on a microbenchmark.

What one query cost, in cycles

Lower is better; the bar between a pair is the gap. Load rows are absent: they have no queries to divide by, and an absolute count under a per-query axis would be a different quantity wearing this one's label.

Demand loads from DRAM per query

Demand loads served from DRAM, times the cache line this host reports. Lower is better. Demand only: the hardware prefetcher's fills are a separate counter and are not in this number, so a row that streams — an exact scan above all — moved far more than this says. On the graph rows, where there is little for a prefetcher to predict, it is most of the traffic and is the outstanding-miss quantity §5.2 argues the engine is limited by.

TLB walks per query

Data TLB misses that reached a page walk, per query. Lower is better. §5.5 argues a memory-resident index needs hugepage care; this is what not taking it costs, and `--no-huge-pages` is the A/B arm that prices it.

Instructions per cycle

How well the core was fed while it ran. Not a score: a low IPC on a memory-bound row is what §5.2 predicts, and a high one on a row that does more work per query is not a win. Read it beside the two charts above.

Branch mispredictions per 1k instructions

A graph traversal is a chain of data-dependent branches and none of them is predictable from the last query, so this is the column that separates a slow kernel from an unpredictable walk. Per thousand instructions rather than per branch: `branches` costs a PMU counter that `ref-cycles` needs more, and MPKI is the comparable form anyway.

workloadstrawmANNQdrant
IPCGHzbranch MPKIcycles/querydemand DRAM/queryTLB walks/queryIPCGHzbranch MPKIcycles/querydemand DRAM/queryTLB walks/query
W15-upload2.452.011.005x nominal2.17---2.522.011.006x nominal1.74---
W151.772.011.006x nominal1.11926,427313.0 KiB826.02.572.011.006x nominal2.161,264,46236.1 KiB1,701.5
W15-sat2.222.011.005x nominal0.71732,566169.5 KiB63.22.892.011.006x nominal2.261,078,14560.7 KiB245.2
W15-filtered1.722.011.005x nominal5.51909,076180.2 KiB64.81.762.011.006x nominal2.352,341,590423.9 KiB1,192.5
W15-k0.9-b0.42.212.011.006x nominal0.71736,544169.7 KiB59.62.922.011.006x nominal2.21969,96153.3 KiB176.8
W15-b02.212.011.006x nominal0.73693,843171.1 KiB60.63.452.011.006x nominal2.06890,06032.9 KiB145.7

Nothing here is scaled. Where perf had to multiplex the group, the values it prints are extrapolations from the fraction of the row each counter was on, and they are withheld rather than shown — the same rule the scheduler table applies to a thread that exited. An event this host does not implement is likewise blank, never zero.

The two instruments disagree on these rows. Qdrant: perf and /proc disagree on context switches by up to 9% on W15-upload. perf keeps an exiting thread's counts and the /proc sums do not, so a gap here is usually the same lost-thread effect the scheduler table reports as coverage.

Collection config what each engine says it built

Read back from the engine rather than taken from the request. --segments 1 sets Qdrant's default_segment_number, which its optimizer treats as a target, and the segment count is the largest confound in the ef comparison.

Where the engines disagree a cell reads strawmANN / Qdrant, and is marked.

collectionsegmentspopulated segmentsrequested segmentspointsindexed vectorsvector sizehnsw mhnsw ef_constructshardsvector residency
bench151 / 2- / 1- / 1522,931522,9314161001cached
Per workload every metric of every row, engine beside engine

Metric down, engine across, so a comparison is two adjacent cells. Metrics a row did not measure are dropped rather than shown empty.

W15-upload text: load the corpus and build its BM25 index

upload · client -p 8 -t 8 -c 1 (-c bfb default) · n=522,931
metricstrawmANNQdrant
wall clock8.9 s30.8 s3.45x
of which upload3.6 s9.3 s2.57x
of which index wait3.0 s19.1 s
time to Green3.0 s (at bfb's polling floor)19.1 s
cpu8.4 s44.6 s5.28x
cpu, % of wall95%145%
waiting for a core8.3 ms440.6 ms52.89x
migrations08,420
faults min/maj107,110 / 0549,705 / 182,736
peak RSS416.6 MiB1.6 GiB3.88x
disk written31.9 MiB810.3 MiB
disk read4.0 KiB400.6 MiB

W15 text: BM25 query, single client 1.21x

upload · collection=bench15 · client -p 1 -t 2 -c 1 (-t, -c bfb default) · n=50,000
metricstrawmANNQdrant
queries/second1,7611,4501.21x
recall@101.00001.0000~equal
client p50539 µs548 µs~equal
client p991.33 ms2.26 ms1.70x
client p99.91.74 ms3.27 ms1.88x
server p50446 µs442 µs~equal
server p991.23 ms2.15 ms1.74x
wall clock28.4 s34.5 s1.21x
queries sent50,00050,000
cpu24.9 s35.0 s1.40x
cpu, % of wall88%101%
waiting for a core11.4 ms4.9 ms0.43x
migrations0370
faults min/maj126 / 0503 / 0
peak RSS430.5 MiB1.6 GiB3.76x
disk written00
disk read00

W15-sat text: BM25 query, saturating (closed loop) 1.44x

upload · collection=bench15 · client -p 64 -t 16 -c 2 · n=50,000
metricstrawmANNQdrant
queries/second19,92713,7921.44x
recall@101.00001.0000~equal
client p503.15 ms4.13 ms1.31x
client p994.33 ms13.41 ms3.10x
client p99.918.02 ms21.35 ms1.18x
server p503.08 ms3.04 ms~equal
server p994.22 ms10.46 ms2.48x
wall clock2.5 s3.7 s1.44x
queries sent50,00050,000
cpu18.5 s28.1 s1.51x
cpu, % of wall728%765%
waiting for a core1.2 ms77,667.6 ms65,753.00x
migrations015,980
faults min/maj109 / 02,524 / 0
peak RSS432.5 MiB1.6 GiB3.74x
disk written00
disk read00

W15-filtered text: BM25 query filtered to half the points 2.37x

upload · collection=bench15 · client -p 64 -t 16 -c 2 · n=50,000
metricstrawmANNQdrant
queries/second16,0116,7552.37x
recall@101.00001.0000~equal
client p503.94 ms9.24 ms2.34x
client p995.39 ms14.86 ms2.76x
client p99.916.57 ms23.21 ms1.40x
server p503.86 ms8.94 ms2.31x
server p995.27 ms14.25 ms2.70x
wall clock3.2 s7.4 s2.35x
queries sent50,00050,000
cpu23.0 s59.0 s2.57x
cpu, % of wall726%793%
waiting for a core1.1 ms12,853.5 ms11,238.61x
migrations01,087
faults min/maj32 / 0827 / 0
peak RSS432.9 MiB1.6 GiB3.74x
disk written00
disk read00

W15-k0.9-b0.4 text: BM25 query at k1 0.9, b 0.4, saturating 1.26x

upload · collection=bench15 · client -p 64 -t 16 -c 2 · n=50,000
metricstrawmANNQdrant
queries/second19,87615,8111.26x
recall@101.00001.0000~equal
client p503.17 ms3.84 ms1.21x
client p994.33 ms8.01 ms1.85x
client p99.914.26 ms17.98 ms1.26x
server p503.09 ms3.35 ms1.08x
server p994.22 ms6.70 ms1.59x
wall clock2.6 s3.2 s1.25x
queries sent50,00050,000
cpu18.6 s24.5 s1.31x
cpu, % of wall729%764%
waiting for a core1.0 ms11,306.7 ms11,334.99x
migrations04,613
faults min/maj13 / 095 / 0
peak RSS433.0 MiB1.6 GiB3.74x
disk written00
disk read00

W15-b0 text: BM25 query at k1 1.2, b 0.0, saturating 1.23x

upload · collection=bench15 · client -p 64 -t 16 -c 2 · n=50,000
metricstrawmANNQdrant
queries/second21,10517,1811.23x
recall@101.00001.0000~equal
client p502.98 ms3.54 ms1.19x
client p994.11 ms7.50 ms1.83x
client p99.912.57 ms18.27 ms1.45x
server p502.90 ms3.04 ms1.05x
server p993.99 ms6.23 ms1.56x
wall clock2.4 s3.0 s1.23x
queries sent50,00050,000
cpu17.5 s22.5 s1.28x
cpu, % of wall729%762%
waiting for a core1.0 ms10,723.0 ms10,354.01x
migrations04,941
faults min/maj4 / 0155 / 0
peak RSS433.2 MiB1.6 GiB3.73x
disk written00
disk read00
Host discipline the §7.1 gate, per check, and ambient load per row

Ambient load per row

The §7.1 gate checks the machine once, at the start. Colour is the engine, as everywhere else; a hatched bar with a red edge is a row that had another process on the box while it ran, which the gate cannot see.

strawmANN gate pass

environment hash 373e8531fd3d1bf6
okAMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M (24 logical)
okgovernor=performance
okboost disabled
oksmt=on
declared rather than required (§7.1 asks for SMT on or off, not
for off), and hashed, so these rows never share a chart with
SMT-off ones
cpu N and cpu N+12 share a core (12 pairs);
a --server-cpus or --client-cpus set holding both cpus of a
pair measures the engine on half the cores it names
okprofile=as-deployed (isolcpus=none nohz_full=none)
the scheduler is running as it ships, so these numbers describe a
deployment rather than the engine in isolation; they may not be
compared against an `isolated` run, and the environment hash
enforces that
okthp=madvise numa_nodes=1 numa_balancing=0
okperf_event_paranoid=-1
oktask_delayacct=1, so time blocked on a device is measurable
okperf at /usr/bin/perf, so `workloads.py run --perf` can attach
okcgroup io controller reaches the engines' own scope, so block-layer read/write operations are measurable
okzig=0.16.0
busy: Xorg(6%)
okmachine is quiescent: 0.08 cores of foreign load, under the 1.00 budget
measurement profile: as-deployed

Qdrant gate pass

environment hash 373e8531fd3d1bf6
okAMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M (24 logical)
okgovernor=performance
okboost disabled
oksmt=on
declared rather than required (§7.1 asks for SMT on or off, not
for off), and hashed, so these rows never share a chart with
SMT-off ones
cpu N and cpu N+12 share a core (12 pairs);
a --server-cpus or --client-cpus set holding both cpus of a
pair measures the engine on half the cores it names
okprofile=as-deployed (isolcpus=none nohz_full=none)
the scheduler is running as it ships, so these numbers describe a
deployment rather than the engine in isolation; they may not be
compared against an `isolated` run, and the environment hash
enforces that
okthp=madvise numa_nodes=1 numa_balancing=0
okperf_event_paranoid=-1
oktask_delayacct=1, so time blocked on a device is measurable
okperf at /usr/bin/perf, so `workloads.py run --perf` can attach
okcgroup io controller reaches the engines' own scope, so block-layer read/write operations are measurable
okzig=0.16.0
busy: Xorg(6%) claude(6%)
okmachine is quiescent: 0.17 cores of foreign load, under the 1.00 budget
measurement profile: as-deployed
What this does not establish

No Qdrant developer has used this tool. Nothing here has been run, checked against something already known, or disagreed with by anyone outside the project: every number has one author and one reviewer, and they are the same person. A result you can refute is more useful to us than one you accept.

It has never been pointed at a real Qdrant regression. The harness detects a known ISA slowdown and correctly reports no difference on a control row. That is internal consistency, not evidence it would flag a regression in your tree or stay quiet through a refactor. bench/harness/qdrant_ab.py exists to run two of your commits through it blind, and that experiment has not been done.

Part of the measured throughput gap is kernel width, not architecture. Read from a dev checkout: Qdrant's distance path tops out at four 256-bit accumulators and has no AVX-512, while these kernels use 512-bit ones on a machine that has them. That is a real difference and it is not the same claim as "the design is faster".

The engines are not equivalent, by construction. §1 removes sharding, replication, consensus, snapshots, sparse vectors, multivectors and disk-resident operation — permanently. Payload storage and filtering were in that list until 2026-09-03 and are now built (M7), so W12 measures a filtered search with a keyword index on both engines. Anything still unbuilt answers UNIMPLEMENTED naming the construct and reports n/a, never a silent degradation. A strawman that was not faster would mean it was badly built; the question is by how much, and where the model was wrong.

Reproducing this

Everything below runs from a clone. The engine has no dependencies; the analysis path is a uv project pinned by bench/uv.lock.

scripts/doctor.py                    # what can this machine measure?
bench/setup.py check                 # §7.1 host gate: governor, boost, isolation, idle
conformance/datasets/datasets.py fetch

# one engine at a time, §7.1
zig build -Doptimize=ReleaseFast
# --connections matters: W4 opens ~32 sockets (-t 16 -c 2) and a server with
# fewer closes the excess before the HTTP/2 preface, which the client reports
# only as "transport error".
./zig-out/bin/strawmann --port 6334 --connections 64
bench/harness/workloads.py run http://localhost:6334 strawmann --sink --report

# the other half of every number: recall, and the licence to compare at all
cd conformance
cargo run --release -- relevance --engine http://localhost:6334 --label strawmann \
  --base $DATA/<base>.fbin --queries $DATA/<queries>.fbin \
  --ground-truth $DATA/gt/<ground truth>.json --metric <metric> \
  --ef 32 64 128 256 512 --json ../bench/results/strawmann/recall.json
cargo run --release -- differ --strawmann http://localhost:6334 --qdrant http://localhost:6434 \
  --base $DATA/<base>.fbin --queries $DATA/<queries>.fbin \
  --ground-truth $DATA/gt/<ground truth>.json \
  --json ../bench/results/strawmann/conformance.json

uv run --project bench bench/harness/report.py strawmann qdrant

Disagreements are the point. docs/spec.md is what all of this is measured against, docs/bugs.md lists the measurement bugs found so far (each produced a plausible wrong number rather than a failure), and docs/validation.md records what has and has not been checked.