nano-plaid · SIMD school
class 03 · builds on class 02 — the GEMM bar, arithmetic intensity

Store less; keep the order.

Classes 04–06 make compressed documents fast. This class decides what the compressed documents are. Four ways to shrink a token vector — round it, keep its sign, point at a cluster and correct, or cut the space into pieces — and one metric that judges them all: does the ranking survive? Every scheme the repo implements is measured on full SciFact; the one it doesn't (product quantization) gets an honest chapter anyway, because it's the road most of the industry took.

01 · why compress at all

The memory wall

Late interaction keeps a vector per token, and that decision has a bill. At dim 128 in float32 a token costs 512 bytes; a modest corpus has millions of tokens. Drag the corpus size and watch each storage scheme cross the line where an index stops fitting in RAM:

index size vs corpus size — bytes/token × tokens
10M

SciFact — the corpus every measured number in this school comes from — is the small end of this slider: 1.19M tokens, 610 MB in float32. A 100M-token corpus (a mid-size wiki) is a 51 GB float index: not a luxury problem, a laptop-killer. Compression isn't an optimization here; it's the admission ticket.

One more reason, and it's class 02's: the float path is memory-bound at scoring time — chapter 06 measured intensity, and bytes moved is time spent. Fewer bytes per token isn't only a smaller disk bill; it's the headroom every kernel in classes 04–05 cashes in. The question is what to throw away.

02 · the metric that judges everything

What must survive: the order

Here's the liberating fact about compressing a search index: nobody ever reads the scores. A query returns documents sorted by score — so quantization error is invisible until it swaps two documents. Add noise to the true scores and watch when the ranking actually breaks:

six documents, true scores from class 02's worked example
0.0

Schematic, deliberately — but notice the shape of the failure: small noise never touches the podium, because A leads B by 1.5 and B leads C by 6.5. Errors only flip near-ties, and near-ties barely move NDCG — swapping ranks 5 and 6 costs almost nothing; swapping 1 and 2 costs a lot, and needs error bigger than their gap. That's why a scheme can throw away 96% of its bits and keep ~98% of its NDCG: the metric the numbers below use (NDCG@10, class 02's) counts order near the top, not reconstruction fidelity.

So every scheme in this class is judged twice: bytes per token (what it costs) and retention — its NDCG@10 as a fraction of the exact-float 0.7629 on full SciFact (what it keeps). Reconstruction error is only ever a means; order is the end.

03 · the polite first cut

Round the floats: scalar quantization

The gentlest move: keep every dimension, spend fewer bits on each. Pick a scale so the largest value maps to 127, round everything to the nearest step, store int8 + one float scale per row. Randomize the input and watch what the rounding costs:

int8 encode — 8 of 128 dims shown
float32 input
int8 codes · scale = max|x| / 127
reconstruction · code × scale

4× smaller (512 → 128 B/token + 4 B scale), and the worst-case error is half a step — tiny next to the values themselves. This is the near-free tier: int8 keeps so much fidelity that this school treats it as "still basically exact." Which is precisely why it's the wrong tool for documents (4× barely dents the memory wall) and the perfect tool for queries — hold that thought for chapter 07.

Float16 sits in the same family (2×, even politer). The pattern to notice: scalar quantization shrinks the values but keeps the structure — 128 independent numbers, one per dimension. Every scheme after this one gets its bigger wins by attacking the structure itself.

04 · the brutal cut

Keep only the sign

Now the other extreme: one bit per dimension. Keep the sign, discard the magnitude — a 128-dim token becomes 16 bytes, 32× less than float32. What's left of a vector when every value is ±1? Its direction:

sign encode — same 8 dims as above
float32 input
stored bits · sign(x)
implied reconstruction · ±α (α = mean|x|)

The per-value error is huge — every magnitude is replaced by the average. But look at which errors are big: the dims that were already near zero. Embedding similarity is dominated by which way the vector points and which dims agree in sign with the query; the magnitudes the sign bit destroys carried the least ranking information. Measured on full SciFact: 0.7460 NDCG@10 — 97.8% retention — at 1/25th the storage.

