Skip to main content

vortex_array/arrays/patched/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::ops::Range;
7
8use vortex_buffer::Buffer;
9use vortex_buffer::BufferMut;
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure;
12use vortex_error::vortex_ensure_eq;
13use vortex_error::vortex_err;
14
15use crate::ArrayRef;
16use crate::ArraySlots;
17use crate::Canonical;
18use crate::ExecutionCtx;
19use crate::IntoArray;
20use crate::array::Array;
21use crate::array::ArrayParts;
22use crate::array::TypedArrayRef;
23use crate::array_slots;
24use crate::arrays::Patched;
25use crate::arrays::PrimitiveArray;
26use crate::arrays::patched::TransposedPatches;
27use crate::arrays::patched::patch_lanes;
28use crate::buffer::BufferHandle;
29use crate::dtype::DType;
30use crate::dtype::IntegerPType;
31use crate::dtype::NativePType;
32use crate::dtype::PType;
33use crate::match_each_native_ptype;
34use crate::match_each_unsigned_integer_ptype;
35use crate::patches::Patches;
36use crate::validity::Validity;
37
38#[derive(Debug, Clone)]
39pub struct PatchedData {
40    /// Number of lanes the patch indices and values have been split into. Each of the `n_chunks`
41    /// of 1024 values is split into `n_lanes` lanes horizontally, each lane having 1024 / n_lanes
42    /// values that might be patched.
43    pub(super) n_lanes: usize,
44
45    /// The offset into that first chunk that is considered in bounds.
46    ///
47    /// The patch indices of the first chunk less than `offset` should be skipped, and the offset
48    /// should be subtracted out of the remaining offsets to get their final position in the
49    /// executed array.
50    pub(super) offset: usize,
51}
52
53#[array_slots(Patched)]
54pub struct PatchedSlots {
55    /// The inner array containing the base unpatched values.
56    #[slot(0)]
57    pub inner: ArrayRef,
58    /// The lane offsets array for locating patches within lanes.
59    #[slot(1)]
60    pub lane_offsets: ArrayRef,
61    /// The indices of patched (exception) values.
62    #[slot(2)]
63    pub patch_indices: ArrayRef,
64    /// The patched (exception) values at the corresponding indices.
65    #[slot(3)]
66    pub patch_values: ArrayRef,
67}
68
69impl Display for PatchedData {
70    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
71        write!(f, "n_lanes: {}, offset: {}", self.n_lanes, self.offset)
72    }
73}
74
75impl PatchedData {
76    pub(crate) fn validate(
77        &self,
78        dtype: &DType,
79        len: usize,
80        slots: &PatchedSlotsView,
81    ) -> VortexResult<()> {
82        vortex_ensure_eq!(
83            slots.inner.dtype(),
84            dtype,
85            "PatchedArray base dtype does not match outer dtype",
86        );
87        vortex_ensure_eq!(
88            slots.inner.len(),
89            len,
90            "PatchedArray base len does not match outer len",
91        );
92        vortex_ensure_eq!(
93            slots.patch_indices.len(),
94            slots.patch_values.len(),
95            "PatchedArray patch indices len does not match patch values len",
96        );
97        Ok(())
98    }
99}
100
101pub trait PatchedArrayExt: PatchedArraySlotsExt {
102    #[inline]
103    fn n_lanes(&self) -> usize {
104        self.n_lanes
105    }
106
107    #[inline]
108    fn offset(&self) -> usize {
109        self.offset
110    }
111
112    #[inline]
113    fn lane_range(
114        &self,
115        chunk: usize,
116        lane: usize,
117        ctx: &mut ExecutionCtx,
118    ) -> VortexResult<Range<usize>> {
119        assert!(chunk * 1024 <= self.len() + self.offset());
120        assert!(lane < self.n_lanes());
121
122        let start = self
123            .lane_offsets()
124            .execute_scalar(chunk * self.n_lanes() + lane, ctx)?;
125        let stop = self
126            .lane_offsets()
127            .execute_scalar(chunk * self.n_lanes() + lane + 1, ctx)?;
128
129        let start = start
130            .as_primitive()
131            .as_::<usize>()
132            .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
133
134        let stop = stop
135            .as_primitive()
136            .as_::<usize>()
137            .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
138
139        Ok(start..stop)
140    }
141
142    fn slice_chunks(&self, chunks: Range<usize>) -> VortexResult<Array<Patched>> {
143        let lane_offsets_start = chunks.start * self.n_lanes();
144        let lane_offsets_stop = chunks.end * self.n_lanes() + 1;
145
146        let sliced_lane_offsets = self
147            .lane_offsets()
148            .slice(lane_offsets_start..lane_offsets_stop)?;
149        let indices = self.patch_indices().clone();
150        let values = self.patch_values().clone();
151
152        let begin = (chunks.start * 1024).saturating_sub(self.offset());
153        let end = (chunks.end * 1024)
154            .saturating_sub(self.offset())
155            .min(self.len());
156
157        let offset = if chunks.start == 0 { self.offset() } else { 0 };
158        let inner = self.inner().slice(begin..end)?;
159        let len = inner.len();
160        let dtype = self.dtype().clone();
161        let slots = PatchedSlots {
162            inner,
163            lane_offsets: sliced_lane_offsets,
164            patch_indices: indices,
165            patch_values: values,
166        }
167        .into_slots();
168
169        Ok(unsafe { Patched::new_unchecked(dtype, len, slots, self.n_lanes(), offset) })
170    }
171}
172
173impl<T: TypedArrayRef<Patched>> PatchedArrayExt for T {}
174
175impl Patched {
176    pub fn from_array_and_patches(
177        inner: ArrayRef,
178        patches: &Patches,
179        ctx: &mut ExecutionCtx,
180    ) -> VortexResult<Array<Patched>> {
181        vortex_ensure!(
182            inner.dtype().eq_with_nullability_superset(patches.dtype()),
183            "array DType must match patches DType"
184        );
185
186        vortex_ensure!(
187            inner.dtype().is_primitive(),
188            "Creating PatchedArray from Patches only supported for primitive arrays"
189        );
190
191        vortex_ensure!(
192            patches.num_patches() <= u32::MAX as usize,
193            "PatchedArray does not support > u32::MAX patch values"
194        );
195
196        vortex_ensure!(
197            patches.values().all_valid(ctx)?,
198            "PatchedArray cannot be built from Patches with nulls"
199        );
200
201        let values_ptype = patches.dtype().as_ptype();
202
203        let TransposedPatches {
204            n_lanes,
205            lane_offsets,
206            indices,
207            values,
208        } = transpose_patches(patches, ctx)?;
209
210        let lane_offsets = PrimitiveArray::from_buffer_handle(
211            BufferHandle::new_host(lane_offsets),
212            PType::U32,
213            Validity::NonNullable,
214        )
215        .into_array();
216        let indices = PrimitiveArray::from_buffer_handle(
217            BufferHandle::new_host(indices),
218            PType::U16,
219            Validity::NonNullable,
220        )
221        .into_array();
222        let values = PrimitiveArray::from_buffer_handle(
223            BufferHandle::new_host(values),
224            values_ptype,
225            Validity::NonNullable,
226        )
227        .into_array();
228
229        let dtype = inner.dtype().clone();
230        let len = inner.len();
231        let slots = PatchedSlots {
232            inner,
233            lane_offsets,
234            patch_indices: indices,
235            patch_values: values,
236        }
237        .into_slots();
238        Ok(unsafe { Self::new_unchecked(dtype, len, slots, n_lanes, 0) })
239    }
240
241    pub(crate) unsafe fn new_unchecked(
242        dtype: DType,
243        len: usize,
244        slots: ArraySlots,
245        n_lanes: usize,
246        offset: usize,
247    ) -> Array<Patched> {
248        unsafe {
249            Array::from_parts_unchecked(ArrayParts::new(
250                Patched,
251                dtype,
252                len,
253                PatchedData { n_lanes, offset },
254                slots,
255            ))
256        }
257    }
258}
259
260/// Transpose a set of patches from the default sorted layout into the data parallel layout.
261fn transpose_patches(patches: &Patches, ctx: &mut ExecutionCtx) -> VortexResult<TransposedPatches> {
262    let array_len = patches.array_len();
263    let offset = patches.offset();
264
265    let indices = patches
266        .indices()
267        .clone()
268        .execute::<Canonical>(ctx)?
269        .into_primitive();
270
271    let values = patches
272        .values()
273        .clone()
274        .execute::<Canonical>(ctx)?
275        .into_primitive();
276
277    let indices_ptype = indices.ptype();
278    let values_ptype = values.ptype();
279
280    let indices = indices.buffer_handle().clone().unwrap_host();
281    let values = values.buffer_handle().clone().unwrap_host();
282
283    match_each_unsigned_integer_ptype!(indices_ptype, |I| {
284        match_each_native_ptype!(values_ptype, |V| {
285            let indices: Buffer<I> = Buffer::from_byte_buffer(indices);
286            let values: Buffer<V> = Buffer::from_byte_buffer(values);
287
288            Ok(transpose(
289                indices.as_slice(),
290                values.as_slice(),
291                offset,
292                array_len,
293            ))
294        })
295    })
296}
297
298#[expect(clippy::cast_possible_truncation)]
299fn transpose<I: IntegerPType, V: NativePType>(
300    indices_in: &[I],
301    values_in: &[V],
302    offset: usize,
303    array_len: usize,
304) -> TransposedPatches {
305    // Total number of slots is number of chunks times number of lanes.
306    let n_chunks = array_len.div_ceil(1024);
307    assert!(
308        n_chunks <= u32::MAX as usize,
309        "Cannot transpose patches for array with >= 4 trillion elements"
310    );
311
312    let n_lanes = patch_lanes::<V>();
313
314    // We know upfront how many indices and values we'll have.
315    let mut indices_buffer = BufferMut::with_capacity(indices_in.len());
316    let mut values_buffer = BufferMut::with_capacity(values_in.len());
317
318    // Number of patches in each chunk/lane.
319    let mut lane_offsets: BufferMut<u32> = BufferMut::zeroed(n_chunks * n_lanes + 1);
320
321    // Scan the index/value pairs once to get chunk/lane counts.
322    for index in indices_in {
323        let index = index.as_() - offset;
324        let chunk = index / 1024;
325        let lane = index % n_lanes;
326
327        lane_offsets[chunk * n_lanes + lane + 1] += 1;
328    }
329
330    for index in 1..lane_offsets.len() {
331        lane_offsets[index] += lane_offsets[index - 1];
332    }
333
334    // Loop over patches, writing them to final positions.
335    let indices_out = indices_buffer.spare_capacity_mut();
336    let values_out = values_buffer.spare_capacity_mut();
337    for (index, &value) in std::iter::zip(indices_in, values_in) {
338        let index = index.as_() - offset;
339        let chunk = index / 1024;
340        let lane = index % n_lanes;
341
342        let position = &mut lane_offsets[chunk * n_lanes + lane];
343        indices_out[*position as usize].write((index % 1024) as u16);
344        values_out[*position as usize].write(value);
345        *position += 1;
346    }
347
348    unsafe {
349        indices_buffer.set_len(indices_in.len());
350        values_buffer.set_len(values_in.len());
351    }
352
353    for index in indices_in {
354        let index = index.as_() - offset;
355        let chunk = index / 1024;
356        let lane = index % n_lanes;
357
358        lane_offsets[chunk * n_lanes + lane] -= 1;
359    }
360
361    TransposedPatches {
362        n_lanes,
363        lane_offsets: lane_offsets.freeze().into_byte_buffer(),
364        indices: indices_buffer.freeze().into_byte_buffer(),
365        values: values_buffer.freeze().into_byte_buffer(),
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use vortex_buffer::Buffer;
372    use vortex_buffer::buffer;
373
374    use super::PatchedSlots;
375    use crate::ArrayRef;
376    use crate::IntoArray;
377    use crate::array_slots;
378    use crate::arrays::Chunked;
379    use crate::arrays::Null;
380    use crate::arrays::PrimitiveArray;
381    use crate::arrays::Union;
382    use crate::validity::Validity;
383
384    #[array_slots(Null)]
385    struct OptionalPatchedSlots {
386        #[slot(0)]
387        required: ArrayRef,
388        #[slot(1)]
389        maybe: Option<ArrayRef>,
390    }
391
392    #[array_slots(Chunked)]
393    struct VariadicSlots {
394        #[slot(0)]
395        offsets: ArrayRef,
396        #[slot(1)]
397        maybe_validity: Option<ArrayRef>,
398        #[slot(2..)]
399        chunks: Vec<ArrayRef>,
400    }
401
402    /// The same layout as [`VariadicSlots`], but with every field declaration moved. The
403    /// `#[slot(..)]` annotations must keep the storage layout identical.
404    #[array_slots(Union)]
405    struct ShuffledVariadicSlots {
406        #[slot(2..)]
407        chunks: Vec<ArrayRef>,
408        #[slot(1)]
409        maybe_validity: Option<ArrayRef>,
410        #[slot(0)]
411        offsets: ArrayRef,
412    }
413
414    #[test]
415    fn generated_slots_round_trip() {
416        let required = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
417        let optional = PrimitiveArray::new(buffer![4u8, 5, 6], Validity::NonNullable).into_array();
418
419        let slot_vec = vec![Some(required.clone()), Some(optional.clone())];
420        let view = OptionalPatchedSlotsView::from_slots(&slot_vec);
421        assert!(ArrayRef::ptr_eq(view.required, &required));
422        assert!(ArrayRef::ptr_eq(
423            view.maybe.expect("optional slot"),
424            &optional
425        ));
426        assert_eq!(view.required.len(), 3);
427        assert_eq!(view.maybe.expect("optional slot").len(), 3);
428
429        let cloned = OptionalPatchedSlots::from_slots(slot_vec.into());
430        assert!(ArrayRef::ptr_eq(&cloned.required, &required));
431        assert!(ArrayRef::ptr_eq(
432            cloned.maybe.as_ref().expect("optional clone"),
433            &optional
434        ));
435        assert_eq!(cloned.required.len(), required.len());
436        assert_eq!(cloned.maybe.expect("optional clone").len(), optional.len());
437
438        let rebuilt = PatchedSlots::from_slots(
439            vec![
440                Some(required.clone()),
441                Some(optional.clone()),
442                Some(required.clone()),
443                Some(optional.clone()),
444            ]
445            .into(),
446        );
447        assert!(ArrayRef::ptr_eq(&rebuilt.inner, &required));
448        assert!(ArrayRef::ptr_eq(&rebuilt.lane_offsets, &optional));
449        assert!(ArrayRef::ptr_eq(&rebuilt.patch_indices, &required));
450        assert!(ArrayRef::ptr_eq(&rebuilt.patch_values, &optional));
451        assert_eq!(rebuilt.inner.len(), required.len());
452        assert_eq!(rebuilt.patch_values.len(), optional.len());
453    }
454
455    #[test]
456    #[allow(clippy::cognitive_complexity)]
457    fn variadic_slots_round_trip() {
458        let offsets = PrimitiveArray::new(buffer![0u64, 3, 5], Validity::NonNullable).into_array();
459        let chunk0 = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
460        let chunk1 = PrimitiveArray::new(buffer![4u8, 5], Validity::NonNullable).into_array();
461
462        assert_eq!(VariadicSlots::OFFSETS, 0);
463        assert_eq!(VariadicSlots::MAYBE_VALIDITY, 1);
464        assert_eq!(VariadicSlots::CHUNKS_OFFSET, 2);
465        assert_eq!(VariadicSlots::FIXED_COUNT, 2);
466        assert_eq!(VariadicSlots::slot_name(0), "offsets");
467        assert_eq!(VariadicSlots::slot_name(3), "chunks[1]");
468
469        let slot_vec = vec![
470            Some(offsets.clone()),
471            None,
472            Some(chunk0.clone()),
473            Some(chunk1.clone()),
474        ];
475
476        let view = VariadicSlotsView::from_slots(&slot_vec);
477        assert!(ArrayRef::ptr_eq(view.offsets, &offsets));
478        assert!(ArrayRef::ptr_eq(&view.chunks[0], &chunk0));
479        assert!(ArrayRef::ptr_eq(
480            view.chunks.get(1).expect("second chunk"),
481            &chunk1
482        ));
483        assert_eq!(view.offsets.len(), 3);
484        assert!(view.maybe_validity.is_none());
485        assert_eq!(view.chunks.len(), 2);
486        assert_eq!(view.chunks[0].len(), 3);
487        assert_eq!(view.chunks.get(1).map(|c| c.len()), Some(2));
488        assert!(view.chunks.get(2).is_none());
489        assert_eq!(
490            view.chunks.iter().map(|c| c.len()).collect::<Vec<_>>(),
491            vec![3, 2]
492        );
493
494        let owned = view.to_owned();
495        assert_eq!(owned.chunks.len(), 2);
496        assert!(ArrayRef::ptr_eq(&owned.offsets, &offsets));
497        assert!(owned.maybe_validity.is_none());
498        assert!(ArrayRef::ptr_eq(&owned.chunks[0], &chunk0));
499        assert!(ArrayRef::ptr_eq(&owned.chunks[1], &chunk1));
500
501        let owned = VariadicSlots::from_slots(slot_vec.into());
502        assert_eq!(owned.offsets.len(), offsets.len());
503        assert!(owned.maybe_validity.is_none());
504        assert_eq!(owned.chunks.len(), 2);
505
506        assert!(ArrayRef::ptr_eq(&owned.offsets, &offsets));
507        assert!(ArrayRef::ptr_eq(&owned.chunks[0], &chunk0));
508        assert!(ArrayRef::ptr_eq(&owned.chunks[1], &chunk1));
509        let slots = owned.into_slots();
510        assert_eq!(slots.len(), 4);
511        for (index, expected) in [(0, &offsets), (2, &chunk0), (3, &chunk1)] {
512            assert!(ArrayRef::ptr_eq(
513                slots[index].as_ref().expect("present slot"),
514                expected
515            ));
516        }
517        assert!(slots[1].is_none());
518        assert_eq!(
519            slots[VariadicSlots::CHUNKS_OFFSET]
520                .as_ref()
521                .map(|c| c.len()),
522            Some(3)
523        );
524    }
525
526    #[test]
527    fn variadic_slots_empty_tail() {
528        let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
529        let slot_vec = vec![Some(offsets), None];
530
531        let view = VariadicSlotsView::from_slots(&slot_vec);
532        assert!(view.chunks.is_empty());
533
534        let owned = VariadicSlots::from_slots(slot_vec.into());
535        assert!(owned.chunks.is_empty());
536        assert_eq!(owned.into_slots().len(), 2);
537    }
538
539    #[test]
540    fn variadic_slots_spilled_tail_keeps_order() {
541        let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
542        let chunks: Vec<ArrayRef> = (0..5u8)
543            .map(|len| {
544                PrimitiveArray::new((0..len).collect::<Buffer<u8>>(), Validity::NonNullable)
545                    .into_array()
546            })
547            .collect();
548
549        let slots = VariadicSlots {
550            offsets: offsets.clone(),
551            maybe_validity: None,
552            chunks,
553        }
554        .into_slots();
555
556        assert!(slots.spilled());
557        assert_eq!(slots.len(), VariadicSlots::FIXED_COUNT + 5);
558        assert_eq!(
559            slots[VariadicSlots::OFFSETS].as_ref().map(|s| s.len()),
560            Some(offsets.len())
561        );
562        assert!(slots[VariadicSlots::MAYBE_VALIDITY].is_none());
563        assert_eq!(
564            slots[VariadicSlots::CHUNKS_OFFSET..]
565                .iter()
566                .map(|s| s.as_ref().map(|c| c.len()))
567                .collect::<Vec<_>>(),
568            (0..5).map(Some).collect::<Vec<_>>()
569        );
570    }
571
572    #[test]
573    fn slot_indices_follow_annotations_not_declaration_order() {
574        assert_eq!(
575            ShuffledVariadicSlots::OFFSETS,
576            VariadicSlots::OFFSETS,
577            "field declaration order must not move a slot"
578        );
579        assert_eq!(
580            ShuffledVariadicSlots::MAYBE_VALIDITY,
581            VariadicSlots::MAYBE_VALIDITY
582        );
583        assert_eq!(
584            ShuffledVariadicSlots::CHUNKS_OFFSET,
585            VariadicSlots::CHUNKS_OFFSET
586        );
587        assert_eq!(
588            ShuffledVariadicSlots::FIXED_COUNT,
589            VariadicSlots::FIXED_COUNT
590        );
591        assert_eq!(ShuffledVariadicSlots::slot_name(0), "offsets");
592        assert_eq!(ShuffledVariadicSlots::slot_name(1), "maybe_validity");
593        assert_eq!(ShuffledVariadicSlots::slot_name(3), "chunks[1]");
594    }
595
596    #[test]
597    fn shuffled_declaration_order_round_trips_through_storage() {
598        let offsets = PrimitiveArray::new(buffer![0u64, 3], Validity::NonNullable).into_array();
599        let validity = PrimitiveArray::new(buffer![1u8, 1], Validity::NonNullable).into_array();
600        let chunk = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
601
602        let slot_vec = vec![
603            Some(offsets.clone()),
604            Some(validity.clone()),
605            Some(chunk.clone()),
606        ];
607
608        let view = ShuffledVariadicSlotsView::from_slots(&slot_vec);
609        assert!(ArrayRef::ptr_eq(view.offsets, &offsets));
610        assert!(ArrayRef::ptr_eq(
611            view.maybe_validity.expect("validity"),
612            &validity
613        ));
614        assert!(ArrayRef::ptr_eq(&view.chunks[0], &chunk));
615        assert_eq!(view.offsets.len(), offsets.len());
616        assert_eq!(view.maybe_validity.map(|v| v.len()), Some(validity.len()));
617        assert_eq!(view.chunks.len(), 1);
618        assert_eq!(view.chunks[0].len(), chunk.len());
619
620        // `into_slots` must emit annotation order, not the shuffled declaration order.
621        let round_tripped = ShuffledVariadicSlots::from_slots(slot_vec.into()).into_slots();
622        assert_eq!(round_tripped.len(), 3);
623        for (index, expected) in [&offsets, &validity, &chunk].into_iter().enumerate() {
624            assert!(ArrayRef::ptr_eq(
625                round_tripped[index].as_ref().expect("present slot"),
626                expected
627            ));
628        }
629        assert_eq!(
630            round_tripped[ShuffledVariadicSlots::OFFSETS]
631                .as_ref()
632                .map(|s| s.len()),
633            Some(offsets.len())
634        );
635        assert_eq!(
636            round_tripped[ShuffledVariadicSlots::MAYBE_VALIDITY]
637                .as_ref()
638                .map(|s| s.len()),
639            Some(validity.len())
640        );
641        assert_eq!(
642            round_tripped[ShuffledVariadicSlots::CHUNKS_OFFSET]
643                .as_ref()
644                .map(|s| s.len()),
645            Some(chunk.len())
646        );
647    }
648}