@@ -35,7 +35,25 @@ use crate::bitpack_decompress::count_exceptions;
3535use crate :: bitpacking:: array:: ChunkLayout ;
3636use 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.
3957pub 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.
6280pub 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+
173261fn 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+
384492pub 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