// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The Lance Authors use std::sync::Arc; use arrow_buffer::bit_util::ceil; use bytes::Bytes; use futures::future::{BoxFuture, FutureExt}; use log::trace; use lance_bitpacking::BitPacking; use lance_core::{Error, Result}; use crate::buffer::LanceBuffer; use crate::data::BlockInfo; use crate::data::{DataBlock, FixedWidthDataBlock}; use crate::decoder::{PageScheduler, PrimitivePageDecoder}; use bytemuck::cast_slice; const LOG_ELEMS_PER_CHUNK: u8 = 10; const ELEMS_PER_CHUNK: u64 = 1 << LOG_ELEMS_PER_CHUNK; #[derive(Debug)] pub struct BitpackedForNonNegScheduler { compressed_bit_width: u64, uncompressed_bits_per_value: u64, buffer_offset: u64, } impl BitpackedForNonNegScheduler { pub fn new( compressed_bit_width: u64, uncompressed_bits_per_value: u64, buffer_offset: u64, ) -> Self { Self { compressed_bit_width, uncompressed_bits_per_value, buffer_offset, } } fn locate_chunk_start(&self, relative_row_num: u64) -> u64 { let chunk_size = ELEMS_PER_CHUNK * self.compressed_bit_width / 8; self.buffer_offset + (relative_row_num / ELEMS_PER_CHUNK * chunk_size) } fn locate_chunk_end(&self, relative_row_num: u64) -> u64 { let chunk_size = ELEMS_PER_CHUNK * self.compressed_bit_width / 8; self.buffer_offset + (relative_row_num / ELEMS_PER_CHUNK * chunk_size) + chunk_size } } impl PageScheduler for BitpackedForNonNegScheduler { fn schedule_ranges( &self, ranges: &[std::ops::Range], scheduler: &Arc, top_level_row: u64, ) -> BoxFuture<'static, Result>> { assert!(!ranges.is_empty()); let mut byte_ranges = vec![]; // map one bytes to multiple ranges, one bytes has at least one range corresponding to it let mut bytes_idx_to_range_indices = vec![]; let first_byte_range = std::ops::Range { start: self.locate_chunk_start(ranges[0].start), end: self.locate_chunk_end(ranges[0].end - 1), }; // the ranges are half-open byte_ranges.push(first_byte_range); bytes_idx_to_range_indices.push(vec![ranges[0].clone()]); for (i, range) in ranges.iter().enumerate().skip(1) { let this_start = self.locate_chunk_start(range.start); let this_end = self.locate_chunk_end(range.end - 1); // when the current range start is in the same chunk as the previous range's end, we colaesce this two bytes ranges // when the current range start is not in the same chunk as the previous range's end, we create a new bytes range if this_start == self.locate_chunk_start(ranges[i - 1].end - 1) { byte_ranges.last_mut().unwrap().end = this_end; bytes_idx_to_range_indices .last_mut() .unwrap() .push(range.clone()); } else { byte_ranges.push(this_start..this_end); bytes_idx_to_range_indices.push(vec![range.clone()]); } } trace!( "Scheduling I/O for {} ranges spread across byte range {}..{}", byte_ranges.len(), byte_ranges[0].start, byte_ranges.last().unwrap().end ); let bytes = scheduler.submit_request(byte_ranges.clone(), top_level_row); // copy the necessary data from `self` to move into the async block let compressed_bit_width = self.compressed_bit_width; let uncompressed_bits_per_value = self.uncompressed_bits_per_value; let num_rows = ranges.iter().map(|range| range.end - range.start).sum(); async move { let bytes = bytes.await?; let decompressed_output = bitpacked_for_non_neg_decode( compressed_bit_width, uncompressed_bits_per_value, &bytes, &bytes_idx_to_range_indices, num_rows, ); Ok(Box::new(BitpackedForNonNegPageDecoder { uncompressed_bits_per_value, decompressed_buf: decompressed_output, }) as Box) } .boxed() } } #[derive(Debug)] struct BitpackedForNonNegPageDecoder { // number of bits in the uncompressed value. E.g. this will be 32 for DataType::UInt32 uncompressed_bits_per_value: u64, decompressed_buf: LanceBuffer, } impl PrimitivePageDecoder for BitpackedForNonNegPageDecoder { fn decode(&self, rows_to_skip: u64, num_rows: u64) -> Result { if ![8, 16, 32, 64].contains(&self.uncompressed_bits_per_value) { return Err(Error::invalid_input_source("BitpackedForNonNegPageDecoder should only has uncompressed_bits_per_value of 8, 16, 32, or 64".into())); } let elem_size_in_bytes = self.uncompressed_bits_per_value / 8; Ok(DataBlock::FixedWidth(FixedWidthDataBlock { data: self.decompressed_buf.slice_with_length( (rows_to_skip * elem_size_in_bytes) as usize, (num_rows * elem_size_in_bytes) as usize, ), bits_per_value: self.uncompressed_bits_per_value, num_values: num_rows, block_info: BlockInfo::new(), })) } } macro_rules! bitpacked_decode { ($uncompressed_type:ty, $compressed_bit_width:expr, $data:expr, $bytes_idx_to_range_indices:expr, $num_rows:expr) => {{ let mut decompressed: Vec<$uncompressed_type> = Vec::with_capacity($num_rows as usize); let packed_chunk_size_in_byte: usize = (ELEMS_PER_CHUNK * $compressed_bit_width) as usize / 8; let mut decompress_chunk_buf = vec![0 as $uncompressed_type; ELEMS_PER_CHUNK as usize]; for (i, bytes) in $data.iter().enumerate() { let mut ranges_idx = 0; let mut curr_range_start = $bytes_idx_to_range_indices[i][0].start; let mut chunk_num = 0; while chunk_num * packed_chunk_size_in_byte < bytes.len() { // Copy for memory alignment // TODO: This copy should not be needed let chunk_in_u8: Vec = bytes[chunk_num * packed_chunk_size_in_byte..] [..packed_chunk_size_in_byte] .to_vec(); chunk_num += 1; let chunk = cast_slice(&chunk_in_u8); unsafe { BitPacking::unchecked_unpack( $compressed_bit_width as usize, chunk, &mut decompress_chunk_buf, ); } loop { // Case 1: All the elements after (curr_range_start % ELEMS_PER_CHUNK) inside this chunk are needed. let elems_after_curr_range_start_in_this_chunk = ELEMS_PER_CHUNK - curr_range_start % ELEMS_PER_CHUNK; if curr_range_start + elems_after_curr_range_start_in_this_chunk <= $bytes_idx_to_range_indices[i][ranges_idx].end { decompressed.extend_from_slice( &decompress_chunk_buf[(curr_range_start % ELEMS_PER_CHUNK) as usize..], ); curr_range_start += elems_after_curr_range_start_in_this_chunk; break; } else { // Case 2: Only part of the elements after (curr_range_start % ELEMS_PER_CHUNK) inside this chunk are needed. let elems_this_range_needed_in_this_chunk = ($bytes_idx_to_range_indices[i][ranges_idx].end - curr_range_start) .min(ELEMS_PER_CHUNK - curr_range_start % ELEMS_PER_CHUNK); decompressed.extend_from_slice( &decompress_chunk_buf[(curr_range_start % ELEMS_PER_CHUNK) as usize..] [..elems_this_range_needed_in_this_chunk as usize], ); if curr_range_start + elems_this_range_needed_in_this_chunk == $bytes_idx_to_range_indices[i][ranges_idx].end { ranges_idx += 1; if ranges_idx == $bytes_idx_to_range_indices[i].len() { break; } curr_range_start = $bytes_idx_to_range_indices[i][ranges_idx].start; } else { curr_range_start += elems_this_range_needed_in_this_chunk; } } } } } LanceBuffer::reinterpret_vec(decompressed) }}; } fn bitpacked_for_non_neg_decode( compressed_bit_width: u64, uncompressed_bits_per_value: u64, data: &[Bytes], bytes_idx_to_range_indices: &[Vec>], num_rows: u64, ) -> LanceBuffer { match uncompressed_bits_per_value { 8 => bitpacked_decode!( u8, compressed_bit_width, data, bytes_idx_to_range_indices, num_rows ), 16 => bitpacked_decode!( u16, compressed_bit_width, data, bytes_idx_to_range_indices, num_rows ), 32 => bitpacked_decode!( u32, compressed_bit_width, data, bytes_idx_to_range_indices, num_rows ), 64 => bitpacked_decode!( u64, compressed_bit_width, data, bytes_idx_to_range_indices, num_rows ), _ => unreachable!( "bitpacked_for_non_neg_decode only supports 8, 16, 32, 64 uncompressed_bits_per_value" ), } } // A physical scheduler for bitpacked buffers #[derive(Debug, Clone, Copy)] pub struct BitpackedScheduler { bits_per_value: u64, uncompressed_bits_per_value: u64, buffer_offset: u64, signed: bool, } impl BitpackedScheduler { pub fn new( bits_per_value: u64, uncompressed_bits_per_value: u64, buffer_offset: u64, signed: bool, ) -> Self { Self { bits_per_value, uncompressed_bits_per_value, buffer_offset, signed, } } } impl PageScheduler for BitpackedScheduler { fn schedule_ranges( &self, ranges: &[std::ops::Range], scheduler: &Arc, top_level_row: u64, ) -> BoxFuture<'static, Result>> { let mut min = u64::MAX; let mut max = 0; let mut buffer_bit_start_offsets: Vec = vec![]; let mut buffer_bit_end_offsets: Vec> = vec![]; let byte_ranges = ranges .iter() .map(|range| { let start_byte_offset = range.start * self.bits_per_value / 8; let mut end_byte_offset = range.end * self.bits_per_value / 8; if !(range.end * self.bits_per_value).is_multiple_of(8) { // If the end of the range is not byte-aligned, we need to read one more byte end_byte_offset += 1; let end_bit_offset = range.end * self.bits_per_value % 8; buffer_bit_end_offsets.push(Some(end_bit_offset as u8)); } else { buffer_bit_end_offsets.push(None); } let start_bit_offset = range.start * self.bits_per_value % 8; buffer_bit_start_offsets.push(start_bit_offset as u8); let start = self.buffer_offset + start_byte_offset; let end = self.buffer_offset + end_byte_offset; min = min.min(start); max = max.max(end); start..end }) .collect::>(); trace!( "Scheduling I/O for {} ranges spread across byte range {}..{}", byte_ranges.len(), min, max ); let bytes = scheduler.submit_request(byte_ranges, top_level_row); let bits_per_value = self.bits_per_value; let uncompressed_bits_per_value = self.uncompressed_bits_per_value; let signed = self.signed; async move { let bytes = bytes.await?; Ok(Box::new(BitpackedPageDecoder { buffer_bit_start_offsets, buffer_bit_end_offsets, bits_per_value, uncompressed_bits_per_value, signed, data: bytes, }) as Box) } .boxed() } } #[derive(Debug)] struct BitpackedPageDecoder { // bit offsets of the first value within each buffer buffer_bit_start_offsets: Vec, // bit offsets of the last value within each buffer. e.g. if there was a buffer // with 2 values, packed into 5 bits, this would be [Some(3)], indicating that // the bits from the 3rd->8th bit in the last byte shouldn't be decoded. buffer_bit_end_offsets: Vec>, // the number of bits used to represent a compressed value. E.g. if the max value // in the page was 7 (0b111), then this will be 3 bits_per_value: u64, // number of bits in the uncompressed value. E.g. this will be 32 for u32 uncompressed_bits_per_value: u64, // whether or not to use the msb as a sign bit during decoding signed: bool, data: Vec, } impl PrimitivePageDecoder for BitpackedPageDecoder { fn decode(&self, rows_to_skip: u64, num_rows: u64) -> Result { let num_bytes = self.uncompressed_bits_per_value / 8 * num_rows; let mut dest = vec![0; num_bytes as usize]; // current maximum supported bits per value = 64 debug_assert!(self.bits_per_value <= 64); let mut rows_to_skip = rows_to_skip; let mut rows_taken = 0; let byte_len = self.uncompressed_bits_per_value / 8; let mut dst_idx = 0; // index for current byte being written to destination buffer // create bit mask for source bits let mask = u64::MAX >> (64 - self.bits_per_value); for i in 0..self.data.len() { let src = &self.data[i]; let (mut src_idx, mut src_offset) = match compute_start_offset( rows_to_skip, src.len(), self.bits_per_value, self.buffer_bit_start_offsets[i], self.buffer_bit_end_offsets[i], ) { StartOffset::SkipFull(rows_to_skip_here) => { rows_to_skip -= rows_to_skip_here; continue; } StartOffset::SkipSome(buffer_start_offset) => ( buffer_start_offset.index, buffer_start_offset.bit_offset as u64, ), }; while src_idx < src.len() && rows_taken < num_rows { rows_taken += 1; let mut curr_mask = mask; // copy mask // current source byte being written to destination let mut curr_src = src[src_idx] & (curr_mask << src_offset) as u8; // how many bits from the current source value have been written to destination let mut src_bits_written = 0; // the offset within the current destination byte to write to let mut dst_offset = 0; let is_negative = is_encoded_item_negative( src, src_idx, src_offset, self.bits_per_value as usize, ); while src_bits_written < self.bits_per_value { // write bits from current source byte into destination dest[dst_idx] += (curr_src >> src_offset) << dst_offset; let bits_written = (self.bits_per_value - src_bits_written) .min(8 - src_offset) .min(8 - dst_offset); src_bits_written += bits_written; dst_offset += bits_written; src_offset += bits_written; curr_mask >>= bits_written; if dst_offset == 8 { dst_idx += 1; dst_offset = 0; } if src_offset == 8 { src_idx += 1; src_offset = 0; if src_idx == src.len() { break; } curr_src = src[src_idx] & curr_mask as u8; } } // if the type is signed, need to pad out the rest of the byte with 1s let mut negative_padded_current_byte = false; if self.signed && is_negative && dst_offset > 0 { negative_padded_current_byte = true; while dst_offset < 8 { dest[dst_idx] |= 1 << dst_offset; dst_offset += 1; } } // advance destination offset to the next location // note that we don't need to do this if we wrote the full number of bits // because source index would have been advanced by the inner loop above if self.uncompressed_bits_per_value != self.bits_per_value { let partial_bytes_written = ceil(self.bits_per_value as usize, 8); // we also want to move one location to the next location in destination, // unless we wrote something byte-aligned in which case the logic above // would have already advanced dst_idx let mut to_next_byte = 1; if self.bits_per_value.is_multiple_of(8) { to_next_byte = 0; } let next_dst_idx = dst_idx + byte_len as usize - partial_bytes_written + to_next_byte; // pad remaining bytes with 1 for negative signed numbers if self.signed && is_negative { if !negative_padded_current_byte { dest[dst_idx] = 0xFF; } for i in dest.iter_mut().take(next_dst_idx).skip(dst_idx + 1) { *i = 0xFF; } } dst_idx = next_dst_idx; } // If we've reached the last byte, there may be some extra bits from the // next value outside the range. We don't want to be taking those. if let Some(buffer_bit_end_offset) = self.buffer_bit_end_offsets[i] && src_idx == src.len() - 1 && src_offset >= buffer_bit_end_offset as u64 { break; } } } Ok(DataBlock::FixedWidth(FixedWidthDataBlock { data: LanceBuffer::from(dest), bits_per_value: self.uncompressed_bits_per_value, num_values: num_rows, block_info: BlockInfo::new(), })) } } fn is_encoded_item_negative(src: &Bytes, src_idx: usize, src_offset: u64, num_bits: usize) -> bool { let mut last_byte_idx = src_idx + ((src_offset as usize + num_bits) / 8); let shift_amount = (src_offset as usize + num_bits) % 8; let shift_amount = if shift_amount == 0 { last_byte_idx -= 1; 7 } else { shift_amount - 1 }; let last_byte = src[last_byte_idx]; let sign_bit_mask = 1 << shift_amount; let sign_bit = last_byte & sign_bit_mask; sign_bit > 0 } #[derive(Debug, PartialEq)] struct BufferStartOffset { index: usize, bit_offset: u8, } #[derive(Debug, PartialEq)] enum StartOffset { // skip the full buffer. The value is how many rows are skipped // by skipping the full buffer (e.g., # rows in buffer) SkipFull(u64), // skip to some start offset in the buffer SkipSome(BufferStartOffset), } /// compute how far ahead in this buffer should we skip ahead and start reading /// /// * `rows_to_skip` - how many rows to skip /// * `buffer_len` - length buf buffer (in bytes) /// * `bits_per_value` - number of bits used to represent a single bitpacked value /// * `buffer_start_bit_offset` - offset of the start of the first value within the /// buffer's first byte /// * `buffer_end_bit_offset` - end bit of the last value within the buffer. Can be /// `None` if the end of the last value is byte aligned with end of buffer. fn compute_start_offset( rows_to_skip: u64, buffer_len: usize, bits_per_value: u64, buffer_start_bit_offset: u8, buffer_end_bit_offset: Option, ) -> StartOffset { let rows_in_buffer = rows_in_buffer( buffer_len, bits_per_value, buffer_start_bit_offset, buffer_end_bit_offset, ); if rows_to_skip >= rows_in_buffer { return StartOffset::SkipFull(rows_in_buffer); } let start_bit = rows_to_skip * bits_per_value + buffer_start_bit_offset as u64; let start_byte = start_bit / 8; StartOffset::SkipSome(BufferStartOffset { index: start_byte as usize, bit_offset: (start_bit % 8) as u8, }) } /// calculates the number of rows in a buffer fn rows_in_buffer( buffer_len: usize, bits_per_value: u64, buffer_start_bit_offset: u8, buffer_end_bit_offset: Option, ) -> u64 { let mut bits_in_buffer = (buffer_len * 8) as u64 - buffer_start_bit_offset as u64; // if the end of the last value of the buffer isn't byte aligned, subtract the // end offset from the total number of bits in buffer if let Some(buffer_end_bit_offset) = buffer_end_bit_offset { bits_in_buffer -= (8 - buffer_end_bit_offset) as u64; } bits_in_buffer / bits_per_value } #[cfg(test)] pub mod test { use super::*; use crate::BufferScheduler; use arrow_buffer::ArrowNativeType; fn fixed_width_values(block: DataBlock) -> Vec { let DataBlock::FixedWidth(FixedWidthDataBlock { data, .. }) = block else { panic!("expected fixed-width data"); }; data.borrow_to_typed_slice::().to_vec() } #[test] fn test_rows_in_buffer() { let test_cases = vec![ (5usize, 5u64, 0u8, None, 8u64), (2, 3, 0, Some(5), 4), (2, 3, 7, Some(6), 2), ]; for ( buffer_len, bits_per_value, buffer_start_bit_offset, buffer_end_bit_offset, expected, ) in test_cases { let result = rows_in_buffer( buffer_len, bits_per_value, buffer_start_bit_offset, buffer_end_bit_offset, ); assert_eq!(expected, result); } } #[test] fn test_compute_start_offset() { let result = compute_start_offset(0, 5, 5, 0, None); assert_eq!( StartOffset::SkipSome(BufferStartOffset { index: 0, bit_offset: 0 }), result ); let result = compute_start_offset(10, 5, 5, 0, None); assert_eq!(StartOffset::SkipFull(8), result); } #[test_log::test(tokio::test)] async fn test_bitpacked_scheduler_non_byte_aligned_ranges() { // Legacy 5-bit LSB-first encoding of values 1..=10, with a two-byte prefix. let data = Bytes::from_static(&[0xFA, 0xCE, 0x41, 0x0C, 0x52, 0xCC, 0x41, 0x49, 0x01]); let io: Arc = Arc::new(BufferScheduler::new(data)); let scheduler = BitpackedScheduler::new(5, 16, 2, false); let decoder = scheduler .schedule_ranges(&[1..3, 5..9], &io, 0) .await .unwrap(); let decoded = decoder.decode(3, 3).unwrap(); // The scheduled rows are [2, 3, 6, 7, 8, 9]. Skipping across the first // Bytes response must begin in the middle of the second response. assert_eq!(fixed_width_values::(decoded), vec![7, 8, 9]); } #[test_log::test(tokio::test)] async fn test_bitpacked_scheduler_signed_byte_aligned() { // Legacy 16-bit little-endian encoding of [513, -1, 4660, -32768]. let data = Bytes::from_static(&[0x01, 0x02, 0xFF, 0xFF, 0x34, 0x12, 0x00, 0x80]); let io: Arc = Arc::new(BufferScheduler::new(data)); let scheduler = BitpackedScheduler::new(16, 32, 0, true); let decoder = scheduler.schedule_ranges(&[1..4], &io, 0).await.unwrap(); let decoded = decoder.decode(0, 3).unwrap(); assert_eq!(fixed_width_values::(decoded), vec![-1, 4660, -32768]); } #[test_log::test(tokio::test)] async fn test_bitpacked_for_non_negative_scheduler_chunk_boundary() { const BIT_WIDTH: usize = 7; const NUM_CHUNKS: usize = 2; const WORDS_PER_CHUNK: usize = ELEMS_PER_CHUNK as usize * BIT_WIDTH / u32::BITS as usize; let values = (0..ELEMS_PER_CHUNK as usize * NUM_CHUNKS) .map(|index| ((index * 13 + 7) % 127) as u32) .collect::>(); let mut packed = vec![0_u32; WORDS_PER_CHUNK * NUM_CHUNKS]; for chunk_index in 0..NUM_CHUNKS { let value_start = chunk_index * ELEMS_PER_CHUNK as usize; let word_start = chunk_index * WORDS_PER_CHUNK; // SAFETY: Both slices have the exact input and output lengths required // for one 1,024-value chunk at this bit width. unsafe { BitPacking::unchecked_pack( BIT_WIDTH, &values[value_start..value_start + ELEMS_PER_CHUNK as usize], &mut packed[word_start..word_start + WORDS_PER_CHUNK], ); } } let mut data = vec![0xFA, 0xCE, 0x01]; data.extend_from_slice(cast_slice(&packed)); let io: Arc = Arc::new(BufferScheduler::new(Bytes::from(data))); let scheduler = BitpackedForNonNegScheduler::new(BIT_WIDTH as u64, 32, 3); let decoder = scheduler .schedule_ranges(&[1020..1028, 1530..1533], &io, 0) .await .unwrap(); let decoded = decoder.decode(2, 7).unwrap(); let expected = (1022..1028) .chain(1530..1531) .map(|index| values[index]) .collect::>(); assert_eq!(fixed_width_values::(decoded), expected); } }