yo_vector/rotate.rs
1//! The random rotation every vector goes through before it is quantised
2//! (`10` section 3).
3//!
4//! RaBitQ quantises a coordinate to its sign, and a sign carries information
5//! only when the coordinates are all about the same size. Real embeddings are
6//! not like that: a handful of dimensions hold most of the energy, and the sign
7//! of the rest is close to a coin toss. A random rotation fixes it, because
8//! rotating a vector by a random orthogonal matrix spreads its length evenly
9//! over the coordinates while leaving every distance and every angle exactly
10//! where it was. That is the whole reason the estimator's error bound holds.
11//!
12//! ```
13//! use yo_vector::Rotation;
14//!
15//! let r = Rotation::new(8, 42);
16//! let mut v = [1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
17//! r.apply(&mut v);
18//!
19//! // The length did not move, and neither did anything else about the vector.
20//! let len: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
21//! assert!((len - 1.0).abs() < 1e-5);
22//! // The one coordinate that held everything now holds a share of it.
23//! assert!(v.iter().all(|x| x.abs() < 0.95));
24//! ```
25//!
26//! # Why this shape and not a matrix
27//!
28//! The obvious rotation is a dense `D` by `D` orthogonal matrix from a QR
29//! decomposition, and it costs `D^2` multiplies per vector. At 768 dimensions
30//! that is 590 thousand multiplies to insert one vector, and the ingest target
31//! is fifty thousand vectors a second on one core, which asks for 29 GFLOP/s of
32//! nothing but rotation. It does not fit, so the rotation has to be structured.
33//!
34//! The usual structured answer is a random sign flip followed by a Hadamard
35//! transform, which is `D log D` and lovely, and needs `D` to be a power of two.
36//! Padding 768 up to 1024 would make a one bit code 128 bytes instead of 96,
37//! which is a third more index for every vector in the collection, so that is
38//! not free either.
39//!
40//! What is here is the same idea without the power of two. A round flips the
41//! sign of a random half of the coordinates, pairs them all up at random, and
42//! replaces each pair with its sum and its difference over the square root of
43//! two. Every step of that is orthogonal by construction rather than
44//! approximately orthogonal after rounding, and a quarter turn on a pair splits
45//! whatever it was holding evenly between the two, which is the part that
46//! actually spreads a spike out. Pairing at random rather than by a fixed stride
47//! is what lets any coordinate reach any other, so `log2(D)` rounds and a couple
48//! more is enough.
49//!
50//! The angle is not random, and that is deliberate. A round with a random angle
51//! keeps roughly nine tenths of what the larger side was holding, so a spike
52//! only decays like `0.9^rounds` and it takes something like sixty rounds at 256
53//! dimensions to flatten. A quarter turn halves it every time and takes eight.
54//! The randomness the estimator needs comes from the pairing and the signs, and
55//! there is plenty of it.
56//!
57//! # It is not written down anywhere
58//!
59//! A rotation is `dim` and a seed, and both live in the collection's catalogue
60//! entry. Rebuilding it is deterministic on every machine and every target,
61//! because the generator underneath is, so the file never holds the tables and
62//! two processes that open the same collection compute the same rotation.
63
64use yo_common::Rng;
65
66/// A quarter turn on a pair is a sum and a difference, both over this.
67const INV_ROOT2: f32 = core::f32::consts::FRAC_1_SQRT_2;
68
69/// One sweep: a sign for every coordinate, a pairing of all of them, and which
70/// way round each pair is turned.
71///
72/// The pairs are stored rather than the permutation they came from, because
73/// rotating in place off a pair list is one pass over the vector and no scratch
74/// buffer, where a permutation would want somewhere to write the shuffled copy.
75struct Round {
76 /// One bit per coordinate, set when its sign is flipped before the pairing.
77 flip: Vec<u64>,
78 /// `(i, j)` for each pair, which together cover every index once, or every
79 /// index but one when the dimension is odd.
80 pairs: Vec<(u32, u32)>,
81 /// One bit per pair, set when the difference goes first rather than second.
82 turn: Vec<u64>,
83}
84
85/// A random orthogonal transform, rebuilt from its seed rather than stored.
86pub struct Rotation {
87 dim: usize,
88 seed: u64,
89 rounds: Vec<Round>,
90}
91
92impl Rotation {
93 /// The rotation a collection of `dim` dimensional vectors uses, from the
94 /// seed in its catalogue entry.
95 ///
96 /// # Panics
97 ///
98 /// If `dim` is zero. A collection of vectors with no coordinates is a
99 /// mistake made somewhere further up rather than a case to handle.
100 #[must_use]
101 pub fn new(dim: usize, seed: u64) -> Rotation {
102 assert!(dim > 0, "a vector has at least one dimension");
103 let mut rng = Rng::new(seed);
104 let sweeps = sweeps(dim);
105 let mut rounds = Vec::with_capacity(sweeps);
106 // The identity, shuffled fresh for every round, which is what makes the
107 // pairing different each time and so lets a coordinate reach across the
108 // whole vector rather than staying in the half it started in.
109 let mut order: Vec<u32> = (0..dim as u32).collect();
110 for _ in 0..sweeps {
111 shuffle(&mut order, &mut rng);
112 let half = dim / 2;
113 let pairs = (0..half)
114 .map(|k| (order[2 * k], order[2 * k + 1]))
115 .collect();
116 rounds.push(Round {
117 flip: bits(dim, &mut rng),
118 pairs,
119 turn: bits(half, &mut rng),
120 });
121 }
122 Rotation { dim, seed, rounds }
123 }
124
125 /// How many coordinates a vector this rotates has.
126 #[must_use]
127 pub fn dim(&self) -> usize {
128 self.dim
129 }
130
131 /// The seed this was built from, which is what a catalogue stores.
132 #[must_use]
133 pub fn seed(&self) -> u64 {
134 self.seed
135 }
136
137 /// Rotate a vector where it lies.
138 ///
139 /// # Panics
140 ///
141 /// If `v` is not [`Rotation::dim`] long.
142 pub fn apply(&self, v: &mut [f32]) {
143 assert_eq!(
144 v.len(),
145 self.dim,
146 "this rotation is for {} dimensions and was handed {}",
147 self.dim,
148 v.len()
149 );
150 for round in &self.rounds {
151 // A sign is the top bit of the float, so flipping one is an xor and
152 // there is no branch to mispredict on a coin toss.
153 for (i, c) in v.iter_mut().enumerate() {
154 let sign = ((round.flip[i / 64] >> (i % 64)) & 1) << 31;
155 *c = f32::from_bits(c.to_bits() ^ (sign as u32));
156 }
157 for (k, &(i, j)) in round.pairs.iter().enumerate() {
158 let (i, j) = (i as usize, j as usize);
159 let (a, b) = (v[i], v[j]);
160 let (sum, difference) = ((a + b) * INV_ROOT2, (a - b) * INV_ROOT2);
161 if (round.turn[k / 64] >> (k % 64)) & 1 == 0 {
162 v[i] = sum;
163 v[j] = difference;
164 } else {
165 v[i] = difference;
166 v[j] = sum;
167 }
168 }
169 }
170 }
171}
172
173/// How many sweeps it takes for a spike to be spread over the whole vector.
174///
175/// A round splits whatever a coordinate is holding between two, so the set one
176/// can have reached after `k` of them is `2^k` wide. `log2(dim)` rounded up is
177/// where that covers the vector, and the two on top are because the pairings are
178/// drawn independently and so overlap near the end rather than tiling neatly.
179fn sweeps(dim: usize) -> usize {
180 ((usize::BITS - (dim - 1).leading_zeros()) as usize + 2).max(4)
181}
182
183/// Fisher and Yates, so that every pairing is equally likely.
184fn shuffle(order: &mut [u32], rng: &mut Rng) {
185 for i in (1..order.len()).rev() {
186 order.swap(i, rng.below(i + 1));
187 }
188}
189
190/// `n` coin tosses, packed.
191fn bits(n: usize, rng: &mut Rng) -> Vec<u64> {
192 (0..n.div_ceil(64)).map(|_| rng.next_u64()).collect()
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn dot(a: &[f32], b: &[f32]) -> f32 {
200 a.iter().zip(b).map(|(x, y)| x * y).sum()
201 }
202
203 /// A number in `[0, 1)` from the generator, for the tests that want one.
204 fn unit(rng: &mut Rng) -> f32 {
205 (rng.next_u64() >> 40) as f32 / (1u32 << 24) as f32
206 }
207
208 /// A handful of vectors that are not all alike.
209 fn sample(dim: usize, n: usize, seed: u64) -> Vec<Vec<f32>> {
210 let mut rng = Rng::new(seed);
211 (0..n)
212 .map(|_| (0..dim).map(|_| unit(&mut rng) * 2.0 - 1.0).collect())
213 .collect()
214 }
215
216 #[test]
217 fn a_rotation_keeps_every_length_and_every_angle() {
218 let r = Rotation::new(64, 7);
219 let vs = sample(64, 8, 11);
220 for v in &vs {
221 let mut spun = v.clone();
222 r.apply(&mut spun);
223 let before = dot(v, v).sqrt();
224 let after = dot(&spun, &spun).sqrt();
225 assert!((before - after).abs() < 1e-4, "{before} became {after}");
226 }
227 // And the angle between any two of them, which is what a distance is
228 // made of and so is the property that actually has to survive.
229 for a in 0..vs.len() {
230 for b in 0..a {
231 let mut x = vs[a].clone();
232 let mut y = vs[b].clone();
233 let before = dot(&x, &y);
234 r.apply(&mut x);
235 r.apply(&mut y);
236 let after = dot(&x, &y);
237 assert!((before - after).abs() < 1e-3, "{before} became {after}");
238 }
239 }
240 }
241
242 #[test]
243 fn the_same_seed_is_the_same_rotation() {
244 let v = sample(32, 1, 3).pop().expect("one vector");
245 let mut a = v.clone();
246 let mut b = v;
247 Rotation::new(32, 99).apply(&mut a);
248 Rotation::new(32, 99).apply(&mut b);
249 assert_eq!(a, b);
250
251 let mut c = a.clone();
252 Rotation::new(32, 100).apply(&mut c);
253 assert_ne!(a, c, "two seeds should not be one rotation");
254 }
255
256 /// The point of the whole thing: a vector whose length sits in one
257 /// coordinate comes out with it spread over all of them, which is what
258 /// makes the sign of a coordinate worth a bit.
259 #[test]
260 fn a_spike_comes_out_flat() {
261 for dim in [64usize, 256, 768] {
262 let r = Rotation::new(dim, 5);
263 let mut v = vec![0.0f32; dim];
264 v[0] = 1.0;
265 r.apply(&mut v);
266
267 let even = 1.0 / (dim as f32).sqrt();
268 let biggest = v.iter().fold(0.0f32, |m, x| m.max(x.abs()));
269 assert!(biggest < even * 5.0, "{dim}: {biggest} against {even}");
270 // And nothing is left sitting at zero either, which is the failure
271 // a fixed pairing would have: half the vector never touched.
272 let alive = v.iter().filter(|x| x.abs() > even / 4.0).count();
273 assert!(alive > dim * 3 / 4, "{dim}: only {alive} coordinates moved");
274 }
275 }
276
277 #[test]
278 fn an_odd_dimension_still_rotates() {
279 let r = Rotation::new(7, 1);
280 let mut v = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
281 let before = dot(&v, &v).sqrt();
282 r.apply(&mut v);
283 let after = dot(&v, &v).sqrt();
284 assert!((before - after).abs() < 1e-3);
285 }
286
287 #[test]
288 fn one_dimension_is_only_a_sign() {
289 // There is nothing to pair it with, so all a round can do is flip it,
290 // and the length still comes out where it went in.
291 let r = Rotation::new(1, 1);
292 let mut v = [3.0f32];
293 r.apply(&mut v);
294 assert_eq!(v[0].abs(), 3.0);
295 }
296}