comphead opened a new issue, #7192:
URL: https://github.com/apache/datafusion-comet/issues/7192

   ### Describe the bug
   
   A broadcast join fails when one string or binary column of the build side 
holds more than 1 GiB in total. Spark runs the same query. Such a column still 
fits a single `VarCharVector`, whose 32-bit offsets address up to 2 GiB.
   
   ```
   org.apache.comet.shaded.arrow.vector.util.OversizedAllocationException: 
Memory required for vector is (2147483648), which is overflow or more than max 
allowed (2147483647). You could consider using 
LargeVarCharVector/LargeVarBinaryVector for large strings/large bytes types
     at 
org.apache.comet.shaded.arrow.vector.BaseVariableWidthVector.checkDataBufferSize(BaseVariableWidthVector.java:461)
     at 
org.apache.comet.shaded.arrow.vector.BaseVariableWidthVector.reallocDataBuffer(BaseVariableWidthVector.java:579)
     at 
org.apache.comet.shaded.arrow.vector.BaseVariableWidthVector.reallocDataBuffer(BaseVariableWidthVector.java:557)
     at 
org.apache.comet.shaded.arrow.vector.util.VectorAppender.visit(VectorAppender.java:134)
     at 
org.apache.comet.shaded.arrow.vector.util.VectorAppender.visit(VectorAppender.java:45)
     at 
org.apache.comet.shaded.arrow.vector.BaseVariableWidthVector.accept(BaseVariableWidthVector.java:1496)
     at 
org.apache.comet.shaded.arrow.vector.util.VectorSchemaRootAppender.append(VectorSchemaRootAppender.java:68)
     at 
org.apache.comet.shaded.arrow.vector.util.VectorSchemaRootAppender.append(VectorSchemaRootAppender.java:83)
     at 
org.apache.spark.sql.comet.util.Utils$.$anonfun$coalesceBroadcastBatches$2(Utils.scala:412)
     at 
org.apache.spark.sql.comet.util.Utils$.$anonfun$coalesceBroadcastBatches$2$adapted(Utils.scala:383)
     at scala.collection.ArrayOps$.foreach$extension(ArrayOps.scala:1324)
     at 
org.apache.spark.sql.comet.util.Utils$.coalesceBroadcastBatches(Utils.scala:383)
     at 
org.apache.spark.sql.comet.CometBroadcastExchangeExec.$anonfun$relationFuture$1(CometBroadcastExchangeExec.scala:151)
   ```
   
   #6779 reported the same exception, but this build includes its fix (#6900) 
and the cause is different.
   
   `coalesceBroadcastBatches` appends every broadcast batch into one 
`VectorSchemaRoot`. Arrow Java's `VectorAppender` grows a variable-width data 
buffer by doubling it 
([VectorAppender.java#L133-L135](https://github.com/apache/arrow-java/blob/v18.3.0/vector/src/main/java/org/apache/arrow/vector/util/VectorAppender.java#L133-L135),
 
[BaseVariableWidthVector.java#L546-L580](https://github.com/apache/arrow-java/blob/v18.3.0/vector/src/main/java/org/apache/arrow/vector/BaseVariableWidthVector.java#L546-L580)).
 From 2^30 bytes the next step asks for 2^31, one byte over `MAX_BUFFER_SIZE`, 
and throws. A coalesced string or binary column is therefore capped at 1 GiB, 
half of what its offsets allow. Arrow Java `main` grows the buffer the same way.
   
   Two things in Comet turn the cap into a query failure:
   
   - The `catch` around the append 
([Utils.scala#L422-L434](https://github.com/apache/datafusion-comet/blob/69111f68a5ff62904342910c445d191f53615aff/spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala#L422-L434))
 falls back to the uncoalesced buffers only on `IllegalArgumentException`. 
`OversizedAllocationException` is a `RuntimeException`, so it fails the 
broadcast.
   - `CometBroadcastExchangeExec` checks `maxBroadcastTableBytes` (8GB by 
default) after coalescing, against the compressed result 
([CometBroadcastExchangeExec.scala#L151-L163](https://github.com/apache/datafusion-comet/blob/69111f68a5ff62904342910c445d191f53615aff/spark/src/main/scala/org/apache/spark/sql/comet/CometBroadcastExchangeExec.scala#L151-L163)).
 Nothing bounds the uncompressed size before the append.
   
   Coalescing came in with #3703 (0.15.0), and every release branch since then 
has this code. Before #3703 each batch was broadcast as its own buffer.
   
   ### Steps to reproduce
   
   Spark 4.0 with a Comet build based on `main` at `d0f367dc02`, which includes 
#6900. `coalesceBroadcastBatches` is unchanged on `main` since then. I ran 
without Comet shuffle because the query has no shuffle.
   
   ```shell
   spark-shell --master local[2] --driver-memory 8g --jars 
comet-spark-spark4.0_2.13-1.2.0-SNAPSHOT.jar \
     --conf spark.plugins=org.apache.spark.CometPlugin \
     --conf spark.comet.shuffle.enabled=false \
     --conf spark.memory.offHeap.enabled=true \
     --conf spark.memory.offHeap.size=4g
   ```
   
   ```scala
   val dir = "/tmp/broadcast-coalesce"
   spark.range(1000).write.mode("overwrite").parquet(s"$dir/a")
   spark.range(1200000)
     .selectExpr("id", "concat(cast(id as string), repeat('x', 1000)) AS s")
     .write.mode("overwrite").parquet(s"$dir/b")
   spark.read.parquet(s"$dir/a").createOrReplaceTempView("a")
   spark.read.parquet(s"$dir/b").createOrReplaceTempView("b")
   spark.sql("SELECT /*+ BROADCAST(b) */ a.id, length(b.s) AS len FROM a JOIN b 
ON a.id = b.id").collect()
   ```
   
   Column `s` of `b` holds 1,207,288,890 bytes (1.12 GiB), and the query fails 
with the exception above.
   
   | rows in `b` | bytes in `s` | Comet | Comet disabled |
   |---:|---:|---|---|
   | 1,000,000 | 1,005,888,890 (0.94 GiB) | 1,000 rows | 1,000 rows |
   | 1,200,000 | 1,207,288,890 (1.12 GiB) | `OversizedAllocationException` | 
1,000 rows |
   
   ### Expected behavior
   
   The query returns the same 1,000 rows as Spark, as it does for the 0.94 GiB 
column.
   
   ### Additional context
   
   A possible fix is to track the bytes of each variable-width vector, nested 
children included, while coalescing, and to start a new output batch before any 
of them would pass 2^30. `coalesceBroadcastBatches` already returns an 
`Array[ChunkedByteBuffer]`, and its uncoalesced fallback returns many buffers, 
so readers accept more than one. The check needs `Long` arithmetic in Comet, 
because `VectorAppender` adds the two sizes as an `Int` and a sum that 
overflows skips its growth loop. Catching `OversizedAllocationException` and 
keeping the uncoalesced buffers would be a backstop.
   
   Past 2 GiB a column no longer fits one array with 32-bit offsets, and 
DataFusion's `HashJoinExec` concatenates its build side into one batch, so that 
case is a separate limit.
   
   Setting `spark.comet.exec.broadcastExchange.enabled=false` works around it. 
The repro then runs the broadcast and the join in Spark and returns 1,000 rows.
   
   #5538 adds a 1 GiB uncompressed budget that skips coalescing, but only on 
its opt-in direct-read path (`nativeIpc = true`).
   
   ### Willingness to contribute
   
   I can contribute a fix for this bug independently
   


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