Skip to content

Commit 5cf73d7

Browse files
committed
Preserve RowFn input short circuits
Classify all inputs before decoding so a null literal can skip an earlier fallible expression or device transfer. Keep validity conjunction lazy and preserve valid-only decoding. Add regression coverage for invalid payloads, lazy expressions, and device-backed inputs. Borrow contiguous argument dtypes without adding a primitive-specific decoder hook or packing policy to the generic framework. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 45af56f commit 5cf73d7

4 files changed

Lines changed: 255 additions & 14 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! columns collected from a general [`ExecutionArgs`](crate::scalar_fn::ExecutionArgs).
99
1010
use crate::ArrayRef;
11+
use crate::dtype::DType;
1112

1213
/// The same-length input columns of one row-function execution, borrowed by index.
1314
///
@@ -22,6 +23,13 @@ pub trait RowInputs {
2223

2324
/// Return input `index`. `index` **must** be less than [`num_inputs`](Self::num_inputs).
2425
fn input(&self, index: usize) -> &ArrayRef;
26+
27+
/// Return the input dtypes in order, if this already holds them contiguously.
28+
///
29+
/// Batch execution otherwise clones them into a collected list.
30+
fn dtypes(&self) -> Option<&[DType]> {
31+
None
32+
}
2533
}
2634

2735
/// [`RowInputs`] over a slice of collected columns.

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//! Input lengths, constants, and validity are properties of the batch. Planning derives the output
77
//! dtype and execution policy from the row signature selected by dispatch.
88
9-
use smallvec::SmallVec;
109
use vortex_error::VortexResult;
1110
use vortex_error::vortex_ensure;
1211
use vortex_error::vortex_ensure_eq;
@@ -27,7 +26,7 @@ use crate::validity::Validity;
2726

