diff --git a/encodings/fastlanes/src/for/array/for_compress.rs b/encodings/fastlanes/src/for/array/for_compress.rs index 62c23b7ba21..471bec8d176 100644 --- a/encodings/fastlanes/src/for/array/for_compress.rs +++ b/encodings/fastlanes/src/for/array/for_compress.rs @@ -83,7 +83,10 @@ mod test { let mut ctx = SESSION.create_execution_ctx(); let array = PrimitiveArray::new((1i32..10).collect::>(), Validity::NonNullable); let compressed = FoRData::encode(array.clone(), &mut ctx).unwrap(); - assert_eq!(i32::try_from(compressed.reference_scalar()).unwrap(), 1); + assert_eq!( + i32::try_from(&compressed.constant_reference().unwrap()).unwrap(), + 1 + ); assert_arrays_eq!(compressed, array, &mut ctx); } @@ -98,7 +101,7 @@ mod test { ); let compressed = FoRData::encode(array, &mut ctx).unwrap(); assert_eq!( - u32::try_from(compressed.reference_scalar()).unwrap(), + u32::try_from(&compressed.constant_reference().unwrap()).unwrap(), 1_000_000u32 ); } @@ -111,8 +114,14 @@ mod test { let dtype = array.dtype().clone(); let compressed = FoRData::encode(array, &mut ctx).unwrap(); - assert_eq!(compressed.reference_scalar().dtype(), &dtype); - assert!(compressed.reference_scalar().dtype().is_signed_int()); + assert_eq!(compressed.constant_reference().unwrap().dtype(), &dtype); + assert!( + compressed + .constant_reference() + .unwrap() + .dtype() + .is_signed_int() + ); assert!(compressed.encoded().dtype().is_signed_int()); let encoded = compressed.encoded().execute_scalar(0, &mut ctx).unwrap(); @@ -160,7 +169,8 @@ mod test { assert_eq!( i8::MIN, compressed - .reference_scalar() + .constant_reference() + .unwrap() .as_primitive() .typed_value::() .unwrap() diff --git a/encodings/fastlanes/src/for/array/for_decompress.rs b/encodings/fastlanes/src/for/array/for_decompress.rs index e41eb27a5be..76990d3b4cb 100644 --- a/encodings/fastlanes/src/for/array/for_decompress.rs +++ b/encodings/fastlanes/src/for/array/for_decompress.rs @@ -1,9 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use std::iter; use std::mem::MaybeUninit; use fastlanes::FoR; +use itertools::Itertools; use num_traits::PrimInt; use num_traits::WrappingAdd; use vortex_array::ArrayView; @@ -15,9 +17,11 @@ use vortex_array::dtype::PhysicalPType; use vortex_array::dtype::UnsignedPType; use vortex_array::match_each_integer_ptype; use vortex_array::match_each_unsigned_integer_ptype; +use vortex_array::scalar::Scalar; use vortex_buffer::Buffer; use vortex_error::VortexExpect; use vortex_error::VortexResult; +use vortex_error::vortex_err; use crate::BitPacked; use crate::BitPackedArrayExt; @@ -51,13 +55,27 @@ impl + FoR> UnpackStrategy for FoRStrategy } pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult { + match array.constant_reference() { + Some(reference) => decompress_one_ref(array, &reference, ctx), + None => { + match_each_integer_ptype!(array.ptype(), |T| { decompress_many_refs::(array, ctx) }) + } + } +} + +/// Decompress an array whose chunks all share `reference`. +fn decompress_one_ref( + array: &FoRArray, + reference: &Scalar, + ctx: &mut ExecutionCtx, +) -> VortexResult { let ptype = array.ptype(); // Try to do fused unpack. - if array.reference_scalar().dtype().is_unsigned_int() + if ptype.is_unsigned_int() && let Some(bp) = array.encoded().as_opt::() { - return match_each_unsigned_integer_ptype!(array.ptype(), |T| { + return match_each_unsigned_integer_ptype!(ptype, |T| { fused_decompress::(array, bp, ctx) }); } @@ -67,8 +85,7 @@ pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult() .vortex_expect("reference must be non-null"); @@ -83,6 +100,32 @@ pub fn decompress(array: &FoRArray, ctx: &mut ExecutionCtx) -> VortexResult( + array: &FoRArray, + ctx: &mut ExecutionCtx, +) -> VortexResult { + let encoded = array.encoded().clone().execute::(ctx)?; + if encoded.is_empty() { + return Ok(encoded); + } + let validity = encoded.validity()?; + let references = array.references().clone().execute::(ctx)?; + let references = references.as_slice::(); + + // The first chunk may be partial when the array was sliced. + let first_len = (FL_CHUNK_SIZE - usize::from(array.offset())).min(array.len()); + let mut values = encoded.into_buffer_mut::(); + let (first, rest) = values.as_mut_slice().split_at_mut(first_len); + let chunks = iter::once(first).chain(rest.chunks_mut(FL_CHUNK_SIZE)); + for (chunk, &reference) in chunks.zip_eq(references) { + for value in chunk { + *value = value.wrapping_add(&reference); + } + } + Ok(PrimitiveArray::new(values.freeze(), validity)) +} + pub(crate) fn fused_decompress< T: PhysicalPType + UnsignedPType + FoR + WrappingAdd, >( @@ -91,7 +134,8 @@ pub(crate) fn fused_decompress< ctx: &mut ExecutionCtx, ) -> VortexResult { let ref_ = for_ - .reference_scalar() + .constant_reference() + .ok_or_else(|| vortex_err!("fused FoR decompression requires a constant reference"))? .as_primitive() .as_::() .vortex_expect("cannot be null"); @@ -110,7 +154,7 @@ pub(crate) fn fused_decompress< )?; let mut builder = PrimitiveBuilder::::with_capacity_in( - for_.reference_scalar().dtype().nullability(), + for_.dtype().nullability(), bp.len(), ctx.allocator(), ); diff --git a/encodings/fastlanes/src/for/array/mod.rs b/encodings/fastlanes/src/for/array/mod.rs index 444ab152c23..49c184ee251 100644 --- a/encodings/fastlanes/src/for/array/mod.rs +++ b/encodings/fastlanes/src/for/array/mod.rs @@ -12,28 +12,49 @@ use vortex_array::scalar::Scalar; use vortex_error::VortexResult; use vortex_error::vortex_ensure; +use crate::FL_CHUNK_SIZE; + pub mod for_compress; pub mod for_decompress; #[array_slots(crate::FoR)] pub struct FoRSlots { - /// The encoded array with the frame-of-reference (minimum value) subtracted. + /// The encoded array with each chunk's reference subtracted. #[slot(0)] pub encoded: ArrayRef, + /// One reference per [`FL_CHUNK_SIZE`]-element chunk. + #[slot(1)] + pub references: ArrayRef, } /// Frame of Reference (FoR) encoded array. /// /// This encoding stores values as offsets from a reference value, which can significantly reduce /// storage requirements when values are clustered around a specific point. +/// +/// Every [`FL_CHUNK_SIZE`]-element chunk has its own reference: element `i` decodes as +/// `encoded[i] + references[(offset + i) / FL_CHUNK_SIZE]` with wrapping arithmetic, where +/// `offset` is the position of the first element within the first chunk. Arrays with a single +/// reference store a constant `references` child. #[derive(Clone, Debug)] pub struct FoRData { - pub(super) reference: Scalar, + pub(super) offset: u16, } pub trait FoRArrayExt: FoRArraySlotsExt { - fn reference_scalar(&self) -> &Scalar { - &self.reference + /// The reference shared by every chunk, if the references are constant. + fn constant_reference(&self) -> Option { + self.references().as_constant() + } + + /// The position of the first element within the first chunk of `references`. + fn offset(&self) -> u16 { + self.offset + } + + #[inline] + fn ptype(&self) -> PType { + self.as_ref().dtype().as_ptype() } } @@ -41,23 +62,21 @@ impl> FoRArrayExt for T {} impl Display for FoRData { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "reference: {}", self.reference) + write!(f, "offset: {}", self.offset) } } impl FoRData { - pub(crate) fn try_new(reference: Scalar) -> VortexResult { - vortex_ensure!(!reference.is_null(), "Reference value cannot be null"); + pub(crate) fn try_new(offset: u16) -> VortexResult { vortex_ensure!( - reference.dtype().is_int(), - "FoR requires an integer reference dtype, got {}", - reference.dtype() + usize::from(offset) < FL_CHUNK_SIZE, + "FoR offset must be less than {FL_CHUNK_SIZE}, got {offset}" ); - Ok(Self { reference }) + Ok(Self { offset }) } +} - #[inline] - pub fn ptype(&self) -> PType { - self.reference.dtype().as_ptype() - } +/// The number of chunks spanned by `len` elements starting at `offset` within the first chunk. +pub(crate) fn num_chunks(offset: u16, len: usize) -> usize { + (usize::from(offset) + len).div_ceil(FL_CHUNK_SIZE) } diff --git a/encodings/fastlanes/src/for/compute/cast.rs b/encodings/fastlanes/src/for/compute/cast.rs index d321ee4a3e3..4c14e0a1075 100644 --- a/encodings/fastlanes/src/for/compute/cast.rs +++ b/encodings/fastlanes/src/for/compute/cast.rs @@ -21,10 +21,11 @@ impl CastReduce for FoR { // For type changes between integers, cast the components let casted_child = array.encoded().cast(dtype.clone())?; - let casted_reference = array.reference_scalar().cast(dtype)?; + // References are always non-nullable. + let casted_references = array.references().cast(dtype.as_nonnullable())?; Ok(Some( - FoR::try_new(casted_child, casted_reference)?.into_array(), + FoR::try_new_chunked(casted_child, casted_references, array.offset())?.into_array(), )) } } diff --git a/encodings/fastlanes/src/for/compute/compare.rs b/encodings/fastlanes/src/for/compute/compare.rs index a5ccce3efc8..2e899a44ef5 100644 --- a/encodings/fastlanes/src/for/compute/compare.rs +++ b/encodings/fastlanes/src/for/compute/compare.rs @@ -69,7 +69,11 @@ where return Ok(None); } - let reference = lhs.reference_scalar(); + // TODO(mk): support many references. + + let Some(reference) = lhs.constant_reference() else { + return Ok(None); + }; let reference = reference.as_primitive().typed_value::(); // We encode the RHS into the FoR domain. diff --git a/encodings/fastlanes/src/for/compute/is_constant.rs b/encodings/fastlanes/src/for/compute/is_constant.rs index d146e3e1d7a..6d3522a24f9 100644 --- a/encodings/fastlanes/src/for/compute/is_constant.rs +++ b/encodings/fastlanes/src/for/compute/is_constant.rs @@ -11,6 +11,7 @@ use vortex_array::scalar::Scalar; use vortex_error::VortexResult; use crate::FoR; +use crate::r#for::array::FoRArrayExt; use crate::r#for::array::FoRArraySlotsExt; /// FoR-specific is_constant kernel. @@ -33,6 +34,10 @@ impl DynAggregateKernel for FoRIsConstantKernel { let Some(array) = batch.as_opt::() else { return Ok(None); }; + // TODO(mk): support many references. + if array.constant_reference().is_none() { + return Ok(None); + } let result = is_constant(array.encoded(), ctx)?; Ok(Some(IsConstant::make_partial(batch, result, ctx)?)) diff --git a/encodings/fastlanes/src/for/compute/is_sorted.rs b/encodings/fastlanes/src/for/compute/is_sorted.rs index 92102190863..fb36acce83b 100644 --- a/encodings/fastlanes/src/for/compute/is_sorted.rs +++ b/encodings/fastlanes/src/for/compute/is_sorted.rs @@ -14,6 +14,7 @@ use vortex_array::scalar::Scalar; use vortex_error::VortexResult; use crate::FoR; +use crate::r#for::array::FoRArrayExt; use crate::r#for::array::FoRArraySlotsExt; #[derive(Debug)] @@ -33,6 +34,10 @@ impl DynAggregateKernel for FoRIsSortedKernel { let Some(array) = batch.as_opt::() else { return Ok(None); }; + // TODO(mk): support many references. + if array.constant_reference().is_none() { + return Ok(None); + } let encoded = array.encoded().clone().execute::(ctx)?; let unsigned_array = PrimitiveArray::from_buffer_handle( diff --git a/encodings/fastlanes/src/for/compute/mod.rs b/encodings/fastlanes/src/for/compute/mod.rs index fdb2a162e5c..22c20a8b062 100644 --- a/encodings/fastlanes/src/for/compute/mod.rs +++ b/encodings/fastlanes/src/for/compute/mod.rs @@ -25,23 +25,23 @@ impl TakeExecute for FoR { indices: &ArrayRef, _ctx: &mut ExecutionCtx, ) -> VortexResult> { + // TODO(mk): support many references. + let Some(reference) = array.constant_reference() else { + return Ok(None); + }; Ok(Some( - FoR::try_new( - array.encoded().take(indices.clone())?, - array.reference_scalar().clone(), - )? - .into_array(), + FoR::try_new(array.encoded().take(indices.clone())?, reference)?.into_array(), )) } } impl FilterReduce for FoR { fn filter(array: ArrayView<'_, Self>, mask: &Mask) -> VortexResult> { - FoR::try_new( - array.encoded().filter(mask.clone())?, - array.reference_scalar().clone(), - ) - .map(|a| Some(a.into_array())) + // TODO(mk): support many references. + let Some(reference) = array.constant_reference() else { + return Ok(None); + }; + FoR::try_new(array.encoded().filter(mask.clone())?, reference).map(|a| Some(a.into_array())) } } diff --git a/encodings/fastlanes/src/for/mod.rs b/encodings/fastlanes/src/for/mod.rs index 1e5d666eb80..8324eff39e2 100644 --- a/encodings/fastlanes/src/for/mod.rs +++ b/encodings/fastlanes/src/for/mod.rs @@ -9,6 +9,9 @@ pub use array::FoRSlots; pub(crate) mod compute; +#[cfg(test)] +mod tests; + mod plugin; pub use plugin::FoRPlugin; diff --git a/encodings/fastlanes/src/for/plugin.rs b/encodings/fastlanes/src/for/plugin.rs index 25f6bd5a13b..e108e6d5ca7 100644 --- a/encodings/fastlanes/src/for/plugin.rs +++ b/encodings/fastlanes/src/for/plugin.rs @@ -3,10 +3,8 @@ //! ArrayPlugin implementation for FoR. -use vortex_array::Array; use vortex_array::ArrayDeserialization; use vortex_array::ArrayId; -use vortex_array::ArrayParts; use vortex_array::ArrayPlugin; use vortex_array::ArrayRef; use vortex_array::ArraySerialization; @@ -14,7 +12,6 @@ use vortex_array::IntoArray; use vortex_array::VTable; use vortex_array::scalar::Scalar; use vortex_array::scalar::ScalarValue; -use vortex_array::smallvec::smallvec; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure; @@ -22,8 +19,8 @@ use vortex_error::vortex_err; use vortex_session::VortexSession; use crate::FoR; -use crate::FoRData; use crate::r#for::array::FoRArrayExt; +use crate::r#for::array::FoRArraySlotsExt; /// Serde for the [`FoR`] array. /// @@ -45,12 +42,16 @@ impl ArrayPlugin for FoRPlugin { let view = array .as_opt::() .ok_or_else(|| vortex_err!("FoR plugin cannot serialize {}", array.encoding_id()))?; + let reference = view + .constant_reference() + .ok_or_else(|| vortex_err!("FoR plugin requires constant references"))?; // Note that we **only** serialize the optional scalar value (not including the dtype). - let metadata = ScalarValue::to_proto_bytes(view.reference_scalar().value()); - Ok(Some(ArraySerialization::from_array( + let metadata = ScalarValue::to_proto_bytes(reference.value()); + Ok(Some(ArraySerialization::new( self.id(), - array, metadata, + vec![], + vec![view.encoded().clone()], ))) } @@ -79,13 +80,7 @@ impl ArrayPlugin for FoRPlugin { let scalar_value = ScalarValue::from_proto_bytes(parts.metadata, parts.dtype, session)?; let reference = Scalar::try_new(parts.dtype.clone(), scalar_value)?; let encoded = parts.children.get(0, parts.dtype, parts.len)?; - let slots = smallvec![Some(encoded)]; - - let data = FoRData::try_new(reference)?; - Ok(Array::try_from_parts( - ArrayParts::new(FoR, parts.dtype.clone(), parts.len, data).with_slots(slots), - )? - .into_array()) + Ok(FoR::try_new(encoded, reference)?.into_array()) } } @@ -143,7 +138,7 @@ mod tests { assert_eq!(serialization.serialized_id, VTable::id(&FoR)); assert_eq!( serialization.metadata, - ScalarValue::to_proto_bytes::>(array.reference_scalar().value()) + ScalarValue::to_proto_bytes::>(array.constant_reference().unwrap().value()) ); let read = roundtrip(array.as_array())?; diff --git a/encodings/fastlanes/src/for/tests.rs b/encodings/fastlanes/src/for/tests.rs new file mode 100644 index 00000000000..5145931ad65 --- /dev/null +++ b/encodings/fastlanes/src/for/tests.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use std::sync::LazyLock; + +use rstest::rstest; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::assert_arrays_eq; +use vortex_array::compute::conformance::consistency::test_array_consistency; +use vortex_array::dtype::NativePType; +use vortex_array::session::ArraySessionExt; +use vortex_buffer::Buffer; +use vortex_error::VortexResult; +use vortex_session::VortexSession; + +use crate::FL_CHUNK_SIZE; +use crate::FoR; +use crate::FoRArray; +use crate::FoRArrayExt; +use crate::FoRArraySlotsExt; + +static SESSION: LazyLock = LazyLock::new(|| { + let session = vortex_array::array_session(); + crate::initialize(&session); + session +}); + +/// Builds a FoR array over `encoded` with the given per-chunk references, and the values it +/// should decode to. +fn chunked( + encoded: PrimitiveArray, + references: &[T], +) -> VortexResult<(FoRArray, PrimitiveArray)> { + let mut ctx = SESSION.create_execution_ctx(); + let expected = PrimitiveArray::from_option_iter( + (0..encoded.len()) + .map(|i| { + let value = encoded.as_slice::()[i]; + let valid = encoded.is_valid(i, &mut ctx)?; + Ok(valid.then(|| value.wrapping_add(&references[i / FL_CHUNK_SIZE]))) + }) + .collect::>>()?, + ); + let expected = if encoded.dtype().is_nullable() { + expected + } else { + PrimitiveArray::new(expected.to_buffer::(), encoded.validity()?) + }; + let references = Buffer::copy_from(references).into_array(); + Ok(( + FoR::try_new_chunked(encoded.into_array(), references, 0)?, + expected, + )) +} + +fn unsigned() -> VortexResult<(FoRArray, PrimitiveArray)> { + chunked( + PrimitiveArray::from_iter((0..3000u32).map(|i| i % 100)), + &[0u32, 1_000_000, 5], + ) +} + +fn signed_wrapping() -> VortexResult<(FoRArray, PrimitiveArray)> { + chunked( + PrimitiveArray::from_iter((0..2100i16).map(|i| i % 7)), + &[i16::MIN, -3, i16::MAX], + ) +} + +fn nullable() -> VortexResult<(FoRArray, PrimitiveArray)> { + chunked( + PrimitiveArray::from_option_iter((0..2500i64).map(|i| (i % 5 != 0).then_some(i % 11))), + &[-1_000i64, 0, 1 << 40], + ) +} + +#[rstest] +#[case::unsigned(unsigned())] +#[case::signed_wrapping(signed_wrapping())] +#[case::nullable(nullable())] +fn decodes_per_chunk(#[case] arrays: VortexResult<(FoRArray, PrimitiveArray)>) -> VortexResult<()> { + let (array, expected) = arrays?; + assert!(array.constant_reference().is_none()); + assert_arrays_eq!(array, expected, &mut SESSION.create_execution_ctx()); + Ok(()) +} + +#[rstest] +#[case::unsigned(unsigned())] +#[case::signed_wrapping(signed_wrapping())] +#[case::nullable(nullable())] +fn consistency(#[case] arrays: VortexResult<(FoRArray, PrimitiveArray)>) -> VortexResult<()> { + let (array, _) = arrays?; + test_array_consistency(&array.into_array(), &mut SESSION.create_execution_ctx()); + Ok(()) +} + +#[rstest] +#[case::within_first_chunk(3, 900)] +#[case::across_chunks(1000, 2100)] +#[case::chunk_aligned(1024, 2048)] +#[case::last_chunk(2048, 3000)] +#[case::empty_unaligned(1500, 1500)] +fn slice_keeps_chunk_alignment(#[case] start: usize, #[case] end: usize) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let (array, expected) = unsigned()?; + let sliced = array.into_array().slice(start..end)?; + assert_arrays_eq!(sliced, expected.into_array().slice(start..end)?, &mut ctx); + + if let Some(sliced) = sliced.as_opt::() { + assert_eq!(usize::from(sliced.offset()), start % FL_CHUNK_SIZE); + // Slicing a slice composes offsets. + let len = end - start; + let inner: ArrayRef = sliced.array().slice(len / 3..len)?; + let expected = unsigned()?.1.into_array().slice(start + len / 3..end)?; + assert_arrays_eq!(inner, expected, &mut ctx); + } + Ok(()) +} + +#[test] +fn constant_references_keep_the_scalar_reference() -> VortexResult<()> { + let array = FoR::try_new( + PrimitiveArray::from_iter(0..3000u32).into_array(), + 7u32.into(), + )?; + assert_eq!(array.references().len(), 3); + assert_eq!(array.constant_reference(), Some(7u32.into())); + let sliced = array.into_array().slice(1500..1600)?; + assert_eq!(sliced.as_::().constant_reference(), Some(7u32.into())); + Ok(()) +} + +#[test] +fn varying_references_do_not_serialize_as_v1() -> VortexResult<()> { + let (array, _) = unsigned()?; + assert!(SESSION.array_serialize(array.as_array()).is_err()); + Ok(()) +} diff --git a/encodings/fastlanes/src/for/vtable/mod.rs b/encodings/fastlanes/src/for/vtable/mod.rs index 7d20a647e4a..d896e0ec7a8 100644 --- a/encodings/fastlanes/src/for/vtable/mod.rs +++ b/encodings/fastlanes/src/for/vtable/mod.rs @@ -16,6 +16,7 @@ use vortex_array::EqMode; use vortex_array::ExecutionCtx; use vortex_array::ExecutionResult; use vortex_array::IntoArray; +use vortex_array::arrays::ConstantArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::buffer::BufferHandle; use vortex_array::dtype::DType; @@ -35,6 +36,7 @@ use crate::FoRData; use crate::r#for::array::FoRSlots; use crate::r#for::array::FoRSlotsView; use crate::r#for::array::for_decompress::decompress; +use crate::r#for::array::num_chunks; use crate::r#for::vtable::rules::PARENT_RULES; mod kernels; @@ -52,13 +54,13 @@ pub(crate) fn initialize(session: &VortexSession) { impl ArrayHash for FoRData { fn array_hash(&self, state: &mut H, _accuracy: EqMode) { - self.reference.hash(state); + self.offset.hash(state); } } impl ArrayEq for FoRData { fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool { - self.reference == other.reference + self.offset == other.offset } } @@ -80,8 +82,8 @@ impl VTable for FoR { len: usize, slots: &[Option], ) -> VortexResult<()> { - let encoded = FoRSlotsView::from_slots(slots).encoded; - validate_parts(encoded.dtype(), encoded.len(), &data.reference, dtype, len) + let slots = FoRSlotsView::from_slots(slots); + validate_parts(slots.encoded, slots.references, data.offset, dtype, len) } fn nbuffers(_array: ArrayView<'_, Self>) -> usize { @@ -150,10 +152,25 @@ impl FoR { let dtype = reference .dtype() .with_nullability(encoded.dtype().nullability()); - let reference = reference.cast(&dtype)?; + let reference = reference.cast(&dtype.as_nonnullable())?; + let references = ConstantArray::new(reference, num_chunks(0, encoded.len())).into_array(); + Self::try_new_chunked(encoded, references, 0) + } + + /// Construct a FoR array with one reference per 1024-element chunk. + /// + /// `references` must be a non-nullable integer array of the encoded array's type, with one + /// entry for each chunk spanned by `offset + encoded.len()` elements. `offset` is the position + /// of the first element within the first chunk. + pub fn try_new_chunked( + encoded: ArrayRef, + references: ArrayRef, + offset: u16, + ) -> VortexResult { + let dtype = encoded.dtype().clone(); let len = encoded.len(); - let data = FoRData::try_new(reference)?; - let slots = smallvec![Some(encoded)]; + let data = FoRData::try_new(offset)?; + let slots = smallvec![Some(encoded), Some(references)]; Array::try_from_parts(ArrayParts::new(FoR, dtype, len, data).with_slots(slots)) } @@ -164,27 +181,34 @@ impl FoR { } fn validate_parts( - encoded_dtype: &DType, - encoded_len: usize, - reference: &Scalar, + encoded: &ArrayRef, + references: &ArrayRef, + offset: u16, dtype: &DType, len: usize, ) -> VortexResult<()> { vortex_ensure!(dtype.is_int(), "FoR requires an integer dtype, got {dtype}"); vortex_ensure!( - reference.dtype() == dtype, - "FoR reference dtype mismatch: expected {dtype}, got {}", - reference.dtype() - ); - vortex_ensure!( - encoded_dtype == dtype, + encoded.dtype() == dtype, "FoR encoded dtype mismatch: expected {dtype}, got {}", - encoded_dtype + encoded.dtype() ); vortex_ensure!( - encoded_len == len, + encoded.len() == len, "FoR encoded length mismatch: expected {len}, got {}", - encoded_len + encoded.len() + ); + let references_dtype = dtype.as_nonnullable(); + vortex_ensure!( + references.dtype() == &references_dtype, + "FoR references dtype mismatch: expected {references_dtype}, got {}", + references.dtype() + ); + let num_chunks = num_chunks(offset, len); + vortex_ensure!( + references.len() == num_chunks, + "FoR expects {num_chunks} references, got {}", + references.len() ); Ok(()) } diff --git a/encodings/fastlanes/src/for/vtable/operations.rs b/encodings/fastlanes/src/for/vtable/operations.rs index 361020824d5..e3d4073043c 100644 --- a/encodings/fastlanes/src/for/vtable/operations.rs +++ b/encodings/fastlanes/src/for/vtable/operations.rs @@ -10,6 +10,7 @@ use vortex_error::VortexExpect; use vortex_error::VortexResult; use super::FoR; +use crate::FL_CHUNK_SIZE; use crate::r#for::array::FoRArrayExt; use crate::r#for::array::FoRArraySlotsExt; impl OperationsVTable for FoR { @@ -22,7 +23,8 @@ impl OperationsVTable for FoR { ) -> VortexResult { let encoded_pvalue = array.encoded().execute_scalar(index, ctx)?; let encoded_pvalue = encoded_pvalue.as_primitive(); - let reference = array.reference_scalar(); + let chunk = (usize::from(array.offset()) + index) / FL_CHUNK_SIZE; + let reference = array.references().execute_scalar(chunk, ctx)?; let reference = reference.as_primitive(); Ok(match_each_integer_ptype!(array.ptype(), |P| { @@ -35,8 +37,8 @@ impl OperationsVTable for FoR { .vortex_expect("FoRArray Reference value cannot be null"), ) }) - .map(|v| Scalar::primitive::

(v, array.reference_scalar().dtype().nullability())) - .unwrap_or_else(|| Scalar::null(array.reference_scalar().dtype().clone())) + .map(|v| Scalar::primitive::

(v, array.dtype().nullability())) + .unwrap_or_else(|| Scalar::null(array.dtype().clone())) })) } } diff --git a/encodings/fastlanes/src/for/vtable/rules.rs b/encodings/fastlanes/src/for/vtable/rules.rs index 2ac1a0ae3b5..3b6a387c92d 100644 --- a/encodings/fastlanes/src/for/vtable/rules.rs +++ b/encodings/fastlanes/src/for/vtable/rules.rs @@ -36,10 +36,14 @@ impl ArrayParentReduceRule for FoRFilterPushDownRule { parent: ArrayView<'_, Filter>, _child_idx: usize, ) -> VortexResult> { + // TODO(mk): support many references. + let Some(reference) = child.constant_reference() else { + return Ok(None); + }; Ok(Some( FoR::try_new( child.encoded().filter(parent.filter_mask().clone())?, - child.reference_scalar().clone(), + reference, )? .into_array(), )) diff --git a/encodings/fastlanes/src/for/vtable/slice.rs b/encodings/fastlanes/src/for/vtable/slice.rs index da2e15ed55a..0c93442de1f 100644 --- a/encodings/fastlanes/src/for/vtable/slice.rs +++ b/encodings/fastlanes/src/for/vtable/slice.rs @@ -9,18 +9,36 @@ use vortex_array::IntoArray; use vortex_array::arrays::slice::SliceReduce; use vortex_error::VortexResult; +use crate::FL_CHUNK_SIZE; use crate::FoR; use crate::r#for::array::FoRArrayExt; use crate::r#for::array::FoRArraySlotsExt; impl SliceReduce for FoR { fn slice(array: ArrayView<'_, Self>, range: Range) -> VortexResult> { + // Every chunk shares one reference, so the result needs no offset. + if let Some(reference) = array.constant_reference() { + return Ok(Some( + FoR::try_new(array.encoded().slice(range)?, reference)?.into_array(), + )); + } + + // Keep the references of the chunks the slice overlaps, and record how far into the first + // of them the slice starts. + // + // E.g. rows 1500..2500 of a 3000-row array with references [r0, r1, r2]: + // references = [r1, r2] (the slice overlaps chunks 1 and 2) + // offset = 476 (row 1500 is 476 rows into chunk 1) + // + // Adding the array's own offset first makes this work for already-sliced arrays too. + let start = usize::from(array.offset()) + range.start; + let end = usize::from(array.offset()) + range.end; + let references = array + .references() + .slice(start / FL_CHUNK_SIZE..end.div_ceil(FL_CHUNK_SIZE))?; + let offset = u16::try_from(start % FL_CHUNK_SIZE)?; Ok(Some( - FoR::try_new( - array.encoded().slice(range)?, - array.reference_scalar().clone(), - )? - .into_array(), + FoR::try_new_chunked(array.encoded().slice(range)?, references, offset)?.into_array(), )) } } diff --git a/vortex-btrblocks/src/schemes/integer/for_.rs b/vortex-btrblocks/src/schemes/integer/for_.rs index 476a0dec282..ae35f3d11e0 100644 --- a/vortex-btrblocks/src/schemes/integer/for_.rs +++ b/vortex-btrblocks/src/schemes/integer/for_.rs @@ -146,7 +146,10 @@ impl Scheme for FoRScheme { let compressed = BitPackingScheme.compress(compressor, &biased_data, leaf_ctx, exec_ctx)?; // TODO(connor): This should really be `new_unchecked`. - let for_compressed = FoR::try_new(compressed, for_array.reference_scalar().clone())?; + let reference = for_array + .constant_reference() + .vortex_expect("FoR::encode uses a single reference"); + let for_compressed = FoR::try_new(compressed, reference)?; for_compressed .as_ref() .statistics() diff --git a/vortex-btrblocks/tests/snapshots/golden__compact__int_negatives.snap b/vortex-btrblocks/tests/snapshots/golden__compact__int_negatives.snap index 24949893190..5ab62912f6d 100644 --- a/vortex-btrblocks/tests/snapshots/golden__compact__int_negatives.snap +++ b/vortex-btrblocks/tests/snapshots/golden__compact__int_negatives.snap @@ -3,7 +3,9 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: i64, len=16384, nbytes=131072 -root: fastlanes.for(i64, len=16384) nbytes=16384 - metadata: reference: -128i64 +root: fastlanes.for(i64, len=16384) nbytes=16387 + metadata: offset: 0 encoded: fastlanes.bitpacked(i64, len=16384) nbytes=16384 metadata: bit_width: 8, offset: 0 + references: vortex.constant(i64, len=16) nbytes=3 + metadata: scalar: -128i64 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__int_monotone_jitter.snap b/vortex-btrblocks/tests/snapshots/golden__regular__int_monotone_jitter.snap index 4800aae289d..d2be0a8c16d 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__int_monotone_jitter.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__int_monotone_jitter.snap @@ -3,7 +3,9 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: u64, len=16384, nbytes=131072 -root: fastlanes.for(u64, len=16384) nbytes=49152 - metadata: reference: 1700000001036u64 +root: fastlanes.for(u64, len=16384) nbytes=49159 + metadata: offset: 0 encoded: fastlanes.bitpacked(u64, len=16384) nbytes=49152 metadata: bit_width: 24, offset: 0 + references: vortex.constant(u64, len=16) nbytes=7 + metadata: scalar: 1700000001036u64 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__int_negatives.snap b/vortex-btrblocks/tests/snapshots/golden__regular__int_negatives.snap index 24949893190..5ab62912f6d 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__int_negatives.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__int_negatives.snap @@ -3,7 +3,9 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: i64, len=16384, nbytes=131072 -root: fastlanes.for(i64, len=16384) nbytes=16384 - metadata: reference: -128i64 +root: fastlanes.for(i64, len=16384) nbytes=16387 + metadata: offset: 0 encoded: fastlanes.bitpacked(i64, len=16384) nbytes=16384 metadata: bit_width: 8, offset: 0 + references: vortex.constant(i64, len=16) nbytes=3 + metadata: scalar: -128i64 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__int_runs.snap b/vortex-btrblocks/tests/snapshots/golden__regular__int_runs.snap index 271de8faf93..3cb3d53145c 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__int_runs.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__int_runs.snap @@ -3,13 +3,17 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: i32, len=16384, nbytes=65536 -root: vortex.runend(i32, len=16384) nbytes=3968 +root: vortex.runend(i32, len=16384) nbytes=3974 metadata: offset: 0 - ends: fastlanes.for(u16, len=1020) nbytes=1792 - metadata: reference: 13u16 + ends: fastlanes.for(u16, len=1020) nbytes=1794 + metadata: offset: 0 encoded: fastlanes.bitpacked(u16, len=1020) nbytes=1792 metadata: bit_width: 14, offset: 0 - values: fastlanes.for(i32, len=1020) nbytes=2176 - metadata: reference: -49931i32 + references: vortex.constant(u16, len=1) nbytes=2 + metadata: scalar: 13u16 + values: fastlanes.for(i32, len=1020) nbytes=2180 + metadata: offset: 0 encoded: fastlanes.bitpacked(i32, len=1020) nbytes=2176 metadata: bit_width: 17, offset: 0 + references: vortex.constant(i32, len=1) nbytes=4 + metadata: scalar: -49931i32 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__int_sparse_outliers.snap b/vortex-btrblocks/tests/snapshots/golden__regular__int_sparse_outliers.snap index 53a5d1e023c..126c1ea5bb9 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__int_sparse_outliers.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__int_sparse_outliers.snap @@ -3,11 +3,13 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: i64, len=16384, nbytes=131072 -root: vortex.sparse(i64, len=16384) nbytes=5540 +root: vortex.sparse(i64, len=16384) nbytes=5546 metadata: fill_value: 1000000i64 patch_indices: vortex.primitive(u16, len=848) nbytes=1696 metadata: ptype: u16 - patch_values: fastlanes.for(i64, len=848) nbytes=3840 - metadata: reference: 1000830099i64 + patch_values: fastlanes.for(i64, len=848) nbytes=3846 + metadata: offset: 0 encoded: fastlanes.bitpacked(i64, len=848) nbytes=3840 metadata: bit_width: 30, offset: 0 + references: vortex.constant(i64, len=1) nbytes=6 + metadata: scalar: 1000830099i64 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__list_of_int_runs.snap b/vortex-btrblocks/tests/snapshots/golden__regular__list_of_int_runs.snap index c9554add05a..9407fcba78e 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__list_of_int_runs.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__list_of_int_runs.snap @@ -3,18 +3,22 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: list(i32), len=4066, nbytes=81804 -root: vortex.list(list(i32), len=4066) nbytes=11146 +root: vortex.list(list(i32), len=4066) nbytes=11152 metadata: - elements: vortex.runend(i32, len=16384) nbytes=3968 + elements: vortex.runend(i32, len=16384) nbytes=3974 metadata: offset: 0 - ends: fastlanes.for(u16, len=1020) nbytes=1792 - metadata: reference: 13u16 + ends: fastlanes.for(u16, len=1020) nbytes=1794 + metadata: offset: 0 encoded: fastlanes.bitpacked(u16, len=1020) nbytes=1792 metadata: bit_width: 14, offset: 0 - values: fastlanes.for(i32, len=1020) nbytes=2176 - metadata: reference: -49931i32 + references: vortex.constant(u16, len=1) nbytes=2 + metadata: scalar: 13u16 + values: fastlanes.for(i32, len=1020) nbytes=2180 + metadata: offset: 0 encoded: fastlanes.bitpacked(i32, len=1020) nbytes=2176 metadata: bit_width: 17, offset: 0 + references: vortex.constant(i32, len=1) nbytes=4 + metadata: scalar: -49931i32 offsets: fastlanes.bitpacked(u16, len=4067) nbytes=7178 metadata: bit_width: 14, offset: 0 patch_indices: vortex.primitive(u16, len=1) nbytes=2 diff --git a/vortex-btrblocks/tests/snapshots/golden__regular__temporal_timestamp_micros.snap b/vortex-btrblocks/tests/snapshots/golden__regular__temporal_timestamp_micros.snap index 6f38e8e6f5d..7375d3e4a77 100644 --- a/vortex-btrblocks/tests/snapshots/golden__regular__temporal_timestamp_micros.snap +++ b/vortex-btrblocks/tests/snapshots/golden__regular__temporal_timestamp_micros.snap @@ -3,9 +3,11 @@ source: vortex-btrblocks/tests/golden.rs expression: rendered --- input: vortex.timestamp[µs, tz=UTC](i64), len=16384, nbytes=131072 -root: vortex.ext(vortex.timestamp[µs, tz=UTC](i64), len=16384) nbytes=67584 +root: vortex.ext(vortex.timestamp[µs, tz=UTC](i64), len=16384) nbytes=67593 metadata: - storage: fastlanes.for(i64, len=16384) nbytes=67584 - metadata: reference: 1700000000891673i64 + storage: fastlanes.for(i64, len=16384) nbytes=67593 + metadata: offset: 0 encoded: fastlanes.bitpacked(i64, len=16384) nbytes=67584 metadata: bit_width: 33, offset: 0 + references: vortex.constant(i64, len=16) nbytes=9 + metadata: scalar: 1700000000891673i64 diff --git a/vortex-cuda/benches/dynamic_dispatch_cuda.rs b/vortex-cuda/benches/dynamic_dispatch_cuda.rs index 2d9db5ddbbd..82afa0a843d 100644 --- a/vortex-cuda/benches/dynamic_dispatch_cuda.rs +++ b/vortex-cuda/benches/dynamic_dispatch_cuda.rs @@ -360,9 +360,14 @@ fn bench_dict_bp_codes_alp_for_bp_values_dynanmic_dispatch(c: &mut Criterion) { let bp = BitPackedData::encode(for_arr.encoded(), values_bit_width, &mut ctx) .vortex_expect("bitpack values"); let values_tree = ALP::new( - FoR::try_new(bp.into_array(), for_arr.reference_scalar().clone()) - .vortex_expect("for_new") - .into_array(), + FoR::try_new( + bp.into_array(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), + ) + .vortex_expect("for_new") + .into_array(), exponents, None, ); @@ -663,13 +668,16 @@ fn bench_dict_bp_codes_alp_for_bp_values_composed_standalone(c: &mut Criterion) let bp = BitPackedData::encode(for_arr.encoded(), values_bit_width, &mut ctx) .vortex_expect("bitpack values"); let values_bp = bp; - let values_reference: i32 = for_arr - .reference_scalar() + let values_reference: i32 = (&for_arr + .constant_reference() + .vortex_expect("constant reference")) .try_into() .vortex_expect("values reference"); let values_for = FoR::try_new( values_bp.clone().into_array(), - for_arr.reference_scalar().clone(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), ) .vortex_expect("for_new") .into_array(); @@ -765,9 +773,14 @@ fn bench_alp_for_bitpacked_f64(c: &mut Criterion) { assert!(bp.patches().is_none(), "expected only ALP patches"); let tree = ALP::new( - FoR::try_new(bp.into_array(), for_arr.reference_scalar().clone()) - .vortex_expect("for_new") - .into_array(), + FoR::try_new( + bp.into_array(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), + ) + .vortex_expect("for_new") + .into_array(), alp_exponents, patches, ); @@ -885,9 +898,14 @@ fn bench_alp_for_bitpacked(c: &mut Criterion) { .vortex_expect("bitpack encode"); let tree = ALP::new( - FoR::try_new(bp.into_array(), for_arr.reference_scalar().clone()) - .vortex_expect("for_new") - .into_array(), + FoR::try_new( + bp.into_array(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), + ) + .vortex_expect("for_new") + .into_array(), exponents, None, ); diff --git a/vortex-cuda/src/dynamic_dispatch/mod.rs b/vortex-cuda/src/dynamic_dispatch/mod.rs index ad9c02ba6a1..80f0838b433 100644 --- a/vortex-cuda/src/dynamic_dispatch/mod.rs +++ b/vortex-cuda/src/dynamic_dispatch/mod.rs @@ -971,7 +971,13 @@ mod tests { let bp = BitPacked::encode(for_arr.encoded(), 6, &mut ctx)?; let tree = ALP::new( - FoR::try_new(bp.into_array(), for_arr.reference_scalar().clone())?.into_array(), + FoR::try_new( + bp.into_array(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), + )? + .into_array(), exponents, None, ); @@ -1904,7 +1910,13 @@ mod tests { let bp = BitPacked::encode(for_arr.encoded(), 6, &mut ctx)?; let tree = ALP::new( - FoR::try_new(bp.into_array(), for_arr.reference_scalar().clone())?.into_array(), + FoR::try_new( + bp.into_array(), + for_arr + .constant_reference() + .vortex_expect("constant reference"), + )? + .into_array(), exponents, None, ); diff --git a/vortex-cuda/src/dynamic_dispatch/plan_builder.rs b/vortex-cuda/src/dynamic_dispatch/plan_builder.rs index 43f6a5a3712..bd143116cdd 100644 --- a/vortex-cuda/src/dynamic_dispatch/plan_builder.rs +++ b/vortex-cuda/src/dynamic_dispatch/plan_builder.rs @@ -128,7 +128,7 @@ fn is_dyn_dispatch_compatible(array: &ArrayRef) -> bool { _ => false, }; } - id == FoR.id() + (id == FoR.id() && array.as_::().constant_reference().is_some()) || id == ZigZag.id() || id == Primitive.id() || id == Slice.id() @@ -167,6 +167,9 @@ pub fn has_standalone_kernel(array: &ArrayRef) -> bool { // FoR fuses with BitPacked (FFOR) and Slice(BitPacked) in one launch. if id == FoR.id() { let for_arr = array.as_::(); + if for_arr.constant_reference().is_none() { + return false; + } let child = for_arr.encoded(); if child.encoding_id() == BitPacked.id() { return true; @@ -639,7 +642,8 @@ impl FusedPlan { ) -> VortexResult { let for_arr = array.as_::(); let ref_pvalue = for_arr - .reference_scalar() + .constant_reference() + .ok_or_else(|| vortex_err!("FoR references must be constant"))? .as_primitive() .pvalue() .ok_or_else(|| vortex_err!("FoR reference scalar is null"))?; diff --git a/vortex-cuda/src/kernel/encodings/for_.rs b/vortex-cuda/src/kernel/encodings/for_.rs index cc3ceed2869..ab0f796140a 100644 --- a/vortex-cuda/src/kernel/encodings/for_.rs +++ b/vortex-cuda/src/kernel/encodings/for_.rs @@ -57,10 +57,15 @@ impl CudaExecute for FoRExecutor { ) -> VortexResult { let array = Self::try_specialize(array).ok_or_else(|| vortex_err!("Expected FoRArray"))?; + // Per-chunk references have no CUDA kernel yet, so decode them on the CPU. + let Some(reference) = array.constant_reference() else { + return array.into_array().execute::(ctx.execution_ctx()); + }; + // Fuse FOR + BP => FFOR if let Some(bitpacked) = array.encoded().as_opt::() { match_each_integer_ptype!(bitpacked.ptype(bitpacked.dtype()), |P| { - let reference: P = array.reference_scalar().try_into()?; + let reference: P = (&reference).try_into()?; return decode_bitpacked(bitpacked.into_owned(), reference, None, ctx).await; }) } @@ -71,7 +76,7 @@ impl CudaExecute for FoRExecutor { { let slice_range = slice_array.slice_range().clone(); let unpacked = match_each_integer_ptype!(bitpacked.ptype(bitpacked.dtype()), |P| { - let reference: P = array.reference_scalar().try_into()?; + let reference: P = (&reference).try_into()?; decode_bitpacked(bitpacked.into_owned(), reference, None, ctx).await? }); @@ -95,7 +100,8 @@ where vortex_ensure!(array_len > 0, "FoR encoded array must not be empty"); let reference: P = array - .reference_scalar() + .constant_reference() + .ok_or_else(|| vortex_err!("CUDA FoR decoding requires a constant reference"))? .as_primitive() .as_::

() .vortex_expect("Cannot have a null reference"); diff --git a/vortex/benches/common_encoding_tree_throughput.rs b/vortex/benches/common_encoding_tree_throughput.rs index 2032eb3467a..8f37b8f5c9f 100644 --- a/vortex/benches/common_encoding_tree_throughput.rs +++ b/vortex/benches/common_encoding_tree_throughput.rs @@ -111,9 +111,14 @@ mod setup { let compressed = FoR::encode(uint_array, &mut ctx).unwrap(); let inner = compressed.encoded(); let bp = BitPacked::encode(inner, 8, &mut ctx).unwrap(); - FoR::try_new(bp.into_array(), compressed.reference_scalar().clone()) - .unwrap() - .into_array() + FoR::try_new( + bp.into_array(), + compressed + .constant_reference() + .vortex_expect("constant reference"), + ) + .unwrap() + .into_array() } /// Create ALP <- FoR <- BitPacked encoding tree for f64 @@ -131,8 +136,13 @@ mod setup { let for_array = FoR::encode(alp_encoded_prim, &mut ctx).unwrap(); let inner = for_array.encoded(); let bp = BitPacked::encode(inner, 8, &mut ctx).unwrap(); - let for_with_bp = - FoR::try_new(bp.into_array(), for_array.reference_scalar().clone()).unwrap(); + let for_with_bp = FoR::try_new( + bp.into_array(), + for_array + .constant_reference() + .vortex_expect("constant reference"), + ) + .unwrap(); ALP::try_new( for_with_bp.into_array(), @@ -209,10 +219,14 @@ mod setup { let ends_for = FoR::encode(ends_prim, &mut ctx).unwrap(); let ends_inner = ends_for.encoded(); let ends_bp = BitPacked::encode(ends_inner, 8, &mut ctx).unwrap(); - let compressed_ends = - FoR::try_new(ends_bp.into_array(), ends_for.reference_scalar().clone()) - .unwrap() - .into_array(); + let compressed_ends = FoR::try_new( + ends_bp.into_array(), + ends_for + .constant_reference() + .vortex_expect("constant reference"), + ) + .unwrap() + .into_array(); // Compress the values with BitPacked let values_prim = runend @@ -358,10 +372,14 @@ mod setup { let days_for = FoR::encode(days_prim, &mut ctx).unwrap(); let days_inner = days_for.encoded(); let days_bp = BitPacked::encode(days_inner, 16, &mut ctx).unwrap(); - let compressed_days = - FoR::try_new(days_bp.into_array(), days_for.reference_scalar().clone()) - .unwrap() - .into_array(); + let compressed_days = FoR::try_new( + days_bp.into_array(), + days_for + .constant_reference() + .vortex_expect("constant reference"), + ) + .unwrap() + .into_array(); // Compress seconds with FoR <- BitPacked let seconds_prim = parts @@ -374,7 +392,9 @@ mod setup { let seconds_bp = BitPacked::encode(seconds_inner, 17, &mut ctx).unwrap(); let compressed_seconds = FoR::try_new( seconds_bp.into_array(), - seconds_for.reference_scalar().clone(), + seconds_for + .constant_reference() + .vortex_expect("constant reference"), ) .unwrap() .into_array(); @@ -389,7 +409,9 @@ mod setup { let subseconds_bp = BitPacked::encode(subseconds_inner, 20, &mut ctx).unwrap(); let compressed_subseconds = FoR::try_new( subseconds_bp.into_array(), - subseconds_for.reference_scalar().clone(), + subseconds_for + .constant_reference() + .vortex_expect("constant reference"), ) .unwrap() .into_array();