Tasks/Chips & Compute Systems/Data Systems

Filtered nearest-neighbour search at 10M scale

Build a filtered ANN index over 10M real vectors

bigann_filtered_vector_search Chips & Compute Systems Data Systems
instruction.mdthis is what the agent is given

A photo library holds 10,000,000 images. Each is a 192-dimensional uint8 embedding plus a set of metadata tags (uploader, camera, place, year, and so on). A query arrives as an embedding plus a tag predicate: one or two tags. An image is a valid answer only if its tag set contains every tag in the predicate. Among the valid images, return the 10 nearest by squared L2 distance. Your job is to make that fast.

Hard Constraints

  • Edit /app/methods/main/solver.py in place. /app/methods/main/ is what gets graded.
  • Keep the exact Solver class contract below. build() is called once, then search_batch() is called once with the entire workload; the row order of what search_batch() returns must match the query order.
  • Answers must satisfy the predicate: an image counts only if its tag set contains every tag in the query's predicate.
  • build() may use every core. Before the timed call every thread of the process is pinned to one CPU (sched_setaffinity, applied to each existing thread and inherited by any created later), so search throughput cannot be bought with parallelism.
  • There is no network at run time: whatever you use has to be already installed or written here. pip list and the image's package manifest are the authoritative record of what is available.

What You Have

/app/data/ holds the library in its published binary form:

file contents
base.10M.u8bin int32 n, int32 d, then n*d raw bytes -- the [10000000, 192] uint8 matrix
base.metadata.10M.spmat int64 nrow, ncol, nnz, then int64 indptr[nrow+1], then int32 indices[nnz] -- the tag sets as CSR
query_vectors.npy 500 visible queries, uint8 [500, 192]
query_tag_indptr.npy, query_tag_indices.npy the visible queries' predicates, same CSR convention
ground_truth.npy the correct top-10 row ids for each visible query, int32 [500, 10]

ann_utils.py (next to your solver) has readers for both binary formats, the tag predicate, a squared-L2 helper, an inverted-index builder and a plain k-means IVF index. All of it is yours to use, rewrite or delete.

base_vectors is memory-mapped. The container has 32 GB of RAM.

Run python /app/selfcheck.py to get your own recall@10 and QPS on the visible queries under the same protocol as the grader, including the one-CPU pinning. It reports the raw metrics only.

What You Submit

Leave your best implementation in /app/methods/main/solver.py, exposing exactly:

class Solver:
    def build(self, base_vectors, tag_indptr, tag_indices):
        """Called once, before search_batch(). Untimed, and free to use every core.
        base_vectors : uint8 memmap [10000000, 192]
        tag_indptr   : int64 [10000001]   CSR row pointers
        tag_indices  : int32 [108210476]  row i carries tags tag_indices[indptr[i]:indptr[i+1]]
        """

    def search_batch(self, query_vectors, query_tag_indptr, query_tag_indices, k=10):
        """Called ONCE with the entire workload. This call is what gets timed.
        query_vectors    : uint8 [n, 192]
        query_tag_indptr : int64 [n + 1]    same CSR convention as the library's tags
        query_tag_indices: int32 [nnz]      query i has tags indices[indptr[i]:indptr[i+1]]
        returns          : [n, k] library row indices; row order must match the queries,
                           order within a row does not matter
        """

There is no submit step and no per-attempt feedback. Work and self-check for as long as your run window allows, then leave your best solver.py in place.

How It Is Judged

Two numbers, both computed on a sealed query workload you never see — a different, larger sample from the same source as your visible queries:

  • recall@10 against the official ground truth, averaged over the hidden queries. This is a gate: below 0.90 the submission scores zero no matter how fast it is.
  • QPS = hidden query count divided by the wall-clock seconds of the single search_batch() call. build() is not counted and has its own, generous, budget.

Your score rises monotonically with QPS once the gate is met, and is not capped at the top. The starter as shipped is the zero of that scale: submitted unchanged it scores 0.

Metric

QPS of one batch search call at recall@10 >= 0.90 · higher is better

1,500 hidden queries / seconds of the single search_batch call, pinned to one CPU. Build is untimed.

anchorvisible setheld-outreward
Bshipped starter, IVF post-filter187.43184.550.00
SParlayANN parlayivf, config q23,682.190.60
Uideal router, correct rows for free106,915.73132,714.341.00
normalisation
recall@10 < 0.900
m <= B0
B < m <= S0.6 * t**1.5, t = (log m - log B)/(log S - log B)
m > S0.6 + 0.4 * (log m - log S)/(log U - log S)

m = this run's held-out metric  ·  B = shipped starter, IVF post-filter  ·  S = ParlayANN parlayivf, config q2  ·  U = ideal router, correct rows for free

