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