Skip to content

Neighbour-major fast-scan blocks: kill test (development measurement)

Question: would a SymphonyQG-style neighbour-major vertex block (each vertex's neighbour ids, the neighbours' fast-scan codes and their factors, plus the vertex's own code, in one contiguous fixed-degree record) close the single-thread gap to the RaBitQ/fast-scan graph leaders on VIBE?

The index already has the code half of that layout: VGraph::with_blocks stores, per vertex, 32-code fast-scan records of the neighbours' residual codes (1, 2 or 4 bits per rotated coordinate, per-edge scale and offset), scanned with fastscan::scan32 (AVX2 vpshufb, NEON tbl, scalar). The new format would only merge the two remaining reads per expansion (the neighbour ids and the vertex's own 8-bit row) into that record. Decision rule, written before the run: with R = QPS(per-row path) / QPS(1-bit blocks) at recall@100 0.95 on a 768-d set, R >= 1.3 means contiguity cannot recover the loss and the format is not built.

Result

Paired, interleaved, locked, 1 thread pinned, Intel Core Ultra 9 185H (no AVX-512), commit 8e66e387, 3 rounds, host load 5.1-10.3 (other work on the host; the arms are interleaved per round, so the load hits every arm). Raw points, protocol and the lock log: benchmarks/results/qgraph_neighbour_blocks_ab_x86_v1.json.

Dataset (recall@100) Target Per-row path 1-bit blocks 2-bit blocks
arxiv-nomic-768, 100k slice, 256 queries 0.90 4,013 3,618 (0.90x) 3,716 (0.93x)
0.95 4,013 2,974 (0.74x) 3,082 (0.77x)
0.99 2,979 2,002 (0.67x) 2,374 (0.80x)
glove-100-angular, 10k queries 0.90 1,353 does not reach 0.90 by ef 1200 1,021 (0.75x)

QPS is the median over rounds; recall targets are interpolated log-linearly between the bracketing ef points (the per-row path already reaches 0.977 at ef 100, the floor for k = 100). Degree 32 does not reach 0.95 on GloVe on either path.

R = 4,013 / 2,974 = 1.35: the new format is not built. Noise: with 256 queries and this host load, one configuration's three rounds spread up to 1.5x (the medians are what the table uses), so R is known to perhaps +-0.2; the recall side of the loss (ef ~170 against <= 100 for 0.95) is exact and load-independent.

Why

  • At matched ef the 1-bit blocks are 1.12-1.26x faster per query on the 768-d slice (medians of this run; 1.09-1.37x in an earlier back-to-back run), so the block read itself is cheap enough. The loss is recall per ef: neighbour order from 1-bit residual estimates needs about 1.7x the ef of the 8-bit per-row estimates for 0.95 (ef ~170 against <= 100).
  • A 1-bit record is already 4,352 bytes per vertex at 768-d (32 codes of the 1024-wide rotation plus 256 bytes of factors). Folding in the 768-byte own row and 96 bytes of ids merges about 0.9 KB into a stream that is already prefetched one expansion ahead; it does not touch the estimate quality that costs 1.35x.
  • Earlier 4-bit result on nytimes (qgraph_block4_untimed_x86_v1.json): no ef penalty, 0.76-0.79x of the per-row path, index 2.7 GB.

Not covered: Euclidean sets (sift-128, fashion-mnist; the blocks are angular only), AVX-512 vpshufb on 64-byte registers (the timing host has no AVX-512), and full-size VIBE sets (the slice is 100k rows; its 8-bit rows, 77 MB, already exceed the host's last-level cache).