Skip to main content

yo_vector/
rabitq.rs

1//! RaBitQ, the quantiser the searchable form of a vector is written in
2//! (`10` section 3).
3//!
4//! A 768 dimensional embedding is 3072 bytes of `f32`, and ten million of them
5//! is 30 GB. RaBitQ writes the same vector as one bit per dimension, which is 96
6//! bytes and a 32x reduction, and that is the difference between an index that
7//! sits in memory and one that does not.
8//!
9//! Binary quantisation on its own is old and it is not good enough: keeping only
10//! the sign of each coordinate throws away how far the point is from the
11//! boundary, and the distances that come back are biased in a way that no amount
12//! of rerank hides. RaBitQ's contribution is the estimator. It stores one extra
13//! number per vector, the cosine between the vector and the corner of the cube
14//! it was rounded to, and dividing by that turns a biased guess into an unbiased
15//! one with an error bound that shrinks as `1/sqrt(D)`. The ordering that comes
16//! out of the codes alone is then good enough that rerank only has to look at a
17//! small multiple of `k` real vectors.
18//!
19//! ```
20//! use yo_vector::{Bits, Quantizer};
21//!
22//! let q = Quantizer::new(64, Bits::One, 7);
23//! let centroid = vec![0.0f32; 64];
24//! let mut code = vec![0u8; q.code_bytes()];
25//!
26//! let v: Vec<f32> = (0..64).map(|i| (i as f32 * 0.37).sin()).collect();
27//! let coded = q.encode(&v, &centroid, &mut code);
28//!
29//! // The query is prepared once and then measured against many codes.
30//! let query = q.query(&v, &centroid);
31//! let guess = query.distance(&code, &coded);
32//! // A vector against itself, so the answer should be near zero.
33//! assert!(guess < 0.2, "{guess}");
34//! ```
35//!
36//! # What is stored
37//!
38//! Per vector: the code, and two `f32`. The first is the length of the residual,
39//! which is the vector minus its partition's centroid, and it is what turns an
40//! angle in the unit sphere back into a distance. The second is the correction,
41//! which is the estimator, and it is kept as its reciprocal because the
42//! estimator only ever divides by it. `10` section 3's table says one `f32` and
43//! it is two,
44//! because both are needed and neither can be recovered from the other. Eight
45//! bytes on top of 96 is still a 30x reduction rather than 32x.
46//!
47//! # One bit and four
48//!
49//! [`Bits::One`] rounds each coordinate to a sign and [`Bits::Four`] rounds it
50//! to one of sixteen levels between the smallest and the largest coordinate the
51//! vector has. Four bits is four times the index and roughly a quarter of the
52//! error, and which one a collection wants is a question about the embedding
53//! family rather than about the engine, which is why both are here and the
54//! choice is per collection.
55//!
56//! The two share one code path. Reconstructing a code is `lo + level * delta`
57//! either way, with `lo` and `delta` fixed by the dimension for one bit and
58//! measured per vector for four, so the estimator is written once and the only
59//! thing that changes is how many bits a level takes.
60//!
61//! # A code is bit planes, and that is what makes the scan fast
62//!
63//! The obvious layout writes a coordinate's level in the bits next to it, and
64//! then measuring a code against a query is a multiply per dimension. That is
65//! 768 multiplies per candidate and it does not fit inside a millisecond search.
66//!
67//! So a code is stored transposed. Plane `b` holds bit `b` of every
68//! coordinate's level, one bit per coordinate, `dim` bits rounded up to whole
69//! 64 bit words, and the planes run least significant first. A one bit code is
70//! one plane and a four bit code is four, and the byte count is the same either
71//! way.
72//!
73//! The query is quantised to [`Bits::query_bits`] and transposed the same way.
74//! Then the sum of the code's levels times the query's levels is
75//!
76//! ```text
77//! sum over a, b of 2^(a+b) * popcount(code plane a AND query plane b)
78//! ```
79//!
80//! which is four ANDs and four popcounts per word for a one bit code, against
81//! 64 float multiplies for the same 64 coordinates. The sums are exact
82//! integers, so the arithmetic is also better behaved than the float version it
83//! replaces, and nothing is left to round until the end.
84//!
85//! # The query is quantised finer than the code
86//!
87//! Quantising the query costs accuracy, and how much was measured rather than
88//! assumed. At one bit the query at four bits is off by about a third of what
89//! the code itself is off by, which is lost in the quadrature and does not
90//! matter. At four bits the code is ten times more accurate and the same four
91//! bit query is off by four times as much as the code, which throws away the
92//! entire reason anyone would pay for four bit codes.
93//!
94//! So the query width follows the code width: four bits against a one bit code
95//! and eight against a four bit one. That puts the query's error back at about
96//! a third of the code's in both cases, and [`Query::cosine`] against
97//! [`Query::cosine_exact`] is the test that holds it there.
98
99use crate::rotate::Rotation;
100
101/// How many 64 bit words one plane of a `dim` dimensional code takes.
102fn words_of(dim: usize) -> usize {
103    dim.div_ceil(64)
104}
105
106/// How many bits a coordinate is rounded to.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum Bits {
109    /// The sign, which is 96 bytes for a 768 dimensional vector.
110    One,
111    /// Sixteen levels, which is 384 bytes for the same vector and about a
112    /// quarter of the error.
113    Four,
114}
115
116impl Bits {
117    /// How many bits one coordinate takes, which is also how many planes a code
118    /// is written in.
119    #[must_use]
120    pub fn count(self) -> usize {
121        match self {
122            Bits::One => 1,
123            Bits::Four => 4,
124        }
125    }
126
127    /// How many bits a query is quantised to before it is scanned against codes
128    /// this wide.
129    ///
130    /// Four bits either side is what RaBitQ specifies, and it is right for a one
131    /// bit code and wrong for a four bit one, because a four bit code is ten
132    /// times more accurate and a four bit query is not. Eight is what puts the
133    /// query's error back under the code's, and the cost is four more ANDs and
134    /// four more popcounts per word on the path that was already paying for four
135    /// times the bytes.
136    #[must_use]
137    pub fn query_bits(self) -> usize {
138        match self {
139            Bits::One => 4,
140            Bits::Four => 8,
141        }
142    }
143
144    /// The largest level a coordinate this wide can round to.
145    fn top(self) -> u64 {
146        (1 << self.count()) - 1
147    }
148}
149
150/// What a code needs alongside it to be measured against a query.
151///
152/// `lo` and `delta` are only worth storing for [`Bits::Four`]. At one bit they
153/// are the same two numbers for every vector in the collection, so a store that
154/// keeps them per vector is keeping the dimension eight bytes at a time.
155#[derive(Debug, Clone, Copy, PartialEq)]
156pub struct Coded {
157    /// The length of the vector minus its centroid.
158    pub norm: f32,
159    /// One over the cosine between the vector and what its code reconstructs
160    /// to. That cosine is the estimator's correction and the whole of RaBitQ
161    /// over plain binary quantisation.
162    ///
163    /// Stored the wrong way up on purpose, because the estimator divides by it
164    /// exactly once per member and a float division was 44 percent of what a
165    /// member cost: 3.56 nanoseconds became 1.98 at 128 dimensions when the
166    /// division became a multiplication. Nothing else here reads it, so nothing
167    /// else pays for the inversion.
168    pub scale: f32,
169    /// The value level zero reconstructs to, over the length of the
170    /// reconstruction.
171    pub lo: f32,
172    /// What one level is worth, over the length of the reconstruction.
173    pub delta: f32,
174}
175
176/// The quantiser for one collection: its rotation and its width.
177#[derive(Debug)]
178pub struct Quantizer {
179    rot: Rotation,
180    bits: Bits,
181}
182
183impl Quantizer {
184    /// The quantiser for `dim` dimensional vectors at `bits`, with `seed`
185    /// choosing the rotation.
186    ///
187    /// # Panics
188    ///
189    /// If `dim` is zero.
190    #[must_use]
191    pub fn new(dim: usize, bits: Bits, seed: u64) -> Quantizer {
192        Quantizer {
193            rot: Rotation::new(dim, seed),
194            bits,
195        }
196    }
197
198    /// How many coordinates a vector has.
199    #[must_use]
200    pub fn dim(&self) -> usize {
201        self.rot.dim()
202    }
203
204    /// How wide a coordinate is written.
205    #[must_use]
206    pub fn bits(&self) -> Bits {
207        self.bits
208    }
209
210    /// The seed the rotation was built from, which is what a catalogue stores.
211    #[must_use]
212    pub fn seed(&self) -> u64 {
213        self.rot.seed()
214    }
215
216    /// How many bytes one code takes.
217    ///
218    /// A plane is whole 64 bit words because the scan reads words, so a
219    /// dimension that is not a multiple of 64 pays for the rest of its last
220    /// word. At the dimensions embedding families actually use there is nothing
221    /// to pay.
222    #[must_use]
223    pub fn code_bytes(&self) -> usize {
224        words_of(self.dim()) * 8 * self.bits.count()
225    }
226
227    /// A vector in the frame everything else here works in.
228    ///
229    /// The rotation is linear, so `rotate(v - c)` is `rotate(v) - rotate(c)`,
230    /// and an index that keeps its centroids already rotated never has to
231    /// rotate one again. That is what [`Quantizer::encode_rotated`] and
232    /// [`Quantizer::query_rotated`] are for, and the rotation is the expensive
233    /// half of both of the two calls above them.
234    ///
235    /// # Panics
236    ///
237    /// If `v` is not [`Quantizer::dim`] long.
238    #[must_use]
239    pub fn rotate(&self, v: &[f32]) -> Vec<f32> {
240        let mut x = v.to_vec();
241        self.rot.apply(&mut x);
242        x
243    }
244
245    /// Write `v`'s code against the centroid of the partition it is going into.
246    ///
247    /// # Panics
248    ///
249    /// If `v` or `centroid` is not [`Quantizer::dim`] long, or `code` is not
250    /// [`Quantizer::code_bytes`] long.
251    pub fn encode(&self, v: &[f32], centroid: &[f32], code: &mut [u8]) -> Coded {
252        let mut x = self.residual(v, centroid);
253        let norm = length(&x);
254        if norm > 0.0 {
255            let by = 1.0 / norm;
256            for c in &mut x {
257                *c *= by;
258            }
259        }
260        self.rot.apply(&mut x);
261        self.write(&x, norm, code)
262    }
263
264    /// The same as [`Quantizer::encode`] with both sides already rotated.
265    ///
266    /// # Panics
267    ///
268    /// If `x` or `centroid` is not [`Quantizer::dim`] long, or `code` is not
269    /// [`Quantizer::code_bytes`] long.
270    pub fn encode_rotated(&self, x: &[f32], centroid: &[f32], code: &mut [u8]) -> Coded {
271        let mut r = self.residual(x, centroid);
272        let norm = length(&r);
273        if norm > 0.0 {
274            let by = 1.0 / norm;
275            for c in &mut r {
276                *c *= by;
277            }
278        }
279        self.write(&r, norm, code)
280    }
281
282    /// The code of a rotated unit residual whose original length was `norm`.
283    fn write(&self, x: &[f32], norm: f32, code: &mut [u8]) -> Coded {
284        assert_eq!(
285            code.len(),
286            self.code_bytes(),
287            "a code here is {} bytes and the buffer is {}",
288            self.code_bytes(),
289            code.len()
290        );
291        code.fill(0);
292        if norm == 0.0 {
293            // The vector is the centroid. There is no direction to write down,
294            // and a scale of one keeps the estimator from producing an infinity
295            // if anyone measures against it anyway.
296            return Coded {
297                norm: 0.0,
298                scale: 1.0,
299                lo: 0.0,
300                delta: 0.0,
301            };
302        }
303        let mut coded = match self.bits {
304            Bits::One => sign_code(x, code),
305            Bits::Four => level_code(x, code),
306        };
307        coded.norm = norm;
308        coded
309    }
310
311    /// Prepare a query against the centroid of a partition being scanned.
312    ///
313    /// This is the per partition half of a search and it happens once, where
314    /// the estimate against a code happens once per vector in the partition.
315    ///
316    /// # Panics
317    ///
318    /// If `q` or `centroid` is not [`Quantizer::dim`] long.
319    #[must_use]
320    pub fn query(&self, q: &[f32], centroid: &[f32]) -> Query {
321        let mut x = self.residual(q, centroid);
322        let norm = length(&x);
323        if norm > 0.0 {
324            let by = 1.0 / norm;
325            for c in &mut x {
326                *c *= by;
327            }
328        }
329        self.rot.apply(&mut x);
330        self.prepare(x, norm)
331    }
332
333    /// The same as [`Quantizer::query`] with both sides already rotated.
334    ///
335    /// A search rotates its query once and then meets every partition it probes
336    /// through this, so the rotation is paid for once rather than once per
337    /// partition.
338    ///
339    /// # Panics
340    ///
341    /// If `q` or `centroid` is not [`Quantizer::dim`] long.
342    #[must_use]
343    pub fn query_rotated(&self, q: &[f32], centroid: &[f32]) -> Query {
344        let mut x = self.residual(q, centroid);
345        let norm = length(&x);
346        if norm > 0.0 {
347            let by = 1.0 / norm;
348            for c in &mut x {
349                *c *= by;
350            }
351        }
352        self.prepare(x, norm)
353    }
354
355    /// Quantise and transpose a rotated unit residual into the form the scan
356    /// meets a code with.
357    fn prepare(&self, x: Vec<f32>, norm: f32) -> Query {
358        // The sum is taken from the unquantised coordinates because it is one
359        // number computed once, so there is nothing to gain by approximating it
360        // and it is half of what the estimator adds up. It comes out of the same
361        // walk as the span because both are the same three kilobytes and one
362        // walk over them is cheaper than two.
363        let (sum, lo, hi) = sum_and_span(&x);
364        let words = words_of(self.dim());
365        let wide = self.bits.query_bits();
366        let top = (1u64 << wide) - 1;
367        let delta = step(lo, hi, top);
368        let by = 1.0 / delta;
369        let mut planes = vec![0u64; wide * words];
370        // Four planes for a one bit code and eight for a four bit one, which is
371        // the whole of [`Bits::query_bits`], so both are a copy of the loop with
372        // the plane count known and there is no arm here that is not taken.
373        match wide {
374            4 => transpose::<4>(&x, lo, by, top, words, &mut planes),
375            8 => transpose::<8>(&x, lo, by, top, words, &mut planes),
376            _ => unreachable!("a query is quantised to four bits or to eight"),
377        }
378        Query {
379            bits: self.bits,
380            words,
381            rotated: x,
382            planes,
383            lo,
384            delta,
385            sum,
386            norm,
387        }
388    }
389
390    /// `v - centroid`.
391    fn residual(&self, v: &[f32], centroid: &[f32]) -> Vec<f32> {
392        assert_eq!(
393            v.len(),
394            self.dim(),
395            "this collection holds {} dimensional vectors and was handed {}",
396            self.dim(),
397            v.len()
398        );
399        assert_eq!(
400            centroid.len(),
401            self.dim(),
402            "the centroid is {} dimensional and the collection is {}",
403            centroid.len(),
404            self.dim()
405        );
406        v.iter().zip(centroid).map(|(a, b)| a - b).collect()
407    }
408}
409
410/// A query, rotated and quantised once and then measured against every code in
411/// a partition.
412#[derive(Debug)]
413pub struct Query {
414    /// The width of the codes this is measured against, so the scan knows how
415    /// many planes each one has.
416    bits: Bits,
417    /// The words one plane takes, for both this query and those codes.
418    words: usize,
419    /// The query's residual, unit length and rotated, which is only what
420    /// [`Query::cosine_exact`] reads.
421    rotated: Vec<f32>,
422    /// The same coordinates at [`Bits::query_bits`], transposed into planes.
423    planes: Vec<u64>,
424    /// What level zero of the query reconstructs to.
425    lo: f32,
426    /// What one level of the query is worth.
427    delta: f32,
428    /// The sum of the rotated coordinates, which the estimator needs and which
429    /// does not depend on the code it is being compared against.
430    sum: f32,
431    /// The length of the query's residual.
432    norm: f32,
433}
434
435impl Query {
436    /// The estimated squared distance from this query to every code in a
437    /// posting, written into `out`.
438    ///
439    /// This is the loop a search spends most of its life in, and it takes a
440    /// whole posting rather than one code because of what that costs. A code is
441    /// `dim / 64` words wide, and `dim` is not a compile time constant, so a
442    /// scan written one code at a time meets a loop whose trip count the
443    /// compiler cannot see and cannot unroll. Measured at 128 dimensions, the
444    /// same popcount arithmetic is 1.98 nanoseconds a member with the word
445    /// count known and 6.31 with it unknown, which is more than three times.
446    /// Deciding the width once for a posting of a few hundred rather than once
447    /// per member is what buys that back.
448    ///
449    /// # Panics
450    ///
451    /// If `out` is not as long as `meta`, or if `codes` is not `meta.len()`
452    /// codes long.
453    pub fn scan(&self, codes: &[u8], meta: &[Coded], out: &mut [f32]) {
454        assert_eq!(
455            meta.len(),
456            out.len(),
457            "{} codes and room for {} answers",
458            meta.len(),
459            out.len()
460        );
461        let stride = self.bits.count() * self.words * 8;
462        assert_eq!(
463            codes.len(),
464            meta.len() * stride,
465            "{} members of {stride} bytes is {} and there are {}",
466            meta.len(),
467            meta.len() * stride,
468            codes.len()
469        );
470        // One arm per width worth having a copy of the loop for: 64, 128, 256,
471        // 384, 512, 768, 1024, 1536 and 3072 dimensions, which is every
472        // embedding family anybody ships. Anything else takes the general loop
473        // and is correct and slower, which is the right way round.
474        match self.words {
475            1 => self.scan_at::<1>(codes, meta, out),
476            2 => self.scan_at::<2>(codes, meta, out),
477            4 => self.scan_at::<4>(codes, meta, out),
478            6 => self.scan_at::<6>(codes, meta, out),
479            8 => self.scan_at::<8>(codes, meta, out),
480            12 => self.scan_at::<12>(codes, meta, out),
481            16 => self.scan_at::<16>(codes, meta, out),
482            24 => self.scan_at::<24>(codes, meta, out),
483            48 => self.scan_at::<48>(codes, meta, out),
484            _ => {
485                for (i, (code, coded)) in codes.chunks_exact(stride).zip(meta).enumerate() {
486                    let (total, cross) =
487                        packed_dot(code, self.bits.count(), self.words, &self.planes);
488                    out[i] = self.settle(total, cross, coded);
489                }
490            }
491        }
492    }
493
494    /// The same with the word count known, so the inner loops unroll.
495    fn scan_at<const W: usize>(&self, codes: &[u8], meta: &[Coded], out: &mut [f32]) {
496        let planes = self.bits.count();
497        let wide = self.bits.query_bits();
498        for (i, (code, coded)) in codes.chunks_exact(planes * W * 8).zip(meta).enumerate() {
499            let (total, cross) = fixed_dot::<W>(code, planes, wide, &self.planes);
500            out[i] = self.settle(total, cross, coded);
501        }
502    }
503
504    /// Turn the two popcount sums into a squared distance.
505    ///
506    /// There is no branch here for a zero length residual, on either side,
507    /// which the one code at a time version used to have. If the code's
508    /// residual is zero then both of its terms below are zero and the answer is
509    /// the query's own length, and if the query's residual is zero then both of
510    /// the query's terms are zero and the answer is the code's. Those are the
511    /// right answers, so the branch was only ever buying a wrong reason to skip
512    /// arithmetic that costs less than the branch.
513    #[inline]
514    fn settle(&self, total: u32, cross: u32, coded: &Coded) -> f32 {
515        // Undo the query's quantisation: every level was `lo + delta * level`,
516        // so the sum over the code's levels needs the `lo` part weighted by how
517        // much level the code is carrying and the `delta` part by the cross
518        // term the popcounts just measured.
519        let levels = self.lo * total as f32 + self.delta * cross as f32;
520        let cos = (coded.lo * self.sum + coded.delta * levels) * coded.scale;
521        // The law of cosines on the triangle the centroid makes with the two
522        // points, which is why the residual lengths had to be kept.
523        (self.norm * self.norm + coded.norm * coded.norm - 2.0 * self.norm * coded.norm * cos)
524            .max(0.0)
525    }
526
527    /// The estimated squared distance between the query and one coded vector.
528    ///
529    /// Squared rather than the distance itself because the square root is
530    /// monotone, so it changes no ordering and nothing above this needs it.
531    ///
532    /// This is the convenient form and not the one a search calls.
533    /// [`Query::scan`] is that one.
534    ///
535    /// # Panics
536    ///
537    /// If `code` is not as long as the codes this query's quantiser writes.
538    #[must_use]
539    pub fn distance(&self, code: &[u8], coded: &Coded) -> f32 {
540        let mut out = [0.0f32];
541        self.scan(code, std::slice::from_ref(coded), &mut out);
542        out[0]
543    }
544
545    /// The estimated cosine between the query's residual and the coded one.
546    ///
547    /// This is RaBitQ's estimator: the inner product against what the code
548    /// reconstructs to, divided by the cosine between that reconstruction and
549    /// the vector it came from. The division is the part that makes it
550    /// unbiased. The inner product is the popcount scan.
551    ///
552    /// # Panics
553    ///
554    /// If `code` is not as long as the codes this query's quantiser writes.
555    #[must_use]
556    pub fn cosine(&self, code: &[u8], coded: &Coded) -> f32 {
557        if coded.norm == 0.0 || self.norm == 0.0 {
558            return 0.0;
559        }
560        let (total, cross) = packed_dot(code, self.bits.count(), self.words, &self.planes);
561        // Undo the query's quantisation: every level was `lo + delta * level`,
562        // so the sum over the code's levels needs the `lo` part weighted by how
563        // much level the code is carrying and the `delta` part by the cross
564        // term the popcounts just measured.
565        let levels = self.lo * total as f32 + self.delta * cross as f32;
566        (coded.lo * self.sum + coded.delta * levels) * coded.scale
567    }
568
569    /// The same estimate with the query left at full precision.
570    ///
571    /// This is the reference the popcount scan is checked against, and it is
572    /// public because a divergence between the two is worth being able to
573    /// measure from outside. It reads one coordinate at a time and it is not
574    /// what a search should call.
575    ///
576    /// # Panics
577    ///
578    /// If `code` is not as long as the codes this query's quantiser writes.
579    #[must_use]
580    pub fn cosine_exact(&self, code: &[u8], coded: &Coded) -> f32 {
581        if coded.norm == 0.0 || self.norm == 0.0 {
582            return 0.0;
583        }
584        let levels = exact_dot(code, self.bits.count(), self.words, &self.rotated);
585        (coded.lo * self.sum + coded.delta * levels) * coded.scale
586    }
587}
588
589/// The smallest and the largest coordinate.
590/// Quantise every coordinate of `x` and write the levels down as `B` planes,
591/// a plane holding one bit of every coordinate.
592///
593/// This is a transpose, and the order it is walked in is nearly all of what it
594/// costs. A coordinate at a time means a read, an or and a write back into `B`
595/// words that are `words` apart, so every coordinate touches `B` different
596/// cache lines and none of the work stays in a register. Sixty four coordinates
597/// at a time keeps those `B` words in registers for the whole run and stores
598/// each of them once, which is what [`level_code`] on the encode path has always
599/// done. Measured through `where_a_probe_goes` at 768 dimensions, preparing a
600/// query against one centroid went from 3.5 microseconds to 0.6, and a search
601/// pays this once for every partition it probes.
602fn transpose<const B: usize>(
603    x: &[f32],
604    lo: f32,
605    by: f32,
606    top: u64,
607    words: usize,
608    planes: &mut [u64],
609) {
610    for (w, chunk) in x.chunks(64).enumerate() {
611        let mut acc = [0u64; B];
612        for (k, &c) in chunk.iter().enumerate() {
613            let level = level_of(c, lo, by, top);
614            for (b, a) in acc.iter_mut().enumerate() {
615                *a |= ((level >> b) & 1) << k;
616            }
617        }
618        for (b, plane) in planes.chunks_exact_mut(words).enumerate() {
619            plane[w] = acc[b];
620        }
621    }
622}
623
624/// The total and the smallest and largest of `x`, in one pass.
625fn sum_and_span(x: &[f32]) -> (f32, f32, f32) {
626    let mut sum = 0.0f32;
627    let mut lo = f32::INFINITY;
628    let mut hi = f32::NEG_INFINITY;
629    for &c in x {
630        sum += c;
631        lo = lo.min(c);
632        hi = hi.max(c);
633    }
634    (sum, lo, hi)
635}
636
637fn span(x: &[f32]) -> (f32, f32) {
638    let mut lo = f32::INFINITY;
639    let mut hi = f32::NEG_INFINITY;
640    for &c in x {
641        lo = lo.min(c);
642        hi = hi.max(c);
643    }
644    (lo, hi)
645}
646
647/// What one level is worth when `lo` to `hi` is cut into `top` of them.
648///
649/// A vector whose coordinates are all the same value has no range to divide up.
650/// It cannot happen after a rotation of a non zero residual, and a division by
651/// zero here would be a silent NaN rather than a loud one.
652fn step(lo: f32, hi: f32, top: u64) -> f32 {
653    if hi > lo { (hi - lo) / top as f32 } else { 1.0 }
654}
655
656/// Which level a coordinate rounds to.
657///
658/// `by` is one over the step rather than the step, because this runs once per
659/// coordinate on both the encode path and the query path and a float division
660/// per coordinate is not worth paying twice for one number.
661///
662/// The half is added rather than [`f32::round`] called, and they are the same
663/// answer here because `lo` is the smallest coordinate there is, so what is
664/// being rounded is never negative and rounding half away from zero is rounding
665/// half up. What it saves is that `round` is a call into the platform's maths
666/// library on any x86-64 target built without SSE4.1, which is the default one,
667/// and this runs once per coordinate on both the encode and the query path.
668fn level_of(c: f32, lo: f32, by: f32, top: u64) -> u64 {
669    (((c - lo) * by + 0.5) as i64).clamp(0, top as i64) as u64
670}
671
672/// Write one word of one plane.
673fn put(code: &mut [u8], words: usize, plane: usize, w: usize, v: u64) {
674    let at = (plane * words + w) * 8;
675    code[at..at + 8].copy_from_slice(&v.to_le_bytes());
676}
677
678/// The sign of each coordinate, written into the one plane a one bit code has.
679///
680/// The reconstruction is `(2 * bit - 1) / sqrt(D)`, which is a unit vector
681/// pointing at one corner of the cube, so `lo` and `delta` fall out of the
682/// dimension and the correction is the sum of the absolute coordinates over
683/// `sqrt(D)`.
684fn sign_code(x: &[f32], code: &mut [u8]) -> Coded {
685    let words = words_of(x.len());
686    let mut abs = 0.0f32;
687    for (w, chunk) in x.chunks(64).enumerate() {
688        let mut bits = 0u64;
689        for (k, &c) in chunk.iter().enumerate() {
690            abs += c.abs();
691            if c >= 0.0 {
692                bits |= 1 << k;
693            }
694        }
695        put(code, words, 0, w, bits);
696    }
697    let root = (x.len() as f32).sqrt();
698    Coded {
699        norm: 0.0,
700        scale: recip(abs / root),
701        lo: -1.0 / root,
702        delta: 2.0 / root,
703    }
704}
705
706/// Sixteen levels between the smallest and the largest coordinate, written
707/// across the four planes a four bit code has.
708fn level_code(x: &[f32], code: &mut [u8]) -> Coded {
709    let words = words_of(x.len());
710    let top = Bits::Four.top();
711    let (lo, hi) = span(x);
712    let delta = step(lo, hi, top);
713    let by = 1.0 / delta;
714    let mut recon = 0.0f32;
715    let mut dot = 0.0f32;
716    for (w, chunk) in x.chunks(64).enumerate() {
717        let mut planes = [0u64; 4];
718        for (k, &c) in chunk.iter().enumerate() {
719            let level = level_of(c, lo, by, top);
720            for (b, plane) in planes.iter_mut().enumerate() {
721                *plane |= ((level >> b) & 1) << k;
722            }
723            let back = lo + level as f32 * delta;
724            recon += back * back;
725            dot += back * c;
726        }
727        for (b, &plane) in planes.iter().enumerate() {
728            put(code, words, b, w, plane);
729        }
730    }
731    let len = recon.sqrt();
732    Coded {
733        norm: 0.0,
734        scale: recip(dot / len),
735        lo: lo / len,
736        delta: delta / len,
737    }
738}
739
740/// The scan: the sum of the code's levels, and the sum of the code's levels
741/// times the query's, both exact.
742///
743/// This is the loop the whole search spends its time in. Every plane of the
744/// code is met by every plane of the query, and a meeting is an AND and a
745/// popcount over 64 coordinates at a time.
746// Clippy wants `as_chunks::<8>()` here and it is 21 percent slower: 17.99
747// microseconds against 14.89 for `scan/one/768`, and 118.58 against 96.96 for
748// `scan/four/768`, both the minimum per iteration out of the same pair of runs.
749// Walking two byte slices side by side is what vectorises, and walking a slice
750// of eight byte arrays against a slice of words is what does not.
751#[allow(clippy::chunks_exact_to_as_chunks)]
752fn packed_dot(code: &[u8], planes: usize, words: usize, query: &[u64]) -> (u32, u32) {
753    assert_eq!(
754        code.len(),
755        planes * words * 8,
756        "a code here is {} bytes and this one is {}",
757        planes * words * 8,
758        code.len()
759    );
760    let mut total = 0u32;
761    let mut cross = 0u32;
762    for (a, plane) in code.chunks_exact(words * 8).enumerate() {
763        let mut ones = 0u32;
764        for chunk in plane.chunks_exact(8) {
765            ones += word(chunk).count_ones();
766        }
767        total += ones << a;
768        for (b, qp) in query.chunks_exact(words).enumerate() {
769            let mut acc = 0u32;
770            for (chunk, &qw) in plane.chunks_exact(8).zip(qp) {
771                acc += (word(chunk) & qw).count_ones();
772            }
773            cross += acc << (a + b);
774        }
775    }
776    (total, cross)
777}
778
779/// The same with the words per plane known at compile time.
780///
781/// Which is the whole point of it. `packed_dot` above walks three nested
782/// `chunks_exact` whose lengths are all runtime values, so the compiler emits
783/// loops where it could have emitted a handful of ANDs and popcounts. Handing
784/// it `W` turns the inner two into straight line code and the arithmetic goes
785/// from 6.31 nanoseconds a member to 1.98 at 128 dimensions.
786///
787/// `planes` and `wide` stay runtime, because there are only two combinations of
788/// them and neither loop is the one that was hurting.
789#[inline]
790fn fixed_dot<const W: usize>(code: &[u8], planes: usize, wide: usize, query: &[u64]) -> (u32, u32) {
791    let mut total = 0u32;
792    let mut cross = 0u32;
793    for a in 0..planes {
794        // Read the plane into words once. Every query plane below meets the
795        // same bytes, and reading them back out of the slice each time is a
796        // load and a bounds check that the register already had the answer to.
797        let mut plane = [0u64; W];
798        let at = a * W * 8;
799        let mut ones = 0u32;
800        for (w, slot) in plane.iter_mut().enumerate() {
801            *slot = word(&code[at + w * 8..at + w * 8 + 8]);
802            ones += slot.count_ones();
803        }
804        total += ones << a;
805        for b in 0..wide {
806            let qp = &query[b * W..(b + 1) * W];
807            let mut acc = 0u32;
808            for (w, &c) in plane.iter().enumerate() {
809                acc += (c & qp[w]).count_ones();
810            }
811            cross += acc << (a + b);
812        }
813    }
814    (total, cross)
815}
816
817/// One over `x`, or zero if there is no such thing.
818///
819/// A correction of zero cannot come out of a non zero residual, because it is
820/// the cosine between a vector and the corner of the cube it rounded to and
821/// that is bounded below by `1/sqrt(D)`. This is here so that if it ever does,
822/// the estimator returns a finite wrong answer for that one member rather than
823/// an infinity that poisons the ordering of everything it is sorted against.
824fn recip(x: f32) -> f32 {
825    if x == 0.0 { 0.0 } else { 1.0 / x }
826}
827
828/// The same dot product against a query that was never quantised, one
829/// coordinate at a time. The reference, not the scan.
830fn exact_dot(code: &[u8], planes: usize, words: usize, query: &[f32]) -> f32 {
831    assert_eq!(
832        code.len(),
833        planes * words * 8,
834        "a code here is {} bytes and this one is {}",
835        planes * words * 8,
836        code.len()
837    );
838    let mut sum = 0.0f32;
839    for (i, &qi) in query.iter().enumerate() {
840        let mut level = 0u32;
841        for b in 0..planes {
842            let at = (b * words + i / 64) * 8;
843            level |= (((word(&code[at..at + 8]) >> (i % 64)) & 1) as u32) << b;
844        }
845        sum += level as f32 * qi;
846    }
847    sum
848}
849
850/// Eight bytes as a word, the way a code stores one.
851fn word(bytes: &[u8]) -> u64 {
852    u64::from_le_bytes(bytes.try_into().expect("eight bytes"))
853}
854
855fn length(v: &[f32]) -> f32 {
856    v.iter().map(|c| c * c).sum::<f32>().sqrt()
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use yo_common::Rng;
863
864    /// Vectors that look a little like embeddings: not uniform, with a few
865    /// coordinates carrying more than their share, which is the case a rotation
866    /// is there to handle.
867    fn corpus(dim: usize, n: usize, seed: u64) -> Vec<Vec<f32>> {
868        let mut rng = Rng::new(seed);
869        (0..n)
870            .map(|_| {
871                let mut v: Vec<f32> = (0..dim)
872                    .map(|i| {
873                        let u = (rng.next_u64() >> 40) as f32 / (1u32 << 24) as f32;
874                        let heavy = if i < dim / 16 { 6.0 } else { 1.0 };
875                        (u * 2.0 - 1.0) * heavy
876                    })
877                    .collect();
878                let len = length(&v);
879                for c in &mut v {
880                    *c /= len;
881                }
882                v
883            })
884            .collect()
885    }
886
887    fn mean(vs: &[Vec<f32>]) -> Vec<f32> {
888        let dim = vs[0].len();
889        let mut c = vec![0.0f32; dim];
890        for v in vs {
891            for (a, b) in c.iter_mut().zip(v) {
892                *a += b;
893            }
894        }
895        for a in &mut c {
896            *a /= vs.len() as f32;
897        }
898        c
899    }
900
901    fn exact(a: &[f32], b: &[f32]) -> f32 {
902        a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
903    }
904
905    fn residual(v: &[f32], c: &[f32]) -> Vec<f32> {
906        v.iter().zip(c).map(|(a, b)| a - b).collect()
907    }
908
909    fn dot(a: &[f32], b: &[f32]) -> f32 {
910        a.iter().zip(b).map(|(x, y)| x * y).sum()
911    }
912
913    /// Encode a corpus and return the codes with what goes beside them.
914    fn encode_all(q: &Quantizer, vs: &[Vec<f32>], c: &[f32]) -> (Vec<u8>, Vec<Coded>) {
915        let width = q.code_bytes();
916        let mut codes = vec![0u8; width * vs.len()];
917        let mut meta = Vec::with_capacity(vs.len());
918        for (i, v) in vs.iter().enumerate() {
919            meta.push(q.encode(v, c, &mut codes[i * width..(i + 1) * width]));
920        }
921        (codes, meta)
922    }
923
924    /// How often the true ten nearest are inside the `keep` best the codes
925    /// picked, which is the number that decides whether rerank has anything to
926    /// work with.
927    fn recall(bits: Bits, dim: usize, keep: usize) -> f32 {
928        let vs = corpus(dim, 800, 1);
929        let qs = corpus(dim, 40, 2);
930        let c = mean(&vs);
931        let q = Quantizer::new(dim, bits, 99);
932        let (codes, meta) = encode_all(&q, &vs, &c);
933        let width = q.code_bytes();
934
935        let mut hits = 0usize;
936        for query in &qs {
937            let mut truth: Vec<(usize, f32)> = vs
938                .iter()
939                .enumerate()
940                .map(|(i, v)| (i, exact(query, v)))
941                .collect();
942            truth.sort_by(|a, b| a.1.total_cmp(&b.1));
943            let want: Vec<usize> = truth[..10].iter().map(|(i, _)| *i).collect();
944
945            let prepared = q.query(query, &c);
946            let mut guess: Vec<(usize, f32)> = (0..vs.len())
947                .map(|i| {
948                    let code = &codes[i * width..(i + 1) * width];
949                    (i, prepared.distance(code, &meta[i]))
950                })
951                .collect();
952            guess.sort_by(|a, b| a.1.total_cmp(&b.1));
953            let got: Vec<usize> = guess[..keep].iter().map(|(i, _)| *i).collect();
954            hits += want.iter().filter(|i| got.contains(i)).count();
955        }
956        hits as f32 / (qs.len() * 10) as f32
957    }
958
959    /// Not a test, a table. Run it with
960    /// `cargo test -p yo-vector --release -- --ignored --nocapture recall_table`
961    /// when the estimator changes, because the two recall tests below only
962    /// assert a floor and this is where the floor came from.
963    #[test]
964    #[ignore = "prints a table rather than asserting anything"]
965    fn recall_table() {
966        for (bits, name) in [(Bits::One, "1 bit"), (Bits::Four, "4 bit")] {
967            for dim in [128usize, 256, 768] {
968                for keep in [10usize, 20, 40, 100] {
969                    println!(
970                        "{name} dim {dim} keep {keep}: {:.3}",
971                        recall(bits, dim, keep)
972                    );
973                }
974            }
975        }
976    }
977
978    #[test]
979    fn a_code_is_the_width_it_says_it_is() {
980        assert_eq!(Quantizer::new(768, Bits::One, 1).code_bytes(), 96);
981        assert_eq!(Quantizer::new(768, Bits::Four, 1).code_bytes(), 384);
982        // A plane is whole words, so a dimension that is not a multiple of 64
983        // pays for the rest of its last one. A hundred coordinates is two
984        // words either way.
985        assert_eq!(Quantizer::new(100, Bits::One, 1).code_bytes(), 16);
986        assert_eq!(Quantizer::new(128, Bits::One, 1).code_bytes(), 16);
987    }
988
989    #[test]
990    #[cfg_attr(
991        miri,
992        ignore = "the count is the claim: recall at ten out of a forty candidate rerank over 256 wide vectors, and a rerank short enough for Miri is not a rerank"
993    )]
994    fn one_bit_finds_the_true_neighbours_inside_a_short_rerank() {
995        // Forty candidates for ten answers, which is the 4k rerank the search
996        // path defaults to.
997        let r = recall(Bits::One, 256, 40);
998        assert!(r >= 0.95, "recall at 10 was {r}");
999    }
1000
1001    #[test]
1002    #[cfg_attr(
1003        miri,
1004        ignore = "the count is the claim: it is one recall figure against another, and the gap between them is only there because both were measured on a corpus big enough to have a wrong answer in it"
1005    )]
1006    fn four_bits_is_better_than_one() {
1007        // Twenty candidates for ten answers, which one bit does not manage and
1008        // four does, so this measures the difference rather than asserting it.
1009        let one = recall(Bits::One, 128, 20);
1010        let four = recall(Bits::Four, 128, 20);
1011        assert!(four > one, "one bit got {one} and four bits got {four}");
1012        assert!(four >= 0.95, "four bit recall at 10 was {four}");
1013    }
1014
1015    #[test]
1016    #[cfg_attr(
1017        miri,
1018        ignore = "the count is the claim: the worst error anywhere in a corpus, and a smaller corpus has less of it to find"
1019    )]
1020    fn the_estimate_is_close_to_the_truth_rather_than_merely_ordered() {
1021        let dim = 256;
1022        let vs = corpus(dim, 200, 5);
1023        let qs = corpus(dim, 20, 6);
1024        let c = mean(&vs);
1025        let q = Quantizer::new(dim, Bits::One, 3);
1026        let (codes, meta) = encode_all(&q, &vs, &c);
1027        let width = q.code_bytes();
1028
1029        let mut worst = 0.0f32;
1030        let mut bias = 0.0f32;
1031        let mut n = 0usize;
1032        for query in &qs {
1033            let prepared = q.query(query, &c);
1034            for (i, v) in vs.iter().enumerate() {
1035                let truth = exact(query, v);
1036                let guess = prepared.distance(&codes[i * width..(i + 1) * width], &meta[i]);
1037                let err = (guess - truth) / truth;
1038                worst = worst.max(err.abs());
1039                bias += err;
1040                n += 1;
1041            }
1042        }
1043        let bias = bias / n as f32;
1044        // Unbiased is the claim, so the average error should sit near zero
1045        // rather than merely being small in absolute value.
1046        assert!(
1047            bias.abs() < 0.02,
1048            "the estimate is off by {bias} on average"
1049        );
1050        assert!(worst < 0.5, "the worst estimate was off by {worst}");
1051    }
1052
1053    /// What quantising the query costs, measured against what the code itself
1054    /// costs, because that is the only comparison that means anything.
1055    ///
1056    /// The query's error and the code's are independent, so they add in
1057    /// quadrature, and a query error a third of the code's makes the whole
1058    /// estimate five percent worse. That is the bar. An absolute threshold here
1059    /// would pass at four bit codes while the query was throwing away the whole
1060    /// reason to pay for them, which is exactly what the first cut did.
1061    #[test]
1062    #[cfg_attr(
1063        miri,
1064        ignore = "the count is the claim: one error measured as a proportion of another, and shrinking the corpus moves both of them"
1065    )]
1066    fn the_query_is_quantised_finer_than_the_code_it_is_measured_against() {
1067        for (bits, dim) in [
1068            (Bits::One, 128),
1069            (Bits::One, 256),
1070            (Bits::One, 768),
1071            (Bits::Four, 256),
1072            (Bits::Four, 768),
1073        ] {
1074            let vs = corpus(dim, 200, 5);
1075            let qs = corpus(dim, 20, 6);
1076            let c = mean(&vs);
1077            let q = Quantizer::new(dim, bits, 3);
1078            let (codes, meta) = encode_all(&q, &vs, &c);
1079            let width = q.code_bytes();
1080
1081            // How far the scan is from the same estimate on an unquantised
1082            // query, and how far that estimate is from the truth.
1083            let mut from_query = 0.0f32;
1084            let mut from_code = 0.0f32;
1085            for query in &qs {
1086                let prepared = q.query(query, &c);
1087                let qr = residual(query, &c);
1088                let qn = length(&qr);
1089                for (i, v) in vs.iter().enumerate() {
1090                    let code = &codes[i * width..(i + 1) * width];
1091                    let fast = prepared.cosine(code, &meta[i]);
1092                    let slow = prepared.cosine_exact(code, &meta[i]);
1093                    let vr = residual(v, &c);
1094                    let truth = dot(&qr, &vr) / (qn * length(&vr));
1095                    from_query += (fast - slow).abs();
1096                    from_code += (slow - truth).abs();
1097                }
1098            }
1099            let ratio = from_query / from_code;
1100            assert!(
1101                ratio < 0.5,
1102                "{dim} at {bits:?}: the query costs {ratio} of what the code costs"
1103            );
1104        }
1105    }
1106
1107    #[test]
1108    fn a_vector_sitting_on_its_centroid_is_not_a_division_by_zero() {
1109        let q = Quantizer::new(16, Bits::One, 1);
1110        let c = vec![0.5f32; 16];
1111        let mut code = vec![0u8; q.code_bytes()];
1112        let coded = q.encode(&c, &c, &mut code);
1113        assert_eq!(coded.norm, 0.0);
1114        assert!(code.iter().all(|b| *b == 0));
1115
1116        let query = q.query(&[1.0f32; 16], &c);
1117        let d = query.distance(&code, &coded);
1118        assert!(d.is_finite(), "{d}");
1119        // The centroid is where it says it is, so the distance is the query's
1120        // own residual and nothing else.
1121        let want: f32 = (0..16).map(|_| 0.25f32).sum();
1122        assert!((d - want).abs() < 1e-3, "{d} against {want}");
1123    }
1124
1125    #[test]
1126    fn a_query_sitting_on_the_centroid_is_not_a_division_by_zero() {
1127        let q = Quantizer::new(16, Bits::One, 1);
1128        let c = vec![0.5f32; 16];
1129        let mut code = vec![0u8; q.code_bytes()];
1130        let v: Vec<f32> = (0..16).map(|i| i as f32 * 0.1).collect();
1131        let coded = q.encode(&v, &c, &mut code);
1132        let d = q.query(&c, &c).distance(&code, &coded);
1133        assert!(d.is_finite(), "{d}");
1134    }
1135
1136    #[test]
1137    fn a_code_is_written_over_whatever_was_in_the_buffer() {
1138        let q = Quantizer::new(32, Bits::One, 1);
1139        let c = vec![0.0f32; 32];
1140        let v: Vec<f32> = (0..32).map(|i| (i as f32).sin()).collect();
1141        let mut fresh = vec![0u8; q.code_bytes()];
1142        let mut dirty = vec![0xffu8; q.code_bytes()];
1143        let a = q.encode(&v, &c, &mut fresh);
1144        let b = q.encode(&v, &c, &mut dirty);
1145        assert_eq!(fresh, dirty);
1146        assert_eq!(a, b);
1147        // And the coordinates that are not there are not set either, because
1148        // the scan popcounts whole words and would count them.
1149        assert!(fresh[4..].iter().all(|b| *b == 0), "{fresh:?}");
1150    }
1151
1152    #[test]
1153    fn the_same_seed_is_the_same_code() {
1154        let v: Vec<f32> = (0..64).map(|i| (i as f32 * 0.3).cos()).collect();
1155        let c = vec![0.0f32; 64];
1156        let mut a = vec![0u8; 8];
1157        let mut b = vec![0u8; 8];
1158        Quantizer::new(64, Bits::One, 12).encode(&v, &c, &mut a);
1159        Quantizer::new(64, Bits::One, 12).encode(&v, &c, &mut b);
1160        assert_eq!(a, b);
1161        let mut d = vec![0u8; 8];
1162        Quantizer::new(64, Bits::One, 13).encode(&v, &c, &mut d);
1163        assert_ne!(a, d, "two seeds should not be one code");
1164    }
1165
1166    /// The planes are the layout the file will hold, so a code has to read back
1167    /// as the levels that went into it.
1168    #[test]
1169    fn a_code_reads_back_as_the_levels_it_was_written_from() {
1170        let dim = 200;
1171        let q = Quantizer::new(dim, Bits::Four, 4);
1172        let c = vec![0.0f32; dim];
1173        let v: Vec<f32> = (0..dim).map(|i| (i as f32 * 0.11).sin()).collect();
1174        let mut code = vec![0u8; q.code_bytes()];
1175        let coded = q.encode(&v, &c, &mut code);
1176
1177        // Pull every level back out of the planes and rebuild the vector the
1178        // code stands for. It should point the same way the original does.
1179        let words = words_of(dim);
1180        let mut back = vec![0.0f32; dim];
1181        for (i, b) in back.iter_mut().enumerate() {
1182            let mut level = 0u32;
1183            for p in 0..4 {
1184                let at = (p * words + i / 64) * 8;
1185                level |= (((word(&code[at..at + 8]) >> (i % 64)) & 1) as u32) << p;
1186            }
1187            *b = coded.lo + level as f32 * coded.delta;
1188        }
1189        let mut spun: Vec<f32> = v.iter().map(|c| c / length(&v)).collect();
1190        crate::Rotation::new(dim, q.seed()).apply(&mut spun);
1191        let cos = back
1192            .iter()
1193            .zip(&spun)
1194            .map(|(a, b)| a * b)
1195            .sum::<f32>()
1196            .abs()
1197            / length(&back);
1198        assert!(cos > 0.95, "the code points somewhere else: {cos}");
1199    }
1200}