Skip to content

Commit d5ffca3

Browse files
committed
feat: select bitpacked widths independently per chunk
Signed-off-by: "Matt Katz" <mhkatz97@gmail.com> Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 60ce538 commit d5ffca3

3 files changed

Lines changed: 414 additions & 19 deletions

File tree

‎encodings/fastlanes/src/bitpacking/array/bitpack_compress.rs‎

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,25 @@ use crate::bitpack_decompress::count_exceptions;
3535
use crate::bitpacking::array::ChunkLayout;
3636
use crate::bitpacking::array::chunk_packed_bytes;
3737

38-
/// Encode with caller-supplied chunk widths, gathering exceptions for values that do not fit.
38+
/// Choose a cost-model width for each chunk, then pack values and gather exceptions.
39+
pub fn bitpack_to_best_chunk_widths(
40+
array: &PrimitiveArray,
41+
ctx: &mut ExecutionCtx,
42+
) -> VortexResult<BitPackedArray> {
43+
let plan = chunk_width_plan(array.as_view(), ctx)?;
44+
bitpack_encode_planned(array, plan, ctx)
45+
}
46+
47+
/// The cost-model-optimal bit width of every 1024-element chunk of `array`.
48+
pub fn best_chunk_widths(
49+
array: ArrayView<'_, Primitive>,
50+
ctx: &mut ExecutionCtx,
51+
) -> VortexResult<ChunkLayout> {
52+
Ok(chunk_width_plan(array, ctx)?.widths)
53+
}
54+
55+
/// Bit-pack `array` at the given per-chunk widths, gathering values that do not fit their chunk's
56+
/// width into patches.
3957
pub fn bitpack_encode_with_widths(
4058
array: &PrimitiveArray,
4159
widths: ChunkLayout,
@@ -58,7 +76,7 @@ pub fn bitpack_encode_with_widths(
5876
/// Bit-pack `array` at the single best global width chosen by [`find_best_bit_width`].
5977
///
6078
/// Every chunk shares that width, so the result serializes under the original
61-
/// `fastlanes.bitpacked` format.
79+
/// `fastlanes.bitpacked` format. See [`bitpack_to_best_chunk_widths`] for per-chunk widths.
6280
pub fn bitpack_to_best_bit_width(
6381
array: &PrimitiveArray,
6482
ctx: &mut ExecutionCtx,
@@ -170,6 +188,76 @@ struct ChunkWidthPlan {
170188
num_exceptions: Option<usize>,
171189
}
172190

191+
fn chunk_width_plan(
192+
array: ArrayView<'_, Primitive>,
193+
ctx: &mut ExecutionCtx,
194+
) -> VortexResult<ChunkWidthPlan> {
195+
match_each_integer_ptype!(array.ptype(), |P| {
196+
chunk_width_plan_typed::<P>(array, ctx)
197+
})
198+
}
199+
200+
fn chunk_width_plan_typed<T: NativePType + PrimInt>(
201+
array: ArrayView<'_, Primitive>,
202+
ctx: &mut ExecutionCtx,
203+
) -> VortexResult<ChunkWidthPlan> {
204+
let bytes_per_exception = bytes_per_exception(T::PTYPE);
205+
let values = array.as_slice::<T>();
206+
let num_chunks = values.len().div_ceil(FL_CHUNK_SIZE);
207+
let bit_width: fn(T) -> usize =
208+
|v: T| (8 * size_of::<T>()) - (PrimInt::leading_zeros(v) as usize);
209+
210+
let mut widths = BufferMut::<u8>::with_capacity(num_chunks);
211+
let mut num_exceptions = 0usize;
212+
let mut histogram = vec![0usize; size_of::<T>() * 8 + 1];
213+
214+
// Score one chunk's histogram and reset it for the next chunk.
215+
let mut finish_chunk = |histogram: &mut [usize]| -> u8 {
216+
let best = best_chunk_width(histogram, bytes_per_exception);
217+
num_exceptions += count_exceptions(best, histogram);
218+
histogram.fill(0);
219+
best
220+
};
221+
222+
match array
223+
.validity()?
224+
.execute_mask(array.as_ref().len(), ctx)?
225+
.bit_buffer()
226+
{
227+
AllOr::All => {
228+
for chunk in values.chunks(FL_CHUNK_SIZE) {
229+
for v in chunk {
230+
histogram[bit_width(*v)] += 1;
231+
}
232+
widths.push(finish_chunk(&mut histogram));
233+
}
234+
}
235+
AllOr::None => {
236+
for _ in 0..num_chunks {
237+
widths.push(0);
238+
}
239+
}
240+
AllOr::Some(buffer) => {
241+
let mut valid = buffer.iter();
242+
for chunk in values.chunks(FL_CHUNK_SIZE) {
243+
for v in chunk {
244+
if valid.next().unwrap_or(true) {
245+
histogram[bit_width(*v)] += 1;
246+
} else {
247+
histogram[0] += 1;
248+
}
249+
}
250+
widths.push(finish_chunk(&mut histogram));
251+
}
252+
}
253+
}
254+
255+
Ok(ChunkWidthPlan {
256+
widths: ChunkLayout::from_widths(widths.freeze()),
257+
num_exceptions: Some(num_exceptions),
258+
})
259+
}
260+
173261
fn bitpack_encode_planned(
174262
array: &PrimitiveArray,
175263
plan: ChunkWidthPlan,
@@ -381,6 +469,26 @@ where
381469
}
382470
}
383471

472+
/// The width minimising one chunk's cost: its packed block plus the exceptions left behind.
473+
///
474+
/// A chunk always occupies a whole `128 * width` byte block, so a partial trailing chunk is
475+
/// charged for its padding.
476+
fn best_chunk_width(bit_width_freq: &[usize], bytes_per_exception: usize) -> u8 {
477+
let len: usize = bit_width_freq.iter().sum();
478+
let mut num_packed = 0;
479+
let mut best_cost = usize::MAX;
480+
let mut best_width = 0;
481+
for (bit_width, freq) in bit_width_freq.iter().enumerate() {
482+
num_packed += *freq;
483+
let cost = chunk_packed_bytes(bit_width as u8) + (len - num_packed) * bytes_per_exception;
484+
if cost < best_cost {
485+
best_cost = cost;
486+
best_width = bit_width;
487+
}
488+
}
489+
best_width as u8
490+
}
491+
384492
pub fn bit_width_histogram(
385493
array: ArrayView<'_, Primitive>,
386494
ctx: &mut ExecutionCtx,
@@ -534,6 +642,19 @@ mod tests {
534642
session
535643
});
536644

645+
#[test]
646+
fn test_best_chunk_width() {
647+
// 1000 3-bit values and 24 10-bit values in a u16 chunk: 3 bits plus 24 exceptions
648+
// (384 + 24 * 6 bytes) beats 10 bits for everything (1280 bytes).
649+
let mut freq = vec![0usize; 17];
650+
freq[3] = 1000;
651+
freq[10] = 24;
652+
assert_eq!(best_chunk_width(&freq, bytes_per_exception(PType::U16)), 3);
653+
// Make the exceptions expensive enough and the wide width wins.
654+
freq[10] = 200;
655+
assert_eq!(best_chunk_width(&freq, bytes_per_exception(PType::U16)), 10);
656+
}
657+
537658
#[test]
538659
fn null_patches() {
539660
let mut ctx = SESSION.create_execution_ctx();

0 commit comments

Comments
 (0)