Skip to content

Commit a906ba7

Browse files
committed
feat(array): migrate BitPacked encoding to scalar probes
Signed-off-by: Gunther Xing <jiengup@gmail.com>
1 parent 6277a31 commit a906ba7

5 files changed

Lines changed: 475 additions & 26 deletions

File tree

‎encodings/fastlanes/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ harness = false
5959
name = "bitpack_blocked_decompress"
6060
harness = false
6161

62+
[[bench]]
63+
name = "bitpacked_probe"
64+
harness = false
65+
6266
[[bench]]
6367
name = "canonicalize_bench"
6468
harness = false
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![expect(clippy::unwrap_used)]
5+
6+
use std::sync::LazyLock;
7+
8+
use divan::Bencher;
9+
use mimalloc::MiMalloc;
10+
use vortex_array::ArrayRef;
11+
use vortex_array::IntoArray;
12+
use vortex_array::VortexSessionExecute;
13+
use vortex_array::arrays::PrimitiveArray;
14+
use vortex_array::scalar::Scalar;
15+
use vortex_fastlanes::BitPacked;
16+
use vortex_fastlanes::BitPackedArrayExt;
17+
use vortex_fastlanes::BitPackedArraySlotsExt;
18+
use vortex_fastlanes::FoR;
19+
use vortex_fastlanes::bitpack_compress::bitpack_encode;
20+
use vortex_fastlanes::bitpack_compress::bitpack_encode_blocked;
21+
use vortex_session::VortexSession;
22+
23+
#[global_allocator]
24+
static GLOBAL: MiMalloc = MiMalloc;
25+
26+
fn main() {
27+
LazyLock::force(&SESSION);
28+
divan::main();
29+
}
30+
31+
const LEN: usize = 65_536;
32+
const ACCESSES: usize = 256;
33+
const FIXTURES: &[(&str, bool)] = &[
34+
("global", false),
35+
("global", true),
36+
("blocked_primitive", false),
37+
("blocked_primitive", true),
38+
("blocked_for", false),
39+
("blocked_for", true),
40+
];
41+
const PATCHED_FIXTURES: &[&str] = &["global", "blocked_primitive", "blocked_for"];
42+
43+
static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
44+
let session = vortex_array::array_session();
45+
vortex_fastlanes::initialize(&session);
46+
session
47+
});
48+
49+
fn fixture(mode: &str, patched: bool) -> ArrayRef {
50+
let widths: Vec<u8> = (0..LEN / 1024).map(|block| 4 + (block % 4) as u8).collect();
51+
let values = PrimitiveArray::from_iter((0..LEN).map(|index| {
52+
let width = widths[index / 1024];
53+
if patched && index % 64 == 0 {
54+
65_535u32
55+
} else {
56+
((index * 7919) as u32) & ((1u32 << width) - 1)
57+
}
58+
}));
59+
let mut ctx = SESSION.create_execution_ctx();
60+
let encoded = if mode == "global" {
61+
bitpack_encode(&values, 7, None, &mut ctx).unwrap()
62+
} else {
63+
bitpack_encode_blocked(&values, &widths, None, &mut ctx).unwrap()
64+
};
65+
assert_eq!(encoded.patches().is_some(), patched);
66+
if mode == "blocked_for" {
67+
let offsets = encoded.block_offsets().unwrap().clone();
68+
let reference = Scalar::zero_value(offsets.dtype());
69+
let offsets = FoR::try_new(offsets, reference).unwrap().into_array();
70+
BitPacked::try_new_with_block_offsets(
71+
encoded.packed().clone(),
72+
encoded.dtype().as_ptype(),
73+
encoded.validity().unwrap(),
74+
encoded.patches(),
75+
offsets,
76+
encoded.len(),
77+
encoded.offset(),
78+
)
79+
.unwrap()
80+
.into_array()
81+
} else {
82+
encoded.into_array()
83+
}
84+
}
85+
86+
fn random_index(read: usize) -> usize {
87+
(read * 7919) % LEN
88+
}
89+
90+
#[divan::bench(args = FIXTURES)]
91+
fn random_once(bencher: Bencher, (mode, patched): (&str, bool)) {
92+
let array = fixture(mode, patched);
93+
bencher
94+
.with_inputs(|| SESSION.create_execution_ctx())
95+
.bench_refs(|ctx| {
96+
for read in 0..ACCESSES {
97+
divan::black_box(array.execute_scalar(random_index(read), ctx).unwrap());
98+
}
99+
});
100+
}
101+
102+
#[divan::bench(args = FIXTURES)]
103+
fn random_repeated(bencher: Bencher, (mode, patched): (&str, bool)) {
104+
let array = fixture(mode, patched);
105+
bencher
106+
.with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe()))
107+
.bench_refs(|(ctx, probe)| {
108+
for read in 0..ACCESSES {
109+
divan::black_box(probe.execute_scalar(random_index(read), ctx).unwrap());
110+
}
111+
});
112+
}
113+
114+
#[divan::bench(args = FIXTURES)]
115+
fn same_block_once(bencher: Bencher, (mode, patched): (&str, bool)) {
116+
let array = fixture(mode, patched);
117+
bencher
118+
.with_inputs(|| SESSION.create_execution_ctx())
119+
.bench_refs(|ctx| {
120+
for read in 0..ACCESSES {
121+
divan::black_box(array.execute_scalar(1024 + read, ctx).unwrap());
122+
}
123+
});
124+
}
125+
126+
#[divan::bench(args = FIXTURES)]
127+
fn same_block_repeated(bencher: Bencher, (mode, patched): (&str, bool)) {
128+
let array = fixture(mode, patched);
129+
bencher
130+
.with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe()))
131+
.bench_refs(|(ctx, probe)| {
132+
for read in 0..ACCESSES {
133+
divan::black_box(probe.execute_scalar(1024 + read, ctx).unwrap());
134+
}
135+
});
136+
}
137+
138+
#[divan::bench(args = PATCHED_FIXTURES)]
139+
fn patch_hit_repeated(bencher: Bencher, mode: &str) {
140+
let array = fixture(mode, true);
141+
bencher
142+
.with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe()))
143+
.bench_refs(|(ctx, probe)| {
144+
for _ in 0..ACCESSES {
145+
divan::black_box(probe.execute_scalar(1024, ctx).unwrap());
146+
}
147+
});
148+
}
149+
150+
#[divan::bench(args = PATCHED_FIXTURES)]
151+
fn patch_miss_repeated(bencher: Bencher, mode: &str) {
152+
let array = fixture(mode, true);
153+
bencher
154+
.with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe()))
155+
.bench_refs(|(ctx, probe)| {
156+
for _ in 0..ACCESSES {
157+
divan::black_box(probe.execute_scalar(1025, ctx).unwrap());
158+
}
159+
});
160+
}

