Skip to content

Commit 9dc388a

Browse files
committed
refactor: add bitpacked chunk offsets child
Signed-off-by: "Matt Katz" <mhkatz97@gmail.com> Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 04fa22c commit 9dc388a

25 files changed

Lines changed: 287 additions & 67 deletions

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ use std::mem::MaybeUninit;
88
use fastlanes::BitPacking;
99
use vortex_array::ArrayRef;
1010
use vortex_array::ExecutionCtx;
11+
use vortex_array::IntoArray;
1112
use vortex_array::TypedArrayRef;
1213
use vortex_array::array_slots;
1314
use vortex_array::arrays::Primitive;
1415
use vortex_array::arrays::PrimitiveArray;
1516
use vortex_array::buffer::BufferHandle;
1617
use vortex_array::dtype::DType;
1718
use vortex_array::dtype::NativePType;
19+
use vortex_array::dtype::Nullability;
1820
use vortex_array::dtype::PType;
1921
use vortex_array::patches::PatchSlotIndices;
2022
use vortex_array::patches::Patches;
2123
use vortex_array::patches::PatchesData;
2224
use vortex_array::validity::Validity;
2325
use vortex_array::vtable::child_to_validity;
26+
use vortex_buffer::Buffer;
2427
use vortex_error::VortexResult;
2528
use vortex_error::vortex_ensure;
2629
use vortex_error::vortex_err;
@@ -49,6 +52,10 @@ pub struct BitPackedSlots {
4952
/// The validity bitmap indicating which elements are non-null.
5053
#[slot(3)]
5154
pub validity_child: Option<ArrayRef>,
55+
/// Non-nullable `u64` byte boundaries, including one trailing boundary.
56+
/// Adjacent boundaries differ by `128 * bit_width` bytes.
57+
#[slot(4)]
58+
pub chunk_offsets: ArrayRef,
5259
}
5360

5461
pub(crate) const PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
@@ -57,6 +64,15 @@ pub(crate) const PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
5764
chunk_offsets: BitPackedSlots::PATCH_CHUNK_OFFSETS,
5865
};
5966

