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

   ## Summary
   
   Arrow `Utf8` and `Binary` arrays store all values of one array in a single 
buffer
   indexed by 32-bit (i32) offsets. One array (one column of one batch) can 
therefore
   hold at most 2^31-1 bytes of values.
   
   Several native operators in Comet build one array out of many input rows: 
they
   merge small batches into bigger ones, or concatenate a whole buffered input 
into a
   single batch. Some batch sources (broadcast, table cache) also build batches 
that
   are larger than, and unaffected by, `spark.comet.batchSize`. When a column 
has large values (for example serialized blobs of a
   few hundred KB to a few MB per row), those merged arrays cross 2 GiB and the 
query
   fails with:
   
   ```
   CometNativeException: Offset overflow error: <n>     // n > 2147483647
   ```
   
   No single value needs to be large. A Parquet `BYTE_ARRAY` value is capped at
   2^31-1, so the overflow is always the sum of many values in one merged array.
   
   Spark does not fail on the same data. Its vectorized reader has the same ~2 
GiB
   per-column-per-batch limit (`WritableColumnVector`, "Cannot reserve 
additional
   contiguous bytes in the vectorized reader"), but its batches never cross a 
row group
   and nothing downstream re-merges them.
   
   ## Example
   
   A table with a binary column averaging ~0.5-1 MB per row:
   
   ```sql
   SELECT blob_col, other_col FROM table_a WHERE length(blob_col) > 0
   ```
   
   Comet plans `ProjectionExec -> FilterExec -> <native scan>`. Each scan batch 
holds
   at most min(batch size, rows in one row group), because the Parquet reader 
builds one
   reader per row group; with typical row groups that is far below 2 GiB. 
`FilterExec` then merges them
   back up to 8192 rows, about 4-8 GB of `blob_col`, and the merge fails. 
Setting
   `spark.comet.batchSize=1` still fails, because `FilterExec` ignores that 
setting
   (sub-issue 1).
   
   ## Root causes
   
   There are three separate problems.
   
   1. **Batch size not honored by an operator.** `FilterExec` is the only native
      operator Comet builds with a hardcoded batch size (DataFusion's default, 
8192)
      instead of the session value. So `spark.comet.batchSize` can't be used as 
a
      workaround.
   2. **Batch size not honored by a batch source.** The broadcast exchange 
merges the
      whole broadcast side into one batch (and fails on the JVM side at about 1 
GiB per
      column), and the in-memory table cache uses Spark's cache batch size 
(default
      10000). Both reach native code unchanged.
   3. **Row-count-only limits and whole-input concatenation.** Even with the 
right batch
      size, several code paths decide how many rows go into one output array by 
row
      count only, or concatenate an entire buffered input:
      - arrow's `BatchCoalescer` (used by `FilterExec` and the native shuffle 
writer)
        has no byte limit.
      - `interleave` / `concat` in the shuffle writer, TopK, joins and window 
operators
        have no retry when they hit the offset limit.
      - DataFusion's sort merge is the exception: it retries with fewer rows on
        `OffsetOverflowError` (`sorts/builder.rs`, `retry_interleave`).
   
   ## Affected operators
   
   Validated against main at 64518088a1c. Line references are for DataFusion 
55.2.0
   and arrow 59.3.1.
   
   | Operator | Where it overflows | Risk | Sub-issue |
   |---|---|---|---|
   | `FilterExec` | Coalescer target hardcoded to 8192 rows (`filter.rs:80`) | 
High, any filter over large values | 1 |
   | Native shuffle writer | `interleave_record_batch` and `BatchCoalescer`, no 
overflow retry; blocks over i32::MAX bytes rejected | Medium | 2 |
   | `WindowAggExec` | `concat_batches` over the whole task input 
(`window_agg_exec.rs:539`); used for `percent_rank`, `cume_dist`, `ntile` and 
any aggregate over an UNBOUNDED FOLLOWING frame, including `sum(x) OVER 
(PARTITION BY k)` | High | 3 |
   | `BatchCoalescer` (arrow) / `LimitedBatchCoalescer` (DataFusion) | No byte 
limit | Root of 1 and 2 at default batch size | 4 |
   | TopK (`SortExec` with fetch) | `emit` interleaves all K rows at once 
(`topk/mod.rs:1015`) | Medium, large LIMIT | 5 |
   | `HashJoinExec`, `NestedLoopJoinExec` | Build side concatenated into one 
batch (`hash_join/exec.rs:2365`, `nested_loop_join.rs:968`) | Medium, large 
build side | 6 |
   | `HashJoinExec` (build side is the outer/semi side) | Unmatched build rows 
handed downstream as one batch (`hash_join/stream.rs:1038-1054`) | Low | 6 |
   | `BoundedWindowAggExec` | Buffer concatenated on every batch while output 
is held back (`bounded_window_agg_exec.rs:706`); long RANGE tie groups, large 
FOLLOWING or lag/lead offsets | Low to medium | 7 |
   | Any of the above | Values stored as i32-offset arrays | Long-term 
