sunchao commented on code in PR #6566:
URL: https://github.com/apache/datafusion-comet/pull/6566#discussion_r4174917856
##########
spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/SparkColumnarArrowReader.scala:
##########
@@ -70,18 +74,30 @@ private[comet] class SparkColumnarArrowReader(
return false
}
- val startNs = System.nanoTime()
- val rowsRemaining = current.numRows() - rowsConsumedInCurrent
- val rowsToProduce =
- if (maxRecordsPerBatch <= 0) rowsRemaining
- else math.min(maxRecordsPerBatch, rowsRemaining)
-
- val writer = ArrowWriter.create(getVectorSchemaRoot, rowsToProduce)
- writer.writeColumns(current, rowsConsumedInCurrent, rowsToProduce)
- rowsConsumedInCurrent += rowsToProduce
-
+ // Without a limit, each Spark batch becomes one Arrow batch.
+ val batchSize =
+ if (maxRecordsPerBatch <= 0) current.numRows() - rowsConsumedInCurrent
+ else maxRecordsPerBatch
+ // Pulling the next source batch is the producer's time, not conversion
time.
+ var conversionNs = 0L
+ var startNs = System.nanoTime()
+ val writer = ArrowWriter.create(getVectorSchemaRoot, batchSize)
+ var rowsProduced = 0
+ var more = true
+ while (more) {
+ val rows = math.min(batchSize - rowsProduced, current.numRows() -
rowsConsumedInCurrent)
+ writer.writeColumns(current, rowsConsumedInCurrent, rows)
+ rowsConsumedInCurrent += rows
+ rowsProduced += rows
+ more = rowsProduced < batchSize && {
+ conversionNs += System.nanoTime() - startNs
+ val advanced = advanceToNonEmptyBatch()
Review Comment:
[P2] Preserve file context before advancing the source. When a native filter
consumes converted Parquet batches and a Spark projection evaluates
`input_file_name()` or `input_file_block_*()`, this call advances `FileScanRDD`
and changes its `InputFileBlockHolder` before the current rows reach that
projection. Combining batches from two files therefore assigns the second
file's metadata to the first file's rows. The reproduced query returned
incorrect metadata for 8,192 rows, while Spark and the base implementation
returned the correct values. Retain source-batch boundaries for these plans, or
materialize the metadata before coalescing.
Evidence: Full Spark 4.1.3/JDK 17 reproduction: write two Parquet files with
8,192 rows each, read both in one partition, set
`spark.sql.parquet.columnarReaderBatchSize=8192`,
`spark.comet.batchSize=16384`, `spark.comet.scan.enabled=false`, and
`spark.comet.convert.parquet.enabled=true`. Apply `where("id >= 0")` then
select `id` and the three `input_file_*()` expressions. Verified plan contains
`Project -> CometColumnarToRow -> CometFilter -> CometSparkColumnarToColumnar`.
Head: 8,192 mismatches among 16,384 rows, including the wrong filename and
block length. Base reader/writer substitution: zero mismatches. Harness:
`/tmp/review6566-bx8ympbd/FullPlanProbe.scala`; results: `full-head.log` and
`full-base.log` in that directory.
##########
spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala:
##########
@@ -266,65 +691,149 @@ private[arrow] abstract class ArrowFieldWriter {
}
private[arrow] abstract class FixedWidthArrowFieldWriter extends
ArrowFieldWriter {
+ import ArrowFieldWriter._
override def valueVector: BaseFixedWidthVector
protected def setValueUnsafe(input: SpecializedGetters, ordinal: Int): Unit
- private def ensureCapacity(inputNumElements: Int): Unit = {
+ protected def ensureCapacity(inputNumElements: Int): Unit = {
while (valueVector.getValueCapacity < inputNumElements) {
valueVector.reAlloc()
}
}
- private def tryBulkCopyNoNull(input: ColumnVector, startRow: Int, numRows:
Int): Boolean = {
- // Spark's bulk getters allocate and fill a temporary array before the
copy into Arrow. Keep
- // slices below 32 on the scalar path to avoid the observed tiny-slice
regression.
- if (count != 0 || numRows < 32 || ByteOrder.nativeOrder() !=
ByteOrder.LITTLE_ENDIAN) {
- return false
- }
-
- val supportedInput = input match {
- case vector: OnHeapColumnVector => !vector.hasDictionary
- case vector: OffHeapColumnVector => !vector.hasDictionary
+ /**
+ * Copies the values of rows `[startRow, startRow + numRows)` of `input`
into the data buffer at
+ * index `count`, leaving validity alone. What lands under a null row is
unspecified. Returns
+ * false, having written nothing, for a vector type with no such copy.
+ *
+ * Spark's own vectors without a dictionary are copied in bulk, on-heap ones
from
+ * [[ArrowFieldWriter.MinOnHeapBulkCopyRows]] rows. Everything else,
including a
+ * dictionary-encoded vector, is read value by value, skipping null rows,
because a dictionary
+ * id under a null may be garbage.
+ */
+ protected def copyValues(input: ColumnVector, startRow: Int, numRows: Int):
Boolean = {
+ val target = valueVector.getDataBufferAddress + count.toLong *
valueVector.getTypeWidth
+ val bulk = input match {
+ case vector: OffHeapColumnVector => LittleEndian && !vector.hasDictionary
+ case vector: OnHeapColumnVector =>
+ LittleEndian && !vector.hasDictionary && numRows >=
MinOnHeapBulkCopyRows
Review Comment:
[P2] Exclude Spark's missing/all-null vectors from bulk data reads. Spark
marks an absent nested Parquet field as `isAllNull` and does not grow its
backing storage with the surrounding collection. An array containing 5,000
structs can therefore have a missing integer child backed by only 4,096 slots.
This new bulk branch calls `getInts(0, 5000)` despite every value being null,
throwing `ArrayIndexOutOfBoundsException`. Materializing that valid evolved
schema through `ArrowCachedBatchSerializer` now fails instead of caching nulls
as Spark and the base writer do. Check `isAllNull` before selecting either
Spark-vector bulk path and write only validity for that case.
Evidence: Reproduced through the full Spark cache path with on-heap vectors
and a 4,096-row Parquet batch size. Write one row containing
`transform(sequence(1,5000), x -> named_struct('a', x)) AS items`, then read
with schema `items array<struct<a:int, added:int>>` and call `.cache().count()`
using `ArrowCachedBatchSerializer`. Head fails with `arraycopy: last source
index 5000 out of bounds for int[4096]`, through `getInts -> bulkCopy ->
StructWriter -> ArrayWriter -> ArrowCachedBatchSerializer`. Base reader/writer
substitution returns `1`; the direct writer comparison also confirms 5,000 null
`added` values. Harnesses and logs are under `/tmp/review6566-bx8ympbd/`
(`FullPlanProbe.scala`, `NestedEvolutionProbe.scala`, `full-head.log`,
`full-base.log`).
--
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]