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


##########
spark/src/test/scala/org/apache/spark/sql/comet/execution/arrow/CacheBatchCoalescerSuite.scala:
##########
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.spark.sql.comet.execution.arrow
+
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.scalatest.funsuite.AnyFunSuite
+
+import org.apache.arrow.memory.RootAllocator
+import org.apache.spark.sql.catalyst.expressions.AttributeReference
+import org.apache.spark.sql.execution.vectorized.OnHeapColumnVector
+import org.apache.spark.sql.types.StringType
+import org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnVector}
+
+class CacheBatchCoalescerSuite extends AnyFunSuite {
+
+  private val numColumns = 4
+  private val attrs = (0 until numColumns).map(i => AttributeReference(s"s$i", 
StringType)())
+
+  // A batch of Spark's own on-heap vectors, which the coalescer always 
copies. Row `i` of column
+  // `c` holds "c<c>r<first + i>", so the output order can be checked.
+  private def sparkBatch(first: Int, rows: Int): ColumnarBatch = {
+    val columns = (0 until numColumns).map { c =>
+      val vector = new OnHeapColumnVector(rows, StringType)
+      (0 until rows).foreach(i => vector.putByteArray(i, s"c${c}r${first + 
i}".getBytes(UTF_8)))
+      vector: ColumnVector
+    }
+    new ColumnarBatch(columns.toArray, rows)
+  }
+
+  // Runs `f` on a coalescer over `inputs`, checks that closing it returns 
every byte it took, and
+  // returns the most the allocator held at once.
+  private def withCoalescer(limit: Long, inputs: Seq[ColumnarBatch], target: 
Int)(

Review Comment:
   You were right that nothing closed a coalescer that was only partly 
consumed. `CacheBatchCoalescerSuite` now has cases for closing after one batch 
of a 25-row input at a target of 10 (the root behind the batch handed out, with 
the input half copied), closing after `hasNext` built a batch that nothing 
took, and a closed coalescer with input left reporting no more batches. It also 
has two allocator-limit cases: one fails while the output batch is being 
started, after two of its four columns are allocated, and one fails once the 
copied rows outgrow the data buffers. Each checks that the allocator is back to 
zero, and each fails when I take out the matching release (the handed-out root, 
the ready root, `pending`, the `root.close()` in `startPending`'s catch) or the 
`closed` check.
   
   Two of the states you listed can't be reached that way. `advance` finishes 
the batch being filled before it returns (when it is full, when a pass-through 
batch arrives, or when the input ends), and it dequeues the one batch it 
queued, so `pending` and `filled` are empty between calls. A 25-row input at a 
target of 10 leaves the input partly copied, not `pending`. `pending` is open 
at `close` only when a copy threw, which is the last case above, and `filled` 
is released defensively with nothing to test it against. Would you rather I 
drop `filled` to a single slot so that is plain?
   



##########
spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/CacheBatchCoalescer.scala:
##########
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.spark.sql.comet.execution.arrow
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+import scala.util.control.NonFatal
+
+import org.apache.arrow.memory.BufferAllocator
+import org.apache.arrow.vector.VectorSchemaRoot
+import org.apache.spark.TaskContext
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.comet.util.Utils
+import org.apache.spark.sql.vectorized.ColumnarBatch
+
+import org.apache.comet.vector.NativeUtil
+
+/**
+ * The columnar input of a cache build, with its small batches combined.
+ *
+ * The columnar write stores each batch it is handed as one cached batch, and 
nothing upstream
+ * bounds how small a batch can be. A shuffle read returns at least one batch 
per map output
+ * block, so a relation cached from a shuffle over many map tasks would 
otherwise hold thousands
+ * of batches of a few hundred rows. Each carries its own message metadata, 
compression framing
+ * and statistics row, and every read of the cache pays for the batch count.
+ *
+ * A batch the writer can store as it stands, Arrow-backed and laid out the 
way the reader reads
+ * it, is passed through when it holds at least half of `targetRows`, so full 
native batches are
+ * still written without a copy. Every other batch is copied into batches of 
`targetRows` rows,
+ * the size the row path cuts at, and split where it crosses that size. A 
small batch is worth the
+ * copy, and a batch the writer cannot store as it stands has to be copied 
into Arrow anyway, so
+ * combining it costs nothing more. The copy reads each column through the 
`ColumnVector`
+ * interface, as `ArrowWriter.writeColumns` does for any input, so it handles 
dictionary-encoded
+ * and nested columns, and offsets that do not start at zero, as a native 
producer's sliced arrays
+ * can have. Arrow's `VectorAppender` assumes they start at zero.
+ *
+ * Row order is kept: the rows copied so far are emitted before a batch that 
passes through. A
+ * large input is copied one output batch at a time, as the consumer asks for 
them, so the batches
+ * open at once do not grow with its size. The input is not advanced until it 
has been copied in
+ * full, because a native producer releases the batch it returned last when it 
is asked for the
+ * next one.
+ *
+ * The batches built here are owned here. Each is released when the next batch 
is requested, and
+ * whatever is still open when the task completes is released then. A batch 
passed through still
+ * belongs to its producer.
+ */
+private[arrow] class CacheBatchCoalescer(
+    input: Iterator[ColumnarBatch],
+    attrs: Seq[Attribute],
+    targetRows: Int,
+    allocator: BufferAllocator)
+    extends Iterator[ColumnarBatch] {
+
+  require(targetRows > 0, s"The target batch size must be positive, but was 
$targetRows")
+
+  private val arrowSchema =
+    Utils.toArrowSchema(Utils.fromAttributes(attrs), 
CometArrowStream.NATIVE_TIMEZONE)
+  private val readerFields = arrowSchema.getFields.asScala.toIndexedSeq
+
+  // A batch at least this large that the writer can store as it stands is not 
copied.
+  private val passThroughRows = math.max(1, targetRows / 2)

Review Comment:
   I measured it, and the copy is not free there. With 5M rows of six columns 
from a native range and projection, a release native library and 
`spark.comet.batchSize` at 4096, copying every full native batch into 10000-row 
batches cut the cached batches from 1232 to 512 and made the aggregate over the 
cache 10% faster (357 to 322 ms), but the build took 5% longer (1191 to 1249 
ms) and the payload was 6% larger (54.3 to 57.5 MB). The two layouts alternated 
in one JVM for 7 rounds, and these are medians. At the default 8192 both 
layouts are identical.
   
   So the threshold now follows Comet's batch size: a batch passes through when 
it holds at least half of the smaller of the cache's batch size and 
`spark.comet.batchSize`. At 4096 the layout is `main`'s again (1232 batches, 
build 1207 against 1194 ms, aggregate 351 against 353 ms), and the small 
batches of a shuffle read are still combined. With the defaults the threshold 
is 4096 rows instead of 5000. The description has the table. Changing the 
sorted-cache test was a consequence: its native sort emits 100-row batches 
under `spark.comet.batchSize` 100, which are now full batches, so it checks the 
order of a sorted cache and leaves the combining to "combines small columnar 
batches in order".
   



##########
docs/source/user-guide/latest/in-memory-cache.md:
##########
@@ -77,6 +77,14 @@ metrics only through that scan.
 
 Each cached batch is stored as a single Arrow IPC record batch message and its 
body.
 
+A cache built from rows cuts its batches at 
`spark.sql.inMemoryColumnarStorage.batchSize` rows,
+10000 by default, as Spark's own format does. A cache built from the Arrow 
batches of Comet

Review Comment:
   Added: "Spark's own format also ends a batch once it holds about 4 MiB, but 
Comet cuts by row count alone, so a relation of wide rows is stored in larger 
batches than Spark's format would use." I checked the cut in the Spark sources 
for 3.4.3, 3.5.9, 4.0.4, 4.1.3 and 4.2.0, where `InMemoryRelation` stops a 
batch when `totalSize` reaches `ColumnBuilder.MAX_BATCH_SIZE_IN_BYTE`, which is 
4 MiB in each.
   



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