Skip to main content

vortex_fastlanes/bitpacking/array/
bitpack_compress.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use 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    // Check array contains no negative values.
56    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        // Nothing we can do
69        vortex_bail!(
70            InvalidArgument: "Cannot pack - specified bit width {bit_width} >= {}",
71            array.ptype().bit_width()
72        )
73    }
74
75    // SAFETY: we check that array only contains non-negative values.
76    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
95/// Bitpack an array into the specified bit-width without checking statistics.
96///
97/// # Safety
98///
99/// It is the caller's responsibility to ensure that all values in the array can lossless pack
100/// into the specified bit-width.
101///
102/// Failure to do so will result in data loss.
103pub unsafe fn bitpack_encode_unchecked(
104    array: PrimitiveArray,
105    bit_width: u8,
106) -> VortexResult<BitPackedArray> {
107    // SAFETY: non-negativity of input checked by caller.
108    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
125/// Bitpack a [PrimitiveArray] to the given width.
126///
127/// On success, returns a [Buffer] containing the packed data.
128///
129/// # Safety
130///
131/// Internally this function will promote the provided array to its unsigned equivalent. This will
132/// violate ordering guarantees if the array contains any negative values.
133///
134/// It is the caller's responsibility to ensure that `parray` is non-negative before calling
135/// this function.
136pub 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
143/// Bitpack a slice of primitives down to the given width.
144///
145/// See `bitpack` for more caller information.
146pub 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    // How many fastlanes vectors we will process.
153    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    // packed_len says how many values of size T we're going to include.
157    // 1024 * bit_width / 8 == the number of bytes we're going to get.
158    // then we divide by the size of T to get the number of elements.
159
160    // Allocate a result byte array.
161    let mut output = BufferMut::<T>::with_capacity(num_chunks * packed_len);
162
163    // Loop over all but the last chunk.
164    (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    // Pad the last chunk with zeros to a full 1024 elements.
178    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            // Record the patch index offset for each chunk.
279            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            // All values are valid.
325            for v in array.as_slice::<T>() {
326                bit_widths[bit_width(*v)] += 1;
327            }
328        }
329        AllOr::None => {
330            // All values are invalid
331            bit_widths[0] = array.len();
332        }
333        AllOr::Some(buffer) => {
334            // Some values are valid
335            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/// Assuming exceptions cost 1 value + 1 u32 index, figure out the best bit-width to use.
353/// We could try to be clever, but we can never really predict how the exceptions will compress.
354#[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); // round up to bytes
369
370        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        // 10 1-bit values, 20 2-bit, etc.
459        let freq = vec![0, 10, 20, 15, 1, 0, 0, 0];
460        // 3-bits => (46 * 3) + (8 * 1 * 5) => 178 bits => 23 bytes and zero exceptions
461        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        // chunk 0 (0-1023): patches at 100, 200 -> starts at patch index 0
563        // chunk 1 (1024-2047): no patches -> points to patch index 2
564        // chunk 2 (2048-3071): patch at 3000 -> starts at patch index 2
565        // chunk 3 (3072-4095): patch at 3100 -> starts at patch index 3
566        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        // chunk 0 (0-1023): patches at 100, 200 -> starts at patch index 0
631        // chunk 1 (1024-2047): patch at 1500 -> starts at patch index 2
632        // chunk 2 (2048-3071): no patches -> points to patch index 3
633        // chunk 3 (3072-4095): no patches -> points to patch index 3 (remaining chunks filled)
634        // chunk 4 (4096-5119): no patches -> points to patch index 3 (remaining chunks filled)
635        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        // Single chunk starting at patch index 0.
668        assert_arrays_eq!(chunk_offsets, PrimitiveArray::from_iter([0u64]), &mut ctx);
669        Ok(())
670    }
671}