schenksj commented on PR #4827:
URL: 
https://github.com/apache/datafusion-comet/pull/4827#issuecomment-6631504867

   @andygrove, replying to the Aug 27 review:
   
   - **Short-circuit.** With the root-only rule, a conditional that contains 
the declined node goes
     to the dispatcher whole, so `CASE`/`IF`/`AND`/`OR`/`COALESCE` run Spark's 
own code. Gluten's
     `ColumnarPartialProjectExec` takes the same line: it extracts the whole 
`If`/`CaseWhen`/
     `Coalesce`/`NaNvl` when it contains a UDF. I added ANSI `10 div x` tests 
for CASE in a projection
     and AND in a filter. To be clear about what they show: with the old 
node-level hook put back,
     these shapes and five others (OR, IF, COALESCE, NVL2, and IF/OR in a 
filter) all still matched
     Spark on current main, because the Rust conditionals there evaluate a 
branch only on the rows that
     reach it. So I couldn't reproduce a failure on current main. The tests pin 
the result, and root-only
     makes it hold without depending on that.
   
     Evaluation order does matter in three places, all about which rows Spark's 
whole-stage code
     evaluates an expression for. None is about the values computed for a row.
     - **Nondeterministic filter.** Whole-stage `FilterExec` evaluates each 
`IsNotNull` only just
       before the first conjunct that reads its column. So in `y IS NOT NULL 
AND abs(rand(42)) < 0.5`,
       `rand` runs for the rows with a NULL `y` too. On the test's 200 rows 
Spark keeps 69, and 62
       without whole-stage codegen, which evaluates the condition the way the 
kernel does. The detour
       now declines a nondeterministic filter condition. It walks the tree for 
this, because
       `Invoke.deterministic` ignores its target. There are tests for that 
shape and for a Spark 4
       variant hidden under `make_valid_utf8`. The same blind spot affects 
projections: Spark defers
       a projection item whose nondeterminism is hidden under an `Invoke`, as 
it would any
       deterministic item, so its `rand` advances only for the rows Spark 
evaluates. The detour now
       also declines a projection item that reports itself deterministic but 
contains a
       nondeterministic node, and there's a Spark 4.0+ test for that.
     - **Subexpression elimination.** The kernel eliminated common 
subexpressions, which whole-stage
       `FilterExec` does not do in its predicate. Outside whole-stage codegen 
it does, and Spark runs
       a filter there when whole-stage codegen is off, when the condition has a 
higher-order function
       or other non-leaf `CodegenFallback`, or when it exceeds 
`spark.sql.codegen.maxFields`. The
       detour now mirrors `CollapseCodegenStages` per filter and compiles with 
or without elimination
       to match, with tests for both forms. Projection items follow Spark's 
form too: under
       `spark.sql.codegen.factoryMode=NO_CODEGEN` Spark interprets the 
projection and evaluates a
       common subexpression only on first use, so the kernel doesn't eliminate 
them there. This
       follows what Spark plans; a run-time fallback of Spark's whole-stage 
code (a huge method, or a
       compile failure) can't be seen at planning time and is documented as a 
difference. Before this
       change, the kernel's elimination moved the division in
       `x <> 0 AND abs(10 div x) + abs(10 div x) > 1` in front of the `AND`, so 
it raised under ANSI
       for `x = 0` where whole-stage Spark returns rows; that case now matches. 
Spark 4.2 also added
       subexpression elimination to whole-stage `FilterExec` (SPARK-56032, from 
4.2.0), interleaved
       with the conjuncts, which the kernel can't reproduce; from 4.2 the 
detour declines a filter
       condition where that applies, with a reason and tests. `ProjectExec` 
also eliminates
       them across items, which a per-item kernel can't. So for two items `x <> 
0 AND abs(10 div x) > 1`
       and `x <> 0 AND abs(10 div x) > 2`, Spark raises for `x = 0` while the 
detour returns rows, as
       Comet's Rust projection already does. That's documented and pinned by a 
test.
     - **Errors on rows Spark skips, or doesn't skip.** This applies to any 
expression that can
       raise, not only ANSI arithmetic: with ANSI off, a `raise_error` in a 
deferred item raises with
       the detour where whole-stage Spark returns no rows, and there's a test 
for that. The kernel
       evaluates every item for every row of the batch. Whole-stage 
`ProjectExec` defers deterministic items until something above
       in the stage reads them, such as a filter, a broadcast hash join or a 
`LIMIT`. So
       `SELECT d FROM (SELECT abs(10 div x) AS d, rand(1) AS r FROM t) WHERE r 
