1use std::cell::RefCell;
7use std::ops::Range;
8
9use rand::{Rng, SeedableRng};
10
11use super::quantized_dot::QuantizedDotKernel;
12use super::{
13 AhCodebook, AhQuery, DEFAULT_ANISOTROPIC_THRESHOLD, FastScanQuery, MAX_SCANN_TREE_LEVELS,
14 ScannEncoding, ScannFormatError, ScannResult, ScannRoutingLevel, ScannTrainedArtifact,
15 ScannTrainedArtifactView,
16};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct RoutedLeaf {
20 pub leaf: u32,
21 pub squared_distance: f32,
22}
23
24type RoutingCandidate = (u32, f32);
28
29#[derive(Clone, Debug, Default)]
30pub struct RoutingScratch {
31 active: Vec<RoutingCandidate>,
32 next: Vec<RoutingCandidate>,
33 scaled_query: Vec<f32>,
35}
36
37thread_local! {
38 static ROUTING_SCRATCH: RefCell<RoutingScratch> = RefCell::new(RoutingScratch::default());
43}
44
45fn with_routing_scratch<T>(scope: impl FnOnce(&mut RoutingScratch) -> T) -> T {
46 ROUTING_SCRATCH.with(|cell| match cell.try_borrow_mut() {
47 Ok(mut scratch) => scope(&mut scratch),
48 Err(_) => scope(&mut RoutingScratch::default()),
51 })
52}
53
54#[derive(Clone, Debug, Default)]
58pub struct FloatEncodeScratch {
59 routing: RoutingScratch,
60 routed: Vec<RoutedLeaf>,
61 residual: Vec<f32>,
62 codes: Vec<u8>,
63 ah: super::AhEncodeScratch,
64}
65
66const QUERY_INTERMEDIATE_ROUTING_BEAM: usize = 64;
70
71#[derive(Clone, Debug, PartialEq)]
72pub struct RoutingTraining {
73 pub tree: FloatRoutingTree,
74 pub assignments: Vec<u32>,
76 pub stats: RoutingTrainingStats,
78}
79
80#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84pub struct RoutingTrainingStats {
85 pub splits: usize,
86 pub max_split_clusters: usize,
87 pub max_depth: usize,
88 pub distance_evaluations: u64,
89 pub assignment_distance_evaluations: u64,
91}
92
93#[derive(Clone, Debug, PartialEq)]
94pub struct FloatRoutingTree {
95 dimension: usize,
96 levels: Vec<Vec<f32>>,
97 child_offsets: Vec<Vec<u32>>,
98}
99
100#[derive(Clone, Debug, PartialEq)]
103pub struct FloatScannModel {
104 pub routing: FloatRoutingTree,
105 pub codebook: AhCodebook,
106 anisotropic_threshold: f32,
107}
108
109#[derive(Clone, Debug, PartialEq)]
110struct QuantizedFloatRoutingLevel {
111 centroid_count: usize,
112 centroid_codes: Range<usize>,
113 minimums: Vec<f32>,
114 steps: Vec<f32>,
115 code_norms: Vec<f32>,
119 child_offsets: Vec<u32>,
120}
121
122struct ScaledLevelQuery {
124 constant: f32,
126}
127
128#[derive(Clone, Debug, PartialEq)]
132pub struct QuantizedFloatScannModel {
133 dimension: usize,
134 num_leaves: usize,
135 artifact_id: u64,
136 artifact_len: usize,
137 levels: Vec<QuantizedFloatRoutingLevel>,
138 codebook: AhCodebook,
139 anisotropic_threshold: f32,
140 kernel: QuantizedDotKernel,
142}
143
144#[derive(Clone, Copy, Debug)]
147pub struct QuantizedFloatScannModelView<'a> {
148 model: &'a QuantizedFloatScannModel,
149 artifact_bytes: &'a [u8],
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct EncodedFloatVector {
154 pub leaf: u32,
155 pub codes: Vec<u8>,
157}
158
159#[derive(Clone, Debug, PartialEq)]
160pub struct FloatScannQuery {
161 routed_leaves: Vec<u32>,
162 centroid_dots: Vec<f32>,
163 ah: AhQuery,
164 fast_scan: FastScanQuery,
167}
168
169impl FloatScannModel {
170 pub fn from_artifact(artifact: &ScannTrainedArtifact) -> ScannResult<Self> {
175 artifact.validate()?;
176 let dimensions_per_block = match artifact.config.encoding {
177 ScannEncoding::AsymmetricHash {
178 dimensions_per_block,
179 bits_per_code: 4,
180 } => usize::from(dimensions_per_block),
181 _ => {
182 return Err(ScannFormatError::new(
183 "float ScaNN model requires a 4-bit asymmetric-hash artifact",
184 ));
185 }
186 };
187 let codebook_artifact = artifact.ah_codebook.as_ref().ok_or_else(|| {
188 ScannFormatError::new("float ScaNN artifact is missing its AH codebook")
189 })?;
190 if usize::from(codebook_artifact.dimensions_per_block) != dimensions_per_block {
191 return Err(ScannFormatError::new(
192 "ScaNN routing encoding and AH codebook block geometry differ",
193 ));
194 }
195 let dimension = artifact.config.dimension as usize;
196 Ok(Self {
197 routing: FloatRoutingTree::from_quantized_levels(&artifact.levels, dimension)?,
198 codebook: AhCodebook::from_artifact(dimension, codebook_artifact)?,
199 anisotropic_threshold: DEFAULT_ANISOTROPIC_THRESHOLD,
200 })
201 }
202
203 #[allow(clippy::too_many_arguments)]
204 pub fn train(
205 data: &[f32],
206 points: usize,
207 dimension: usize,
208 level_counts: &[u32],
209 dimensions_per_block: usize,
210 iterations: usize,
211 seed: u64,
212 anisotropic_threshold: f32,
213 ) -> ScannResult<(Self, Vec<EncodedFloatVector>)> {
214 let model = Self::train_model(
215 data,
216 points,
217 dimension,
218 level_counts,
219 dimensions_per_block,
220 iterations,
221 seed,
222 anisotropic_threshold,
223 )?;
224 let encoded = data
225 .chunks_exact(dimension)
226 .map(|vector| model.encode(vector))
227 .collect::<ScannResult<Vec<_>>>()?;
228 Ok((model, encoded))
229 }
230
231 #[allow(clippy::too_many_arguments)]
235 pub fn train_model(
236 data: &[f32],
237 points: usize,
238 dimension: usize,
239 level_counts: &[u32],
240 dimensions_per_block: usize,
241 iterations: usize,
242 seed: u64,
243 anisotropic_threshold: f32,
244 ) -> ScannResult<Self> {
245 if !anisotropic_threshold.is_finite() || !(0.0..1.0).contains(&anisotropic_threshold) {
246 return Err(ScannFormatError::new(
247 "ScaNN anisotropic threshold must be in [0, 1)",
248 ));
249 }
250 let routing = train_routing_tree(data, points, dimension, level_counts, iterations, seed)?;
251 let codebook = AhCodebook::train_from_assigned_vectors(
252 data,
253 &routing.assignments,
254 routing.tree.leaf_centroids(),
255 points,
256 dimension,
257 dimensions_per_block,
258 iterations,
259 seed.wrapping_add(0xd1b5_4a32_d192_ed03),
260 )?;
261 Ok(Self {
262 routing: routing.tree,
263 codebook,
264 anisotropic_threshold,
265 })
266 }
267
268 pub fn encode(&self, vector: &[f32]) -> ScannResult<EncodedFloatVector> {
269 let mut scratch = FloatEncodeScratch::default();
270 let (leaf, codes) = self.encode_with_scratch(vector, &mut scratch)?;
271 Ok(EncodedFloatVector {
272 leaf,
273 codes: codes.to_vec(),
274 })
275 }
276
277 pub fn encode_with_scratch<'a>(
278 &self,
279 vector: &[f32],
280 scratch: &'a mut FloatEncodeScratch,
281 ) -> ScannResult<(u32, &'a [u8])> {
282 if vector.len() != self.routing.dimension {
283 return Err(ScannFormatError::new(
284 "ScaNN vector dimension does not match trained model",
285 ));
286 }
287 let FloatEncodeScratch {
288 routing,
289 routed,
290 residual,
291 codes,
292 ah,
293 } = scratch;
294 self.routing
295 .route_with_scratch(vector, 1, routing, routed)?;
296 let leaf = routed[0].leaf;
297 let centroid = &self.routing.leaf_centroids()
298 [leaf as usize * self.routing.dimension..(leaf as usize + 1) * self.routing.dimension];
299 residual.resize(self.routing.dimension, 0.0);
300 for ((value, &original), ¢er) in residual.iter_mut().zip(vector).zip(centroid) {
301 *value = original - center;
302 }
303 codes.resize(self.codebook.blocks(), 0);
304 self.codebook.encode_with_scratch(
305 residual,
306 vector,
307 self.anisotropic_threshold,
308 codes,
309 ah,
310 )?;
311 Ok((leaf, codes))
312 }
313
314 pub fn prepare_query(&self, query: &[f32], probes: usize) -> ScannResult<FloatScannQuery> {
315 let routed = self.routing.route(query, probes)?;
316 let mut routed_leaves = Vec::with_capacity(routed.len());
317 let mut centroid_dots = Vec::with_capacity(routed.len());
318 for routed_leaf in routed {
319 let centroid = &self.routing.leaf_centroids()[routed_leaf.leaf as usize
320 * self.routing.dimension
321 ..(routed_leaf.leaf as usize + 1) * self.routing.dimension];
322 routed_leaves.push(routed_leaf.leaf);
323 centroid_dots.push(crate::structures::simd::dot_product_f32(
324 query,
325 centroid,
326 query.len(),
327 ));
328 }
329 Ok(FloatScannQuery::new(
330 routed_leaves,
331 centroid_dots,
332 self.codebook.query_dot_product(query)?,
333 ))
334 }
335
336 pub fn anisotropic_threshold(&self) -> f32 {
337 self.anisotropic_threshold
338 }
339}
340
341impl QuantizedFloatScannModel {
342 pub fn from_artifact_view(artifact: &ScannTrainedArtifactView<'_>) -> ScannResult<Self> {
343 let dimensions_per_block = match artifact.config.encoding {
344 ScannEncoding::AsymmetricHash {
345 dimensions_per_block,
346 bits_per_code: 4,
347 } => usize::from(dimensions_per_block),
348 _ => {
349 return Err(ScannFormatError::new(
350 "quantized float ScaNN model requires a 4-bit AH artifact",
351 ));
352 }
353 };
354 let dimension = artifact.config.dimension as usize;
355 let codebook_ref = artifact.ah_codebook().ok_or_else(|| {
356 ScannFormatError::new("float ScaNN artifact is missing its AH codebook")
357 })?;
358 if usize::from(codebook_ref.dimensions_per_block) != dimensions_per_block {
359 return Err(ScannFormatError::new(
360 "ScaNN routing encoding and AH codebook block geometry differ",
361 ));
362 }
363 let codebook = AhCodebook::from_artifact_ref(dimension, codebook_ref)?;
364 let mut levels = Vec::with_capacity(artifact.level_count());
365 for index in 0..artifact.level_count() {
366 let level = artifact
367 .level(index)
368 .ok_or_else(|| ScannFormatError::new("ScaNN artifact routing level disappeared"))?;
369 let centroid_codes = artifact.level_centroid_codes_range(index).ok_or_else(|| {
370 ScannFormatError::new("ScaNN artifact centroid range disappeared")
371 })?;
372 let steps: Vec<f32> = level.steps().collect();
373 let code_norms = quantized_code_norms(&steps, level.centroid_codes, dimension)?;
374 levels.push(QuantizedFloatRoutingLevel {
375 centroid_count: level.centroid_count as usize,
376 centroid_codes,
377 minimums: level.minimums().collect(),
378 steps,
379 code_norms,
380 child_offsets: level.child_offsets().collect(),
381 });
382 }
383 let model = Self {
384 dimension,
385 num_leaves: artifact.config.num_leaves as usize,
386 artifact_id: artifact.artifact_id,
387 artifact_len: artifact.bytes().len(),
388 levels,
389 codebook,
390 anisotropic_threshold: DEFAULT_ANISOTROPIC_THRESHOLD,
391 kernel: QuantizedDotKernel::resolve(),
392 };
393 model.validate_metadata()?;
394 Ok(model)
395 }
396
397 pub fn view<'a>(
401 &'a self,
402 artifact_bytes: &'a [u8],
403 ) -> ScannResult<QuantizedFloatScannModelView<'a>> {
404 let stored_id = artifact_bytes
405 .get(12..20)
406 .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
407 .map(u64::from_le_bytes);
408 if artifact_bytes.len() != self.artifact_len || stored_id != Some(self.artifact_id) {
409 return Err(ScannFormatError::new(
410 "quantized float ScaNN model was paired with a different artifact mapping",
411 ));
412 }
413 Ok(QuantizedFloatScannModelView {
414 model: self,
415 artifact_bytes,
416 })
417 }
418
419 pub fn estimated_memory_bytes(&self) -> usize {
420 self.levels
421 .iter()
422 .fold(self.codebook.estimated_memory_bytes(), |total, level| {
423 total
424 .saturating_add(level.minimums.len() * std::mem::size_of::<f32>())
425 .saturating_add(level.steps.len() * std::mem::size_of::<f32>())
426 .saturating_add(level.code_norms.len() * std::mem::size_of::<f32>())
427 .saturating_add(level.child_offsets.len() * std::mem::size_of::<u32>())
428 })
429 }
430
431 fn validate_metadata(&self) -> ScannResult<()> {
432 if self.dimension == 0
433 || self.levels.is_empty()
434 || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
435 || self.levels.last().map(|level| level.centroid_count) != Some(self.num_leaves)
436 || self.codebook.dimension() != self.dimension
437 {
438 return Err(ScannFormatError::new(
439 "invalid quantized float ScaNN model metadata",
440 ));
441 }
442 for (index, level) in self.levels.iter().enumerate() {
443 if level.minimums.len() != self.dimension
444 || level.steps.len() != self.dimension
445 || level.code_norms.len() != level.centroid_count
446 || level.centroid_codes.len() != level.centroid_count.saturating_mul(self.dimension)
447 || level.centroid_codes.end > self.artifact_len
448 {
449 return Err(ScannFormatError::new(format!(
450 "invalid quantized float ScaNN routing level {index}",
451 )));
452 }
453 if index + 1 == self.levels.len() {
454 if !level.child_offsets.is_empty() {
455 return Err(ScannFormatError::new(
456 "quantized ScaNN leaf level must not have children",
457 ));
458 }
459 } else if level.child_offsets.len() != level.centroid_count + 1
460 || level.child_offsets.first() != Some(&0)
461 || level.child_offsets.last().copied()
462 != Some(self.levels[index + 1].centroid_count as u32)
463 {
464 return Err(ScannFormatError::new(format!(
465 "invalid quantized float ScaNN child directory at level {index}",
466 )));
467 }
468 }
469 Ok(())
470 }
471}
472
473impl QuantizedFloatScannModelView<'_> {
474 pub fn encode(&self, vector: &[f32]) -> ScannResult<EncodedFloatVector> {
475 let mut scratch = FloatEncodeScratch::default();
476 let (leaf, codes) = self.encode_with_scratch(vector, &mut scratch)?;
477 Ok(EncodedFloatVector {
478 leaf,
479 codes: codes.to_vec(),
480 })
481 }
482
483 pub fn encode_with_scratch<'a>(
484 &self,
485 vector: &[f32],
486 scratch: &'a mut FloatEncodeScratch,
487 ) -> ScannResult<(u32, &'a [u8])> {
488 if vector.len() != self.model.dimension {
489 return Err(ScannFormatError::new(
490 "ScaNN vector dimension does not match trained model",
491 ));
492 }
493 let FloatEncodeScratch {
494 routing,
495 routed,
496 residual,
497 codes: encoded,
498 ah,
499 } = scratch;
500 self.route_with_scratch(vector, 1, routing, routed)?;
501 let leaf = routed[0].leaf;
502 let level = self
503 .model
504 .levels
505 .last()
506 .expect("validated non-empty levels");
507 let codes = self.level_codes(level);
508 let row = &codes
509 [leaf as usize * self.model.dimension..(leaf as usize + 1) * self.model.dimension];
510 residual.resize(self.model.dimension, 0.0);
511 for (coordinate, (&value, residual)) in vector.iter().zip(residual.iter_mut()).enumerate() {
512 *residual = value
513 - (level.minimums[coordinate]
514 + level.steps[coordinate] * f32::from(row[coordinate]));
515 }
516 encoded.resize(self.model.codebook.blocks(), 0);
517 self.model.codebook.encode_with_scratch(
518 residual,
519 vector,
520 self.model.anisotropic_threshold,
521 encoded,
522 ah,
523 )?;
524 Ok((leaf, encoded))
525 }
526
527 pub fn prepare_query(&self, query: &[f32], probes: usize) -> ScannResult<FloatScannQuery> {
528 with_routing_scratch(|scratch| {
529 let mut routed = Vec::with_capacity(probes);
530 self.route_with_scratch(query, probes, scratch, &mut routed)?;
531 let level = self
532 .model
533 .levels
534 .last()
535 .expect("validated non-empty levels");
536 let codes = self.level_codes(level);
537 let dimension = self.model.dimension;
541 let scaled = &mut scratch.scaled_query;
542 scaled.clear();
543 scaled.extend(
544 query
545 .iter()
546 .zip(&level.steps)
547 .map(|(&value, &step)| value.algebraic_mul(step)),
548 );
549 let offset = query
550 .iter()
551 .zip(&level.minimums)
552 .fold(0.0f64, |acc, (&value, &minimum)| {
553 acc + f64::from(value) * f64::from(minimum)
554 }) as f32;
555 let mut routed_leaves = Vec::with_capacity(routed.len());
556 let mut centroid_dots = Vec::with_capacity(routed.len());
557 for routed_leaf in &routed {
558 let leaf = routed_leaf.leaf as usize;
559 let row = &codes[leaf * dimension..(leaf + 1) * dimension];
560 routed_leaves.push(routed_leaf.leaf);
561 centroid_dots.push(offset.algebraic_add(self.model.kernel.dot(scaled, row)));
562 }
563 Ok(FloatScannQuery::new(
564 routed_leaves,
565 centroid_dots,
566 self.model.codebook.query_dot_product(query)?,
567 ))
568 })
569 }
570
571 pub fn anisotropic_threshold(&self) -> f32 {
572 self.model.anisotropic_threshold
573 }
574
575 #[cfg(test)]
576 fn route(&self, query: &[f32], probes: usize) -> ScannResult<Vec<RoutedLeaf>> {
577 let mut output = Vec::with_capacity(probes);
578 with_routing_scratch(|scratch| {
579 self.route_with_scratch(query, probes, scratch, &mut output)
580 })?;
581 Ok(output)
582 }
583
584 fn route_with_scratch(
585 &self,
586 query: &[f32],
587 probes: usize,
588 scratch: &mut RoutingScratch,
589 output: &mut Vec<RoutedLeaf>,
590 ) -> ScannResult<()> {
591 if query.len() != self.model.dimension || query.iter().any(|value| !value.is_finite()) {
592 return Err(ScannFormatError::new(
593 "ScaNN routing query has the wrong dimension or non-finite values",
594 ));
595 }
596 if probes == 0 {
597 return Err(ScannFormatError::new(
598 "ScaNN routing probes must be positive",
599 ));
600 }
601 scratch.active.clear();
602 let scaled = Self::scale_query(&self.model.levels[0], query, &mut scratch.scaled_query);
603 self.score_range_into(
604 &self.model.levels[0],
605 &scaled,
606 &scratch.scaled_query,
607 0,
608 self.model.levels[0].centroid_count,
609 &mut scratch.active,
610 );
611 if self.model.levels.len() == 1 {
612 keep_best(&mut scratch.active, probes);
613 } else {
614 sort_routing_candidates(&mut scratch.active);
615 let initial_width = super::routing_prefix_for_child_coverage(
616 &scratch.active,
617 &self.model.levels[0].child_offsets,
618 intermediate_routing_beam(probes),
619 probes,
620 |candidate| candidate.0 as usize,
621 );
622 scratch.active.truncate(initial_width);
623 }
624 for level_index in 1..self.model.levels.len() {
625 let level = &self.model.levels[level_index];
626 let offsets = &self.model.levels[level_index - 1].child_offsets;
627 let scaled = Self::scale_query(level, query, &mut scratch.scaled_query);
628 scratch.next.clear();
629 for &(parent, _) in &scratch.active {
630 let parent = parent as usize;
631 self.score_range_into(
632 level,
633 &scaled,
634 &scratch.scaled_query,
635 offsets[parent] as usize,
636 offsets[parent + 1] as usize,
637 &mut scratch.next,
638 );
639 }
640 if level_index + 1 == self.model.levels.len() {
641 keep_best(&mut scratch.next, probes);
642 } else {
643 sort_routing_candidates(&mut scratch.next);
644 let width = super::routing_prefix_for_child_coverage(
645 &scratch.next,
646 &level.child_offsets,
647 intermediate_routing_beam(probes),
648 probes,
649 |candidate| candidate.0 as usize,
650 );
651 scratch.next.truncate(width);
652 }
653 std::mem::swap(&mut scratch.active, &mut scratch.next);
654 }
655 output.clear();
656 output.extend(
657 scratch
658 .active
659 .iter()
660 .map(|&(leaf, squared_distance)| RoutedLeaf {
661 leaf,
662 squared_distance,
663 }),
664 );
665 Ok(())
666 }
667
668 fn scale_query(
673 level: &QuantizedFloatRoutingLevel,
674 query: &[f32],
675 scaled: &mut Vec<f32>,
676 ) -> ScaledLevelQuery {
677 scaled.clear();
678 scaled.reserve(query.len());
679 let mut constant = 0.0f64;
680 for ((&value, &minimum), &step) in query.iter().zip(&level.minimums).zip(&level.steps) {
681 let shifted = value - minimum;
682 constant += f64::from(shifted) * f64::from(shifted);
683 scaled.push(shifted.algebraic_mul(step));
684 }
685 ScaledLevelQuery {
686 constant: constant as f32,
687 }
688 }
689
690 fn score_range_into(
694 &self,
695 level: &QuantizedFloatRoutingLevel,
696 scaled: &ScaledLevelQuery,
697 scaled_query: &[f32],
698 start: usize,
699 end: usize,
700 output: &mut Vec<RoutingCandidate>,
701 ) {
702 let codes = self.level_codes(level);
703 let dimension = self.model.dimension;
704 let kernel = self.model.kernel;
705 output.reserve(end.saturating_sub(start));
706 output.extend((start..end).map(|centroid| {
707 let row = &codes[centroid * dimension..(centroid + 1) * dimension];
708 let dot = kernel.dot(scaled_query, row);
709 let distance = scaled
710 .constant
711 .algebraic_add(level.code_norms[centroid].algebraic_sub(dot.algebraic_mul(2.0)));
712 (centroid as u32, distance.max(0.0))
714 }));
715 }
716
717 fn level_codes(&self, level: &QuantizedFloatRoutingLevel) -> &[u8] {
718 &self.artifact_bytes[level.centroid_codes.clone()]
719 }
720}
721
722impl FloatScannQuery {
723 fn new(routed_leaves: Vec<u32>, centroid_dots: Vec<f32>, ah: AhQuery) -> Self {
724 let fast_scan = FastScanQuery::new(&ah);
725 if fast_scan.is_degenerate() {
726 crate::observe::scann_degenerate_fast_scan_query();
727 super::warn_rate_limited(&DEGENERATE_FAST_SCAN_QUERIES, |count| {
728 format!(
729 "[scann] FastScan lookup table is degenerate (all-zero AH scores); every \
730 leaf row scores as its centroid dot ({count} queries so far)"
731 )
732 });
733 }
734 Self {
735 routed_leaves,
736 centroid_dots,
737 ah,
738 fast_scan,
739 }
740 }
741
742 pub fn routed_leaves(&self) -> &[u32] {
743 &self.routed_leaves
744 }
745
746 pub fn routed(&self) -> impl ExactSizeIterator<Item = (u32, f32)> + '_ {
749 self.routed_leaves
750 .iter()
751 .copied()
752 .zip(self.centroid_dots.iter().copied())
753 }
754
755 pub fn centroid_dot(&self, leaf: u32) -> Option<f32> {
758 self.routed_leaves
759 .iter()
760 .position(|&candidate| candidate == leaf)
761 .map(|position| self.centroid_dots[position])
762 }
763
764 pub fn ah_query(&self) -> &AhQuery {
766 &self.ah
767 }
768
769 pub fn fast_scan(&self) -> &FastScanQuery {
771 &self.fast_scan
772 }
773
774 pub fn score(&self, vector: &EncodedFloatVector) -> ScannResult<Option<f32>> {
776 let Some(position) = self
777 .routed_leaves
778 .iter()
779 .position(|&leaf| leaf == vector.leaf)
780 else {
781 return Ok(None);
782 };
783 self.ah
784 .score_unpacked(&vector.codes, self.centroid_dots[position])
785 .map(Some)
786 }
787}
788
789impl FloatRoutingTree {
790 pub fn dimension(&self) -> usize {
791 self.dimension
792 }
793
794 pub fn level_counts(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
795 self.levels.iter().map(|level| level.len() / self.dimension)
796 }
797
798 pub fn leaf_centroids(&self) -> &[f32] {
799 self.levels.last().map_or(&[], Vec::as_slice)
800 }
801
802 pub fn levels(&self) -> &[Vec<f32>] {
803 &self.levels
804 }
805
806 pub fn child_offsets(&self) -> &[Vec<u32>] {
807 &self.child_offsets
808 }
809
810 pub fn route(&self, query: &[f32], probes: usize) -> ScannResult<Vec<RoutedLeaf>> {
812 let mut output = Vec::with_capacity(probes);
813 with_routing_scratch(|scratch| {
814 self.route_with_scratch(query, probes, scratch, &mut output)
815 })?;
816 Ok(output)
817 }
818
819 pub fn route_with_scratch(
821 &self,
822 query: &[f32],
823 probes: usize,
824 scratch: &mut RoutingScratch,
825 output: &mut Vec<RoutedLeaf>,
826 ) -> ScannResult<()> {
827 if query.len() != self.dimension || query.iter().any(|value| !value.is_finite()) {
828 return Err(ScannFormatError::new(
829 "ScaNN routing query has the wrong dimension or non-finite values",
830 ));
831 }
832 if probes == 0 {
833 return Err(ScannFormatError::new(
834 "ScaNN routing probes must be positive",
835 ));
836 }
837
838 scratch.active.clear();
839 score_range_into(
840 &self.levels[0],
841 self.dimension,
842 0,
843 self.level_counts().next().unwrap(),
844 query,
845 &mut scratch.active,
846 );
847 if self.levels.len() == 1 {
848 keep_best(&mut scratch.active, probes);
849 } else {
850 sort_routing_candidates(&mut scratch.active);
851 let initial_width = super::routing_prefix_for_child_coverage(
852 &scratch.active,
853 &self.child_offsets[0],
854 intermediate_routing_beam(probes),
855 probes,
856 |candidate| candidate.0 as usize,
857 );
858 scratch.active.truncate(initial_width);
859 }
860 for level in 1..self.levels.len() {
861 let offsets = &self.child_offsets[level - 1];
862 scratch.next.clear();
863 for &(parent, _) in &scratch.active {
864 let start = offsets[parent as usize] as usize;
865 let end = offsets[parent as usize + 1] as usize;
866 score_range_into(
867 &self.levels[level],
868 self.dimension,
869 start,
870 end,
871 query,
872 &mut scratch.next,
873 );
874 }
875 if level + 1 == self.levels.len() {
876 keep_best(&mut scratch.next, probes);
877 } else {
878 sort_routing_candidates(&mut scratch.next);
879 let width = super::routing_prefix_for_child_coverage(
880 &scratch.next,
881 &self.child_offsets[level],
882 intermediate_routing_beam(probes),
883 probes,
884 |candidate| candidate.0 as usize,
885 );
886 scratch.next.truncate(width);
887 }
888 std::mem::swap(&mut scratch.active, &mut scratch.next);
889 }
890 output.clear();
891 output.extend(
892 scratch
893 .active
894 .iter()
895 .map(|&(leaf, squared_distance)| RoutedLeaf {
896 leaf,
897 squared_distance,
898 }),
899 );
900 Ok(())
901 }
902
903 pub fn to_quantized_levels(&self) -> Vec<ScannRoutingLevel> {
905 self.levels
906 .iter()
907 .enumerate()
908 .map(|(level_index, centroids)| {
909 let count = centroids.len() / self.dimension;
910 let mut minimums = vec![f32::INFINITY; self.dimension];
911 let mut maximums = vec![f32::NEG_INFINITY; self.dimension];
912 for centroid in centroids.chunks_exact(self.dimension) {
913 for coordinate in 0..self.dimension {
914 minimums[coordinate] = minimums[coordinate].min(centroid[coordinate]);
915 maximums[coordinate] = maximums[coordinate].max(centroid[coordinate]);
916 }
917 }
918 let steps: Vec<f32> = minimums
919 .iter()
920 .zip(&maximums)
921 .map(|(&minimum, &maximum)| {
922 let range = maximum - minimum;
923 if range.is_finite() && range > 0.0 {
924 range / 255.0
925 } else {
926 1.0
927 }
928 })
929 .collect();
930 let centroid_codes = centroids
931 .chunks_exact(self.dimension)
932 .flat_map(|centroid| {
933 centroid.iter().enumerate().map(|(coordinate, &value)| {
934 ((value - minimums[coordinate]) / steps[coordinate])
935 .round()
936 .clamp(0.0, 255.0) as u8
937 })
938 })
939 .collect();
940 ScannRoutingLevel {
941 centroid_count: count as u32,
942 centroid_codes,
943 minimums,
944 steps,
945 child_offsets: self
946 .child_offsets
947 .get(level_index)
948 .cloned()
949 .unwrap_or_default(),
950 }
951 })
952 .collect()
953 }
954
955 pub fn from_quantized_levels(
956 levels: &[ScannRoutingLevel],
957 dimension: usize,
958 ) -> ScannResult<Self> {
959 if dimension == 0 || levels.is_empty() || levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
960 {
961 return Err(ScannFormatError::new(
962 "invalid ScaNN quantized routing shape",
963 ));
964 }
965 let mut decoded = Vec::with_capacity(levels.len());
966 let mut child_offsets = Vec::with_capacity(levels.len().saturating_sub(1));
967 for (index, level) in levels.iter().enumerate() {
968 if level.minimums.len() != dimension
969 || level.steps.len() != dimension
970 || level.centroid_codes.len() != level.centroid_count as usize * dimension
971 || level
972 .minimums
973 .iter()
974 .chain(&level.steps)
975 .any(|value| !value.is_finite())
976 {
977 return Err(ScannFormatError::new(format!(
978 "invalid ScaNN quantized routing level {index}"
979 )));
980 }
981 let centroids = level
982 .centroid_codes
983 .chunks_exact(dimension)
984 .flat_map(|centroid| {
985 centroid.iter().enumerate().map(|(coordinate, &code)| {
986 level.minimums[coordinate] + level.steps[coordinate] * f32::from(code)
987 })
988 })
989 .collect();
990 decoded.push(centroids);
991 if index + 1 < levels.len() {
992 if level.child_offsets.len() != level.centroid_count as usize + 1
993 || level.child_offsets.first() != Some(&0)
994 || level.child_offsets.last() != Some(&levels[index + 1].centroid_count)
995 || level.child_offsets.windows(2).any(|pair| pair[0] > pair[1])
996 {
997 return Err(ScannFormatError::new(format!(
998 "invalid ScaNN child offsets at routing level {index}"
999 )));
1000 }
1001 child_offsets.push(level.child_offsets.clone());
1002 } else if !level.child_offsets.is_empty() {
1003 return Err(ScannFormatError::new(
1004 "terminal ScaNN routing level must not have children",
1005 ));
1006 }
1007 }
1008 Ok(Self {
1009 dimension,
1010 levels: decoded,
1011 child_offsets,
1012 })
1013 }
1014}
1015
1016const MAX_LOCAL_KMEANS_BRANCHES: usize = 64;
1020
1021pub fn train_routing_tree(
1026 data: &[f32],
1027 points: usize,
1028 dimension: usize,
1029 level_counts: &[u32],
1030 iterations: usize,
1031 seed: u64,
1032) -> ScannResult<RoutingTraining> {
1033 if points == 0
1034 || dimension == 0
1035 || data.len() != points.saturating_mul(dimension)
1036 || data.iter().any(|value| !value.is_finite())
1037 || level_counts.is_empty()
1038 || level_counts.len() > usize::from(MAX_SCANN_TREE_LEVELS)
1039 || level_counts.contains(&0)
1040 || level_counts.windows(2).any(|pair| pair[0] > pair[1])
1041 || level_counts.last().copied().unwrap_or_default() as usize > points
1042 {
1043 return Err(ScannFormatError::new(
1044 "invalid ScaNN routing training data or geometry",
1045 ));
1046 }
1047
1048 let leaf_count = *level_counts.last().unwrap() as usize;
1049 let mut stats = RoutingTrainingStats::default();
1050 let PartitionTraining {
1051 centroids: leaf_centroids,
1052 group_sizes: leaf_group_sizes,
1053 point_order: leaf_point_order,
1054 } = train_partition(
1055 data, points, dimension, leaf_count, iterations, seed, 0, &mut stats,
1056 )?;
1057 let mut assignments = vec![u32::MAX; points];
1058 let mut cursor = 0usize;
1059 for (leaf, group_size) in leaf_group_sizes.into_iter().enumerate() {
1060 let end = cursor + group_size;
1061 for &point in &leaf_point_order[cursor..end] {
1062 assignments[point] = leaf as u32;
1063 }
1064 cursor = end;
1065 }
1066 debug_assert_eq!(cursor, points);
1067 debug_assert!(!assignments.contains(&u32::MAX));
1068 let mut leaf_current_to_original: Vec<usize> = (0..leaf_count).collect();
1069 let mut levels = vec![leaf_centroids];
1070 let mut child_offsets = Vec::with_capacity(level_counts.len().saturating_sub(1));
1071
1072 for (round, &parent_count) in level_counts[..level_counts.len() - 1]
1073 .iter()
1074 .rev()
1075 .enumerate()
1076 {
1077 let children = levels[0].len() / dimension;
1078 let partition = train_partition(
1079 &levels[0],
1080 children,
1081 dimension,
1082 parent_count as usize,
1083 iterations,
1084 seed.wrapping_add(0x9e37_79b9_u64.wrapping_mul(round as u64 + 1)),
1085 round + 1,
1086 &mut stats,
1087 )?;
1088 let leaf_new_to_old = reorder_descendants(
1089 &mut levels,
1090 &mut child_offsets,
1091 &partition.point_order,
1092 dimension,
1093 )?;
1094 leaf_current_to_original = leaf_new_to_old
1095 .into_iter()
1096 .map(|old| leaf_current_to_original[old])
1097 .collect();
1098 child_offsets.insert(0, group_offsets(&partition.group_sizes)?);
1099 levels.insert(0, partition.centroids);
1100 }
1101
1102 let tree = FloatRoutingTree {
1103 dimension,
1104 levels,
1105 child_offsets,
1106 };
1107 let mut original_to_current = vec![0u32; leaf_count];
1108 for (current, original) in leaf_current_to_original.into_iter().enumerate() {
1109 original_to_current[original] = current as u32;
1110 }
1111 for assignment in &mut assignments {
1112 *assignment = original_to_current[*assignment as usize];
1113 }
1114 Ok(RoutingTraining {
1115 tree,
1116 assignments,
1117 stats,
1118 })
1119}
1120
1121struct PartitionTraining {
1122 centroids: Vec<f32>,
1123 group_sizes: Vec<usize>,
1124 point_order: Vec<usize>,
1125}
1126
1127#[allow(clippy::too_many_arguments)]
1128fn train_partition(
1129 data: &[f32],
1130 points: usize,
1131 dimension: usize,
1132 clusters: usize,
1133 iterations: usize,
1134 seed: u64,
1135 depth: usize,
1136 stats: &mut RoutingTrainingStats,
1137) -> ScannResult<PartitionTraining> {
1138 if points == 0
1139 || dimension == 0
1140 || data.len() != points.saturating_mul(dimension)
1141 || clusters == 0
1142 || clusters > points
1143 {
1144 return Err(ScannFormatError::new(
1145 "invalid ScaNN recursive partition shape",
1146 ));
1147 }
1148 let mut point_order: Vec<usize> = (0..points).collect();
1149 let mut centroids = Vec::with_capacity(clusters.saturating_mul(dimension));
1150 let mut group_sizes = Vec::with_capacity(clusters);
1151 train_partition_node(
1152 data,
1153 &mut point_order,
1154 dimension,
1155 clusters,
1156 iterations,
1157 seed,
1158 depth,
1159 &mut centroids,
1160 &mut group_sizes,
1161 stats,
1162 );
1163 if centroids.len() != clusters.saturating_mul(dimension)
1164 || group_sizes.len() != clusters
1165 || group_sizes.iter().sum::<usize>() != points
1166 {
1167 return Err(ScannFormatError::new(
1168 "ScaNN recursive partition produced the wrong shape",
1169 ));
1170 }
1171 Ok(PartitionTraining {
1172 centroids,
1173 group_sizes,
1174 point_order,
1175 })
1176}
1177
1178#[allow(clippy::too_many_arguments)]
1179fn train_partition_node(
1180 data: &[f32],
1181 point_ids: &mut [usize],
1182 dimension: usize,
1183 clusters: usize,
1184 iterations: usize,
1185 seed: u64,
1186 depth: usize,
1187 output: &mut Vec<f32>,
1188 group_sizes: &mut Vec<usize>,
1189 stats: &mut RoutingTrainingStats,
1190) {
1191 let points = point_ids.len();
1192 stats.max_depth = stats.max_depth.max(depth);
1193 if clusters == 1 {
1194 append_mean(data, point_ids, dimension, output);
1195 group_sizes.push(points);
1196 return;
1197 }
1198 if clusters == points {
1199 for &point_id in point_ids.iter() {
1200 output.extend_from_slice(&data[point_id * dimension..(point_id + 1) * dimension]);
1201 }
1202 group_sizes.resize(group_sizes.len() + points, 1);
1203 return;
1204 }
1205
1206 let branches = training_branch_factor(clusters).min(points);
1207 let model = train_local_kmeans(
1208 data, point_ids, dimension, branches, iterations, seed, stats,
1209 );
1210 stats.splits = stats.splits.saturating_add(1);
1211 stats.max_split_clusters = stats.max_split_clusters.max(branches);
1212 let sizes: Vec<usize> = model
1213 .member_offsets
1214 .windows(2)
1215 .map(|range| range[1] - range[0])
1216 .collect();
1217 let allocations = apportion_clusters(&sizes, clusters);
1218 reorder_point_ids(point_ids, &model.assignments, &model.member_offsets);
1219 for (branch, &allocation) in allocations.iter().enumerate() {
1220 let start = model.member_offsets[branch];
1221 let end = model.member_offsets[branch + 1];
1222 train_partition_node(
1223 data,
1224 &mut point_ids[start..end],
1225 dimension,
1226 allocation,
1227 iterations,
1228 mix_seed(seed, depth, branch),
1229 depth + 1,
1230 output,
1231 group_sizes,
1232 stats,
1233 );
1234 }
1235}
1236
1237struct LocalKMeans {
1238 assignments: Vec<usize>,
1239 member_offsets: Vec<usize>,
1240}
1241
1242#[allow(clippy::too_many_arguments)]
1243fn train_local_kmeans(
1244 data: &[f32],
1245 point_ids: &[usize],
1246 dimension: usize,
1247 clusters: usize,
1248 iterations: usize,
1249 seed: u64,
1250 stats: &mut RoutingTrainingStats,
1251) -> LocalKMeans {
1252 debug_assert!(clusters > 1 && clusters < point_ids.len());
1253 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1254 let points = point_ids.len();
1255 let mut selected = std::collections::BTreeSet::new();
1256 let mut selected_rows = Vec::with_capacity(clusters);
1257 for upper in points - clusters..points {
1258 let candidate = rng.random_range(0..=upper);
1259 let row = if selected.insert(candidate) {
1260 candidate
1261 } else {
1262 selected.insert(upper);
1263 upper
1264 };
1265 selected_rows.push(row);
1266 }
1267 let mut centroids: Vec<f32> = selected_rows
1268 .into_iter()
1269 .flat_map(|row| {
1270 let point_id = point_ids[row];
1271 data[point_id * dimension..(point_id + 1) * dimension]
1272 .iter()
1273 .copied()
1274 })
1275 .collect();
1276 let mut assignments = vec![usize::MAX; points];
1277 for _ in 0..iterations.max(1) {
1278 let mut distances = vec![0.0f32; points];
1279 for (row, &point_id) in point_ids.iter().enumerate() {
1280 let point = &data[point_id * dimension..(point_id + 1) * dimension];
1281 let (cluster, distance) = nearest_centroid(¢roids, dimension, point);
1282 assignments[row] = cluster;
1283 distances[row] = distance;
1284 }
1285 stats.distance_evaluations = stats
1286 .distance_evaluations
1287 .saturating_add(u64::try_from(points.saturating_mul(clusters)).unwrap_or(u64::MAX));
1288 ensure_non_empty(&mut assignments, &distances, clusters);
1289 let mut sums = vec![0.0f32; clusters * dimension];
1290 let mut counts = vec![0usize; clusters];
1291 for (&point_id, &cluster) in point_ids.iter().zip(&assignments) {
1292 let point = &data[point_id * dimension..(point_id + 1) * dimension];
1293 counts[cluster] += 1;
1294 for coordinate in 0..dimension {
1295 sums[cluster * dimension + coordinate] += point[coordinate];
1296 }
1297 }
1298 for cluster in 0..clusters {
1299 let inverse = (counts[cluster] as f32).recip();
1300 for coordinate in 0..dimension {
1301 sums[cluster * dimension + coordinate] *= inverse;
1302 }
1303 }
1304 if sums == centroids {
1305 break;
1306 }
1307 centroids = sums;
1308 }
1309 let mut distances = vec![0.0f32; points];
1310 for (row, &point_id) in point_ids.iter().enumerate() {
1311 let point = &data[point_id * dimension..(point_id + 1) * dimension];
1312 let (cluster, distance) = nearest_centroid(¢roids, dimension, point);
1313 assignments[row] = cluster;
1314 distances[row] = distance;
1315 }
1316 stats.distance_evaluations = stats
1317 .distance_evaluations
1318 .saturating_add(u64::try_from(points.saturating_mul(clusters)).unwrap_or(u64::MAX));
1319 ensure_non_empty(&mut assignments, &distances, clusters);
1320 let mut member_offsets = vec![0usize; clusters + 1];
1321 for &cluster in &assignments {
1322 member_offsets[cluster + 1] += 1;
1323 }
1324 for cluster in 0..clusters {
1325 member_offsets[cluster + 1] += member_offsets[cluster];
1326 }
1327 LocalKMeans {
1328 assignments,
1329 member_offsets,
1330 }
1331}
1332
1333fn training_branch_factor(clusters: usize) -> usize {
1334 if clusters <= MAX_LOCAL_KMEANS_BRANCHES {
1335 clusters
1336 } else {
1337 ((clusters as f64).sqrt().ceil() as usize).clamp(2, MAX_LOCAL_KMEANS_BRANCHES)
1338 }
1339}
1340
1341fn append_mean(data: &[f32], point_ids: &[usize], dimension: usize, output: &mut Vec<f32>) {
1342 let start = output.len();
1343 output.resize(start + dimension, 0.0);
1344 for &point_id in point_ids {
1345 let point = &data[point_id * dimension..(point_id + 1) * dimension];
1346 for (sum, &value) in output[start..].iter_mut().zip(point) {
1347 *sum += value;
1348 }
1349 }
1350 let inverse = (point_ids.len() as f32).recip();
1351 for value in &mut output[start..] {
1352 *value *= inverse;
1353 }
1354}
1355
1356fn apportion_clusters(sizes: &[usize], total_clusters: usize) -> Vec<usize> {
1357 debug_assert!(!sizes.is_empty());
1358 debug_assert!(sizes.iter().all(|&size| size > 0));
1359 debug_assert!(total_clusters >= sizes.len());
1360 debug_assert!(total_clusters <= sizes.iter().sum());
1361 let total_points: usize = sizes.iter().sum();
1362 let mut allocations = vec![1usize; sizes.len()];
1363 let mut assigned = sizes.len();
1364 for (allocation, &size) in allocations.iter_mut().zip(sizes) {
1365 let target = (total_clusters.saturating_mul(size) / total_points)
1366 .max(1)
1367 .min(size);
1368 assigned += target - 1;
1369 *allocation = target;
1370 }
1371 while assigned < total_clusters {
1372 let next = (0..sizes.len())
1373 .filter(|&index| allocations[index] < sizes[index])
1374 .max_by(|&left, &right| {
1375 let left_deficit = (total_clusters as i128) * (sizes[left] as i128)
1376 - (allocations[left] as i128) * (total_points as i128);
1377 let right_deficit = (total_clusters as i128) * (sizes[right] as i128)
1378 - (allocations[right] as i128) * (total_points as i128);
1379 left_deficit
1380 .cmp(&right_deficit)
1381 .then_with(|| right.cmp(&left))
1382 })
1383 .expect("remaining points provide centroid capacity");
1384 allocations[next] += 1;
1385 assigned += 1;
1386 }
1387 while assigned > total_clusters {
1388 let next = (0..sizes.len())
1389 .filter(|&index| allocations[index] > 1)
1390 .max_by(|&left, &right| {
1391 let left_excess = (allocations[left] as i128) * (total_points as i128)
1392 - (total_clusters as i128) * (sizes[left] as i128);
1393 let right_excess = (allocations[right] as i128) * (total_points as i128)
1394 - (total_clusters as i128) * (sizes[right] as i128);
1395 left_excess
1396 .cmp(&right_excess)
1397 .then_with(|| right.cmp(&left))
1398 })
1399 .expect("at least one branch can release a centroid");
1400 allocations[next] -= 1;
1401 assigned -= 1;
1402 }
1403 allocations
1404}
1405
1406fn reorder_point_ids(point_ids: &mut [usize], assignments: &[usize], offsets: &[usize]) {
1407 let original = point_ids.to_vec();
1408 let mut cursors = offsets[..offsets.len() - 1].to_vec();
1409 for (&point_id, &cluster) in original.iter().zip(assignments) {
1410 point_ids[cursors[cluster]] = point_id;
1411 cursors[cluster] += 1;
1412 }
1413}
1414
1415fn group_offsets(group_sizes: &[usize]) -> ScannResult<Vec<u32>> {
1416 let mut offsets = Vec::with_capacity(group_sizes.len() + 1);
1417 offsets.push(0);
1418 let mut cursor = 0usize;
1419 for &size in group_sizes {
1420 cursor = cursor
1421 .checked_add(size)
1422 .ok_or_else(|| ScannFormatError::new("ScaNN child count overflows usize"))?;
1423 offsets.push(
1424 u32::try_from(cursor)
1425 .map_err(|_| ScannFormatError::new("ScaNN child count exceeds u32"))?,
1426 );
1427 }
1428 Ok(offsets)
1429}
1430
1431fn reorder_descendants(
1432 levels: &mut [Vec<f32>],
1433 child_offsets: &mut [Vec<u32>],
1434 top_order: &[usize],
1435 dimension: usize,
1436) -> ScannResult<Vec<usize>> {
1437 if levels.len() != child_offsets.len() + 1
1438 || levels.first().map_or(0, |level| level.len() / dimension) != top_order.len()
1439 {
1440 return Err(ScannFormatError::new(
1441 "ScaNN bottom-up subtree shape mismatch",
1442 ));
1443 }
1444 let mut parent_order = top_order.to_vec();
1445 let mut permutation = Vec::new();
1446 for depth in 0..child_offsets.len() {
1447 let old_offsets = &child_offsets[depth];
1448 if old_offsets.len() != parent_order.len() + 1 {
1449 return Err(ScannFormatError::new(
1450 "ScaNN bottom-up child directory mismatch",
1451 ));
1452 }
1453 let mut next_order = Vec::with_capacity(levels[depth + 1].len() / dimension);
1454 let mut next_offsets = Vec::with_capacity(parent_order.len() + 1);
1455 next_offsets.push(0);
1456 for &old_parent in &parent_order {
1457 let start = old_offsets[old_parent] as usize;
1458 let end = old_offsets[old_parent + 1] as usize;
1459 if start > end || end > levels[depth + 1].len() / dimension {
1460 return Err(ScannFormatError::new(
1461 "ScaNN bottom-up child range is invalid",
1462 ));
1463 }
1464 next_order.extend(start..end);
1465 next_offsets.push(
1466 u32::try_from(next_order.len())
1467 .map_err(|_| ScannFormatError::new("ScaNN descendant count exceeds u32"))?,
1468 );
1469 }
1470 if next_order.len() != levels[depth + 1].len() / dimension {
1471 return Err(ScannFormatError::new(
1472 "ScaNN bottom-up child permutation is incomplete",
1473 ));
1474 }
1475 reorder_rows(
1476 &mut levels[depth + 1],
1477 dimension,
1478 &next_order,
1479 &mut permutation,
1480 );
1481 child_offsets[depth] = next_offsets;
1482 parent_order = next_order;
1483 }
1484 reorder_rows(&mut levels[0], dimension, top_order, &mut permutation);
1485 Ok(parent_order)
1486}
1487
1488fn reorder_rows(
1489 data: &mut [f32],
1490 dimension: usize,
1491 new_to_old: &[usize],
1492 permutation: &mut Vec<usize>,
1493) {
1494 debug_assert_eq!(data.len(), new_to_old.len() * dimension);
1495 permutation.clear();
1496 permutation.resize(new_to_old.len(), usize::MAX);
1497 for (new, &old) in new_to_old.iter().enumerate() {
1498 permutation[old] = new;
1499 }
1500 debug_assert!(!permutation.contains(&usize::MAX));
1501 for index in 0..new_to_old.len() {
1502 while permutation[index] != index {
1503 let other = permutation[index];
1504 for coordinate in 0..dimension {
1505 data.swap(
1506 index * dimension + coordinate,
1507 other * dimension + coordinate,
1508 );
1509 }
1510 permutation.swap(index, other);
1511 }
1512 }
1513}
1514
1515fn mix_seed(seed: u64, depth: usize, branch: usize) -> u64 {
1516 seed ^ (depth as u64 + 1).wrapping_mul(0x9e37_79b9_7f4a_7c15)
1517 ^ (branch as u64 + 1).wrapping_mul(0xbf58_476d_1ce4_e5b9)
1518}
1519
1520fn ensure_non_empty(assignments: &mut [usize], distances: &[f32], clusters: usize) {
1521 let mut counts = vec![0usize; clusters];
1522 for &cluster in assignments.iter() {
1523 counts[cluster] += 1;
1524 }
1525 for empty in 0..clusters {
1526 if counts[empty] != 0 {
1527 continue;
1528 }
1529 let donor = (0..assignments.len())
1530 .filter(|&row| counts[assignments[row]] > 1)
1531 .max_by(|&left, &right| {
1532 distances[left]
1533 .total_cmp(&distances[right])
1534 .then_with(|| right.cmp(&left))
1535 })
1536 .expect("clusters do not exceed points");
1537 counts[assignments[donor]] -= 1;
1538 assignments[donor] = empty;
1539 counts[empty] = 1;
1540 }
1541}
1542
1543fn nearest_centroid(centroids: &[f32], dimension: usize, point: &[f32]) -> (usize, f32) {
1544 centroids
1545 .chunks_exact(dimension)
1546 .enumerate()
1547 .map(|(index, centroid)| (index, squared_l2(point, centroid)))
1548 .min_by(|left, right| {
1549 left.1
1550 .total_cmp(&right.1)
1551 .then_with(|| left.0.cmp(&right.0))
1552 })
1553 .unwrap()
1554}
1555
1556fn score_range_into(
1557 centroids: &[f32],
1558 dimension: usize,
1559 start: usize,
1560 end: usize,
1561 query: &[f32],
1562 output: &mut Vec<RoutingCandidate>,
1563) {
1564 output.reserve(end.saturating_sub(start));
1565 output.extend((start..end).map(|index| {
1566 (
1567 index as u32,
1568 squared_l2(
1569 ¢roids[index * dimension..(index + 1) * dimension],
1570 query,
1571 ),
1572 )
1573 }));
1574}
1575
1576fn quantized_code_norms(
1581 steps: &[f32],
1582 centroid_codes: &[u8],
1583 dimension: usize,
1584) -> ScannResult<Vec<f32>> {
1585 if dimension == 0 || steps.len() != dimension || !centroid_codes.len().is_multiple_of(dimension)
1586 {
1587 return Err(ScannFormatError::new(
1588 "quantized ScaNN routing level shape does not match its dimension",
1589 ));
1590 }
1591 let step_squares: Vec<f32> = steps.iter().map(|&step| step * step).collect();
1592 Ok(centroid_codes
1593 .chunks_exact(dimension)
1594 .map(|row| {
1595 row.iter()
1596 .zip(&step_squares)
1597 .fold(0.0f64, |acc, (&code, &step_square)| {
1598 let code = f64::from(code);
1599 acc + f64::from(step_square) * code * code
1600 }) as f32
1601 })
1602 .collect())
1603}
1604
1605static DEGENERATE_FAST_SCAN_QUERIES: std::sync::atomic::AtomicU64 =
1606 std::sync::atomic::AtomicU64::new(0);
1607
1608fn keep_best(values: &mut Vec<RoutingCandidate>, count: usize) {
1609 let compare = |left: &RoutingCandidate, right: &RoutingCandidate| {
1610 left.1
1611 .total_cmp(&right.1)
1612 .then_with(|| left.0.cmp(&right.0))
1613 };
1614 let keep = count.min(values.len());
1615 if keep == 0 {
1616 values.clear();
1617 return;
1618 }
1619 if keep < values.len() {
1620 values.select_nth_unstable_by(keep, compare);
1621 values.truncate(keep);
1622 }
1623 values.sort_unstable_by(compare);
1626}
1627
1628fn sort_routing_candidates(values: &mut [RoutingCandidate]) {
1629 values.sort_unstable_by(|left, right| {
1630 left.1
1631 .total_cmp(&right.1)
1632 .then_with(|| left.0.cmp(&right.0))
1633 });
1634}
1635
1636#[inline]
1637fn intermediate_routing_beam(probes: usize) -> usize {
1638 if probes == 1 {
1639 1
1641 } else {
1642 QUERY_INTERMEDIATE_ROUTING_BEAM
1643 }
1644}
1645
1646#[inline]
1647fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
1648 crate::structures::simd::squared_l2_f32(left, right)
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use super::*;
1654
1655 fn clustered_points() -> Vec<f32> {
1656 let mut points = Vec::new();
1657 for cluster in 0..8 {
1658 for row in 0..16 {
1659 points.push(cluster as f32 * 10.0 + row as f32 * 0.01);
1660 points.push((cluster % 3) as f32 * 5.0 - row as f32 * 0.005);
1661 }
1662 }
1663 points
1664 }
1665
1666 #[test]
1667 fn hierarchical_training_is_deterministic_and_nested() {
1668 let data = clustered_points();
1669 let first = train_routing_tree(&data, 128, 2, &[2, 8], 8, 17).unwrap();
1670 let second = train_routing_tree(&data, 128, 2, &[2, 8], 8, 17).unwrap();
1671 assert_eq!(first, second);
1672 assert_eq!(first.tree.level_counts().collect::<Vec<_>>(), [2, 8]);
1673 assert_eq!(first.tree.child_offsets()[0].first(), Some(&0));
1674 assert_eq!(first.tree.child_offsets()[0].last(), Some(&8));
1675 }
1676
1677 #[test]
1678 fn query_beam_is_recall_oriented_but_bounded() {
1679 assert_eq!(intermediate_routing_beam(1), 1);
1680 assert_eq!(
1681 intermediate_routing_beam(2),
1682 QUERY_INTERMEDIATE_ROUTING_BEAM
1683 );
1684 assert_eq!(
1685 intermediate_routing_beam(usize::MAX),
1686 QUERY_INTERMEDIATE_ROUTING_BEAM
1687 );
1688 }
1689
1690 #[test]
1691 fn full_probe_reaches_every_leaf_past_sixty_four_root_parents() {
1692 let root_count = 65usize;
1693 let leaf_count = root_count * root_count;
1694 let mut offsets = Vec::with_capacity(root_count + 1);
1695 for parent in 0..=root_count {
1696 offsets.push((parent * root_count) as u32);
1697 }
1698 let tree = FloatRoutingTree {
1699 dimension: 1,
1700 levels: vec![vec![0.0; root_count], vec![0.0; leaf_count]],
1701 child_offsets: vec![offsets],
1702 };
1703 let routed = tree.route(&[0.0], leaf_count).unwrap();
1704 assert_eq!(routed.len(), leaf_count);
1705 assert_eq!(
1706 routed.iter().map(|leaf| leaf.leaf).collect::<Vec<_>>(),
1707 (0..leaf_count as u32).collect::<Vec<_>>()
1708 );
1709 }
1710
1711 #[test]
1712 fn model_only_training_matches_compatibility_training() {
1713 let data = clustered_points();
1714 let model = FloatScannModel::train_model(
1715 &data,
1716 128,
1717 2,
1718 &[2, 8],
1719 1,
1720 4,
1721 0x5ca1,
1722 DEFAULT_ANISOTROPIC_THRESHOLD,
1723 )
1724 .unwrap();
1725 let (compatibility_model, encoded) = FloatScannModel::train(
1726 &data,
1727 128,
1728 2,
1729 &[2, 8],
1730 1,
1731 4,
1732 0x5ca1,
1733 DEFAULT_ANISOTROPIC_THRESHOLD,
1734 )
1735 .unwrap();
1736
1737 assert_eq!(model, compatibility_model);
1738 assert_eq!(encoded.len(), 128);
1739 }
1740
1741 #[test]
1742 fn float_encode_scratch_reuses_dimension_and_code_buffers() {
1743 let data = clustered_points();
1744 let model = FloatScannModel::train_model(
1745 &data,
1746 128,
1747 2,
1748 &[2, 8],
1749 1,
1750 4,
1751 0x5ca1,
1752 DEFAULT_ANISOTROPIC_THRESHOLD,
1753 )
1754 .unwrap();
1755 let mut scratch = FloatEncodeScratch::default();
1756
1757 model.encode_with_scratch(&data[..2], &mut scratch).unwrap();
1758 let residual_allocation = scratch.residual.as_ptr();
1759 let code_allocation = scratch.codes.as_ptr();
1760 let residual_capacity = scratch.residual.capacity();
1761 let code_capacity = scratch.codes.capacity();
1762
1763 let (leaf, codes) = model
1764 .encode_with_scratch(&data[2..4], &mut scratch)
1765 .unwrap();
1766 assert!(leaf < 8);
1767 assert_eq!(codes.len(), model.codebook.blocks());
1768 assert_eq!(scratch.residual.as_ptr(), residual_allocation);
1769 assert_eq!(scratch.codes.as_ptr(), code_allocation);
1770 assert_eq!(scratch.residual.capacity(), residual_capacity);
1771 assert_eq!(scratch.codes.capacity(), code_capacity);
1772 }
1773
1774 #[test]
1775 fn recursive_training_work_is_bounded_by_local_fanout() {
1776 let points = 4_096usize;
1777 let dimension = 4usize;
1778 let data: Vec<f32> = (0..points * dimension)
1779 .map(|index| {
1780 let row = index / dimension;
1781 let coordinate = index % dimension;
1782 (((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32 / 498.5) - 1.0
1783 })
1784 .collect();
1785 let trained =
1786 train_routing_tree(&data, points, dimension, &[16, 1_024], 2, 0x5ca1).unwrap();
1787
1788 assert_eq!(trained.tree.level_counts().collect::<Vec<_>>(), [16, 1_024]);
1789 assert!(trained.stats.splits > 1);
1790 assert!(
1791 trained.stats.max_split_clusters <= MAX_LOCAL_KMEANS_BRANCHES,
1792 "local split widened to {} clusters",
1793 trained.stats.max_split_clusters,
1794 );
1795 assert!(trained.stats.max_split_clusters < 1_024);
1796 assert_eq!(trained.stats.assignment_distance_evaluations, 0);
1797 assert!(trained.assignments.iter().all(|&leaf| leaf < 1_024));
1798 let one_flat_assignment = (points * 1_024) as u64;
1799 assert!(
1800 trained.stats.distance_evaluations < one_flat_assignment,
1801 "recursive trainer performed {} distance evaluations vs {one_flat_assignment} for one flat leaf pass",
1802 trained.stats.distance_evaluations,
1803 );
1804 }
1805
1806 #[test]
1807 fn quantized_tree_roundtrip_preserves_routing_recall() {
1808 let data = clustered_points();
1809 let trained = train_routing_tree(&data, 128, 2, &[2, 8], 8, 91).unwrap();
1810 let restored =
1811 FloatRoutingTree::from_quantized_levels(&trained.tree.to_quantized_levels(), 2)
1812 .unwrap();
1813 let mut recalled = 0usize;
1814 for point in data.chunks_exact(2) {
1815 let exact = nearest_centroid(trained.tree.leaf_centroids(), 2, point).0 as u32;
1816 if restored
1817 .route(point, 4)
1818 .unwrap()
1819 .iter()
1820 .any(|leaf| leaf.leaf == exact)
1821 {
1822 recalled += 1;
1823 }
1824 }
1825 assert!(recalled as f32 / 128.0 >= 0.98);
1826 }
1827
1828 #[test]
1829 fn executable_model_reopens_from_the_persisted_generation() {
1830 let data = clustered_points();
1831 let (model, _) = FloatScannModel::train(
1832 &data,
1833 128,
1834 2,
1835 &[2, 8],
1836 1,
1837 8,
1838 117,
1839 DEFAULT_ANISOTROPIC_THRESHOLD,
1840 )
1841 .unwrap();
1842 let artifact = ScannTrainedArtifact::new(
1843 19,
1844 100_000,
1845 super::super::ScannConfig {
1846 dimension: 2,
1847 tree_levels: 2,
1848 num_leaves: 8,
1849 encoding: ScannEncoding::AsymmetricHash {
1850 dimensions_per_block: 1,
1851 bits_per_code: 4,
1852 },
1853 },
1854 model.routing.to_quantized_levels(),
1855 Some(model.codebook.to_artifact()),
1856 )
1857 .unwrap();
1858
1859 let reopened = FloatScannModel::from_artifact(&artifact).unwrap();
1860 let artifact_bytes = artifact.to_bytes().unwrap();
1861 let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1862 let quantized = QuantizedFloatScannModel::from_artifact_view(&artifact_view).unwrap();
1863 let quantized_view = quantized.view(&artifact_bytes).unwrap();
1864 assert_eq!(reopened.codebook, model.codebook);
1865 assert_eq!(
1866 reopened.anisotropic_threshold(),
1867 DEFAULT_ANISOTROPIC_THRESHOLD
1868 );
1869 assert_eq!(reopened.routing.level_counts().collect::<Vec<_>>(), [2, 8]);
1870 for point in data.chunks_exact(2).take(16) {
1871 let encoded = reopened.encode(point).unwrap();
1872 let query = reopened.prepare_query(point, 8).unwrap();
1873 assert!(query.score(&encoded).unwrap().unwrap().is_finite());
1874 assert_eq!(quantized_view.encode(point).unwrap(), encoded);
1875 let quantized_query = quantized_view.prepare_query(point, 8).unwrap();
1879 assert_eq!(quantized_query.routed_leaves(), query.routed_leaves());
1880 assert_eq!(quantized_query.ah_query(), query.ah_query());
1881 for ((leaf, quantized_dot), (_, exact_dot)) in
1882 quantized_query.routed().zip(query.routed())
1883 {
1884 assert!(
1885 (quantized_dot - exact_dot).abs() <= 1e-4 * (1.0 + exact_dot.abs()),
1886 "leaf {leaf}: quantized centroid dot {quantized_dot} vs {exact_dot}"
1887 );
1888 }
1889 }
1890 }
1891
1892 #[test]
1895 fn quantized_kernel_routing_matches_decoded_reference_distances() {
1896 let dimension = 24usize;
1897 let points = 512usize;
1898 let data: Vec<f32> = (0..points * dimension)
1899 .map(|index| {
1900 let row = index / dimension;
1901 let coordinate = index % dimension;
1902 (((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32 / 498.5) - 1.0
1903 })
1904 .collect();
1905 let (model, _) = FloatScannModel::train(
1906 &data,
1907 points,
1908 dimension,
1909 &[4, 32],
1910 4,
1911 3,
1912 0xfa57,
1913 DEFAULT_ANISOTROPIC_THRESHOLD,
1914 )
1915 .unwrap();
1916 let artifact = ScannTrainedArtifact::new(
1917 5,
1918 100_000,
1919 super::super::ScannConfig {
1920 dimension: dimension as u32,
1921 tree_levels: 2,
1922 num_leaves: 32,
1923 encoding: ScannEncoding::AsymmetricHash {
1924 dimensions_per_block: 4,
1925 bits_per_code: 4,
1926 },
1927 },
1928 model.routing.to_quantized_levels(),
1929 Some(model.codebook.to_artifact()),
1930 )
1931 .unwrap();
1932 let bytes = artifact.to_bytes().unwrap();
1933 let view = ScannTrainedArtifactView::parse(&bytes).unwrap();
1934 let quantized = QuantizedFloatScannModel::from_artifact_view(&view).unwrap();
1935 let quantized_view = quantized.view(&bytes).unwrap();
1936 let decoded = FloatRoutingTree::from_quantized_levels(&artifact.levels, dimension).unwrap();
1938 for point in data.chunks_exact(dimension).take(64) {
1939 let expected = decoded.route(point, 6).unwrap();
1940 let got = quantized_view.route(point, 6).unwrap();
1941 let expected_leaves: Vec<u32> = expected.iter().map(|leaf| leaf.leaf).collect();
1942 let got_leaves: Vec<u32> = got.iter().map(|leaf| leaf.leaf).collect();
1943 if expected_leaves != got_leaves {
1944 let worst_expected = expected.last().unwrap().squared_distance;
1947 for leaf in &got {
1948 assert!(
1949 leaf.squared_distance <= worst_expected + 1e-3,
1950 "kernel route kept leaf {} at {} beyond reference frontier {worst_expected}",
1951 leaf.leaf,
1952 leaf.squared_distance
1953 );
1954 }
1955 }
1956 for (reference, kernel) in expected.iter().zip(&got) {
1957 assert!(
1958 (reference.squared_distance - kernel.squared_distance).abs()
1959 <= 1e-3 * (1.0 + reference.squared_distance),
1960 "distance drift {} vs {}",
1961 reference.squared_distance,
1962 kernel.squared_distance
1963 );
1964 }
1965 }
1966 }
1967
1968 #[test]
1969 fn float_scann_end_to_end_recall_is_deterministic() {
1970 let mut data = Vec::with_capacity(256 * 8);
1971 for row in 0..256 {
1972 let mut vector: Vec<f32> = (0..8)
1973 .map(|coordinate| {
1974 let raw = ((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32;
1975 raw / 498.5 - 1.0
1976 })
1977 .collect();
1978 let norm = vector.iter().map(|value| value * value).sum::<f32>().sqrt();
1979 vector.iter_mut().for_each(|value| *value /= norm);
1980 data.extend(vector);
1981 }
1982 let (model, encoded) =
1983 FloatScannModel::train(&data, 256, 8, &[2, 8], 2, 7, 123, 0.2).unwrap();
1984 let mut recalled = 0usize;
1985 let queries = 32usize;
1986 let k = 10usize;
1987 for query in data.chunks_exact(8).take(queries) {
1988 let mut exact: Vec<(usize, f32)> = data
1989 .chunks_exact(8)
1990 .enumerate()
1991 .map(|(row, vector)| {
1992 (
1993 row,
1994 crate::structures::simd::dot_product_f32(query, vector, 8),
1995 )
1996 })
1997 .collect();
1998 exact.sort_unstable_by(|left, right| right.1.total_cmp(&left.1));
1999 let prepared = model.prepare_query(query, 8).unwrap();
2000 let mut approximate: Vec<(usize, f32)> = encoded
2001 .iter()
2002 .enumerate()
2003 .map(|(row, vector)| (row, prepared.score(vector).unwrap().unwrap()))
2004 .collect();
2005 approximate.sort_unstable_by(|left, right| right.1.total_cmp(&left.1));
2006 recalled += approximate[..k]
2007 .iter()
2008 .filter(|(row, _)| exact[..k].iter().any(|(exact_row, _)| exact_row == row))
2009 .count();
2010 }
2011 let recall = recalled as f32 / (queries * k) as f32;
2012 assert!(recall >= 0.70, "unexpected float ScaNN recall@10: {recall}");
2013 }
2014}