Skip to content

Commit b978a04

Browse files
committed
feat(fastlanes): put per-chunk bit widths behind a preview edition
Declare preview2026.09.0 with fastlanes.bitpacked_v2 and make it the default preview edition. Add BitPackingV2Scheme: it produces BitPacked arrays with a width per chunk and declares that its output needs the v2 format, so a writer whose editions do not permit v2 drops it and falls back to BitPackingScheme. While it is present BitPackingScheme defers to it, so FoR residuals and sampled estimates use per-chunk widths too. The CUDA-compatible preset excludes it. Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent ea6c403 commit b978a04

16 files changed

Lines changed: 290 additions & 76 deletions

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vortex-btrblocks/src/builder.rs‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ pub const ALL_SCHEMES: &[&dyn Scheme] = &[
3434
// Per-chunk bit widths need the fastlanes.bitpacked_v2 format, so the writer keeps this
3535
// scheme only when an enabled edition permits it. Its presence switches BitPackingScheme
3636
// to per-chunk widths as well.
37-
#[cfg(feature = "unstable_encodings")]
3837
&integer::BitPackingV2Scheme,
3938
&integer::SparseScheme,
4039
&integer::IntDictScheme,
@@ -170,10 +169,6 @@ impl BtrBlocksCompressorBuilder {
170169
pub fn only_cuda_compatible(self) -> Self {
171170
// Keep FSST, which has a CUDA decoder and direct Arrow offset-based export. Other
172171
// string fragmentation and dictionary schemes still require unsupported decode paths.
173-
#[cfg_attr(
174-
not(any(feature = "pco", feature = "unstable_encodings")),
175-
allow(unused_mut)
176-
)]
177172
let mut excluded: Vec<SchemeId> = vec![
178173
integer::SparseScheme.id(),
179174
integer::IntRLEScheme.id(),
@@ -191,7 +186,6 @@ impl BtrBlocksCompressorBuilder {
191186
#[cfg(feature = "unstable_encodings")]
192187
excluded.push(integer::DeltaScheme::default().id());
193188
// CUDA bit-unpacking has no per-chunk width kernel yet.
194-
#[cfg(feature = "unstable_encodings")]
195189
excluded.push(integer::BitPackingV2Scheme.id());
196190
#[cfg(feature = "pco")]
197191
excluded.extend([integer::PcoScheme.id(), float::PcoScheme.id()]);
@@ -300,6 +294,25 @@ mod tests {
300294
}
301295
}
302296

297+
/// Per-chunk bit widths have no CUDA decode kernel yet, so the preset keeps only the
298+
/// single-width bit-packing scheme.
299+
#[test]
300+
fn cuda_compatible_excludes_per_chunk_bitpacking() {
301+
let builder = BtrBlocksCompressorBuilder::default().only_cuda_compatible();
302+
assert!(
303+
!builder
304+
.schemes
305+
.iter()
306+
.any(|s| s.id() == integer::BitPackingV2Scheme.id())
307+
);
308+
assert!(
309+
builder
310+
.schemes
311+
.iter()
312+
.any(|s| s.id() == integer::BitPackingScheme.id())
313+
);
314+
}
315+
303316
#[test]
304317
fn cuda_compatible_uses_fsst_for_strings() {
305318
let builder = BtrBlocksCompressorBuilder::default().only_cuda_compatible();

‎vortex-btrblocks/src/schemes/integer/bitpacking.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::ArrayAndStats;
2525
use crate::CascadingCompressor;
2626
use crate::CompressorContext;
2727
use crate::Scheme;
28+
use crate::SchemeExt;
2829
use crate::compress_patches;
2930

3031
/// BitPacking encoding for non-negative integers.
@@ -66,11 +67,17 @@ impl Scheme for BitPackingScheme {
6667

6768
fn compress(
6869
&self,
69-
_compressor: &CascadingCompressor,
70+
compressor: &CascadingCompressor,
7071
data: &ArrayAndStats,
71-
_compress_ctx: CompressorContext,
72+
compress_ctx: CompressorContext,
7273
exec_ctx: &mut ExecutionCtx,
7374
) -> VortexResult<ArrayRef> {
75+
// Per-chunk widths are strictly better when the writer may serialize them; the v2 scheme
76+
// is present exactly then.
77+
if compressor.has_scheme(super::BitPackingV2Scheme.id()) {
78+
return super::BitPackingV2Scheme.compress(compressor, data, compress_ctx, exec_ctx);
79+
}
80+
7481
let primitive_array = data.array_as_primitive();
7582

7683
let histogram = bit_width_histogram(primitive_array, exec_ctx)?;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! BitPacking with a bit width per FastLanes chunk.
5+
6+
use vortex_array::ArrayId;
7+
use vortex_array::ArrayRef;
8+
use vortex_array::Canonical;
9+
use vortex_array::ExecutionCtx;
10+
use vortex_array::IntoArray;
11+
use vortex_array::VTable;
12+
use vortex_compressor::scheme::CompressionEstimate;
13+
use vortex_compressor::scheme::DeferredEstimate;
14+
use vortex_compressor::scheme::EstimateVerdict;
15+
use vortex_error::VortexResult;
16+
use vortex_fastlanes::BitPacked;
17+
use vortex_fastlanes::BitPackedArrayExt;
18+
use vortex_fastlanes::bitpack_compress::bitpack_to_best_chunk_widths;
19+
use vortex_fastlanes::bitpacked_v2_id;
20+
21+
use crate::ArrayAndStats;
22+
use crate::CascadingCompressor;
23+
use crate::CompressorContext;
24+
use crate::Scheme;
25+
use crate::compress_patches;
26+
27+
/// BitPacking for non-negative integers with a width chosen per 1024-element chunk.
28+
///
29+
/// The output is an ordinary [`BitPacked`] array, but one whose chunks may differ in width, which
30+
/// serializes as `fastlanes.bitpacked_v2`. The writer therefore keeps this scheme only when an
31+
/// enabled edition permits that format; while it is present, [`super::BitPackingScheme`] defers
32+
/// to it so bit-packed residuals get per-chunk widths too.
33+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
34+
pub struct BitPackingV2Scheme;
35+
36+
impl Scheme for BitPackingV2Scheme {
37+
fn scheme_name(&self) -> &'static str {
38+
"vortex.int.bitpacking_v2"
39+
}
40+
41+
fn matches(&self, canonical: &Canonical) -> bool {
42+
canonical.dtype().is_int()
43+
}
44+
45+
fn produced_encodings(&self) -> Vec<ArrayId> {
46+
vec![BitPacked.id()]
47+
}
48+
49+
fn produced_serialized_ids(&self) -> Vec<ArrayId> {
50+
vec![bitpacked_v2_id()]
51+
}
52+
53+
fn expected_compression_ratio(
54+
&self,
55+
data: &ArrayAndStats,
56+
_compress_ctx: CompressorContext,
57+
exec_ctx: &mut ExecutionCtx,
58+
) -> CompressionEstimate {
59+
let stats = data.integer_stats(exec_ctx);
60+
61+
// BitPacking only works for non-negative values.
62+
if stats.erased().min_is_negative() {
63+
return CompressionEstimate::Verdict(EstimateVerdict::Skip);
64+
}
65+
66+
CompressionEstimate::Deferred(DeferredEstimate::Sample)
67+
}
68+
69+
fn compress(
70+
&self,
71+
_compressor: &CascadingCompressor,
72+
data: &ArrayAndStats,
73+
_compress_ctx: CompressorContext,
74+
exec_ctx: &mut ExecutionCtx,
75+
) -> VortexResult<ArrayRef> {
76+
let primitive_array = data.array_as_primitive().into_owned();
77+
let packed = bitpack_to_best_chunk_widths(&primitive_array, exec_ctx)?;
78+
79+
// If every chunk needs the full bit-width, return the original array.
80+
if packed.chunk_widths().uniform_width().map(usize::from)
81+
== Some(primitive_array.ptype().bit_width())
82+
{
83+
return Ok(primitive_array.into_array());
84+
}
85+
86+
let Some(patches) = packed.patches() else {
87+
return Ok(packed.into_array());
88+
};
89+
90+
// Compress the patches and place them back into the array.
91+
let packed_stats = packed.statistics().to_owned();
92+
let ptype = packed.dtype().as_ptype();
93+
let mut parts = BitPacked::into_parts(packed);
94+
parts.patches = Some(compress_patches(patches, exec_ctx)?);
95+
Ok(BitPacked::try_new(
96+
parts.packed,
97+
ptype,
98+
parts.validity,
99+
parts.patches,
100+
parts.widths,
101+
parts.len,
102+
parts.offset,
103+
)?
104+
.with_stats_set(packed_stats)
105+
.into_array())
106+
}
107+
}

‎vortex-btrblocks/src/schemes/integer/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! Integer compression schemes.
55
66
mod bitpacking;
7+
mod bitpacking_v2;
78
#[cfg(feature = "unstable_encodings")]
89
mod delta;
910
mod for_;
@@ -17,6 +18,7 @@ mod zigzag;
1718
mod pco;
1819

1920
pub use bitpacking::BitPackingScheme;
21+
pub use bitpacking_v2::BitPackingV2Scheme;
2022
#[cfg(feature = "unstable_encodings")]
2123
pub use delta::DeltaScheme;
2224
pub use for_::FoRScheme;

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: f64, len=16384, nbytes=131072
6-
root: vortex.alp(f64, len=16384) nbytes=10233
6+
root: vortex.alp(f64, len=16384) nbytes=10232
77
metadata: exponents: e: 16, f: 12, patch_offset: 0
8-
encoded: vortex.dict(i64, len=16384) nbytes=6200
8+
encoded: vortex.dict(i64, len=16384) nbytes=6199
99
metadata: all_values_referenced: true
1010
codes: fastlanes.bitpacked(u8, len=16384) nbytes=6144
1111
metadata: bit_width: 3, offset: 0
12-
values: vortex.primitive(i64, len=7) nbytes=56
13-
metadata: ptype: i64
12+
values: fastlanes.bitpacked(i64, len=7) nbytes=55
13+
metadata: bit_width: 0, offset: 0
14+
patch_indices: vortex.primitive(u8, len=6) nbytes=6
15+
metadata: ptype: u8
16+
patch_values: vortex.primitive(i64, len=6) nbytes=48
17+
metadata: ptype: i64
18+
patch_chunk_offsets: vortex.primitive(u8, len=1) nbytes=1
19+
metadata: ptype: u8
1420
patch_indices: vortex.primitive(u16, len=1996) nbytes=3992
1521
metadata: ptype: u16
1622
patch_values: vortex.constant(f64, len=1996) nbytes=9

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: i64, len=16384, nbytes=131072
6-
root: vortex.dict(i64, len=16384) nbytes=6192
6+
root: vortex.dict(i64, len=16384) nbytes=6190
77
metadata: all_values_referenced: true
88
codes: fastlanes.bitpacked(u8, len=16384) nbytes=6144
99
metadata: bit_width: 3, offset: 0
10-
values: vortex.primitive(i64, len=6) nbytes=48
11-
metadata: ptype: i64
10+
values: fastlanes.bitpacked(i64, len=6) nbytes=46
11+
metadata: bit_width: 0, offset: 0
12+
patch_indices: vortex.primitive(u8, len=5) nbytes=5
13+
metadata: ptype: u8
14+
patch_values: vortex.primitive(i64, len=5) nbytes=40
15+
metadata: ptype: i64
16+
patch_chunk_offsets: vortex.primitive(u8, len=1) nbytes=1
17+
metadata: ptype: u8

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: u64, len=16384, nbytes=131072
6-
root: fastlanes.for(u64, len=16384) nbytes=49152
6+
root: fastlanes.for(u64, len=16384) nbytes=47232
77
metadata: reference: 1700000001036u64
8-
encoded: fastlanes.bitpacked(u64, len=16384) nbytes=49152
9-
metadata: bit_width: 24, offset: 0
8+
encoded: fastlanes.bitpacked(u64, len=16384) nbytes=47232
9+
metadata: bit_widths: 16 chunks, max 24, offset: 0

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: list(i32), len=4066, nbytes=81804
6-
root: vortex.list(list(i32), len=4066) nbytes=11146
6+
root: vortex.list(list(i32), len=4066) nbytes=10940
77
metadata:
88
elements: vortex.runend(i32, len=16384) nbytes=3968
99
metadata: offset: 0
@@ -15,11 +15,11 @@ root: vortex.list(list(i32), len=4066) nbytes=11146
1515
metadata: reference: -49931i32
1616
encoded: fastlanes.bitpacked(i32, len=1020) nbytes=2176
1717
metadata: bit_width: 17, offset: 0
18-
offsets: fastlanes.bitpacked(u16, len=4067) nbytes=7178
19-
metadata: bit_width: 14, offset: 0
20-
patch_indices: vortex.primitive(u16, len=1) nbytes=2
18+
offsets: fastlanes.bitpacked(u16, len=4067) nbytes=6972
19+
metadata: bit_widths: 4 chunks, max 14, offset: 0
20+
patch_indices: vortex.primitive(u16, len=14) nbytes=28
21+
metadata: ptype: u16
22+
patch_values: vortex.primitive(u16, len=14) nbytes=28
2123
metadata: ptype: u16
22-
patch_values: vortex.constant(u16, len=1) nbytes=4
23-
metadata: scalar: 16384u16
2424
patch_chunk_offsets: vortex.primitive(u8, len=4) nbytes=4
2525
metadata: ptype: u8

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ source: vortex-btrblocks/tests/golden.rs
33
expression: rendered
44
---
55
input: utf8, len=16384, nbytes=653785
6-
root: vortex.fsst(utf8, len=16384) nbytes=151382
6+
root: vortex.fsst(utf8, len=16384) nbytes=147181
77
metadata: len: 16384, nsymbols: 223
88
uncompressed_lengths: vortex.sparse(u8, len=16384) nbytes=3154
99
metadata: fill_value: 24u8
1010
patch_indices: vortex.primitive(u16, len=1575) nbytes=3150
1111
metadata: ptype: u16
1212
patch_values: vortex.constant(u8, len=1575) nbytes=2
1313
metadata: scalar: 23u8
14-
codes_offsets: fastlanes.bitpacked(u32, len=16385) nbytes=36992
15-
metadata: bit_width: 17, offset: 0
14+
codes_offsets: fastlanes.bitpacked(u32, len=16385) nbytes=32791
15+
metadata: bit_widths: 17 chunks, max 17, offset: 0
16+
patch_indices: vortex.primitive(u16, len=1) nbytes=2
17+
metadata: ptype: u16
18+
patch_values: vortex.constant(u32, len=1) nbytes=4
19+
metadata: scalar: 109229u32
20+
patch_chunk_offsets: vortex.primitive(u8, len=17) nbytes=17
21+
metadata: ptype: u8

0 commit comments

Comments
 (0)