viirya commented on code in PR #6371:
URL: https://github.com/apache/datafusion-comet/pull/6371#discussion_r4308293598


##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -532,40 +533,130 @@ object Utils extends CometTypeShim with Logging {
   def getBatchFieldVectorsWithProviders(
       batch: ColumnarBatch): Seq[(FieldVector, Option[DictionaryProvider])] = {
     val rows = batch.numRows()
-    (0 until batch.numCols()).map { index =>
-      batch.column(index) match {
-        case a: CometVector =>
-          val valueVector = a.getValueVector
-          val provider =
-            if (valueVector.getField.getDictionary != null) 
Some(a.getDictionaryProvider)
-            else None
-
-          (getFieldVector(valueVector, "serialize"), provider)
-
-        case cv: ConstantColumnVector =>
-          // Spark wraps file-source partition columns and other per-batch 
constants in
-          // `ConstantColumnVector`. Materialise to an Arrow vector so the 
serialisation path
-          // doesn't reject the batch. "UTC" is intentional -- see 
`ConstantColumnVectors`.
-          val materialized = ConstantColumnVectors.materialize(
-            cv,
-            cv.dataType(),
-            rows,
-            s"_const_$index",
-            org.apache.comet.CometArrowAllocator,
-            "UTC")
-          (materialized, None)
-
-        case c =>
-          throw new SparkException(
-            s"Comet execution only takes Arrow Arrays, but got ${c.getClass}. 
" +
-              "This typically happens when a Comet scan falls back to Spark 
due to unsupported " +
-              "data types (e.g., complex types like structs, arrays, or maps). 
" +
-              "To resolve this, you can: " +
-              "(1) enable spark.comet.scan.allowIncompatible=true to use a 
compatible native " +
-              "scan variant, or " +
-              "(2) enable spark.comet.convert.parquet.enabled=true to convert 
Spark Parquet " +
-              "data to Arrow format automatically.")
+    // Vectors made here rather than taken from `batch`. The caller releases 
them with the rest of
+    // what it writes, but if a later column fails it never gets them, so they 
are closed here.
+    val made = scala.collection.mutable.ArrayBuffer.empty[FieldVector]
+    def keep(vector: FieldVector): FieldVector = {
+      made += vector
+      vector
+    }
+
+    try {
+      (0 until batch.numCols()).map { index =>
+        batch.column(index) match {
+          case a: CometVector =>
+            val valueVector = a.getValueVector
+            val provider =
+              if (valueVector.getField.getDictionary != null) 
Some(a.getDictionaryProvider)
+              else None
+
+            val fieldVector = valueVector match {
+              case v: LargeVarCharVector => keep(narrowOffsets(v, 
ArrowType.Utf8.INSTANCE))

Review Comment:
   `getBatchFieldVectorsWithProviders` has a second caller, 
`CachedBatchIpc.decodeDictionaries`. It closes only the vectors it decoded and 
documents the rest as "returned as they are and stay owned by `batch`". That 
stays true today only because `isArrowBacked` and `matchesReaderLayout` both 
keep large-offset batches off the direct cache path. Could the scaladoc of this 
method say it may now return vectors it allocated, which the caller releases by 
clearing them? Then the next caller won't assume every returned vector is 
borrowed.



##########
spark/src/test/scala/org/apache/spark/sql/comet/util/UtilsSuite.scala:
##########
@@ -166,29 +173,16 @@ class UtilsSuite extends CometTestBase {
 
   test("isArrowBacked rejects large-offset Arrow vectors") {
     // A CometPlainVector can wrap a LargeVarCharVector or 
LargeVarBinaryVector -- an accelerated
-    // mapInArrow returning pa.large_string() produces one -- but 
getFieldVector rejects both. If
-    // isArrowBacked accepted them, a caller would take the direct write path 
and then fail, so it
-    // must report false and let the caller convert the batch instead.
-    val numRows = 2
-    Seq[org.apache.arrow.vector.FieldVector](
-      {
-        val v = new org.apache.arrow.vector.LargeVarCharVector("s", 
CometArrowAllocator)
-        v.allocateNew(numRows)
-        v.setSafe(0, "hello".getBytes("UTF-8"))
-        v.setSafe(1, "world".getBytes("UTF-8"))
-        v.setValueCount(numRows)
-        v
-      }, {
-        val v = new org.apache.arrow.vector.LargeVarBinaryVector("b", 
CometArrowAllocator)
-        v.allocateNew(numRows)
-        v.setSafe(0, "hello".getBytes("UTF-8"))
-        v.setSafe(1, "world".getBytes("UTF-8"))
-        v.setValueCount(numRows)
-        v
-      }).foreach { vector =>
+    // mapInArrow returning pa.large_string() produces one -- but 
getFieldVector rejects both, and
+    // getBatchFieldVectors writes a 32-bit copy of them rather than the 
vector itself. A caller

Review Comment:
   Nit: "A caller taking the direct write path expects the batch's own vectors" 
reads as if the cache would misbehave on a narrowed copy. What actually keeps 
such a batch off the direct path is also `matchesReaderLayout`, since 
`LargeUtf8` doesn't match the reader's `Utf8`. Could the comment say which of 
the two checks it relies on, or that both apply?



##########
spark/src/test/scala/org/apache/spark/sql/comet/util/UtilsSuite.scala:
##########
@@ -197,4 +191,213 @@ class UtilsSuite extends CometTestBase {
       }
     }
   }
+
+  test("serializeBatches writes large-offset string and binary vectors with 
32-bit offsets") {
+    // A PyArrow UDF returning pa.large_string() or pa.large_binary() hands 
Comet a
+    // LargeVarCharVector or LargeVarBinaryVector. The column is still 
StringType or BinaryType,
+    // so it is written as the Utf8 or Binary vector every reader of the bytes 
expects for those.
+    val values = Seq("hello", null, "", "wörld")
+    val strings = fill(new LargeVarCharVector("s", CometArrowAllocator), 
values)
+    val binaries = fill(new LargeVarBinaryVector("b", CometArrowAllocator), 
values)
+    try {
+      val batch =
+        new ColumnarBatch(Array(cometVector(strings), cometVector(binaries)), 
values.length)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> values,
+          classOf[VarBinaryVector] -> values))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("serializeBatches rebases large-offset vectors whose offsets start past 
zero") {
+    // Offsets need not start at zero. A slice imported over the C Data 
interface keeps its
+    // parent's offsets and data buffer, so they start past bytes that no 
value covers. The copy
+    // holds only the bytes the values cover, with offsets counted from the 
first of them.
+    val values = Seq("hello", null, "wörld")
+    val strings = startingPast("xx", new LargeVarCharVector("s", 
CometArrowAllocator), values)
+    val binaries = startingPast("xx", new LargeVarBinaryVector("b", 
CometArrowAllocator), values)
+    try {
+      assert(strings.getOffsetBuffer.getLong(0) == 2)
+      assert(binaries.getOffsetBuffer.getLong(0) == 2)
+      val batch =
+        new ColumnarBatch(Array(cometVector(strings), cometVector(binaries)), 
values.length)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> values,
+          classOf[VarBinaryVector] -> values))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("serializeBatches writes empty large-offset vectors with 32-bit 
offsets") {
+    // A vector that holds no values need not have an offset buffer, so none 
is read.
+    val strings = new LargeVarCharVector("s", CometArrowAllocator)
+    val binaries = new LargeVarBinaryVector("b", CometArrowAllocator)
+    try {
+      strings.setValueCount(0)
+      binaries.setValueCount(0)
+      assert(strings.getOffsetBuffer.capacity == 0)
+      val batch = new ColumnarBatch(Array(cometVector(strings), 
cometVector(binaries)), 0)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> Seq.empty,
+          classOf[VarBinaryVector] -> Seq.empty))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("coalesceBroadcastBatches appends a large-offset batch to a 32-bit 
one") {

Review Comment:
   Nit: the name says a large-offset batch is appended to a 32-bit one, but the 
test serializes the large batch first. Something like "coalesceBroadcastBatches 
coalesces batches of mixed offset widths" would match either order.



##########
spark/src/test/scala/org/apache/spark/sql/comet/util/UtilsSuite.scala:
##########
@@ -197,4 +191,213 @@ class UtilsSuite extends CometTestBase {
       }
     }
   }
+
+  test("serializeBatches writes large-offset string and binary vectors with 
32-bit offsets") {
+    // A PyArrow UDF returning pa.large_string() or pa.large_binary() hands 
Comet a
+    // LargeVarCharVector or LargeVarBinaryVector. The column is still 
StringType or BinaryType,
+    // so it is written as the Utf8 or Binary vector every reader of the bytes 
expects for those.
+    val values = Seq("hello", null, "", "wörld")
+    val strings = fill(new LargeVarCharVector("s", CometArrowAllocator), 
values)
+    val binaries = fill(new LargeVarBinaryVector("b", CometArrowAllocator), 
values)
+    try {
+      val batch =
+        new ColumnarBatch(Array(cometVector(strings), cometVector(binaries)), 
values.length)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> values,
+          classOf[VarBinaryVector] -> values))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("serializeBatches rebases large-offset vectors whose offsets start past 
zero") {
+    // Offsets need not start at zero. A slice imported over the C Data 
interface keeps its
+    // parent's offsets and data buffer, so they start past bytes that no 
value covers. The copy
+    // holds only the bytes the values cover, with offsets counted from the 
first of them.
+    val values = Seq("hello", null, "wörld")
+    val strings = startingPast("xx", new LargeVarCharVector("s", 
CometArrowAllocator), values)
+    val binaries = startingPast("xx", new LargeVarBinaryVector("b", 
CometArrowAllocator), values)
+    try {
+      assert(strings.getOffsetBuffer.getLong(0) == 2)
+      assert(binaries.getOffsetBuffer.getLong(0) == 2)
+      val batch =
+        new ColumnarBatch(Array(cometVector(strings), cometVector(binaries)), 
values.length)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> values,
+          classOf[VarBinaryVector] -> values))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("serializeBatches writes empty large-offset vectors with 32-bit 
offsets") {
+    // A vector that holds no values need not have an offset buffer, so none 
is read.
+    val strings = new LargeVarCharVector("s", CometArrowAllocator)
+    val binaries = new LargeVarBinaryVector("b", CometArrowAllocator)
+    try {
+      strings.setValueCount(0)
+      binaries.setValueCount(0)
+      assert(strings.getOffsetBuffer.capacity == 0)
+      val batch = new ColumnarBatch(Array(cometVector(strings), 
cometVector(binaries)), 0)
+      assert(
+        roundTrip(batch) == Seq(
+          classOf[VarCharVector] -> Seq.empty,
+          classOf[VarBinaryVector] -> Seq.empty))
+    } finally {
+      strings.close()
+      binaries.close()
+    }
+  }
+
+  test("coalesceBroadcastBatches appends a large-offset batch to a 32-bit 
one") {
+    // The coalescer appends every batch to a root built from the first one's 
schema, and leaves
+    // the broadcast uncoalesced when a batch does not match it. A producer 
can hand back
+    // large_string for one batch and string for the next, so both must 
serialize alike.
+    val large = fill(new LargeVarCharVector("s", CometArrowAllocator), 
Seq("a", null))
+    val regular = fill(new VarCharVector("s", CometArrowAllocator), Seq("b", 
"c"))
+    try {
+      val batches = Seq(large, regular).map(v => new 
ColumnarBatch(Array(cometVector(v)), 2))
+      val bufs = 
Utils.serializeBatches(batches.iterator).map(_._2).toSeq.iterator
+      val (coalesced, batchCount, totalRows) = 
Utils.coalesceBroadcastBatches(bufs)
+      // A batch count of zero is the fallback to the uncoalesced buffers.
+      assert(batchCount == 2)
+      assert(totalRows == 4)
+
+      val it = coalesced.iterator.flatMap(Utils.decodeBatches(_, "test"))
+      val out = it.next()
+      val got = stringValues(out.column(0), out.numRows())
+      assert(!it.hasNext)
+      assert(got == Seq("a", null, "b", "c"))
+    } finally {
+      large.close()
+      regular.close()
+    }
+  }
+
+  test("serializeBatches refuses a large-offset column too big for 32-bit 
offsets") {

Review Comment:
   Nit: this test and `serializeBatches releases the vectors it made when a 
later column fails` both build `tooBigForIntOffsets` and assert the same 
message. The cleanup test already covers the refusal, so this one could go.



##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -532,40 +533,130 @@ object Utils extends CometTypeShim with Logging {
   def getBatchFieldVectorsWithProviders(
       batch: ColumnarBatch): Seq[(FieldVector, Option[DictionaryProvider])] = {
     val rows = batch.numRows()
-    (0 until batch.numCols()).map { index =>
-      batch.column(index) match {
-        case a: CometVector =>
-          val valueVector = a.getValueVector
-          val provider =
-            if (valueVector.getField.getDictionary != null) 
Some(a.getDictionaryProvider)
-            else None
-
-          (getFieldVector(valueVector, "serialize"), provider)
-
-        case cv: ConstantColumnVector =>
-          // Spark wraps file-source partition columns and other per-batch 
constants in
-          // `ConstantColumnVector`. Materialise to an Arrow vector so the 
serialisation path
-          // doesn't reject the batch. "UTC" is intentional -- see 
`ConstantColumnVectors`.
-          val materialized = ConstantColumnVectors.materialize(
-            cv,
-            cv.dataType(),
-            rows,
-            s"_const_$index",
-            org.apache.comet.CometArrowAllocator,
-            "UTC")
-          (materialized, None)
-
-        case c =>
-          throw new SparkException(
-            s"Comet execution only takes Arrow Arrays, but got ${c.getClass}. 
" +
-              "This typically happens when a Comet scan falls back to Spark 
due to unsupported " +
-              "data types (e.g., complex types like structs, arrays, or maps). 
" +
-              "To resolve this, you can: " +
-              "(1) enable spark.comet.scan.allowIncompatible=true to use a 
compatible native " +
-              "scan variant, or " +
-              "(2) enable spark.comet.convert.parquet.enabled=true to convert 
Spark Parquet " +
-              "data to Arrow format automatically.")
+    // Vectors made here rather than taken from `batch`. The caller releases 
them with the rest of
+    // what it writes, but if a later column fails it never gets them, so they 
are closed here.
+    val made = scala.collection.mutable.ArrayBuffer.empty[FieldVector]
+    def keep(vector: FieldVector): FieldVector = {
+      made += vector
+      vector
+    }
+
+    try {
+      (0 until batch.numCols()).map { index =>
+        batch.column(index) match {
+          case a: CometVector =>
+            val valueVector = a.getValueVector
+            val provider =
+              if (valueVector.getField.getDictionary != null) 
Some(a.getDictionaryProvider)
+              else None
+
+            val fieldVector = valueVector match {
+              case v: LargeVarCharVector => keep(narrowOffsets(v, 
ArrowType.Utf8.INSTANCE))
+              case v: LargeVarBinaryVector => keep(narrowOffsets(v, 
ArrowType.Binary.INSTANCE))
+              case v => getFieldVector(v, "serialize")
+            }
+            (fieldVector, provider)
+
+          case cv: ConstantColumnVector =>
+            // Spark wraps file-source partition columns and other per-batch 
constants in
+            // `ConstantColumnVector`. Materialise to an Arrow vector so the 
serialisation path
+            // doesn't reject the batch. "UTC" is intentional -- see 
`ConstantColumnVectors`.
+            val materialized = keep(
+              ConstantColumnVectors.materialize(
+                cv,
+                cv.dataType(),
+                rows,
+                s"_const_$index",
+                org.apache.comet.CometArrowAllocator,
+                "UTC"))
+            (materialized, None)
+
+          case c =>
+            throw new SparkException(
+              s"Comet execution only takes Arrow Arrays, but got 
${c.getClass}. " +
+                "This typically happens when a Comet scan falls back to Spark 
due to " +
+                "unsupported data types (e.g., complex types like structs, 
arrays, or maps). " +
+                "To resolve this, you can: " +
+                "(1) enable spark.comet.scan.allowIncompatible=true to use a 
compatible native " +
+                "scan variant, or " +
+                "(2) enable spark.comet.convert.parquet.enabled=true to 
convert Spark Parquet " +
+                "data to Arrow format automatically.")
+        }
+      }
+    } catch {
+      case NonFatal(e) =>

Review Comment:
   This covers a failure inside the column loop. A failure after this method 
returns still drops the copies before any root is cleared, though. In 
`serializeBatches` that means `combineDictionaryProviders` or 
`writer.writeBatch`, and in `CachedBatchIpc` it means `lookupDictionary` or 
`DictionaryEncoder.decode`. The gap already existed for materialized constants, 
but a narrowed copy can be close to 2 GiB of `CometArrowAllocator` memory. Not 
blocking. Would returning which vectors the caller owns, or putting the root's 
lifecycle in `serializeBatches` in a `try`/`finally`, close it more completely 
than tracking `made` here?



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