This is the scheme class 04 builds its kernel around, and its quality-per-bit is the benchmark the fancier schemes below have to justify themselves against. It also sets up this class's one genuinely surprising measurement — chapter 05 ends with a scheme that spends the same 20 bytes and does 11 points worse.

05 · the scheme the engine ships

Point at a cluster, then correct

Between int8's 128 bytes and binary's 16 sits the scheme ColBERTv2 and PLAID made standard, and the one next-plaid ships: run k-means over all token vectors, store each token as a centroid id plus a low-bit correction. The centroid does the pointing; the residual does the polishing. Pick the centroid and watch what happens to the numbers that need storing:

token = centroid + residual — 8 dims
token vector
residual = token − centroid (this is what gets quantized)
residual bits/dim:

The whole trick is in the residual's size. With the right centroid, residuals are small, near-zero corrections — so a coarse 4-bit grid covers them finely. Pick a wrong centroid and the residual is as big as the data; no bit budget saves you. Storage: 4 B centroid id + nbits×128/8 B of codes — 68 B at 4 bits, 36 at 2, 20 at 1. (How the 4-bit grid is trained — one shared 16-entry quantile table, small enough to live in a register — is class 05's opening act; here it's enough that the residual is small and the grid is learned.)

Measured on full SciFact, the knob behaves like a knob should — until it doesn't: residual-4: 0.7609 (99.7%) · residual-2: 0.7635 (100.1% — parity with exact; the codec's error is below this benchmark's resolution) · residual-1: 0.7470 (97.9%). The last number has a history worth more than the number. This page originally reported 0.6312 — 11 NDCG points behind binary at the same 20 bytes — and drew a confident lesson from it about one bit of residual being mostly noise. The lesson was wrong. The engine was scoring raw reconstructions, and quantization bends unit vectors — so a token's accidental reconstruction length was deciding MaxSim. One cached scalar per token (renormalization — class 05, chapter 13) recovered +0.116 NDCG at nbits=1, and residual-1 now edges binary at equal bytes. The retraction earns its place in the course: a measured negative is only as strong as the scoring semantics it was measured under.

06 · the road the industry took

Cut the space itself: product quantization

The fourth idea is the one this repo doesn't implement — and the one behind FAISS and most billion-scale ANN systems, so it gets a real chapter. PQ splits the 128-dim space into M subspaces and k-means-quantizes each subspace separately: a token becomes M tiny codebook indices. Step through encoding and scoring a dim-8 toy (M = 4 subspaces × 2 dims, 4 entries each):

PQ in four steps — encode, then score without decoding
Step 1 splits the vector into subspaces.

At real sizes: M = 16 subspaces × 8 dims, 256 entries per codebook → 16 bytes per token, same budget as binary. Scoring uses the ADC trick you just stepped through: per query (token), dot the query's sub-vectors against every codebook entry once — building M little lookup tables — then every document token costs just M table reads and adds. No decoding, ever. Sound familiar? It should: it's the same "score the codes, skip the floats" instinct as classes 04–05, discovered a decade earlier.

So why did the ColBERT lineage pick centroid + scalar residual instead? Three honest trade-offs, not a verdict:

