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, ¢roid, &mut code);
28//!
29//! // The query is prepared once and then measured against many codes.
30//! let query = q.query(&v, ¢roid);
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}