Skip to content

Commit 6340306

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

5 files changed

Lines changed: 339 additions & 119 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: 96 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,31 @@
99
1010
use prost::Message;
1111
use vortex_array::ArrayRef;
12-
use vortex_array::ExecutionCtx;
13-
use vortex_array::IntoArray;
14-
use vortex_array::arrays::Constant;
15-
use vortex_array::arrays::ConstantArray;
16-
use vortex_array::arrays::ExtensionArray;
17-
use vortex_array::arrays::PrimitiveArray;
1812
use vortex_array::arrays::ScalarFn as ScalarFnArrayEncoding;
1913
use vortex_array::arrays::ScalarFnArray;
20-
use vortex_array::arrays::extension::ExtensionArrayExt;
2114
use vortex_array::arrays::scalar_fn::ScalarFnArrayExt;
2215
use vortex_array::arrays::scalar_fn::ScalarFnArrayView;
2316
use vortex_array::arrays::scalar_fn::plugin::ScalarFnArrayParts;
2417
use vortex_array::arrays::scalar_fn::plugin::ScalarFnArrayVTable;
2518
use vortex_array::dtype::DType;
26-
use vortex_array::dtype::Nullability;
2719
use vortex_array::dtype::proto::dtype as pb;
28-
use vortex_array::expr::Expression;
29-
use vortex_array::expr::union_child_validities;
3020
use vortex_array::match_each_float_ptype;
31-
use vortex_array::scalar::Scalar;
32-
use vortex_array::scalar_fn::Arity;
33-
use vortex_array::scalar_fn::ChildName;
3421
use vortex_array::scalar_fn::EmptyOptions;
35-
use vortex_array::scalar_fn::ExecutionArgs;
3622
use vortex_array::scalar_fn::ScalarFnId;
37-
use vortex_array::scalar_fn::ScalarFnVTable;
3823
use vortex_array::scalar_fn::ScalarFnVTableExt;
24+
use vortex_array::scalar_fn::unstable::row::InitializedElement;
25+
use vortex_array::scalar_fn::unstable::row::RowFn;
26+
use vortex_array::scalar_fn::unstable::row::RowVisitor;
27+
use vortex_array::scalar_fn::unstable::row::UninitElementSink;
3928
use vortex_array::serde::ArrayChildren;
40-
use vortex_buffer::Buffer;
41-
use vortex_error::VortexExpect;
4229
use vortex_error::VortexResult;
4330
use vortex_error::vortex_err;
4431
use vortex_session::VortexSession;
4532
use vortex_session::registry::CachedId;
4633

47-
use crate::matcher::AnyTensor;
4834
use crate::scalar_fns::arithmetic::l2_norm_row;
49-
use crate::utils::extract_flat_elements;
50-
use crate::utils::validate_tensor_float_input;
35+
use crate::scalar_fns::row::TensorRow;
36+
use crate::scalar_fns::row::tensor_element_ptype;
5137

5238
/// L2 norm (Euclidean norm) of a tensor or vector column.
5339
///
@@ -75,118 +61,49 @@ impl L2Norm {
7561
}
7662
}
7763

78-
impl ScalarFnVTable for L2Norm {
64+
impl RowFn for L2Norm {
7965
type Options = EmptyOptions;
8066

67+
const ARG_NAMES: &'static [&'static str] = &["input"];
68+
const INFALLIBLE: bool = true;
69+
8170
fn id(&self) -> ScalarFnId {
8271
static ID: CachedId = CachedId::new("vortex.tensor.l2_norm");
8372
*ID
8473
}
8574

86-
fn arity(&self, _options: &Self::Options) -> Arity {
87-
Arity::Exact(1)
88-
}
89-
90-
fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
91-
match child_idx {
92-
0 => ChildName::from("input"),
93-
_ => unreachable!("L2Norm must have exactly one child"),
94-
}
95-
}
96-
97-
fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
98-
let input_dtype = &arg_dtypes[0];
99-
let tensor_match = validate_tensor_float_input(input_dtype)?;
100-
let ptype = tensor_match.element_ptype();
101-
102-
let nullability = Nullability::from(input_dtype.is_nullable());
103-
Ok(DType::Primitive(ptype, nullability))
75+
fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
76+
Ok(Some(vec![]))
10477
}
10578

