lance_encoding/encodings/physical/
byte_stream_split.rs1use std::fmt::Debug;
59
60use crate::buffer::LanceBuffer;
61use crate::compression::MiniBlockDecompressor;
62use crate::compression_config::BssMode;
63use crate::data::{BlockInfo, DataBlock, FixedWidthDataBlock};
64use crate::encodings::logical::primitive::miniblock::{
65 MiniBlockChunk, MiniBlockCompressed, MiniBlockCompressionContext, MiniBlockCompressor,
66};
67use crate::format::ProtobufUtils21;
68use crate::format::pb21::CompressiveEncoding;
69use crate::statistics::{GetStat, Stat};
70use arrow_array::{cast::AsArray, types::UInt64Type};
71use lance_core::Result;
72
73#[derive(Debug, Clone)]
79pub struct ByteStreamSplitEncoder {
80 bits_per_value: usize,
81}
82
83impl ByteStreamSplitEncoder {
84 pub fn new(bits_per_value: usize) -> Self {
85 assert!(
86 bits_per_value == 32 || bits_per_value == 64,
87 "ByteStreamSplit only supports 32-bit (f32) or 64-bit (f64) values"
88 );
89 Self { bits_per_value }
90 }
91
92 fn bytes_per_value(&self) -> usize {
93 self.bits_per_value / 8
94 }
95
96 fn max_chunk_size(&self) -> usize {
97 match self.bits_per_value {
102 32 => 1024,
103 64 => 512,
104 _ => unreachable!("ByteStreamSplit only supports 32 or 64 bit values"),
105 }
106 }
107}
108
109impl MiniBlockCompressor for ByteStreamSplitEncoder {
110 fn compress(
111 &self,
112 _context: MiniBlockCompressionContext,
113 page: DataBlock,
114 ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
115 match page {
116 DataBlock::FixedWidth(data) => {
117 let num_values = data.num_values;
118 let bytes_per_value = self.bytes_per_value();
119
120 if num_values == 0 {
121 return Ok((
122 MiniBlockCompressed {
123 data: vec![],
124 chunks: vec![],
125 num_values: 0,
126 },
127 ProtobufUtils21::byte_stream_split(ProtobufUtils21::flat(
128 self.bits_per_value as u64,
129 None,
130 )),
131 ));
132 }
133
134 let total_size = num_values as usize * bytes_per_value;
135 let mut global_buffer = vec![0u8; total_size];
136
137 let mut chunks = Vec::new();
138 let data_slice = data.data.as_ref();
139 let mut processed_values = 0usize;
140 let max_chunk_size = self.max_chunk_size();
141
142 while processed_values < num_values as usize {
143 let chunk_size = (num_values as usize - processed_values).min(max_chunk_size);
144 let chunk_offset = processed_values * bytes_per_value;
145
146 for i in 0..chunk_size {
148 let src_offset = (processed_values + i) * bytes_per_value;
149 for j in 0..bytes_per_value {
150 let dst_offset = chunk_offset + j * chunk_size + i;
152 global_buffer[dst_offset] = data_slice[src_offset + j];
153 }
154 }
155
156 let chunk_bytes = chunk_size * bytes_per_value;
157 let log_num_values = if processed_values + chunk_size == num_values as usize {
158 0 } else {
160 chunk_size.ilog2() as u8
161 };
162
163 debug_assert!(chunk_bytes > 0);
164 chunks.push(MiniBlockChunk {
165 buffer_sizes: vec![chunk_bytes as u32],
166 log_num_values,
167 });
168
169 processed_values += chunk_size;
170 }
171
172 let data_buffers = vec![LanceBuffer::from(global_buffer)];
173
174 let encoding = ProtobufUtils21::byte_stream_split(ProtobufUtils21::flat(
176 self.bits_per_value as u64,
177 None,
178 ));
179
180 Ok((
181 MiniBlockCompressed {
182 data: data_buffers,
183 chunks,
184 num_values,
185 },
186 encoding,
187 ))
188 }
189 _ => Err(lance_core::Error::invalid_input_source(
190 "ByteStreamSplit encoding only supports FixedWidth data blocks".into(),
191 )),
192 }
193 }
194}
195
196#[derive(Debug)]
198pub struct ByteStreamSplitDecompressor {
199 bits_per_value: usize,
200}
201
202impl ByteStreamSplitDecompressor {
203 pub fn new(bits_per_value: usize) -> Self {
204 assert!(
205 bits_per_value == 32 || bits_per_value == 64,
206 "ByteStreamSplit only supports 32-bit (f32) or 64-bit (f64) values"
207 );
208 Self { bits_per_value }
209 }
210
211 fn bytes_per_value(&self) -> usize {
212 self.bits_per_value / 8
213 }
214}
215
216impl MiniBlockDecompressor for ByteStreamSplitDecompressor {
217 fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
218 if num_values == 0 {
219 return Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
220 data: LanceBuffer::empty(),
221 bits_per_value: self.bits_per_value as u64,
222 num_values: 0,
223 block_info: BlockInfo::new(),
224 }));
225 }
226
227 let bytes_per_value = self.bytes_per_value();
228 let total_bytes = num_values as usize * bytes_per_value;
229
230 if data.len() != 1 {
231 return Err(lance_core::Error::invalid_input_source(
232 format!(
233 "ByteStreamSplit decompression expects 1 buffer, but got {}",
234 data.len()
235 )
236 .into(),
237 ));
238 }
239
240 let input_buffer = &data[0];
241
242 if input_buffer.len() != total_bytes {
243 return Err(lance_core::Error::invalid_input_source(
244 format!(
245 "Expected {} bytes for decompression, but got {}",
246 total_bytes,
247 input_buffer.len()
248 )
249 .into(),
250 ));
251 }
252
253 let mut output = vec![0u8; total_bytes];
254
255 for i in 0..num_values as usize {
257 for j in 0..bytes_per_value {
258 let src_offset = j * num_values as usize + i;
259 output[i * bytes_per_value + j] = input_buffer[src_offset];
260 }
261 }
262
263 Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
264 data: LanceBuffer::from(output),
265 bits_per_value: self.bits_per_value as u64,
266 num_values,
267 block_info: BlockInfo::new(),
268 }))
269 }
270}
271
272pub fn should_use_bss(data: &FixedWidthDataBlock, mode: BssMode) -> bool {
274 if data.bits_per_value != 32 && data.bits_per_value != 64 {
278 return false;
279 }
280
281 let sensitivity = mode.to_sensitivity();
282
283 if sensitivity <= 0.0 {
285 return false;
286 }
287 if sensitivity >= 1.0 {
288 return true;
289 }
290
291 evaluate_entropy_for_bss(data, sensitivity)
293}
294
295fn evaluate_entropy_for_bss(data: &FixedWidthDataBlock, sensitivity: f32) -> bool {
297 let Some(entropy_stat) = data.get_stat(Stat::BytePositionEntropy) else {
299 return false; };
301
302 let entropies = entropy_stat.as_primitive::<UInt64Type>();
303 if entropies.is_empty() {
304 return false;
305 }
306
307 let sum: u64 = entropies.values().iter().sum();
309 let avg_entropy = sum as f64 / entropies.len() as f64 / 1000.0; let entropy_threshold = sensitivity as f64 * 8.0;
316
317 avg_entropy < entropy_threshold
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_round_trip_f32() {
328 let encoder = ByteStreamSplitEncoder::new(32);
329 let decompressor = ByteStreamSplitDecompressor::new(32);
330
331 let values: Vec<f32> = vec![
333 1.0,
334 2.5,
335 -3.7,
336 4.2,
337 0.0,
338 -0.0,
339 f32::INFINITY,
340 f32::NEG_INFINITY,
341 ];
342 let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
343
344 let data_block = DataBlock::FixedWidth(FixedWidthDataBlock {
345 data: LanceBuffer::from(bytes),
346 bits_per_value: 32,
347 num_values: values.len() as u64,
348 block_info: BlockInfo::new(),
349 });
350
351 let (compressed, _encoding) = encoder
353 .compress(MiniBlockCompressionContext::new(0, true, true), data_block)
354 .unwrap();
355
356 let decompressed = decompressor
358 .decompress(compressed.data, values.len() as u64)
359 .unwrap();
360 let DataBlock::FixedWidth(decompressed_fixed) = &decompressed else {
361 panic!("Expected FixedWidth DataBlock")
362 };
363
364 let result_bytes = decompressed_fixed.data.as_ref();
366 let result_values: Vec<f32> = result_bytes
367 .chunks_exact(4)
368 .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
369 .collect();
370
371 assert_eq!(values, result_values);
372 }
373
374 #[test]
375 fn test_round_trip_f64() {
376 let encoder = ByteStreamSplitEncoder::new(64);
377 let decompressor = ByteStreamSplitDecompressor::new(64);
378
379 let values: Vec<f64> = vec![
381 1.0,
382 2.5,
383 -3.7,
384 4.2,
385 0.0,
386 -0.0,
387 f64::INFINITY,
388 f64::NEG_INFINITY,
389 ];
390 let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
391
392 let data_block = DataBlock::FixedWidth(FixedWidthDataBlock {
393 data: LanceBuffer::from(bytes),
394 bits_per_value: 64,
395 num_values: values.len() as u64,
396 block_info: BlockInfo::new(),
397 });
398
399 let (compressed, _encoding) = encoder
401 .compress(MiniBlockCompressionContext::new(0, true, true), data_block)
402 .unwrap();
403
404 let decompressed = decompressor
406 .decompress(compressed.data, values.len() as u64)
407 .unwrap();
408 let DataBlock::FixedWidth(decompressed_fixed) = &decompressed else {
409 panic!("Expected FixedWidth DataBlock")
410 };
411
412 let result_bytes = decompressed_fixed.data.as_ref();
414 let result_values: Vec<f64> = result_bytes
415 .chunks_exact(8)
416 .map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap()))
417 .collect();
418
419 assert_eq!(values, result_values);
420 }
421
422 #[test]
423 fn test_empty_data() {
424 let encoder = ByteStreamSplitEncoder::new(32);
425 let decompressor = ByteStreamSplitDecompressor::new(32);
426
427 let data_block = DataBlock::FixedWidth(FixedWidthDataBlock {
428 data: LanceBuffer::empty(),
429 bits_per_value: 32,
430 num_values: 0,
431 block_info: BlockInfo::new(),
432 });
433
434 let (compressed, _encoding) = encoder
436 .compress(MiniBlockCompressionContext::new(0, true, true), data_block)
437 .unwrap();
438
439 let decompressed = decompressor.decompress(compressed.data, 0).unwrap();
441 let DataBlock::FixedWidth(decompressed_fixed) = &decompressed else {
442 panic!("Expected FixedWidth DataBlock")
443 };
444
445 assert_eq!(decompressed_fixed.num_values, 0);
446 assert_eq!(decompressed_fixed.data.len(), 0);
447 }
448}