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 base_lut_i8: Vec<i8>,
519 qjl_lut_i8: Vec<i8>,
520 base_dequant: f32,
521 qjl_dequant: f32,
522 #[cfg(test)]
524 reference_luts: (Vec<f32>, Vec<f32>),
525}
526
527impl std::fmt::Debug for TqQueryPlan {
528 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 formatter
530 .debug_struct("TqQueryPlan")
531 .field("padded_dim", &self.padded_dim)
532 .field("fingerprint", &self.fingerprint)
533 .finish()
534 }
535}
536
537impl TqQueryPlan {
538 pub fn build(codec: &TqCodec, query: &[f32]) -> Self {
539 assert_eq!(query.len(), codec.dim, "TQ query dimension mismatch");
540 let padded_dim = codec.padded_dim;
541 let norm = crate::structures::simd::dot_product_f32(query, query, query.len()).sqrt();
542 let inverse_norm = if norm.is_finite() && norm > 0.0 {
543 1.0 / norm
544 } else {
545 0.0
546 };
547 let normalized: Vec<f32> = query.iter().map(|value| value * inverse_norm).collect();
548 let mut fwht = Vec::with_capacity(padded_dim);
549 let mut rotated = vec![0.0f32; padded_dim];
550 codec
551 .stage1_rotation
552 .apply(&normalized, &mut fwht, &mut rotated);
553 let mut qjl_rotated = vec![0.0f32; padded_dim];
554 codec
555 .qjl_rotation
556 .apply(&rotated, &mut fwht, &mut qjl_rotated);
557
558 let mut base_lut = vec![0.0f32; padded_dim * 16];
559 let mut qjl_lut = vec![0.0f32; padded_dim * 16];
560 for dim in 0..padded_dim {
561 for nibble in 0..16 {
562 let level = codec.codebook.levels[nibble >> 1];
563 let sign = if nibble & 1 == 1 { 1.0 } else { -1.0 };
564 base_lut[dim * 16 + nibble] = rotated[dim] * level;
565 qjl_lut[dim * 16 + nibble] = sign * codec.qjl_scale * qjl_rotated[dim];
566 }
567 }
568 let (base_lut_i8, base_dequant) = quantize_lut(&base_lut);
569 let (qjl_lut_i8, qjl_dequant) = quantize_lut(&qjl_lut);
570 Self {
571 padded_dim,
572 fingerprint: codec.fingerprint,
573 base_lut_i8,
574 qjl_lut_i8,
575 base_dequant,
576 qjl_dequant,
577 #[cfg(test)]
578 reference_luts: (base_lut, qjl_lut),
579 }
580 }
581
582 #[inline]
583 pub fn padded_dim(&self) -> usize {
584 self.padded_dim
585 }
586
587 #[inline]
588 pub fn fingerprint(&self) -> u64 {
589 self.fingerprint
590 }
591
592 #[cfg(test)]
595 pub(crate) fn estimate_row(&self, nibbles: &[u8], gamma: f32) -> f32 {
596 debug_assert_eq!(nibbles.len(), self.padded_dim);
597 let (base_lut, qjl_lut) = &self.reference_luts;
598 let mut base = 0.0f32;
599 let mut qjl = 0.0f32;
600 for (dim, &nibble) in nibbles.iter().enumerate() {
601 base += base_lut[dim * 16 + nibble as usize];
602 qjl += qjl_lut[dim * 16 + nibble as usize];
603 }
604 base + gamma * qjl
605 }
606}
607
608fn quantize_lut(values: &[f32]) -> (Vec<i8>, f32) {
609 let max_abs = values
610 .iter()
611 .fold(0.0f32, |acc, &value| acc.max(value.abs()));
612 if !max_abs.is_finite() || max_abs <= 0.0 {
613 return (vec![0i8; values.len()], 0.0);
614 }
615 let quantize_scale = 127.0 / max_abs;
616 let quantized = values
617 .iter()
618 .map(|&value| (value * quantize_scale).round().clamp(-127.0, 127.0) as i8)
619 .collect();
620 (quantized, max_abs / 127.0)
621}
622
623pub fn tq_score_block(plan: &TqQueryPlan, block: &[u8], scores: &mut [f32; TQ_BLOCK_LANES]) {
627 debug_assert_eq!(block.len(), tq_block_bytes(plan.padded_dim / 2));
628 let (gamma_bytes, nibble_bytes) = block.split_at(TQ_BLOCK_LANES * size_of::<f32>());
629 let mut base = [0i32; TQ_BLOCK_LANES];
630 let mut qjl = [0i32; TQ_BLOCK_LANES];
631 lut16::accumulate_block(
632 &plan.base_lut_i8,
633 &plan.qjl_lut_i8,
634 nibble_bytes,
635 plan.padded_dim,
636 &mut base,
637 &mut qjl,
638 );
639 for lane in 0..TQ_BLOCK_LANES {
640 let gamma = f32::from_le_bytes(
641 gamma_bytes[lane * 4..lane * 4 + 4]
642 .try_into()
643 .expect("gamma slice is 4 bytes"),
644 );
645 scores[lane] =
646 base[lane] as f32 * plan.base_dequant + gamma * qjl[lane] as f32 * plan.qjl_dequant;
647 }
648}
649
650pub fn tq_pack_block(rows: &[&[u8]], gammas: &[f32], padded_dim: usize, output: &mut Vec<u8>) {
653 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
654 for lane in 0..TQ_BLOCK_LANES {
655 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
656 output.extend_from_slice(&gamma.to_le_bytes());
657 }
658 pack_nibble_rows(rows, padded_dim, output);
659}
660
661pub fn tq_pack_ivf_block(
663 rows: &[&[u8]],
664 scales: &[f32],
665 gammas: &[f32],
666 padded_dim: usize,
667 output: &mut Vec<u8>,
668) {
669 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
670 assert_eq!(scales.len(), gammas.len());
671 for lane in 0..TQ_BLOCK_LANES {
672 let scale = scales.get(lane).copied().unwrap_or(0.0);
673 output.extend_from_slice(&scale.to_le_bytes());
674 }
675 for lane in 0..TQ_BLOCK_LANES {
676 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
677 output.extend_from_slice(&gamma.to_le_bytes());
678 }
679 pack_nibble_rows(rows, padded_dim, output);
680}
681
682fn pack_nibble_rows(rows: &[&[u8]], padded_dim: usize, output: &mut Vec<u8>) {
683 for dim in 0..padded_dim {
684 for byte_index in 0..TQ_BLOCK_LANES / 2 {
685 let low = rows.get(byte_index).map_or(0, |row| row[dim] & 0x0F);
686 let high = rows
687 .get(byte_index + TQ_BLOCK_LANES / 2)
688 .map_or(0, |row| row[dim] & 0x0F);
689 output.push(low | (high << 4));
690 }
691 }
692}
693
694pub fn tq_score_ivf_block(
698 plan: &TqQueryPlan,
699 block: &[u8],
700 cluster_dot: f32,
701 scores: &mut [f32; TQ_BLOCK_LANES],
702) {
703 debug_assert_eq!(block.len(), tq_ivf_block_bytes(plan.padded_dim() / 2));
704 let lane_f32 = TQ_BLOCK_LANES * size_of::<f32>();
705 let (scale_bytes, rest) = block.split_at(lane_f32);
706 let (gamma_bytes, nibble_bytes) = rest.split_at(lane_f32);
707 let mut base = [0i32; TQ_BLOCK_LANES];
708 let mut qjl = [0i32; TQ_BLOCK_LANES];
709 lut16::accumulate_block(
710 &plan.base_lut_i8,
711 &plan.qjl_lut_i8,
712 nibble_bytes,
713 plan.padded_dim,
714 &mut base,
715 &mut qjl,
716 );
717 for lane in 0..TQ_BLOCK_LANES {
718 let scale = f32::from_le_bytes(
719 scale_bytes[lane * 4..lane * 4 + 4]
720 .try_into()
721 .expect("scale slice is 4 bytes"),
722 );
723 let gamma = f32::from_le_bytes(
724 gamma_bytes[lane * 4..lane * 4 + 4]
725 .try_into()
726 .expect("gamma slice is 4 bytes"),
727 );
728 scores[lane] = cluster_dot
729 + scale
730 * (base[lane] as f32 * plan.base_dequant
731 + gamma * qjl[lane] as f32 * plan.qjl_dequant);
732 }
733}
734
735mod lut16 {
736 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
737 use super::TQ_ACCUMULATE_CHUNK_DIMS;
738 use super::TQ_BLOCK_LANES;
739
740 pub(super) fn accumulate_block(
744 base_lut: &[i8],
745 qjl_lut: &[i8],
746 nibble_bytes: &[u8],
747 padded_dim: usize,
748 base: &mut [i32; TQ_BLOCK_LANES],
749 qjl: &mut [i32; TQ_BLOCK_LANES],
750 ) {
751 #[cfg(target_arch = "aarch64")]
752 {
753 unsafe { accumulate_block_neon(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl) }
755 return;
756 }
757 #[cfg(target_arch = "x86_64")]
758 {
759 if std::arch::is_x86_feature_detected!("avx2") {
760 unsafe {
761 accumulate_block_avx2(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl)
762 }
763 return;
764 }
765 if std::arch::is_x86_feature_detected!("ssse3") {
766 unsafe {
767 accumulate_block_ssse3(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl)
768 }
769 return;
770 }
771 }
772 #[allow(unreachable_code)]
773 accumulate_block_scalar(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl);
774 }
775
776 pub(super) fn accumulate_block_scalar(
779 base_lut: &[i8],
780 qjl_lut: &[i8],
781 nibble_bytes: &[u8],
782 padded_dim: usize,
783 base: &mut [i32; TQ_BLOCK_LANES],
784 qjl: &mut [i32; TQ_BLOCK_LANES],
785 ) {
786 for dim in 0..padded_dim {
787 let row = &nibble_bytes[dim * 8..dim * 8 + 8];
788 let base_table = &base_lut[dim * 16..dim * 16 + 16];
789 let qjl_table = &qjl_lut[dim * 16..dim * 16 + 16];
790 for (lane, &byte) in row.iter().enumerate() {
791 let low = (byte & 0x0F) as usize;
792 let high = (byte >> 4) as usize;
793 base[lane] += i32::from(base_table[low]);
794 base[lane + 8] += i32::from(base_table[high]);
795 qjl[lane] += i32::from(qjl_table[low]);
796 qjl[lane + 8] += i32::from(qjl_table[high]);
797 }
798 }
799 }
800
801 #[cfg(target_arch = "aarch64")]
802 #[target_feature(enable = "neon")]
803 unsafe fn accumulate_block_neon(
804 base_lut: &[i8],
805 qjl_lut: &[i8],
806 nibble_bytes: &[u8],
807 padded_dim: usize,
808 base: &mut [i32; TQ_BLOCK_LANES],
809 qjl: &mut [i32; TQ_BLOCK_LANES],
810 ) {
811 use std::arch::aarch64::*;
812 unsafe {
813 let mask = vdup_n_u8(0x0F);
814 let mut base_lo_i32 = [vdupq_n_s32(0); 4];
815 let mut qjl_lo_i32 = [vdupq_n_s32(0); 4];
816 let mut dim = 0;
817 while dim < padded_dim {
818 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
819 let mut base_acc = [vdupq_n_s16(0); 2];
820 let mut qjl_acc = [vdupq_n_s16(0); 2];
821 while dim < chunk_end {
822 let row = vld1_u8(nibble_bytes.as_ptr().add(dim * 8));
823 let low = vand_u8(row, mask);
824 let high = vshr_n_u8::<4>(row);
825 let lanes = vcombine_u8(low, high);
826 let base_table = vld1q_s8(base_lut.as_ptr().add(dim * 16));
827 let qjl_table = vld1q_s8(qjl_lut.as_ptr().add(dim * 16));
828 let base_values =
829 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(base_table), lanes));
830 let qjl_values =
831 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(qjl_table), lanes));
832 base_acc[0] = vaddw_s8(base_acc[0], vget_low_s8(base_values));
833 base_acc[1] = vaddw_s8(base_acc[1], vget_high_s8(base_values));
834 qjl_acc[0] = vaddw_s8(qjl_acc[0], vget_low_s8(qjl_values));
835 qjl_acc[1] = vaddw_s8(qjl_acc[1], vget_high_s8(qjl_values));
836 dim += 1;
837 }
838 for half in 0..2 {
839 base_lo_i32[half * 2] =
840 vaddw_s16(base_lo_i32[half * 2], vget_low_s16(base_acc[half]));
841 base_lo_i32[half * 2 + 1] =
842 vaddw_s16(base_lo_i32[half * 2 + 1], vget_high_s16(base_acc[half]));
843 qjl_lo_i32[half * 2] =
844 vaddw_s16(qjl_lo_i32[half * 2], vget_low_s16(qjl_acc[half]));
845 qjl_lo_i32[half * 2 + 1] =
846 vaddw_s16(qjl_lo_i32[half * 2 + 1], vget_high_s16(qjl_acc[half]));
847 }
848 }
849 for quarter in 0..4 {
850 vst1q_s32(base.as_mut_ptr().add(quarter * 4), base_lo_i32[quarter]);
851 vst1q_s32(qjl.as_mut_ptr().add(quarter * 4), qjl_lo_i32[quarter]);
852 }
853 }
854 }
855
856 #[cfg(target_arch = "x86_64")]
860 #[target_feature(enable = "avx2")]
861 unsafe fn accumulate_block_avx2(
862 base_lut: &[i8],
863 qjl_lut: &[i8],
864 nibble_bytes: &[u8],
865 padded_dim: usize,
866 base: &mut [i32; TQ_BLOCK_LANES],
867 qjl: &mut [i32; TQ_BLOCK_LANES],
868 ) {
869 use std::arch::x86_64::*;
870 unsafe {
871 let mask = _mm_set1_epi8(0x0F);
872 let zero256 = _mm256_setzero_si256();
873 let mut base_i32 = [zero256; 2];
874 let mut qjl_i32 = [zero256; 2];
875 let mut dim = 0;
876 while dim < padded_dim {
877 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
878 let mut base_acc = zero256;
879 let mut qjl_acc = zero256;
880 while dim + 2 <= chunk_end {
881 let rows = _mm_loadu_si128(nibble_bytes.as_ptr().add(dim * 8).cast());
883 let low = _mm_and_si128(rows, mask);
884 let high = _mm_and_si128(_mm_srli_epi16(rows, 4), mask);
885 let lanes_first = _mm_unpacklo_epi64(low, high);
887 let lanes_second = _mm_unpackhi_epi64(low, high);
888 let lanes = _mm256_inserti128_si256(
889 _mm256_castsi128_si256(lanes_first),
890 lanes_second,
891 1,
892 );
893 let base_tables = _mm256_loadu_si256(base_lut.as_ptr().add(dim * 16).cast());
894 let qjl_tables = _mm256_loadu_si256(qjl_lut.as_ptr().add(dim * 16).cast());
895 let base_values = _mm256_shuffle_epi8(base_tables, lanes);
896 let qjl_values = _mm256_shuffle_epi8(qjl_tables, lanes);
897 base_acc = _mm256_add_epi16(
898 base_acc,
899 _mm256_cvtepi8_epi16(_mm256_castsi256_si128(base_values)),
900 );
901 base_acc = _mm256_add_epi16(
902 base_acc,
903 _mm256_cvtepi8_epi16(_mm256_extracti128_si256(base_values, 1)),
904 );
905 qjl_acc = _mm256_add_epi16(
906 qjl_acc,
907 _mm256_cvtepi8_epi16(_mm256_castsi256_si128(qjl_values)),
908 );
909 qjl_acc = _mm256_add_epi16(
910 qjl_acc,
911 _mm256_cvtepi8_epi16(_mm256_extracti128_si256(qjl_values, 1)),
912 );
913 dim += 2;
914 }
915 while dim < chunk_end {
917 let row = _mm_loadl_epi64(nibble_bytes.as_ptr().add(dim * 8).cast());
918 let low = _mm_and_si128(row, mask);
919 let high = _mm_and_si128(_mm_srli_epi16(row, 4), mask);
920 let lanes = _mm_unpacklo_epi64(low, high);
921 let base_table = _mm_loadu_si128(base_lut.as_ptr().add(dim * 16).cast());
922 let qjl_table = _mm_loadu_si128(qjl_lut.as_ptr().add(dim * 16).cast());
923 base_acc = _mm256_add_epi16(
924 base_acc,
925 _mm256_cvtepi8_epi16(_mm_shuffle_epi8(base_table, lanes)),
926 );
927 qjl_acc = _mm256_add_epi16(
928 qjl_acc,
929 _mm256_cvtepi8_epi16(_mm_shuffle_epi8(qjl_table, lanes)),
930 );
931 dim += 1;
932 }
933 for (accumulators, chunk) in [(&mut base_i32, base_acc), (&mut qjl_i32, qjl_acc)] {
934 accumulators[0] = _mm256_add_epi32(
935 accumulators[0],
936 _mm256_cvtepi16_epi32(_mm256_castsi256_si128(chunk)),
937 );
938 accumulators[1] = _mm256_add_epi32(
939 accumulators[1],
940 _mm256_cvtepi16_epi32(_mm256_extracti128_si256(chunk, 1)),
941 );
942 }
943 }
944 _mm256_storeu_si256(base.as_mut_ptr().cast(), base_i32[0]);
945 _mm256_storeu_si256(base.as_mut_ptr().add(8).cast(), base_i32[1]);
946 _mm256_storeu_si256(qjl.as_mut_ptr().cast(), qjl_i32[0]);
947 _mm256_storeu_si256(qjl.as_mut_ptr().add(8).cast(), qjl_i32[1]);
948 }
949 }
950
951 #[cfg(target_arch = "x86_64")]
952 #[target_feature(enable = "ssse3")]
953 unsafe fn accumulate_block_ssse3(
954 base_lut: &[i8],
955 qjl_lut: &[i8],
956 nibble_bytes: &[u8],
957 padded_dim: usize,
958 base: &mut [i32; TQ_BLOCK_LANES],
959 qjl: &mut [i32; TQ_BLOCK_LANES],
960 ) {
961 use std::arch::x86_64::*;
962 unsafe {
963 let mask = _mm_set1_epi8(0x0F);
964 let zero = _mm_setzero_si128();
965 let mut base_i32 = [zero; 4];
966 let mut qjl_i32 = [zero; 4];
967 let mut dim = 0;
968 while dim < padded_dim {
969 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
970 let mut base_acc = [zero; 2];
971 let mut qjl_acc = [zero; 2];
972 while dim < chunk_end {
973 let row = _mm_loadl_epi64(nibble_bytes.as_ptr().add(dim * 8).cast());
974 let low = _mm_and_si128(row, mask);
975 let high = _mm_and_si128(_mm_srli_epi16(row, 4), mask);
976 let lanes = _mm_unpacklo_epi64(low, high);
977 let base_table = _mm_loadu_si128(base_lut.as_ptr().add(dim * 16).cast());
978 let qjl_table = _mm_loadu_si128(qjl_lut.as_ptr().add(dim * 16).cast());
979 let base_values = _mm_shuffle_epi8(base_table, lanes);
980 let qjl_values = _mm_shuffle_epi8(qjl_table, lanes);
981 let base_sign = _mm_cmpgt_epi8(zero, base_values);
983 let qjl_sign = _mm_cmpgt_epi8(zero, qjl_values);
984 base_acc[0] =
985 _mm_add_epi16(base_acc[0], _mm_unpacklo_epi8(base_values, base_sign));
986 base_acc[1] =
987 _mm_add_epi16(base_acc[1], _mm_unpackhi_epi8(base_values, base_sign));
988 qjl_acc[0] = _mm_add_epi16(qjl_acc[0], _mm_unpacklo_epi8(qjl_values, qjl_sign));
989 qjl_acc[1] = _mm_add_epi16(qjl_acc[1], _mm_unpackhi_epi8(qjl_values, qjl_sign));
990 dim += 1;
991 }
992 for half in 0..2 {
993 let base_sign = _mm_cmpgt_epi16(zero, base_acc[half]);
994 let qjl_sign = _mm_cmpgt_epi16(zero, qjl_acc[half]);
995 base_i32[half * 2] = _mm_add_epi32(
996 base_i32[half * 2],
997 _mm_unpacklo_epi16(base_acc[half], base_sign),
998 );
999 base_i32[half * 2 + 1] = _mm_add_epi32(
1000 base_i32[half * 2 + 1],
1001 _mm_unpackhi_epi16(base_acc[half], base_sign),
1002 );
1003 qjl_i32[half * 2] = _mm_add_epi32(
1004 qjl_i32[half * 2],
1005 _mm_unpacklo_epi16(qjl_acc[half], qjl_sign),
1006 );
1007 qjl_i32[half * 2 + 1] = _mm_add_epi32(
1008 qjl_i32[half * 2 + 1],
1009 _mm_unpackhi_epi16(qjl_acc[half], qjl_sign),
1010 );
1011 }
1012 }
1013 for quarter in 0..4 {
1014 _mm_storeu_si128(base.as_mut_ptr().add(quarter * 4).cast(), base_i32[quarter]);
1015 _mm_storeu_si128(qjl.as_mut_ptr().add(quarter * 4).cast(), qjl_i32[quarter]);
1016 }
1017 }
1018 }
1019}
1020
1021#[cfg(feature = "native")]
1028pub struct TqFlatBuilder {
1029 codec: std::sync::Arc<TqCodec>,
1030 pub doc_ids: Vec<u32>,
1031 pub ordinals: Vec<u16>,
1032 pub codes: Vec<u8>,
1033 pending_rows: Vec<Vec<u8>>,
1034 pending_gammas: Vec<f32>,
1035}
1036
1037#[cfg(feature = "native")]
1038impl TqFlatBuilder {
1039 pub fn new(codec: std::sync::Arc<TqCodec>) -> Self {
1040 Self {
1041 codec,
1042 doc_ids: Vec::new(),
1043 ordinals: Vec::new(),
1044 codes: Vec::new(),
1045 pending_rows: Vec::with_capacity(TQ_BLOCK_LANES),
1046 pending_gammas: Vec::with_capacity(TQ_BLOCK_LANES),
1047 }
1048 }
1049
1050 #[inline]
1051 pub fn codec(&self) -> &TqCodec {
1052 &self.codec
1053 }
1054
1055 #[inline]
1056 pub fn len(&self) -> usize {
1057 self.doc_ids.len()
1058 }
1059
1060 #[inline]
1061 pub fn is_empty(&self) -> bool {
1062 self.doc_ids.is_empty()
1063 }
1064
1065 pub fn add_batch(
1068 &mut self,
1069 labels: &[(u32, u16)],
1070 vectors: &[f32],
1071 ) -> Result<(), &'static str> {
1072 use rayon::prelude::*;
1073
1074 let dim = self.codec.dim();
1075 let vector_count = labels.len();
1076 let expected = vector_count
1077 .checked_mul(dim)
1078 .ok_or("TQ input size overflow")?;
1079 if vectors.len() != expected {
1080 return Err("TQ vector and label matrices are inconsistent");
1081 }
1082 let padded_dim = self.codec.padded_dim();
1083 let codec = std::sync::Arc::clone(&self.codec);
1084 let mut rows = vec![0u8; vector_count * padded_dim];
1088 let mut gammas = vec![0.0f32; vector_count];
1089 vectors
1090 .par_chunks_exact(dim)
1091 .zip(rows.par_chunks_exact_mut(padded_dim))
1092 .zip(gammas.par_iter_mut())
1093 .for_each_init(
1094 TqEncodeScratch::default,
1095 |scratch, ((vector, row), gamma)| {
1096 *gamma = codec.encode_into(vector, row, scratch);
1097 },
1098 );
1099
1100 let mut index = 0;
1103 while index < vector_count && !self.pending_rows.is_empty() {
1104 let (doc_id, ordinal) = labels[index];
1105 self.doc_ids.push(doc_id);
1106 self.ordinals.push(ordinal);
1107 self.pending_rows
1108 .push(rows[index * padded_dim..(index + 1) * padded_dim].to_vec());
1109 self.pending_gammas.push(gammas[index]);
1110 if self.pending_rows.len() == TQ_BLOCK_LANES {
1111 self.flush_block();
1112 }
1113 index += 1;
1114 }
1115 while vector_count - index >= TQ_BLOCK_LANES {
1116 let row_refs: Vec<&[u8]> = (0..TQ_BLOCK_LANES)
1117 .map(|lane| {
1118 let row = index + lane;
1119 &rows[row * padded_dim..(row + 1) * padded_dim]
1120 })
1121 .collect();
1122 tq_pack_block(
1123 &row_refs,
1124 &gammas[index..index + TQ_BLOCK_LANES],
1125 padded_dim,
1126 &mut self.codes,
1127 );
1128 for &(doc_id, ordinal) in &labels[index..index + TQ_BLOCK_LANES] {
1129 self.doc_ids.push(doc_id);
1130 self.ordinals.push(ordinal);
1131 }
1132 index += TQ_BLOCK_LANES;
1133 }
1134 for row in index..vector_count {
1135 let (doc_id, ordinal) = labels[row];
1136 self.doc_ids.push(doc_id);
1137 self.ordinals.push(ordinal);
1138 self.pending_rows
1139 .push(rows[row * padded_dim..(row + 1) * padded_dim].to_vec());
1140 self.pending_gammas.push(gammas[row]);
1141 }
1142 Ok(())
1143 }
1144
1145 fn flush_block(&mut self) {
1146 let rows: Vec<&[u8]> = self.pending_rows.iter().map(Vec::as_slice).collect();
1147 tq_pack_block(
1148 &rows,
1149 &self.pending_gammas,
1150 self.codec.padded_dim(),
1151 &mut self.codes,
1152 );
1153 self.pending_rows.clear();
1154 self.pending_gammas.clear();
1155 }
1156
1157 pub fn finish(&mut self) {
1159 if !self.pending_rows.is_empty() {
1160 self.flush_block();
1161 }
1162 debug_assert_eq!(
1163 self.codes.len(),
1164 tq_codes_column_len(self.doc_ids.len(), self.codec.code_size())
1165 );
1166 }
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171 use super::*;
1172
1173 fn seeded_unit_vector(dim: usize, seed: u64) -> Vec<f32> {
1174 let mut state = seed;
1176 let mut values: Vec<f32> = (0..dim)
1177 .map(|_| {
1178 let a = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
1179 let b = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
1180 let gaussian = (-2.0 * (1.0 - a).max(f64::MIN_POSITIVE).ln()).sqrt()
1181 * (2.0 * std::f64::consts::PI * b).cos();
1182 gaussian as f32
1183 })
1184 .collect();
1185 let norm = values.iter().map(|v| v * v).sum::<f32>().sqrt();
1186 values.iter_mut().for_each(|v| *v /= norm);
1187 values
1188 }
1189
1190 #[test]
1191 fn rotation_is_orthonormal_and_deterministic() {
1192 let rotation = TqRotation::new(100, 42);
1193 assert_eq!(rotation.padded_dim(), 100);
1194 let mut fwht_scratch = Vec::new();
1195 let mut probe = vec![0.0f32; 100];
1196 assert_eq!(TqRotation::new(99, 42).padded_dim(), 100);
1198 TqRotation::new(99, 42).apply(&vec![1.0; 99], &mut fwht_scratch, &mut probe);
1199 let input = seeded_unit_vector(100, 7);
1200 let mut fwht = Vec::new();
1201 let mut output = vec![0.0f32; 100];
1202 rotation.apply(&input, &mut fwht, &mut output);
1203 let norm: f32 = output.iter().map(|v| v * v).sum();
1204 assert!(
1205 (norm - 1.0).abs() < 1e-5,
1206 "rotation must preserve norm, got {norm}"
1207 );
1208
1209 let mut second = vec![0.0f32; 100];
1210 TqRotation::new(100, 42).apply(&input, &mut fwht, &mut second);
1211 assert_eq!(output, second, "rotation must be deterministic");
1212
1213 let other = seeded_unit_vector(100, 8);
1215 let mut other_rotated = vec![0.0f32; 100];
1216 rotation.apply(&other, &mut fwht, &mut other_rotated);
1217 let dot_before: f32 = input.iter().zip(&other).map(|(a, b)| a * b).sum();
1218 let dot_after: f32 = output.iter().zip(&other_rotated).map(|(a, b)| a * b).sum();
1219 assert!(
1220 (dot_before - dot_after).abs() < 1e-4,
1221 "rotation must preserve inner products: {dot_before} vs {dot_after}"
1222 );
1223 }
1224
1225 #[test]
1226 fn analytic_codebook_is_symmetric_and_monotonic() {
1227 for padded_dim in [8, 128, 1024] {
1228 let codebook = TqCodebook::analytic(padded_dim);
1229 let levels = &codebook.levels;
1230 for pair in levels.windows(2) {
1231 assert!(pair[0] < pair[1], "levels must be strictly increasing");
1232 }
1233 for index in 0..TQ_STAGE1_LEVELS / 2 {
1234 assert_eq!(
1235 levels[index],
1236 -levels[TQ_STAGE1_LEVELS - 1 - index],
1237 "levels must be exactly symmetric for P={padded_dim}: {levels:?}"
1238 );
1239 }
1240 assert!(levels[TQ_STAGE1_LEVELS - 1] < 1.0);
1241 let scale = 1.0 / (padded_dim as f32).sqrt();
1244 assert!(
1245 levels[TQ_STAGE1_LEVELS - 1] < 6.0 * scale,
1246 "top level {} is implausibly large for P={padded_dim}",
1247 levels[TQ_STAGE1_LEVELS - 1]
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn encode_coordinate_matches_nearest_level() {
1254 let codebook = TqCodebook::analytic(256);
1255 for step in -1000i32..=1000 {
1256 let value = step as f32 / 1000.0;
1257 let code = codebook.encode_coordinate(value) as usize;
1258 let nearest = codebook
1259 .levels
1260 .iter()
1261 .enumerate()
1262 .min_by(|a, b| (a.1 - value).abs().total_cmp(&(b.1 - value).abs()))
1263 .unwrap()
1264 .0;
1265 assert_eq!(
1266 code, nearest,
1267 "value {value} coded {code}, nearest {nearest}"
1268 );
1269 }
1270 }
1271
1272 #[test]
1273 fn estimator_is_unbiased_and_tight() {
1274 let dim = 96;
1275 let codec = TqCodec::new(dim);
1276 let mut scratch = TqEncodeScratch::default();
1277 let mut nibbles = vec![0u8; codec.padded_dim()];
1278
1279 let pairs = 512;
1280 let mut signed_error_sum = 0.0f64;
1281 let mut squared_error_sum = 0.0f64;
1282 let mut stage1_signed_error_sum = 0.0f64;
1283 for pair in 0..pairs {
1284 let vector = seeded_unit_vector(dim, 1000 + pair);
1285 let query = seeded_unit_vector(dim, 900_000 + pair);
1286 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1287 let plan = TqQueryPlan::build(&codec, &query);
1288 let estimate = plan.estimate_row(&nibbles, gamma);
1289 let stage1_only = plan.estimate_row(&nibbles, 0.0);
1290 let truth: f32 = vector.iter().zip(&query).map(|(a, b)| a * b).sum();
1291 signed_error_sum += f64::from(estimate - truth);
1292 squared_error_sum += f64::from(estimate - truth).powi(2);
1293 stage1_signed_error_sum += f64::from(stage1_only - truth);
1294 }
1295 let mean_error = signed_error_sum / pairs as f64;
1296 let rmse = (squared_error_sum / pairs as f64).sqrt();
1297 let stage1_mean_error = stage1_signed_error_sum / pairs as f64;
1298 assert!(
1299 mean_error.abs() < 3e-3,
1300 "QJL-corrected estimator must be unbiased: mean error {mean_error}"
1301 );
1302 assert!(rmse < 0.05, "estimator RMSE too large: {rmse}");
1303 assert!(
1304 mean_error.abs() <= stage1_mean_error.abs() + 1e-4,
1305 "QJL correction must not increase bias: {mean_error} vs stage-1 {stage1_mean_error}"
1306 );
1307 }
1308
1309 #[test]
1310 fn block_scoring_matches_row_estimates() {
1311 let dim = 100;
1312 let codec = TqCodec::new(dim);
1313 let mut scratch = TqEncodeScratch::default();
1314 let query = seeded_unit_vector(dim, 3);
1315 let plan = TqQueryPlan::build(&codec, &query);
1316
1317 let lanes = 13; let mut rows = Vec::new();
1319 let mut gammas = Vec::new();
1320 for lane in 0..lanes {
1321 let vector = seeded_unit_vector(dim, 50 + lane as u64);
1322 let mut nibbles = vec![0u8; codec.padded_dim()];
1323 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1324 rows.push(nibbles);
1325 gammas.push(gamma);
1326 }
1327 let row_refs: Vec<&[u8]> = rows.iter().map(Vec::as_slice).collect();
1328 let mut block = Vec::new();
1329 tq_pack_block(&row_refs, &gammas, codec.padded_dim(), &mut block);
1330 assert_eq!(block.len(), tq_block_bytes(codec.code_size()));
1331
1332 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1333 tq_score_block(&plan, &block, &mut scores);
1334 for lane in 0..lanes {
1335 let expected = plan.estimate_row(&rows[lane], gammas[lane]);
1336 assert!(
1338 (scores[lane] - expected).abs() < 0.02,
1339 "lane {lane}: block score {} vs row estimate {expected}",
1340 scores[lane]
1341 );
1342 }
1343 }
1344
1345 #[test]
1346 fn simd_accumulation_matches_scalar_reference_exactly() {
1347 let padded_dim = 192; let mut state = 99u64;
1349 let mut base_lut = vec![0i8; padded_dim * 16];
1350 let mut qjl_lut = vec![0i8; padded_dim * 16];
1351 for value in base_lut.iter_mut().chain(qjl_lut.iter_mut()) {
1352 *value = (splitmix64(&mut state) as i32 % 255 - 127) as i8;
1353 }
1354 let mut nibble_bytes = vec![0u8; padded_dim * 8];
1355 for byte in nibble_bytes.iter_mut() {
1356 *byte = splitmix64(&mut state) as u8;
1357 }
1358
1359 let mut base_reference = [0i32; TQ_BLOCK_LANES];
1360 let mut qjl_reference = [0i32; TQ_BLOCK_LANES];
1361 lut16::accumulate_block_scalar(
1362 &base_lut,
1363 &qjl_lut,
1364 &nibble_bytes,
1365 padded_dim,
1366 &mut base_reference,
1367 &mut qjl_reference,
1368 );
1369 let mut base_dispatch = [0i32; TQ_BLOCK_LANES];
1370 let mut qjl_dispatch = [0i32; TQ_BLOCK_LANES];
1371 lut16::accumulate_block(
1372 &base_lut,
1373 &qjl_lut,
1374 &nibble_bytes,
1375 padded_dim,
1376 &mut base_dispatch,
1377 &mut qjl_dispatch,
1378 );
1379 assert_eq!(
1380 base_reference, base_dispatch,
1381 "base sums must match exactly"
1382 );
1383 assert_eq!(qjl_reference, qjl_dispatch, "qjl sums must match exactly");
1384 }
1385
1386 #[test]
1387 fn fingerprint_pins_codec_constants() {
1388 let codec = TqCodec::new(768);
1389 assert_eq!(codec.padded_dim(), 768, "codec v2 is padding-free");
1390 assert_eq!(codec.code_size(), 384);
1391 assert_ne!(codec.fingerprint(), 0);
1392 assert_eq!(codec.fingerprint(), TqCodec::new(768).fingerprint());
1393 assert_ne!(codec.fingerprint(), TqCodec::new(769).fingerprint());
1394 assert_eq!(
1398 codec.fingerprint(),
1399 GOLDEN_FINGERPRINT_768,
1400 "TQ fingerprint for dim 768 changed; existing segments would be \
1401 rejected. Bump TQ_CODEC_VERSION deliberately instead."
1402 );
1403 }
1404
1405 const GOLDEN_FINGERPRINT_768: u64 = 7026088428300072418;
1406
1407 #[cfg(feature = "native")]
1408 #[test]
1409 fn builder_packs_blocks_in_input_order() {
1410 let dim = 32;
1411 let codec = std::sync::Arc::new(TqCodec::new(dim));
1412 let mut builder = TqFlatBuilder::new(std::sync::Arc::clone(&codec));
1413 let count = 37; let labels: Vec<(u32, u16)> = (0..count).map(|i| (i as u32, (i % 3) as u16)).collect();
1415 let mut vectors = Vec::new();
1416 for i in 0..count {
1417 vectors.extend(seeded_unit_vector(dim, 7_000 + i as u64));
1418 }
1419 builder.add_batch(&labels, &vectors).unwrap();
1420 builder.finish();
1421 assert_eq!(builder.doc_ids.len(), count);
1422 assert_eq!(
1423 builder.codes.len(),
1424 tq_codes_column_len(count, codec.code_size())
1425 );
1426
1427 let query = seeded_unit_vector(dim, 1);
1429 let plan = TqQueryPlan::build(&codec, &query);
1430 let block_bytes = tq_block_bytes(codec.code_size());
1431 let mut scratch = TqEncodeScratch::default();
1432 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1433 for index in 0..count {
1434 let block = &builder.codes[(index / TQ_BLOCK_LANES) * block_bytes..][..block_bytes];
1435 tq_score_block(&plan, block, &mut scores);
1436 let mut nibbles = vec![0u8; codec.padded_dim()];
1437 let gamma = codec.encode_into(
1438 &vectors[index * dim..(index + 1) * dim],
1439 &mut nibbles,
1440 &mut scratch,
1441 );
1442 let expected = plan.estimate_row(&nibbles, gamma);
1443 assert!(
1444 (scores[index % TQ_BLOCK_LANES] - expected).abs() < 0.02,
1445 "vector {index} scored {} expected {expected}",
1446 scores[index % TQ_BLOCK_LANES]
1447 );
1448 }
1449 }
1450}