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::VortexSessionExecute;
21use crate::array::Array;
22use crate::array::ArrayParts;
23use crate::array::TypedArrayRef;
24use crate::array_slots;
25use crate::arrays::Patched;
26use crate::arrays::PrimitiveArray;
27use crate::arrays::patched::TransposedPatches;
28use crate::arrays::patched::patch_lanes;
29use crate::buffer::BufferHandle;
30use crate::dtype::DType;
31use crate::dtype::IntegerPType;
32use crate::dtype::NativePType;
33use crate::dtype::PType;
34use crate::legacy_session;
35use crate::match_each_native_ptype;
36use crate::match_each_unsigned_integer_ptype;
37use crate::patches::Patches;
38use crate::validity::Validity;
39
40#[derive(Debug, Clone)]
41pub struct PatchedData {
42    /// Number of lanes the patch indices and values have been split into. Each of the `n_chunks`
43    /// of 1024 values is split into `n_lanes` lanes horizontally, each lane having 1024 / n_lanes
44    /// values that might be patched.
45    pub(super) n_lanes: usize,
46
47    /// The offset into that first chunk that is considered in bounds.
48    ///
49    /// The patch indices of the first chunk less than `offset` should be skipped, and the offset
50    /// should be subtracted out of the remaining offsets to get their final position in the
51    /// executed array.
52    pub(super) offset: usize,
53}
54
55#[array_slots(Patched)]
56pub struct PatchedSlots {
57    /// The inner array containing the base unpatched values.
58    #[slot(0)]
59    pub inner: ArrayRef,
60    /// The lane offsets array for locating patches within lanes.
61    #[slot(1)]
62    pub lane_offsets: ArrayRef,
63    /// The indices of patched (exception) values.
64    #[slot(2)]
65    pub patch_indices: ArrayRef,
66    /// The patched (exception) values at the corresponding indices.
67    #[slot(3)]
68    pub patch_values: ArrayRef,
69}
70
71impl Display for PatchedData {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        write!(f, "n_lanes: {}, offset: {}", self.n_lanes, self.offset)
74    }
75}
76
77impl PatchedData {
78    pub(crate) fn validate(
79        &self,
80        dtype: &DType,
81        len: usize,
82        slots: &PatchedSlotsView,
83    ) -> VortexResult<()> {
84        vortex_ensure_eq!(
85            slots.inner.dtype(),
86            dtype,
87            "PatchedArray base dtype does not match outer dtype",
88        );
89        vortex_ensure_eq!(
90            slots.inner.len(),
91            len,
92            "PatchedArray base len does not match outer len",
93        );
94        vortex_ensure_eq!(
95            slots.patch_indices.len(),
96            slots.patch_values.len(),
97            "PatchedArray patch indices len does not match patch values len",
98        );
99        Ok(())
100    }
101}
102
103pub trait PatchedArrayExt: PatchedArraySlotsExt {
104    #[inline]
105    fn n_lanes(&self) -> usize {
106        self.n_lanes
107    }
108
109    #[inline]
110    fn offset(&self) -> usize {
111        self.offset
112    }
113
114    #[inline]
115    #[allow(clippy::disallowed_methods)]
116    fn lane_range(&self, chunk: usize, lane: usize) -> VortexResult<Range<usize>> {
117        assert!(chunk * 1024 <= self.as_ref().len() + self.offset());
118        assert!(lane < self.n_lanes());
119
120        let start = self.lane_offsets().execute_scalar(
121            chunk * self.n_lanes() + lane,
122            &mut legacy_session().create_execution_ctx(),
123        )?;
124        let stop = self.lane_offsets().execute_scalar(
125            chunk * self.n_lanes() + lane + 1,
126            &mut legacy_session().create_execution_ctx(),
127        )?;
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.as_ref().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.as_ref().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_eq!(view.required.len(), 3);
422        assert_eq!(view.maybe.expect("optional slot").len(), 3);
423
424        let cloned = OptionalPatchedSlots::from_slots(slot_vec.into());
425        assert_eq!(cloned.required.len(), required.len());
426        assert_eq!(cloned.maybe.expect("optional clone").len(), optional.len());
427
428        let rebuilt = PatchedSlots::from_slots(
429            vec![
430                Some(required.clone()),
431                Some(optional.clone()),
432                Some(required.clone()),
433                Some(optional.clone()),
434            ]
435            .into(),
436        );
437        assert_eq!(rebuilt.inner.len(), required.len());
438        assert_eq!(rebuilt.patch_values.len(), optional.len());
439    }
440
441    #[test]
442    fn variadic_slots_round_trip() {
443        let offsets = PrimitiveArray::new(buffer![0u64, 3, 5], Validity::NonNullable).into_array();
444        let chunk0 = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
445        let chunk1 = PrimitiveArray::new(buffer![4u8, 5], Validity::NonNullable).into_array();
446
447        assert_eq!(VariadicSlots::OFFSETS, 0);
448        assert_eq!(VariadicSlots::MAYBE_VALIDITY, 1);
449        assert_eq!(VariadicSlots::CHUNKS_OFFSET, 2);
450        assert_eq!(VariadicSlots::FIXED_COUNT, 2);
451        assert_eq!(VariadicSlots::slot_name(0), "offsets");
452        assert_eq!(VariadicSlots::slot_name(3), "chunks[1]");
453
454        let slot_vec = vec![Some(offsets.clone()), None, Some(chunk0), Some(chunk1)];
455
456        let view = VariadicSlotsView::from_slots(&slot_vec);
457        assert_eq!(view.offsets.len(), 3);
458        assert!(view.maybe_validity.is_none());
459        assert_eq!(view.chunks.len(), 2);
460        assert_eq!(view.chunks[0].len(), 3);
461        assert_eq!(view.chunks.get(1).map(|c| c.len()), Some(2));
462        assert!(view.chunks.get(2).is_none());
463        assert_eq!(
464            view.chunks.iter().map(|c| c.len()).collect::<Vec<_>>(),
465            vec![3, 2]
466        );
467
468        let owned = view.to_owned();
469        assert_eq!(owned.chunks.len(), 2);
470
471        let owned = VariadicSlots::from_slots(slot_vec.into());
472        assert_eq!(owned.offsets.len(), offsets.len());
473        assert!(owned.maybe_validity.is_none());
474        assert_eq!(owned.chunks.len(), 2);
475
476        let slots = owned.into_slots();
477        assert_eq!(slots.len(), 4);
478        assert!(slots[1].is_none());
479        assert_eq!(
480            slots[VariadicSlots::CHUNKS_OFFSET]
481                .as_ref()
482                .map(|c| c.len()),
483            Some(3)
484        );
485    }
486
487    #[test]
488    fn variadic_slots_empty_tail() {
489        let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
490        let slot_vec = vec![Some(offsets), None];
491
492        let view = VariadicSlotsView::from_slots(&slot_vec);
493        assert!(view.chunks.is_empty());
494
495        let owned = VariadicSlots::from_slots(slot_vec.into());
496        assert!(owned.chunks.is_empty());
497        assert_eq!(owned.into_slots().len(), 2);
498    }
499
500    #[test]
501    fn variadic_slots_spilled_tail_keeps_order() {
502        let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
503        let chunks: Vec<ArrayRef> = (0..5u8)
504            .map(|len| {
505                PrimitiveArray::new((0..len).collect::<Buffer<u8>>(), Validity::NonNullable)
506                    .into_array()
507            })
508            .collect();
509
510        let slots = VariadicSlots {
511            offsets: offsets.clone(),
512            maybe_validity: None,
513            chunks,
514        }
515        .into_slots();
516
517        assert!(slots.spilled());
518        assert_eq!(slots.len(), VariadicSlots::FIXED_COUNT + 5);
519        assert_eq!(
520            slots[VariadicSlots::OFFSETS].as_ref().map(|s| s.len()),
521            Some(offsets.len())
522        );
523        assert!(slots[VariadicSlots::MAYBE_VALIDITY].is_none());
524        assert_eq!(
525            slots[VariadicSlots::CHUNKS_OFFSET..]
526                .iter()
527                .map(|s| s.as_ref().map(|c| c.len()))
528                .collect::<Vec<_>>(),
529            (0..5).map(Some).collect::<Vec<_>>()
530        );
531    }
532
533    #[test]
534    fn slot_indices_follow_annotations_not_declaration_order() {
535        assert_eq!(
536            ShuffledVariadicSlots::OFFSETS,
537            VariadicSlots::OFFSETS,
538            "field declaration order must not move a slot"
539        );
540        assert_eq!(
541            ShuffledVariadicSlots::MAYBE_VALIDITY,
542            VariadicSlots::MAYBE_VALIDITY
543        );
544        assert_eq!(
545            ShuffledVariadicSlots::CHUNKS_OFFSET,
546            VariadicSlots::CHUNKS_OFFSET
547        );
548        assert_eq!(
549            ShuffledVariadicSlots::FIXED_COUNT,
550            VariadicSlots::FIXED_COUNT
551        );
552        assert_eq!(ShuffledVariadicSlots::slot_name(0), "offsets");
553        assert_eq!(ShuffledVariadicSlots::slot_name(1), "maybe_validity");
554        assert_eq!(ShuffledVariadicSlots::slot_name(3), "chunks[1]");
555    }
556
557    #[test]
558    fn shuffled_declaration_order_round_trips_through_storage() {
559        let offsets = PrimitiveArray::new(buffer![0u64, 3], Validity::NonNullable).into_array();
560        let validity = PrimitiveArray::new(buffer![1u8, 1], Validity::NonNullable).into_array();
561        let chunk = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
562
563        let slot_vec = vec![
564            Some(offsets.clone()),
565            Some(validity.clone()),
566            Some(chunk.clone()),
567        ];
568
569        let view = ShuffledVariadicSlotsView::from_slots(&slot_vec);
570        assert_eq!(view.offsets.len(), offsets.len());
571        assert_eq!(view.maybe_validity.map(|v| v.len()), Some(validity.len()));
572        assert_eq!(view.chunks.len(), 1);
573        assert_eq!(view.chunks[0].len(), chunk.len());
574
575        // `into_slots` must emit annotation order, not the shuffled declaration order.
576        let round_tripped = ShuffledVariadicSlots::from_slots(slot_vec.into()).into_slots();
577        assert_eq!(round_tripped.len(), 3);
578        assert_eq!(
579            round_tripped[ShuffledVariadicSlots::OFFSETS]
580                .as_ref()
581                .map(|s| s.len()),
582            Some(offsets.len())
583        );
584        assert_eq!(
585            round_tripped[ShuffledVariadicSlots::MAYBE_VALIDITY]
586                .as_ref()
587                .map(|s| s.len()),
588            Some(validity.len())
589        );
590        assert_eq!(
591            round_tripped[ShuffledVariadicSlots::CHUNKS_OFFSET]
592                .as_ref()
593                .map(|s| s.len()),
594            Some(chunk.len())
595        );
596    }
597}