product quantizationcentroid + scalar residual (ch 05)
codebooksM learned tables × 256 entries — captures per-subspace structure, better rate–distortion at the same bitsone shared 16-entry table for all 128 dims — cruder, but 16 bytes total
scoring state per queryM × 256 floats of ADC tables — lives in L1/L2, a memory lookup per subspacethe table rides in a register; lookup is one instruction (class 05's tbl/pshufb)
who's already pointingPQ often carries the whole vector alonethe IVF centroid — which stage 1 needs anyway for candidate pruning (class 06) — absorbs the direction; residuals come out small and near-interchangeable, which is what lets one table serve every dim

And here's the convergence that makes the whole design space feel small: FAISS's fastest PQ variant, 4-bit fast-scan, shrinks its codebooks from 256 entries to 16 precisely so the ADC tables fit in a SIMD register and the lookup becomes pshufb — the identical instruction class 05 builds on. Two schemes, two lineages, one register-shaped conclusion: if you want compressed scoring at CPU speed, your table must fit in 16 bytes.

07 · the design rule hiding in every scheme

Two quantizers, one asymmetry

Every scheme above quantized documents. Nobody quantizes the query the same way — and the reason is just counting:

document tokens · 1,190,000 (SciFact) — stored forever, index size ∝ this query tokens · ~32 — live for one query, re-encoded every time

Compressing documents 25× saves 580 MB. Compressing the query 25× saves… 14 kilobytes, and costs ranking quality on every comparison it makes. So the query keeps chapter 03's near-free int8 (magnitude intact), documents take the brutal cuts, and the scoring problem becomes deliberately lopsided: int8 query × coded documents. The literature calls this asymmetric quantization — PQ's ADC is the same move (float query × coded docs). Classes 04 and 05 are entirely about making that lopsided arithmetic fast; when class 04 opens with "two quantizers, deliberately unequal," this chapter is the why.

08 · measured, all of it

The scoreboard

Full SciFact — 5,183 docs, 1.19M tokens, dim 128, 300 queries, numpy reference paths (eval.py --profile). Quality first, and the column that motivates the rest of the school last:

NDCG@10 by scheme — bar length = retention of exact float
bytes/token includes the 4-byte centroid id where the scheme rides the IVF index (residual + binary rows).
schemebytes/tokenNDCG@10retentionp50 ms/query · numpy
exhaustive float325120.7629100%20.0
residual nbits=4680.760999.7%111
residual nbits=2360.7635100.1%82
residual nbits=1200.747097.9%64
binary (sign bits)200.746097.8%17.7

Two readings of one table. The quality story is a triumph: 99.7% of exact ranking at 1/7.5th the bytes, 97.9% at 1/25th — and the 2-bit row at outright parity. Order really does survive brutal compression — chapter 02's promise, kept. The latency column is a scandal: the best-quality scheme is 5.5× slower than the brute-force float GEMM it was supposed to beat (111 ms vs 20), because its numpy path decompresses every candidate back to floats and hands them to BLAS — repaying the entire compression win at query time, plus interest. Compression chose what to store; it wrote a check that scoring has to cash. The next two classes are how: binary's masked-sum identity (class 04: 17.7 → 6.0 ms), then the residual family's in-register table (class 05: 111 → 6.9 ms, and the red column dies).

09 · choosing, in the wild

A field guide

The same four ideas, as they ship in real systems — so when you meet one in a paper or a codebase you can place it on this class's map:

schemebytes/token · dim 128reach for it whenshipped by
scalar int8 / f16128 / 256queries; anywhere 2–4× is enough and fidelity is sacredeveryone — it's the universal query-side move
binary (sign bits)16–20maximum compression per point of NDCG; simplest possible decoderLucene/Elasticsearch BBQ, nano-plaid's headline scheme, next-plaid's binary route
centroid + scalar residual20–68 (nbits 1–4)a tunable quality knob on top of an IVF index you already need for pruningColBERTv2 / PLAID, next-plaid's residual routes
product quantization8–32single-vector ANN at huge scale; best rate–distortion when no centroid is doing the pointingFAISS IVF-PQ, ScaNN, most vector DBs

One closing observation before the kernels. Every row of that table was invented by people optimizing storage — yet the schemes that won each niche are exactly the ones whose decode step could later be made to disappear into an instruction: sign bits into a masked sum, 16-entry tables into tbl/pshufb. The bits you choose to keep determine the kernel you're allowed to write. We've chosen our bits; now we owe them speed.

10 · check yourself

Six questions

Instant feedback, explanations included. Miss one? The chapter number is next to it.

next → class 04 The 1-bit × int8 kernel: score a document you never decompress