2827
impl BatchProbe {
2928
/// Inspect the borrowed inputs: check their row counts, classify batch constants, and conjoin
30-
/// their validity. Return the probe with the input dtypes.
29+
/// their validity.
3130
///
3231
/// This **must not** be used for a nullary function. With no input columns, there is no
3332
/// validity to propagate and the all-constant check would pass vacuously.
@@ -36,7 +35,7 @@ impl BatchProbe {
3635
pub(crate) fn new<I: RowInputs + ?Sized>(
3736
id: impl Fn() -> ScalarFnId,
3837
inputs: &I,
39-
) -> VortexResult<(Self, SmallVec<[DType; 4]>)> {
38+
) -> VortexResult<Self> {
4039
let row_count = inputs.row_count();
4140
let num_inputs = inputs.num_inputs();
4241
vortex_ensure!(
@@ -45,7 +44,6 @@ impl BatchProbe {
4544
id(),
4645
);
4746

48-
let mut arg_dtypes: SmallVec<[DType; 4]> = SmallVec::with_capacity(num_inputs);
4947
let mut validity = Validity::NonNullable;
5048
let mut const_mask = 0u32;
5149
let mut has_null_const = false;
@@ -71,20 +69,16 @@ impl BatchProbe {
7169
nullability = Nullability::Nullable;
7270
validity = validity.and(input.validity()?)?;
7371
}
74-
arg_dtypes.push(input.dtype().clone());
7572
}
7673

77-
let probe = Self {
74+
Ok(Self {
7875
row_count,
7976
validity,
8077
const_mask,
8178
has_null_const,
8279
nullability,
83-
};
84-
85-
Ok((probe, arg_dtypes))
80+
})
8681
}
87-
8882
/// Return the output validity of a _direct_ batch, or return the probe unchanged for any other
8983
/// batch.
9084
///

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

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::any::Any;
45
use std::mem::MaybeUninit;
6+
use std::ops::Range;
57
use std::sync::Arc;
68
use std::sync::LazyLock;
79
use std::sync::atomic::AtomicUsize;
810
use std::sync::atomic::Ordering;
911

12+
use futures::future::BoxFuture;
1013
use rstest::rstest;
14+
use vortex_buffer::Alignment;
1115
use vortex_buffer::Buffer;
1216
use vortex_buffer::BufferAllocatorRef;
1317
use vortex_buffer::BufferMut;
18+
use vortex_buffer::ByteBuffer;
1419
use vortex_error::VortexResult;
1520
use vortex_error::vortex_bail;
1621
use vortex_error::vortex_ensure;
@@ -28,11 +33,13 @@ use crate::arrays::Constant;
2833
use crate::arrays::ConstantArray;
2934
use crate::arrays::ExtensionArray;
3035
use crate::arrays::FixedSizeListArray;
36+
use crate::arrays::MaskedArray;
3137
use crate::arrays::PrimitiveArray;
3238
use crate::arrays::VarBinViewArray;
3339
use crate::arrays::varbinview::BinaryView;
3440
use crate::assert_arrays_eq;
3541
use crate::buffer::BufferHandle;
42+
use crate::buffer::DeviceBuffer;
3643
use crate::dtype::DType;
3744
use crate::dtype::NativePType;
3845
use crate::dtype::Nullability;
@@ -44,6 +51,8 @@ use crate::scalar::Scalar;
4451
use crate::scalar_fn::EmptyOptions;
4552
use crate::scalar_fn::ScalarFnId;
4653
use crate::scalar_fn::VecExecutionArgs;
54+
use crate::scalar_fn::fns::binary::Binary;
55+
use crate::scalar_fn::fns::operators::Operator;
4756
use crate::scalar_fn::unstable::row::FixedSizeListSink;
4857
use crate::scalar_fn::unstable::row::InitializedRow;
4958
use crate::scalar_fn::unstable::row::InputElement;
@@ -61,6 +70,70 @@ use crate::validity::Validity;
6170

6271
static SESSION: LazyLock<VortexSession> = LazyLock::new(VortexSession::empty);
6372

73+
#[derive(Clone)]
74+
struct FilteredBoolean;
75+
76+
impl RowFn for FilteredBoolean {
77+
type Options = EmptyOptions;
78+
79+
const ARG_NAMES: &'static [&'static str] = &["lhs", "rhs"];
80+
const INFALLIBLE: bool = true;
81+
82+
fn id(&self) -> ScalarFnId {
83+
static ID: CachedId = CachedId::new("test.filtered_boolean");
84+
*ID
85+
}
86+
87+
fn dispatch<V: RowVisitor>(
88+
&self,
89+
_options: &Self::Options,
90+
_args: &[DType],
91+
visitor: V,
92+
) -> VortexResult<V::VisitResult> {
93+
visitor.visit_bool::<(FilterOnlyI64, i64), false>(|(lhs, rhs)| lhs > rhs)
94+
}
95+
}
96+
97+
#[derive(Debug, PartialEq, Eq, Hash)]
98+
struct UnreadableDeviceBuffer {
99+
len: usize,
100+
alignment: Alignment,
101+
}
102+
103+
impl DeviceBuffer for UnreadableDeviceBuffer {
104+
fn as_any(&self) -> &dyn Any {
105+
self
106+
}
107+
108+
fn len(&self) -> usize {
109+
self.len
110+
}
111+
112+
fn alignment(&self) -> Alignment {
113+
self.alignment
114+
}
115+
116+
fn copy_to_host_sync(&self, _alignment: Alignment) -> VortexResult<ByteBuffer> {
117+
vortex_bail!("execution must not read the device buffer")
118+
}
119+
120+
fn copy_to_host(
121+
&self,
122+
_alignment: Alignment,
123+
) -> VortexResult<BoxFuture<'static, VortexResult<ByteBuffer>>> {
124+
vortex_bail!("execution must not read the device buffer")
125+
}
126+
127+
fn slice(&self, _range: Range<usize>) -> Arc<dyn DeviceBuffer> {
128+
panic!("execution must not slice the device buffer")
129+
}
130+
131+
fn aligned(self: Arc<Self>, alignment: Alignment) -> VortexResult<Arc<dyn DeviceBuffer>> {
132+
assert_eq!(alignment, self.alignment());
133+
Ok(self)
134+
}
135+
}
136+
64137
#[derive(Clone, Default)]
65138
struct DeferredAdd {
66139
/// The number of preparations across the dense attempt and any valid-row retry.
@@ -807,6 +880,64 @@ fn test_bool_output_handles_partial_constants() -> VortexResult<()> {
807880
assert_bool_output_handles_partial_constants::<true>()
808881
}
809882

883+
#[test]
884+
fn test_bool_output_handles_null_literal_constant() -> VortexResult<()> {
885+
let varying = PrimitiveArray::from_iter(0_i64..65).into_array();
886+
let null = Scalar::null(DType::Primitive(i64::PTYPE, Nullability::Nullable));
887+
let constant = ConstantArray::new(null, 65).into_array();
888+
let args = VecExecutionArgs::new(vec![varying, constant], 65);
889+
let mut ctx = SESSION.create_execution_ctx();
890+
891+
let actual = execute_rows(&PackedGreaterThan::<true>, &EmptyOptions, &args, &mut ctx)?;
892+
let expected = BoolArray::from_iter(std::iter::repeat_n(None::<bool>, 65)).into_array();
893+
894+
assert_arrays_eq!(&actual, &expected, &mut ctx);
895+
Ok(())
896+
}
897+
898+
#[test]
899+
fn test_bool_output_handles_masked_constant() -> VortexResult<()> {
900+
let mut ctx = SESSION.create_execution_ctx();
901+
let varying = PrimitiveArray::from_iter(0_i64..65).into_array();
902+
let valid: Vec<bool> = (0..65).map(|index| index % 3 != 0).collect();
903+
let constant = MaskedArray::try_new(
904+
ConstantArray::new(32_i64, 65).into_array(),
905+
Validity::from_iter(valid.iter().copied()),
906+
&mut ctx,
907+
)?
908+
.into_array();
909+
let args = VecExecutionArgs::new(vec![varying, constant], 65);
910+
911+
let actual = execute_rows(&PackedGreaterThan::<true>, &EmptyOptions, &args, &mut ctx)?;
912+
let expected = BoolArray::from_iter(
913+
(0_i64..65)
914+
.zip(valid)
915+
.map(|(value, valid)| valid.then_some(value > 32)),
916+
)
917+
.into_array();
918+
919+
assert_arrays_eq!(&actual, &expected, &mut ctx);
920+
Ok(())
921+
}
922+
923+
#[test]
924+
fn test_bool_output_rejects_wrong_row_count() {
925+
let short = PrimitiveArray::from_iter(0_i64..64).into_array();
926+
let long = PrimitiveArray::from_iter(0_i64..65).into_array();
927+
let args = VecExecutionArgs::new(vec![long, short], 65);
928+
let mut ctx = SESSION.create_execution_ctx();
929+
930+
let error = execute_rows(&PackedGreaterThan::<true>, &EmptyOptions, &args, &mut ctx)
931+
.expect_err("a short input must fail");
932+
933+
assert!(
934+
error
935+
.to_string()
936+
.contains("the test.packed_greater_than input 1 has the wrong row count"),
937+
"{error}",
938+
);
939+
}
940+
810941
#[test]
811942
fn test_bool_output_handles_all_constant_input() -> VortexResult<()> {
812943
let input = ConstantArray::new(7_i64, 65).into_array();
@@ -1539,3 +1670,99 @@ fn constant_output_retains_execution_allocator_payload() -> VortexResult<()> {
15391670
tracker.assert_owns(value.inner().as_slice());
15401671
Ok(())
15411672
}
1673+
1674+
#[test]
1675+
fn valid_only_bool_skips_invalid_decode_payloads() -> VortexResult<()> {
1676+
let lhs = PrimitiveArray::new(vec![i64::MIN, 7], Validity::from_iter([false, true]));
1677+
let rhs = ConstantArray::new(0_i64, 2);
1678+
let args = VecExecutionArgs::new(vec![lhs.into_array(), rhs.into_array()], 2);
1679+
let mut ctx = SESSION.create_execution_ctx();
1680+
1681+
let actual = execute_rows(&FilteredBoolean, &EmptyOptions, &args, &mut ctx)?;
1682+
assert_arrays_eq!(actual, BoolArray::from_iter([None, Some(true)]), &mut ctx);
1683+
Ok(())
1684+
}
1685+
1686+
#[test]
1687+
fn null_literal_skips_earlier_decode() -> VortexResult<()> {
1688+
let lhs = PrimitiveArray::from_iter([i64::MIN, 7]);
1689+
let rhs = ConstantArray::new(
1690+
Scalar::null(DType::Primitive(i64::PTYPE, Nullability::Nullable)),
1691+
2,
1692+
);
1693+
let args = VecExecutionArgs::new(vec![lhs.into_array(), rhs.into_array()], 2);
1694+
let mut ctx = SESSION.create_execution_ctx();
1695+
1696+
let actual = execute_rows(&FilteredBoolean, &EmptyOptions, &args, &mut ctx)?;
1697+
assert_arrays_eq!(actual, BoolArray::from_iter([None::<bool>, None]), &mut ctx);
1698+
Ok(())
1699+
}
1700+
1701+
#[test]
1702+
fn null_literal_skips_lazy_input_execution() -> VortexResult<()> {
1703+
let lhs = Binary::try_new(
1704+
PrimitiveArray::from_iter([1_i64, 2]).into_array(),
1705+
ConstantArray::new(0_i64, 2).into_array(),
1706+
Operator::Div,
1707+
)?
1708+
.into_array();
1709+
let rhs = ConstantArray::new(
1710+
Scalar::null(DType::Primitive(i64::PTYPE, Nullability::Nullable)),
1711+
2,
1712+
);
1713+
let args = VecExecutionArgs::new(vec![lhs, rhs.into_array()], 2);
1714+
let mut ctx = SESSION.create_execution_ctx();
1715+
1716+
let actual = execute_rows(&PackedGreaterThan::<false>, &EmptyOptions, &args, &mut ctx)?;
1717+
assert_arrays_eq!(actual, BoolArray::from_iter([None::<bool>, None]), &mut ctx);
1718+
Ok(())
1719+
}
1720+
1721+
#[test]
1722+
fn device_validity_stays_lazy() -> VortexResult<()> {
1723+
let validity = BoolArray::try_new_from_handle(
1724+
BufferHandle::new_device(Arc::new(UnreadableDeviceBuffer {
1725+
len: 1,
1726+
alignment: Alignment::of::<u8>(),
1727+
})),
1728+
0,
1729+
2,
1730+
Validity::NonNullable,
1731+
)?
1732+
.into_array();
1733+
let lhs = PrimitiveArray::new(vec![1_i64, 2], Validity::Array(validity.clone()));
1734+
let rhs = PrimitiveArray::new(vec![3_i64, 4], Validity::Array(validity));
1735+
let args = VecExecutionArgs::new(vec![lhs.into_array(), rhs.into_array()], 2);
1736+
let mut ctx = SESSION.create_execution_ctx();
1737+
1738+
let actual = execute_rows(&DeferredAdd::default(), &EmptyOptions, &args, &mut ctx)?;
1739+
assert_eq!(actual.len(), 2);
1740+
assert_eq!(
1741+
actual.dtype(),
1742+
&DType::Primitive(i64::PTYPE, Nullability::Nullable)
1743+
);
1744+
assert!(matches!(actual.validity()?, Validity::Array(_)));
1745+
Ok(())
1746+
}
1747+
1748+
#[test]
1749+
fn null_literal_skips_device_input_transfer() -> VortexResult<()> {
1750+
let input = PrimitiveArray::from_buffer_handle(
1751+
BufferHandle::new_device(Arc::new(UnreadableDeviceBuffer {
1752+
len: 2 * size_of::<i64>(),
1753+
alignment: Alignment::of::<i64>(),
1754+
})),
1755+
i64::PTYPE,
1756+
Validity::NonNullable,
1757+
);
1758+
let null = ConstantArray::new(
1759+
Scalar::null(DType::Primitive(i64::PTYPE, Nullability::Nullable)),
1760+
2,
1761+
);
1762+
let args = VecExecutionArgs::new(vec![input.into_array(), null.into_array()], 2);
1763+
let mut ctx = SESSION.create_execution_ctx();
1764+
1765+
let actual = execute_rows(&PackedGreaterThan::<false>, &EmptyOptions, &args, &mut ctx)?;
1766+
assert_arrays_eq!(actual, BoolArray::from_iter([None::<bool>, None]), &mut ctx);
1767+
Ok(())
1768+
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,32 @@ pub(crate) fn execute_row_inputs<F: RowFn, I: RowInputs + ?Sized>(
153153
return execute_nullary_rows(function, options, inputs.row_count(), ctx);
154154
}
155155

156-
let (probe, arg_dtypes) = BatchProbe::new(|| RowFn::id(function), inputs)?;
156+
// Classify every input before decoding. A later null literal can skip an earlier fallible
157+
// expression or device transfer.
158+
let probe = BatchProbe::new(|| RowFn::id(function), inputs)?;
159+
let collected: SmallVec<[DType; 4]>;
160+
let arg_dtypes = match inputs.dtypes() {
161+
Some(dtypes) => dtypes,
162+
None => {
163+
collected = (0..inputs.num_inputs())
164+
.map(|index| inputs.input(index).dtype().clone())
165+
.collect();
166+
&collected
167+
}
168+
};
157169

158170
// Plan and execute in one dispatch. Only a partially valid batch that needs dense retry or
159171
// valid-row execution dispatches again.
160172
let (batch, plan) = match function.dispatch(
161173
options,
162-
&arg_dtypes,
163-
ExecuteFused::<F, I>::new(function, inputs, probe, &arg_dtypes, ctx),
174+
arg_dtypes,
175+
ExecuteFused::<F, I>::new(function, inputs, probe, arg_dtypes, ctx),
164176
)? {
165177
Fused::Done(values) => return Ok(values),
166178
Fused::Partial(batch, plan) => (batch, plan),
167179
};
168180

169-
batch.with_plan(&arg_dtypes, plan).execute_partially_valid(
181+
batch.with_plan(arg_dtypes, plan).execute_partially_valid(
170182
|args, validity, ctx| execute_row_kernel(function, options, args, validity, ctx),
171183
|args, ctx| execute_dense_attempt(function, options, args, ctx),
172184
|args, valid, ctx| try_execute_valid_rows(function, options, args, valid, ctx),

0 commit comments

Comments
 (0)