Skip to content

Parallel bulk insert (AddEntries) and EcoQoS opt-out in benchmarks - #29

Merged
matengo merged 1 commit into
masterfrom
insert-parallel
Sep 16, 2026
Merged

matengo merged 1 commit into
masterfrom
insert-parallel

Conversation

@matengo

@matengo matengo commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Adds QvecDatabase.AddEntries(IReadOnlyList<QvecInsert>, int maxDegreeOfParallelism = 0) — multi-threaded HNSW index construction — and fixes a measurement problem that had been silently throttling every benchmark so far.

Parallel insert

  • Chunks of 8,192 entries: phase A serial (slot, layer, vector/metadata/guid to disk, guid index), phase B Parallel.For linking nodes with hnswlib-style striped node locks (1,024 stripes, never nested) and a reader/writer lock around the entry point.
  • The whole call holds the database write lock, so concurrent readers see either the old or the new state; nothing changes for AddEntry.
  • dop = 1 produces a byte-identical graph to a loop of AddEntry (tested via RawGraph).
  • Same semantics as AddEntry: ids returned in input order, duplicate ExternalIds skipped (within a batch and against existing data), dimension mismatch throws QvecDimensionException.
  • Design doc: docs/design-insert-parallel.md.

Measured (same machine, back to back, throttling disabled)

Dataset 1 thread 12 threads Speed-up Recall
Cohere 100K float, M = 32 370.9 s (270/s) 58.6 s (1,707/s) 6.3× recall@100 −0.09 pp @ ef 100
SIFT-1M, M = 32 1,145.6 s (873/s) 142.1 s (7,037/s) 8.1× recall@10 within 0.1 pp at all ef

Trade-off, stated in README: parallel builds are not byte-reproducible (layers are seeded, link order is not). Scaling is sub-linear because ~20 % of thread time on Cohere is spent waiting on hub-node locks; the rest is memory latency.

EcoQoS discovery

12 threads initially got only ~4 cores' worth of CPU time with no kernel time, GC pauses or lock contention to explain it. Root cause: Windows 11 (Balanced plan) applies EcoQoS power throttling to console processes that are not in the foreground. The benchmark now opts out via SetProcessInformation(ProcessPowerThrottling) at startup (--allow-throttling restores the old behaviour). Serial Cohere 100K went from 589 s throttled to 371 s exempt, which also explains the historic ±40 % QPS variance. All earlier README figures were measured throttled; SIFT-1M and Cohere 100K tables are re-measured and marked as such.

Benchmark

--threads N (0 = all cores) builds through AddEntries in batches of 50,000. Report line shows build threads.

Tests

8 new ParallelInsertTests: stores every entry & recall vs serial (float + int8), byte identity at dop = 1, well-formed neighbour lists with bounded layer-0 orphan count vs serial, duplicates within batch and across calls, growth + deleted-slot reuse, empty batch no-op, wrong dimension throws.

Fast suite 333 tests + Slow suite 7 green locally; build 0 warnings.

Not in scope

  • PartitionedQvecDatabase does not get AddEntries yet.
  • Lock-free back-linking to recover the hub-lock wait.

QvecDatabase.AddEntries(IReadOnlyList<QvecInsert>, maxDegreeOfParallelism)
inserts a batch with every core linking nodes into the HNSW graph at
once. Each chunk of 8,192 entries is prepared serially (slot, layer,
vector/metadata/guid on disk, guid index) and then linked in parallel
using hnswlib-style striped node locks (1,024 stripes, never nested)
plus a reader/writer lock guarding the entry point. The whole call
holds the database write lock, so readers see either the old or the
new state. dop = 1 produces a byte-identical graph to serial AddEntry.

Measured back to back, power throttling disabled:
  Cohere 100K float  370.9 s -> 58.6 s  (6.3x, 1,707 inserts/s), recall@100 -0.09 pp
  SIFT-1M            1,145.6 s -> 142.1 s (8.1x, 7,037 inserts/s), recall@10 within 0.1 pp

Parallel builds are not byte-reproducible (layers are seeded, link
order is not); documented in README and docs/design-insert-parallel.md.

While measuring, 12 threads only got ~4 cores' worth of CPU time.
Root cause is Windows 11 EcoQoS throttling background console
processes on the Balanced power plan. The benchmark now calls
SetProcessInformation(ProcessPowerThrottling) at startup to opt out
(--allow-throttling restores the old behaviour). All earlier README
figures were measured throttled; the SIFT-1M and Cohere 100K tables
are re-measured and marked as such.

Benchmark gains --threads N (0 = all cores), building via AddEntries
in batches of 50,000.

Tests: 8 new ParallelInsertTests (recall vs serial, byte identity at
dop=1, well-formed neighbour lists with bounded orphan count, duplicate
ids within and across batches, growth and deleted-slot reuse, empty
batch, wrong dimension).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@matengo
matengo merged commit e152338 into master Sep 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant