Skip to main content

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}