How remex and remax compress an embedding
An embedding is a direction. A nearest-neighbour query asks which stored direction sits closest to its own, and never asks what the coordinates were. So a 768-dimensional float32 vector spends 3072 bytes to answer a question about angles, and most of those bytes are not answering it.
remex and remax cut that number by dropping bits per coordinate rather than by dropping coordinates. At a matched byte budget that has won on every corpus we have measured: on the bekko corpus, one bit at d=384 is 48 bytes and R@10 0.564, against float32 truncated to d=64 at 256 bytes and 0.520. Five times smaller, and better.
The rotation
Drop bits per coordinate naively and it fails at once, because the coordinates of a real embedding are not alike. Some dimensions are wide, some are nearly constant, and in raw SPECTER2 coordinates one dimension has a mean of 15.5 while the rest sit near zero. That single dimension drags the whole corpus off into a lump far from the origin — measured as a ratio, the corpus mean vector is 0.92 times as long as a typical vector in it. Any fixed set of cut points is well matched to a few dimensions and wrong for all the others.
Multiply every vector by the same random orthogonal matrix. The angles between them do not move — an orthogonal matrix preserves every inner product, so the ranking after the rotation is the ranking before it, exactly. Nothing is approximated at this step and nothing is lost.
What does move is the mass. Projecting a fixed unit vector onto a uniformly random axis gives a coordinate that is a sum of d small contributions pulling in arbitrary directions, so it comes out approximately Gaussian with variance 1/d. Every coordinate of every vector, after the rotation, is a draw from that same distribution.
Now there is one distribution to design a code for instead of 768, and no dimension is odd enough to ruin the average. What the rotation does not do is move the corpus onto the origin: an offset survives it, rotated into a different set of coordinates, which is the job centering does further down.
remex's default is a Haar-random rotation, a dense d×d matmul. rotation="rht" is a randomized Hadamard transform (permute, flip signs, fast Walsh–Hadamard) that does the same job at O(d log d) per vector.
The codebook
Lloyd-Max places 2k levels on a known distribution so as to minimise reconstruction error. At one bit the only boundary is zero. At two bits the boundaries fall at 0 and ±0.98, with centroids at ±0.45 and ±1.51.
Because the rotation fixed the distribution, those boundaries can be solved once, offline, from nothing but d and the bit count. No corpus is involved, so there is no fitting step, nothing to overfit, and no per-corpus artifact to ship alongside the codes: a reader recomputes the table rather than receiving it. It is tiny in any case: 28 bytes at 2 bits, 2044 at 8 bits, and that one table serves every dimension of every vector in the index.
Note what Lloyd-Max is optimising, because it matters later. It minimises squared reconstruction error. What retrieval consumes is the order of scores, and those two objectives are close but not the same thing.
Length and direction
remex stores the exact float32 norm and quantizes the unit direction, rather than quantizing the vector whole.
The reason is in the dot product itself: a·b = ‖a‖‖b‖ cos θ. Each length is a single number. The angle needs all d coordinates to pin down. So the lengths are the cheapest and most valuable quantity in the vector, worth storing exactly at four bytes, and the direction is where the rest of the budget has to go.
The factorization is also where a defect lived through six releases. The decoded direction is not a unit vector: at two bits it comes back between 0.89 and 0.97 long, and which value a vector gets depends on the vector. A uniform shortfall would be harmless, since it scales every score alike and cannot reorder anything. The spread around it is what does the damage, and it is enough to swap any two neighbours whose scores sit within a few percent of each other.
Every synthetic benchmark in the repository missed it for six releases, because on Gaussian data the neighbours are not packed tightly enough for a few percent to matter. I found it in September by reading RSLM, which stores an explicit two-byte scale for this correction. Checking why remex needed no such scale showed that it did.
One bit
At k=1 the code is the sign of each rotated coordinate, and it is worth deriving what that buys, because it is not obvious that one bit per dimension should work at all.
Take a random hyperplane through the origin. The probability that it separates two vectors — that they land on opposite sides — is exactly θ/π, their angle divided by a half turn. A sign bit records which side of one such hyperplane a vector fell on. Two signatures therefore disagree in a given bit exactly when that hyperplane separated the two vectors. Count the disagreements across d hyperplanes, divide by d, and you have an estimate of θ/π. An angle is all a cosine ranking consumes.
That construction is Charikar's SimHash, published in 2002. remex's one-bit codebook produces it without being asked to: the single Lloyd-Max boundary for a zero-mean Gaussian is at zero, so the code is the sign.
Every one of those hyperplanes passes through the origin, which is where the SPECTER2 offset comes back. A cloud sitting off to one side falls entirely on one side of nearly every hyperplane through the origin. The bits come out almost all the same and almost nothing is separated, so there is little left for the estimate to read. Subtracting the corpus mean before encoding moved SPECTER2 sign-bit R@10 from 0.468 to 0.635.
Comparison at one bit is XOR and popcount: one instruction per 64 dimensions, no codebook lookup, no floating point. remax's C kernel scans at 5 to 11 GB/s.
Two ways to add precision
Each bit is a coin whose bias is the angle: it comes up “disagree” with probability θ/π. A d-bit signature is d such coins, and the angle estimate has the spread of d flips. Stack a second independent rotation and there are 2d coins measuring the same angle; the spread falls as 1/k.
The two libraries are therefore sharpening different things. remex cuts more Lloyd-Max cells into each coordinate, which makes each coordinate more accurate and costs the popcount, since a multi-level code has to be scored as an inner product. remax stacks rotations, which makes the estimate more accurate while the scan stays XOR and popcount. A ranking consumes the estimate.
Same bytes, two allocations, and which one wins is a property of the corpus. On MongoDB's leaf-mt at 128 bytes per vector, d=512 with two stacks scored 0.542 against d=1024 with one stack at 0.503. An earlier sweep on a different encoder had it the other way round.
One-bit remex against remax
The codes are the same bits. Give both the same rotation and the same centering and they emit identical bytes. What differs is the machine built around them.
| remex, 1 bit | remax | |
|---|---|---|
| stored per vector | d/8 B of codes + 4 B exact norm | d/8 B of codes |
| score | asymmetric inner product: float32 query against dequantized centroids, times the stored norm | Hamming distance: XOR and popcount |
| more precision from | more cells per coordinate | more rotations stacked |
| where the codes come from | the top bit of its own 8-bit code, bit for bit | its own index |
On the bekko corpus at 48 bytes per vector, remex at one bit scored 0.564 and remax at k=1 scored 0.553. The norm weighting is worth about a point of recall and costs the popcount. remax can have it back with asymmetric=True, measured at +0.019 nDCG@10 at 128 bytes and +0.084 at 16, and substantially slower.
Because remex's one-bit code is the top bit of the eight-bit code it already stores, an index that keeps 8-bit codes gets the coarse tier for the price of packing it. remax is a standalone index, with an on-disk Corpus, memory-mapped residency and recipes for scanning the same bytes out of Parquet on Athena.
Measured gains per release
The construction above is four months old in its current form. What changed in that time was almost entirely implementation, and the numbers are large enough to be worth stating rather than summarising.
remex 0.6.0 (August 4) collected four months and 29 pull requests: a Mojo port of the quantizer at checkable parity with Python, GPU and Apple Metal kernels, IVFCoarseIndex over the Matryoshka coarse tier, the rht rotation, and construction that stopped being the slow part. lloyd_max_codebook had been solving its cells through scipy's scalar norm.cdf and norm.pdf; vectorized, it builds a quantizer at d=768 in 0.22 s where that had taken 30.6 s, and at d=3072 in 1.79 s where it had taken about 358 s. The codebooks it produces are byte for byte the ones it produced before.
remex 0.7.0 (September 11) is the release the RSLM reading produced. The length correction, renorm=True and on by default, moves SPECTER2 broad R@10 from 0.517 to 0.773 at 2 bits, 0.736 to 0.917 at 4, and 0.974 to 0.994 at 8. It costs no stored bytes, because the length is recoverable from the codes, and a file written either way decodes under the other. Centered mode arrived alongside it and takes 2-bit from 0.773 to 0.852 on the same corpus — opt-in, because the gain tracks ‖mean‖/mean‖x‖, which is 0.92 on SPECTER2 and 0.51 on MiniLM, where centering at one bit costs 0.026.
remex 0.8.0 (September 17) took the rotation apart. rht now applies in operator form through a C kernel compiled on first use, so resident rotation state drops from d² floats — 37.7 MB at d=3072 — to a few kilobytes. That exposed two things. Codes had never been reproducible across machines, because sgemm picks different kernels on different CPUs; with the operator and float32-derived boundaries, rht now produces identical codes on x86, ARM, Apple Silicon and Windows. And with the rotation cheap, np.searchsorted turned out to be 82% of an 8-bit encode, so it is compiled too: 10,000 × 768 at 8 bits went from 553 ms to 161 ms, and peak allocation at d=3072 from 522 MB to 39 MB.
remax 0.2.0 (August 4) was throughput and a clear-out. A threaded scan through ctypes, which releases the GIL, so the C needed no change. Counting select: Hamming distances are integers in [0, 8B], so a histogram selects the top k where argpartition had been building a permutation — 2 KB of counters against 80 MB of it at n=107. Memory-mapped residency. Asymmetric search reachable through Corpus for the first time. Two places in the code claimed a native speedup of 50–60× and the README claimed 23×. All three now say 25–35×, which is what the benchmark measures.
The unreleased work goes further on the same axis. The single-query path now blocks and filters each block against the k-th distance it already holds, which is an exact bound rather than an estimate: 1.7× end to end at n=108 on one thread, 938 ms to 567 ms. Selection had been 0.81× the cost of the scan at that size and is now 0.095×.
Picking a configuration
Start at the model card, and read it for a quantization rather than for truncation dims. If the vendor ships a binary or int8 export with a rescore recipe, that is your baseline and it may be the answer: MongoDB's leaf-mt ships sign bits that tied remex and remax on both our corpora, because its robustness to quantization was distilled in from the teacher.
Then set the arms up — their advertised Matryoshka against remex at matched bytes, their binary export against remax — and run them. Encoding the corpus is the expensive part and you have already paid it; quantizing the cached vectors five ways and scoring R@k against your own float32 ranking takes minutes. Every result below is a prior about what your own measurement will say. Run it anyway.
Expect cutting bits to beat cutting dimensions, as it has on every corpus we have measured. The bekko numbers are in the opening. On leaf-mt, remex at 2 bits and d=1024 beat the float32 Matryoshka floor at d=64 by 0.073 and 0.117 on the two corpora, and tied the uncompressed 4096-byte vector.
Take asymmetric scoring before buying document bytes, since the query is a single vector and keeping it in float32 is free; on leaf-mt that reached full float32 quality at 64 bytes a document. Centre if ‖mean‖/mean‖x‖ is large, and not otherwise. And check whether you have a second stage: with the full vectors in Postgres or S3 the in-memory tier only has to preserve rank well enough to survive a 1.5% candidate cut, which is a much weaker requirement than being the final answer.
Every number here comes from SPECTER2, MiniLM, bekko, jina-v5-nano and leaf-mt, at corpus sizes of 10,000 vectors and under. The 100M figures are arithmetic on those bytes, not measurements. Where your encoder lands is something you will have to encode and score.
Sources
Every figure above is a number from one of these. The library CHANGELOGs carry the per-release detail the summaries here compress.
| Measurement | Where it was run |
|---|---|
| Centering takes SPECTER2 sign bits from 0.468 to 0.635 | Your Embedding Has a Free Coarse Index In It; remex/bench/RESULTS.md |
| The 1-bit-beats-2-bit reversal, and its retraction | One Bit Beats Two, corrected in September; the same test on jina |
| Length correction: 2-bit 0.517 → 0.773, 8-bit 0.974 → 0.994 | remex#82, bench/norm_correction_eval.py, CHANGELOG v0.7.0 |
| Centered mode: 2-bit 0.773 → 0.852, and where it costs recall | remex#83, bench/centered_eval.py, One Bit From the Centre Beats One Bit From Zero |
| Codebook construction, 30.6 s → 0.22 s at d=768 | remex#71, CHANGELOG v0.6.0 |
| rht in operator form, cross-platform code identity, encode 553 → 161 ms | experiments/rht-operator-native, remex#87, remex#88 |
| remax native scan at 25–35×, threading, counting select | experiments/remax-hamming-speedup, QUERY_PATH_SPEED.md |
| Blocked single-query filter, 938 → 567 ms at n=108 | remax v0.3.0, SCALE_100M.md |
| Learned rotations (ITQ) losing to parameter-free SimHash on the ladder | experiments/rotation-decorrelation, LFM25_LEARNED_ROTATION.md |
| Dims against stacks, and the inversion between corpora | experiments/mdbr-leaf-mt-bench, experiments/kb-k-sweep |
| bekko at vendor tiers: 48 B at 0.564 against 256 B at 0.520 | experiments/bekko-embedding-bench, The Compression Result Is on the Model Card, Below the Fold |
| Quantizing against truncating, on two more encoders | Quantize, Don't Truncate, Matryoshka Doesn't Buy You Sign-Bit Compression, When Matryoshka Does |
| Training for quantization, and whether it pays | Don't Train for Quantization, A Four-Bit Model and a One-Bit Index |
| PCA next to quantization, and where static PCA breaks | PCA Next to Quantization, Static PCA Brittleness |
| The 9.6 GB arithmetic for 100M SPECTER2 vectors | Three Gigs to Search a Hundred Million Papers |
| Two things built on these codes | remax_kb: Hybrid Search in a File, 65 Repos, 190 ms, One Bit Per Dimension |
| Where a 1-bit index loses to a float matmul, and why | The 1-Bit Search Was Losing to a Float Matmul, Building an Empty Index Took Four and a Half Minutes |