> 2` returns nothing in
       Spark and can raise with the detour, and so can
       `SELECT t.d FROM (SELECT x, abs(10 div x) AS d FROM t) t JOIN u ON t.x = 
u.x`. Filters differ in both directions,
       because whole-stage `FilterExec` places each `IsNotNull` itself while 
the kernel goes left to
       right, as Spark without whole-stage codegen does:
       - `y IS NOT NULL AND abs(10 div x) > 0` with a row `(0, NULL)` raises in 
whole-stage Spark,
         while the detour and Comet's Rust filter return the other rows.
       - `abs(10 div coalesce(x, 0)) > 0 AND x IS NOT NULL` with a NULL `x` 
returns rows in
         whole-stage Spark, while the detour raises.
   
       I didn't try to reproduce Spark's evaluation order. The compatibility 
guide now has a "JVM
       expression detour" section describing these cases, and tests pin both 
filter directions.
   
   - **"Outermost" wording.** It's gone. The detour is now explicitly at the 
root, and the whole-item
     tests pin which tree gets dispatched.
   - **Benchmark numbers.** I reran the benchmark on the final code and added 
the shape you asked
     about: the detoured projection (and, separately, the detoured filter) 
under a `GROUP BY`
     aggregate with a shuffle. Setup: AMD Ryzen 9 8945HX, Spark 4.1, release 
build, `local[1]`, 16M
     rows, `sqrt` sent through the detour by the testing config in both Comet 
cases. Two runs, best
     ms, run 1 / run 2:
   
     | Shape                                      | Spark     | Detour off | 
Detour on |
     | ------------------------------------------ | --------- | ---------- | 
--------- |
     | one detoured item + 6 passthrough columns  | 845 / 841 | 353 / 339  | 
349 / 356 |
     | 7 Rust-implemented items + 1 detoured item | 831 / 844 | 344 / 341  | 
361 / 357 |
     | filter                                     | 716 / 700 | 379 / 364  | 
338 / 324 |
     | projection under `GROUP BY`                | 516 / 512 | 310 / 329  | 
282 / 276 |
     | filter under `GROUP BY`                    | 562 / 570 | 292 / 295  | 
295 / 290 |
   
     Each case moved by at most 6% between runs. In the executed plans after 
AQE, with the detour
     off, the aggregates and the shuffle above the projection or filter fall 
back to Spark as well,
     above a `CometColumnarToRow` over the scan. With it on, every operator, 
inside the shuffle
     stage too, is a Comet operator. Your data-dependence point holds. The 
detour is 9% to 16% faster for the
     projection under `GROUP BY` and about 11% faster for the filter. It's 
about the same for the
     passthrough shape and for the filter under `GROUP BY`, and about 5% slower 
for the mixed
     projection. So keeping the plan in Comet doesn't by itself make these 
queries faster; the gain depends on
     the shape (the filter under `GROUP BY` keeps its aggregate and shuffle in 
Comet and gains
     nothing, while the plain filter gains 11%). The full table, the plans and 
the machine
     load are in the PR description.
   
   - **Fallback rollup.** There are two tests. One checks that a detoured item 
leaves no fallback
     reason when the operator stays in Comet. The other uses `abs` detoured 
plus `upper` disabled in
     one projection: the operator falls back and reports only the `upper` 
reason. The detour now
     snapshots the tree's explain state (journal reasons, reason tags, coverage 
tags) before the
     conversion. It restores that snapshot before the detour, and merges the 
conversion's state back
     only if the detour is refused. A subtree shared with another item, as 
`CollapseProject` can leave
     it, therefore keeps that item's tags. There are tests for the 
shared-subtree case and for a
     refused detour.
   - **Batch evaluation.** The config doc says the detour evaluates the item 
for every row of the
     batch, so an expression that can raise an error, under ANSI or 
`raise_error` / `assert_true`, can
     raise where Spark does not, or not raise where Spark does. It points at 
the compatibility guide's
     "JVM expression detour" section, which covers the deferred-item, 
filter-ordering and cross-item
     subexpression cases. There are tests that pin these, including a 
`raise_error` deferred-item
     case with ANSI off.
   


-- 
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