All in log(QPS); the band spans 185 to 132,714. gamma = 1.5 makes the last stretch up to SOTA cost more. One number, no per-case average.

Rollouts

182 minwall clock
$26.32spend
32.1Mtokens
6versions, 6 kept
6k 8k 10k 12k 14k 16k $0 $7.5 $15 $22 cumulative spend on the run QPS on the 500 visible queries, higher is better v0 v1 v2 v3 v4 v5
keptrevertedno scoreturning point
  1. v0Shipped starter archived unchanged: inverted index, one global IVF, python loopnot measured27 min · $4.11
  2. v1Full C rewrite: IVF nlist=2048, 4-bit PQ fast scan per (tag, cell), AVX-512 rerankCompile the search and lay codes out per (tag, cell), so a predicate scan is one contiguous block scan, not a set intersection.6,78152 min · $6.86
  3. v2Cost model picks full scan / fine cells / coarse groups per query; groups G=512Different predicates want different index levels, and coarse groups make posting runs long enough to fill a 32-vector block.9,207140 min · $19.54
  4. v3Bloom hash bits per tag 8 -> 2; multi-tag fallback re-verifies the same coverageMeasure the filter, do not assume it: 8 bits/tag gives 8% false positives, not 0.3%; the tag tail (p99 = 62 tags) saturates it.11,144140 min · $19.54
  5. v4PQ subquantizers swept at matched recall: 48 beats 64 and 32; mt_mul=1, gboost=216,351172 min · $24.80
  6. v5Run-ahead prefetch of the next posting runs (pfrun=2); batched centroid reduction16,351178 min · $25.62

Six snapshots in 3 h for $26. v2 and v3 were archived by one command at 140 min, so the timeline puts both at the same cost and elapsed time.

On the hidden set

held-out metricreward
shipped starter, IVF post-filter184.550.00
ParlayANN parlayivf, config q23,682.190.60
ideal router, correct rows for free132,714.341.00
this run4,776.610.629
88 minwall clock
$29.29spend
44.1Mtokens
34versions, 28 kept
0 1,500 3,000 4,500 6,000 7,500 $0 $7.5 $15 $22 cumulative spend on the run QPS on the 500 visible queries, higher is better v0 v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 v21 v22 v23 v24 v25 v26 v27 v28 v29 v30 v31 v32 v33
keptrevertedno scoreturning point
  1. v0Inherited starter unchanged: inverted index, global IVF-1024, 32 probes101.331 min · $0.21
  2. v1Binary-search two-tag intersection; exact scoring under 10K candidate rows116.115 min · $0.80
  3. v2Build-time per-tag postings grouped by IVF cell; single-tag gathers probed ranges175.3910 min · $1.50
  4. v3Fused AVX-512 uint8 L2 and top-k kernel compiled in the untimed buildScoring cost was interpreted numpy, not arithmetic; compile one fused distance and top-k kernel during the untimed build.cached warm 472-80315 min · $2.26
  5. v4Cell selection by precomputed centroid norms and one float32 matrix-vector productcached warm 859-1,22816 min · $2.52
  6. v5First native intersection and fused cell top-k; pointer conversion raised TypeErrorTypeError, no result18 min · $2.87
  7. v6Native two-tag intersection, fused cell-range scoring, exact under 100K rowscached warm 1,188-1,48619 min · $3.01
  8. v7Single-thread AVX-512 centroid selection to avoid pinned BLAS contention; IVF-2048cached warm 1,69427 min · $4.51
  9. v8Sorted-posting binary/merge crossover raised 8x -> 256xpair scan 137-146 ms29 min · $5.05
  10. v9Dense membership bitmaps for tags over 20K rows (566 x 1.25MB); SIMD pair filterTwo-tag intersection was the bottleneck; above 20K rows a 1.25MB membership bitmap turns merging into a filtered scan.cached warm 2,834-2,85431 min · $5.36
  11. v10Probe schedule chosen by candidate-count band, 24/20/10/8/8cached warm 3,420-3,50533 min · $5.92
  12. v11Query pre-widened once per exact search; AVX-512 gather/compress bitmap filtercached warm 3,505-3,62835 min · $6.47
  13. v12Cache the worst top-k slot, rescan the ten retained distances only after insertioncached warm 3,632-3,74135 min · $6.68
  14. v13Software prefetch of all three vector cache lines eight candidates aheadcached warm 5,45536 min · $6.91
  15. v14Vector prefetch lookahead 8 -> 16cached warm 5,21836 min · $7.12
  16. v15Vector prefetch lookahead 8 -> 4cached warm 5,19537 min · $7.41
  17. v1664-byte int8 PCA codes, shortlist of 100 per query, then exact rerankScore every selected candidate once on a compressed PCA code and pay the full uint8 distance only on a short rerank list.cached warm 6,59443 min · $9.03
  18. v17Fixed-array max-heap for PCA shortlist maintenance, O(log R) per insertheap sweep, ms only44 min · $9.48
  19. v18PCA-code prefetch lookahead 8 -> 16; shortlist over all candidates, not per cellwarm ms only, R64 kept45 min · $9.75
  20. v19Locked IVF-2048, probe schedule 24/20/10/8/8, shortlist 644,943.1249 min · $10.95
  21. v20Back to IVF-1024 with schedule 18/16/8/6/6 now that PCA caps scoring cost5,212.8555 min · $13.28
  22. v21IVF-512 with schedule 12/10/5/4/44,777.0859 min · $14.85
  23. v22IVF-768 with schedule 15/13/7/5/55,053.562 min · $17.19
  24. v23IVF-768 probes cut to 14/12/6/5/54,224.5866 min · $18.68
  25. v24All 500 PCA query projections batched into one matrix multiplycached warm 6,63468 min · $19.53
  26. v25Whole query loop moved into one native call with reusable scratch buffers5,225.9770 min · $19.96
  27. v26Queries internally sorted by dominant tag for posting and bitmap reusecached warm 9,33373 min · $21.28
  28. v27PCA-code prefetch lookahead 16 -> 32cached warm 9,21374 min · $21.85
  29. v28PCA-code prefetch lookahead 32 -> 64cached warm 9,00075 min · $22.43
  30. v29Cache the worst retained centroid, rescan the probe set only after insertioncached warm 9,59576 min · $23.04
  31. v30Shortlist 64 -> 50 on the IVF-768 schedule6,959.477 min · $23.77
  32. v31Native single-query fallback guarded to k <= 64; NumPy fallback query inithardening, not timed81 min · $25.63
  33. v32Selected cell row-ids concatenated into native scratch so prefetch spans short runs5,530.1983 min · $26.45
  34. v33100K entries reserved up front for the reusable pair and cell scratch vectors5,185.2286 min · $27.73

