dwsmith1983 commented on PR #6125:
URL: 
https://github.com/apache/datafusion-comet/pull/6125#issuecomment-6115269080

   > @dwsmith1983 you changed how native scan partitions are read in #6753, so 
you know this area. If you have time, would you be able to review the memory 
accounting here? Thanks.
   
   Thanks for the ping. The points @comphead, @viirya and @sunchao raised still 
apply at this head: the swallowed try_grow error, a consumer per reader, the 
file-independent decode estimate, the stale memory_management.md (lines 394 and 
480 still describe the scan as uncharged), the Scala metric list, and a way to 
turn it off. A few things changed on main since the last commit that affect the 
design.
   
   grow rather than try_grow. The description gives the reason for try_grow as 
the unified pools panicking on an infallible grow. Since #6128 they no longer 
do: grow records the bytes and carries what Spark declines as overcommit, and 
the next try_grow from a consumer that can spill pays it back. The fetched 
pages already exist when they are charged, so this is not admission control. 
With grow they would be charged, the pressure would land on consumers that can 
spill, and both the error classification question and scan_io_unreserved_bytes 
would go away. The trade off is that grow ignores the fair limit.
   
   A sketch for reserve_buffers after merging main (on this branch's base grow 
still unwraps try_grow), which also gets the fetch down to one grow and one 
shrink by sharing the reservation instead of splitting it per buffer. 
ReservedBytes::_reservation becomes an Arc<MemoryReservation>, the call site 
drops the metrics argument, and the bytes are released when the last buffer of 
the fetch drops rather than buffer by buffer, so 
fetched_data_pages_hold_a_reservation_until_dropped and 
refused_reservation_returns_data_unaccounted would change:
   
   fn reserve_buffers(reservation: &MemoryReservation, buffers: Vec<Bytes>) -> 
Vec<Bytes> {
       let fetch = reservation.new_empty();
       // Recorded even when Spark declines part of it; the shortfall is 
carried as overcommit.
       fetch.grow(buffers.iter().map(Bytes::len).sum());
       let fetch = Arc::new(fetch);
       buffers
           .into_iter()
           .map(|bytes| {
               Bytes::from_owner(ReservedBytes {
                   bytes,
                   _reservation: Arc::clone(&fetch),
               })
           })
           .collect()
   }
   
   If try_grow stays, only a refusal should let the scan keep reading. A spill 
failure, a kill interrupt or a JNI failure arrives as External, and today both 
get_byte_ranges and create_reader return Ok when the pool fails that way (a 
pool whose try_grow returns DataFusionError::External shows it). Classifying 
the error keeps the existing tests passing:
   
   fn reserve_buffers(
       reservation: &MemoryReservation,
       buffers: Vec<Bytes>,
       scan_io_metrics: &ScanIoMetrics,
   ) -> DFResult<Vec<Bytes>> {
       let total = buffers.iter().map(Bytes::len).sum();
       let fetch = reservation.new_empty();
       match fetch.try_grow(total) {
           Ok(()) => {}
           Err(e) if matches!(e.find_root(), 
DataFusionError::ResourcesExhausted(_)) => {
               scan_io_metrics.unreserved_bytes.add(total);
               return Ok(buffers);
           }
           Err(e) => return Err(e),
       }
       // split per buffer as now
   }
   
   // get_byte_ranges
   Some(reservation) => reserve_buffers(reservation, bytes, &scan_io_metrics)
       .map_err(|e| ParquetError::External(Box::new(e)))?,
   
   The decode reservation in create_reader takes the same match inside its 
closure and ends with .transpose()?.
   
   Overcommit debt. If try_grow stays, each scan call also asks Spark for the 
tas, since SparkMemory::try_acquire asks for additional plus the debt. The 
scanbecomes the most frequent caller and the only one that cannot act on a 
refusal, so under pressure it keeps asking for the whole debt, gets a short 
grant, hands it back and reads anyway: a JNI round trip per row group, and 
possibly a JVM spill that buys nothing.
   
   One consumer per scan. #6247 registers the writers once per task because 
everyers the share of every other one in the task. The same applies here. 
Oneconsumer on the factory with new_empty() per reader would also keep the 
count steady while DataFusion holds the second reader for Bloom filter pruning. 
With a pool that counts registrations, three readers register three consumers 
at this head. Storing the registration instead of the pool (memory_pool: 
Option<Arc<dyn MemoryPool>> becomes reservation: Option<MemoryReservation>, 
which is Send + Sync) makes it one:
   
   // with_memory_pool: register once for the scan
   self.reservation = Some(MemoryConsumer::new("ParquetScan").register(&pool));
   
   // create_reader: every reader charges the same consumer
   let reservation = 
self.reservation.as_ref().map(MemoryReservation::new_empty);
   
   Sizing the decode estimate. The factory already loads the footer and offset 
index, so the estimate could come from the file: per projected leaf, the 
smaller of one page and the largest selected column chunk's uncompressed size, 
plus min(batch_size, num_rows) * row_width. Charging it at the first 
get_byte_ranges rather than in create_reader keeps the JNI call out of the 
planning step, and a file that is pruned entirely never pays it. Capping by 
file size would not bound it, since decompressed pages can be larger than the 
file.
   
   Shrinks per page. With the page index and a row selection, parquet 59.3 
fetches one range per selected page (scan_ranges_from_selectors), and on the 
local path each comes back as its
   own buffer. split per buffer then gives every page its own reservation and 
itsrow group drops. One reservation shared by the whole fetch, as in the 
sketchabove, keeps it at one grow and one shrink.
   
   Release from the FFI callback. If a page buffer ever ends up aliased by an 
expnk runs from the release callback, and a panic there cannot unwind across 
extern "C", so it aborts the executor rather than failing the task. A Utf8View 
hint in the file's ARROW:schema is kept unless the cast to the Spark type 
copies, so that copy is what the
   invariant depends on. Worth recording in the comment.
   
   Two of the earlier points are mostly settled on main: partial grants log at 
DEp since #6269, and since #6261 a blocked acquire hands the worker's other 
tasksto another thread, though that thread still waits in Spark.
   
   For the rebase, the conflicts are in eager_page_index_reader_factory.rs, 
parquet_exec.rs and metrics.md, and the scan metric list in CometExecSuite is 
now around line 2538.


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