@@ -51,7 +51,7 @@ pub const fn chunk_packed_bytes(bit_width: u8) -> usize {
5151}
5252
5353/// Chunk widths and byte offsets used while encoding or executing bit-packed data.
54- /// Operations use this view to address each packed chunk .
54+ /// Execution borrows the materialized children; only encoding computes prefix sums .
5555#[ derive( Clone , Debug ) ]
5656pub struct ChunkWidths {
5757 widths : Widths ,
@@ -238,6 +238,10 @@ pub struct BitPackedSlots {
238238 /// One non-nullable `u8` width per 1024-element chunk. Uniform widths use a constant array.
239239 #[ slot( 4 ) ]
240240 pub width_table : ArrayRef ,
241+ /// Non-nullable `u64` byte boundaries, with one trailing entry after the last chunk.
242+ /// The first offset is the origin of the packed buffer and may be nonzero after slicing.
243+ #[ slot( 5 ) ]
244+ pub chunk_offsets : ArrayRef ,
241245}
242246
243247pub ( crate ) const PATCH_SLOTS : PatchSlotIndices = PatchSlotIndices {
@@ -249,6 +253,9 @@ pub(crate) const PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
249253/// The dtype of the width table child: one byte per chunk.
250254pub ( crate ) const WIDTH_TABLE_DTYPE : DType = DType :: Primitive ( PType :: U8 , Nullability :: NonNullable ) ;
251255
256+ pub ( crate ) const CHUNK_OFFSETS_DTYPE : DType =
257+ DType :: Primitive ( PType :: U64 , Nullability :: NonNullable ) ;
258+
252259impl IntoArray for ChunkWidths {
253260 fn into_array ( self ) -> ArrayRef {
254261 if self . is_uniform ( ) {
@@ -259,23 +266,34 @@ impl IntoArray for ChunkWidths {
259266 }
260267}
261268
262- /// Read the width child without executing it during reduction.
263- pub ( crate ) fn materialized_widths ( table : & ArrayRef ) -> VortexResult < Option < ChunkWidths > > {
264- if let Some ( constant) = table. as_opt :: < Constant > ( ) {
265- return Ok ( Some ( ChunkWidths :: uniform (
266- u8:: try_from ( constant. scalar ( ) ) ?,
267- table. len ( ) ,
268- ) ) ) ;
269- }
270- Ok ( table
269+ /// Read materialized children without executing them during reduction.
270+ pub ( crate ) fn materialized_widths (
271+ table : & ArrayRef ,
272+ offsets : & ArrayRef ,
273+ ) -> VortexResult < Option < ChunkWidths > > {
274+ let widths = if let Some ( constant) = table. as_opt :: < Constant > ( ) {
275+ Widths :: Uniform {
276+ width : u8:: try_from ( constant. scalar ( ) ) ?,
277+ len : table. len ( ) ,
278+ }
279+ } else if let Some ( primitive) = table
280+ . as_opt :: < Primitive > ( )
281+ . filter ( |a| a. buffer_handle ( ) . is_on_host ( ) )
282+ {
283+ Widths :: PerChunk ( primitive. to_buffer :: < u8 > ( ) )
284+ } else {
285+ return Ok ( None ) ;
286+ } ;
287+ Ok ( offsets
271288 . as_opt :: < Primitive > ( )
272289 . filter ( |a| a. buffer_handle ( ) . is_on_host ( ) )
273- . map ( |a| ChunkWidths :: new ( a. to_buffer :: < u8 > ( ) ) ) )
290+ . map ( |a| ChunkWidths :: from_buffers ( widths , a. to_buffer :: < u64 > ( ) ) ) )
274291}
275292
276293pub struct BitPackedDataParts {
277294 pub offset : u16 ,
278295 pub widths : ArrayRef ,
296+ pub chunk_offsets : ArrayRef ,
279297 pub len : usize ,
280298 pub packed : BufferHandle ,
281299 pub patches : Option < Patches > ,
@@ -324,9 +342,11 @@ impl BitPackedData {
324342 /// * `validity` must have `length` len
325343 /// * Any patches must have any `array_len` equal to `length`
326344 /// * The width-table child must hold one non-nullable `u8` per chunk.
345+ /// * The offsets child must hold `num_chunks + 1` non-nullable `u64` byte boundaries.
327346 ///
328347 /// Once the widths are materialized, they must be no wider than `ptype`, and the packed
329- /// buffer must be exactly the sum of the chunks' packed sizes. Compressed children are checked at execution time, before unpacking.
348+ /// buffer must be exactly the sum of the chunks' packed sizes. Offset differences must
349+ /// match the widths. Compressed children are checked at execution time, before unpacking.
330350 ///
331351 /// Any violation of these preconditions will result in an error.
332352 pub fn try_new (
@@ -352,6 +372,7 @@ impl BitPackedData {
352372 validity : & Validity ,
353373 patches : Option < & Patches > ,
354374 table : & ArrayRef ,
375+ offsets : & ArrayRef ,
355376 length : usize ,
356377 ) -> VortexResult < ( ) > {
357378 vortex_ensure ! ( ptype. is_int( ) , MismatchedTypes : "integer" , ptype) ;
@@ -378,8 +399,19 @@ impl BitPackedData {
378399 "Expected {num_chunks} chunk widths, got {}" ,
379400 table. len( )
380401 ) ;
402+ vortex_ensure ! (
403+ offsets. dtype( ) == & CHUNK_OFFSETS_DTYPE ,
404+ "BitPacked chunk offsets must be {CHUNK_OFFSETS_DTYPE}, got {}" ,
405+ offsets. dtype( )
406+ ) ;
407+ vortex_ensure ! (
408+ offsets. len( ) == num_chunks + 1 ,
409+ "Expected {} chunk offsets, got {}" ,
410+ num_chunks + 1 ,
411+ offsets. len( )
412+ ) ;
381413 // Compressed children are checked once materialized, before any unchecked unpacking.
382- if let Some ( widths) = materialized_widths ( table) ? {
414+ if let Some ( widths) = materialized_widths ( table, offsets ) ? {
383415 Self :: validate_widths ( & self . packed , ptype, & widths) ?;
384416 }
385417 Ok ( ( ) )
@@ -504,24 +536,40 @@ pub trait BitPackedArrayExt: BitPackedArraySlotsExt {
504536 BitPackedData :: packed ( self )
505537 }
506538
507- /// Prepare and validate the width child once for a bulk operation.
539+ /// Prepare and validate both children once for a bulk operation, without computing prefix sums .
508540 fn chunk_widths ( & self , ctx : & mut ExecutionCtx ) -> VortexResult < ChunkWidths > {
509- let widths = match materialized_widths ( self . width_table ( ) ) ? {
541+ let widths = match materialized_widths ( self . width_table ( ) , self . chunk_offsets ( ) ) ? {
510542 Some ( widths) => widths,
511- None => ChunkWidths :: new (
512- self . width_table ( )
543+ None => {
544+ let table = self . width_table ( ) ;
545+ let widths = if let Some ( constant) = table. as_opt :: < Constant > ( ) {
546+ Widths :: Uniform {
547+ width : u8:: try_from ( constant. scalar ( ) ) ?,
548+ len : table. len ( ) ,
549+ }
550+ } else {
551+ Widths :: PerChunk (
552+ table
553+ . clone ( )
554+ . execute :: < PrimitiveArray > ( ctx) ?
555+ . to_buffer :: < u8 > ( ) ,
556+ )
557+ } ;
558+ let offsets = self
559+ . chunk_offsets ( )
513560 . clone ( )
514561 . execute :: < PrimitiveArray > ( ctx) ?
515- . to_buffer :: < u8 > ( ) ,
516- ) ,
562+ . to_buffer :: < u64 > ( ) ;
563+ ChunkWidths :: from_buffers ( widths, offsets)
564+ }
517565 } ;
518566 BitPackedData :: validate_widths ( self . packed ( ) , self . as_ref ( ) . dtype ( ) . as_ptype ( ) , & widths) ?;
519567 Ok ( widths)
520568 }
521569
522- /// Read and validate widths only when the child is already materialized.
570+ /// Read and validate widths and offsets only when their children are already materialized.
523571 fn materialized_chunk_widths ( & self ) -> VortexResult < Option < ChunkWidths > > {
524- let widths = materialized_widths ( self . width_table ( ) ) ?;
572+ let widths = materialized_widths ( self . width_table ( ) , self . chunk_offsets ( ) ) ?;
525573 if let Some ( widths) = & widths {
526574 BitPackedData :: validate_widths (
527575 self . packed ( ) ,
@@ -532,18 +580,70 @@ pub trait BitPackedArrayExt: BitPackedArraySlotsExt {
532580 Ok ( widths)
533581 }
534582
535- /// Locate and validate one chunk using the width table.
583+ /// Read one byte boundary without executing the entire offsets child.
584+ fn chunk_byte_offset ( & self , boundary : usize , ctx : & mut ExecutionCtx ) -> VortexResult < u64 > {
585+ vortex_ensure ! (
586+ boundary < self . chunk_offsets( ) . len( ) ,
587+ "Chunk boundary out of bounds"
588+ ) ;
589+ if let Some ( offsets) = self
590+ . chunk_offsets ( )
591+ . as_opt :: < Primitive > ( )
592+ . filter ( |a| a. buffer_handle ( ) . is_on_host ( ) )
593+ {
594+ Ok ( offsets. as_slice :: < u64 > ( ) [ boundary] )
595+ } else {
596+ u64:: try_from ( & self . chunk_offsets ( ) . execute_scalar ( boundary, ctx) ?)
597+ }
598+ }
599+
600+ /// Locate and validate one chunk using scalar child access, without materializing the tables.
536601 fn chunk_range (
537602 & self ,
538603 chunk : usize ,
539604 ctx : & mut ExecutionCtx ,
540605 ) -> VortexResult < ( Range < usize > , u8 ) > {
541- let widths = self . chunk_widths ( ctx) ?;
542- vortex_ensure ! ( chunk < widths. len( ) , "Chunk index out of bounds" ) ;
543- Ok ( (
544- widths. byte_offset ( chunk) ..widths. byte_offset ( chunk + 1 ) ,
545- widths. width ( chunk) ,
546- ) )
606+ vortex_ensure ! (
607+ chunk < self . width_table( ) . len( ) ,
608+ "Chunk index out of bounds"
609+ ) ;
610+ let width = if let Some ( table) = self
611+ . width_table ( )
612+ . as_opt :: < Primitive > ( )
613+ . filter ( |a| a. buffer_handle ( ) . is_on_host ( ) )
614+ {
615+ table. as_slice :: < u8 > ( ) [ chunk]
616+ } else if let Some ( table) = self . width_table ( ) . as_opt :: < Constant > ( ) {
617+ u8:: try_from ( table. scalar ( ) ) ?
618+ } else {
619+ u8:: try_from ( & self . width_table ( ) . execute_scalar ( chunk, ctx) ?) ?
620+ } ;
621+ vortex_ensure ! (
622+ width as usize <= self . as_ref( ) . dtype( ) . as_ptype( ) . bit_width( ) ,
623+ "Unsupported bit width {width}"
624+ ) ;
625+ let base = self . chunk_byte_offset ( 0 , ctx) ?;
626+ let start = if chunk == 0 {
627+ base
628+ } else {
629+ self . chunk_byte_offset ( chunk, ctx) ?
630+ } ;
631+ let end = self . chunk_byte_offset ( chunk + 1 , ctx) ?;
632+ vortex_ensure ! (
633+ end. checked_sub( start) == Some ( chunk_packed_bytes( width) as u64 ) ,
634+ "Chunk {chunk} offsets do not match its bit width"
635+ ) ;
636+ let start = start
637+ . checked_sub ( base)
638+ . ok_or_else ( || vortex_err ! ( "Chunk offset precedes buffer origin" ) ) ?;
639+ let end = end
640+ . checked_sub ( base)
641+ . ok_or_else ( || vortex_err ! ( "Chunk offset precedes buffer origin" ) ) ?;
642+ vortex_ensure ! (
643+ start % ( FL_CHUNK_SIZE / 8 ) as u64 == 0 && end <= self . packed( ) . len( ) as u64 ,
644+ "Chunk offsets are unaligned or exceed the packed buffer"
645+ ) ;
646+ Ok ( ( usize:: try_from ( start) ?..usize:: try_from ( end) ?, width) )
547647 }
548648
549649 #[ inline]
0 commit comments