1use rand::{Rng, SeedableRng};
7
8use super::{ScannAhCodebook, ScannAhCodebookRef, ScannFormatError, ScannResult};
9
10pub const CENTERS_PER_BLOCK: usize = 16;
11pub const DEFAULT_ANISOTROPIC_THRESHOLD: f32 = 0.2;
12
13#[derive(Clone, Debug, PartialEq)]
14pub struct AhCodebook {
15 dimension: usize,
16 dimensions_per_block: usize,
17 centers: Vec<f32>,
19}
20
21#[derive(Clone, Debug, PartialEq)]
22pub struct AhQuery {
23 values: Vec<f32>,
25}
26
27#[derive(Clone, Debug, Default)]
29pub struct AhEncodeScratch {
30 local_errors: Vec<[f32; CENTERS_PER_BLOCK]>,
31 projections: Vec<[f32; CENTERS_PER_BLOCK]>,
32}
33
34impl AhCodebook {
35 pub fn train(
36 residuals: &[f32],
37 points: usize,
38 dimension: usize,
39 dimensions_per_block: usize,
40 iterations: usize,
41 seed: u64,
42 ) -> ScannResult<Self> {
43 if points < CENTERS_PER_BLOCK
44 || dimension == 0
45 || dimensions_per_block == 0
46 || dimensions_per_block > dimension
47 || dimensions_per_block > u16::MAX as usize
48 || residuals.len() != points.saturating_mul(dimension)
49 || residuals.iter().any(|value| !value.is_finite())
50 {
51 return Err(ScannFormatError::new(
52 "invalid ScaNN AH training data or block geometry",
53 ));
54 }
55 let blocks = dimension.div_ceil(dimensions_per_block);
56 let mut centers = Vec::with_capacity(blocks * CENTERS_PER_BLOCK * dimensions_per_block);
57 for block in 0..blocks {
58 let start = block * dimensions_per_block;
59 let block_dimension = dimensions_per_block.min(dimension - start);
60 let mut block_data = Vec::with_capacity(points * block_dimension);
61 for row in residuals.chunks_exact(dimension) {
62 block_data.extend_from_slice(&row[start..start + block_dimension]);
63 }
64 let trained = train_subspace(
65 &block_data,
66 points,
67 block_dimension,
68 iterations,
69 seed.wrapping_add(0x517c_c1b7_2722_0a95u64.wrapping_mul(block as u64 + 1)),
70 );
71 for center in trained.chunks_exact(block_dimension) {
72 centers.extend_from_slice(center);
73 centers.resize(centers.len() + dimensions_per_block - block_dimension, 0.0);
74 }
75 }
76 Ok(Self {
77 dimension,
78 dimensions_per_block,
79 centers,
80 })
81 }
82
83 #[allow(clippy::too_many_arguments)]
88 pub(crate) fn train_from_assigned_vectors(
89 data: &[f32],
90 assignments: &[u32],
91 leaf_centroids: &[f32],
92 points: usize,
93 dimension: usize,
94 dimensions_per_block: usize,
95 iterations: usize,
96 seed: u64,
97 ) -> ScannResult<Self> {
98 if points < CENTERS_PER_BLOCK
99 || dimension == 0
100 || dimensions_per_block == 0
101 || dimensions_per_block > dimension
102 || dimensions_per_block > u16::MAX as usize
103 || data.len() != points.saturating_mul(dimension)
104 || assignments.len() != points
105 || leaf_centroids.is_empty()
106 || !leaf_centroids.len().is_multiple_of(dimension)
107 || data.iter().any(|value| !value.is_finite())
108 || leaf_centroids.iter().any(|value| !value.is_finite())
109 {
110 return Err(ScannFormatError::new(
111 "invalid assigned ScaNN AH training data or block geometry",
112 ));
113 }
114 let leaves = leaf_centroids.len() / dimension;
115 if assignments.iter().any(|&leaf| leaf as usize >= leaves) {
116 return Err(ScannFormatError::new(
117 "ScaNN AH training assignment references a missing leaf",
118 ));
119 }
120 Self::assigned_training_workspace_bytes(points, dimension, dimensions_per_block)?;
121
122 let blocks = dimension.div_ceil(dimensions_per_block);
123 let mut centers = Vec::with_capacity(blocks * CENTERS_PER_BLOCK * dimensions_per_block);
124 for block in 0..blocks {
125 let start = block * dimensions_per_block;
126 let block_dimension = dimensions_per_block.min(dimension - start);
127 let mut block_data = Vec::with_capacity(points * block_dimension);
128 for (point, &leaf) in data.chunks_exact(dimension).zip(assignments) {
129 let centroid =
130 &leaf_centroids[leaf as usize * dimension..(leaf as usize + 1) * dimension];
131 block_data.extend(
132 (start..start + block_dimension)
133 .map(|coordinate| point[coordinate] - centroid[coordinate]),
134 );
135 }
136 let trained = train_subspace(
137 &block_data,
138 points,
139 block_dimension,
140 iterations,
141 seed.wrapping_add(0x517c_c1b7_2722_0a95u64.wrapping_mul(block as u64 + 1)),
142 );
143 for center in trained.chunks_exact(block_dimension) {
144 centers.extend_from_slice(center);
145 centers.resize(centers.len() + dimensions_per_block - block_dimension, 0.0);
146 }
147 }
148 Ok(Self {
149 dimension,
150 dimensions_per_block,
151 centers,
152 })
153 }
154
155 pub(crate) fn assigned_training_workspace_bytes(
159 points: usize,
160 dimension: usize,
161 dimensions_per_block: usize,
162 ) -> ScannResult<usize> {
163 if points < CENTERS_PER_BLOCK
164 || dimension == 0
165 || dimensions_per_block == 0
166 || dimensions_per_block > dimension
167 {
168 return Err(ScannFormatError::new("invalid ScaNN AH workspace geometry"));
169 }
170 let block_dimension = dimensions_per_block.min(dimension);
171 let block_values = points.checked_mul(block_dimension).ok_or_else(|| {
172 ScannFormatError::new("ScaNN AH block workspace size overflows usize")
173 })?;
174 let center_values = CENTERS_PER_BLOCK
175 .checked_mul(block_dimension)
176 .ok_or_else(|| ScannFormatError::new("ScaNN AH center workspace overflows usize"))?;
177 let nearest_bytes = points
178 .checked_mul(std::mem::size_of::<f32>())
179 .ok_or_else(|| ScannFormatError::new("ScaNN AH nearest workspace overflows usize"))?;
180 let assignment_bytes = points
181 .checked_mul(std::mem::size_of::<usize>())
182 .ok_or_else(|| {
183 ScannFormatError::new("ScaNN AH assignment workspace overflows usize")
184 })?;
185 let center_bytes = center_values
186 .checked_mul(2 * std::mem::size_of::<f32>())
187 .ok_or_else(|| ScannFormatError::new("ScaNN AH center workspace overflows usize"))?;
188 let selected_and_count_bytes = 2 * CENTERS_PER_BLOCK * std::mem::size_of::<usize>();
189 block_values
190 .checked_mul(std::mem::size_of::<f32>())
191 .and_then(|bytes| bytes.checked_add(nearest_bytes))
192 .and_then(|bytes| bytes.checked_add(assignment_bytes))
193 .and_then(|bytes| bytes.checked_add(center_bytes))
194 .and_then(|bytes| bytes.checked_add(selected_and_count_bytes))
195 .ok_or_else(|| ScannFormatError::new("ScaNN AH workspace size overflows usize"))
196 }
197
198 pub fn from_artifact(dimension: usize, artifact: &ScannAhCodebook) -> ScannResult<Self> {
199 if artifact.centers_per_block as usize != CENTERS_PER_BLOCK {
200 return Err(ScannFormatError::new(
201 "ScaNN AH artifact must have sixteen centers per block",
202 ));
203 }
204 let dimensions_per_block = artifact.dimensions_per_block as usize;
205 let blocks = dimension.div_ceil(dimensions_per_block.max(1));
206 if dimension == 0
207 || dimensions_per_block == 0
208 || dimensions_per_block > dimension
209 || artifact.centers.len() != blocks * CENTERS_PER_BLOCK * dimensions_per_block
210 || artifact.centers.iter().any(|value| !value.is_finite())
211 {
212 return Err(ScannFormatError::new("invalid ScaNN AH artifact shape"));
213 }
214 Ok(Self {
215 dimension,
216 dimensions_per_block,
217 centers: artifact.centers.clone(),
218 })
219 }
220
221 pub fn from_artifact_ref(
224 dimension: usize,
225 artifact: ScannAhCodebookRef<'_>,
226 ) -> ScannResult<Self> {
227 if artifact.centers_per_block as usize != CENTERS_PER_BLOCK {
228 return Err(ScannFormatError::new(
229 "ScaNN AH artifact must have sixteen centers per block",
230 ));
231 }
232 let dimensions_per_block = artifact.dimensions_per_block as usize;
233 let blocks = dimension.div_ceil(dimensions_per_block.max(1));
234 let centers: Vec<f32> = artifact.centers().collect();
235 if dimension == 0
236 || dimensions_per_block == 0
237 || dimensions_per_block > dimension
238 || centers.len() != blocks * CENTERS_PER_BLOCK * dimensions_per_block
239 || centers.iter().any(|value| !value.is_finite())
240 {
241 return Err(ScannFormatError::new("invalid ScaNN AH artifact shape"));
242 }
243 Ok(Self {
244 dimension,
245 dimensions_per_block,
246 centers,
247 })
248 }
249
250 pub fn to_artifact(&self) -> ScannAhCodebook {
251 ScannAhCodebook {
252 dimensions_per_block: self.dimensions_per_block as u16,
253 centers_per_block: CENTERS_PER_BLOCK as u16,
254 centers: self.centers.clone(),
255 }
256 }
257
258 pub fn dimension(&self) -> usize {
259 self.dimension
260 }
261
262 pub fn dimensions_per_block(&self) -> usize {
263 self.dimensions_per_block
264 }
265
266 pub fn blocks(&self) -> usize {
267 self.dimension.div_ceil(self.dimensions_per_block)
268 }
269
270 pub fn code_bytes(&self) -> usize {
271 self.blocks().div_ceil(2)
272 }
273
274 pub fn estimated_memory_bytes(&self) -> usize {
275 self.centers.len() * std::mem::size_of::<f32>()
276 }
277
278 pub fn encode(
281 &self,
282 residual: &[f32],
283 original: &[f32],
284 anisotropic_threshold: f32,
285 codes: &mut [u8],
286 ) -> ScannResult<()> {
287 self.encode_with_scratch(
288 residual,
289 original,
290 anisotropic_threshold,
291 codes,
292 &mut AhEncodeScratch::default(),
293 )
294 }
295
296 pub fn encode_with_scratch(
297 &self,
298 residual: &[f32],
299 original: &[f32],
300 anisotropic_threshold: f32,
301 codes: &mut [u8],
302 scratch: &mut AhEncodeScratch,
303 ) -> ScannResult<()> {
304 if residual.len() != self.dimension
305 || original.len() != self.dimension
306 || codes.len() != self.blocks()
307 || residual.iter().any(|value| !value.is_finite())
308 || original.iter().any(|value| !value.is_finite())
309 || !anisotropic_threshold.is_finite()
310 || !(0.0..1.0).contains(&anisotropic_threshold)
311 {
312 return Err(ScannFormatError::new(
313 "invalid ScaNN AH encode input or anisotropic threshold",
314 ));
315 }
316 let norm = dot(original, original).sqrt();
317 let inverse_norm = if norm.is_finite() && norm > f32::EPSILON {
318 norm.recip()
319 } else {
320 0.0
321 };
322 scratch
323 .local_errors
324 .resize(self.blocks(), [0.0; CENTERS_PER_BLOCK]);
325 scratch
326 .projections
327 .resize(self.blocks(), [0.0; CENTERS_PER_BLOCK]);
328 let mut total_projection = 0.0f32;
329 for (block, code) in codes.iter_mut().enumerate() {
330 let (start, block_dimension) = self.block_shape(block);
331 let mut best = (0usize, f32::INFINITY);
332 for center in 0..CENTERS_PER_BLOCK {
333 let center_values = self.center(block, center);
334 let mut squared_error = 0.0;
335 let mut projection = 0.0;
336 for coordinate in 0..block_dimension {
337 let error = residual[start + coordinate] - center_values[coordinate];
338 squared_error += error * error;
339 projection += error * original[start + coordinate] * inverse_norm;
340 }
341 scratch.local_errors[block][center] = squared_error;
342 scratch.projections[block][center] = projection;
343 if squared_error < best.1 {
344 best = (center, squared_error);
345 }
346 }
347 *code = best.0 as u8;
348 total_projection += scratch.projections[block][best.0];
349 }
350
351 if inverse_norm == 0.0 || anisotropic_threshold == 0.0 {
352 return Ok(());
353 }
354 let parallel_weight = anisotropic_threshold * anisotropic_threshold
355 / (1.0 - anisotropic_threshold * anisotropic_threshold).max(f32::EPSILON)
356 * self.dimension as f32;
357 for _ in 0..3 {
358 let mut changed = false;
359 for (block, code) in codes.iter_mut().enumerate() {
360 let current = *code as usize;
361 let without_current = total_projection - scratch.projections[block][current];
362 let mut best = current;
363 let mut best_objective = scratch.local_errors[block][current]
364 + parallel_weight * total_projection * total_projection;
365 for center in 0..CENTERS_PER_BLOCK {
366 let candidate_projection = without_current + scratch.projections[block][center];
367 let objective = scratch.local_errors[block][center]
368 + parallel_weight * candidate_projection * candidate_projection;
369 if objective < best_objective {
370 best = center;
371 best_objective = objective;
372 }
373 }
374 if best != current {
375 total_projection = without_current + scratch.projections[block][best];
376 *code = best as u8;
377 changed = true;
378 }
379 }
380 if !changed {
381 break;
382 }
383 }
384 Ok(())
385 }
386
387 pub fn encode_packed(
388 &self,
389 residual: &[f32],
390 original: &[f32],
391 anisotropic_threshold: f32,
392 output: &mut [u8],
393 ) -> ScannResult<()> {
394 if output.len() != self.code_bytes() {
395 return Err(ScannFormatError::new("invalid packed ScaNN AH code length"));
396 }
397 let mut codes = vec![0u8; self.blocks()];
398 self.encode(residual, original, anisotropic_threshold, &mut codes)?;
399 output.fill(0);
400 for (block, &code) in codes.iter().enumerate() {
401 output[block / 2] |= code << ((block % 2) * 4);
402 }
403 Ok(())
404 }
405
406 pub fn query_dot_product(&self, query: &[f32]) -> ScannResult<AhQuery> {
407 if query.len() != self.dimension || query.iter().any(|value| !value.is_finite()) {
408 return Err(ScannFormatError::new("invalid ScaNN AH query vector"));
409 }
410 let mut values = Vec::with_capacity(self.blocks() * CENTERS_PER_BLOCK);
411 for block in 0..self.blocks() {
412 let (start, block_dimension) = self.block_shape(block);
413 for center in 0..CENTERS_PER_BLOCK {
414 values.push(dot(
415 &query[start..start + block_dimension],
416 &self.center(block, center)[..block_dimension],
417 ));
418 }
419 }
420 Ok(AhQuery { values })
421 }
422
423 fn block_shape(&self, block: usize) -> (usize, usize) {
424 let start = block * self.dimensions_per_block;
425 (start, self.dimensions_per_block.min(self.dimension - start))
426 }
427
428 fn center(&self, block: usize, center: usize) -> &[f32] {
429 let offset = (block * CENTERS_PER_BLOCK + center) * self.dimensions_per_block;
430 &self.centers[offset..offset + self.dimensions_per_block]
431 }
432}
433
434impl AhQuery {
435 pub fn blocks(&self) -> usize {
436 self.values.len() / CENTERS_PER_BLOCK
437 }
438
439 pub fn score_unpacked(&self, codes: &[u8], centroid_dot: f32) -> ScannResult<f32> {
440 if codes.len() != self.blocks()
441 || codes.iter().any(|&code| code as usize >= CENTERS_PER_BLOCK)
442 {
443 return Err(ScannFormatError::new("invalid unpacked ScaNN AH codes"));
444 }
445 Ok(codes
446 .iter()
447 .enumerate()
448 .fold(centroid_dot, |score, (block, &code)| {
449 score.algebraic_add(self.values[block * CENTERS_PER_BLOCK + code as usize])
450 }))
451 }
452
453 pub fn score_packed(&self, codes: &[u8], centroid_dot: f32) -> ScannResult<f32> {
454 if codes.len() != self.blocks().div_ceil(2) {
455 return Err(ScannFormatError::new("invalid packed ScaNN AH codes"));
456 }
457 let mut score = centroid_dot;
458 for block in 0..self.blocks() {
459 let code = (codes[block / 2] >> ((block % 2) * 4)) & 0x0f;
460 score = score.algebraic_add(self.values[block * CENTERS_PER_BLOCK + code as usize]);
461 }
462 Ok(score)
463 }
464
465 pub(crate) fn values(&self) -> &[f32] {
466 &self.values
467 }
468}
469
470fn train_subspace(
471 data: &[f32],
472 points: usize,
473 dimension: usize,
474 iterations: usize,
475 seed: u64,
476) -> Vec<f32> {
477 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
478 let mut selected = Vec::with_capacity(CENTERS_PER_BLOCK);
479 selected.push(rng.random_range(0..points));
480 let mut nearest = vec![f32::INFINITY; points];
481 while selected.len() < CENTERS_PER_BLOCK {
482 let last = &data[selected[selected.len() - 1] * dimension..][..dimension];
483 for (row, point) in data.chunks_exact(dimension).enumerate() {
484 nearest[row] = nearest[row].min(squared_l2(point, last));
485 }
486 let total: f64 = nearest.iter().map(|&value| f64::from(value)).sum();
487 let next = if total > 0.0 && total.is_finite() {
488 let target = rng.random::<f64>() * total;
489 let mut sum = 0.0;
490 nearest
491 .iter()
492 .position(|&value| {
493 sum += f64::from(value);
494 sum > target
495 })
496 .unwrap_or(points - 1)
497 } else {
498 (0..points)
499 .find(|candidate| !selected.contains(candidate))
500 .unwrap_or(0)
501 };
502 selected.push(next);
503 }
504 let mut centers: Vec<f32> = selected
505 .iter()
506 .flat_map(|&row| data[row * dimension..(row + 1) * dimension].iter().copied())
507 .collect();
508 let mut assignments = vec![0usize; points];
509 for _ in 0..iterations.max(1) {
510 for (row, point) in data.chunks_exact(dimension).enumerate() {
511 assignments[row] = centers
512 .chunks_exact(dimension)
513 .enumerate()
514 .map(|(center, values)| (center, squared_l2(point, values)))
515 .min_by(|left, right| {
516 left.1
517 .total_cmp(&right.1)
518 .then_with(|| left.0.cmp(&right.0))
519 })
520 .unwrap()
521 .0;
522 }
523 let mut sums = vec![0.0f32; centers.len()];
524 let mut counts = [0usize; CENTERS_PER_BLOCK];
525 for (point, ¢er) in data.chunks_exact(dimension).zip(&assignments) {
526 counts[center] += 1;
527 for coordinate in 0..dimension {
528 sums[center * dimension + coordinate] += point[coordinate];
529 }
530 }
531 for center in 0..CENTERS_PER_BLOCK {
532 if counts[center] == 0 {
533 let replacement = selected[center];
534 sums[center * dimension..(center + 1) * dimension]
535 .copy_from_slice(&data[replacement * dimension..(replacement + 1) * dimension]);
536 } else {
537 let inverse = (counts[center] as f32).recip();
538 for value in &mut sums[center * dimension..(center + 1) * dimension] {
539 *value *= inverse;
540 }
541 }
542 }
543 centers = sums;
544 }
545 centers
546}
547
548#[inline]
549fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
550 crate::structures::simd::squared_l2_f32(left, right)
551}
552
553#[inline]
554fn dot(left: &[f32], right: &[f32]) -> f32 {
555 crate::structures::simd::dot_product_f32(left, right, left.len())
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 fn training_data() -> Vec<f32> {
563 (0..256)
564 .flat_map(|row| {
565 (0..7)
566 .map(move |coordinate| ((row * 17 + coordinate * 29) % 101) as f32 / 50.0 - 1.0)
567 })
568 .collect()
569 }
570
571 #[test]
572 fn ah_training_and_artifact_roundtrip_are_deterministic() {
573 let data = training_data();
574 let first = AhCodebook::train(&data, 256, 7, 2, 6, 42).unwrap();
575 let second = AhCodebook::train(&data, 256, 7, 2, 6, 42).unwrap();
576 assert_eq!(first, second);
577 assert_eq!(
578 AhCodebook::from_artifact(7, &first.to_artifact()).unwrap(),
579 first
580 );
581 }
582
583 #[test]
584 fn assigned_vector_training_matches_materialized_residual_training() {
585 let data = training_data();
586 let dimension = 7usize;
587 let points = 256usize;
588 let leaves = 4usize;
589 let leaf_centroids: Vec<f32> = (0..leaves)
590 .flat_map(|leaf| {
591 (0..dimension)
592 .map(move |coordinate| (leaf as f32 - 1.5) * 0.1 + coordinate as f32 * 0.003)
593 })
594 .collect();
595 let assignments: Vec<u32> = (0..points).map(|row| (row % leaves) as u32).collect();
596 let residuals: Vec<f32> = data
597 .chunks_exact(dimension)
598 .zip(&assignments)
599 .flat_map(|(point, &leaf)| {
600 let centroid =
601 &leaf_centroids[leaf as usize * dimension..(leaf as usize + 1) * dimension];
602 point
603 .iter()
604 .zip(centroid)
605 .map(|(&value, ¢er)| value - center)
606 })
607 .collect();
608
609 let materialized = AhCodebook::train(&residuals, points, dimension, 2, 6, 42).unwrap();
610 let blockwise = AhCodebook::train_from_assigned_vectors(
611 &data,
612 &assignments,
613 &leaf_centroids,
614 points,
615 dimension,
616 2,
617 6,
618 42,
619 )
620 .unwrap();
621 assert_eq!(blockwise, materialized);
622 }
623
624 #[test]
625 fn assigned_training_workspace_is_block_bounded_at_default_billion_scale_budget() {
626 let points = 1_048_576usize;
627 let dimension = 1_024usize;
628 let dimensions_per_block = 2usize;
629 let sample_bytes = points * dimension * std::mem::size_of::<f32>();
630 let workspace =
631 AhCodebook::assigned_training_workspace_bytes(points, dimension, dimensions_per_block)
632 .unwrap();
633
634 assert!(workspace < sample_bytes / 100);
635 assert!(workspace < 24 * 1024 * 1024);
636 }
637
638 #[test]
639 fn packed_and_unpacked_scores_match() {
640 let data = training_data();
641 let codebook = AhCodebook::train(&data, 256, 7, 2, 5, 9).unwrap();
642 let vector = &data[..7];
643 let mut unpacked = vec![0u8; codebook.blocks()];
644 codebook
645 .encode(vector, vector, DEFAULT_ANISOTROPIC_THRESHOLD, &mut unpacked)
646 .unwrap();
647 let mut packed = vec![0u8; codebook.code_bytes()];
648 codebook
649 .encode_packed(vector, vector, DEFAULT_ANISOTROPIC_THRESHOLD, &mut packed)
650 .unwrap();
651 let query = codebook.query_dot_product(vector).unwrap();
652 assert_eq!(
653 query.score_unpacked(&unpacked, 0.25).unwrap(),
654 query.score_packed(&packed, 0.25).unwrap()
655 );
656 }
657}