Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions diskann-quantization/src/bits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub mod distances;
pub use length::{Dynamic, Length, Static};
pub use ptr::{AsMutPtr, AsPtr, MutSlicePtr, SlicePtr};
pub use slice::{
Binary, BitSlice, BitSliceBase, BitTranspose, BoxedBitSlice, ConstructionError, Dense,
EncodingError, GetError, IndexOutOfBounds, MutBitSlice, PermutationStrategy, Representation,
SetError, Unsigned,
Binary, BitSlice, BitSliceBase, BitTranspose, BoxedBitSlice, ConstructionError,
DecodeLengthMismatch, Dense, EncodingError, GetError, IndexOutOfBounds, MutBitSlice,
PermutationStrategy, Representation, SetError, Unsigned,
};
150 changes: 150 additions & 0 deletions diskann-quantization/src/bits/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ unsafe impl PermutationStrategy<4> for BitTranspose {
// Errors //
////////////

/// The destination cannot hold exactly the logical decoded values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("decoded output has {output} elements, expected {input}")]
pub struct DecodeLengthMismatch {
pub input: usize,
pub output: usize,
}

#[derive(Debug, Error, Clone, Copy)]
#[error("input span has length {got} bytes but expected {expected}")]
pub struct ConstructionError {
Expand Down Expand Up @@ -732,6 +740,64 @@ where
}
}

impl<const NBITS: usize, Repr, Ptr, Len> BitSliceBase<NBITS, Repr, Ptr, Dense, Len>
where
Repr: Representation<NBITS>,
Ptr: AsPtr<Type = u8>,
Len: Length,
{
/// Decode in logical order, applying `convert` once to each value.
///
/// Unused bits in the last byte are ignored. Unlike repeated `get` calls, bulk
/// decoding can reuse each packed byte across adjacent values.
///
/// # Errors
///
/// Returns [`DecodeLengthMismatch`] without writing or calling `convert` if the
/// output length differs from the logical input length.
#[inline]
pub fn decode_into<T>(
&self,
output: &mut [T],
mut convert: impl FnMut(i64) -> T,
) -> Result<(), DecodeLengthMismatch> {
const { assert!(NBITS > 0 && NBITS <= 8) };
if output.len() != self.len() {
return Err(DecodeLengthMismatch {
input: self.len(),
output: output.len(),
});
}
let packed = self.as_slice();
let divisor = 1usize << NBITS.trailing_zeros();
let codes_per_block = 8 / divisor;
let bytes_per_block = NBITS / divisor;
let blocks = output.len() / codes_per_block;
let (output, tail) = output.split_at_mut(blocks * codes_per_block);
let (packed, rest) = packed.split_at(blocks * bytes_per_block);
let mask = (1u64 << NBITS) - 1;
// Use the smallest byte-aligned group: 8 / gcd(NBITS, 8) codes.
for (block, bytes) in output
.chunks_exact_mut(codes_per_block)
.zip(packed.chunks_exact(bytes_per_block))
{
let mut word = [0u8; 8];
word[..bytes_per_block].copy_from_slice(bytes);
let word = u64::from_le_bytes(word);
for (i, out) in block.iter_mut().enumerate() {
*out = convert(Repr::decode(((word >> (i * NBITS)) & mask) as u8));
}
}
let mut word = [0u8; 8];
word[..rest.len()].copy_from_slice(rest);
let word = u64::from_le_bytes(word);
for (i, out) in tail.iter_mut().enumerate() {
*out = convert(Repr::decode(((word >> (i * NBITS)) & mask) as u8));
}
Ok(())
}
}

impl<const NBITS: usize, Repr, Perm, Len>
BitSliceBase<NBITS, Repr, Poly<[u8], GlobalAllocator>, Perm, Len>
where
Expand Down Expand Up @@ -947,6 +1013,90 @@ mod tests {
use super::*;
use crate::{bits::Static, test_util::AlwaysFails};

fn check_bulk_decoding<const NBITS: usize>()
where
Unsigned: Representation<NBITS>,
{
for len in [0, 1, 2, 3, 7, 8, 9, 15, 16, 17, 255, 256, 257, 513] {
for offset in [0, 1, 7] {
let byte_count = BitSlice::<NBITS, Unsigned>::bytes_for(len);
let mut bytes = vec![0xff; offset + byte_count];
let bytes = &mut bytes[offset..];
let mut codes = MutBitSlice::<NBITS, Unsigned>::new(bytes, len).unwrap();
for i in 0..len {
codes
.set(i, ((i * 37 + i / 2) & ((1 << NBITS) - 1)) as i64)
.unwrap();
}
let expected: Vec<_> = (0..len).map(|i| codes.get(i).unwrap()).collect();
let mut guarded = vec![i64::MIN; len + 2];
let mut calls = 0;
codes
.decode_into(&mut guarded[1..=len], |value| {
assert_eq!(value, expected[calls]);
calls += 1;
value
})
.unwrap();
assert_eq!(calls, len);
assert_eq!(&guarded[1..=len], expected);
assert_eq!(guarded[0], i64::MIN);
assert_eq!(guarded[len + 1], i64::MIN);
}
}
}

#[test]
fn bulk_decoding_preserves_order_tails_and_output_bounds() {
check_bulk_decoding::<1>();
check_bulk_decoding::<2>();
check_bulk_decoding::<3>();
check_bulk_decoding::<4>();
check_bulk_decoding::<5>();
check_bulk_decoding::<6>();
check_bulk_decoding::<7>();
check_bulk_decoding::<8>();
}

#[test]
fn bulk_decoding_applies_representation_and_conversion() {
let codes =
BitSlice::<1, Binary, Dense, Static<3>>::new(&[0b101u8][..], Static::<3>).unwrap();
let mut output = [0i16; 3];
codes
.decode_into(&mut output, |value| (value * 3) as i16)
.unwrap();
assert_eq!(output, [3, -3, 3]);
let codes = BoxedBitSlice::<4, Unsigned>::new_boxed(2);
let mut output = [1u8; 2];
codes.decode_into(&mut output, |value| value as u8).unwrap();
assert_eq!(output, [0, 0]);
}

#[test]
fn bulk_decoding_length_mismatch_does_not_write_or_convert() {
let codes = BitSlice::<4, Unsigned>::new(&[0x21u8][..], 2).unwrap();
for len in [0, 1, 3] {
let mut output = vec![99; len];
let mut calls = 0;
let error = codes
.decode_into(&mut output, |value| {
calls += 1;
value
})
.unwrap_err();
assert_eq!(
error,
DecodeLengthMismatch {
input: 2,
output: len
}
);
assert_eq!(calls, 0);
assert_eq!(output, vec![99; len]);
}
}

////////////
// Errors //
////////////
Expand Down
5 changes: 5 additions & 0 deletions diskann-quantization/src/matrix_kernels/maxsim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
pub(crate) mod packed_f32_x_unpacked_f16;
pub(crate) mod packed_f32_x_unpacked_f32;
pub(crate) mod packed_i8_x_unpacked_i8;
#[cfg_attr(
not(test),
expect(dead_code, reason = "TODO: will integrate in the next PR")
)]
pub(crate) mod packed_minmax_x_unpacked_minmax;

#[cfg(test)]
mod test;
Loading
Loading