Skip to main content

nodedb_vector/distance/simd/
bbq.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! BBQ rerank kernels: L2 from the exact centred query to a 1-bit-encoded
4//! candidate. `centered` is the centred query as packed little-endian f32 bytes
5//! (`dim * 4`), `packed` the candidate's sign bits (`dim.div_ceil(8)`, MSB-first
6//! per byte), `residual_norm` the candidate's stored corrective factor.
7//!
8//! The scalar kernel here is the reference; every SIMD tier in the sibling
9//! modules must agree within `PARITY_REL` (see the tests below).
10//!
11//! Tier coverage is host-dependent, and CI covers only one of them. Both jobs in
12//! `.github/workflows/test.yml` run on `ubuntu-24.04-arm`, so CI exercises the
13//! NEON tier and never an x86 one. The x86 tiers are checked on an x86_64 host;
14//! the 512-bit tier additionally needs hardware or Intel SDE (`sde64 -spr`).
15//! Each x86 test returns early, with a printed note, on a host that lacks the
16//! feature its tier needs.
17
18pub(super) fn recon_scale(residual_norm: f32, dim: usize) -> f32 {
19    if dim > 0 {
20        residual_norm / (dim as f32).sqrt()
21    } else {
22        0.0
23    }
24}
25
26/// One centered lane from the little-endian payload.
27#[inline]
28pub(super) fn centered_at(centered: &[u8], i: usize) -> f32 {
29    let b = &centered[i * 4..i * 4 + 4];
30    f32::from_le_bytes([b[0], b[1], b[2], b[3]])
31}
32
33/// Panics unless both byte slices hold what `dim` dimensions need: `dim * 4`
34/// bytes of little-endian f32 query lanes, and `dim.div_ceil(8)` bytes of sign
35/// bits. Every safe entry point runs this before any pointer arithmetic, so a
36/// caller handing over short slices gets a panic instead of a read past the end.
37pub(super) fn assert_payload_shapes(centered: &[u8], packed: &[u8], dim: usize) {
38    // Saturating: `dim * 4` must not wrap to a small value and slip past the
39    // check on an absurd `dim`.
40    let need_centered = dim.saturating_mul(4);
41    let need_packed = dim.div_ceil(8);
42    assert!(
43        centered.len() >= need_centered,
44        "bbq l2: centered payload too short: {} bytes, need {need_centered} for dim {dim}",
45        centered.len()
46    );
47    assert!(
48        packed.len() >= need_packed,
49        "bbq l2: packed bits too short: {} bytes, need {need_packed} for dim {dim}",
50        packed.len()
51    );
52}
53
54/// Whole-range scalar kernel: the tail helper applied from 0.
55pub fn l2_bbq(centered: &[u8], packed: &[u8], residual_norm: f32, dim: usize) -> f32 {
56    assert_payload_shapes(centered, packed, dim);
57    let scale = recon_scale(residual_norm, dim);
58    l2_scalar_from_bytes(centered, packed, scale, 0, dim).sqrt()
59}
60
61/// Scalar accumulation of `(q − ±scale)²` for `from..dim`. Every tier's tail
62/// uses this, so a vector tail cannot diverge from the head formula.
63#[inline]
64pub(super) fn l2_scalar_from_bytes(
65    centered: &[u8],
66    packed: &[u8],
67    scale: f32,
68    from: usize,
69    dim: usize,
70) -> f32 {
71    let mut acc = 0.0f32;
72    for i in from..dim {
73        let bit = (packed[i / 8] >> (7 - (i % 8))) & 1;
74        let recon = if bit != 0 { scale } else { -scale };
75        let d = centered_at(centered, i) - recon;
76        acc += d * d;
77    }
78    acc
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    /// Relative tolerance between accumulation orders, with an absolute floor
86    /// for near-zero distances.
87    const PARITY_REL: f64 = 1e-4;
88    const PARITY_ABS: f64 = 1e-6;
89
90    /// Dimensions under test: the lane widths (16 f32 lanes for AVX-512, 8 for
91    /// AVX2, 4 for NEON and wasm), their multiples, and the partial tails
92    /// between them.
93    const DIMS: [usize; 30] = [
94        0, 1, 3, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 100, 127, 128, 129, 191, 255, 256,
95        257, 383, 384, 511, 512, 513, 767, 768,
96    ];
97
98    /// The unfused reference: reconstruct each dimension, then measure L2.
99    /// Accumulated in f64 so the oracle is independent of f32 order.
100    fn reference_l2(centered: &[u8], packed: &[u8], residual_norm: f32, dim: usize) -> f64 {
101        let scale = recon_scale(residual_norm, dim) as f64;
102        let mut acc = 0.0f64;
103        for i in 0..dim {
104            let bit = (packed[i / 8] >> (7 - (i % 8))) & 1;
105            let recon = if bit != 0 { scale } else { -scale };
106            let d = centered_at(centered, i) as f64 - recon;
107            acc += d * d;
108        }
109        acc.sqrt()
110    }
111
112    fn within_parity(got: f32, expected: f64) -> bool {
113        ((got as f64) - expected).abs() <= PARITY_ABS.max(PARITY_REL * expected.abs())
114    }
115
116    /// Deterministic pseudo-random inputs, so a failure reproduces.
117    fn sample(dim: usize, seed: u64) -> (Vec<u8>, Vec<u8>, f32) {
118        let mut state = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
119        let mut next = || {
120            state ^= state << 13;
121            state ^= state >> 7;
122            state ^= state << 17;
123            state
124        };
125        let lanes: Vec<f32> = (0..dim.max(1))
126            .map(|_| ((next() % 2000) as f32 / 1000.0) - 1.0)
127            .collect();
128        let mut centered = Vec::with_capacity(dim * 4);
129        for x in &lanes[..dim] {
130            centered.extend_from_slice(&x.to_le_bytes());
131        }
132        let packed: Vec<u8> = (0..dim.max(1).div_ceil(8))
133            .map(|_| (next() % 256) as u8)
134            .collect();
135        let residual_norm = ((next() % 1000) as f32 / 100.0) + 1.0;
136        (centered, packed, residual_norm)
137    }
138
139    #[test]
140    fn the_scalar_kernel_always_matches_the_reference() {
141        for dim in DIMS {
142            let (centered, packed, residual_norm) = sample(dim, 7 + dim as u64);
143            let expected = reference_l2(&centered, &packed, residual_norm, dim);
144            let got = l2_bbq(&centered, &packed, residual_norm, dim);
145            assert!(
146                within_parity(got, expected),
147                "scalar dim {dim}: {got} != reference {expected}"
148            );
149        }
150    }
151
152    /// The length guard runs before any pointer arithmetic, so a short slice is
153    /// rejected rather than read past its end.
154    #[test]
155    #[should_panic(expected = "centered payload too short")]
156    fn short_centered_payload_is_rejected() {
157        let _ = l2_bbq(&[], &[0u8; 2], 1.0, 16);
158    }
159
160    #[test]
161    #[should_panic(expected = "packed bits too short")]
162    fn short_packed_payload_is_rejected() {
163        let centered = vec![0u8; 16 * 4];
164        let _ = l2_bbq(&centered, &[], 1.0, 16);
165    }
166
167    #[test]
168    fn dispatch_selects_a_tier_that_matches_the_reference() {
169        let (centered, packed, residual_norm) = sample(768, 99);
170        let expected = reference_l2(&centered, &packed, residual_norm, 768);
171        let kernel = crate::distance::simd::runtime::runtime();
172        let got = (kernel.l2_bbq)(&centered, &packed, residual_norm, 768);
173        assert!(
174            within_parity(got, expected),
175            "{}: {got} != reference {expected}",
176            kernel.name
177        );
178    }
179
180    /// x86_64 tiers exercised directly, gated on the host's feature bits. Under
181    /// `sde64 -spr` the 512-bit tier runs here; on an AVX2-only host its test
182    /// returns early.
183    #[cfg(all(target_arch = "x86_64", target_endian = "little"))]
184    mod x86 {
185        use super::*;
186
187        fn check_tier(name: &str, f: fn(&[u8], &[u8], f32, usize) -> f32) {
188            for dim in DIMS {
189                let (centered, packed, residual_norm) = sample(dim, 1234 + dim as u64);
190                let expected = reference_l2(&centered, &packed, residual_norm, dim);
191                let got = f(&centered, &packed, residual_norm, dim);
192                assert!(
193                    within_parity(got, expected),
194                    "{name} dim {dim}: {got} != reference {expected}"
195                );
196                // Parity against the scalar kernel directly, not only the oracle.
197                let scalar = l2_bbq(&centered, &packed, residual_norm, dim);
198                assert!(
199                    within_parity(got, scalar as f64),
200                    "{name} dim {dim}: {got} != scalar {scalar}"
201                );
202            }
203        }
204
205        #[test]
206        fn avx512_tier_matches_the_reference() {
207            if !std::is_x86_feature_detected!("avx512f") {
208                eprintln!("avx512 tier: skipped (no avx512f; run under sde64 -spr)");
209                return;
210            }
211            // The entry point is safe but does not probe the CPU itself; the
212            // check above is what keeps this off a host without avx512f.
213            check_tier("avx512", crate::distance::simd::avx512::l2_bbq);
214        }
215
216        #[test]
217        fn avx2_tier_matches_the_reference() {
218            if !(std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma")) {
219                eprintln!("avx2 tier: skipped (no avx2+fma)");
220                return;
221            }
222            // The entry point is safe but does not probe the CPU itself; the
223            // checks above are what keep this off a host without avx2+fma.
224            check_tier("avx2", crate::distance::simd::avx2::l2_bbq);
225        }
226
227        /// Each tier's safe entry rejects short slices. The guard runs before
228        /// the first vector load, so these panic on any host and need no
229        /// AVX-512 or AVX2 hardware to be valid.
230        #[test]
231        #[should_panic(expected = "centered payload too short")]
232        fn avx512_rejects_empty_slices() {
233            let _ = crate::distance::simd::avx512::l2_bbq(&[], &[], 1.0, 16);
234        }
235
236        #[test]
237        #[should_panic(expected = "centered payload too short")]
238        fn avx2_rejects_empty_slices() {
239            let _ = crate::distance::simd::avx2::l2_bbq(&[], &[], 1.0, 16);
240        }
241    }
242
243    /// aarch64 and wasm tiers compile-check and self-test on their targets.
244    #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
245    #[test]
246    fn neon_tier_matches_the_reference() {
247        // NEON is baseline on aarch64 builds that pass this cfg.
248        let f = crate::distance::simd::neon::l2_bbq;
249        for dim in DIMS {
250            let (centered, packed, residual_norm) = sample(dim, 55 + dim as u64);
251            let expected = reference_l2(&centered, &packed, residual_norm, dim);
252            let got = f(&centered, &packed, residual_norm, dim);
253            assert!(
254                within_parity(got, expected),
255                "neon dim {dim}: {got} != reference {expected}"
256            );
257        }
258    }
259
260    #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
261    #[test]
262    #[should_panic(expected = "centered payload too short")]
263    fn neon_rejects_empty_slices() {
264        let _ = crate::distance::simd::neon::l2_bbq(&[], &[], 1.0, 16);
265    }
266
267    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
268    #[test]
269    fn wasm_simd128_tier_matches_the_reference() {
270        for dim in DIMS {
271            let (centered, packed, residual_norm) = sample(dim, 55 + dim as u64);
272            let expected = reference_l2(&centered, &packed, residual_norm, dim);
273            let got =
274                crate::distance::simd::wasm_simd128::l2_bbq(&centered, &packed, residual_norm, dim);
275            assert!(
276                within_parity(got, expected),
277                "wasm dim {dim}: {got} != reference {expected}"
278            );
279        }
280    }
281}