Skip to content

Commit a80d07c

Browse files
committed
Implement RowFn batch execution
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 25e944b commit a80d07c

17 files changed

Lines changed: 1677 additions & 16 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Execution arguments paired with the metadata selected during planning.
5+
//!
6+
//! [`BorrowedExecutionArgs`] can point at original, sliced, or filtered arrays while retaining the
7+
//! dtypes, output dtype, and null policy of the original batch plan.
8+
9+
use vortex_error::VortexResult;
10+
use vortex_error::vortex_err;
11+
12+
use crate::ArrayRef;
13+
use crate::dtype::DType;
14+
use crate::scalar_fn::ExecutionArgs;
15+
use crate::scalar_fn::unstable::row::visitor::RowPolicy;
16+
17+
/// A borrowed [`ExecutionArgs`] view with the planning metadata selected for its row kernel.
18+
///
19+
/// `arrays` may be filtered or sliced, while `dtypes` and `output_dtype` always describe the
20+
/// original planned batch. Keeping them together prevents an execution path from pairing an input
21+
/// view with unrelated planning metadata.
22+
#[derive(Clone, Copy)]
23+
pub(crate) struct BorrowedExecutionArgs<'a> {
24+
/// The input arrays for this kernel invocation.
25+
arrays: &'a [ArrayRef],
26+
27+
/// The number of rows in this kernel invocation.
28+
row_count: usize,
29+
30+
/// The original input dtypes used to select the row implementation.
31+
dtypes: &'a [DType],
32+
33+
/// The non-nullable dtype built by the selected output capability.
34+
output_dtype: &'a DType,
35+
36+
/// The nullable execution policy selected during planning.
37+
policy: RowPolicy,
38+
}
39+
40+
impl<'a> BorrowedExecutionArgs<'a> {
41+
/// Pair one input view with the planning metadata selected for its batch.
42+
pub(crate) fn new(
43+
arrays: &'a [ArrayRef],
44+
row_count: usize,
45+
dtypes: &'a [DType],
46+
output_dtype: &'a DType,
47+
policy: RowPolicy,
48+
) -> Self {
49+
Self {
50+
arrays,
51+
row_count,
52+
dtypes,
53+
output_dtype,
54+
policy,
55+
}
56+
}
57+
58+
/// Return the original input dtypes used to select the row implementation.
59+
pub(crate) fn dtypes(&self) -> &'a [DType] {
60+
self.dtypes
61+
}
62+
63+
/// Return the non-nullable dtype built by the selected output capability.
64+
pub(crate) fn output_dtype(&self) -> &'a DType {
65+
self.output_dtype
66+
}
67+
68+
/// Return the nullable execution policy selected during planning.
69+
pub(crate) fn policy(&self) -> RowPolicy {
70+
self.policy
71+
}
72+
}
73+
74+
impl ExecutionArgs for BorrowedExecutionArgs<'_> {
75+
fn get(&self, index: usize) -> VortexResult<ArrayRef> {
76+
self.arrays.get(index).cloned().ok_or_else(|| {
77+
vortex_err!(
78+
"row-function input index must be less than {}, got {index}",
79+
self.arrays.len(),
80+
)
81+
})
82+
}
83+
84+
fn num_inputs(&self) -> usize {
85+
self.arrays.len()
86+
}
87+
88+
fn row_count(&self) -> usize {
89+
self.row_count
90+
}
91+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use smallvec::SmallVec;
5+
use vortex_error::VortexResult;
6+
7+
use super::super::Batch;
8+
use super::super::args::BorrowedExecutionArgs;
9+
use crate::ArrayRef;
10+
use crate::ExecutionCtx;
11+
use crate::IntoArray;
12+
use crate::arrays::ConstantArray;
13+
use crate::scalar_fn::unstable::row::execute::RowExecution;
14+
15+
impl Batch {
16+
/// Evaluate one row of constant inputs and broadcast the validated result.
17+
pub(super) fn broadcast_one_row(
18+
&self,
19+
kernel: impl Fn(BorrowedExecutionArgs<'_>, &mut ExecutionCtx) -> VortexResult<RowExecution>,
20+
ctx: &mut ExecutionCtx,
21+
) -> VortexResult<ArrayRef> {
22+
let one_row: SmallVec<[ArrayRef; 4]> = self
23+
.inputs
24+
.iter()
25+
.map(|input| input.slice(0..1))
26+
.collect::<VortexResult<_>>()?;
27+
28+
let result = VortexResult::from(kernel(self.execution_args(&one_row, 1), ctx)?)?;
29+
let result = self.validate_kernel_output(result, 1, ctx)?;
30+
let result = self.finalize_output(result, 1)?;
31+
let scalar = result.execute_scalar(0, ctx)?;
32+
33+
Ok(ConstantArray::new(scalar, self.row_count).into_array())
34+
}
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
6+
use super::super::Batch;
7+
use super::super::args::BorrowedExecutionArgs;
8+
use crate::ArrayRef;
9+
use crate::ExecutionCtx;
10+
use crate::builtins::ArrayBuiltins;
11+
use crate::scalar_fn::unstable::row::execute::RowExecution;
12+
use crate::validity::Validity;
13+
14+
impl Batch {
15+
/// Run every stored payload, then attach the input validity without materializing its mask.
16+
pub(super) fn execute_dense(
17+
&self,
18+
kernel: impl Fn(BorrowedExecutionArgs<'_>, &mut ExecutionCtx) -> VortexResult<RowExecution>,
19+
retry_deferred_error: bool,
20+
ctx: &mut ExecutionCtx,
21+
) -> VortexResult<ArrayRef> {
22+
let values = match kernel(self.execution_args(&self.inputs, self.row_count), ctx)? {
23+
RowExecution::Output(values) => values,
24+
RowExecution::DeferredError(error) if retry_deferred_error => {
25+
let valid = self.validity.clone().execute_mask(self.row_count, ctx)?;
26+
27+
// Unlike `resolve_validity`, all-true preserves the deferred error and all-false
28+
// suppresses evidence that came entirely from null rows. An empty loop cannot
29+
// produce deferred evidence, so the ambiguous empty mask cannot reach this arm.
30+
if valid.all_true() {
31+
return Err(error);
32+
}
33+
if valid.all_false() {
34+
return Ok(self.all_null());
35+
}
36+
37+
// Deferred retry receives only the dense kernel. Filter first so the retry cannot
38+
// evaluate null rows again.
39+
return self.filter_and_scatter(kernel, &valid, ctx);
40+
}
41+
RowExecution::DeferredError(error) => return Err(error),
42+
};
43+
let values = self.validate_kernel_output(values, self.row_count, ctx)?;
44+
45+
match self.validity.clone() {
46+
Validity::NonNullable | Validity::AllValid => {
47+
self.finalize_output(values, self.row_count)
48+
}
49+
Validity::Array(valid) => self.finalize_output(values.mask(valid)?, self.row_count),
50+
// Handled by the guard in `Batch::execute`, before the kernel ran.
51+
Validity::AllInvalid => Ok(self.all_null()),
52+
}
53+
}
54+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use smallvec::SmallVec;
5+
use vortex_error::VortexResult;
6+
use vortex_error::vortex_bail;
7+
use vortex_error::vortex_ensure_eq;
8+
use vortex_mask::AllOr;
9+
use vortex_mask::Mask;
10+
11+
use super::super::Batch;
12+
use super::super::args::BorrowedExecutionArgs;
13+
use crate::ArrayRef;
14+
use crate::ExecutionCtx;
15+
use crate::IntoArray;
16+
use crate::arrays::BoolArray;
17+
use crate::arrays::MaskedArray;
18+
use crate::arrays::PrimitiveArray;
19+
use crate::builtins::ArrayBuiltins;
20+
use crate::dtype::Nullability;
21+
use crate::scalar_fn::unstable::row::execute::RowExecution;
22+
use crate::validity::Validity;
23+
24+
impl Batch {
25+
/// Filter to valid rows, run the kernel, then scatter into a null-padded output.
26+
pub(super) fn filter_and_scatter(
27+
&self,
28+
kernel: impl Fn(BorrowedExecutionArgs<'_>, &mut ExecutionCtx) -> VortexResult<RowExecution>,
29+
valid: &Mask,
30+
ctx: &mut ExecutionCtx,
31+
) -> VortexResult<ArrayRef> {
32+
let filtered: SmallVec<[ArrayRef; 4]> = self
33+
.inputs
34+
.iter()
35+
.map(|input| input.filter(valid.clone()))
36+
.collect::<VortexResult<_>>()?;
37+
38+
let values = VortexResult::from(kernel(
39+
self.execution_args(&filtered, valid.true_count()),
40+
ctx,
41+
)?)?;
42+
let values = self.validate_kernel_output(values, valid.true_count(), ctx)?;
43+
44+
self.finalize_output(self.scatter_valid(values, valid)?, valid.len())
45+
}
46+
47+
/// Scatter `values` (one per set bit of `valid`, in order) back to the positions of the set
48+
/// bits, producing an array of length `valid.len()` that is null at every unset position.
49+
fn scatter_valid(&self, values: ArrayRef, valid: &Mask) -> VortexResult<ArrayRef> {
50+
vortex_ensure_eq!(
51+
values.len(),
52+
valid.true_count(),
53+
"the {} kernel output must contain {} filtered rows, got {}",
54+
self.id,
55+
valid.true_count(),
56+
values.len(),
57+
);
58+
59+
let AllOr::Some(slices) = valid.slices() else {
60+
// The caller handles the all-true and all-false masks.
61+
vortex_bail!(
62+
"scatter_valid requires valid and invalid rows, got an all-valid or all-invalid mask"
63+
);
64+
};
65+
66+
// Gather indices: row i of the output reads values[rank(i)]. Rows behind nulls read index
67+
// 0, and any in-bounds index would do since they are masked out below (values is non-empty
68+
// here).
69+
let mut indices = vec![0u64; valid.len()];
70+
let mut rank = 0u64;
71+
for &(start, end) in slices {
72+
for index in &mut indices[start..end] {
73+
*index = rank;
74+
rank += 1;
75+
}
76+
}
77+
let indices = PrimitiveArray::new(indices, Validity::NonNullable).into_array();
78+
79+
let scattered = values.take(indices)?;
80+
81+
// A nullable gathered array cannot be wrapped because a `Masked` child must be all valid.
82+
// The general masking pass unions its nulls with the batch validity instead.
83+
if scattered.dtype().is_nullable() {
84+
let mask = BoolArray::new(valid.to_bit_buffer(), Validity::NonNullable).into_array();
85+
return scattered.mask(mask);
86+
}
87+
88+
// The gathered values are all valid, so attaching validity is sufficient.
89+
Ok(MaskedArray::try_new(
90+
scattered,
91+
Validity::from_mask(valid.clone(), Nullability::Nullable),
92+
)?
93+
.into_array())
94+
}
95+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Selects a batch execution strategy.
5+
//!
6+
//! [`Batch::execute`] handles universal fast paths, then delegates to dense or valid-only
7+
//! execution.
8+
9+
use vortex_error::VortexResult;
10+
use vortex_mask::Mask;
11+
12+
use super::Batch;
13+
use super::RowPolicy;
14+
use super::args::BorrowedExecutionArgs;
15+
use crate::ArrayRef;
16+
use crate::ExecutionCtx;
17+
use crate::arrays::Constant;
18+
use crate::scalar_fn::unstable::row::execute::RowExecution;
19+
use crate::scalar_fn::unstable::row::types::batch_const;
20+
use crate::validity::Validity;
21+
22+
mod constant;
23+
mod dense;
24+
mod filter_scatter;
25+
mod valid_only;
26+
27+
mod output;
28+
pub(crate) use output::finalize_kernel_output;
29+
30+
impl Batch {
31+
/// Apply constant folding and null handling around `kernel`.
32+
///
33+
/// When the mask contains valid and invalid rows, `try_unfiltered` may avoid filtering.
34+
/// `Ok(None)` filters the valid rows and scatters the output back. Every result is checked
35+
/// against the planned shape and dtype.
36+
pub(crate) fn execute(
37+
&self,
38+
kernel: impl Fn(BorrowedExecutionArgs<'_>, &mut ExecutionCtx) -> VortexResult<RowExecution>,
39+
try_unfiltered: impl FnOnce(
40+
BorrowedExecutionArgs<'_>,
41+
&Mask,
42+
&mut ExecutionCtx,
43+
) -> VortexResult<Option<RowExecution>>,
44+
ctx: &mut ExecutionCtx,
45+
) -> VortexResult<ArrayRef> {
46+
// Strictness: an all-null batch has no observable row work. Keep the literal-constant
47+
// check explicit alongside the conjoined validity invariant.
48+
if matches!(self.validity, Validity::AllInvalid)
49+
|| self.inputs.iter().any(|input| {
50+
input
51+
.as_opt::<Constant>()
52+
.is_some_and(|constant| constant.scalar().is_null())
53+
})
54+
{
55+
return Ok(self.all_null());
56+
}
57+
58+
// All inputs constant, and their conjoined validity proves every row non-null. This sees
59+
// through extension and masked wrappers just like argument decoding does.
60+
if self.row_count > 0
61+
&& self.validity.definitely_no_nulls()
62+
&& self.inputs.iter().all(|input| batch_const(input).is_some())
63+
{
64+
return self.broadcast_one_row(kernel, ctx);
65+
}
66+
67+
match self.policy {
68+
RowPolicy::Dense => self.execute_dense(kernel, false, ctx),
69+
RowPolicy::DenseWithRetry => self.execute_dense(kernel, true, ctx),
70+
RowPolicy::ValidOnly => self.execute_valid_only(kernel, try_unfiltered, ctx),
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)