1use crate::lloyd;
5use crate::rhdh::{padded_dim, Rhdh};
6
7pub struct VecqIndex {
14 pub(crate) dim: usize,
15 padded: usize,
16 pub(crate) seed: u64,
17 transform: Rhdh,
18 pub(crate) codes: Vec<u8>, pub(crate) scales: Vec<f32>, pub(crate) n: usize,
21}
22
23impl VecqIndex {
24 pub fn new(dim: usize, seed: u64) -> Self {
27 let padded = padded_dim(dim);
28 Self {
29 dim,
30 padded,
31 seed,
32 transform: Rhdh::new(dim, seed),
33 codes: Vec::new(),
34 scales: Vec::new(),
35 n: 0,
36 }
37 }
38
39 pub fn len(&self) -> usize {
40 self.n
41 }
42
43 pub fn is_empty(&self) -> bool {
44 self.n == 0
45 }
46
47 pub fn dim(&self) -> usize {
48 self.dim
49 }
50
51 pub fn seed(&self) -> u64 {
52 self.seed
53 }
54
55 #[cfg(test)]
56 pub(crate) fn padded(&self) -> usize {
57 self.padded
58 }
59
60 pub fn add(&mut self, v: &[f32]) {
62 assert_eq!(v.len(), self.dim, "vector dim mismatch");
63 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
64 assert!(norm > 0.0, "zero vector");
65 let unit: Vec<f32> = v.iter().map(|x| x / norm).collect();
66
67 let mut rotated = Vec::with_capacity(self.padded);
68 self.transform.apply(&unit, &mut rotated);
69
70 let bytes_per_vec = self.padded / 2;
72 let base = self.codes.len();
73 self.codes.resize(base + bytes_per_vec, 0);
74 let mut sum_sq = 0f32;
75 for (i, &x) in rotated.iter().enumerate() {
76 let code = lloyd::quantize_4bit(x);
77 let b = base + i / 2;
78 if i % 2 == 0 {
79 self.codes[b] |= code;
80 } else {
81 self.codes[b] |= code << 4;
82 }
83 sum_sq += lloyd::dequantize_4bit(code).powi(2);
84 }
85
86 self.scales.push(1.0 / sum_sq.sqrt());
89 self.n += 1;
90 }
91
92 pub fn prepare_query(&self, q: &[f32]) -> PreparedQuery {
94 assert_eq!(q.len(), self.dim);
95 let norm: f32 = q.iter().map(|x| x * x).sum::<f32>().sqrt();
96 let unit: Vec<f32> = q.iter().map(|x| x / norm).collect();
97 let mut rotated = Vec::with_capacity(self.padded);
98 self.transform.apply(&unit, &mut rotated);
99 let rnorm: f32 = rotated.iter().map(|x| x * x).sum::<f32>().sqrt();
102 for x in rotated.iter_mut() {
103 *x /= rnorm;
104 }
105 let mut lut = [0f32; 16];
108 for (c, slot) in lut.iter_mut().enumerate() {
109 *slot = lloyd::dequantize_4bit(c as u8);
110 }
111 PreparedQuery { rotated, lut, norm }
112 }
113
114 #[inline]
123 pub fn score(&self, pq: &PreparedQuery, idx: usize) -> f32 {
124 let base = idx * (self.padded / 2);
125 let codes = &self.codes[base..base + self.padded / 2];
126 let q = &pq.rotated[..self.padded];
127 #[cfg(target_arch = "aarch64")]
128 {
129 let raw = unsafe { neon::score_neon(codes, q, &pq.lut) };
131 raw * self.scales[idx]
132 }
133 #[cfg(not(target_arch = "aarch64"))]
134 {
135 score_scalar(codes, q, &pq.lut) * self.scales[idx]
136 }
137 }
138
139 pub fn search(&self, q: &[f32], k: usize) -> Vec<(usize, f32)> {
145 use std::cmp::Reverse;
146 use std::collections::BinaryHeap;
147
148 let pq = self.prepare_query(q);
149 let k = k.min(self.n).max(1);
150 #[cfg(target_arch = "aarch64")]
151 let bpv = self.padded / 2;
152 let key = |s: f32| -> u32 {
155 let b = s.to_bits();
156 if b & 0x8000_0000 != 0 {
157 !b
158 } else {
159 b ^ 0x8000_0000
160 }
161 };
162 let mut heap: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(k + 1);
163 let consider = |s: f32, idx: usize, heap: &mut BinaryHeap<Reverse<(u32, usize)>>| {
164 let ks = key(s);
165 if heap.len() < k {
166 heap.push(Reverse((ks, idx)));
167 } else if ks > heap.peek().map(|r| r.0 .0).unwrap_or(0) {
168 heap.push(Reverse((ks, idx)));
169 heap.pop();
170 }
171 };
172 #[cfg(target_arch = "aarch64")]
173 let q_rot = &pq.rotated[..self.padded];
174 let mut idx = 0;
175 #[cfg(target_arch = "aarch64")]
176 {
177 while idx + 4 <= self.n {
179 let codes4 = &self.codes[idx * bpv..(idx + 4) * bpv];
180 let raw = unsafe { neon::score_neon4(codes4, q_rot, &pq.lut) };
181 for (v, &r) in raw.iter().enumerate() {
182 consider(r * self.scales[idx + v], idx + v, &mut heap);
183 }
184 idx += 4;
185 }
186 }
187 while idx < self.n {
188 consider(self.score(&pq, idx), idx, &mut heap);
189 idx += 1;
190 }
191 let key_undo = |k: u32| -> u32 {
193 if k & 0x8000_0000 != 0 {
194 k ^ 0x8000_0000 } else {
196 !k }
198 };
199 let mut out: Vec<(usize, f32)> = heap
200 .into_iter()
201 .map(|r| (r.0 .1, f32::from_bits(key_undo(r.0 .0))))
202 .collect();
203 out.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("no NaN scores"));
204 out
205 }
206}
207
208#[cfg_attr(target_arch = "aarch64", cfg(test))]
211pub(crate) fn score_scalar(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
212 let nb = codes.len();
213 let mut acc = [0f32; 8];
214 let mut i = 0;
215 while i + 8 <= nb {
216 for j in 0..8 {
217 let b = codes[i + j];
218 let c = (i + j) * 2;
219 acc[j] += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
220 }
221 i += 8;
222 }
223 let mut tail = 0f32;
224 while i < nb {
225 let b = codes[i];
226 let c = i * 2;
227 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
228 i += 1;
229 }
230 let s01 = acc[0] + acc[1];
231 let s23 = acc[2] + acc[3];
232 let s45 = acc[4] + acc[5];
233 let s67 = acc[6] + acc[7];
234 (s01 + s23) + (s45 + s67) + tail
235}
236
237#[cfg(target_arch = "aarch64")]
238mod neon {
239 use std::arch::aarch64::*;
252
253 #[inline]
259 unsafe fn gather16(tbl: uint8x16x4_t, nibbles: uint8x16_t) -> [float32x4_t; 4] {
260 let idx = vmulq_u8(nibbles, vdupq_n_u8(4)); let one = vdupq_n_u8(1);
262 let two = vdupq_n_u8(2);
263 let b0 = vqtbl4q_u8(tbl, idx);
264 let b1 = vqtbl4q_u8(tbl, vaddq_u8(idx, one));
265 let b2 = vqtbl4q_u8(tbl, vaddq_u8(idx, two));
266 let b3 = vqtbl4q_u8(tbl, vaddq_u8(idx, vdupq_n_u8(3)));
267 let z01 = vzip1q_u8(b0, b1); let z23 = vzip1q_u8(b2, b3); let z01b = vzip2q_u8(b0, b1);
271 let z23b = vzip2q_u8(b2, b3);
272 let lo16 = vreinterpretq_u16_u8(z01);
273 let hi16 = vreinterpretq_u16_u8(z23);
274 let lo16b = vreinterpretq_u16_u8(z01b);
275 let hi16b = vreinterpretq_u16_u8(z23b);
276 [
277 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16, hi16))),
278 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16, hi16))),
279 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16b, hi16b))),
280 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16b, hi16b))),
281 ]
282 }
283
284 #[inline]
286 pub unsafe fn score_neon(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
287 let mut bytes = [0u8; 64];
289 for (c, &v) in lut.iter().enumerate() {
290 bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
291 }
292 let tbl = uint8x16x4_t(
293 vld1q_u8(bytes[0..16].as_ptr()),
294 vld1q_u8(bytes[16..32].as_ptr()),
295 vld1q_u8(bytes[32..48].as_ptr()),
296 vld1q_u8(bytes[48..64].as_ptr()),
297 );
298 let mut acc_lo = vdupq_n_f32(0.0);
300 let mut acc_hi = vdupq_n_f32(0.0);
301 let nb = codes.len();
302 let mut i = 0;
303 while i + 8 <= nb {
304 let b8 = vld1_u8(codes.as_ptr().add(i)); let lo = vand_u8(b8, vdup_n_u8(0x0F));
308 let hi = vshr_n_u8(b8, 4);
309 let nibbles = vcombine_u8(lo, hi); let g = gather16(tbl, nibbles);
311 let q0 = vld1q_f32(q.as_ptr().add(i * 2));
315 let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
316 let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
317 let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
318 let q_even_lo = vuzp1q_f32(q0, q1); let q_even_hi = vuzp1q_f32(q2, q3);
320 let q_odd_lo = vuzp2q_f32(q0, q1);
321 let q_odd_hi = vuzp2q_f32(q2, q3);
322 let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
324 let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
325 acc_lo = vaddq_f32(acc_lo, t_lo);
326 acc_hi = vaddq_f32(acc_hi, t_hi);
327 i += 8;
328 }
329 let mut acc = [0f32; 8];
331 acc[0] = vgetq_lane_f32(acc_lo, 0);
332 acc[1] = vgetq_lane_f32(acc_lo, 1);
333 acc[2] = vgetq_lane_f32(acc_lo, 2);
334 acc[3] = vgetq_lane_f32(acc_lo, 3);
335 acc[4] = vgetq_lane_f32(acc_hi, 0);
336 acc[5] = vgetq_lane_f32(acc_hi, 1);
337 acc[6] = vgetq_lane_f32(acc_hi, 2);
338 acc[7] = vgetq_lane_f32(acc_hi, 3);
339 let mut tail = 0f32;
342 while i < nb {
343 let b = codes[i];
344 let c = i * 2;
345 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
346 i += 1;
347 }
348 let s01 = acc[0] + acc[1];
349 let s23 = acc[2] + acc[3];
350 let s45 = acc[4] + acc[5];
351 let s67 = acc[6] + acc[7];
352 (s01 + s23) + (s45 + s67) + tail
353 }
354
355 #[inline]
361 pub unsafe fn score_neon4(codes4: &[u8], q: &[f32], lut: &[f32; 16]) -> [f32; 4] {
362 let mut bytes = [0u8; 64];
363 for (c, &v) in lut.iter().enumerate() {
364 bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
365 }
366 let tbl = uint8x16x4_t(
367 vld1q_u8(bytes[0..16].as_ptr()),
368 vld1q_u8(bytes[16..32].as_ptr()),
369 vld1q_u8(bytes[32..48].as_ptr()),
370 vld1q_u8(bytes[48..64].as_ptr()),
371 );
372 let nb = codes4.len() / 4; let mut acc_lo = [vdupq_n_f32(0.0); 4];
374 let mut acc_hi = [vdupq_n_f32(0.0); 4];
375 let mut i = 0;
376 while i + 8 <= nb {
377 let q0 = vld1q_f32(q.as_ptr().add(i * 2));
379 let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
380 let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
381 let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
382 let q_even_lo = vuzp1q_f32(q0, q1);
383 let q_even_hi = vuzp1q_f32(q2, q3);
384 let q_odd_lo = vuzp2q_f32(q0, q1);
385 let q_odd_hi = vuzp2q_f32(q2, q3);
386 for v in 0..4 {
387 let b8 = vld1_u8(codes4.as_ptr().add(v * nb + i));
388 let lo = vand_u8(b8, vdup_n_u8(0x0F));
389 let hi = vshr_n_u8(b8, 4);
390 let nibbles = vcombine_u8(lo, hi);
391 let g = gather16(tbl, nibbles);
392 let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
393 let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
394 acc_lo[v] = vaddq_f32(acc_lo[v], t_lo);
395 acc_hi[v] = vaddq_f32(acc_hi[v], t_hi);
396 }
397 i += 8;
398 }
399 let mut out = [0f32; 4];
400 for v in 0..4 {
401 let mut a = [0f32; 8];
403 a[0] = vgetq_lane_f32(acc_lo[v], 0);
404 a[1] = vgetq_lane_f32(acc_lo[v], 1);
405 a[2] = vgetq_lane_f32(acc_lo[v], 2);
406 a[3] = vgetq_lane_f32(acc_lo[v], 3);
407 a[4] = vgetq_lane_f32(acc_hi[v], 0);
408 a[5] = vgetq_lane_f32(acc_hi[v], 1);
409 a[6] = vgetq_lane_f32(acc_hi[v], 2);
410 a[7] = vgetq_lane_f32(acc_hi[v], 3);
411 let mut tail = 0f32;
413 let mut j = i;
414 while j < nb {
415 let b = codes4[v * nb + j];
416 let c = j * 2;
417 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
418 j += 1;
419 }
420 let s01 = a[0] + a[1];
421 let s23 = a[2] + a[3];
422 let s45 = a[4] + a[5];
423 let s67 = a[6] + a[7];
424 out[v] = (s01 + s23) + (s45 + s67) + tail;
425 }
426 out
427 }
428}
429
430pub struct PreparedQuery {
432 rotated: Vec<f32>,
433 lut: [f32; 16],
434 #[allow(dead_code)]
435 norm: f32,
436}
437
438pub fn cosine_f32(a: &[f32], b: &[f32]) -> f32 {
440 let mut dot = 0f32;
441 let mut na = 0f32;
442 let mut nb = 0f32;
443 for i in 0..a.len() {
444 dot += a[i] * b[i];
445 na += a[i] * a[i];
446 nb += b[i] * b[i];
447 }
448 dot / (na.sqrt() * nb.sqrt())
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 fn rand_unit(dim: usize, seed: u64) -> Vec<f32> {
456 let mut x = seed | 1;
458 let mut v = Vec::with_capacity(dim);
459 for _ in 0..dim {
460 x ^= x << 13;
461 x ^= x >> 7;
462 x ^= x << 17;
463 let u1 = ((x >> 11) as f64 / (1u64 << 53) as f64).max(1e-12);
464 x ^= x << 16;
465 let u2 = (x >> 11) as f64 / (1u64 << 53) as f64;
466 v.push(((-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()) as f32);
467 }
468 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
469 v.into_iter().map(|a| a / norm).collect()
470 }
471
472 #[test]
473 fn score_correlates_with_exact_cosine() {
474 let dim = 128;
475 let mut idx = VecqIndex::new(dim, 7);
476 let base: Vec<Vec<f32>> = (0..200).map(|i| rand_unit(dim, i + 1)).collect();
477 for v in &base {
478 idx.add(v);
479 }
480 let q = rand_unit(dim, 999);
481 let pq = idx.prepare_query(&q);
482 let exact: Vec<f32> = base.iter().map(|v| cosine_f32(&q, v)).collect();
483 let mut max_err = 0f32;
484 for (i, &e) in exact.iter().enumerate().take(200) {
485 let est = idx.score(&pq, i);
486 max_err = max_err.max((est - e).abs());
487 }
488 assert!(max_err < 0.2, "max score error {max_err}");
489 }
490
491 #[test]
492 fn score_reproducible_and_close_to_naive() {
493 let dim = 128;
494 let mut idx = VecqIndex::new(dim, 13);
495 for i in 0..50 {
496 idx.add(&rand_unit(dim, i + 21));
497 }
498 let q = rand_unit(dim, 321);
499 let pq = idx.prepare_query(&q);
500 for vi in 0..50 {
501 let base = vi * (idx.padded() / 2);
502 let mut naive = 0f32;
503 for i in 0..idx.padded() {
504 let b = idx.codes[base + i / 2];
505 let code = if i % 2 == 0 { b & 0x0F } else { b >> 4 };
506 naive += pq.rotated[i] * pq.lut[code as usize];
507 }
508 let s = idx.score(&pq, vi);
509 assert_eq!(s.to_bits(), idx.score(&pq, vi).to_bits());
510 assert!((s - naive * idx.scales[vi]).abs() < 1e-5, "vector {vi}");
511 }
512 }
513
514 #[test]
515 fn neon_matches_scalar_bitwise() {
516 let dim = 128;
517 let mut idx = VecqIndex::new(dim, 42);
518 for i in 0..30 {
519 idx.add(&rand_unit(dim, i + 500));
520 }
521 let q = rand_unit(dim, 777);
522 let pq = idx.prepare_query(&q);
523 for vi in 0..30 {
524 let base = vi * (idx.padded() / 2);
525 let codes = &idx.codes[base..base + idx.padded() / 2];
526 let qslice = &pq.rotated[..idx.padded()];
527 #[cfg(target_arch = "aarch64")]
528 {
529 let neon = unsafe { neon::score_neon(codes, qslice, &pq.lut) };
530 let scalar = score_scalar(codes, qslice, &pq.lut);
531 assert_eq!(
532 neon.to_bits(),
533 scalar.to_bits(),
534 "vector {vi}: NEON and scalar diverged"
535 );
536 }
537 #[cfg(not(target_arch = "aarch64"))]
538 {
539 let _ = (base, codes, qslice);
540 }
541 }
542 }
543
544 #[cfg(target_arch = "aarch64")]
545 #[test]
546 fn neon4_matches_neon_bitwise() {
547 let dim = 128;
548 let mut idx = VecqIndex::new(dim, 91);
549 for i in 0..12 {
550 idx.add(&rand_unit(dim, i + 90));
551 }
552 let q = rand_unit(dim, 1234);
553 let pq = idx.prepare_query(&q);
554 let bpv = idx.padded() / 2;
555 for chunk_start in (0..12).step_by(4) {
556 let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
557 let qslice = &pq.rotated[..idx.padded()];
558 {
559 let batched = unsafe { neon::score_neon4(codes4, qslice, &pq.lut) };
560 for v in 0..4 {
561 let single = unsafe {
562 neon::score_neon(&codes4[v * bpv..(v + 1) * bpv], qslice, &pq.lut)
563 };
564 assert_eq!(
565 batched[v].to_bits(),
566 single.to_bits(),
567 "chunk {chunk_start} vec {v}: neon4 diverged from neon"
568 );
569 }
570 }
571 }
572 }
573
574 #[test]
575 fn search_returns_sorted_results() {
576 let dim = 64;
577 let mut idx = VecqIndex::new(dim, 3);
578 for i in 0..50 {
579 idx.add(&rand_unit(dim, i * 31 + 5));
580 }
581 let q = rand_unit(dim, 77);
582 let res = idx.search(&q, 5);
583 assert_eq!(res.len(), 5);
584 for w in res.windows(2) {
585 assert!(w[0].1 >= w[1].1);
586 }
587 }
588
589 #[test]
590 fn quantized_size_is_one_eighth() {
591 let dim = 384;
592 let mut idx = VecqIndex::new(dim, 1);
593 idx.add(&rand_unit(dim, 11));
594 assert_eq!(idx.codes.len(), 512 / 2);
595 }
596}