34 snapshots in 88 min for $29. Only 12 ran the official self-check; the rest were tuned on a cached index, whose warm QPS reads far higher.

On the hidden set

held-out metricreward
shipped starter, IVF post-filter184.550.00
ParlayANN parlayivf, config q23,682.190.60
ideal router, correct rows for free132,714.341.00
this run4,775.210.629
90 minwall clock
$15.01spend
22.6Mtokens
21versions, 15 kept
0 750 1,500 2,250 3,000 $0 $3 $6 $9 $12 cumulative spend on the run QPS on the 500 visible queries, higher is better v0 v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20
keptrevertedno scoreturning point
  1. v0Inherited 1024-list global IVF, 32 probes; baseline snapshot, recall 0.9278174.050 min · $0.12
  2. v1AVX2 uint8 fused filter/gather/distance kernel; centroid distances in one GEMMThe starter's cost was python-level array shuffling, not arithmetic. One fused native kernel gave 4.33x at unchanged recall.752.698 min · $0.75
  3. v2Exact search for predicates matching <= 1,000 rows, cardinality found nativelySmall predicates are cheaper answered exactly, and the recall that buys pays for fewer probes. Cutoff 1K, not 100K, for margin.718.8813 min · $1.25
  4. v3Dense per-tag/per-cell postings plus CSR row-tag membership instead of merging525.8717 min · $1.72
  5. v4Keep dense postings for one-tag queries, restore sorted merge for two-tag904.5318 min · $1.94
  6. v5Exact cutoff to 3K; probe count depends on the rarest marginal posting1,208.0225 min · $2.73
  7. v6Binary-advance two-posting intersection above 64x size skew; buffer <=3K1,685.0330 min · $3.27
  8. v7IVF resolution doubled to 2,048 cells, probes retuned to {21,25,28,35}1,751.6935 min · $4.05
  9. v84,096 IVF cells with 250K assignment chunkssweep only, 1,822 QPS40 min · $4.72
  10. v9Exact early-abandon L2 with a bound check after every 64 dimensions1,820.2344 min · $5.36
  11. v10Runtime-dispatched sweep over the early-abandon checkpoint stridesweep, 1,672-1,85547 min · $6.03
  12. v111.92 GB scoring copy with dimensions permuted by descending library varianceEarly abandonment only pays if the first dimensions carry the most distance, so reorder the scoring copy by library variance.2,023.3951 min · $6.56
  13. v12Sort probed cells by centroid distance to tighten the top-10 bound earlier1,995.1653 min · $7.07
  14. v13Move down the pre-screened frontier to probes {18,21,24,30}2,106.4857 min · $7.61
  15. v14Group queries by exact predicate, write results back to original positions2,154.5360 min · $8.15
  16. v15Galloping instead of full-range binary advance on skewed two-tag postings2,311.6965 min · $9.26
  17. v16AVX-512BW exact L2 kernel, same 64-dimension early bounds2,644.4468 min · $9.89
  18. v17Fuse the int16 multiply-add with VNNI vpdpwssd2,618.2271 min · $10.51
  19. v18Explicit first-cache-line prefetch of future candidate vectors, lookahead 242,726.2977 min · $11.78
  20. v19Fine recall-frontier sweep: probes {16,19,22,27} with exact search below 4K2,754.6883 min · $13.10
  21. v20Intermediate 3,072-cell IVF with 300K assignment chunkssweep, 2,533-2,82286 min · $14.02