alternative | 8 |
   | Broadcast exchange | All broadcast batches merged into one 
(`Utils.scala:379`, `coalesceBroadcastBatches`); Arrow Java fails at ~1 GiB per 
column with an uncaught exception | Medium, large broadcast side | 9 |
   | In-memory table cache | Batch size from 
`spark.sql.inMemoryColumnarStorage.batchSize` on the row and AQE write paths 
(`ArrowCachedBatchSerializer.scala:664`, `CometCacheBuildRule.scala:63`) | 
Medium, cached tables | 10 |
   | `MergeRowsExec`, `AggregateExec` emit | Up to ~2x input rows 
(`merge_rows.rs:663`); all groups in one batch before slicing 
(`aggregate_hash_table/common.rs:241-249`) | Low | 11 |
   | `FilterExec` / `LimitedBatchCoalescer` memory | Buffered copies and the 
`concat` output are not tracked by the memory pool | Medium (memory accounting) 
| 12 |
   
   Known and tracked elsewhere: multi-column GROUP BY keys over 2 GiB fail with 
a
   clear "offset overflow" error (`multi_group_by/bytes.rs:204`); a single
   string/binary GROUP BY key over 2 GiB panics instead (`binary_map.rs:470`). 
See
   #4718 and PR #4791 (open, experimental).
   
   Other offset overflow issues outside this epic: #7247 (a large scalar string
   expanded to the batch length) and #7254 (a nested dictionary-encoded binary 
column
   flattened to `Utf8` per batch).
   
   ## Audit: what already honors the batch size
   
   `spark.comet.batchSize` reaches native code as the session batch size, and 
every
   plan runs with a task context built from that session, so operators that 
read the
   batch size at execute time see it. Comet runs no DataFusion physical 
optimizer
   rules that could reset it.
   
   Honors it: the native Parquet, Iceberg and CSV scans (one row group at most 
per
   batch; Iceberg deletes and transforms only shrink batches), range, `SortExec`
   (including spill re-merge, which retries on overflow), `AggregateExec` 
output,
   `HashJoinExec` probe output, `NestedLoopJoinExec` output, 
`SortMergeJoinExec` (can
   reach about 2x the batch size), explode, the native shuffle writer, JVM 
inputs
   (Spark-to-columnar, local table scan; except the AQE cache build, sub-issue 
10) and
   JVM columnar shuffle.
   
   Pass-through only: projections, limits, expand (one batch per projection, 
each the
   size of its input), sample, rank limit, dynamic filters and shuffle read 
(one block
   per batch, as written).
   
   Not yet checked: Celeborn shuffle read framing, Python UDF (`mapInBatch`) 
output,
   sort-merge join spill read-back, the Iceberg Java fallback reader.
   
   ## Sub-issues
   
   - [ ] 1. Pass the session batch size to `FilterExec`
   - [ ] 2. Native shuffle writer: retry with fewer rows on offset overflow
   - [ ] 3. `WindowAggExec` concatenates the whole task input into one batch
   - [ ] 4. Byte limit in arrow `BatchCoalescer` and DataFusion 
`LimitedBatchCoalescer` (upstream)
   - [ ] 5. TopK `emit` interleaves all K rows into one batch (upstream)
   - [ ] 6. Hash join and nested loop join build side concatenated into one 
batch (upstream)
   - [ ] 7. `BoundedWindowAggExec` concatenates the unfinished partition buffer
   - [ ] 8. Read large string/binary columns as view types
   - [ ] 9. Broadcast exchange merges every batch into one
   - [ ] 10. In-memory table cache ignores `spark.comet.batchSize`
   - [ ] 11. `MergeRowsExec` and aggregate emit build batches larger than the 
batch size
   - [ ] 12. Filter merge buffers are not tracked by the memory pool
   
   Suggested order: 1, then 2, 4, 9 and 10 in parallel, then 3, then 5-7, 11 
and 12 as
   upstream allows (12 pairs with 4). 8 is a separate feature.
   
   ## Testing approach
   
   A real 2 GiB reproduction is too heavy for CI. Each sub-issue should instead:
   
   - test the mechanism directly, for example that the operator honors a small 
batch
     size or byte limit, using a few MB of data and a small configured limit; 
and
   - where an end-to-end check is possible, compare results with Spark at a 
small
     `spark.comet.batchSize` over a column with values of a few hundred KB.
   
   Prefer SQL-file tests. Use Scala or Rust tests only when the property being 
tested
   (batch row counts, byte sizes) can't be seen from SQL.
   
   ## Workaround today
   
   For the filter case, `spark.comet.exec.filter.enabled=false` moves filters 
back to
   Spark. Lowering `spark.comet.batchSize` has no effect on `FilterExec` until
   sub-issue 1 lands. After that, lowering it (for ~1 MB rows, ~1024 or less) 
avoids
   the filter and shuffle cases, but not the whole-input cases (3, 6, 7) or the 
batch
   sources that ignore it (9, 10).
   


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