strawmANN against Qdrant · AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M (24 logical) · generated 2026-10-14 06:03:31 CEST
strawmANN serves 3.68x to 6.30x 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 8,674 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.
Every figure is the median of 3 passes per engine, run alternately (A/B/A/B). What this does not establish.
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.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.
| workload | strawmANN | Qdrant | ratio | notes | |
|---|---|---|---|---|---|
| W15 | text: BM25 query, single client | 1,635 | 298 | 5.50x | |
| W15-sat | text: BM25 query, saturating (closed loop) | 16,486 | 2,631 | 6.27x | |
| W15-filtered | text: BM25 query filtered to half the points | 9,166 | 2,488 | 3.68x | |
| W15-k0.9-b0.4 | text: BM25 query at k1 0.9, b 0.4, saturating | 16,424 | 2,737 | 6.00x | |
| W15-b0 | text: BM25 query at k1 1.2, b 0.0, saturating | 17,737 | 2,817 | 6.30x |
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.
| rows | variant | strawmANN nDCG@10 | Qdrant nDCG@10 | strawmANN MRR@10 | Qdrant MRR@10 |
|---|---|---|---|---|---|
| W15, W15-sat | k1 1.2, b 0.75 | 0.3138 | 0.3139 | 0.2033 | 0.2033 |
| W15-filtered | k1 1.2, b 0.75, filtered to keyword_0 | 0.2615 | 0.2615 | 0.2104 | 0.2104 |
| W15-k0.9-b0.4 | k1 0.9, b 0.4 | 0.2555 | 0.2555 | 0.1632 | 0.1631 |
| W15-b0 | k1 1.2, b 0 | 0.1599 | 0.1599 | 0.0983 | 0.0983 |
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.
| workload | strawmANN | Qdrant | ||
|---|---|---|---|---|
| W15-upload | upload | 3.01 s † | 5.04 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.
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.
| strawmANN | Qdrant | |
|---|---|---|
| storage on disk | 548.0 KiBallocated at --capacity | 26.1 MiB |
| peak memory (RSS) | 144.7 MiB | 395.0 MiB |
| read from disk | 4.0 KiB | 41.3 MiB |
| written to disk | 552.0 KiB | 74.5 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.
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.
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 753b32c8da5ba0d3.
Every BM25 score strawmANN returned is within 6.001e-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 8,674 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.
strawmANNsm-arguana-perf-1014 | Qdrantqd-arguana-perf-1014 | |
|---|---|---|
| engine | commit 1bb4b85e0580ReleaseFast, native build, vnni onbinary sha256 0e3374874428e058built 2026-10-13T19:44:01Z with zig 0.16.0 | version 1.19.3-dev binary ~/.cache/strawmann/qdrant-ab/bin/qdrant-dev-d6d3bd35bsha256 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 |
| measured | 2026-10-14T00:45:09Z to 2026-10-14T01:04:34Z (3 passes) | 2026-10-14T00:47:33Z to 2026-10-14T01:10:30Z (3 passes) |
| profile | as-deployed · both engines | |
| §7.1 gate | pass · both engines | |
| environment hash | 373e8531fd3d1bf6 · both engines | |
| load generator | bfb 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 | |
| client | qdrant-client 1.16.1-dev (git dev branch) · both engines | |
| launched as | ~/Workspace/strawmann/zig-out/bin/strawmann --port 6334 --capacity 8674 --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 use | 4-11 (8 cores) requested by the harness4-11 (8 cores) observed on 8 threads0-23 (24 cores) observed on 1 thread | 4-11 (8 cores) requested by the harness4-11 (8 cores) observed on 33 threads |
datasets.json| workload | strawmANN | Qdrant | ratio | notes | |
|---|---|---|---|---|---|
| W15 | text: BM25 query, single client | 1,635 | 298 | 5.50x | |
| W15-sat | text: BM25 query, saturating (closed loop) | 16,486 | 2,631 | 6.27x | |
| W15-filtered | text: BM25 query filtered to half the points | 9,166 | 2,488 | 3.68x | |
| W15-k0.9-b0.4 | text: BM25 query at k1 0.9, b 0.4, saturating | 16,424 | 2,737 | 6.00x | |
| W15-b0 | text: BM25 query at k1 1.2, b 0.0, saturating | 17,737 | 2,817 | 6.30x |
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.
| workload | strawmANN p50 | strawmANN p95 | strawmANN p99 | strawmANN p99.9 | strawmANN max | Qdrant p50 | Qdrant p95 | Qdrant p99 | Qdrant p99.9 | Qdrant max | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| W15 | text: BM25 query, single client | 595 µs | 831 µs | 1.03 ms | 1.26 ms | 1.93 ms | 3.13 ms | 6.00 ms | 7.95 ms | 11.65 ms | 14.56 ms |
| W15-sat | text: BM25 query, saturating (closed loop) | 3.84 ms | 4.40 ms | 4.60 ms | 15.99 ms | 18.60 ms | 23.85 ms | 31.46 ms | 40.65 ms | 57.67 ms | 86.89 ms |
| W15-filtered | text: BM25 query filtered to half the points | 6.90 ms | 8.13 ms | 8.52 ms | 21.84 ms | 26.05 ms | 25.30 ms | 31.84 ms | 34.93 ms | 39.00 ms | 49.74 ms |
| W15-k0.9-b0.4 | text: BM25 query at k1 0.9, b 0.4, saturating | 3.85 ms | 4.40 ms | 4.58 ms | 18.82 ms | 21.61 ms | 23.01 ms | 28.96 ms | 31.43 ms | 35.11 ms | 46.54 ms |
| W15-b0 | text: BM25 query at k1 1.2, b 0.0, saturating | 3.56 ms | 4.05 ms | 4.23 ms | 18.77 ms | 21.02 ms | 22.39 ms | 28.00 ms | 30.38 ms | 34.62 ms | 45.04 ms |
The syscall rows count every descriptor, sockets included, so on a search row they measure the network rather than the disk.
| strawmANN | Qdrant | |
|---|---|---|
| storage on disk | 548.0 KiBallocated at --capacity | 26.1 MiB |
| peak RSS | 144.7 MiB | 395.0 MiB |
| of which anonymous | 58.5 MiB | 242.2 MiB |
| of which file-backed | 2.0 MiB | 47.5 MiB |
| disk read bytes | 4.0 KiB | 41.3 MiB |
| disk write bytes | 552.0 KiB | 74.5 MiB |
| disk read ops | 3 | 1,293 |
| disk write ops | 14 | 3,354 |
| syscall reads (all fds) | 901,973 | 328 |
| syscall writes (all fds) | 498,924 | 495,559 |
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.
A search row doing block-layer reads is an engine going to disk to answer a query.
| workload | strawmANN read | strawmANN written | strawmANN read ops | strawmANN write ops | Qdrant read | Qdrant written | Qdrant read ops | Qdrant write ops |
|---|---|---|---|---|---|---|---|---|
| W15-upload | 4.0 KiB | 552.0 KiB | 3 | 0 | 41.3 MiB | 74.5 MiB | 1,293 | 3,354 |
| W15 | 0 | 0 | 0 | 14 | 0 | 0 | 0 | 0 |
| W15-sat | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| W15-filtered | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| W15-k0.9-b0.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| W15-b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
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.
Lower is better; the bar between a pair is the gap.
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.
| workload | strawmANN | Qdrant | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cpu | of wall | waiting | switches vol/invol | migrations | faults min/maj | threads | cpu | of wall | waiting | switches vol/invol | migrations | faults min/maj | threads | |
| W15-upload | 0.8 s | 14% | 0.3 ms | 8,908 / 3 | 0 | 11,803 / 0 | 9 95% (banked) | 4.1 s | 57% | 36.2 ms | 5,047 / 2,606 | 272 | 68,306 / 35,828 | 49 28% (banked) |
| W15 | 27.2 s | 89% | 12.3 ms | 150,507 / 40 | 0 | 0 / 0 | 9 | 166.9 s | 99% | 17.7 ms | 559,848 / 766 | 681 | 5,571 / 0 | 45 |
| W15-sat | 22.6 s | 736% | 1.5 ms | 54,614 / 34 | 0 | 171 / 0 | 9 | 151.1 s | 793% | 97,131.1 ms | 45,925 / 47,711 | 3,286 | 4,818 / 0 | 50 0% (banked) |
| W15-filtered | 39.8 s | 723% | 3.2 ms | 72,703 / 69 | 0 | 121 / 0 | 9 | 160.3 s | 796% | 28,857.7 ms | 45,455 / 42,996 | 462 | 2,893 / 0 | 50 |
| W15-k0.9-b0.4 | 22.6 s | 734% | 1.4 ms | 54,712 / 31 | 0 | 43 / 0 | 9 | 146.0 s | 797% | 26,811.8 ms | 42,695 / 40,682 | 333 | 2,440 / 0 | 50 |
| W15-b0 | 21.1 s | 738% | 1.0 ms | 51,778 / 22 | 0 | 12 / 0 | 9 | 141.8 s | 797% | 26,408.5 ms | 42,628 / 40,540 | 440 | 1,767 / 0 | 50 |
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 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.
| workload | strawmANN | Qdrant | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| waiting | blocked on disk | cpu some/full | io some/full | memory some/full | waiting | blocked on disk | cpu some/full | io some/full | memory some/full | |
| W15-upload | 0.3 ms | 0 ms | 2 / 2 ms | 0 / 0 ms | 0 / 0 ms | 36.2 ms | 0 ms | 11 / 2 ms | 44 / 44 ms | 0 / 0 ms |
| W15 | 12.3 ms | 0 ms | 36 / 36 ms | 0 / 0 ms | 0 / 0 ms | 17.7 ms | 0 ms | 197 / 197 ms | 0 / 0 ms | 0 / 0 ms |
| W15-sat | 1.5 ms | 0 ms | 2 / 2 ms | 0 / 0 ms | 0 / 0 ms | 97,131.1 ms | 0 ms | 5,602 / 2 ms | 0 / 0 ms | 0 / 0 ms |
| W15-filtered | 3.2 ms | 0 ms | 2 / 2 ms | 0 / 0 ms | 0 / 0 ms | 28,857.7 ms | 0 ms | 3,565 / 1 ms | 0 / 0 ms | 0 / 0 ms |
| W15-k0.9-b0.4 | 1.4 ms | 0 ms | 2 / 2 ms | 0 / 0 ms | 0 / 0 ms | 26,811.8 ms | 0 ms | 3,322 / 1 ms | 0 / 0 ms | 0 / 0 ms |
| W15-b0 | 1.0 ms | 0 ms | 2 / 2 ms | 0 / 0 ms | 0 / 0 ms | 26,408.5 ms | 0 ms | 3,267 / 1 ms | 0 / 0 ms | 0 / 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.
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.
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 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.
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.
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.
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.
| workload | strawmANN | Qdrant | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| IPC | GHz | branch MPKI | cycles/query | demand DRAM/query | TLB walks/query | IPC | GHz | branch MPKI | cycles/query | demand DRAM/query | TLB walks/query | |
| W15-upload | 2.81 | 2.011.006x nominal | 2.55 | - | - | - | 2.99 | 2.011.006x nominal | 2.33 | - | - | - |
| W15 | 2.66 | 2.011.007x nominal | 3.59 | 1,038,098 | 13.6 KiB | 679.6 | 3.22 | 2.011.007x nominal | 2.91 | 6,472,116 | 36.1 KiB | 1,834.4 |
| W15-sat | 3.06 | 2.011.005x nominal | 2.83 | 895,424 | 6.8 KiB | 46.4 | 3.43 | 2.011.006x nominal | 2.82 | 6,029,819 | 51.3 KiB | 195.1 |
| W15-filtered | 1.90 | 2.011.006x nominal | 7.96 | 1,578,120 | 6.3 KiB | 67.2 | 3.35 | 2.011.006x nominal | 2.68 | 6,401,733 | 53.3 KiB | 179.3 |
| W15-k0.9-b0.4 | 3.05 | 2.011.006x nominal | 2.83 | 897,995 | 7.0 KiB | 46.3 | 3.41 | 2.011.006x nominal | 2.79 | 5,827,379 | 45.6 KiB | 150.6 |
| W15-b0 | 3.02 | 2.011.005x nominal | 3.10 | 834,359 | 7.1 KiB | 45.3 | 3.47 | 2.011.006x nominal | 2.75 | 5,659,826 | 45.3 KiB | 148.4 |
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 17% 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.
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.
| collection | segments | populated segments | requested segments | points | indexed vectors | vector size | hnsw m | hnsw ef_construct | shards | vector residency |
|---|---|---|---|---|---|---|---|---|---|---|
| bench15 | 1 / 2 | - / 1 | - / 1 | 8,674 | 8,674 | 4 | 16 | 100 | 1 | cached |
Metric down, engine across, so a comparison is two adjacent cells. Metrics a row did not measure are dropped rather than shown empty.
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| wall clock | 5.7 s | 7.2 s | 1.27x |
| of which upload | 0.6 s | 0.1 s | 0.12x |
| of which index wait | 3.0 s | 5.0 s | |
| time to Green | 3.0 s (at bfb's polling floor) | 5.0 s | |
| cpu | 0.8 s | 4.1 s | 5.29x |
| cpu, % of wall | 14% | 57% | |
| waiting for a core | 0.3 ms | 36.2 ms | 135.37x |
| migrations | 0 | 272 | |
| faults min/maj | 11,803 / 0 | 68,306 / 35,828 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 552.0 KiB | 74.5 MiB | |
| disk read | 4.0 KiB | 41.3 MiB |
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| queries/second | 1,635 | 298 | 5.50x |
| recall@10 | 1.0000 | 1.0000 | ~equal |
| client p50 | 595 µs | 3.13 ms | 5.26x |
| client p99 | 1.03 ms | 7.95 ms | 7.73x |
| client p99.9 | 1.26 ms | 11.65 ms | 9.22x |
| server p50 | 499 µs | 2.93 ms | 5.87x |
| server p99 | 930 µs | 7.67 ms | 8.25x |
| wall clock | 30.6 s | 168.1 s | 5.49x |
| queries sent | 50,000 | 50,000 | |
| cpu | 27.2 s | 166.9 s | 6.13x |
| cpu, % of wall | 89% | 99% | |
| waiting for a core | 12.3 ms | 17.7 ms | 1.44x |
| migrations | 0 | 681 | |
| faults min/maj | 0 / 0 | 5,571 / 0 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 0 | 0 | |
| disk read | 0 | 0 |
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| queries/second | 16,486 | 2,631 | 6.27x |
| recall@10 | 1.0000 | 1.0000 | ~equal |
| client p50 | 3.84 ms | 23.85 ms | 6.21x |
| client p99 | 4.60 ms | 40.65 ms | 8.83x |
| client p99.9 | 15.99 ms | 57.67 ms | 3.61x |
| server p50 | 3.75 ms | 23.24 ms | 6.20x |
| server p99 | 4.49 ms | 35.80 ms | 7.97x |
| wall clock | 3.1 s | 19.1 s | 6.20x |
| queries sent | 50,000 | 50,000 | |
| cpu | 22.6 s | 151.1 s | 6.68x |
| cpu, % of wall | 736% | 793% | |
| waiting for a core | 1.5 ms | 97,131.1 ms | 64,300.58x |
| migrations | 0 | 3,286 | |
| faults min/maj | 171 / 0 | 4,818 / 0 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 0 | 0 | |
| disk read | 0 | 0 |
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| queries/second | 9,166 | 2,488 | 3.68x |
| recall@10 | 1.0000 | 1.0000 | ~equal |
| client p50 | 6.90 ms | 25.30 ms | 3.66x |
| client p99 | 8.52 ms | 34.93 ms | 4.10x |
| client p99.9 | 21.84 ms | 39.00 ms | 1.79x |
| server p50 | 6.81 ms | 24.94 ms | 3.66x |
| server p99 | 8.40 ms | 34.47 ms | 4.10x |
| wall clock | 5.5 s | 20.1 s | 3.66x |
| queries sent | 50,000 | 50,000 | |
| cpu | 39.8 s | 160.3 s | 4.03x |
| cpu, % of wall | 723% | 796% | |
| waiting for a core | 3.2 ms | 28,857.7 ms | 8,995.21x |
| migrations | 0 | 462 | |
| faults min/maj | 121 / 0 | 2,893 / 0 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 0 | 0 | |
| disk read | 0 | 0 |
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| queries/second | 16,424 | 2,737 | 6.00x |
| recall@10 | 1.0000 | 1.0000 | ~equal |
| client p50 | 3.85 ms | 23.01 ms | 5.98x |
| client p99 | 4.58 ms | 31.43 ms | 6.86x |
| client p99.9 | 18.82 ms | 35.11 ms | 1.86x |
| server p50 | 3.75 ms | 22.66 ms | 6.04x |
| server p99 | 4.48 ms | 30.98 ms | 6.92x |
| wall clock | 3.1 s | 18.3 s | 5.94x |
| queries sent | 50,000 | 50,000 | |
| cpu | 22.6 s | 146.0 s | 6.44x |
| cpu, % of wall | 734% | 797% | |
| waiting for a core | 1.4 ms | 26,811.8 ms | 19,631.68x |
| migrations | 0 | 333 | |
| faults min/maj | 43 / 0 | 2,440 / 0 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 0 | 0 | |
| disk read | 0 | 0 |
| metric | strawmANN | Qdrant | |
|---|---|---|---|
| queries/second | 17,737 | 2,817 | 6.30x |
| recall@10 | 1.0000 | 1.0000 | ~equal |
| client p50 | 3.56 ms | 22.39 ms | 6.28x |
| client p99 | 4.23 ms | 30.38 ms | 7.18x |
| client p99.9 | 18.77 ms | 34.62 ms | 1.84x |
| server p50 | 3.47 ms | 22.01 ms | 6.35x |
| server p99 | 4.12 ms | 29.93 ms | 7.27x |
| wall clock | 2.9 s | 17.8 s | 6.22x |
| queries sent | 50,000 | 50,000 | |
| cpu | 21.1 s | 141.8 s | 6.72x |
| cpu, % of wall | 738% | 797% | |
| waiting for a core | 1.0 ms | 26,408.5 ms | 25,240.44x |
| migrations | 0 | 440 | |
| faults min/maj | 12 / 0 | 1,767 / 0 | |
| peak RSS | 144.7 MiB | 395.0 MiB | 2.73x |
| disk written | 0 | 0 | |
| disk read | 0 | 0 |
373e8531fd3d1bf6373e8531fd3d1bf6No 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.
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.