21 snapshots in 90 min for $15. Six were reverted; v8, v10 and v20 gave only sweep numbers. v19 was restored at the end, re-measured at 2,757.30.

On the hidden set

held-out metricreward
shipped starter, IVF post-filter184.550.00
ParlayANN parlayivf, config q23,682.190.60
ideal router, correct rows for free132,714.341.00
this run2,257.340.4591
136 minwall clock
-spend
-tokens
14versions, 14 kept
1,000 2,000 3,000 4,000 5,000 0 40 80 120 160 agent step (this harness reports no tokens or timestamps) QPS on the 500 visible queries, higher is better v0 v1 v2 v3 v5 v6 v8 v10 v13 v14 v15 v18 v19 v20
keptrevertedno scoreturning point
  1. v0Inherited starter unchanged: global IVF-1024, 32 probes, inverted-index intersectnot measured
  2. v1C AVX-512 exact L2; inverted index in IVF-cell order, exhaustive under 50K rowsThe starter's cost was numpy, not arithmetic; compile exact L2 and store postings in IVF-cell order so a probe reads one run.768
  3. v2Search params only: nprobe 64 -> 24, exhaustive cutoff 2K, swept on the tune split1,225
  4. v3Tags over 20K rows packed into contiguous vectors with per-cell pointers; nprobe 48Copy each big tag's rows into one contiguous block, so filtering is a sequential scan and extra probes turn cheap.1,353
  5. v5Membership bitset for tags over 50K rows; fused two-tag scanProfiling put two-tag intersection at 217 of 370 ms; a bitset turns that merge into a filter over the rarer posting.2,510
  6. v6nprobe 48 -> 24, chosen over 16 because the hold-out split matched the tune split3,230
  7. v8PACK_MIN 20K -> 8K, BITSET_MIN 50K -> 20K; packed exhaustive and packed bitset scan4,251
  8. v10Prefetch only the bitset hits in the packed two-tag scan4,594
  9. v13Query pre-unpacked into six ZMM int16 registers, reused across the whole L2 scan4,733
  10. v14nprobe 24 -> 205,156.04
  11. v15Two-wide sequential L2 in the contiguous scans, compiled with -funroll-loops5,337.88
  12. v18search_batch grouped by predicate so a tag's packed block stays in cache5,294.39
  13. v19BITSET_MIN 20K -> 8K; unused remapped CSR droppedofficial 4,653-5,663
  14. v20Prefault X, packs, postings and bitsets in the untimed build; -march=native5,309.11

14 snapshots in 136 min; this harness reports no token or cost data. QPS is the agent's own 500-query eval up to v13, official self-check from v14.

On the hidden set

held-out metricreward
shipped starter, IVF post-filter184.550.00
ParlayANN parlayivf, config q23,682.190.60
ideal router, correct rows for free132,714.341.00
this run2,078.370.4365
76 minwall clock
$0.70spend
2.9Mtokens
3versions, 3 kept
180 210 240 270 300 $0 $0.2 $0.3 $0.5 cumulative spend on the run QPS on the 500 visible queries, higher is better v1 v2 v3
keptrevertedno scoreturning point
  1. v1Contiguous uint8 cache, precomputed norms, int32 L2; IVF bypassed under 5K rowsHold the vectors in one contiguous uint8 array and answer small predicates exactly, so the hot loop is int32 arithmetic.173.359 min · $0.18
  2. v2Retuned to NLIST=2048, NPROBE=20; recall margin traded down from 0.978 to 0.941Smaller IVF cells let fewer probes cover the same rows, so spend the spare recall margin above the gate on throughput.284.2639 min · $0.34
  3. v3np.take for candidate slicing; partition instead of argsort on tiny candidate lists303.7257 min · $0.52

Three snapshots in 76 min for $0.70, all pure numpy. Two later self-checks of v3 read 299.56 and 322.06 QPS; the log keeps 303.72.

On the hidden set

held-out metricreward
shipped starter, IVF post-filter184.550.00
ParlayANN parlayivf, config q23,682.190.60
ideal router, correct rows for free132,714.341.00
this run277.740.0303