The rounding table is solved from two numbers, and neither is yours

1 month ago

Type beach into your photos app and it finds beach pictures. Search like that works by turning every photo into a long list of numbers that stands for what is in it. turbovec is a search index. It stores those lists, searches them, and keeps each one in a fraction of the space.

Ask

Ask about this presentation

Answers are generated from this presentation.

Chapters

  1. 0:00turbovec · a search index for long lists of numbers, packed small
  2. 0:15Five lines of Python, no training call
  3. 0:26Normally the rounding table is fitted to a sample of your own data
  4. 0:47The rounding table is solved from two numbers, and neither is yours
  5. 1:09Every list is scrambled the same fixed way before anything is rounded
  6. 1:43The scramble is a constant in the source, so every machine encodes the same bytes
  7. 2:20Everything runs on your own machine
  8. 2:40The rounding table depends on your settings, never on your data
Show transcript

turbovec · a search index for long lists of numbers, packed small

one list of 1,536 numbers, 4 bytes a number
6,144 B
→
packed codes · 2 bit · 1,536 dims
384 B
plus one 4 byte scale a row · 388 B stored
16× packed codes against 4 bytes a number · 15.83× row to row

turbovec

a search index for long lists of numbers, packed small
Rust with Python bindingsMITv1.0.0PyPI and crates.io
turbovec v1.0.0 · packing rule from turbovec/src/pack.rs

Type beach into your photos app and it finds beach pictures. Search like that works by turning every photo into a long list of numbers that stands for what is in it. turbovec is a search index. It stores those lists, searches them, and keeps each one in a fraction of the space.

Five lines of Python, no training call

TurboQuantIndex(dim=1536, bit_width=4)
index.add(vectors)
scores, indices = index.search(query, k=10)
index.write("my_index.tv")
index.sync("my_index.tv")
README quickstart · turbovec v1.0.0

You tell turbovec how long your lists are and how hard to squeeze them. You add the lists. You search them. You write the index to a file. That is the whole surface. There is no step where you hand over a sample and wait.

Normally the rounding table is fitted to a sample of your own data

1,536 dims · 2 bit
10,000,000 lists at 4 bytes a number · 6,144 B each
61.4 GB
1,536 dims · 2 bit
10,000,000 stored rows · 388 B each
3.9 GB
a fitted table describes the sample it was fitted to
packing rule from turbovec/src/pack.rs · arithmetic ours

Shrinking a list means rounding: every number gets pushed to the nearest value on a short list, and you store which one you picked. That short list is the rounding table. To build one you normally take a sample of your own lists and see where your numbers tend to sit. So the table fits that sample. If the lists you add later sit somewhere else, the table you fitted no longer fits them.

The rounding table is solved from two numbers, and neither is yours

pub(crate) fn codebook(bits: usize, dim: usize) -> (Vec<f32>, Vec<f32>) {
…
let computed = lloyd_max(bits, dim, 200, 1e-12);
pub(crate) fn codebook(bits: usize, dim: usize) -> (Vec<f32>, Vec<f32>) {
…
let computed = lloyd_max(bits, dim, 200, 1e-12);
turbovec/src/codebook.rs L50 and L60 · v1.0.0

Here is the clever part. The function that builds turbovec's rounding table takes two things: how many bits you spend on each number, two, three or four, and how long the lists are. From those two numbers, the function solves for the best places to round to. There is nowhere in that list of inputs to put one of your lists, and the note above the function says the answer depends on those two numbers and nothing else.

Every list is scrambled the same fixed way before anything is rounded

1
take the length off and keep it as one number
→
2
scramble what is left
3
optional two number fit
After orthogonal rotation, each coordinate of a unit vector on S^{d-1}
follows Beta((d-1)/2, (d-1)/2) on [-1, 1].
4
solve the rounding table from that spread
→
5
pack the small numbers tight
→
6
put the length back on
turbovec/src/codebook.rs L1–5 · v1.0.0

You cannot work out the best rounding points for data in general. So turbovec makes the data stop being general. Every list gets its length taken off and stored as one number, so what is left is pure direction. Then every direction is pushed through the same shuffle. After that shuffle, the numbers in a list spread out in a shape you can work out in advance, whatever the list started as. Once you know the shape, where to round to is arithmetic, and the answer is the same for everybody. The small numbers get packed tight, and the length goes back on. The idea of scrambling first comes from Google Research.

The scramble is a constant in the source, so every machine encodes the same bytes

DO NOT CHANGE — baked into every encoded vector.
const ROTATION_SEED: [u8; 32] = [
164143161123885061102341841612041051201844314020011724180247841416811016122822332242
shuffle across all the numbers
ChaCha8 Fisher-Yates permutation
→
flip the sign of each one
ChaCha8 ±1 sign flip
→
mix inside each block
normalized Walsh-Hadamard
K = 2
block-Hadamard rounds
the version before this one built its randomness at run time
turbovec/src/rotation.rs L82–85, L71–81, L28–39 · v1.0.0

The scramble needs random shuffles and sign flips. Here is where it gets good: the randomness is thirty-two numbers typed straight into the source file. Change one of them and every index ever written under this format becomes unreadable. One round is a shuffle, a sign flip on every number, then a mixing step inside each block, and the whole round runs twice. Before this version, turbovec made that randomness while it ran, so the encoded bytes depended on the machine and its processor core count. An index written on one computer could decode differently on another. The new scramble also dropped a forty-two megabyte library.

Everything runs on your own machine

pip install turbovec
cargo add turbovec
Python 3.9+
macOS arm64 · Linux aarch64 · Linux x86_64 · Windows
MITv1.0.0tagged 2026-08-18
optional calibration reads about a thousand of your lists and fits two numbers per position
PyPI and crates.io, 2026-08-23 · in-tree LICENSE · tag object

It is MIT licensed, and version one point oh is five days old. Install it from the Python package index or the Rust one, and it runs entirely on your own machine, with no service in the middle. One step is optional and it does read your data: calibration takes about a thousand of your lists, fits two numbers for each position in the list, and redoes every list you already stored.

The rounding table depends on your settings, never on your data

The rounding table depends on your settings, never on your data.

github.com/RyanCodrai/turbovec
New repo tomorrow.

Compression normally means finding the patterns in your own data. turbovec makes your data stop being particular first, and then one answer serves everybody. Ryan Codrai slash turbovec, on GitHub. New repo tomorrow.