use super::{ScannFormatError, ScannResult};
pub const MAX_SCANN_TREE_LEVELS: u8 = 3;
pub const MAX_SCANN_LEAVES: u32 = 30_000_000;
pub const MIN_POINTS_FOR_PARTITIONING: u64 = 100_000;
pub const MIN_PARTITION_TRAINING_POINTS_PER_LEAF: u64 = 8;
pub const PARTITION_TRAINING_POINTS_PER_CENTROID: u64 = 200;
pub const DEFAULT_TRAINING_SAMPLE_SIZE: u64 = 100_000;
pub const SCANN_FAST_SCAN_LANES: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScannEncoding {
AsymmetricHash {
dimensions_per_block: u16,
bits_per_code: u8,
},
BinaryHamming,
}
impl ScannEncoding {
pub(crate) fn tag(self) -> u8 {
match self {
Self::AsymmetricHash { .. } => 1,
Self::BinaryHamming => 2,
}
}
pub(crate) fn parameters(self) -> (u16, u8) {
match self {
Self::AsymmetricHash {
dimensions_per_block,
bits_per_code,
} => (dimensions_per_block, bits_per_code),
Self::BinaryHamming => (0, 0),
}
}
pub(crate) fn from_parts(tag: u8, dimensions_per_block: u16, bits: u8) -> ScannResult<Self> {
match (tag, dimensions_per_block, bits) {
(1, dimensions_per_block, bits_per_code) => Ok(Self::AsymmetricHash {
dimensions_per_block,
bits_per_code,
}),
(2, 0, 0) => Ok(Self::BinaryHamming),
_ => Err(ScannFormatError::new(
"invalid ScaNN leaf encoding or encoding parameters",
)),
}
}
pub fn row_code_bytes(self, dimension: u32) -> ScannResult<usize> {
let dimension = usize::try_from(dimension)
.map_err(|_| ScannFormatError::new("ScaNN dimension exceeds usize"))?;
match self {
Self::AsymmetricHash {
dimensions_per_block,
bits_per_code,
} => {
if dimensions_per_block == 0 || bits_per_code != 4 {
return Err(ScannFormatError::new(
"ScaNN AH encoding requires non-zero block dimensions and 4-bit codes",
));
}
let blocks = dimension.div_ceil(usize::from(dimensions_per_block));
blocks
.checked_mul(usize::from(bits_per_code))
.and_then(|bits| bits.checked_add(7))
.map(|bits| bits / 8)
.ok_or_else(|| ScannFormatError::new("ScaNN AH row size overflows usize"))
}
Self::BinaryHamming => {
if !dimension.is_multiple_of(8) {
return Err(ScannFormatError::new(
"binary ScaNN dimension must be a multiple of eight bits",
));
}
Ok(dimension / 8)
}
}
}
pub fn leaf_code_bytes(self, dimension: u32, rows: usize) -> ScannResult<usize> {
match self {
Self::BinaryHamming => self
.row_code_bytes(dimension)?
.checked_mul(rows)
.ok_or_else(|| ScannFormatError::new("binary ScaNN leaf size overflows")),
Self::AsymmetricHash {
dimensions_per_block,
..
} => {
self.row_code_bytes(dimension)?;
let blocks = (dimension as usize).div_ceil(usize::from(dimensions_per_block));
let full_rows = rows / SCANN_FAST_SCAN_LANES;
let tail_rows = rows % SCANN_FAST_SCAN_LANES;
let full_block_bytes = super::packed_block_bytes(blocks)
.ok_or_else(|| ScannFormatError::new("ScaNN FastScan block size overflows"))?;
let tail_row_bytes = blocks.div_ceil(2);
full_rows
.checked_mul(full_block_bytes)
.and_then(|bytes| {
tail_rows
.checked_mul(tail_row_bytes)
.and_then(|tail| bytes.checked_add(tail))
})
.ok_or_else(|| ScannFormatError::new("ScaNN FastScan leaf size overflows"))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScannConfig {
pub dimension: u32,
pub tree_levels: u8,
pub num_leaves: u32,
pub encoding: ScannEncoding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScannTrainingState {
AwaitingData { observed: u64, required: u64 },
Ready { observed: u64, required: u64 },
}
impl ScannConfig {
pub fn validate(&self) -> ScannResult<()> {
if self.dimension == 0 {
return Err(ScannFormatError::new(
"ScaNN vector dimension must be positive",
));
}
if self.tree_levels == 0 || self.tree_levels > MAX_SCANN_TREE_LEVELS {
return Err(ScannFormatError::new(format!(
"ScaNN tree_levels must be in 1..={MAX_SCANN_TREE_LEVELS}"
)));
}
if self.num_leaves < 2 || self.num_leaves > MAX_SCANN_LEAVES {
return Err(ScannFormatError::new(
"ScaNN num_leaves must be in 2..=30,000,000",
));
}
self.encoding.row_code_bytes(self.dimension)?;
Ok(())
}
pub fn minimum_training_sample(&self) -> ScannResult<u64> {
self.validate()?;
u64::from(self.num_leaves)
.checked_mul(MIN_PARTITION_TRAINING_POINTS_PER_LEAF)
.ok_or_else(|| ScannFormatError::new("ScaNN minimum training sample overflows u64"))
}
pub fn desired_training_sample(&self, observed_vectors: u64) -> ScannResult<u64> {
self.validate()?;
let desired = u64::from(self.num_leaves)
.checked_mul(PARTITION_TRAINING_POINTS_PER_CENTROID)
.ok_or_else(|| ScannFormatError::new("ScaNN training sample target overflows u64"))?
.max(DEFAULT_TRAINING_SAMPLE_SIZE);
Ok(observed_vectors.min(desired))
}
pub fn effective_training_threshold(&self) -> ScannResult<u64> {
self.validate()?;
Ok(MIN_POINTS_FOR_PARTITIONING.max(self.minimum_training_sample()?))
}
pub fn training_state(&self, observed_vectors: u64) -> ScannResult<ScannTrainingState> {
let required = self.effective_training_threshold()?;
Ok(if observed_vectors < required {
ScannTrainingState::AwaitingData {
observed: observed_vectors,
required,
}
} else {
ScannTrainingState::Ready {
observed: observed_vectors,
required,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config(encoding: ScannEncoding) -> ScannConfig {
ScannConfig {
dimension: 128,
tree_levels: 3,
num_leaves: 1_000_000,
encoding,
}
}
#[test]
fn scann_training_waits_for_partition_floor_and_minimum_leaf_sample() {
let config = config(ScannEncoding::BinaryHamming);
assert_eq!(config.effective_training_threshold().unwrap(), 8_000_000);
assert_eq!(
config.training_state(7_999_999).unwrap(),
ScannTrainingState::AwaitingData {
observed: 7_999_999,
required: 8_000_000,
}
);
assert!(matches!(
config.training_state(8_000_000).unwrap(),
ScannTrainingState::Ready { .. }
));
assert_eq!(
config.desired_training_sample(8_000_000).unwrap(),
8_000_000
);
let mut smaller = config;
smaller.num_leaves = 1_000;
assert_eq!(smaller.effective_training_threshold().unwrap(), 100_000);
assert_eq!(smaller.desired_training_sample(1_000_000).unwrap(), 200_000);
}
#[test]
fn binary_scann_requires_a_byte_aligned_bit_dimension() {
let mut config = config(ScannEncoding::BinaryHamming);
config.dimension = 127;
assert!(config.validate().is_err());
}
#[test]
fn ah_row_size_is_derived_without_rounding_down() {
let encoding = ScannEncoding::AsymmetricHash {
dimensions_per_block: 2,
bits_per_code: 4,
};
assert_eq!(encoding.row_code_bytes(5).unwrap(), 2);
assert_eq!(encoding.leaf_code_bytes(5, 32).unwrap(), 64);
assert_eq!(encoding.leaf_code_bytes(5, 33).unwrap(), 66);
assert_eq!(encoding.leaf_code_bytes(8, 32).unwrap(), 64);
assert_eq!(encoding.leaf_code_bytes(8, 64).unwrap(), 128);
}
}