Skip to content

Commit 4edb768

Browse files
committed
Execute tensor L2 norm with RowFn
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent c8c0e86 commit 4edb768

5 files changed

Lines changed: 339 additions & 125 deletions

File tree

‎vortex-tensor/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ version = { workspace = true }
1717
workspace = true
1818

1919
[dependencies]
20-
vortex-array = { workspace = true }
20+
vortex-array = { workspace = true, features = ["unstable_row_fns"] }
2121
vortex-arrow = { workspace = true }
2222
vortex-buffer = { workspace = true }
2323
vortex-compressor = { workspace = true }

‎vortex-tensor/src/scalar_fns/l2_norm.rs‎

Lines changed: 93 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,41 @@
33

44
//! L2 norm expression for tensor-like types.
55
6-
use num_traits::Float;
76
use prost::Message;
87
use vortex_array::ArrayRef;
9-
use vortex_array::ExecutionCtx;
10-
use vortex_array::IntoArray;
11-
use vortex_array::arrays::Constant;
12-
use vortex_array::arrays::ConstantArray;
13-
use vortex_array::arrays::ExtensionArray;
14-
use vortex_array::arrays::PrimitiveArray;
158
use vortex_array::arrays::ScalarFn as ScalarFnArrayEncoding;
169
use vortex_array::arrays::ScalarFnArray;
17-
use vortex_array::arrays::extension::ExtensionArrayExt;
1810
use vortex_array::arrays::scalar_fn::ScalarFnArrayExt;
1911
use vortex_array::arrays::scalar_fn::ScalarFnArrayView;
2012
use vortex_array::arrays::scalar_fn::plugin::ScalarFnArrayParts;
2113
use vortex_array::arrays::scalar_fn::plugin::ScalarFnArrayVTable;
2214
use vortex_array::dtype::DType;
23-
use vortex_array::dtype::NativePType;
24-
use vortex_array::dtype::Nullability;
2515
use vortex_array::dtype::proto::dtype as pb;
26-
use vortex_array::expr::Expression;
27-
use vortex_array::expr::union_child_validities;
2816
use vortex_array::match_each_float_ptype;
29-
use vortex_array::scalar::Scalar;
30-
use vortex_array::scalar_fn::Arity;
31-
use vortex_array::scalar_fn::ChildName;
3217
use vortex_array::scalar_fn::EmptyOptions;
33-
use vortex_array::scalar_fn::ExecutionArgs;
3418
use vortex_array::scalar_fn::ScalarFnId;
35-
use vortex_array::scalar_fn::ScalarFnVTable;
3619
use vortex_array::scalar_fn::ScalarFnVTableExt;
20+
use vortex_array::scalar_fn::unstable::row::InitializedElement;
21+
use vortex_array::scalar_fn::unstable::row::RowFn;
22+
use vortex_array::scalar_fn::unstable::row::RowVisitor;
23+
use vortex_array::scalar_fn::unstable::row::UninitElementSink;
3724
use vortex_array::serde::ArrayChildren;
38-
use vortex_buffer::Buffer;
39-
use vortex_error::VortexExpect;
4025
use vortex_error::VortexResult;
4126
use vortex_error::vortex_err;
4227
use vortex_session::VortexSession;
4328
use vortex_session::registry::CachedId;
4429

45-
use crate::matcher::AnyTensor;
46-
use crate::utils::extract_flat_elements;
47-
use crate::utils::validate_tensor_float_input;
30+
use crate::scalar_fns::row::TensorRow;
31+
use crate::scalar_fns::row::tensor_element_ptype;
32+
use crate::utils::l2_norm_row;
4833

4934
/// L2 norm (Euclidean norm) of a tensor or vector column.
5035
///
5136
/// Computes `||v|| = sqrt(sum(v_i^2))` over the flat backing buffer of each tensor-like type.
5237
///
5338
/// The input must be a tensor-like extension array with a float element type. The output is a float
5439
/// column of the same float type.
55-
#[derive(Clone)]
40+
#[derive(Clone, Debug, Default)]
5641
pub struct L2Norm;
5742

5843
impl L2Norm {
@@ -67,110 +52,41 @@ impl L2Norm {
6752
}
6853
}
6954