106-
fn execute(
79+
fn deserialize(
10780
&self,
108-
_options: &Self::Options,
109-
args: &dyn ExecutionArgs,
110-
ctx: &mut ExecutionCtx,
111-
) -> VortexResult<ArrayRef> {
112-
let input_ref = args.get(0)?;
113-
let row_count = args.row_count();
114-
115-
let ext = input_ref.dtype().as_extension();
116-
let tensor_match = ext
117-
.metadata_opt::<AnyTensor>()
118-
.vortex_expect("L2Norm::return_dtype validated the input tensor metadata");
119-
let tensor_flat_size = tensor_match.list_size() as usize;
120-
let element_ptype = tensor_match.element_ptype();
121-
122-
let norm_dtype = DType::Primitive(element_ptype, ext.nullability());
123-
124-
// Optimize for the constant array case.
125-
if let Some(array) = input_ref.as_opt::<Constant>() {
126-
let scalar = array.scalar().as_extension().to_storage_scalar();
127-
128-
let Some(elements) = scalar.as_list().elements() else {
129-
return Ok(ConstantArray::new(Scalar::null(norm_dtype), row_count).into_array());
130-
};
131-
132-
let norm_scalar = match_each_float_ptype!(element_ptype, |T| {
133-
let values: Vec<T> = elements
134-
.iter()
135-
.map(|element| {
136-
element
137-
.as_primitive()
138-
.as_::<T>()
139-
.vortex_expect("L2Norm::return_dtype validated the float element type")
140-
})
141-
.collect();
142-
let norm = l2_norm_row::<T>(&values);
143-
144-
Scalar::try_new(norm_dtype, Some(norm.into()))
145-
})?;
146-
147-
let norms = ConstantArray::new(norm_scalar, row_count).into_array();
148-
return Ok(norms);
149-
}
150-
151-
let input: ExtensionArray = input_ref.execute(ctx)?;
152-
let validity = input.as_ref().validity()?;
153-
154-
let storage = input.storage_array();
155-
let flat = extract_flat_elements(storage, tensor_flat_size, ctx)?;
156-
157-
match_each_float_ptype!(flat.ptype(), |T| {
158-
let buffer: Buffer<T> = (0..row_count)
159-
.map(|row_index| l2_norm_row(flat.row::<T>(row_index)))
160-
.collect();
161-
162-
// SAFETY: The buffer length equals `row_count`, which matches the source validity
163-
// length.
164-
Ok(unsafe { PrimitiveArray::new_unchecked(buffer, validity) }.into_array())
165-
})
81+
_metadata: &[u8],
82+
_session: &VortexSession,
83+
) -> VortexResult<Self::Options> {
84+
Ok(EmptyOptions)
16685
}
16786

168-
fn validity(
87+
fn dispatch<V: RowVisitor>(
16988
&self,
17089
_options: &Self::Options,
171-
expression: &Expression,
172-
) -> VortexResult<Option<Expression>> {
173-
union_child_validities(expression)
174-
}
175-
176-
fn is_strict(&self, _options: &Self::Options) -> bool {
177-
true
178-
}
179-
180-
fn is_infallible(&self, _options: &Self::Options) -> bool {
181-
true
90+
args: &[DType],
91+
visitor: V,
92+
) -> VortexResult<V::VisitResult> {
93+
match_each_float_ptype!(tensor_element_ptype(args)?, |T| {
94+
visitor.visit_into::<(TensorRow<T>,), UninitElementSink<T>, _>((), |(row,), output| {
95+
// SAFETY: `output` is the `UninitElementSink` row supplied for this callback.
96+
unsafe { InitializedElement::write(output, l2_norm_row(row)) }
97+
})
98+
})
18299
}
183100
}
184101

185102
/// Metadata for a serialized [`L2Norm`] array: the single `input` child's [`DType`], which carries
186103
/// the extension type (`FixedShapeTensor` vs `Vector`), dimension, and nullability that are not
187104
/// recoverable from the parent's primitive-float output.
188105
#[derive(Clone, prost::Message)]
189-
pub(super) struct L2NormMetadata {
106+
struct L2NormMetadata {
190107
#[prost(message, optional, tag = "1")]
191108
input_dtype: Option<pb::DType>,
192109
}
@@ -240,6 +157,7 @@ mod tests {
240157
use vortex_array::arrays::PrimitiveArray;
241158
use vortex_array::arrays::scalar_fn::plugin::ScalarFnArrayPlugin;
242159
use vortex_array::dtype::DType;
160+
use vortex_array::dtype::NativePType;
243161
use vortex_array::dtype::Nullability;
244162
use vortex_array::dtype::PType;
245163
use vortex_array::dtype::extension::ExtDType;
@@ -254,13 +172,29 @@ mod tests {
254172
use crate::utils::test_helpers::literal_vector_array;
255173
use crate::utils::test_helpers::tensor_array;
256174
use crate::utils::test_helpers::vector_array;
175+
use crate::utils::test_helpers::zero_width_vector_array;
257176

258-
/// Evaluates L2 norm on a tensor/vector array and returns the result as `Vec<f64>`.
259-
fn eval_l2_norm(input: ArrayRef) -> VortexResult<Vec<f64>> {
177+
fn evaluate_l2_norm<T: NativePType>(input: ArrayRef) -> VortexResult<Vec<T>> {
260178
let result = L2Norm::try_new(input)?;
261179
let mut ctx = SESSION.create_execution_ctx();
262-
let prim: PrimitiveArray = result.into_array().execute(&mut ctx)?;
263-
Ok(prim.as_slice::<f64>().to_vec())
180+
let output: PrimitiveArray = result.into_array().execute(&mut ctx)?;
181+
182+
Ok(output.as_slice::<T>().to_vec())
183+
}
184+
185+
#[test]
186+
fn test_zero_width_and_empty_inputs() -> VortexResult<()> {
187+
assert_close(
188+
&evaluate_l2_norm(zero_width_vector_array::<f64>(3)?)?,
189+
&[0.0, 0.0, 0.0],
190+
);
191+
assert!(evaluate_l2_norm::<f64>(vector_array::<f64>(2, &[])?)?.is_empty());
192+
assert_close(
193+
&evaluate_l2_norm(Vector::constant_array::<f64>(&[], 3)?)?,
194+
&[0.0, 0.0, 0.0],
195+
);
196+
197+
Ok(())
264198
}
265199

266200
#[rstest]
@@ -274,7 +208,7 @@ mod tests {
274208
#[case] expected: &[f64],
275209
) -> VortexResult<()> {
276210
let arr = tensor_array(shape, elements)?;
277-
assert_close(&eval_l2_norm(arr)?, expected);
211+
assert_close(&evaluate_l2_norm(arr)?, expected);
278212
Ok(())
279213
}
280214

@@ -288,7 +222,7 @@ mod tests {
288222
1.0, 1.0, 1.0, // norm = sqrt(3)
289223
],
290224
)?;
291-
assert_close(&eval_l2_norm(arr)?, &[5.0, 0.0, 3.0_f64.sqrt()]);
225+
assert_close(&evaluate_l2_norm(arr)?, &[5.0, 0.0, 3.0_f64.sqrt()]);
292226
Ok(())
293227
}
294228

@@ -301,7 +235,7 @@ mod tests {
301235
3.0, 4.0, 0.0, // norm = 5.0
302236
],
303237
)?;
304-
assert_close(&eval_l2_norm(arr)?, &[1.0, 5.0]);
238+
assert_close(&evaluate_l2_norm(arr)?, &[1.0, 5.0]);
305239
Ok(())
306240
}
307241

@@ -347,6 +281,50 @@ mod tests {
347281
Ok(())
348282
}
349283

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! [`L2Normalize`]: l2_normalize::L2Normalize
1414
1515
mod arithmetic;
16+
mod row;
1617

1718
pub mod cosine_similarity;
1819
pub mod inner_product;

0 commit comments

Comments
 (0)