1pub const TQ_CODEC_VERSION: u32 = 1;
17pub const TQ_BITS: u32 = 4;
19pub const TQ_BLOCK_LANES: usize = 16;
21pub const TQ_MIN_PADDED_DIM: usize = 8;
24
25const TQ_STAGE1_LEVELS: usize = 8;
26const TQ_STAGE1_SEED: u64 = 0x7154_5354_4147_4531; const TQ_QJL_SEED: u64 = 0x7154_514a_4c53_4b31; const TQ_LLOYD_GRID: usize = 8192;
29const TQ_LLOYD_MAX_ITERATIONS: usize = 64;
30const TQ_LLOYD_TOLERANCE: f64 = 1e-9;
31#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
34const TQ_ACCUMULATE_CHUNK_DIMS: usize = 128;
35
36#[inline]
39pub fn tq_padded_dim(dim: usize) -> usize {
40 dim.next_power_of_two().max(TQ_MIN_PADDED_DIM)
41}
42
43#[inline]
45pub fn tq_expected_fingerprint(dim: usize) -> u64 {
46 tq_fingerprint(dim, tq_padded_dim(dim))
47}
48
49#[inline]
51pub const fn tq_block_bytes(code_size: usize) -> usize {
52 TQ_BLOCK_LANES * (size_of::<f32>() + code_size)
53}
54
55#[inline]
57pub const fn tq_codes_column_len(count: usize, code_size: usize) -> usize {
58 count.div_ceil(TQ_BLOCK_LANES) * tq_block_bytes(code_size)
59}
60
61#[inline]
63pub fn tq_codes_column_len_checked(count: usize, code_size: usize) -> Option<usize> {
64 count
65 .div_ceil(TQ_BLOCK_LANES)
66 .checked_mul(tq_block_bytes(code_size))
67}
68
69#[inline]
72pub const fn tq_ivf_block_bytes(code_size: usize) -> usize {
73 TQ_BLOCK_LANES * (2 * size_of::<f32>() + code_size)
74}
75
76#[inline]
78pub fn tq_ivf_codes_column_len_checked(count: usize, code_size: usize) -> Option<usize> {
79 count
80 .div_ceil(TQ_BLOCK_LANES)
81 .checked_mul(tq_ivf_block_bytes(code_size))
82}
83
84#[inline]
85fn splitmix64(state: &mut u64) -> u64 {
86 *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
87 let mut z = *state;
88 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
89 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
90 z ^ (z >> 31)
91}
92
93#[derive(Debug, Clone)]
97pub struct TqRotation {
98 input_dim: usize,
99 padded_dim: usize,
100 signs: Vec<f32>,
102 perm: Vec<u32>,
104}
105
106impl TqRotation {
107 pub fn new(input_dim: usize, seed: u64) -> Self {
108 let padded_dim = tq_padded_dim(input_dim);
109 let mut state = seed;
110 let signs = (0..padded_dim)
111 .map(|_| {
112 if splitmix64(&mut state) & 1 == 1 {
113 1.0
114 } else {
115 -1.0
116 }
117 })
118 .collect();
119 let mut perm: Vec<u32> = (0..padded_dim as u32).collect();
120 for i in (1..padded_dim).rev() {
121 let j = (splitmix64(&mut state) % (i as u64 + 1)) as usize;
122 perm.swap(i, j);
123 }
124 Self {
125 input_dim,
126 padded_dim,
127 signs,
128 perm,
129 }
130 }
131
132 #[inline]
133 pub fn padded_dim(&self) -> usize {
134 self.padded_dim
135 }
136
137 pub fn apply(&self, input: &[f32], scratch: &mut Vec<f32>, output: &mut [f32]) {
141 debug_assert!(input.len() == self.input_dim || input.len() == self.padded_dim);
142 debug_assert_eq!(output.len(), self.padded_dim);
143 scratch.clear();
144 scratch.extend(
145 self.signs
146 .iter()
147 .enumerate()
148 .map(|(index, sign)| input.get(index).copied().unwrap_or(0.0) * sign),
149 );
150 fwht_normalized(scratch);
151 for (slot, &source) in output.iter_mut().zip(&self.perm) {
152 *slot = scratch[source as usize];
153 }
154 }
155}
156
157fn fwht_normalized(values: &mut [f32]) {
159 let len = values.len();
160 debug_assert!(len.is_power_of_two());
161 let mut step = 1;
162 while step < len {
163 let mut base = 0;
164 while base < len {
165 for offset in base..base + step {
166 let left = values[offset];
167 let right = values[offset + step];
168 values[offset] = left + right;
169 values[offset + step] = left - right;
170 }
171 base += step * 2;
172 }
173 step *= 2;
174 }
175 let scale = 1.0 / (len as f32).sqrt();
176 for value in values.iter_mut() {
177 *value *= scale;
178 }
179}
180
181#[derive(Debug, Clone)]
185pub struct TqCodebook {
186 levels: [f32; TQ_STAGE1_LEVELS],
187 boundaries: [f32; TQ_STAGE1_LEVELS - 1],
189}
190
191impl TqCodebook {
192 pub fn analytic(padded_dim: usize) -> Self {
193 assert!(
194 padded_dim >= TQ_MIN_PADDED_DIM,
195 "TQ codebook requires padded_dim >= {TQ_MIN_PADDED_DIM}, got {padded_dim}"
196 );
197 let exponent = (padded_dim as f64 - 3.0) / 2.0;
198 let cell = 2.0 / TQ_LLOYD_GRID as f64;
199 let mut weights = vec![0.0f64; TQ_LLOYD_GRID];
203 let mut positions = vec![0.0f64; TQ_LLOYD_GRID];
204 for index in 0..TQ_LLOYD_GRID {
205 let t = -1.0 + (index as f64 + 0.5) * cell;
206 positions[index] = t;
207 let log_density = exponent * (1.0 - t * t).max(f64::MIN_POSITIVE).ln();
208 weights[index] = log_density.exp();
209 }
210
211 let total_mass: f64 = weights.iter().sum();
213 let mut levels = [0.0f64; TQ_STAGE1_LEVELS];
214 let mut boundaries = [0.0f64; TQ_STAGE1_LEVELS - 1];
215 let mut accumulated = 0.0f64;
216 let mut next_boundary = 0usize;
217 for index in 0..TQ_LLOYD_GRID {
218 accumulated += weights[index];
219 while next_boundary < TQ_STAGE1_LEVELS - 1
220 && accumulated
221 >= total_mass * (next_boundary as f64 + 1.0) / TQ_STAGE1_LEVELS as f64
222 {
223 boundaries[next_boundary] = positions[index];
224 next_boundary += 1;
225 }
226 }
227
228 for _ in 0..TQ_LLOYD_MAX_ITERATIONS {
231 let mut mass = [0.0f64; TQ_STAGE1_LEVELS];
232 let mut moment = [0.0f64; TQ_STAGE1_LEVELS];
233 let mut bucket = 0usize;
234 for index in 0..TQ_LLOYD_GRID {
235 let t = positions[index];
236 while bucket < TQ_STAGE1_LEVELS - 1 && t > boundaries[bucket] {
237 bucket += 1;
238 }
239 mass[bucket] += weights[index];
240 moment[bucket] += weights[index] * t;
241 }
242 let mut shift = 0.0f64;
243 for level in 0..TQ_STAGE1_LEVELS {
244 if mass[level] > 0.0 {
245 let updated = moment[level] / mass[level];
246 shift = shift.max((updated - levels[level]).abs());
247 levels[level] = updated;
248 }
249 }
250 for boundary in 0..TQ_STAGE1_LEVELS - 1 {
251 boundaries[boundary] = 0.5 * (levels[boundary] + levels[boundary + 1]);
252 }
253 if shift < TQ_LLOYD_TOLERANCE {
254 break;
255 }
256 }
257
258 for index in 0..TQ_STAGE1_LEVELS / 2 {
262 let magnitude = 0.5 * (levels[TQ_STAGE1_LEVELS - 1 - index] - levels[index]);
263 levels[index] = -magnitude;
264 levels[TQ_STAGE1_LEVELS - 1 - index] = magnitude;
265 }
266 for boundary in 0..TQ_STAGE1_LEVELS - 1 {
267 boundaries[boundary] = 0.5 * (levels[boundary] + levels[boundary + 1]);
268 }
269
270 Self {
271 levels: levels.map(|level| level as f32),
272 boundaries: boundaries.map(|boundary| boundary as f32),
273 }
274 }
275
276 #[inline]
278 pub fn encode_coordinate(&self, value: f32) -> u8 {
279 let mut code = 0u8;
280 for &boundary in &self.boundaries {
281 code += u8::from(value > boundary);
282 }
283 code
284 }
285}
286
287#[derive(Debug, Clone)]
290pub struct TqCodec {
291 dim: usize,
292 padded_dim: usize,
293 stage1_rotation: TqRotation,
294 qjl_rotation: TqRotation,
295 codebook: TqCodebook,
296 qjl_scale: f32,
298 fingerprint: u64,
299}
300
301impl TqCodec {
302 pub fn new(dim: usize) -> Self {
303 assert!(dim > 0, "TQ codec requires a non-zero dimension");
304 let stage1_rotation = TqRotation::new(dim, TQ_STAGE1_SEED);
305 let padded_dim = stage1_rotation.padded_dim();
306 let qjl_rotation = TqRotation::new(padded_dim, TQ_QJL_SEED);
307 debug_assert_eq!(qjl_rotation.padded_dim(), padded_dim);
308 let codebook = TqCodebook::analytic(padded_dim);
309 let qjl_scale = (std::f64::consts::PI / 2.0).sqrt() as f32 / (padded_dim as f32).sqrt();
310 let fingerprint = tq_fingerprint(dim, padded_dim);
311 Self {
312 dim,
313 padded_dim,
314 stage1_rotation,
315 qjl_rotation,
316 codebook,
317 qjl_scale,
318 fingerprint,
319 }
320 }
321
322 #[inline]
323 pub fn dim(&self) -> usize {
324 self.dim
325 }
326
327 #[inline]
328 pub fn padded_dim(&self) -> usize {
329 self.padded_dim
330 }
331
332 #[inline]
334 pub fn code_size(&self) -> usize {
335 self.padded_dim / 2
336 }
337
338 #[inline]
340 pub fn fingerprint(&self) -> u64 {
341 self.fingerprint
342 }
343
344 pub fn estimated_memory_bytes(&self) -> usize {
347 2 * self.padded_dim * (size_of::<f32>() + size_of::<u32>()) + size_of::<TqCodebook>()
348 }
349
350 pub fn encode_into(
354 &self,
355 vector: &[f32],
356 nibbles: &mut [u8],
357 scratch: &mut TqEncodeScratch,
358 ) -> f32 {
359 self.encode_residual_into(vector, nibbles, scratch).1
360 }
361
362 pub fn encode_residual_into(
368 &self,
369 vector: &[f32],
370 nibbles: &mut [u8],
371 scratch: &mut TqEncodeScratch,
372 ) -> (f32, f32) {
373 assert_eq!(vector.len(), self.dim, "TQ encode dimension mismatch");
374 assert_eq!(nibbles.len(), self.padded_dim, "TQ nibble buffer mismatch");
375 let norm = crate::structures::simd::dot_product_f32(vector, vector, vector.len()).sqrt();
376 if !norm.is_finite() || norm <= 0.0 {
377 nibbles.fill(0);
378 return (0.0, 0.0);
379 }
380 scratch.normalized.clear();
381 scratch
382 .normalized
383 .extend(vector.iter().map(|value| value / norm));
384
385 scratch.rotated.resize(self.padded_dim, 0.0);
386 let (normalized, rotated, fwht) =
387 (&scratch.normalized, &mut scratch.rotated, &mut scratch.fwht);
388 self.stage1_rotation.apply(normalized, fwht, rotated);
389
390 scratch.residual.resize(self.padded_dim, 0.0);
392 let mut residual_norm_sq = 0.0f32;
393 for ((&value, nibble), residual_slot) in scratch
394 .rotated
395 .iter()
396 .zip(nibbles.iter_mut())
397 .zip(scratch.residual.iter_mut())
398 {
399 let code = self.codebook.encode_coordinate(value);
400 *nibble = code << 1;
401 let residual = value - self.codebook.levels[code as usize];
402 *residual_slot = residual;
403 residual_norm_sq += residual * residual;
404 }
405
406 scratch.rotated_residual.resize(self.padded_dim, 0.0);
408 let (residual, rotated_residual, fwht) = (
409 &scratch.residual,
410 &mut scratch.rotated_residual,
411 &mut scratch.fwht,
412 );
413 self.qjl_rotation.apply(residual, fwht, rotated_residual);
414 for (nibble, &rotated) in nibbles.iter_mut().zip(scratch.rotated_residual.iter()) {
415 *nibble |= u8::from(rotated >= 0.0);
416 }
417 (norm, residual_norm_sq.sqrt())
418 }
419}
420
421#[derive(Debug, Default)]
423pub struct TqEncodeScratch {
424 normalized: Vec<f32>,
425 rotated: Vec<f32>,
426 residual: Vec<f32>,
427 rotated_residual: Vec<f32>,
428 fwht: Vec<f32>,
429}
430
431fn tq_fingerprint(dim: usize, padded_dim: usize) -> u64 {
432 let mut hash = 0xcbf2_9ce4_8422_2325u64; let mut mix = |bytes: &[u8]| {
434 for &byte in bytes {
435 hash ^= u64::from(byte);
436 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
437 }
438 };
439 mix(b"hermes-tq");
440 mix(&TQ_CODEC_VERSION.to_le_bytes());
441 mix(&TQ_BITS.to_le_bytes());
442 mix(&(dim as u64).to_le_bytes());
443 mix(&(padded_dim as u64).to_le_bytes());
444 mix(&TQ_STAGE1_SEED.to_le_bytes());
445 mix(&TQ_QJL_SEED.to_le_bytes());
446 if hash == 0 { 1 } else { hash }
447}
448
449pub struct TqQueryPlan {
457 padded_dim: usize,
458 fingerprint: u64,
459 base_lut_i8: Vec<i8>,
460 qjl_lut_i8: Vec<i8>,
461 base_dequant: f32,
462 qjl_dequant: f32,
463 #[cfg(test)]
465 reference_luts: (Vec<f32>, Vec<f32>),
466}
467
468impl std::fmt::Debug for TqQueryPlan {
469 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470 formatter
471 .debug_struct("TqQueryPlan")
472 .field("padded_dim", &self.padded_dim)
473 .field("fingerprint", &self.fingerprint)
474 .finish()
475 }
476}
477
478impl TqQueryPlan {
479 pub fn build(codec: &TqCodec, query: &[f32]) -> Self {
480 assert_eq!(query.len(), codec.dim, "TQ query dimension mismatch");
481 let padded_dim = codec.padded_dim;
482 let norm = crate::structures::simd::dot_product_f32(query, query, query.len()).sqrt();
483 let inverse_norm = if norm.is_finite() && norm > 0.0 {
484 1.0 / norm
485 } else {
486 0.0
487 };
488 let normalized: Vec<f32> = query.iter().map(|value| value * inverse_norm).collect();
489 let mut fwht = Vec::with_capacity(padded_dim);
490 let mut rotated = vec![0.0f32; padded_dim];
491 codec
492 .stage1_rotation
493 .apply(&normalized, &mut fwht, &mut rotated);
494 let mut qjl_rotated = vec![0.0f32; padded_dim];
495 codec
496 .qjl_rotation
497 .apply(&rotated, &mut fwht, &mut qjl_rotated);
498
499 let mut base_lut = vec![0.0f32; padded_dim * 16];
500 let mut qjl_lut = vec![0.0f32; padded_dim * 16];
501 for dim in 0..padded_dim {
502 for nibble in 0..16 {
503 let level = codec.codebook.levels[nibble >> 1];
504 let sign = if nibble & 1 == 1 { 1.0 } else { -1.0 };
505 base_lut[dim * 16 + nibble] = rotated[dim] * level;
506 qjl_lut[dim * 16 + nibble] = sign * codec.qjl_scale * qjl_rotated[dim];
507 }
508 }
509 let (base_lut_i8, base_dequant) = quantize_lut(&base_lut);
510 let (qjl_lut_i8, qjl_dequant) = quantize_lut(&qjl_lut);
511 Self {
512 padded_dim,
513 fingerprint: codec.fingerprint,
514 base_lut_i8,
515 qjl_lut_i8,
516 base_dequant,
517 qjl_dequant,
518 #[cfg(test)]
519 reference_luts: (base_lut, qjl_lut),
520 }
521 }
522
523 #[inline]
524 pub fn padded_dim(&self) -> usize {
525 self.padded_dim
526 }
527
528 #[inline]
529 pub fn fingerprint(&self) -> u64 {
530 self.fingerprint
531 }
532
533 #[cfg(test)]
536 pub(crate) fn estimate_row(&self, nibbles: &[u8], gamma: f32) -> f32 {
537 debug_assert_eq!(nibbles.len(), self.padded_dim);
538 let (base_lut, qjl_lut) = &self.reference_luts;
539 let mut base = 0.0f32;
540 let mut qjl = 0.0f32;
541 for (dim, &nibble) in nibbles.iter().enumerate() {
542 base += base_lut[dim * 16 + nibble as usize];
543 qjl += qjl_lut[dim * 16 + nibble as usize];
544 }
545 base + gamma * qjl
546 }
547}
548
549fn quantize_lut(values: &[f32]) -> (Vec<i8>, f32) {
550 let max_abs = values
551 .iter()
552 .fold(0.0f32, |acc, &value| acc.max(value.abs()));
553 if !max_abs.is_finite() || max_abs <= 0.0 {
554 return (vec![0i8; values.len()], 0.0);
555 }
556 let quantize_scale = 127.0 / max_abs;
557 let quantized = values
558 .iter()
559 .map(|&value| (value * quantize_scale).round().clamp(-127.0, 127.0) as i8)
560 .collect();
561 (quantized, max_abs / 127.0)
562}
563
564pub fn tq_score_block(plan: &TqQueryPlan, block: &[u8], scores: &mut [f32; TQ_BLOCK_LANES]) {
568 debug_assert_eq!(block.len(), tq_block_bytes(plan.padded_dim / 2));
569 let (gamma_bytes, nibble_bytes) = block.split_at(TQ_BLOCK_LANES * size_of::<f32>());
570 let mut base = [0i32; TQ_BLOCK_LANES];
571 let mut qjl = [0i32; TQ_BLOCK_LANES];
572 lut16::accumulate_block(
573 &plan.base_lut_i8,
574 &plan.qjl_lut_i8,
575 nibble_bytes,
576 plan.padded_dim,
577 &mut base,
578 &mut qjl,
579 );
580 for lane in 0..TQ_BLOCK_LANES {
581 let gamma = f32::from_le_bytes(
582 gamma_bytes[lane * 4..lane * 4 + 4]
583 .try_into()
584 .expect("gamma slice is 4 bytes"),
585 );
586 scores[lane] =
587 base[lane] as f32 * plan.base_dequant + gamma * qjl[lane] as f32 * plan.qjl_dequant;
588 }
589}
590
591pub fn tq_pack_block(rows: &[&[u8]], gammas: &[f32], padded_dim: usize, output: &mut Vec<u8>) {
594 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
595 for lane in 0..TQ_BLOCK_LANES {
596 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
597 output.extend_from_slice(&gamma.to_le_bytes());
598 }
599 pack_nibble_rows(rows, padded_dim, output);
600}
601
602pub fn tq_pack_ivf_block(
604 rows: &[&[u8]],
605 scales: &[f32],
606 gammas: &[f32],
607 padded_dim: usize,
608 output: &mut Vec<u8>,
609) {
610 assert!(rows.len() <= TQ_BLOCK_LANES && rows.len() == gammas.len());
611 assert_eq!(scales.len(), gammas.len());
612 for lane in 0..TQ_BLOCK_LANES {
613 let scale = scales.get(lane).copied().unwrap_or(0.0);
614 output.extend_from_slice(&scale.to_le_bytes());
615 }
616 for lane in 0..TQ_BLOCK_LANES {
617 let gamma = gammas.get(lane).copied().unwrap_or(0.0);
618 output.extend_from_slice(&gamma.to_le_bytes());
619 }
620 pack_nibble_rows(rows, padded_dim, output);
621}
622
623fn pack_nibble_rows(rows: &[&[u8]], padded_dim: usize, output: &mut Vec<u8>) {
624 for dim in 0..padded_dim {
625 for byte_index in 0..TQ_BLOCK_LANES / 2 {
626 let low = rows.get(byte_index).map_or(0, |row| row[dim] & 0x0F);
627 let high = rows
628 .get(byte_index + TQ_BLOCK_LANES / 2)
629 .map_or(0, |row| row[dim] & 0x0F);
630 output.push(low | (high << 4));
631 }
632 }
633}
634
635pub fn tq_score_ivf_block(
639 plan: &TqQueryPlan,
640 block: &[u8],
641 cluster_dot: f32,
642 scores: &mut [f32; TQ_BLOCK_LANES],
643) {
644 debug_assert_eq!(block.len(), tq_ivf_block_bytes(plan.padded_dim() / 2));
645 let lane_f32 = TQ_BLOCK_LANES * size_of::<f32>();
646 let (scale_bytes, rest) = block.split_at(lane_f32);
647 let (gamma_bytes, nibble_bytes) = rest.split_at(lane_f32);
648 let mut base = [0i32; TQ_BLOCK_LANES];
649 let mut qjl = [0i32; TQ_BLOCK_LANES];
650 lut16::accumulate_block(
651 &plan.base_lut_i8,
652 &plan.qjl_lut_i8,
653 nibble_bytes,
654 plan.padded_dim,
655 &mut base,
656 &mut qjl,
657 );
658 for lane in 0..TQ_BLOCK_LANES {
659 let scale = f32::from_le_bytes(
660 scale_bytes[lane * 4..lane * 4 + 4]
661 .try_into()
662 .expect("scale slice is 4 bytes"),
663 );
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] = cluster_dot
670 + scale
671 * (base[lane] as f32 * plan.base_dequant
672 + gamma * qjl[lane] as f32 * plan.qjl_dequant);
673 }
674}
675
676mod lut16 {
677 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
678 use super::TQ_ACCUMULATE_CHUNK_DIMS;
679 use super::TQ_BLOCK_LANES;
680
681 pub(super) fn accumulate_block(
685 base_lut: &[i8],
686 qjl_lut: &[i8],
687 nibble_bytes: &[u8],
688 padded_dim: usize,
689 base: &mut [i32; TQ_BLOCK_LANES],
690 qjl: &mut [i32; TQ_BLOCK_LANES],
691 ) {
692 #[cfg(target_arch = "aarch64")]
693 {
694 unsafe { accumulate_block_neon(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl) }
696 return;
697 }
698 #[cfg(target_arch = "x86_64")]
699 {
700 if std::arch::is_x86_feature_detected!("ssse3") {
701 unsafe {
702 accumulate_block_ssse3(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl)
703 }
704 return;
705 }
706 }
707 #[allow(unreachable_code)]
708 accumulate_block_scalar(base_lut, qjl_lut, nibble_bytes, padded_dim, base, qjl);
709 }
710
711 pub(super) fn accumulate_block_scalar(
714 base_lut: &[i8],
715 qjl_lut: &[i8],
716 nibble_bytes: &[u8],
717 padded_dim: usize,
718 base: &mut [i32; TQ_BLOCK_LANES],
719 qjl: &mut [i32; TQ_BLOCK_LANES],
720 ) {
721 for dim in 0..padded_dim {
722 let row = &nibble_bytes[dim * 8..dim * 8 + 8];
723 let base_table = &base_lut[dim * 16..dim * 16 + 16];
724 let qjl_table = &qjl_lut[dim * 16..dim * 16 + 16];
725 for (lane, &byte) in row.iter().enumerate() {
726 let low = (byte & 0x0F) as usize;
727 let high = (byte >> 4) as usize;
728 base[lane] += i32::from(base_table[low]);
729 base[lane + 8] += i32::from(base_table[high]);
730 qjl[lane] += i32::from(qjl_table[low]);
731 qjl[lane + 8] += i32::from(qjl_table[high]);
732 }
733 }
734 }
735
736 #[cfg(target_arch = "aarch64")]
737 #[target_feature(enable = "neon")]
738 unsafe fn accumulate_block_neon(
739 base_lut: &[i8],
740 qjl_lut: &[i8],
741 nibble_bytes: &[u8],
742 padded_dim: usize,
743 base: &mut [i32; TQ_BLOCK_LANES],
744 qjl: &mut [i32; TQ_BLOCK_LANES],
745 ) {
746 use std::arch::aarch64::*;
747 unsafe {
748 let mask = vdup_n_u8(0x0F);
749 let mut base_lo_i32 = [vdupq_n_s32(0); 4];
750 let mut qjl_lo_i32 = [vdupq_n_s32(0); 4];
751 let mut dim = 0;
752 while dim < padded_dim {
753 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
754 let mut base_acc = [vdupq_n_s16(0); 2];
755 let mut qjl_acc = [vdupq_n_s16(0); 2];
756 while dim < chunk_end {
757 let row = vld1_u8(nibble_bytes.as_ptr().add(dim * 8));
758 let low = vand_u8(row, mask);
759 let high = vshr_n_u8::<4>(row);
760 let lanes = vcombine_u8(low, high);
761 let base_table = vld1q_s8(base_lut.as_ptr().add(dim * 16));
762 let qjl_table = vld1q_s8(qjl_lut.as_ptr().add(dim * 16));
763 let base_values =
764 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(base_table), lanes));
765 let qjl_values =
766 vreinterpretq_s8_u8(vqtbl1q_u8(vreinterpretq_u8_s8(qjl_table), lanes));
767 base_acc[0] = vaddw_s8(base_acc[0], vget_low_s8(base_values));
768 base_acc[1] = vaddw_s8(base_acc[1], vget_high_s8(base_values));
769 qjl_acc[0] = vaddw_s8(qjl_acc[0], vget_low_s8(qjl_values));
770 qjl_acc[1] = vaddw_s8(qjl_acc[1], vget_high_s8(qjl_values));
771 dim += 1;
772 }
773 for half in 0..2 {
774 base_lo_i32[half * 2] =
775 vaddw_s16(base_lo_i32[half * 2], vget_low_s16(base_acc[half]));
776 base_lo_i32[half * 2 + 1] =
777 vaddw_s16(base_lo_i32[half * 2 + 1], vget_high_s16(base_acc[half]));
778 qjl_lo_i32[half * 2] =
779 vaddw_s16(qjl_lo_i32[half * 2], vget_low_s16(qjl_acc[half]));
780 qjl_lo_i32[half * 2 + 1] =
781 vaddw_s16(qjl_lo_i32[half * 2 + 1], vget_high_s16(qjl_acc[half]));
782 }
783 }
784 for quarter in 0..4 {
785 vst1q_s32(base.as_mut_ptr().add(quarter * 4), base_lo_i32[quarter]);
786 vst1q_s32(qjl.as_mut_ptr().add(quarter * 4), qjl_lo_i32[quarter]);
787 }
788 }
789 }
790
791 #[cfg(target_arch = "x86_64")]
792 #[target_feature(enable = "ssse3")]
793 unsafe fn accumulate_block_ssse3(
794 base_lut: &[i8],
795 qjl_lut: &[i8],
796 nibble_bytes: &[u8],
797 padded_dim: usize,
798 base: &mut [i32; TQ_BLOCK_LANES],
799 qjl: &mut [i32; TQ_BLOCK_LANES],
800 ) {
801 use std::arch::x86_64::*;
802 unsafe {
803 let mask = _mm_set1_epi8(0x0F);
804 let zero = _mm_setzero_si128();
805 let mut base_i32 = [zero; 4];
806 let mut qjl_i32 = [zero; 4];
807 let mut dim = 0;
808 while dim < padded_dim {
809 let chunk_end = (dim + TQ_ACCUMULATE_CHUNK_DIMS).min(padded_dim);
810 let mut base_acc = [zero; 2];
811 let mut qjl_acc = [zero; 2];
812 while dim < chunk_end {
813 let row = _mm_loadl_epi64(nibble_bytes.as_ptr().add(dim * 8).cast());
814 let low = _mm_and_si128(row, mask);
815 let high = _mm_and_si128(_mm_srli_epi16(row, 4), mask);
816 let lanes = _mm_unpacklo_epi64(low, high);
817 let base_table = _mm_loadu_si128(base_lut.as_ptr().add(dim * 16).cast());
818 let qjl_table = _mm_loadu_si128(qjl_lut.as_ptr().add(dim * 16).cast());
819 let base_values = _mm_shuffle_epi8(base_table, lanes);
820 let qjl_values = _mm_shuffle_epi8(qjl_table, lanes);
821 let base_sign = _mm_cmpgt_epi8(zero, base_values);
823 let qjl_sign = _mm_cmpgt_epi8(zero, qjl_values);
824 base_acc[0] =
825 _mm_add_epi16(base_acc[0], _mm_unpacklo_epi8(base_values, base_sign));
826 base_acc[1] =
827 _mm_add_epi16(base_acc[1], _mm_unpackhi_epi8(base_values, base_sign));
828 qjl_acc[0] = _mm_add_epi16(qjl_acc[0], _mm_unpacklo_epi8(qjl_values, qjl_sign));
829 qjl_acc[1] = _mm_add_epi16(qjl_acc[1], _mm_unpackhi_epi8(qjl_values, qjl_sign));
830 dim += 1;
831 }
832 for half in 0..2 {
833 let base_sign = _mm_cmpgt_epi16(zero, base_acc[half]);
834 let qjl_sign = _mm_cmpgt_epi16(zero, qjl_acc[half]);
835 base_i32[half * 2] = _mm_add_epi32(
836 base_i32[half * 2],
837 _mm_unpacklo_epi16(base_acc[half], base_sign),
838 );
839 base_i32[half * 2 + 1] = _mm_add_epi32(
840 base_i32[half * 2 + 1],
841 _mm_unpackhi_epi16(base_acc[half], base_sign),
842 );
843 qjl_i32[half * 2] = _mm_add_epi32(
844 qjl_i32[half * 2],
845 _mm_unpacklo_epi16(qjl_acc[half], qjl_sign),
846 );
847 qjl_i32[half * 2 + 1] = _mm_add_epi32(
848 qjl_i32[half * 2 + 1],
849 _mm_unpackhi_epi16(qjl_acc[half], qjl_sign),
850 );
851 }
852 }
853 for quarter in 0..4 {
854 _mm_storeu_si128(base.as_mut_ptr().add(quarter * 4).cast(), base_i32[quarter]);
855 _mm_storeu_si128(qjl.as_mut_ptr().add(quarter * 4).cast(), qjl_i32[quarter]);
856 }
857 }
858 }
859}
860
861#[cfg(feature = "native")]
868pub struct TqFlatBuilder {
869 codec: std::sync::Arc<TqCodec>,
870 pub doc_ids: Vec<u32>,
871 pub ordinals: Vec<u16>,
872 pub codes: Vec<u8>,
873 pending_rows: Vec<Vec<u8>>,
874 pending_gammas: Vec<f32>,
875}
876
877#[cfg(feature = "native")]
878impl TqFlatBuilder {
879 pub fn new(codec: std::sync::Arc<TqCodec>) -> Self {
880 Self {
881 codec,
882 doc_ids: Vec::new(),
883 ordinals: Vec::new(),
884 codes: Vec::new(),
885 pending_rows: Vec::with_capacity(TQ_BLOCK_LANES),
886 pending_gammas: Vec::with_capacity(TQ_BLOCK_LANES),
887 }
888 }
889
890 #[inline]
891 pub fn codec(&self) -> &TqCodec {
892 &self.codec
893 }
894
895 #[inline]
896 pub fn len(&self) -> usize {
897 self.doc_ids.len()
898 }
899
900 #[inline]
901 pub fn is_empty(&self) -> bool {
902 self.doc_ids.is_empty()
903 }
904
905 pub fn add_batch(
908 &mut self,
909 labels: &[(u32, u16)],
910 vectors: &[f32],
911 ) -> Result<(), &'static str> {
912 use rayon::prelude::*;
913
914 let dim = self.codec.dim();
915 let expected = labels
916 .len()
917 .checked_mul(dim)
918 .ok_or("TQ input size overflow")?;
919 if vectors.len() != expected {
920 return Err("TQ vector and label matrices are inconsistent");
921 }
922 let padded_dim = self.codec.padded_dim();
923 let codec = std::sync::Arc::clone(&self.codec);
924 let rows: Vec<(Vec<u8>, f32)> = vectors
925 .par_chunks_exact(dim)
926 .map_init(TqEncodeScratch::default, |scratch, vector| {
927 let mut nibbles = vec![0u8; padded_dim];
928 let gamma = codec.encode_into(vector, &mut nibbles, scratch);
929 (nibbles, gamma)
930 })
931 .collect();
932 for (&(doc_id, ordinal), (nibbles, gamma)) in labels.iter().zip(rows) {
933 self.doc_ids.push(doc_id);
934 self.ordinals.push(ordinal);
935 self.pending_rows.push(nibbles);
936 self.pending_gammas.push(gamma);
937 if self.pending_rows.len() == TQ_BLOCK_LANES {
938 self.flush_block();
939 }
940 }
941 Ok(())
942 }
943
944 fn flush_block(&mut self) {
945 let rows: Vec<&[u8]> = self.pending_rows.iter().map(Vec::as_slice).collect();
946 tq_pack_block(
947 &rows,
948 &self.pending_gammas,
949 self.codec.padded_dim(),
950 &mut self.codes,
951 );
952 self.pending_rows.clear();
953 self.pending_gammas.clear();
954 }
955
956 pub fn finish(&mut self) {
958 if !self.pending_rows.is_empty() {
959 self.flush_block();
960 }
961 debug_assert_eq!(
962 self.codes.len(),
963 tq_codes_column_len(self.doc_ids.len(), self.codec.code_size())
964 );
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971
972 fn seeded_unit_vector(dim: usize, seed: u64) -> Vec<f32> {
973 let mut state = seed;
975 let mut values: Vec<f32> = (0..dim)
976 .map(|_| {
977 let a = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
978 let b = (splitmix64(&mut state) >> 11) as f64 / (1u64 << 53) as f64;
979 let gaussian = (-2.0 * (1.0 - a).max(f64::MIN_POSITIVE).ln()).sqrt()
980 * (2.0 * std::f64::consts::PI * b).cos();
981 gaussian as f32
982 })
983 .collect();
984 let norm = values.iter().map(|v| v * v).sum::<f32>().sqrt();
985 values.iter_mut().for_each(|v| *v /= norm);
986 values
987 }
988
989 #[test]
990 fn rotation_is_orthonormal_and_deterministic() {
991 let rotation = TqRotation::new(100, 42);
992 assert_eq!(rotation.padded_dim(), 128);
993 let input = seeded_unit_vector(100, 7);
994 let mut fwht = Vec::new();
995 let mut output = vec![0.0f32; 128];
996 rotation.apply(&input, &mut fwht, &mut output);
997 let norm: f32 = output.iter().map(|v| v * v).sum();
998 assert!(
999 (norm - 1.0).abs() < 1e-5,
1000 "rotation must preserve norm, got {norm}"
1001 );
1002
1003 let mut second = vec![0.0f32; 128];
1004 TqRotation::new(100, 42).apply(&input, &mut fwht, &mut second);
1005 assert_eq!(output, second, "rotation must be deterministic");
1006
1007 let other = seeded_unit_vector(100, 8);
1009 let mut other_rotated = vec![0.0f32; 128];
1010 rotation.apply(&other, &mut fwht, &mut other_rotated);
1011 let dot_before: f32 = input.iter().zip(&other).map(|(a, b)| a * b).sum();
1012 let dot_after: f32 = output.iter().zip(&other_rotated).map(|(a, b)| a * b).sum();
1013 assert!(
1014 (dot_before - dot_after).abs() < 1e-4,
1015 "rotation must preserve inner products: {dot_before} vs {dot_after}"
1016 );
1017 }
1018
1019 #[test]
1020 fn analytic_codebook_is_symmetric_and_monotonic() {
1021 for padded_dim in [8, 128, 1024] {
1022 let codebook = TqCodebook::analytic(padded_dim);
1023 let levels = &codebook.levels;
1024 for pair in levels.windows(2) {
1025 assert!(pair[0] < pair[1], "levels must be strictly increasing");
1026 }
1027 for index in 0..TQ_STAGE1_LEVELS / 2 {
1028 assert_eq!(
1029 levels[index],
1030 -levels[TQ_STAGE1_LEVELS - 1 - index],
1031 "levels must be exactly symmetric for P={padded_dim}: {levels:?}"
1032 );
1033 }
1034 assert!(levels[TQ_STAGE1_LEVELS - 1] < 1.0);
1035 let scale = 1.0 / (padded_dim as f32).sqrt();
1038 assert!(
1039 levels[TQ_STAGE1_LEVELS - 1] < 6.0 * scale,
1040 "top level {} is implausibly large for P={padded_dim}",
1041 levels[TQ_STAGE1_LEVELS - 1]
1042 );
1043 }
1044 }
1045
1046 #[test]
1047 fn encode_coordinate_matches_nearest_level() {
1048 let codebook = TqCodebook::analytic(256);
1049 for step in -1000i32..=1000 {
1050 let value = step as f32 / 1000.0;
1051 let code = codebook.encode_coordinate(value) as usize;
1052 let nearest = codebook
1053 .levels
1054 .iter()
1055 .enumerate()
1056 .min_by(|a, b| (a.1 - value).abs().total_cmp(&(b.1 - value).abs()))
1057 .unwrap()
1058 .0;
1059 assert_eq!(
1060 code, nearest,
1061 "value {value} coded {code}, nearest {nearest}"
1062 );
1063 }
1064 }
1065
1066 #[test]
1067 fn estimator_is_unbiased_and_tight() {
1068 let dim = 96;
1069 let codec = TqCodec::new(dim);
1070 let mut scratch = TqEncodeScratch::default();
1071 let mut nibbles = vec![0u8; codec.padded_dim()];
1072
1073 let pairs = 512;
1074 let mut signed_error_sum = 0.0f64;
1075 let mut squared_error_sum = 0.0f64;
1076 let mut stage1_signed_error_sum = 0.0f64;
1077 for pair in 0..pairs {
1078 let vector = seeded_unit_vector(dim, 1000 + pair);
1079 let query = seeded_unit_vector(dim, 900_000 + pair);
1080 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1081 let plan = TqQueryPlan::build(&codec, &query);
1082 let estimate = plan.estimate_row(&nibbles, gamma);
1083 let stage1_only = plan.estimate_row(&nibbles, 0.0);
1084 let truth: f32 = vector.iter().zip(&query).map(|(a, b)| a * b).sum();
1085 signed_error_sum += f64::from(estimate - truth);
1086 squared_error_sum += f64::from(estimate - truth).powi(2);
1087 stage1_signed_error_sum += f64::from(stage1_only - truth);
1088 }
1089 let mean_error = signed_error_sum / pairs as f64;
1090 let rmse = (squared_error_sum / pairs as f64).sqrt();
1091 let stage1_mean_error = stage1_signed_error_sum / pairs as f64;
1092 assert!(
1093 mean_error.abs() < 3e-3,
1094 "QJL-corrected estimator must be unbiased: mean error {mean_error}"
1095 );
1096 assert!(rmse < 0.05, "estimator RMSE too large: {rmse}");
1097 assert!(
1098 mean_error.abs() <= stage1_mean_error.abs() + 1e-4,
1099 "QJL correction must not increase bias: {mean_error} vs stage-1 {stage1_mean_error}"
1100 );
1101 }
1102
1103 #[test]
1104 fn block_scoring_matches_row_estimates() {
1105 let dim = 100; let codec = TqCodec::new(dim);
1107 let mut scratch = TqEncodeScratch::default();
1108 let query = seeded_unit_vector(dim, 3);
1109 let plan = TqQueryPlan::build(&codec, &query);
1110
1111 let lanes = 13; let mut rows = Vec::new();
1113 let mut gammas = Vec::new();
1114 for lane in 0..lanes {
1115 let vector = seeded_unit_vector(dim, 50 + lane as u64);
1116 let mut nibbles = vec![0u8; codec.padded_dim()];
1117 let gamma = codec.encode_into(&vector, &mut nibbles, &mut scratch);
1118 rows.push(nibbles);
1119 gammas.push(gamma);
1120 }
1121 let row_refs: Vec<&[u8]> = rows.iter().map(Vec::as_slice).collect();
1122 let mut block = Vec::new();
1123 tq_pack_block(&row_refs, &gammas, codec.padded_dim(), &mut block);
1124 assert_eq!(block.len(), tq_block_bytes(codec.code_size()));
1125
1126 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1127 tq_score_block(&plan, &block, &mut scores);
1128 for lane in 0..lanes {
1129 let expected = plan.estimate_row(&rows[lane], gammas[lane]);
1130 assert!(
1132 (scores[lane] - expected).abs() < 0.02,
1133 "lane {lane}: block score {} vs row estimate {expected}",
1134 scores[lane]
1135 );
1136 }
1137 }
1138
1139 #[test]
1140 fn simd_accumulation_matches_scalar_reference_exactly() {
1141 let padded_dim = 192; let mut state = 99u64;
1143 let mut base_lut = vec![0i8; padded_dim * 16];
1144 let mut qjl_lut = vec![0i8; padded_dim * 16];
1145 for value in base_lut.iter_mut().chain(qjl_lut.iter_mut()) {
1146 *value = (splitmix64(&mut state) as i32 % 255 - 127) as i8;
1147 }
1148 let mut nibble_bytes = vec![0u8; padded_dim * 8];
1149 for byte in nibble_bytes.iter_mut() {
1150 *byte = splitmix64(&mut state) as u8;
1151 }
1152
1153 let mut base_reference = [0i32; TQ_BLOCK_LANES];
1154 let mut qjl_reference = [0i32; TQ_BLOCK_LANES];
1155 lut16::accumulate_block_scalar(
1156 &base_lut,
1157 &qjl_lut,
1158 &nibble_bytes,
1159 padded_dim,
1160 &mut base_reference,
1161 &mut qjl_reference,
1162 );
1163 let mut base_dispatch = [0i32; TQ_BLOCK_LANES];
1164 let mut qjl_dispatch = [0i32; TQ_BLOCK_LANES];
1165 lut16::accumulate_block(
1166 &base_lut,
1167 &qjl_lut,
1168 &nibble_bytes,
1169 padded_dim,
1170 &mut base_dispatch,
1171 &mut qjl_dispatch,
1172 );
1173 assert_eq!(
1174 base_reference, base_dispatch,
1175 "base sums must match exactly"
1176 );
1177 assert_eq!(qjl_reference, qjl_dispatch, "qjl sums must match exactly");
1178 }
1179
1180 #[test]
1181 fn fingerprint_pins_codec_constants() {
1182 let codec = TqCodec::new(768);
1183 assert_eq!(codec.padded_dim(), 1024);
1184 assert_eq!(codec.code_size(), 512);
1185 assert_ne!(codec.fingerprint(), 0);
1186 assert_eq!(codec.fingerprint(), TqCodec::new(768).fingerprint());
1187 assert_ne!(codec.fingerprint(), TqCodec::new(769).fingerprint());
1188 assert_eq!(
1192 codec.fingerprint(),
1193 GOLDEN_FINGERPRINT_768,
1194 "TQ fingerprint for dim 768 changed; existing segments would be \
1195 rejected. Bump TQ_CODEC_VERSION deliberately instead."
1196 );
1197 }
1198
1199 const GOLDEN_FINGERPRINT_768: u64 = 4674713535241508736;
1200
1201 #[cfg(feature = "native")]
1202 #[test]
1203 fn builder_packs_blocks_in_input_order() {
1204 let dim = 32;
1205 let codec = std::sync::Arc::new(TqCodec::new(dim));
1206 let mut builder = TqFlatBuilder::new(std::sync::Arc::clone(&codec));
1207 let count = 37; let labels: Vec<(u32, u16)> = (0..count).map(|i| (i as u32, (i % 3) as u16)).collect();
1209 let mut vectors = Vec::new();
1210 for i in 0..count {
1211 vectors.extend(seeded_unit_vector(dim, 7_000 + i as u64));
1212 }
1213 builder.add_batch(&labels, &vectors).unwrap();
1214 builder.finish();
1215 assert_eq!(builder.doc_ids.len(), count);
1216 assert_eq!(
1217 builder.codes.len(),
1218 tq_codes_column_len(count, codec.code_size())
1219 );
1220
1221 let query = seeded_unit_vector(dim, 1);
1223 let plan = TqQueryPlan::build(&codec, &query);
1224 let block_bytes = tq_block_bytes(codec.code_size());
1225 let mut scratch = TqEncodeScratch::default();
1226 let mut scores = [0.0f32; TQ_BLOCK_LANES];
1227 for index in 0..count {
1228 let block = &builder.codes[(index / TQ_BLOCK_LANES) * block_bytes..][..block_bytes];
1229 tq_score_block(&plan, block, &mut scores);
1230 let mut nibbles = vec![0u8; codec.padded_dim()];
1231 let gamma = codec.encode_into(
1232 &vectors[index * dim..(index + 1) * dim],
1233 &mut nibbles,
1234 &mut scratch,
1235 );
1236 let expected = plan.estimate_row(&nibbles, gamma);
1237 assert!(
1238 (scores[index % TQ_BLOCK_LANES] - expected).abs() < 0.02,
1239 "vector {index} scored {} expected {expected}",
1240 scores[index % TQ_BLOCK_LANES]
1241 );
1242 }
1243 }
1244}