Don't Train for Quantization
Binary quantization throws away everything about an embedding except the sign of each number. A 4,096-byte vector becomes 128 bytes, and on the model I've been testing that costs about 1.5% of search quality. Two previous posts covered how well it holds up and a free way to claw back most of the loss.
So: train the model to lose less. Put the rounding step inside the training loop, let gradients flow through it, and the weights should drift toward sign patterns that survive. This is a completely standard idea. It has a name, quantization-aware training, and in the context of compressing model weights it works fine.
Applied to embedding geometry, it produced the worst retrieval score I have measured on this dataset. It also produced the best quantization error of anything I tried. Those two facts are the same fact, and untangling them turned out to be more useful than the thing I set out to build.
The collapse
Before taking signs, the library rotates each vector. A random rotation spreads information evenly across coordinates so no single dimension carries too much of what is about to be destroyed. That rotation is data-independent, which makes it an obvious candidate for learning.
Three attempts, all producing an index of identical size:
| Method | Quantization error | nDCG@10 |
|---|---|---|
| random rotation | 976.98 | 0.6772 |
| ITQ, closed form, from 2011 | 972.60 | 0.6972 |
| gradient descent through the quantizer | 922.23 | 0.4967 |
Quantization error measures how far a rounded vector sits from the original. Nothing in that definition requires the transform to preserve any information. A coordinate squashed toward zero contributes almost nothing to the error, because there is almost nothing left to round, and an unconstrained linear map is perfectly free to squash whichever coordinates are proving expensive.
That is what happened. The diagnostic I nearly didn't compute was how far the learned transform had drifted from being a rotation at all: 98.4, where a true rotation scores 0. It had flattened the space. The loss curve looked healthy throughout.
Constraining the optimizer so that every reachable point was provably a rotation fixed the collapse and gained nothing measurable. Meanwhile ITQ, an alternating least-squares procedure with no gradients, no learning rate and nothing to tune, took thirteen seconds and won. It has its own price: a learned rotation has to be shipped alongside the index, 4.19 MB for this model, which exceeds the size of the entire index below roughly 33,000 vectors.
The reversal
The other approach was to ignore quantization completely and fine-tune the model to be better at retrieval. Training signal was 221 pairs of question and relevant abstract, which is a very small training set. Nothing in the objective mentioned compression.
Scored on 100 queries the model never saw:
| full precision | 1-bit, 128 B/vector | gap | |
|---|---|---|---|
| base model | 0.8017 | 0.7806 | 0.0211 |
| fine-tuned | 0.8332 | 0.8286 | 0.0046 |
| change | +0.0314 | +0.0480 |
The compressed index improved half again as much as the uncompressed one. Binary went from 97.4% of full precision to 99.5%.
The mechanism is almost embarrassingly simple in hindsight. Contrastive training pulls relevant documents toward the query and pushes everything else away, which widens the margins between neighbouring vectors. A sign bit only destroys a distinction when two vectors sit on opposite sides of a boundary by a hair. Widen every margin and fewer distinctions are decided by a hair. Nobody has to ask for quantization robustness; it is what larger margins already are.
Which is also why the gradient method failed. It optimized the geometry of the rounding in isolation and got a geometry that rounds beautifully and retrieves badly. Nothing in its objective was attached to the task.
What the split was hiding
Measured on the queries used for training, the same fine-tune shows +0.1201. On held-out queries it shows +0.0314. Training loss fell from 0.071 to 0.009 across three epochs on 221 pairs, which is a model committing its training set to memory, and memorization reads as a spectacular result if you measure it against the thing memorized.
A near-miss on the same theme: an early run used 300 documents as the evaluation corpus instead of 2,000, because it was faster. That run reported a baseline of 0.9077. The same model against 2,000 documents scores 0.8017, and against the full 5,183 it scores 0.7122. Retrieval scores are a function of how many wrong answers are on offer, so a score without a corpus size attached carries no information, and two scores from different corpus sizes cannot be compared. That restriction applies to my own earlier posts as much as anyone's.
One thing I cannot resolve. Comparing one epoch against three on the held-out set, full precision peaks at one epoch (0.8387, against 0.8332 at three) while the 1-bit index keeps improving (0.8188 to 0.8286). If that is real, stopping when validation stops improving is the wrong rule for a compressed deployment, because the later epochs buy compression robustness after they stop buying retrieval quality. But those differences are 0.0055 and 0.0098 against a 100-query validation set, from one seed, with no confidence interval. It is a hypothesis with a single supporting observation, and I am recording it mainly because results this thin usually get dropped and then never tested by anyone.
The rest of the numbers deserve the same suspicion in proportion: one dataset, one model, one seed, and in-batch negatives capped at seven per anchor by the 15 GB of RAM available, where a serious run would use sixty-four or more. The direction of the main result is large enough to survive that. The magnitudes are not.
All of it ran on four CPU cores with no GPU, about eight minutes per training epoch. The scripts and raw numbers are in oaustegard/remax under bench/, and the fine-tuning script performs its own query-level split, so the holdout is not something you have to remember.
I went looking for a way to make quantization cheaper and found instead that it was never the thing to optimize. The compression was already nearly free. What it was waiting for was a better model.