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