andygrove opened a new pull request, #5626:
URL: https://github.com/apache/datafusion-comet/pull/5626

   ## Which issue does this PR close?
   
   Closes #5625.
   
   **Experimental / RFC.** Off by default, and I am not convinced the gain 
justifies the hazard yet -
   see the caveats at the bottom. Opening as a draft to get the measurements 
and the safety argument
   in front of people rather than because I think it is ready to land.
   
   ## Rationale for this change
   
   Comet has to do a columnar-to-row conversion before a write because native 
write support is still
   experimental. That conversion materialises an `UnsafeRow` per row, and 
nothing on Spark's write
   path needs one:
   
   - `OutputWriter.write(InternalRow)`, 
`FileFormatDataWriter.write(InternalRow)` and
     `WriteTaskStatsTracker.newRow(String, InternalRow)` are all typed on 
`InternalRow`
   - `ParquetWriteSupport extends WriteSupport<InternalRow>` and reads fields 
through
     `SpecializedGetters`
   - `BasicWriteTaskStatsTracker.newRow` ignores the row entirely and just 
increments a counter
   
   Meanwhile `CometColumnarToRowExec.doExecute` already produces 
`batch.rowIterator()` - a reused
   `ColumnarBatchRow` that is a zero-copy view over the Arrow buffers - and 
then throws it away by
   applying an `UnsafeProjection`. For a write that copy is pure overhead: the 
writer decodes straight
   back out of the row it was just given.
   
   This does not touch what actually makes a write expensive, which is 
parquet-mr encoding. Only the
   native writer changes that. What it does do is make the interim path cheaper 
while native writes
   remain experimental, and unlike the native writer it stays Spark-compatible 
by construction,
   because Spark's own writer still produces the bytes.
   
   ## What changes are included in this PR?
   
   `CometColumnarToRowViewExec` returns `batch.rowIterator()` unprojected. It 
is deliberately **not**
   `CodegenSupport`: whole-stage codegen would generate an `UnsafeRowWriter` 
loop and reintroduce
   exactly the copy this exists to avoid.
   
   `EliminateRedundantTransitions` plants it under `WriteFilesExec` (planned 
writes) or
   `DataWritingCommandExec` (when `plannedWrite` is off). The row it hands over 
is reused and mutable,
   so it is only correct for a consumer that finishes with a row before pulling 
the next one. Three
   gates keep it there:
   
   - **unpartitioned and unbucketed**, which is what makes `FileFormatWriter` 
choose
     `SingleDirectoryDataWriter`. The other writers do not qualify: the 
required ordering on
     partition/bucket columns puts a `SortExec` in between and 
`UnsafeExternalSorter` needs
     `UnsafeRow`, and `DynamicPartitionDataConcurrentWriter` spills through
     `UnsafeKVExternalSorter.insertKV`, which is typed on `UnsafeRow`.
   - **one of Spark's own `FileFormat`s**, whose `OutputWriter`s encode each 
row on the spot (Parquet
     through `ParquetWriteSupport`, ORC through `OrcSerializer` into a 
`VectorizedRowBatch`, the text
     formats directly). A third-party format is free to buffer the 
`InternalRow` it is handed.
   - **a schema containing a struct, array or map.** On flat schemas the 
projection is a generated
     fixed-width copy and the saving is inside the noise, which does not pay 
for the hazard above.
   
   Behind `spark.comet.exec.write.rowView.enabled`, default false.
   
   Also adds `CometParquetWriteBenchmark`. Note it sets `spark.shuffle.manager` 
explicitly - without
   it `isCometLoaded` disables Comet and every "Comet" arm silently measures 
Spark, which is #5624.
   Each arm asserts through a `QueryExecutionListener` that it planned the 
transition its label names,
   and warns into the results file if not.
   
   ## Measurements
   
   M3 Max, 1M rows, Spark 4.1, release build, best-of-N, versus today's 
`CometColumnarToRowExec`:
   
   | schema | uncompressed | snappy |
   | --- | --- | --- |
   | fixed width (10 cols) | 542 -> 526 ms (declined by the gate; noise) | 553 
-> 543 ms (noise) |
   | strings | 620 -> 618 ms (declined; noise) | - |
   | wide, 50 columns | 3166 -> 3096 ms (declined; noise) | - |
   | struct + array + map | 1027 -> 920 ms (**10%**) | 1104 -> 934 ms (**15%**) 
|
   | struct + array-of-structs + map-of-array-of-structs | 1838 -> 1630 ms 
(**11%**) | 1850 -> 1616 ms (**13%**) |
   | single struct, depth 1 | 351 -> 315 ms (**10%**) | 350 -> 323 ms (**8%**) |
   | single struct, depth 2 | 431 -> 394 ms (**9%**) | 434 -> 382 ms (**12%**) |
   | single struct, depth 4 | 575 -> 538 ms (**6%**) | 573 -> 537 ms (**6%**) |
   | single struct, depth 8 | 875 -> 816 ms (**7%**) | 881 -> 815 ms (**8%**) |
   
   The declined rows are both arms running the identical plan, so they double 
as a noise-floor
   estimate: about 0-3%.
   
   Two things worth noting. The gain comes from complex types being present, 
not from depth - one
   level already captures it, and it flattens out after that. And the native C2R
   (`spark.comet.exec.columnarToRow.native.enabled`) is consistently *slower* 
than the JVM one on
   nested data here, 0.9X against Spark in several groups, which matches its 
documented per-batch JNI
   cost.
   
   ## How are these changes tested?
   
   New `CometWriteRowViewSuite`, 11 tests, registered in both PR build 
workflows. The bar for each is
   that turning the config on changes nothing observable but the plan:
   
   - byte-identical round trip against the same Comet plan with the config off, 
for hand-written
     primitives/strings/nulls/nested, for fuzz-generated flat and nested 
schemas, and for a
     deliberately deep four-level schema with nulls at every level
   - the same for ORC and JSON, not just Parquet
   - `maxRecordsPerFile` set and unset
   - the transition is present exactly once when expected, absent by default, 
absent for a flat
     schema, absent for partitioned and for bucketed writes, and present for a 
complex column sitting
     alongside flat ones
   
   Also ran `CometParquetWriterSuite` and `CometNativeColumnarToRowSuite` 
alongside it: 67 tests pass.
   
   ## Caveats I would want a second opinion on
   
   1. **Is 6-15% on complex-typed writes worth a reused-mutable-row in the plan 
at all?** The gates are
      argued from Spark's source rather than enforced by anything, and a future 
Spark change to a
      writer that starts retaining rows would be silent data corruption rather 
than a test failure. A
      defensive option would be to restrict it to `ParquetFileFormat` only.
   2. **The format gate is a package-name prefix check**, which is crude. I 
could not find a better
      signal for "this `OutputWriter` does not retain the row".
   3. Only exercised on Spark 4.1 so far.
   


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