adriangb opened a new issue, #25791:
URL: https://github.com/apache/datafusion/issues/25791

   ### Describe the bug
   
   A sort that follows `WindowAggExec` and a hash `RepartitionExec` can fail 
with `ResourcesExhausted` on its first input batch when the data has `Utf8View` 
columns. The batch holds about 5 MB of string data, but `ExternalSorter` tries 
to reserve about 700 MB for it. The same query with `Utf8` columns runs in a 2 
GB pool. With `Utf8View` it needs a pool of more than 8 GB, even though its 
peak RSS without a limit is lower (721 MB with views, 1.09 GB with `Utf8`).
   
   ### To Reproduce
   
   With `datafusion-cli` 55.1.0 (also reproduced on `main` at 33574a1559e6):
   
   ```sql
   -- gen.sql
   COPY (
     SELECT
       i % 100000 AS k1,
       (i * 7919) % 100003 AS k2,
       arrow_cast(concat(md5(CAST(i AS VARCHAR)), md5(CAST(i + 1 AS VARCHAR)), 
md5(CAST(i + 2 AS VARCHAR)), md5(CAST(i + 3 AS VARCHAR))), 'Utf8') AS payload
     FROM generate_series(1, 2000000) AS t(i)
   ) TO 'data.parquet' STORED AS PARQUET;
   ```
   
   ```sql
   -- query.sql
   SET datafusion.execution.target_partitions = 8;
   SET datafusion.execution.parquet.schema_force_view_types = true;
   SELECT count(*), sum(length(payload)), max(m1), max(m2)
   FROM (
     SELECT k2, payload, m1, MAX(k1) OVER (PARTITION BY k2) AS m2
     FROM (
       SELECT k1, k2, payload, MAX(k2) OVER (PARTITION BY k1) AS m1
       FROM 'data.parquet'
     )
   );
   ```
   
   ```
   datafusion-cli -q -f gen.sql
   datafusion-cli -q -m 4g --mem-pool-type greedy -f query.sql
   ```
   
   Actual:
   
   ```
   Resources exhausted: Failed to allocate additional 674.5 MB for 
ExternalSorter[5] with 0.0 B already allocated for this reservation - 663.6 MB 
remain available for the total memory pool: greedy(used: 3.4 GB, pool_size: 4.0 
GB)
   ```
   
   | Pool (greedy) | `schema_force_view_types = true` | `= false` |
   |---|---|---|
   | 2g | fails (715 MB request) | ok |
   | 4g | fails (675 MB request) | ok |
   | 8g | fails (660 MB request) | ok |
   | 16g | ok | ok |
   
   ### Expected behavior
   
   The Utf8View query should run with a memory limit close to the one the 
`Utf8` query needs. A sort should not need hundreds of MB to accept one input 
batch that holds about 5 MB of data.
   
   ### Analysis
   
   The plan is `DataSourceExec` → `RepartitionExec(Hash k1)` → `SortExec` → 
`WindowAggExec` → `RepartitionExec(Hash k2)` → `SortExec` → `WindowAggExec`. I 
measured the batches between the operators with a small harness. "Accounted" is 
`get_record_batch_memory_size(batch) + batch.get_sliced_size()`, which 
`ExternalSorter::insert_batch` tries to reserve. "Referenced" is the view bytes 
plus the payload bytes the views use.
   
   | Output of | Rows per batch | Accounted | Referenced | Buffer entries / 
distinct buffer bytes |
   |---|---|---|---|---|
   | first `SortExec` | 8192 | 52 MB | 1.2 MB | 26 / 26 MB |
   | first `WindowAggExec` | 250k (one batch per partition) | 714 MB | 40 MB | 
668 / 33 MB |
   | second `RepartitionExec` (input of second sort) | 31k | 727 MB | 5.3 MB | 
662 / 33 MB |
   | second `SortExec` | 8192 | 4.3 GB | 1.3 MB | about 3,900 / 267 MB |
   
   Three things combine:
   
   1. `WindowAggExec::compute_aggregates` calls `concat_batches` over all 
buffered input. For view arrays, `concat` uses 
`GenericByteViewBuilder::append_array`, which appends each input array's full 
data-buffer list. The sorted input batches share the same buffers, so the 
output repeats each buffer about 25 times (668 entries, 33 MB distinct). Arrow 
chose not to deduplicate here (https://github.com/apache/arrow-rs/issues/10692).
   2. The hash `RepartitionExec` `take` keeps that buffer list. Its 
`LimitedBatchCoalescer` sets `biggest_coalesce_batch_size = batch_size / 2`, so 
outputs of at least 4096 rows pass through without the byte-view gc the 
coalescer does for sparse sources.
   3. `get_reserved_bytes_for_record_batch` adds `get_sliced_size()`. For view 
arrays that sum includes the capacity of every data-buffer entry, and it does 
not deduplicate. On `main` this comes from `ArrayData::get_slice_memory_size` 
(https://github.com/apache/arrow-rs/pull/10519). On 55.1.0 it comes from the 
`byte_view_data_buffer_size` workaround. In the table, 
`get_record_batch_memory_size` (which counts each buffer once) is 43 MB, but 
`get_sliced_size` is 684 MB.
   
   `ExternalSorter::reserve_memory_for_batch_and_maybe_spill` returns the error 
when the reservation fails and `in_mem_batches` is empty. Splitting the batch 
would not help, because each slice keeps the same buffers.
   
   ### Possible fixes
   
   - Count each data buffer once in the sort reservation, for example with 
`RecordBatchMemoryCounter` or by deduplicating by pointer in `get_sliced_size`. 
Here this reduces the request from 727 MB to about 78 MB.
   - Compact view arrays whose buffer capacity is much larger than the bytes 
they use, with the same 2x rule as `BatchCoalescer`, before `ExternalSorter` 
reserves memory for them. As a minimum, do this before returning 
`ResourcesExhausted` when there is nothing to spill. Here this reduces the 
request to about 11 MB and frees the pinned upstream buffers.
   
   Related: #22862 (the same fix for hash join build batches), #25712, #25271, 
#19679, #14748.
   


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