-
Notifications
You must be signed in to change notification settings - Fork 198
feat: add dense union encoding #9367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9a13499
feat: add dense union encoding
HarukiMoriarty ee4547d
docs: fix dense union dtype link
HarukiMoriarty 4e1e86b
bench: execute dense union take
HarukiMoriarty dc7f161
fix: package dense union readme
HarukiMoriarty 7ad1027
fix: address dense union review feedback
HarukiMoriarty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| [package] | ||
| name = "vortex-dense-union" | ||
| authors = { workspace = true } | ||
| categories = { workspace = true } | ||
| description = "Dense union encoding for Vortex arrays" | ||
| edition = { workspace = true } | ||
| homepage = { workspace = true } | ||
| include = { workspace = true } | ||
| keywords = { workspace = true } | ||
| license = { workspace = true } | ||
| readme = "README.md" | ||
| repository = { workspace = true } | ||
| rust-version = { workspace = true } | ||
| version = { workspace = true } | ||
|
|
||
| [dependencies] | ||
| prost = { workspace = true } | ||
| vortex-array = { workspace = true } | ||
| vortex-buffer = { workspace = true } | ||
| vortex-error = { workspace = true } | ||
| vortex-mask = { workspace = true } | ||
| vortex-session = { workspace = true } | ||
|
|
||
| [dev-dependencies] | ||
| divan = { workspace = true } | ||
| rstest = { workspace = true } | ||
| vortex-array = { workspace = true, features = ["_test-harness"] } | ||
|
|
||
| [lints] | ||
| workspace = true | ||
|
|
||
| [[bench]] | ||
| name = "take" | ||
| harness = false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Vortex Dense Union | ||
|
|
||
| An external dense physical encoding for Vortex's logical `DType::Union`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: Copyright the Vortex contributors | ||
|
|
||
| #![expect(clippy::unwrap_used)] | ||
| #![expect(clippy::cast_possible_truncation)] | ||
|
|
||
| use std::sync::LazyLock; | ||
|
|
||
| use divan::Bencher; | ||
| use vortex_array::ArrayRef; | ||
| use vortex_array::IntoArray; | ||
| use vortex_array::RecursiveCanonical; | ||
| use vortex_array::VortexSessionExecute; | ||
| use vortex_array::array_session; | ||
| use vortex_array::arrays::PrimitiveArray; | ||
| use vortex_array::arrays::UnionArray; | ||
| use vortex_array::dtype::DType; | ||
| use vortex_array::dtype::FieldNames; | ||
| use vortex_array::dtype::Nullability; | ||
| use vortex_array::dtype::PType; | ||
| use vortex_array::dtype::UnionVariants; | ||
| use vortex_dense_union::DenseUnion; | ||
| use vortex_dense_union::initialize; | ||
| use vortex_session::VortexSession; | ||
|
|
||
| const LEN: usize = 65_536; | ||
| const N_VARIANTS: usize = 28; | ||
| const TAKE_LEN: usize = 4_096; | ||
|
|
||
| fn main() { | ||
| LazyLock::force(&SESSION); | ||
| divan::main(); | ||
| } | ||
|
|
||
| static SESSION: LazyLock<VortexSession> = LazyLock::new(|| { | ||
| let session = array_session(); | ||
| initialize(&session); | ||
| session | ||
| }); | ||
|
|
||
| fn variants() -> UnionVariants { | ||
| let names = FieldNames::from_iter((0..N_VARIANTS).map(|index| format!("variant_{index}"))); | ||
| let dtypes = vec![DType::Primitive(PType::I32, Nullability::NonNullable); N_VARIANTS]; | ||
| let type_ids = (1..=N_VARIANTS).map(|type_id| type_id as u8).collect(); | ||
| UnionVariants::try_new(names, dtypes, type_ids).unwrap() | ||
| } | ||
|
|
||
| fn selectors() -> (ArrayRef, ArrayRef, Vec<usize>) { | ||
| let mut child_lengths = vec![0usize; N_VARIANTS]; | ||
| let mut type_ids = Vec::with_capacity(LEN); | ||
| let mut offsets = Vec::with_capacity(LEN); | ||
| for row in 0..LEN { | ||
| let child_index = row % N_VARIANTS; | ||
| type_ids.push((child_index + 1) as u8); | ||
| offsets.push(child_lengths[child_index] as i32); | ||
| child_lengths[child_index] += 1; | ||
| } | ||
| ( | ||
| PrimitiveArray::from_iter(type_ids).into_array(), | ||
| PrimitiveArray::from_iter(offsets).into_array(), | ||
| child_lengths, | ||
| ) | ||
| } | ||
|
|
||
| fn dense_union() -> ArrayRef { | ||
| let (type_ids, offsets, child_lengths) = selectors(); | ||
| let children = child_lengths | ||
| .into_iter() | ||
| .map(|len| PrimitiveArray::from_iter(0..len as i32).into_array()) | ||
| .collect::<Vec<_>>(); | ||
| DenseUnion::try_new(type_ids, offsets, variants(), children) | ||
| .unwrap() | ||
| .into_array() | ||
| } | ||
|
|
||
| fn sparse_union() -> ArrayRef { | ||
| let (type_ids, ..) = selectors(); | ||
| let children = (0..N_VARIANTS) | ||
| .map(|child_index| { | ||
| PrimitiveArray::from_iter((0..LEN).map(move |row| { | ||
| if row % N_VARIANTS == child_index { | ||
| (row / N_VARIANTS) as i32 | ||
| } else { | ||
| 0 | ||
| } | ||
| })) | ||
| .into_array() | ||
| }) | ||
| .collect::<Vec<_>>(); | ||
| UnionArray::try_new(type_ids, variants(), children) | ||
| .unwrap() | ||
| .into_array() | ||
| } | ||
|
|
||
| fn indices() -> ArrayRef { | ||
| PrimitiveArray::from_iter((0..TAKE_LEN).rev().map(|index| index as u32)).into_array() | ||
| } | ||
|
|
||
| fn bench_take(bencher: Bencher, array: ArrayRef, indices: ArrayRef) { | ||
| bencher | ||
| .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) | ||
| .bench_refs(|(array, indices, ctx)| { | ||
| array | ||
| .take((*indices).clone()) | ||
| .unwrap() | ||
| .execute::<RecursiveCanonical>(ctx) | ||
| }); | ||
| } | ||
|
|
||
| #[divan::bench] | ||
| fn dense_take(bencher: Bencher) { | ||
| bench_take(bencher, dense_union(), indices()); | ||
| } | ||
|
|
||
| #[divan::bench] | ||
| fn sparse_take(bencher: Bencher) { | ||
| bench_take(bencher, sparse_union(), indices()); | ||
| } | ||
|
HarukiMoriarty marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: Copyright the Vortex contributors | ||
|
|
||
| use vortex_array::Array; | ||
| use vortex_array::ArrayParts; | ||
| use vortex_array::ArrayRef; | ||
| use vortex_array::ArraySlots; | ||
| use vortex_array::EmptyArrayData; | ||
| use vortex_array::TypedArrayRef; | ||
| use vortex_array::array_slots; | ||
| use vortex_array::dtype::DType; | ||
| use vortex_array::dtype::UnionVariants; | ||
| use vortex_error::VortexExpect; | ||
| use vortex_error::VortexResult; | ||
| use vortex_error::vortex_err; | ||
|
|
||
| /// A [`DenseUnion`]-encoded Vortex array. | ||
| pub type DenseUnionArray = Array<DenseUnion>; | ||
|
|
||
| /// Slot layout of a dense union array. | ||
| #[array_slots(DenseUnion)] | ||
| pub struct DenseUnionSlots { | ||
| /// The row-aligned type IDs selecting union variants. | ||
| #[slot(0)] | ||
| pub type_ids: ArrayRef, | ||
| /// The row-aligned offsets into the selected compact child. | ||
| #[slot(1)] | ||
| pub offsets: ArrayRef, | ||
| /// The compact children in variant order. | ||
| #[slot(2..)] | ||
| pub children: Vec<ArrayRef>, | ||
| } | ||
|
|
||
| /// Concrete parts of a [`DenseUnionArray`]. | ||
| pub struct DenseUnionDataParts { | ||
| /// The union variant schema. | ||
| pub variants: UnionVariants, | ||
| /// The row-aligned type IDs. | ||
| pub type_ids: ArrayRef, | ||
| /// The row-aligned compact-child offsets. | ||
| pub offsets: ArrayRef, | ||
| /// The compact children in variant order. | ||
| pub children: Vec<ArrayRef>, | ||
| } | ||
|
|
||
| pub(crate) fn make_parts( | ||
| type_ids: ArrayRef, | ||
| offsets: ArrayRef, | ||
| variants: UnionVariants, | ||
| children: impl IntoIterator<Item = ArrayRef>, | ||
| ) -> ArrayParts<DenseUnion> { | ||
| let len = type_ids.len(); | ||
| let nullability = type_ids.dtype().nullability(); | ||
| let children = children.into_iter(); | ||
| let (lower, _) = children.size_hint(); | ||
| let mut slots = ArraySlots::with_capacity(DenseUnionSlots::CHILDREN_OFFSET + lower); | ||
| slots.push(Some(type_ids)); | ||
| slots.push(Some(offsets)); | ||
| slots.extend(children.map(Some)); | ||
|
|
||
| ArrayParts::new( | ||
| DenseUnion, | ||
| DType::Union(variants, nullability), | ||
| len, | ||
| EmptyArrayData, | ||
| ) | ||
| .with_slots(slots) | ||
| } | ||
|
|
||
| /// Accessors for a dense union array. | ||
| pub trait DenseUnionArrayExt: DenseUnionArraySlotsExt { | ||
| /// Return the union's variant schema. | ||
| fn variants(&self) -> &UnionVariants { | ||
| match self.as_ref().dtype() { | ||
| DType::Union(variants, _) => variants, | ||
| _ => unreachable!("DenseUnionArrayExt requires a union dtype"), | ||
| } | ||
| } | ||
|
|
||
| /// Iterate over compact children in variant order. | ||
| fn iter_children(&self) -> impl ExactSizeIterator<Item = &ArrayRef> + '_ { | ||
| self.children().iter() | ||
| } | ||
|
|
||
| /// Return a compact child by variant index. | ||
| fn child(&self, index: usize) -> Option<&ArrayRef> { | ||
| self.children().get(index) | ||
| } | ||
|
|
||
| /// Return a compact child selected by a data-level type ID. | ||
| fn child_by_type_id(&self, type_id: u8) -> Option<&ArrayRef> { | ||
| self.child(self.variants().tag_to_child_index(type_id)?) | ||
| } | ||
|
|
||
| /// Return a compact child selected by variant name, if present. | ||
| fn child_by_name_opt(&self, name: impl AsRef<str>) -> Option<&ArrayRef> { | ||
| self.child(self.variants().find(name)?) | ||
| } | ||
|
|
||
| /// Return a compact child selected by variant name. | ||
| fn child_by_name(&self, name: impl AsRef<str>) -> VortexResult<&ArrayRef> { | ||
| let name = name.as_ref(); | ||
| self.child_by_name_opt(name).ok_or_else(|| { | ||
| vortex_err!( | ||
| "Variant {name} not found in dense union array with names {:?}", | ||
| self.variants().names() | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl<T: TypedArrayRef<DenseUnion>> DenseUnionArrayExt for T {} | ||
|
|
||
| /// The dense physical encoding for the logical [`DType::Union`] type. | ||
| #[derive(Clone, Debug)] | ||
| pub struct DenseUnion; | ||
|
|
||
| impl DenseUnion { | ||
| /// Construct a dense union array. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if the components do not satisfy the invariants documented by [`Self::try_new`]. | ||
| pub fn new( | ||
| type_ids: ArrayRef, | ||
| offsets: ArrayRef, | ||
| variants: UnionVariants, | ||
| children: impl IntoIterator<Item = ArrayRef>, | ||
| ) -> DenseUnionArray { | ||
| Self::try_new(type_ids, offsets, variants, children) | ||
| .vortex_expect("DenseUnion construction failed") | ||
| } | ||
|
|
||
| /// Try to construct a dense union array. | ||
|
HarukiMoriarty marked this conversation as resolved.
|
||
| /// | ||
| /// The logical union's nullability is inherited from `type_ids`; nullable type IDs represent | ||
| /// outer union nulls. `type_ids` must be a nullable or non-nullable `u8` array, `offsets` must | ||
| /// be a non-nullable `i32` array of the same length, and the compact children must match the | ||
| /// variant count and dtypes. Type IDs and offsets are structurally validated, but their | ||
| /// individual values are checked only when the array is accessed or converted to its canonical | ||
| /// sparse representation. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns an error when the selector arrays or compact children do not satisfy these | ||
| /// structural invariants. | ||
| pub fn try_new( | ||
| type_ids: ArrayRef, | ||
| offsets: ArrayRef, | ||
| variants: UnionVariants, | ||
| children: impl IntoIterator<Item = ArrayRef>, | ||
| ) -> VortexResult<DenseUnionArray> { | ||
| Array::try_from_parts(make_parts(type_ids, offsets, variants, children)) | ||
| } | ||
| } | ||
|
|
||
| /// Owned accessors for a dense union array. | ||
| pub trait DenseUnionArrayOwnedExt { | ||
| /// Deconstruct this array into its type IDs, offsets, schema, and compact children. | ||
| fn into_data_parts(self) -> DenseUnionDataParts; | ||
| } | ||
|
|
||
| impl DenseUnionArrayOwnedExt for Array<DenseUnion> { | ||
| fn into_data_parts(self) -> DenseUnionDataParts { | ||
| let variants = self.variants().clone(); | ||
| let type_ids = self.type_ids().clone(); | ||
| let offsets = self.offsets().clone(); | ||
| let children = self.iter_children().cloned().collect(); | ||
| DenseUnionDataParts { | ||
| variants, | ||
| type_ids, | ||
| offsets, | ||
| children, | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.