70-
impl ScalarFnVTable for L2Norm {
55+
impl RowFn for L2Norm {
7156
type Options = EmptyOptions;
7257

58+
const ARG_NAMES: &'static [&'static str] = &["input"];
59+
const INFALLIBLE: bool = true;
60+
7361
fn id(&self) -> ScalarFnId {
7462
static ID: CachedId = CachedId::new("vortex.tensor.l2_norm");
7563
*ID
7664
}
7765

78-
fn arity(&self, _options: &Self::Options) -> Arity {
79-
Arity::Exact(1)
80-
}
81-
82-
fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
83-
match child_idx {
84-
0 => ChildName::from("input"),
85-
_ => unreachable!("L2Norm must have exactly one child"),
86-
}
87-
}
88-
89-
fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
90-
let input_dtype = &arg_dtypes[0];
91-
let tensor_match = validate_tensor_float_input(input_dtype)?;
92-
let ptype = tensor_match.element_ptype();
93-
94-
let nullability = Nullability::from(input_dtype.is_nullable());
95-
Ok(DType::Primitive(ptype, nullability))
66+
fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
67+
Ok(Some(vec![]))
9668
}
9769

98-
fn execute(
70+
fn deserialize(
9971
&self,
100-
_options: &Self::Options,
101-
args: &dyn ExecutionArgs,
102-
ctx: &mut ExecutionCtx,
103-
) -> VortexResult<ArrayRef> {
104-
let input_ref = args.get(0)?;
105-
let row_count = args.row_count();
106-
107-
let ext = input_ref.dtype().as_extension();
108-
let tensor_match = ext
109-
.metadata_opt::<AnyTensor>()
110-
.vortex_expect("we already validated this in `return_dtype`");
111-
let tensor_flat_size = tensor_match.list_size() as usize;
112-
let element_ptype = tensor_match.element_ptype();
113-
114-
let norm_dtype = DType::Primitive(element_ptype, ext.nullability());
115-
116-
// Optimize for the constant array case.
117-
if let Some(array) = input_ref.as_opt::<Constant>() {
118-
let scalar = array.scalar().as_extension().to_storage_scalar();
119-
120-
let Some(elements) = scalar.as_list().elements() else {
121-
return Ok(ConstantArray::new(Scalar::null(norm_dtype), row_count).into_array());
122-
};
123-
124-
let norm_scalar = match_each_float_ptype!(element_ptype, |T| {
125-
let values: Vec<T> = elements
126-
.iter()
127-
.map(|s| {
128-
s.as_primitive()
129-
.as_::<T>()
130-
.vortex_expect("element was somehow not the correct float")
131-
})
132-
.collect();
133-
let norm = l2_norm_row::<T>(&values);
134-
135-
Scalar::try_new(norm_dtype, Some(norm.into()))
136-
})?;
137-
138-
let norms = ConstantArray::new(norm_scalar, row_count).into_array();
139-
return Ok(norms);
140-
}
141-
142-
let input: ExtensionArray = input_ref.execute(ctx)?;
143-
let validity = input.as_ref().validity()?;
144-
145-
let storage = input.storage_array();
146-
let flat = extract_flat_elements(storage, tensor_flat_size, ctx)?;
147-
148-
match_each_float_ptype!(flat.ptype(), |T| {
149-
let buffer: Buffer<T> = (0..row_count)
150-
.map(|i| l2_norm_row(flat.row::<T>(i)))
151-
.collect();
152-
153-
// SAFETY: The buffer length equals `row_count`, which matches the source validity
154-
// length.
155-
Ok(unsafe { PrimitiveArray::new_unchecked(buffer, validity) }.into_array())
156-
})
72+
_metadata: &[u8],
73+
_session: &VortexSession,
74+
) -> VortexResult<Self::Options> {
75+
Ok(EmptyOptions)
15776
}
15877

159-
fn validity(
78+
fn dispatch<V: RowVisitor>(
16079
&self,
16180
_options: &Self::Options,
162-
expression: &Expression,
163-
) -> VortexResult<Option<Expression>> {
164-
// The result is null if the input tensor is null.
165-
union_child_validities(expression)
166-
}
167-
168-
fn is_strict(&self, _options: &Self::Options) -> bool {
169-
true
170-
}
171-
172-
fn is_infallible(&self, _options: &Self::Options) -> bool {
173-
true
81+
args: &[DType],
82+
visitor: V,
83+
) -> VortexResult<V::VisitResult> {
84+
match_each_float_ptype!(tensor_element_ptype(args)?, |T| {
85+
visitor.visit_into::<(TensorRow<T>,), UninitElementSink<T>, _>((), |(row,), output| {
86+
// SAFETY: `output` is the `UninitElementSink` row supplied for this callback.
87+
unsafe { InitializedElement::write(output, l2_norm_row(row)) }
88+
})
89+
})
17490
}
17591
}
17692

@@ -217,17 +133,6 @@ impl ScalarFnArrayVTable for L2Norm {
217133
}
218134
}
219135