‎encodings/fastlanes/src/bitpacking/array/bitpack_decompress/blocked.rs‎

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use num_traits::AsPrimitive;
1212
use vortex_array::ArrayRef;
1313
use vortex_array::ArrayView;
1414
use vortex_array::ExecutionCtx;
15+
use vortex_array::ProbeChildren;
1516
use vortex_array::arrays::Primitive;
1617
use vortex_array::arrays::PrimitiveArray;
1718
use vortex_array::builders::ArrayBuilder;
@@ -21,12 +22,14 @@ use vortex_array::match_each_integer_ptype;
2122
use vortex_array::match_each_unsigned_integer_ptype;
2223
use vortex_array::scalar::Scalar;
2324
use vortex_buffer::Buffer;
25+
use vortex_error::VortexExpect;
2426
use vortex_error::VortexResult;
2527
use vortex_error::vortex_ensure;
2628

2729
use super::global::apply_patches_to_uninit_range;
2830
use crate::BitPacked;
2931
use crate::BitPackedArrayExt;
32+
use crate::BitPackedSlots;
3033
use crate::FL_CHUNK_SIZE;
3134
use crate::bitpacking::array::block_range;
3235
use crate::bitpacking::array::validate_primitive_offsets;
@@ -223,17 +226,8 @@ pub fn unpack_single_blocked(
223226
block + 1 < offsets.len(),
224227
"BitPacked index {index} has no block boundaries"
225228
);
226-
let (base, start, end) = if let Some(primitive) = offsets.as_opt::<Primitive>()
227-
&& primitive.buffer_handle().is_on_host()
228-
{
229-
match_each_unsigned_integer_ptype!(primitive.ptype(), |I| {
230-
let offsets = primitive.as_slice::<I>();
231-
(
232-
AsPrimitive::<u64>::as_(offsets[0]),
233-
AsPrimitive::<u64>::as_(offsets[block]),
234-
AsPrimitive::<u64>::as_(offsets[block + 1]),
235-
)
236-
})
229+
let (base, start, end) = if let Some(bounds) = primitive_block_bounds(offsets, block) {
230+
bounds
237231
} else {
238232
let base = u64::try_from(&offsets.execute_scalar(0, ctx)?)?;
239233
let start = if block == 0 {
@@ -251,6 +245,104 @@ pub fn unpack_single_blocked(
251245
array.dtype().as_ptype().bit_width() as u64,
252246
array.packed().len(),
253247
)?;
248+
unpack_single_in_range(array, index_in_encoded, range)
249+
}
250+
251+
/// Read only the selected block's boundaries through the parent's stable child slot.
252+
pub(crate) fn unpack_single_blocked_with_probes(
253+
array: ArrayView<'_, BitPacked>,
254+
offsets: &ArrayRef,
255+
index: usize,
256+
children: &mut ProbeChildren<'_, '_, BitPacked>,
257+
mut cache: Option<&mut BlockProbeCache>,
258+
ctx: &mut ExecutionCtx,
259+
) -> VortexResult<Scalar> {
260+
let index_in_encoded = index + array.offset() as usize;
261+
let block = index_in_encoded / FL_CHUNK_SIZE;
262+
if let Some(cached) = cache.as_ref().and_then(|entry| entry.last)
263+
&& cached.block == block
264+
{
265+
return unpack_single_in_range(array, index_in_encoded, cached.start..cached.end);
266+
}
267+
vortex_ensure!(
268+
block + 1 < offsets.len(),
269+
"BitPacked index {index} has no block boundaries"
270+
);
271+
let (base, start, end) = if let Some(bounds) = primitive_block_bounds(offsets, block) {
272+
bounds
273+
} else {
274+
let mut probe = children
275+
.slot(BitPackedSlots::BLOCK_OFFSETS)?
276+
.vortex_expect("blocked BitPacked must have block offsets");
277+
let base = if let Some(base) = cache.as_ref().and_then(|entry| entry.base) {
278+
base
279+
} else {
280+
let base = u64::try_from(&probe.execute_scalar(0, ctx)?)?;
281+
if let Some(cache) = cache.as_deref_mut() {
282+
cache.base = Some(base);
283+
}
284+
base
285+
};
286+
let start = if block == 0 {
287+
base
288+
} else {
289+
u64::try_from(&probe.execute_scalar(block, ctx)?)?
290+
};
291+
let end = u64::try_from(&probe.execute_scalar(block + 1, ctx)?)?;
292+
(base, start, end)
293+
};
294+
let range = block_range(
295+
base,
296+
start,
297+
end,
298+
array.dtype().as_ptype().bit_width() as u64,
299+
array.packed().len(),
300+
)?;
301+
if let Some(cache) = cache {
302+
cache.last = Some(ValidatedBlock {
303+
block,
304+
start: range.start,
305+
end: range.end,
306+
});
307+
}
308+
unpack_single_in_range(array, index_in_encoded, range)
309+
}
310+
311+
/// Preparation retained for one immutable BitPacked array.
312+
#[derive(Default)]
313+
pub(crate) struct BlockProbeCache {
314+
base: Option<u64>,
315+
last: Option<ValidatedBlock>,
316+
}
317+
318+
/// A validated byte range for one block of an immutable BitPacked array.
319+
#[derive(Clone, Copy)]
320+
struct ValidatedBlock {
321+
block: usize,
322+
start: usize,
323+
end: usize,
324+
}
325+
326+
fn primitive_block_bounds(offsets: &ArrayRef, block: usize) -> Option<(u64, u64, u64)> {
327+
let primitive = offsets.as_opt::<Primitive>()?;
328+
if !primitive.buffer_handle().is_on_host() {
329+
return None;
330+
}
331+
Some(match_each_unsigned_integer_ptype!(primitive.ptype(), |I| {
332+
let offsets = primitive.as_slice::<I>();
333+
(
334+
AsPrimitive::<u64>::as_(offsets[0]),
335+
AsPrimitive::<u64>::as_(offsets[block]),
336+
AsPrimitive::<u64>::as_(offsets[block + 1]),
337+
)
338+
}))
339+
}
340+
341+
fn unpack_single_in_range(
342+
array: ArrayView<'_, BitPacked>,
343+
index_in_encoded: usize,
344+
range: Range<usize>,
345+
) -> VortexResult<Scalar> {
254346
let bit_width = range.len() / 128;
255347
match_each_integer_ptype!(array.dtype().as_ptype(), |P| {
256348
let packed = array.packed_slice::<<P as PhysicalPType>::Physical>();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub(crate) use blocked::for_each_packed_block;
99
pub use blocked::unpack_array_blocked;
1010
pub(crate) use blocked::unpack_into_primitive_builder_blocked;
1111
pub use blocked::unpack_single_blocked;
12+
pub(crate) use blocked::unpack_single_blocked_with_probes;
13+
pub(crate) use blocked::BlockProbeCache;
1214
pub use global::count_exceptions;
1315
pub use global::unpack_array;
1416
pub(crate) use global::unpack_into_primitive_builder;

0 commit comments

Comments
 (0)