Yes, I did write this by hand. I actually had the LLM separate most of my writing out into tracking issues!
Right now, functions whose natural implementation operates on one row still have to own much of the machinery for executing that operation over columnar Vortex arrays.
This Epic tracks a RowFn definition that separates those concerns. A scalar function implementor can choose typed row elements and define the computation/operation, while the framework handles batch decoding, dtype validation, constants, null rows, output construction, and more.
Status
Proposed.
There is a working (albeit highly experimental) prototype with primitive, bytes, tensor, and geometry functions (see github or diffshub since there's 10k lines of AI-generated code). The remaining work is to settle the API and execution contracts and then split the implementation into reviewable changes.
Subissues
Goal
A scalar function whose natural implementation operates on one typed row should be able to define that operation without also implementing the surrounding array machinery.
The goals are:
- Define a stable
RowFn API for choosing typed inputs, preparing batch state, computing rows, and building outputs.
- Share validation, batch decoding, constant handling, null propagation, output allocation, and validity handling across row functions.
- Allow crates such as
vortex-tensor and vortex-geo to add row representations without changing vortex-array.
- Preserve encoding-aware shortcuts for functions that have a better answer for a specific encoding.
- Avoid a performance penalty for already-efficient row kernels, while making commonly missed optimizations reusable.
- Support optional row outputs for strict functions that may return null from otherwise valid inputs.
Note that we will focus solely on strict functions. as the semantics around non-strict functions are complicated enough that it's probably not worth extending this already-somewhat-complicated API further.
RowFn is also not intended for columnar or zero-copy kernels (not, list_length), kernels with state shared across rows (like), or heterogeneous variadic kernels.
Motivation
We have a good number of scalar function implementations in the Vortex codebase, and because it is simply a trait implementation of ScalarFnVTable, anyone can add their own scalar function.
However, writing a good implementation is not exactly trivial. Suppose we want to add a scalar function Hypot to Vortex that is similar to hypot. This simply gets distance from the origin to the point (x, y) via sqrt(x^2 + y^2).
Even though this is arguably a "basic" scalar function, there are MANY things that the implementor needs to worry about w.r.t. correctness and performance.
- Depending on what the implementor wants, they might need to implement this for when
x and y are arbitrary floating-point types such as f16, f32, and f64. Maybe they want to accept integers without an explicit cast!
- How should they deal with null values / validity? What should the result of
hypot(null, 5.0) be?
- Depending on how they want their null semantics, they might want to do a bunch of stuff with validity up front before moving onto the compute (perhaps intersect the validity of the 2 inputs).
- This
sqrt(x^2 + y^2) operation is small enough that the implementor would want to ensure auto-vectorization can happen, so they need to make sure there is no branching in hot loops.
- How do they pre-allocate memory correctly?
- If one or both columns of
x and y are ConstantArray, then they definitely want to precompute x^2 and y^2 rather than doing it n times.
Note that in practice we would probably want to decompose hypot() into an expression that does sqrt(x^2 + y^2) as a tree of numeric expressions for better optimizations, but hopefully you get the idea that there are several things that many scalar function implementations have to worry about, even if the function is "simple".
More often than not, the scalar function implementor is only going to worry about a subset of these and leave potential performance optimizations on the table.
So this begs the question: Can scalar functions be defined in terms of their natural operation on a single typed row, without giving up Vortex’s array semantics, extension types, encoding-aware shortcuts, or performance?
RowFn rationale
Every row-oriented scalar function execution follows roughly the same execution pipeline:
- Validate the input types.
- Handle degenerate inputs, such as null constants or entirely constant arguments.
- Decode each input column once.
- Compute anything that is constant/static for the batch.
- Read and compute one row at a time (hopefully in a vectorized manner).
- Build the output and apply its validity.
Without a shared definition, every scalar function has to assemble this pipeline itself.
RowFn makes the owner of each step explicit:
InputElement validates each element dtype and ElementTuple validates arity.
- Null constants and entirely constant inputs are handled automatically.
InputElement::decode prepares each column once, including stride-0 access for constants.
- The prepare step of
visit_prepared_into computes state derived from constant arguments once per batch.
ElementTuple::get reads each row and the function's closure performs the actual computation.
- An
OutputSink builds the result, after which the framework applies the input validity.
A blanket implementation turns every RowFn directly into a ScalarFnVTable, so there is no additional authoring layer between the row definition and the scalar function.
Preliminary performance
The previous shared-VM figures are superseded by the benchmark and codegen follow-up. It records the benchmark machinery, methodology, two-run results, control limitations, and representative generated LLVM IR/assembly in folded sections.
Unresolved questions
Yes, I did write this by hand. I actually had the LLM separate most of my writing out into tracking issues!
Right now, functions whose natural implementation operates on one row still have to own much of the machinery for executing that operation over columnar Vortex arrays.
This Epic tracks a
RowFndefinition that separates those concerns. A scalar function implementor can choose typed row elements and define the computation/operation, while the framework handles batch decoding,dtypevalidation, constants, null rows, output construction, and more.Status
Proposed.
There is a working (albeit highly experimental) prototype with primitive, bytes, tensor, and geometry functions (see github or diffshub since there's 10k lines of AI-generated code). The remaining work is to settle the API and execution contracts and then split the implementation into reviewable changes.
Subissues
RowFnAPI #9129RowFnover Vortex arrays #9130Goal
A scalar function whose natural implementation operates on one typed row should be able to define that operation without also implementing the surrounding array machinery.
The goals are:
RowFnAPI for choosing typed inputs, preparing batch state, computing rows, and building outputs.vortex-tensorandvortex-geoto add row representations without changingvortex-array.Note that we will focus solely on strict functions. as the semantics around non-strict functions are complicated enough that it's probably not worth extending this already-somewhat-complicated API further.
RowFnis also not intended for columnar or zero-copy kernels (not, list_length), kernels with state shared across rows (like), or heterogeneous variadic kernels.Motivation
We have a good number of scalar function implementations in the Vortex codebase, and because it is simply a trait implementation of
ScalarFnVTable, anyone can add their own scalar function.However, writing a good implementation is not exactly trivial. Suppose we want to add a scalar function
Hypotto Vortex that is similar tohypot. This simply gets distance from the origin to the point(x, y)viasqrt(x^2 + y^2).Even though this is arguably a "basic" scalar function, there are MANY things that the implementor needs to worry about w.r.t. correctness and performance.
xandyare arbitrary floating-point types such asf16,f32, andf64. Maybe they want to accept integers without an explicit cast!hypot(null, 5.0)be?sqrt(x^2 + y^2)operation is small enough that the implementor would want to ensure auto-vectorization can happen, so they need to make sure there is no branching in hot loops.xandyareConstantArray, then they definitely want to precomputex^2andy^2rather than doing itntimes.Note that in practice we would probably want to decompose
hypot()into an expression that doessqrt(x^2 + y^2)as a tree of numeric expressions for better optimizations, but hopefully you get the idea that there are several things that many scalar function implementations have to worry about, even if the function is "simple".More often than not, the scalar function implementor is only going to worry about a subset of these and leave potential performance optimizations on the table.
So this begs the question: Can scalar functions be defined in terms of their natural operation on a single typed row, without giving up Vortex’s array semantics, extension types, encoding-aware shortcuts, or performance?
RowFnrationaleEvery row-oriented scalar function execution follows roughly the same execution pipeline:
Without a shared definition, every scalar function has to assemble this pipeline itself.
RowFnmakes the owner of each step explicit:InputElementvalidates each element dtype andElementTuplevalidates arity.InputElement::decodeprepares each column once, including stride-0 access for constants.visit_prepared_intocomputes state derived from constant arguments once per batch.ElementTuple::getreads each row and the function's closure performs the actual computation.OutputSinkbuilds the result, after which the framework applies the input validity.A blanket implementation turns every
RowFndirectly into aScalarFnVTable, so there is no additional authoring layer between the row definition and the scalar function.Preliminary performance
The previous shared-VM figures are superseded by the benchmark and codegen follow-up. It records the benchmark machinery, methodology, two-run results, control limitations, and representative generated LLVM IR/assembly in folded sections.
Unresolved questions
list_sumandvariant_get.InputElement,OutputElement, andOutputSinksupported downstream extension points in the first stable API, or only cross-crate extension points within Vortex?