220-
/// Computes the L2 norm (Euclidean norm) of a float slice.
221-
///
222-
/// Returns `sqrt(sum(v_i^2))`. A zero-length or all-zero input produces `0.0`.
223-
fn l2_norm_row<T: Float + NativePType>(v: &[T]) -> T {
224-
let mut sum_sq = T::zero();
225-
for &x in v {
226-
sum_sq = sum_sq + x * x;
227-
}
228-
sum_sq.sqrt()
229-
}
230-
231136
#[cfg(test)]
232137
mod tests {
233138

@@ -258,6 +163,7 @@ mod tests {
258163
use crate::utils::test_helpers::literal_vector_array;
259164
use crate::utils::test_helpers::tensor_array;
260165
use crate::utils::test_helpers::vector_array;
166+
use crate::utils::test_helpers::zero_width_vector_array;
261167

262168
/// Evaluates L2 norm on a tensor/vector array and returns the result as `Vec<f64>`.
263169
fn eval_l2_norm(input: ArrayRef) -> VortexResult<Vec<f64>> {
@@ -267,6 +173,27 @@ mod tests {
267173
Ok(prim.as_slice::<f64>().to_vec())
268174
}
269175

176+
fn eval_l2_norm_f32(input: ArrayRef) -> VortexResult<Vec<f32>> {
177+
let result = L2Norm::try_new(input)?;
178+
let mut ctx = SESSION.create_execution_ctx();
179+
let prim: PrimitiveArray = result.into_array().execute(&mut ctx)?;
180+
Ok(prim.as_slice::<f32>().to_vec())
181+
}
182+
183+
#[test]
184+
fn zero_width_and_empty_inputs() -> VortexResult<()> {
185+
assert_close(
186+
&eval_l2_norm(zero_width_vector_array::<f64>(3)?)?,
187+
&[0.0, 0.0, 0.0],
188+
);
189+
assert!(eval_l2_norm(vector_array(2, &[] as &[f64])?)?.is_empty());
190+
assert_close(
191+
&eval_l2_norm(Vector::constant_array::<f64>(&[], 3)?)?,
192+
&[0.0, 0.0, 0.0],
193+
);
194+
Ok(())
195+
}
196+
270197
#[rstest]
271198
#[case::three_four_five(&[2], &[3.0, 4.0], &[5.0])]
272199
#[case::zero_vector(&[3], &[0.0, 0.0, 0.0], &[0.0])]
@@ -351,6 +278,48 @@ mod tests {
351278
Ok(())
352279
}
353280

281+
#[test]
282+
fn extension_backed_constant_yields_constant_output() -> VortexResult<()> {
283+
let input = Vector::constant_array(&[3.0f64, 4.0], 4)?;
284+
285+
let result = L2Norm::try_new(input)?.into_array();
286+
let mut ctx = SESSION.create_execution_ctx();
287+
let output = result.execute_until::<Constant>(&mut ctx)?;
288+
289+
let constant = output
290+
.as_opt::<Constant>()
291+
.expect("L2Norm over constant-backed extension storage must be constant");
292+
assert_eq!(constant.len(), 4);
293+
let norm = constant
294+
.scalar()
295+
.as_primitive()
296+
.as_::<f64>()
297+
.expect("norm scalar must be a non-null primitive");
298+
assert_eq!(norm.to_bits(), 5.0f64.to_bits());
299+
Ok(())
300+
}
301+
302+
#[test]
303+
fn encoded_constants_match_materialized_rows_bitwise() -> VortexResult<()> {
304+
let row = [f32::MAX, 1.0, -1.0];
305+
let materialized = vector_array(3, &[row, row, row].concat())?;
306+
let storage_constant = Vector::constant_array(&row, 3)?;
307+
let literal_constant = literal_vector_array(&row, 3);
308+
309+
let expected: Vec<_> = eval_l2_norm_f32(materialized)?
310+
.into_iter()
311+
.map(f32::to_bits)
312+
.collect();
313+
for encoded in [storage_constant, literal_constant] {
314+
let actual: Vec<_> = eval_l2_norm_f32(encoded)?
315+
.into_iter()
316+
.map(f32::to_bits)
317+
.collect();
318+
assert_eq!(actual, expected);
319+
}
320+
Ok(())
321+
}
322+
354323
/// A constant input whose scalar is null should short-circuit to a null [`ConstantArray`] of
355324
/// the correct primitive dtype and length.
356325
#[test]

‎vortex-tensor/src/scalar_fns/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ pub mod cosine_similarity;
77
pub mod inner_product;
88
pub mod l2_norm;
99
pub mod l2_normalize;
10+
11+
mod row;

0 commit comments

Comments
 (0)