Skip to main content

Module rotate

Module rotate 

Source
Expand description

The random rotation every vector goes through before it is quantised (10 section 3).

RaBitQ quantises a coordinate to its sign, and a sign carries information only when the coordinates are all about the same size. Real embeddings are not like that: a handful of dimensions hold most of the energy, and the sign of the rest is close to a coin toss. A random rotation fixes it, because rotating a vector by a random orthogonal matrix spreads its length evenly over the coordinates while leaving every distance and every angle exactly where it was. That is the whole reason the estimator’s error bound holds.

use yo_vector::Rotation;

let r = Rotation::new(8, 42);
let mut v = [1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
r.apply(&mut v);

// The length did not move, and neither did anything else about the vector.
let len: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((len - 1.0).abs() < 1e-5);
// The one coordinate that held everything now holds a share of it.
assert!(v.iter().all(|x| x.abs() < 0.95));

§Why this shape and not a matrix

The obvious rotation is a dense D by D orthogonal matrix from a QR decomposition, and it costs D^2 multiplies per vector. At 768 dimensions that is 590 thousand multiplies to insert one vector, and the ingest target is fifty thousand vectors a second on one core, which asks for 29 GFLOP/s of nothing but rotation. It does not fit, so the rotation has to be structured.

The usual structured answer is a random sign flip followed by a Hadamard transform, which is D log D and lovely, and needs D to be a power of two. Padding 768 up to 1024 would make a one bit code 128 bytes instead of 96, which is a third more index for every vector in the collection, so that is not free either.

What is here is the same idea without the power of two. A round flips the sign of a random half of the coordinates, pairs them all up at random, and replaces each pair with its sum and its difference over the square root of two. Every step of that is orthogonal by construction rather than approximately orthogonal after rounding, and a quarter turn on a pair splits whatever it was holding evenly between the two, which is the part that actually spreads a spike out. Pairing at random rather than by a fixed stride is what lets any coordinate reach any other, so log2(D) rounds and a couple more is enough.

The angle is not random, and that is deliberate. A round with a random angle keeps roughly nine tenths of what the larger side was holding, so a spike only decays like 0.9^rounds and it takes something like sixty rounds at 256 dimensions to flatten. A quarter turn halves it every time and takes eight. The randomness the estimator needs comes from the pairing and the signs, and there is plenty of it.

§It is not written down anywhere

A rotation is dim and a seed, and both live in the collection’s catalogue entry. Rebuilding it is deterministic on every machine and every target, because the generator underneath is, so the file never holds the tables and two processes that open the same collection compute the same rotation.

Structs§

Rotation
A random orthogonal transform, rebuilt from its seed rather than stored.