nodedb_vector/rerank/codecs/
bbq.rs1use nodedb_codec::vector_quant::bbq::BbqCodec;
19use nodedb_codec::vector_quant::codec::VectorCodec as _;
20use nodedb_codec::vector_quant::layout::{QuantMode, UnifiedQuantizedVectorRef};
21
22use crate::{
23 rerank::codec::{CodecName, PreparedQuery, RerankCodec},
24 rerank::types::RerankError,
25};
26
27fn encode_payload(query_norm: f32, centered: &[f32]) -> Vec<u8> {
30 let mut buf = Vec::with_capacity(4 + centered.len() * 4);
32 buf.extend_from_slice(&query_norm.to_le_bytes());
33 for &x in centered {
34 buf.extend_from_slice(&x.to_le_bytes());
35 }
36 buf
37}
38
39fn payload_len(dim: usize) -> Option<usize> {
42 dim.checked_mul(4)?.checked_add(4)
43}
44
45pub const DEFAULT_OVERSAMPLE: u8 = 4;
49
50pub struct BbqRerank {
59 codec: Option<BbqCodec>,
60 dim: usize,
61 oversample: u8,
62}
63
64impl BbqRerank {
65 pub fn new(dim: usize, oversample: u8) -> Self {
70 Self {
71 codec: None,
72 dim,
73 oversample,
74 }
75 }
76
77 pub fn from_codec(codec: BbqCodec) -> Self {
79 let dim = codec.dim;
80 Self {
81 codec: Some(codec),
82 dim,
83 oversample: DEFAULT_OVERSAMPLE,
84 }
85 }
86}
87
88impl RerankCodec for BbqRerank {
89 fn encode(&self, v: &[f32]) -> Result<Vec<u8>, RerankError> {
95 if v.len() != self.dim {
96 return Err(RerankError::BadInput(format!(
97 "bbq encode: vector len {} != codec dim {}",
98 v.len(),
99 self.dim
100 )));
101 }
102 let codec = self.codec.as_ref().ok_or_else(|| {
103 RerankError::NotTrained(
104 "bbq: codec must be trained before encoding (call train() with a sample of vectors)"
105 .to_string(),
106 )
107 })?;
108 let quantized = codec.encode(v);
109 Ok(quantized.as_ref().as_bytes().to_vec())
110 }
111
112 fn prepare_query(&self, q: &[f32]) -> Result<PreparedQuery, RerankError> {
118 if q.len() != self.dim {
119 return Err(RerankError::BadInput(format!(
120 "bbq prepare_query: query len {} != codec dim {}",
121 q.len(),
122 self.dim
123 )));
124 }
125 let codec = self.codec.as_ref().ok_or_else(|| {
126 RerankError::NotTrained(
127 "bbq: codec must be trained before prepare_query (call train() with a sample of vectors)"
128 .to_string(),
129 )
130 })?;
131 let query = codec.prepare_query(q);
132 Ok(PreparedQuery::Bytes(encode_payload(
133 query.query_norm,
134 &query.centered,
135 )))
136 }
137
138 fn distance_prepared(
147 &self,
148 prepared: &PreparedQuery,
149 encoded: &[u8],
150 ) -> Result<f32, RerankError> {
151 let payload = match prepared {
152 PreparedQuery::Bytes(b) => b.as_slice(),
153 _ => {
154 return Err(RerankError::BadInput(
155 "bbq distance: prepared query is not Bytes".to_string(),
156 ));
157 }
158 };
159
160 let Some(expected) = payload_len(self.dim) else {
161 return Err(RerankError::BadInput(format!(
162 "bbq distance: dim {} overflows the prepared payload length",
163 self.dim
164 )));
165 };
166 if payload.len() != expected {
167 return Err(RerankError::BadInput(format!(
168 "bbq distance: payload len {} != expected {} for dim {}",
169 payload.len(),
170 expected,
171 self.dim
172 )));
173 }
174
175 let packed_len = self.dim.div_ceil(8);
176 let uqv_ref = UnifiedQuantizedVectorRef::from_bytes(encoded, packed_len).map_err(|e| {
177 RerankError::BadInput(format!("bbq distance: failed to parse encoded bytes: {e}"))
178 })?;
179
180 let header = uqv_ref.header();
185 if usize::from(header.dim) != self.dim {
186 return Err(RerankError::BadInput(format!(
187 "bbq distance: candidate dim {} != codec dim {}",
188 header.dim, self.dim
189 )));
190 }
191 if header.quant_mode != QuantMode::Bbq as u16 {
192 return Err(RerankError::BadInput(format!(
193 "bbq distance: candidate quant mode {} is not BBQ ({})",
194 header.quant_mode,
195 QuantMode::Bbq as u16
196 )));
197 }
198
199 Ok((crate::distance::simd::runtime().l2_bbq)(
203 &payload[4..],
204 uqv_ref.packed_bits(),
205 header.residual_norm,
206 self.dim,
207 ))
208 }
209
210 fn name(&self) -> CodecName {
211 CodecName::Bbq
212 }
213
214 fn to_bytes(&self) -> Result<Vec<u8>, RerankError> {
215 let codec = self.codec.as_ref().ok_or_else(|| {
216 RerankError::NotTrained("bbq sidecar serialize: codec not trained".to_string())
217 })?;
218 codec
219 .to_bytes()
220 .map_err(|e| RerankError::BadInput(format!("bbq to_bytes: {e}")))
221 }
222
223 fn train(&mut self, samples: &[&[f32]]) -> Result<(), RerankError> {
232 if samples.is_empty() {
233 return Err(RerankError::BadInput(
234 "bbq train: empty sample set".to_string(),
235 ));
236 }
237 for s in samples {
238 if s.len() != self.dim {
239 return Err(RerankError::BadInput(format!(
240 "bbq train: sample has len {} but codec dim is {}",
241 s.len(),
242 self.dim
243 )));
244 }
245 }
246 let codec = BbqCodec::calibrate(samples, self.dim, self.oversample);
247 self.codec = Some(codec);
248 Ok(())
249 }
250}
251
252#[cfg(test)]
255mod tests {
256 use super::*;
257
258 const DIM: usize = 16;
259 const N: usize = 64;
260
261 fn det_vec(i: usize, dim: usize) -> Vec<f32> {
262 (0..dim)
263 .map(|j| ((i * 31 + j) % 100) as f32 / 100.0)
264 .collect()
265 }
266
267 fn trained() -> BbqRerank {
268 let vecs: Vec<Vec<f32>> = (0..N).map(|i| det_vec(i, DIM)).collect();
269 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
270 let mut codec = BbqRerank::new(DIM, DEFAULT_OVERSAMPLE);
271 codec.train(&refs).expect("train must succeed");
272 codec
273 }
274
275 #[test]
276 fn train_then_encode_roundtrip() {
277 let codec = trained();
278 let v = det_vec(0, DIM);
279 let enc = codec.encode(&v).expect("encode");
280 let prep = codec.prepare_query(&v).expect("prepare_query");
281 let dist = codec.distance_prepared(&prep, &enc).expect("distance");
282 assert!(dist.is_finite(), "distance must be finite, got {dist}");
283 assert!(dist >= 0.0, "distance must be non-negative, got {dist}");
284 }
285
286 #[test]
291 fn distance_prepared_matches_the_unfused_l2() {
292 let codec = trained();
293 let v = det_vec(7, DIM);
294 let enc = codec.encode(&v).expect("encode");
295 let prep = codec.prepare_query(&v).expect("prepare_query");
296 let got = codec.distance_prepared(&prep, &enc).expect("distance");
297
298 let PreparedQuery::Bytes(payload) = &prep else {
299 panic!("prepare_query must yield PreparedQuery::Bytes");
300 };
301
302 let residual_norm = f32::from_le_bytes([enc[8], enc[9], enc[10], enc[11]]);
307 let packed = &enc[32..32 + DIM.div_ceil(8)];
308 let centered: Vec<f64> = payload[4..]
309 .as_chunks::<4>()
310 .0
311 .iter()
312 .map(|b| f32::from_le_bytes(*b) as f64)
313 .collect();
314 assert_eq!(centered.len(), DIM, "centred query must carry `dim` lanes");
315
316 let scale = residual_norm as f64 / (DIM as f64).sqrt();
317 let expected = centered
318 .iter()
319 .enumerate()
320 .map(|(i, q)| {
321 let bit = (packed[i / 8] >> (7 - (i % 8))) & 1;
322 let recon = if bit != 0 { scale } else { -scale };
323 (q - recon).powi(2)
324 })
325 .sum::<f64>()
326 .sqrt();
327
328 assert!(
330 expected > 1e-3,
331 "reference distance collapsed to {expected}"
332 );
333
334 let rel = 1e-4f64;
335 let abs = 1e-6f64;
336 assert!(
337 ((got as f64) - expected).abs() <= abs.max(rel * expected.abs()),
338 "distance_prepared = {got}, unfused L2 = {expected}"
339 );
340 }
341
342 #[test]
343 fn encode_before_train_returns_not_trained() {
344 let codec = BbqRerank::new(DIM, DEFAULT_OVERSAMPLE);
345 let v = det_vec(0, DIM);
346 let err = codec.encode(&v).unwrap_err();
347 let msg = format!("{err}");
348 assert!(
349 msg.contains("not trained") || msg.contains("trained"),
350 "expected 'trained' in error, got: {msg}"
351 );
352 }
353
354 #[test]
355 fn train_with_empty_samples_fails() {
356 let mut codec = BbqRerank::new(DIM, DEFAULT_OVERSAMPLE);
357 let err = codec.train(&[]).unwrap_err();
358 let msg = format!("{err}");
359 assert!(
360 msg.contains("bad input") || msg.contains("empty"),
361 "expected bad input error, got: {msg}"
362 );
363 }
364
365 #[test]
366 fn train_with_dim_mismatch_fails() {
367 let vecs: Vec<Vec<f32>> = (0..N).map(|i| det_vec(i, DIM)).collect();
368 let mut refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
369 let bad = det_vec(0, DIM + 4);
370 refs.push(bad.as_slice());
371 let mut codec = BbqRerank::new(DIM, DEFAULT_OVERSAMPLE);
372 let err = codec.train(&refs).unwrap_err();
373 let msg = format!("{err}");
374 assert!(
375 msg.contains("bad input") || msg.contains("dim"),
376 "expected bad input error, got: {msg}"
377 );
378 }
379
380 #[test]
384 fn candidate_encoded_for_another_dim_is_rejected() {
385 let codec = trained();
386 let prep = codec
387 .prepare_query(&det_vec(0, DIM))
388 .expect("prepare_query");
389
390 let other_dim = DIM * 2;
391 let vecs: Vec<Vec<f32>> = (0..N).map(|i| det_vec(i, other_dim)).collect();
392 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
393 let mut other = BbqRerank::new(other_dim, DEFAULT_OVERSAMPLE);
394 other.train(&refs).expect("train must succeed");
395 let enc = other.encode(&det_vec(0, other_dim)).expect("encode");
396
397 let err = codec.distance_prepared(&prep, &enc).unwrap_err();
398 let msg = format!("{err}");
399 assert!(
400 msg.contains("candidate dim"),
401 "expected a dimension mismatch, got: {msg}"
402 );
403 }
404
405 #[test]
406 fn candidate_with_another_quant_mode_is_rejected() {
407 let codec = trained();
408 let v = det_vec(0, DIM);
409 let prep = codec.prepare_query(&v).expect("prepare_query");
410 let mut enc = codec.encode(&v).expect("encode");
411
412 enc[0..2].copy_from_slice(&(QuantMode::Sq8 as u16).to_le_bytes());
414
415 let err = codec.distance_prepared(&prep, &enc).unwrap_err();
416 let msg = format!("{err}");
417 assert!(
418 msg.contains("quant mode"),
419 "expected a quant-mode mismatch, got: {msg}"
420 );
421 }
422
423 #[test]
424 fn prepare_query_wrong_dim_fails() {
425 let codec = trained();
426 let bad = det_vec(0, DIM + 2);
427 match codec.prepare_query(&bad) {
428 Err(e) => {
429 let msg = format!("{e}");
430 assert!(
431 msg.contains("bad input") || msg.contains("dim"),
432 "expected bad input error, got: {msg}"
433 );
434 }
435 Ok(_) => panic!("expected an error for wrong dim"),
436 }
437 }
438
439 #[test]
440 fn distance_prepared_wrong_variant_fails() {
441 let codec = trained();
442 let v = det_vec(0, DIM);
443 let enc = codec.encode(&v).expect("encode");
444 let bad_prepared = PreparedQuery::Raw(vec![0.0f32; DIM]);
445 let err = codec.distance_prepared(&bad_prepared, &enc).unwrap_err();
446 let msg = format!("{err}");
447 assert!(
448 msg.contains("Bytes") || msg.contains("not Bytes"),
449 "error message should mention Bytes variant, got: {msg}"
450 );
451 }
452
453 #[test]
454 fn name_is_expected() {
455 let codec = BbqRerank::new(DIM, DEFAULT_OVERSAMPLE);
456 assert_eq!(codec.name(), CodecName::Bbq);
457 }
458}