Skip to content

Commit 348583f

Browse files
committed
Execute spatial distance with RowFn
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent c1382ba commit 348583f

17 files changed

Lines changed: 361 additions & 100 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Fallback execution for row kernels that cannot operate directly on partially valid inputs.
5+
//!
6+
//! This path filters every input, runs the dense kernel, and expands its output back to the
7+
//! original batch shape.
8+
9+
use smallvec::SmallVec;
10+
use vortex_error::VortexResult;
11+
use vortex_error::vortex_bail;
12+
use vortex_error::vortex_ensure_eq;
13+
use vortex_mask::AllOr;
14+
use vortex_mask::Mask;
15+
16+
use super::super::RowFnExecutionArgs;
17+
use super::super::args::BorrowedRowFnArgs;
18+
use crate::ArrayRef;
19+
use crate::ExecutionCtx;
20+
use crate::IntoArray;
21+
use crate::arrays::BoolArray;
22+
use crate::arrays::MaskedArray;
23+
use crate::arrays::PrimitiveArray;
24+
use crate::builtins::ArrayBuiltins;
25+
use crate::dtype::Nullability;
26+
use crate::validity::Validity;
27+
28+
impl RowFnExecutionArgs {
29+
/// Filter to valid rows, run the kernel, then scatter into a null-padded output.
30+
pub(super) fn filter_and_scatter(
31+
&self,
32+
kernel: impl Fn(BorrowedRowFnArgs<'_>, &mut ExecutionCtx) -> VortexResult<ArrayRef>,
33+
valid: &Mask,
34+
ctx: &mut ExecutionCtx,
35+
) -> VortexResult<ArrayRef> {
36+
let filtered: SmallVec<[ArrayRef; 4]> = self
37+
.inputs
38+
.iter()
39+
.map(|input| input.filter(valid.clone()))
40+
.collect::<VortexResult<_>>()?;
41+
42+
let values = kernel(self.execution_args(&filtered, valid.true_count()), ctx)?;
43+
let values = self.validate_kernel_output(values, valid.true_count(), ctx)?;
44+
45+
self.finalize_output(self.scatter_valid(values, valid)?, valid.len())
46+
}
47+
48+
/// Scatter `values` (one per set bit of `valid`, in order) back to the positions of the set
49+
/// bits, producing an array of length `valid.len()` that is null at every unset position.
50+
fn scatter_valid(&self, values: ArrayRef, valid: &Mask) -> VortexResult<ArrayRef> {
51+
vortex_ensure_eq!(
52+
values.len(),
53+
valid.true_count(),
54+
"the {} kernel output must contain {} filtered rows, got {}",
55+
self.id,
56+
valid.true_count(),
57+
values.len(),
58+
);
59+
60+
let AllOr::Some(slices) = valid.slices() else {
61+
// The caller handles the all-true and all-false masks.
62+
vortex_bail!(
63+
"scatter_valid requires valid and invalid rows, got an all-valid or all-invalid mask"
64+
);
65+
};
66+
67+
// `take` emits one row per source index. Supplying one index per original batch row expands
68+
// the compact kernel output back to the batch shape. Null rows temporarily read index zero;
69+
// any in-bounds index works because the result is masked below.
70+
let mut source_indices = vec![0u64; valid.len()];
71+
let mut rank = 0u64;
72+
for &(start, end) in slices {
73+
for index in &mut source_indices[start..end] {
74+
*index = rank;
75+
rank += 1;
76+
}
77+
}
78+
let source_indices =
79+
PrimitiveArray::new(source_indices, Validity::NonNullable).into_array();
80+
81+
let expanded = values.take(source_indices)?;
82+
83+
// A nullable gathered array cannot be wrapped because a `Masked` child must be all valid.
84+
// The general masking pass unions its nulls with the batch validity instead.
85+
if expanded.dtype().is_nullable() {
86+
let mask = BoolArray::new(valid.to_bit_buffer(), Validity::NonNullable).into_array();
87+
return expanded.mask(mask);
88+
}
89+
90+
// The gathered values are all valid, so attaching validity is sufficient.
91+
Ok(MaskedArray::try_new(
92+
expanded,
93+
Validity::from_mask(valid.clone(), Nullability::Nullable),
94+
)?
95+
.into_array())
96+
}
97+
}

‎vortex-array/src/scalar_fn/unstable/row/batch/execute/mod.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::validity::Validity;
2020

2121
mod constant;
2222
mod dense;
23+
mod filter_scatter;
2324
mod valid_only;
2425

2526
mod output;
@@ -30,8 +31,8 @@ impl RowFnExecutionArgs {
3031
/// Apply encoded reductions, constant folding, and null handling around `kernel`.
3132
///
3233
/// `reduce` receives the original inputs before constant broadcasting. For a partially valid
33-
/// batch, `try_valid_rows` executes only valid rows over the original inputs. Every result is
34-
/// checked against the planned shape and dtype.
34+
/// batch, `try_valid_rows` can avoid filtering. `Ok(None)` filters the valid rows and scatters
35+
/// the output back. Every result is checked against the planned shape and dtype.
3536
pub(crate) fn execute(
3637
&self,
3738
reduce: impl FnOnce(BorrowedRowFnArgs<'_>, &mut ExecutionCtx) -> VortexResult<Option<ArrayRef>>,

‎vortex-array/src/scalar_fn/unstable/row/batch/execute/valid_only.rs‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use vortex_error::VortexResult;
5-
use vortex_error::vortex_panic;
65
use vortex_mask::Mask;
76

87
use super::super::RowFnExecutionArgs;
@@ -24,12 +23,7 @@ enum ResolvedValidity {
2423
}
2524

2625
impl RowFnExecutionArgs {
27-
/// Resolve validity, then execute valid rows over the original inputs.
28-
///
29-
/// # Panics
30-
///
31-
/// Panics if the concrete row signature cannot use direct valid-row execution. Inputs must
32-
/// support null-tolerant decoding, and output sinks must initialize skipped rows.
26+
/// Resolve validity, try direct valid-row execution, then fall back to filtering.
3327
pub(super) fn execute_valid_only(
3428
&self,
3529
kernel: impl Fn(BorrowedRowFnArgs<'_>, &mut ExecutionCtx) -> VortexResult<ArrayRef>,
@@ -49,10 +43,7 @@ impl RowFnExecutionArgs {
4943
return Ok(result);
5044
}
5145

52-
vortex_panic!(
53-
"valid-only execution requires direct valid-row support; {} selected an unsupported signature",
54-
self.id,
55-
)
46+
self.filter_and_scatter(kernel, &valid, ctx)
5647
}
5748

5849
/// Materialize validity and handle all-valid or all-null batches.

‎vortex-array/src/scalar_fn/unstable/row/batch/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ pub(crate) struct RowFnExecutionArgs {
4040
/// The number of rows in the original execution scope.
4141
row_count: usize,
4242

43-
/// The input columns, collected once for validity, constant handling, and execution.
43+
/// The input columns, collected once: constant folding inspects them and the filter strategy
44+
/// filters them.
4445
inputs: SmallVec<[ArrayRef; 4]>,
4546

4647
/// The input dtypes, collected with the columns and reused by both planning and execution.

‎vortex-array/src/scalar_fn/unstable/row/batch/tests.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use vortex_error::VortexResult;
99
use vortex_error::vortex_bail;
1010
use vortex_session::registry::CachedId;
1111

12+
use super::BatchPlan;
13+
use super::RowFnExecutionArgs;
14+
use super::RowPolicy;
1215
use super::finalize_kernel_output;
1316
use crate::ArrayRef;
1417
use crate::ExecutionCtx;
@@ -311,6 +314,35 @@ fn test_reduce_encoded_precedes_constant_broadcast() -> VortexResult<()> {
311314
Ok(())
312315
}
313316

317+
#[test]
318+
fn test_valid_only_filters_and_scatters() -> VortexResult<()> {
319+
static ID: CachedId = CachedId::new("test.filter_and_scatter");
320+
321+
let input = PrimitiveArray::new(
322+
vec![10_i64, 20, 30, 40],
323+
Validity::from_iter([true, false, true, false]),
324+
)
325+
.into_array();
326+
let args = VecExecutionArgs::new(vec![input.clone()], 4);
327+
let batch = RowFnExecutionArgs::new(*ID, &args, |_| {
328+
Ok(BatchPlan {
329+
output_dtype: DType::from(i64::PTYPE),
330+
policy: RowPolicy::ValidOnly,
331+
})
332+
})?;
333+
let mut ctx = array_session().create_execution_ctx();
334+
335+
let actual = batch.execute(
336+
|_args, _ctx| Ok(None),
337+
|args, _ctx| Ok(args.arrays()[0].clone()),
338+
|_args, _valid, _ctx| Ok(None),
339+
&mut ctx,
340+
)?;
341+
342+
assert_arrays_eq!(&actual, &input, &mut ctx);
343+
Ok(())
344+
}
345+
314346
#[test]
315347
fn test_kernel_output_rejects_nulls_at_function_boundary() -> VortexResult<()> {
316348
let input = PrimitiveArray::new(vec![1_i64, 2], Validity::NonNullable).into_array();

‎vortex-array/src/scalar_fn/unstable/row/execute/sink.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ where
9898
/// Write only the rows set in `valid`, or decline when the inputs or sink cannot support
9999
/// skip-invalid execution.
100100
///
101-
/// `Ok(None)` signals that direct skip-invalid execution is unavailable. Batch execution decides
102-
/// how to handle the decline.
101+
/// `Ok(None)` signals batch execution to filter every input to the valid rows, run the dense
102+
/// kernel, and scatter the results back into a null-padded array.
103103
pub(crate) fn execute_sink_valid_rows<Args, Prepared, Sink, ApplyResult, Options>(
104104
args: &dyn ExecutionArgs,
105105
valid: &Mask,
@@ -222,8 +222,8 @@ where
222222
return Ok(None);
223223
};
224224

225-
// Null-tolerant decoding exposes values behind nulls without filtering. Decline when any input
226-
// cannot provide those values safely.
225+
// Null-tolerant decoding exposes values behind nulls without filtering. Decline so batch
226+
// execution can filter when any input cannot provide those values safely.
227227
let Some(columns) = Args::decode_null_tolerant(args, ctx)? else {
228228
return Ok(None);
229229
};

‎vortex-array/src/scalar_fn/unstable/row/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
//! Unlike a general strict function, a [`RowFn`] cannot produce null from valid inputs.
1515
//!
1616
//! A _partially valid_ batch contains both valid and invalid rows. _Skip-invalid_ runs the kernel
17-
//! only for valid rows without changing row positions.
17+
//! only for valid rows without changing row positions. _Filter-and-scatter_ compacts valid rows,
18+
//! runs the kernel, and restores their positions.
1819
//!
1920
//! Prepared visits move work derived from constant operands outside the hot loop. Deferred visits
2021
//! reduce compact failure evidence without constructing errors in that loop.

‎vortex-array/src/scalar_fn/unstable/row/types/sink.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub unsafe trait OutputSink<Options>: 'static + Sized {
8484
/// `Some` enables this strategy. The initializer **must** make every row safe to finish.
8585
/// Callbacks overwrite valid rows, and batch execution masks skipped rows.
8686
///
87-
/// `None` makes skip-invalid execution unavailable for this sink.
87+
/// `None` makes the executor fall back to filtering the inputs.
8888
fn skipped_rows_initializer() -> Option<for<'a> fn(&mut Self::Rows<'a>)> {
8989
None
9090
}

‎vortex-array/src/scalar_fn/unstable/row/visitor/execute.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//! Visitors that execute dense and skip-invalid row loops.
55
//!
66
//! Each visit revalidates its concrete signature and checks that its output dtype and execution
7-
//! policy match the plan before entering a row loop. [`ExecuteValidRows`] can decline when the
8-
//! signature cannot execute over the original inputs.
7+
//! policy match the plan before entering a row loop. [`ExecuteValidRows`] can decline, so the batch
8+
//! layer filters the inputs and retries with [`ExecuteRows`].
99
1010
use vortex_error::VortexResult;
1111
use vortex_error::vortex_ensure_eq;

‎vortex-array/src/scalar_fn/unstable/row/visitor/plan.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub(crate) enum RowPolicy {
133133
/// Evaluate all rows and mask the result.
134134
Dense,
135135

136-
/// Execute only valid rows over the original inputs.
136+
/// Execute only valid rows over the original inputs before filtering.
137137
ValidOnly,
138138
}
139139

0 commit comments

Comments
 (0)