This is an automated email from the ASF dual-hosted git repository.

agrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new f9f32a1d perf: Optimize CometSparkToColumnar for columnar input (#892)
f9f32a1d is described below

commit f9f32a1d5e0efe5d57e26184d67a645486da61b9
Author: Matt Butrovich <[email protected]>
AuthorDate: Tue Sep 3 17:02:15 2024 -0400

    perf: Optimize CometSparkToColumnar for columnar input (#892)
    
    * Modify `CometSparkToColumnarExec` to use `ColumnarArray` rather than row 
iteration when the child is producing columnar data.
    
    * Modify `CometSparkToColumnarExec` to use `ColumnarArray` rather than row 
iteration when the child is producing columnar data.
    
    * Run `mvn spotless:apply` to clean up formatting, changed to camelCase for 
variables, do not call `ColumnarArray.numElements()` in a tight loop, added 
`writeColNoNull` as an optimization, reserve the initial vector capacity in 
`writeCol` as an optimization.
    
    * Fix one more variable name.
    
    * Clean up duplicated code.
    
    * Add test for basic types.
    
    * Add more types to test.
    
    * Revert testing changes to CometExecBenchmark.
    
    * Add benchmark.
    
    * Link to issue in ArrowWriters.
    
    * Address review feedback.
    
    * make format
    
    * Address review feedback, mostly naming suggestions. Ran `make format` 
again.
---
 .../sql/comet/execution/arrow/ArrowWriters.scala   |  35 ++++++
 .../execution/arrow/CometArrowConverters.scala     | 133 ++++++++++++++++-----
 .../spark/sql/comet/CometSparkToColumnarExec.scala |  51 ++++----
 .../org/apache/comet/exec/CometExecSuite.scala     |  52 ++++++++
 .../spark/sql/benchmark/CometExecBenchmark.scala   |  10 ++
 5 files changed, 228 insertions(+), 53 deletions(-)

diff --git 
a/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala
 
b/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala
index 8d9f373f..f12ab7c4 100644
--- 
a/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala
+++ 
b/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala
@@ -28,12 +28,15 @@ import 
org.apache.spark.sql.catalyst.expressions.SpecializedGetters
 import org.apache.spark.sql.comet.util.Utils
 import org.apache.spark.sql.errors.QueryExecutionErrors
 import org.apache.spark.sql.types._
+import org.apache.spark.sql.vectorized.ColumnarArray
 
 /**
  * This file is mostly copied from Spark SQL's
  * org.apache.spark.sql.execution.arrow.ArrowWriter.scala. Comet shadows Arrow 
classes to avoid
  * potential conflicts with Spark's Arrow dependencies, hence we cannot reuse 
Spark's ArrowWriter
  * directly.
+ *
+ * Performance enhancement: 
https://github.com/apache/datafusion-comet/issues/888
  */
 private[arrow] object ArrowWriter {
   def create(root: VectorSchemaRoot): ArrowWriter = {
@@ -103,6 +106,16 @@ class ArrowWriter(val root: VectorSchemaRoot, fields: 
Array[ArrowFieldWriter]) {
     count += 1
   }
 
+  def writeCol(input: ColumnarArray, columnIndex: Int): Unit = {
+    fields(columnIndex).writeCol(input)
+    count = input.numElements()
+  }
+
+  def writeColNoNull(input: ColumnarArray, columnIndex: Int): Unit = {
+    fields(columnIndex).writeColNoNull(input)
+    count = input.numElements()
+  }
+
   def finish(): Unit = {
     root.setRowCount(count)
     fields.foreach(_.finish())
@@ -137,6 +150,28 @@ private[arrow] abstract class ArrowFieldWriter {
     count += 1
   }
 
+  def writeCol(input: ColumnarArray): Unit = {
+    val inputNumElements = input.numElements()
+    valueVector.setInitialCapacity(inputNumElements)
+    while (count < inputNumElements) {
+      if (input.isNullAt(count)) {
+        setNull()
+      } else {
+        setValue(input, count)
+      }
+      count += 1
+    }
+  }
+
+  def writeColNoNull(input: ColumnarArray): Unit = {
+    val inputNumElements = input.numElements()
+    valueVector.setInitialCapacity(inputNumElements)
+    while (count < inputNumElements) {
+      setValue(input, count)
+      count += 1
+    }
+  }
+
   def finish(): Unit = {
     valueVector.setValueCount(count)
   }
diff --git 
a/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/CometArrowConverters.scala
 
b/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/CometArrowConverters.scala
index 9dbd8dcd..e0489ae7 100644
--- 
a/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/CometArrowConverters.scala
+++ 
b/common/src/main/scala/org/apache/spark/sql/comet/execution/arrow/CometArrowConverters.scala
@@ -21,18 +21,19 @@ package org.apache.spark.sql.comet.execution.arrow
 
 import org.apache.arrow.memory.{BufferAllocator, RootAllocator}
 import org.apache.arrow.vector.VectorSchemaRoot
+import org.apache.arrow.vector.types.pojo.Schema
 import org.apache.spark.TaskContext
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.comet.util.Utils
 import org.apache.spark.sql.types.StructType
-import org.apache.spark.sql.vectorized.ColumnarBatch
+import org.apache.spark.sql.vectorized.{ColumnarArray, ColumnarBatch}
 
 import org.apache.comet.vector.NativeUtil
 
 object CometArrowConverters extends Logging {
   // TODO: we should reuse the same root allocator in the comet code base?
-  val rootAllocator: BufferAllocator = new RootAllocator(Long.MaxValue)
+  private val rootAllocator: BufferAllocator = new RootAllocator(Long.MaxValue)
 
   // This is similar how Spark converts internal row to Arrow format except 
that it is transforming
   // the result batch to Comet's ColumnarBatch instead of serialized bytes.
@@ -44,24 +45,23 @@ object CometArrowConverters extends Logging {
   // exported process increases the reference count of the Arrow vectors. The 
reference count is
   // only decreased when the native plan is done with the vectors, which is 
usually longer than
   // all the ColumnarBatches are consumed.
-  private[sql] class ArrowBatchIterator(
-      rowIter: Iterator[InternalRow],
+
+  abstract private[sql] class ArrowBatchIterBase(
       schema: StructType,
-      maxRecordsPerBatch: Long,
       timeZoneId: String,
       context: TaskContext)
       extends Iterator[ColumnarBatch]
       with AutoCloseable {
 
-    private val arrowSchema = Utils.toArrowSchema(schema, timeZoneId)
+    protected val arrowSchema: Schema = Utils.toArrowSchema(schema, timeZoneId)
     // Reuse the same root allocator here.
-    private val allocator =
+    protected val allocator: BufferAllocator =
       rootAllocator.newChildAllocator(s"to${this.getClass.getSimpleName}", 0, 
Long.MaxValue)
-    private val root = VectorSchemaRoot.create(arrowSchema, allocator)
-    private val arrowWriter = ArrowWriter.create(root)
+    protected val root: VectorSchemaRoot = 
VectorSchemaRoot.create(arrowSchema, allocator)
+    protected val arrowWriter: ArrowWriter = ArrowWriter.create(root)
 
-    private var currentBatch: ColumnarBatch = null
-    private var closed: Boolean = false
+    protected var currentBatch: ColumnarBatch = null
+    protected var closed: Boolean = false
 
     Option(context).foreach {
       _.addTaskCompletionListener[Unit] { _ =>
@@ -69,9 +69,27 @@ object CometArrowConverters extends Logging {
       }
     }
 
-    override def hasNext: Boolean = rowIter.hasNext || {
+    override def close(): Unit = {
       close(false)
-      false
+    }
+
+    protected def close(closeAllocator: Boolean): Unit = {
+      try {
+        if (!closed) {
+          if (currentBatch != null) {
+            arrowWriter.reset()
+            currentBatch.close()
+            currentBatch = null
+          }
+          root.close()
+          closed = true
+        }
+      } finally {
+        // the allocator shall be closed when the task is finished
+        if (closeAllocator) {
+          allocator.close()
+        }
+      }
     }
 
     override def next(): ColumnarBatch = {
@@ -79,11 +97,25 @@ object CometArrowConverters extends Logging {
       currentBatch
     }
 
-    override def close(): Unit = {
+    protected def nextBatch(): ColumnarBatch
+
+  }
+
+  private[sql] class RowToArrowBatchIter(
+      rowIter: Iterator[InternalRow],
+      schema: StructType,
+      maxRecordsPerBatch: Long,
+      timeZoneId: String,
+      context: TaskContext)
+      extends ArrowBatchIterBase(schema, timeZoneId, context)
+      with AutoCloseable {
+
+    override def hasNext: Boolean = rowIter.hasNext || {
       close(false)
+      false
     }
 
-    private def nextBatch(): ColumnarBatch = {
+    override protected def nextBatch(): ColumnarBatch = {
       if (rowIter.hasNext) {
         // the arrow writer shall be reset before writing the next batch
         arrowWriter.reset()
@@ -99,33 +131,68 @@ object CometArrowConverters extends Logging {
         null
       }
     }
+  }
 
-    private def close(closeAllocator: Boolean): Unit = {
-      try {
-        if (!closed) {
-          if (currentBatch != null) {
-            arrowWriter.reset()
-            currentBatch.close()
-            currentBatch = null
+  def rowToArrowBatchIter(
+      rowIter: Iterator[InternalRow],
+      schema: StructType,
+      maxRecordsPerBatch: Long,
+      timeZoneId: String,
+      context: TaskContext): Iterator[ColumnarBatch] = {
+    new RowToArrowBatchIter(rowIter, schema, maxRecordsPerBatch, timeZoneId, 
context)
+  }
+
+  private[sql] class ColumnBatchToArrowBatchIter(
+      colBatch: ColumnarBatch,
+      schema: StructType,
+      maxRecordsPerBatch: Int,
+      timeZoneId: String,
+      context: TaskContext)
+      extends ArrowBatchIterBase(schema, timeZoneId, context)
+      with AutoCloseable {
+
+    private var rowsProduced: Int = 0
+
+    override def hasNext: Boolean = rowsProduced < colBatch.numRows() || {
+      close(false)
+      false
+    }
+
+    override protected def nextBatch(): ColumnarBatch = {
+      val rowsInBatch = colBatch.numRows()
+      if (rowsProduced < rowsInBatch) {
+        // the arrow writer shall be reset before writing the next batch
+        arrowWriter.reset()
+        val rowsToProduce =
+          if (maxRecordsPerBatch <= 0) rowsInBatch - rowsProduced
+          else Math.min(maxRecordsPerBatch, rowsInBatch - rowsProduced)
+
+        for (columnIndex <- 0 until colBatch.numCols()) {
+          val column = colBatch.column(columnIndex)
+          val columnArray = new ColumnarArray(column, rowsProduced, 
rowsToProduce)
+          if (column.hasNull) {
+            arrowWriter.writeCol(columnArray, columnIndex)
+          } else {
+            arrowWriter.writeColNoNull(columnArray, columnIndex)
           }
-          root.close()
-          closed = true
-        }
-      } finally {
-        // the allocator shall be closed when the task is finished
-        if (closeAllocator) {
-          allocator.close()
         }
+
+        rowsProduced += rowsToProduce
+
+        arrowWriter.finish()
+        NativeUtil.rootAsBatch(root)
+      } else {
+        null
       }
     }
   }
 
-  def toArrowBatchIterator(
-      rowIter: Iterator[InternalRow],
+  def columnarBatchToArrowBatchIter(
+      colBatch: ColumnarBatch,
       schema: StructType,
-      maxRecordsPerBatch: Long,
+      maxRecordsPerBatch: Int,
       timeZoneId: String,
       context: TaskContext): Iterator[ColumnarBatch] = {
-    new ArrowBatchIterator(rowIter, schema, maxRecordsPerBatch, timeZoneId, 
context)
+    new ColumnBatchToArrowBatchIter(colBatch, schema, maxRecordsPerBatch, 
timeZoneId, context)
   }
 }
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometSparkToColumnarExec.scala
 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometSparkToColumnarExec.scala
index d615846c..640bb877 100644
--- 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometSparkToColumnarExec.scala
+++ 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometSparkToColumnarExec.scala
@@ -19,8 +19,6 @@
 
 package org.apache.spark.sql.comet
 
-import scala.collection.JavaConverters._
-
 import org.apache.spark.TaskContext
 import org.apache.spark.broadcast.Broadcast
 import org.apache.spark.rdd.RDD
@@ -65,31 +63,44 @@ case class CometSparkToColumnarExec(child: SparkPlan)
     val timeZoneId = conf.sessionLocalTimeZone
     val schema = child.schema
 
-    val rdd: RDD[InternalRow] = if (child.supportsColumnar) {
+    if (child.supportsColumnar) {
       child
         .executeColumnar()
         .mapPartitionsInternal { iter =>
-          iter.flatMap(_.rowIterator().asScala)
+          iter.flatMap { columnBatch =>
+            val context = TaskContext.get()
+            CometArrowConverters.columnarBatchToArrowBatchIter(
+              columnBatch,
+              schema,
+              maxRecordsPerBatch,
+              timeZoneId,
+              context)
+          }
+        }
+        .map { batch =>
+          numInputRows += batch.numRows()
+          numOutputBatches += 1
+          batch
         }
     } else {
-      child.execute()
+      child
+        .execute()
+        .mapPartitionsInternal { iter =>
+          val context = TaskContext.get()
+          CometArrowConverters.rowToArrowBatchIter(
+            iter,
+            schema,
+            maxRecordsPerBatch,
+            timeZoneId,
+            context)
+        }
+        .map { batch =>
+          numInputRows += batch.numRows()
+          numOutputBatches += 1
+          batch
+        }
     }
 
-    rdd
-      .mapPartitionsInternal { iter =>
-        val context = TaskContext.get()
-        CometArrowConverters.toArrowBatchIterator(
-          iter,
-          schema,
-          maxRecordsPerBatch,
-          timeZoneId,
-          context)
-      }
-      .map { batch =>
-        numInputRows += batch.numRows()
-        numOutputBatches += 1
-        batch
-      }
   }
 
   override protected def withNewChildInternal(newChild: SparkPlan): 
CometSparkToColumnarExec =
diff --git a/spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala 
b/spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala
index ad963fb8..ad2242d2 100644
--- a/spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala
@@ -1547,6 +1547,58 @@ class CometExecSuite extends CometTestBase {
     })
   }
 
+  test("SparkToColumnar over BatchScan (Spark Parquet reader)") {
+    Seq("", "parquet").foreach { v1List =>
+      Seq(true, false).foreach { parquetVectorized =>
+        Seq(
+          "cast(id as tinyint)",
+          "cast(id as smallint)",
+          "cast(id as integer)",
+          "cast(id as bigint)",
+          "cast(id as float)",
+          "cast(id as double)",
+          "cast(id as decimal)",
+          "cast(id as timestamp)",
+          "cast(id as string)",
+          "cast(id as binary)",
+          "struct(id)").foreach { valueType =>
+          {
+            withSQLConf(
+              SQLConf.USE_V1_SOURCE_LIST.key -> v1List,
+              CometConf.COMET_NATIVE_SCAN_ENABLED.key -> "false",
+              CometConf.COMET_CONVERT_FROM_PARQUET_ENABLED.key -> "true",
+              SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> 
parquetVectorized.toString) {
+              withTempPath { dir =>
+                var df = spark
+                  .range(10000)
+                  .selectExpr("id as key", s"$valueType as value")
+                  .toDF("key", "value")
+
+                df.write.parquet(dir.toString)
+
+                df = spark.read.parquet(dir.toString)
+                checkSparkAnswerAndOperator(
+                  df.select("*").groupBy("key", "value").count(),
+                  includeClasses = Seq(classOf[CometSparkToColumnarExec]))
+
+                // Verify that the BatchScanExec nodes supported columnar 
output when requested for Spark 3.4+.
+                // Earlier versions support columnar output for fewer type.
+                if (isSpark34Plus) {
+                  val leaves = df.queryExecution.executedPlan.collectLeaves()
+                  if (parquetVectorized && isSpark34Plus) {
+                    assert(leaves.forall(_.supportsColumnar))
+                  } else {
+                    assert(!leaves.forall(_.supportsColumnar))
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
   test("SparkToColumnar over InMemoryTableScanExec") {
     Seq("true", "false").foreach(aqe => {
       Seq("true", "false").foreach(cacheVectorized => {
diff --git 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometExecBenchmark.scala 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometExecBenchmark.scala
index 400d9b82..7205484e 100644
--- 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometExecBenchmark.scala
+++ 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometExecBenchmark.scala
@@ -96,6 +96,16 @@ object CometExecBenchmark extends CometBenchmarkBase {
           }
         }
 
+        benchmark.addCase("SQL Parquet - Spark (Scan), Comet (Exec)") { _ =>
+          withSQLConf(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_NATIVE_SCAN_ENABLED.key -> "false",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_CONVERT_FROM_PARQUET_ENABLED.key -> "true") {
+            spark.sql("select c2 + 1, c1 + 2 from parquetV1Table where c1 + 1 
> 0").noop()
+          }
+        }
+
         benchmark.run()
       }
     }


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

Reply via email to