Skip to content

Commit ba5fcfc

Browse files
authored
Blocked FoR with one reference per 1024-element chunk (#10105)
Stacked on #10104. ## Summary The in-memory `FoR` array now allows one reference per 1024-element chunk instead of a single global reference per array. This is supported by adding a `references` child that records each reference. Element `i` decodes as `encoded[i] + references[(offset + i) / 1024]` with wrapping arithmetic. This also requires adding an `offset` pointer. This is the position of the first element within the first chunk and only matters for sliced arrays. The wire format is unchanged. `fastlanes.for` reads its single reference as a `ConstantArray` child, and `FoRPlugin` writes `fastlanes.for` only when the references are constant. Note that FoR encoding always uses a constant reference per array still and serialization refuses arrays that have non-constant references, so this change will not lead to breaks. ## Example ### References A 3000-row array spans three chunks, each with its own reference. `encoded` holds each value minus its chunk's reference. ```text rows 0 ─────────── 1023 │ 1024 ────────── 2047 │ 2048 ──────── 2999 references [ 1_000_000 │ 5_000_000 │ 9_000_000 ] encoded [ small values │ small values │ small values ] value[1500] = encoded[1500] + references[1500 / 1024] = encoded[1500] + references[1] = encoded[1500] + 5_000_000 ``` ### Offset Slicing is zero-copy, so a slice can start partway through a chunk. `offset` records how far into its first chunk the slice starts. ```text │◄──── chunk 0 ────►│◄──── chunk 1 ────►│◄──── chunk 2 ────►│ rows 0 1024 1500 2048 2500 3000 slice [═══════════════════) │◄ 476 ►│ slice(1500..2500): encoded = encoded.slice(1500..2500) 1000 rows references = references.slice(1..3) [5_000_000, 9_000_000] offset = 1500 % 1024 476 slice row i uses references[(476 + i) / 1024]: i = 0..=547 → references[0] = 5_000_000 (original chunk 1) i = 548..=999 → references[1] = 9_000_000 (original chunk 2) ``` The new offset is always below 1024. Slicing a slice adds the offsets and slices the references again. ### Constant references (existing files) ```text fastlanes.for on disk metadata = reference 42, children = [encoded] │ read ▲ write (only when references are constant) ▼ │ in memory references = ConstantArray(42, len = num_chunks), offset = 0 ``` ## API - `FoRSlots` gains `references`. - `FoRData` holds only `offset: u16`. The stored reference scalar is gone. - `FoRArrayExt::reference_scalar() -> &Scalar` is replaced by `constant_reference() -> Option<Scalar>`, which is `Some` when `references` is a `ConstantArray`. `offset()` and `ptype()` move onto the extension trait. - `FoR::try_new(encoded, reference)` keeps its signature and builds the constant child. `FoR::try_new_chunked(encoded, references, offset)` accepts any references. ## Kernels With constant references, every kernel takes its existing path. That includes Eq/NotEq compare pushdown, is_constant, is_sorted, take, filter, the fused BitPacked decode, and CUDA FFOR and dynamic dispatch. With varying references: - Decoding adds each chunk's reference in place. `scalar_at` looks up the element's chunk reference. `slice` slices the references by chunk and carries the new offset. `cast` casts the references. - Compare, is_constant, is_sorted, take and filter return `None` and fall back to decoding. - CUDA decodes these arrays on the CPU and never fuses them into a dispatch plan. Callers that rewrap a FoR's encoded child (the btrblocks FoR scheme, benches, CUDA tests) now pass through `references()` and `offset()` via `try_new_chunked`. --------- Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent a06672a commit ba5fcfc

28 files changed

Lines changed: 499 additions & 140 deletions

‎encodings/fastlanes/src/for/array/for_compress.rs‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ mod test {
8383
let mut ctx = SESSION.create_execution_ctx();
8484
let array = PrimitiveArray::new((1i32..10).collect::<Buffer<_>>(), Validity::NonNullable);
8585
let compressed = FoRData::encode(array.clone(), &mut ctx).unwrap();
86-
assert_eq!(i32::try_from(compressed.reference_scalar()).unwrap(), 1);
86+
assert_eq!(
87+
i32::try_from(&compressed.constant_reference().unwrap()).unwrap(),
88+
1
89+
);
8790

8891
assert_arrays_eq!(compressed, array, &mut ctx);
8992
}
@@ -98,7 +101,7 @@ mod test {
98101
);
99102
let compressed = FoRData::encode(array, &mut ctx).unwrap();
100103
assert_eq!(
101-
u32::try_from(compressed.reference_scalar()).unwrap(),
104+
u32::try_from(&compressed.constant_reference().unwrap()).unwrap(),
102105
1_000_000u32
103106
);
104107
}
@@ -111,8 +114,14 @@ mod test {
111114

112115
let dtype = array.dtype().clone();
113116
let compressed = FoRData::encode(array, &mut ctx).unwrap();
114-
assert_eq!(compressed.reference_scalar().dtype(), &dtype);
115-
assert!(compressed.reference_scalar().dtype().is_signed_int());
117+
assert_eq!(compressed.constant_reference().unwrap().dtype(), &dtype);
118+
assert!(
119+
compressed
120+
.constant_reference()
121+
.unwrap()
122+
.dtype()
123+
.is_signed_int()
124+
);
116125
assert!(compressed.encoded().dtype().is_signed_int());
117126

118127
let encoded = compressed.encoded().execute_scalar(0, &mut ctx).unwrap();
@@ -160,7 +169,8 @@ mod test {
160169
assert_eq!(
161170
i8::MIN,
162171
compressed
163-
.reference_scalar()
172+
.constant_reference()
173+
.unwrap()
164174
.as_primitive()
165175
.typed_value::<i8>()
166176
.unwrap()

‎encodings/fastlanes/src/for/array/for_decompress.rs‎

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::iter;
45
use std::mem::MaybeUninit;
56

67
use fastlanes::FoR;
8+
use itertools::Itertools;
79
use num_traits::PrimInt;
810
use num_traits::WrappingAdd;
911
use vortex_array::ArrayView;
@@ -15,9 +17,11 @@ use vortex_array::dtype::PhysicalPType;
1517
use vortex_array::dtype::UnsignedPType;
1618
use vortex_array::match_each_integer_ptype;
1719
use vortex_array::match_each_unsigned_integer_ptype;
20+
use vortex_array::scalar::Scalar;
1821
use vortex_buffer::Buffer;
1922
use vortex_error::VortexExpect;
2023
use vortex_error::VortexResult;
24+
use vortex_error::vortex_err;
2125

2226
use crate::BitPacked;
2327
use crate::BitPackedArrayExt;
@@ -51,13 +55,27 @@ impl<T: PhysicalPType<Physical = T> + FoR> UnpackStrategy<T> for FoRStrategy<T>
5155
}
5256

5357
pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult<PrimitiveArray> {
58+
match array.constant_reference() {
59+
Some(reference) => decompress_one_ref(array, &reference, ctx),
60+
None => {
61+
match_each_integer_ptype!(array.ptype(), |T| { decompress_many_refs::<T>(array, ctx) })
62+
}
63+
}
64+
}
65+
66+
/// Decompress an array whose chunks all share `reference`.
67+
fn decompress_one_ref(
68+
array: &FoRArray,
69+
reference: &Scalar,
70+
ctx: &mut ExecutionCtx,
71+
) -> VortexResult<PrimitiveArray> {
5472
let ptype = array.ptype();
5573

5674
// Try to do fused unpack.
57-
if array.reference_scalar().dtype().is_unsigned_int()
75+
if ptype.is_unsigned_int()
5876
&& let Some(bp) = array.encoded().as_opt::<BitPacked>()
5977
{
60-
return match_each_unsigned_integer_ptype!(array.ptype(), |T| {
78+
return match_each_unsigned_integer_ptype!(ptype, |T| {
6179
fused_decompress::<T>(array, bp, ctx)
6280
});
6381
}
@@ -67,8 +85,7 @@ pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult<Prim
6785
let validity = encoded.validity()?;
6886

6987
Ok(match_each_integer_ptype!(ptype, |T| {
70-
let min = array
71-
.reference_scalar()
88+
let min = reference
7289
.as_primitive()
7390
.typed_value::<T>()
7491
.vortex_expect("reference must be non-null");
@@ -83,6 +100,32 @@ pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult<Prim
83100
}))
84101
}
85102

103+
/// Decompress an array whose chunks have different references.
104+
fn decompress_many_refs<T: NativePType + WrappingAdd + PrimInt>(
105+
array: &FoRArray,
106+
ctx: &mut ExecutionCtx,
107+
) -> VortexResult<PrimitiveArray> {
108+
let encoded = array.encoded().clone().execute::<PrimitiveArray>(ctx)?;
109+
if encoded.is_empty() {
110+
return Ok(encoded);
111+
}
112+
let validity = encoded.validity()?;
113+
let references = array.references().clone().execute::<PrimitiveArray>(ctx)?;
114+
let references = references.as_slice::<T>();
115+
116+
// The first chunk may be partial when the array was sliced.
117+
let first_len = (FL_CHUNK_SIZE - usize::from(array.offset())).min(array.len());
118+
let mut values = encoded.into_buffer_mut::<T>();
119+
let (first, rest) = values.as_mut_slice().split_at_mut(first_len);
120+
let chunks = iter::once(first).chain(rest.chunks_mut(FL_CHUNK_SIZE));
121+
for (chunk, &reference) in chunks.zip_eq(references) {
122+
for value in chunk {
123+
*value = value.wrapping_add(&reference);
124+
}
125+
}
126+
Ok(PrimitiveArray::new(values.freeze(), validity))
127+
}
128+
86129
pub(crate) fn fused_decompress<
87130
T: PhysicalPType<Physical = T> + UnsignedPType + FoR + WrappingAdd,
88131
>(
@@ -91,7 +134,8 @@ pub(crate) fn fused_decompress<
91134
ctx: &mut ExecutionCtx,
92135
) -> VortexResult<PrimitiveArray> {
93136
let ref_ = for_
94-
.reference_scalar()
137+
.constant_reference()
138+
.ok_or_else(|| vortex_err!("fused FoR decompression requires a constant reference"))?
95139
.as_primitive()
96140
.as_::<T>()
97141
.vortex_expect("cannot be null");
@@ -110,7 +154,7 @@ pub(crate) fn fused_decompress<
110154
)?;
111155

112156
let mut builder = PrimitiveBuilder::<T>::with_capacity_in(
113-
for_.reference_scalar().dtype().nullability(),
157+
for_.dtype().nullability(),
114158
bp.len(),
115159
ctx.allocator(),
116160
);

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

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,71 @@ use vortex_array::scalar::Scalar;
1212
use vortex_error::VortexResult;
1313
use vortex_error::vortex_ensure;
1414

15+
use crate::FL_CHUNK_SIZE;
16+
1517
pub mod for_compress;
1618
pub mod for_decompress;
1719

1820
#[array_slots(crate::FoR)]
1921
pub struct FoRSlots {
20-
/// The encoded array with the frame-of-reference (minimum value) subtracted.
22+
/// The encoded array with each chunk's reference subtracted.
2123
#[slot(0)]
2224
pub encoded: ArrayRef,
25+
/// One reference per [`FL_CHUNK_SIZE`]-element chunk.
26+
#[slot(1)]
27+
pub references: ArrayRef,
2328
}
2429

2530
/// Frame of Reference (FoR) encoded array.
2631
///
2732
/// This encoding stores values as offsets from a reference value, which can significantly reduce
2833
/// storage requirements when values are clustered around a specific point.
34+
///
35+
/// Every [`FL_CHUNK_SIZE`]-element chunk has its own reference: element `i` decodes as
36+
/// `encoded[i] + references[(offset + i) / FL_CHUNK_SIZE]` with wrapping arithmetic, where
37+
/// `offset` is the position of the first element within the first chunk. Arrays with a single
38+
/// reference store a constant `references` child.
2939
#[derive(Clone, Debug)]
3040
pub struct FoRData {
31-
pub(super) reference: Scalar,
41+
pub(super) offset: u16,
3242
}
3343

3444
pub trait FoRArrayExt: FoRArraySlotsExt {
35-
fn reference_scalar(&self) -> &Scalar {
36-
&self.reference
45+
/// The reference shared by every chunk, if the references are constant.
46+
fn constant_reference(&self) -> Option<Scalar> {
47+
self.references().as_constant()
48+
}
49+
50+
/// The position of the first element within the first chunk of `references`.
51+
fn offset(&self) -> u16 {
52+
self.offset
53+
}
54+
55+
#[inline]
56+
fn ptype(&self) -> PType {
57+
self.as_ref().dtype().as_ptype()
3758
}
3859
}
3960

4061
impl<T: TypedArrayRef<crate::FoR>> FoRArrayExt for T {}
4162

4263
impl Display for FoRData {
4364
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44-
write!(f, "reference: {}", self.reference)
65+
write!(f, "offset: {}", self.offset)
4566
}
4667
}
4768

4869
impl FoRData {
49-
pub(crate) fn try_new(reference: Scalar) -> VortexResult<Self> {
50-
vortex_ensure!(!reference.is_null(), "Reference value cannot be null");
70+
pub(crate) fn try_new(offset: u16) -> VortexResult<Self> {
5171
vortex_ensure!(
52-
reference.dtype().is_int(),
53-
"FoR requires an integer reference dtype, got {}",
54-
reference.dtype()
72+
usize::from(offset) < FL_CHUNK_SIZE,
73+
"FoR offset must be less than {FL_CHUNK_SIZE}, got {offset}"
5574
);
56-
Ok(Self { reference })
75+
Ok(Self { offset })
5776
}
77+
}
5878

59-
#[inline]
60-
pub fn ptype(&self) -> PType {
61-
self.reference.dtype().as_ptype()
62-
}
79+
/// The number of chunks spanned by `len` elements starting at `offset` within the first chunk.
80+
pub(crate) fn num_chunks(offset: u16, len: usize) -> usize {
81+
(usize::from(offset) + len).div_ceil(FL_CHUNK_SIZE)
6382
}

‎encodings/fastlanes/src/for/compute/cast.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ impl CastReduce for FoR {
2121

2222
// For type changes between integers, cast the components
2323
let casted_child = array.encoded().cast(dtype.clone())?;
24-
let casted_reference = array.reference_scalar().cast(dtype)?;
24+
// References are always non-nullable.
25+
let casted_references = array.references().cast(dtype.as_nonnullable())?;
2526

2627
Ok(Some(
27-
FoR::try_new(casted_child, casted_reference)?.into_array(),
28+
FoR::try_new_chunked(casted_child, casted_references, array.offset())?.into_array(),
2829
))
2930
}
3031
}

‎encodings/fastlanes/src/for/compute/compare.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ where
6969
return Ok(None);
7070
}
7171

72-
let reference = lhs.reference_scalar();
72+
// TODO(mk): support many references.
73+
74+
let Some(reference) = lhs.constant_reference() else {
75+
return Ok(None);
76+
};
7377
let reference = reference.as_primitive().typed_value::<T>();
7478

7579
// We encode the RHS into the FoR domain.

‎encodings/fastlanes/src/for/compute/is_constant.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use vortex_array::scalar::Scalar;
1111
use vortex_error::VortexResult;
1212

1313
use crate::FoR;
14+
use crate::r#for::array::FoRArrayExt;
1415
use crate::r#for::array::FoRArraySlotsExt;
1516

1617
/// FoR-specific is_constant kernel.
@@ -33,6 +34,10 @@ impl DynAggregateKernel for FoRIsConstantKernel {
3334
let Some(array) = batch.as_opt::<FoR>() else {
3435
return Ok(None);
3536
};
37+
// TODO(mk): support many references.
38+
if array.constant_reference().is_none() {
39+
return Ok(None);
40+
}
3641

3742
let result = is_constant(array.encoded(), ctx)?;
3843
Ok(Some(IsConstant::make_partial(batch, result, ctx)?))

‎encodings/fastlanes/src/for/compute/is_sorted.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use vortex_array::scalar::Scalar;
1414
use vortex_error::VortexResult;
1515

1616
use crate::FoR;
17+
use crate::r#for::array::FoRArrayExt;
1718
use crate::r#for::array::FoRArraySlotsExt;
1819

1920
#[derive(Debug)]
@@ -33,6 +34,10 @@ impl DynAggregateKernel for FoRIsSortedKernel {
3334
let Some(array) = batch.as_opt::<FoR>() else {
3435
return Ok(None);
3536
};
37+
// TODO(mk): support many references.
38+
if array.constant_reference().is_none() {
39+
return Ok(None);
40+
}
3641

3742
let encoded = array.encoded().clone().execute::<PrimitiveArray>(ctx)?;
3843
let unsigned_array = PrimitiveArray::from_buffer_handle(

‎encodings/fastlanes/src/for/compute/mod.rs‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ impl TakeExecute for FoR {
2525
indices: &ArrayRef,
2626
_ctx: &mut ExecutionCtx,
2727
) -> VortexResult<Option<ArrayRef>> {
28+
// TODO(mk): support many references.
29+
let Some(reference) = array.constant_reference() else {
30+
return Ok(None);
31+
};
2832
Ok(Some(
29-
FoR::try_new(
30-
array.encoded().take(indices.clone())?,
31-
array.reference_scalar().clone(),
32-
)?
33-
.into_array(),
33+
FoR::try_new(array.encoded().take(indices.clone())?, reference)?.into_array(),
3434
))
3535
}
3636
}
3737

3838
impl FilterReduce for FoR {
3939
fn filter(array: ArrayView<'_, Self>, mask: &Mask) -> VortexResult<Option<ArrayRef>> {
40-
FoR::try_new(
41-
array.encoded().filter(mask.clone())?,
42-
array.reference_scalar().clone(),
43-
)
44-
.map(|a| Some(a.into_array()))
40+
// TODO(mk): support many references.
41+
let Some(reference) = array.constant_reference() else {
42+
return Ok(None);
43+
};
44+
FoR::try_new(array.encoded().filter(mask.clone())?, reference).map(|a| Some(a.into_array()))
4545
}
4646
}
4747

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ pub use array::FoRSlots;
99

1010
pub(crate) mod compute;
1111

12+
#[cfg(test)]
13+
mod tests;
14+
1215
mod plugin;
1316
pub use plugin::FoRPlugin;
1417

0 commit comments

Comments
 (0)