1use fastlanes::BitPacking;
5use itertools::Itertools;
6use num_traits::PrimInt;
7use vortex_array::ArrayView;
8use vortex_array::ExecutionCtx;
9use vortex_array::IntoArray;
10use vortex_array::arrays::Primitive;
11use vortex_array::arrays::PrimitiveArray;
12use vortex_array::arrays::primitive::PrimitiveArrayExt;
13use vortex_array::buffer::BufferHandle;
14use vortex_array::dtype::IntegerPType;
15use vortex_array::dtype::NativePType;
16use vortex_array::dtype::PType;
17use vortex_array::match_each_integer_ptype;
18use vortex_array::match_each_unsigned_integer_ptype;
19use vortex_array::patches::Patches;
20use vortex_array::validity::Validity;
21use vortex_buffer::Buffer;
22use vortex_buffer::BufferMut;
23use vortex_buffer::ByteBuffer;
24use vortex_error::VortexExpect;
25use vortex_error::VortexResult;
26use vortex_error::vortex_bail;
27use vortex_mask::AllOr;
28use vortex_mask::Mask;
29
30use crate::BitPacked;
31use crate::BitPackedArray;
32use crate::bitpack_decompress;
33
34pub fn bitpack_to_best_bit_width(
35 array: &PrimitiveArray,
36 ctx: &mut ExecutionCtx,
37) -> VortexResult<BitPackedArray> {
38 let bit_width_freq = bit_width_histogram(array.as_view(), ctx)?;
39 let best_bit_width = find_best_bit_width(array.ptype(), &bit_width_freq)?;
40 bitpack_encode(array, best_bit_width, Some(&bit_width_freq), ctx)
41}
42
43#[expect(unused_comparisons, clippy::absurd_extreme_comparisons)]
44pub fn bitpack_encode(
45 array: &PrimitiveArray,
46 bit_width: u8,
47 bit_width_freq: Option<&[usize]>,
48 ctx: &mut ExecutionCtx,
49) -> VortexResult<BitPackedArray> {
50 let bit_width_freq = match bit_width_freq {
51 Some(freq) => freq,
52 None => &bit_width_histogram(array.as_view(), ctx)?,
53 };
54
55 if array.ptype().is_signed_int() {
57 let has_negative_values = match_each_integer_ptype!(array.ptype(), |P| {
58 array.statistics().compute_min::<P>(ctx).unwrap_or_default() < 0
59 });
60 if has_negative_values {
61 vortex_bail!(InvalidArgument: "cannot bitpack_encode array containing negative integers")
62 }
63 }
64
65 let num_exceptions = bitpack_decompress::count_exceptions(bit_width, bit_width_freq);
66
67 if bit_width >= array.ptype().bit_width() as u8 {
68 vortex_bail!(
70 InvalidArgument: "Cannot pack - specified bit width {bit_width} >= {}",
71 array.ptype().bit_width()
72 )
73 }
74
75 let packed = unsafe { bitpack_unchecked(array, bit_width) };
77 let patches = (num_exceptions > 0)
78 .then(|| gather_patches(array, bit_width, num_exceptions, ctx))
79 .transpose()?
80 .flatten();
81
82 let bitpacked = BitPacked::try_new(
83 BufferHandle::new_host(packed),
84 array.ptype(),
85 array.validity()?,
86 patches,
87 bit_width,
88 array.len(),
89 0,
90 )?;
91 bitpacked.statistics().inherit_from(array.statistics());
92 Ok(bitpacked)
93}
94
95pub unsafe fn bitpack_encode_unchecked(
104 array: PrimitiveArray,
105 bit_width: u8,
106) -> VortexResult<BitPackedArray> {
107 let packed = unsafe { bitpack_unchecked(&array, bit_width) };
109
110 let arr_ref = array.clone().into_array();
111 let bitpacked = BitPacked::try_new(
112 BufferHandle::new_host(packed),
113 array.ptype(),
114 array.validity()?,
115 None,
116 bit_width,
117 array.len(),
118 0,
119 )
120 .vortex_expect("bitpacked array construction should succeed");
121 bitpacked.statistics().inherit_from(arr_ref.statistics());
122 Ok(bitpacked)
123}
124
125pub unsafe fn bitpack_unchecked(parray: &PrimitiveArray, bit_width: u8) -> ByteBuffer {
137 let parray = parray.reinterpret_cast(parray.ptype().to_unsigned());
138 match_each_unsigned_integer_ptype!(parray.ptype(), |P| {
139 bitpack_primitive(parray.as_slice::<P>(), bit_width).into_byte_buffer()
140 })
141}
142
143pub fn bitpack_primitive<T: NativePType + BitPacking>(array: &[T], bit_width: u8) -> Buffer<T> {
147 if bit_width == 0 {
148 return Buffer::<T>::empty();
149 }
150 let bit_width = bit_width as usize;
151
152 let num_chunks = array.len().div_ceil(1024);
154 let num_full_chunks = array.len() / 1024;
155 let packed_len = 128 * bit_width / size_of::<T>();
156 let mut output = BufferMut::<T>::with_capacity(num_chunks * packed_len);
162
163 (0..num_full_chunks).for_each(|i| {
165 let start_elem = i * 1024;
166 let output_len = output.len();
167 unsafe {
168 output.set_len(output_len + packed_len);
169 BitPacking::unchecked_pack(
170 bit_width,
171 &array[start_elem..][..1024],
172 &mut output[output_len..][..packed_len],
173 );
174 };
175 });
176
177 if num_chunks != num_full_chunks {
179 let last_chunk_size = array.len() % 1024;
180 let mut last_chunk: [T; 1024] = [T::zero(); 1024];
181 last_chunk[..last_chunk_size].copy_from_slice(&array[array.len() - last_chunk_size..]);
182
183 let output_len = output.len();
184 unsafe {
185 output.set_len(output_len + packed_len);
186 BitPacking::unchecked_pack(
187 bit_width,
188 &last_chunk,
189 &mut output[output_len..][..packed_len],
190 );
191 };
192 }
193
194 output.freeze()
195}
196
197pub fn gather_patches(
198 parray: &PrimitiveArray,
199 bit_width: u8,
200 num_exceptions_hint: usize,
201 ctx: &mut ExecutionCtx,
202) -> VortexResult<Option<Patches>> {
203 let patch_validity = match parray.validity()? {
204 Validity::NonNullable => Validity::NonNullable,
205 _ => Validity::AllValid,
206 };
207
208 let array_len = parray.len();
209 let validity_mask = parray
210 .as_ref()
211 .validity()?
212 .execute_mask(parray.len(), ctx)?;
213
214 let patches = if array_len < u8::MAX as usize {
215 match_each_integer_ptype!(parray.ptype(), |T| {
216 gather_patches_impl::<T, u8>(
217 parray.as_slice::<T>(),
218 bit_width,
219 num_exceptions_hint,
220 patch_validity,
221 validity_mask,
222 )?
223 })
224 } else if array_len < u16::MAX as usize {
225 match_each_integer_ptype!(parray.ptype(), |T| {
226 gather_patches_impl::<T, u16>(
227 parray.as_slice::<T>(),
228 bit_width,
229 num_exceptions_hint,
230 patch_validity,
231 validity_mask,
232 )?
233 })
234 } else if array_len < u32::MAX as usize {
235 match_each_integer_ptype!(parray.ptype(), |T| {
236 gather_patches_impl::<T, u32>(
237 parray.as_slice::<T>(),
238 bit_width,
239 num_exceptions_hint,
240 patch_validity,
241 validity_mask,
242 )?
243 })
244 } else {
245 match_each_integer_ptype!(parray.ptype(), |T| {
246 gather_patches_impl::<T, u64>(
247 parray.as_slice::<T>(),
248 bit_width,
249 num_exceptions_hint,
250 patch_validity,
251 validity_mask,
252 )?
253 })
254 };
255
256 Ok(patches)
257}
258
259fn gather_patches_impl<T, P>(
260 data: &[T],
261 bit_width: u8,
262 num_exceptions_hint: usize,
263 patch_validity: Validity,
264 validity_mask: Mask,
265) -> VortexResult<Option<Patches>>
266where
267 T: PrimInt + NativePType,
268 P: IntegerPType,
269{
270 let mut indices: BufferMut<P> = BufferMut::with_capacity(num_exceptions_hint);
271 let mut values: BufferMut<T> = BufferMut::with_capacity(num_exceptions_hint);
272
273 let total_chunks = data.len().div_ceil(1024);
274 let mut chunk_offsets: BufferMut<u64> = BufferMut::with_capacity(total_chunks);
275
276 for ((idx, value), valid) in data.iter().enumerate().zip(validity_mask.iter()) {
277 if (idx % 1024) == 0 {
278 chunk_offsets.push(values.len() as u64);
280 }
281
282 if (value.leading_zeros() as usize) < T::PTYPE.bit_width() - bit_width as usize && valid {
283 indices.push(P::from(idx).vortex_expect("cast index from usize"));
284 values.push(*value);
285 }
286 }
287
288 if indices.is_empty() {
289 Ok(None)
290 } else {
291 Ok(Some(Patches::new(
292 data.len(),
293 0,
294 indices.into_array(),
295 PrimitiveArray::new(values, patch_validity).into_array(),
296 Some(chunk_offsets.into_array()),
297 )?))
298 }
299}
300
301pub fn bit_width_histogram(
302 array: ArrayView<'_, Primitive>,
303 ctx: &mut ExecutionCtx,
304) -> VortexResult<Vec<usize>> {
305 match_each_integer_ptype!(array.ptype(), |P| {
306 bit_width_histogram_typed::<P>(array, ctx)
307 })
308}
309
310fn bit_width_histogram_typed<T: NativePType + PrimInt>(
311 array: ArrayView<'_, Primitive>,
312 ctx: &mut ExecutionCtx,
313) -> VortexResult<Vec<usize>> {
314 let bit_width: fn(T) -> usize =
315 |v: T| (8 * size_of::<T>()) - (PrimInt::leading_zeros(v) as usize);
316
317 let mut bit_widths = vec![0usize; size_of::<T>() * 8 + 1];
318 match array
319 .validity()?
320 .execute_mask(array.as_ref().len(), ctx)?
321 .bit_buffer()
322 {
323 AllOr::All => {
324 for v in array.as_slice::<T>() {
326 bit_widths[bit_width(*v)] += 1;
327 }
328 }
329 AllOr::None => {
330 bit_widths[0] = array.len();
332 }
333 AllOr::Some(buffer) => {
334 for (is_valid, v) in buffer.iter().zip_eq(array.as_slice::<T>()) {
336 if is_valid {
337 bit_widths[bit_width(*v)] += 1;
338 } else {
339 bit_widths[0] += 1;
340 }
341 }
342 }
343 }
344
345 Ok(bit_widths)
346}
347
348pub fn find_best_bit_width(ptype: PType, bit_width_freq: &[usize]) -> VortexResult<u8> {
349 best_bit_width(bit_width_freq, bytes_per_exception(ptype))
350}
351
352#[expect(
355 clippy::cast_possible_truncation,
356 reason = "bit_width is bounded by check above and result fits in u8"
357)]
358fn best_bit_width(bit_width_freq: &[usize], bytes_per_exception: usize) -> VortexResult<u8> {
359 if bit_width_freq.len() > u8::MAX as usize {
360 vortex_bail!("Too many bit widths");
361 }
362
363 let len: usize = bit_width_freq.iter().sum();
364 let mut num_packed = 0;
365 let mut best_cost = len * bytes_per_exception;
366 let mut best_width = 0;
367 for (bit_width, freq) in bit_width_freq.iter().enumerate() {
368 let packed_cost = (bit_width * len).div_ceil(8); num_packed += *freq;
371 let exceptions_cost = (len - num_packed) * bytes_per_exception;
372
373 let cost = exceptions_cost + packed_cost;
374 if cost < best_cost {
375 best_cost = cost;
376 best_width = bit_width;
377 }
378 }
379
380 Ok(best_width as u8)
381}
382
383fn bytes_per_exception(ptype: PType) -> usize {
384 ptype.byte_width() + 4
385}
386
387#[cfg(feature = "_test-harness")]
388pub mod test_harness {
389 use rand::RngExt;
390 use rand::rngs::StdRng;
391 use vortex_array::ArrayRef;
392 use vortex_array::ExecutionCtx;
393 use vortex_array::IntoArray;
394 use vortex_array::arrays::PrimitiveArray;
395 use vortex_array::validity::Validity;
396 use vortex_buffer::BufferMut;
397 use vortex_error::VortexResult;
398
399 use super::bitpack_encode;
400
401 pub fn make_array(
402 rng: &mut StdRng,
403 len: usize,
404 fraction_patches: f64,
405 fraction_null: f64,
406 ctx: &mut ExecutionCtx,
407 ) -> VortexResult<ArrayRef> {
408 let values = (0..len)
409 .map(|_| {
410 let mut v = rng.random_range(0..100i32);
411 if rng.random_bool(fraction_patches) {
412 v += 1 << 13
413 };
414 v
415 })
416 .collect::<BufferMut<i32>>();
417
418 let values = if fraction_null == 0.0 {
419 values.into_array().execute::<PrimitiveArray>(ctx)?
420 } else {
421 let validity = Validity::from_iter((0..len).map(|_| !rng.random_bool(fraction_null)));
422 PrimitiveArray::new(values, validity)
423 };
424
425 bitpack_encode(&values, 12, None, ctx).map(|a| a.into_array())
426 }
427}
428
429#[cfg(test)]
430mod test {
431 use std::sync::LazyLock;
432
433 use rand::SeedableRng;
434 use rand::rngs::StdRng;
435 use vortex_array::VortexSessionExecute;
436 use vortex_array::arrays::ChunkedArray;
437 use vortex_array::assert_arrays_eq;
438 use vortex_array::builders::ArrayBuilder;
439 use vortex_array::builders::PrimitiveBuilder;
440 use vortex_buffer::Buffer;
441 use vortex_error::VortexError;
442 use vortex_error::vortex_err;
443 use vortex_session::VortexSession;
444
445 use super::*;
446 use crate::BitPackedData;
447 use crate::bitpack_compress::test_harness::make_array;
448 use crate::bitpacking::array::BitPackedArrayExt;
449
450 static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
451 let session = vortex_array::array_session();
452 crate::initialize(&session);
453 session
454 });
455
456 #[test]
457 fn test_best_bit_width() {
458 let freq = vec![0, 10, 20, 15, 1, 0, 0, 0];
460 assert_eq!(
462 best_bit_width(&freq, bytes_per_exception(PType::U8)).unwrap(),
463 3
464 );
465 }
466
467 #[test]
468 fn null_patches() {
469 let mut ctx = SESSION.create_execution_ctx();
470 let valid_values = (0..24).map(|v| v < 1 << 4).collect::<Vec<_>>();
471 let values = PrimitiveArray::new(
472 (0u32..24).collect::<Buffer<_>>(),
473 Validity::from_iter(valid_values),
474 );
475 assert!(values.ptype().is_unsigned_int());
476 let compressed = BitPackedData::encode(&values.into_array(), 4, &mut ctx).unwrap();
477 assert!(compressed.patches().is_none());
478 assert_eq!(
479 (0..(1 << 4)).collect::<Vec<_>>(),
480 compressed
481 .as_ref()
482 .validity()
483 .unwrap()
484 .execute_mask(compressed.as_ref().len(), &mut ctx)
485 .unwrap()
486 .to_bit_buffer()
487 .set_indices()
488 .collect::<Vec<_>>()
489 )
490 }
491
492 #[test]
493 fn compress_signed_fails() {
494 let mut ctx = SESSION.create_execution_ctx();
495 let values: Buffer<i64> = (-500..500).collect();
496 let array = PrimitiveArray::new(values, Validity::AllValid);
497 assert!(array.ptype().is_signed_int());
498
499 let err = BitPackedData::encode(&array.into_array(), 1024u32.ilog2() as u8, &mut ctx)
500 .unwrap_err();
501 assert!(matches!(err, VortexError::InvalidArgument(_, _)));
502 }
503
504 #[test]
505 fn canonicalize_chunked_of_bitpacked() -> VortexResult<()> {
506 let mut ctx = SESSION.create_execution_ctx();
507 let mut rng = StdRng::seed_from_u64(0);
508
509 let chunks = (0..10)
510 .map(|_| make_array(&mut rng, 100, 0.25, 0.25, &mut ctx).unwrap())
511 .collect::<Vec<_>>();
512 let chunked = ChunkedArray::from_iter(chunks).into_array();
513
514 let into_ca = chunked.clone().execute::<PrimitiveArray>(&mut ctx)?;
515 let mut primitive_builder = PrimitiveBuilder::<i32>::with_capacity_in(
516 chunked.dtype().nullability(),
517 10 * 100,
518 vortex_buffer::BufferAllocatorRef::static_ref(),
519 );
520 chunked.append_to_builder(&mut primitive_builder, &mut ctx)?;
521 let ca_into = primitive_builder.finish();
522
523 assert_arrays_eq!(into_ca, ca_into, &mut ctx);
524
525 let mut primitive_builder = PrimitiveBuilder::<i32>::with_capacity_in(
526 chunked.dtype().nullability(),
527 10 * 100,
528 vortex_buffer::BufferAllocatorRef::static_ref(),
529 );
530 chunked.append_to_builder(&mut primitive_builder, &mut ctx)?;
531 let ca_into = primitive_builder.finish();
532
533 assert_arrays_eq!(into_ca, ca_into, &mut ctx);
534
535 Ok(())
536 }
537
538 #[test]
539 fn test_chunk_offsets() -> VortexResult<()> {
540 let mut ctx = SESSION.create_execution_ctx();
541 let patch_value = 1u32 << 20;
542 let patch_indices = [100usize, 200, 3000, 3100];
543 let mut values = vec![0u32; 4096usize];
544
545 patch_indices
546 .iter()
547 .for_each(|&idx| values[idx] = patch_value);
548
549 let array = PrimitiveArray::from_iter(values);
550 let bitpacked = bitpack_encode(&array, 4, None, &mut ctx)?;
551
552 let patches = bitpacked
553 .patches()
554 .ok_or_else(|| vortex_err!("expected patches"))?;
555 let chunk_offsets = patches
556 .chunk_offsets()
557 .as_ref()
558 .ok_or_else(|| vortex_err!("expected chunk offsets"))?
559 .clone()
560 .execute::<PrimitiveArray>(&mut ctx)?;
561
562 assert_arrays_eq!(
567 chunk_offsets,
568 PrimitiveArray::from_iter([0u64, 2, 2, 3]),
569 &mut ctx
570 );
571 Ok(())
572 }
573
574 #[test]
575 fn test_chunk_offsets_no_patches_in_middle() -> VortexResult<()> {
576 let mut ctx = SESSION.create_execution_ctx();
577 let patch_value = 1u32 << 20;
578 let patch_indices = [100usize, 200, 2500];
579 let mut values = vec![0u32; 3072usize];
580
581 patch_indices
582 .iter()
583 .for_each(|&idx| values[idx] = patch_value);
584
585 let array = PrimitiveArray::from_iter(values);
586 let bitpacked = bitpack_encode(&array, 4, None, &mut ctx)?;
587
588 let patches = bitpacked
589 .patches()
590 .ok_or_else(|| vortex_err!("expected patches"))?;
591 let chunk_offsets = patches
592 .chunk_offsets()
593 .as_ref()
594 .ok_or_else(|| vortex_err!("expected chunk offsets"))?
595 .clone()
596 .execute::<PrimitiveArray>(&mut ctx)?;
597
598 assert_arrays_eq!(
599 chunk_offsets,
600 PrimitiveArray::from_iter([0u64, 2, 2]),
601 &mut ctx
602 );
603 Ok(())
604 }
605
606 #[test]
607 fn test_chunk_offsets_trailing_empty_chunks() -> VortexResult<()> {
608 let mut ctx = SESSION.create_execution_ctx();
609 let patch_value = 1u32 << 20;
610 let patch_indices = [100usize, 200, 1500];
611 let mut values = vec![0u32; 5120usize];
612
613 patch_indices
614 .iter()
615 .for_each(|&idx| values[idx] = patch_value);
616
617 let array = PrimitiveArray::from_iter(values);
618 let bitpacked = bitpack_encode(&array, 4, None, &mut ctx)?;
619
620 let patches = bitpacked
621 .patches()
622 .ok_or_else(|| vortex_err!("expected patches"))?;
623 let chunk_offsets = patches
624 .chunk_offsets()
625 .as_ref()
626 .ok_or_else(|| vortex_err!("expected chunk offsets"))?
627 .clone()
628 .execute::<PrimitiveArray>(&mut ctx)?;
629
630 assert_arrays_eq!(
636 chunk_offsets,
637 PrimitiveArray::from_iter([0u64, 2, 3, 3, 3]),
638 &mut ctx
639 );
640 Ok(())
641 }
642
643 #[test]
644 fn test_chunk_offsets_single_chunk() -> VortexResult<()> {
645 let mut ctx = SESSION.create_execution_ctx();
646 let patch_value = 1u32 << 20;
647 let patch_indices = [100usize, 200];
648 let mut values = vec![0u32; 500usize];
649
650 patch_indices
651 .iter()
652 .for_each(|&idx| values[idx] = patch_value);
653
654 let array = PrimitiveArray::from_iter(values);
655 let bitpacked = bitpack_encode(&array, 4, None, &mut ctx)?;
656
657 let patches = bitpacked
658 .patches()
659 .ok_or_else(|| vortex_err!("expected patches"))?;
660 let chunk_offsets = patches
661 .chunk_offsets()
662 .as_ref()
663 .ok_or_else(|| vortex_err!("expected chunk offsets"))?
664 .clone()
665 .execute::<PrimitiveArray>(&mut ctx)?;
666
667 assert_arrays_eq!(chunk_offsets, PrimitiveArray::from_iter([0u64]), &mut ctx);
669 Ok(())
670 }
671}