andygrove commented on code in PR #5692:
URL: https://github.com/apache/datafusion-comet/pull/5692#discussion_r3938192745


##########
spark/src/main/scala/org/apache/comet/serde/statics.scala:
##########
@@ -78,16 +95,38 @@ object CometStaticInvoke extends 
CometExpressionSerde[StaticInvoke] {
       case Some(handler) =>
         handler.convert(expr, inputs, binding)
       case None =>
-        // Every Iceberg system function is named `invoke`, so name the 
declaring class too.
-        withFallbackReason(
-          expr,
-          s"Static invoke expression: ${expr.functionName} is not supported " +
-            s"(declared on ${expr.staticObject.getName})")
-        None
+        // Nothing in the allowlist covers this lowering, so run Spark's own 
implementation inside
+        // the Comet pipeline rather than failing the whole operator back to 
Spark.
+        // `StaticInvoke.doGenCode` emits a static method call, so the kernel 
matches Spark by
+        // construction. Spark 4.x keeps lowering more `RuntimeReplaceable` 
functions this way
+        // (`encode`, `is_valid_utf8`, the `TIME` family, ...) and `lpad` / 
`rpad` on binary has
+        // lowered to `StaticInvoke(ByteArray, ...)` since Spark 3.4.
+        //
+        // The encoder and deserializer trees that make up most `StaticInvoke` 
usage in typed
+        // Dataset operations are unaffected: their arguments are 
`ObjectType`, which
+        // `CometBatchKernelCodegen.isSupportedDataType` rejects, so the 
dispatcher declines them
+        // and they fall back exactly as before.
+        CometStaticInvokeCodegenDispatch.convert(expr, inputs, binding).orElse 
{

Review Comment:
   Added `CometCodegenDispatchBenchmark` in 9f8fade and ran it.
   
   Three arms per case: codegen dispatch, 
`spark.comet.exec.scalaUDF.codegen.enabled=false` (which is exactly the 
operator fallback that shipped before this PR), and Comet off as a reference. 
Same corpus for all three, at 1024 rows (sub-batch) and 1,048,576 rows (~128 
batches). All three arms are run and their rows compared before anything is 
timed, and every table repeats its baseline at the end so the noise floor sits 
next to the arms rather than in a footnote.
   
   Spark 4.1.3, OpenJDK 17.0.10, Scala 2.13.17, `local[1]`, Apple M3 Max on 
macOS 26.6.2, `spark.comet.batchSize=8192`, whole-stage codegen on. Warmup is 
Spark's `Benchmark` default: 2s untimed per case, then at least two iterations 
and at least 2s of timed iterations. Best times in ms at 1,048,576 rows:
   
   | case | dispatch off (pre-PR) | codegen dispatch | Comet off | baseline 
repeated |
   | --- | --- | --- | --- | --- |
   | `lpad(binary)` | 51 | 61 | 71 | 53 |
   | `rpad(binary)` | 64 | 61 | 79 | 67 |
   | `encode(utf-8)` | 79 | 82 | 99 | 79 |
   | `to_binary(utf-8)` | 76 | 79 | 97 | 76 |
   | `to_time(fmt)` | 261 | 261 | 274 | 260 |
   | mixed projection | 137 | 101 | 226 | 141 |
   | group by dispatch | 73 | 65 | 61 | 73 |
   
   The last column is the same work as the first, so ±4ms is noise here.
   
   Where the projection is nothing but the one dispatched call, dispatch runs 
between parity and about 20% slower than letting the projection fall back. 
`lpad` is the worst of them at 51 → 61ms; `rpad` and `to_time` are parity; 
`encode` and `to_binary` are ~4% slower. That is the bridge cost you were 
asking about and it is real — for a bare `SELECT f(col) FROM t` there is 
nothing else in the projection for staying in Comet to save, so the transport 
and the Arrow output are pure addition.
   
   It goes the other way as soon as the plan has anything else in it. The mixed 
projection is `length(c_str), c_long + 1, substring(c_str, 1, 4), lpad(c_bin, 
24, c_pad)`, and dispatch is 1.4x there (137 → 101ms) because the three native 
expressions no longer follow the fourth out to Spark. The grouped case is 
`count(*)` over `lpad(c_pad, 8, c_pad)`, 1.1x (73 → 65ms), because the partial 
aggregate, the exchange and the final aggregate all stay native instead of 
following the projection. Comet-off is nominally best on that last row, but its 
average is 84ms against dispatch's 69ms with a 44ms stdev, so I would not read 
the 61 as a real win.
   
   At 1024 rows nothing is resolvable. Every Comet arm lands in 4–11ms, fixed 
job cost dominates, and the baseline's own repeat moves by as much as the arms 
differ from each other. I would not read a direction into that table in either 
direction.
   
   First use is separated out into its own table (1024 rows, taken before 
anything else has executed these queries, and after warming the dispatcher on 
an unrelated expression so that what is left is mostly the one kernel). 1st run 
/ 2nd run / difference, in ms: `lpad` 85 / 29 / 56, `rpad` 44 / 27 / 17, 
`encode` 55 / 29 / 26, `to_binary` 33 / 23 / 10, `to_time` 69 / 25 / 44, mixed 
103 / 39 / 64, group by 214 / 38 / 175. The dispatcher reported exactly one 
compile in each first run. These are upper bounds rather than compile times: 
the grouped case's 175ms is mostly the first shuffle in that JVM, and 
`to_binary` looks cheap only because `encode` ran before it and the two compile 
to the same source, which `CodeGenerator` caches JVM-wide. Net of that it is 
tens of ms per distinct kernel source per JVM, paid once and amortised by the 
per-task cache after.
   
   Two things about how the benchmark is built, because a case that quietly 
measures the wrong thing is worse than no case. Each table asserts before 
timing that the dispatch arm is fully Comet native, that the dispatch-off arm 
is not, and that the dispatcher actually ran, and writes a warning into the 
results file if any of that fails. The first two shapes I tried for "the 
operator above stays native" — a filter, and an ungrouped `sum` — both tripped 
it, because I had written `length()` over binary and that has no native Comet 
path, so the operator was falling back for a reason that has nothing to do with 
this PR. Both would have printed a clean 1.0x.
   
   Where that leaves me: the shapes that lose are the ones where falling back 
was cheapest to begin with, and they lose single digits to 20% on a query that 
is mostly scan; the shapes that win are the ones where falling back was 
expensive. I think that is the right trade, and I would rather not gate the 
catch-all on something like "only dispatch when the projection has other 
convertible expressions" until a real query regresses, since that gate would 
also have to be right about what is downstream of the projection and getting it 
wrong costs more than the 10ms.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to