feat: Lambda function support from DataFusion, illustrated with array_filter - #4744
kazantsev-maksim wants to merge 118 commits into
Conversation
This reverts commit 768b3e9.
Moving lambda evaluation into DataFusion instead of paying a JNI call per batch is clearly the right direction, and the design here is thoughtful: resolving Four things. The configs are read once per JVM, not per session case class CometHighOrderFunction[T <: HigherOrderFunction](name: String)
extends CometExpressionSerde[T] {
private val nativeHofEnabled = CometConf.COMET_EXEC_HIGHER_ORDER_FUNCTION_NATIVE_ENABLED.get()
private val codegenEnabled = CometConf.COMET_SCALA_UDF_CODEGEN_ENABLED.get()
Other serdes read configs inside Defaulting the native path to A new native lambda execution path with a scope stack, exprId resolution, and projection compaction, enabled by default in its first release. Given how much new machinery this is, would The module doc no longer matches
Since No performance number The stated motivation is removing a per-batch JNI call. What does that buy for |
|
Posting benchmark numbers for this PR from the suite I mentioned on the Setup: Spark 4.1.3 on a single 28-core host with local NVMe. 24 Results (Parquet, speedup vs vanilla Spark, 24 queries):
The two queries that lose to vanilla on
A few non-lambda extended queries regress on the PR head (e14 +54%, Raw records (per-query medians, native/fallback share, parity flags) |
# Conflicts: # native/core/src/execution/mod.rs # native/core/src/execution/planner.rs
Removed LambdaParamsCaptureThe This workaround became unnecessary after apache/datafusion#24162 landed in DataFusion 55: Verified: three levels of nesting, an inner HOF whose value argument is an outer lambda variable, sibling nested HOFs, an inner lambda referencing an outer variable, and multi-param lambdas with unused parameters all match Spark. Benchmark resultsSimple benchmark result (Apple M1 Pro, OpenJDK 17.0.19, 2 iterations, single run on final code; dispatch-path selection verified via serde logging):
The native path is 3.6-8.0x faster than vanilla Spark and 2.1-8.2x faster than the JVM codegen dispatch path across all scenarios. The codegen dispatch path (running Spark's own lambda evaluation inside the Comet kernel) is on par with or slower than vanilla Spark on lambda-heavy queries (chained filters: 0.7x), confirming the per-batch JNI and row-wise evaluation overhead this PR removes. @comphead @andygrove Could you please take another look? |
# Conflicts: # native/core/src/execution/planner.rs
Which issue does this PR close?
Rationale for this change
Running higher-order functions through JVM codegen is expensive: each batch incurs a JNI call into Spark's own implementation. Moving the lambda evaluation into the native DataFusion engine removes that overhead and brings the plan closer to fully native execution.
What changes are included in this PR?
Protobuf (native/proto/src/proto/expr.proto) - Added three new messages: HigherOrderFunc (function name + value arguments + lambdas), LambdaFunction (body + arguments), and NamedLambdaVariable (name, type, nullable, expr_id). Added high_order_func (71) and named_lambda_variable (72) fields to Expr.
Lambda Infrastructure & Scope Management - new lambda module: Introduced native/core/src/execution/lambda.rs to manage nested lambda variable scopes. This ensures that NamedLambdaVariables are correctly resolved by their Spark exprId, preventing name shadowing or column collisions. Optimizer Anchoring: Implemented LambdaParamsCapture (with a helper factory pin_unused_params). This is a critical mechanism to prevent DataFusion's optimizer from pruning "unused" lambda parameters. Since the runtime expects a specific batch structure, this wrapper "anchors" the parameters in the expression tree to maintain index consistency with the physical plan.
Physical Planner Enhancements - HOF Planning: Extended PhysicalPlanner to support HigherOrderFunc expressions. It now includes logic to: Plan input value expressions. Query the UDF contract to resolve lambda parameter field types. Recursive plan the lambda body under the scope of created parameters.
Variable Resolution: Added support for mapping NamedLambdaVariable protobuf definitions to physical LambdaVariable expressions, correctly binding them to the resolved indices in the lambda parameter schema.
Infrastructure & Helpers - UDF Registration Helper: Added create_comet_hof_func in a new module comet_high_order_funcs.rs to simplify fetching supported HOFs from the DataFusion FunctionRegistry.
Module Exposure: Updated native/core/src/execution/mod.rs to expose the new lambda-related modules to the rest of the core crate.
Spark — serialization (CometHighOrderFunction.scala, QueryPlanSerde.scala, arrays.scala). New generic serializer CometHighOrderFunction[T] that converts a Spark HigherOrderFunction (along with LambdaFunction and NamedLambdaVariable) into protobuf. CometArrayFilter now extends this serializer: when spark.comet.exec.scalaUDF.codegen.enabled is disabled it takes the new native path, otherwise the old behavior is preserved (including the fast-path for array_compact).
How are these changes tested?