67+
/// Non-nullable byte boundaries for the packed chunks.
68+
pub(crate) const CHUNK_OFFSETS_DTYPE: DType =
69+
DType::Primitive(PType::U64, Nullability::NonNullable);
70+
71+
pub(crate) fn uniform_chunk_offsets(bit_width: u8, num_chunks: usize) -> ArrayRef {
72+
Buffer::<u64>::from_iter((0..=num_chunks).map(|i| (i * 128 * bit_width as usize) as u64))
73+
.into_array()
74+
}
75+
6076
pub struct BitPackedDataParts {
6177
pub offset: u16,
6278
pub bit_width: u8,
@@ -182,6 +198,40 @@ impl BitPackedData {
182198
Ok(())
183199
}
184200

201+
pub(crate) fn validate_chunk_offsets(
202+
&self,
203+
offsets: &ArrayRef,
204+
length: usize,
205+
) -> VortexResult<()> {
206+
let num_chunks = (length + self.offset as usize).div_ceil(FL_CHUNK_SIZE);
207+
vortex_ensure!(
208+
offsets.dtype() == &CHUNK_OFFSETS_DTYPE,
209+
"Expected non-nullable u64 chunk offsets"
210+
);
211+
vortex_ensure!(
212+
offsets.len() == num_chunks + 1,
213+
"Expected {} chunk boundaries, got {}",
214+
num_chunks + 1,
215+
offsets.len()
216+
);
217+
// Kernels still use the scalar width until they are migrated to the offsets child.
218+
let offsets = offsets
219+
.as_opt::<Primitive>()
220+
.filter(|array| array.buffer_handle().is_on_host())
221+
.ok_or_else(|| {
222+
vortex_err!("Chunk offsets must be materialized while kernels use bit_width")
223+
})?;
224+
vortex_ensure!(
225+
offsets
226+
.as_slice::<u64>()
227+
.windows(2)
228+
.all(|pair| pair[1].checked_sub(pair[0]) == Some(128 * self.bit_width as u64)),
229+
"Chunk offsets must imply bit_width {}",
230+
self.bit_width
231+
);
232+
Ok(())
233+
}
234+
185235
fn validate_patches(patches: &Patches, ptype: PType, len: usize) -> VortexResult<()> {
186236
// Ensure that array and patches have same ptype
187237
vortex_ensure!(
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Tests for the chunk offsets child of uniformly bit-packed arrays.
5+
6+
use std::sync::LazyLock;
7+
8+
use vortex_array::IntoArray;
9+
use vortex_array::VortexSessionExecute;
10+
use vortex_array::arrays::PrimitiveArray;
11+
use vortex_array::assert_arrays_eq;
12+
use vortex_error::VortexResult;
13+
use vortex_session::VortexSession;
14+
15+
use crate::BitPacked;
16+
use crate::BitPackedArray;
17+
use crate::BitPackedArrayExt;
18+
use crate::BitPackedArraySlotsExt;
19+
use crate::bitpacking::bitpack_compress::bitpack_to_best_bit_width;
20+
21+
static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
22+
let session = vortex_array::array_session();
23+
crate::initialize(&session);
24+
session
25+
});
26+
27+
fn encode(values: &[u32]) -> VortexResult<BitPackedArray> {
28+
let mut ctx = SESSION.create_execution_ctx();
29+
bitpack_to_best_bit_width(&PrimitiveArray::from_iter(values.iter().copied()), &mut ctx)
30+
}
31+
32+
#[test]
33+
fn chunk_offsets_are_validated() -> VortexResult<()> {
34+
let mut ctx = SESSION.create_execution_ctx();
35+
let uniform = encode(&(0..3000u32).map(|i| i % 128).collect::<Vec<_>>())?;
36+
assert_eq!(uniform.bit_width(), 7);
37+
assert_arrays_eq!(
38+
uniform.chunk_offsets(),
39+
vortex_buffer::buffer![0u64, 896, 1792, 2688].into_array(),
40+
&mut ctx
41+
);
42+
for offsets in [
43+
vortex_buffer::buffer![0u64, 896, 1792],
44+
vortex_buffer::buffer![0u64, 896, 1791, 2688],
45+
vortex_buffer::buffer![0u64, 896, 768, 2688],
46+
vortex_buffer::buffer![0u64, 768, 1792, 2688],
47+
] {
48+
assert!(BitPacked::with_chunk_offsets(uniform.clone(), offsets.into_array()).is_err());
49+
}
50+
let wrong_dtype = PrimitiveArray::from_iter([0u32, 896, 1792, 2688]).into_array();
51+
assert!(BitPacked::with_chunk_offsets(uniform.clone(), wrong_dtype).is_err());
52+
let nullable =
53+
PrimitiveArray::from_option_iter([Some(0u64), Some(896), None, Some(2688)]).into_array();
54+
assert!(BitPacked::with_chunk_offsets(uniform.clone(), nullable).is_err());
55+
let rebased = vortex_buffer::buffer![128u64, 1024, 1920, 2816].into_array();
56+
let replaced = BitPacked::with_chunk_offsets(uniform.clone(), rebased)?;
57+
assert_arrays_eq!(uniform, replaced, &mut ctx);
58+
Ok(())
59+
}
60+
61+
#[test]
62+
fn empty_and_zero_width_offsets() -> VortexResult<()> {
63+
let mut ctx = SESSION.create_execution_ctx();
64+
let empty = encode(&[])?;
65+
assert_arrays_eq!(
66+
empty.chunk_offsets(),
67+
vortex_buffer::buffer![0u64].into_array(),
68+
&mut ctx
69+
);
70+
let zeros = encode(&vec![0u32; 2049])?;
71+
assert_arrays_eq!(
72+
zeros.chunk_offsets(),
73+
vortex_buffer::buffer![0u64, 0, 0, 0].into_array(),
74+
&mut ctx
75+
);
76+
assert_arrays_eq!(zeros, PrimitiveArray::from_iter(vec![0u32; 2049]), &mut ctx);
77+
Ok(())
78+
}

‎encodings/fastlanes/src/bitpacking/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ pub(crate) fn initialize(session: &vortex_session::VortexSession) {
2727

2828
#[cfg(test)]
2929
mod serde_tests;
30+
31+
#[cfg(test)]
32+
mod chunk_widths_tests;

‎encodings/fastlanes/src/bitpacking/plugin/bitpacked.rs‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ use vortex_session::VortexSession;
2828
use crate::BitPacked;
2929
use crate::BitPackedArrayExt;
3030
use crate::BitPackedData;
31+
use crate::FL_CHUNK_SIZE;
32+
use crate::bitpacking::array::BitPackedSlots;
33+
use crate::bitpacking::array::uniform_chunk_offsets;
3134

3235
/// Metadata of the frozen `fastlanes.bitpacked` wire format.
3336
#[derive(Clone, prost::Message)]
@@ -70,10 +73,16 @@ impl ArrayPlugin for BitPackedPlugin {
7073
.transpose()?,
7174
}
7275
.encode_to_vec();
73-
Ok(Some(ArraySerialization::from_array(
76+
let children = array.slots()[..BitPackedSlots::CHUNK_OFFSETS]
77+
.iter()
78+
.flatten()
79+
.cloned()
80+
.collect();
81+
Ok(Some(ArraySerialization::new(
7482
self.id(),
75-
array,
7683
metadata,
84+
array.buffers(),
85+
children,
7786
)))
7887
}
7988

@@ -141,15 +150,9 @@ impl ArrayPlugin for BitPackedPlugin {
141150
})
142151
.transpose()?;
143152

144-
let slots = {
145-
let mut s = ArraySlots::with_capacity(4);
146-
PatchesData::push_slots(&mut s, patches.as_ref());
147-
s.push(validity_to_child(&validity, len));
148-
s
149-
};
150153
let data = BitPackedData::try_new(
151154
packed,
152-
patches,
155+
patches.clone(),
153156
u8::try_from(metadata.bit_width).map_err(|_| {
154157
vortex_err!(
155158
"BitPackedMetadata bit_width {} does not fit in u8",
@@ -163,6 +166,14 @@ impl ArrayPlugin for BitPackedPlugin {
163166
)
164167
})?,
165168
)?;
169+
let slots = {
170+
let mut s = ArraySlots::with_capacity(BitPackedSlots::COUNT);
171+
PatchesData::push_slots(&mut s, patches.as_ref());
172+
s.push(validity_to_child(&validity, len));
173+
let num_chunks = (len + data.offset() as usize).div_ceil(FL_CHUNK_SIZE);
174+
s.push(Some(uniform_chunk_offsets(data.bit_width(), num_chunks)));
175+
s
176+
};
166177
Ok(Array::<BitPacked>::try_from_parts(
167178
ArrayParts::new(BitPacked, dtype.clone(), len, data).with_slots(slots),
168179
)?

‎encodings/fastlanes/src/bitpacking/plugin/patched.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ mod tests {
125125
let array = bitpacked.as_array();
126126

127127
let serialization = SESSION.array_serialize(array)?.unwrap();
128-
let children = array.children();
128+
let children = serialization.children;
129129
let buffers = array
130130
.buffers()
131131
.into_iter()
@@ -178,7 +178,7 @@ mod tests {
178178
let array = bitpacked.as_array();
179179

180180
let serialization = SESSION.array_serialize(array)?.unwrap();
181-
let children = array.children();
181+
let children = serialization.children;
182182
let buffers = array
183183
.buffers()
184184
.into_iter()
@@ -211,7 +211,7 @@ mod tests {
211211
let array = PrimitiveArray::from_iter([1i32, 2, 3]).into_array();
212212

213213
let serialization = SESSION.array_serialize(&array)?.unwrap();
214-
let children = array.children();
214+
let children = serialization.children;
215215
let buffers = array
216216
.buffers()
217217
.into_iter()

‎encodings/fastlanes/src/bitpacking/serde_tests.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fn uniform_widths_serialize_as_original_format() -> VortexResult<()> {
5757
let values: Vec<u32> = (0..3000).map(|i| i % 128).collect();
5858
let packed =
5959
bitpack_to_best_bit_width(&PrimitiveArray::from_iter(values.iter().copied()), &mut ctx)?;
60-
assert!(packed.as_array().children().is_empty());
60+
assert_eq!(packed.as_array().children().len(), 1);
6161
assert!(
6262
SESSION
6363
.array_serialize(packed.as_array())?

‎encodings/fastlanes/src/bitpacking/vtable/mod.rs‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ use vortex_session::registry::CachedId;
4141
use crate::BitPackedArrayExt;
4242
use crate::BitPackedData;
4343
use crate::BitPackedDataParts;
44+
use crate::FL_CHUNK_SIZE;
4445
use crate::bitpack_decompress::unpack_array;
4546
use crate::bitpack_decompress::unpack_into_primitive_builder;
4647
use crate::bitpacking::array::BitPackedSlots;
4748
use crate::bitpacking::array::BitPackedSlotsView;
4849
use crate::bitpacking::array::PATCH_SLOTS;
50+
use crate::bitpacking::array::uniform_chunk_offsets;
4951
use crate::bitpacking::vtable::rules::RULES;
5052
mod kernels;
5153
mod operations;
@@ -95,6 +97,16 @@ impl VTable for BitPacked {
9597
len: usize,
9698
slots: &[Option<ArrayRef>],
9799
) -> VortexResult<()> {
100+
vortex_ensure!(
101+
slots.len() == BitPackedSlots::COUNT,
102+
"Expected {} slots, got {}",
103+
BitPackedSlots::COUNT,
104+
slots.len()
105+
);
106+
vortex_ensure!(
107+
slots[BitPackedSlots::CHUNK_OFFSETS].is_some(),
108+
"Missing chunk offsets"
109+
);
98110
let bp_slots = BitPackedSlotsView::from_slots(slots);
99111

100112
let validity = child_to_validity(bp_slots.validity_child, dtype.nullability());
@@ -108,7 +120,8 @@ impl VTable for BitPacked {
108120
data.bit_width,
109121
len,
110122
data.offset,
111-
)
123+
)?;
124+
data.validate_chunk_offsets(bp_slots.chunk_offsets, len)
112125
}
113126

114127
fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
@@ -225,15 +238,31 @@ impl BitPacked {
225238
) -> VortexResult<BitPackedArray> {
226239
let dtype = DType::Primitive(ptype, validity.nullability());
227240
let slots = {
228-
let mut s = ArraySlots::with_capacity(4);
241+
let mut s = ArraySlots::with_capacity(BitPackedSlots::COUNT);
229242
PatchesData::push_slots(&mut s, patches.as_ref());
230243
s.push(validity_to_child(&validity, len));
244+
let num_chunks = (len + offset as usize).div_ceil(FL_CHUNK_SIZE);
245+
s.push(Some(uniform_chunk_offsets(bit_width, num_chunks)));
231246
s
232247
};
233248
let data = BitPackedData::try_new(packed, patches, bit_width, offset)?;
234249
Array::try_from_parts(ArrayParts::new(BitPacked, dtype, len, data).with_slots(slots))
235250
}
236251

252+
/// Replace the byte boundaries. Every chunk must still imply the scalar `bit_width`.
253+
pub fn with_chunk_offsets(
254+
array: BitPackedArray,
255+
table: ArrayRef,
256+
) -> VortexResult<BitPackedArray> {
257+
let mut slots: ArraySlots = array.slots().iter().cloned().collect();
258+
slots[BitPackedSlots::CHUNK_OFFSETS] = Some(table);
259+
let dtype = array.dtype().clone();
260+
let len = array.len();
261+
Array::try_from_parts(
262+
ArrayParts::new(BitPacked, dtype, len, array.into_data()).with_slots(slots),
263+
)
264+
}
265+
237266
pub fn into_parts(array: BitPackedArray) -> BitPackedDataParts {
238267
let len = array.len();
239268
let patches = array.patches();

‎vortex-btrblocks/src/trace_tests.rs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,14 @@ fn trace_scan_like_on_compressed_comment() -> VortexResult<()> {
360360
iter 0 current=vortex.like(bool, len=4096) builder_active=false
361361
execute_until target=AnyCanonical root=fastlanes.delta(u16, len=4097)
362362
iter 0 current=fastlanes.delta(u16, len=4097) builder_active=false
363-
execute_until target=AnyCanonical root=fastlanes.bitpacked(u16, len=5120)
364-
iter 0 current=fastlanes.bitpacked(u16, len=5120) builder_active=false
365-
Done array=vortex.primitive(u16, len=5120)
363+
execute_until target=AnyCanonical root=vortex.dict(u16, len=5120)
364+
iter 0 current=vortex.dict(u16, len=5120) builder_active=false
365+
execute_until target=AnyCanonical root=fastlanes.bitpacked(u8, len=5120)
366+
iter 0 current=fastlanes.bitpacked(u8, len=5120) builder_active=false
367+
Done array=vortex.primitive(u8, len=5120)
368+
iter 1 current=vortex.primitive(u8, len=5120) builder_active=false
369+
return output=vortex.primitive(u8, len=5120)
370+
child_execute_parent session[0]:execute_parent_fn slot=1 parent=vortex.dict(u16, len=5120) child=vortex.primitive(u16, len=24) -> vortex.primitive(u16, len=5120)
366371
iter 1 current=vortex.primitive(u16, len=5120) builder_active=false
367372
return output=vortex.primitive(u16, len=5120)
368373
Done array=vortex.primitive(u16, len=4097)

‎vortex-btrblocks/tests/snapshots/golden__onpair__string_fsst_structured.snap‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: utf8, len=16384, nbytes=653785
6-
root: vortex.onpair(utf8, len=16384) nbytes=139746
6+
root: vortex.onpair(utf8, len=16384) nbytes=140258
77
metadata: dict_bytes_len: 11866
88
dict_offsets: vortex.primitive(u16, len=1626) nbytes=3252
99
metadata: ptype: u16
10-
codes: fastlanes.bitpacked(u16, len=63845) nbytes=88704
10+
codes: fastlanes.bitpacked(u16, len=63845) nbytes=89216
1111
metadata: bit_width: 11, offset: 0
12+
chunk_offsets: vortex.primitive(u64, len=64) nbytes=512
13+
metadata: ptype: u64
1214
codes_offsets: vortex.primitive(u16, len=16385) nbytes=32770
1315
metadata: ptype: u16
1416
uncompressed_lengths: vortex.sparse(u8, len=16384) nbytes=3154

‎vortex-btrblocks/tests/snapshots/golden__regular__binary_low_cardinality.snap‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: binary, len=16384, nbytes=315856
6-
root: vortex.dict(binary, len=16384) nbytes=6196
6+
root: vortex.dict(binary, len=16384) nbytes=6332
77
metadata: all_values_referenced: true
8-
codes: fastlanes.bitpacked(u8, len=16384) nbytes=6144
8+
codes: fastlanes.bitpacked(u8, len=16384) nbytes=6280
99
metadata: bit_width: 3, offset: 0
10+
chunk_offsets: vortex.primitive(u64, len=17) nbytes=136
11+
metadata: ptype: u64
1012
values: vortex.varbin(binary, len=5) nbytes=52
1113
metadata:
1214
offsets: vortex.primitive(u8, len=6) nbytes=6

0 commit comments

Comments
 (0)