1pub const TQ_CODEC_VERSION: u32 = 2;
20pub const TQ_BITS: u32 = 4;
22pub const TQ_BLOCK_LANES: usize = 16;
24pub const TQ_MIN_PADDED_DIM: usize = 8;
27
28const TQ_STAGE1_LEVELS: usize = 8;
29const TQ_STAGE1_SEED: u64 = 0x7154_5354_4147_4531; const TQ_QJL_SEED: u64 = 0x7154_514a_4c53_4b31; const TQ_LLOYD_GRID: usize = 8192;
32const TQ_LLOYD_MAX_ITERATIONS: usize = 64;
33const TQ_LLOYD_TOLERANCE: f64 = 1e-9;
34#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
37const TQ_ACCUMULATE_CHUNK_DIMS: usize = 128;
38
39#[inline]
45pub fn tq_padded_dim(dim: usize) -> usize {
46 dim.next_multiple_of(2).max(TQ_MIN_PADDED_DIM)
47}
48
49#[inline]
51pub fn tq_expected_fingerprint(dim: usize) -> u64 {
52 tq_fingerprint(dim, tq_padded_dim(dim))
53}
54
55pub fn tq_shared_codec(dim: usize) -> std::sync::Arc<TqCodec> {
59 static CODECS: std::sync::OnceLock<
60 std::sync::Mutex<rustc_hash::FxHashMap<usize, std::sync::Arc<TqCodec>>>,
61 > = std::sync::OnceLock::new();
62 let cache = CODECS.get_or_init(Default::default);
63 let mut guard = cache
64 .lock()
65 .unwrap_or_else(|poisoned| poisoned.into_inner());
66 std::sync::Arc::clone(
67 guard
68 .entry(dim)
69 .or_insert_with(|| std::sync::Arc::new(TqCodec::new(dim))),
70 )
71}
72
73#[inline]
75pub const fn tq_block_bytes(code_size: usize) -> usize {
76 TQ_BLOCK_LANES * (size_of::<f32>() + code_size)
77}
78
79#[inline]
81pub const fn tq_codes_column_len(count: usize, code_size: usize) -> usize {
82 count.div_ceil(TQ_BLOCK_LANES) * tq_block_bytes(code_size)
83}
84
85#[inline]
87pub fn tq_codes_column_len_checked(count: usize, code_size: usize) -> Option<usize> {
88 count
89 .div_ceil(TQ_BLOCK_LANES)
90 .checked_mul(tq_block_bytes(code_size))
91}
92
93#[inline]
96pub const fn tq_ivf_block_bytes(code_size: usize) -> usize {
97 TQ_BLOCK_LANES * (2 * size_of::<f32>() + code_size)
98}
99
100#[inline]
102pub fn tq_ivf_codes_column_len_checked(count: usize, code_size: usize) -> Option<usize> {
103 count
104 .div_ceil(TQ_BLOCK_LANES)
105 .checked_mul(tq_ivf_block_bytes(code_size))
106}
107
108#[inline]
109fn splitmix64(state: &mut u64) -> u64 {
110 *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
111 let mut z = *state;
112 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
113 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
114 z ^ (z >> 31)
115}
116
117const TQ_ROTATION_ROUNDS: usize = 3;
122
123#[derive(Debug, Clone)]
130pub struct TqRotation {
131 input_dim: usize,
132 padded_dim: usize,
133 fwht_len: usize,
135 signs: Vec<f32>,
137 perms: Vec<u32>,
139}
140
141impl TqRotation {
142 pub fn new(input_dim: usize, seed: u64) -> Self {
143 let padded_dim = tq_padded_dim(input_dim);
144 let fwht_len = if padded_dim.is_power_of_two() {
145 padded_dim
146 } else {
147 padded_dim.next_power_of_two() / 2
148 };
149 let mut state = seed;
150 let mut signs = Vec::with_capacity(TQ_ROTATION_ROUNDS * padded_dim);
151 let mut perms = Vec::with_capacity(TQ_ROTATION_ROUNDS * padded_dim);
152 for _ in 0..TQ_ROTATION_ROUNDS {
153 signs.extend((0..padded_dim).map(|_| {
154 if splitmix64(&mut state) & 1 == 1 {
155 1.0f32
156 } else {
157 -1.0f32
158 }
159 }));
160 let round_base = perms.len();
161 perms.extend(0..padded_dim as u32);
162 for i in (1..padded_dim).rev() {
163 let j = (splitmix64(&mut state) % (i as u64 + 1)) as usize;
164 perms.swap(round_base + i, round_base + j);
165 }
166 }
167 Self {
168 input_dim,
169 padded_dim,
170 fwht_len,
171 signs,
172 perms,
173 }
174 }
175
176 #[inline]
177 pub fn padded_dim(&self) -> usize {
178 self.padded_dim
179 }
180
181 pub fn apply(&self, input: &[f32], scratch: &mut Vec<f32>, output: &mut [f32]) {
185 debug_assert!(input.len() == self.input_dim || input.len() == self.padded_dim);
186 debug_assert_eq!(output.len(), self.padded_dim);
187 let padded_dim = self.padded_dim;
188 scratch.clear();
189 scratch.resize(padded_dim, 0.0);
190 let signs = &self.signs[..padded_dim];
193 for (slot, (sign, index)) in scratch.iter_mut().zip(signs.iter().zip(0..)) {
194 *slot = input.get(index).copied().unwrap_or(0.0) * sign;
195 }
196 fwht_normalized(&mut scratch[..self.fwht_len]);
197 let perm = &self.perms[..padded_dim];
198 for (slot, &source) in output.iter_mut().zip(perm) {
199 *slot = scratch[source as usize];
200 }
201 for round in 1..TQ_ROTATION_ROUNDS {
202 let signs = &self.signs[round * padded_dim..(round + 1) * padded_dim];
203 for (slot, (&value, sign)) in scratch.iter_mut().zip(output.iter().zip(signs)) {
204 *slot = value * sign;
205 }
206 fwht_normalized(&mut scratch[..self.fwht_len]);
207 let perm = &self.perms[round * padded_dim..(round + 1) * padded_dim];
208 for (slot, &source) in output.iter_mut().zip(perm) {
209 *slot = scratch[source as usize];
210 }
211 }
212 }
213}
214
215fn fwht_normalized(values: &mut [f32]) {
217 let len = values.len();
218 debug_assert!(len.is_power_of_two());
219 let mut step = 1;
220 while step < len {
221 let mut base = 0;
222 while base < len {
223 let (left_half, right_half) = values[base..base + step * 2].split_at_mut(step);
224 for (left, right) in left_half.iter_mut().zip(right_half.iter_mut()) {
225 let sum = *left + *right;
226 let difference = *left - *right;
227 *left = sum;
228 *right = difference;
229 }
230 base += step * 2;
231 }
232 step *= 2;
233 }
234 let scale = 1.0 / (len as f32).sqrt();
235 for value in values.iter_mut() {
236 *value *= scale;
237 }
238}
239
240#[derive(Debug, Clone)]
244pub struct TqCodebook {
245 levels: [f32; TQ_STAGE1_LEVELS],
246 boundaries: [f32; TQ_STAGE1_LEVELS - 1],
248}
249
250impl TqCodebook {
251 pub fn analytic(padded_dim: usize) -> Self {
252 assert!(
253 padded_dim >= TQ_MIN_PADDED_DIM,
254 "TQ codebook requires padded_dim >= {TQ_MIN_PADDED_DIM}, got {padded_dim}"
255 );
256 let exponent = (padded_dim as f64 - 3.0) / 2.0;
257 let cell = 2.0 / TQ_LLOYD_GRID as f64;
258 let mut weights = vec![0.0f64; TQ_LLOYD_GRID];
262 let mut positions = vec![0.0f64; TQ_LLOYD_GRID];
263 for index in 0..TQ_LLOYD_GRID {
264 let t = -1.0 + (index as f64 + 0.5) * cell;
265 positions[index] = t;
266 let log_density = exponent * (1.0 - t * t).max(f64::MIN_POSITIVE).ln();
267 weights[index] = log_density.exp();
268 }
269
270 let total_mass: f64 = weights.iter().sum();
272 let mut levels = [0.0f64; TQ_STAGE1_LEVELS];
273 let mut boundaries = [0.0f64; TQ_STAGE1_LEVELS - 1];
274 let mut accumulated = 0.0f64;
275 let mut next_boundary = 0usize;
276 for index in 0..TQ_LLOYD_GRID {
277 accumulated += weights[index];
278 while next_boundary < TQ_STAGE1_LEVELS - 1
279 && accumulated
280 >= total_mass * (next_boundary as f64 + 1.0) / TQ_STAGE1_LEVELS as f64
281 {
282 boundaries[next_boundary] = positions[index];
283 next_boundary += 1;
284 }
285 }
286
287 for _ in 0..TQ_LLOYD_MAX_ITERATIONS {
290 let mut mass = [0.0f64; TQ_STAGE1_LEVELS];
291 let mut moment = [0.0f64; TQ_STAGE1_LEVELS];
292 let mut bucket = 0usize;
293 for index in 0..TQ_LLOYD_GRID {
294 let t = positions[index];
295 while bucket < TQ_STAGE1_LEVELS - 1 && t > boundaries[bucket] {
296 bucket += 1;
297 }
298 mass[bucket] += weights[index];
299 moment[bucket] += weights[index] * t;
300 }
301 let mut shift = 0.0f64;
302 for level in 0..TQ_STAGE1_LEVELS {
303 if mass[level] > 0.0 {
304 let updated = moment[level] / mass[level];
305 shift = shift.max((updated - levels[level]).abs());
306 levels[level] = updated;
307 }
308 }
309 for boundary in 0..TQ_STAGE1_LEVELS - 1 {
310 boundaries[boundary] = 0.5 * (levels[boundary] + levels[boundary + 1]);
311 }
312 if shift < TQ_LLOYD_TOLERANCE {
313 break;
314 }
315 }
316
317 for index in 0..TQ_STAGE1_LEVELS / 2 {
321 let magnitude = 0.5 * (levels[TQ_STAGE1_LEVELS - 1 - index] - levels[index]);
322 levels[index] = -magnitude;
323 levels[TQ_STAGE1_LEVELS - 1 - index] = magnitude;
324 }
325 for boundary in 0..TQ_STAGE1_LEVELS - 1 {
326 boundaries[boundary] = 0.5 * (levels[boundary] + levels[boundary + 1]);
327 }
328
329 Self {
330 levels: levels.map(|level| level as f32),
331 boundaries: boundaries.map(|boundary| boundary as f32),
332 }
333 }
334
335 #[inline]
337 pub fn encode_coordinate(&self, value: f32) -> u8 {
338 let mut code = 0u8;
339 for &boundary in &self.boundaries {
340 code += u8::from(value > boundary);
341 }
342 code
343 }
344}
345
346#[derive(Debug, Clone)]
349pub struct TqCodec {
350 dim: usize,
351 padded_dim: usize,
352 stage1_rotation: TqRotation,
353 qjl_rotation: TqRotation,
354 codebook: TqCodebook,
355 qjl_scale: f32,
357 fingerprint: u64,
358}
359
360impl TqCodec {
361 pub fn new(dim: usize) -> Self {
362 assert!(dim > 0, "TQ codec requires a non-zero dimension");
363 let stage1_rotation = TqRotation::new(dim, TQ_STAGE1_SEED);
364 let padded_dim = stage1_rotation.padded_dim();
365 let qjl_rotation = TqRotation::new(padded_dim, TQ_QJL_SEED);
366 debug_assert_eq!(qjl_rotation.padded_dim(), padded_dim);
367 let codebook = TqCodebook::analytic(padded_dim);
368 let qjl_scale = (std::f64::consts::PI / 2.0).sqrt() as f32 / (padded_dim as f32).sqrt();
369 let fingerprint = tq_fingerprint(dim, padded_dim);
370 Self {
371 dim,
372 padded_dim,
373 stage1_rotation,
374 qjl_rotation,
375 codebook,
376 qjl_scale,
377 fingerprint,
378 }
379 }
380
381 #[inline]
382 pub fn dim(&self) -> usize {
383 self.dim
384 }
385
386 #[inline]
387 pub fn padded_dim(&self) -> usize {
388 self.padded_dim
389 }
390
391 #[inline]
393 pub fn code_size(&self) -> usize {
394 self.padded_dim / 2
395 }
396
397 #[inline]
399 pub fn fingerprint(&self) -> u64 {
400 self.fingerprint
401 }
402
403 pub fn estimated_memory_bytes(&self) -> usize {
406 2 * self.padded_dim * (size_of::<f32>() + size_of::<u32>()) + size_of::<TqCodebook>()
407 }
408
409 pub fn encode_into(
413 &self,
414 vector: &[f32],
415 nibbles: &mut [u8],
416 scratch: &mut TqEncodeScratch,
417 ) -> f32 {
418 self.encode_residual_into(vector, nibbles, scratch).1
419 }
420
421 pub fn encode_residual_into(
427 &self,
428 vector: &[f32],
429 nibbles: &mut [u8],
430 scratch: &mut TqEncodeScratch,
431 ) -> (f32, f32) {
432 assert_eq!(vector.len(), self.dim, "TQ encode dimension mismatch");
433 assert_eq!(nibbles.len(), self.padded_dim, "TQ nibble buffer mismatch");
434 let norm = crate::structures::simd::dot_product_f32(vector, vector, vector.len()).sqrt();
435 if !norm.is_finite() || norm <= 0.0 {
436 nibbles.fill(0);
437 return (0.0, 0.0);
438 }
439 scratch.normalized.clear();
440 scratch
441 .normalized
442 .extend(vector.iter().map(|value| value / norm));
443
444 scratch.rotated.resize(self.padded_dim, 0.0);
445 let (normalized, rotated, fwht) =
446 (&scratch.normalized, &mut scratch.rotated, &mut scratch.fwht);
447 self.stage1_rotation.apply(normalized, fwht, rotated);
448
449 scratch.residual.resize(self.padded_dim, 0.0);
451 let mut residual_norm_sq = 0.0f32;
452 for ((&value, nibble), residual_slot) in scratch
453 .rotated
454 .iter()
455 .zip(nibbles.iter_mut())
456 .zip(scratch.residual.iter_mut())
457 {
458 let code = self.codebook.encode_coordinate(value);
459 *nibble = code << 1;
460 let residual = value - self.codebook.levels[code as usize];
461 *residual_slot = residual;
462 residual_norm_sq += residual * residual;
463 }
464
465 scratch.rotated_residual.resize(self.padded_dim, 0.0);
467 let (residual, rotated_residual, fwht) = (
468 &scratch.residual,
469 &mut scratch.rotated_residual,
470 &mut scratch.fwht,
471 );
472 self.qjl_rotation.apply(residual, fwht, rotated_residual);
473 for (nibble, &rotated) in nibbles.iter_mut().zip(scratch.rotated_residual.iter()) {
474 *nibble |= u8::from(rotated >= 0.0);
475 }
476 (norm, residual_norm_sq.sqrt())
477 }
478}
479
480#[derive(Debug, Default)]
482pub struct TqEncodeScratch {
483 normalized: Vec<f32>,
484 rotated: Vec<f32>,
485 residual: Vec<f32>,
486 rotated_residual: Vec<f32>,
487 fwht: Vec<f32>,
488}
489
490fn tq_fingerprint(dim: usize, padded_dim: usize) -> u64 {
491 let mut hash = 0xcbf2_9ce4_8422_2325u64; let mut mix = |bytes: &[u8]| {
493 for &byte in bytes {
494 hash ^= u64::from(byte);
495 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
496 }
497 };
498 mix(b"hermes-tq");
499 mix(&TQ_CODEC_VERSION.to_le_bytes());
500 mix(&TQ_BITS.to_le_bytes());
501 mix(&(dim as u64).to_le_bytes());
502 mix(&(padded_dim as u64).to_le_bytes());
503 mix(&TQ_STAGE1_SEED.to_le_bytes());
504 mix(&TQ_QJL_SEED.to_le_bytes());
505 if hash == 0 { 1 } else { hash }
506}
507
508pub struct TqQueryPlan {
516 padded_dim: usize,
517 fingerprint: u64,
518 query_bits: Box<[u32]>,
525 base_lut_i8: Vec<i8>,
526 qjl_lut_i8: Vec<i8>,
527 base_dequant: f32,
528 qjl_dequant: f32,
529 #[cfg(test)]
531 reference_luts: (Vec<f32>, Vec<f32>),
532}
533
534impl std::fmt::Debug for TqQueryPlan {
535 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 formatter
537 .debug_struct("TqQueryPlan")
538 .field("padded_dim", &self.padded_dim)
539 .field("fingerprint", &self.fingerprint)
540 .field("query_dim", &self.query_bits.len())
541 .finish()
542 }
543}
544
545impl TqQueryPlan {
546 pub fn build(codec: &TqCodec, query: &[f32]) -> Self {
547 assert_eq!(query.len(), codec.dim, "TQ query dimension mismatch");
548 let padded_dim = codec.padded_dim;
549 let norm = crate::structures::simd::dot_product_f32(query, query, query.len()).sqrt();
550 let inverse_norm = if norm.is_finite() && norm > 0.0 {
551 1.0 / norm
552 } else {
553 0.0
554 };
555 let normalized: Vec<f32> = query.iter().map(|value| value * inverse_norm).collect();
556 let mut fwht = Vec::with_capacity(padded_dim);
557 let mut rotated = vec![0.0f32; padded_dim];
558 codec
559 .stage1_rotation
560 .apply(&normalized, &mut fwht, &mut rotated);
561 let mut qjl_rotated = vec![0.0f32; padded_dim];
562 codec
563 .qjl_rotation
564 .apply(&rotated, &mut fwht, &mut qjl_rotated);
565
566 let mut base_lut = vec![0.0f32; padded_dim * 16];
567 let mut qjl_lut = vec![0.0f32; padded_dim * 16];
568 for dim in 0..padded_dim {
569 for nibble in 0..16 {
570 let level = codec.codebook.levels[nibble >> 1];
571 let sign = if nibble & 1 == 1 { 1.0 } else { -1.0 };
572 base_lut[dim * 16 + nibble] = rotated[dim] * level;
573 qjl_lut[dim * 16 + nibble] = sign * codec.qjl_scale * qjl_rotated[dim];
574 }
575 }
576 let (base_lut_i8, base_dequant) = quantize_lut(&base_lut);
577 let (qjl_lut_i8, qjl_dequant) = quantize_lut(&qjl_lut);
578 Self {
579 padded_dim,
580 fingerprint: codec.fingerprint,
581 query_bits: query
582 .iter()
583 .map(|value| value.to_bits())
584 .collect::<Vec<_>>()
585 .into_boxed_slice(),
586 base_lut_i8,
587 qjl_lut_i8,
588 base_dequant,
589 qjl_dequant,
590 #[cfg(test)]
591 reference_luts: (base_lut, qjl_lut),
592 }
593 }
594
595 #[inline]
596 pub fn padded_dim(&self) -> usize {
597 self.padded_dim
598 }
599
600 #[inline]
601 pub fn fingerprint(&self) -> u64 {
602 self.fingerprint
603 }
604
605 #[inline]
607 pub(crate) fn matches_query(&self, query: &[f32]) -> bool {
608 self.query_bits.len() == query.len()
609 && self
610 .query_bits
611 .iter()
612 .zip(query)
613 .all(|(&bits, value)| bits == value.to_bits())
614 }
615
616 #[cfg(test)]
619 pub(crate) fn estimate_row(&self, nibbles: &[u8], gamma: f32) -> f32 {
620 debug_assert_eq!(nibbles.len(), self.padded_dim);
621 let (base_lut, qjl_lut) = &self.reference_luts;
622 let mut base = 0.0f32;
623 let mut qjl = 0.0f32;
624 for (dim, &nibble) in nibbles.iter().enumerate() {
625 base += base_lut[dim * 16 + nibble as usize];
626 qjl += qjl_lut[dim * 16 + nibble as usize];
627 }
628 base + gamma * qjl
629 }
630}
631
632fn quantize_lut(values: &[f32]) -> (Vec<i8>, f32) {
633 let max_abs = values
634 .iter()
635 .fold(0.0f32, |acc, &value| acc.max(value.abs()));
636 if !max_abs.is_finite() || max_abs <= 0.0 {
637 return (vec![0i8; values.len()], 0.0);
638 }
639 let quantize_scale = 127.0 / max_abs;
640 let quantized = values
641 .iter()
642 .map(|&value| (value * quantize_scale).round().clamp(-127.0, 127.0) as i8)
643 .collect();
644 (quantized, max_abs / 127.0)
645}
646
647pub fn tq_score_block(plan: &TqQueryPlan, block: &[u8], scores: &mut [f32; TQ_BLOCK_LANES]) {
651 debug_assert_eq!(block.len(), tq_block_bytes(plan.padded_dim / 2));
652 let (gamma_bytes, nibble_bytes) = block.split_at(TQ_BLOCK_LANES * size_of::<f32>());
653 let mut base = [0i32; TQ_BLOCK_LANES];
654 let mut qjl = [0i32; TQ_BLOCK_LANES];
655 lut16::accumulate_block(
656 &plan.base_lut_i8,
657 &plan.qjl_lut_i8,
658 nibble_bytes,
659 plan.padded_dim,
660 &mut base,
661 &mut qjl,
662 );
663 for lane in 0..TQ_BLOCK_LANES {
664 let gamma = f32::from_le_bytes(
665 gamma_bytes[lane * 4..lane * 4 + 4]
666 .try_into()
667 .expect("gamma slice is 4 bytes"),
668 );
669 scores[lane] =
670 base[lane] as f32 * plan.base_dequant + gamma * qjl[lane] as f32 * plan.qjl_dequant;
671 }
672}
673
674pub fn tq_pack_block(rows: &[&[u8]], gammas: &[f32], padded_dim: usize, output: &mut Vec<u8>) {
677 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
678 for lane in 0..TQ_BLOCK_LANES {
679 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
680 output.extend_from_slice(&gamma.to_le_bytes());
681 }
682 pack_nibble_rows(rows, padded_dim, output);
683}
684
685pub fn tq_pack_ivf_block(
687 rows: &[&[u8]],
688 scales: &[f32],
689 gammas: &[f32],
690 padded_dim: usize,
691 output: &mut Vec<u8>,
692) {
693 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
694 assert_eq!(scales.len(), gammas.len());
695 for lane in 0..TQ_BLOCK_LANES {
696 let scale = scales.get(lane).copied().unwrap_or(0.0);
697 output.extend_from_slice(&scale.to_le_bytes());
698 }
699 for lane in 0..TQ_BLOCK_LANES {
700 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
701 output.extend_from_slice(&gamma.to_le_bytes());
702 }
703 pack_nibble_rows(rows, padded_dim, output);
704}
705
706fn pack_nibble_rows(rows: &[&[u8]], padded_dim: usize, output: &mut Vec<u8>) {
707 for dim in 0..padded_dim {
708 for byte_index in 0..TQ_BLOCK_LANES / 2 {
709 let low = rows.get(byte_index).map_or(0, |row| row[dim] & 0x0F);
710 let high = rows
711 .get(byte_index + TQ_BLOCK_LANES / 2)
712 .map_or(0, |row| row[dim] & 0x0F);
713 output.push(low | (high << 4));
714 }
715 }
716}
717
718pub fn tq_score_ivf_block(
722 plan: &TqQueryPlan,
723 block: &[u8],
724 cluster_dot: f32,
725 scores: &mut [f32; TQ_BLOCK_LANES],
726) {
727 debug_assert_eq!(block.len(), tq_ivf_block_bytes(plan.padded_dim() / 2));
728 let lane_f32 = TQ_BLOCK_LANES * size_of::<f32>();
729 let (scale_bytes, rest) = block.split_at(lane_f32);
730 let (gamma_bytes, nibble_bytes) = rest.split_at(lane_f32);
731 let mut base = [0i32; TQ_BLOCK_LANES];
732 let mut qjl = [0i32; TQ_BLOCK_LANES];
733 lut16::accumulate_block(
734 &plan.base_lut_i8,
735 &plan.qjl_lut_i8,
736 nibble_bytes,
737 plan.padded_dim,
738 &mut base,
739 &mut qjl,
740 );
741 for lane in 0..TQ_BLOCK_LANES {
742 let scale = f32::from_le_bytes(
743 scale_bytes[lane * 4..lane * 4 + 4]
744 .try_into()
745 .expect("scale slice is 4 bytes"),
746 );
747 let gamma = f32::from_le_bytes(
748 gamma_bytes[lane * 4..lane * 4 + 4]
749 .try_into()
750 .expect("gamma slice is 4 bytes"),
751 );
752 scores[lane] = cluster_dot
753 + scale
754 * (base[lane] as f32 * plan.base_dequant
755 + gamma * qjl[lane] as f32 * plan.qjl_dequant);
756 }
757}
758
759mod lut16 {
760 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
761 use super::TQ_ACCUMULATE_CHUNK_DIMS;
762 use super::TQ_BLOCK_LANES;
763
764 pub(super) fn accumulate_block(
768 base_lut: &[i8],
769 qjl_lut: &[i8],
770 nibble_bytes: &[u8],
771 padded_dim: usize,
772 base: &mut [i32; TQ_BLOCK_LANES],
773 qjl: &mut [i32; TQ_BLOCK_LANES],
774 ) {
775 #[cfg(target_arch = "aarch64")]
776 {
777 unsafe { accumulate_block_neon(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl) }
779 return;
780 }
781 #[cfg(target_arch = "x86_64")]
782 {
783 if std::arch::is_x86_feature_detected!("avx2") {
784 unsafe {
785 accumulate_block_avx2(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl)
786 }
787 return;
788 }
789 if std::arch::is_x86_feature_detected!("ssse3") {
790 unsafe {
791 accumulate_block_ssse3(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl)
792 }
793 return;
794 }
795 }
796 #[allow(unreachable_code)]
797 accumulate_block_scalar(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl);
798 }
799
800 pub(super) fn accumulate_block_scalar(
803 base_lut: &[i8],
804 qjl_lut: &[i8],
805 nibble_bytes: &[u8],
806 padded_dim: usize,
807 base: &mut [i32; TQ_BLOCK_LANES],
808 qjl: &mut [i32; TQ_BLOCK_LANES],
809 ) {
810 for dim in 0..padded_dim {
811 let row = &nibble_bytes[dim * 8..dim * 8 + 8];
812 let base_table = &base_lut[dim * 16..dim * 16 + 16];
813 let qjl_table = &qjl_lut[dim * 16..dim * 16 + 16];
814 for (lane, &byte) in row.iter().enumerate() {
815 let low = (byte & 0x0F) as usize;
816 let high = (byte >> 4) as usize;
817 base[lane] += i32::from(base_table[low]);
818 base[lane + 8] += i32::from(base_table[high]);
819 qjl[lane] += i32::from(qjl_table[low]);
820 qjl[lane + 8] += i32::from(qjl_table[high]);
821 }
822 }
823 }
824
825 #[cfg(target_arch = "aarch64")]
826 #[target_feature(enable = "neon")]
827 unsafe fn accumulate_block_neon(
828 base_lut: &[i8],
829 qjl_lut: &[i8],
830 nibble_bytes: &[u8],
831 padded_dim: usize,
832 base: &mut [i32; TQ_BLOCK_LANES],
833 qjl: &mut [i32; TQ_BLOCK_LANES],
834 ) {
835 use std::arch::aarch64::*;
836 unsafe {
837 let mask = vdup_n_u8(0x0F);
838 let mut base_lo_i32 = [vdupq_n_s32(0); 4];
839 let mut qjl_lo_i32 = [vdupq_n_s32(0); 4];
840 let mut dim = 0;
841 while dim < padded_dim {
842 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
843 let mut base_acc = [vdupq_n_s16(0); 2];
844 let mut qjl_acc = [vdupq_n_s16(0); 2];
845 while dim < chunk_end {
846 let row = vld1_u8(nibble_bytes.as_ptr().add(dim * 8));
847 let low = vand_u8(row, mask);
848 let high = vshr_n_u8::<4>(row);
849 let lanes = vcombine_u8(low, high);
850 let base_table = vld1q_s8(base_lut.as_ptr().add(dim * 16));
851 let qjl_table = vld1q_s8(qjl_lut.as_ptr().add(dim * 16));
852 let base_values =
853 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(base_table), lanes));
854 let qjl_values =
855 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(qjl_table), lanes));
856 base_acc[0] = vaddw_s8(base_acc[0], vget_low_s8(base_values));
857 base_acc[1] = vaddw_s8(base_acc[1], vget_high_s8(base_values));
858 qjl_acc[0] = vaddw_s8(qjl_acc[0], vget_low_s8(qjl_values));
859 qjl_acc[1] = vaddw_s8(qjl_acc[1], vget_high_s8(qjl_values));
860 dim += 1;
861 }
862 for half in 0..2 {
863 base_lo_i32[half * 2] =
864 vaddw_s16(base_lo_i32[half * 2], vget_low_s16(base_acc[half]));
865 base_lo_i32[half * 2 + 1] =
866 vaddw_s16(base_lo_i32[half * 2 + 1], vget_high_s16(base_acc[half]));
867 qjl_lo_i32[half * 2] =
868 vaddw_s16(qjl_lo_i32[half * 2], vget_low_s16(qjl_acc[half]));
869 qjl_lo_i32[half * 2 + 1] =
870 vaddw_s16(qjl_lo_i32[half * 2 + 1], vget_high_s16(qjl_acc[half]));
871 }
872 }
873 for quarter in 0..4 {
874 vst1q_s32(base.as_mut_ptr().add(quarter * 4), base_lo_i32[quarter]);
875 vst1q_s32(qjl.as_mut_ptr().add(quarter * 4), qjl_lo_i32[quarter]);
876 }
877 }
878 }
879
880 #[cfg(target_arch = "x86_64")]
884 #[target_feature(enable = "avx2")]
885 unsafe fn accumulate_block_avx2(
886 base_lut: &[i8],
887 qjl_lut: &[i8],
888 nibble_bytes: &[u8],
889 padded_dim: usize,
890 base: &mut [i32; TQ_BLOCK_LANES],
891 qjl: &mut [i32; TQ_BLOCK_LANES],
892 ) {
893 use std::arch::x86_64::*;
894 unsafe {
895 let mask = _mm_set1_epi8(0x0F);
896 let zero256 = _mm256_setzero_si256();
897 let mut base_i32 = [zero256; 2];
898 let mut qjl_i32 = [zero256; 2];
899 let mut dim = 0;
900 while dim < padded_dim {
901 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
902 let mut base_acc = zero256;
903 let mut qjl_acc = zero256;
904 while dim + 2 <= chunk_end {
905 let rows = _mm_loadu_si128(nibble_bytes.as_ptr().add(dim * 8).cast());
907 let low = _mm_and_si128(rows, mask);
908 let high = _mm_and_si128(_mm_srli_epi16(rows, 4), mask);
909 let lanes_first = _mm_unpacklo_epi64(low, high);
911 let lanes_second = _mm_unpackhi_epi64(low, high);
912 let lanes = _mm256_inserti128_si256(
913 _mm256_castsi128_si256(lanes_first),
914 lanes_second,
915 1,
916 );
917 let base_tables = _mm256_loadu_si256(base_lut.as_ptr().add(dim * 16).cast());
918 let qjl_tables = _mm256_loadu_si256(qjl_lut.as_ptr().add(dim * 16).cast());
919 let base_values = _mm256_shuffle_epi8(base_tables, lanes);
920 let qjl_values = _mm256_shuffle_epi8(qjl_tables, lanes);
921 base_acc = _mm256_add_epi16(
922 base_acc,
923 _mm256_cvtepi8_epi16(_mm256_castsi256_si128(base_values)),
924 );
925 base_acc = _mm256_add_epi16(
926 base_acc,
927 _mm256_cvtepi8_epi16(_mm256_extracti128_si256(base_values, 1)),
928 );
929 qjl_acc = _mm256_add_epi16(
930 qjl_acc,
931 _mm256_cvtepi8_epi16(_mm256_castsi256_si128(qjl_values)),
932 );
933 qjl_acc = _mm256_add_epi16(
934 qjl_acc,
935 _mm256_cvtepi8_epi16(_mm256_extracti128_si256(qjl_values, 1)),
936 );
937 dim += 2;
938 }
939 while dim < chunk_end {
941 let row = _mm_loadl_epi64(nibble_bytes.as_ptr().add(dim * 8).cast());
942 let low = _mm_and_si128(row, mask);
943 let high = _mm_and_si128(_mm_srli_epi16(row, 4), mask);
944 let lanes = _mm_unpacklo_epi64(low, high);
945 let base_table = _mm_loadu_si128(base_lut.as_ptr().add(dim * 16).cast());
946 let qjl_table = _mm_loadu_si128(qjl_lut.as_ptr().add(dim * 16).cast());
947 base_acc = _mm256_add_epi16(
948 base_acc,
949 _mm256_cvtepi8_epi16(_mm_shuffle_epi8(base_table, lanes)),
950 );
951 qjl_acc = _mm256_add_epi16(
952 qjl_acc,
953 _mm256_cvtepi8_epi16(_mm_shuffle_epi8(qjl_table, lanes)),
954 );
955 dim += 1;
956 }
957 for (accumulators, chunk) in [(&mut base_i32, base_acc), (&mut qjl_i32, qjl_acc)] {
958 accumulators[0] = _mm256_add_epi32(
959 accumulators[0],
960 _mm256_cvtepi16_epi32(_mm256_castsi256_si128(chunk)),
961 );
962 accumulators[1] = _mm256_add_epi32(
963 accumulators[1],
964 _mm256_cvtepi16_epi32(_mm256_extracti128_si256(chunk, 1)),
965 );
966 }
967 }
968 _mm256_storeu_si256(base.as_mut_ptr().cast(), base_i32[0]);
969 _mm256_storeu_si256(base.as_mut_ptr().add(8).cast(), base_i32[1]);
970 _mm256_storeu_si256(qjl.as_mut_ptr().cast(), qjl_i32[0]);
971 _mm256_storeu_si256(qjl.as_mut_ptr().add(8).cast(), qjl_i32[1]);
972 }
973 }
974
975 #[cfg(target_arch = "x86_64")]
976 #[target_feature(enable = "ssse3")]
977 unsafe fn accumulate_block_ssse3(
978 base_lut: &[i8],
979 qjl_lut: &[i8],
980 nibble_bytes: &[u8],
981 padded_dim: usize,
982 base: &mut [i32; TQ_BLOCK_LANES],
983 qjl: &mut [i32; TQ_BLOCK_LANES],
984 ) {
985 use std::arch::x86_64::*;
986 unsafe {
987 let mask = _mm_set1_epi8(0x0F);
988 let zero = _mm_setzero_si128();
989 let mut base_i32 = [zero; 4];
990 let mut qjl_i32 = [zero; 4];
991 let mut dim = 0;
992 while dim < padded_dim {
993 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
994 let mut base_acc = [zero; 2];
995 let mut qjl_acc = [zero; 2];
996 while dim < chunk_end {
997 let row = _mm_loadl_epi64(nibble_bytes.as_ptr().add(dim * 8).cast());
998 let low = _mm_and_si128(row, mask);
999 let high = _mm_and_si128(_mm_srli_epi16(row, 4), mask);
1000 let lanes = _mm_unpacklo_epi64(low, high);
1001 let base_table = _mm_loadu_si128(base_lut.as_ptr().add(dim * 16).cast());
1002 let qjl_table = _mm_loadu_si128(qjl_lut.as_ptr().add(dim * 16).cast());
1003 let base_values = _mm_shuffle_epi8(base_table, lanes);
1004 let qjl_values = _mm_shuffle_epi8(qjl_table, lanes);
1005 let base_sign = _mm_cmpgt_epi8(zero, base_values);
1007 let qjl_sign = _mm_cmpgt_epi8(zero, qjl_values);
1008 base_acc[0] =
1009 _mm_add_epi16(base_acc[0], _mm_unpacklo_epi8(base_values, base_sign));
1010 base_acc[1] =
1011 _mm_add_epi16(base_acc[1], _mm_unpackhi_epi8(base_values, base_sign));
1012 qjl_acc[0] = _mm_add_epi16(qjl_acc[0], _mm_unpacklo_epi8(qjl_values, qjl_sign));
1013 qjl_acc[1] = _mm_add_epi16(qjl_acc[1], _mm_unpackhi_epi8(qjl_values, qjl_sign));
1014 dim += 1;
1015 }
1016 for half in 0..2 {
1017 let base_sign = _mm_cmpgt_epi16(zero, base_acc[half]);
1018 let qjl_sign = _mm_cmpgt_epi16(zero, qjl_acc[half]);
1019 base_i32[half * 2] = _mm_add_epi32(
1020 base_i32[half * 2],
1021 _mm_unpacklo_epi16(base_acc[half], base_sign),
1022 );
1023 base_i32[half * 2 + 1] = _mm_add_epi32(
1024 base_i32[half * 2 + 1],
1025 _mm_unpackhi_epi16(base_acc[half], base_sign),
1026 );
1027 qjl_i32[half * 2] = _mm_add_epi32(
1028 qjl_i32[half * 2],
1029 _mm_unpacklo_epi16(qjl_acc[half], qjl_sign),
1030 );
1031 qjl_i32[half * 2 + 1] = _mm_add_epi32(
1032 qjl_i32[half * 2 + 1],
1033 _mm_unpackhi_epi16(qjl_acc[half], qjl_sign),
1034 );
1035 }
1036 }
1037 for quarter in 0..4 {
1038 _mm_storeu_si128(base.as_mut_ptr().add(quarter * 4).cast(), base_i32[quarter]);
1039 _mm_storeu_si128(qjl.as_mut_ptr().add(quarter * 4).cast(), qjl_i32[quarter]);
1040 }
1041 }
1042 }
1043}
1044
1045#[cfg(feature = "native")]
1052pub struct TqFlatBuilder {
1053 codec: std::sync::Arc<TqCodec>,
1054 pub doc_ids: Vec<u32>,
1055 pub ordinals: Vec<u16>,
1056 pub codes: Vec<u8>,
1057 pending_rows: Vec<Vec<u8>>,
1058 pending_gammas: Vec<f32>,
1059}
1060
1061#[cfg(feature = "native")]
1062impl TqFlatBuilder {
1063 pub fn new(codec: std::sync::Arc<TqCodec>) -> Self {
1064 Self {
1065 codec,
1066 doc_ids: Vec::new(),
1067 ordinals: Vec::new(),
1068 codes: Vec::new(),
1069 pending_rows: Vec::with_capacity(TQ_BLOCK_LANES),
1070 pending_gammas: Vec::with_capacity(TQ_BLOCK_LANES),
1071 }
1072 }
1073
1074 #[inline]
1075 pub fn codec(&self) -> &TqCodec {
1076 &self.codec
1077 }
1078
1079 #[inline]
1080 pub fn len(&self) -> usize {
1081 self.doc_ids.len()
1082 }
1083
1084 #[inline]
1085 pub fn is_empty(&self) -> bool {
1086 self.doc_ids.is_empty()
1087 }
1088
1089 pub fn add_batch(
1092 &mut self,
1093 labels: &[(u32, u16)],
1094 vectors: &[f32],
1095 ) -> Result<(), &'static str> {
1096 use rayon::prelude::*;
1097
1098 let dim = self.codec.dim();
1099 let vector_count = labels.len();
1100 let expected = vector_count
1101 .checked_mul(dim)
1102 .ok_or("TQ input size overflow")?;
1103 if vectors.len() != expected {
1104 return Err("TQ vector and label matrices are inconsistent");
1105 }
1106 let padded_dim = self.codec.padded_dim();
1107 let codec = std::sync::Arc::clone(&self.codec);
1108 let mut rows = vec![0u8; vector_count * padded_dim];
1112 let mut gammas = vec![0.0f32; vector_count];
1113 vectors
1114 .par_chunks_exact(dim)
1115 .zip(rows.par_chunks_exact_mut(padded_dim))
1116 .zip(gammas.par_iter_mut())
1117 .for_each_init(
1118 TqEncodeScratch::default,
1119 |scratch, ((vector, row), gamma)| {
1120 *gamma = codec.encode_into(vector, row, scratch);
1121 },
1122 );
1123
1124 let mut index = 0;
1127 while index < vector_count && !self.pending_rows.is_empty() {
1128 let (doc_id, ordinal) = labels[index];
1129 self.doc_ids.push(doc_id);
1130 self.ordinals.push(ordinal);
1131 self.pending_rows
1132 .push(rows[index * padded_dim..(index + 1) * padded_dim].to_vec());
1133 self.pending_gammas.push(gammas[index]);
1134 if self.pending_rows.len() == TQ_BLOCK_LANES {
1135 self.flush_block();
1136 }
1137 index += 1;
1138 }
1139 while vector_count - index >= TQ_BLOCK_LANES {
1140 let row_refs: Vec<&[u8]> = (0..TQ_BLOCK_LANES)
1141 .map(|lane| {
1142 let row = index + lane;
1143 &rows[row * padded_dim..(row + 1) * padded_dim]
1144 })
1145 .collect();
1146 tq_pack_block(
1147 &row_refs,
1148 &gammas[index..index + TQ_BLOCK_LANES],
1149 padded_dim,
1150 &mut self.codes,
1151 );
1152 for &(doc_id, ordinal) in &labels[index..index + TQ_BLOCK_LANES] {
1153 self.doc_ids.push(doc_id);
1154 self.ordinals.push(ordinal);
1155 }
1156 index += TQ_BLOCK_LANES;
1157 }
1158 for row in index..vector_count {
1159 let (doc_id, ordinal) = labels[row];
1160 self.doc_ids.push(doc_id);
1161 self.ordinals.push(ordinal);
1162 self.pending_rows
1163 .push(rows[row * padded_dim..(row + 1) * padded_dim].to_vec());
1164 self.pending_gammas.push(gammas[row]);
1165 }
1166 Ok(())
1167 }
1168
1169 fn flush_block(&mut self) {
1170 let rows: Vec<&[u8]> = self.pending_rows.iter().map(Vec::as_slice).collect();
1171 tq_pack_block(
1172 &rows,
1173 &self.pending_gammas,
1174 self.codec.padded_dim(),
1175 &mut self.codes,
1176 );
1177 self.pending_rows.clear();
1178 self.pending_gammas.clear();
1179 }
1180
1181 pub fn finish(&mut self) {
1183 if !self.pending_rows.is_empty() {
1184 self.flush_block();
1185 }
1186 debug_assert_eq!(
1187 self.codes.len(),
1188 tq_codes_column_len(self.doc_ids.len(), self.codec.code_size())
1189 );
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196
1197 fn seeded_unit_vector(dim: usize, seed: u64) -> Vec<f32> {
1198 let mut state = seed;
1200 let mut values: Vec<f32> = (0..dim)
1201 .map(|_| {
1202 let a = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
1203 let b = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
1204 let gaussian = (-2.0 * (1.0 - a).max(f64::MIN_POSITIVE).ln()).sqrt()
1205 * (2.0 * std::f64::consts::PI * b).cos();
1206 gaussian as f32
1207 })
1208 .collect();
1209 let norm = values.iter().map(|v| v * v).sum::<f32>().sqrt();
1210 values.iter_mut().for_each(|v| *v /= norm);
1211 values
1212 }
1213
1214 #[test]
1215 fn rotation_is_orthonormal_and_deterministic() {
1216 let rotation = TqRotation::new(100, 42);
1217 assert_eq!(rotation.padded_dim(), 100);
1218 let mut fwht_scratch = Vec::new();
1219 let mut probe = vec![0.0f32; 100];
1220 assert_eq!(TqRotation::new(99, 42).padded_dim(), 100);
1222 TqRotation::new(99, 42).apply(&vec![1.0; 99], &mut fwht_scratch, &mut probe);
1223 let input = seeded_unit_vector(100, 7);
1224 let mut fwht = Vec::new();
1225 let mut output = vec![0.0f32; 100];
1226 rotation.apply(&input, &mut fwht, &mut output);
1227 let norm: f32 = output.iter().map(|v| v * v).sum();
1228 assert!(
1229 (norm - 1.0).abs() < 1e-5,
1230 "rotation must preserve norm, got {norm}"
1231 );
1232
1233 let mut second = vec![0.0f32; 100];
1234 TqRotation::new(100, 42).apply(&input, &mut fwht, &mut second);
1235 assert_eq!(output, second, "rotation must be deterministic");
1236
1237 let other = seeded_unit_vector(100, 8);
1239 let mut other_rotated = vec![0.0f32; 100];
1240 rotation.apply(&other, &mut fwht, &mut other_rotated);
1241 let dot_before: f32 = input.iter().zip(&other).map(|(a, b)| a * b).sum();
1242 let dot_after: f32 = output.iter().zip(&other_rotated).map(|(a, b)| a * b).sum();
1243 assert!(
1244 (dot_before - dot_after).abs() < 1e-4,
1245 "rotation must preserve inner products: {dot_before} vs {dot_after}"
1246 );
1247 }
1248
1249 #[test]
1250 fn analytic_codebook_is_symmetric_and_monotonic() {
1251 for padded_dim in [8, 128, 1024] {
1252 let codebook = TqCodebook::analytic(padded_dim);
1253 let levels = &codebook.levels;
1254 for pair in levels.windows(2) {
1255 assert!(pair[0] < pair[1], "levels must be strictly increasing");
1256 }
1257 for index in 0..TQ_STAGE1_LEVELS / 2 {
1258 assert_eq!(
1259 levels[index],
1260 -levels[TQ_STAGE1_LEVELS - 1 - index],
1261 "levels must be exactly symmetric for P={padded_dim}: {levels:?}"
1262 );
1263 }
1264 assert!(levels[TQ_STAGE1_LEVELS - 1] < 1.0);
1265 let scale = 1.0 / (padded_dim as f32).sqrt();
1268 assert!(
1269 levels[TQ_STAGE1_LEVELS - 1] < 6.0 * scale,
1270 "top level {} is implausibly large for P={padded_dim}",
1271 levels[TQ_STAGE1_LEVELS - 1]
1272 );
1273 }
1274 }
1275
1276 #[test]
1277 fn encode_coordinate_matches_nearest_level() {
1278 let codebook = TqCodebook::analytic(256);
1279 for step in -1000i32..=1000 {
1280 let value = step as f32 / 1000.0;
1281 let code = codebook.encode_coordinate(value) as usize;
1282 let nearest = codebook
1283 .levels
1284 .iter()
1285 .enumerate()
1286 .min_by(|a, b| (a.1 - value).abs().total_cmp(&(b.1 - value).abs()))
1287 .unwrap()
1288 .0;
1289 assert_eq!(
1290 code, nearest,
1291 "value {value} coded {code}, nearest {nearest}"
1292 );
1293 }
1294 }
1295
1296 #[test]
1297 fn estimator_is_unbiased_and_tight() {
1298 let dim = 96;
1299 let codec = TqCodec::new(dim);
1300 let mut scratch = TqEncodeScratch::default();
1301 let mut nibbles = vec![0u8; codec.padded_dim()];
1302
1303 let pairs = 512;
1304 let mut signed_error_sum = 0.0f64;
1305 let mut squared_error_sum = 0.0f64;
1306 let mut stage1_signed_error_sum = 0.0f64;
1307 for pair in 0..pairs {
1308 let vector = seeded_unit_vector(dim, 1000 + pair);
1309 let query = seeded_unit_vector(dim, 900_000 + pair);
1310 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1311 let plan = TqQueryPlan::build(&codec, &query);
1312 let estimate = plan.estimate_row(&nibbles, gamma);
1313 let stage1_only = plan.estimate_row(&nibbles, 0.0);
1314 let truth: f32 = vector.iter().zip(&query).map(|(a, b)| a * b).sum();
1315 signed_error_sum += f64::from(estimate - truth);
1316 squared_error_sum += f64::from(estimate - truth).powi(2);
1317 stage1_signed_error_sum += f64::from(stage1_only - truth);
1318 }
1319 let mean_error = signed_error_sum / pairs as f64;
1320 let rmse = (squared_error_sum / pairs as f64).sqrt();
1321 let stage1_mean_error = stage1_signed_error_sum / pairs as f64;
1322 assert!(
1323 mean_error.abs() < 3e-3,
1324 "QJL-corrected estimator must be unbiased: mean error {mean_error}"
1325 );
1326 assert!(rmse < 0.05, "estimator RMSE too large: {rmse}");
1327 assert!(
1328 mean_error.abs() <= stage1_mean_error.abs() + 1e-4,
1329 "QJL correction must not increase bias: {mean_error} vs stage-1 {stage1_mean_error}"
1330 );
1331 }
1332
1333 #[test]
1334 fn block_scoring_matches_row_estimates() {
1335 let dim = 100;
1336 let codec = TqCodec::new(dim);
1337 let mut scratch = TqEncodeScratch::default();
1338 let query = seeded_unit_vector(dim, 3);
1339 let plan = TqQueryPlan::build(&codec, &query);
1340
1341 let lanes = 13; let mut rows = Vec::new();
1343 let mut gammas = Vec::new();
1344 for lane in 0..lanes {
1345 let vector = seeded_unit_vector(dim, 50 + lane as u64);
1346 let mut nibbles = vec![0u8; codec.padded_dim()];
1347 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1348 rows.push(nibbles);
1349 gammas.push(gamma);
1350 }
1351 let row_refs: Vec<&[u8]> = rows.iter().map(Vec::as_slice).collect();
1352 let mut block = Vec::new();
1353 tq_pack_block(&row_refs, &gammas, codec.padded_dim(), &mut block);
1354 assert_eq!(block.len(), tq_block_bytes(codec.code_size()));
1355
1356 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1357 tq_score_block(&plan, &block, &mut scores);
1358 for lane in 0..lanes {
1359 let expected = plan.estimate_row(&rows[lane], gammas[lane]);
1360 assert!(
1362 (scores[lane] - expected).abs() < 0.02,
1363 "lane {lane}: block score {} vs row estimate {expected}",
1364 scores[lane]
1365 );
1366 }
1367 }
1368
1369 #[test]
1370 fn simd_accumulation_matches_scalar_reference_exactly() {
1371 let padded_dim = 192; let mut state = 99u64;
1373 let mut base_lut = vec![0i8; padded_dim * 16];
1374 let mut qjl_lut = vec![0i8; padded_dim * 16];
1375 for value in base_lut.iter_mut().chain(qjl_lut.iter_mut()) {
1376 *value = (splitmix64(&mut state) as i32 % 255 - 127) as i8;
1377 }
1378 let mut nibble_bytes = vec![0u8; padded_dim * 8];
1379 for byte in nibble_bytes.iter_mut() {
1380 *byte = splitmix64(&mut state) as u8;
1381 }
1382
1383 let mut base_reference = [0i32; TQ_BLOCK_LANES];
1384 let mut qjl_reference = [0i32; TQ_BLOCK_LANES];
1385 lut16::accumulate_block_scalar(
1386 &base_lut,
1387 &qjl_lut,
1388 &nibble_bytes,
1389 padded_dim,
1390 &mut base_reference,
1391 &mut qjl_reference,
1392 );
1393 let mut base_dispatch = [0i32; TQ_BLOCK_LANES];
1394 let mut qjl_dispatch = [0i32; TQ_BLOCK_LANES];
1395 lut16::accumulate_block(
1396 &base_lut,
1397 &qjl_lut,
1398 &nibble_bytes,
1399 padded_dim,
1400 &mut base_dispatch,
1401 &mut qjl_dispatch,
1402 );
1403 assert_eq!(
1404 base_reference, base_dispatch,
1405 "base sums must match exactly"
1406 );
1407 assert_eq!(qjl_reference, qjl_dispatch, "qjl sums must match exactly");
1408 }
1409
1410 #[test]
1411 fn fingerprint_pins_codec_constants() {
1412 let codec = TqCodec::new(768);
1413 assert_eq!(codec.padded_dim(), 768, "codec v2 is padding-free");
1414 assert_eq!(codec.code_size(), 384);
1415 assert_ne!(codec.fingerprint(), 0);
1416 assert_eq!(codec.fingerprint(), TqCodec::new(768).fingerprint());
1417 assert_ne!(codec.fingerprint(), TqCodec::new(769).fingerprint());
1418 assert_eq!(
1422 codec.fingerprint(),
1423 GOLDEN_FINGERPRINT_768,
1424 "TQ fingerprint for dim 768 changed; existing segments would be \
1425 rejected. Bump TQ_CODEC_VERSION deliberately instead."
1426 );
1427 }
1428
1429 #[test]
1430 fn query_plan_identity_uses_exact_float_bits() {
1431 let codec = TqCodec::new(4);
1432 let query = [0.0, -0.0, 1.0, f32::from_bits(0x7fc0_0001)];
1433 let plan = TqQueryPlan::build(&codec, &query);
1434
1435 assert!(plan.matches_query(&query));
1436 assert!(!plan.matches_query(&query[..3]));
1437
1438 let mut different_zero = query;
1439 different_zero[1] = 0.0;
1440 assert!(!plan.matches_query(&different_zero));
1441
1442 let mut different_nan = query;
1443 different_nan[3] = f32::from_bits(0x7fc0_0002);
1444 assert!(!plan.matches_query(&different_nan));
1445 }
1446
1447 const GOLDEN_FINGERPRINT_768: u64 = 7026088428300072418;
1448
1449 #[cfg(feature = "native")]
1450 #[test]
1451 fn builder_packs_blocks_in_input_order() {
1452 let dim = 32;
1453 let codec = std::sync::Arc::new(TqCodec::new(dim));
1454 let mut builder = TqFlatBuilder::new(std::sync::Arc::clone(&codec));
1455 let count = 37; let labels: Vec<(u32, u16)> = (0..count).map(|i| (i as u32, (i % 3) as u16)).collect();
1457 let mut vectors = Vec::new();
1458 for i in 0..count {
1459 vectors.extend(seeded_unit_vector(dim, 7_000 + i as u64));
1460 }
1461 builder.add_batch(&labels, &vectors).unwrap();
1462 builder.finish();
1463 assert_eq!(builder.doc_ids.len(), count);
1464 assert_eq!(
1465 builder.codes.len(),
1466 tq_codes_column_len(count, codec.code_size())
1467 );
1468
1469 let query = seeded_unit_vector(dim, 1);
1471 let plan = TqQueryPlan::build(&codec, &query);
1472 let block_bytes = tq_block_bytes(codec.code_size());
1473 let mut scratch = TqEncodeScratch::default();
1474 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1475 for index in 0..count {
1476 let block = &builder.codes[(index / TQ_BLOCK_LANES) * block_bytes..][..block_bytes];
1477 tq_score_block(&plan, block, &mut scores);
1478 let mut nibbles = vec![0u8; codec.padded_dim()];
1479 let gamma = codec.encode_into(
1480 &vectors[index * dim..(index + 1) * dim],
1481 &mut nibbles,
1482 &mut scratch,
1483 );
1484 let expected = plan.estimate_row(&nibbles, gamma);
1485 assert!(
1486 (scores[index % TQ_BLOCK_LANES] - expected).abs() < 0.02,
1487 "vector {index} scored {} expected {expected}",
1488 scores[index % TQ_BLOCK_LANES]
1489 );
1490 }
1491 }
1492}